nmag-python-3 0.0.2__py3-none-any.whl

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.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,182 @@
1
+ """Construction of meshes from geometry and point/simplicial inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from .backend import backend
9
+ from .mesh_model import MeshBase, Point, Simplex
10
+
11
+ if TYPE_CHECKING:
12
+ from .geometry import Body, MeshObject
13
+ from .mesher.meshing_parameters import MeshingParameters
14
+
15
+ MeshCallback = Callable[[int, int, object], None]
16
+
17
+
18
+ def _as_float_points(points: Sequence[Sequence[float]] | None) -> list[Point]:
19
+ return [list(map(float, point)) for point in (points or [])]
20
+
21
+
22
+ def _as_int_simplices(simplices: Sequence[Sequence[int]] | None) -> list[Simplex]:
23
+ return [list(map(int, simplex)) for simplex in (simplices or [])]
24
+
25
+
26
+ def _as_region_ids(regions: Sequence[int] | None) -> list[int]:
27
+ return [int(region) for region in (regions or [])]
28
+
29
+
30
+ def _normalise_periodic(periodic: Sequence[bool] | Sequence[float] | None, dim: int) -> list[float]:
31
+ if not periodic:
32
+ return [0.0] * dim
33
+ return [1.0 if bool(value) else 0.0 for value in periodic]
34
+
35
+
36
+ def get_default_meshing_parameters() -> MeshingParameters:
37
+ """Returns default meshing parameters."""
38
+ from .mesher.meshing_parameters import MeshingParameters
39
+
40
+ return MeshingParameters()
41
+
42
+
43
+ # --- Loading Utilities ---
44
+
45
+
46
+ class Mesh(MeshBase):
47
+ """Generate a simplex mesh from implicit geometric objects.
48
+
49
+ Args:
50
+ bounding_box: Lower and upper coordinate corners enclosing the model.
51
+ objects: Geometry primitives or boolean combinations to mesh.
52
+ a0: Target initial point spacing in mesh-coordinate units.
53
+ density: Optional legacy density expression.
54
+ periodic: Periodic coordinate selectors for bounding-box meshing.
55
+ fixed_points: Additional points held fixed during relaxation.
56
+ mobile_points: Additional mobile seed points.
57
+ simply_points: Additional simple seed points.
58
+ callback: Mesher progress callback and interval.
59
+ mesh_bounding_box: Mesh the complete bounding box when true.
60
+ meshing_parameters: Explicit mesher controls.
61
+ cache_name: Optional compatibility cache label.
62
+ hints: Mesh/object hint pairs.
63
+ **kwargs: Named meshing-parameter overrides.
64
+
65
+ Raises:
66
+ ValueError: If the bounding box or object configuration is invalid.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ bounding_box: Sequence[Sequence[float]] | None,
72
+ objects: Sequence[MeshObject] | None = None,
73
+ a0: float = 1.0,
74
+ density: str = "",
75
+ periodic: Sequence[bool] | Sequence[float] | None = None,
76
+ fixed_points: Sequence[Sequence[float]] | None = None,
77
+ mobile_points: Sequence[Sequence[float]] | None = None,
78
+ simply_points: Sequence[Sequence[float]] | None = None,
79
+ callback: tuple[MeshCallback, int] | None = None,
80
+ mesh_bounding_box: bool = False,
81
+ meshing_parameters: MeshingParameters | None = None,
82
+ cache_name: str = "",
83
+ hints: Sequence[tuple[MeshBase, MeshObject]] | None = None,
84
+ **kwargs: Any,
85
+ ) -> None:
86
+ if bounding_box is None:
87
+ raise ValueError("Bounding box must be provided.")
88
+
89
+ object_list = list(objects or [])
90
+ hint_list = list(hints or [])
91
+ bb = _as_float_points(bounding_box)
92
+ dim = len(bb[0])
93
+ mesh_ext = 1 if mesh_bounding_box else 0
94
+
95
+ self.bounding_box = bb
96
+ self.mesh_exterior = mesh_ext
97
+
98
+ if not object_list and not mesh_bounding_box:
99
+ raise ValueError("No objects to mesh and bounding box meshing disabled.")
100
+
101
+ if periodic and not mesh_bounding_box and any(periodic):
102
+ raise ValueError("Can only produce periodic meshes when meshing the bounding box.")
103
+
104
+ params = meshing_parameters or get_default_meshing_parameters()
105
+ self.meshing_parameters = params
106
+ for k, v in kwargs.items():
107
+ params[k] = v
108
+
109
+ obj_bodies: list[Body] = []
110
+ self.obj = obj_bodies
111
+ self.cache_name = cache_name
112
+ self.density = density
113
+ self._fixed_points = _as_float_points(fixed_points)
114
+ self._mobile_points = _as_float_points(mobile_points)
115
+ self._simply_points = _as_float_points(simply_points)
116
+
117
+ for obj in object_list:
118
+ obj_bodies.append(obj._require_body())
119
+ self._fixed_points.extend(obj.fixed_points)
120
+ self._mobile_points.extend(obj.mobile_points)
121
+
122
+ resolved_hints = [
123
+ [hint_mesh.raw_mesh, hint_object._require_body()]
124
+ for hint_mesh, hint_object in hint_list
125
+ ]
126
+
127
+ from .mesher.driver import make_mg_gendriver
128
+
129
+ periodic_floats = _normalise_periodic(periodic, dim)
130
+ self.periodic = periodic_floats
131
+
132
+ def no_callback(_piece: int, _step: int, _mesh: object) -> None:
133
+ return None
134
+
135
+ cb_func, cb_interval = callback if callback else (no_callback, 1_000_000)
136
+ self.fun_driver = cb_func
137
+ self.driver = make_mg_gendriver(cb_interval, cb_func)
138
+ self.mesher_config = backend.copy_mesher_defaults(backend.mesher_defaults)
139
+ params.apply_to_mesher(self.mesher_config, dim)
140
+
141
+ raw = backend.mesh_bodies_raw(
142
+ self.driver,
143
+ self.mesher_config,
144
+ bb[0],
145
+ bb[1],
146
+ mesh_ext,
147
+ obj_bodies,
148
+ float(a0),
149
+ density,
150
+ self._fixed_points,
151
+ self._mobile_points,
152
+ self._simply_points,
153
+ periodic_floats,
154
+ cache_name,
155
+ resolved_hints,
156
+ )
157
+
158
+ super().__init__(raw)
159
+
160
+ def default_fun(self, nr_piece: int, n: int, mesh: object) -> None:
161
+ """Default callback function."""
162
+ pass
163
+
164
+ def extended_fun_driver(self, nr_piece: int, iteration_nr: int, mesh: object) -> None:
165
+ """Extended driver callback."""
166
+ if hasattr(self, "fun_driver"):
167
+ self.fun_driver(nr_piece, iteration_nr, mesh)
168
+
169
+ def fixed_points(self, points: Sequence[Sequence[float]] | None) -> None:
170
+ """Adds fixed points to the mesh configuration."""
171
+ if points:
172
+ self._fixed_points.extend(_as_float_points(points))
173
+
174
+ def mobile_points(self, points: Sequence[Sequence[float]] | None) -> None:
175
+ """Adds mobile points to the mesh configuration."""
176
+ if points:
177
+ self._mobile_points.extend(_as_float_points(points))
178
+
179
+ def simply_points(self, points: Sequence[Sequence[float]] | None) -> None:
180
+ """Adds simply points to the mesh configuration."""
181
+ if points:
182
+ self._simply_points.extend(_as_float_points(points))
nmesh/mesh_io.py ADDED
@@ -0,0 +1,227 @@
1
+ """Legacy and modern mesh file loading and serialization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+ from typing import TextIO
9
+
10
+ from .backend import RawMesh, backend
11
+ from .mesh_generation import _as_float_points, _as_int_simplices, _as_region_ids
12
+ from .mesh_model import MeshBase
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def _is_nmesh_ascii_file(filename: str | Path) -> bool:
18
+ try:
19
+ with Path(filename).open(encoding="utf-8") as stream:
20
+ return stream.readline().startswith("# PYFEM")
21
+ except (OSError, UnicodeDecodeError):
22
+ return False
23
+
24
+
25
+ def _is_nmesh_hdf5_file(filename: str | Path) -> bool:
26
+ from .io.legacy_nmesh_hdf5 import is_legacy_nmesh_hdf5
27
+
28
+ return is_legacy_nmesh_hdf5(filename)
29
+
30
+
31
+ def hdf5_mesh_get_permutation(filename: str | Path) -> list[int] | None:
32
+ """Stub for retrieving permutation from HDF5."""
33
+ log.warning("hdf5_mesh_get_permutation: HDF5 support is stubbed.")
34
+ return None
35
+
36
+
37
+ # --- Mesh Classes ---
38
+
39
+
40
+ class MeshFromFile(MeshBase):
41
+ """Loads a mesh from a file."""
42
+
43
+ def __init__(
44
+ self,
45
+ filename: str | Path,
46
+ reorder: bool = False,
47
+ distribute: bool = True,
48
+ ) -> None:
49
+ if reorder:
50
+ raise NotImplementedError("Mesh reordering is not implemented.")
51
+ if not distribute:
52
+ raise NotImplementedError("Manual mesh distribution is not implemented.")
53
+ path = Path(filename)
54
+ if not path.exists():
55
+ raise FileNotFoundError(f"File {filename} not found")
56
+
57
+ if _is_nmesh_ascii_file(path):
58
+ from .io.ascii import read_ascii_nmesh
59
+
60
+ raw = read_ascii_nmesh(path)
61
+ elif _is_nmesh_hdf5_file(path):
62
+ from .io.legacy_nmesh_hdf5 import load_raw_mesh_from_legacy_nmesh_hdf5
63
+
64
+ raw = load_raw_mesh_from_legacy_nmesh_hdf5(path)
65
+ else:
66
+ from .io import load_raw_mesh_with_meshio
67
+
68
+ raw = load_raw_mesh_with_meshio(path)
69
+
70
+ super().__init__(raw)
71
+
72
+
73
+ class mesh_from_points_and_simplices(MeshBase):
74
+ """Construct a mesh from explicit points, simplex indices, and regions.
75
+
76
+ Args:
77
+ points: Coordinate vectors with a consistent dimension.
78
+ simplices_indices: Point-index vectors defining simplex cells.
79
+ simplices_regions: Positive region ID for every simplex.
80
+ periodic_point_indices: Optional groups of equivalent periodic points.
81
+ The micromagnetic simulation does not currently support periodic
82
+ boundary physics.
83
+ initial: Use ``1`` to convert one-based simplex indices to zero-based.
84
+ do_reorder: Request unsupported legacy node reordering.
85
+ do_distribute: Keep true; manual distribution is unsupported.
86
+
87
+ Raises:
88
+ ValueError: If points, simplices, regions, or indices are inconsistent.
89
+ NotImplementedError: If reordering or manual distribution is requested.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ points: Sequence[Sequence[float]] | None = None,
95
+ simplices_indices: Sequence[Sequence[int]] | None = None,
96
+ simplices_regions: Sequence[int] | None = None,
97
+ periodic_point_indices: Sequence[Sequence[int]] | None = None,
98
+ initial: int = 0,
99
+ do_reorder: bool = False,
100
+ do_distribute: bool = True,
101
+ ) -> None:
102
+ if do_reorder:
103
+ raise NotImplementedError("Mesh reordering is not implemented.")
104
+ if not do_distribute:
105
+ raise NotImplementedError("Manual mesh distribution is not implemented.")
106
+ points_list = _as_float_points(points)
107
+ simplices_list = _as_int_simplices(simplices_indices)
108
+ if initial == 1:
109
+ simplices_list = [[index - 1 for index in simplex] for simplex in simplices_list]
110
+
111
+ raw = backend.mesh_from_points_and_simplices(
112
+ len(points_list[0]) if points_list else 3,
113
+ points_list,
114
+ simplices_list,
115
+ _as_region_ids(simplices_regions),
116
+ [list(map(int, group)) for group in (periodic_point_indices or [])],
117
+ do_reorder,
118
+ do_distribute,
119
+ )
120
+ super().__init__(raw)
121
+
122
+
123
+ def load(
124
+ filename: str | Path,
125
+ reorder: bool = False,
126
+ distribute: bool = True,
127
+ ) -> MeshFromFile:
128
+ """Load legacy Nmesh or a Meshio-supported mesh file.
129
+
130
+ Args:
131
+ filename: Source mesh path.
132
+ reorder: Request unsupported legacy node reordering.
133
+ distribute: Keep true; manual distribution is unsupported.
134
+
135
+ Returns:
136
+ Mesh wrapper containing points, simplices, regions, and topology.
137
+
138
+ Raises:
139
+ FileNotFoundError: If ``filename`` does not exist.
140
+ ValueError: If the mesh layout or supported cells are invalid.
141
+ NotImplementedError: If reordering or manual distribution is requested.
142
+ """
143
+ return MeshFromFile(filename, reorder, distribute)
144
+
145
+
146
+ def save(mesh: MeshBase, filename: str | Path) -> None:
147
+ """Save a mesh using the format selected by the destination suffix.
148
+
149
+ Args:
150
+ mesh: Mesh to serialize.
151
+ filename: Destination. ``.h5`` selects Nmesh HDF5, ``.nmesh`` selects
152
+ Nmesh ASCII, and other suffixes are delegated to Meshio.
153
+ """
154
+ mesh.save(filename)
155
+
156
+
157
+ # --- Utilities ---
158
+
159
+
160
+ def write_mesh(
161
+ mesh_data: RawMesh
162
+ | tuple[
163
+ Sequence[Sequence[float]],
164
+ Sequence[tuple[int, Sequence[int]]],
165
+ Sequence[tuple[int, Sequence[int]]],
166
+ ],
167
+ out: str | Path | TextIO | None = None,
168
+ check: bool = True,
169
+ float_fmt: str = " %.17g",
170
+ ) -> None:
171
+ """
172
+ Writes mesh data to a file in nmesh format.
173
+
174
+ `mesh_data` may be a `RawMesh` or a legacy `(points, simplices, surfaces)` tuple.
175
+ """
176
+ if isinstance(mesh_data, RawMesh):
177
+ points = mesh_data.points
178
+ simplices = list(
179
+ zip(
180
+ mesh_data.regions or [1] * len(mesh_data.simplices),
181
+ mesh_data.simplices,
182
+ strict=True,
183
+ )
184
+ )
185
+ surfaces = list(
186
+ zip(
187
+ [1] * len(mesh_data.surfaces),
188
+ mesh_data.surfaces,
189
+ strict=True,
190
+ )
191
+ )
192
+ periodic_groups = mesh_data.periodic_point_indices
193
+ else:
194
+ points, simplices, surfaces = mesh_data
195
+ periodic_groups = []
196
+
197
+ lines = ["# PYFEM mesh file version 1.0"]
198
+ dim = len(points[0]) if points else 0
199
+ lines.append(
200
+ f"# dim = {dim} \t nodes = {len(points)} \t simplices = {len(simplices)} "
201
+ f"\t surfaces = {len(surfaces)} \t periodic = {len(periodic_groups)}"
202
+ )
203
+
204
+ lines.append(str(len(points)))
205
+ for p in points:
206
+ lines.append("".join(float_fmt % x for x in p))
207
+
208
+ lines.append(str(len(simplices)))
209
+ for body, nodes in simplices:
210
+ lines.append(f" {body} " + " ".join(str(n) for n in nodes))
211
+
212
+ lines.append(str(len(surfaces)))
213
+ for body, nodes in surfaces:
214
+ lines.append(f" {body} " + " ".join(str(n) for n in nodes))
215
+
216
+ lines.append(str(len(periodic_groups)))
217
+ for group_index, point_indices in enumerate(periodic_groups):
218
+ lines.append(f" {group_index} " + " ".join(str(index) for index in point_indices))
219
+
220
+ content = "\n".join(lines) + "\n"
221
+
222
+ if out is None:
223
+ print(content)
224
+ elif isinstance(out, (str, Path)):
225
+ Path(out).write_text(content, encoding="utf-8")
226
+ else:
227
+ out.write(content)
nmesh/mesh_model.py ADDED
@@ -0,0 +1,147 @@
1
+ """Mesh model wrapper and backend-backed cached accessors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any, TypeVar, cast
8
+
9
+ from .backend import RawMesh, backend
10
+
11
+ PYFEM_SUFFIXES = {"", ".nmesh", ".pyfem"}
12
+ Point = list[float]
13
+ Simplex = list[int]
14
+ _T = TypeVar("_T")
15
+
16
+
17
+ class MeshBase:
18
+ """Expose mesh geometry and topology through cached Python values.
19
+
20
+ Attributes:
21
+ points: Node coordinates.
22
+ simplices: Point indices for every simplex cell.
23
+ regions: Region ID for every simplex.
24
+ dim: Coordinate dimension.
25
+ surfaces: Boundary simplex indices.
26
+ point_regions: Incident region IDs for every point.
27
+ links: Unique mesh edges as point-index pairs.
28
+ region_volumes: Computed volume for each region.
29
+ """
30
+
31
+ def __init__(self, raw_mesh: RawMesh) -> None:
32
+ self.raw_mesh = raw_mesh
33
+ self._cache: dict[str, Any] = {}
34
+
35
+ def _cached_backend_value(
36
+ self,
37
+ cache_key: str,
38
+ getter: Callable[[RawMesh], _T],
39
+ ) -> _T:
40
+ if cache_key not in self._cache:
41
+ self._cache[cache_key] = getter(self.raw_mesh)
42
+ return cast(_T, self._cache[cache_key])
43
+
44
+ def scale_node_positions(self, scale: float) -> None:
45
+ """Multiply every node coordinate by ``scale`` and clear caches."""
46
+ backend.mesh_scale_node_positions(self.raw_mesh, float(scale))
47
+ for key in (
48
+ "points",
49
+ "simplices",
50
+ "regions",
51
+ "point_regions",
52
+ "links",
53
+ "region_volumes",
54
+ "periodic_indices",
55
+ ):
56
+ self._cache.pop(key, None)
57
+
58
+ def save(self, filename: str | Path) -> None:
59
+ """Save the mesh using the format selected by the filename suffix."""
60
+ path = Path(filename)
61
+ suffix = path.suffix.lower()
62
+ if suffix == ".h5":
63
+ from .io import save_raw_mesh_as_legacy_nmesh_hdf5
64
+
65
+ save_raw_mesh_as_legacy_nmesh_hdf5(path, self.raw_mesh)
66
+ return
67
+
68
+ if suffix not in PYFEM_SUFFIXES:
69
+ from .io import save_raw_mesh_with_meshio
70
+
71
+ save_raw_mesh_with_meshio(path, self.raw_mesh)
72
+ return
73
+
74
+ from .mesh_io import write_mesh
75
+
76
+ write_mesh(self.raw_mesh, out=path)
77
+
78
+ def __str__(self) -> str:
79
+ pts = backend.mesh_nr_points(self.raw_mesh)
80
+ simps = backend.mesh_nr_simplices(self.raw_mesh)
81
+ return f"Mesh with {pts} points and {simps} simplices"
82
+
83
+ def to_lists(self) -> list[object]:
84
+ """Return the backend's legacy mesh-data list representation."""
85
+ return backend.mesh_plotinfo(self.raw_mesh)
86
+
87
+ @property
88
+ def points(self) -> list[Point]:
89
+ """Return node coordinates."""
90
+ return self._cached_backend_value("points", backend.mesh_plotinfo_points)
91
+
92
+ @property
93
+ def simplices(self) -> list[Simplex]:
94
+ """Return point indices for every simplex cell."""
95
+ return self._cached_backend_value("simplices", backend.mesh_plotinfo_simplices)
96
+
97
+ @property
98
+ def regions(self) -> list[int]:
99
+ """Return the region ID for every simplex cell."""
100
+ return self._cached_backend_value("regions", backend.mesh_plotinfo_simplicesregions)
101
+
102
+ @property
103
+ def dim(self) -> int:
104
+ """Return the mesh coordinate dimension."""
105
+ return backend.mesh_dim(self.raw_mesh)
106
+
107
+ @property
108
+ def surfaces(self) -> list[list[int]]:
109
+ """Return point indices for detected boundary simplices."""
110
+ return backend.mesh_plotinfo_surfaces_and_surfacesregions(self.raw_mesh)[0]
111
+
112
+ @property
113
+ def point_regions(self) -> list[list[int]]:
114
+ """Return incident region IDs for each point."""
115
+ return self._cached_backend_value("point_regions", backend.mesh_plotinfo_pointsregions)
116
+
117
+ @property
118
+ def links(self) -> list[tuple[int, int]]:
119
+ """Return unique mesh edges as point-index pairs."""
120
+ return self._cached_backend_value("links", backend.mesh_plotinfo_links)
121
+
122
+ @property
123
+ def region_volumes(self) -> list[float]:
124
+ """Return geometric volume for each region."""
125
+ return self._cached_backend_value("region_volumes", backend.mesh_plotinfo_regionvolumes)
126
+
127
+ @property
128
+ def num_regions(self) -> int:
129
+ """Return the number of mesh regions."""
130
+ return len(self.region_volumes)
131
+
132
+ @property
133
+ def periodic_point_indices(self) -> list[list[int]]:
134
+ """Return groups of equivalent periodic point indices."""
135
+ return self._cached_backend_value(
136
+ "periodic_indices",
137
+ backend.mesh_plotinfo_periodic_points_indices,
138
+ )
139
+
140
+ @property
141
+ def permutation(self) -> list[int]:
142
+ """Return the node permutation mapping."""
143
+ return backend.mesh_get_permutation(self.raw_mesh)
144
+
145
+ def set_vertex_distribution(self, dist: object) -> None:
146
+ """Sets vertex distribution."""
147
+ backend.mesh_set_vertex_distribution(self.raw_mesh, dist)
@@ -0,0 +1,79 @@
1
+ """Small utility constructors and reporting helpers for mesh objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence
7
+
8
+ from . import utils
9
+ from .mesh_io import mesh_from_points_and_simplices
10
+ from .mesh_model import MeshBase, Point, Simplex
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ def memory_report(tag: str) -> None:
16
+ """Reports memory usage."""
17
+ t, vmem, rss = utils.time_vmem_rss()
18
+ log.log(15, f"Memory report: T= {t:f} VMEM= {int(vmem)} KB RSS= {int(rss)} KB {tag}")
19
+
20
+
21
+ # --- Configuration ---
22
+
23
+
24
+ def outer_corners(mesh: MeshBase) -> tuple[Point | None, Point | None]:
25
+ """Determines the bounding box of the mesh nodes."""
26
+ coords = mesh.points
27
+ if not coords:
28
+ return None, None
29
+ transpose = list(zip(*coords, strict=True))
30
+ return [min(t) for t in transpose], [max(t) for t in transpose]
31
+
32
+
33
+ def generate_1d_mesh_components(
34
+ regions: Sequence[tuple[float, float]],
35
+ discretization: float,
36
+ ) -> tuple[list[Point], list[Simplex], list[int]]:
37
+ """Generates 1D mesh components (points, simplices, regions)."""
38
+ points: list[Point] = []
39
+ simplices: list[Simplex] = []
40
+ regions_ids: list[int] = []
41
+ point_map: dict[float, int] = {}
42
+
43
+ def get_idx(value: float) -> int:
44
+ vk = round(value, 8)
45
+ if vk not in point_map:
46
+ point_map[vk] = len(points)
47
+ points.append([float(value)])
48
+ return point_map[vk]
49
+
50
+ for rid, (start, end) in enumerate(regions, 1):
51
+ if start > end:
52
+ start, end = end, start
53
+ steps = max(1, int(abs((end - start) / discretization)))
54
+ step = (end - start) / steps
55
+ last = get_idx(start)
56
+ for i in range(1, steps + 1):
57
+ curr = get_idx(start + i * step)
58
+ simplices.append([last, curr])
59
+ regions_ids.append(rid)
60
+ last = curr
61
+
62
+ return points, simplices, regions_ids
63
+
64
+
65
+ def generate_1d_mesh(
66
+ regions: Sequence[tuple[float, float]],
67
+ discretization: float,
68
+ ) -> MeshBase:
69
+ """Generates a 1D mesh with specified regions and step size."""
70
+ pts, simps, regs = generate_1d_mesh_components(regions, discretization)
71
+ return mesh_from_points_and_simplices(pts, simps, regs)
72
+
73
+
74
+ def to_lists(mesh: MeshBase) -> list[object]:
75
+ """Returns mesh data as Python lists."""
76
+ return mesh.to_lists()
77
+
78
+
79
+ tolists = to_lists
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from .driver import (
4
+ Callback,
5
+ EngineFunc,
6
+ MeshEngineCommand,
7
+ MeshEngineStatus,
8
+ do_every_n_steps_driver,
9
+ make_mg_gendriver,
10
+ )
11
+ from .meshing_parameters import MeshingParameters
12
+
13
+ __all__ = [
14
+ "Callback",
15
+ "EngineFunc",
16
+ "MeshingParameters",
17
+ "MeshEngineCommand",
18
+ "MeshEngineStatus",
19
+ "do_every_n_steps_driver",
20
+ "make_mg_gendriver",
21
+ ]