├── .gitattributes
├── .gitignore
├── BUILD.txt
├── CHANGELOG.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── README.rst
├── build_pyvXRAY.bat
├── cython
├── cythonMods.cpp
└── cythonMods.pyx
├── pyvXRAY
├── __init__.py
├── elemTypes.py
├── pyvXRAYDB.py
├── pyvXRAY_plugin.py
├── version.py
└── virtualXrays.py
├── setup.cfg
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 |
46 | [Dd]ebug/
47 | [Rr]elease/
48 | x64/
49 | build/
50 | [Bb]in/
51 | [Oo]bj/
52 |
53 | # MSTest test Results
54 | [Tt]est[Rr]esult*/
55 | [Bb]uild[Ll]og.*
56 |
57 | *_i.c
58 | *_p.c
59 | *.ilk
60 | *.meta
61 | *.obj
62 | *.pch
63 | *.pdb
64 | *.pgc
65 | *.pgd
66 | *.rsp
67 | *.sbr
68 | *.tlb
69 | *.tli
70 | *.tlh
71 | *.tmp
72 | *.tmp_proj
73 | *.log
74 | *.vspscc
75 | *.vssscc
76 | .builds
77 | *.pidb
78 | *.log
79 | *.scc
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 |
101 | # TeamCity is a build add-in
102 | _TeamCity*
103 |
104 | # DotCover is a Code Coverage Tool
105 | *.dotCover
106 |
107 | # NCrunch
108 | *.ncrunch*
109 | .*crunch*.local.xml
110 |
111 | # Installshield output folder
112 | [Ee]xpress/
113 |
114 | # DocProject is a documentation generator add-in
115 | DocProject/buildhelp/
116 | DocProject/Help/*.HxT
117 | DocProject/Help/*.HxC
118 | DocProject/Help/*.hhc
119 | DocProject/Help/*.hhk
120 | DocProject/Help/*.hhp
121 | DocProject/Help/Html2
122 | DocProject/Help/html
123 |
124 | # Click-Once directory
125 | publish/
126 |
127 | # Publish Web Output
128 | *.Publish.xml
129 | *.pubxml
130 |
131 | # NuGet Packages Directory
132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
133 | #packages/
134 |
135 | # Windows Azure Build Output
136 | csx
137 | *.build.csdef
138 |
139 | # Windows Store app package directory
140 | AppPackages/
141 |
142 | # Others
143 | sql/
144 | *.Cache
145 | ClientBin/
146 | [Ss]tyle[Cc]op.*
147 | ~$*
148 | *~
149 | *.dbmdl
150 | *.[Pp]ublish.xml
151 | *.pfx
152 | *.publishsettings
153 |
154 | # RIA/Silverlight projects
155 | Generated_Code/
156 |
157 | # Backup & report files from converting an old project file to a newer
158 | # Visual Studio version. Backup files are not needed, because we have git ;-)
159 | _UpgradeReport_Files/
160 | Backup*/
161 | UpgradeLog*.XML
162 | UpgradeLog*.htm
163 |
164 | # SQL Server files
165 | App_Data/*.mdf
166 | App_Data/*.ldf
167 |
168 | #############
169 | ## Windows detritus
170 | #############
171 |
172 | # Windows image file caches
173 | Thumbs.db
174 | ehthumbs.db
175 |
176 | # Folder config file
177 | Desktop.ini
178 |
179 | # Recycle Bin used on file shares
180 | $RECYCLE.BIN/
181 |
182 | # Mac crap
183 | .DS_Store
184 |
185 |
186 | #############
187 | ## Python
188 | #############
189 |
190 | *.py[cod]
191 |
192 | # Packages
193 | *.egg
194 | *.egg-info
195 | dist/
196 | build/
197 | eggs/
198 | parts/
199 | var/
200 | sdist/
201 | develop-eggs/
202 | .installed.cfg
203 | dev/
204 |
205 | # Installer logs
206 | pip-log.txt
207 |
208 | # Unit test / coverage reports
209 | .coverage
210 | .tox
211 |
212 | #Translations
213 | *.mo
214 |
215 | #Mr Developer
216 | .mr.developer.cfg
217 |
218 | #Spyder
219 | *.spyderproject
220 | *.spyderworkspace
221 |
222 | # Packaging
223 | MANIFEST
224 | *.html
225 |
--------------------------------------------------------------------------------
/BUILD.txt:
--------------------------------------------------------------------------------
1 | The pyvXRAY source distribution was built using the following procedure:
2 | 1. Open command prompt and go to pyvXRAY directory
3 | 2. Create cythonMods.cpp file:
4 | $ cd cython
5 | $ cython -a cythonMods.pyx --cplus
6 | 3. Build the source distribution
7 | $ cd..
8 | $ abaqus python setup.py sdist
9 |
10 | The pyvXRAY binary distribution was then built using the following procedure:
11 |
12 | $ abaqus python setup.py bdist
13 |
14 | The binary distribution is made available for 32-bit and 64-bit Windows only.
15 | This may also be done using a stand alone Python using the following command:
16 |
17 | $ python setup.py bdist --compiler=msvc
18 |
19 | To do this, the stand alone Python installation must use the same Python
20 | and numpy versions used in ABAQUS. This is typically Python 2.6.x and numpy 1.4.x.
21 | The mingw32 compiler (--compiler=mingw32) may also be used if using a stand alone
22 | Python installation, but not otherwise.
23 |
24 | NOTES:
25 | 1. When creating binary distribution using ABAQUS Python, the --compiler
26 | option is not available. It will automatically use msvc if installed and
27 | give an error otherwise.
28 | 2. setup.cfg was used with the following settings:
29 | [install]
30 | install_lib=abaqus_plugins
31 | 3. To upload to PyPi, use the following command:
32 | $ abaqus python setup.py sdist upload
33 | 4. For some reason, PyPi doesn't like my binary distributions. Perhaps because
34 | it does not use the standard install directory. Therefore, binaries are
35 | made available at https://github.com/mhogg/pyvxray/releases
36 |
--------------------------------------------------------------------------------
/CHANGELOG.txt:
--------------------------------------------------------------------------------
1 |
2 | Version 0.2.1: Release date 2015-08-12
3 | --------------------------------------
4 | 1. Fixed bug when import PIL / Pillow in ABAQUS GUI for Abaqus v6.13.
5 | 2. Changed cython to use C++, not C
6 |
7 | Version 0.2.0: Release date 2013-10-10
8 | --------------------------------------
9 | 1. Added support for assembly element sets, part instance element sets and all elements
10 | from a part instance. Previously only part instance element sets were supported.
11 | 2. Added support for all linear and quadratic element types. Previously only C3D4 and
12 | C3D10 were supported. Now C3D4, C3D4H, C3D10, C3D10H, C3D10I, C3D10M and C3D10MH are
13 | all supported.
14 | 3. Re-write of GUI. Added automatic filling of inputs and checking of inputs by the GUI
15 | (as opposed to the kernel). This includes auto detection of odbs in session, of element
16 | sets for bone and implant, of bone density variable, of steps and of csyses.
17 | 4. Various bug fixes.
18 | 5. Updated README. Now use markdown (md), rather than txt
19 | 6. Update of setup.py so that LICENSE.txt and README.md are included in bdist (not just sdist)
20 | 7. Cleanup of redundant code
21 | 8. Tested and added support for Pillow
22 |
23 |
24 | Version 0.1.4: Release date 2013-08-30
25 | --------------------------------------
26 | 1. Added option for manual scaling of images. This is to allow the same scale factors to
27 | be applied to different models.
28 | 2. Major re-write of cythonMods.pyx. No longer use lapack from numpy to solve system
29 | of linear equations, but replaced with function SolveLinearEquations that performs
30 | Guassian Elimination with partial pivoting. This resulted in a significant speed up
31 | in creation of element maps.
32 | 3. Addition of file version.py to include version information.
33 |
34 |
35 | Version 0.1.2: Release date 2013-08-13
36 | --------------------------------------
37 | 1. Added support for C3D4 elements
38 |
39 |
40 | Version 0.1.1: Release date 2013-07-22
41 | --------------------------------------
42 | 1. Bug fix. Fixed checkInputs function that was checking element types of implant even
43 | when showImplant was set to False
44 |
45 |
46 | Version 0.1.0: Release date 2013-04-14
47 | --------------------------------------
48 | 1. Functions fmin and fmax used in cythonMods.pyx. Cython module builds ok using gcc, but
49 | failed when using MS Visual Studio 2008, because these functions are not defined in
50 | MSVS 2008 file "math.h". Fix was to write own functions.
51 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Michael Hogg (michael.christopher.hogg@gmail.com)
2 |
3 | The MIT License
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
10 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.in
2 | include *.txt
3 | include *.md
4 | recursive-include pyvXRAY *.pyd
5 | recursive-include cython *.pyx
6 | recursive-include cython *.cpp
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyvXRAY
2 |
3 | **An ABAQUS plug-in for the creation of virtual x-rays from 3D finite element bone/implant models.**
4 |
5 | **Developed together with [bonemapy](https://github.com/mhogg/bonemapy) and [BMDanalyse](https://github.com/mhogg/BMDanalyse) to provide tools for preparation and post-processing of bone/implant computer models.**
6 |
7 | [](https://pypi.python.org/pypi/pyvxray/)
8 | [](https://pypi.python.org/pypi/pyvxray/)
9 |
10 | Copyright 2013, Michael Hogg (michael.christopher.hogg@gmail.com)
11 |
12 | MIT license - See LICENSE.txt for details on usage and redistribution
13 |
14 | ## Requirements
15 |
16 | ### Software requirements
17 |
18 | * ABAQUS >= 6.11 (tested on 6.11, 6.12 and 6.13)
19 | * Python Image Library (PIL) >= 1.1.6 OR Pillow >= 2.2.0 (Note: Pillow is in active development and is recommended over PIL)
20 |
21 | For building cython modules from source (e.g. if not using releases with pre-built modules):
22 | * A C compiler. Using ABAQUS Python on Windows requires Microsoft C++. Can use other compilers (i.e. mingw32) if you have a separate Python installation.
23 | * Cython >= 0.17. This is optional, as .cpp files generated by Cython are provided
24 |
25 | **NOTES:**
26 |
27 | 1. ABAQUS is a commerical software package and requires a license from [Simulia](http://www.3ds.com/products-services/simulia/overview/)
28 | 2. The authors of pyvXRAY are not associated with ABAQUS/Simulia
29 | 3. Python and numpy are heavily used by pyvXRAY. These are built in to ABAQUS. All of the last few releases (v6.11 - v6.13) use Python 2.6.x and numpy 1.4.x
30 |
31 | ### Model setup requirements
32 |
33 | * The model must contain only linear or quadrilateral tetrahedral elements (ABAQUS element types C3D4, C3D4H, C3D10, C3D10H, C3D10I, C3D10M, and C3D10MH are all supported).
34 |
35 | * The model must have a scalar fieldoutput variable representing bone density. This is typically a state variable such as SDV1.
36 | This scalar variable must be available in the last frame of each step to be analysed, as only the last frame is used.
37 |
38 | ## Installation
39 |
40 | ####1. Installation of pyvXRAY plug-in
41 |
42 | pyvXRAY is an ABAQUS plug-in. ABAQUS plug-ins may be installed in several ways. Only one of the ways is discussed here. For other options the user is referred to the ABAQUS user manuals.
43 |
44 | * _Releases with pre-built modules_
45 |
46 | + Download the latest pyvXRAY release with pre-built modules. This is available for 32-bit and 64-bit Windows from [releases page](https://github.com/mhogg/pyvxray/releases)
47 |
48 | + Unpack this to a convenient location
49 |
50 | + Move the `abaqus_plugins\pyvXRAY` folder to the correct location of the `abaqus_plugins` directory within your ABAQUS installation. The location of this directory depends on your ABAQUS version. Some possible locations are:
51 |
52 | v6.11-x: `C:\SIMULIA\Abaqus\6.11-x\abaqus_plugins`
53 |
54 | v6.12-x: `C:\SIMULIA\Abaqus\6.12-x\code\python\lib\abaqus_plugins`
55 |
56 | v6.13-x: `C:\SIMULIA\Abaqus\6.13-x\code\python\lib\abaqus_plugins`
57 |
58 | * _Installation from source_
59 |
60 | + Download the latest pyvXRAY source, typically called `pyvXRAY-x.x.x.zip` or `pyvXRAY-x.x.x.tar.gz`, where `x.x.x` is the version number
61 |
62 | + Unpack this to a convenient location
63 |
64 | + Open a command prompt and browse to directory `pyvXRAY-x.x.x` (containing file `setup.py`)
65 |
66 | + Run the following command:
67 |
68 | abaqus python setup.py build_ext --inplace
69 |
70 | which will build the Cython modules. If Cython is available, it will be used. Otherwise the .cpp files previously generated using Cython will be compiled directly.
71 |
72 | + Copy the pyvXRAY sub-folder to the `abaqus_plugins` directory within your ABAQUS installation, following the instructions above for pre-built distribution
73 |
74 | ####2. Installation of pyvXRAY dependencies
75 |
76 | The ABAQUS GUI is built on Python, and has its own Python installation. This Python installation is not the typically Python setup, so some guidance is provided here on how to install pyvXRAY's dependencies.
77 |
78 | Currently pyvXRAY has only one dependency that is not part of the ABAQUS Python, which is PIL / Pillow (NOTE: Pillow is a PIL fork that appears to have largely superseded PIL).
79 | On Windows it is easiest to install PIL / Pillow using a binary installer, particularly because PIL / Pillow have many dependencies. There are currently several issues with this:
80 |
81 | * ABAQUS Python is typically not registered in the Windows registry, and therefore installation with binary installers will not work by default because the ABAQUS Python
82 | installation does not appear in the list of available Python installations
83 |
84 | * PIL binaries are available only for Python 2.6 on 32-bit Windows at [pythonware.com](http://www.pythonware.com/products/pil/), but not 64-bit Windows. However, Pillow binaries for Python 2.6 on both 32-bit and 64-bit Windows are available on [PyPi](https://pypi.python.org/pypi/Pillow).
85 |
86 | Given these limitations, there are two obvious choices for installating PIL / Pillow with ABAQUS Python.
87 |
88 | * _Use a separate Python installation and binary installer_
89 |
90 | The SIMULIA support site suggests that a separate Python installation be setup. The dependencies can be installed easily into this separate Python installation, after which they can
91 | then be used by ABAQUS Python. This separate Python installation version must match the ABAQUS Python version, which has been 2.6.x for the following few ABAQUS versions.
92 | This method is the best solution if you do not feel comfortable modifying the Windows registry (as recommended next).
93 |
94 | + Download and install Python 2.6 (i.e. the plain vanilla version from www.python.org/download is recommended)
95 |
96 | + Download and run the corresponding binary installer for [PIL](http://www.pythonware.com/products/pil/) or [Pillow](https://pypi.python.org/pypi/Pillow).
97 |
98 | + Create an environment variable `PYTHONPATH=C:\Python26\Lib\site-packages` that tells ABAQUS Python where these packages are installed, assuming that `C:\Python26` is the installation directory.
99 |
100 | * _Edit the Windows registry and use a binary installer (Windows only)_
101 |
102 | By editing the Windows registry the binary installers will be able to detect the ABAQUS Python version and install as usual. Use caution when editing the Windows registry or backup your
103 | registry before hand.
104 |
105 | + Download and run the corresponding binary installer for [PIL](http://www.pythonware.com/products/pil/) or [Pillow](https://pypi.python.org/pypi/Pillow).
106 |
107 | + Edit the Windows registry to create key `HKEY_LOCAL_MACHINE\Software\Python\Pythoncore\2.6\InstallPath` with data name "(Default)" and data value containing the location of your ABAQUS Python directory location. Registry key
108 | `HKEY_CURRENT_USER` also works. This location depends on the ABAQUS version. For the default ABAQUS installation location, possible locations are:
109 |
110 | v6.11-x: `C:\\SIMULIA\\Abaqus\\6.11-x\\External\\Python`
111 |
112 | v6.12-x: `C:\\SIMULIA\\Abaqus\\6.12-x\\tools\\SMApy`
113 |
114 | v6.13-x: `C:\\SIMULIA\\Abaqus\\6.13-x\\tools\\SMApy\\python2.6`
115 |
116 | Editing the Windows registry can be done using the regedit utility. You can load regedit by typing "regedit" at the command prompt.
117 |
118 | + Install PIL / Pillow using the binary installer. Follow the instructions and make sure to select the ABAQUS Python version if you have multiple Python versions installed. If ABAQUS Python is not in the list of available Python 2.6 versions, then the Windows registry was not edited correctly.
119 |
120 | ## Usage
121 |
122 | * Open ABAQUS/CAE
123 |
124 | * Open an odb file
125 |
126 | * To launch the pyvXRAY GUI, go to the menubar at the top of the screen and select:
127 |
128 | Plug-ins --> pyvXRAY --> Create virtual x-rays
129 |
130 | * Complete the required inputs in the GUI to suit the current model. More information is given below about the inputs
131 |
132 | * Click OK to run pyvXRAY
133 |
134 | * Look at the message area at the bottom of the screen for messages. On completion 'Finished' will be shown.
135 |
136 | ## Required inputs
137 |
138 | A basic description of each of the inputs required by pyvXRAY is listed here.
139 |
140 |
141 | GUI tab | Input name | Input description |
142 |
143 |
144 | Select regions |
145 | Result file: Odb |
146 | The ABAQUS result file |
147 |
148 |
149 |
150 | |
151 | Bone region: Bone set |
152 | The name of the element set representing the bone |
153 |
154 |
155 |
156 | |
157 | Bone region: Density variable |
158 | A scalar fieldoutput variable representing bone density. This is most often a state variable i.e. SDV1 |
159 |
160 |
161 |
162 | |
163 | Implant region: Show implant on x-rays |
164 | Option to include implant on the virtual x-rays |
165 |
166 |
167 |
168 | |
169 | Implant region: Implant set |
170 | The name of the element set representing the implant |
171 |
172 |
173 |
174 | |
175 | Implant region: Density (kg/m^3) |
176 | The density of the implant material in kg/m^3 i.e. 4500 for Titanium Alloy |
177 |
178 |
179 |
180 | Inputs |
181 | Required inputs: Step list |
182 | A list of steps to be analysed i.e. 1, 2, 3. A virtual x-ray is created for the last frame of each step in this list. |
183 |
184 |
185 |
186 | |
187 | Required inputs: Coordinate system |
188 | The name of the coordinate system used to create the projections. By default this is the global coordinate system. However, the views can be changed by creating a new coordinate
189 | system in ABAQUS and using it instead. |
190 |
191 |
192 |
193 | |
194 | Required inputs: Mapping resolution (mm) |
195 | pyvXRAY works by mapping the results of the bone density variable onto a regular grid. The mapping resolution is the cell spacing of this regular grid. Decreasing this number
196 | increases the accuracy of the mapping, but also increases the calculation time. As a first pass, a value of around 2mm is recommended to ensure that output is as expected. |
197 |
198 |
199 |
200 | X-ray settings |
201 | Settings: Base name of xray file(s) |
202 | This is the base or root name of the virtual x-ray image files. That is, image files are labelled basename_projection_stepnumber i.e. basename_XY_1 for
203 | the X-Y projection from Step 1. |
204 |
205 |
206 |
207 | |
208 | Settings: Approx size of x-ray images |
209 | Resizing of images is performed to make the number of pixels along the largest image dimension equal to this value. |
210 |
211 |
212 |
213 | |
214 | Settings: Image file format |
215 | Output format of images. Options are bmp, jpeg and png. |
216 |
217 |
218 |
219 | |
220 | Settings: Smooth images |
221 | Turn on image smoothing. PIL.ImageFilter.SMOOTH is used to perform the smoothing. |
222 |
223 |
224 |
225 | |
226 | Settings: Manual scaling of images |
227 | pyvXRAY scales the mapped bone density values when creating the virtual x-ray images. The image files are 24-bit (or 8-bit per channel), so the gray scale range is essentially 0-255.
228 | The scale factor used ensures that this range is fully utilised and that none of the images in the series are over-exposed. Activating this option reports the scale factors used and gives
229 | the user the ability to change these values. This may be desirable when comparing virtual x-rays from different models; an equal comparison is possible only if the same scale factors are
230 | used for both. |
231 |
232 |
233 |
234 |
235 | ## Outputs
236 |
237 | pyvXRAY outputs a series of virtual x-rays correponding to the bone density results in a list of specified analysis steps. The bone density is mapped from the Finite Element Model to a
238 | overlapping regular grid of points and then projected onto each of the three Cartesian coordinate planes. If the model has an implant, then this can also be shown. The virtual x-ray images
239 | are saved in common image formats (bmp, jpeg, and png) and can be opened in any graphics package. These images can then be analysed to determine changes in the grey scale values, which
240 | can be related to the change in Bone Mineral Density (BMD) over time.
241 |
242 | The recommended package for analysing these images is [BMDanalyse](https://github.com/mhogg/BMDanalyse), which is available free under the MIT license. BMDanalyse can be used to create
243 | regions of interest (ROIs) and determine the change in the average grey scale value within each ROI for all images in the series.
244 |
245 | ## Help
246 |
247 | For help create an Issue or a Pull Request on Github.
248 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | pyvXRAY
2 | =======
3 |
4 | **An ABAQUS plug-in for the creation of virtual x-rays from 3D finite
5 | element bone/implant models.**
6 |
7 |
8 | .. image:: https://img.shields.io/pypi/v/pyvxray.svg
9 | :target: https://pypi.python.org/pypi/pyvxray/
10 | :alt: Latest PyPI version
11 |
12 | .. image:: https://img.shields.io/pypi/dm/pyvxray.svg
13 | :target: https://pypi.python.org/pypi/pyvxray/
14 | :alt: Number of PyPI downloads
15 |
16 | ====
17 |
18 | Developed together with `bonemapy`_ and `BMDanalyse`_ to provide tools
19 | for preparation and post-processing of bone/implant computer models.
20 |
21 | .. _bonemapy: https://github.com/mhogg/bonemapy
22 | .. _BMDanalyse: https://github.com/mhogg/BMDanalyse
23 |
24 | Copyright 2013, Michael Hogg (michael.christopher.hogg@gmail.com)
25 |
26 | MIT license - See LICENSE.txt for details on usage and redistribution
27 |
28 | Requirements
29 | ------------
30 |
31 | Software requirements
32 | ~~~~~~~~~~~~~~~~~~~~~
33 |
34 | - ABAQUS >= 6.11 (tested on 6.11, 6.12 and 6.13)
35 | - Python Image Library (PIL) >= 1.1.6 OR Pillow >= 2.2.0 (Note: Pillow
36 | is in active development and is recommended over PIL)
37 |
38 | For building cython modules from source (e.g. if not using releases with
39 | pre-built modules):
40 |
41 | - A C compiler. Using ABAQUS Python on Windows requires Microsoft C++. Can use other compilers (i.e. mingw32) if you have a separate Python installation.
42 |
43 | - Cython >= 0.17. This is optional, as .cpp files generated by Cython are provided
44 |
45 | **NOTES:**
46 |
47 | 1. ABAQUS is a commerical software package and requires a license from
48 | `Simulia `__
49 | 2. The authors of pyvXRAY are not associated with ABAQUS/Simulia
50 | 3. Python and numpy are heavily used by pyvXRAY. These are built in to
51 | ABAQUS. All of the last few releases (v6.11 - v6.13) use Python 2.6.x
52 | and numpy 1.4.x
53 |
54 | Model setup requirements
55 | ~~~~~~~~~~~~~~~~~~~~~~~~
56 |
57 | - The model must contain only linear or quadrilateral tetrahedral
58 | elements (ABAQUS element types C3D4, C3D4H, C3D10, C3D10H, C3D10I,
59 | C3D10M, and C3D10MH are all supported).
60 |
61 | - The model must have a scalar fieldoutput variable representing bone
62 | density. This is typically a state variable such as SDV1. This scalar
63 | variable must be available in the last frame of each step to be
64 | analysed, as only the last frame is used.
65 |
66 | Installation
67 | ------------
68 |
69 | 1. Installation of pyvXRAY plug-in
70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
71 |
72 | pyvXRAY is an ABAQUS plug-in. ABAQUS plug-ins may be installed in
73 | several ways. Only one of the ways is discussed here. For other options
74 | the user is referred to the ABAQUS user manuals.
75 |
76 | - *Releases with pre-built modules*
77 |
78 | - Download the latest pyvXRAY release with pre-built modules. This is available for 32-bit and 64-bit Windows from `releases page `__
79 |
80 | - Unpack this to a convenient location
81 |
82 | - Move the ``abaqus_plugins\pyvXRAY`` folder to the correct location of
83 | the ``abaqus_plugins`` directory within your ABAQUS installation. The
84 | location of this directory depends on your ABAQUS version. Some
85 | possible locations are:
86 |
87 | v6.11-x: ``C:\SIMULIA\Abaqus\6.11-x\abaqus_plugins``
88 |
89 | v6.12-x: ``C:\SIMULIA\Abaqus\6.12-x\code\python\lib\abaqus_plugins``
90 |
91 | v6.13-x: ``C:\SIMULIA\Abaqus\6.13-x\code\python\lib\abaqus_plugins``
92 |
93 | - *Installation from source*
94 |
95 | - Download the latest pyvXRAY source, typically called
96 | ``pyvXRAY-x.x.x.zip`` or ``pyvXRAY-x.x.x.tar.gz``, where ``x.x.x`` is
97 | the version number
98 |
99 | - Unpack this to a convenient location
100 |
101 | - Open a command prompt and browse to directory ``pyvXRAY-x.x.x``
102 | (containing file ``setup.py``)
103 |
104 | - Run the following command:
105 |
106 | ::
107 |
108 | abaqus python setup.py build_ext --inplace
109 |
110 | which will build the Cython modules. If Cython is available, it will
111 | be used. Otherwise the .cpp files previously generated using Cython
112 | will be compiled directly.
113 |
114 | - Copy the pyvXRAY sub-folder to the ``abaqus_plugins`` directory
115 | within your ABAQUS installation, following the instructions above for
116 | pre-built distribution
117 |
118 | 2. Installation of pyvXRAY dependencies
119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
120 |
121 | The ABAQUS GUI is built on Python, and has its own Python installation.
122 | This Python installation is not the typically Python setup, so some
123 | guidance is provided here on how to install pyvXRAY's dependencies.
124 |
125 | Currently pyvXRAY has only one dependency that is not part of the ABAQUS
126 | Python, which is PIL / Pillow (NOTE: Pillow is a PIL fork that appears
127 | to have largely superseded PIL). On Windows it is easiest to install PIL
128 | / Pillow using a binary installer, particularly because PIL / Pillow
129 | have many dependencies. There are currently several issues with this:
130 |
131 | - ABAQUS Python is typically not registered in the Windows registry,
132 | and therefore installation with binary installers will not work by
133 | default because the ABAQUS Python installation does not appear in the
134 | list of available Python installations
135 |
136 | - PIL binaries are available only for Python 2.6 on 32-bit Windows at
137 | `pythonware.com `__, but not
138 | 64-bit Windows. However, Pillow binaries for Python 2.6 on both
139 | 32-bit and 64-bit Windows are available on
140 | `PyPi `__.
141 |
142 | Given these limitations, there are two obvious choices for installating
143 | PIL / Pillow with ABAQUS Python.
144 |
145 | - *Use a separate Python installation and binary installer*
146 |
147 | The SIMULIA support site suggests that a separate Python installation be
148 | setup. The dependencies can be installed easily into this separate
149 | Python installation, after which they can then be used by ABAQUS Python.
150 | This separate Python installation version must match the ABAQUS Python
151 | version, which has been 2.6.x for the following few ABAQUS versions.
152 | This method is the best solution if you do not feel comfortable
153 | modifying the Windows registry (as recommended next).
154 |
155 | - Download and install Python 2.6 (i.e. the plain vanilla version from
156 | www.python.org/download is recommended)
157 |
158 | - Download and run the corresponding binary installer for
159 | `PIL `__ or
160 | `Pillow `__.
161 |
162 | - Create an environment variable
163 | ``PYTHONPATH=C:\Python26\Lib\site-packages`` that tells ABAQUS Python
164 | where these packages are installed, assuming that ``C:\Python26`` is
165 | the installation directory.
166 |
167 | - *Edit the Windows registry and use a binary installer (Windows only)*
168 |
169 | By editing the Windows registry the binary installers will be able to
170 | detect the ABAQUS Python version and install as usual. Use caution when
171 | editing the Windows registry or backup your registry before hand.
172 |
173 | - Download and run the corresponding binary installer for
174 | `PIL `__ or
175 | `Pillow `__.
176 |
177 | - Edit the Windows registry to create key
178 | ``HKEY_LOCAL_MACHINE\Software\Python\Pythoncore\2.6\InstallPath``
179 | with data name "(Default)" and data value containing the location of
180 | your ABAQUS Python directory location. Registry key
181 | ``HKEY_CURRENT_USER`` also works. This location depends on the ABAQUS
182 | version. For the default ABAQUS installation location, possible
183 | locations are:
184 |
185 | v6.11-x: ``C:\\SIMULIA\\Abaqus\\6.11-x\\External\\Python``
186 |
187 | v6.12-x: ``C:\\SIMULIA\\Abaqus\\6.12-x\\tools\\SMApy``
188 |
189 | v6.13-x: ``C:\\SIMULIA\\Abaqus\\6.13-x\\tools\\SMApy\\python2.6``
190 |
191 | Editing the Windows registry can be done using the regedit utility.
192 | You can load regedit by typing "regedit" at the command prompt.
193 |
194 | - Install PIL / Pillow using the binary installer. Follow the
195 | instructions and make sure to select the ABAQUS Python version if you
196 | have multiple Python versions installed. If ABAQUS Python is not in
197 | the list of available Python 2.6 versions, then the Windows registry
198 | was not edited correctly.
199 |
200 | Usage
201 | -----
202 |
203 | - Open ABAQUS/CAE
204 |
205 | - Open an odb file
206 |
207 | - To launch the pyvXRAY GUI, go to the menubar at the top of the screen
208 | and select:
209 |
210 | ::
211 |
212 | Plug-ins --> pyvXRAY --> Create virtual x-rays
213 |
214 | - Complete the required inputs in the GUI to suit the current model.
215 | More information is given below about the inputs
216 |
217 | - Click OK to run pyvXRAY
218 |
219 | - Look at the message area at the bottom of the screen for messages. On
220 | completion 'Finished' will be shown.
221 |
222 | Required inputs
223 | ---------------
224 |
225 | A basic description of each of the inputs required by pyvXRAY is listed
226 | here.
227 |
228 | ================ ================================ =====================================================
229 | GUI tab Input name Input description
230 | ================ ================================ =====================================================
231 | Select regions Result file: Odb The ABAQUS result file
232 | ---------------- -------------------------------- -----------------------------------------------------
233 | \ Bone region: Bone set The name of the element set representing the bone
234 | ---------------- -------------------------------- -----------------------------------------------------
235 | \ Bone region: Density variable A scalar fieldoutput variable representing bone
236 | density.
237 | This is most often a state variable i.e. SDV1
238 | ---------------- -------------------------------- -----------------------------------------------------
239 | \ Implant region: Show implant Option to include implant on the virtual x-rays
240 | on x-rays
241 | ---------------- -------------------------------- -----------------------------------------------------
242 | \ Implant region: Implant set The name of the element set representing the implant
243 | ---------------- -------------------------------- -----------------------------------------------------
244 | \ Implant region: Density (kg/m^3) The density of the implant material in kg/m^3 i.e.
245 | 4500 for Titanium Alloy
246 | ---------------- -------------------------------- -----------------------------------------------------
247 | Inputs Required inputs: Step list A list of steps to be analysed i.e. 1, 2, 3. A
248 | virtual x-ray is created for the last frame of each
249 | step in this list.
250 | ---------------- -------------------------------- -----------------------------------------------------
251 | \ Required inputs: Coordinate The name of the coordinate system used to create the
252 | system projections. By default this is the global coordinate
253 | system. However, the views can be changed by creating
254 | a new coordinate system in ABAQUS and using it
255 | instead.
256 | ---------------- -------------------------------- -----------------------------------------------------
257 | \ Required inputs: Mapping pyvXRAY works by mapping the results of the bone
258 | resolution (mm) density variable onto a regular grid. The mapping
259 | resolution is the cell spacing of this regular grid.
260 | Decreasing this number increases the accuracy of the
261 | mapping, but also increases the calculation time.
262 | As a first pass, a value of around 2mm is
263 | recommended to ensure that output is as expected.
264 |
265 | ---------------- -------------------------------- -----------------------------------------------------
266 | X-ray settings Settings: Base name of xray This is the base or root name of the virtual x-ray
267 | file(s) image files. That is, image files are labelled
268 | basename\_projection\_stepnumber i.e.
269 | basename\_XY\_1 for the X-Y projection from Step 1.
270 |
271 | ---------------- -------------------------------- -----------------------------------------------------
272 | \ Settings: Approx size of x-ray Resizing of images is performed to make the number of
273 | images pixels along the largest image dimension equal to
274 | this value.
275 |
276 | ---------------- -------------------------------- -----------------------------------------------------
277 | \ Settings: Image file format Output format of images. Options are bmp, jpeg and
278 | png.
279 | ---------------- -------------------------------- -----------------------------------------------------
280 | \ Settings: Smooth images Turn on image smoothing. PIL.ImageFilter.SMOOTH is
281 | used to perform the smoothing.
282 | ---------------- -------------------------------- -----------------------------------------------------
283 | \ Settings: Manual scaling of pyvXRAY scales the mapped bone density values when
284 | images creating the virtual x-ray images. The image files
285 | are 24-bit (or 8-bit per channel), so the gray scale
286 | range is essentially 0-255. The scale factor used
287 | ensures that this range is fully utilised and that
288 | none of the images in the series are over-exposed.
289 | Activating this option reports the scale factors
290 | used and gives the user the ability to change these
291 | values. This may be desirable when comparing virtual
292 | x-rays from different models; an equal comparison
293 | is possible only if the same scale factors are used
294 | for both.
295 | ================ ================================ =====================================================
296 |
297 | +-----------------+-------------------------------------+------------------------------------------+
298 | | GUI tab | Input name | Input description |
299 | +=================+=====================================+==========================================+
300 | | | Settings: Manual scaling of images | pyvXRAY scales the mapped bone density |
301 | | | | values when creating the virtual x-ray |
302 | | | | images. The image files are 24-bit (or |
303 | | | | 8-bit per channel), so the gray scale |
304 | +-----------------+-------------------------------------+------------------------------------------+
305 |
306 | .. list-table::
307 | :widths: 10 1000 10
308 | :header-rows: 1
309 |
310 | * - GUI tab
311 | - Input name
312 | - Input description
313 | * - Select regions
314 | - Result file: Odb
315 | - The ABAQUS result file
316 | * - \
317 | - Bone region: Bone set
318 | - The name of the element set representing the bone
319 | * - \
320 | - Bone region: Density variable
321 | - A scalar fieldoutput variable representing bone density.
322 | This is most often a state variable i.e. SDV1
323 | * - \
324 | - Implant region: Show implant on x-rays
325 | - Option to include implant on the virtual x-rays
326 | * - \
327 | - Implant region: Implant set
328 | - The name of the element set representing the implant
329 | * - \
330 | - Implant region: Density (kg/m^3)
331 | - The density of the implant material in kg/m^3 i.e. 4500 for Titanium Alloy
332 | * - Inputs
333 | - Required inputs: Step list
334 | - A list of steps to be analysed i.e. 1, 2, 3. A virtual x-ray is created
335 | for the last frame of each step in this list.
336 | * - \
337 | - Required inputs: Coordinate system
338 | - The name of the coordinate system used to create the projections. By
339 | default this is the global coordinate system. However, the views can
340 | be changed by creating a new coordinate system in ABAQUS and using it instead.
341 | * - \
342 | - Required inputs: Mapping resolution (mm)
343 | - pyvXRAY works by mapping the results of the bone density variable onto a
344 | regular grid. The mapping resolution is the cell spacing of this regular
345 | grid. Decreasing this number increases the accuracy of the mapping, but
346 | also increases the calculation time. As a first pass, a value of around
347 | 2mm is recommended to ensure that output is as expected.
348 | * - X-ray settings
349 | - Settings: Base name of xray file(s)
350 | - This is the base or root name of the virtual x-ray image files. That is,
351 | image files are labelled basename\_projection\_stepnumber i.e.
352 | basename\_XY\_1 for the X-Y projection from Step 1.
353 | * - \
354 | - Settings: Approx size of x-ray images
355 | - Resizing of images is performed to make the number of pixels along the
356 | largest image dimension equal to this value.
357 | * - \
358 | - Settings: Image file format
359 | - Output format of images. Options are bmp, jpeg and png.
360 | * - \
361 | - Settings: Smooth images
362 | - Turn on image smoothing. PIL.ImageFilter.SMOOTH is used to perform the smoothing.
363 | * - \
364 | - Settings: Manual scaling of images
365 | - pyvXRAY scales the mapped bone density values when creating the virtual x-ray
366 | images. The image files are 24-bit (or 8-bit per channel), so the gray scale
367 | range is essentially 0-255. The scale factor used ensures that this range is
368 | fully utilised and that none of the images in the series are over-exposed.
369 | Activating this option reports the scale factors used and gives the user the
370 | ability to change these values. This may be desirable when comparing virtual
371 | x-rays from different models; an equal comparison is possible only if the same
372 | scale factors are used for both.
373 |
374 |
375 | Outputs
376 | -------
377 |
378 | pyvXRAY outputs a series of virtual x-rays correponding to the bone
379 | density results in a list of specified analysis steps. The bone density
380 | is mapped from the Finite Element Model to a overlapping regular grid of
381 | points and then projected onto each of the three Cartesian coordinate
382 | planes. If the model has an implant, then this can also be shown. The
383 | virtual x-ray images are saved in common image formats (bmp, jpeg, and
384 | png) and can be opened in any graphics package. These images can then be
385 | analysed to determine changes in the grey scale values, which can be
386 | related to the change in Bone Mineral Density (BMD) over time.
387 |
388 | The recommended package for analysing these images is
389 | `BMDanalyse `__, which is available
390 | free under the MIT license. BMDanalyse can be used to create regions of
391 | interest (ROIs) and determine the change in the average grey scale value
392 | within each ROI for all images in the series.
393 |
394 | Help
395 | ----
396 |
397 | For help create an Issue or a Pull Request on Github.
398 |
--------------------------------------------------------------------------------
/build_pyvXRAY.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | REM Script use to build pyvXRAY source and binary distributions
3 | cd cython
4 | cython -a cythonMods.pyx --cplus
5 | cd..
6 | @CALL abaqus python setup.py sdist
7 | @CALL abaqus python setup.py bdist
8 |
9 | REM @CALL abaqus python setup.py sdist upload
10 | REM @CALL c:\python26_32bit\python.exe setup.py bdist
11 | REM del pyvXRAY\*.pyd
12 |
--------------------------------------------------------------------------------
/cython/cythonMods.pyx:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | # cython: profile=False
8 | # cython: boundscheck=False
9 | # cython: wraparound=False
10 | # cython: infer_types=True
11 | # cython: nonecheck=False
12 | # cython: cdivision=False
13 |
14 | import numpy as np
15 | cimport numpy as np
16 | from libc.math cimport fabs, sqrt
17 | from libc.stdlib cimport malloc, free
18 | ctypedef np.float64_t float64
19 | ctypedef np.int32_t int32
20 |
21 |
22 | # Note: fmin and fmax are not included in math.h for MSVC! (Although they are included
23 | # in gcc). Work-around is to write own functions
24 | cdef double fmax(double a, double b):
25 | if (a>=b): return a
26 | else: return b
27 |
28 |
29 | cdef double fmin(double a, double b):
30 | if (a<=b): return a
31 | else: return b
32 |
33 |
34 | # Packed structure for use in numpy record array
35 | cdef packed struct mappedPoint:
36 | int32 cte
37 | float64 g,h,r
38 |
39 |
40 | #cpdef double LinearTetInterpFunc(double[::1] nv, double[::1] ipc):
41 | # """Shape function for first order tetrahedral (C3D4) element"""
42 | # cdef double N[4], U
43 | # CLinearTetShapeFuncMatrix(ipc,N)
44 | # U = vectDot(nv,N,4)
45 | # return U
46 |
47 |
48 | #cpdef double QuadTetInterpFunc(double[::1] nv, double[::1] ipc):
49 | # """Shape function for second order tetrahedral (C3D10) element"""
50 | # cdef double N[10], U
51 | # CQuadTetShapeFuncMatrix(ipc,N)
52 | # U = vectDot(nv,N,10)
53 | # return U
54 |
55 |
56 | cdef int convert3Dto1Dindex(int i, int j, int k, int NX, int NY, int NZ):
57 | """Converts 3D array index to 1D array index"""
58 | return i+j*NX+k*NX*NY
59 |
60 |
61 | cdef int getMinVals(double[:,::1] arr, double minVals[]):
62 | """Get minimum values in each column of array"""
63 | cdef int i, numvals, dim
64 | dim=arr.shape[0]; numvals=arr.shape[1]
65 | for i in range(dim):
66 | minVals[i] = arr[i,0]
67 | for i in range(dim):
68 | for j in range(1,numvals):
69 | minVals[i] = fmin(minVals[i],arr[i,j])
70 | return 0
71 |
72 |
73 | cdef int getMaxVals(double[:,::1] arr, double maxVals[]):
74 | """Get maximum values in each column of array"""
75 | cdef int i, numvals, dim
76 | dim=arr.shape[0]; numvals=arr.shape[1]
77 | for i in range(dim):
78 | maxVals[i] = arr[i,0]
79 | for i in range(dim):
80 | for j in range(1,numvals):
81 | maxVals[i] = fmax(maxVals[i],arr[i,j])
82 | return 0
83 |
84 |
85 | cdef int getNearest(double[:] arr, double val, int side):
86 | """Get nearest index to value in list"""
87 | cdef int indx, i, numvals = arr.shape[0]
88 | if val<=arr[0]: return 0
89 | if val> arr[numvals-1]: return numvals-1
90 | for i in range(numvals-1):
91 | if val>arr[i] and val<=arr[i+1]:
92 | if side==0: return i
93 | if side==1: return i+1
94 |
95 |
96 | cdef int MatMult(double[:,::1] A, double [:,::1] B, double[:,::1] C):
97 | """Matrix multiplication: A(l,m) x B(m,n) = C(l,n)"""
98 | cdef int i,j,k,l,m,n
99 | cdef double csum
100 | l = A.shape[0]; m=A.shape[1]; n=B.shape[1]
101 | if m!=B.shape[0] or l!=C.shape[0] or n!=C.shape[1]: return 1
102 | for i in range(l):
103 | for j in range(n):
104 | csum = 0.0
105 | for k in range(m):
106 | csum += A[i,k]*B[k,j]
107 | C[i,j] = csum
108 | return 0
109 |
110 |
111 | cdef int MatVecMult(double[:,::1] M, double v[], int vsize, double y[], int ysize):
112 | """Multiplies a matrix by a vector, y = M.v"""
113 | cdef int i,j,m,n
114 | cdef double csum
115 | m = M.shape[0]; n=M.shape[1];
116 | if m!=ysize or n!=vsize: return 1
117 | for i in range(m):
118 | csum = 0.0
119 | for j in range(n):
120 | csum += M[i,j]*v[j]
121 | y[i] = csum
122 | return 0
123 |
124 |
125 | cdef double vectDot(double[::1] v1, double v2[], int v2size):
126 | """Vector dot product"""
127 | cdef double vdot
128 | cdef int i
129 | vdot = 0.0
130 | for i in range(v1.shape[0]):
131 | vdot += v1[i]*v2[i]
132 | return vdot
133 |
134 |
135 | def createElementMap(dict nodeList, np.ndarray[int32,ndim=1] nConnect_labels,
136 | np.ndarray[int32,ndim=2] nConnect_connectivity, int numNodesPerElem,
137 | double[:] x, double[:] y, double[:] z):
138 |
139 | """Creates a map between a list of points and a list of solid tetrahedral elements"""
140 |
141 | cdef:
142 | int i,j,k,e,nlabel,NX,NY,NZ,iLow,jLow,kLow,iUpp,jUpp,kUpp,numElems,elemLabel,numGridPoints
143 | double xLow,yLow,zLow,xUpp,yUpp,zUpp
144 | double[::1] gridPointCoords=np.empty(3), ipc=np.zeros(3)
145 | double[:,::1] tetNodeCoords=np.empty((3,numNodesPerElem)), JM=np.empty((3,3))
146 | double[:,::1] dNdG=np.empty((numNodesPerElem,3))
147 | double tetCoordsLow[3], tetCoordsUpp[3]
148 |
149 | NX=x.shape[0]; NY=y.shape[0]; NZ=z.shape[0]
150 | numGridPoints = NX*NY*NZ
151 | dtype=np.dtype([('cte',np.int32),('g',np.float64),('h',np.float64),('r',np.float64)])
152 | cdef np.ndarray[mappedPoint,ndim=1] elementMap = np.zeros(numGridPoints,dtype=dtype)
153 |
154 | # Select correct function depending on linear or quadratic element
155 | if numNodesPerElem == 4: testPointInElement = TestPointInLinearTetElem
156 | if numNodesPerElem == 10: testPointInElement = TestPointInQuadTetElem
157 |
158 | # Create the element map
159 | numElems = nConnect_labels.shape[0]
160 | for e in range(numElems):
161 |
162 | # Tet element label
163 | elemLabel = nConnect_labels[e]
164 |
165 | # Tet node coordinates
166 | for j in range(numNodesPerElem):
167 | nlabel = nConnect_connectivity[e,j]
168 | for i in range(3):
169 | tetNodeCoords[i,j] = nodeList[nlabel][i]
170 |
171 | # Get bounding box around tet to limit grid points searched
172 | getMinVals(tetNodeCoords,tetCoordsLow)
173 | getMaxVals(tetNodeCoords,tetCoordsUpp)
174 | iLow = getNearest(x,tetCoordsLow[0],0); iUpp = getNearest(x,tetCoordsUpp[0],1)
175 | jLow = getNearest(y,tetCoordsLow[1],0); jUpp = getNearest(y,tetCoordsUpp[1],1)
176 | kLow = getNearest(z,tetCoordsLow[2],0); kUpp = getNearest(z,tetCoordsUpp[2],1)
177 |
178 | # Find intersections between tet and grid points
179 | for k in range(kLow,kUpp+1):
180 | for j in range(jLow,jUpp+1):
181 | for i in range(iLow,iUpp+1):
182 | gridPointCoords[0] = x[i]
183 | gridPointCoords[1] = y[j]
184 | gridPointCoords[2] = z[k]
185 | foundIntersection = testPointInElement(gridPointCoords,ipc,tetNodeCoords,dNdG,JM)
186 | if foundIntersection:
187 | gridPointIndex = convert3Dto1Dindex(i,j,k,NX,NY,NZ)
188 | elementMap[gridPointIndex].cte = elemLabel
189 | elementMap[gridPointIndex].g = ipc[0]
190 | elementMap[gridPointIndex].h = ipc[1]
191 | elementMap[gridPointIndex].r = ipc[2]
192 |
193 | return elementMap
194 |
195 |
196 | cdef int TestPointInLinearTetElem(double[::1] X2, double[::1] G, double[:,::1] nv,
197 | double[:,::1] dNdG, double[:,::1] JM):
198 |
199 | """Tests if a point lies within a first order tetrahedral (C3D4) element.
200 | This is a direct calculation, performed using the Jacobian matrix"""
201 |
202 | cdef:
203 | double tol,lowLim,uppLim,dX[3],N[4],X1[3]
204 | int result
205 |
206 | # Initialise variables
207 | tol=1.0e-4; lowLim=0.0-tol; uppLim=1.0+tol
208 | for i in range(3): G[i]=0.0
209 |
210 | CLinearTetShapeFuncMatrix(G,N)
211 | # nv(3x4) x N(4,1) = X1(3x1)
212 | result = MatVecMult(nv,N,4,X1,3)
213 | for i in range(3):
214 | dX[i] = X2[i]-X1[i]
215 |
216 | CLinearTetShapeFuncDerivMatrix(G,dNdG)
217 | # nv(3x4) x dNdG(4,3) = JM(3x3)
218 | result = MatMult(nv,dNdG,JM)
219 |
220 | # Solve system of linear equations, Dx = J Dg
221 | result = SolveLinearEquations(JM,dX)
222 | for i in range(3):
223 | G[i] += dX[i]
224 |
225 | # Test if point lies within tet element
226 | if((G[0]+G[1]+G[2])<=uppLim and \
227 | (G[0]>= lowLim and G[0]<=uppLim) and \
228 | (G[1]>= lowLim and G[1]<=uppLim) and \
229 | (G[2]>= lowLim and G[2]<=uppLim)):
230 | return 1
231 | else:
232 | return 0
233 |
234 |
235 | cdef int CLinearTetShapeFuncMatrix(double[::1] ipc, double N[]):
236 |
237 | cdef double g,h,r
238 |
239 | # Unpack isoparametric coordinates
240 | g = ipc[0]; h=ipc[1]; r=ipc[2]
241 |
242 | # Element shape functions
243 | N[0] = (1.0-g-h-r)
244 | N[1] = g
245 | N[2] = h
246 | N[3] = r
247 |
248 | return 0
249 |
250 |
251 | cdef int CLinearTetShapeFuncDerivMatrix(double[::1] ipc, double[:,::1] dNdG):
252 |
253 | cdef double g,h,r
254 |
255 | # Unpack isoparametric coordinates
256 | g = ipc[0]; h=ipc[1]; r=ipc[2]
257 |
258 | # Partial derivates of shape functions
259 | # dNdg
260 | dNdG[0,0] = -1.0
261 | dNdG[1,0] = 1.0
262 | dNdG[2,0] = 0.0
263 | dNdG[3,0] = 0.0
264 | # dNdh
265 | dNdG[0,1] = -1.0
266 | dNdG[1,1] = 0.0
267 | dNdG[2,1] = 1.0
268 | dNdG[3,1] = 0.0
269 | # dNdr
270 | dNdG[0,2] = -1.0
271 | dNdG[1,2] = 0.0
272 | dNdG[2,2] = 0.0
273 | dNdG[3,2] = 1.0
274 |
275 | return 0
276 |
277 |
278 | cdef int TestPointInQuadTetElem(double[::1] X2, double[::1] G, double[:,::1] nv,
279 | double[:,::1] dNdG, double[:,::1] JM):
280 |
281 | """Tests if a point lies within a second order tetrahedral (C3D10) element.
282 | This is an interative process performed using the Newton-Raphson method"""
283 |
284 | cdef:
285 | int maxIter,numIter,result,converged
286 | double tol,lowLim,uppLim,err1,err2,f[3],X1[3],N[10]
287 |
288 | # Solver parameters
289 | maxIter=50; tol=1.0e-6; lowLim=0.0-tol; uppLim=1.0+tol
290 |
291 | # Set initial values
292 | numIter=1; converged=0; G[0]=0.0; G[1]=0.0; G[2]=0.0
293 |
294 | # Run iterative loop to find iso-parametric coordinates G=(g,h,r) corresponding to point X2.
295 | while (numIter<=maxIter):
296 |
297 | CQuadTetShapeFuncMatrix(G,N)
298 | # nv(3x10) x N(10,1) = X1(3x1)
299 | result = MatVecMult(nv,N,10,X1,3)
300 | for i in range(3):
301 | f[i]= X2[i]-X1[i]
302 |
303 | err1=0.0
304 | for i in range(3):
305 | err1 += f[i]**2.0
306 | err1 = sqrt(err1)
307 |
308 | CQuadTetShapeFuncDerivMatrix(G,dNdG)
309 | # nv(3x10) x dNdG(10,3) = JM(3x3)
310 | result = MatMult(nv,dNdG,JM)
311 |
312 | # Solve system of linear equations, -f = J.dX
313 | result = SolveLinearEquations(JM,f)
314 | for i in range(3):
315 | G[i] += f[i]
316 |
317 | err2=0.0
318 | for i in range(3):
319 | err2 += f[i]**2.0
320 | err2 = sqrt(err2)
321 |
322 | # Break if error is within tolerance
323 | if (err1<=tol and err2<=tol):
324 | converged=1
325 | break
326 |
327 | # Increment loop counter
328 | numIter+=1
329 |
330 | # Test if point lies within tet element
331 | if converged and ((G[0]+G[1]+G[2])<=uppLim and \
332 | (G[0]>=lowLim and G[0]<=uppLim) and \
333 | (G[1]>=lowLim and G[1]<=uppLim) and \
334 | (G[2]>=lowLim and G[2]<=uppLim)):
335 | return 1
336 | else:
337 | return 0
338 |
339 |
340 | cdef int CQuadTetShapeFuncMatrix(double[::1] ipc, double N[]):
341 |
342 | cdef double g,h,r
343 |
344 | # Unpack isoparametric coordinates
345 | g = ipc[0]; h=ipc[1]; r=ipc[2]
346 |
347 | # Element shape functions
348 | N[0] = (2.0*(1.0-g-h-r)-1.0)*(1.0-g-h-r)
349 | N[1] = (2.0*g-1.0)*g
350 | N[2] = (2.0*h-1.0)*h
351 | N[3] = (2.0*r-1.0)*r
352 | N[4] = 4.0*(1.0-g-h-r)*g
353 | N[5] = 4.0*g*h
354 | N[6] = 4.0*(1.0-g-h-r)*h
355 | N[7] = 4.0*(1.0-g-h-r)*r
356 | N[8] = 4.0*g*r
357 | N[9] = 4.0*h*r
358 |
359 | return 0
360 |
361 |
362 | cdef int CQuadTetShapeFuncDerivMatrix(double[::1] ipc, double[:,::1] dNdG):
363 |
364 | cdef double g,h,r
365 |
366 | # Unpack isoparametric coordinates
367 | g = ipc[0]; h=ipc[1]; r=ipc[2]
368 |
369 | # Partial derivates of shape functions
370 | # dNdg
371 | dNdG[0,0] = 4.0*(g+h+r-1.0)+1.0
372 | dNdG[1,0] = 4.0*g-1.0
373 | dNdG[2,0] = 0.0
374 | dNdG[3,0] = 0.0
375 | dNdG[4,0] = 4.0*(1.0-2.0*g-h-r)
376 | dNdG[5,0] = 4.0*h
377 | dNdG[6,0] = -4.0*h
378 | dNdG[7,0] = -4.0*r
379 | dNdG[8,0] = 4.0*r
380 | dNdG[9,0] = 0.0
381 | # dNdh
382 | dNdG[0,1] = 4.0*(g+h+r-1.0)+1.0
383 | dNdG[1,1] = 0.0
384 | dNdG[2,1] = 4.0*h-1.0
385 | dNdG[3,1] = 0.0
386 | dNdG[4,1] = -4.0*g
387 | dNdG[5,1] = 4.0*g
388 | dNdG[6,1] = 4.0*(1.0-g-2.0*h-r)
389 | dNdG[7,1] = -4.0*r
390 | dNdG[8,1] = 0.0
391 | dNdG[9,1] = 4.0*r
392 | # dNdr
393 | dNdG[0,2] = 4.0*(g+h+r-1.0)+1.0
394 | dNdG[1,2] = 0.0
395 | dNdG[2,2] = 0.0
396 | dNdG[3,2] = 4.0*r-1.0
397 | dNdG[4,2] = -4.0*g
398 | dNdG[5,2] = 0.0
399 | dNdG[6,2] = -4.0*h
400 | dNdG[7,2] = 4.0*(1.0-g-h-2.0*r)
401 | dNdG[8,2] = 4.0*g
402 | dNdG[9,2] = 4.0*h
403 |
404 | return 0
405 |
406 |
407 | cdef int SolveLinearEquations(double[:,::1] A, double b[]):
408 |
409 | """Solves a system of linear equations, Ax=b, using Gaussian elimination
410 | with partial pivoting. A must be a square matrix. Both A and b are modified"""
411 |
412 | cdef int i,j,k,m,n,pvtStore
413 | cdef double pvt,temp
414 | m = A.shape[1]; n = A.shape[0]
415 |
416 | # Allocate memory for dynamic arrays
417 | cdef double *x = malloc(n*sizeof(double))
418 | cdef int *pivot = malloc(n*sizeof(int))
419 |
420 | # Solve equations
421 | for j in range(n-1):
422 | pvt = fabs(A[j,j])
423 | pvtStore = j
424 | pivot[j] = pvtStore
425 | # Find pivot row
426 | for i in range(j+1,n):
427 | temp = fabs(A[i,j])
428 | if (temp > pvt):
429 | pvt = temp
430 | pvtStore = i
431 | # Switch rows if necessary
432 | if (pivot[j] != pvtStore):
433 | pivot[j] = pvtStore
434 | pivot[pvtStore] = j
435 | for k in range(n):
436 | temp = A[j,k]
437 | A[j,k] = A[pivot[j],k]
438 | A[pivot[j],k] = temp
439 | temp = b[j]
440 | b[j] = b[pivot[j]]
441 | b[pivot[j]] = temp
442 | # Store multipliers
443 | for i in range(j+1,n):
444 | A[i,j] = A[i,j]/A[j,j]
445 | # Create zeros below the main diagonal
446 | for i in range(j+1,n):
447 | for k in range(j+1,n):
448 | A[i,k] = A[i,k]-A[i,j]*A[j,k]
449 | b[i] = b[i]-A[i,j]*b[j]
450 |
451 | # Back substitution
452 | x[n-1] = b[n-1]/A[n-1,n-1]
453 | for j in range(n-2,-1,-1):
454 | x[j] = b[j]
455 | for k in range(n-1,j,-1):
456 | x[j] = x[j]-x[k]*A[j,k]
457 | x[j] = x[j]/A[j,j]
458 |
459 | # Return x as b
460 | for i in range(n):
461 | b[i] = x[i]
462 |
463 | # Free memory
464 | free(x); free(pivot)
465 |
466 | return 0
467 |
--------------------------------------------------------------------------------
/pyvXRAY/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | # Version and date
8 | import version
9 | __version__ = version.version
10 | __releasedate__ = version.releasedate
--------------------------------------------------------------------------------
/pyvXRAY/elemTypes.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of bonemapy - See LICENSE.txt for information on usage and redistribution
6 |
7 | import numpy as np
8 | from abaqusConstants import C3D4, C3D4H, C3D10, C3D10H, C3D10I, C3D10M, C3D10MH
9 |
10 | # ~~~~~~~~~~
11 |
12 | class elementC3D4():
13 |
14 | def __init__(self):
15 | self.name = 'C3D4'
16 | self.desc = 'Linear tetrahedral element'
17 | self.numNodes = 4
18 | self.N = np.zeros(self.numNodes)
19 | self.nv = np.zeros(self.numNodes)
20 |
21 | def evalN(self,ipc):
22 | g,h,r = ipc
23 | self.N[0] = (1.0-g-h-r)
24 | self.N[1] = g
25 | self.N[2] = h
26 | self.N[3] = r
27 |
28 | def interp(self,ipc,nv=None):
29 | self.evalN(ipc)
30 | if nv==None: return np.dot(self.N,self.nv)
31 | else: return np.dot(self.N,nv)
32 |
33 | def setNodalValueByIndex(self,indx,val):
34 | self.nv[indx]=val
35 |
36 | # ~~~~~~~~~~
37 |
38 | class elementC3D4H(elementC3D4):
39 |
40 | def __init__(self):
41 | elementC3D4.__init__(self)
42 | self.name = 'C3D4H'
43 | self.desc = 'Linear tetrahedral element with hybrid formulation'
44 |
45 | # ~~~~~~~~~~
46 |
47 | class elementC3D10():
48 |
49 | def __init__(self):
50 | self.name = 'C3D10'
51 | self.desc = 'Quadratic tetrahedral element'
52 | self.numNodes = 10
53 | self.N = np.zeros(self.numNodes)
54 | self.nv = np.zeros(self.numNodes)
55 |
56 | def evalN(self,ipc):
57 | g,h,r = ipc
58 | self.N[0] = (2.0*(1.0-g-h-r)-1.0)*(1.0-g-h-r)
59 | self.N[1] = (2.0*g-1.0)*g
60 | self.N[2] = (2.0*h-1.0)*h
61 | self.N[3] = (2.0*r-1.0)*r
62 | self.N[4] = 4.0*(1.0-g-h-r)*g
63 | self.N[5] = 4.0*g*h
64 | self.N[6] = 4.0*(1.0-g-h-r)*h
65 | self.N[7] = 4.0*(1.0-g-h-r)*r
66 | self.N[8] = 4.0*g*r
67 | self.N[9] = 4.0*h*r
68 |
69 | def interp(self,ipc,nv=None):
70 | self.evalN(ipc)
71 | if nv==None: return np.dot(self.N,self.nv)
72 | else: return np.dot(self.N,nv)
73 |
74 | def setNodalValueByIndex(self,indx,val):
75 | self.nv[indx]=val
76 |
77 | # ~~~~~~~~~~
78 |
79 | class elementC3D10M(elementC3D10):
80 |
81 | def __init__(self):
82 | elementC3D10.__init__(self)
83 | self.name = 'C3D10M'
84 | self.desc = 'Quadratic tetrahedral element with modified formulation'
85 |
86 | # ~~~~~~~~~~
87 |
88 | class elementC3D10H(elementC3D10):
89 |
90 | def __init__(self):
91 | elementC3D10.__init__(self)
92 | self.name = 'C3D10H'
93 | self.desc = 'Quadratic tetrahedral element with hybrid formulation'
94 |
95 | # ~~~~~~~~~~
96 |
97 | class elementC3D10MH(elementC3D10M):
98 |
99 | def __init__(self):
100 | elementC3D10M.__init__(self)
101 | self.name = 'C3D10MH'
102 | self.desc = 'Quadratic tetrahedral element with modified hybrid formulation'
103 |
104 | # ~~~~~~~~~~
105 |
106 | class elementC3D10I(elementC3D10):
107 |
108 | def __init__(self):
109 | elementC3D10.__init__(self)
110 | self.name = 'C3D10I'
111 | self.desc = 'Quadratic tetrahedral element with imporved surface stress formulation'
112 |
113 | # ~~~~~~~~~~
114 |
115 | # Supported element types
116 | seTypes = {}
117 | seTypes['C3D4'] = elementC3D4
118 | seTypes['C3D4H'] = elementC3D4H
119 | seTypes['C3D10'] = elementC3D10
120 | seTypes['C3D10H'] = elementC3D10H
121 | seTypes['C3D10I'] = elementC3D10I
122 | seTypes['C3D10M'] = elementC3D10M
123 | seTypes['C3D10MH'] = elementC3D10MH
124 |
125 |
--------------------------------------------------------------------------------
/pyvXRAY/pyvXRAYDB.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | from abaqusConstants import *
8 | from abaqusGui import *
9 | import os
10 |
11 | thisPath = os.path.abspath(__file__)
12 | thisDir = os.path.dirname(thisPath)
13 |
14 | class PyvXRAYDB(AFXDataDialog):
15 |
16 | def __init__(self, form):
17 |
18 | # Construct the base class.
19 | AFXDataDialog.__init__(self, form, 'pyvXRAY - Create virtual x-rays',self.OK|self.CANCEL, DIALOG_ACTIONS_SEPARATOR)
20 | self.form = form
21 |
22 | okBtn = self.getActionButton(self.ID_CLICKED_OK)
23 | okBtn.setText('OK')
24 |
25 | # Define Tab book
26 | TabBook = FXTabBook(p=self, tgt=None, sel=0, opts=TABBOOK_NORMAL,x=0, y=0, w=0, h=0, pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,pt=DEFAULT_SPACING, pb=DEFAULT_SPACING)
27 |
28 | # Define Regions Tab
29 | FXTabItem(p=TabBook, text='Select regions', ic=None, opts=TAB_TOP_NORMAL,x=0, y=0, w=0, h=0, pl=6, pr=6, pt=DEFAULT_PAD, pb=DEFAULT_PAD)
30 | TabItem_1 = FXVerticalFrame(p=TabBook, opts=FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X,
31 | x=0, y=0, w=0, h=0, pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,
32 | pt=DEFAULT_SPACING, pb=DEFAULT_SPACING, hs=DEFAULT_SPACING, vs=DEFAULT_SPACING)
33 |
34 | # Select odb
35 | GroupBox_1 = FXGroupBox(p=TabItem_1, text='Result file', opts=FRAME_GROOVE|LAYOUT_FILL_X|LAYOUT_FILL_Y)
36 | VAligner_1 = AFXVerticalAligner(p=GroupBox_1, opts=0, x=0, y=0, w=0, h=0, pl=0, pr=0, pt=0, pb=0)
37 |
38 | ComboBox_1 = AFXComboBox(p=VAligner_1, ncols=35, nvis=1, text='%-27s'%'Odb:', tgt=form.odbNameKw, sel=0, pt=5, pb=5)
39 | if len(form.odbList)>0:
40 | for odbName in form.odbList:
41 | ComboBox_1.appendItem(odbName)
42 | form.odbNameKw.setValue(form.odbList[0])
43 | ComboBox_1.setMaxVisible(10)
44 | self.odbName = form.odbList[0]
45 | else: self.odbName=None
46 |
47 | # Select bone region
48 | GroupBox_2 = FXGroupBox(p=TabItem_1, text='Bone region', opts=FRAME_GROOVE|LAYOUT_FILL_X|LAYOUT_FILL_Y)
49 | VAligner_2 = AFXVerticalAligner(p=GroupBox_2, opts=0, x=0, y=0, w=0, h=0, pl=0, pr=0, pt=0, pb=0)
50 |
51 | self.ComboBox_2 = AFXComboBox(p=VAligner_2, ncols=35, nvis=1, text='Bone set:', tgt=form.bSetNameKw, sel=0, pt=5, pb=5)
52 | self.ComboBox_2.setMaxVisible(10)
53 | self.populateElementListComboBox()
54 |
55 | self.ComboBox_3 = AFXComboBox(p=VAligner_2, ncols=35, nvis=1, text='%-20s'%'Density variable:', tgt=form.BMDfonameKw, sel=0, pt=5, pb=5)
56 | self.ComboBox_3.setMaxVisible(10)
57 | self.populateScalarListComboBox()
58 |
59 | # Select implant region
60 | GroupBox_3 = FXGroupBox(p=TabItem_1, text='Implant region', opts=FRAME_GROOVE|LAYOUT_FILL_X|LAYOUT_FILL_Y)
61 |
62 | self.cb1 = FXCheckButton(p=GroupBox_3, text='Show implant on x-rays', tgt=form.showImplantKw, sel=0)
63 |
64 | VAligner_3 = AFXVerticalAligner(p=GroupBox_3, opts=0, x=0, y=0, w=0, h=0, pl=0, pr=0, pt=0, pb=0)
65 | self.ComboBox_4 = AFXComboBox(p=VAligner_3, ncols=35, nvis=1, text='Implant set:', tgt=form.iSetNameKw, sel=0, pt=5, pb=5)
66 | self.ComboBox_4.setMaxVisible(10)
67 | self.populateElementListComboBoxImplant()
68 |
69 | self.tf1 = AFXTextField(p=VAligner_3, ncols=0, labelText='Density (kg/m^3):', tgt=form.iDensityKw, sel=0, w=331, opts=LAYOUT_FIX_WIDTH)
70 |
71 | # Inputs Tab
72 | FXTabItem(p=TabBook, text='Inputs', ic=None, opts=TAB_TOP_NORMAL, x=0, y=0, w=0, h=0, pl=6, pr=6, pt=DEFAULT_PAD, pb=DEFAULT_PAD)
73 | TabItem_2 = FXVerticalFrame(p=TabBook, opts=FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X,
74 | x=0, y=0, w=0, h=0, pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,
75 | pt=DEFAULT_SPACING, pb=DEFAULT_SPACING, hs=DEFAULT_SPACING, vs=DEFAULT_SPACING)
76 | GroupBox_4 = FXGroupBox(p=TabItem_2, text='Required inputs', opts=FRAME_GROOVE|LAYOUT_FILL_X|LAYOUT_FILL_Y)
77 | VAligner_4 = AFXVerticalAligner(p=GroupBox_4, opts=0, x=0, y=0, w=0, h=0, pl=0, pr=0, pt=10, pb=10)
78 |
79 | AFXTextField(p=VAligner_4, ncols=26, labelText='Step list:', tgt=form.stepListKw, sel=0, pt=5, pb=5)
80 | self.popStepListComboBox()
81 |
82 | self.ComboBox_5 = AFXComboBox(p=VAligner_4, ncols=24, nvis=1, text='Coordinate system:', tgt=form.csysNameKw, sel=0, pt=5, pb=5)
83 | self.popCsysListComboBox()
84 |
85 | AFXTextField(p=VAligner_4, ncols=26, labelText='%-30s'%'Mapping resolution (mm):', tgt=form.resGridKw, sel=0, pt=5, pb=5)
86 |
87 | # X-ray settings Tab
88 | FXTabItem(p=TabBook, text='X-ray settings', ic=None, opts=TAB_TOP_NORMAL, x=0, y=0, w=0, h=0, pl=6, pr=6, pt=DEFAULT_PAD, pb=DEFAULT_PAD)
89 | TabItem_3 = FXVerticalFrame(p=TabBook, opts=FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X,
90 | x=0, y=0, w=0, h=0, pl=DEFAULT_SPACING, pr=DEFAULT_SPACING,
91 | pt=DEFAULT_SPACING, pb=DEFAULT_SPACING, hs=DEFAULT_SPACING, vs=DEFAULT_SPACING)
92 | GroupBox_5 = FXGroupBox(p=TabItem_3, text='Settings', opts=FRAME_GROOVE|LAYOUT_FILL_X|LAYOUT_FILL_Y)
93 | VAligner_5 = AFXVerticalAligner(p=GroupBox_5, opts=0, x=0, y=0, w=0, h=0, pl=0, pr=0, pt=10, pb=0)
94 |
95 | AFXTextField(p=VAligner_5, ncols=18, labelText='Base name of xray file(s):', tgt=form.imageNameBaseKw, sel=0, pt=5, pb=5)
96 |
97 | AFXTextField(p=VAligner_5, ncols=18, labelText='%-42s'%'Approx size of x-ray images (in pixels):', tgt=form.preferredXraySizeKw, sel=0, pt=5, pb=5)
98 |
99 | ComboBox_6 = AFXComboBox(p=VAligner_5, ncols=16, nvis=1, text='Image file format', tgt=form.imageFormatKw, sel=0, pt=5, pb=5)
100 | for imageFormat in form.imageFormats:
101 | ComboBox_6.appendItem(text=imageFormat)
102 | ComboBox_6.setMaxVisible(5)
103 |
104 | FXCheckButton(p=GroupBox_5, text='Smooth images', tgt=form.smoothKw, sel=0, pt=10, pb=5)
105 |
106 | FXCheckButton(p=GroupBox_5, text='Manual scaling of images', tgt=form.manualScalingKw, sel=0, pt=10, pb=5)
107 |
108 | def populateElementListComboBox(self):
109 | """Populate comboBox containing element sets for bone"""
110 | if len(self.form.elementSets)==0: return
111 | self.ComboBox_2.clearItems()
112 | for elementSet in self.form.elementSets:
113 | self.ComboBox_2.appendItem(elementSet)
114 | self.form.bSetNameKw.setValue(self.form.elementSets[0])
115 |
116 | def populateScalarListComboBox(self):
117 | """Populate comboBox containing scalar fieldoutputs"""
118 | if len(self.form.scalarList)==0: return
119 | self.ComboBox_3.clearItems()
120 | for scalar in self.form.scalarList:
121 | self.ComboBox_3.appendItem(scalar)
122 | self.form.BMDfonameKw.setValue(self.form.scalarList[0])
123 |
124 | def populateElementListComboBoxImplant(self):
125 | """Populate comboBox containing element sets for implant"""
126 | if len(self.form.elementSets)==0: return
127 | self.ComboBox_4.clearItems()
128 | for elementSet in self.form.elementSets:
129 | self.ComboBox_4.appendItem(elementSet)
130 | self.form.iSetNameKw.setValue(self.form.elementSets[0])
131 |
132 | def popStepListComboBox(self):
133 | self.form.stepListKw.setValue(', '.join(self.form.stepList))
134 |
135 | def popCsysListComboBox(self):
136 | self.ComboBox_5.clearItems()
137 | csyses = []
138 | for csysType,csysNames in self.form.csysList.items():
139 | for csysName in csysNames:
140 | listText = '%s (%s)' % (csysName,csysType)
141 | csyses.append(listText)
142 | csyses.sort()
143 | csyses.insert(0,'GLOBAL') # Add global to the start of the sorted list
144 | self.form.csysNameKw.setValue(csyses[0])
145 | for csys in csyses:
146 | self.ComboBox_5.appendItem(text=csys)
147 | self.ComboBox_5.setMaxVisible(5)
148 |
149 | def processUpdates(self):
150 | """Update form"""
151 | # If odb name changes, the re-populate the region list
152 | if self.form.odbNameKw.getValue() != self.odbName:
153 | # Update odb name
154 | self.odbName = self.form.odbNameKw.getValue()
155 | # Get odb details
156 | self.form.setOdb(self.odbName)
157 | self.form.getElementSetList()
158 | self.form.getScalarList()
159 | self.form.getSteps()
160 | self.form.getCsyses()
161 | # Re-populate combo boxes
162 | self.populateElementListComboBox()
163 | self.populateElementListComboBoxImplant()
164 | self.populateScalarListComboBox()
165 | self.popStepListComboBox()
166 | self.popCsysListComboBox()
167 | # Disable implant option if show implant not checked
168 | tfs = [self.tf1,self.ComboBox_4]
169 | if self.cb1.getCheck():
170 | for tf in tfs: tf.enable()
171 | else:
172 | for tf in tfs: tf.disable()
173 | return
174 |
175 |
--------------------------------------------------------------------------------
/pyvXRAY/pyvXRAY_plugin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | from abaqusGui import *
8 | from abaqusConstants import ALL, CARTESIAN, SCALAR, INTEGRATION_POINT, CENTROID, ELEMENT_NODAL
9 | from version import version as __version__
10 | # Required to ensure the CSYS list is up to date
11 | from kernelAccess import session
12 |
13 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 |
15 | class PyvXRAY_plugin(AFXForm):
16 |
17 | def __init__(self, owner):
18 |
19 | # Construct the base class
20 | AFXForm.__init__(self, owner)
21 |
22 | self.odb = None
23 | self.csysList = None
24 | self.stepList = None
25 | self.elementSets = None
26 | self.scalarList = None
27 | self.imageFormats = ['bmp','jpeg','png']
28 | self.suppLocs = [INTEGRATION_POINT, CENTROID, ELEMENT_NODAL]
29 |
30 | # Keyword definitions
31 | self.cmd = AFXGuiCommand(mode=self, method='createVirtualXrays',objectName='virtualXrays', registerQuery=False)
32 | self.odbNameKw = AFXStringKeyword(self.cmd, 'odbName', True, '')
33 | self.bSetNameKw = AFXStringKeyword(self.cmd, 'bRegionSetName', True, '')
34 | self.BMDfonameKw = AFXStringKeyword(self.cmd, 'BMDfoname', True, '')
35 | self.showImplantKw = AFXBoolKeyword(self.cmd, 'showImplant', AFXBoolKeyword.TRUE_FALSE, True, False)
36 | self.iSetNameKw = AFXStringKeyword(self.cmd, 'iRegionSetName', True, '')
37 | self.iDensityKw = AFXFloatKeyword(self.cmd, 'iDensity', True, 4500)
38 | self.stepListKw = AFXStringKeyword(self.cmd, 'stepList', True, '')
39 | self.csysNameKw = AFXStringKeyword(self.cmd, 'csysName', True, '')
40 | self.resGridKw = AFXFloatKeyword(self.cmd, 'resGrid', True, 2)
41 | self.imageNameBaseKw = AFXStringKeyword(self.cmd, 'imageNameBase', True, 'vxray')
42 | self.preferredXraySizeKw = AFXIntKeyword(self.cmd, 'preferredXraySize', True, 800)
43 | self.imageFormatKw = AFXStringKeyword(self.cmd, 'imageFormat', True, self.imageFormats[-1])
44 | self.smoothKw = AFXBoolKeyword(self.cmd, 'smooth', AFXBoolKeyword.TRUE_FALSE, True, True)
45 | self.manualScalingKw = AFXBoolKeyword(self.cmd, 'manualImageScaling', AFXBoolKeyword.TRUE_FALSE, True, False)
46 |
47 | def getOdbList(self):
48 | """Get a list of all available odbs in session"""
49 | self.odbList = session.odbs.keys()
50 |
51 | def getFirstOdb(self):
52 | """Set first odb in first Dialog Box"""
53 | if self.odbList==None or len(self.odbList)==0: return
54 | self.setOdb(self.odbList[0])
55 |
56 | def setOdb(self,odbName):
57 | """Set odb from name"""
58 | if odbName=='': return
59 | self.odb = session.odbs[odbName]
60 |
61 | def getElementSetList(self):
62 | """Get list of all element sets in the current odb"""
63 | self.elementSets=[]
64 | if self.odb==None: return
65 | # Check part instances
66 | for instName,inst in self.odb.rootAssembly.instances.items():
67 | self.elementSets.append('.'.join([instName,'ALL']))
68 | for setName in inst.elementSets.keys():
69 | self.elementSets.append('.'.join([instName,setName]))
70 | # Check assembly
71 | for setName in self.odb.rootAssembly.elementSets.keys():
72 | self.elementSets.append(setName)
73 | self.elementSets.sort()
74 |
75 | def getCsyses(self):
76 | """Get list of all available csyses"""
77 | # Check scratch odb csyses
78 | self.csysList = {'Session':[], 'ODB':[]}
79 | for k,v in session.scratchOdbs.items():
80 | for csysName,csys in v.rootAssembly.datumCsyses.items():
81 | if csys.type==CARTESIAN:
82 | self.csysList['Session'].append(csysName)
83 | # Check odb csyses if an odb is open in the current viewport
84 | if self.odb != None:
85 | for csysName,csys in self.odb.rootAssembly.datumCsyses.items():
86 | if csys.type==CARTESIAN:
87 | self.csysList['ODB'].append(csysName)
88 |
89 | def getSteps(self):
90 | """Get list of all available steps"""
91 | self.stepList=[]
92 | if self.odb==None: return
93 | for stepName in self.odb.steps.keys():
94 | stepNumber = stepName.split('-')[-1]
95 | self.stepList.append(stepNumber)
96 | return
97 |
98 | def getScalarList(self):
99 | """Get list of available scalars. Check last frame in all steps"""
100 | self.scalarList=[]
101 | if self.odb==None: return
102 | includeList={}; excludeList={}
103 | for step in self.odb.steps.values():
104 | frame = step.frames[-1]
105 | for k in frame.fieldOutputs.keys():
106 | if excludeList.has_key(k) or includeList.has_key(k): continue
107 | v = frame.fieldOutputs[k]
108 | loc = [True for loc in v.locations if loc.position in self.suppLocs]
109 | if any(loc) and v.type==SCALAR: includeList[k]=1
110 | else: excludeList[k]=1
111 | self.scalarList = includeList.keys()
112 | self.scalarList.sort()
113 |
114 | def getFirstDialog(self):
115 | """Create the dialog box"""
116 | # Get odb information to populate the dialog box
117 | self.getOdbList()
118 | self.getFirstOdb()
119 | self.getElementSetList()
120 | self.getCsyses()
121 | self.getSteps()
122 | self.getScalarList()
123 | # Create dialog box
124 | import pyvXRAYDB
125 | return pyvXRAYDB.PyvXRAYDB(self)
126 |
127 | def doCustomChecks(self):
128 | """Define empty class function doCustomChecks to check user inputs"""
129 |
130 | # Check that odb exists
131 | self.getOdbList()
132 | if self.odbNameKw.getValue() not in self.odbList:
133 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Odb %s does not exist' % self.modelNameKw.getValue())
134 | return False
135 |
136 | # Check that bone region exists in model
137 | self.getElementSetList()
138 | if self.bSetNameKw.getValue() not in self.elementSets:
139 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Bone region %s does not exist' % self.bSetNameKw.getValue())
140 | return False
141 |
142 | # If implant is requested, check implant inputs
143 | if self.showImplantKw.getValue():
144 | # Check implant region
145 | self.getElementSetList()
146 | if self.iSetNameKw.getValue() not in self.elementSets:
147 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Implant region %s does not exist' % self.iSetNameKw.getValue())
148 | return False
149 | # Check input density
150 | iDensity = self.iDensityKw.getValue()
151 | try: float(iDensity)
152 | except:
153 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Implant density value is not a valid number')
154 | return False
155 | if iDensity<0:
156 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Implant density must be greater than 0')
157 | return False
158 |
159 | # Check that values in stepList are valid
160 | stepList = self.stepListKw.getValue()
161 | try:
162 | stepList = [int(s) for s in stepList.replace(',',' ').split()]
163 | except:
164 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Cannot convert step list values to integers')
165 | return False
166 | stepList.sort()
167 |
168 | # Check that all steps in step list exist and that density variable exists in all steps (last frame)
169 | BMDfoname = self.BMDfonameKw.getValue()
170 | stepInfo = {}
171 | for stepName,step in self.odb.steps.items():
172 | stepNumber = int(stepName.split('-')[-1])
173 | stepInfo[stepNumber] = step.frames[-1].fieldOutputs.keys()
174 | for stepNumber in stepList:
175 | if stepNumber not in stepInfo:
176 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Step number %i is not available in odb' % stepNumber)
177 | return False
178 | if BMDfoname not in stepInfo[stepNumber]:
179 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Density variable %s is not available in Step number %i' % (BMDfoname,stepNumber))
180 | return False
181 |
182 | # Check mapping resolution, resGrid
183 | resGrid = self.resGridKw.getValue()
184 | try: resGrid = float(resGrid)
185 | except:
186 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: "Inputs: Mapping resolution" value not valid')
187 | return False
188 |
189 | # Check preferred size of images
190 | preferredXraySize = self.preferredXraySizeKw.getValue()
191 | try: preferredXraySize = int(preferredXraySize)
192 | except:
193 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: "X-ray Settings: Approx size of x-ray" value not valid')
194 | return False
195 | minXraySize = 100
196 | if preferredXraySize < minXraySize:
197 | showAFXErrorDialog(self.getCurrentDialog(), 'Error: Minimum virtual x-ray image size is %i pixels' % minXraySize)
198 | return False
199 |
200 | # Check for Abaqus version >= 6.11
201 | majorNumber, minorNumber, updateNumber = getAFXApp().getVersionNumbers()
202 | if majorNumber==6 and minorNumber < 11:
203 | showAFXErrorDialog( self.getCurrentDialog(), 'Error: ABAQUS 6.11 and above is required' )
204 | return False
205 |
206 | return True
207 |
208 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
209 |
210 | # Register the plug-in
211 |
212 | desc = 'An ABAQUS plugin used to generate a time series of virtual x-rays from the output of a bone remodelling analysis.\n\n' + \
213 | 'The resulting images can be used to analyse the change in bone density over time in a number of regions of interest or "Gruen Zones", ' + \
214 | 'typically due to changes in loading following insertion of an orthopaedic implant. Associated tool BMDanalyse, available on PyPi, was ' + \
215 | 'created for this sole purpose.\n\nRequires an odb file to be open within the current viewport which has a fieldoutput representing ' + \
216 | 'bone density. Works by mapping the bone density fieldoutput onto a regular 3D grid and then projecting the values onto the three ' + \
217 | 'orthogonal planes. Currently only models with C3D4 or C3D10 elements are supported.'
218 |
219 | toolset = getAFXApp().getAFXMainWindow().getPluginToolset()
220 | toolset.registerGuiMenuButton(
221 | buttonText='pyvXRAY|Create virtual x-rays',
222 | object=PyvXRAY_plugin(toolset),
223 | messageId=AFXMode.ID_ACTIVATE,
224 | icon=None,
225 | kernelInitString='import virtualXrays',
226 | applicableModules=ALL,
227 | version=__version__,
228 | author='Michael Hogg',
229 | description=desc,
230 | helpUrl='https://github.com/mhogg/pyvxray'
231 | )
232 |
--------------------------------------------------------------------------------
/pyvXRAY/version.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | # Version and date
8 | version = '0.2.1'
9 | releasedate = '2015-08-12'
--------------------------------------------------------------------------------
/pyvXRAY/virtualXrays.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | import os
8 | from abaqus import session, getInputs
9 | from abaqusConstants import ELEMENT_NODAL
10 | from cythonMods import createElementMap
11 | import elemTypes as et
12 | import copy
13 | from odbAccess import OdbMeshElementType
14 | import numpy as np
15 |
16 | # ~~~~~~~~~~
17 |
18 | def convert3Dto1Dindex(i,j,k,NX,NY,NZ):
19 | """Converts 3D array index to 1D array index"""
20 | index = i+j*NX+k*NX*NY
21 | return index
22 |
23 | # ~~~~~~~~~~
24 |
25 | def convert1Dto3Dindex(index,NX,NY,NZ):
26 | """Converts 1D array index to 1D array index"""
27 | k = index / (NX*NY)
28 | j = (index - k*NX*NY) / NX
29 | i = index - k*NX*NY - j*NX
30 | return [i,j,k]
31 |
32 | # ~~~~~~~~~~
33 |
34 | def transformPoint(TM,point):
35 | """Transforms point using supplied transform"""
36 | point = np.append(point,1.0)
37 | return np.dot(TM,point)[:3]
38 |
39 | # ~~~~~~~~~~
40 |
41 | def createTransformationMatrix(Ma,Mb,Vab,rel='a'):
42 | """
43 | Creates a transformation matrix that can be used to transform a point from csys a to csys b.
44 | Ma = 3x3 matrix containing unit vectors of orthogonal coordinate directions for csys a
45 | Mb = 3x3 matrix containing unit vectors of orthogonal coordinate directions for csys b
46 | Vab = 3x1 vector from origin of csys a to csys b
47 | rel = 'a' or 'b' = Character to indicate if Vab is relative to csys a or csys b
48 | """
49 | if rel!='a' and rel!='b': return None
50 | a1,a2,a3 = Ma
51 | b1,b2,b3 = Mb
52 | # Rotation matrix
53 | R = np.identity(4,np.float)
54 | R[0,0:3] = [np.dot(b1,a1), np.dot(b1,a2), np.dot(b1,a3)]
55 | R[1,0:3] = [np.dot(b2,a1), np.dot(b2,a2), np.dot(b2,a3)]
56 | R[2,0:3] = [np.dot(b3,a1), np.dot(b3,a2), np.dot(b3,a3)]
57 | # Transformation matrix
58 | if rel=='b':
59 | Vab = np.append(Vab,1.0)
60 | Vab = np.dot(R.T,Vab)[0:3]
61 | T = np.identity(4,np.float)
62 | T[0:3,3] = -Vab
63 | # Transformation matrix
64 | return np.dot(R,T)
65 |
66 | # ~~~~~~~~~~
67 |
68 | def getTMfromCsys(odb,csysName):
69 | if csysName=='GLOBAL': return None
70 | # Parse coordinate system name
71 | csysName = csysName.split(r'(')[0].strip()
72 | # Get ABAQUS datumCsys
73 | lcsys = None
74 | # Check odb csyses
75 | if csysName in odb.rootAssembly.datumCsyses.keys():
76 | lcsys = odb.rootAssembly.datumCsyses[csysName]
77 | # Check scratch odb csyses
78 | if odb.path in session.scratchOdbs.keys():
79 | if csysName in session.scratchOdbs[odb.path].rootAssembly.datumCsyses.keys():
80 | lcsys = session.scratchOdbs[odb.path].rootAssembly.datumCsyses[csysName]
81 | if lcsys==None: return None
82 | # Global coordinate system
83 | Og = np.zeros(3)
84 | Mg = np.identity(3)
85 | # Local coordinate system
86 | Ol = lcsys.origin
87 | Ml = np.zeros((3,3))
88 | Ml[0] = lcsys.xAxis/np.linalg.norm(lcsys.xAxis) # NOTE: This should already be a unit vector
89 | Ml[1] = lcsys.yAxis/np.linalg.norm(lcsys.yAxis) # Shouldn't need to normalise
90 | Ml[2] = lcsys.zAxis/np.linalg.norm(lcsys.zAxis)
91 | # Create transformation matrix
92 | Vgl = Ol-Og
93 | TM = createTransformationMatrix(Mg,Ml,Vgl,rel='a')
94 | return TM
95 |
96 | # ~~~~~~~~~~
97 |
98 | def projectXrayPlane(spaceArray3D,whichPlane):
99 | """Project 3D BMD data onto the specified plane"""
100 | # Perform the projection by summing along orthogonal axis
101 | planeSelect = {'xy':2, 'yz':0, 'xz':1}
102 | return np.sum(spaceArray3D,axis=planeSelect[whichPlane],dtype=spaceArray3D.dtype)
103 |
104 | # ~~~~~~~~~~
105 |
106 | def writeImageFile(xrayImageFilename,BMDprojected,imageSize,imageFormat='bmp',smooth=True):
107 | """Create an image from array and write to file"""
108 |
109 | # Re-import PIL.Image and PIL.ImageFilter into current function namespace
110 | from PIL import Image, ImageFilter
111 |
112 | # Convert to 8-bit
113 | xray = BMDprojected.copy()
114 | xray = np.asarray(xray,dtype=np.int8)
115 |
116 | # Create image from array.
117 | xray = xray[:,::-1]
118 | xrayImage = Image.fromarray(xray.transpose(),mode='L')
119 |
120 | # Resize image
121 | xsize,ysize = xrayImage.size
122 | bigSide = np.argmax(xrayImage.size)
123 | if bigSide==0: scale = float(imageSize)/xsize
124 | else: scale = float(imageSize)/ysize
125 | xsize = int(np.rint(scale*xsize))
126 | ysize = int(np.rint(scale*ysize))
127 | xrayImage = xrayImage.resize((xsize,ysize),Image.BILINEAR)
128 | if smooth: xrayImage = xrayImage.filter(ImageFilter.SMOOTH)
129 |
130 | # Save xray image to file
131 | if xrayImageFilename.split('.')[-1]!=imageFormat: xrayImageFilename+='.%s' % imageFormat
132 | xrayImage.save(xrayImageFilename,imageFormat)
133 |
134 | return
135 |
136 | # ~~~~~~~~~~
137 |
138 | def parseRegionSetName(regionSetName):
139 | """ Get region and setName from regionSetName """
140 | if '.' in regionSetName: region,setName = regionSetName.split('.')
141 | else: region,setName = 'Assembly',regionSetName
142 | return region,setName
143 |
144 | # ~~~~~~~~~~
145 |
146 | def getElements(odb,regionSetName):
147 |
148 | """Get element type and number of nodes per element"""
149 |
150 | # Get region set and elements
151 | region,setName = parseRegionSetName(regionSetName)
152 | if region=='Assembly':
153 | setRegion = odb.rootAssembly.elementSets[regionSetName]
154 | if type(setRegion.elements[0])==OdbMeshElementType:
155 | elements = setRegion.elements
156 | else:
157 | elements=[]
158 | for meshElemArray in setRegion.elements:
159 | for e in meshElemArray:
160 | elements.append(e)
161 | else:
162 | if setName=='ALL':
163 | setRegion = odb.rootAssembly.instances[region]
164 | elements = setRegion.elements
165 | else:
166 | setRegion = odb.rootAssembly.instances[region].elementSets[setName]
167 | elements = setRegion.elements
168 |
169 | # Get part information: (1) instance names, (2) element types and (3) number of each element type
170 | partInfo={}
171 | for e in elements:
172 | if not partInfo.has_key(e.instanceName): partInfo[e.instanceName]={}
173 | if not partInfo[e.instanceName].has_key(e.type): partInfo[e.instanceName][e.type]=0
174 | partInfo[e.instanceName][e.type]+=1
175 |
176 | # Put all element types from all part instances in a list
177 | eTypes = []
178 | for k1 in partInfo.keys():
179 | for k2 in partInfo[k1].keys(): eTypes.append(k2)
180 | eTypes = dict.fromkeys(eTypes,1).keys()
181 |
182 | # Check that elements are supported
183 | usTypes=[]
184 | for eType in eTypes:
185 | if not any([True for seType in et.seTypes.keys() if seType==eType]):
186 | usTypes.append(str(eType))
187 | if len(usTypes)>0:
188 | if len(usTypes)==1: strvars = ('',usTypes[0],regionSetName,'is')
189 | else: strvars = ('s',', '.join(usTypes),regionSetName,'are')
190 | print '\nElement type%s %s in region %s %s not supported' % strvars
191 | return None
192 |
193 | return partInfo, setRegion, elements
194 |
195 | # ~~~~~~~~~~
196 |
197 | def getPartData(odb,regionSetName,TM):
198 |
199 | """Get region data based on original (undeformed) coordinates"""
200 |
201 | # Get elements and part info
202 | result = getElements(odb,regionSetName)
203 | if result==None: return None
204 | else:
205 | regionInfo, regionSet, elements = result
206 | numElems = len(elements)
207 | ec = dict([(ename,eclass()) for ename,eclass in et.seTypes.items()])
208 |
209 | # Create empty dictionary,array to store element data
210 | elemData = copy.deepcopy(regionInfo)
211 | for instName in elemData.keys():
212 | for k,v in elemData[instName].items():
213 | elemData[instName][k] = np.zeros(v,dtype=[('label','|i4'),('econn','|i4',(ec[k].numNodes,))])
214 | eCount = dict([(k1,dict([k2,0] for k2 in regionInfo[k1].keys())) for k1 in regionInfo.keys()])
215 | setNodeLabs = dict([(k,{}) for k in regionInfo.keys()])
216 | # Create a list of element connectivities (list of nodes connected to each element)
217 | for e in xrange(numElems):
218 |
219 | elem = elements[e]
220 | eConn = elem.connectivity
221 | eInst = elem.instanceName
222 | eType = elem.type
223 |
224 | eIndex = eCount[eInst][eType]
225 | elemData[eInst][eType][eIndex] = (elem.label,eConn)
226 | eCount[eInst][eType] +=1
227 |
228 | for n in eConn:
229 | setNodeLabs[eInst][n] = 1
230 |
231 | numSetNodes = np.sum([len(setNodeLabs[k]) for k in setNodeLabs.keys()])
232 | setNodes = np.zeros(numSetNodes,dtype=[('instName','|a80'),('label','|i4'),('coord','|f4',(3,))])
233 | nodeCount = 0
234 | for instName in setNodeLabs.keys():
235 | inst = odb.rootAssembly.instances[instName]
236 | nodes = inst.nodes
237 | numNodes = len(nodes)
238 | for n in xrange(numNodes):
239 | node = nodes[n]
240 | label = node.label
241 | if label in setNodeLabs[instName]:
242 | setNodes[nodeCount] = (instName,label,node.coordinates)
243 | nodeCount+=1
244 |
245 | # Transform the coordinates from the global csys to the local csys
246 | if TM is not None:
247 | print 'TM is not None'
248 | for i in xrange(numSetNodes):
249 | setNodes['coord'][i] = transformPoint(TM,setNodes['coord'][i])
250 |
251 | # Get bounding box
252 | low = np.min(setNodes['coord'],axis=0)
253 | upp = np.max(setNodes['coord'],axis=0)
254 | bbox = (low,upp)
255 |
256 | # Convert setNodes to a dictionary for fast indexing by node label
257 | setNodeList = dict([(k,{}) for k in regionInfo.keys()])
258 | for instName in setNodeList.keys():
259 | indx = np.where(setNodes['instName']==instName)
260 | setNodeList[instName] = dict(zip(setNodes[indx]['label'],setNodes[indx]['coord']))
261 |
262 | return regionSet,elemData,setNodeList,bbox
263 |
264 | # ~~~~~~~~~~
265 |
266 | def checkDependencies():
267 | """Check pyvxray dependencies are available"""
268 | try:
269 | from PIL import Image, ImageFilter
270 | except:
271 | print 'Error: Cannot load PIL / Pillow package'
272 | return False
273 | return True
274 |
275 | # ~~~~~~~~~~
276 |
277 | def createVirtualXrays(odbName,bRegionSetName,BMDfoname,showImplant,iRegionSetName,
278 | iDensity,stepList,csysName,resGrid,imageNameBase,preferredXraySize,
279 | imageFormat,smooth=False,manualImageScaling=False):
280 | """Creates virtual x-rays from an ABAQUS odb file. The odb file should contain \n""" + \
281 | """a number of steps with a fieldoutput variable representing bone mineral density (BMD)"""
282 |
283 | # User message
284 | print '\npyvXRAY: Create virtual x-rays plugin'
285 |
286 | # Check dependencies
287 | if not checkDependencies():
288 | print 'Error: Virtual x-rays not created\n'
289 | return
290 |
291 | # Process inputs
292 | resGrid = float(resGrid)
293 | stepList = [int(s) for s in stepList.replace(',',' ').split()]
294 | preferredXraySize = int(preferredXraySize)
295 |
296 | # Set variables
297 | dx,dy,dz = (resGrid,)*3
298 | iDensity /= 1000.
299 | odb = session.odbs[odbName]
300 | ec = dict([(ename,eclass()) for ename,eclass in et.seTypes.items()])
301 |
302 | # Get transformation matrix to convert from global to local coordinate system
303 | TM = getTMfromCsys(odb,csysName)
304 | print '\nX-ray views will be relative to %s' % csysName
305 |
306 | # Get part data and create a bounding box. The bounding box should include the implant if specified
307 | bRegion,bElemData,bNodeList,bBBox = getPartData(odb,bRegionSetName,TM)
308 | if showImplant:
309 | iRegion,iElemData,iNodeList,iBBox = getPartData(odb,iRegionSetName,TM)
310 | bbLow = np.min((bBBox[0],iBBox[0]),axis=0)
311 | bbUpp = np.max((bBBox[1],iBBox[1]),axis=0)
312 | else:
313 | bbLow,bbUpp = bBBox
314 |
315 | border = 0.05*(bbUpp-bbLow)
316 | bbLow = bbLow - border
317 | bbUpp = bbUpp + border
318 | bbSides = bbUpp - bbLow
319 | x0,y0,z0 = bbLow
320 | xN,yN,zN = bbUpp
321 | lx,ly,lz = bbSides
322 |
323 | # Generate Xray grid
324 | NX = int(np.ceil(lx/dx+1))
325 | x = np.linspace(x0,xN,NX)
326 | NY = int(np.ceil(ly/dy+1))
327 | y = np.linspace(y0,yN,NY)
328 | NZ = int(np.ceil(lz/dz+1))
329 | z = np.linspace(z0,zN,NZ)
330 |
331 | # Create element map for the implant, map to 3D space array and then project onto 3 planes
332 | if showImplant:
333 | # Get a map for each instance and element type. Then combine maps together
334 | iElementMap=np.zeros((NX*NY*NZ),dtype=[('inst','|a80'),('cte',int),('g',float),('h',float),('r',float)])
335 | for instName in iElemData.keys():
336 | for etype in iElemData[instName].keys():
337 | edata = iElemData[instName][etype]
338 | emap = createElementMap(iNodeList[instName],edata['label'],edata['econn'],ec[etype].numNodes,x,y,z)
339 | indx = np.where(emap['cte']>0)
340 | iElementMap['inst'][indx] = instName
341 | iElementMap['cte'][indx] = emap['cte'][indx]
342 | iElementMap['g'][indx] = emap['g'][indx]
343 | iElementMap['h'][indx] = emap['h'][indx]
344 | iElementMap['r'][indx] = emap['r'][indx]
345 | # Mask 3D array
346 | iMask = np.zeros((NX,NY,NZ),dtype=np.float64)
347 | for gpi in xrange(iElementMap.size):
348 | gridPoint = iElementMap[gpi]
349 | if gridPoint['cte'] > 0:
350 | i,j,k = convert1Dto3Dindex(gpi,NX,NY,NZ)
351 | iMask[i,j,k] = iDensity
352 | # Create projections of 3D space array onto planes
353 | iProjectedXY = projectXrayPlane(iMask,'xy')
354 | iProjectedYZ = projectXrayPlane(iMask,'yz')
355 | iProjectedXZ = projectXrayPlane(iMask,'xz')
356 | # Create xrays of implant without bone
357 | iprojXY = iProjectedXY.copy()
358 | iprojYZ = iProjectedYZ.copy()
359 | iprojXZ = iProjectedXZ.copy()
360 | prXY = [np.min(iprojXY),np.max(iprojXY)]
361 | prYZ = [np.min(iprojYZ),np.max(iprojYZ)]
362 | prXZ = [np.min(iprojXZ),np.max(iprojXZ)]
363 | iprojXY[:,:] = (iprojXY[:,:]-prXY[0])/(prXY[1]-prXY[0])*255.
364 | iprojYZ[:,:] = (iprojYZ[:,:]-prYZ[0])/(prYZ[1]-prYZ[0])*255.
365 | iprojXZ[:,:] = (iprojXZ[:,:]-prXZ[0])/(prXZ[1]-prXZ[0])*255.
366 | #writeImageFile('implant_XY',iprojXY,preferredXraySize,imageFormat,smooth)
367 | #writeImageFile('implant_YZ',iprojYZ,preferredXraySize,imageFormat,smooth)
368 | #writeImageFile('implant_XZ',iprojXZ,preferredXraySize,imageFormat,smooth)
369 |
370 | # Create the element map for the bone
371 | bElementMap=np.zeros((NX*NY*NZ),dtype=[('inst','|a80'),('cte',int),('g',float),('h',float),('r',float)])
372 | for instName in bElemData.keys():
373 | for etype in bElemData[instName].keys():
374 | edata = bElemData[instName][etype]
375 | emap = createElementMap(bNodeList[instName],edata['label'],edata['econn'],ec[etype].numNodes,x,y,z)
376 | indx = np.where(emap['cte']>0)
377 | bElementMap['inst'][indx] = instName
378 | bElementMap['cte'][indx] = emap['cte'][indx]
379 | bElementMap['g'][indx] = emap['g'][indx]
380 | bElementMap['h'][indx] = emap['h'][indx]
381 | bElementMap['r'][indx] = emap['r'][indx]
382 |
383 | # Interpolate HU values from tet mesh onto grid using quadratic tet shape function
384 | # (a) Get HU values from frame
385 | numSteps = len(stepList)
386 | xraysXY = np.zeros((numSteps,NX,NY),dtype=np.float64)
387 | xraysYZ = np.zeros((numSteps,NY,NZ),dtype=np.float64)
388 | xraysXZ = np.zeros((numSteps,NX,NZ),dtype=np.float64)
389 | mappedBMD = np.zeros((NX,NY,NZ),dtype=np.float64)
390 |
391 | # Initialise BMDvalues
392 | BMDvalues = dict([(k,{}) for k in bElemData.keys()])
393 | for instName,instData in bElemData.items():
394 | for etype,eData in instData.items():
395 | for i in xrange(eData.size):
396 | BMDvalues[instName][eData[i]['label']] = et.seTypes[etype]()
397 |
398 | for s in xrange(numSteps):
399 | # Step details
400 | stepId = stepList[s]
401 | stepName = "Step-%i" % (stepId)
402 | frame = odb.steps[stepName].frames[-1]
403 | # Get BMD data for bRegion in current frame
404 | print 'Getting BMDvalues in %s' % stepName
405 | BMDfov = frame.fieldOutputs[BMDfoname].getSubset(region=bRegion, position=ELEMENT_NODAL).values
406 | cel = 0
407 | for i in xrange(len(BMDfov)):
408 | val = BMDfov[i]
409 | instanceName = val.instance.name
410 | elementLabel = val.elementLabel
411 | if elementLabel!=cel:
412 | cel=elementLabel
413 | indx=0
414 | else:
415 | indx+=1
416 | BMDvalues[instanceName][elementLabel].setNodalValueByIndex(indx,val.data)
417 |
418 | # Perform the interpolation from elementMap to 3D space array
419 | print 'Mapping BMD values in %s' % stepName
420 | for gpi in xrange(bElementMap.size):
421 | gridPoint = bElementMap[gpi]
422 | instName = gridPoint['inst']
423 | cte = gridPoint['cte']
424 | if cte > 0:
425 | ipc = [gridPoint['g'],gridPoint['h'],gridPoint['r']]
426 | i,j,k = convert1Dto3Dindex(gpi,NX,NY,NZ)
427 | mappedBMD[i,j,k] = BMDvalues[instName][cte].interp(ipc)
428 | # Project onto orthogonal planes
429 | xraysXY[s] = projectXrayPlane(mappedBMD,'xy')
430 | xraysYZ[s] = projectXrayPlane(mappedBMD,'yz')
431 | xraysXZ[s] = projectXrayPlane(mappedBMD,'xz')
432 |
433 | # Get min/max pixel values. Use zero for lower limit (corresponding to background)
434 | prXY = [0.,np.max(xraysXY)]
435 | prYZ = [0.,np.max(xraysYZ)]
436 | prXZ = [0.,np.max(xraysXZ)]
437 | # Allow user to change scale factors if desired
438 | if manualImageScaling:
439 | fields = (('X-Y','%.6f'%prXY[1]),('Y-Z','%.6f'%prYZ[1]),('X-Z','%.6f'%prXZ[1]))
440 | usf = getInputs(fields=fields,label='X-ray image scale factors:')
441 | if usf[0] != None:
442 | try: usf = [float(sf) for sf in usf]
443 | except: print 'Error in user supplied X-ray image scale factors. Using pyvXRAY values'
444 | else: prXY[1],prYZ[1],prXZ[1] = usf
445 | # Add projected implant to projected bone
446 | if showImplant:
447 | xraysXY[:] += iProjectedXY
448 | xraysYZ[:] += iProjectedYZ
449 | xraysXZ[:] += iProjectedXZ
450 | # Scale each projection using pixel range from bone
451 | xraysXY[:,:,:] = (xraysXY[:,:,:]-prXY[0])/(prXY[1]-prXY[0])*255.
452 | xraysYZ[:,:,:] = (xraysYZ[:,:,:]-prYZ[0])/(prYZ[1]-prYZ[0])*255.
453 | xraysXZ[:,:,:] = (xraysXZ[:,:,:]-prXZ[0])/(prXZ[1]-prXZ[0])*255.
454 | xraysXY[np.where(xraysXY<0.)] = 0.
455 | xraysYZ[np.where(xraysYZ<0.)] = 0.
456 | xraysXZ[np.where(xraysXZ<0.)] = 0.
457 | xraysXY[np.where(xraysXY>255.)] = 255.
458 | xraysYZ[np.where(xraysYZ>255.)] = 255.
459 | xraysXZ[np.where(xraysXZ>255.)] = 255.
460 |
461 | # Create images
462 | print 'Writing virtual x-ray image files'
463 | for s in xrange(numSteps):
464 | stepId = stepList[s]
465 | writeImageFile(('%s_XY_%i' % (imageNameBase,stepId)),xraysXY[s,:,:],preferredXraySize,imageFormat,smooth)
466 | writeImageFile(('%s_YZ_%i' % (imageNameBase,stepId)),xraysYZ[s,:,:],preferredXraySize,imageFormat,smooth)
467 | writeImageFile(('%s_XZ_%i' % (imageNameBase,stepId)),xraysXZ[s,:,:],preferredXraySize,imageFormat,smooth)
468 |
469 | # User message
470 | print 'Virtual x-rays have been created in %s' % os.getcwd()
471 | print '\nFinished\n'
472 |
473 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [install]
2 | install_lib=abaqus_plugins
3 |
4 |
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2015 Michael Hogg
4 |
5 | # This file is part of pyvXRAY - See LICENSE.txt for information on usage and redistribution
6 |
7 | import pyvXRAY
8 |
9 | from distutils.core import setup
10 | from distutils.extension import Extension
11 | import numpy
12 | try:
13 | from Cython.Distutils import build_ext
14 | except ImportError:
15 | use_cython = False
16 | else:
17 | use_cython = True
18 |
19 | cmdclass = {}
20 | ext_modules = []
21 | if use_cython:
22 | ext_modules += [ Extension("pyvXRAY.cythonMods", sources=["cython/cythonMods.pyx"],include_dirs=[numpy.get_include()],language="c++")]
23 | cmdclass.update({ 'build_ext':build_ext })
24 | else:
25 | ext_modules += [ Extension("pyvXRAY.cythonMods", sources=["cython/cythonMods.cpp"],include_dirs=[numpy.get_include()],language="c++")]
26 |
27 | setup(
28 | name = 'pyvXRAY',
29 | version = pyvXRAY.__version__,
30 | description = 'ABAQUS plug-in to create virtual x-rays from 3D finite element bone/implant models',
31 | license = 'MIT license',
32 | keywords = ["ABAQUS","plug-in","virtual","x-rays","finite","element","bone","python","cython"],
33 | author = 'Michael Hogg',
34 | author_email = 'michael.christopher.hogg@gmail.com',
35 | url = "https://github.com/mhogg/pyvxray",
36 | download_url = "https://github.com/mhogg/pyvxray/releases",
37 | packages = ['','pyvXRAY'],
38 | package_data = {'':['LICENSE.txt','README.md'],'pyvXRAY': ['cythonMods.pyd',]},
39 | classifiers = [
40 | "Programming Language :: Python",
41 | "Programming Language :: Cython",
42 | "Programming Language :: Python :: 2",
43 | "Programming Language :: Python :: 2.6",
44 | "Development Status :: 4 - Beta",
45 | "Environment :: Other Environment",
46 | "Environment :: Plugins",
47 | "Intended Audience :: Healthcare Industry",
48 | "Intended Audience :: Science/Research",
49 | "License :: OSI Approved :: MIT License",
50 | "Operating System :: OS Independent",
51 | "Topic :: Scientific/Engineering :: Medical Science Apps.",
52 | "Topic :: Scientific/Engineering :: Visualization",
53 | ],
54 | ext_modules = ext_modules,
55 | cmdclass = cmdclass,
56 | long_description = """
57 |
58 | An ABAQUS plug-in to generate virtual x-rays from 3D finite element bone/implant models.
59 |
60 | Creates a time series of virtual x-rays of a bone/implant model by projecting a scalar representing bone density onto three orthogonal planes. This is intended for post-processing of numerical bone remodelling analyses, the main objective of which typically is to detect temporal changes in the bone density around an implant and predict the long term stability of the implant in the absence of clinical data.
61 |
62 | The resulting series of virtual x-ray images are in a format similar to clinical results making the result (1) more easily understood and accepted by clinicians and (2) able to be directly compared to clinical data, which enables validation of numerical bone remodelling algorithms.
63 |
64 | The series of images (which are outputted in common image formats such as bmp, jpeg and png) can be analysed using `BMDanalyse `_, which was developed for this purpose. This tool enables the quick creation of regions of interest (ROIs) over which changes in bone density over time can be determined.
65 | """,
66 | )
67 |
--------------------------------------------------------------------------------