├── .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 |
3 |
Gmsh for Python.
4 |
5 |
6 | [](https://pypi.org/project/pygmsh/)
7 | [](https://pypi.org/project/pygmsh/)
8 | [](https://doi.org/10.5281/zenodo.1173105)
9 | [](https://github.com/nschloe/pygmsh)
10 | [](https://pypistats.org/packages/pygmsh)
11 |
12 | [](https://discord.gg/hnTJ5MRX2Y)
13 | [](https://pygmsh.readthedocs.io/en/latest/?badge=latest)
14 |
15 | [](https://github.com/nschloe/pygmsh/actions?query=workflow%3Aci)
16 | [](https://codecov.io/gh/nschloe/pygmsh)
17 | [](https://lgtm.com/projects/g/nschloe/pygmsh)
18 | [](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 |
--------------------------------------------------------------------------------