cadgmsh 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cadgmsh-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Straub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cadgmsh-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: cadgmsh
3
+ Version: 0.1.0
4
+ Summary: Mesh CadQuery/build123d geometry with gmsh
5
+ Author-email: David Straub <straub@protonmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 David Straub
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/DavidMStraub/cadgmsh
29
+ Requires-Python: >=3.10
30
+ License-File: LICENSE
31
+ Requires-Dist: gmsh
32
+ Requires-Dist: meshio
33
+ Requires-Dist: numpy
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest; extra == "test"
36
+ Requires-Dist: pytest-cov; extra == "test"
37
+ Requires-Dist: build123d; extra == "test"
38
+ Requires-Dist: cadquery; extra == "test"
39
+ Provides-Extra: dev
40
+ Requires-Dist: cadgmsh[test]; extra == "dev"
41
+ Requires-Dist: pyright; extra == "dev"
42
+ Requires-Dist: ruff; extra == "dev"
43
+ Dynamic: license-file
@@ -0,0 +1,68 @@
1
+ # cadgmsh
2
+
3
+ Mesh [CadQuery](https://github.com/CadQuery/cadquery) / [build123d](https://github.com/gumyr/build123d) geometry with [gmsh](https://gmsh.info). No temp files, no exposed `initialize`/`finalize`.
4
+
5
+ ```python
6
+ import cadgmsh
7
+
8
+ mesh = cadgmsh.mesh(shape, lc=0.3)
9
+
10
+ mesh = cadgmsh.mesh(
11
+ [box, sphere],
12
+ physical={
13
+ "steel": box,
14
+ "rubber": sphere,
15
+ "fixed": box.faces().sort_by(Axis.Z)[0],
16
+ },
17
+ imprint=True,
18
+ lc=0.3,
19
+ )
20
+ ```
21
+
22
+ Returns a [`meshio.Mesh`](https://github.com/nschloe/meshio). Works with [CadQuery](https://github.com/CadQuery/cadquery) and [build123d](https://github.com/gumyr/build123d) shapes interchangeably.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install cadgmsh
28
+ ```
29
+
30
+ Requires [CadQuery](https://github.com/CadQuery/cadquery) or [build123d](https://github.com/gumyr/build123d) in your environment — neither is a hard dependency of cadgmsh itself.
31
+
32
+ ## API
33
+
34
+ ```python
35
+ cadgmsh.mesh(
36
+ shapes, # single shape or list of shapes
37
+ *,
38
+ physical=None, # dict[str, shape | list[shape]]
39
+ dim=3, # max mesh dimension (1 / 2 / 3)
40
+ order=1, # element order (1 = linear, 2 = quadratic, …)
41
+ algorithm=None, # gmsh algorithm number; None = default
42
+ lc=None, # characteristic length (max element size)
43
+ lc_min=None, # min characteristic length
44
+ imprint=False, # boolean-fragment for conforming multi-body meshes
45
+ verbose=False, # show gmsh output
46
+ ) -> meshio.Mesh
47
+ ```
48
+
49
+ **`physical`** maps string labels to shapes (solids, faces, edges, or vertices). Labels become named cell sets in the returned mesh, usable for boundary conditions and material assignment.
50
+
51
+ **`imprint`** runs `BooleanFragments` on all input shapes so that shared interfaces are meshed conformally. Required for multi-domain simulations. Note: interface faces tagged in `physical` will have their OCC pointer invalidated by the fragment operation — tag volumes instead.
52
+
53
+ ## Custom shapes
54
+
55
+ cadgmsh accepts any shape that exposes `.wrapped._address()` (OCP pybind11 pointer):
56
+
57
+ ```python
58
+ from cadgmsh import OccShape
59
+
60
+ class MyShape:
61
+ @property
62
+ def wrapped(self) -> object: # must have ._address()
63
+ return self._occ_shape
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cadgmsh"
7
+ version = "0.1.0"
8
+ description = "Mesh CadQuery/build123d geometry with gmsh"
9
+ authors = [{name = "David Straub", email = "straub@protonmail.com"}]
10
+ license = {file = "LICENSE"}
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "gmsh",
14
+ "meshio",
15
+ "numpy",
16
+ ]
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/DavidMStraub/cadgmsh"
20
+
21
+ [project.optional-dependencies]
22
+ test = ["pytest", "pytest-cov", "build123d", "cadquery"]
23
+ dev = ["cadgmsh[test]", "pyright", "ruff"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ pythonpath = ["src"]
31
+
32
+ [tool.pyright]
33
+ include = ["src"]
34
+ pythonVersion = "3.10"
35
+ typeCheckingMode = "standard"
36
+
37
+ [tool.ruff]
38
+ line-length = 88
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
42
+
43
+ [tool.ruff.lint.per-file-ignores]
44
+ "tests/*" = ["B011"] # allow assert False in tests
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from cadgmsh._mesh import mesh
2
+ from cadgmsh._types import OccShape, Shape
3
+
4
+ __all__ = ["OccShape", "Shape", "mesh"]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import gmsh
4
+ import meshio
5
+ import numpy as np
6
+ from numpy.typing import ArrayLike, NDArray
7
+
8
+
9
+ def _to_meshio() -> meshio.Mesh:
10
+ """
11
+ Extract the current gmsh model into a :class:`meshio.Mesh`.
12
+
13
+ Reads nodes, elements, and any named physical groups from the active gmsh
14
+ session and returns them as a meshio-compatible object.
15
+ Must be called while a gmsh session is active (between ``initialize`` /
16
+ ``finalize``) and after ``gmsh.model.mesh.generate``.
17
+ """
18
+ idx, raw_pts, _ = gmsh.model.mesh.getNodes()
19
+ pts: NDArray[np.float64] = np.asarray(raw_pts).reshape(-1, 3)
20
+ node_idx: NDArray[np.int64] = np.asarray(idx, dtype=np.int64) - 1
21
+ srt = np.argsort(node_idx)
22
+ if not np.all(node_idx[srt] == np.arange(len(node_idx))):
23
+ raise ValueError("gmsh returned non-consecutive node indices")
24
+ pts = pts[srt]
25
+
26
+ elem_types, elem_tags, node_tags = gmsh.model.mesh.getElements()
27
+
28
+ cells: list[tuple[str, ArrayLike] | meshio.CellBlock] = []
29
+ for etype, etags, ntags in zip(elem_types, elem_tags, node_tags, strict=True):
30
+ n: int = gmsh.model.mesh.getElementProperties(etype)[3]
31
+ conn: NDArray[np.int64] = np.asarray(ntags, dtype=np.int64).reshape(-1, n) - 1
32
+ conn = conn[np.argsort(np.asarray(etags, dtype=np.int64))]
33
+ cells.append(meshio.CellBlock(meshio.gmsh.gmsh_to_meshio_type[etype], conn))
34
+
35
+ cell_sets: dict[str, list[ArrayLike]] = {}
36
+ for dim, tag in gmsh.model.getPhysicalGroups():
37
+ name = gmsh.model.getPhysicalName(dim, tag)
38
+ groups: list[list[NDArray[np.int64]]] = [[] for _ in range(len(cells))]
39
+ for etag in gmsh.model.getEntitiesForPhysicalGroup(dim, tag):
40
+ etypes_e, etags_e, _ = gmsh.model.mesh.getElements(dim, etag)
41
+ if not etypes_e:
42
+ continue
43
+ mtype = meshio.gmsh.gmsh_to_meshio_type[etypes_e[0]]
44
+ for k, cb in enumerate(cells):
45
+ if isinstance(cb, meshio.CellBlock) and cb.type == mtype:
46
+ groups[k].append(np.asarray(etags_e[0], dtype=np.int64) - 1)
47
+ cell_sets[name] = [
48
+ np.concatenate(ids) if ids else np.empty(0, dtype=np.int64)
49
+ for ids in groups
50
+ ]
51
+
52
+ return meshio.Mesh(pts, cells, cell_sets=cell_sets)
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import gmsh
4
+ import meshio
5
+
6
+ from cadgmsh._extract import _to_meshio
7
+ from cadgmsh._occ import _pointer
8
+ from cadgmsh._resolve import _resolve_tags
9
+ from cadgmsh._types import Shape
10
+
11
+
12
+ def mesh(
13
+ shapes: Shape | list[Shape],
14
+ *,
15
+ lc: float | None = None,
16
+ lc_min: float | None = None,
17
+ imprint: bool = False,
18
+ dim: int = 3,
19
+ order: int = 1,
20
+ algorithm: int | None = None,
21
+ physical: dict[str, Shape | list[Shape]] | None = None,
22
+ verbose: bool = False,
23
+ ) -> meshio.Mesh:
24
+ """
25
+ Mesh one or more CAD shapes and return a :class:`meshio.Mesh`.
26
+
27
+ Parameters
28
+ ----------
29
+ shapes:
30
+ A single cadquery/build123d shape or a list of them.
31
+ lc:
32
+ Characteristic length (maximum element size).
33
+ lc_min:
34
+ Minimum characteristic length.
35
+ imprint:
36
+ If ``True``, boolean-fragment all input shapes so shared faces are
37
+ conformally meshed. Required for conforming multi-domain assemblies.
38
+
39
+ .. warning::
40
+ OCC Boolean operations on coincident or touching faces can produce
41
+ a hard segfault inside the OCC kernel that cannot be caught as a
42
+ Python exception. Verify that input shapes have no coincident
43
+ boundary faces before enabling this option.
44
+ dim:
45
+ Maximum mesh dimension (1, 2, or 3).
46
+ order:
47
+ Element order (1 = linear, 2 = quadratic, …).
48
+ algorithm:
49
+ gmsh meshing algorithm number (see gmsh docs). ``None`` = gmsh default.
50
+ physical:
51
+ Dict mapping string labels to shapes (or lists of shapes).
52
+ Each value may be a solid, face, edge, or vertex from cadquery or
53
+ build123d. Labels become named cell sets in the returned mesh.
54
+ verbose:
55
+ Show gmsh terminal output.
56
+ """
57
+ shapes_list: list[Shape] = shapes if isinstance(shapes, list) else [shapes]
58
+
59
+ gmsh.initialize()
60
+ try:
61
+ gmsh.option.setNumber("General.Terminal", 1 if verbose else 0)
62
+ gmsh.model.add("model")
63
+
64
+ if lc is not None:
65
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMax", lc)
66
+ if lc_min is not None:
67
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMin", lc_min)
68
+
69
+ all_dim_tags: list[list[tuple[int, int]]] = []
70
+ for s in shapes_list:
71
+ dt = gmsh.model.occ.importShapesNativePointer(
72
+ _pointer(s), highestDimOnly=False
73
+ )
74
+ all_dim_tags.append(list(dt))
75
+
76
+ if imprint and len(shapes_list) > 1:
77
+ flat: list[tuple[int, int]] = [
78
+ dt for group in all_dim_tags for dt in group
79
+ ]
80
+ gmsh.model.occ.fragment(flat, [], removeObject=True, removeTool=True)
81
+
82
+ gmsh.model.occ.synchronize()
83
+
84
+ if physical:
85
+ for label, value in physical.items():
86
+ entries: list[Shape] = value if isinstance(value, list) else [value]
87
+ tags_by_dim: dict[int, list[int]] = {}
88
+ for entry in entries:
89
+ for d, t in _resolve_tags(entry):
90
+ tags_by_dim.setdefault(d, []).append(t)
91
+ for d, tags in tags_by_dim.items():
92
+ pg = gmsh.model.addPhysicalGroup(d, tags)
93
+ gmsh.model.setPhysicalName(d, pg, label)
94
+
95
+ if algorithm is not None:
96
+ gmsh.option.setNumber("Mesh.Algorithm", algorithm)
97
+
98
+ gmsh.model.mesh.generate(dim)
99
+
100
+ if order != 1:
101
+ gmsh.model.mesh.setOrder(order)
102
+
103
+ return _to_meshio()
104
+
105
+ finally:
106
+ gmsh.finalize()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from cadgmsh._types import Shape
6
+
7
+
8
+ def _unwrap(shape: Shape) -> Any:
9
+ """Return the raw OCP ``TopoDS_*`` object, or pass through if already unwrapped."""
10
+ return shape.wrapped if hasattr(shape, "wrapped") else shape
11
+
12
+
13
+ def _pointer(shape: Shape) -> int:
14
+ """
15
+ Return the integer memory address of a ``TopoDS_Shape``.
16
+
17
+ Compatible with ``gmsh.model.occ.importShapesNativePointer``.
18
+ Supports OCP (pybind11) via ``._address()`` and PythonOCC (SWIG) via ``.this``.
19
+ """
20
+ occ: Any = _unwrap(shape)
21
+ if hasattr(occ, "_address"):
22
+ return int(occ._address())
23
+ if hasattr(occ, "this"):
24
+ return int(occ.this)
25
+ raise TypeError(f"Cannot extract OCC pointer from {type(shape)}")
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import gmsh
4
+
5
+ from cadgmsh._occ import _pointer
6
+ from cadgmsh._types import Shape
7
+
8
+
9
+ def _resolve_tags(shape: Shape) -> list[tuple[int, int]]:
10
+ """
11
+ Return the gmsh ``(dim, tag)`` pairs for *shape* by re-importing its OCC pointer.
12
+
13
+ gmsh tracks topology by TShape pointer identity, so importing a sub-shape that
14
+ is already part of the model returns existing tags rather than creating new ones.
15
+ Must be called after ``gmsh.model.occ.synchronize()``.
16
+ """
17
+ return list(
18
+ gmsh.model.occ.importShapesNativePointer(_pointer(shape), highestDimOnly=True)
19
+ )
@@ -0,0 +1,31 @@
1
+ """Centralized type definitions for cadgmsh."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
6
+
7
+ if TYPE_CHECKING:
8
+ from build123d import Shape as _B3DShape
9
+ from cadquery.occ_impl.shapes import (
10
+ Shape as _CQShape, # type: ignore[import-untyped]
11
+ )
12
+
13
+
14
+ @runtime_checkable
15
+ class OccShape(Protocol):
16
+ """
17
+ Structural protocol for OCC-backed shapes accepted by cadgmsh.
18
+
19
+ Satisfied by cadquery and build123d shapes.
20
+ Implement ``.wrapped`` pointing to an OCC ``TopoDS_Shape`` to integrate
21
+ custom shape types without depending on either library.
22
+ """
23
+
24
+ @property
25
+ def wrapped(self) -> object: ...
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ Shape = _B3DShape | _CQShape | OccShape
30
+ else:
31
+ Shape = OccShape
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: cadgmsh
3
+ Version: 0.1.0
4
+ Summary: Mesh CadQuery/build123d geometry with gmsh
5
+ Author-email: David Straub <straub@protonmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 David Straub
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/DavidMStraub/cadgmsh
29
+ Requires-Python: >=3.10
30
+ License-File: LICENSE
31
+ Requires-Dist: gmsh
32
+ Requires-Dist: meshio
33
+ Requires-Dist: numpy
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest; extra == "test"
36
+ Requires-Dist: pytest-cov; extra == "test"
37
+ Requires-Dist: build123d; extra == "test"
38
+ Requires-Dist: cadquery; extra == "test"
39
+ Provides-Extra: dev
40
+ Requires-Dist: cadgmsh[test]; extra == "dev"
41
+ Requires-Dist: pyright; extra == "dev"
42
+ Requires-Dist: ruff; extra == "dev"
43
+ Dynamic: license-file
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cadgmsh/__init__.py
5
+ src/cadgmsh/_extract.py
6
+ src/cadgmsh/_mesh.py
7
+ src/cadgmsh/_occ.py
8
+ src/cadgmsh/_resolve.py
9
+ src/cadgmsh/_types.py
10
+ src/cadgmsh.egg-info/PKG-INFO
11
+ src/cadgmsh.egg-info/SOURCES.txt
12
+ src/cadgmsh.egg-info/dependency_links.txt
13
+ src/cadgmsh.egg-info/requires.txt
14
+ src/cadgmsh.egg-info/top_level.txt
15
+ tests/test_extract.py
16
+ tests/test_mesh.py
17
+ tests/test_occ.py
@@ -0,0 +1,14 @@
1
+ gmsh
2
+ meshio
3
+ numpy
4
+
5
+ [dev]
6
+ cadgmsh[test]
7
+ pyright
8
+ ruff
9
+
10
+ [test]
11
+ pytest
12
+ pytest-cov
13
+ build123d
14
+ cadquery
@@ -0,0 +1 @@
1
+ cadgmsh
@@ -0,0 +1,78 @@
1
+ import gmsh
2
+ import numpy as np
3
+ import pytest
4
+
5
+ from cadgmsh._extract import _to_meshio
6
+
7
+
8
+ @pytest.fixture
9
+ def box_mesh():
10
+ gmsh.initialize()
11
+ gmsh.option.setNumber("General.Terminal", 0)
12
+ gmsh.model.add("test")
13
+ gmsh.model.occ.addBox(0, 0, 0, 1, 1, 1)
14
+ gmsh.model.occ.synchronize()
15
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.5)
16
+ gmsh.model.mesh.generate(3)
17
+ yield
18
+ gmsh.finalize()
19
+
20
+
21
+ @pytest.fixture
22
+ def box_mesh_with_physical():
23
+ gmsh.initialize()
24
+ gmsh.option.setNumber("General.Terminal", 0)
25
+ gmsh.model.add("test")
26
+ vol_tag = gmsh.model.occ.addBox(0, 0, 0, 1, 1, 1)
27
+ gmsh.model.occ.synchronize()
28
+ pg_vol = gmsh.model.addPhysicalGroup(3, [vol_tag])
29
+ gmsh.model.setPhysicalName(3, pg_vol, "volume")
30
+ face_tags = [t for _, t in gmsh.model.getEntities(2)[:2]]
31
+ pg_face = gmsh.model.addPhysicalGroup(2, face_tags)
32
+ gmsh.model.setPhysicalName(2, pg_face, "surface")
33
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.5)
34
+ gmsh.model.mesh.generate(3)
35
+ yield
36
+ gmsh.finalize()
37
+
38
+
39
+ def test_points_shape(box_mesh):
40
+ m = _to_meshio()
41
+ assert m.points.ndim == 2
42
+ assert m.points.shape[1] == 3
43
+
44
+
45
+ def test_points_nonempty(box_mesh):
46
+ m = _to_meshio()
47
+ assert len(m.points) > 0
48
+
49
+
50
+ def test_cells_nonempty(box_mesh):
51
+ m = _to_meshio()
52
+ assert len(m.cells) > 0
53
+ assert sum(len(cb.data) for cb in m.cells) > 0
54
+
55
+
56
+ def test_no_physical_groups(box_mesh):
57
+ m = _to_meshio()
58
+ assert m.cell_sets == {}
59
+
60
+
61
+ def test_physical_group_names(box_mesh_with_physical):
62
+ m = _to_meshio()
63
+ assert "volume" in m.cell_sets
64
+ assert "surface" in m.cell_sets
65
+
66
+
67
+ def test_physical_group_nonempty(box_mesh_with_physical):
68
+ m = _to_meshio()
69
+ volume_arrays = [a for a in m.cell_sets["volume"] if a is not None]
70
+ assert len(volume_arrays) > 0
71
+ assert sum(len(a) for a in volume_arrays) > 0
72
+
73
+
74
+ def test_node_indices_are_consecutive(box_mesh):
75
+ idx, _, _ = gmsh.model.mesh.getNodes()
76
+ idx -= 1
77
+ srt = np.argsort(idx)
78
+ assert np.all(idx[srt] == np.arange(len(idx)))
@@ -0,0 +1,81 @@
1
+ import meshio
2
+ import pytest
3
+
4
+ bd = pytest.importorskip("build123d")
5
+
6
+
7
+ def box():
8
+ return bd.Box(1, 1, 1)
9
+
10
+
11
+ def sphere():
12
+ return bd.Sphere(0.4)
13
+
14
+
15
+ def test_returns_meshio_mesh():
16
+ import cadgmsh
17
+ result = cadgmsh.mesh(box(), lc=0.5)
18
+ assert isinstance(result, meshio.Mesh)
19
+
20
+
21
+ def test_points_and_cells():
22
+ import cadgmsh
23
+ result = cadgmsh.mesh(box(), lc=0.5)
24
+ assert result.points.shape[1] == 3
25
+ assert len(result.points) > 0
26
+ assert len(result.cells) > 0
27
+
28
+
29
+ def test_list_input():
30
+ import cadgmsh
31
+ result = cadgmsh.mesh([box()], lc=0.5)
32
+ assert isinstance(result, meshio.Mesh)
33
+
34
+
35
+ def test_physical_volume():
36
+ import cadgmsh
37
+ b = box()
38
+ result = cadgmsh.mesh(b, physical={"steel": b}, lc=0.5)
39
+ assert "steel" in result.cell_sets
40
+ volume_arrays = [a for a in result.cell_sets["steel"] if a is not None]
41
+ assert len(volume_arrays) > 0
42
+
43
+
44
+ def test_physical_face():
45
+ import cadgmsh
46
+ b = box()
47
+ bottom = b.faces().sort_by(bd.Axis.Z)[0]
48
+ result = cadgmsh.mesh(b, physical={"bottom": bottom}, lc=0.5)
49
+ assert "bottom" in result.cell_sets
50
+
51
+
52
+ def test_dim2():
53
+ import cadgmsh
54
+ result = cadgmsh.mesh(box(), dim=2, lc=0.5)
55
+ cell_types = {cb.type for cb in result.cells}
56
+ assert not any("tetra" in t for t in cell_types)
57
+
58
+
59
+ def test_order2():
60
+ import cadgmsh
61
+ result = cadgmsh.mesh(box(), order=2, lc=0.5)
62
+ cell_types = {cb.type for cb in result.cells}
63
+ assert "tetra10" in cell_types
64
+
65
+
66
+ def test_imprint_two_separated_boxes():
67
+ import cadgmsh
68
+ # imprint=True with non-touching shapes: exercises the code path without
69
+ # triggering OCC Boolean failures on coincident faces
70
+ b1 = bd.Box(1, 1, 1)
71
+ b2 = bd.Location((2, 0, 0)) * bd.Box(1, 1, 1)
72
+ result = cadgmsh.mesh([b1, b2], imprint=True, lc=0.5)
73
+ assert isinstance(result, meshio.Mesh)
74
+ assert len(result.points) > 0
75
+
76
+
77
+ def test_lc_affects_mesh_density():
78
+ import cadgmsh
79
+ coarse = cadgmsh.mesh(box(), lc=0.5)
80
+ fine = cadgmsh.mesh(box(), lc=0.12)
81
+ assert len(fine.points) > len(coarse.points)
@@ -0,0 +1,61 @@
1
+ import pytest
2
+
3
+ from cadgmsh._occ import _pointer, _unwrap
4
+
5
+
6
+ class _WithAddress:
7
+ def _address(self):
8
+ return 42
9
+
10
+
11
+ class _WithThis:
12
+ this = 99
13
+
14
+
15
+ def test_unwrap_passthrough():
16
+ obj = object()
17
+ assert _unwrap(obj) is obj
18
+
19
+
20
+ def test_unwrap_unwraps():
21
+ inner = object()
22
+
23
+ class Wrapped:
24
+ wrapped = inner
25
+
26
+ assert _unwrap(Wrapped()) is inner
27
+
28
+
29
+ def test_pointer_address():
30
+ assert _pointer(_WithAddress()) == 42
31
+
32
+
33
+ def test_pointer_this():
34
+ assert _pointer(_WithThis()) == 99
35
+
36
+
37
+ def test_pointer_wrapped_address():
38
+ class Shape:
39
+ wrapped = _WithAddress()
40
+
41
+ assert _pointer(Shape()) == 42
42
+
43
+
44
+ def test_pointer_wrapped_this():
45
+ class Shape:
46
+ wrapped = _WithThis()
47
+
48
+ assert _pointer(Shape()) == 99
49
+
50
+
51
+ def test_pointer_raises_on_unknown():
52
+ with pytest.raises(TypeError):
53
+ _pointer(object())
54
+
55
+
56
+ def test_pointer_raises_on_wrapped_unknown():
57
+ class Shape:
58
+ wrapped = object()
59
+
60
+ with pytest.raises(TypeError):
61
+ _pointer(Shape())