├── .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 | [![PyPi version](https://img.shields.io/pypi/v/pyvxray.svg)](https://pypi.python.org/pypi/pyvxray/) 8 | [![PyPi downloads](https://img.shields.io/pypi/dm/pyvxray.svg)](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 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 190 | 191 | 192 | 193 | 194 | 195 | 197 | 198 | 199 | 200 | 201 | 202 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 231 | 232 | 233 |
GUI tabInput name Input description
Select regionsResult file: OdbThe ABAQUS result file
Bone region: Bone setThe name of the element set representing the bone
Bone region: Density variableA scalar fieldoutput variable representing bone density.
This is most often a state variable i.e. SDV1
Implant region: Show implant on x-raysOption to include implant on the virtual x-rays
Implant region: Implant setThe name of the element set representing the implant
Implant region: Density (kg/m^3)The density of the implant material in kg/m^3 i.e. 4500 for Titanium Alloy
InputsRequired inputs: Step listA 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.
Required inputs: Coordinate systemThe 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.
Required inputs: Mapping resolution (mm)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.
X-ray settingsSettings: Base name of xray file(s)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.
Settings: Approx size of x-ray imagesResizing of images is performed to make the number of pixels along the largest image dimension equal to this value.
Settings: Image file formatOutput format of images. Options are bmp, jpeg and png.
Settings: Smooth imagesTurn on image smoothing. PIL.ImageFilter.SMOOTH is used to perform the smoothing.
Settings: Manual scaling of imagespyvXRAY 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.
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 | --------------------------------------------------------------------------------