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/io/ascii.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Reader for the legacy ASCII ``.nmesh``/PYFEM mesh format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..backend import RawMesh
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_ascii_nmesh(path: str | Path) -> RawMesh:
|
|
13
|
+
"""Read a legacy ASCII ``.nmesh``/PYFEM mesh into :class:`RawMesh`."""
|
|
14
|
+
source = Path(path)
|
|
15
|
+
lines = [
|
|
16
|
+
line.strip()
|
|
17
|
+
for line in source.read_text(encoding="utf-8").splitlines()
|
|
18
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
19
|
+
]
|
|
20
|
+
cursor = 0
|
|
21
|
+
point_count = int(lines[cursor])
|
|
22
|
+
cursor += 1
|
|
23
|
+
points = [_parse_float_row(lines[cursor + index]) for index in range(point_count)]
|
|
24
|
+
cursor += point_count
|
|
25
|
+
dim = len(points[0]) if points else 0
|
|
26
|
+
|
|
27
|
+
simplex_count = int(lines[cursor])
|
|
28
|
+
cursor += 1
|
|
29
|
+
simplices: list[list[int]] = []
|
|
30
|
+
regions: list[int] = []
|
|
31
|
+
for _ in range(simplex_count):
|
|
32
|
+
values = _parse_int_row(lines[cursor])
|
|
33
|
+
cursor += 1
|
|
34
|
+
regions.append(values[0])
|
|
35
|
+
simplices.append(values[1:])
|
|
36
|
+
|
|
37
|
+
surface_count = int(lines[cursor])
|
|
38
|
+
cursor += 1
|
|
39
|
+
surfaces: list[list[int]] = []
|
|
40
|
+
for _ in range(surface_count):
|
|
41
|
+
values = _parse_int_row(lines[cursor])
|
|
42
|
+
cursor += 1
|
|
43
|
+
surfaces.append(values[-dim:] if dim > 0 else [])
|
|
44
|
+
|
|
45
|
+
periodic_groups: list[list[int]] = []
|
|
46
|
+
if cursor < len(lines):
|
|
47
|
+
periodic_count = int(lines[cursor])
|
|
48
|
+
cursor += 1
|
|
49
|
+
for _ in range(periodic_count):
|
|
50
|
+
values = _parse_int_row(lines[cursor])
|
|
51
|
+
cursor += 1
|
|
52
|
+
periodic_groups.append(values[1:])
|
|
53
|
+
|
|
54
|
+
return RawMesh(
|
|
55
|
+
points=points,
|
|
56
|
+
simplices=simplices,
|
|
57
|
+
regions=regions,
|
|
58
|
+
point_regions=_build_point_regions(len(points), simplices, regions),
|
|
59
|
+
surfaces=surfaces,
|
|
60
|
+
links=_build_links(simplices),
|
|
61
|
+
region_volumes=_region_volumes(points, simplices, regions, dim),
|
|
62
|
+
periodic_point_indices=periodic_groups,
|
|
63
|
+
permutation=list(range(len(points))),
|
|
64
|
+
dim=dim,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_float_row(line: str) -> list[float]:
|
|
69
|
+
return [float(value) for value in line.split()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_int_row(line: str) -> list[int]:
|
|
73
|
+
return [int(value) for value in line.split()]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_point_regions(
|
|
77
|
+
point_count: int,
|
|
78
|
+
simplices: list[list[int]],
|
|
79
|
+
regions: list[int],
|
|
80
|
+
) -> list[list[int]]:
|
|
81
|
+
memberships: list[set[int]] = [set() for _ in range(point_count)]
|
|
82
|
+
for simplex, region in zip(simplices, regions, strict=True):
|
|
83
|
+
for point_index in simplex:
|
|
84
|
+
memberships[int(point_index)].add(int(region))
|
|
85
|
+
return [sorted(group) for group in memberships]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_links(simplices: list[list[int]]) -> list[tuple[int, int]]:
|
|
89
|
+
links: set[tuple[int, int]] = set()
|
|
90
|
+
for simplex in simplices:
|
|
91
|
+
for left_index, left in enumerate(simplex):
|
|
92
|
+
for right in simplex[left_index + 1 :]:
|
|
93
|
+
a = int(left)
|
|
94
|
+
b = int(right)
|
|
95
|
+
links.add((a, b) if a <= b else (b, a))
|
|
96
|
+
return sorted(links)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _region_volumes(
|
|
100
|
+
points: list[list[float]],
|
|
101
|
+
simplices: list[list[int]],
|
|
102
|
+
regions: list[int],
|
|
103
|
+
dim: int,
|
|
104
|
+
) -> list[float]:
|
|
105
|
+
if not simplices:
|
|
106
|
+
return []
|
|
107
|
+
coords = np.asarray(points, dtype=float)
|
|
108
|
+
simplex_array = np.asarray(simplices, dtype=int)
|
|
109
|
+
measures = _simplex_measures(coords, simplex_array, dim)
|
|
110
|
+
totals = {int(region): 0.0 for region in regions}
|
|
111
|
+
for region, measure in zip(regions, measures, strict=True):
|
|
112
|
+
totals[int(region)] += float(measure)
|
|
113
|
+
return [totals[region] for region in sorted(totals)]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _simplex_measures(points: np.ndarray, simplices: np.ndarray, dim: int) -> np.ndarray:
|
|
117
|
+
if len(simplices) == 0:
|
|
118
|
+
return np.empty(0, dtype=float)
|
|
119
|
+
if dim == 1:
|
|
120
|
+
edges = points[simplices[:, 1]] - points[simplices[:, 0]]
|
|
121
|
+
return np.linalg.norm(edges, axis=1)
|
|
122
|
+
|
|
123
|
+
edge_matrices = points[simplices[:, 1:]] - points[simplices[:, [0]]]
|
|
124
|
+
gram_matrices = edge_matrices @ np.swapaxes(edge_matrices, 1, 2)
|
|
125
|
+
return np.sqrt(np.abs(np.linalg.det(gram_matrices))) / float(_factorial(dim))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _factorial(value: int) -> int:
|
|
129
|
+
result = 1
|
|
130
|
+
for item in range(2, value + 1):
|
|
131
|
+
result *= item
|
|
132
|
+
return result
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Support for the legacy ``.nmesh.h5`` mesh format."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from ..backend import RawMesh
|
|
12
|
+
from .ascii import _build_links, _build_point_regions, _region_volumes
|
|
13
|
+
|
|
14
|
+
FloatArray = NDArray[np.float64]
|
|
15
|
+
IntArray = NDArray[np.int_]
|
|
16
|
+
|
|
17
|
+
_REQUIRED_MESH_DATASETS = frozenset({"points", "simplices", "simplicesregions"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require_group(
|
|
21
|
+
parent: h5py.File | h5py.Group,
|
|
22
|
+
name: str,
|
|
23
|
+
*,
|
|
24
|
+
path: Path,
|
|
25
|
+
) -> h5py.Group:
|
|
26
|
+
"""Return a required child group after validating its HDF5 node type."""
|
|
27
|
+
node = parent.get(name)
|
|
28
|
+
if node is None:
|
|
29
|
+
raise ValueError(f"{path} is missing the /{name} group")
|
|
30
|
+
if not isinstance(node, h5py.Group):
|
|
31
|
+
raise ValueError(f"{path} has /{name}, but it is not an HDF5 group")
|
|
32
|
+
return node
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _optional_dataset(
|
|
36
|
+
parent: h5py.File | h5py.Group,
|
|
37
|
+
name: str,
|
|
38
|
+
*,
|
|
39
|
+
path: Path,
|
|
40
|
+
hdf5_path: str,
|
|
41
|
+
) -> h5py.Dataset | None:
|
|
42
|
+
"""Return an optional dataset after validating its HDF5 node type."""
|
|
43
|
+
node = parent.get(name)
|
|
44
|
+
if node is None:
|
|
45
|
+
return None
|
|
46
|
+
if not isinstance(node, h5py.Dataset):
|
|
47
|
+
raise ValueError(f"{path} has {hdf5_path}, but it is not an HDF5 dataset")
|
|
48
|
+
return node
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _require_dataset(
|
|
52
|
+
parent: h5py.File | h5py.Group,
|
|
53
|
+
name: str,
|
|
54
|
+
*,
|
|
55
|
+
path: Path,
|
|
56
|
+
hdf5_path: str,
|
|
57
|
+
) -> h5py.Dataset:
|
|
58
|
+
"""Return a required dataset after validating its HDF5 node type."""
|
|
59
|
+
dataset = _optional_dataset(
|
|
60
|
+
parent,
|
|
61
|
+
name,
|
|
62
|
+
path=path,
|
|
63
|
+
hdf5_path=hdf5_path,
|
|
64
|
+
)
|
|
65
|
+
if dataset is None:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{path} is missing one of /mesh/points, /mesh/simplices, or /mesh/simplicesregions"
|
|
68
|
+
)
|
|
69
|
+
return dataset
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _decode_hdf5_string(value: Any) -> str | None:
|
|
73
|
+
"""Convert HDF5 scalar or array string values into plain Python strings."""
|
|
74
|
+
if value is None:
|
|
75
|
+
return None
|
|
76
|
+
if isinstance(value, bytes):
|
|
77
|
+
return value.decode("utf-8")
|
|
78
|
+
if isinstance(value, str):
|
|
79
|
+
return value
|
|
80
|
+
if isinstance(value, np.ndarray):
|
|
81
|
+
array = np.asarray(value, dtype=object)
|
|
82
|
+
if array.shape == () or array.size == 1:
|
|
83
|
+
return _decode_hdf5_string(array.item())
|
|
84
|
+
return str(cast(object, value))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_legacy_nmesh_hdf5(path: str | Path) -> bool:
|
|
88
|
+
"""Return whether *path* has the recognizable legacy Nmesh HDF5 layout.
|
|
89
|
+
|
|
90
|
+
Older Nmesh files identify themselves with ``/etc/filetype == "nmesh"``.
|
|
91
|
+
Some valid historical files omit that optional marker, so the complete set
|
|
92
|
+
of required datasets in ``/mesh`` is also accepted as a signature.
|
|
93
|
+
"""
|
|
94
|
+
path = Path(path)
|
|
95
|
+
is_hdf5 = cast(Callable[[str], bool], vars(h5py)["is_hdf5"])
|
|
96
|
+
if not is_hdf5(str(path)):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with h5py.File(str(path), "r") as handle:
|
|
101
|
+
filetype_node = handle.get("etc/filetype")
|
|
102
|
+
if isinstance(filetype_node, h5py.Dataset):
|
|
103
|
+
return _decode_hdf5_string(filetype_node[()]) == "nmesh"
|
|
104
|
+
|
|
105
|
+
mesh_group = handle.get("mesh")
|
|
106
|
+
return isinstance(mesh_group, h5py.Group) and _REQUIRED_MESH_DATASETS.issubset(
|
|
107
|
+
mesh_group.keys()
|
|
108
|
+
)
|
|
109
|
+
except OSError:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _infer_dim(points: FloatArray, simplices: IntArray) -> int:
|
|
114
|
+
"""Infer the mesh dimension from simplex arity or point coordinates."""
|
|
115
|
+
if simplices.ndim == 2 and simplices.shape[1] in (2, 3, 4):
|
|
116
|
+
return simplices.shape[1] - 1
|
|
117
|
+
if points.ndim == 2 and points.shape[1] > 0:
|
|
118
|
+
return int(points.shape[1])
|
|
119
|
+
return 3
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _periodic_points_from_hdf5(
|
|
123
|
+
periodic_raw: IntArray | None,
|
|
124
|
+
) -> list[list[int]]:
|
|
125
|
+
"""Decode periodic-point rows, dropping the legacy ``-1`` padding markers."""
|
|
126
|
+
if periodic_raw is None:
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
periodic = np.asarray(periodic_raw, dtype=int)
|
|
130
|
+
if periodic.size == 0:
|
|
131
|
+
return []
|
|
132
|
+
if periodic.ndim == 1:
|
|
133
|
+
periodic = periodic[np.newaxis, :]
|
|
134
|
+
|
|
135
|
+
return [[idx for idx in row.tolist() if idx != -1] for row in periodic]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_raw_mesh_from_legacy_nmesh_hdf5(path: str | Path) -> RawMesh:
|
|
139
|
+
"""Load a :class:`RawMesh` from the legacy ``.nmesh.h5`` file layout."""
|
|
140
|
+
path = Path(path)
|
|
141
|
+
|
|
142
|
+
with h5py.File(str(path), "r") as handle:
|
|
143
|
+
mesh_group = _require_group(handle, "mesh", path=path)
|
|
144
|
+
|
|
145
|
+
filetype_node = _optional_dataset(
|
|
146
|
+
handle,
|
|
147
|
+
"etc/filetype",
|
|
148
|
+
path=path,
|
|
149
|
+
hdf5_path="/etc/filetype",
|
|
150
|
+
)
|
|
151
|
+
filetype = _decode_hdf5_string(filetype_node[()] if filetype_node is not None else None)
|
|
152
|
+
if filetype not in (None, "nmesh"):
|
|
153
|
+
raise ValueError(f"{path} has filetype '{filetype}', expected 'nmesh'")
|
|
154
|
+
|
|
155
|
+
points_dataset = _require_dataset(
|
|
156
|
+
mesh_group,
|
|
157
|
+
"points",
|
|
158
|
+
path=path,
|
|
159
|
+
hdf5_path="/mesh/points",
|
|
160
|
+
)
|
|
161
|
+
simplices_dataset = _require_dataset(
|
|
162
|
+
mesh_group,
|
|
163
|
+
"simplices",
|
|
164
|
+
path=path,
|
|
165
|
+
hdf5_path="/mesh/simplices",
|
|
166
|
+
)
|
|
167
|
+
regions_dataset = _require_dataset(
|
|
168
|
+
mesh_group,
|
|
169
|
+
"simplicesregions",
|
|
170
|
+
path=path,
|
|
171
|
+
hdf5_path="/mesh/simplicesregions",
|
|
172
|
+
)
|
|
173
|
+
points: FloatArray = np.asarray(points_dataset[...], dtype=np.float64)
|
|
174
|
+
simplices: IntArray = np.asarray(simplices_dataset[...], dtype=np.int_)
|
|
175
|
+
regions: IntArray = np.asarray(regions_dataset[...], dtype=np.int_).flatten()
|
|
176
|
+
|
|
177
|
+
periodic_dataset = _optional_dataset(
|
|
178
|
+
mesh_group,
|
|
179
|
+
"periodicpointindices",
|
|
180
|
+
path=path,
|
|
181
|
+
hdf5_path="/mesh/periodicpointindices",
|
|
182
|
+
)
|
|
183
|
+
permutation_dataset = _optional_dataset(
|
|
184
|
+
mesh_group,
|
|
185
|
+
"permutation",
|
|
186
|
+
path=path,
|
|
187
|
+
hdf5_path="/mesh/permutation",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
periodic_point_indices = _periodic_points_from_hdf5(
|
|
191
|
+
None if periodic_dataset is None else np.asarray(periodic_dataset[...], dtype=np.int_)
|
|
192
|
+
)
|
|
193
|
+
permutation: list[int] = (
|
|
194
|
+
[]
|
|
195
|
+
if permutation_dataset is None
|
|
196
|
+
else cast(
|
|
197
|
+
list[int],
|
|
198
|
+
np.asarray(permutation_dataset[...], dtype=np.int_).flatten().tolist(),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Validate data consistency
|
|
203
|
+
if len(points) == 0:
|
|
204
|
+
raise ValueError(f"{path} contains no points")
|
|
205
|
+
if len(simplices) == 0:
|
|
206
|
+
raise ValueError(f"{path} contains no simplices")
|
|
207
|
+
if len(regions) != len(simplices):
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"{path} has mismatched regions ({len(regions)}) and simplices ({len(simplices)})"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Validate simplex indices are within bounds
|
|
213
|
+
simplices_list = simplices.tolist()
|
|
214
|
+
max_index = max(max(simplex) for simplex in simplices_list)
|
|
215
|
+
if max_index >= len(points):
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"{path} has simplex with out-of-bounds point index {max_index} "
|
|
218
|
+
f"(only {len(points)} points)"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
points_list = points.tolist()
|
|
222
|
+
simplices_list = simplices.tolist()
|
|
223
|
+
regions_list = regions.tolist()
|
|
224
|
+
dim = _infer_dim(points, simplices)
|
|
225
|
+
|
|
226
|
+
return RawMesh(
|
|
227
|
+
points=points_list,
|
|
228
|
+
simplices=simplices_list,
|
|
229
|
+
regions=regions_list,
|
|
230
|
+
point_regions=_build_point_regions(len(points_list), simplices_list, regions_list),
|
|
231
|
+
links=_build_links(simplices_list),
|
|
232
|
+
region_volumes=_region_volumes(points_list, simplices_list, regions_list, dim),
|
|
233
|
+
periodic_point_indices=periodic_point_indices,
|
|
234
|
+
permutation=permutation,
|
|
235
|
+
dim=dim,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def save_raw_mesh_as_legacy_nmesh_hdf5(path: str | Path, raw_mesh: RawMesh) -> None:
|
|
240
|
+
"""Write a :class:`RawMesh` using the legacy ``.nmesh.h5`` file layout."""
|
|
241
|
+
|
|
242
|
+
path = Path(path)
|
|
243
|
+
points = np.asarray(raw_mesh.points, dtype=float)
|
|
244
|
+
simplices = np.asarray(raw_mesh.simplices, dtype=np.int32)
|
|
245
|
+
regions = np.asarray(raw_mesh.regions or [1] * len(raw_mesh.simplices), dtype=np.int32)
|
|
246
|
+
if len(points) == 0:
|
|
247
|
+
raise ValueError("Cannot save an empty mesh without points.")
|
|
248
|
+
if len(simplices) == 0:
|
|
249
|
+
raise ValueError("Cannot save an empty mesh without simplices.")
|
|
250
|
+
if len(regions) != len(simplices):
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"Cannot save mesh with {len(regions)} regions for {len(simplices)} simplices."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
with h5py.File(str(path), "w") as handle:
|
|
256
|
+
_set_pytables_group_attrs(handle, title="")
|
|
257
|
+
etc_group = handle.create_group("etc")
|
|
258
|
+
mesh_group = handle.create_group("mesh")
|
|
259
|
+
_set_pytables_group_attrs(etc_group, title="Configuration and version data")
|
|
260
|
+
_set_pytables_group_attrs(mesh_group, title="Mesh data")
|
|
261
|
+
|
|
262
|
+
filetype = etc_group.create_dataset("filetype", data=np.asarray([b"nmesh"]))
|
|
263
|
+
_set_pytables_array_attrs(filetype, title="data file type", version="2.3")
|
|
264
|
+
fileversion = etc_group.create_dataset("fileversion", data=np.asarray([b"1.0"]))
|
|
265
|
+
_set_pytables_array_attrs(fileversion, title="data file type version", version="2.3")
|
|
266
|
+
points_dataset = mesh_group.create_dataset("points", data=points)
|
|
267
|
+
_set_pytables_array_attrs(
|
|
268
|
+
points_dataset,
|
|
269
|
+
title="Positions of mesh nodes (=points)",
|
|
270
|
+
)
|
|
271
|
+
simplices_dataset = mesh_group.create_dataset("simplices", data=simplices)
|
|
272
|
+
_set_pytables_array_attrs(
|
|
273
|
+
simplices_dataset,
|
|
274
|
+
title="Indices of nodes (starting from zero). Each row is one simplex.",
|
|
275
|
+
)
|
|
276
|
+
regions_dataset = mesh_group.create_dataset("simplicesregions", data=regions)
|
|
277
|
+
_set_pytables_array_attrs(
|
|
278
|
+
regions_dataset,
|
|
279
|
+
title="Region ids (one for each simplex).",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if raw_mesh.periodic_point_indices:
|
|
283
|
+
periodic = _padded_int_rows(raw_mesh.periodic_point_indices, fill=-1)
|
|
284
|
+
periodic_dataset = mesh_group.create_dataset("periodicpointindices", data=periodic)
|
|
285
|
+
_set_pytables_array_attrs(periodic_dataset, title="Periodic point indices")
|
|
286
|
+
if raw_mesh.permutation:
|
|
287
|
+
permutation_dataset = mesh_group.create_dataset(
|
|
288
|
+
"permutation",
|
|
289
|
+
data=np.asarray(raw_mesh.permutation, dtype=np.int32),
|
|
290
|
+
)
|
|
291
|
+
_set_pytables_array_attrs(permutation_dataset, title="Node permutation")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _padded_int_rows(rows: list[list[int]], *, fill: int) -> np.ndarray:
|
|
295
|
+
width = max(len(row) for row in rows)
|
|
296
|
+
result = np.full((len(rows), width), fill, dtype=np.int32)
|
|
297
|
+
for row_index, row in enumerate(rows):
|
|
298
|
+
result[row_index, : len(row)] = np.asarray(row, dtype=np.int32)
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _set_pytables_group_attrs(group: h5py.Group | h5py.File, *, title: str) -> None:
|
|
303
|
+
group.attrs["CLASS"] = np.bytes_(b"GROUP")
|
|
304
|
+
group.attrs["TITLE"] = np.bytes_(title.encode("utf-8"))
|
|
305
|
+
group.attrs["VERSION"] = np.bytes_(b"1.0")
|
|
306
|
+
if isinstance(group, h5py.File):
|
|
307
|
+
group.attrs["PYTABLES_FORMAT_VERSION"] = np.bytes_(b"2.0")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _set_pytables_array_attrs(
|
|
311
|
+
dataset: h5py.Dataset,
|
|
312
|
+
*,
|
|
313
|
+
title: str,
|
|
314
|
+
version: str = "1.0",
|
|
315
|
+
) -> None:
|
|
316
|
+
dataset.attrs["CLASS"] = np.bytes_(b"CARRAY")
|
|
317
|
+
dataset.attrs["TITLE"] = np.bytes_(title.encode("utf-8"))
|
|
318
|
+
dataset.attrs["VERSION"] = np.bytes_(version.encode("utf-8"))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Mesh file IO built around ``meshio`` and legacy Nmesh HDF5 support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, cast
|
|
8
|
+
|
|
9
|
+
import meshio
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
from ..backend import RawMesh
|
|
14
|
+
from ..utils.types import ArrayLike
|
|
15
|
+
from .legacy_nmesh_hdf5 import (
|
|
16
|
+
is_legacy_nmesh_hdf5,
|
|
17
|
+
load_raw_mesh_from_legacy_nmesh_hdf5,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_CELL_TYPE_BY_DIM = {
|
|
21
|
+
1: "line",
|
|
22
|
+
2: "triangle",
|
|
23
|
+
3: "tetra",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_DIM_BY_CELL_TYPE = {value: key for key, value in _CELL_TYPE_BY_DIM.items()}
|
|
27
|
+
|
|
28
|
+
_REGION_DATA_KEYS = ("region", "gmsh:physical", "cell_tags", "gmsh:geometrical")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _CellBlock(Protocol):
|
|
32
|
+
type: str
|
|
33
|
+
data: NDArray[np.integer]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _MeshData(Protocol):
|
|
37
|
+
points: NDArray[np.floating]
|
|
38
|
+
cells: Sequence[_CellBlock]
|
|
39
|
+
cell_data: Mapping[str, Sequence[ArrayLike]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_meshio(path: Path) -> _MeshData:
|
|
43
|
+
read = cast("object", vars(meshio)["read"])
|
|
44
|
+
if not callable(read):
|
|
45
|
+
raise TypeError("meshio.read is not callable")
|
|
46
|
+
return cast(_MeshData, read(path))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _write_meshio(path: Path, mesh: object) -> None:
|
|
50
|
+
write = cast("object", vars(meshio)["write"])
|
|
51
|
+
if not callable(write):
|
|
52
|
+
raise TypeError("meshio.write is not callable")
|
|
53
|
+
write(path, mesh)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cell_type_for(raw_mesh: RawMesh) -> str:
|
|
57
|
+
"""Return the meshio cell type that matches the raw mesh topology."""
|
|
58
|
+
if raw_mesh.simplices and len(raw_mesh.simplices) > 0:
|
|
59
|
+
simplex_size = len(raw_mesh.simplices[0])
|
|
60
|
+
if simplex_size == 2:
|
|
61
|
+
return "line"
|
|
62
|
+
if simplex_size == 3:
|
|
63
|
+
return "triangle"
|
|
64
|
+
if simplex_size == 4:
|
|
65
|
+
return "tetra"
|
|
66
|
+
|
|
67
|
+
return _CELL_TYPE_BY_DIM.get(raw_mesh.dim, "tetra")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _regions_from_meshio(
|
|
71
|
+
mesh: _MeshData,
|
|
72
|
+
cell_blocks: Sequence[tuple[int, _CellBlock]],
|
|
73
|
+
) -> list[int]:
|
|
74
|
+
"""Extract region ids aligned with the selected meshio cell blocks."""
|
|
75
|
+
cell_data = getattr(mesh, "cell_data", {})
|
|
76
|
+
total_count = sum(len(cell_block.data) for _, cell_block in cell_blocks)
|
|
77
|
+
for key in _REGION_DATA_KEYS:
|
|
78
|
+
if key not in cell_data:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
values_by_block = cell_data[key]
|
|
82
|
+
regions: list[int] = []
|
|
83
|
+
for block_index, cell_block in cell_blocks:
|
|
84
|
+
if block_index >= len(values_by_block):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Region data {key!r} has no entry for cell block {block_index} "
|
|
87
|
+
f"({cell_block.type})"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
values = np.asarray(values_by_block[block_index])
|
|
91
|
+
expected_count = len(cell_block.data)
|
|
92
|
+
if values.size != expected_count:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Region data {key!r} for cell block {block_index} "
|
|
95
|
+
f"({cell_block.type}) has {values.size} values; "
|
|
96
|
+
f"expected {expected_count}"
|
|
97
|
+
)
|
|
98
|
+
integer_values = np.asarray(values, dtype=np.int_)
|
|
99
|
+
regions.extend(cast(list[int], integer_values.tolist()))
|
|
100
|
+
return regions
|
|
101
|
+
|
|
102
|
+
return [1] * total_count
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_raw_mesh_from_meshio(path: Path) -> RawMesh:
|
|
106
|
+
"""Load a supported simplex mesh directly through ``meshio``."""
|
|
107
|
+
mesh = _read_meshio(path)
|
|
108
|
+
supported_blocks = [
|
|
109
|
+
(index, cell_block)
|
|
110
|
+
for index, cell_block in enumerate(mesh.cells)
|
|
111
|
+
if cell_block.type in _DIM_BY_CELL_TYPE
|
|
112
|
+
]
|
|
113
|
+
if not supported_blocks:
|
|
114
|
+
raise ValueError(f"No supported simplex cells found in {path}")
|
|
115
|
+
|
|
116
|
+
dim = max(_DIM_BY_CELL_TYPE[cell_block.type] for _, cell_block in supported_blocks)
|
|
117
|
+
selected_blocks = [
|
|
118
|
+
(index, cell_block)
|
|
119
|
+
for index, cell_block in supported_blocks
|
|
120
|
+
if _DIM_BY_CELL_TYPE[cell_block.type] == dim
|
|
121
|
+
]
|
|
122
|
+
simplices = [
|
|
123
|
+
simplex
|
|
124
|
+
for _, cell_block in selected_blocks
|
|
125
|
+
for simplex in np.asarray(cell_block.data, dtype=int).tolist()
|
|
126
|
+
]
|
|
127
|
+
return RawMesh(
|
|
128
|
+
points=mesh.points.astype(float).tolist(),
|
|
129
|
+
simplices=simplices,
|
|
130
|
+
regions=_regions_from_meshio(mesh, selected_blocks),
|
|
131
|
+
dim=dim,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def save_raw_mesh_with_meshio(path: str | Path, raw_mesh: RawMesh) -> None:
|
|
136
|
+
"""Write a raw mesh to any meshio-supported format."""
|
|
137
|
+
cell_type = _cell_type_for(raw_mesh)
|
|
138
|
+
cells: list[tuple[str, ArrayLike] | meshio.CellBlock] = [
|
|
139
|
+
(cell_type, np.asarray(raw_mesh.simplices, dtype=int))
|
|
140
|
+
]
|
|
141
|
+
cell_data: dict[str, list[ArrayLike]] | None = None
|
|
142
|
+
if raw_mesh.regions:
|
|
143
|
+
cell_data = {"region": [np.asarray(raw_mesh.regions, dtype=int)]}
|
|
144
|
+
|
|
145
|
+
mesh = meshio.Mesh(
|
|
146
|
+
points=np.asarray(raw_mesh.points, dtype=float),
|
|
147
|
+
cells=cells,
|
|
148
|
+
cell_data=cell_data,
|
|
149
|
+
)
|
|
150
|
+
_write_meshio(Path(path), mesh)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def load_raw_mesh_with_meshio(path: str | Path) -> RawMesh:
|
|
154
|
+
"""Load a raw mesh through ``meshio`` or the recognized legacy HDF5 loader.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Path to the mesh file.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The loaded mesh as a RawMesh object.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: If the file format is not supported or the file is malformed.
|
|
164
|
+
IOError/OSError: If the file cannot be read.
|
|
165
|
+
"""
|
|
166
|
+
path = Path(path)
|
|
167
|
+
if is_legacy_nmesh_hdf5(path):
|
|
168
|
+
return load_raw_mesh_from_legacy_nmesh_hdf5(path)
|
|
169
|
+
|
|
170
|
+
return _load_raw_mesh_from_meshio(path)
|