stellarmesh 0.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stellarmesh Developers
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.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: stellarmesh
3
+ Version: 0.0.0
4
+ Summary: Meshing library for nuclear workflows.
5
+ Author: Alex Koen
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy
10
+ Requires-Dist: pymmg
11
+ Requires-Dist: meshio[all]
12
+ Provides-Extra: dev
13
+ Requires-Dist: cadquery-ocp-stubs==7.8.1.1.post1; extra == "dev"
14
+ Requires-Dist: ocp-vscode>=2.9.0; extra == "dev"
15
+ Requires-Dist: pip>=25.2; extra == "dev"
16
+ Requires-Dist: pytest>=8.4.1; extra == "dev"
17
+ Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
18
+ Requires-Dist: pytest-benchmark>=5.1.0; extra == "dev"
19
+ Requires-Dist: ruff>=0.12.8; extra == "dev"
20
+ Dynamic: license-file
@@ -0,0 +1,98 @@
1
+ .. figure:: https://github.com/Thea-Energy/stellarmesh/raw/main/docs/logo.png
2
+ :width: 100%
3
+ :align: center
4
+
5
+
6
+ |Tests| |PyPI Version| |Conda Version|
7
+
8
+ Stellarmesh is a meshing library for nuclear workflows. Principally, it
9
+ supports the creation of DAGMC geometry from CAD models.
10
+
11
+ **Features**:
12
+
13
+ * ✅ Import of `CadQuery <https://github.com/CadQuery/cadquery>`__,
14
+ `build123d <https://github.com/gumyr/build123d>`__, STEP and BREP
15
+ geometry
16
+ * ✅ Surface and volume meshing
17
+ * ✅ Gmsh and OpenCASCADE meshing backends
18
+ * ✅ Linear and angular mesh tolerances
19
+ * ✅ Surface boundary conditions
20
+ * ✅ Imprinting and merging of conformal geometry
21
+ * ✅ Mesh refinement
22
+ * ✅ Programmatic manipulation of .h5m tags
23
+ * ✅ Automated testing and integration
24
+
25
+ ---------------
26
+ Getting Started
27
+ ---------------
28
+
29
+ * `Installation <https://stellarmesh.readthedocs.io/en/latest/install.html>`__
30
+ * `Tutorials <https://stellarmesh.readthedocs.io/en/latest/tutorials.html>`__
31
+ * `API <https://stellarmesh.readthedocs.io/en/latest/api>`__
32
+
33
+ -------
34
+ Example
35
+ -------
36
+
37
+ .. code:: python
38
+
39
+ import build123d as bd
40
+ import stellarmesh as sm
41
+
42
+ solids = [bd.Solid.make_torus(1000, 100)]
43
+ for _ in range(3):
44
+ solids.append(bd.Solid.thicken(solids[-1].faces()[0], 100))
45
+ solids = solids[1:]
46
+
47
+ geometry = sm.Geometry(solids[::-1], material_names=["a", "a", "c"])
48
+ mesh = sm.SurfaceMesh.from_geometry(
49
+ geometry, sm.GmshSurfaceOptions(min_mesh_size=50, max_mesh_size=200)
50
+ )
51
+ mesh.write("test.msh")
52
+ mesh.render("docs/torus-mesh-reversed.png", rotation_xyz=(90, 0, -90), normals=15)
53
+
54
+ h5m = sm.DAGMCModel.from_mesh(mesh)
55
+ h5m.write("dagmc.h5m")
56
+ h5m.write("dagmc.vtk")
57
+
58
+
59
+ .. figure:: https://github.com/Thea-Energy/stellarmesh/raw/main/docs/torus-mesh.png
60
+ :width: 80%
61
+ :align: center
62
+ :alt: Rendered mesh with normals.
63
+
64
+ Rendered mesh with normals.
65
+
66
+
67
+ .. note::
68
+ Stellarmesh uses the logging library for debug, info and warning messages. Set the level with:
69
+
70
+
71
+ .. code:: python
72
+
73
+ import logging
74
+
75
+ logging.basicConfig() # Required in Jupyter to correctly set output stream
76
+ logging.getLogger("stellarmesh").setLevel(logging.INFO)
77
+
78
+
79
+
80
+ ----------------
81
+ Acknowledgements
82
+ ----------------
83
+
84
+ Stellarmesh is originally a project of Thea Energy, who are building the
85
+ world’s first planar coil stellarator.
86
+
87
+ .. raw:: html
88
+
89
+ <img src="https://github.com/user-attachments/assets/37b9ba1c-b22c-4837-b226-a6212854127e" width="200px">
90
+
91
+ .. raw:: html
92
+
93
+ .. |Tests| image:: https://github.com/stellarmesh/stellarmesh/actions/workflows/test.yml/badge.svg
94
+ :target: https://github.com/stellarmesh/stellarmesh/actions/workflows/test.yml
95
+ .. |PyPI Version| image:: https://img.shields.io/pypi/v/stellarmesh.svg
96
+ :target: https://pypi.org/project/stellarmesh/
97
+ .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/stellarmesh.svg
98
+ :target: https://anaconda.org/conda-forge/stellarmesh
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "stellarmesh"
3
+ dynamic = ["version"]
4
+ authors = [{ name = "Alex Koen" }]
5
+ description = "Meshing library for nuclear workflows."
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ dependencies = ["numpy", "pymmg", "meshio[all]"]
9
+
10
+ [project.optional-dependencies]
11
+ dev = [
12
+ "cadquery-ocp-stubs==7.8.1.1.post1",
13
+ "ocp-vscode>=2.9.0",
14
+ "pip>=25.2",
15
+ "pytest>=8.4.1",
16
+ "pytest-cov>=6.2.1",
17
+ "pytest-benchmark>=5.1.0",
18
+ "ruff>=0.12.8",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["setuptools"]
23
+ build-backend = "setuptools.build_meta"
24
+
25
+ [tool.ruff]
26
+ target-version = "py311"
27
+ extend-include = ["*.ipynb"]
28
+
29
+
30
+ [tool.ruff.lint]
31
+ select = [
32
+ "E",
33
+ "W",
34
+ "I",
35
+ "N",
36
+ "A",
37
+ "B",
38
+ "F",
39
+ "D",
40
+ "FBT",
41
+ "C4",
42
+ "SIM",
43
+ "TD",
44
+ "PL",
45
+ "TRY",
46
+ "NPY",
47
+ "PERF",
48
+ "RUF",
49
+ ]
50
+ ignore = [
51
+ "TRY003", # Exception with string (vs. defining custom exception)
52
+ "PLR2004", # Magic number
53
+ ]
54
+ pydocstyle.convention = "google"
55
+
56
+ [tool.ruff.lint.extend-per-file-ignores]
57
+ "tests/**/test_*.py" = ["F811"] # Variable redefinition (required for fixtures)
58
+
59
+
60
+ [tool.pyright]
61
+ pythonVersion = "3.11"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ """Gmsh wrapper and DAGMC geometry creator."""
2
+
3
+ from .geometry import Geometry
4
+ from .mesh import (
5
+ GmshSurfaceAlgo,
6
+ GmshSurfaceOptions,
7
+ GmshVolumeAlgo,
8
+ GmshVolumeOptions,
9
+ Mesh,
10
+ OCCSurfaceAlgo,
11
+ OCCSurfaceOptions,
12
+ SurfaceMesh,
13
+ VolumeMesh,
14
+ )
15
+ from .moab import DAGMCModel, DAGMCSurface, DAGMCVolume, MOABModel
16
+
17
+ __all__ = [
18
+ "DAGMCModel",
19
+ "DAGMCSurface",
20
+ "DAGMCVolume",
21
+ "Geometry",
22
+ "GmshSurfaceAlgo",
23
+ "GmshSurfaceOptions",
24
+ "GmshVolumeAlgo",
25
+ "GmshVolumeOptions",
26
+ "MOABModel",
27
+ "Mesh",
28
+ "OCCSurfaceAlgo",
29
+ "OCCSurfaceOptions",
30
+ "SurfaceMesh",
31
+ "VolumeMesh",
32
+ ]
33
+
34
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ import os
2
+ from typing import Union
3
+
4
+ PathLike = Union[str, bytes, os.PathLike]
@@ -0,0 +1,320 @@
1
+ """Stellarmesh geometry.
2
+
3
+ name: geometry.py
4
+ author: Alex Koen
5
+
6
+ desc: Geometry class represents a CAD geometry to be meshed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import warnings
13
+ from typing import (
14
+ Optional,
15
+ Protocol,
16
+ Sequence,
17
+ Type,
18
+ overload,
19
+ )
20
+
21
+ try:
22
+ from OCP.BOPAlgo import BOPAlgo_MakeConnected
23
+ from OCP.BRep import BRep_Builder
24
+ from OCP.BRepTools import BRepTools
25
+ from OCP.IFSelect import IFSelect_RetDone
26
+ from OCP.STEPControl import STEPControl_Reader
27
+ from OCP.TopAbs import TopAbs_ShapeEnum
28
+ from OCP.TopExp import TopExp_Explorer
29
+ from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid
30
+ except ImportError as e:
31
+ raise ImportError(
32
+ "OCP not found. See Stellarmesh installation instructions."
33
+ ) from e
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class Face(Protocol):
39
+ """Interface for a CadQuery or Build123d Face."""
40
+
41
+ wrapped: TopoDS_Face | None
42
+
43
+
44
+ class Shell(Protocol):
45
+ """Interface for a CadQuery or Build123d Shell."""
46
+
47
+ wrapped: TopoDS_Shell | None
48
+
49
+
50
+ class Solid(Protocol):
51
+ """Interface for a CadQuery or Build123d Solid."""
52
+
53
+ wrapped: TopoDS_Solid | None
54
+
55
+
56
+ class Geometry:
57
+ """Geometry, representing an ordered list of solids, to be meshed."""
58
+
59
+ solids: list[TopoDS_Solid]
60
+ material_names: list[str]
61
+ faces: list[TopoDS_Face]
62
+ face_boundary_conditions: list[str]
63
+
64
+ def __init__(
65
+ self,
66
+ solids: Optional[Sequence[Solid | TopoDS_Solid]] = None,
67
+ material_names: Optional[Sequence[str]] = None,
68
+ surfaces: Optional[Sequence[Face | Shell | TopoDS_Face | TopoDS_Shell]] = None,
69
+ surface_boundary_conditions: Optional[Sequence[str]] = None,
70
+ ):
71
+ """Construct geometry from solids.
72
+
73
+ Args:
74
+ solids: List of solids, where each solid is a build123d Solid, CadQuery
75
+ Solid, or OCP TopoDS_Solid.
76
+ material_names: List of materials. Must match length of solids.
77
+ surfaces: List of surfaces, where each surface is a build123d or Cadquery
78
+ Face or Shell, or an OCP TopoDS_Face or TopoDS_Shell.
79
+ surface_boundary_conditions: List of boundary condition names. Must match
80
+ length of surfaces.
81
+ """
82
+ if (solids and not material_names) or (material_names and not solids):
83
+ raise ValueError(
84
+ "If solids or material_names are provided"
85
+ ", both must be provided and match in length."
86
+ )
87
+
88
+ if (surfaces and not surface_boundary_conditions) or (
89
+ surface_boundary_conditions and not surfaces
90
+ ):
91
+ raise ValueError(
92
+ "If surfaces or surface_boundary_conditions are provided"
93
+ ", both must be provided and match in length."
94
+ )
95
+
96
+ self.solids = []
97
+ self.material_names = []
98
+ if solids and material_names:
99
+ for i, (s, mat_name) in enumerate(zip(solids, material_names, strict=True)):
100
+ s_wrapped = (
101
+ s if isinstance(s, TopoDS_Shape) else getattr(s, "wrapped", None)
102
+ )
103
+
104
+ if s_wrapped is None:
105
+ raise ValueError(
106
+ f"{s} {i} has no wrapped TopoDS_Shape. Is it valid?"
107
+ )
108
+
109
+ self.solids.append(s_wrapped)
110
+ self.material_names.append(mat_name)
111
+
112
+ self.faces = []
113
+ self.face_boundary_conditions = []
114
+ if surfaces and surface_boundary_conditions:
115
+ for i, (s, bc) in enumerate(
116
+ zip(surfaces, surface_boundary_conditions, strict=True)
117
+ ):
118
+ s_wrapped = (
119
+ s
120
+ if isinstance(s, (TopoDS_Face, TopoDS_Shell))
121
+ else getattr(s, "wrapped", None)
122
+ )
123
+
124
+ if s.wrapped is None: # type: ignore
125
+ raise ValueError(
126
+ f"{s} {i} has no wrapped TopoDS_Shape. Is it valid?"
127
+ )
128
+
129
+ if isinstance(s_wrapped, TopoDS_Face):
130
+ self.faces.append(s_wrapped)
131
+ self.face_boundary_conditions.append(bc)
132
+ elif isinstance(s_wrapped, TopoDS_Shell):
133
+ child_faces = self._get_child_shapes(s_wrapped, TopoDS_Face)
134
+ self.faces.extend(child_faces)
135
+ self.face_boundary_conditions.extend([bc] * len(child_faces))
136
+
137
+ else:
138
+ raise TypeError(
139
+ f"Surface {i} is of invalid type {type(s).__name__}"
140
+ )
141
+
142
+ @staticmethod
143
+ @overload
144
+ def _get_child_shapes(
145
+ parent: TopoDS_Shape, shape_type: Type[TopoDS_Face]
146
+ ) -> list[TopoDS_Face]: ...
147
+
148
+ @staticmethod
149
+ @overload
150
+ def _get_child_shapes(
151
+ parent: TopoDS_Shape, shape_type: Type[TopoDS_Shell]
152
+ ) -> list[TopoDS_Shell]: ...
153
+
154
+ @staticmethod
155
+ @overload
156
+ def _get_child_shapes(
157
+ parent: TopoDS_Shape, shape_type: Type[TopoDS_Solid]
158
+ ) -> list[TopoDS_Solid]: ...
159
+
160
+ @staticmethod
161
+ def _get_child_shapes(
162
+ parent: TopoDS_Shape, shape_type: Type[TopoDS_Shape]
163
+ ) -> Sequence[TopoDS_Shape]:
164
+ """Return all the child shapes of this shape."""
165
+ type_map = {
166
+ "TopoDS_Face": TopAbs_ShapeEnum.TopAbs_FACE,
167
+ "TopoDS_Shell": TopAbs_ShapeEnum.TopAbs_SHELL,
168
+ "TopoDS_Solid": TopAbs_ShapeEnum.TopAbs_SOLID,
169
+ }
170
+
171
+ top_abs_type = type_map.get(shape_type.__name__)
172
+ if top_abs_type is None:
173
+ raise ValueError(f"Unsupported shape_type: {shape_type}")
174
+
175
+ shapes = []
176
+ explorer = TopExp_Explorer(parent, top_abs_type)
177
+ while explorer.More():
178
+ assert explorer.Current().ShapeType() == top_abs_type
179
+ shapes.append(explorer.Current())
180
+ explorer.Next()
181
+ return shapes
182
+
183
+ @classmethod
184
+ def _get_solids_from_shape(cls, shape: TopoDS_Shape) -> list[TopoDS_Solid]:
185
+ """Return all the solids in this shape."""
186
+ solids: list[TopoDS_Solid] = []
187
+ if shape.ShapeType() == TopAbs_ShapeEnum.TopAbs_SOLID:
188
+ solids.append(TopoDS.Solid_s(shape))
189
+ elif shape.ShapeType() == TopAbs_ShapeEnum.TopAbs_COMPOUND:
190
+ solids = cls._get_child_shapes(shape, TopoDS_Solid)
191
+ return solids
192
+
193
+ # TODO(akoen): from_step and from_brep are not DRY
194
+ # https://github.com/Thea-Energy/stellarmesh/issues/2
195
+ @classmethod
196
+ def from_step(
197
+ cls,
198
+ filename: str,
199
+ material_names: Sequence[str],
200
+ ) -> Geometry:
201
+ """Import model from a step file.
202
+
203
+ Args:
204
+ filename: File path to import.
205
+ material_names: Ordered list of material names matching solids in file.
206
+
207
+ Returns:
208
+ Model.
209
+ """
210
+ logger.info(f"Importing {filename}")
211
+
212
+ reader = STEPControl_Reader()
213
+ read_status = reader.ReadFile(filename)
214
+ if read_status != IFSelect_RetDone:
215
+ raise ValueError(f"STEP File {filename} could not be loaded")
216
+ for i in range(reader.NbRootsForTransfer()):
217
+ reader.TransferRoot(i + 1)
218
+
219
+ solids = []
220
+ for i in range(reader.NbShapes()):
221
+ shape = reader.Shape(i + 1)
222
+ solids.extend(cls._get_solids_from_shape(shape))
223
+
224
+ return cls(solids, material_names)
225
+
226
+ @classmethod
227
+ def import_step(
228
+ cls,
229
+ filename: str,
230
+ material_names: Sequence[str],
231
+ ) -> Geometry:
232
+ """Import model from a step file.
233
+
234
+ Args:
235
+ filename: File path to import.
236
+ material_names: Ordered list of material names matching solids in file.
237
+
238
+ Returns:
239
+ Model.
240
+ """
241
+ warnings.warn(
242
+ "The import_step method is deprecated. Use from_step instead.",
243
+ FutureWarning,
244
+ stacklevel=2,
245
+ )
246
+ return cls.from_step(filename, material_names)
247
+
248
+ @classmethod
249
+ def from_brep(
250
+ cls,
251
+ filename: str,
252
+ material_names: Sequence[str],
253
+ ) -> Geometry:
254
+ """Import model from a brep (cadquery, build123d native) file.
255
+
256
+ Args:
257
+ filename: File path to import.
258
+ material_names: Ordered list of material names matching solids in file.
259
+
260
+ Returns:
261
+ Model.
262
+ """
263
+ logger.info(f"Importing {filename}")
264
+
265
+ shape = TopoDS_Shape()
266
+ builder = BRep_Builder()
267
+ BRepTools.Read_s(shape, filename, builder)
268
+
269
+ if shape.IsNull():
270
+ raise ValueError(f"Could not import {filename}")
271
+ solids = cls._get_solids_from_shape(shape)
272
+
273
+ logger.info(f"Importing {len(solids)} from {filename}")
274
+ return cls(solids, material_names)
275
+
276
+ @classmethod
277
+ def import_brep(
278
+ cls,
279
+ filename: str,
280
+ material_names: Sequence[str],
281
+ ) -> Geometry:
282
+ """Import model from a brep (cadquery, build123d native) file.
283
+
284
+ Args:
285
+ filename: File path to import.
286
+ material_names: Ordered list of material names matching solids in file.
287
+
288
+ Returns:
289
+ Model.
290
+ """
291
+ warnings.warn(
292
+ "The import_brep method is deprecated. Use from_brep instead.",
293
+ FutureWarning,
294
+ stacklevel=2,
295
+ )
296
+ return cls.from_brep(filename, material_names)
297
+
298
+ def imprint(self) -> Geometry:
299
+ """Imprint faces of current geometry.
300
+
301
+ Returns:
302
+ A new geometry with the imprinted and merged geometry.
303
+ """
304
+ bldr = BOPAlgo_MakeConnected()
305
+ bldr.SetRunParallel(theFlag=True)
306
+ bldr.SetUseOBB(theUseOBB=True)
307
+
308
+ for solid in self.solids:
309
+ bldr.AddArgument(solid)
310
+
311
+ bldr.Perform()
312
+ res = bldr.Shape()
313
+ res_solids = self._get_solids_from_shape(res)
314
+
315
+ if (l0 := len(res_solids)) != (l1 := len(self.solids)):
316
+ raise RuntimeError(
317
+ f"Length of imprinted solids {l0} != length of original solids {l1}"
318
+ )
319
+
320
+ return type(self)(res_solids, self.material_names)