├── .env ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── deploy.yml ├── .gitignore ├── .python-version ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── Sandbox.ipynb ├── docs └── source │ ├── Makefile │ ├── api │ ├── modules.rst │ ├── unitcell.analysis.rst │ ├── unitcell.geometry.rst │ ├── unitcell.mesh.rst │ └── unitcell.rst │ ├── conf.py │ ├── images │ └── overview.png │ ├── index.rst │ └── make.bat ├── examples ├── examples.ipynb └── geometry.stl ├── pyproject.toml ├── requirements.in ├── src └── unitcellengine │ ├── __init__.py │ ├── analysis │ ├── __init__.py │ ├── homogenization.py │ ├── internal.py │ ├── material.py │ └── multiprocess.py │ ├── design.py │ ├── geometry │ ├── __init__.py │ ├── definitions │ │ ├── Body centered cubic foam.ltcx │ │ ├── Body centered cubic.ltcx │ │ ├── Column.ltcx │ │ ├── Columns.ltcx │ │ ├── Diamond.ltcx │ │ ├── Face centered cubic foam.ltcx │ │ ├── Face centered cubic.ltcx │ │ ├── Fluorite.ltcx │ │ ├── Hex prism central axis edge.ltcx │ │ ├── Hex prism diamond.ltcx │ │ ├── Hex prism edge.ltcx │ │ ├── Hex prism laves phase.ltcx │ │ ├── Hex prism vertex centroid.ltcx │ │ ├── Hexagonal honeycomb.ltcx │ │ ├── IsoTruss.ltcx │ │ ├── Kelvin cell.ltcx │ │ ├── Oct vertex centroid.ltcx │ │ ├── Octet.ltcx │ │ ├── Re-entrant honeycomb.ltcx │ │ ├── Re-entrant.ltcx │ │ ├── Simple cubic foam.ltcx │ │ ├── Simple cubic.ltcx │ │ ├── Square honeycomb rotated.ltcx │ │ ├── Square honeycomb.ltcx │ │ ├── Tet oct vertex centroid.ltcx │ │ ├── Triangular honeycomb rotated.ltcx │ │ ├── Triangular honeycomb.ltcx │ │ ├── Truncated cube.ltcx │ │ ├── Truncated octahedron.ltcx │ │ ├── Weaire-Phelan.ltcx │ │ └── definitions.json │ ├── gmsh.py │ ├── sdf.py │ └── tests │ │ ├── unitcellDefinition.json │ │ ├── unitcellGeometry.stl │ │ └── unitcellProperties.json │ ├── mesh │ ├── __init__.py │ ├── gmsh.py │ ├── internal.py │ └── tests │ │ └── test.npz │ └── utilities.py ├── test.npz ├── test.vtk ├── test.vtu ├── test2.vtk ├── tests ├── __init__.py ├── analysis │ ├── __init__.py │ ├── resources │ │ ├── fullyDense.e │ │ ├── fullyDense.npz │ │ ├── fullyDense_conductance_results.npz │ │ ├── fullyDense_elastic_results.npz │ │ ├── halesGyroid_0_24.npz │ │ ├── halesGyroid_0_24.vtu │ │ ├── halesGyroid_0_24_conductance_results.npz │ │ ├── halesSchwarz_0_23.npz │ │ ├── halesSchwarz_0_23_conductance_results.npz │ │ ├── halesTMPS.png │ │ ├── hexHoneycomb_5x5x1.e │ │ ├── hexHoneycomb_5x5x1.npz │ │ ├── hexHoneycomb_5x5x1_elastic_results.npz │ │ ├── octetPatil.e │ │ ├── octetPatil.npz │ │ └── octetPatil_elastic_results.npz │ └── test_homogenization.py ├── geometry │ ├── __init__.py │ ├── resources │ │ └── unitcellGeometry.stl │ └── test_geometry.py ├── resources │ ├── octetPatil.jpeg │ └── octetPatil.pdf └── test_unitcellengine.py └── uv.lock /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ntop filter=lfs diff=lfs merge=lfs -text 2 | *.e filter=lfs diff=lfs merge=lfs -text 3 | *.pdf filter=lfs diff=lfs merge=lfs -text 4 | *.npz filter=lfs diff=lfs merge=lfs -text 5 | *.vtu filter=lfs diff=lfs merge=lfs -text 6 | *.pkl filter=lfs diff=lfs merge=lfs -text 7 | *.exo filter=lfs diff=lfs merge=lfs -text 8 | *.png filter=lfs diff=lfs merge=lfs -text 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - Python version: 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build and deploy to PyPi 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | on: 10 | push: 11 | branches: [ "main", "develop" ] 12 | # Publish semver tags as releases. 13 | tags: [ 'v*' ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | env: 18 | ASSET_FILNAME: ${{ github.ref_name }} 19 | 20 | jobs: 21 | build: 22 | name: Build wheels 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: write # Needed to enable write permissions 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | # This repository uses git-lfs, so we need to enable support. 32 | # Otherwise, the lfs files won't be properly pulled. 33 | lfs: true 34 | fetch-depth: 0 # fetch the entire repo history, which is required for proper semver bumping 35 | 36 | # - name: Pull from git lfs 37 | # run: git lfs pull 38 | 39 | - name: Install uv 40 | uses: astral-sh/setup-uv@v3 41 | with: 42 | version: "0.4.27" 43 | enable-cache: true 44 | cache-dependency-glob: "uv.lock" 45 | 46 | - name: Setup Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version-file: ".python-version" 50 | 51 | - name: Bump the version number to dev if a tag isn't specified 52 | # This help differentiate development builds on Test PyPi 53 | if: ${{ ! startsWith(github.ref, 'refs/tags/v') }} 54 | run: | 55 | # We need to first check in any files that are shown as modified, 56 | # which can happen due to line ending differences between operating 57 | # systems. 58 | git config user.name "github-actions[bot]" 59 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 60 | git commit --all --allow-empty --message "Dummy checking" 61 | uv tool run bump-my-version bump patch 62 | uv tool run bump-my-version show-bump 63 | 64 | - name: Build source and wheels 65 | run: uv build 66 | 67 | - name: Create tagged release 68 | if: startsWith(github.ref, 'refs/tags/v') 69 | uses: softprops/action-gh-release@v2 70 | with: 71 | prerelease: false 72 | 73 | - name: Publish to PyPi (production) 74 | if: startsWith(github.ref, 'refs/tags/v') 75 | run: uv publish --token ${{ secrets.PYPI_TOKEN }} 76 | 77 | - name: Publish to TestPyPi (development) 78 | if: github.event_name == 'push' && ! startsWith(github.ref, 'refs/tags/v') 79 | run: uv publish --publish-url https://test.pypi.org/legacy/ --token ${{ secrets.TESTPYPI_TOKEN }} 80 | 81 | 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | Database/* 3 | *.pyc 4 | dist/ 5 | unitcell.egg* 6 | *.ipynb_checkpoints* 7 | *.nbc 8 | *.nbi 9 | config.json 10 | 11 | .venv/* 12 | 13 | # Ignore generated test files 14 | .pytest_cache/* 15 | unitcell/analysis/tests/*.i 16 | unitcell/analysis/tests/*result*.e 17 | unitcell/analysis/tests/*.e.* 18 | unitcell/analysis/tests/*homogenization.e 19 | unitcell/analysis/tests/*dd_sol* 20 | unitcell/analysis/tests/*krylov_sol* 21 | unitcell/geometry/tests/* 22 | unitcell/mesh/tests/*.e 23 | tests/analysis/resources/*homog*.* 24 | docs/build 25 | docs/source/_autosummary 26 | *.rslt 27 | *.inp 28 | *.diatom 29 | *.tar 30 | *.zip 31 | tests/meshconvergence/* 32 | **/in.json 33 | **/out.json 34 | .conda 35 | tests/geometry/resources/unitcellDefinition.json 36 | tests/geometry/resources/unitcellProperties.json 37 | unitcell/mesh/tests/test.npz 38 | unitcell/mesh/tests/test_elastic_homogenization.json 39 | unitcell/mesh/tests/test_elastic_results.np 40 | **/Database 41 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "env": {"PYTHONPATH": "${workspaceFolder}"}, 14 | "cwd": "${workspaceFolder}", 15 | "justMyCode": true 16 | }, 17 | { 18 | "name": "(1) Windows .venv Python: Current File", 19 | "type": "python", 20 | "request": "launch", 21 | "program": "${relativeFile}", 22 | "console": "integratedTerminal", 23 | "python": "${workspaceFolder}/.venv/Scripts/python.exe", 24 | "cwd": "${workspaceFolder}", 25 | "justMyCode": true, 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.inp": "c", 4 | "*.template": "shellscript", 5 | "*.i": "shellscript" 6 | }, 7 | "python.pythonPath": "C:\\Users\\watkinrt\\AppData\\Local\\Continuum\\miniconda3\\python.exe", 8 | "python.testing.unittestArgs": [ 9 | "-v", 10 | "-s", 11 | "./unitcell", 12 | "-p", 13 | "test*.py" 14 | ], 15 | "python.testing.pytestEnabled": true, 16 | "python.testing.nosetestsEnabled": false, 17 | "python.testing.unittestEnabled": false, 18 | "python.testing.pytestArgs": [ 19 | "tests" 20 | ], 21 | "python.envFile": "${workspaceFolder}/.env", 22 | "terminal.integrated.env.windows": {"PYTHONPATH": "${workspaceFolder}"} 23 | // "python.terminal.executeInFileDir": true 24 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnitcellEngine 2 | 3 | > [!WARNING] 4 | > This is alpha software that is in active development and is likely to experience breaking changes. 5 | 6 | *UnitcellEngine* is part of the *UnitcellHub* software ecosystem, providing functionality to generate and simulate lattice unitcell geometries. 7 | Written fully in Python, it brings together state-of-the-art technology like implicit geometry representation, meshless finite element analysis, and homogenization theory, to provide an automated yet robust pipeline. 8 | 9 | ![](docs/source/images/overview.png) 10 | 11 | ## Installation 12 | 13 | UnitcellEngine only supports Python 3. 14 | If running on a Windows machine, you may sometimes need to first install Microsoft Visual C++ for some of the dependencies to install correctly (such as number and scikit-learn). See https://visualstudio.microsoft.com/visual-cpp-build-tools/ for details on how to install the Windows C++ Build Tools; 15 | additionally, due to performance limitations of the openblas-based installation of numpy via pip, it is recommended that conda be used to setup the base numpy/scipy environment. 16 | 17 | For the current stable release 18 | 19 | ``` 20 | pip install unitcellengine 21 | 22 | ``` 23 | 24 | ## Examples 25 | 26 | *UnitcellEngine* has a range of built-in lattice geometry forms, including graph-style (such as beam and plate) and thin walled Triply Periodic Minimal Surface (TPMS) lattices. 27 | The general workflow is to define the unitcell, generate the geometry, generate the mesh, and solve the relevant homogenization problems. 28 | Each step of the process calculates relevant quantities and stores them in output files that feed into subsequent calculations (for example, a mesh file is required before a homogenization can be run). 29 | 30 | ```python 31 | from unitcellengine.design import UnitcellDesign, UnitcellDesigns 32 | import numpy as np 33 | 34 | # Set numpy precision to allow for better printout of homogenized matrices 35 | np.set_printoptions(precision=4) 36 | 37 | # Define unitcell 38 | design = UnitcellDesign('Octet', 1, 1, 1, thickness=0.1) 39 | 40 | # The default behavior is to reuse existing date. Set to False to force regeneration. 41 | reuse = True 42 | 43 | # Generate geometry (which calculated propertes like relative denstiy and relative surface area) 44 | design.generateGeometry(reuse=reuse) 45 | 46 | # Generate mesh 47 | design.generateMesh(reuse=reuse) 48 | 49 | # Calculate homogenized elastic properties 50 | design.homogenizationElastic.run(reuse) 51 | 52 | # Post process homogenization results 53 | design.homogenizationElastic.process() 54 | 55 | # Calculate homogenized conductance properties 56 | design.homogenizationConductance.run(reuse) 57 | 58 | # Post process conductance results 59 | design.homogenizationConductance.process() 60 | 61 | # Print the homogenizated stiffness matrix 62 | print(design.homogenizationElastic) 63 | print(design.homogenizationConductance) 64 | ``` 65 | The results are by default stored in the folder structure "Database///", where is either "graph" or "walledtpms", "unicell type" is the unitcell type, and "" is a folder with the Length, Width, Height, and Thickness properties of the lattice. 66 | For example, in the case above, it will store the results in "Database/graph/Octet/L1_000_W1_000_H1_000_T0_100". 67 | 68 | For more usage examples, see [examples/examples.ipynb](examples/examples.ipynb). 69 | 70 | ## High Performance Computing and remote Linux servers 71 | When running on remote systems, there are a few features that don't work straight out of the box. 72 | If geometry rendering is required (which relies on VTK), additional utitilies will likely need to be installed. 73 | 74 | ### CENTOS and RH systems without root privileges 75 | Xvfb is an X server that can run on machines with no display hardware and no physical input devices. 76 | It emulates a dumb frame buffer using virtual memory. 77 | Install xvfb following the steps defined here: https://stackoverflow.com/questions/36651091/how-to-install-packages-in-linux-centos-without-root-user-with-automatic-depen. 78 | Note, on most HPC systems, these files should be installed on a high performance file system rather than a network drive. 79 | 80 | In addition to xvfb, you will need to install the OpenGL libraries "mesa-libGL" and "mesa-libGL-devel" using the same steps as for xvfb. 81 | In some cases, you might still get an "GLSL 1.50 is not supported. 82 | Supported versions are: 1.10, 1.20, 1.30, 1.00 ES, and 3.00 ES" error. 83 | In this case, add the following environmental variable 84 | 85 | ``` 86 | export MESA_GL_VERSION_OVERRIDE=3.2 87 | ``` 88 | -------------------------------------------------------------------------------- /Sandbox.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "901ca637-c4cf-4363-9475-ad1394b22ef7", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import os\n", 11 | "os.chdir(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "c316ca53-eedb-42cd-b42d-253e6339f81d", 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "ename": "ModuleNotFoundError", 22 | "evalue": "No module named 'mkl'", 23 | "output_type": "error", 24 | "traceback": [ 25 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 26 | "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", 27 | "Cell \u001b[1;32mIn[2], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdesign\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m UnitcellDesign\n", 28 | "File \u001b[1;32m~\\Documents\\Code\\unitcellhub\\unitcellengine\\unitcell\\__init__.py:7\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mlogging\u001b[39;00m\n\u001b[0;32m 5\u001b[0m logging\u001b[38;5;241m.\u001b[39mgetLogger(\u001b[38;5;18m__name__\u001b[39m)\u001b[38;5;241m.\u001b[39maddHandler(logging\u001b[38;5;241m.\u001b[39mNullHandler())\n\u001b[1;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdesign\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m UnitcellDesign, UnitcellDesigns\n", 29 | "File \u001b[1;32m~\\Documents\\Code\\unitcellhub\\unitcellengine\\unitcell\\design.py:19\u001b[0m\n\u001b[0;32m 16\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgeometry\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m (DEFAULT_THICKNESS, DEFAULT_ELEMENT_SIZE, \n\u001b[0;32m 17\u001b[0m DEFINITION_FILENAME, PROPERTIES_FILENAME)\n\u001b[0;32m 18\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmesh\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01minternal\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mmesh\u001b[39;00m\n\u001b[1;32m---> 19\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01manalysis\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mhomogenization\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mhomog\u001b[39;00m\n\u001b[0;32m 20\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mlogging\u001b[39;00m\n\u001b[0;32m 21\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mlogging\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mhandlers\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mhandlers\u001b[39;00m\n", 30 | "File \u001b[1;32m~\\Documents\\Code\\unitcellhub\\unitcellengine\\unitcell\\analysis\\homogenization.py:1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01munitcell\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01manalysis\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01minternal\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m homogenization, isotropicC, isotropicK\n\u001b[0;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mos\u001b[39;00m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n", 31 | "File \u001b[1;32m~\\Documents\\Code\\unitcellhub\\unitcellengine\\unitcell\\analysis\\internal.py:4\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mscipy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msparse\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m coo_matrix, csc_matrix, csr_matrix\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpypardiso\u001b[39;00m\n\u001b[1;32m----> 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mmkl\u001b[39;00m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mscipy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msparse\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlinalg\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m cg\n\u001b[0;32m 6\u001b[0m \u001b[38;5;66;03m# import cvxopt\u001b[39;00m\n\u001b[0;32m 7\u001b[0m \u001b[38;5;66;03m# import cvxopt.cholmod\u001b[39;00m\n", 32 | "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'mkl'" 33 | ] 34 | } 35 | ], 36 | "source": [ 37 | "from unitcell.design import UnitcellDesign" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "964b36c8-9688-4531-b492-75642514b88d", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "design = UnitcellDesign(\"BCC\", 1, 1, " 48 | ] 49 | } 50 | ], 51 | "metadata": { 52 | "kernelspec": { 53 | "display_name": "Python 3 (ipykernel)", 54 | "language": "python", 55 | "name": "python3" 56 | }, 57 | "language_info": { 58 | "codemirror_mode": { 59 | "name": "ipython", 60 | "version": 3 61 | }, 62 | "file_extension": ".py", 63 | "mimetype": "text/x-python", 64 | "name": "python", 65 | "nbconvert_exporter": "python", 66 | "pygments_lexer": "ipython3", 67 | "version": "3.10.10" 68 | } 69 | }, 70 | "nbformat": 4, 71 | "nbformat_minor": 5 72 | } 73 | -------------------------------------------------------------------------------- /docs/source/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = ../build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/api/modules.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | unitcell 8 | -------------------------------------------------------------------------------- /docs/source/api/unitcell.analysis.rst: -------------------------------------------------------------------------------- 1 | unitcell.analysis package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | unitcell.analysis.internal module 8 | --------------------------------- 9 | 10 | .. automodule:: unitcell.analysis.internal 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | unitcell.analysis.homogenization module 17 | --------------------------------------- 18 | 19 | .. automodule:: unitcell.analysis.homogenization 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | unitcell.analysis.multiprocess module 25 | ------------------------------------- 26 | 27 | .. automodule:: unitcell.analysis.multiprocess 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: unitcell.analysis 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/api/unitcell.geometry.rst: -------------------------------------------------------------------------------- 1 | unitcell.geometry package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | unitcell.geometry.sdf module 8 | ---------------------------- 9 | 10 | .. automodule:: unitcell.geometry.sdf 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | unitcell.geometry.gmsh module 17 | ----------------------------- 18 | 19 | .. automodule:: unitcell.geometry.gmsh 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: unitcell.geometry 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/api/unitcell.mesh.rst: -------------------------------------------------------------------------------- 1 | unitcell.mesh package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | unitcell.mesh.internal module 8 | ----------------------------- 9 | 10 | .. automodule:: unitcell.mesh.internal 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | unitcell.mesh.gmsh module 17 | ------------------------- 18 | 19 | .. automodule:: unitcell.mesh.gmsh 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: unitcell.mesh 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/api/unitcell.rst: -------------------------------------------------------------------------------- 1 | unitcell package 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | unitcell.geometry 11 | unitcell.mesh 12 | unitcell.analysis 13 | 14 | unitcell.design module 15 | ---------------------- 16 | 17 | .. automodule:: unitcell.design 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import sphinx_readable_theme 4 | 5 | sys.path.insert(0, os.path.abspath("../../unitcell")) 6 | 7 | # Configuration file for the Sphinx documentation builder. 8 | # 9 | # For the full list of built-in configuration values, see the documentation: 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 11 | 12 | # -- Project information ----------------------------------------------------- 13 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 14 | 15 | project = "UnitcellEngine" 16 | copyright = "2024, Ryan Watkins" 17 | author = "Ryan Watkins" 18 | release = "0.0.1" 19 | 20 | # -- General configuration --------------------------------------------------- 21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 22 | 23 | extensions = [ 24 | "sphinx.ext.autodoc", 25 | "sphinx.ext.autosummary", 26 | "sphinx.ext.mathjax", 27 | ] 28 | 29 | autosummary_generate = True 30 | numpydoc_class_members_toctree = True 31 | numpydoc_show_class_members = False 32 | 33 | templates_path = ["_templates"] 34 | exclude_patterns = ["build", "_autosummary"] 35 | 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 39 | 40 | html_theme = "readable" 41 | html_theme_path = [sphinx_readable_theme.get_html_theme_path()] 42 | html_static_path = ["_static"] 43 | -------------------------------------------------------------------------------- /docs/source/images/overview.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f4787b96e2439b494a5a613620416873386b635d2c1e19c2f2a75f2321f015f0 3 | size 144374 4 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. UnitcellEngine documentation master file, created by 2 | sphinx-quickstart on Thu May 9 08:29:18 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | UnitcellEngine: a unitcell geometry and simulation tool 7 | ======================================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | api/modules 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/source/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=../build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "cec21941-9f3b-4590-834b-a4f0db0e7493", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import os\n", 11 | "os.chdir(\"..\")" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 15, 17 | "id": "7fe20a53-93f1-4218-8137-93dceb8ce1ef", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from unitcellengine.design import UnitcellDesign, UnitcellDesigns\n", 22 | "import numpy as np\n", 23 | "import pyvista as pv\n", 24 | "from tqdm.notebook import tqdm\n", 25 | "import plotly.graph_objects as go\n", 26 | "from pathlib import Path\n", 27 | "\n", 28 | "# Set numpy precision to allow for better printout of homogenized matrices\n", 29 | "np.set_printoptions(precision=4)" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "id": "7515e6a1-d0f1-412d-8115-bfe2b4df7b87", 35 | "metadata": {}, 36 | "source": [ 37 | "# Single point design" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "27218d69-86c6-44d5-a03e-7251e1c51249", 43 | "metadata": {}, 44 | "source": [ 45 | "Create a single point design of a unit aspect ratio Octet truss lattice and investigate its properties.\n", 46 | "Note that, in the default setup, the assumed material has a modulus of elasticity of 1 and Poisson's ratio of 0.3." 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "id": "d03ebab2-7f43-4334-b618-9205effde603", 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "# Define unitcell\n", 57 | "design = UnitcellDesign('Octet', 1, 1, 1, thickness=0.1)\n", 58 | "\n", 59 | "# The default behavior is to reuse existing date. Set to False to force regeneration.\n", 60 | "reuse = False\n", 61 | "\n", 62 | "# Generate geometry (which calculated properties like relative density and relative surface area)\n", 63 | "design.generateGeometry(reuse=reuse)\n", 64 | "\n", 65 | "# Generate mesh\n", 66 | "design.generateMesh(reuse=reuse)\n", 67 | "\n", 68 | "# Calculate homogenized elastic properties\n", 69 | "design.homogenizationElastic.run(reuse)\n", 70 | "\n", 71 | "# Post process homogenization results\n", 72 | "design.homogenizationElastic.process()\n", 73 | "\n", 74 | "# Calculate homogenized conductance properties\n", 75 | "design.homogenizationConductance.run(reuse)\n", 76 | "\n", 77 | "# Post process conductance results\n", 78 | "design.homogenizationConductance.process()\n", 79 | "\n", 80 | "# Print the homogenizated stiffness matrix\n", 81 | "print(design.homogenizationElastic)\n", 82 | "print(design.homogenizationConductance)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "id": "82d92468-fa5d-4bfd-a1cd-42518cb4abe3", 88 | "metadata": {}, 89 | "source": [ 90 | "## Visualize geometry" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "id": "0d41e13c-ca91-4da4-8c20-c0fdab03a1b6", 96 | "metadata": {}, 97 | "source": [ 98 | "The scalar colors corresponds to the underlying signed-distance field, which defines the distance of a point to the closed point on the geometry surface (thus, a value of 0 corresponds to the surface of the geometry).\n", 99 | "\n", 100 | "Note: the python package \"trame\", along with the trame-jupyter-extension jupyter extension, \n", 101 | "are required for display of an interactive plot. See https://tutorial.pyvista.org/tutorial/00_jupyter/index.html\n", 102 | "for more details" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "id": "d1c37a54-69df-4e5a-a74c-bb666497c6ef", 109 | "metadata": { 110 | "scrolled": true 111 | }, 112 | "outputs": [], 113 | "source": [ 114 | "geometry = design.geometry.visualizeVTK()\n", 115 | "geometry.plot()" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "id": "ba3051eb-e561-4c4a-be19-f6532958fc81", 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "design = UnitcellDesign('Simple cubic', 1, 1, 1)\n", 126 | "geometry = design.geometry.visualizeVTK()\n", 127 | "mesh = geometry.extract_geometry().triangulate()\n", 128 | "mesh.flip_normals()\n", 129 | "mesh.plot()" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "id": "4c189562", 135 | "metadata": {}, 136 | "source": [ 137 | "This geometry can be exported to an STL (or other surface geometry form like obj or ply)." 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "id": "97ab881e-ab0a-4b43-b78e-f7a4b69bd267", 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "geometry = design.geometry.visualizeVTK()\n", 148 | "mesh = geometry.extract_geometry().triangulate()\n", 149 | "mesh.flip_normals()\n", 150 | "mesh.save(\"geometry.stl\")" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "id": "004913e6", 156 | "metadata": {}, 157 | "source": [ 158 | "Additionally, the resolution of the generated geometry can be increased at the cost of increased generation time.\n", 159 | "This is done by specifying the discretization size of the geometry through its \"elementSize\" property.\n", 160 | "This property is defined relative to the thickness parameter of the unitcell.\n", 161 | "So, a value of 1/4 corresponds to a discretization that can fit 4 discrete elements across the thickness of the unitcell beams/faces.\n", 162 | "Thus, as elementSize decreases, the resolution increases." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "c4448f6c", 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "design.geometry.elementSize = 0.25\n", 173 | "geometry = design.geometry.visualizeVTK()\n", 174 | "mesh = geometry.extract_geometry().triangulate()\n", 175 | "mesh.flip_normals()\n", 176 | "mesh.plot()" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "id": "444e8ac2", 182 | "metadata": {}, 183 | "source": [ 184 | "## Fillets in graph unitcells" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "4da60ea5", 190 | "metadata": {}, 191 | "source": [ 192 | "Graph unitcells are defined by sets of beams and faces.\n", 193 | "At the intersection of these beams and faces, it is possible to specify a fillet radius.\n", 194 | "This is done by specifying the \"radius\" parameter in the UnitcellDesign, which defines the fillet radius relative to the beam/face thickness.\n", 195 | "For example, if the beam/face thickness is 0.3 mm and radius=0.5, then the corresponding fillet radius of 0.5*0.3mm=0.15 mm.\n", 196 | "The below example shows the same \"Simple Cubic\" unitcell with varying fillet radii." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "id": "7966884a", 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "meshes = []\n", 207 | "pl = pv.Plotter(shape=(1, 3))\n", 208 | "for i, radius in enumerate([0., 0.5, 1.]):\n", 209 | " design = UnitcellDesign('Simple cubic', 1, 1, 1, radius=radius)\n", 210 | " geometry = design.geometry.visualizeVTK()\n", 211 | " mesh = geometry.extract_geometry().triangulate()\n", 212 | " mesh.flip_normals()\n", 213 | " pl.subplot(0, i)\n", 214 | " pl.add_mesh(mesh)\n", 215 | " pl.add_text(f\"Radius: {radius}\")\n", 216 | "\n", 217 | "pl.link_views()\n", 218 | "pl.view_isometric()\n", 219 | "pl.show()\n", 220 | "\n" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "id": "aa1453d0-594e-49a0-99f6-22d7ff7282f0", 226 | "metadata": { 227 | "jp-MarkdownHeadingCollapsed": true 228 | }, 229 | "source": [ 230 | "## Visualize anistropy of elastic properties" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "id": "ae7e197c-a66c-49f1-acb3-a163cf9b70f0", 236 | "metadata": {}, 237 | "source": [ 238 | "For more details regaring the below visualizations, see Nordmann, J., Aßmus, M., & Altenbach, H. (2018). *Visualising elastic anisotropy: theoretical background and computational implementation*. Continuum Mechanics and Thermodynamics, 30, 689-708." 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 22, 244 | "id": "11e5e28b", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "# Define unitcell\n", 249 | "design = UnitcellDesign('Octet', 1, 1, 1, thickness=0.1)\n", 250 | "\n", 251 | "# Reuse data calculate earlier in the notebook\n", 252 | "reuse = True" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "id": "048b0bef", 258 | "metadata": {}, 259 | "source": [ 260 | "Plotting the directional dependence of the lattice stiffness provides insight into the anisotropy of the structure.\n", 261 | "If the structure was isotropic, the corresponding plot would be a sphere as the stiffness is the same in all loading directions.\n", 262 | "As the plot deviates from the sphere, the anisotropy of the structure increases." 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": null, 268 | "id": "8156b2ed-5ff1-413f-9cc9-2f2a2607a5a4", 269 | "metadata": {}, 270 | "outputs": [], 271 | "source": [ 272 | "design.homogenizationElastic.plotE()" 273 | ] 274 | }, 275 | { 276 | "cell_type": "markdown", 277 | "id": "218878eb", 278 | "metadata": {}, 279 | "source": [ 280 | "Visualization and interpretation of the other structural properties, like shear modulus (plotG) and Poisson's ratio (plotNu), is more challenging because of their depending on both loading direction and plane of reference. \n", 281 | "Below is an example of the shear modulus properties.\n" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "id": "807411b0-0cdc-40d5-b737-b454b5bff7b7", 288 | "metadata": { 289 | "scrolled": true 290 | }, 291 | "outputs": [], 292 | "source": [ 293 | "design.homogenizationElastic.plotG()" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "id": "d26456a7-dc99-4228-97e8-4a061c70056f", 299 | "metadata": {}, 300 | "source": [ 301 | "# Export design to a centeralized database" 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "id": "2ed2e879-2056-48ec-b585-dafca4167e52", 307 | "metadata": {}, 308 | "source": [ 309 | "Export to a centeralized HDF5 database" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": null, 315 | "id": "9a056169-99d3-4d57-aabf-a8eecff4b98d", 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [ 319 | "design.export()" 320 | ] 321 | }, 322 | { 323 | "cell_type": "markdown", 324 | "id": "1f1e2bf2-445a-4859-ba11-40ae3d99e448", 325 | "metadata": {}, 326 | "source": [ 327 | "The centralized database is located in the root database folder and can be found as follows:" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": null, 333 | "id": "9d4db2bb-9b96-41ac-8193-d69307a0db42", 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "design.databaseFilename" 338 | ] 339 | }, 340 | { 341 | "cell_type": "markdown", 342 | "id": "e6c8b924-e2ad-4148-b406-40b7b8aa6b8e", 343 | "metadata": {}, 344 | "source": [ 345 | "# Design groups" 346 | ] 347 | }, 348 | { 349 | "cell_type": "markdown", 350 | "id": "e7e8f629-1616-47a8-bfed-308fa30a86be", 351 | "metadata": {}, 352 | "source": [ 353 | "Generate the properties for a set of lattices and manage their properties as a group." 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": null, 359 | "id": "d6e5e28e-3be8-4385-b594-1237b02f0b79", 360 | "metadata": {}, 361 | "outputs": [], 362 | "source": [ 363 | "cases = [\n", 364 | " dict(length=1, width=1, height=1, thickness=0.2),\n", 365 | " dict(length=2, width=1, height=1, thickness=0.2),\n", 366 | " dict(length=3, width=1, height=1, thickness=0.2),\n", 367 | " dict(length=4, width=1, height=1, thickness=0.2),\n", 368 | "]\n", 369 | "\n", 370 | "# The default behavior is to reuse existing date. Set to False to force regeneration.\n", 371 | "reuse = False\n", 372 | "\n", 373 | "for case in tqdm(cases):\n", 374 | " # Define unitcell\n", 375 | " design = UnitcellDesign('Gyroid', **case)\n", 376 | " \n", 377 | " # Generate geometry (which calculated properties like relative density and relative surface area)\n", 378 | " design.generateGeometry(reuse=reuse)\n", 379 | " \n", 380 | " # Generate mesh\n", 381 | " design.generateMesh(reuse=reuse)\n", 382 | " \n", 383 | " # Calculate homogenized elastic properties\n", 384 | " design.homogenizationElastic.run(reuse=reuse, solver='iterative')\n", 385 | " \n", 386 | " # Post process homogenization results\n", 387 | " design.homogenizationElastic.process(reuse=False)" 388 | ] 389 | }, 390 | { 391 | "cell_type": "markdown", 392 | "id": "e684d145-429b-4718-8910-0d097f922621", 393 | "metadata": {}, 394 | "source": [ 395 | "Load the data for all of the Octet truss designs" 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": null, 401 | "id": "660ee8c4-7d4a-41c9-a9b7-d6585bdde4a4", 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "designs = UnitcellDesigns(\"Gyroid\", form=\"walledtpms\")" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "id": "15a400ed-53b7-4187-a163-8de36a161207", 411 | "metadata": {}, 412 | "source": [ 413 | "Get various properties from the set of unitcell designs" 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": null, 419 | "id": "a91e0f0b-a936-437b-ad56-4001b50c192c", 420 | "metadata": {}, 421 | "outputs": [], 422 | "source": [ 423 | "fig = go.Figure()\n", 424 | "fig.add_trace(go.Scatter(x=designs.lengths, y=designs.relativeDensities))\n", 425 | "fig.update_xaxes(title=\"Unitcell length\")\n", 426 | "fig.update_yaxes(title=\"Relative density\")" 427 | ] 428 | }, 429 | { 430 | "cell_type": "markdown", 431 | "id": "63b28e99-3433-44b1-9399-1453edd3db2c", 432 | "metadata": {}, 433 | "source": [ 434 | "Calculate the normalized elastic stiffness in the 1 direction (i.e., length-wise direction)" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": null, 440 | "id": "a0dab5ea-da31-4049-9c9c-ccaf73e735c1", 441 | "metadata": {}, 442 | "outputs": [], 443 | "source": [ 444 | "E11s = [d.homogenizationElastic.Ei([1, 0, 0]) for d in designs.designs]\n", 445 | "fig = go.Figure()\n", 446 | "fig.add_trace(go.Scatter(x=designs.relativeDensities, y=E11s))\n", 447 | "fig.update_yaxes(title=\"E11\")\n", 448 | "fig.update_xaxes(title=\"Relative density\")" 449 | ] 450 | }, 451 | { 452 | "cell_type": "markdown", 453 | "id": "4ea3be8c", 454 | "metadata": {}, 455 | "source": [ 456 | "# Create a custom unitcell" 457 | ] 458 | }, 459 | { 460 | "cell_type": "markdown", 461 | "id": "f86b6f12", 462 | "metadata": {}, 463 | "source": [ 464 | "UnitcellEngine currently allows for the creation of custom \"graph\" style unitcells. \n", 465 | "To do so, a template for the unitcell geometry needs to be created.\n", 466 | "This template is a python dictionary with the keys, \"node\", \"beam\", and \"face\":\n", 467 | "- \"node\" is an Nx3 numpy array defining the coordinates of N vertices in the unitcell geometry.\n", 468 | "- \"beam\" is [] or None if the unitcell has no beams; otherwise, it is an Mx2 numpy array defining the vertex connectivity of M beams by specifying the \"node\" integer indeces for the starting and ending points (noting that 0 indexing is used)\n", 469 | "- \"face\" is [] or None if the unitcell has no faces; otherwise, it is an PxQ numpy array defining the vertex connectivity of P faces by specifying the \"node\" integer indeces for the Q points in the face ordered in a clock-wise direction (i.e., order matters). Faces can be triangles (Q=3) or quadralaterals (Q=4). If it is desirable to mix triangles and quadralaterals, set Q=4 and specify an index of -1 in the last column for any triangle references. \n", 470 | "T" 471 | ] 472 | }, 473 | { 474 | "cell_type": "markdown", 475 | "id": "be7c72f9", 476 | "metadata": {}, 477 | "source": [ 478 | "\n", 479 | "Here, we'll replicate the creation of the simple cubic unitcell from scratch." 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": null, 485 | "id": "75dbb536", 486 | "metadata": {}, 487 | "outputs": [], 488 | "source": [ 489 | "template = {\n", 490 | " \"node\": np.array(\n", 491 | " [\n", 492 | " [-0.5, -0.5, -0.5],\n", 493 | " [0.5, -0.5, -0.5],\n", 494 | " [-0.5, 0.5, -0.5],\n", 495 | " [0.5, 0.5, -0.5],\n", 496 | " [-0.5, -0.5, 0.5],\n", 497 | " [0.5, -0.5, 0.5],\n", 498 | " [-0.5, 0.5, 0.5],\n", 499 | " [0.5, 0.5, 0.5],\n", 500 | " ]\n", 501 | " ),\n", 502 | " \"beam\": np.array(\n", 503 | " [\n", 504 | " [0, 1],\n", 505 | " [0, 2],\n", 506 | " [2, 3],\n", 507 | " [1, 3],\n", 508 | " [4, 5],\n", 509 | " [4, 6],\n", 510 | " [6, 7],\n", 511 | " [5, 7],\n", 512 | " [0, 4],\n", 513 | " [1, 5],\n", 514 | " [2, 6],\n", 515 | " [3, 7],\n", 516 | " ]\n", 517 | " ),\n", 518 | " \"face\": {},\n", 519 | "}\n", 520 | "custom = dict(name=\"Custom Simple Cubic\", definition=template)\n", 521 | "design = UnitcellDesign(\n", 522 | " custom,\n", 523 | " 1,\n", 524 | " 1,\n", 525 | " 3,\n", 526 | " thickness=0.3,\n", 527 | " radius=0.1,\n", 528 | " # directory=Path(__file__).parent / Path(\"tests\"),\n", 529 | " elementSize=0.5,\n", 530 | " form=\"graph\",\n", 531 | ")\n", 532 | "design.geometry.run(reuse=False, export=True)" 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": null, 538 | "id": "8f691258", 539 | "metadata": {}, 540 | "outputs": [], 541 | "source": [ 542 | "geometry = design.geometry.visualizeVTK()\n", 543 | "mesh = geometry.extract_geometry().triangulate()\n", 544 | "mesh.flip_normals()\n", 545 | "mesh.plot()" 546 | ] 547 | }, 548 | { 549 | "cell_type": "code", 550 | "execution_count": null, 551 | "id": "5926e2ec", 552 | "metadata": {}, 553 | "outputs": [], 554 | "source": [] 555 | } 556 | ], 557 | "metadata": { 558 | "kernelspec": { 559 | "display_name": ".venv", 560 | "language": "python", 561 | "name": "python3" 562 | }, 563 | "language_info": { 564 | "codemirror_mode": { 565 | "name": "ipython", 566 | "version": 3 567 | }, 568 | "file_extension": ".py", 569 | "mimetype": "text/x-python", 570 | "name": "python", 571 | "nbconvert_exporter": "python", 572 | "pygments_lexer": "ipython3", 573 | "version": "3.10.15" 574 | } 575 | }, 576 | "nbformat": 4, 577 | "nbformat_minor": 5 578 | } 579 | -------------------------------------------------------------------------------- /examples/geometry.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/examples/geometry.stl -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "unitcellengine" 3 | description = "Lattice unitcell geometry and analysis framework" 4 | version = "0.0.12" 5 | readme = "README.md" 6 | requires-python = ">=3.10, <3.12" 7 | license = {file = "LICENSE"} 8 | keywords = ["unitcell", "unitcellhub", "lattice", "homogenization", "finite element"] 9 | authors = [ 10 | {name = "Ryan Watkins", email = "watkinrt@gmail.com"} 11 | ] 12 | classifiers = [ 13 | "Development Status :: 3 - Alpha", 14 | ] 15 | dependencies = [ 16 | "dill>=0.3.9", 17 | "gmsh>=4.13.1", 18 | "mkl-service>=2.4.1", 19 | "numba>=0.60.0", 20 | "numpy<2.0", 21 | "pandas>=2.2.3", 22 | "plotly>=5.24.1", 23 | "pyamg>=5.2.1", 24 | "pypardiso>=0.4.6", 25 | "pyvista>=0.44.1", 26 | "scikit-image>=0.24.0", 27 | "scipy>=1.14.1", 28 | "tables>=3.10.1", 29 | "trimesh>=4.5.1", 30 | "unitcellsdf>=0.0.2", 31 | "vtk>=9.3.1", 32 | ] 33 | 34 | [project.urls] 35 | "Homepage" = "https://github.com/unitcellhub/unitcellengine" 36 | "Bug Reports" = "https://github.com/unitcellhub/unitcellengine/issues" 37 | 38 | # This is needed to properly set the path for the pytest framework 39 | # https://stackoverflow.com/questions/50155464/using-pytest-with-a-src-layer 40 | [tool.pytest.ini_options] 41 | pythonpath = ["src"] 42 | 43 | [tool.bumpversion] 44 | current_version = "0.0.12" 45 | parse = """(?x) 46 | (?P0|[1-9]\\d*)\\. 47 | (?P0|[1-9]\\d*)\\. 48 | (?P0|[1-9]\\d*) 49 | (?: 50 | - # dash separator for pre-release section 51 | (?P[a-zA-Z-]+) # pre-release label 52 | (?P0|[1-9]\\d*) # pre-release version number 53 | )? # pre-release section is optional 54 | """ 55 | serialize = [ 56 | "{major}.{minor}.{patch}-{pre_l}{distance_to_latest_tag}", 57 | "{major}.{minor}.{patch}", 58 | ] 59 | search = "version = \"{current_version}\"" 60 | replace = "version = \"{new_version}\"" 61 | regex = false 62 | ignore_missing_version = false 63 | tag = true 64 | sign_tags = false 65 | tag_name = "v{new_version}" 66 | tag_message = "Bump version: {current_version} → {new_version}" 67 | allow_dirty = false 68 | commit = true 69 | message = "Bump version: {current_version} → {new_version}" 70 | pre_commit_hooks = ["uv sync", "git add uv.lock"] 71 | commit_args = "" 72 | 73 | [tool.bumpversion.parts.pre_l] 74 | values = ["dev", "final"] 75 | optional_value = "final" 76 | 77 | [[tool.bumpversion.files]] 78 | filename = "src/unitcellengine/__init__.py" 79 | search = "version = \"{current_version}\"" 80 | replace = "version = \"{new_version}\"" 81 | 82 | [[tool.bumpversion.files]] 83 | filename = "pyproject.toml" 84 | search = "version = \"{current_version}\"" 85 | replace = "version = \"{new_version}\"" 86 | 87 | [dependency-groups] 88 | dev = [ 89 | "ipykernel>=6.29.5", 90 | "jupyter>=1.1.1", 91 | "pytest>=8.3.3", 92 | "python-dotenv>=1.0.1", 93 | "tqdm>=4.66.5", 94 | "twine>=5.1.1", 95 | ] 96 | 97 | [build-system] 98 | requires = ["hatchling"] 99 | build-backend = "hatchling.build" 100 | 101 | 102 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | mkl-service 2 | numpy<2.1 3 | scipy 4 | trimesh 5 | plotly 6 | gmsh 7 | numba 8 | scikit-image 9 | tables 10 | dill 11 | pyamg 12 | vtk 13 | pyvista 14 | pypardiso 15 | git+https://github.com/fogleman/sdf@main#egg=sdf 16 | -------------------------------------------------------------------------------- /src/unitcellengine/__init__.py: -------------------------------------------------------------------------------- 1 | """ UnitcellEngine is a geometry and simulation package to model lattice structures """ 2 | 3 | import logging 4 | 5 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 6 | 7 | # from unitcellengine.design import UnitcellDesign, UnitcellDesigns 8 | 9 | __version__ = version = "0.0.12" 10 | -------------------------------------------------------------------------------- /src/unitcellengine/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/src/unitcellengine/analysis/__init__.py -------------------------------------------------------------------------------- /src/unitcellengine/analysis/multiprocess.py: -------------------------------------------------------------------------------- 1 | # Dummy module required for multiprocessing with shared arrays -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Body centered cubic foam.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Body centered cubic.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Column.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Columns.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Diamond.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Face centered cubic foam.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Face centered cubic.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Fluorite.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hex prism central axis edge.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hex prism diamond.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hex prism edge.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hex prism laves phase.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hex prism vertex centroid.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Hexagonal honeycomb.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/IsoTruss.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Kelvin cell.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Oct vertex centroid.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Octet.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Re-entrant honeycomb.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Re-entrant.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Simple cubic foam.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Simple cubic.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Square honeycomb rotated.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Square honeycomb.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Tet oct vertex centroid.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Triangular honeycomb rotated.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Triangular honeycomb.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Truncated cube.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Truncated octahedron.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/definitions/Weaire-Phelan.ltcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 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 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/gmsh.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import numpy as np 3 | from unitcellengine.geometry import Geometry, DEFAULT_THICKNESS 4 | import logging 5 | from unitcellengine.utilities import timing, suppressStream 6 | import sys 7 | 8 | # Temporarily remove the current directly from the search path tl load 9 | # the gmsh package. 10 | tmp = sys.path.pop(0) 11 | import gmsh as _gmsh 12 | sys.path.insert(0, tmp) 13 | 14 | # Create logger 15 | # logging.basicConfig(level=logging.DEBUG) 16 | logger = logging.getLogger(__name__) 17 | 18 | def _simpleCubic(L, W, H): 19 | """ Simple cubic graph lines """ 20 | L2, W2, H2 = L/2, W/2, H/2 21 | return [[[-L2, -W2, -H2], [-L2, -W2, H2]], 22 | [[-L2, W2, -H2], [-L2, W2, H2]], 23 | [[L2, -W2, -H2], [L2, -W2, H2]], 24 | [[L2, W2, -H2], [L2, W2, H2]], 25 | 26 | [[-L2, -W2, -H2], [-L2, W2, -H2]], 27 | [[L2, -W2, -H2], [L2, W2, -H2]], 28 | [[-L2, -W2, H2], [-L2, W2, H2]], 29 | [[L2, -W2, H2], [L2, W2, H2]], 30 | 31 | [[-L2, -W2, -H2], [L2, -W2, -H2]], 32 | [[-L2, -W2, H2], [L2, -W2, H2]], 33 | [[-L2, W2, -H2], [L2, W2, -H2]], 34 | [[-L2, W2, H2], [L2, W2, H2]], 35 | ] 36 | 37 | def _bcc(L, W, H): 38 | """ Body centered cubic graph lines """ 39 | L2, W2, H2 = L/2, W/2, H/2 40 | return [[[-L2, -W2, -H2], [L2, W2, H2]], 41 | [[L2, -W2, -H2], [-L2, W2, H2]], 42 | [[-L2, W2, -H2], [L2, -W2, H2]], 43 | [[L2, W2, -H2], [-L2, -W2, H2]] 44 | ] 45 | 46 | def _fcc(L, W, H): 47 | """ Face centered cubic graph lines """ 48 | L2, W2, H2 = L/2, W/2, H/2 49 | return [[[-L2, -W2, -H2], [-L2, W2, H2]], 50 | [[-L2, -W2, H2], [-L2, W2, -H2]], 51 | [[L2, -W2, -H2], [L2, W2, H2]], 52 | [[L2, -W2, H2], [L2, W2, -H2]], 53 | 54 | [[-L2, -W2, -H2], [L2, -W2, H2]], 55 | [[-L2, -W2, H2], [L2, -W2, -H2]], 56 | [[-L2, W2, -H2], [L2, W2, H2]], 57 | [[-L2, W2, H2], [L2, W2, -H2]], 58 | 59 | [[-L2, -W2, -H2], [L2, W2, -H2]], 60 | [[-L2, W2, -H2], [L2, -W2, -H2]], 61 | [[-L2, -W2, H2], [L2, W2, H2]], 62 | [[-L2, W2, H2], [L2, -W2, H2]], 63 | ] 64 | 65 | def _column(L, W, H): 66 | """ Single central column """ 67 | return [[[0, 0, -H/2], [0, 0, H/2]]] 68 | 69 | # @TODO Columns doesn't currently work 70 | def _columns(L, W, H): 71 | """ Single central column and corner columns""" 72 | L2, W2, H2 = L/2, W/2, H/2 73 | return [[[0, 0, -H2], [0, 0, H2]], 74 | [[-L2, -W2, -H2], [-L2, -W2, H2]], 75 | [[-L2, W2, -H2], [-L2, W2, H2]], 76 | [[L2, -W2, -H2], [L2, -W2, H2]], 77 | [[L2, W2, -H2], [L2, W2, H2]], 78 | ] 79 | 80 | # Due to the nodal points at the corners of the unit cell, you will 81 | # miss key volume data unless you over build the unit cell. We 82 | # therefore output the nominal geometry, along with overhanging 83 | # ligaments 84 | def _diamond(L, W, H): 85 | """ Diamond unit cell """ 86 | L2, W2, H2 = L/2, W/2, H/2 87 | L4, W4, H4 = L/4, W/4, H/4 88 | 89 | lines = [[[-L4, -W4, H4], [-L2, -W2, H2]], 90 | [[L4, W4, H4], [L2, W2, H2]], 91 | [[-L4, -W4, H4], [0, 0, H2]], 92 | [[L4, W4, H4], [0, 0, H2]], 93 | 94 | [[-L4, W4, -H4], [-L2, W2, -H2]], 95 | [[L4, -W4, -H4], [L2, -W2, -H2]], 96 | [[-L4, W4, -H4], [0, 0, -H2]], 97 | [[L4, -W4, -H4], [0, 0, -H2]], 98 | 99 | [[-L4, -W4, H4], [-L2, 0, 0]], 100 | [[L4, W4, H4], [L2, 0, 0]], 101 | [[-L4, -W4, H4], [0, -W2, 0]], 102 | [[L4, W4, H4], [0, W2, 0]], 103 | 104 | [[-L4, W4, -H4], [-L2, 0, 0]], 105 | [[L4, -W4, -H4], [L2, 0, 0]], 106 | [[-L4, W4, -H4], [0, W2, 0]], 107 | [[L4, -W4, -H4], [0, -W2, 0]], 108 | 109 | [[L2, 0, 0], [L2+L4, -W4, H4]], 110 | [[L2, 0, 0], [L2+L4, W4, -H4]], 111 | [[-L2, 0, 0], [-L2-L4, W4, H4]], 112 | [[-L2, 0, 0], [-L2-L4, -W4, -H4]], 113 | 114 | [[0, W2, 0], [L4, W2+W4, -H4]], 115 | [[0, W2, 0], [-L4, W2+W4, H4]], 116 | [[0, -W2, 0], [L4, -W2-W4, H4]], 117 | [[0, -W2, 0], [-L4, -W2-W4, -H4]], 118 | 119 | [[0, 0, H2], [L4, -W4, H2+H4]], 120 | [[0, 0, H2], [-L4, W4, H2+H4]], 121 | [[0, 0, -H2], [L4, W4, -(H2+H4)]], 122 | [[0, 0, -H2], [-L4, -W4, -(H2+H4)]], 123 | 124 | [[L2, -W2, H2], [L2-L4, -W2-W4, H2-H4]], 125 | [[L2, -W2, H2], [L2+L4, -W2+W4, H2-H4]], 126 | [[L2, -W2, H2], [L2-L4, -W2+W4, H2+H4]], 127 | 128 | [[-L2, W2, H2], [-L2-L4, W2-W4, H2-H4]], 129 | [[-L2, W2, H2], [-L2+L4, W2+W4, H2-H4]], 130 | [[-L2, W2, H2], [-L2+L4, W2-W4, H2+H4]], 131 | 132 | [[L2, W2, -H2], [L2-L4, W2-W4, -H2-H4]], 133 | [[L2, W2, -H2], [L2-L4, W2+W4, -H2+H4]], 134 | [[L2, W2, -H2], [L2+L4, W2-W4, -H2+H4]], 135 | 136 | [[-L2, -W2, -H2], [-L2+L4, -W2-W4, -H2+H4]], 137 | [[-L2, -W2, -H2], [-L2-L4, -W2+W4, -H2+H4]], 138 | [[-L2, -W2, -H2], [-L2+L4, -W2+W4, -H2-H4]], 139 | ] 140 | 141 | return lines 142 | 143 | 144 | _GRAPH_GEOMETRY = {'Simple cubic': _simpleCubic, 145 | 'Body centered cubic': _bcc, 146 | 'Face centered cubic': _fcc, 147 | 'Column': _column, 148 | 'Columns': _columns, 149 | 'Diamond': _diamond} 150 | 151 | PI2 = np.pi*2 152 | 153 | def _schwarz(x, y, z, L, W, H, c): 154 | return np.cos(PI2/L*x) + np.cos(PI2/W*y) + np.cos(PI2/H*z) - c 155 | 156 | _WALLED_TPMS_GEOMETRY = {'Schwarz': _schwarz} 157 | 158 | class GmshGeometry(Geometry): 159 | """ Unitcell geometry generation with gmsh """ 160 | 161 | # List of graph-based unit cells in nTop. Note that below order if 162 | # import and must match the order shown in nTop. 163 | _GRAPH_UNITCELLS = ('Simple cubic', 164 | 'Body centered cubic', 165 | 'Face centered cubic', 166 | 'Column', 167 | 'Columns', 168 | 'Diamond', 169 | 'Fluorite', 170 | 'Octet', 171 | 'Truncated cube', 172 | 'Truncated octahedron', 173 | 'Kelvin cell', 174 | 'IsoTruss', 175 | 'Re-entrant', 176 | 'Weaire-Phelan', 177 | 'Triangular honeycomb', 178 | 'Triangular honeycomb rotated', 179 | 'Hexagonal honeycomb', 180 | 'Re-entrant honeycomb', 181 | 'Square honeycomb rotated', 182 | 'Square honeycomb', 183 | 'Face centered cubic foam', 184 | 'Body centered cubic foam', 185 | 'Simple cubic foam', 186 | 'Hex prism diamond', 187 | 'Hex prism edge', 188 | 'Hex prism vertex centroid', 189 | 'Hex prism central axis edge', 190 | 'Hex prism laves phase', 191 | 'Tet oct vertex centroid', 192 | 'Oct vertex centroid') 193 | 194 | # List of Walled TMPS-based unit cells in nTop. Note that below order is 195 | # import and must match the order shown in nTop. 196 | _WALLED_TPMS_UNITCELLS = ('Gyroid', 197 | 'Schwarz', 198 | 'Diamond', 199 | 'Lidinoid', 200 | 'SplitP', 201 | 'Neovius') 202 | 203 | _geometryExtension = "step" 204 | 205 | def __init__(self, unitcell, length, width, height, 206 | thickness=DEFAULT_THICKNESS, 207 | directory=".", form=None): 208 | """ Initialize unit cell object 209 | 210 | Arguments 211 | --------- 212 | unitcell: str 213 | Specifies the unit cell name. 214 | length: float > 0 215 | Defines the normalized length of the unit cell. 216 | width: float > 0 217 | Defines the normalized width of the unit cell. 218 | height: float > 0 219 | Defines the normalized height of the unit cell. 220 | 221 | Keywords 222 | -------- 223 | thickness: float > 0 (default = depends on unitcell) 224 | Defines the normalized thickness of the unitcell 225 | ligaments/walls. 226 | directory: str of Path (default="Database") 227 | Defines the base database output folder where results will 228 | be stored. 229 | form: None, "graph", or "walled tmps" (Default=None) 230 | Defines the unitcell form. If None, the form is 231 | automatically determined based on the *unitcell* name. 232 | 233 | """ 234 | 235 | # Run the superclass constructor 236 | super().__init__(unitcell, length, width, height, 237 | thickness=thickness, 238 | directory=directory, form=form) 239 | 240 | # Define the absolute dimension that most normalization 241 | # reference 242 | self._reference = 10 243 | 244 | @property 245 | def mshFilename(self): 246 | """ Filename of the generated msh file""" 247 | return self.directory / Path(f"unitcellGeometry.geo") 248 | 249 | @timing(logger) 250 | def run(self, reuse=True, blocking=True): 251 | """ Run nTop file to generate lattice geometry 252 | 253 | Keywords 254 | -------- 255 | headless: boolean (default=True) 256 | Specifies whether or not to run nTop in headless mode. If 257 | True, nTop runs in the background while, if False, nTop 258 | opens a session window. 259 | reuse: boolean (default=True) 260 | Specifies whether or not to rerun the geometry generation. 261 | If True, existing geometry will be used if it already 262 | exists, otherwise, the geometry will be recreated. 263 | blocking: boolean (default=True) 264 | Specifies whether or not nTop should block further execution 265 | (True) or should run in the background (False). If run in 266 | the background, the process status can be monitored by the 267 | returned Popen object. 268 | 269 | Returns 270 | ------- 271 | Popen object containing the nTop process 272 | """ 273 | 274 | logger.info(f"Running geometry generation of {self}.") 275 | 276 | _gmsh.initialize() 277 | 278 | # if logger.level == logger.DEBUG: 279 | _gmsh.option.setNumber("General.Terminal", 1) 280 | 281 | 282 | # Define geometry sizing 283 | L = self.length*self._reference 284 | W = self.width*self._reference 285 | H = self.height*self._reference 286 | t = self.thickness*self._reference 287 | L2, W2, H2 = L/2, W/2, H/2 288 | 289 | boundaries = dict(xmin=-L2, xmax=L2, 290 | ymin=-W2, ymax=W2, 291 | zmin=-H2, zmax=H2) 292 | dims = dict(x=L, y=W, z=H) 293 | 294 | # lines = [[[-L2, -W2, -H2], [L2, W2, H2]], 295 | # [[L2, -W2, -H2], [-L2, W2, H2]], 296 | # [[-L2, W2, -H2], [L2, -W2, H2]], 297 | # [[L2, W2, -H2], [-L2, -W2, H2]] 298 | # ] 299 | 300 | lines = _GRAPH_GEOMETRY[self.unitcell](L, W, H) 301 | 302 | # Create geometric bodies 303 | _gmsh.model.occ.addBox(-L2, -W2, -H2, L, W, H, 10) 304 | base = 20 305 | cylinders = [] 306 | for i, ((x1, y1, z1), (x2, y2, z2)) in enumerate(lines): 307 | 308 | # Parse the coordinates into relevant GMSH paremeters 309 | dx, dy, dz = x2-x1, y2-y1, z2-z1 310 | 311 | # Do a quick spot check that the ligament has positive 312 | assert dx**2 + dy**2 + dz**2 > 0 313 | 314 | # Create the ligament 315 | cylinder = _gmsh.model.occ.addCylinder(x1, y1, z1, 316 | dx, dy, dz, t/2) 317 | cylinders.append((3, cylinder)) 318 | 319 | # Increment the ligament counter 320 | i += 1 321 | 322 | # _gmsh.model.occ.synchronize() 323 | # _gmsh.fltk.run() 324 | 325 | # if i > 0: 326 | # # Boolean the geometries together 327 | # # [(3, base+tag+1) for tag in range(len(lines)-1)] 328 | # # fuse = _gmsh.model.occ.fuse([(3, base+tag+1) for tag in range(len(lines)-1)], 329 | # # [(3, base)]) 330 | fuse = _gmsh.model.occ.fuse(cylinders, cylinders) 331 | # # ref = fuse[0] + [x[0] for x in fuse[1] if x] 332 | # ref = fuse[0] 333 | # else: 334 | # ref = [(3, base)] 335 | 336 | # Cut of the edges to create the final unit cell body 337 | intersect = _gmsh.model.occ.intersect(fuse[0], [(3, 10)]) 338 | 339 | # Synchronize all of the model updates 340 | _gmsh.model.occ.synchronize() 341 | 342 | # Ask OpenCASCADE to compute more accurate bounding boxes of 343 | # entities 344 | _gmsh.option.setNumber("Geometry.OCCBoundsUseStl", 1) 345 | 346 | # Calculate the relative density of the structure 347 | volume = sum([_gmsh.model.occ.getMass(*body) for body in intersect[0]]) 348 | relativeDensity = volume/(L*W*H) 349 | logger.info(f"Geometry relative density: {relativeDensity*100:.2f}%") 350 | 351 | 352 | # Export the geometry 353 | _gmsh.write(self.geometryFilename.as_posix()) 354 | 355 | 356 | # Export unit cell properties 357 | 358 | # Close gmsh interface 359 | _gmsh.finalize() 360 | 361 | if __name__ == "__main__": 362 | design = GmshGeometry('Diamond', 4, 1.25, 1, thickness=0.3, form='graph', 363 | directory=Path(__file__).parent/Path("tests")) 364 | # design = GmshGeometry('Diamond', 1.5, 5, 1.25, T=0.3, form='graph', 365 | # directory=Path(r"F:\Lattice\Database")) 366 | design._geometryExtension = "stl" 367 | p = design.run() 368 | print(design) 369 | -------------------------------------------------------------------------------- /src/unitcellengine/geometry/tests/unitcellDefinition.json: -------------------------------------------------------------------------------- 1 | {"unitcell": "Gyroid", "length": 1, "width": 1, "height": 1, "thickness": 0.1, "radius": 0.25, "elementSize": 0.25, "form": "walledtpms"} -------------------------------------------------------------------------------- /src/unitcellengine/geometry/tests/unitcellGeometry.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/src/unitcellengine/geometry/tests/unitcellGeometry.stl -------------------------------------------------------------------------------- /src/unitcellengine/geometry/tests/unitcellProperties.json: -------------------------------------------------------------------------------- 1 | {"relativeDensity": 0.2917011022592413, "relativeSurfaceArea": 0.9930838219514599} -------------------------------------------------------------------------------- /src/unitcellengine/mesh/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/src/unitcellengine/mesh/__init__.py -------------------------------------------------------------------------------- /src/unitcellengine/mesh/gmsh.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import numpy as np 3 | from unitcellengine.geometry import Geometry 4 | import logging 5 | from unitcellengine.utilities import timing, suppressStream 6 | import sys 7 | 8 | # Temporarily remove the current directly from the search path tl load 9 | # the gmsh package. 10 | tmp = sys.path.pop(0) 11 | import gmsh as _gmsh 12 | sys.path.insert(0, tmp) 13 | 14 | # Create logger 15 | # logging.basicConfig(level=logging.DEBUG) 16 | logger = logging.getLogger(__name__) 17 | 18 | def mesh(geometry, elementSize): 19 | """ Create triply periodic mesh of unit cell geometry """ 20 | 21 | # Check that the geometry file exists 22 | geometry = Path(geometry) 23 | if not geometry.exists(): 24 | text = f"Geometry file {geometry} doesn't exist. Couldn't " +\ 25 | "mesh geometry" 26 | logger.critical(text) 27 | raise IOError(text) 28 | 29 | logger.info(f"Meshing geometry file {geometry}.") 30 | 31 | # Initialize gmsh 32 | _gmsh.initialize() 33 | 34 | # Allow gmsh print output based on logger level 35 | # if logger.level == logging.DEBUG: 36 | _gmsh.option.setNumber("General.Terminal", 1) 37 | 38 | # # Load geometry 39 | # if 'msh' in geometry.suffix: 40 | # # Load the mesh file, which contains native gmsh geometry 41 | # # definition and is the preferred file format. 42 | # _gmsh.merge(geometry.as_posix()) 43 | 44 | # # # Check to see if physical names have been defined 45 | # # surfaces = [s[1] for s in _gmsh.model.getEntities(2)] 46 | # # groups = _gmsh.model.getPhysicalGroupsForEntity(2, surfaces) 47 | # # flagGroups = False 48 | # # expected = {'xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax', 'interior'} 49 | # # check = expected.intersection(set(groups)) 50 | # # if len(check) != len(expected): 51 | # # flagGroups = False 52 | # # if len(check) > 0: 53 | # # difference = expected.difference(check) 54 | # # logger.info(f"Physical groups [{', '.join([f'{g}' for g in check])}] " 55 | # # "already exist, but missing " 56 | # # f"[{', '.join([f'{g}' for g in difference])}]. " 57 | # # "Flagging for additional group processing.") 58 | # else: 59 | # # Load the geometry with the OpenCASCADE importer 60 | volumes = _gmsh.model.occ.importShapes(geometry.as_posix()) 61 | for volume in volumes: 62 | _gmsh.model.occ.healShapes([volume]) 63 | _gmsh.model.occ.removeAllDuplicates() 64 | _gmsh.model.occ.synchronize() 65 | 66 | 67 | # Ask OpenCASCADE to compute more accurate bounding boxes of 68 | # entities. 69 | _gmsh.option.setNumber("Geometry.OCCBoundsUseStl", 1) 70 | 71 | # Pull out geometry boundaries and sizes 72 | vbounds = np.array([_gmsh.model.occ.getBoundingBox(*v) for v in volumes]) 73 | xmin, ymin, zmin = vbounds[:, :3].min(axis=0).tolist() 74 | xmax, ymax, zmax = vbounds[:, 3:].max(axis=0).tolist() 75 | 76 | 77 | boundaries = dict(xmin=xmin, ymin=ymin, zmin=zmin, 78 | xmax=xmax, ymax=ymax, zmax=zmax) 79 | dims = dict(x=xmax-xmin, y=ymax-ymin, z=zmax-zmin) 80 | 81 | 82 | # Define function to finding matching surfaces 83 | eps = 1e-2 84 | signs = [-1]*3 + [1]*3 85 | def matching(): 86 | matches = dict(x=[], y=[], z=[]) 87 | for i, dim in enumerate('xyz'): 88 | # Create the baseline bounding box 89 | inputs = [boundaries[bound]+eps*sign for bound, sign in zip( 90 | ['xmin', 'ymin', 'zmin', 91 | 'xmax', 'ymax', 'zmax'], 92 | signs)] 93 | # Update the bounding box to slice the relevant min boundary only 94 | inputs[i+3] = boundaries[dim+'min']+eps 95 | 96 | # Pull out the surfaces within that bounding box slice 97 | tmins = _gmsh.model.getEntitiesInBoundingBox(*inputs, 2) 98 | 99 | # Make sure surfaces are found. If not, throw a warning 100 | if len(tmins) == 0: 101 | logger.warning(f"No surfaces found on the {dim} min boundary. " 102 | "This may be a feature of the unit cell, but " 103 | "should be verified.") 104 | continue 105 | 106 | # For each identified surface, pull out the corresponding 'max' 107 | # surface 108 | flagMatch = False 109 | for tmin in tmins: 110 | # Get the bounding box for the surface of interest 111 | bmin = _gmsh.model.getBoundingBox(*tmin) 112 | 113 | # Translate the bounding box to the other size of the unitcell 114 | # and pull out the corresponding surface 115 | inputs = [bound+eps*sign for bound, sign in zip(bmin, signs)] 116 | inputs[i] += dims[dim] 117 | inputs[i+3] += dims[dim] 118 | tmaxes = _gmsh.model.getEntitiesInBoundingBox(*inputs, 2) 119 | 120 | # Check that surfaces were found on the opposing face 121 | if len(tmaxes) == 0: 122 | text = "Unable to find any surfaces opposing " +\ 123 | f"surface {tmin[1]} in the {dim} direction." 124 | logger.critical(text) 125 | raise RuntimeError(text) 126 | 127 | if len(tmaxes) > 1: 128 | text = "Multiple matching surfaces were found for " +\ 129 | f"surface {tmin[1]} in the {dim} direction. " +\ 130 | "This is unexpected behavior. Verify that the " +\ 131 | "geometry is valid and doesn't contain " +\ 132 | "duplicate surfaces." 133 | logger.critical(text) 134 | raise RuntimeError(text) 135 | 136 | # For all the matches, we compare the corresponding bounding boxes... 137 | tmax = tmaxes[0] 138 | # Pull out the bounds of the identifies surface 139 | bmax = _gmsh.model.getBoundingBox(*tmax) 140 | 141 | # Translate the bounds back to the min size of the unit cell 142 | bmax = list(bmax) 143 | bmax[i] -= dims[dim] 144 | bmax[i+3] -= dims[dim] 145 | 146 | # ...and if they match, we apply the periodicity constraint 147 | xmin, ymin, zmin, xmax, ymax, zmax = bmin 148 | xmin2, ymin2, zmin2, xmax2, ymax2, zmax2 = bmax 149 | bmax = _gmsh.model.getBoundingBox(*tmax) 150 | if (abs(xmin2 - xmin) < eps and abs(xmax2 - xmax) < eps 151 | and abs(ymin2 - ymin) < eps and abs(ymax2 - ymax) < eps 152 | and abs(zmin2 - zmin) < eps and abs(zmax2 - zmax) < eps): 153 | logger.info(f"Dim {dim}: Linked surface {tmin[1]} with " 154 | f"{tmax[1]}.") 155 | matches[dim].append((tmin[1], tmax[1])) 156 | else: 157 | # something went wrong 158 | text = "Something went wrong will matching periodic " +\ 159 | f"faces in the {dim} direction. It seems as " +\ 160 | f"surfaces {tmin[1]} and {tmax[1]} match, but " +\ 161 | "their bounding boxes don't quite line up. " +\ 162 | "Try updating the matching tolerance." 163 | logger.critical(text) 164 | raise RuntimeError(text) 165 | return matches 166 | 167 | 168 | # Enforce periodicity across the unit cell 169 | 170 | # Pull out all the surfaces on the "min" side of the unit cell and link 171 | # them to the "max" side 172 | # boundaries = dict(xmin=-L2, xmax=L2, 173 | # ymin=-W2, ymax=W2, 174 | # zmin=-H2, zmax=H2) 175 | # dims = dict(x=L, y=W, z=H) 176 | periodic = matching() 177 | for i, dim in enumerate('xyz'): 178 | # For each periodic face set, imprint geometries where necessary 179 | # to ensure matching geometry definitions 180 | for face1, face2 in periodic[dim]: 181 | tmin = (2, face1) 182 | tmax = (2, face2) 183 | 184 | # Get the bounding box for the surface of interest 185 | bmin = _gmsh.model.getBoundingBox(*tmin) 186 | bmax = _gmsh.model.getBoundingBox(*tmax) 187 | 188 | # Pull out the surface points 189 | boxmin = [bound+eps*sign for bound, sign in zip(bmin, signs)] 190 | boxmax = [bound+eps*sign for bound, sign in zip(bmax, signs)] 191 | pmin = _gmsh.model.getEntitiesInBoundingBox(*boxmin, 0) 192 | pmax = _gmsh.model.getEntitiesInBoundingBox(*boxmax, 0) 193 | cmin = _gmsh.model.getEntitiesInBoundingBox(*boxmin, 1) 194 | cmax = _gmsh.model.getEntitiesInBoundingBox(*boxmax, 1) 195 | 196 | # Pull out the coordinates of each point on the periodic 197 | # surfaces 198 | coordmin = [_gmsh.model.getValue(p[0], p[1], [0]) for p in pmin] 199 | coordmax = [_gmsh.model.getValue(p[0], p[1], [0]) for p in pmax] 200 | 201 | # Convert the coordinates to each surfaces parameterization 202 | uvmins = [_gmsh.model.getParametrization(2, tmin[1], c) 203 | for c in coordmin] 204 | uvmaxes = [_gmsh.model.getParametrization(2, tmax[1], c) 205 | for c in coordmax] 206 | 207 | # Identify points that exist on one surface, but 208 | # not the other. Group them accordingly. 209 | nuvmins = [] 210 | nuvmaxes = [] 211 | for uvs, uvrefs, new in zip([uvmins, uvmaxes], 212 | [uvmaxes, uvmins], 213 | [nuvmaxes, nuvmins]): 214 | for uv in uvs: 215 | check = np.isclose(np.array([uv]*len(uvrefs)), 216 | np.array(uvrefs)).sum(axis=1) == 2 217 | if check.sum() == 0: 218 | new.append(uv) 219 | 220 | 221 | # Loop over each matching surface and impose 222 | # periodic geometry, splitting curves where 223 | # necessary. 224 | for surface, new, curves in zip([tmin, tmax], 225 | [nuvmins, nuvmaxes], 226 | [cmin, cmax]): 227 | points = [] 228 | # Loop over each missing point 229 | # that is missing on this surface and add 230 | # it. 231 | for uv in new: 232 | coords = _gmsh.model.getValue(2, surface[1], uv) 233 | 234 | # Find the right curve to reparameterize 235 | flagReparam = False 236 | for c in curves: 237 | # Pull out the parametric bounds for 238 | # the curve 239 | bounds = _gmsh.model.getParametrizationBounds(*c) 240 | 241 | # Discretize the curve to determine 242 | # if the current point lies on this 243 | # curve 244 | ts = np.linspace(*bounds, 1000) 245 | xyzs = _gmsh.model.getValue(*c, ts).reshape(-1, 3) 246 | 247 | # Check the distance between the 248 | # current point of interest and all 249 | # points on the discretized curve 250 | length = np.linalg.norm(xyzs[1:, :]-xyzs[:-1, :], axis=1).sum() 251 | distance = np.linalg.norm(coords-xyzs, axis=1) 252 | 253 | # If the point is close to the 254 | # curve, assume the point is on the 255 | # curve 256 | if (distance < length/len(ts)).any(): 257 | # Point is on curve 258 | t = ts[distance.argmin()] 259 | 260 | # Create a new point at this 261 | # location and split the surface 262 | # at this point 263 | # uv = _gmsh.model.reparametrizeOnSurface(*c, t, surface[1]) 264 | # coords = _gmsh.model.getValue(*c, t) 265 | p = _gmsh.model.occ.addPoint(*coords) 266 | # _gmsh.model.occ.synchronize() 267 | _gmsh.model.occ.fragment(volumes, 268 | [(0, p)], 269 | removeObject=True, 270 | removeTool=True) 271 | 272 | flagReparam = True 273 | break 274 | assert flagReparam 275 | 276 | # Now that all of the periodic faces have been reparameterized to 277 | # match exactly, synchronize the (which will updated the surface 278 | # numbering) and update the periodic surface matches 279 | _gmsh.model.occ.synchronize() 280 | periodic = matching() 281 | 282 | for i, dim in enumerate('xyz'): 283 | # Define the periodicity through a 4x4 affine matrix, which is 284 | # flattened into a list. Note that, the last column corresponds to 285 | # the translation element and is the only component that should be 286 | # modified 287 | translate = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] 288 | translate[i*4+3] = dims[dim] 289 | 290 | # Apply periodicity 291 | fmin = [f for f, _ in periodic[dim]] 292 | fmax = [f for _, f in periodic[dim]] 293 | _gmsh.model.mesh.setPeriodic(2, fmax, fmin, 294 | translate) 295 | 296 | # Group surfaces based on their geometry location. Note, we do this 297 | # after the periodic linking since the mesh matching procedure 298 | # affects the surface definitions 299 | physicalNames = dict(xmin=[], xmax=[], ymin=[], ymax=[], 300 | zmin=[], zmax=[], interior=[]) 301 | for surface in _gmsh.model.getEntities(2): 302 | # Get the surface bounding box 303 | box = _gmsh.model.getBoundingBox(*surface) 304 | 305 | # If any of the extremes are essentially the same, check to 306 | # see if they are on the boundary 307 | check = [np.isclose(box[i], box[i+3], atol=eps) for i in range(3)] 308 | if any(check): 309 | # Pull out the relevant boundary zero width 310 | i = np.array([0, 1, 2])[check] 311 | assert len(i) == 1 312 | i = i[0] 313 | dim = 'xyz'[i] 314 | 315 | # Determine if the surface is on the min or max surface 316 | for extreme, offset in zip(['min', 'max'], [0, 3]): 317 | if np.isclose(box[i+offset], boundaries[dim+extreme], 318 | atol=eps): 319 | logger.debug(f"Surface {surface[1]} found to be " 320 | f"on the {dim[0]} {extreme} boundary.") 321 | physicalNames[dim+extreme].append(surface) 322 | continue 323 | 324 | # If the surface is on the exterior, than it is an interior 325 | # surface 326 | logger.debug(f"Surface {surface[1]} found to be " 327 | "an interior surface.") 328 | physicalNames['interior'].append(surface) 329 | 330 | # Specify physical names for each surface grouping 331 | for name, surfaces in physicalNames.items(): 332 | tags = _gmsh.model.addPhysicalGroup(2, surfaces) 333 | _gmsh.model.setPhysicalName(2, tags, name) 334 | 335 | # Set the mesh size 336 | volumes = _gmsh.model.getEntities(3) 337 | ps = _gmsh.model.getBoundary(volumes, False, False, True) # Get all points 338 | _gmsh.model.mesh.setSize(ps, elementSize) 339 | 340 | # Create a physical name for all elements 341 | tags = _gmsh.model.addPhysicalGroup(3, volumes) 342 | _gmsh.model.setPhysicalName(3, tags, "all") 343 | 344 | # Mesh the geometry 345 | _gmsh.model.mesh.generate(3) 346 | 347 | # # Create 2nd order tets 348 | # _gmsh.model.mesh.setOrder(2) 349 | 350 | # # Delete surface mesh 351 | # # Note that 3 node tri elements are of type 2 and 9 node tri 352 | # # elements are of type 9 353 | # dims, etags, _ = _gmsh.model.mesh.getElements(2) 354 | # dimTags = [(2, tag) for tag in etags[0]] 355 | # _gmsh.model.mesh.clear(etags) 356 | 357 | 358 | # _gmsh.fltk.run() 359 | 360 | # Save the mesh in 2 formats: .msh and .inp 361 | # _gmsh.option.setNumber("Mesh.SaveAll", 1) 362 | # _gmsh.option.setNumber("Mesh.SaveGroupsOfElements", 1) 363 | # _gmsh.option.setNumber("Mesh.SaveGroupsOfNodes", 1) 364 | _gmsh.write(geometry.with_suffix('.msh').as_posix()) 365 | _gmsh.write(geometry.with_suffix('.inp').as_posix()) 366 | _gmsh.write(geometry.with_suffix('.bdf').as_posix()) 367 | # _gmsh.write(geometry.with_suffix('.bdf').as_posix()) 368 | # _gmsh.write(geometry.with_suffix('.dat').as_posix()) 369 | 370 | # Close out gmsh 371 | _gmsh.finalize() 372 | 373 | if __name__ == "__main__": 374 | mesh(Path("unitcell/geometry/tests/unitcellGeometry.step"), 0.5) 375 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /src/unitcellengine/mesh/internal.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import logging 3 | from unitcellengine.utilities import timing, Timing 4 | from unitcellengine.geometry.sdf import SDFGeometry 5 | import numba as nb 6 | import numpy.typing as npt 7 | import typing 8 | from pathlib import Path 9 | import pyvista as pv 10 | 11 | # Create logger 12 | logger = logging.getLogger(__name__) 13 | 14 | NODE_EL = 8 15 | 16 | def nodeDofs(n: npt.ArrayLike, ndof: int=3) -> npt.NDArray[np.int_]: 17 | """ DOF numbers corresponding to the specified nodes 18 | 19 | Arguments 20 | --------- 21 | n: list like 22 | List of N nodes to extract degree of freedom numbers from. 23 | 24 | Keywords 25 | -------- 26 | ndof: int 27 | Number of degrees of freedom per node in mesh. 28 | 29 | Returns 30 | ------- 31 | (ndof, N) numpy array mapping degrees of freedom in the rows for 32 | each node. 33 | 34 | Notes 35 | ----- 36 | - This implementation assumes that all nodes in the mesh have the 37 | number of degrees of freedom and leverages this to create the 38 | mapping. 39 | """ 40 | 41 | basedofs = np.array(n)*ndof 42 | dofs = np.vstack([basedofs]*ndof) 43 | for i in range(ndof): 44 | dofs[i, :] += i 45 | 46 | return dofs.astype(np.int64) 47 | 48 | @timing(logger) 49 | @nb.njit(cache=True) 50 | def emapping(nelx: int, nely: int, nelz: int, nrhos: npt.NDArray[np.float64]) -> \ 51 | typing.Tuple[npt.NDArray[np.uint64], npt.NDArray[np.float64], npt.NDArray[np.bool_], npt.NDArray[np.bool_]]: 52 | """ Element/node mapping for a given voxel mesh 53 | 54 | Arguments 55 | --------- 56 | nelx, nely, nelz: int 57 | Number of voxel elements in the x|y|z direction 58 | nrhos: numpy (N,) array 59 | Nodal densities (1 if in the body and 0 if out of the body) 60 | 61 | Returns 62 | ------- 63 | e2n: (nel, nodes/element) numpy unsigned int array 64 | Hexahedral-based nodal definitions for each element in the mesh. 65 | Each row corresponds to an element definition and the columns 66 | define the nodes in the element using the standard CCW notation. 67 | rhos: (nel,) numpy float array 68 | The density of each element in the mesh. 69 | eind: (nel,) boolean array 70 | Defines the indices that are inside the geometry (True) and those 71 | that are not (False) 72 | nind: (nnode, ) boolean array 73 | Defines the nodes that are inside the geometry (True) and those 74 | that are not (False) 75 | 76 | Notes 77 | ----- 78 | - This is for an 8 node hexahedral element 79 | - This mapping methodology assumes a specific form for the voxel: 80 | - Node 0 starts at the min x, y, z location. 81 | - Node numbering increments in the x direction until the end of 82 | the row. It then increments in y and then starts from xmin again. 83 | - Once a voxel slice is complete, the z direction is increased 84 | and the node number method is continued. 85 | - The node order is defined as follows 86 | Bottom slice Top slice 87 | 3<-----2 7<-----6 88 | | ^ | ^ 89 | | | | | 90 | 0----->1 4----->5 91 | """ 92 | 93 | # Mesh properties 94 | nel = nelx*nely*nelz # Number of elements 95 | nel_slice = nelx*nely # Number of elements per voxel slice 96 | nn_slice = (1+nelx)*(1+nely) # Number of nodes per voxel slice 97 | 98 | # Initialize relevant arrays 99 | e2n=np.zeros((nel, NODE_EL), dtype=np.uint64) 100 | rhos = np.zeros(nel) 101 | einds = np.zeros(nel, dtype=np.bool_) 102 | ninds = np.zeros((1+nelx)*(1+nely)*(1+nelz), dtype=np.bool_) 103 | nodes = np.zeros(8, dtype=np.int64) 104 | 105 | # Build the element/node mapping matrix used to define the mesh. 106 | for elx in range(nelx): 107 | for ely in range(nely): 108 | for elz in range(nelz): 109 | # Element ID 110 | el = elx + ely*nelx + elz*nel_slice 111 | 112 | # Base node in row 1, slice 1 113 | n1 = elx + (nelx+1)*ely + elz*nn_slice 114 | # Base node in row 2, slice 1 115 | n2 = elx + (nelx+1)*(ely+1) + elz*nn_slice 116 | 117 | # Determine if element is within the geometry based on 118 | # the nodal densities. If any node has a positive 119 | # density, then the element is considered to be in the mesh. 120 | nodes[:4] = np.array([n1, n1+1, n2+1, n2]) 121 | nodes[4:] = nodes[:4] + nn_slice 122 | subnrhos = nrhos[nodes] 123 | if np.any(subnrhos > 0.): 124 | einds[el] = True 125 | ninds[nodes] = True 126 | rhos[el] = np.mean(subnrhos) 127 | 128 | 129 | # Define node indexing on slice 1 in a counter-clockwise direction 130 | e2n[el, :4] = np.array([n1, n1+1, n2+1, n2]) 131 | 132 | # Define node indexing on slice 2 in a counter-clockwise direction 133 | e2n[el, 4:] = e2n[el, :4] + nn_slice 134 | 135 | return e2n, rhos, einds, ninds 136 | 137 | @timing(logger) 138 | def mesh(sdf: typing.Callable, elementSize: float=1, 139 | dimensions: npt.ArrayLike=[1, 1, 1], 140 | center: npt.ArrayLike=[0, 0, 0], 141 | filename: typing.Union[None, str, Path]=None) -> \ 142 | typing.Tuple[npt.NDArray[np.float64], npt.NDArray[np.bool_], npt.NDArray[np.uint64], typing.List[float], 143 | npt.NDArray[np.float64], npt.NDArray[np.bool_]]: 144 | """ Hex mesh sign distance function over a regtangular grid 145 | 146 | Arguments 147 | --------- 148 | sdf: callable function sdf((N, 3)) -> (N,) 149 | Sign distance function definition for the geometry, that takes 150 | in points in 3D space and outputs there distance from the 151 | nearest geometric surface (with negative values being interior 152 | to the geometry and positive being exterior) 153 | 154 | Keywords 155 | -------- 156 | elementSize: float > 0 (default is 1) 157 | Defines the approximate voxel mesh grid spacing, nothing that 158 | the value gets slightly modified to ensure perfect spacing 159 | within the dimensions of the geometric bounds defined by 160 | *dimensions*. 161 | center: array-like of length 3 (default is [0, 0, 0]) 162 | Defines the (x, y, z) centerpoint of the geometry. 163 | dimensions: array-like of length 3 (default is [1, 1, 1]) 164 | Defines the (x, y, z) bounding box sizes. 165 | filename: None, str, or Path object 166 | If not None, defines the filename to save the mesh file to. Must 167 | either be a pkl (python Pickle file) or vtu (Paraview) format. 168 | 169 | Returns 170 | ------- 171 | ns: (nnodes, 4) numpy array 172 | Node list in the form (nid, x, y, z) 173 | ninds: (nnodes,) boolean array 174 | Defines the node indices that reside within the geometry. 175 | e2n: (nel, 8) numpy array 176 | Defines the node mapping for each element in the mesh using a 177 | counterclockwise convention. 178 | nel: length 3 list 179 | Defines the number of elements in the x, y, and z directions. 180 | erhos: (nel,) numpy array 181 | Element densities (0 <= rho <= 1) where 0 defines an inactive 182 | element, 1 defines a fully active element, and intermediate 183 | values indicate a partially active element. 184 | einds: (nel,) boolean array 185 | Defines the element indices that reside within the geometry 186 | 187 | """ 188 | 189 | # Convert inputs to relevant mesh properties 190 | L, W, H = dimensions 191 | xc, yc, zc = center 192 | xmin, xmax = -L/2+xc, L/2+xc 193 | ymin, ymax = -W/2+yc, W/2+yc 194 | zmin, zmax = -H/2+zc, H/2+zc 195 | nelx = int(L/elementSize) 196 | nely = int(W/elementSize) 197 | nelz = int(H/elementSize) 198 | nnodes = (nelx+1)*(nely+1)*(nelz+1) 199 | nodes = np.arange(nnodes) 200 | nn_slice = (1+nelx)*(1+nely) 201 | dx, dy, dz = L/nelx, W/nely, H/nelz 202 | 203 | # Generate the base voxel mesh. Note, to get the nodes laid out in 204 | # the correct orientation for element definition, the x, y, and z 205 | # inputs are arranged such that mesh grid outputs the results in the 206 | # desired format. 207 | X, Y, Z = np.meshgrid(np.linspace(xmin, xmax, nelx+1), 208 | np.linspace(ymin, ymax, nely+1), 209 | np.linspace(zmin, zmax, nelz+1), indexing='ij') 210 | # Y, Z, X = np.meshgrid(np.linspace(ymin, ymax, nely+1), 211 | # np.linspace(zmin, zmax, nelz+1), 212 | # np.linspace(xmin, xmax, nelx+1)) 213 | 214 | # Flatten the voxel mesh and assign the coordinates to a node number 215 | xs = X.flatten('F') 216 | ys = Y.flatten('F') 217 | zs = Z.flatten('F') 218 | ns = np.vstack((nodes, xs, ys, zs)).T 219 | logger.debug(f"Total number of nodes in the voxel mesh: {ns.shape[0]}") 220 | 221 | # Calculate nodal sign distance function values 222 | with Timing("Calculating nodal SDF values", logger=logger): 223 | nsdf = sdf(ns[:, 1:]).flatten() 224 | nrhos = np.ones(nsdf.shape) 225 | nrhos[nsdf>0] = 0 226 | 227 | # Create full element to node map 228 | e2n, rhosmean, einds, ninds = emapping(nelx, nely, nelz, nrhos) 229 | logger.debug(f"Total number of active nodes: {ninds.sum()}") 230 | logger.debug(f"Total number of active elements: {einds.sum()}") 231 | 232 | # Go through and implement a more accurate element density for 233 | # boundary elements (i.e., 0 < rhomean < 1) by integrating the 234 | # sign distance field using Gauss quadrature. He, an 8 point 235 | # quatrature is used. Note: the weights for these quadrature points 236 | # are all 1 and are therefore omitted. 237 | 238 | with Timing("Intermediate density integration", logger=logger): 239 | # Update the density only at locations of intermediate density (to 240 | # improve efficiency while processing large meshes) 241 | inds = np.logical_and(einds, rhosmean < 1) 242 | if np.any(inds): 243 | subns = ns[e2n[inds, 0], 1:] 244 | gps = 0.5*(1+np.array([-1/np.sqrt(3), 1/np.sqrt(3)])) 245 | 246 | # Calculate the sign distance function at each integration point 247 | corners = np.hstack([-sdf(subns + np.array([dX, dY, dZ])) 248 | for dX in [0, dx] 249 | for dY in [0, dy] 250 | for dZ in [0, dz]]) 251 | offsets = np.hstack([-sdf(subns + np.array([dX, dY, dZ])) 252 | for dX in dx*gps 253 | for dY in dy*gps 254 | for dZ in dz*gps]) 255 | 256 | # Combine the integration and nodal point data 257 | prhos = np.hstack((corners, offsets)) 258 | # Sum up the integration points and normalize them by cell volume. 259 | # This is then the volume average distance from the surface in 260 | # absolute units. We then normalize with respect to the element 261 | # diagonal size, which gives us the mean nomalized distance. We then 262 | # offset this number by 0.5 to get a density of 0.5 when the mean 263 | # distance is zero (i.e., half the body is in the cell and the other 264 | # half isn't) 265 | # NOTE: TPMS implicit representations aren't actually sign 266 | # distance functions (they are nonlinear in distance and have 267 | # incorrect magnitude.) We therefore have to normalize based on 268 | # the local information. 269 | normc1 = np.abs(corners[:, 1]-corners[:, 0])/dz 270 | normc2 = np.abs(corners[:, 2]-corners[:, 0])/dy 271 | normc3 = np.abs(corners[:, 4]-corners[:, 0])/dx 272 | normc4 = np.abs(corners[:, 7]-corners[:, 0])/np.sqrt(dx**2+dy**2+dz**2) 273 | scaling = max(normc1.max(), normc2.max(), 274 | normc3.max(), normc4.max())*np.sqrt(dx**2+dy**2+dz**2) 275 | erhos = rhosmean.copy() 276 | erhos[inds] = prhos.sum(axis=1)/16/scaling+0.5 277 | 278 | # Make sure there aren't any small negative densities 279 | erhos[erhos < 0.] = 0. 280 | 281 | # Store the integration point binary densities (which are used 282 | # to calculate stresses) 283 | # intrhos = np.zeros() 284 | else: 285 | # If there are no intermediate density elements, just use 286 | # the current element density definition 287 | erhos = rhosmean.copy() 288 | 289 | # Save the mesh if desired 290 | out = (ns, nsdf, ninds, e2n, [nelx, nely, nelz], erhos, einds) 291 | if filename: 292 | filename = Path(filename) 293 | 294 | # Parse the input suffix 295 | suffix = filename.suffix 296 | if suffix == "": 297 | suffixes = [".npz", ".vtu"] 298 | elif not (suffix == ".npz" or suffix == ".vtu"): 299 | logger.warning(f"Suffix {suffix} is an unsupported export " 300 | "Exporting as '.pkl' and '.vtu' instead.") 301 | suffixes = [".npz", ".vtu"] 302 | else: 303 | suffixes = [suffix] 304 | 305 | # Write out files for each file type specified 306 | for suffix in suffixes: 307 | if suffix == ".npz": 308 | # Save as a python numpy array 309 | save = {k: v for k, v in 310 | zip(["nodes", "nsdf", "ninds", \ 311 | "e2n", "nels", "erhos", "einds"], 312 | out)} 313 | with Timing(f"Saving mesh to {filename.with_suffix('.npz')}", 314 | logger=logger): 315 | np.savez_compressed(filename.with_suffix(".npz"), **save) 316 | else: 317 | # Save as a paraview file 318 | nel = einds.sum() 319 | with Timing(f"Saving mesh to {filename.with_suffix('.vtu')}", 320 | logger=logger): 321 | grid = _convert2pyvista(ns, nsdf, e2n, erhos, einds, {"sdf": nsdf}, {"density": erhos[einds]}) 322 | grid.save(filename.with_suffix(".vtu")) 323 | return out 324 | 325 | def periodic(nodes: npt.ArrayLike, e2n: npt.ArrayLike, nels: list) ->\ 326 | typing.Tuple[npt.ArrayLike, npt.ArrayLike, npt.ArrayLike]: 327 | """ Return periodic element node mapping 328 | 329 | Arguments 330 | --------- 331 | nodes: (N, 4) numpy array 332 | Defines all of the nodes in the mesh, including the node index 333 | (nid), and the (x, y, z) coordinate of the nodate: (nind, x, y, 334 | z) 335 | e2n: (nel, 8) numpy array 336 | Defines the node mapping for each element in the mesh based on a 337 | counter clockwise structure. 338 | nels: length 3 list 339 | Defines the number of elements in the x, y, and z directions 340 | 341 | Returns 342 | ------- 343 | pe2n: (nel, 8) numpy array 344 | New the node mapping for each element with periodic boundaries 345 | lns: (M,) numpy array 346 | "Leading" periodic nodes such that *lns[i]* = *fns[i]* 347 | fns: (M,) numpy array 348 | "Following" periodic nodes such that *lns[i]* = *fns[i]* 349 | 350 | """ 351 | 352 | # Pull out the number of elements in each direction 353 | nelx, nely, nelz = nels 354 | 355 | # Reshape the nodes into a 3D grid, relying on the known structure 356 | # of the grid to do so 357 | Ns = nodes[:, 0].reshape((nelx+1, nely+1, nelz+1), 358 | order='F').astype(np.int64) 359 | 360 | # Pull out the "leading" nodes (-X, -Y, and -Z faces) 361 | lns = np.hstack((# Faces (without edges) 362 | Ns[0, 1:-1, 1:-1].flatten(), 363 | Ns[1:-1, 0, 1:-1].flatten(), 364 | Ns[1:-1, 1:-1, 0].flatten(), 365 | # Edges (without end nodes) 366 | Ns[0, 0, 1:-1].flatten(), 367 | Ns[0, 0, 1:-1].flatten(), 368 | Ns[0, 0, 1:-1].flatten(), 369 | Ns[0, 1:-1, 0].flatten(), 370 | Ns[0, 1:-1, 0].flatten(), 371 | Ns[0, 1:-1, 0].flatten(), 372 | Ns[1:-1, 0, 0].flatten(), 373 | Ns[1:-1, 0, 0].flatten(), 374 | Ns[1:-1, 0, 0].flatten(), 375 | # Vertices 376 | np.array([Ns[0, 0, 0]]*7), 377 | )) 378 | # lns = np.hstack((Ns[0, :, :].flatten(), 379 | # Ns[:, 0, :].flatten(), 380 | # Ns[:, :, 0].flatten())) 381 | 382 | # Pull out the "following" nodes (+X, +Y, and +Z faces) 383 | fns = np.hstack((# Faces (without edges) 384 | Ns[-1, 1:-1, 1:-1].flatten(), 385 | Ns[1:-1, -1, 1:-1].flatten(), 386 | Ns[1:-1, 1:-1, -1].flatten(), 387 | # Edges (without end nodes) 388 | Ns[0, -1, 1:-1].flatten(), 389 | Ns[-1, 0, 1:-1].flatten(), 390 | Ns[-1, -1, 1:-1].flatten(), 391 | Ns[0, 1:-1, -1].flatten(), 392 | Ns[-1, 1:-1, 0].flatten(), 393 | Ns[-1, 1:-1, -1].flatten(), 394 | Ns[1:-1, 0, -1].flatten(), 395 | Ns[1:-1, -1, 0].flatten(), 396 | Ns[1:-1, -1, -1].flatten(), 397 | # Vertices 398 | np.array([Ns[0, 0, -1], 399 | Ns[0, -1, 0], 400 | Ns[-1, 0, 0], 401 | Ns[0, -1, -1], 402 | Ns[-1, 0, -1], 403 | Ns[-1, -1, 0], 404 | Ns[-1, -1, -1]]), 405 | )) 406 | # fns = np.hstack((Ns[-1, :, :].flatten(), 407 | # Ns[:, -1, :].flatten(), 408 | # Ns[:, :, -1].flatten())) 409 | 410 | # Ns[-1, :, :] = Ns[0, :, :] 411 | # Ns[:, -1, :] = Ns[:, 0, :] 412 | # Ns[:, :, -1] = Ns[:, :, 0] 413 | 414 | # Create a new element node mapping that wraps around periodically 415 | pns = Ns.flatten('F') 416 | pns[fns] = pns[lns] 417 | return pns[e2n], lns, fns 418 | 419 | def _convert2pyvista(ns, nsdf, e2n, erhos, einds, pointData, cellData): 420 | """ Create a pyvista plotting object from the given raw mesh details 421 | 422 | Parameters 423 | ---------- 424 | ns: Nx4 numpy array 425 | Nodal coordinates for the mesh, with the first 1st column defining 426 | the node number and the last 3 columns defining the (x, y, z) coordinates. 427 | nsdf: 428 | Geometry signed distance field values at each node 429 | e2n: numpy array 430 | Element nodal connectivity mapping, with each row defining the nodal conductivity. 431 | erhos: numpy array 432 | Volume fraction of each element (percentage of the geometry that fills the element). 433 | einds: numpy array 434 | Index array defining the active elements in the mesh (within or on the boundary of the geometry). 435 | pointData: dict [Default = {}] 436 | Dictionary of nodal data, where the dictionary key defines the 437 | vtk variable name and the value must be a number of nodes x dofs 438 | array. 439 | cellData: dict [Default = {}] 440 | Dictionary of cell data, where the dictionary key defines the 441 | vtk variable name and the value must be a number of cells x dofs 442 | array. 443 | 444 | Returns 445 | ------- 446 | pyvista UnstructuredGrid object that contains mesh data (such as the geometry 447 | signed-distance function values and the element fill ration) and the specified 448 | nodal and cell data. 449 | 450 | """ 451 | # Incorporate the basis mesh data 452 | pointData.update({"sdf": nsdf}) 453 | cellData.update({"density": erhos[einds]}) 454 | 455 | # Write results to file 456 | nel = einds.sum() 457 | with Timing(f"Creating pyvista representation of mesh {mesh}", logger=logger): 458 | # Define the point connectivity for each cell 459 | cells = np.hstack((np.ones((nel, 1), dtype=int)*e2n.shape[1], e2n[einds, :].astype(int))) 460 | # Define the cell type for each cell in the mesh (which are all hexahedron) 461 | # @TODO This is a fragile way to determine the cellType based on the node connectivity matrix. 462 | cellType = pv.CellType.HEXAHEDRON if e2n.shape[1] == 8 else pv.CellType.QUADRATIC_HEXAHEDRON 463 | cellTypes = np.full(nel, cellType, dtype=np.uint8) 464 | 465 | # Create the pyvista grid object and add in the supplied data 466 | grid = pv.UnstructuredGrid(cells.ravel(), cellTypes, ns[:, 1:]) 467 | for k, v in pointData.items(): 468 | grid.point_data[k] = v 469 | for k, v in cellData.items(): 470 | grid.cell_data[k] = v 471 | 472 | return grid 473 | 474 | def convert2pyvista(mesh: str|Path, pointData:dict={}, cellData: dict={}) -> pv.UnstructuredGrid: 475 | """ Create a pyvista plotting object for the given mesh and data 476 | 477 | Parameters 478 | --------- 479 | mesh: str or Path object with .npz extension 480 | Filename of mesh file generated by internal homogenization 481 | engine. 482 | pointData: dict [Default = {}] 483 | Dictionary of nodal data, where the dictionary key defines the 484 | vtk variable name and the value must be a number of nodes x dofs 485 | array. 486 | cellData: dict [Default = {}] 487 | Dictionary of cell data, where the dictionary key defines the 488 | vtk variable name and the value must be a number of cells x dofs 489 | array. 490 | 491 | Returns 492 | ------- 493 | pyvista UnstructuredGrid object that contains mesh data (such as the geometry 494 | signed-distance function values and the element fill ration) and the specified 495 | nodal and cell data. 496 | 497 | """ 498 | 499 | # Process mesh filename 500 | mesh = Path(mesh) 501 | assert mesh.suffix == ".npz", ( 502 | f"Input mesh {mesh} has the incorrect " "suffix. Should be .npz." 503 | ) 504 | 505 | # Load mesh 506 | with np.load(mesh) as data: 507 | ns = data["nodes"] 508 | nsdf = data["nsdf"] 509 | e2n = data["e2n"] 510 | erhos = data["erhos"] 511 | einds = data["einds"] 512 | 513 | grid = _convert2pyvista(ns, nsdf, e2n, erhos, einds, pointData, cellData) 514 | 515 | return grid 516 | 517 | if __name__ == "__main__": 518 | logging.basicConfig(level=logging.DEBUG) 519 | 520 | from pathlib import Path 521 | 522 | geometry = SDFGeometry("Face centered cubic", 1, 1, 1, 0.3, 523 | radius=0.25, form="graph") 524 | # geometry = SDFGeometry("Gyroid", 5, 5, 5, 0.05, form="walledtpms") 525 | # geometry.run(reuse=False) 526 | 527 | 528 | L = geometry.DIMENSION*geometry.length 529 | W = geometry.DIMENSION*geometry.width 530 | H = geometry.DIMENSION*geometry.height 531 | T = geometry.DIMENSION*geometry.thickness 532 | 533 | dims = [L, W, H] 534 | elementSize = 0.05*T 535 | filename = Path(__file__).parent/Path("tests")/Path("test.npz") 536 | ns, nsdf, ninds, e2n, (nelx, nely, nelz), rhos, einds = \ 537 | mesh(geometry.sdf, elementSize=elementSize, dimensions=dims, 538 | filename=filename) 539 | # ns, nsdf, ninds, e2n, (nelx, nely, nelz), rhos, einds = mesh(geometry.sdf, elementSize=elementSize, dimensions=dims, filename=None) 540 | grid = convert2pyvista(filename.with_suffix(".npz")) 541 | grid.plot() 542 | # print(nsdf.min()) 543 | # print(geometry.relativeDensity) 544 | print("done") 545 | -------------------------------------------------------------------------------- /src/unitcellengine/mesh/tests/test.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9a12619e250afcd871fb8a5f25a3ab36e7e529764d63ebd72db1b7c8da2490f6 3 | size 8755726 4 | -------------------------------------------------------------------------------- /src/unitcellengine/utilities.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import wraps 3 | from contextlib import contextmanager 4 | import tempfile 5 | import os 6 | 7 | def timing(logger=None): 8 | """ Function timing decorator 9 | 10 | Keywords 11 | -------- 12 | logger: None or logging object 13 | If none, print is used to output the timing results. If a 14 | logging object, the timing information is logged with the INFO 15 | logging priority. 16 | 17 | Returns 18 | ------- 19 | Decorator wrapped function 20 | 21 | """ 22 | def inner(f): 23 | @wraps(f) 24 | def wrap(*args, **kw): 25 | ts = time.time() 26 | result = f(*args, **kw) 27 | te = time.time() 28 | dt = time.gmtime(te-ts) 29 | text = f"Function '{f.__name__}' completed in " +\ 30 | f"{time.strftime('%H:%M:%S', dt)}" 31 | if logger: 32 | # Use the specified logger if provided 33 | logger.info(text) 34 | else: 35 | print(text) 36 | return result 37 | return wrap 38 | return inner 39 | 40 | class Timing: 41 | """ Timer context manager for local line execution timing""" 42 | 43 | def __init__(self, title, logger=None): 44 | self.title = title 45 | self.logger = logger 46 | self.ts = time.time() 47 | 48 | def __enter__(self): 49 | self.ts = time.time() 50 | if self.logger: 51 | self.logger.info(f"{self.title}: running...") 52 | else: 53 | print(f"{self.title}: running...", end="") 54 | 55 | def __exit__(self, *args): 56 | tf = time.time() 57 | dt = time.gmtime(tf - self.ts) 58 | if self.logger: 59 | self.logger.info(f"{self.title} completed in " 60 | f"{time.strftime('%H:%M:%S', dt)}") 61 | else: 62 | print(f"completed in {time.strftime('%H:%M:%S', dt)}") 63 | 64 | # Taken from https://stackoverflow.com/questions/4037481/caching-class-attributes-in-python 65 | class cachedProperty(object): 66 | """ 67 | Descriptor (non-data) for building an attribute on-demand on first use. 68 | """ 69 | def __init__(self, factory): 70 | """ 71 | is called such: factory(instance) to build the attribute. 72 | """ 73 | self._attr_name = factory.__name__ 74 | self._factory = factory 75 | 76 | def __get__(self, instance, owner): 77 | # Build the attribute. 78 | attr = self._factory(instance) 79 | 80 | # Cache the value; hide ourselves. 81 | setattr(instance, self._attr_name, attr) 82 | 83 | return attr 84 | 85 | # There doesn't seem to be an easy way to prevent cubit (or other C 86 | # apis) from printing to the console, which can unfortunately clutter 87 | # the console very quickly. Initial digging suggested stopping the 88 | # sys.stdout stream, but cubit doesn't print to this stream. After a lot 89 | # more digging, I found that the python cubit library, which is a 90 | # wrapper to the underlying C library, prints to filedescriptor 1. So, I 91 | # adopted the general std.output capture methodology, but to 92 | # filedescripter 1. See the following for reference: 93 | # - https://stackoverflow.com/questions/42952623/stop-python-module-from-printing 94 | # - https://stackoverflow.com/questions/9488560/capturing-print-output-from-shared-library-called-from-python-with-ctypes-module/41262627 95 | @contextmanager 96 | def suppressStream(): 97 | # Open up a silent stream and point it to a temp file 98 | silent = tempfile.TemporaryFile() 99 | 100 | # Capture the current state of the stdout 101 | stdout = os.dup(1) 102 | 103 | # Point all output to the silent stream 104 | os.dup2(silent.fileno(), 1) 105 | 106 | # Process the command 107 | try: 108 | yield silent 109 | finally: 110 | # Upon exit, replace the output stream with the standard stdout 111 | os.dup2(stdout, 1) 112 | 113 | 114 | # Close the temp file stream 115 | silent.close() 116 | 117 | # Close old stdout stream. Note: this was added as you will 118 | # eventually run into a "Too many open files" error. See 119 | # https://stackoverflow.com/questions/36647498/how-to-close-file-descriptors-in-python 120 | # for reference. 121 | os.close(stdout) -------------------------------------------------------------------------------- /test.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:129a9672f60ad52143dddd51894159e2610b25ba03eb4abb5476c5418b314bf2 3 | size 10958843 4 | -------------------------------------------------------------------------------- /test.vtk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/test.vtk -------------------------------------------------------------------------------- /test.vtu: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:af484c76867f7ac46bb8a1625df6b39e628c9f92246658e12830e58221a126c8 3 | size 16213054 4 | -------------------------------------------------------------------------------- /test2.vtk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/test2.vtk -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/tests/__init__.py -------------------------------------------------------------------------------- /tests/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/tests/analysis/__init__.py -------------------------------------------------------------------------------- /tests/analysis/resources/fullyDense.e: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:44f7c9a34dc0fe6f45200f3451d97883e65d91f4b60c4555a1b6ae087bf68ed3 3 | size 1320080 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/fullyDense.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:93190063cad66b942f532ab55d7c56afb1adbe9ce4d38ce4ba512a5a39c45a4e 3 | size 12547 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/fullyDense_conductance_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cb1067f9cec50403e8644108d8f518098fe2f209ab7356f0ae0dc4313c694679 3 | size 18871 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/fullyDense_elastic_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:02b83d5b6ae889b4fd62ffff26d46678b9ea0c3f654d9e9f67690a8e5876b8bb 3 | size 257740 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesGyroid_0_24.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f79c1c8e5a5fe0e35b893fb7a43162333cee04ccba6a9d2206f09ab160711b59 3 | size 2198301 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesGyroid_0_24.vtu: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:26bc2f51703c7ee68bbcef8aa67583ebae0505bcb21567ada8a49045e71e5d73 3 | size 7734217 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesGyroid_0_24_conductance_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cb0a63d628573fbee9a6109adfca33d476eacf6f96e6c91f1ce820aa09e43b35 3 | size 3234787 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesSchwarz_0_23.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:82e2a2689ba1b1b0feaa5a4658d38ba5575dceb32560d49eb775e07e5db803c1 3 | size 908567 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesSchwarz_0_23_conductance_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9deb4f3a6b6a3d2aa8e334e9cf2f3f8925f5f8b418d41589de6adc4581cbe494 3 | size 1197132 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/halesTMPS.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:113535b617fc3b71bef1568c0b77fac16383d566c2260fd55d405501b6fff408 3 | size 68337 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/hexHoneycomb_5x5x1.e: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0e8f6901a90e7a7188c13750d8edc9cd83f67be1a61bf17c5f05679312236907 3 | size 10199264 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/hexHoneycomb_5x5x1.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cebd917837a0d443d4e285ea7471663acbb22699b23f995b0059d0e4cb30eaa0 3 | size 16437298 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/hexHoneycomb_5x5x1_elastic_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5d709f324e8f8d268094c9ba8026ab46bdb1c7d53e54400825ffcd43bca28eed 3 | size 59646876 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/octetPatil.e: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f6e6af994a31695e4e9fe5885ae53c5039fe269455e2bc6a1600d906ad1f4ad2 3 | size 1896692 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/octetPatil.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4998688cc1afa57c194aa999b11142be6eb0a34deebaf344034f4236ef00b3b7 3 | size 849263 4 | -------------------------------------------------------------------------------- /tests/analysis/resources/octetPatil_elastic_results.npz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7eff3ba27f30fe27570b28f7bf60819a55176e8c67aa9a1f64ac562fd761bb51 3 | size 10094063 4 | -------------------------------------------------------------------------------- /tests/geometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/tests/geometry/__init__.py -------------------------------------------------------------------------------- /tests/geometry/resources/unitcellGeometry.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/tests/geometry/resources/unitcellGeometry.stl -------------------------------------------------------------------------------- /tests/geometry/test_geometry.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from pathlib import Path 4 | 5 | import numpy as np 6 | 7 | import unitcellengine.geometry.sdf as sdf 8 | 9 | # class TestNtop(unittest.TestCase): 10 | 11 | # def text_bcc(self): 12 | # """ Test the generation of a body centered cubic lattice """ 13 | # lattice = ntop.LatticeUnitcell('Body centered cubic', 1, 1, 1, 14 | # directory=Path(__file__) / Path('ntop')) 15 | 16 | 17 | class TestSDFGeometry(unittest.TestCase): 18 | """Test elastic homogenization method""" 19 | 20 | def testSimpleCubicFoam(self): 21 | """Compare geometry hand calcs with output values""" 22 | 23 | # Define geometry object 24 | L = 1 25 | t = 0.15 26 | directory = Path(__file__).parent / Path("resources") 27 | lattice = sdf.SDFGeometry( 28 | "Simple cubic foam", L, L, L, thickness=t, directory=directory 29 | ) 30 | 31 | # Generate the geometry and process the results 32 | lattice.run(reuse=False) 33 | 34 | # Check that the necessary output files are present 35 | self.assertTrue(lattice.definitionFilename.exists()) 36 | with lattice.definitionFilename.open("r") as f: 37 | definition = json.load(f) 38 | self.assertListEqual( 39 | list(definition.keys()), 40 | [ 41 | "unitcell", 42 | "length", 43 | "width", 44 | "height", 45 | "thickness", 46 | "radius", 47 | "elementSize", 48 | "form", 49 | ], 50 | ) 51 | self.assertTrue(lattice.propertiesFilename.exists()) 52 | with lattice.propertiesFilename.open("r") as f: 53 | properties = json.load(f) 54 | self.assertListEqual( 55 | list(properties.keys()), ["relativeDensity", "relativeSurfaceArea"] 56 | ) 57 | 58 | # Check relative density 59 | expDensity = (L * L * t + L * (L - t) * t + (L - t) ** 2 * t) / (L**3) 60 | self.assertAlmostEqual(lattice.relativeDensity, expDensity, 2) 61 | 62 | # Check exposed internal surface area 63 | expArea = 2 * 3 * ((L - t) ** 2) / (6 * L * L) 64 | self.assertAlmostEqual(lattice.relativeSurfaceArea, expArea, 1) 65 | 66 | def testCustomGraph(self): 67 | """Compare custom simple cubic definition with built in definition""" 68 | 69 | # Define an output directory 70 | directory = Path(__file__).parent / Path("resources") 71 | 72 | # Define a simple cubic template geometry 73 | template = { 74 | "node": np.array( 75 | [ 76 | [-0.5, -0.5, -0.5], 77 | [0.5, -0.5, -0.5], 78 | [-0.5, 0.5, -0.5], 79 | [0.5, 0.5, -0.5], 80 | [-0.5, -0.5, 0.5], 81 | [0.5, -0.5, 0.5], 82 | [-0.5, 0.5, 0.5], 83 | [0.5, 0.5, 0.5], 84 | ] 85 | ), 86 | "beam": np.array( 87 | [ 88 | [0, 1], 89 | [0, 2], 90 | [2, 3], 91 | [1, 3], 92 | [4, 5], 93 | [4, 6], 94 | [6, 7], 95 | [5, 7], 96 | [0, 4], 97 | [1, 5], 98 | [2, 6], 99 | [3, 7], 100 | ] 101 | ), 102 | "face": {}, 103 | } 104 | 105 | geometry = dict( 106 | length=1, 107 | width=1.5, 108 | height=1.25, 109 | thickness=0.3, 110 | radius=0.1, 111 | elementSize=0.5, 112 | form="graph", 113 | directory=directory, 114 | ) 115 | builtin = sdf.SDFGeometry("Simple cubic", **geometry) 116 | custom = sdf.SDFGeometry( 117 | dict(name="Custom Simple Cubic", definition=template), **geometry 118 | ) 119 | builtin.run(reuse=False, export=True) 120 | custom.run(reuse=False, export=True) 121 | 122 | # Check relative density and relative surface areas 123 | self.assertAlmostEqual(builtin.relativeDensity, custom.relativeDensity, 4) 124 | self.assertAlmostEqual( 125 | builtin.relativeSurfaceArea, custom.relativeSurfaceArea, 4 126 | ) 127 | 128 | 129 | if __name__ == "__main__": 130 | unittest.main() 131 | -------------------------------------------------------------------------------- /tests/resources/octetPatil.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitcellhub/unitcellengine/57d64b58224953d9d3a4372618b4b8b50c1227e6/tests/resources/octetPatil.jpeg -------------------------------------------------------------------------------- /tests/resources/octetPatil.pdf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8988c4d0f622d9af7952e66d90ad1c6b46efa7bada3ffe47b08afae63c8c7833 3 | size 2486381 4 | -------------------------------------------------------------------------------- /tests/test_unitcellengine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from unitcellengine.design import UnitcellDesign 7 | 8 | 9 | class TestUnitcell(unittest.TestCase): 10 | """Test unitcellengine definition and quantification""" 11 | 12 | def testMeshConvergence(self): 13 | """Verify that default mesh is sufficiently converged""" 14 | 15 | # Create default geometry to pull out the reference point 16 | defaultElementSize = 0.25 17 | design = UnitcellDesign("Octet", 1, 1, 1) 18 | design.generateGeometry(reuse=False) 19 | design.generateMesh(elementSize=defaultElementSize, reuse=False) 20 | design.homogenizationElastic.run(blocking=True, reuse=False) 21 | check = design.homogenizationElastic.check() 22 | design.homogenizationElastic.process(check=True, reuse=False) 23 | 24 | defaultC = design.homogenizationElastic.CH.copy() 25 | # defaultMaxStresses = design.homogenizationElastic.maxStresses 26 | 27 | # Check what happens when the element size is cut in half 28 | refinedElementSize = defaultElementSize * 0.5 29 | design.generateMesh(elementSize=refinedElementSize, reuse=False) 30 | design.homogenizationElastic.run(blocking=True, reuse=False) 31 | check = design.homogenizationElastic.check() 32 | design.homogenizationElastic.process(check=True, reuse=False) 33 | 34 | refinedC = design.homogenizationElastic.CH.copy() 35 | # refinedMaxStresses = design.homogenizationElastic.maxStresses 36 | 37 | # Check stiffness convergence 38 | atolC = refinedC.max() * 1e-4 39 | print(defaultC, refinedC) 40 | self.assertTrue(np.allclose(defaultC, refinedC, atol=atolC)) 41 | 42 | # # Check stress convergences 43 | # atolStress = refinedMaxStresses.max()*1e-4 44 | # self.assertTrue(np.allclose(defaultMaxStresses, refinedMaxStresses, 45 | # atol=atolStress)) 46 | 47 | # # Loop through and very the element size in the mesh 48 | # elementSizes = [defaultElementSize*x for x in [4, 2, 0.5, 0.25]] 49 | # Cs = [] 50 | # maxStresses = [] 51 | # for elementSize in elementSizes: 52 | # # Update the element size and reprocesses 53 | # design.elementSize = elementSize 54 | # design.generateMesh(check=True, reuse=False) 55 | # design.homogenizationElastic.run(blocking=True, reuse=False) 56 | # check = design.homogenizationElastic.check() 57 | # design.homogenizationElastic.process(check=True) 58 | 59 | # # Store results 60 | # Cs.append(design.result) 61 | # maxStresses.append(design.maxStress) 62 | 63 | # # Calculate differentials to check for convergence 64 | # tmpCs = Cs.copy() 65 | # tmpCs.insert(2, defaultC) 66 | # tmpStresses = maxStresses.copy() 67 | # tmpStresses.insert(2, defaultMaxStress) 68 | # tmpElementSizes = elementSizes.copy() 69 | # tmpElementSizes.insert(2, defaultElementSize) 70 | # diffCs = [] 71 | # diffMaxStresses = [] 72 | # for i in range(len(tmpElementSizes)-1): 73 | # diffElementSizes = tmpElementSizes[i]-tmpElementSizes[i+1] 74 | # diffCs.append((tmpCs[i]-tmpCs[i+1])/diffElementSizes) 75 | # diffMaxStresses.append((tmpStresses[i]-tmpStresses[i+1])/diffElementSizes) 76 | 77 | # # Use the upper and lower bounds on element size to set the 78 | # # absolute tolerance and the 2x middle cases to define the 79 | # # relative tolerance 80 | # atolC = Cs[0] - Cs[-1] 81 | # atolStress = diffMaxStresses[0] - diffMaxStresses[-1] 82 | 83 | # self.assertAlmostEqual(defaultMaxStress, ) 84 | 85 | 86 | if __name__ == "__main__": 87 | unittest.main() 88 | --------------------------------------------------------------------------------