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.
- anisotropy/__init__.py +28 -0
- anisotropy/anisotropy.py +13 -0
- anisotropy/evaluation.py +70 -0
- anisotropy/model.py +200 -0
- anisotropy/predefined.py +202 -0
- anisotropy/py.typed +1 -0
- anisotropy/values.py +84 -0
- mag_material/__init__.py +3 -0
- mag_material/mag_material.py +231 -0
- mag_material/parameters.py +150 -0
- mag_material/py.typed +1 -0
- nmag/__init__.py +36 -0
- nmag/backends.py +493 -0
- nmag/checkpoint.py +327 -0
- nmag/config.py +174 -0
- nmag/demag/__init__.py +39 -0
- nmag/demag/bem_operator.py +149 -0
- nmag/demag/geometry.py +134 -0
- nmag/demag/lindholm.py +133 -0
- nmag/demag/lindholm_fast.py +463 -0
- nmag/demag/linear.py +489 -0
- nmag/dynamics/__init__.py +255 -0
- nmag/output.py +27 -0
- nmag/parallel.py +45 -0
- nmag/py.typed +1 -0
- nmag/resources.py +38 -0
- nmag/simulation/__init__.py +519 -0
- nmag/simulation/anisotropy/__init__.py +5 -0
- nmag/simulation/anisotropy/fields.py +56 -0
- nmag/simulation/anisotropy/materials.py +138 -0
- nmag/simulation/demag/__init__.py +1 -0
- nmag/simulation/demag/bem/__init__.py +11 -0
- nmag/simulation/demag/bem/diagnostics.py +73 -0
- nmag/simulation/demag/bem/dirichlet.py +85 -0
- nmag/simulation/demag/bem/hierarchical.py +74 -0
- nmag/simulation/demag/bem/operator.py +292 -0
- nmag/simulation/demag/fem/__init__.py +11 -0
- nmag/simulation/demag/fem/assembly.py +114 -0
- nmag/simulation/demag/fem/charges.py +38 -0
- nmag/simulation/demag/fem/geometry.py +261 -0
- nmag/simulation/demag/fields/__init__.py +11 -0
- nmag/simulation/demag/fields/auxiliary.py +186 -0
- nmag/simulation/demag/fields/probe.py +77 -0
- nmag/simulation/demag/fields/recovery.py +207 -0
- nmag/simulation/demag/solver.py +17 -0
- nmag/simulation/dynamics/__init__.py +92 -0
- nmag/simulation/dynamics/advance.py +199 -0
- nmag/simulation/dynamics/integrator.py +263 -0
- nmag/simulation/exchange/__init__.py +13 -0
- nmag/simulation/exchange/coefficients.py +185 -0
- nmag/simulation/exchange/fields.py +106 -0
- nmag/simulation/exchange/llg_rhs.py +218 -0
- nmag/simulation/fields/__init__.py +19 -0
- nmag/simulation/fields/arrays.py +111 -0
- nmag/simulation/fields/availability.py +188 -0
- nmag/simulation/fields/averages.py +293 -0
- nmag/simulation/fields/derived.py +164 -0
- nmag/simulation/fields/maxangle.py +163 -0
- nmag/simulation/fields/probes.py +106 -0
- nmag/simulation/implicit_dynamics.py +221 -0
- nmag/simulation/mesh/__init__.py +13 -0
- nmag/simulation/mesh/geometry.py +153 -0
- nmag/simulation/mesh/materials.py +299 -0
- nmag/simulation/mesh/probe.py +234 -0
- nmag/simulation/restart.py +103 -0
- nmag/simulation/support.py +224 -0
- nmag_python_3-0.0.2.dist-info/METADATA +157 -0
- nmag_python_3-0.0.2.dist-info/RECORD +147 -0
- nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
- nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
- nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
- nmesh/__init__.py +130 -0
- nmesh/backend.py +286 -0
- nmesh/geometry/__init__.py +52 -0
- nmesh/geometry/boolean_operations.py +157 -0
- nmesh/geometry/primitives.py +453 -0
- nmesh/geometry/transform.py +126 -0
- nmesh/io/__init__.py +50 -0
- nmesh/io/ascii.py +132 -0
- nmesh/io/legacy_nmesh_hdf5.py +318 -0
- nmesh/io/meshio_support.py +170 -0
- nmesh/mesh_generation.py +182 -0
- nmesh/mesh_io.py +227 -0
- nmesh/mesh_model.py +147 -0
- nmesh/mesh_utilities.py +79 -0
- nmesh/mesher/__init__.py +21 -0
- nmesh/mesher/driver.py +146 -0
- nmesh/mesher/meshing_defaults.py +252 -0
- nmesh/mesher/meshing_parameters.py +185 -0
- nmesh/mesher/parity.py +21 -0
- nmesh/mesher/parity_canonical.py +142 -0
- nmesh/mesher/parity_comparison.py +191 -0
- nmesh/mesher/parity_metrics.py +114 -0
- nmesh/mesher/periodic.py +97 -0
- nmesh/mesher/relaxation/__init__.py +14 -0
- nmesh/mesher/relaxation/_constants.py +20 -0
- nmesh/mesher/relaxation/_types.py +15 -0
- nmesh/mesher/relaxation/density.py +170 -0
- nmesh/mesher/relaxation/engine/__init__.py +18 -0
- nmesh/mesher/relaxation/engine/state.py +155 -0
- nmesh/mesher/relaxation/engine/steps.py +248 -0
- nmesh/mesher/relaxation/engine/topology.py +230 -0
- nmesh/mesher/relaxation/forces/__init__.py +96 -0
- nmesh/mesher/relaxation/forces/jit.py +102 -0
- nmesh/mesher/relaxation/forces/neighbors.py +186 -0
- nmesh/mesher/relaxation/forces/simplex.py +302 -0
- nmesh/mesher/relaxation/forces/summary.py +207 -0
- nmesh/mesher/relaxation/forces/types.py +92 -0
- nmesh/mesher/relaxation/geometry/__init__.py +6 -0
- nmesh/mesher/relaxation/geometry/builder.py +154 -0
- nmesh/mesher/relaxation/geometry/model.py +194 -0
- nmesh/mesher/relaxation/seeding/__init__.py +74 -0
- nmesh/mesher/relaxation/seeding/periodic.py +88 -0
- nmesh/mesher/relaxation/seeding/points.py +88 -0
- nmesh/mesher/relaxation/seeding/sampling.py +142 -0
- nmesh/mesher/relaxation/topology/__init__.py +297 -0
- nmesh/mesher/relaxation/topology/finalize.py +78 -0
- nmesh/mesher/relaxation/topology/recovery.py +310 -0
- nmesh/mesher/sectioned_config.py +70 -0
- nmesh/nmesh.py +99 -0
- nmesh/py.typed +1 -0
- nmesh/utils/__init__.py +33 -0
- nmesh/utils/array_list_utils.py +128 -0
- nmesh/utils/constants.py +22 -0
- nmesh/utils/timing_memory_utils.py +51 -0
- nmesh/utils/types.py +13 -0
- si/constants.py +49 -0
- si/physical.py +722 -0
- si/py.typed +1 -0
- simulation/__init__.py +1 -0
- simulation/clock.py +237 -0
- simulation/data_writer.py +273 -0
- simulation/data_writer_collection.py +267 -0
- simulation/hysteresis.py +74 -0
- simulation/hysteresis_runner.py +286 -0
- simulation/hysteresis_schedule.py +180 -0
- simulation/inference/__init__.py +3 -0
- simulation/inference/inference.py +95 -0
- simulation/py.typed +1 -0
- simulation/quantity.py +88 -0
- simulation/simulation_core.py +458 -0
- throttler/__init__.py +3 -0
- throttler/py.typed +1 -0
- throttler/throttler.py +55 -0
- when/__init__.py +3 -0
- when/py.typed +1 -0
- when/when.py +416 -0
nmesh/mesh_generation.py
ADDED
|
@@ -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)
|
nmesh/mesh_utilities.py
ADDED
|
@@ -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
|
nmesh/mesher/__init__.py
ADDED
|
@@ -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
|
+
]
|