├── .codecov.yml ├── .flake8 ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CITATION.cff ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── doc ├── Makefile ├── common.rst ├── conf.py ├── geo.rst ├── index.rst ├── occ.rst └── requirements.txt ├── justfile ├── pyproject.toml ├── src └── pygmsh │ ├── __about__.py │ ├── __init__.py │ ├── _cli.py │ ├── _optimize.py │ ├── common │ ├── __init__.py │ ├── bezier.py │ ├── bspline.py │ ├── circle_arc.py │ ├── curve_loop.py │ ├── dummy.py │ ├── ellipse_arc.py │ ├── geometry.py │ ├── line.py │ ├── line_base.py │ ├── plane_surface.py │ ├── point.py │ ├── polygon.py │ ├── size_field.py │ ├── spline.py │ ├── surface.py │ ├── surface_loop.py │ └── volume.py │ ├── geo │ ├── __init__.py │ ├── dummy.py │ └── geometry.py │ ├── helpers.py │ └── occ │ ├── __init__.py │ ├── ball.py │ ├── box.py │ ├── cone.py │ ├── cylinder.py │ ├── disk.py │ ├── dummy.py │ ├── geometry.py │ ├── rectangle.py │ ├── torus.py │ └── wedge.py ├── tests ├── built_in │ ├── helpers.py │ ├── test_airfoil.py │ ├── test_bsplines.py │ ├── test_circle.py │ ├── test_circle_transform.py │ ├── test_cube.py │ ├── test_ellipsoid.py │ ├── test_embed.py │ ├── test_hex.py │ ├── test_hole_in_square.py │ ├── test_layers.py │ ├── test_pacman.py │ ├── test_physical.py │ ├── test_pipes.py │ ├── test_quads.py │ ├── test_recombine.py │ ├── test_rectangle.py │ ├── test_rectangle_with_hole.py │ ├── test_regular_extrusion.py │ ├── test_rotated_layers.py │ ├── test_rotation.py │ ├── test_screw.py │ ├── test_splines.py │ ├── test_subdomains.py │ ├── test_swiss_cheese.py │ ├── test_symmetrize.py │ ├── test_tori.py │ ├── test_torus.py │ ├── test_torus_crowd.py │ ├── test_transfinite.py │ ├── test_unordered_unoriented.py │ └── test_volume.py ├── helpers.py ├── occ │ ├── helpers.py │ ├── test_ball_with_stick.py │ ├── test_logo.py │ ├── test_meshio_logo.py │ ├── test_opencascade_ball.py │ ├── test_opencascade_boolean.py │ ├── test_opencascade_booleans.py │ ├── test_opencascade_box.py │ ├── test_opencascade_builtin_mix.py │ ├── test_opencascade_cone.py │ ├── test_opencascade_cylinder.py │ ├── test_opencascade_ellipsoid.py │ ├── test_opencascade_extrude.py │ ├── test_opencascade_regular_extrusion.py │ ├── test_opencascade_torus.py │ ├── test_opencascade_wedge.py │ ├── test_refinement.py │ └── test_translations.py ├── test_boundary_layers.py ├── test_extrusion_entities.py ├── test_helpers.py ├── test_labels.py └── test_optimize.py └── tox.ini /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: no 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, E741 3 | max-line-length = 80 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | doc: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.x" 18 | - uses: actions/checkout@v2 19 | - run: | 20 | pip install sphinx sphinx-autodoc-typehints 21 | sphinx-build -M html doc/ build/ 22 | 23 | lint: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Check out repo 27 | uses: actions/checkout@v2 28 | - name: Set up Python 29 | uses: actions/setup-python@v2 30 | - name: Run pre-commit 31 | uses: pre-commit/action@v2.0.3 32 | 33 | build: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | python-version: ["3.7", "3.8", "3.9", "3.10"] 38 | steps: 39 | - uses: actions/setup-python@v2 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - uses: actions/checkout@v2 43 | # install gmsh from system -- not sure why this is necessary 44 | - name: Install gmsh 45 | run: | 46 | sudo apt-get install -y python3-gmsh 47 | - name: Test with tox 48 | run: | 49 | pip install tox 50 | tox -- --cov pygmsh --cov-report xml --cov-report term 51 | - uses: codecov/codecov-action@v1 52 | if: ${{ matrix.python-version == '3.9' }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.geo 3 | *.msh 4 | *.e 5 | *.vtk 6 | *.vtu 7 | .DS_Store 8 | .cache/ 9 | .tox/ 10 | *.xml 11 | MANIFEST 12 | README.rst 13 | build/ 14 | dist/ 15 | pygmsh.egg-info/ 16 | doc/_build/ 17 | *.pos 18 | *.prof 19 | .pytest_cache/ 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/isort 3 | rev: 5.10.1 4 | hooks: 5 | - id: isort 6 | 7 | - repo: https://github.com/psf/black 8 | rev: 22.1.0 9 | hooks: 10 | - id: black 11 | language_version: python3 12 | 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 4.0.1 15 | hooks: 16 | - id: flake8 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3 5 | # use pip for installation, see 6 | # 7 | install: 8 | - requirements: doc/requirements.txt 9 | - path: . 10 | method: pip 11 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Schlömer" 5 | given-names: "Nico" 6 | orcid: "https://orcid.org/0000-0001-5228-0946" 7 | title: "pygmsh: A Python frontend for Gmsh" 8 | doi: 10.5281/zenodo.1173105 9 | url: https://github.com/nschloe/pygmsh 10 | license: GPL-3.0 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tests/helpers.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | pygmsh 3 |

Gmsh for Python.

4 |

5 | 6 | [![PyPi Version](https://img.shields.io/pypi/v/pygmsh.svg?style=flat-square)](https://pypi.org/project/pygmsh/) 7 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pygmsh.svg?style=flat-square)](https://pypi.org/project/pygmsh/) 8 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1173105.svg?style=flat-square)](https://doi.org/10.5281/zenodo.1173105) 9 | [![GitHub stars](https://img.shields.io/github/stars/nschloe/pygmsh.svg?style=flat-square&logo=github&label=Stars&logoColor=white)](https://github.com/nschloe/pygmsh) 10 | [![PyPi downloads](https://img.shields.io/pypi/dm/pygmsh.svg?style=flat-square)](https://pypistats.org/packages/pygmsh) 11 | 12 | [![Discord](https://img.shields.io/static/v1?logo=discord&label=chat&message=on%20discord&color=7289da&style=flat-square)](https://discord.gg/hnTJ5MRX2Y) 13 | [![Documentation Status](https://readthedocs.org/projects/pygmsh/badge/?version=latest&style=flat-square)](https://pygmsh.readthedocs.io/en/latest/?badge=latest) 14 | 15 | [![gh-actions](https://img.shields.io/github/workflow/status/nschloe/pygmsh/ci?style=flat-square)](https://github.com/nschloe/pygmsh/actions?query=workflow%3Aci) 16 | [![codecov](https://img.shields.io/codecov/c/github/nschloe/pygmsh.svg?style=flat-square)](https://codecov.io/gh/nschloe/pygmsh) 17 | [![LGTM](https://img.shields.io/lgtm/grade/python/github/nschloe/pygmsh.svg?style=flat-square)](https://lgtm.com/projects/g/nschloe/pygmsh) 18 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) 19 | 20 | pygmsh combines the power of [Gmsh](https://gmsh.info/) with the versatility of Python. 21 | It provides useful abstractions from Gmsh's own Python interface so you can create 22 | complex geometries more easily. 23 | 24 | To use, install Gmsh itself and pygmsh from [pypi](https://pypi.org/project/pygmsh/): 25 | 26 | ``` 27 | [sudo] apt install python3-gmsh 28 | pip install pygmsh 29 | ``` 30 | 31 | This document and the [`tests/`](https://github.com/nschloe/pygmsh/tree/main/tests/) 32 | directory contain many small examples. See 33 | [here](https://pygmsh.readthedocs.io/en/latest/index.html) for the full documentation. 34 | 35 | #### Flat shapes 36 | 37 | | | | | 38 | | :-------------------------------------------------------------------: | :------------------------------------------------------------------: | :-------------------------------------------------------------------: | 39 | | Polygon | Circle | (B-)Splines | 40 | 41 | Codes: 42 | 43 | ```python 44 | import pygmsh 45 | 46 | with pygmsh.geo.Geometry() as geom: 47 | geom.add_polygon( 48 | [ 49 | [0.0, 0.0], 50 | [1.0, -0.2], 51 | [1.1, 1.2], 52 | [0.1, 0.7], 53 | ], 54 | mesh_size=0.1, 55 | ) 56 | mesh = geom.generate_mesh() 57 | 58 | # mesh.points, mesh.cells, ... 59 | # mesh.write("out.vtk") 60 | ``` 61 | 62 | ```python 63 | import pygmsh 64 | 65 | with pygmsh.geo.Geometry() as geom: 66 | geom.add_circle([0.0, 0.0], 1.0, mesh_size=0.2) 67 | mesh = geom.generate_mesh() 68 | ``` 69 | 70 | ```python 71 | import pygmsh 72 | 73 | with pygmsh.geo.Geometry() as geom: 74 | lcar = 0.1 75 | p1 = geom.add_point([0.0, 0.0], lcar) 76 | p2 = geom.add_point([1.0, 0.0], lcar) 77 | p3 = geom.add_point([1.0, 0.5], lcar) 78 | p4 = geom.add_point([1.0, 1.0], lcar) 79 | s1 = geom.add_bspline([p1, p2, p3, p4]) 80 | 81 | p2 = geom.add_point([0.0, 1.0], lcar) 82 | p3 = geom.add_point([0.5, 1.0], lcar) 83 | s2 = geom.add_spline([p4, p3, p2, p1]) 84 | 85 | ll = geom.add_curve_loop([s1, s2]) 86 | pl = geom.add_plane_surface(ll) 87 | 88 | mesh = geom.generate_mesh() 89 | ``` 90 | 91 | The return value is always a [meshio](https://pypi.org/project/meshio/) mesh, so to 92 | store it to a file you can 93 | 94 | 95 | 96 | ```python 97 | mesh.write("test.vtk") 98 | ``` 99 | 100 | The output file can be visualized with various tools, e.g., 101 | [ParaView](https://www.paraview.org/). 102 | 103 | With 104 | 105 | 106 | 107 | ```python 108 | pygmsh.write("test.msh") 109 | ``` 110 | 111 | you can access Gmsh's native file writer. 112 | 113 | #### Extrusions 114 | 115 | | | | | 116 | | :-------------------------------------------------------------------: | :-------------------------------------------------------------------: | :-----------------------------------------------------------------: | 117 | | `extrude` | `revolve` | `twist` | 118 | 119 | ```python 120 | import pygmsh 121 | 122 | with pygmsh.geo.Geometry() as geom: 123 | poly = geom.add_polygon( 124 | [ 125 | [0.0, 0.0], 126 | [1.0, -0.2], 127 | [1.1, 1.2], 128 | [0.1, 0.7], 129 | ], 130 | mesh_size=0.1, 131 | ) 132 | geom.extrude(poly, [0.0, 0.3, 1.0], num_layers=5) 133 | mesh = geom.generate_mesh() 134 | ``` 135 | 136 | ```python 137 | from math import pi 138 | import pygmsh 139 | 140 | with pygmsh.geo.Geometry() as geom: 141 | poly = geom.add_polygon( 142 | [ 143 | [0.0, 0.2, 0.0], 144 | [0.0, 1.2, 0.0], 145 | [0.0, 1.2, 1.0], 146 | ], 147 | mesh_size=0.1, 148 | ) 149 | geom.revolve(poly, [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], 0.8 * pi) 150 | mesh = geom.generate_mesh() 151 | ``` 152 | 153 | ```python 154 | from math import pi 155 | import pygmsh 156 | 157 | with pygmsh.geo.Geometry() as geom: 158 | poly = geom.add_polygon( 159 | [ 160 | [+0.0, +0.5], 161 | [-0.1, +0.1], 162 | [-0.5, +0.0], 163 | [-0.1, -0.1], 164 | [+0.0, -0.5], 165 | [+0.1, -0.1], 166 | [+0.5, +0.0], 167 | [+0.1, +0.1], 168 | ], 169 | mesh_size=0.05, 170 | ) 171 | 172 | geom.twist( 173 | poly, 174 | translation_axis=[0, 0, 1], 175 | rotation_axis=[0, 0, 1], 176 | point_on_axis=[0, 0, 0], 177 | angle=pi / 3, 178 | ) 179 | 180 | mesh = geom.generate_mesh() 181 | ``` 182 | 183 | #### OpenCASCADE 184 | 185 | | | | | 186 | | :------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | :------------------------------------------------------------------: | 187 | | | | 188 | 189 | Gmsh also supports OpenCASCADE (`occ`), allowing for a CAD-style geometry specification. 190 | 191 | ```python 192 | from math import pi, cos 193 | import pygmsh 194 | 195 | with pygmsh.occ.Geometry() as geom: 196 | geom.characteristic_length_max = 0.1 197 | r = 0.5 198 | disks = [ 199 | geom.add_disk([-0.5 * cos(7 / 6 * pi), -0.25], 1.0), 200 | geom.add_disk([+0.5 * cos(7 / 6 * pi), -0.25], 1.0), 201 | geom.add_disk([0.0, 0.5], 1.0), 202 | ] 203 | geom.boolean_intersection(disks) 204 | 205 | mesh = geom.generate_mesh() 206 | ``` 207 | 208 | ```python 209 | # ellpsoid with holes 210 | import pygmsh 211 | 212 | with pygmsh.occ.Geometry() as geom: 213 | geom.characteristic_length_max = 0.1 214 | ellipsoid = geom.add_ellipsoid([0.0, 0.0, 0.0], [1.0, 0.7, 0.5]) 215 | 216 | cylinders = [ 217 | geom.add_cylinder([-1.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.3), 218 | geom.add_cylinder([0.0, -1.0, 0.0], [0.0, 2.0, 0.0], 0.3), 219 | geom.add_cylinder([0.0, 0.0, -1.0], [0.0, 0.0, 2.0], 0.3), 220 | ] 221 | geom.boolean_difference(ellipsoid, geom.boolean_union(cylinders)) 222 | 223 | mesh = geom.generate_mesh() 224 | ``` 225 | 226 | ```python 227 | # puzzle piece 228 | import pygmsh 229 | 230 | with pygmsh.occ.Geometry() as geom: 231 | geom.characteristic_length_min = 0.1 232 | geom.characteristic_length_max = 0.1 233 | 234 | rectangle = geom.add_rectangle([-1.0, -1.0, 0.0], 2.0, 2.0) 235 | disk1 = geom.add_disk([-1.2, 0.0, 0.0], 0.5) 236 | disk2 = geom.add_disk([+1.2, 0.0, 0.0], 0.5) 237 | 238 | disk3 = geom.add_disk([0.0, -0.9, 0.0], 0.5) 239 | disk4 = geom.add_disk([0.0, +0.9, 0.0], 0.5) 240 | flat = geom.boolean_difference( 241 | geom.boolean_union([rectangle, disk1, disk2]), 242 | geom.boolean_union([disk3, disk4]), 243 | ) 244 | 245 | geom.extrude(flat, [0, 0, 0.3]) 246 | 247 | mesh = geom.generate_mesh() 248 | ``` 249 | 250 | #### Mesh refinement/boundary layers 251 | 252 | | | | | 253 | | :---------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: | 254 | | | | 255 | 256 | ```python 257 | # boundary refinement 258 | import pygmsh 259 | 260 | with pygmsh.geo.Geometry() as geom: 261 | poly = geom.add_polygon( 262 | [ 263 | [0.0, 0.0], 264 | [2.0, 0.0], 265 | [3.0, 1.0], 266 | [1.0, 2.0], 267 | [0.0, 1.0], 268 | ], 269 | mesh_size=0.3, 270 | ) 271 | 272 | field0 = geom.add_boundary_layer( 273 | edges_list=[poly.curves[0]], 274 | lcmin=0.05, 275 | lcmax=0.2, 276 | distmin=0.0, 277 | distmax=0.2, 278 | ) 279 | field1 = geom.add_boundary_layer( 280 | nodes_list=[poly.points[2]], 281 | lcmin=0.05, 282 | lcmax=0.2, 283 | distmin=0.1, 284 | distmax=0.4, 285 | ) 286 | geom.set_background_mesh([field0, field1], operator="Min") 287 | 288 | mesh = geom.generate_mesh() 289 | ``` 290 | 291 | 292 | 293 | ```python 294 | # mesh refinement with callback 295 | import pygmsh 296 | 297 | with pygmsh.geo.Geometry() as geom: 298 | geom.add_polygon( 299 | [ 300 | [-1.0, -1.0], 301 | [+1.0, -1.0], 302 | [+1.0, +1.0], 303 | [-1.0, +1.0], 304 | ] 305 | ) 306 | geom.set_mesh_size_callback( 307 | lambda dim, tag, x, y, z: 6.0e-2 + 2.0e-1 * (x**2 + y**2) 308 | ) 309 | 310 | mesh = geom.generate_mesh() 311 | ``` 312 | 313 | 314 | 315 | ```python 316 | # ball with mesh refinement 317 | from math import sqrt 318 | import pygmsh 319 | 320 | 321 | with pygmsh.occ.Geometry() as geom: 322 | geom.add_ball([0.0, 0.0, 0.0], 1.0) 323 | 324 | geom.set_mesh_size_callback( 325 | lambda dim, tag, x, y, z: abs(sqrt(x**2 + y**2 + z**2) - 0.5) + 0.1 326 | ) 327 | mesh = geom.generate_mesh() 328 | ``` 329 | 330 | #### Optimization 331 | 332 | pygmsh can optimize existing meshes, too. 333 | 334 | 335 | 336 | ```python 337 | import meshio 338 | 339 | mesh = meshio.read("mymesh.vtk") 340 | optimized_mesh = pygmsh.optimize(mesh, method="") 341 | ``` 342 | 343 | You can also use the command-line utility 344 | 345 | ``` 346 | pygmsh-optimize input.vtk output.xdmf 347 | ``` 348 | 349 | where input and output can be any format supported by 350 | [meshio](https://pypi.org/project/meshio/). 351 | 352 | ### Testing 353 | 354 | To run the pygmsh unit tests, check out this repository and type 355 | 356 | ``` 357 | pytest 358 | ``` 359 | 360 | ### Building Documentation 361 | 362 | Docs are built using [Sphinx](http://www.sphinx-doc.org/en/stable/). 363 | 364 | To build, run 365 | 366 | ``` 367 | sphinx-build -b html doc doc/_build 368 | ``` 369 | 370 | ### License 371 | 372 | This software is published under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html). 373 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pygmsh.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pygmsh.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pygmsh" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pygmsh" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /doc/common.rst: -------------------------------------------------------------------------------- 1 | Common 2 | ====== 3 | 4 | Common functions shared between the geo and the occ kernels. 5 | 6 | Geometry 7 | -------- 8 | .. automodule:: pygmsh.common.geometry 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | Bspline 14 | ------- 15 | .. automodule:: pygmsh.common.bspline 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | CircleArc 21 | --------- 22 | .. automodule:: pygmsh.common.circle_arc 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | EllipseArc 28 | ---------- 29 | .. automodule:: pygmsh.common.ellipse_arc 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | LineBase 35 | -------- 36 | .. automodule:: pygmsh.common.line_base 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | CurveLoop 42 | --------- 43 | .. automodule:: pygmsh.common.curve_loop 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | Line 49 | ---- 50 | .. automodule:: pygmsh.common.line 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | Point 56 | ----- 57 | .. automodule:: pygmsh.common.point 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | Spline 63 | ------ 64 | .. automodule:: pygmsh.common.spline 65 | :members: 66 | :undoc-members: 67 | :show-inheritance: 68 | 69 | SurfaceLoop 70 | ----------- 71 | .. automodule:: pygmsh.common.surface_loop 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | Surface 77 | ------- 78 | .. automodule:: pygmsh.common.surface 79 | :members: 80 | :undoc-members: 81 | :show-inheritance: 82 | 83 | Volume 84 | ------ 85 | .. automodule:: pygmsh.common.volume 86 | :members: 87 | :undoc-members: 88 | :show-inheritance: 89 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # pygmsh documentation build configuration file, created by 2 | # sphinx-quickstart on Tue Oct 27 19:56:53 2015. 3 | # 4 | # This file is execfile()d with the current directory set to its 5 | # containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | import os 13 | import sys 14 | from pathlib import Path 15 | from unittest import mock 16 | 17 | this_dir = Path(__file__).resolve().parent 18 | about = {} 19 | with open(this_dir / ".." / "src" / "pygmsh" / "__about__.py") as f: 20 | d = exec(f.read(), about) 21 | 22 | __version__ = about["__version__"] 23 | 24 | ON_RTD = os.environ.get("READTHEDOCS", None) == "True" 25 | 26 | MOCK_MODULES = ["meshio", "gmsh"] 27 | for mod_name in MOCK_MODULES: 28 | sys.modules[mod_name] = mock.Mock() 29 | 30 | # If extensions (or modules to document with autodoc) are in another directory, 31 | # add these directories to sys.path here. If the directory is relative to the 32 | # documentation root, use os.path.abspath to make it absolute, like shown here. 33 | # sys.path.insert(0, os.path.abspath('.')) 34 | 35 | # -- General configuration ------------------------------------------------ 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | "sphinx.ext.autodoc", 45 | "sphinx.ext.mathjax", 46 | "sphinx.ext.napoleon", 47 | "sphinx_autodoc_typehints", 48 | ] 49 | 50 | # Napoleon settings 51 | napoleon_google_docstring = False 52 | napoleon_numpy_docstring = True 53 | napoleon_include_init_with_doc = False 54 | napoleon_include_private_with_doc = False 55 | napoleon_include_special_with_doc = False 56 | napoleon_use_admonition_for_examples = False 57 | napoleon_use_admonition_for_notes = False 58 | napoleon_use_admonition_for_references = False 59 | napoleon_use_ivar = True 60 | napoleon_use_param = True 61 | napoleon_use_rtype = True 62 | 63 | # Add any paths that contain templates here, relative to this directory. 64 | templates_path = ["_templates"] 65 | 66 | # The suffix(es) of source filenames. 67 | # You can specify multiple suffix as a list of string: 68 | # source_suffix = ['.rst', '.md'] 69 | source_suffix = ".rst" 70 | 71 | # The encoding of source files. 72 | # source_encoding = 'utf-8-sig' 73 | 74 | # The master toctree document. 75 | master_doc = "index" 76 | 77 | # General information about the project. 78 | project = "pygmsh" 79 | copyright = "2013-2022, Nico Schlömer et al." 80 | author = "Nico Schlömer" 81 | 82 | # The version info for the project you're documenting, acts as replacement for 83 | # |version| and |release|, also used in various other places throughout the 84 | # built documents. 85 | # 86 | # The short X.Y version. 87 | version = ".".join(__version__.split(".")[:2]) 88 | 89 | # The language for content autogenerated by Sphinx. Refer to documentation 90 | # for a list of supported languages. 91 | # 92 | # This is also used if you do content translation via gettext catalogs. 93 | # Usually you set "language" from the command line for these cases. 94 | language = None 95 | 96 | # There are two options for replacing |today|: either, you set today to some 97 | # non-false value, then it is used: 98 | # today = '' 99 | # Else, today_fmt is used as the format for a strftime call. 100 | # today_fmt = '%B %d, %Y' 101 | 102 | # List of patterns, relative to source directory, that match files and 103 | # directories to ignore when looking for source files. 104 | exclude_patterns = ["_build"] 105 | 106 | # The reST default role (used for this markup: `text`) to use for all 107 | # documents. 108 | # default_role = None 109 | 110 | # If true, '()' will be appended to :func: etc. cross-reference text. 111 | # add_function_parentheses = True 112 | 113 | # If true, the current module name will be prepended to all description 114 | # unit titles (such as .. function::). 115 | # add_module_names = True 116 | 117 | # If true, sectionauthor and moduleauthor directives will be shown in the 118 | # output. They are ignored by default. 119 | # show_authors = False 120 | 121 | # The name of the Pygments (syntax highlighting) style to use. 122 | pygments_style = "sphinx" 123 | 124 | # A list of ignored prefixes for module index sorting. 125 | # modindex_common_prefix = [] 126 | 127 | # If true, keep warnings as "system message" paragraphs in the built documents. 128 | # keep_warnings = False 129 | 130 | # If true, `todo` and `todoList` produce output, else they produce nothing. 131 | todo_include_todos = False 132 | 133 | 134 | # -- Options for HTML output ---------------------------------------------- 135 | 136 | # The theme to use for HTML and HTML Help pages. See the documentation for 137 | # a list of builtin themes. 138 | html_theme = "default" 139 | if not ON_RTD: 140 | try: 141 | import sphinx_rtd_theme 142 | 143 | html_theme = "sphinx_rtd_theme" 144 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 145 | except ImportError: 146 | pass 147 | 148 | # Theme options are theme-specific and customize the look and feel of a theme 149 | # further. For a list of options available for each theme, see the 150 | # documentation. 151 | # html_theme_options = {} 152 | 153 | # Add any paths that contain custom themes here, relative to this directory. 154 | # html_theme_path = [] 155 | 156 | # The name for this set of Sphinx documents. If None, it defaults to 157 | # " v documentation". 158 | # html_title = None 159 | 160 | # A shorter title for the navigation bar. Default is the same as html_title. 161 | # html_short_title = None 162 | 163 | # The name of an image file (relative to this directory) to place at the top 164 | # of the sidebar. 165 | # html_logo = None 166 | 167 | # The name of an image file (within the static path) to use as favicon of the 168 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 169 | # pixels large. 170 | # html_favicon = None 171 | 172 | # Add any paths that contain custom static files (such as style sheets) here, 173 | # relative to this directory. They are copied after the builtin static files, 174 | # so a file named "default.css" will overwrite the builtin "default.css". 175 | # html_static_path = ['_static'] 176 | 177 | # Add any extra paths that contain custom files (such as robots.txt or 178 | # .htaccess) here, relative to this directory. These files are copied 179 | # directly to the root of the documentation. 180 | # html_extra_path = [] 181 | 182 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 183 | # using the given strftime format. 184 | # html_last_updated_fmt = '%b %d, %Y' 185 | 186 | # If true, SmartyPants will be used to convert quotes and dashes to 187 | # typographically correct entities. 188 | # html_use_smartypants = True 189 | 190 | # Custom sidebar templates, maps document names to template names. 191 | # html_sidebars = {} 192 | 193 | # Additional templates that should be rendered to pages, maps page names to 194 | # template names. 195 | # html_additional_pages = {} 196 | 197 | # If false, no module index is generated. 198 | # html_domain_indices = True 199 | 200 | # If false, no index is generated. 201 | # html_use_index = True 202 | 203 | # If true, the index is split into individual pages for each letter. 204 | # html_split_index = False 205 | 206 | # If true, links to the reST sources are added to the pages. 207 | # html_show_sourcelink = True 208 | 209 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 210 | # html_show_sphinx = True 211 | 212 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 213 | # html_show_copyright = True 214 | 215 | # If true, an OpenSearch description file will be output, and all pages will 216 | # contain a tag referring to it. The value of this option must be the 217 | # base URL from which the finished HTML is served. 218 | # html_use_opensearch = '' 219 | 220 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 221 | # html_file_suffix = None 222 | 223 | # Language to be used for generating the HTML full-text search index. 224 | # Sphinx supports the following languages: 225 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 226 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 227 | # html_search_language = 'en' 228 | 229 | # A dictionary with options for the search language support, empty by default. 230 | # Now only 'ja' uses this config value 231 | # html_search_options = {'type': 'default'} 232 | 233 | # The name of a javascript file (relative to the configuration directory) that 234 | # implements a search results scorer. If empty, the default will be used. 235 | # html_search_scorer = 'scorer.js' 236 | 237 | # Output file base name for HTML help builder. 238 | htmlhelp_basename = "pygmshdoc" 239 | 240 | # -- Options for LaTeX output --------------------------------------------- 241 | 242 | latex_elements = { 243 | # The paper size ('letterpaper' or 'a4paper'). 244 | # 'papersize': 'letterpaper', 245 | # The font size ('10pt', '11pt' or '12pt'). 246 | # 'pointsize': '10pt', 247 | # Additional stuff for the LaTeX preamble. 248 | # 'preamble': '', 249 | # Latex figure (float) alignment 250 | # 'figure_align': 'htbp', 251 | } 252 | 253 | # Grouping the document tree into LaTeX files. List of tuples 254 | # (source start file, target name, title, 255 | # author, documentclass [howto, manual, or own class]). 256 | latex_documents = [ 257 | (master_doc, "pygmsh.tex", "pygmsh Documentation", "Nico Schlömer", "manual") 258 | ] 259 | 260 | # The name of an image file (relative to this directory) to place at the top of 261 | # the title page. 262 | # latex_logo = None 263 | 264 | # For "manual" documents, if this is true, then toplevel headings are parts, 265 | # not chapters. 266 | # latex_use_parts = False 267 | 268 | # If true, show page references after internal links. 269 | # latex_show_pagerefs = False 270 | 271 | # If true, show URL addresses after external links. 272 | # latex_show_urls = False 273 | 274 | # Documents to append as an appendix to all manuals. 275 | # latex_appendices = [] 276 | 277 | # If false, no module index is generated. 278 | # latex_domain_indices = True 279 | 280 | 281 | # -- Options for manual page output --------------------------------------- 282 | 283 | # One entry per manual page. List of tuples 284 | # (source start file, name, description, authors, manual section). 285 | man_pages = [(master_doc, "pygmsh", "pygmsh Documentation", [author], 1)] 286 | 287 | # If true, show URL addresses after external links. 288 | # man_show_urls = False 289 | 290 | 291 | # -- Options for Texinfo output ------------------------------------------- 292 | 293 | # Grouping the document tree into Texinfo files. List of tuples 294 | # (source start file, target name, title, author, 295 | # dir menu entry, description, category) 296 | texinfo_documents = [ 297 | ( 298 | master_doc, 299 | "pygmsh", 300 | "pygmsh Documentation", 301 | author, 302 | "pygmsh", 303 | "Python interface for Gmsh", 304 | "Miscellaneous", 305 | ) 306 | ] 307 | 308 | # Documents to append as an appendix to all manuals. 309 | # texinfo_appendices = [] 310 | 311 | # If false, no module index is generated. 312 | # texinfo_domain_indices = True 313 | 314 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 315 | # texinfo_show_urls = 'footnote' 316 | 317 | # If true, do not generate a @detailmenu in the "Top" node's menu. 318 | # texinfo_no_detailmenu = False 319 | -------------------------------------------------------------------------------- /doc/geo.rst: -------------------------------------------------------------------------------- 1 | Built-in Engine 2 | =============== 3 | 4 | The default Gmsh kernel with basic geometry construction functions. 5 | For advanced geometries it is recommended to use the openCASCADE kernel. 6 | 7 | .. automodule:: pygmsh.geo 8 | 9 | Geometry 10 | -------- 11 | .. automodule:: pygmsh.geo.geometry 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pygmsh documentation master file, created by 2 | sphinx-quickstart on Tue Oct 27 19:56:53 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pygmsh's documentation! 7 | ================================== 8 | 9 | This class provides a Python interface for the Gmsh scripting language. It aims 10 | at working around some of Gmsh's inconveniences (e.g., having to manually 11 | assign an ID for every entity created) and providing access to Python's 12 | features. 13 | 14 | In Gmsh, the user must manually provide a unique ID for every point, curve, 15 | volume created. This can get messy when a lot of entities are created and it 16 | isn't clear which IDs are already in use. Some Gmsh commands even create new 17 | entities and silently reserve IDs in that way. This module tries to work around 18 | this by providing routines in the style of add_point(x) which _return_ the ID. 19 | To make variable names in Gmsh unique, keep track of how many points, circles, 20 | etc. have already been created. Variable names will then be p1, p2, etc. for 21 | points, c1, c2, etc. for circles and so on. 22 | 23 | Geometry Overview 24 | ----------------- 25 | 26 | Gmsh’s geometry module provides a simple CAD engine, using a boundary representation 27 | (“BRep”) approach: you need to first define points (using the Point command: see below), 28 | then lines (using Line, Circle, Spline, …, commands or by extruding points), then surfaces 29 | (using for example the Plane Surface or Surface commands, or by extruding lines), 30 | and finally volumes (using the Volume command or by extruding surfaces). 31 | 32 | These geometrical entities are called “elementary” in Gmsh’s jargon, and 33 | are assigned identification numbers (stricly positive) when they are created: 34 | 35 | 1. Each elementary point must possess a unique identification number; 36 | 2. Each elementary line must possess a unique identification number; 37 | 3. Each elementary surface must possess a unique identification number; 38 | 4. Each elementary volume must possess a unique identification number. 39 | 40 | Elementary geometrical entities can then be manipulated in various ways, for 41 | example using the Translate, Rotate, Scale or Symmetry commands. 42 | They can be deleted with the Delete command, provided that no 43 | higher-dimension entity references them. Zero or negative identification 44 | numbers are reserved by the system for special uses: do not use them in your scripts. 45 | 46 | Groups of elementary geometrical entities can also be defined and are called 47 | “physical” entities. These physical entities cannot be modified by geometry 48 | commands: their only purpose is to assemble elementary entities into larger 49 | groups so that they can be referred to by the mesh module as single entities. 50 | As is the case with elementary entities, each physical point, physical line, 51 | physical surface or physical volume must be assigned a unique identification number. 52 | 53 | Contents: 54 | 55 | .. toctree:: 56 | :maxdepth: 1 57 | :caption: Table of Contents 58 | 59 | common 60 | geo 61 | occ 62 | -------------------------------------------------------------------------------- /doc/occ.rst: -------------------------------------------------------------------------------- 1 | openCASCADE Engine 2 | ================== 3 | 4 | Using the openCASCADE kernel instead of the built-in geometry kernel. Models 5 | can be built using constructive solid geometry, allowing for 2D and 3D polygon 6 | boolean operations. 7 | 8 | .. automodule:: pygmsh.occ 9 | 10 | Geometry 11 | -------- 12 | .. automodule:: pygmsh.occ.geometry 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | Ball 18 | ---- 19 | .. automodule:: pygmsh.occ.ball 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | Box 25 | --- 26 | .. automodule:: pygmsh.occ.box 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | Cone 32 | ---- 33 | .. automodule:: pygmsh.occ.cone 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | Cylinder 39 | -------- 40 | .. automodule:: pygmsh.occ.cylinder 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | Disk 46 | ---- 47 | .. automodule:: pygmsh.occ.disk 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | Rectangle 53 | --------- 54 | .. automodule:: pygmsh.occ.rectangle 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | Torus 60 | ----- 61 | .. automodule:: pygmsh.occ.torus 62 | :members: 63 | :undoc-members: 64 | :show-inheritance: 65 | 66 | Wedge 67 | ----- 68 | .. automodule:: pygmsh.occ.wedge 69 | :members: 70 | :undoc-members: 71 | :show-inheritance: 72 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | numpy 3 | sphinxcontrib-napoleon 4 | sphinx-autodoc-typehints 5 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | version := `python3 -c "from src.pygmsh.__about__ import __version__; print(__version__)"` 2 | 3 | default: 4 | @echo "\"just publish\"?" 5 | 6 | publish: 7 | @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi 8 | gh release create "v{{version}}" 9 | flit publish 10 | 11 | clean: 12 | @find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf 13 | @rm -rf src/*.egg-info/ build/ dist/ .tox/ 14 | 15 | format: 16 | isort . 17 | black . 18 | blacken-docs README.md 19 | 20 | lint: 21 | black --check . 22 | flake8 . 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.isort] 6 | profile = "black" 7 | 8 | [project] 9 | name = "pygmsh" 10 | authors = [{name = "Nico Schlömer", email = "nico.schloemer@gmail.com"}] 11 | description = "Python frontend for Gmsh" 12 | readme = "README.md" 13 | license = {file = "LICENSE.txt"} 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Science/Research", 17 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Topic :: Scientific/Engineering", 26 | "Topic :: Scientific/Engineering :: Mathematics", 27 | "Topic :: Utilities", 28 | ] 29 | dynamic = ["version"] 30 | requires-python = ">=3.7" 31 | dependencies = [ 32 | "gmsh", 33 | "meshio >= 4.3.2, <6", 34 | "numpy >= 1.20.0", 35 | ] 36 | keywords = ["mesh", "gmsh", "mesh generation", "mathematics", "engineering"] 37 | 38 | [project.urls] 39 | Code = "https://github.com/nschloe/pygmsh" 40 | Documentation = "https://pygmsh.readthedocs.io/en/latest" 41 | Funding = "https://github.com/sponsors/nschloe" 42 | Issues = "https://github.com/nschloe/pygmsh/issues" 43 | 44 | [project.scripts] 45 | pygmsh-optimize = "pygmsh._cli:optimize_cli" 46 | -------------------------------------------------------------------------------- /src/pygmsh/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "7.1.17" 2 | -------------------------------------------------------------------------------- /src/pygmsh/__init__.py: -------------------------------------------------------------------------------- 1 | from . import geo, occ 2 | from .__about__ import __version__ 3 | from ._optimize import optimize 4 | from .helpers import orient_lines, rotation_matrix, write 5 | 6 | __all__ = [ 7 | "geo", 8 | "occ", 9 | "rotation_matrix", 10 | "orient_lines", 11 | "write", 12 | "optimize", 13 | "__version__", 14 | ] 15 | -------------------------------------------------------------------------------- /src/pygmsh/_cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from sys import version_info 3 | 4 | import meshio 5 | 6 | from .__about__ import __version__ 7 | from ._optimize import optimize 8 | 9 | 10 | def optimize_cli(argv=None): 11 | parser = argparse.ArgumentParser( 12 | description=("Optimize mesh."), 13 | formatter_class=argparse.RawTextHelpFormatter, 14 | ) 15 | 16 | parser.add_argument("infile", type=str, help="mesh to optimize") 17 | parser.add_argument("outfile", type=str, help="optimized mesh") 18 | 19 | parser.add_argument( 20 | "-q", 21 | "--quiet", 22 | dest="verbose", 23 | action="store_false", 24 | default=True, 25 | help="suppress output", 26 | ) 27 | 28 | parser.add_argument( 29 | "-m", 30 | "--method", 31 | default="", 32 | # Valid choices are on 33 | # https://gmsh.info/doc/texinfo/gmsh.html#Namespace-gmsh_002fmodel_002fmesh 34 | help='method (e.g., "", Netgen, ...)', 35 | ) 36 | 37 | parser.add_argument( 38 | "-v", 39 | "--version", 40 | action="version", 41 | version=_get_version_text(), 42 | help="display version information", 43 | ) 44 | args = parser.parse_args(argv) 45 | 46 | mesh = meshio.read(args.infile) 47 | optimize(mesh, method=args.method, verbose=args.verbose).write(args.outfile) 48 | 49 | 50 | def _get_version_text(): 51 | try: 52 | # Python 3.8 53 | from importlib import metadata 54 | 55 | __gmsh_version__ = metadata.version("gmsh") 56 | except Exception: 57 | __gmsh_version__ = "unknown" 58 | 59 | return "\n".join( 60 | [ 61 | f"pygmsh {__version__} " 62 | f"[Gmsh {__gmsh_version__}, " 63 | f"Python {version_info.major}.{version_info.minor}.{version_info.micro}]", 64 | "Copyright (c) 2013-2022 Nico Schlömer et al.", 65 | ] 66 | ) 67 | -------------------------------------------------------------------------------- /src/pygmsh/_optimize.py: -------------------------------------------------------------------------------- 1 | import gmsh 2 | import meshio 3 | import numpy as np 4 | 5 | from .helpers import extract_to_meshio 6 | 7 | 8 | def optimize(mesh, method="", verbose=False): 9 | # mesh.remove_lower_dimensional_cells() 10 | mesh.cell_data = {} 11 | 12 | # read into meshio like 13 | # 14 | gmsh.initialize() 15 | # add dummy entity 16 | dim = 3 17 | tag = gmsh.model.addDiscreteEntity(dim=dim) 18 | # 19 | nodes = np.arange(1, len(mesh.points) + 1) 20 | assert mesh.points.shape[1] == 3 21 | gmsh.model.mesh.addNodes(dim, tag, nodes, mesh.points.flat) 22 | for cell_block in mesh.cells: 23 | gmsh.model.mesh.addElementsByType( 24 | tag, 25 | meshio.gmsh.meshio_to_gmsh_type[cell_block.type], 26 | [], 27 | cell_block.data.flatten() + 1, 28 | ) 29 | gmsh.model.mesh.optimize(method, force=True) 30 | mesh = extract_to_meshio() 31 | gmsh.finalize() 32 | 33 | # This writes a temporary file and reads it into gmsh ("merge"). There are other 34 | # ways of feeding gmsh a mesh 35 | # (https://gitlab.onelab.info/gmsh/gmsh/-/issues/1030#note_11435), but let's not do 36 | # that for now. 37 | # with tempfile.TemporaryDirectory() as tmpdirname: 38 | # tmpdir = Path(tmpdirname) 39 | # tmpfile = tmpdir / "tmp.msh" 40 | # mesh.write(tmpfile) 41 | # gmsh.initialize() 42 | # if verbose: 43 | # gmsh.option.setNumber("General.Terminal", 1) 44 | # gmsh.merge(str(tmpfile)) 45 | # # We need force=True because we're reading from a discrete mesh 46 | # gmsh.model.mesh.optimize(method, force=True) 47 | # mesh = extract_to_meshio() 48 | # gmsh.finalize() 49 | return mesh 50 | 51 | 52 | def print_stats(mesh): 53 | import termplotlib 54 | 55 | q = mesh.q_radius_ratio 56 | q_hist, q_bin_edges = np.histogram( 57 | q, bins=np.linspace(0.0, 1.0, num=41, endpoint=True) 58 | ) 59 | 60 | grid = termplotlib.subplot_grid((1, 2), column_widths=None, border_style=None) 61 | grid[0, 0].hist(q_hist, q_bin_edges, bar_width=1, strip=True) 62 | grid[0, 1].aprint(f"min quality: {np.min(q):5.3f}") 63 | grid[0, 1].aprint(f"avg quality: {np.average(q):5.3f}") 64 | grid[0, 1].aprint(f"max quality: {np.max(q):5.3f}") 65 | 66 | grid.show() 67 | -------------------------------------------------------------------------------- /src/pygmsh/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import CommonGeometry 2 | 3 | __all__ = ["CommonGeometry"] 4 | -------------------------------------------------------------------------------- /src/pygmsh/common/bezier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .line_base import LineBase 4 | from .point import Point 5 | 6 | 7 | class Bezier(LineBase): 8 | """ 9 | Creates a B-spline. 10 | 11 | Parameters 12 | ---------- 13 | control_points : Contains the identification numbers of the control points. 14 | """ 15 | 16 | def __init__(self, env, control_points: list[Point]): 17 | for c in control_points: 18 | assert isinstance(c, Point) 19 | assert len(control_points) > 1 20 | 21 | id0 = env.addBezier([c._id for c in control_points]) 22 | super().__init__(id0, control_points) 23 | -------------------------------------------------------------------------------- /src/pygmsh/common/bspline.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .line_base import LineBase 4 | from .point import Point 5 | 6 | 7 | class BSpline(LineBase): 8 | """ 9 | Creates a B-spline. 10 | 11 | Parameters 12 | ---------- 13 | control_points : Contains the identification numbers of the control points. 14 | """ 15 | 16 | def __init__(self, env, control_points: list[Point]): 17 | for c in control_points: 18 | assert isinstance(c, Point) 19 | assert len(control_points) > 1 20 | 21 | id0 = env.addBSpline([c._id for c in control_points]) 22 | super().__init__(id0, control_points) 23 | -------------------------------------------------------------------------------- /src/pygmsh/common/circle_arc.py: -------------------------------------------------------------------------------- 1 | from .line_base import LineBase 2 | from .point import Point 3 | 4 | 5 | class CircleArc(LineBase): 6 | """ 7 | Creates a circle arc. 8 | 9 | Parameters 10 | ---------- 11 | start : Coordinates of start point needed to construct circle-arc. 12 | center : Coordinates of center point needed to construct circle-arc. 13 | end : Coordinates of end point needed to construct circle-arc. 14 | """ 15 | 16 | def __init__(self, env, start: Point, center: Point, end: Point): 17 | assert isinstance(start, Point) 18 | assert isinstance(center, Point) 19 | assert isinstance(end, Point) 20 | id0 = env.addCircleArc(start._id, center._id, end._id) 21 | super().__init__(id0, [start, center, end]) 22 | -------------------------------------------------------------------------------- /src/pygmsh/common/curve_loop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class CurveLoop: 5 | """ 6 | Increments the Line ID every time a new object is created that inherits 7 | from LineBase. 8 | 9 | Parameters 10 | ---------- 11 | curves : Containing the lines defining the shape. 12 | 13 | Notes 14 | ----- 15 | A line loop must be a closed loop, and the elementary lines should be ordered and 16 | oriented (negating to specify reverse orientation). If the orientation is correct, 17 | but the ordering is wrong, Gmsh will actually reorder the list internally to create 18 | a consistent loop. 19 | """ 20 | 21 | dim = 1 22 | 23 | def __init__(self, env, curves: list): 24 | for k in range(len(curves) - 1): 25 | assert curves[k].points[-1] == curves[k + 1].points[0] 26 | assert curves[-1].points[-1] == curves[0].points[0] 27 | self._id = env.addCurveLoop([c._id for c in curves]) 28 | self.dim_tag = (1, self._id) 29 | self.dim_tags = [self.dim_tag] 30 | self.curves = curves 31 | 32 | def __len__(self): 33 | return len(self.curves) 34 | 35 | def __repr__(self): 36 | curves = ", ".join([str(l._id) for l in self.curves]) 37 | return f"" 38 | -------------------------------------------------------------------------------- /src/pygmsh/common/dummy.py: -------------------------------------------------------------------------------- 1 | class Dummy: 2 | def __init__(self, dim, id0): 3 | assert isinstance(id0, int) 4 | self.dim = dim 5 | self.id = id0 6 | self._id = id0 7 | self.dim_tag = (dim, id0) 8 | self.dim_tags = [self.dim_tag] 9 | 10 | def __repr__(self): 11 | return f"" 12 | -------------------------------------------------------------------------------- /src/pygmsh/common/ellipse_arc.py: -------------------------------------------------------------------------------- 1 | from .line_base import LineBase 2 | from .point import Point 3 | 4 | 5 | class EllipseArc(LineBase): 6 | """ 7 | Creates an ellipse arc. 8 | 9 | Parameters 10 | ---------- 11 | start : Coordinates of start point needed to construct elliptic arc. 12 | center : Coordinates of center point needed to construct elliptic arc. 13 | point_on_major_axis : Point on the center axis of ellipse. 14 | end : Coordinates of end point needed to construct elliptic arc. 15 | """ 16 | 17 | def __init__( 18 | self, env, start: Point, center: Point, point_on_major_axis: Point, end: Point 19 | ): 20 | assert isinstance(start, Point) 21 | assert isinstance(center, Point) 22 | assert isinstance(point_on_major_axis, Point) 23 | assert isinstance(end, Point) 24 | 25 | id0 = env.addEllipseArc(start._id, center._id, point_on_major_axis._id, end._id) 26 | super().__init__(id0, [start, center, end]) 27 | 28 | self.points = [start, center, end] 29 | self.point_on_major_axis = point_on_major_axis 30 | 31 | def __repr__(self): 32 | pts = ", ".join(str(p._id) for p in self.points) 33 | return f"" 34 | -------------------------------------------------------------------------------- /src/pygmsh/common/geometry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | import gmsh 6 | 7 | from ..helpers import extract_to_meshio 8 | from .bezier import Bezier 9 | from .bspline import BSpline 10 | from .circle_arc import CircleArc 11 | from .curve_loop import CurveLoop 12 | from .dummy import Dummy 13 | from .ellipse_arc import EllipseArc 14 | from .line import Line 15 | from .plane_surface import PlaneSurface 16 | from .point import Point 17 | from .polygon import Polygon 18 | from .size_field import BoundaryLayer, SetBackgroundMesh 19 | from .spline import Spline 20 | from .surface import Surface 21 | from .surface_loop import SurfaceLoop 22 | from .volume import Volume 23 | 24 | 25 | class CommonGeometry: 26 | """Geometry base class containing all methods that can be shared between built-in 27 | and occ. 28 | """ 29 | 30 | def __init__(self, env, init_argv=None): 31 | self.env = env 32 | self.init_argv = init_argv 33 | self._COMPOUND_ENTITIES = [] 34 | self._RECOMBINE_ENTITIES = [] 35 | self._EMBED_QUEUE = [] 36 | self._TRANSFINITE_CURVE_QUEUE = [] 37 | self._TRANSFINITE_SURFACE_QUEUE = [] 38 | self._TRANSFINITE_VOLUME_QUEUE = [] 39 | self._AFTER_SYNC_QUEUE = [] 40 | self._SIZE_QUEUE = [] 41 | self._PHYSICAL_QUEUE = [] 42 | self._OUTWARD_NORMALS = [] 43 | 44 | def __enter__(self): 45 | gmsh.initialize([] if self.init_argv is None else self.init_argv) 46 | gmsh.model.add("pygmsh model") 47 | return self 48 | 49 | def __exit__(self, *_): 50 | try: 51 | # Gmsh >= 4.7.0 52 | # https://gitlab.onelab.info/gmsh/gmsh/-/issues/1036 53 | gmsh.model.mesh.removeSizeCallback() 54 | except AttributeError: 55 | pass 56 | gmsh.finalize() 57 | 58 | def synchronize(self): 59 | self.env.synchronize() 60 | 61 | def __repr__(self): 62 | return "" 63 | 64 | def add_bspline(self, *args, **kwargs): 65 | return BSpline(self.env, *args, **kwargs) 66 | 67 | def add_bezier(self, *args, **kwargs): 68 | return Bezier(self.env, *args, **kwargs) 69 | 70 | def add_circle_arc(self, *args, **kwargs): 71 | return CircleArc(self.env, *args, **kwargs) 72 | 73 | def add_ellipse_arc(self, *args, **kwargs): 74 | return EllipseArc(self.env, *args, **kwargs) 75 | 76 | def add_line(self, *args, **kwargs): 77 | return Line(self.env, *args, **kwargs) 78 | 79 | def add_curve_loop(self, *args, **kwargs): 80 | return CurveLoop(self.env, *args, **kwargs) 81 | 82 | def add_plane_surface(self, *args, **kwargs): 83 | return PlaneSurface(self.env, *args, **kwargs) 84 | 85 | def add_point(self, *args, **kwargs): 86 | return Point(self.env, *args, **kwargs) 87 | 88 | def add_spline(self, *args, **kwargs): 89 | return Spline(self.env, *args, **kwargs) 90 | 91 | def add_surface(self, *args, **kwargs): 92 | return Surface(self.env, *args, **kwargs) 93 | 94 | def add_surface_loop(self, *args, **kwargs): 95 | return SurfaceLoop(self.env, *args, **kwargs) 96 | 97 | def add_volume(self, *args, **kwargs): 98 | return Volume(self.env, *args, **kwargs) 99 | 100 | def add_polygon(self, *args, **kwargs): 101 | return Polygon(self, *args, **kwargs) 102 | 103 | def add_physical(self, entities, label: str | None = None): 104 | if label in [label for _, label in self._PHYSICAL_QUEUE]: 105 | raise ValueError(f'Label "{label}" already exists.') 106 | 107 | if not isinstance(entities, list): 108 | entities = [entities] 109 | 110 | # make sure the dimensionality is the same for all entities 111 | dim = entities[0].dim 112 | for e in entities: 113 | assert e.dim == dim 114 | 115 | if label is None: 116 | # 2021-02-18 117 | warnings.warn( 118 | "Physical groups without label are deprecated. " 119 | 'Use add_physical(entities, "dummy").' 120 | ) 121 | else: 122 | if not isinstance(label, str): 123 | raise ValueError(f"Physical label must be string, not {type(label)}.") 124 | 125 | self._PHYSICAL_QUEUE.append((entities, label)) 126 | 127 | def set_transfinite_curve( 128 | self, curve, num_nodes: int, mesh_type: str, coeff: float 129 | ): 130 | assert mesh_type in ["Progression", "Bump", "Beta"] 131 | self._TRANSFINITE_CURVE_QUEUE.append((curve._id, num_nodes, mesh_type, coeff)) 132 | 133 | def set_transfinite_surface(self, surface, arrangement: str, corner_pts): 134 | corner_tags = [pt._id for pt in corner_pts] 135 | self._TRANSFINITE_SURFACE_QUEUE.append((surface._id, arrangement, corner_tags)) 136 | 137 | def set_transfinite_volume(self, volume, corner_pts): 138 | corner_tags = [pt._id for pt in corner_pts] 139 | self._TRANSFINITE_VOLUME_QUEUE.append((volume._id, corner_tags)) 140 | 141 | def set_recombined_surfaces(self, surfaces): 142 | for i, surface in enumerate(surfaces): 143 | assert surface.dim == 2, f"item {i} is not a surface" 144 | self._RECOMBINE_ENTITIES += [s.dim_tags[0] for s in surfaces] 145 | 146 | def extrude( 147 | self, 148 | input_entity, 149 | translation_axis: tuple[float, float, float], 150 | num_layers: int | list[int] | None = None, 151 | heights: list[float] | None = None, 152 | recombine: bool = False, 153 | ): 154 | """Extrusion of any entity along a given translation_axis.""" 155 | if isinstance(num_layers, int): 156 | num_layers = [num_layers] 157 | if num_layers is None: 158 | num_layers = [] 159 | assert heights is None 160 | heights = [] 161 | else: 162 | if heights is None: 163 | heights = [] 164 | else: 165 | assert len(num_layers) == len(heights) 166 | 167 | assert len(translation_axis) == 3 168 | 169 | ie_list = input_entity if isinstance(input_entity, list) else [input_entity] 170 | 171 | out_dim_tags = self.env.extrude( 172 | [e.dim_tag for e in ie_list], 173 | *translation_axis, 174 | numElements=num_layers, 175 | heights=heights, 176 | recombine=recombine, 177 | ) 178 | top = Dummy(*out_dim_tags[0]) 179 | extruded = Dummy(*out_dim_tags[1]) 180 | lateral = [Dummy(*e) for e in out_dim_tags[2:]] 181 | return top, extruded, lateral 182 | 183 | def _revolve( 184 | self, 185 | input_entity, 186 | rotation_axis: tuple[float, float, float], 187 | point_on_axis: tuple[float, float, float], 188 | angle: float, 189 | num_layers: int | list[int] | None = None, 190 | heights: list[float] | None = None, 191 | recombine: bool = False, 192 | ): 193 | """Rotation of any entity around a given rotation_axis, about a given angle.""" 194 | if isinstance(num_layers, int): 195 | num_layers = [num_layers] 196 | if num_layers is None: 197 | num_layers = [] 198 | heights = [] 199 | else: 200 | if heights is None: 201 | heights = [] 202 | else: 203 | assert len(num_layers) == len(heights) 204 | 205 | assert len(point_on_axis) == 3 206 | assert len(rotation_axis) == 3 207 | out_dim_tags = self.env.revolve( 208 | input_entity.dim_tags, 209 | *point_on_axis, 210 | *rotation_axis, 211 | angle, 212 | numElements=num_layers, 213 | heights=heights, 214 | recombine=recombine, 215 | ) 216 | 217 | top = Dummy(*out_dim_tags[0]) 218 | extruded = Dummy(*out_dim_tags[1]) 219 | lateral = [Dummy(*e) for e in out_dim_tags[2:]] 220 | return top, extruded, lateral 221 | 222 | def translate(self, obj, vector: tuple[float, float, float]): 223 | """Translates input_entity itself by vector. 224 | 225 | Changes the input object. 226 | """ 227 | self.env.translate(obj.dim_tags, *vector) 228 | 229 | def rotate( 230 | self, 231 | obj, 232 | point: tuple[float, float, float], 233 | angle: float, 234 | axis: tuple[float, float, float], 235 | ): 236 | """Rotate input_entity around a given point with a given angle. 237 | Rotation axis has to be specified. 238 | 239 | Changes the input object. 240 | """ 241 | self.env.rotate(obj.dim_tags, *point, *axis, angle) 242 | 243 | def copy(self, obj): 244 | dim_tag = self.env.copy(obj.dim_tags) 245 | assert len(dim_tag) == 1 246 | return Dummy(*dim_tag[0]) 247 | 248 | def symmetrize(self, obj, coefficients: tuple[float, float, float, float]): 249 | """Transforms all elementary entities symmetrically to a plane. The vector 250 | should contain four expressions giving the coefficients of the plane's equation. 251 | """ 252 | self.env.symmetrize(obj.dim_tags, *coefficients) 253 | 254 | def dilate( 255 | self, obj, x0: tuple[float, float, float], abc: tuple[float, float, float] 256 | ): 257 | self.env.dilate(obj.dim_tags, *x0, *abc) 258 | 259 | def mirror(self, obj, abcd: tuple[float, float, float, float]): 260 | self.env.mirror(obj.dim_tags, *abcd) 261 | 262 | def remove(self, obj, recursive: bool = False): 263 | self.env.remove(obj.dim_tags, recursive=recursive) 264 | 265 | def in_surface(self, input_entity, surface): 266 | """Embed the point(s) or curve(s) in the given surface. The surface mesh will 267 | conform to the mesh of the point(s) or curves(s). 268 | """ 269 | self._EMBED_QUEUE.append((input_entity, surface)) 270 | 271 | def in_volume(self, input_entity, volume): 272 | """Embed the point(s)/curve(s)/surface(s) in the given volume. The volume mesh 273 | will conform to the mesh of the input entities. 274 | """ 275 | self._EMBED_QUEUE.append((input_entity, volume)) 276 | 277 | def set_mesh_size_callback(self, fun, ignore_other_mesh_sizes=True): 278 | gmsh.model.mesh.setSizeCallback(fun) 279 | # 280 | # If a mesh size is set from a function, ignore the mesh sizes from the 281 | # entities. 282 | # 283 | # From : 284 | # ``` 285 | # To determine the size of mesh elements, Gmsh locally computes the minimum of 286 | # 287 | # 1) the size of the model bounding box; 288 | # 2) if `Mesh.CharacteristicLengthFromPoints' is set, the mesh size specified at 289 | # geometrical points; 290 | # 3) if `Mesh.CharacteristicLengthFromCurvature' is set, the mesh size based on 291 | # the curvature and `Mesh.MinimumElementsPerTwoPi'; 292 | # 4) the background mesh field; 293 | # 5) any per-entity mesh size constraint. 294 | # 295 | # This value is then constrained in the interval 296 | # [`Mesh.CharacteristicLengthMin', `Mesh.CharacteristicLengthMax'] and 297 | # multiplied by `Mesh.CharacteristicLengthFactor'. In addition, boundary mesh 298 | # sizes (on curves or surfaces) are interpolated inside the enclosed entity 299 | # (surface or volume, respectively) if the option 300 | # `Mesh.CharacteristicLengthExtendFromBoundary' is set (which is the case by 301 | # default). 302 | # ``` 303 | if ignore_other_mesh_sizes: 304 | gmsh.option.setNumber("Mesh.CharacteristicLengthExtendFromBoundary", 0) 305 | gmsh.option.setNumber("Mesh.CharacteristicLengthFromPoints", 0) 306 | gmsh.option.setNumber("Mesh.CharacteristicLengthFromCurvature", 0) 307 | 308 | def add_boundary_layer(self, *args, **kwargs): 309 | layer = BoundaryLayer(*args, **kwargs) 310 | self._AFTER_SYNC_QUEUE.append(layer) 311 | return layer 312 | 313 | def set_background_mesh(self, *args, **kwargs): 314 | setter = SetBackgroundMesh(*args, **kwargs) 315 | self._AFTER_SYNC_QUEUE.append(setter) 316 | 317 | def generate_mesh( # noqa: C901 318 | self, 319 | dim: int = 3, 320 | order: int | None = None, 321 | # http://gmsh.info/doc/texinfo/gmsh.html#index-Mesh_002eAlgorithm 322 | algorithm: int | None = None, 323 | verbose: bool = False, 324 | ): 325 | """Return a meshio.Mesh, storing the mesh points, cells, and data, generated by 326 | Gmsh from the `self`. 327 | """ 328 | self.synchronize() 329 | 330 | for item in self._AFTER_SYNC_QUEUE: 331 | item.exec() 332 | 333 | for item, host in self._EMBED_QUEUE: 334 | gmsh.model.mesh.embed(item.dim, [item._id], host.dim, host._id) 335 | 336 | # set compound entities after sync 337 | for c in self._COMPOUND_ENTITIES: 338 | gmsh.model.mesh.setCompound(*c) 339 | 340 | for s in self._RECOMBINE_ENTITIES: 341 | gmsh.model.mesh.setRecombine(*s) 342 | 343 | for t in self._TRANSFINITE_CURVE_QUEUE: 344 | gmsh.model.mesh.setTransfiniteCurve(*t) 345 | 346 | for t in self._TRANSFINITE_SURFACE_QUEUE: 347 | gmsh.model.mesh.setTransfiniteSurface(*t) 348 | 349 | for e in self._TRANSFINITE_VOLUME_QUEUE: 350 | gmsh.model.mesh.setTransfiniteVolume(*e) 351 | 352 | for item, size in self._SIZE_QUEUE: 353 | gmsh.model.mesh.setSize( 354 | gmsh.model.getBoundary(item.dim_tags, False, False, True), size 355 | ) 356 | 357 | for entities, label in self._PHYSICAL_QUEUE: 358 | d = entities[0].dim 359 | assert all(e.dim == d for e in entities) 360 | tag = gmsh.model.addPhysicalGroup(d, [e._id for e in entities]) 361 | if label is not None: 362 | gmsh.model.setPhysicalName(d, tag, label) 363 | 364 | for entity in self._OUTWARD_NORMALS: 365 | gmsh.model.mesh.setOutwardOrientation(entity.id) 366 | 367 | gmsh.option.setNumber("General.Terminal", 1 if verbose else 0) 368 | 369 | # set algorithm 370 | # http://gmsh.info/doc/texinfo/gmsh.html#index-Mesh_002eAlgorithm 371 | if algorithm: 372 | gmsh.option.setNumber("Mesh.Algorithm", algorithm) 373 | 374 | gmsh.model.mesh.generate(dim) 375 | 376 | # setOrder() after generate(), see 377 | # 378 | if order is not None: 379 | gmsh.model.mesh.setOrder(order) 380 | 381 | return extract_to_meshio() 382 | 383 | def save_geometry(self, filename: str): 384 | # filename is typically a geo_unrolled or brep file 385 | self.synchronize() 386 | gmsh.write(filename) 387 | -------------------------------------------------------------------------------- /src/pygmsh/common/line.py: -------------------------------------------------------------------------------- 1 | from .line_base import LineBase 2 | from .point import Point 3 | 4 | 5 | class Line(LineBase): 6 | """ 7 | Creates a straight line segment. 8 | 9 | Parameters 10 | ---------- 11 | p0 : Point object that represents the start of the line. 12 | p1 : Point object that represents the end of the line. 13 | 14 | Attributes 15 | ---------- 16 | points : array-like[1][2] 17 | List containing the begin and end points of the line. 18 | """ 19 | 20 | dim = 1 21 | 22 | def __init__(self, env, p0: Point, p1: Point): 23 | assert isinstance(p0, Point) 24 | assert isinstance(p1, Point) 25 | id0 = env.addLine(p0._id, p1._id) 26 | self.dim_tag = (1, id0) 27 | self.dim_tags = [self.dim_tag] 28 | super().__init__(id0, [p0, p1]) 29 | 30 | def __repr__(self): 31 | pts = ", ".join(str(p._id) for p in self.points) 32 | return f"" 33 | -------------------------------------------------------------------------------- /src/pygmsh/common/line_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | 5 | 6 | class LineBase: 7 | dim = 1 8 | 9 | def __init__(self, id0: int, points: list[int]): 10 | self._id = id0 11 | self.dim_tag = (1, self._id) 12 | self.dim_tags = [self.dim_tag] 13 | self.points = points 14 | 15 | def __neg__(self): 16 | neg_self = copy.deepcopy(self) 17 | neg_self._id = -self._id 18 | neg_self.points = self.points[::-1] 19 | return neg_self 20 | -------------------------------------------------------------------------------- /src/pygmsh/common/plane_surface.py: -------------------------------------------------------------------------------- 1 | from .curve_loop import CurveLoop 2 | 3 | 4 | class PlaneSurface: 5 | """ 6 | Creates a plane surface. 7 | 8 | Parameters 9 | ---------- 10 | curve_loop : Object 11 | Each unique line in the line loop will be used for the surface construction. 12 | holes : list 13 | List of line loops that represents polygon holes. 14 | 15 | Notes 16 | ----- 17 | The first line loop defines the exterior boundary of the surface; all other line 18 | loops define holes in the surface. 19 | 20 | A line loop defining a hole should not have any lines in common with the exterior 21 | line loop (in which case it is not a hole, and the two surfaces should be defined 22 | separately). 23 | 24 | Likewise, a line loop defining a hole should not have any lines in common with 25 | another line loop defining a hole in the same surface (in which case the two line 26 | loops should be combined). 27 | """ 28 | 29 | dim = 2 30 | 31 | def __init__(self, env, curve_loop, holes=None): 32 | assert isinstance(curve_loop, CurveLoop) 33 | self.curve_loop = curve_loop 34 | 35 | if holes is None: 36 | holes = [] 37 | 38 | # The input holes are either line loops or entities that contain line loops 39 | # (like polygons). 40 | self.holes = [h if isinstance(h, CurveLoop) else h.curve_loop for h in holes] 41 | 42 | self.num_edges = len(self.curve_loop) + sum(len(h) for h in self.holes) 43 | 44 | curve_loops = [self.curve_loop] + self.holes 45 | self._id = env.addPlaneSurface([ll._id for ll in curve_loops]) 46 | self.dim_tag = (2, self._id) 47 | self.dim_tags = [self.dim_tag] 48 | 49 | def __repr__(self): 50 | return ( 51 | "" 53 | ) 54 | -------------------------------------------------------------------------------- /src/pygmsh/common/point.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Point: 5 | """ 6 | Creates an elementary point. 7 | 8 | Parameters 9 | ---------- 10 | x : Give the coordinates X, Y (and Z) of the point in the three-dimensional 11 | Euclidean space. 12 | mesh_size : The prescribed mesh element size at this point. 13 | 14 | 15 | Attributes 16 | ---------- 17 | x : array-like 18 | Point coordinates. 19 | """ 20 | 21 | dim = 0 22 | 23 | def __init__( 24 | self, 25 | env, 26 | x: tuple[float, float] | tuple[float, float, float], 27 | mesh_size: float | None = None, 28 | ): 29 | if len(x) == 2: 30 | x = (x[0], x[1], 0.0) 31 | 32 | assert len(x) == 3 33 | self.x = x 34 | args = list(x) 35 | if mesh_size is not None: 36 | args.append(mesh_size) 37 | self._id = env.addPoint(*args) 38 | self.dim_tag = (0, self._id) 39 | self.dim_tags = [self.dim_tag] 40 | 41 | def __repr__(self): 42 | X = ", ".join(str(x) for x in self.x) 43 | return f"" 44 | -------------------------------------------------------------------------------- /src/pygmsh/common/polygon.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from numpy.typing import ArrayLike 5 | 6 | 7 | class Polygon: 8 | dim = 2 9 | 10 | def __init__( 11 | self, 12 | host, 13 | points: ArrayLike, 14 | mesh_size: float | list[float | None] | None = None, 15 | holes=None, 16 | make_surface: bool = True, 17 | ): 18 | if holes is None: 19 | holes = [] 20 | else: 21 | assert make_surface 22 | 23 | points = np.asarray(points) 24 | 25 | if isinstance(mesh_size, list): 26 | assert len(points) == len(mesh_size) 27 | else: 28 | mesh_size = len(points) * [mesh_size] 29 | 30 | if points.shape[1] == 2: 31 | points = np.column_stack([points, np.zeros_like(points[:, 0])]) 32 | 33 | # Create points. 34 | self.points = [ 35 | host.add_point(x, mesh_size=size) for x, size in zip(points, mesh_size) 36 | ] 37 | # Create lines 38 | self.curves = [ 39 | host.add_line(self.points[k], self.points[k + 1]) 40 | for k in range(len(self.points) - 1) 41 | ] + [host.add_line(self.points[-1], self.points[0])] 42 | 43 | self.lines = self.curves 44 | 45 | self.curve_loop = host.add_curve_loop(self.curves) 46 | # self.surface = host.add_plane_surface(ll, holes) if make_surface else None 47 | if make_surface: 48 | self.surface = host.add_plane_surface(self.curve_loop, holes) 49 | self.dim_tag = self.surface.dim_tag 50 | self.dim_tags = self.surface.dim_tags 51 | self._id = self.surface._id 52 | 53 | def __repr__(self): 54 | return "" 55 | -------------------------------------------------------------------------------- /src/pygmsh/common/size_field.py: -------------------------------------------------------------------------------- 1 | import gmsh 2 | 3 | 4 | class BoundaryLayer: 5 | def __init__( 6 | self, 7 | lcmin, 8 | lcmax, 9 | distmin, 10 | distmax, 11 | edges_list=None, 12 | faces_list=None, 13 | nodes_list=None, 14 | num_points_per_curve=None, 15 | ): 16 | self.lcmin = lcmin 17 | self.lcmax = lcmax 18 | self.distmin = distmin 19 | self.distmax = distmax 20 | # Don't use [] as default argument, cf. 21 | # 22 | self.edges_list = edges_list or [] 23 | self.faces_list = faces_list or [] 24 | self.nodes_list = nodes_list or [] 25 | self.num_points_per_curve = num_points_per_curve 26 | 27 | def exec(self): 28 | tag1 = gmsh.model.mesh.field.add("Distance") 29 | 30 | if self.edges_list: 31 | gmsh.model.mesh.field.setNumbers( 32 | tag1, "EdgesList", [e._id for e in self.edges_list] 33 | ) 34 | # edge nodes must be specified, too, cf. 35 | # 36 | # nodes = list(set([p for e in self.edges_list for p in e.points])) 37 | # gmsh.model.mesh.field.setNumbers(tag1, "NodesList", [n._id for n in nodes]) 38 | if self.faces_list: 39 | gmsh.model.mesh.field.setNumbers( 40 | tag1, "FacesList", [f._id for f in self.faces_list] 41 | ) 42 | if self.nodes_list: 43 | gmsh.model.mesh.field.setNumbers( 44 | tag1, "NodesList", [n._id for n in self.nodes_list] 45 | ) 46 | if self.num_points_per_curve: 47 | gmsh.model.mesh.field.setNumber( 48 | tag1, "NumPointsPerCurve", self.num_points_per_curve 49 | ) 50 | 51 | tag2 = gmsh.model.mesh.field.add("Threshold") 52 | gmsh.model.mesh.field.setNumber(tag2, "IField", tag1) 53 | gmsh.model.mesh.field.setNumber(tag2, "LcMin", self.lcmin) 54 | gmsh.model.mesh.field.setNumber(tag2, "LcMax", self.lcmax) 55 | gmsh.model.mesh.field.setNumber(tag2, "DistMin", self.distmin) 56 | gmsh.model.mesh.field.setNumber(tag2, "DistMax", self.distmax) 57 | self.tag = tag2 58 | 59 | 60 | class SetBackgroundMesh: 61 | def __init__(self, fields, operator): 62 | self.fields = fields 63 | self.operator = operator 64 | 65 | def exec(self): 66 | tag = gmsh.model.mesh.field.add(self.operator) 67 | gmsh.model.mesh.field.setNumbers( 68 | tag, "FieldsList", [f.tag for f in self.fields] 69 | ) 70 | gmsh.model.mesh.field.setAsBackgroundMesh(tag) 71 | -------------------------------------------------------------------------------- /src/pygmsh/common/spline.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .line_base import LineBase 4 | from .point import Point 5 | 6 | 7 | class Spline(LineBase): 8 | """ 9 | With the built-in geometry kernel this constructs a Catmull-Rom spline. 10 | 11 | Parameters 12 | ---------- 13 | points : List containing Point objects 14 | """ 15 | 16 | def __init__(self, env, points: list[Point]): 17 | for c in points: 18 | assert isinstance(c, Point) 19 | assert len(points) > 1 20 | 21 | id0 = env.addSpline([c._id for c in points]) 22 | super().__init__(id0, points) 23 | -------------------------------------------------------------------------------- /src/pygmsh/common/surface.py: -------------------------------------------------------------------------------- 1 | from .curve_loop import CurveLoop 2 | 3 | 4 | class Surface: 5 | """ 6 | Generates a Surface from a CurveLoop. 7 | 8 | Parameters 9 | ---------- 10 | curve_loop : Object 11 | CurveLoop object that contains all the Line objects for the loop construction. 12 | 13 | Notes 14 | ----- 15 | With the built-in kernel, the first line loop should be composed of either three or 16 | four elementary lines. 17 | 18 | With the built-in kernel, the optional In Sphere argument forces the surface to be a 19 | spherical patch (the extra parameter gives the identification number of the center 20 | of the sphere). 21 | """ 22 | 23 | dim = 2 24 | 25 | def __init__(self, env, curve_loop): 26 | assert isinstance(curve_loop, CurveLoop) 27 | self.curve_loop = curve_loop 28 | self.num_edges = len(curve_loop) 29 | self._id = env.addSurfaceFilling([self.curve_loop._id]) 30 | self.dim_tag = (2, self._id) 31 | self.dim_tags = [self.dim_tag] 32 | 33 | def __repr__(self): 34 | return f"" 35 | -------------------------------------------------------------------------------- /src/pygmsh/common/surface_loop.py: -------------------------------------------------------------------------------- 1 | class SurfaceLoop: 2 | """ 3 | Creates a surface loop (a shell). 4 | 5 | Parameters 6 | ---------- 7 | surfaces : list 8 | Contain the identification numbers of all the elementary surfaces that 9 | constitute the surface loop. 10 | 11 | Notes 12 | ----- 13 | A surface loop must always represent a closed shell, and the elementary surfaces 14 | should be oriented consistently (using negative identification numbers to specify 15 | reverse orientation). 16 | """ 17 | 18 | dim = 2 19 | 20 | def __init__(self, env, surfaces): 21 | self.surfaces = surfaces 22 | self._id = env.addSurfaceLoop([s._id for s in surfaces]) 23 | self.dim_tag = (2, self._id) 24 | self.dim_tags = [self.dim_tag] 25 | -------------------------------------------------------------------------------- /src/pygmsh/common/volume.py: -------------------------------------------------------------------------------- 1 | class Volume: 2 | """ 3 | Creates a volume. 4 | 5 | Parameters 6 | ---------- 7 | surface_loop : list 8 | Contain the identification numbers of all the surface 9 | loops defining the volume. 10 | holes : list 11 | List containing surface loop objects that represents polygon holes. 12 | 13 | Notes 14 | ----- 15 | The first surface loop defines the exterior boundary of the volume; 16 | all other surface loops define holes in the volume. 17 | 18 | A surface loop defining a hole should not have any surfaces in common 19 | with the exterior surface loop (in which case it is not a hole, 20 | and the two volumes should be defined separately). 21 | 22 | Likewise, a surface loop defining a hole should not have any surfaces 23 | in common with another surface loop defining a hole in the same volume 24 | (in which case the two surface loops should be combined). 25 | """ 26 | 27 | dim = 3 28 | 29 | def __init__(self, env, surface_loop, holes=None): 30 | if holes is None: 31 | holes = [] 32 | 33 | self.surface_loop = surface_loop 34 | self.holes = holes 35 | 36 | surface_loops = [surface_loop] + holes 37 | self._id = env.addVolume([s._id for s in surface_loops]) 38 | self.dim_tag = (3, self._id) 39 | self.dim_tags = [self.dim_tag] 40 | -------------------------------------------------------------------------------- /src/pygmsh/geo/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | 3 | __all__ = ["Geometry"] 4 | -------------------------------------------------------------------------------- /src/pygmsh/geo/dummy.py: -------------------------------------------------------------------------------- 1 | class Dummy: 2 | def __init__(self, dim, id0): 3 | assert isinstance(id0, int) 4 | self.dim = dim 5 | self._id = id0 6 | self.dim_tag = (dim, id0) 7 | self.dim_tags = [self.dim_tag] 8 | 9 | def __repr__(self): 10 | return f"" 11 | -------------------------------------------------------------------------------- /src/pygmsh/geo/geometry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | 5 | import gmsh 6 | import numpy as np 7 | 8 | from .. import common 9 | from .dummy import Dummy 10 | 11 | 12 | class Circle: 13 | def __init__( 14 | self, 15 | x0: list[float], 16 | radius: float, 17 | R, 18 | compound, 19 | num_sections: int, 20 | holes, 21 | curve_loop, 22 | plane_surface, 23 | mesh_size: float | None = None, 24 | ): 25 | self.x0 = x0 26 | self.radius = radius 27 | self.mesh_size = mesh_size 28 | self.R = R 29 | self.compound = compound 30 | self.num_sections = num_sections 31 | self.holes = holes 32 | self.curve_loop = curve_loop 33 | self.plane_surface = plane_surface 34 | 35 | 36 | class Geometry(common.CommonGeometry): 37 | def __init__(self, init_argv=None): 38 | super().__init__(gmsh.model.geo, init_argv=init_argv) 39 | 40 | def revolve(self, *args, **kwargs): 41 | if len(args) >= 4: 42 | angle = args[3] 43 | else: 44 | assert "angle" in kwargs 45 | angle = kwargs["angle"] 46 | 47 | assert angle < math.pi 48 | return super()._revolve(*args, **kwargs) 49 | 50 | def twist( 51 | self, 52 | input_entity, 53 | translation_axis: list[float], 54 | rotation_axis: list[float], 55 | point_on_axis: list[float], 56 | angle: float, 57 | num_layers: int | list[int] | None = None, 58 | heights: list[float] | None = None, 59 | recombine: bool = False, 60 | ): 61 | """Twist (translation + rotation) of any entity along a given translation_axis, 62 | around a given rotation_axis, about a given angle. 63 | """ 64 | if isinstance(num_layers, int): 65 | num_layers = [num_layers] 66 | if num_layers is None: 67 | num_layers = [] 68 | heights = [] 69 | else: 70 | if heights is None: 71 | heights = [] 72 | else: 73 | assert len(num_layers) == len(heights) 74 | 75 | assert len(point_on_axis) == 3 76 | assert len(rotation_axis) == 3 77 | assert len(translation_axis) == 3 78 | assert angle < math.pi 79 | out_dim_tags = self.env.twist( 80 | input_entity.dim_tags, 81 | *point_on_axis, 82 | *translation_axis, 83 | *rotation_axis, 84 | angle, 85 | numElements=num_layers, 86 | heights=heights, 87 | recombine=recombine, 88 | ) 89 | top = Dummy(*out_dim_tags[0]) 90 | extruded = Dummy(*out_dim_tags[1]) 91 | lateral = [Dummy(*e) for e in out_dim_tags[2:]] 92 | return top, extruded, lateral 93 | 94 | def add_circle( 95 | self, 96 | x0: list[float], 97 | radius: float, 98 | mesh_size: float | None = None, 99 | R=None, 100 | compound=False, 101 | num_sections: int = 3, 102 | holes=None, 103 | make_surface: bool = True, 104 | ): 105 | """Add circle in the :math:`x`-:math:`y`-plane.""" 106 | if holes is None: 107 | holes = [] 108 | else: 109 | assert make_surface 110 | 111 | # Define points that make the circle (midpoint and the four cardinal 112 | # directions). 113 | X = np.zeros((num_sections + 1, len(x0))) 114 | if num_sections == 4: 115 | # For accuracy, the points are provided explicitly. 116 | X[1:, [0, 1]] = np.array( 117 | [[radius, 0.0], [0.0, radius], [-radius, 0.0], [0.0, -radius]] 118 | ) 119 | else: 120 | X[1:, [0, 1]] = np.array( 121 | [ 122 | [ 123 | radius * np.cos(2 * np.pi * k / num_sections), 124 | radius * np.sin(2 * np.pi * k / num_sections), 125 | ] 126 | for k in range(num_sections) 127 | ] 128 | ) 129 | 130 | if R is not None: 131 | assert np.allclose( 132 | abs(np.linalg.eigvals(R)), np.ones(X.shape[1]) 133 | ), "The transformation matrix doesn't preserve circles; at least one eigenvalue lies off the unit circle." 134 | X = np.dot(X, R.T) 135 | 136 | X += x0 137 | 138 | # Add Gmsh Points. 139 | p = [self.add_point(x, mesh_size=mesh_size) for x in X] 140 | 141 | # Define the circle arcs. 142 | arcs = [ 143 | self.add_circle_arc(p[k], p[0], p[k + 1]) for k in range(1, len(p) - 1) 144 | ] + [self.add_circle_arc(p[-1], p[0], p[1])] 145 | 146 | if compound: 147 | self._COMPOUND_ENTITIES.append((1, [arc._id for arc in arcs])) 148 | 149 | curve_loop = self.add_curve_loop(arcs) 150 | 151 | if make_surface: 152 | plane_surface = self.add_plane_surface(curve_loop, holes) 153 | if compound: 154 | self._COMPOUND_ENTITIES.append((2, [plane_surface._id])) 155 | else: 156 | plane_surface = None 157 | 158 | return Circle( 159 | x0, 160 | radius, 161 | R, 162 | compound, 163 | num_sections, 164 | holes, 165 | curve_loop, 166 | plane_surface, 167 | mesh_size=mesh_size, 168 | ) 169 | 170 | def add_rectangle( 171 | self, 172 | xmin: float, 173 | xmax: float, 174 | ymin: float, 175 | ymax: float, 176 | z: float, 177 | mesh_size: float | None = None, 178 | holes=None, 179 | make_surface: bool = True, 180 | ): 181 | return self.add_polygon( 182 | [[xmin, ymin, z], [xmax, ymin, z], [xmax, ymax, z], [xmin, ymax, z]], 183 | mesh_size=mesh_size, 184 | holes=holes, 185 | make_surface=make_surface, 186 | ) 187 | 188 | def add_ellipsoid( 189 | self, 190 | x0: list[float], 191 | radii: list[float], 192 | mesh_size: float | None = None, 193 | with_volume: bool = True, 194 | holes=None, 195 | ): 196 | """Creates an ellipsoid with radii around a given midpoint :math:`x_0`.""" 197 | if holes is None: 198 | holes = [] 199 | 200 | if holes: 201 | assert with_volume 202 | 203 | # Add points. 204 | p = [ 205 | self.add_point(x0, mesh_size=mesh_size), 206 | self.add_point([x0[0] + radii[0], x0[1], x0[2]], mesh_size=mesh_size), 207 | self.add_point([x0[0], x0[1] + radii[1], x0[2]], mesh_size=mesh_size), 208 | self.add_point([x0[0], x0[1], x0[2] + radii[2]], mesh_size=mesh_size), 209 | self.add_point([x0[0] - radii[0], x0[1], x0[2]], mesh_size=mesh_size), 210 | self.add_point([x0[0], x0[1] - radii[1], x0[2]], mesh_size=mesh_size), 211 | self.add_point([x0[0], x0[1], x0[2] - radii[2]], mesh_size=mesh_size), 212 | ] 213 | # Add skeleton. 214 | # Alternative for circles: 215 | # `self.add_circle_arc(a, b, c)` 216 | c = [ 217 | self.add_ellipse_arc(p[1], p[0], p[6], p[6]), 218 | self.add_ellipse_arc(p[6], p[0], p[4], p[4]), 219 | self.add_ellipse_arc(p[4], p[0], p[3], p[3]), 220 | self.add_ellipse_arc(p[3], p[0], p[1], p[1]), 221 | self.add_ellipse_arc(p[1], p[0], p[2], p[2]), 222 | self.add_ellipse_arc(p[2], p[0], p[4], p[4]), 223 | self.add_ellipse_arc(p[4], p[0], p[5], p[5]), 224 | self.add_ellipse_arc(p[5], p[0], p[1], p[1]), 225 | self.add_ellipse_arc(p[6], p[0], p[2], p[2]), 226 | self.add_ellipse_arc(p[2], p[0], p[3], p[3]), 227 | self.add_ellipse_arc(p[3], p[0], p[5], p[5]), 228 | self.add_ellipse_arc(p[5], p[0], p[6], p[6]), 229 | ] 230 | 231 | # Add surfaces (1/8th of the ball surface). 232 | # Make sure the loops are oriented outwards! 233 | ll = [ 234 | # one half 235 | self.add_curve_loop([c[4], c[9], c[3]]), 236 | self.add_curve_loop([c[8], -c[4], c[0]]), 237 | self.add_curve_loop([-c[9], c[5], c[2]]), 238 | self.add_curve_loop([-c[5], -c[8], c[1]]), 239 | # the other half 240 | self.add_curve_loop([c[7], -c[3], c[10]]), 241 | self.add_curve_loop([c[11], -c[0], -c[7]]), 242 | self.add_curve_loop([-c[10], -c[2], c[6]]), 243 | self.add_curve_loop([-c[1], -c[11], -c[6]]), 244 | ] 245 | 246 | # Create a surface for each line loop. 247 | s = [self.add_surface(l) for l in ll] 248 | 249 | # Combine the surfaces to avoid seams 250 | # 251 | # Cannot enable those yet, 252 | self._COMPOUND_ENTITIES.append((2, [surf._id for surf in s[:4]])) 253 | self._COMPOUND_ENTITIES.append((2, [surf._id for surf in s[4:]])) 254 | 255 | # Create the surface loop. 256 | surface_loop = self.add_surface_loop(s) 257 | # if holes: 258 | # # Create an array of surface loops; the first entry is the outer 259 | # # surface loop, the following ones are holes. 260 | # surface_loop = self.add_array([surface_loop] + holes) 261 | # Create volume. 262 | volume = self.add_volume(surface_loop, holes) if with_volume else None 263 | 264 | class Ellipsoid: 265 | dim = 3 266 | 267 | def __init__(self, x0, radii, surface_loop, volume, mesh_size=None): 268 | self.x0 = x0 269 | self.mesh_size = mesh_size 270 | self.radii = radii 271 | self.surface_loop = surface_loop 272 | self.volume = volume 273 | return 274 | 275 | return Ellipsoid(x0, radii, surface_loop, volume, mesh_size=mesh_size) 276 | 277 | def add_ball(self, x0: list[float], radius: float, **kwargs): 278 | return self.add_ellipsoid(x0, [radius, radius, radius], **kwargs) 279 | 280 | def add_box( 281 | self, 282 | x0: float, 283 | x1: float, 284 | y0: float, 285 | y1: float, 286 | z0: float, 287 | z1: float, 288 | mesh_size: float | None = None, 289 | with_volume: bool = True, 290 | holes=None, 291 | ): 292 | if holes is None: 293 | holes = [] 294 | 295 | if holes: 296 | assert with_volume 297 | 298 | # Define corner points. 299 | p = [ 300 | self.add_point([x1, y1, z1], mesh_size=mesh_size), 301 | self.add_point([x1, y1, z0], mesh_size=mesh_size), 302 | self.add_point([x1, y0, z1], mesh_size=mesh_size), 303 | self.add_point([x1, y0, z0], mesh_size=mesh_size), 304 | self.add_point([x0, y1, z1], mesh_size=mesh_size), 305 | self.add_point([x0, y1, z0], mesh_size=mesh_size), 306 | self.add_point([x0, y0, z1], mesh_size=mesh_size), 307 | self.add_point([x0, y0, z0], mesh_size=mesh_size), 308 | ] 309 | # Define edges. 310 | e = [ 311 | self.add_line(p[0], p[1]), 312 | self.add_line(p[0], p[2]), 313 | self.add_line(p[0], p[4]), 314 | self.add_line(p[1], p[3]), 315 | self.add_line(p[1], p[5]), 316 | self.add_line(p[2], p[3]), 317 | self.add_line(p[2], p[6]), 318 | self.add_line(p[3], p[7]), 319 | self.add_line(p[4], p[5]), 320 | self.add_line(p[4], p[6]), 321 | self.add_line(p[5], p[7]), 322 | self.add_line(p[6], p[7]), 323 | ] 324 | 325 | # Define the six line loops. 326 | ll = [ 327 | self.add_curve_loop([e[0], e[3], -e[5], -e[1]]), 328 | self.add_curve_loop([e[0], e[4], -e[8], -e[2]]), 329 | self.add_curve_loop([e[1], e[6], -e[9], -e[2]]), 330 | self.add_curve_loop([e[3], e[7], -e[10], -e[4]]), 331 | self.add_curve_loop([e[5], e[7], -e[11], -e[6]]), 332 | self.add_curve_loop([e[8], e[10], -e[11], -e[9]]), 333 | ] 334 | 335 | # Create a surface for each line loop. 336 | s = [self.add_surface(l) for l in ll] 337 | # Create the surface loop. 338 | surface_loop = self.add_surface_loop(s) 339 | 340 | # Create volume 341 | vol = self.add_volume(surface_loop, holes) if with_volume else None 342 | 343 | class Box: 344 | def __init__( 345 | self, x0, x1, y0, y1, z0, z1, surface_loop, volume, mesh_size=None 346 | ): 347 | self.x0 = x0 348 | self.x1 = x1 349 | self.y0 = y0 350 | self.y1 = y1 351 | self.z0 = z0 352 | self.z1 = z1 353 | self.mesh_size = mesh_size 354 | self.surface_loop = surface_loop 355 | self.volume = volume 356 | 357 | return Box(x0, x1, y0, y1, z0, z1, surface_loop, vol, mesh_size=mesh_size) 358 | 359 | def add_torus( 360 | self, 361 | irad: float, 362 | orad: float, 363 | mesh_size: float | None = None, 364 | R=np.eye(3), 365 | x0=np.array([0.0, 0.0, 0.0]), 366 | variant: str = "extrude_lines", 367 | ): 368 | 369 | if variant == "extrude_lines": 370 | return self._add_torus_extrude_lines( 371 | irad, orad, mesh_size=mesh_size, R=R, x0=x0 372 | ) 373 | assert variant == "extrude_circle" 374 | return self._add_torus_extrude_circle( 375 | irad, orad, mesh_size=mesh_size, R=R, x0=x0 376 | ) 377 | 378 | def _add_torus_extrude_lines( 379 | self, 380 | irad: float, 381 | orad: float, 382 | mesh_size: float = None, 383 | R=np.eye(3), 384 | x0=np.array([0.0, 0.0, 0.0]), 385 | ): 386 | """Create Gmsh code for the torus in the x-y plane under the coordinate 387 | transformation 388 | 389 | .. math:: 390 | \\hat{x} = R x + x_0. 391 | 392 | :param irad: inner radius of the torus 393 | :param orad: outer radius of the torus 394 | """ 395 | # Add circle 396 | x0t = np.dot(R, np.array([0.0, orad, 0.0])) 397 | # Get circles in y-z plane 398 | Rc = np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]) 399 | c = self.add_circle(x0 + x0t, irad, mesh_size=mesh_size, R=np.dot(R, Rc)) 400 | 401 | rot_axis = [0.0, 0.0, 1.0] 402 | rot_axis = np.dot(R, rot_axis) 403 | point_on_rot_axis = [0.0, 0.0, 0.0] 404 | point_on_rot_axis = np.dot(R, point_on_rot_axis) + x0 405 | 406 | # Form the torus by extruding the circle three times by 2/3*pi. This 407 | # works around the inability of Gmsh to extrude by pi or more. The 408 | # Extrude() macro returns an array; the first [0] entry in the array is 409 | # the entity that has been extruded at the far end. This can be used 410 | # for the following Extrude() step. The second [1] entry of the array 411 | # is the surface that was created by the extrusion. 412 | previous = c.curve_loop.curves 413 | angle = 2 * np.pi / 3 414 | all_surfaces = [] 415 | for _ in range(3): 416 | for k, p in enumerate(previous): 417 | # ts1[] = Extrude {{0,0,1}, {0,0,0}, 2*Pi/3}{Line{tc1};}; 418 | # ... 419 | top, surf, _ = self.revolve( 420 | p, 421 | rotation_axis=rot_axis, 422 | point_on_axis=point_on_rot_axis, 423 | angle=angle, 424 | ) 425 | all_surfaces.append(surf) 426 | previous[k] = top 427 | 428 | # compound_surface = CompoundSurface(all_surfaces) 429 | 430 | surface_loop = self.add_surface_loop(all_surfaces) 431 | vol = self.add_volume(surface_loop) 432 | return vol 433 | 434 | def _add_torus_extrude_circle( 435 | self, 436 | irad, 437 | orad, 438 | mesh_size=None, 439 | R=np.eye(3), 440 | x0=np.array([0.0, 0.0, 0.0]), 441 | ): 442 | """Create Gmsh code for the torus under the coordinate transformation 443 | 444 | .. math:: 445 | \\hat{x} = R x + x_0. 446 | 447 | :param irad: inner radius of the torus 448 | :param orad: outer radius of the torus 449 | """ 450 | # Add circle 451 | x0t = np.dot(R, np.array([0.0, orad, 0.0])) 452 | Rc = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) 453 | c = self.add_circle(x0 + x0t, irad, mesh_size=mesh_size, R=np.dot(R, Rc)) 454 | 455 | rot_axis = [0.0, 0.0, 1.0] 456 | rot_axis = np.dot(R, rot_axis) 457 | point_on_rot_axis = [0.0, 0.0, 0.0] 458 | point_on_rot_axis = np.dot(R, point_on_rot_axis) + x0 459 | 460 | # Form the torus by extruding the circle three times by 2/3*pi. This 461 | # works around the inability of Gmsh to extrude by pi or more. The 462 | # Extrude() macro returns an array; the first [0] entry in the array is 463 | # the entity that has been extruded at the far end. This can be used 464 | # for the following Extrude() step. The second [1] entry of the array 465 | # is the surface that was created by the extrusion. The third [2-end] 466 | # is a list of all the planes of the lateral surface. 467 | previous = c.plane_surface 468 | all_volumes = [] 469 | num_steps = 3 470 | for _ in range(num_steps): 471 | top, vol, _ = self.revolve( 472 | previous, 473 | rotation_axis=rot_axis, 474 | point_on_axis=point_on_rot_axis, 475 | angle=2 * np.pi / num_steps, 476 | ) 477 | previous = top 478 | all_volumes.append(vol) 479 | 480 | assert int(gmsh.__version__.split(".")[0]) 481 | self._COMPOUND_ENTITIES.append((3, [v._id for v in all_volumes])) 482 | 483 | def add_pipe( 484 | self, 485 | outer_radius, 486 | inner_radius, 487 | length, 488 | R=np.eye(3), 489 | x0=np.array([0.0, 0.0, 0.0]), 490 | mesh_size=None, 491 | variant="rectangle_rotation", 492 | ): 493 | if variant == "rectangle_rotation": 494 | return self._add_pipe_by_rectangle_rotation( 495 | outer_radius, inner_radius, length, R=R, x0=x0, mesh_size=mesh_size 496 | ) 497 | assert variant == "circle_extrusion" 498 | return self._add_pipe_by_circle_extrusion( 499 | outer_radius, inner_radius, length, R=R, x0=x0, mesh_size=mesh_size 500 | ) 501 | 502 | def _add_pipe_by_rectangle_rotation( 503 | self, 504 | outer_radius, 505 | inner_radius, 506 | length, 507 | R=np.eye(3), 508 | x0=np.array([0.0, 0.0, 0.0]), 509 | mesh_size=None, 510 | ): 511 | """Hollow cylinder. 512 | Define a rectangle, extrude it by rotation. 513 | """ 514 | X = np.array( 515 | [ 516 | [0.0, outer_radius, -0.5 * length], 517 | [0.0, outer_radius, +0.5 * length], 518 | [0.0, inner_radius, +0.5 * length], 519 | [0.0, inner_radius, -0.5 * length], 520 | ] 521 | ) 522 | # Apply transformation. 523 | X = [np.dot(R, x) + x0 for x in X] 524 | # Create points set. 525 | p = [self.add_point(x, mesh_size=mesh_size) for x in X] 526 | 527 | # Define edges. 528 | e = [ 529 | self.add_line(p[0], p[1]), 530 | self.add_line(p[1], p[2]), 531 | self.add_line(p[2], p[3]), 532 | self.add_line(p[3], p[0]), 533 | ] 534 | 535 | rot_axis = [0.0, 0.0, 1.0] 536 | rot_axis = np.dot(R, rot_axis) 537 | point_on_rot_axis = [0.0, 0.0, 0.0] 538 | point_on_rot_axis = np.dot(R, point_on_rot_axis) + x0 539 | 540 | # Extrude all edges three times by 2*Pi/3. 541 | previous = e 542 | angle = 2 * np.pi / 3 543 | all_surfaces = [] 544 | # com = [] 545 | for _ in range(3): 546 | for k, p in enumerate(previous): 547 | # ts1[] = Extrude {{0,0,1}, {0,0,0}, 2*Pi/3}{Line{tc1};}; 548 | top, surf, _ = self.revolve( 549 | p, 550 | rotation_axis=rot_axis, 551 | point_on_axis=point_on_rot_axis, 552 | angle=angle, 553 | ) 554 | # if k==0: 555 | # com.append(surf) 556 | # else: 557 | # all_names.appends(surf) 558 | all_surfaces.append(surf) 559 | previous[k] = top 560 | # 561 | # cs = CompoundSurface(com) 562 | # Now just add surface loop and volume. 563 | # all_surfaces = all_names + [cs] 564 | surface_loop = self.add_surface_loop(all_surfaces) 565 | vol = self.add_volume(surface_loop) 566 | return vol 567 | 568 | def _add_pipe_by_circle_extrusion( 569 | self, 570 | outer_radius, 571 | inner_radius, 572 | length, 573 | R=np.eye(3), 574 | x0=np.array([0.0, 0.0, 0.0]), 575 | mesh_size=None, 576 | ): 577 | """Hollow cylinder. 578 | Define a ring, extrude it by translation. 579 | """ 580 | # Define ring which to Extrude by translation. 581 | Rc = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) 582 | c_inner = self.add_circle( 583 | x0, 584 | inner_radius, 585 | mesh_size=mesh_size, 586 | R=np.dot(R, Rc), 587 | make_surface=False, 588 | ) 589 | circ = self.add_circle( 590 | x0, 591 | outer_radius, 592 | mesh_size=mesh_size, 593 | R=np.dot(R, Rc), 594 | holes=[c_inner.curve_loop], 595 | ) 596 | 597 | # Now Extrude the ring surface. 598 | _, vol, _ = self.extrude( 599 | circ.plane_surface, translation_axis=np.dot(R, [length, 0, 0]) 600 | ) 601 | return vol 602 | 603 | def in_surface(self, input_entity, surface): 604 | """Embed the point(s) or curve(s) in the given surface. The surface mesh will 605 | conform to the mesh of the point(s) or curves(s). 606 | """ 607 | self._EMBED_QUEUE.append((input_entity, surface)) 608 | 609 | def in_volume(self, input_entity, volume): 610 | """Embed the point(s)/curve(s)/surface(s) in the given volume. The volume mesh 611 | will conform to the mesh of the input entities. 612 | """ 613 | self._EMBED_QUEUE.append((input_entity, volume)) 614 | -------------------------------------------------------------------------------- /src/pygmsh/helpers.py: -------------------------------------------------------------------------------- 1 | import gmsh 2 | import meshio 3 | import numpy as np 4 | 5 | 6 | def write(filename: str): 7 | import gmsh 8 | 9 | gmsh.write(filename) 10 | 11 | 12 | def rotation_matrix(u, theta): 13 | """Return matrix that implements the rotation around the vector :math:`u` 14 | by the angle :math:`\\theta`, cf. 15 | https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle. 16 | 17 | :param u: rotation vector 18 | :param theta: rotation angle 19 | """ 20 | assert np.isclose(np.inner(u, u), 1.0), "the rotation axis must be unitary" 21 | 22 | # Cross-product matrix. 23 | cpm = np.array([[0.0, -u[2], u[1]], [u[2], 0.0, -u[0]], [-u[1], u[0], 0.0]]) 24 | c = np.cos(theta) 25 | s = np.sin(theta) 26 | R = np.eye(3) * c + s * cpm + (1.0 - c) * np.outer(u, u) 27 | return R 28 | 29 | 30 | def orient_lines(lines): 31 | """Given a sequence of unordered and unoriented lines defining a closed polygon, 32 | returns a reordered list of reoriented lines of that polygon. 33 | 34 | :param lines: a sequence of lines defining a closed polygon 35 | """ 36 | # Categorise graph edges by their vertex pair ids 37 | point_pair_ids = np.array( 38 | [[line.points[0]._id, line.points[1]._id] for line in lines] 39 | ) 40 | 41 | # Indices of reordering 42 | order = np.arange(len(point_pair_ids), dtype=int) 43 | # Compute orientations where oriented[j] == False requires edge j to be reversed 44 | oriented = np.array([True] * len(point_pair_ids), dtype=bool) 45 | 46 | for j in range(1, len(point_pair_ids)): 47 | out = point_pair_ids[j - 1, 1] # edge out from vertex 48 | inn = point_pair_ids[j:, 0] # candidates for edge into vertices 49 | wh = np.where(inn == out)[0] + j 50 | if len(wh) == 0: 51 | # look for candidates in those which are not correctly oriented 52 | inn = point_pair_ids[j:, 1] 53 | wh = np.where(inn == out)[0] + j 54 | # reorient remaining edges 55 | point_pair_ids[j:] = np.flip(point_pair_ids[j:], axis=1) 56 | oriented[j:] ^= True 57 | 58 | # reorder 59 | point_pair_ids[[j, wh[0]]] = point_pair_ids[[wh[0], j]] 60 | order[[j, wh[0]]] = order[[wh[0], j]] 61 | 62 | # Reconstruct an ordered and oriented line loop 63 | lines = [lines[o] for o in order] 64 | lines = [lines[j] if oriented[j] else -lines[j] for j in range(len(oriented))] 65 | 66 | return lines 67 | 68 | 69 | def extract_to_meshio(): 70 | # extract point coords 71 | idx, points, _ = gmsh.model.mesh.getNodes() 72 | points = np.asarray(points).reshape(-1, 3) 73 | idx -= 1 74 | srt = np.argsort(idx) 75 | assert np.all(idx[srt] == np.arange(len(idx))) 76 | points = points[srt] 77 | 78 | # extract cells 79 | elem_types, elem_tags, node_tags = gmsh.model.mesh.getElements() 80 | cells = [] 81 | for elem_type, elem_tags, node_tags in zip(elem_types, elem_tags, node_tags): 82 | # `elementName', `dim', `order', `numNodes', `localNodeCoord', 83 | # `numPrimaryNodes' 84 | num_nodes_per_cell = gmsh.model.mesh.getElementProperties(elem_type)[3] 85 | 86 | node_tags_reshaped = np.asarray(node_tags).reshape(-1, num_nodes_per_cell) - 1 87 | node_tags_sorted = node_tags_reshaped[np.argsort(elem_tags)] 88 | cells.append( 89 | meshio.CellBlock( 90 | meshio.gmsh.gmsh_to_meshio_type[elem_type], node_tags_sorted 91 | ) 92 | ) 93 | 94 | cell_sets = {} 95 | for dim, tag in gmsh.model.getPhysicalGroups(): 96 | name = gmsh.model.getPhysicalName(dim, tag) 97 | cell_sets[name] = [[] for _ in range(len(cells))] 98 | for e in gmsh.model.getEntitiesForPhysicalGroup(dim, tag): 99 | # TODO node_tags? 100 | # elem_types, elem_tags, node_tags 101 | elem_types, elem_tags, _ = gmsh.model.mesh.getElements(dim, e) 102 | assert len(elem_types) == len(elem_tags) 103 | assert len(elem_types) == 1 104 | elem_type = elem_types[0] 105 | elem_tags = elem_tags[0] 106 | 107 | meshio_cell_type = meshio.gmsh.gmsh_to_meshio_type[elem_type] 108 | # make sure that the cell type appears only once in the cell list 109 | # -- for now 110 | idx = [] 111 | for k, cell_block in enumerate(cells): 112 | if cell_block.type == meshio_cell_type: 113 | idx.append(k) 114 | assert len(idx) == 1 115 | idx = idx[0] 116 | cell_sets[name][idx].append(elem_tags - 1) 117 | 118 | cell_sets[name] = [ 119 | (None if len(idcs) == 0 else np.concatenate(idcs)) 120 | for idcs in cell_sets[name] 121 | ] 122 | 123 | # make meshio mesh 124 | return meshio.Mesh(points, cells, cell_sets=cell_sets) 125 | -------------------------------------------------------------------------------- /src/pygmsh/occ/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | 3 | __all__ = ["Geometry"] 4 | -------------------------------------------------------------------------------- /src/pygmsh/occ/ball.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | import gmsh 4 | 5 | 6 | class Ball: 7 | """ 8 | Creates a sphere. 9 | 10 | Parameters 11 | ---------- 12 | center: array-like[3] 13 | Center of the ball. 14 | radius: float 15 | Radius of the ball. 16 | x0: float 17 | If specified and `x0 > -1`, the ball is cut off at `x0*radius` 18 | parallel to the y-z plane. 19 | x1: float 20 | If specified and `x1 < +1`, the ball is cut off at `x1*radius` 21 | parallel to the y-z plane. 22 | alpha: float 23 | If specified and `alpha < 2*pi`, the points between `alpha` and 24 | `2*pi` w.r.t. to the x-y plane are not part of the object. 25 | char_length: float 26 | If specified, sets the `Characteristic Length` property. 27 | """ 28 | 29 | dim = 3 30 | 31 | def __init__(self, center, radius, angle1=-pi / 2, angle2=pi / 2, angle3=2 * pi): 32 | self.center = center 33 | self.radius = radius 34 | self._id = gmsh.model.occ.addSphere( 35 | *center, radius, angle1=angle1, angle2=angle2, angle3=angle3 36 | ) 37 | self.dim_tag = (3, self._id) 38 | self.dim_tags = [self.dim_tag] 39 | 40 | def __repr__(self): 41 | return f"" 42 | -------------------------------------------------------------------------------- /src/pygmsh/occ/box.py: -------------------------------------------------------------------------------- 1 | import gmsh 2 | 3 | 4 | class Box: 5 | """ 6 | Creates a box. 7 | 8 | Parameters 9 | ---------- 10 | x0 : array-like[3] 11 | List containing the x, y, z values of the start point. 12 | extents : array-like[3] 13 | List of the 3 extents of the box edges. 14 | char_length : float 15 | Characteristic length of the mesh elements of this polygon. 16 | """ 17 | 18 | dim = 3 19 | 20 | def __init__(self, x0, extents, char_length=None): 21 | assert len(x0) == 3 22 | assert len(extents) == 3 23 | self.x0 = x0 24 | self.extents = extents 25 | self._id = gmsh.model.occ.addBox(*x0, *extents) 26 | self.dim_tag = (3, self._id) 27 | self.dim_tags = [self.dim_tag] 28 | -------------------------------------------------------------------------------- /src/pygmsh/occ/cone.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | import gmsh 4 | 5 | 6 | class Cone: 7 | """ 8 | Creates a cone. 9 | 10 | center : array-like[3] 11 | The 3 coordinates of the center of the first circular face. 12 | axis : array-like[3] 13 | The 3 components of the vector defining its axis. 14 | radius0 : float 15 | Radius of the first circle. 16 | radius1 : float 17 | Radius of the second circle. 18 | angle : float 19 | Angular opening of the the Cone. 20 | """ 21 | 22 | dim = 3 23 | 24 | def __init__(self, center, axis, radius0, radius1, angle=2 * pi): 25 | assert len(center) == 3 26 | assert len(axis) == 3 27 | 28 | self.center = center 29 | self.axis = axis 30 | self.radius0 = radius0 31 | self.radius1 = radius1 32 | 33 | self._id = gmsh.model.occ.addCone(*center, *axis, radius0, radius1, angle=angle) 34 | self.dim_tag = (3, self._id) 35 | self.dim_tags = [self.dim_tag] 36 | 37 | def __repr__(self): 38 | return f"" 39 | -------------------------------------------------------------------------------- /src/pygmsh/occ/cylinder.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | import gmsh 4 | 5 | 6 | class Cylinder: 7 | """ 8 | Creates a cylinder. 9 | 10 | Parameters 11 | ---------- 12 | x0 : array-like[3] 13 | The 3 coordinates of the center of the first circular face. 14 | axis : array-like[3] 15 | The 3 components of the vector defining its axis. 16 | radius : float 17 | Radius value of the cylinder. 18 | angle : float 19 | Angular opening of the cylinder. 20 | """ 21 | 22 | dim = 3 23 | 24 | def __init__(self, x0, axis, radius, angle=2 * pi): 25 | assert len(x0) == 3 26 | assert len(axis) == 3 27 | 28 | self.x0 = x0 29 | self.axis = axis 30 | self.radius = radius 31 | self.angle = angle 32 | 33 | self._id = gmsh.model.occ.addCylinder(*x0, *axis, radius, angle=angle) 34 | self.dim_tag = (3, self._id) 35 | self.dim_tags = [self.dim_tag] 36 | 37 | def __repr__(self): 38 | return f"" 39 | -------------------------------------------------------------------------------- /src/pygmsh/occ/disk.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gmsh 4 | 5 | 6 | class Disk: 7 | """ 8 | Creates a disk. 9 | 10 | Parameters 11 | ---------- 12 | x0 : array-like[3] 13 | The 3 coordinates of the center of the disk face. 14 | radius0 : float 15 | Radius value of the disk. 16 | radius1 : float 17 | Radius along Y, leading to an ellipse. 18 | """ 19 | 20 | dim = 2 21 | 22 | def __init__( 23 | self, 24 | x0: tuple[float, float] | tuple[float, float, float], 25 | radius0: float, 26 | radius1: float | None = None, 27 | ): 28 | if len(x0) == 2: 29 | x0 = (x0[0], x0[1], 0.0) 30 | 31 | assert len(x0) == 3 32 | 33 | if radius1 is None: 34 | radius1 = radius0 35 | 36 | assert radius0 >= radius1 37 | 38 | self.x0 = x0 39 | self.radius0 = radius0 40 | self.radius1 = radius1 41 | 42 | self._id = gmsh.model.occ.addDisk(*x0, radius0, radius1) 43 | self.dim_tag = (self.dim, self._id) 44 | self.dim_tags = [self.dim_tag] 45 | 46 | def __repr__(self): 47 | return f"" 48 | -------------------------------------------------------------------------------- /src/pygmsh/occ/dummy.py: -------------------------------------------------------------------------------- 1 | class Dummy: 2 | def __init__(self, dim, id0): 3 | assert isinstance(id0, int) 4 | self.dim = dim 5 | self._id = id0 6 | self.dim_tag = (dim, id0) 7 | self.dim_tags = [self.dim_tag] 8 | 9 | def __repr__(self): 10 | return f"" 11 | -------------------------------------------------------------------------------- /src/pygmsh/occ/geometry.py: -------------------------------------------------------------------------------- 1 | import math 2 | import warnings 3 | from itertools import groupby 4 | 5 | import gmsh 6 | 7 | from .. import common 8 | from .ball import Ball 9 | from .box import Box 10 | from .cone import Cone 11 | from .cylinder import Cylinder 12 | from .disk import Disk 13 | from .dummy import Dummy 14 | from .rectangle import Rectangle 15 | from .torus import Torus 16 | from .wedge import Wedge 17 | 18 | 19 | # 20 | def _all_equal(iterable): 21 | g = groupby(iterable) 22 | return next(g, True) and not next(g, False) 23 | 24 | 25 | class Geometry(common.CommonGeometry): 26 | def __init__(self, init_argv=None): 27 | super().__init__(gmsh.model.occ, init_argv=init_argv) 28 | 29 | def __exit__(self, *_): 30 | # TODO remove once gmsh 4.7.0 is out long enough (out November 5, 2020) 31 | # 32 | gmsh.option.setNumber("Mesh.CharacteristicLengthMin", 0.0) 33 | gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 1.0e22) 34 | gmsh.finalize() 35 | 36 | @property 37 | def characteristic_length_min(self): 38 | return gmsh.option.getNumber("Mesh.CharacteristicLengthMin") 39 | 40 | @property 41 | def characteristic_length_max(self): 42 | return gmsh.option.getNumber("Mesh.CharacteristicLengthMax") 43 | 44 | @characteristic_length_min.setter 45 | def characteristic_length_min(self, val): 46 | gmsh.option.setNumber("Mesh.CharacteristicLengthMin", val) 47 | 48 | @characteristic_length_max.setter 49 | def characteristic_length_max(self, val): 50 | gmsh.option.setNumber("Mesh.CharacteristicLengthMax", val) 51 | 52 | def force_outward_normals(self, tag): 53 | self._OUTWARD_NORMALS.append(tag) 54 | 55 | def revolve(self, *args, **kwargs): 56 | if len(args) >= 4: 57 | angle = args[3] 58 | else: 59 | assert "angle" in kwargs 60 | angle = kwargs["angle"] 61 | 62 | assert angle < 2 * math.pi 63 | return super()._revolve(*args, **kwargs) 64 | 65 | def add_rectangle(self, *args, mesh_size=None, **kwargs): 66 | entity = Rectangle(*args, **kwargs) 67 | if mesh_size is not None: 68 | self._SIZE_QUEUE.append((entity, mesh_size)) 69 | return entity 70 | 71 | def add_disk(self, *args, mesh_size=None, **kwargs): 72 | entity = Disk(*args, **kwargs) 73 | if mesh_size is not None: 74 | self._SIZE_QUEUE.append((entity, mesh_size)) 75 | return entity 76 | 77 | def add_ball(self, *args, mesh_size=None, **kwargs): 78 | obj = Ball(*args, **kwargs) 79 | if mesh_size is not None: 80 | self._SIZE_QUEUE.append((obj, mesh_size)) 81 | return obj 82 | 83 | def add_box(self, *args, mesh_size=None, **kwargs): 84 | box = Box(*args, **kwargs) 85 | if mesh_size is not None: 86 | self._SIZE_QUEUE.append((box, mesh_size)) 87 | return box 88 | 89 | def add_cone(self, *args, mesh_size=None, **kwargs): 90 | cone = Cone(*args, **kwargs) 91 | if mesh_size is not None: 92 | self._SIZE_QUEUE.append((cone, mesh_size)) 93 | return cone 94 | 95 | def add_cylinder(self, *args, mesh_size=None, **kwargs): 96 | cyl = Cylinder(*args, **kwargs) 97 | if mesh_size is not None: 98 | self._SIZE_QUEUE.append((cyl, mesh_size)) 99 | return cyl 100 | 101 | def add_ellipsoid(self, center, radii, mesh_size=None): 102 | obj = Ball(center, 1.0) 103 | self.dilate(obj, center, radii) 104 | if mesh_size is not None: 105 | self._SIZE_QUEUE.append((obj, mesh_size)) 106 | return obj 107 | 108 | def add_torus(self, *args, mesh_size=None, **kwargs): 109 | obj = Torus(*args, **kwargs) 110 | if mesh_size is not None: 111 | self._SIZE_QUEUE.append((obj, mesh_size)) 112 | return obj 113 | 114 | def add_wedge(self, *args, mesh_size=None, **kwargs): 115 | obj = Wedge(*args, **kwargs) 116 | if mesh_size is not None: 117 | self._SIZE_QUEUE.append((obj, mesh_size)) 118 | return obj 119 | 120 | def boolean_intersection( 121 | self, entities, delete_first: bool = True, delete_other: bool = True 122 | ): 123 | """Boolean intersection, see 124 | https://gmsh.info/doc/texinfo/gmsh.html#Boolean-operations input_entity 125 | and tool_entity are called object and tool in gmsh documentation. 126 | """ 127 | entities = [e if isinstance(e, list) else [e] for e in entities] 128 | 129 | ent = [e.dim_tag for e in entities[0]] 130 | # form subsequent intersections 131 | # https://gitlab.onelab.info/gmsh/gmsh/-/issues/999 132 | for e in entities[1:]: 133 | out, _ = gmsh.model.occ.intersect( 134 | ent, 135 | [ee.dim_tag for ee in e], 136 | removeObject=delete_first, 137 | removeTool=delete_other, 138 | ) 139 | if len(out) == 0: 140 | raise RuntimeError("Empty intersection.") 141 | if not _all_equal(out): 142 | raise RuntimeError( 143 | f"Expected all-equal elements, but got dim_tags {out}" 144 | ) 145 | ent = [out[0]] 146 | 147 | # remove entities from SIZE_QUEUE if necessary 148 | all_entities = [] 149 | if delete_first: 150 | all_entities += entities[0] 151 | if delete_other: 152 | for e in entities[1:]: 153 | all_entities += e 154 | for s in self._SIZE_QUEUE: 155 | if s[0] in all_entities: 156 | warnings.warn( 157 | f"Specified mesh size for {s[0]} " 158 | "discarded in Boolean intersection operation." 159 | ) 160 | self._SIZE_QUEUE = [s for s in self._SIZE_QUEUE if s[0] not in all_entities] 161 | 162 | return [Dummy(*ent[0])] 163 | 164 | def boolean_union( 165 | self, entities, delete_first: bool = True, delete_other: bool = True 166 | ): 167 | """Boolean union, see 168 | https://gmsh.info/doc/texinfo/gmsh.html#Boolean-operations input_entity 169 | and tool_entity are called object and tool in gmsh documentation. 170 | """ 171 | entities = [e if isinstance(e, list) else [e] for e in entities] 172 | 173 | dim_tags, _ = gmsh.model.occ.fuse( 174 | [e.dim_tag for e in entities[0]], 175 | [ee.dim_tag for e in entities[1:] for ee in e], 176 | removeObject=delete_first, 177 | removeTool=delete_other, 178 | ) 179 | 180 | # remove entities from SIZE_QUEUE if necessary 181 | all_entities = [] 182 | if delete_first: 183 | all_entities += entities[0] 184 | if delete_other: 185 | for ent in entities[1:]: 186 | all_entities += ent 187 | for s in self._SIZE_QUEUE: 188 | if s[0] in all_entities: 189 | warnings.warn( 190 | f"Specified mesh size for {s[0]} " 191 | "discarded in Boolean union operation." 192 | ) 193 | self._SIZE_QUEUE = [s for s in self._SIZE_QUEUE if s[0] not in all_entities] 194 | 195 | return [Dummy(*dim_tag) for dim_tag in dim_tags] 196 | 197 | def boolean_difference( 198 | self, d0, d1, delete_first: bool = True, delete_other: bool = True 199 | ): 200 | """Boolean difference, see 201 | https://gmsh.info/doc/texinfo/gmsh.html#Boolean-operations input_entity 202 | and tool_entity are called object and tool in gmsh documentation. 203 | """ 204 | d0 = d0 if isinstance(d0, list) else [d0] 205 | d1 = d1 if isinstance(d1, list) else [d1] 206 | dim_tags, _ = gmsh.model.occ.cut( 207 | [d.dim_tag for d in d0], 208 | [d.dim_tag for d in d1], 209 | removeObject=delete_first, 210 | removeTool=delete_other, 211 | ) 212 | 213 | # remove entities from SIZE_QUEUE if necessary 214 | all_entities = [] 215 | if delete_first: 216 | all_entities += d0 217 | if delete_other: 218 | all_entities += d1 219 | for s in self._SIZE_QUEUE: 220 | if s[0] in all_entities: 221 | warnings.warn( 222 | f"Specified mesh size for {s[0]} " 223 | "discarded in Boolean difference operation." 224 | ) 225 | self._SIZE_QUEUE = [s for s in self._SIZE_QUEUE if s[0] not in all_entities] 226 | 227 | return [Dummy(*dim_tag) for dim_tag in dim_tags] 228 | 229 | def boolean_fragments( 230 | self, d0, d1, delete_first: bool = True, delete_other: bool = True 231 | ): 232 | """Boolean fragments, see 233 | https://gmsh.info/doc/texinfo/gmsh.html#Boolean-operations input_entity 234 | and tool_entity are called object and tool in gmsh documentation. 235 | """ 236 | d0 = d0 if isinstance(d0, list) else [d0] 237 | d1 = d1 if isinstance(d1, list) else [d1] 238 | dim_tags, _ = gmsh.model.occ.fragment( 239 | [d.dim_tag for d in d0], 240 | [d.dim_tag for d in d1], 241 | removeObject=delete_first, 242 | removeTool=delete_other, 243 | ) 244 | 245 | # remove entities from SIZE_QUEUE if necessary 246 | all_entities = [] 247 | if delete_first: 248 | all_entities += d0 249 | if delete_other: 250 | all_entities += d1 251 | for s in self._SIZE_QUEUE: 252 | if s[0] in all_entities: 253 | warnings.warn( 254 | f"Specified mesh size for {s[0]} " 255 | "discarded in Boolean fragments operation." 256 | ) 257 | self._SIZE_QUEUE = [s for s in self._SIZE_QUEUE if s[0] not in all_entities] 258 | 259 | return [Dummy(*dim_tag) for dim_tag in dim_tags] 260 | 261 | def import_shapes(self, filename: str): 262 | s = gmsh.model.occ.importShapes(filename) 263 | return [Dummy(*i) for i in s] 264 | -------------------------------------------------------------------------------- /src/pygmsh/occ/rectangle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gmsh 4 | 5 | 6 | class Rectangle: 7 | """ 8 | Creates a rectangle. 9 | 10 | x0 : array-like[3] 11 | The 3 first expressions define the lower-left corner. 12 | a : float 13 | Rectangle width. 14 | b : float 15 | Rectangle height. 16 | corner_radius : float 17 | Defines a radius to round the rectangle corners. 18 | """ 19 | 20 | dim = 2 21 | 22 | def __init__( 23 | self, 24 | x0: tuple[float, float, float], 25 | a: float, 26 | b: float, 27 | corner_radius: float | None = None, 28 | ): 29 | assert len(x0) == 3 30 | 31 | self.x0 = x0 32 | self.a = a 33 | self.b = b 34 | self.corner_radius = corner_radius 35 | 36 | if corner_radius is None: 37 | corner_radius = 0.0 38 | 39 | self._id = gmsh.model.occ.addRectangle(*x0, a, b, roundedRadius=corner_radius) 40 | self.dim_tag = (self.dim, self._id) 41 | self.dim_tags = [self.dim_tag] 42 | 43 | def __repr__(self): 44 | return f"" 45 | -------------------------------------------------------------------------------- /src/pygmsh/occ/torus.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | import gmsh 4 | 5 | 6 | class Torus: 7 | """ 8 | Creates a torus. 9 | 10 | center : array-like[3] 11 | The 3 coordinates of its center. 12 | radius0 : float 13 | Inner radius. 14 | radius1 : float 15 | Outer radius. 16 | alpha : float 17 | Defines the angular opening. 18 | """ 19 | 20 | dim = 3 21 | 22 | def __init__(self, center, radius0, radius1, alpha=2 * pi): 23 | assert len(center) == 3 24 | 25 | self.center = center 26 | self.radius0 = radius0 27 | self.radius1 = radius1 28 | self.alpha = alpha 29 | 30 | self._id = gmsh.model.occ.addTorus(*center, radius0, radius1, angle=alpha) 31 | self.dim_tag = (3, self._id) 32 | self.dim_tags = [self.dim_tag] 33 | 34 | def __repr__(self): 35 | return f"" 36 | -------------------------------------------------------------------------------- /src/pygmsh/occ/wedge.py: -------------------------------------------------------------------------------- 1 | import gmsh 2 | 3 | 4 | class Wedge: 5 | """ 6 | Creates a right angular wedge. 7 | 8 | x0 : array-like[3] 9 | The 3 coordinates of the right-angle point. 10 | extends : array-like[3] 11 | List of the 3 extends of the box edges. 12 | top_extend : float 13 | Defines the top X extent. 14 | """ 15 | 16 | dim = 3 17 | 18 | def __init__(self, x0, extents, top_extent=None): 19 | self.x0 = x0 20 | self.extents = extents 21 | self.top_extent = top_extent 22 | 23 | self._id = gmsh.model.occ.addWedge(*x0, *extents, ltx=top_extent) 24 | self.dim_tags = [(3, self._id)] 25 | 26 | def __repr__(self): 27 | return f"" 28 | -------------------------------------------------------------------------------- /tests/built_in/helpers.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | 6 | def prune_nodes(points, cells): 7 | # Only points/cells that actually used 8 | uvertices, uidx = np.unique(cells, return_inverse=True) 9 | cells = uidx.reshape(cells.shape) 10 | points = points[uvertices] 11 | return points, cells 12 | 13 | 14 | def get_triangle_volumes(pts, cells): 15 | # Works in any dimension; taken from voropy 16 | local_idx = np.array([[1, 2], [2, 0], [0, 1]]).T 17 | idx_hierarchy = cells.T[local_idx] 18 | 19 | half_edge_coords = pts[idx_hierarchy[1]] - pts[idx_hierarchy[0]] 20 | ei_dot_ej = np.einsum( 21 | "ijk, ijk->ij", half_edge_coords[[1, 2, 0]], half_edge_coords[[2, 0, 1]] 22 | ) 23 | 24 | vols = 0.5 * np.sqrt( 25 | +ei_dot_ej[2] * ei_dot_ej[0] 26 | + ei_dot_ej[0] * ei_dot_ej[1] 27 | + ei_dot_ej[1] * ei_dot_ej[2] 28 | ) 29 | return vols 30 | 31 | 32 | def get_simplex_volumes(pts, cells): 33 | """Signed volume of a simplex in nD. Note that signing only makes sense for 34 | n-simplices in R^n. 35 | """ 36 | n = pts.shape[1] 37 | assert cells.shape[1] == n + 1 38 | 39 | p = pts[cells] 40 | p = np.concatenate([p, np.ones(list(p.shape[:2]) + [1])], axis=-1) 41 | return np.abs(np.linalg.det(p) / math.factorial(n)) 42 | 43 | 44 | def compute_volume(mesh): 45 | if "tetra" in mesh.cells_dict: 46 | vol = math.fsum( 47 | get_simplex_volumes(*prune_nodes(mesh.points, mesh.cells_dict["tetra"])) 48 | ) 49 | elif "triangle" in mesh.cells_dict or "quad" in mesh.cells_dict: 50 | vol = 0.0 51 | if "triangle" in mesh.cells_dict: 52 | # triangles 53 | vol += math.fsum( 54 | get_triangle_volumes( 55 | *prune_nodes(mesh.points, mesh.cells_dict["triangle"]) 56 | ) 57 | ) 58 | if "quad" in mesh.cells_dict: 59 | # quad: treat as two triangles 60 | quads = mesh.cells_dict["quad"].T 61 | split_cells = np.column_stack( 62 | [[quads[0], quads[1], quads[2]], [quads[0], quads[2], quads[3]]] 63 | ).T 64 | vol += math.fsum( 65 | get_triangle_volumes(*prune_nodes(mesh.points, split_cells)) 66 | ) 67 | else: 68 | assert "line" in mesh.cells_dict 69 | segs = np.diff(mesh.points[mesh.cells_dict["line"]], axis=1).squeeze() 70 | vol = np.sum(np.sqrt(np.einsum("...j, ...j", segs, segs))) 71 | 72 | return vol 73 | 74 | 75 | def plot(filename, points, triangles): 76 | from matplotlib import pyplot as plt 77 | 78 | pts = points[:, :2] 79 | for e in triangles: 80 | for idx in [[0, 1], [1, 2], [2, 0]]: 81 | X = pts[e[idx]] 82 | plt.plot(X[:, 0], X[:, 1], "-k") 83 | plt.gca().set_aspect("equal", "datalim") 84 | plt.axis("off") 85 | 86 | # plt.show() 87 | plt.savefig(filename, transparent=True) 88 | -------------------------------------------------------------------------------- /tests/built_in/test_airfoil.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(): 8 | # Airfoil coordinates 9 | airfoil_coordinates = np.array( 10 | [ 11 | [1.000000, 0.000000, 0.0], 12 | [0.999023, 0.000209, 0.0], 13 | [0.996095, 0.000832, 0.0], 14 | [0.991228, 0.001863, 0.0], 15 | [0.984438, 0.003289, 0.0], 16 | [0.975752, 0.005092, 0.0], 17 | [0.965201, 0.007252, 0.0], 18 | [0.952825, 0.009744, 0.0], 19 | [0.938669, 0.012538, 0.0], 20 | [0.922788, 0.015605, 0.0], 21 | [0.905240, 0.018910, 0.0], 22 | [0.886092, 0.022419, 0.0], 23 | [0.865417, 0.026096, 0.0], 24 | [0.843294, 0.029903, 0.0], 25 | [0.819807, 0.033804, 0.0], 26 | [0.795047, 0.037760, 0.0], 27 | [0.769109, 0.041734, 0.0], 28 | [0.742094, 0.045689, 0.0], 29 | [0.714107, 0.049588, 0.0], 30 | [0.685258, 0.053394, 0.0], 31 | [0.655659, 0.057071, 0.0], 32 | [0.625426, 0.060584, 0.0], 33 | [0.594680, 0.063897, 0.0], 34 | [0.563542, 0.066977, 0.0], 35 | [0.532136, 0.069789, 0.0], 36 | [0.500587, 0.072303, 0.0], 37 | [0.469022, 0.074486, 0.0], 38 | [0.437567, 0.076312, 0.0], 39 | [0.406350, 0.077752, 0.0], 40 | [0.375297, 0.078743, 0.0], 41 | [0.344680, 0.079180, 0.0], 42 | [0.314678, 0.079051, 0.0], 43 | [0.285418, 0.078355, 0.0], 44 | [0.257025, 0.077096, 0.0], 45 | [0.229618, 0.075287, 0.0], 46 | [0.203313, 0.072945, 0.0], 47 | [0.178222, 0.070096, 0.0], 48 | [0.154449, 0.066770, 0.0], 49 | [0.132094, 0.063005, 0.0], 50 | [0.111248, 0.058842, 0.0], 51 | [0.091996, 0.054325, 0.0], 52 | [0.074415, 0.049504, 0.0], 53 | [0.058573, 0.044427, 0.0], 54 | [0.044532, 0.039144, 0.0], 55 | [0.032343, 0.033704, 0.0], 56 | [0.022051, 0.028152, 0.0], 57 | [0.013692, 0.022531, 0.0], 58 | [0.007292, 0.016878, 0.0], 59 | [0.002870, 0.011224, 0.0], 60 | [0.000439, 0.005592, 0.0], 61 | [0.000000, 0.000000, 0.0], 62 | [0.001535, -0.005395, 0.0], 63 | [0.005015, -0.010439, 0.0], 64 | [0.010421, -0.015126, 0.0], 65 | [0.017725, -0.019451, 0.0], 66 | [0.026892, -0.023408, 0.0], 67 | [0.037880, -0.026990, 0.0], 68 | [0.050641, -0.030193, 0.0], 69 | [0.065120, -0.033014, 0.0], 70 | [0.081257, -0.035451, 0.0], 71 | [0.098987, -0.037507, 0.0], 72 | [0.118239, -0.039185, 0.0], 73 | [0.138937, -0.040493, 0.0], 74 | [0.161004, -0.041444, 0.0], 75 | [0.184354, -0.042054, 0.0], 76 | [0.208902, -0.042343, 0.0], 77 | [0.234555, -0.042335, 0.0], 78 | [0.261221, -0.042058, 0.0], 79 | [0.288802, -0.041541, 0.0], 80 | [0.317197, -0.040817, 0.0], 81 | [0.346303, -0.039923, 0.0], 82 | [0.376013, -0.038892, 0.0], 83 | [0.406269, -0.037757, 0.0], 84 | [0.437099, -0.036467, 0.0], 85 | [0.468187, -0.035009, 0.0], 86 | [0.499413, -0.033414, 0.0], 87 | [0.530654, -0.031708, 0.0], 88 | [0.561791, -0.029917, 0.0], 89 | [0.592701, -0.028066, 0.0], 90 | [0.623264, -0.026176, 0.0], 91 | [0.653358, -0.024269, 0.0], 92 | [0.682867, -0.022360, 0.0], 93 | [0.711672, -0.020466, 0.0], 94 | [0.739659, -0.018600, 0.0], 95 | [0.766718, -0.016774, 0.0], 96 | [0.792738, -0.014999, 0.0], 97 | [0.817617, -0.013284, 0.0], 98 | [0.841253, -0.011637, 0.0], 99 | [0.863551, -0.010068, 0.0], 100 | [0.884421, -0.008583, 0.0], 101 | [0.903777, -0.007191, 0.0], 102 | [0.921540, -0.005900, 0.0], 103 | [0.937637, -0.004717, 0.0], 104 | [0.952002, -0.003650, 0.0], 105 | [0.964576, -0.002708, 0.0], 106 | [0.975305, -0.001896, 0.0], 107 | [0.984145, -0.001222, 0.0], 108 | [0.991060, -0.000691, 0.0], 109 | [0.996020, -0.000308, 0.0], 110 | [0.999004, -0.000077, 0.0], 111 | ] 112 | ) 113 | 114 | # Scale airfoil to input coord 115 | coord = 1.0 116 | airfoil_coordinates *= coord 117 | 118 | # Instantiate geometry object 119 | with pygmsh.geo.Geometry() as geom: 120 | # Create polygon for airfoil 121 | char_length = 1.0e-1 122 | airfoil = geom.add_polygon(airfoil_coordinates, char_length, make_surface=False) 123 | 124 | # Create surface for numerical domain with an airfoil-shaped hole 125 | left_dist = 1.0 126 | right_dist = 3.0 127 | top_dist = 1.0 128 | bottom_dist = 1.0 129 | xmin = airfoil_coordinates[:, 0].min() - left_dist * coord 130 | xmax = airfoil_coordinates[:, 0].max() + right_dist * coord 131 | ymin = airfoil_coordinates[:, 1].min() - bottom_dist * coord 132 | ymax = airfoil_coordinates[:, 1].max() + top_dist * coord 133 | domainCoordinates = np.array( 134 | [[xmin, ymin, 0.0], [xmax, ymin, 0.0], [xmax, ymax, 0.0], [xmin, ymax, 0.0]] 135 | ) 136 | polygon = geom.add_polygon(domainCoordinates, char_length, holes=[airfoil]) 137 | geom.set_recombined_surfaces([polygon.surface]) 138 | 139 | ref = 10.525891646546 140 | mesh = geom.generate_mesh() 141 | 142 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 143 | return mesh 144 | 145 | 146 | if __name__ == "__main__": 147 | test().write("airfoil.vtu") 148 | -------------------------------------------------------------------------------- /tests/built_in/test_bsplines.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | lcar = 0.1 9 | p1 = geom.add_point([0.0, 0.0, 0.0], lcar) 10 | p2 = geom.add_point([1.0, 0.0, 0.0], lcar) 11 | p3 = geom.add_point([1.0, 0.5, 0.0], lcar) 12 | p4 = geom.add_point([1.0, 1.0, 0.0], lcar) 13 | s1 = geom.add_bspline([p1, p2, p3, p4]) 14 | 15 | p2 = geom.add_point([0.0, 1.0, 0.0], lcar) 16 | p3 = geom.add_point([0.5, 1.0, 0.0], lcar) 17 | s2 = geom.add_bspline([p4, p3, p2, p1]) 18 | 19 | ll = geom.add_curve_loop([s1, s2]) 20 | pl = geom.add_plane_surface(ll) 21 | 22 | # test some __repr__ 23 | print(p1) 24 | print(ll) 25 | print(s1) 26 | print(pl) 27 | 28 | mesh = geom.generate_mesh(verbose=True) 29 | # ref = 0.9156598733673261 30 | ref = 0.7474554072002251 31 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 32 | return mesh 33 | 34 | 35 | if __name__ == "__main__": 36 | test().write("bsplines.vtu") 37 | -------------------------------------------------------------------------------- /tests/built_in/test_circle.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | geom.add_circle( 9 | [0.0, 0.0, 0.0], 10 | 1.0, 11 | mesh_size=0.1, 12 | num_sections=4, 13 | # If compound==False, the section borders have to be points of the 14 | # discretization. If using a compound circle, they don't; gmsh can 15 | # choose by itself where to point the circle points. 16 | compound=True, 17 | ) 18 | # geom.add_physical(c.plane_surface, "super disk") 19 | mesh = geom.generate_mesh() 20 | 21 | ref = 3.1363871677682247 22 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 23 | return mesh 24 | 25 | 26 | if __name__ == "__main__": 27 | test().write("circle.vtk") 28 | -------------------------------------------------------------------------------- /tests/built_in/test_circle_transform.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(radius=1.0): 8 | with pygmsh.geo.Geometry() as geom: 9 | R = [ 10 | pygmsh.rotation_matrix(np.eye(1, 3, d)[0], theta) 11 | for d, theta in enumerate(np.pi / np.array([2.0, 3.0, 5])) 12 | ] 13 | geom.add_circle([7.0, 11.0, 13.0], radius, 0.1, R[0] @ R[1] @ R[2]) 14 | ref = np.pi * radius**2 15 | mesh = geom.generate_mesh() 16 | 17 | assert np.isclose(compute_volume(mesh), ref, rtol=1e-2) 18 | return mesh 19 | 20 | 21 | if __name__ == "__main__": 22 | test().write("circle_transformed.vtk") 23 | -------------------------------------------------------------------------------- /tests/built_in/test_cube.py: -------------------------------------------------------------------------------- 1 | """Creates a mesh on a cube. 2 | """ 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.geo.Geometry() as geom: 10 | geom.add_box(0, 1, 0, 1, 0, 1, 1.0) 11 | mesh = geom.generate_mesh() 12 | 13 | ref = 1.0 14 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 15 | return mesh 16 | 17 | 18 | if __name__ == "__main__": 19 | test().write("cube.vtu") 20 | -------------------------------------------------------------------------------- /tests/built_in/test_ellipsoid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates a mesh for an ellipsoid. 3 | """ 4 | from helpers import compute_volume 5 | 6 | import pygmsh 7 | 8 | 9 | def test(): 10 | with pygmsh.geo.Geometry() as geom: 11 | geom.add_ellipsoid([0.0, 0.0, 0.0], [1.0, 0.5, 0.75], 0.05) 12 | mesh = geom.generate_mesh() 13 | ref = 1.5676038497587947 14 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 15 | return mesh 16 | 17 | 18 | if __name__ == "__main__": 19 | test().write("ellipsoid.vtu") 20 | -------------------------------------------------------------------------------- /tests/built_in/test_embed.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test_in_surface(): 8 | with pygmsh.geo.Geometry() as geom: 9 | poly = geom.add_polygon( 10 | [ 11 | [0, 0.3], 12 | [0, 1.1], 13 | [0.9, 1.1], 14 | [0.9, 0.3], 15 | [0.6, 0.7], 16 | [0.3, 0.7], 17 | [0.2, 0.4], 18 | ], 19 | mesh_size=[0.2, 0.2, 0.2, 0.2, 0.03, 0.03, 0.01], 20 | ) 21 | geom.in_surface(poly.lines[4], poly) 22 | geom.in_surface(poly.points[6], poly) 23 | mesh = geom.generate_mesh() 24 | 25 | ref = 0.505 26 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 27 | return mesh 28 | 29 | 30 | # Exception: PLC Error: A segment and a facet intersect at point 31 | @pytest.mark.skip 32 | def test_in_volume(): 33 | with pygmsh.geo.Geometry() as geom: 34 | box = geom.add_box(-1, 2, -1, 2, 0, 1, mesh_size=0.5) 35 | poly = geom.add_polygon( 36 | [ 37 | [0.0, 0.3], 38 | [0.0, 1.1], 39 | [0.9, 1.1], 40 | [0.9, 0.3], 41 | [0.6, 0.7], 42 | [0.3, 0.7], 43 | [0.2, 0.4], 44 | ], 45 | mesh_size=[0.2, 0.2, 0.2, 0.2, 0.03, 0.03, 0.01], 46 | ) 47 | geom.in_volume(poly.lines[4], box.volume) 48 | geom.in_volume(poly.points[6], box.volume) 49 | geom.in_volume(poly, box.volume) 50 | 51 | mesh = geom.generate_mesh() 52 | ref = 30.505 53 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 54 | return mesh 55 | 56 | 57 | if __name__ == "__main__": 58 | test_in_surface().write("test.vtk") 59 | -------------------------------------------------------------------------------- /tests/built_in/test_hex.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | 3 | import meshio 4 | from helpers import compute_volume 5 | 6 | import pygmsh 7 | 8 | 9 | def test(lcar=1.0): 10 | with pygmsh.geo.Geometry() as geom: 11 | lbw = [2, 3, 5] 12 | points = [geom.add_point([x, 0.0, 0.0], lcar) for x in [0.0, lbw[0]]] 13 | line = geom.add_line(*points) 14 | 15 | _, rectangle, _ = geom.extrude( 16 | line, translation_axis=[0.0, lbw[1], 0.0], num_layers=lbw[1], recombine=True 17 | ) 18 | geom.extrude( 19 | rectangle, 20 | translation_axis=[0.0, 0.0, lbw[2]], 21 | num_layers=lbw[2], 22 | recombine=True, 23 | ) 24 | # compute_volume only supports 3D for tetras, but does return surface area for 25 | # quads 26 | mesh = geom.generate_mesh() 27 | # mesh.remove_lower_dimensional_cells() 28 | # mesh.remove_orphaned_nodes() 29 | 30 | ref = sum(l * w for l, w in permutations(lbw, 2)) # surface area 31 | # TODO compute hex volumes 32 | quad_mesh = meshio.Mesh(mesh.points, {"quad": mesh.cells_dict["quad"]}) 33 | assert abs(compute_volume(quad_mesh) - ref) < 1.0e-2 * ref 34 | return mesh 35 | 36 | 37 | if __name__ == "__main__": 38 | meshio.write("hex.vtu", test()) 39 | -------------------------------------------------------------------------------- /tests/built_in/test_hole_in_square.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | # Characteristic length 8 | lcar = 1e-1 9 | 10 | # Coordinates of lower-left and upper-right vertices of a square domain 11 | xmin = 0.0 12 | xmax = 5.0 13 | ymin = 0.0 14 | ymax = 5.0 15 | 16 | # Vertices of a square hole 17 | squareHoleCoordinates = np.array([[1.0, 1.0], [4.0, 1.0], [4.0, 4.0], [1.0, 4.0]]) 18 | 19 | with pygmsh.geo.Geometry() as geom: 20 | # Create square hole 21 | squareHole = geom.add_polygon(squareHoleCoordinates, lcar, make_surface=False) 22 | # Create square domain with square hole 23 | geom.add_rectangle( 24 | xmin, xmax, ymin, ymax, 0.0, lcar, holes=[squareHole.curve_loop] 25 | ) 26 | mesh = geom.generate_mesh(order=2) 27 | 28 | assert "triangle6" in mesh.cells_dict 29 | 30 | # TODO support for volumes of triangle6 31 | # ref = 16.0 32 | # from helpers import compute_volume 33 | # assert abs(compute_volume(points, cells) - ref) < 1.0e-2 * ref 34 | return mesh 35 | 36 | 37 | if __name__ == "__main__": 38 | test().write("hole_in_square.vtu") 39 | -------------------------------------------------------------------------------- /tests/built_in/test_layers.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(mesh_size=0.05): 7 | with pygmsh.geo.Geometry() as geom: 8 | # Draw a cross with a circular hole 9 | circ = geom.add_circle( 10 | [0.0, 0.0, 0.0], 0.1, mesh_size=mesh_size, make_surface=False 11 | ) 12 | poly = geom.add_polygon( 13 | [ 14 | [+0.0, +0.5, 0.0], 15 | [-0.1, +0.1, 0.0], 16 | [-0.5, +0.0, 0.0], 17 | [-0.1, -0.1, 0.0], 18 | [+0.0, -0.5, 0.0], 19 | [+0.1, -0.1, 0.0], 20 | [+0.5, +0.0, 0.0], 21 | [+0.1, +0.1, 0.0], 22 | ], 23 | mesh_size=mesh_size, 24 | holes=[circ], 25 | ) 26 | axis = [0, 0, 1.0] 27 | geom.extrude(poly, translation_axis=axis, num_layers=1) 28 | mesh = geom.generate_mesh() 29 | 30 | ref = 0.16951514066385628 31 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 32 | return mesh 33 | 34 | 35 | if __name__ == "__main__": 36 | test().write("layers.vtu") 37 | -------------------------------------------------------------------------------- /tests/built_in/test_pacman.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | from numpy import cos, pi, sin 3 | 4 | import pygmsh 5 | 6 | 7 | def test(lcar=0.3): 8 | with pygmsh.geo.Geometry() as geom: 9 | r = 1.25 * 3.4 10 | p1 = geom.add_point([0.0, 0.0, 0.0], lcar) 11 | # p2 = geom.add_point([+r, 0.0, 0.0], lcar) 12 | p3 = geom.add_point([-r, 0.0, 0.0], lcar) 13 | p4 = geom.add_point([0.0, +r, 0.0], lcar) 14 | p5 = geom.add_point([0.0, -r, 0.0], lcar) 15 | p6 = geom.add_point([r * cos(+pi / 12.0), r * sin(+pi / 12.0), 0.0], lcar) 16 | p7 = geom.add_point([r * cos(-pi / 12.0), r * sin(-pi / 12.0), 0.0], lcar) 17 | p8 = geom.add_point([0.5 * r, 0.0, 0.0], lcar) 18 | 19 | c0 = geom.add_circle_arc(p6, p1, p4) 20 | c1 = geom.add_circle_arc(p4, p1, p3) 21 | c2 = geom.add_circle_arc(p3, p1, p5) 22 | c3 = geom.add_circle_arc(p5, p1, p7) 23 | l1 = geom.add_line(p7, p8) 24 | l2 = geom.add_line(p8, p6) 25 | ll = geom.add_curve_loop([c0, c1, c2, c3, l1, l2]) 26 | 27 | pacman = geom.add_plane_surface(ll) 28 | 29 | # test setting physical groups 30 | geom.add_physical(p1, label="c") 31 | geom.add_physical(c0, label="arc") 32 | geom.add_physical(pacman, "dummy") 33 | geom.add_physical(pacman, label="77") 34 | 35 | mesh = geom.generate_mesh() 36 | 37 | ref = 54.312974717523744 38 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 39 | return mesh 40 | 41 | 42 | if __name__ == "__main__": 43 | test().write("pacman.vtu") 44 | -------------------------------------------------------------------------------- /tests/built_in/test_physical.py: -------------------------------------------------------------------------------- 1 | import meshio 2 | 3 | import pygmsh 4 | 5 | 6 | def test(lcar=0.5): 7 | with pygmsh.geo.Geometry() as geom: 8 | poly = geom.add_polygon([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], lcar) 9 | 10 | top, volume, lat = geom.extrude(poly, [0, 0, 2]) 11 | 12 | geom.add_physical(poly, label="bottom") 13 | geom.add_physical(top, label="top") 14 | geom.add_physical(volume, label="volume") 15 | geom.add_physical(lat, label="lat") 16 | geom.add_physical(poly.lines[0], label="line") 17 | 18 | mesh = geom.generate_mesh() 19 | assert len(mesh.cell_sets) == 5 20 | return mesh 21 | 22 | 23 | if __name__ == "__main__": 24 | test().write("physical.vtu") 25 | read_mesh = meshio.read("physical.vtu") 26 | assert len(read_mesh.cell_sets) == 5 27 | -------------------------------------------------------------------------------- /tests/built_in/test_pipes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(): 8 | """Pipe with double-ring enclosure, rotated in space.""" 9 | with pygmsh.geo.Geometry() as geom: 10 | sqrt2on2 = 0.5 * np.sqrt(2.0) 11 | R = pygmsh.rotation_matrix([sqrt2on2, sqrt2on2, 0], np.pi / 6.0) 12 | geom.add_pipe( 13 | inner_radius=0.3, outer_radius=0.4, length=1.0, R=R, mesh_size=0.04 14 | ) 15 | 16 | R = np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]) 17 | geom.add_pipe( 18 | inner_radius=0.3, 19 | outer_radius=0.4, 20 | length=1.0, 21 | mesh_size=0.04, 22 | R=R, 23 | variant="circle_extrusion", 24 | ) 25 | mesh = geom.generate_mesh() 26 | 27 | ref = 0.43988203517453256 28 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 29 | return mesh 30 | 31 | 32 | if __name__ == "__main__": 33 | test().write("pipes.vtu") 34 | -------------------------------------------------------------------------------- /tests/built_in/test_quads.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | rectangle = geom.add_rectangle(0.0, 1.0, 0.0, 1.0, 0.0, 0.1) 9 | geom.set_recombined_surfaces([rectangle.surface]) 10 | mesh = geom.generate_mesh(dim=2) 11 | 12 | ref = 1.0 13 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 14 | return mesh 15 | 16 | 17 | if __name__ == "__main__": 18 | test().write("quads.vtu") 19 | -------------------------------------------------------------------------------- /tests/built_in/test_recombine.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | pts = [ 9 | geom.add_point((0.0, 0.0, 0.0), mesh_size=1.0), 10 | geom.add_point((2.0, 0.0, 0.0), mesh_size=1.0), 11 | geom.add_point((0.0, 1.0, 0.0), mesh_size=1.0), 12 | geom.add_point((2.0, 1.0, 0.0), mesh_size=1.0), 13 | ] 14 | lines = [ 15 | geom.add_line(pts[0], pts[1]), 16 | geom.add_line(pts[1], pts[3]), 17 | geom.add_line(pts[3], pts[2]), 18 | geom.add_line(pts[2], pts[0]), 19 | ] 20 | ll0 = geom.add_curve_loop(lines) 21 | rs0 = geom.add_surface(ll0) 22 | 23 | geom.set_transfinite_curve(lines[3], 3, "Progression", 1.0) 24 | geom.set_transfinite_curve(lines[1], 3, "Progression", 1.0) 25 | geom.set_transfinite_curve(lines[2], 3, "Progression", 1.0) 26 | geom.set_transfinite_curve(lines[0], 3, "Progression", 1.0) 27 | geom.set_transfinite_surface(rs0, "Left", pts) 28 | geom.set_recombined_surfaces([rs0]) 29 | 30 | mesh = geom.generate_mesh() 31 | 32 | assert "quad" in mesh.cells_dict.keys() 33 | ref = np.array([[0, 4, 8, 7], [7, 8, 6, 2], [4, 1, 5, 8], [8, 5, 3, 6]]) 34 | assert np.array_equal(ref, mesh.cells_dict["quad"]) 35 | return mesh 36 | 37 | 38 | if __name__ == "__main__": 39 | test().write("rectangle_structured.vtu") 40 | -------------------------------------------------------------------------------- /tests/built_in/test_rectangle.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | geom.add_rectangle(0.0, 1.0, 0.0, 1.0, 0.0, 0.1) 9 | mesh = geom.generate_mesh() 10 | 11 | ref = 1.0 12 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 13 | return mesh 14 | 15 | 16 | if __name__ == "__main__": 17 | test().write("rectangle.vtu") 18 | -------------------------------------------------------------------------------- /tests/built_in/test_rectangle_with_hole.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates a mesh for a square with a round hole. 3 | """ 4 | from helpers import compute_volume 5 | 6 | import pygmsh 7 | 8 | 9 | def test(): 10 | with pygmsh.geo.Geometry() as geom: 11 | circle = geom.add_circle( 12 | x0=[0.5, 0.5, 0.0], 13 | radius=0.25, 14 | mesh_size=0.1, 15 | num_sections=4, 16 | make_surface=False, 17 | ) 18 | geom.add_rectangle( 19 | 0.0, 1.0, 0.0, 1.0, 0.0, mesh_size=0.1, holes=[circle.curve_loop] 20 | ) 21 | mesh = geom.generate_mesh() 22 | 23 | ref = 0.8086582838174551 24 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 25 | return mesh 26 | 27 | 28 | if __name__ == "__main__": 29 | test().write("rectangle_with_hole.vtu") 30 | -------------------------------------------------------------------------------- /tests/built_in/test_regular_extrusion.py: -------------------------------------------------------------------------------- 1 | """Creates regular cube mesh by extrusion. 2 | """ 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | x = 5 10 | y = 4 11 | z = 3 12 | x_layers = 10 13 | y_layers = 5 14 | z_layers = 3 15 | 16 | with pygmsh.geo.Geometry() as geom: 17 | p = geom.add_point([0, 0, 0], 1) 18 | _, l, _ = geom.extrude(p, [x, 0, 0], num_layers=x_layers) 19 | 20 | _, s, _ = geom.extrude(l, [0, y, 0], num_layers=y_layers) 21 | geom.extrude(s, [0, 0, z], num_layers=z_layers) 22 | mesh = geom.generate_mesh() 23 | 24 | ref_vol = x * y * z 25 | assert abs(compute_volume(mesh) - ref_vol) < 1.0e-2 * ref_vol 26 | 27 | # Each grid-cell from layered extrusion will result in 6 tetrahedra. 28 | ref_tetras = 6 * x_layers * y_layers * z_layers 29 | assert len(mesh.cells_dict["tetra"]) == ref_tetras 30 | 31 | return mesh 32 | 33 | 34 | if __name__ == "__main__": 35 | test().write("cube.vtu") 36 | -------------------------------------------------------------------------------- /tests/built_in/test_rotated_layers.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(mesh_size=0.05): 9 | with pygmsh.geo.Geometry() as geom: 10 | # Draw a square 11 | poly = geom.add_polygon( 12 | [ 13 | [+0.5, +0.0, 0.0], 14 | [+0.0, +0.5, 0.0], 15 | [-0.5, +0.0, 0.0], 16 | [+0.0, -0.5, 0.0], 17 | ], 18 | mesh_size=mesh_size, 19 | ) 20 | axis = [0, 0, 1.0] 21 | geom.twist( 22 | poly, 23 | translation_axis=axis, 24 | rotation_axis=axis, 25 | point_on_axis=[0.0, 0.0, 0.0], 26 | angle=0.5 * pi, 27 | num_layers=5, 28 | recombine=True, 29 | ) 30 | mesh = geom.generate_mesh() 31 | 32 | ref = 3.98156496566 33 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 34 | return mesh 35 | 36 | 37 | if __name__ == "__main__": 38 | test().write("rotated_layers.vtu") 39 | -------------------------------------------------------------------------------- /tests/built_in/test_rotation.py: -------------------------------------------------------------------------------- 1 | """Test translation for all dimensions.""" 2 | import numpy as np 3 | 4 | import pygmsh 5 | 6 | 7 | def test_rotation2d(): 8 | """Rotation of a surface object.""" 9 | angle = np.pi / 5 10 | 11 | # Generate reference geometry 12 | with pygmsh.geo.Geometry() as geom: 13 | rect = geom.add_rectangle(0.0, 2.0, 0.0, 1.0, 0.0, 0.1) 14 | mesh_unrot = geom.generate_mesh() 15 | vertex_index = mesh_unrot.cells_dict["vertex"] 16 | vertex_index = vertex_index.reshape((vertex_index.shape[0],)) 17 | 18 | with pygmsh.geo.Geometry() as geom: 19 | # Generate rotated geometry 20 | geom = pygmsh.geo.Geometry() 21 | rect = geom.add_rectangle(0.0, 2.0, 0.0, 1.0, 0.0, 0.1) 22 | geom.rotate(rect.surface, (0, 0, 0), angle, (0, 0, 1)) 23 | mesh = geom.generate_mesh() 24 | 25 | new_vertex_index = mesh.cells_dict["vertex"] 26 | new_vertex_index = new_vertex_index.reshape((new_vertex_index.shape[0],)) 27 | 28 | # Generate rotation matrix and compare with rotated geometry 29 | Rm = pygmsh.helpers.rotation_matrix([0, 0, 1], angle) 30 | for v, v_new in zip(vertex_index, new_vertex_index): 31 | point = mesh_unrot.points[v, :] 32 | rot_point = np.dot(Rm, point) 33 | new_point = mesh.points[v, :] 34 | assert np.allclose(rot_point, new_point) 35 | 36 | 37 | if __name__ == "__main__": 38 | test_rotation2d() 39 | -------------------------------------------------------------------------------- /tests/built_in/test_screw.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(mesh_size=0.05): 8 | with pygmsh.geo.Geometry() as geom: 9 | # Draw a cross with a circular hole 10 | circ = geom.add_circle([0.0, 0.0], 0.1, mesh_size=mesh_size) 11 | poly = geom.add_polygon( 12 | [ 13 | [+0.0, +0.5], 14 | [-0.1, +0.1], 15 | [-0.5, +0.0], 16 | [-0.1, -0.1], 17 | [+0.0, -0.5], 18 | [+0.1, -0.1], 19 | [+0.5, +0.0], 20 | [+0.1, +0.1], 21 | ], 22 | mesh_size=mesh_size, 23 | holes=[circ], 24 | ) 25 | 26 | geom.twist( 27 | poly, 28 | translation_axis=[0.0, 0.0, 1.0], 29 | rotation_axis=[0.0, 0.0, 1.0], 30 | point_on_axis=[0.0, 0.0, 0.0], 31 | angle=2.0 / 6.0 * np.pi, 32 | ) 33 | mesh = geom.generate_mesh() 34 | 35 | ref = 0.16951514066385628 36 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 37 | return mesh 38 | 39 | 40 | if __name__ == "__main__": 41 | test().write("screw.vtu") 42 | -------------------------------------------------------------------------------- /tests/built_in/test_splines.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | lcar = 0.1 9 | p1 = geom.add_point([0.0, 0.0, 0.0], lcar) 10 | p2 = geom.add_point([1.0, 0.0, 0.0], lcar) 11 | p3 = geom.add_point([1.0, 0.5, 0.0], lcar) 12 | p4 = geom.add_point([1.0, 1.0, 0.0], lcar) 13 | s1 = geom.add_spline([p1, p2, p3, p4]) 14 | 15 | p2 = geom.add_point([0.0, 1.0, 0.0], lcar) 16 | p3 = geom.add_point([0.5, 1.0, 0.0], lcar) 17 | s2 = geom.add_spline([p4, p3, p2, p1]) 18 | 19 | ll = geom.add_curve_loop([s1, s2]) 20 | geom.add_plane_surface(ll) 21 | 22 | mesh = geom.generate_mesh() 23 | ref = 1.0809439490373247 24 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 25 | return mesh 26 | 27 | 28 | if __name__ == "__main__": 29 | test().write("splines.vtu") 30 | -------------------------------------------------------------------------------- /tests/built_in/test_subdomains.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | lcar = 0.1 9 | circle = geom.add_circle([0.5, 0.5, 0.0], 1.0, lcar) 10 | triangle = geom.add_polygon( 11 | [[2.0, -0.5, 0.0], [4.0, -0.5, 0.0], [4.0, 1.5, 0.0]], lcar 12 | ) 13 | rectangle = geom.add_rectangle(4.75, 6.25, -0.24, 1.25, 0.0, lcar) 14 | # hold all domain 15 | geom.add_polygon( 16 | [ 17 | [-1.0, -1.0, 0.0], 18 | [+7.0, -1.0, 0.0], 19 | [+7.0, +2.0, 0.0], 20 | [-1.0, +2.0, 0.0], 21 | ], 22 | lcar, 23 | holes=[circle.curve_loop, triangle.curve_loop, rectangle.curve_loop], 24 | ) 25 | mesh = geom.generate_mesh() 26 | 27 | ref = 24.0 28 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 29 | return mesh 30 | 31 | 32 | if __name__ == "__main__": 33 | test().write("subdomains.vtu") 34 | -------------------------------------------------------------------------------- /tests/built_in/test_swiss_cheese.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(): 8 | X0 = np.array( 9 | [[+0.0, +0.0, 0.0], [+0.5, +0.3, 0.1], [-0.5, +0.3, 0.1], [+0.5, -0.3, 0.1]] 10 | ) 11 | R = np.array([0.1, 0.2, 0.1, 0.14]) 12 | 13 | with pygmsh.geo.Geometry() as geom: 14 | holes = [ 15 | geom.add_ball(x0, r, with_volume=False, mesh_size=0.2 * r).surface_loop 16 | for x0, r in zip(X0, R) 17 | ] 18 | # geom.add_box( 19 | # -1, 1, 20 | # -1, 1, 21 | # -1, 1, 22 | # mesh_size=0.2, 23 | # holes=holes 24 | # ) 25 | geom.add_ball([0, 0, 0], 1.0, mesh_size=0.2, holes=holes) 26 | # geom.add_physical_volume(ball, label="cheese") 27 | mesh = geom.generate_mesh(algorithm=5) 28 | 29 | ref = 4.07064892966291 30 | assert abs(compute_volume(mesh) - ref) < 2.0e-2 * ref 31 | return mesh 32 | 33 | 34 | if __name__ == "__main__": 35 | test().write("swiss_cheese.vtu") 36 | -------------------------------------------------------------------------------- /tests/built_in/test_symmetrize.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.geo.Geometry() as geom: 8 | poly = geom.add_polygon( 9 | [[0.0, 0.5], [1.0, 0.5], [1.0, 1.0], [0.0, 1.0]], 10 | mesh_size=0.05, 11 | ) 12 | cp = geom.copy(poly) 13 | geom.symmetrize(cp, [0.0, 1.0, 0.0, -0.5]) 14 | mesh = geom.generate_mesh() 15 | 16 | ref = 1.0 17 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 18 | return mesh 19 | 20 | 21 | if __name__ == "__main__": 22 | test().write("symmetry.vtk") 23 | -------------------------------------------------------------------------------- /tests/built_in/test_tori.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(irad=0.05, orad=0.6): 8 | """Torus, rotated in space.""" 9 | with pygmsh.geo.Geometry() as geom: 10 | R = pygmsh.rotation_matrix([1.0, 0.0, 0.0], np.pi / 2) 11 | geom.add_torus(irad=irad, orad=orad, mesh_size=0.03, x0=[0.0, 0.0, -1.0], R=R) 12 | 13 | R = pygmsh.rotation_matrix([0.0, 1.0, 0.0], np.pi / 2) 14 | geom.add_torus( 15 | irad=irad, 16 | orad=orad, 17 | mesh_size=0.03, 18 | x0=[0.0, 0.0, 1.0], 19 | variant="extrude_circle", 20 | ) 21 | mesh = geom.generate_mesh() 22 | 23 | ref = 2 * 2 * np.pi**2 * orad * irad**2 24 | assert np.isclose(compute_volume(mesh), ref, rtol=5e-2) 25 | return mesh 26 | 27 | 28 | if __name__ == "__main__": 29 | test().write("torus.vtu") 30 | -------------------------------------------------------------------------------- /tests/built_in/test_torus.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(irad=0.05, orad=0.6): 8 | """Torus, rotated in space.""" 9 | with pygmsh.geo.Geometry() as geom: 10 | R = pygmsh.rotation_matrix([1.0, 0.0, 0.0], np.pi / 2) 11 | geom.add_torus(irad=irad, orad=orad, mesh_size=0.03, x0=[0.0, 0.0, -1.0], R=R) 12 | mesh = geom.generate_mesh() 13 | 14 | ref = 2 * np.pi**2 * orad * irad**2 15 | assert np.isclose(compute_volume(mesh), ref, rtol=5e-2) 16 | return mesh 17 | 18 | 19 | if __name__ == "__main__": 20 | test().write("torus.vtu") 21 | -------------------------------------------------------------------------------- /tests/built_in/test_torus_crowd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from helpers import compute_volume 3 | 4 | import pygmsh 5 | 6 | 7 | def test(): 8 | # internal radius of torus 9 | irad = 0.15 10 | # external radius of torus 11 | orad = 0.27 12 | 13 | Z_pos = (irad + orad) * np.concatenate( 14 | [+np.ones(8), -np.ones(8), +np.ones(8), -np.ones(8)] 15 | ) 16 | 17 | Alpha = np.concatenate( 18 | [ 19 | np.arange(8) * np.pi / 4.0, 20 | np.arange(8) * np.pi / 4.0 + np.pi / 16.0, 21 | np.arange(8) * np.pi / 4.0, 22 | np.arange(8) * np.pi / 4.0 + np.pi / 16.0, 23 | ] 24 | ) 25 | 26 | A1 = ( 27 | (irad + orad) 28 | / np.tan(np.pi / 8.0) 29 | * np.concatenate( 30 | [1.6 * np.ones(8), 1.6 * np.ones(8), 1.9 * np.ones(8), 1.9 * np.ones(8)] 31 | ) 32 | ) 33 | 34 | with pygmsh.geo.Geometry() as geom: 35 | for alpha, a1, z in zip(Alpha, A1, Z_pos): 36 | # Rotate torus to the y-z-plane. 37 | R1 = pygmsh.rotation_matrix([0.0, 1.0, 0.0], 0.5 * np.pi) 38 | R2 = pygmsh.rotation_matrix([0.0, 0.0, 1.0], alpha) 39 | x0 = np.array([a1, 0.0, 0.0]) 40 | x1 = np.array([0.0, 0.0, z]) 41 | # First rotate to y-z-plane, then move out to a1, rotate by angle 42 | # alpha, move up by z. 43 | # 44 | # xnew = R2*(R1*x+x0) + x1 45 | # 46 | geom.add_torus( 47 | irad=irad, 48 | orad=orad, 49 | mesh_size=0.1, 50 | R=np.dot(R2, R1), 51 | x0=np.dot(R2, x0) + x1, 52 | ) 53 | geom.add_box(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, mesh_size=0.3) 54 | mesh = geom.generate_mesh() 55 | 56 | ref = len(A1) * 2 * np.pi**2 * orad * irad**2 + 2.0**3 57 | assert np.isclose(compute_volume(mesh), ref, rtol=2e-2) 58 | return mesh 59 | 60 | 61 | if __name__ == "__main__": 62 | test().write("torus_crowd.vtu") 63 | -------------------------------------------------------------------------------- /tests/built_in/test_transfinite.py: -------------------------------------------------------------------------------- 1 | import pygmsh 2 | 3 | 4 | def test(lcar=0.1): 5 | with pygmsh.geo.Geometry() as geom: 6 | poly = geom.add_polygon( 7 | [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]], lcar 8 | ) 9 | geom.set_transfinite_surface(poly, "Left", corner_pts=[]) 10 | mesh = geom.generate_mesh() 11 | assert len(mesh.cells_dict["triangle"]) == 10 * 10 * 2 12 | return mesh 13 | 14 | 15 | if __name__ == "__main__": 16 | test().write("transfinite.vtu") 17 | -------------------------------------------------------------------------------- /tests/built_in/test_unordered_unoriented.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import numpy as np 4 | from helpers import compute_volume 5 | 6 | import pygmsh 7 | 8 | 9 | def test(): 10 | with pygmsh.geo.Geometry() as geom: 11 | # Generate an approximation of a circle 12 | t = np.arange(0, 2.0 * np.pi, 0.05) 13 | x = np.column_stack([np.cos(t), np.sin(t), np.zeros_like(t)]) 14 | points = [geom.add_point(p) for p in x] 15 | 16 | # Shuffle the orientation of lines by point order 17 | o = [0 if k % 3 == 0 else 1 for k in range(len(points))] 18 | 19 | lines = [ 20 | geom.add_line(points[k + o[k]], points[k + (o[k] + 1) % 2]) 21 | for k in range(len(points) - 1) 22 | ] 23 | lines.append(geom.add_line(points[-1], points[0])) 24 | 25 | # Shuffle the order of lines 26 | random.seed(1) 27 | random.shuffle(lines) 28 | 29 | oriented_lines = pygmsh.orient_lines(lines) 30 | ll = geom.add_curve_loop(oriented_lines) 31 | geom.add_plane_surface(ll) 32 | 33 | mesh = geom.generate_mesh() 34 | 35 | ref = np.pi 36 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 37 | return mesh 38 | 39 | 40 | if __name__ == "__main__": 41 | test().write("physical.vtu") 42 | -------------------------------------------------------------------------------- /tests/built_in/test_volume.py: -------------------------------------------------------------------------------- 1 | import meshio 2 | import numpy as np 3 | from helpers import compute_volume 4 | 5 | 6 | def test_volume(): 7 | points = np.array( 8 | [ 9 | [0.0, 0.0, 0.0], 10 | [1.0, 0.0, 0.0], 11 | [2.0, 0.0, 0.0], 12 | [3.0, 0.0, 0.0], 13 | [3.0, 1.0, 0.0], 14 | [2.0, 1.0, 0.0], 15 | [1.0, 1.0, 0.0], 16 | [0.0, 1.0, 0.0], 17 | ] 18 | ) 19 | cells = { 20 | "triangle": np.array([[0, 1, 6], [0, 6, 7]]), 21 | "quad": np.array([[1, 2, 5, 6], [2, 3, 4, 5]]), 22 | } 23 | vol = compute_volume(meshio.Mesh(points, cells)) 24 | assert abs(vol - 3.0) < 1.0e-14 25 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | 6 | def prune_nodes(points, cells): 7 | # Only points/cells that actually used 8 | uvertices, uidx = np.unique(cells, return_inverse=True) 9 | cells = uidx.reshape(cells.shape) 10 | points = points[uvertices] 11 | return points, cells 12 | 13 | 14 | def get_triangle_volumes(pts, cells): 15 | # Works in any dimension; taken from voropy 16 | local_idx = np.array([[1, 2], [2, 0], [0, 1]]).T 17 | idx_hierarchy = cells.T[local_idx] 18 | 19 | half_edge_coords = pts[idx_hierarchy[1]] - pts[idx_hierarchy[0]] 20 | ei_dot_ej = np.einsum( 21 | "ijk, ijk->ij", half_edge_coords[[1, 2, 0]], half_edge_coords[[2, 0, 1]] 22 | ) 23 | 24 | vols = 0.5 * np.sqrt( 25 | +ei_dot_ej[2] * ei_dot_ej[0] 26 | + ei_dot_ej[0] * ei_dot_ej[1] 27 | + ei_dot_ej[1] * ei_dot_ej[2] 28 | ) 29 | return vols 30 | 31 | 32 | def get_simplex_volumes(pts, cells): 33 | """Signed volume of a simplex in nD. Note that signing only makes sense for 34 | n-simplices in R^n. 35 | """ 36 | n = pts.shape[1] 37 | assert cells.shape[1] == n + 1 38 | 39 | p = pts[cells] 40 | p = np.concatenate([p, np.ones(list(p.shape[:2]) + [1])], axis=-1) 41 | return np.abs(np.linalg.det(p) / math.factorial(n)) 42 | 43 | 44 | def compute_volume(mesh): 45 | if "tetra" in mesh.cells_dict: 46 | vol = math.fsum( 47 | get_simplex_volumes(*prune_nodes(mesh.points, mesh.cells_dict["tetra"])) 48 | ) 49 | elif "triangle" in mesh.cells_dict or "quad" in mesh.cells_dict: 50 | vol = 0.0 51 | if "triangle" in mesh.cells_dict: 52 | # triangles 53 | vol += math.fsum( 54 | get_triangle_volumes( 55 | *prune_nodes(mesh.points, mesh.cells_dict["triangle"]) 56 | ) 57 | ) 58 | if "quad" in mesh.cells_dict: 59 | # quad: treat as two triangles 60 | quads = mesh.cells_dict["quad"].T 61 | split_cells = np.column_stack( 62 | [[quads[0], quads[1], quads[2]], [quads[0], quads[2], quads[3]]] 63 | ).T 64 | vol += math.fsum( 65 | get_triangle_volumes(*prune_nodes(mesh.points, split_cells)) 66 | ) 67 | else: 68 | assert "line" in mesh.cells_dict 69 | segs = np.diff(mesh.points[mesh.cells_dict["line"]], axis=1).squeeze() 70 | vol = np.sum(np.sqrt(np.einsum("...j, ...j", segs, segs))) 71 | 72 | return vol 73 | 74 | 75 | def plot(filename, points, triangles): 76 | from matplotlib import pyplot as plt 77 | 78 | pts = points[:, :2] 79 | for e in triangles: 80 | for idx in [[0, 1], [1, 2], [2, 0]]: 81 | X = pts[e[idx]] 82 | plt.plot(X[:, 0], X[:, 1], "-k") 83 | plt.gca().set_aspect("equal", "datalim") 84 | plt.axis("off") 85 | 86 | # plt.show() 87 | plt.savefig(filename, transparent=True) 88 | -------------------------------------------------------------------------------- /tests/occ/helpers.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | 6 | def prune_nodes(points, cells): 7 | # Only points/cells that actually used 8 | uvertices, uidx = np.unique(cells, return_inverse=True) 9 | cells = uidx.reshape(cells.shape) 10 | points = points[uvertices] 11 | return points, cells 12 | 13 | 14 | def get_triangle_volumes(pts, cells): 15 | # Works in any dimension; taken from voropy 16 | local_idx = np.array([[1, 2], [2, 0], [0, 1]]).T 17 | idx_hierarchy = cells.T[local_idx] 18 | 19 | half_edge_coords = pts[idx_hierarchy[1]] - pts[idx_hierarchy[0]] 20 | ei_dot_ej = np.einsum( 21 | "ijk, ijk->ij", half_edge_coords[[1, 2, 0]], half_edge_coords[[2, 0, 1]] 22 | ) 23 | 24 | vols = 0.5 * np.sqrt( 25 | +ei_dot_ej[2] * ei_dot_ej[0] 26 | + ei_dot_ej[0] * ei_dot_ej[1] 27 | + ei_dot_ej[1] * ei_dot_ej[2] 28 | ) 29 | return vols 30 | 31 | 32 | def get_simplex_volumes(pts, cells): 33 | """Signed volume of a simplex in nD. Note that signing only makes sense for 34 | n-simplices in R^n. 35 | """ 36 | n = pts.shape[1] 37 | assert cells.shape[1] == n + 1 38 | 39 | p = pts[cells] 40 | p = np.concatenate([p, np.ones(list(p.shape[:2]) + [1])], axis=-1) 41 | return np.abs(np.linalg.det(p) / math.factorial(n)) 42 | 43 | 44 | def compute_volume(mesh): 45 | if "tetra" in mesh.cells_dict: 46 | vol = math.fsum( 47 | get_simplex_volumes(*prune_nodes(mesh.points, mesh.cells_dict["tetra"])) 48 | ) 49 | elif "triangle" in mesh.cells_dict or "quad" in mesh.cells_dict: 50 | vol = 0.0 51 | if "triangle" in mesh.cells_dict: 52 | # triangles 53 | vol += math.fsum( 54 | get_triangle_volumes( 55 | *prune_nodes(mesh.points, mesh.cells_dict["triangle"]) 56 | ) 57 | ) 58 | if "quad" in mesh.cells_dict: 59 | # quad: treat as two triangles 60 | quads = mesh.cells_dict["quad"].T 61 | split_cells = np.column_stack( 62 | [[quads[0], quads[1], quads[2]], [quads[0], quads[2], quads[3]]] 63 | ).T 64 | vol += math.fsum( 65 | get_triangle_volumes(*prune_nodes(mesh.points, split_cells)) 66 | ) 67 | else: 68 | assert "line" in mesh.cells_dict 69 | segs = np.diff(mesh.points[mesh.cells_dict["line"]], axis=1).squeeze() 70 | vol = np.sum(np.sqrt(np.einsum("...j, ...j", segs, segs))) 71 | 72 | return vol 73 | 74 | 75 | def plot(filename, points, triangles): 76 | from matplotlib import pyplot as plt 77 | 78 | pts = points[:, :2] 79 | for e in triangles: 80 | for idx in [[0, 1], [1, 2], [2, 0]]: 81 | X = pts[e[idx]] 82 | plt.plot(X[:, 0], X[:, 1], "-k") 83 | plt.gca().set_aspect("equal", "datalim") 84 | plt.axis("off") 85 | 86 | # plt.show() 87 | plt.savefig(filename, transparent=True) 88 | -------------------------------------------------------------------------------- /tests/occ/test_ball_with_stick.py: -------------------------------------------------------------------------------- 1 | import pygmsh 2 | 3 | 4 | def test(): 5 | with pygmsh.occ.Geometry() as geom: 6 | geom.characteristic_length_min = 0.1 7 | geom.characteristic_length_max = 0.1 8 | 9 | ball = geom.add_ball([0.0, 0.0, 0.0], 1.0) 10 | box1 = geom.add_box([0, 0, 0], [1, 1, 1]) 11 | box2 = geom.add_box([-2, -0.5, -0.5], [1.5, 0.8, 0.8]) 12 | 13 | cut = geom.boolean_difference(ball, box1) 14 | frag = geom.boolean_fragments(cut, box2) 15 | 16 | # The three fragments are: 17 | # frag[0]: The ball with two cuts 18 | # frag[1]: The intersection of the stick and the ball 19 | # frag[2]: The stick without the ball 20 | geom.add_physical([frag[0], frag[1]], label="Sphere cut by box 1") 21 | geom.add_physical(frag[2], label="Box 2 cut by sphere") 22 | 23 | mesh = geom.generate_mesh(algorithm=6) 24 | 25 | assert "Sphere cut by box 1" in mesh.cell_sets 26 | assert "Box 2 cut by sphere" in mesh.cell_sets 27 | 28 | # mesh.remove_lower_dimensional_cells() 29 | # mesh.sets_to_int_data() 30 | return mesh 31 | 32 | 33 | if __name__ == "__main__": 34 | test().write("ball-with-stick.vtu") 35 | -------------------------------------------------------------------------------- /tests/occ/test_logo.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | # test setters, getters 9 | print(geom.characteristic_length_min) 10 | print(geom.characteristic_length_max) 11 | geom.characteristic_length_min = 2.0 12 | geom.characteristic_length_max = 2.0 13 | 14 | rect1 = geom.add_rectangle([10.0, 0.0, 0.0], 20.0, 40.0, corner_radius=5.0) 15 | rect2 = geom.add_rectangle([0.0, 10.0, 0.0], 40.0, 20.0, corner_radius=5.0) 16 | disk1 = geom.add_disk([14.5, 35.0, 0.0], 1.85) 17 | disk2 = geom.add_disk([25.5, 5.0, 0.0], 1.85) 18 | 19 | rect3 = geom.add_rectangle([10.0, 30.0, 0.0], 10.0, 1.0) 20 | rect4 = geom.add_rectangle([20.0, 9.0, 0.0], 10.0, 1.0) 21 | 22 | r1 = geom.add_rectangle([9.0, 0.0, 0.0], 21.0, 20.5, corner_radius=8.0) 23 | r2 = geom.add_rectangle([10.0, 00.0, 0.0], 20.0, 19.5, corner_radius=7.0) 24 | diff1 = geom.boolean_difference(r1, r2) 25 | r22 = geom.add_rectangle([9.0, 10.0, 0.0], 11.0, 11.0) 26 | inter1 = geom.boolean_intersection([diff1, r22]) 27 | 28 | r3 = geom.add_rectangle([10.0, 19.5, 0.0], 21.0, 21.0, corner_radius=8.0) 29 | r4 = geom.add_rectangle([10.0, 20.5, 0.0], 20.0, 20.0, corner_radius=7.0) 30 | diff2 = geom.boolean_difference(r3, r4) 31 | r33 = geom.add_rectangle([20.0, 19.0, 0.0], 11.0, 11.0) 32 | inter2 = geom.boolean_intersection([diff2, r33]) 33 | 34 | geom.boolean_difference( 35 | geom.boolean_union([rect1, rect2]), 36 | geom.boolean_union([disk1, disk2, rect3, rect4, inter1, inter2]), 37 | ) 38 | 39 | mesh = geom.generate_mesh() 40 | ref = 1082.4470502181903 41 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 42 | return mesh 43 | 44 | 45 | if __name__ == "__main__": 46 | mesh = test() 47 | points = mesh.points 48 | cells = mesh.get_cells_type("triangle") 49 | 50 | # import optimesh 51 | 52 | # # points, cells = optimesh.cvt.quasi_newton_uniform_lloyd( 53 | # # points, cells, 1.0e-5, 1000, omega=2.0, verbose=True 54 | # # ) 55 | # # points, cells = optimesh.cvt.quasi_newton_uniform_blocks( 56 | # # points, cells, 1.0e-5, 1000, verbose=True 57 | # # ) 58 | # points, cells = optimesh.cvt.quasi_newton_uniform_full( 59 | # points, cells, 1.0e-5, 1000, verbose=True 60 | # ) 61 | 62 | # # from helpers import plot 63 | # # plot("logo.png", points, {"triangle": cells}) 64 | import meshio 65 | 66 | # meshio.write_points_cells("logo.vtu", points, {"triangle": cells}) 67 | mesh = meshio.Mesh(points, {"triangle": cells}) 68 | meshio.svg.write( 69 | "logo.svg", mesh, float_fmt=".3f", stroke_width="1", force_width=300 70 | ) 71 | -------------------------------------------------------------------------------- /tests/occ/test_meshio_logo.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | container = geom.add_rectangle([0.0, 0.0, 0.0], 10.0, 10.0) 9 | 10 | letter_i = geom.add_rectangle([2.0, 2.0, 0.0], 1.0, 4.5) 11 | i_dot = geom.add_disk([2.5, 7.5, 0.0], 0.6) 12 | 13 | disk1 = geom.add_disk([6.25, 4.5, 0.0], 2.5) 14 | disk2 = geom.add_disk([6.25, 4.5, 0.0], 1.5) 15 | letter_o = geom.boolean_difference(disk1, disk2) 16 | 17 | geom.boolean_difference( 18 | container, geom.boolean_union([letter_i, i_dot, letter_o]) 19 | ) 20 | 21 | mesh = geom.generate_mesh() 22 | 23 | ref = 81.9131851877 24 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 25 | return mesh 26 | 27 | 28 | if __name__ == "__main__": 29 | # import meshio 30 | # meshio.write_points_cells('m.vtu', *test()) 31 | from helpers import plot 32 | 33 | mesh = test() 34 | plot("meshio_logo.png", mesh.points, mesh.get_cells_type("triangle")) 35 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_ball.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.add_ball([0.0, 0.0, 0.0], 1.0, mesh_size=0.1) 11 | mesh = geom.generate_mesh() 12 | 13 | ref = 4 / 3 * pi 14 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 15 | return mesh 16 | 17 | 18 | if __name__ == "__main__": 19 | test().write("occ_ball.vtu") 20 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_boolean.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test_union(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.characteristic_length_min = 0.1 11 | geom.characteristic_length_max = 0.1 12 | rectangle = geom.add_rectangle([-1.0, -1.0, 0.0], 2.0, 2.0) 13 | disk_w = geom.add_disk([-1.0, 0.0, 0.0], 0.5) 14 | disk_e = geom.add_disk([+1.0, 0.0, 0.0], 0.5) 15 | geom.boolean_union([rectangle, disk_w, disk_e]) 16 | mesh = geom.generate_mesh() 17 | 18 | ref = 4.780361 19 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 20 | return mesh 21 | 22 | 23 | def test_intersection(): 24 | with pygmsh.occ.Geometry() as geom: 25 | angles = [math.pi * 3 / 6, math.pi * 7 / 6, math.pi * 11 / 6] 26 | disks = [ 27 | geom.add_disk([math.cos(angles[0]), math.sin(angles[0]), 0.0], 1.5), 28 | geom.add_disk([math.cos(angles[1]), math.sin(angles[1]), 0.0], 1.5), 29 | geom.add_disk([math.cos(angles[2]), math.sin(angles[2]), 0.0], 1.5), 30 | ] 31 | geom.boolean_intersection(disks) 32 | mesh = geom.generate_mesh() 33 | 34 | ref = 1.0290109753807914 35 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 36 | return mesh 37 | 38 | 39 | def test_difference(): 40 | with pygmsh.occ.Geometry() as geom: 41 | geom.characteristic_length_min = 0.1 42 | geom.characteristic_length_max = 0.1 43 | rectangle = geom.add_rectangle([-1.0, -1.0, 0.0], 2.0, 2.0) 44 | disk_w = geom.add_disk([-1.0, 0.0, 0.0], 0.5) 45 | disk_e = geom.add_disk([+1.0, 0.0, 0.0], 0.5) 46 | geom.boolean_union([disk_w, disk_e]) 47 | geom.boolean_difference(rectangle, geom.boolean_union([disk_w, disk_e])) 48 | mesh = geom.generate_mesh() 49 | 50 | ref = 3.2196387 51 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 52 | return mesh 53 | 54 | 55 | def test_all(): 56 | with pygmsh.occ.Geometry() as geom: 57 | geom.characteristic_length_min = 0.1 58 | geom.characteristic_length_max = 0.1 59 | 60 | rectangle = geom.add_rectangle([-1.0, -1.0, 0.0], 2.0, 2.0) 61 | disk1 = geom.add_disk([-1.0, 0.0, 0.0], 0.5) 62 | disk2 = geom.add_disk([+1.0, 0.0, 0.0], 0.5) 63 | union = geom.boolean_union([rectangle, disk1, disk2]) 64 | 65 | disk3 = geom.add_disk([0.0, -1.0, 0.0], 0.5) 66 | disk4 = geom.add_disk([0.0, +1.0, 0.0], 0.5) 67 | geom.boolean_difference(union, geom.boolean_union([disk3, disk4])) 68 | mesh = geom.generate_mesh() 69 | 70 | ref = 4.0 71 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 72 | return mesh 73 | 74 | 75 | if __name__ == "__main__": 76 | test_difference().write("boolean.vtu") 77 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_booleans.py: -------------------------------------------------------------------------------- 1 | """Test module for boolean operations.""" 2 | import meshio 3 | import numpy as np 4 | import pytest 5 | from helpers import compute_volume 6 | 7 | import pygmsh 8 | 9 | 10 | def square_loop(geom): 11 | """Construct square using built in geometry.""" 12 | points = [ 13 | geom.add_point([-0.5, -0.5], 0.05), 14 | geom.add_point([-0.5, 0.5], 0.05), 15 | geom.add_point([0.5, 0.5], 0.05), 16 | geom.add_point([0.5, -0.5], 0.05), 17 | ] 18 | lines = [ 19 | geom.add_line(points[0], points[1]), 20 | geom.add_line(points[1], points[2]), 21 | geom.add_line(points[2], points[3]), 22 | geom.add_line(points[3], points[0]), 23 | ] 24 | return geom.add_curve_loop(lines) 25 | 26 | 27 | def circle_loop(geom): 28 | """construct circle using geo geometry module.""" 29 | points = [ 30 | geom.add_point([+0.0, +0.0], 0.05), 31 | geom.add_point([+0.0, +0.1], 0.05), 32 | geom.add_point([-0.1, +0.0], 0.05), 33 | geom.add_point([+0.0, -0.1], 0.05), 34 | geom.add_point([+0.1, +0.0], 0.05), 35 | ] 36 | quarter_circles = [ 37 | geom.add_circle_arc(points[1], points[0], points[2]), 38 | geom.add_circle_arc(points[2], points[0], points[3]), 39 | geom.add_circle_arc(points[3], points[0], points[4]), 40 | geom.add_circle_arc(points[4], points[0], points[1]), 41 | ] 42 | return geom.add_curve_loop(quarter_circles) 43 | 44 | 45 | def _square_hole_classical(geom): 46 | """Construct surface using builtin and boolean methods.""" 47 | # construct surface with hole using standard built in 48 | geom.characteristic_length_min = 0.05 49 | geom.characteristic_length_max = 0.05 50 | square = square_loop(geom) 51 | circle = circle_loop(geom) 52 | geom.add_plane_surface(square, [circle]) 53 | 54 | 55 | def _square_hole_cad(geom): 56 | # construct surface using boolean 57 | geom.characteristic_length_min = 0.05 58 | geom.characteristic_length_max = 0.05 59 | square2 = square_loop(geom) 60 | curve_loop2 = circle_loop(geom) 61 | surf1 = geom.add_plane_surface(square2) 62 | surf2 = geom.add_plane_surface(curve_loop2) 63 | geom.boolean_difference(surf1, surf2) 64 | 65 | 66 | @pytest.mark.parametrize("fun", [_square_hole_classical, _square_hole_cad]) 67 | def test_square_circle_hole(fun): 68 | """Test planar surface with holes. 69 | 70 | Construct it with boolean operations and verify that it is the same. 71 | """ 72 | with pygmsh.occ.Geometry() as geom: 73 | fun(geom) 74 | mesh = geom.generate_mesh() 75 | surf = 1 - 0.1**2 * np.pi 76 | assert np.abs((compute_volume(mesh) - surf) / surf) < 1e-3 77 | 78 | 79 | @pytest.mark.skip() 80 | def test_square_circle_slice(): 81 | """Test planar surface square with circular hole. 82 | 83 | Also test for surface area of fragments. 84 | """ 85 | with pygmsh.occ.Geometry() as geom: 86 | square = square_loop(geom) 87 | curve_loop = circle_loop(geom) 88 | surf1 = geom.add_plane_surface(square) 89 | surf2 = geom.add_plane_surface(curve_loop) 90 | geom.boolean_fragments(surf1, surf2) 91 | mesh = geom.generate_mesh() 92 | 93 | ref = 1.0 94 | val = compute_volume(mesh) 95 | assert np.abs(val - ref) < 1e-3 * ref 96 | 97 | # Gmsh 4 default format MSH4 doesn't have geometrical entities. 98 | outer_mask = np.where(mesh.cell_data["gmsh:geometrical"][2] == 13)[0] 99 | outer_cells = {} 100 | outer_cells["triangle"] = mesh.cells_dict["triangle"][outer_mask] 101 | 102 | inner_mask = np.where(mesh.cell_data["gmsh:geometrical"][2] == 12)[0] 103 | inner_cells = {} 104 | inner_cells["triangle"] = mesh.cells_dict["triangle"][inner_mask] 105 | 106 | ref = 1 - 0.1**2 * np.pi 107 | value = compute_volume(meshio.Mesh(mesh.points, outer_cells)) 108 | assert np.abs(value - ref) < 1e-2 * ref 109 | 110 | 111 | @pytest.mark.skip("cell data not working yet") 112 | def test_fragments_diff_union(): 113 | """Test planar surface with holes. 114 | 115 | Construct it with boolean operations and verify that it is the same. 116 | """ 117 | # construct surface using boolean 118 | with pygmsh.occ.Geometry() as geom: 119 | geom.characteristic_length_min = 0.04 120 | geom.characteristic_length_max = 0.04 121 | square = square_loop(geom) 122 | surf1 = geom.add_plane_surface(square) 123 | curve_loop = circle_loop(geom) 124 | surf2 = geom.add_plane_surface(curve_loop) 125 | 126 | geom.add_physical([surf1], label="1") 127 | geom.add_physical([surf2], label="2") 128 | geom.boolean_difference(surf1, surf2, delete_other=False) 129 | mesh = geom.generate_mesh() 130 | 131 | ref = 1.0 132 | assert np.abs(compute_volume(mesh) - ref) < 1e-3 * ref 133 | 134 | surf = 1 - 0.1**2 * np.pi 135 | outer_mask = np.where(mesh.cell_data_dict["gmsh:geometrical"]["triangle"] == 1)[0] 136 | outer_cells = {} 137 | outer_cells["triangle"] = mesh.cells_dict["triangle"][outer_mask] 138 | 139 | inner_mask = np.where(mesh.cell_data_dict["gmsh:geometrical"]["triangle"] == 2)[0] 140 | inner_cells = {} 141 | inner_cells["triangle"] = mesh.cells_dict["triangle"][inner_mask] 142 | 143 | value = compute_volume(meshio.Mesh(mesh.points, outer_cells)) 144 | assert np.abs(value - surf) < 1e-2 * surf 145 | 146 | 147 | @pytest.mark.skip("cell data not working yet") 148 | def test_diff_physical_assignment(): 149 | """construct surface using boolean. 150 | 151 | Ensure that after a difference operation the initial volume physical label 152 | is kept for the operated geometry. 153 | """ 154 | with pygmsh.occ.Geometry() as geom: 155 | geom.characteristic_length_min = 0.05 156 | geom.characteristic_length_max = 0.05 157 | square2 = square_loop(geom) 158 | curve_loop2 = circle_loop(geom) 159 | surf1 = geom.add_plane_surface(square2) 160 | surf2 = geom.add_plane_surface(curve_loop2) 161 | geom.add_physical(surf1, label="1") 162 | geom.boolean_difference(surf1, surf2) 163 | mesh = geom.generate_mesh() 164 | assert np.allclose( 165 | mesh.cell_data_dict["gmsh:geometrical"]["triangle"], 166 | np.ones(mesh.cells_dict["triangle"].shape[0]), 167 | ) 168 | surf = 1 - 0.1**2 * np.pi 169 | assert np.abs((compute_volume(mesh) - surf) / surf) < 1e-3 170 | 171 | 172 | def test_polygon_diff(): 173 | with pygmsh.occ.Geometry() as geom: 174 | poly = geom.add_polygon([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]) 175 | disk = geom.add_disk([0, 0, 0], 0.5) 176 | geom.boolean_difference(poly, disk) 177 | 178 | 179 | def test_mesh_size_removal(): 180 | with pygmsh.occ.Geometry() as geom: 181 | box0 = geom.add_box([0.0, 0, 0], [1, 1, 1], mesh_size=0.1) 182 | box1 = geom.add_box([0.5, 0.5, 1], [0.5, 0.5, 1], mesh_size=0.2) 183 | geom.boolean_union([box0, box1]) 184 | geom.generate_mesh() 185 | 186 | 187 | if __name__ == "__main__": 188 | test_square_circle_slice() 189 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_box.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | geom.add_box([0.0, 0.0, 0.0], [1, 2, 3], mesh_size=0.1) 9 | mesh = geom.generate_mesh() 10 | 11 | ref = 6.0 12 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 13 | return mesh 14 | 15 | 16 | if __name__ == "__main__": 17 | test().write("occ_box.vtu") 18 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_builtin_mix.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | geom.characteristic_length_max = 0.1 9 | p0 = geom.add_point([-0.5, -0.5, 0], 0.01) 10 | p1 = geom.add_point([+0.5, -0.5, 0], 0.01) 11 | p2 = geom.add_point([+0.5, +0.5, 0], 0.01) 12 | p3 = geom.add_point([-0.5, +0.5, 0], 0.01) 13 | l0 = geom.add_line(p0, p1) 14 | l1 = geom.add_line(p1, p2) 15 | l2 = geom.add_line(p2, p3) 16 | l3 = geom.add_line(p3, p0) 17 | ll0 = geom.add_curve_loop([l0, l1, l2, l3]) 18 | square_builtin = geom.add_plane_surface(ll0) 19 | square_occ = geom.add_rectangle([0, 0, 0], 1.0, 1.0) 20 | geom.boolean_difference(square_occ, square_builtin) 21 | mesh = geom.generate_mesh() 22 | 23 | ref = 0.75 24 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 25 | return mesh 26 | 27 | 28 | if __name__ == "__main__": 29 | test().write("mix.vtu") 30 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_cone.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.add_cone( 11 | [0.0, 0.0, 0.0], 12 | [0.0, 0.0, 1.0], 13 | 1.0, 14 | 0.3, 15 | mesh_size=0.1, 16 | angle=1.25 * pi, 17 | ) 18 | mesh = geom.generate_mesh() 19 | 20 | ref = 0.90779252263 21 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 22 | return mesh 23 | 24 | 25 | if __name__ == "__main__": 26 | test().write("occ_cone.vtu") 27 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_cylinder.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.add_cylinder( 11 | [0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 0.5, 0.25 * pi, mesh_size=0.1 12 | ) 13 | mesh = geom.generate_mesh() 14 | 15 | ref = 0.097625512963 16 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 17 | return mesh 18 | 19 | 20 | if __name__ == "__main__": 21 | test().write("occ_cylinder.vtu") 22 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_ellipsoid.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.add_ellipsoid([1.0, 1.0, 1.0], [1.0, 2.0, 3.0], mesh_size=0.1) 11 | mesh = geom.generate_mesh() 12 | 13 | ref = 8.0 * pi 14 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 15 | return mesh 16 | 17 | 18 | if __name__ == "__main__": 19 | test().write("occ_ellipsoid.vtu") 20 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_extrude.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | geom.characteristic_length_max = 0.05 9 | 10 | rectangle = geom.add_rectangle([-1.0, -1.0, 0.0], 2.0, 2.0, corner_radius=0.2) 11 | disk1 = geom.add_disk([-1.2, 0.0, 0.0], 0.5) 12 | disk2 = geom.add_disk([+1.2, 0.0, 0.0], 0.5, 0.3) 13 | 14 | disk3 = geom.add_disk([0.0, -0.9, 0.0], 0.5) 15 | disk4 = geom.add_disk([0.0, +0.9, 0.0], 0.5) 16 | flat = geom.boolean_difference( 17 | geom.boolean_union([rectangle, disk1, disk2]), 18 | geom.boolean_union([disk3, disk4]), 19 | ) 20 | geom.extrude(flat, [0, 0, 0.3]) 21 | mesh = geom.generate_mesh() 22 | 23 | ref = 1.1742114942 24 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 25 | return mesh 26 | 27 | 28 | def test2(): 29 | with pygmsh.occ.Geometry() as geom: 30 | geom.characteristic_length_max = 1.0 31 | 32 | mesh_size = 1 33 | h = 25 34 | w = 10 35 | length = 100 36 | # x_fin = -0.5 * length 37 | cr = 1 38 | 39 | f = 0.5 * w 40 | y = [-f, -f + cr, +f - cr, +f] 41 | z = [0.0, h - cr, h] 42 | f = 0.5 * cr 43 | x = [-f, f] 44 | points = [ 45 | geom.add_point((x[0], y[0], z[0]), mesh_size=mesh_size), 46 | geom.add_point((x[0], y[0], z[1]), mesh_size=mesh_size), 47 | geom.add_point((x[0], y[1], z[1]), mesh_size=mesh_size), 48 | geom.add_point((x[0], y[1], z[2]), mesh_size=mesh_size), 49 | geom.add_point((x[0], y[2], z[2]), mesh_size=mesh_size), 50 | geom.add_point((x[0], y[2], z[1]), mesh_size=mesh_size), 51 | geom.add_point((x[0], y[3], z[1]), mesh_size=mesh_size), 52 | geom.add_point((x[0], y[3], z[0]), mesh_size=mesh_size), 53 | ] 54 | 55 | lines = [ 56 | geom.add_line(points[0], points[1]), 57 | geom.add_circle_arc(points[1], points[2], points[3]), 58 | geom.add_line(points[3], points[4]), 59 | geom.add_circle_arc(points[4], points[5], points[6]), 60 | geom.add_line(points[6], points[7]), 61 | geom.add_line(points[7], points[0]), 62 | ] 63 | 64 | curve_loop = geom.add_curve_loop(lines) 65 | surface = geom.add_plane_surface(curve_loop) 66 | geom.extrude(surface, translation_axis=[length, 0, 0]) 67 | 68 | mesh = geom.generate_mesh() 69 | 70 | ref = 24941.503891355664 71 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 72 | return mesh 73 | 74 | 75 | if __name__ == "__main__": 76 | test().write("occ_extrude.vtu") 77 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_regular_extrusion.py: -------------------------------------------------------------------------------- 1 | """Creates regular cube mesh by extrusion. 2 | """ 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | x = 5 10 | y = 4 11 | z = 3 12 | x_layers = 10 13 | y_layers = 5 14 | z_layers = 3 15 | with pygmsh.occ.Geometry() as geom: 16 | p = geom.add_point([0, 0, 0], 1) 17 | _, l, _ = geom.extrude(p, [x, 0, 0], num_layers=x_layers) 18 | _, s, _ = geom.extrude(l, [0, y, 0], num_layers=y_layers) 19 | geom.extrude(s, [0, 0, z], num_layers=z_layers) 20 | mesh = geom.generate_mesh() 21 | 22 | ref_vol = x * y * z 23 | assert abs(compute_volume(mesh) - ref_vol) < 1.0e-2 * ref_vol 24 | 25 | # Each grid-cell from layered extrusion will result in 6 tetrahedrons. 26 | ref_tetras = 6 * x_layers * y_layers * z_layers 27 | assert len(mesh.cells_dict["tetra"]) == ref_tetras 28 | 29 | return mesh 30 | 31 | 32 | if __name__ == "__main__": 33 | test().write("cube.vtu") 34 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_torus.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.occ.Geometry() as geom: 10 | geom.add_torus([0.0, 0.0, 0.0], 1.0, 0.3, 1.25 * pi, mesh_size=0.1) 11 | mesh = geom.generate_mesh() 12 | 13 | ref = 1.09994740709 14 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 15 | return mesh 16 | 17 | 18 | if __name__ == "__main__": 19 | test().write("occ_torus.vtu") 20 | -------------------------------------------------------------------------------- /tests/occ/test_opencascade_wedge.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test(): 7 | with pygmsh.occ.Geometry() as geom: 8 | geom.add_wedge([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], top_extent=0.4, mesh_size=0.1) 9 | mesh = geom.generate_mesh() 10 | 11 | ref = 0.7 12 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 13 | return mesh 14 | 15 | 16 | if __name__ == "__main__": 17 | test().write("occ_wedge.vtu") 18 | -------------------------------------------------------------------------------- /tests/occ/test_refinement.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | import pytest 4 | 5 | import pygmsh 6 | 7 | 8 | @pytest.mark.skip("Only works in Gmsh 4.7.0+") 9 | def test(): 10 | with pygmsh.occ.Geometry() as geom: 11 | geom.add_ball([0.0, 0.0, 0.0], 1.0) 12 | geom.set_mesh_size_callback( 13 | lambda dim, tag, x, y, z: abs(sqrt(x**2 + y**2 + z**2) - 0.5) + 0.1 14 | ) 15 | mesh = geom.generate_mesh() 16 | 17 | assert mesh.cells[0].data.shape[0] > 1500 18 | -------------------------------------------------------------------------------- /tests/occ/test_translations.py: -------------------------------------------------------------------------------- 1 | """Test translation for all dimensions.""" 2 | import numpy as np 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | # def test_translation1d(): 8 | # """Translation of a line.""" 9 | # geom = pygmsh.geo.Geometry() 10 | # points = [] 11 | # for array in [[1, 0, 0], [0, 0, 0], [0, 1, 0]]: 12 | # points.append(geom.add_point(array, 0.5)) 13 | # circle = geom.add_circle_arc(*points) 14 | # # mesh = geom.generate_mesh() 15 | # geom.translate(circle, [1.5, 0, 0]) 16 | # translated_mesh = geom.generate_mesh() 17 | # points[:, 0] = points[:, 0] + 1.5 18 | # assert np.allclose(points, translated_mesh.points) 19 | 20 | 21 | def test_translation2d(): 22 | """Translation of a surface object.""" 23 | with pygmsh.occ.Geometry() as geom: 24 | geom.characteristic_length_min = 0.05 25 | geom.characteristic_length_max = 0.05 26 | disk = geom.add_disk([0, 0, 0], 1) 27 | disk2 = geom.add_disk([1.5, 0, 0], 1) 28 | geom.translate(disk, [1.5, 0, 0]) 29 | geom.boolean_union([disk2, disk]) 30 | mesh = geom.generate_mesh() 31 | surf = np.pi 32 | assert np.abs(compute_volume(mesh) - surf) < 1e-3 * surf 33 | 34 | 35 | def test_translation3d(): 36 | """Translation of a volume object.""" 37 | with pygmsh.occ.Geometry() as geom: 38 | geom.characteristic_length_min = 0.2 39 | geom.characteristic_length_max = 0.2 40 | ball = geom.add_ball([0, 0, 0], 1) 41 | ball2 = geom.add_ball([1.5, 0, 0], 1) 42 | geom.translate(ball, [1.5, 0, 0]) 43 | geom.boolean_union([ball2, ball]) 44 | mesh = geom.generate_mesh() 45 | surf = 4 / 3 * np.pi 46 | assert np.abs(compute_volume(mesh) - surf) < 2e-2 * surf 47 | 48 | 49 | if __name__ == "__main__": 50 | # test_translation1d() 51 | test_translation2d() 52 | test_translation3d() 53 | -------------------------------------------------------------------------------- /tests/test_boundary_layers.py: -------------------------------------------------------------------------------- 1 | from helpers import compute_volume 2 | 3 | import pygmsh 4 | 5 | 6 | def test_geo(): 7 | with pygmsh.geo.Geometry() as geom: 8 | poly = geom.add_polygon( 9 | [ 10 | [0.0, 0.0, 0.0], 11 | [2.0, 0.0, 0.0], 12 | [3.0, 1.0, 0.0], 13 | [1.0, 2.0, 0.0], 14 | [0.0, 1.0, 0.0], 15 | ], 16 | mesh_size=0.1, 17 | ) 18 | 19 | field0 = geom.add_boundary_layer( 20 | edges_list=[poly.curve_loop.curves[0]], 21 | lcmin=0.01, 22 | lcmax=0.1, 23 | distmin=0.0, 24 | distmax=0.2, 25 | ) 26 | field1 = geom.add_boundary_layer( 27 | nodes_list=[poly.curve_loop.curves[1].points[1]], 28 | lcmin=0.01, 29 | lcmax=0.1, 30 | distmin=0.0, 31 | distmax=0.2, 32 | ) 33 | geom.set_background_mesh([field0, field1], operator="Min") 34 | 35 | ref = 4.0 36 | mesh = geom.generate_mesh() 37 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 38 | 39 | return mesh 40 | 41 | 42 | def test_occ(): 43 | with pygmsh.occ.Geometry() as geom: 44 | geom.add_rectangle([0.0, 0.5, 0.0], 5.0, 0.5) 45 | 46 | edge1 = pygmsh.occ.dummy.Dummy(dim=1, id0=1) 47 | point1 = pygmsh.occ.dummy.Dummy(dim=0, id0=3) 48 | 49 | field0 = geom.add_boundary_layer( 50 | edges_list=[edge1], 51 | lcmin=0.01, 52 | lcmax=0.1, 53 | distmin=0.0, 54 | distmax=0.2, 55 | num_points_per_curve=50, 56 | ) 57 | field1 = geom.add_boundary_layer( 58 | nodes_list=[point1], 59 | lcmin=0.01, 60 | lcmax=0.1, 61 | distmin=0.0, 62 | distmax=0.2, 63 | num_points_per_curve=50, 64 | ) 65 | geom.set_background_mesh([field0, field1], operator="Min") 66 | 67 | ref = 2.5 68 | mesh = geom.generate_mesh() 69 | assert abs(compute_volume(mesh) - ref) < 1.0e-2 * ref 70 | return mesh 71 | 72 | 73 | if __name__ == "__main__": 74 | test_geo().write("boundary_layers_geo.vtu") 75 | test_occ().write("boundary_layers_occ.vtu") 76 | -------------------------------------------------------------------------------- /tests/test_extrusion_entities.py: -------------------------------------------------------------------------------- 1 | """Create several entities by extrusion, check that the expected 2 | sub-entities are returned and the resulting mesh is correct. 3 | """ 4 | import numpy as np 5 | import pytest 6 | 7 | import pygmsh 8 | 9 | 10 | @pytest.mark.parametrize("kernel", [pygmsh.geo, pygmsh.occ]) 11 | def test(kernel): 12 | with kernel.Geometry() as geom: 13 | p = geom.add_point([0, 0], 1) 14 | p_top, _, _ = geom.extrude(p, translation_axis=[1, 0, 0]) 15 | 16 | # The mesh should now contain exactly two points, the second one should be where 17 | # the translation pointed. 18 | mesh = geom.generate_mesh() 19 | assert len(mesh.points) == 2 20 | assert np.array_equal(mesh.points[-1], [1, 0, 0]) 21 | 22 | # Check that the top entity (a PointBase) can be extruded correctly again. 23 | _, _, _ = geom.extrude(p_top, translation_axis=[1, 0, 0]) 24 | mesh = geom.generate_mesh() 25 | assert len(mesh.points) == 3 26 | assert np.array_equal(mesh.points[-1], [2, 0, 0]) 27 | 28 | # Set up new geometry with one line. 29 | with kernel.Geometry() as geom: 30 | p1 = geom.add_point([0, 0], 1.0) 31 | p2 = geom.add_point([1, 0], 1.0) 32 | line = geom.add_line(p1, p2) 33 | 34 | l_top, _, _ = geom.extrude(line, [0, 1, 0]) 35 | mesh = geom.generate_mesh() 36 | assert len(mesh.points) == 5 37 | assert np.array_equal(mesh.points[-2], [1, 1, 0]) 38 | 39 | # Check again for top entity (a LineBase). 40 | _, _, _ = geom.extrude(l_top, [0, 1, 0]) 41 | mesh = geom.generate_mesh() 42 | assert len(mesh.points) == 8 43 | assert np.array_equal(mesh.points[-3], [1, 2, 0]) 44 | 45 | # Check that extrusion works on a Polygon 46 | poly = geom.add_polygon([[5.0, 0.0], [6.0, 0.0], [5.0, 1.0]], mesh_size=1e20) 47 | a, b, poly_lat = geom.extrude(poly, [0.0, 0.0, 1.0], num_layers=1) 48 | mesh = geom.generate_mesh() 49 | assert len(mesh.points) == 8 + 6 50 | assert len(poly_lat) == 3 51 | 52 | 53 | if __name__ == "__main__": 54 | test(pygmsh.geo) 55 | # test(pygmsh.occ) 56 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | """Tests module for helpers in tests.""" 2 | import numpy as np 3 | from helpers import compute_volume 4 | 5 | import pygmsh 6 | 7 | 8 | def test(): 9 | with pygmsh.geo.Geometry() as geom: 10 | geom.add_circle([0, 0, 0], 1, 0.1, make_surface=False) 11 | mesh = geom.generate_mesh() 12 | ref = 2 * np.pi 13 | assert np.abs(compute_volume(mesh) - ref) < 1e-2 * ref 14 | 15 | 16 | def test_save_geo(): 17 | with pygmsh.geo.Geometry() as geom: 18 | geom.add_circle([0, 0, 0], 1, 0.1, make_surface=False) 19 | geom.save_geometry("out.geo_unrolled") 20 | 21 | 22 | if __name__ == "__main__": 23 | test() 24 | -------------------------------------------------------------------------------- /tests/test_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_raise_duplicate(): 5 | import pygmsh 6 | 7 | with pygmsh.geo.Geometry() as geom: 8 | p = geom.add_rectangle(-1, 1, -1, 1, z=0, mesh_size=1) 9 | geom.add_physical(p.lines[0], label="A") 10 | with pytest.raises(ValueError): 11 | geom.add_physical(p.lines[1], label="A") 12 | -------------------------------------------------------------------------------- /tests/test_optimize.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import pygmsh 4 | 5 | 6 | @pytest.mark.skip() 7 | def test(): 8 | with pygmsh.occ.Geometry() as geom: 9 | geom.add_ball([0.0, 0.0, 0.0], 1.0, mesh_size=0.1) 10 | mesh = geom.generate_mesh() 11 | 12 | pygmsh.optimize(mesh) 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3 3 | isolated_build = True 4 | 5 | [testenv] 6 | deps = 7 | gmsh 8 | matplotlib 9 | pytest 10 | pytest-codeblocks 11 | pytest-cov 12 | extras = all 13 | commands = 14 | pytest {posargs} --codeblocks 15 | --------------------------------------------------------------------------------