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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Canonical mesh signatures for exact parity comparisons."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from ..backend import RawMesh
|
|
9
|
+
|
|
10
|
+
CoordinateKey: TypeAlias = tuple[str | int, ...]
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class CanonicalMeshSignature:
|
|
14
|
+
"""Order-independent mesh signature for legacy parity checks.
|
|
15
|
+
|
|
16
|
+
Topology and metadata are compared exactly after canonical point reindexing.
|
|
17
|
+
Coordinate tolerance is opt-in and only affects coordinate keys; leaving it
|
|
18
|
+
as ``None`` uses exact binary floating-point identity.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
dim: int
|
|
22
|
+
point_keys: tuple[CoordinateKey, ...]
|
|
23
|
+
point_regions: tuple[tuple[int, ...], ...]
|
|
24
|
+
simplices: tuple[tuple[int, tuple[int, ...]], ...]
|
|
25
|
+
surfaces: tuple[tuple[int, ...], ...]
|
|
26
|
+
links: tuple[tuple[int, int], ...]
|
|
27
|
+
periodic_groups: tuple[tuple[int, ...], ...]
|
|
28
|
+
region_volumes: tuple[float, ...]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def canonical_mesh_signature(
|
|
32
|
+
raw_mesh: RawMesh,
|
|
33
|
+
*,
|
|
34
|
+
coordinate_tolerance: float | None = None,
|
|
35
|
+
) -> CanonicalMeshSignature:
|
|
36
|
+
"""Return a canonical signature suitable for exact parity assertions."""
|
|
37
|
+
|
|
38
|
+
point_keys = [_coordinate_key(point, coordinate_tolerance) for point in raw_mesh.points]
|
|
39
|
+
canonical_order = sorted(range(len(point_keys)), key=point_keys.__getitem__)
|
|
40
|
+
canonical_index = {old_index: new_index for new_index, old_index in enumerate(canonical_order)}
|
|
41
|
+
_require_unique_points(point_keys)
|
|
42
|
+
|
|
43
|
+
ordered_point_keys = tuple(point_keys[index] for index in canonical_order)
|
|
44
|
+
ordered_point_regions = tuple(
|
|
45
|
+
tuple(sorted(map(int, _point_regions_at(raw_mesh, index)))) for index in canonical_order
|
|
46
|
+
)
|
|
47
|
+
canonical_simplices = tuple(
|
|
48
|
+
sorted(
|
|
49
|
+
(
|
|
50
|
+
int(region),
|
|
51
|
+
tuple(canonical_index[int(point_index)] for point_index in simplex),
|
|
52
|
+
)
|
|
53
|
+
for region, simplex in zip(raw_mesh.regions, raw_mesh.simplices, strict=True)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
canonical_surfaces = tuple(
|
|
57
|
+
sorted(
|
|
58
|
+
tuple(sorted(canonical_index[int(point_index)] for point_index in surface))
|
|
59
|
+
for surface in raw_mesh.surfaces
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
canonical_links = tuple(
|
|
63
|
+
sorted(
|
|
64
|
+
(
|
|
65
|
+
min(canonical_index[int(left)], canonical_index[int(right)]),
|
|
66
|
+
max(canonical_index[int(left)], canonical_index[int(right)]),
|
|
67
|
+
)
|
|
68
|
+
for left, right in raw_mesh.links
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
canonical_periodic = tuple(
|
|
72
|
+
sorted(
|
|
73
|
+
tuple(sorted(canonical_index[int(point_index)] for point_index in group))
|
|
74
|
+
for group in raw_mesh.periodic_point_indices
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return CanonicalMeshSignature(
|
|
78
|
+
dim=int(raw_mesh.dim),
|
|
79
|
+
point_keys=ordered_point_keys,
|
|
80
|
+
point_regions=ordered_point_regions,
|
|
81
|
+
simplices=canonical_simplices,
|
|
82
|
+
surfaces=canonical_surfaces,
|
|
83
|
+
links=canonical_links,
|
|
84
|
+
periodic_groups=canonical_periodic,
|
|
85
|
+
region_volumes=tuple(float(volume) for volume in raw_mesh.region_volumes),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def assert_canonical_mesh_equal(
|
|
90
|
+
actual: RawMesh,
|
|
91
|
+
expected: RawMesh,
|
|
92
|
+
*,
|
|
93
|
+
coordinate_tolerance: float | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Assert exact canonical parity between two meshes."""
|
|
96
|
+
|
|
97
|
+
actual_signature = canonical_mesh_signature(
|
|
98
|
+
actual,
|
|
99
|
+
coordinate_tolerance=coordinate_tolerance,
|
|
100
|
+
)
|
|
101
|
+
expected_signature = canonical_mesh_signature(
|
|
102
|
+
expected,
|
|
103
|
+
coordinate_tolerance=coordinate_tolerance,
|
|
104
|
+
)
|
|
105
|
+
if actual_signature != expected_signature:
|
|
106
|
+
raise AssertionError(
|
|
107
|
+
"Canonical mesh signatures differ:\n"
|
|
108
|
+
f"actual={actual_signature!r}\n"
|
|
109
|
+
f"expected={expected_signature!r}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _coordinate_key(
|
|
114
|
+
point: list[float],
|
|
115
|
+
coordinate_tolerance: float | None,
|
|
116
|
+
) -> CoordinateKey:
|
|
117
|
+
"""Return an exact or explicitly quantized coordinate key."""
|
|
118
|
+
|
|
119
|
+
if coordinate_tolerance is None:
|
|
120
|
+
return tuple(float(value).hex() for value in point)
|
|
121
|
+
if coordinate_tolerance <= 0.0:
|
|
122
|
+
raise ValueError("coordinate_tolerance must be positive when provided")
|
|
123
|
+
return tuple(int(round(float(value) / coordinate_tolerance)) for value in point)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _require_unique_points(point_keys: list[CoordinateKey]) -> None:
|
|
127
|
+
"""Reject signatures where canonical point identity is ambiguous."""
|
|
128
|
+
|
|
129
|
+
if len(set(point_keys)) != len(point_keys):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"Canonical mesh signature requires unique point coordinates at the "
|
|
132
|
+
"selected coordinate tolerance"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _point_regions_at(raw_mesh: RawMesh, index: int) -> list[int]:
|
|
137
|
+
"""Return point-region membership for a point, tolerating absent metadata."""
|
|
138
|
+
|
|
139
|
+
if index < len(raw_mesh.point_regions):
|
|
140
|
+
return raw_mesh.point_regions[index]
|
|
141
|
+
return []
|
|
142
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Tolerance-aware comparisons for mesh metric summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ..backend import RawMesh
|
|
8
|
+
from .parity_metrics import MeshMetricSummary, mesh_metric_summary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class MeshMetricComparison:
|
|
13
|
+
"""Result of a metric-level comparison between two meshes."""
|
|
14
|
+
|
|
15
|
+
actual: MeshMetricSummary
|
|
16
|
+
expected: MeshMetricSummary
|
|
17
|
+
failures: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def passed(self) -> bool:
|
|
21
|
+
"""Return whether all configured metric checks passed."""
|
|
22
|
+
|
|
23
|
+
return not self.failures
|
|
24
|
+
def compare_mesh_metrics(
|
|
25
|
+
actual: RawMesh,
|
|
26
|
+
expected: RawMesh,
|
|
27
|
+
*,
|
|
28
|
+
count_relative_tolerance: float = 0.25,
|
|
29
|
+
volume_relative_tolerance: float = 0.05,
|
|
30
|
+
length_relative_tolerance: float = 0.10,
|
|
31
|
+
absolute_tolerance: float = 1.0e-9,
|
|
32
|
+
) -> MeshMetricComparison:
|
|
33
|
+
"""Compare two meshes with documented metric-level tolerances."""
|
|
34
|
+
|
|
35
|
+
actual_summary = mesh_metric_summary(actual)
|
|
36
|
+
expected_summary = mesh_metric_summary(expected)
|
|
37
|
+
failures: list[str] = []
|
|
38
|
+
_compare_exact("dim", actual_summary.dim, expected_summary.dim, failures)
|
|
39
|
+
_compare_relative_count(
|
|
40
|
+
"point_count",
|
|
41
|
+
actual_summary.point_count,
|
|
42
|
+
expected_summary.point_count,
|
|
43
|
+
count_relative_tolerance,
|
|
44
|
+
failures,
|
|
45
|
+
)
|
|
46
|
+
_compare_relative_count(
|
|
47
|
+
"simplex_count",
|
|
48
|
+
actual_summary.simplex_count,
|
|
49
|
+
expected_summary.simplex_count,
|
|
50
|
+
count_relative_tolerance,
|
|
51
|
+
failures,
|
|
52
|
+
)
|
|
53
|
+
_compare_relative_count(
|
|
54
|
+
"surface_count",
|
|
55
|
+
actual_summary.surface_count,
|
|
56
|
+
expected_summary.surface_count,
|
|
57
|
+
count_relative_tolerance,
|
|
58
|
+
failures,
|
|
59
|
+
)
|
|
60
|
+
_compare_exact(
|
|
61
|
+
"region ids", _region_ids(actual_summary), _region_ids(expected_summary), failures
|
|
62
|
+
)
|
|
63
|
+
_compare_exact(
|
|
64
|
+
"periodic_group_count",
|
|
65
|
+
actual_summary.periodic_group_count,
|
|
66
|
+
expected_summary.periodic_group_count,
|
|
67
|
+
failures,
|
|
68
|
+
)
|
|
69
|
+
_compare_vector(
|
|
70
|
+
"bbox_min",
|
|
71
|
+
actual_summary.bbox_min,
|
|
72
|
+
expected_summary.bbox_min,
|
|
73
|
+
length_relative_tolerance,
|
|
74
|
+
absolute_tolerance,
|
|
75
|
+
failures,
|
|
76
|
+
)
|
|
77
|
+
_compare_vector(
|
|
78
|
+
"bbox_max",
|
|
79
|
+
actual_summary.bbox_max,
|
|
80
|
+
expected_summary.bbox_max,
|
|
81
|
+
length_relative_tolerance,
|
|
82
|
+
absolute_tolerance,
|
|
83
|
+
failures,
|
|
84
|
+
)
|
|
85
|
+
_compare_region_volumes(
|
|
86
|
+
actual_summary.region_volumes,
|
|
87
|
+
expected_summary.region_volumes,
|
|
88
|
+
volume_relative_tolerance,
|
|
89
|
+
absolute_tolerance,
|
|
90
|
+
failures,
|
|
91
|
+
)
|
|
92
|
+
for field_name in (
|
|
93
|
+
"simplex_measure_min",
|
|
94
|
+
"simplex_measure_mean",
|
|
95
|
+
"simplex_measure_max",
|
|
96
|
+
"edge_length_min",
|
|
97
|
+
"edge_length_mean",
|
|
98
|
+
"edge_length_max",
|
|
99
|
+
):
|
|
100
|
+
_compare_float(
|
|
101
|
+
field_name,
|
|
102
|
+
float(getattr(actual_summary, field_name)),
|
|
103
|
+
float(getattr(expected_summary, field_name)),
|
|
104
|
+
length_relative_tolerance,
|
|
105
|
+
absolute_tolerance,
|
|
106
|
+
failures,
|
|
107
|
+
)
|
|
108
|
+
return MeshMetricComparison(
|
|
109
|
+
actual=actual_summary,
|
|
110
|
+
expected=expected_summary,
|
|
111
|
+
failures=tuple(failures),
|
|
112
|
+
)
|
|
113
|
+
def _compare_exact(name: str, actual: object, expected: object, failures: list[str]) -> None:
|
|
114
|
+
if actual != expected:
|
|
115
|
+
failures.append(f"{name}: actual={actual!r}, expected={expected!r}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _compare_relative_count(
|
|
119
|
+
name: str,
|
|
120
|
+
actual: int,
|
|
121
|
+
expected: int,
|
|
122
|
+
relative_tolerance: float,
|
|
123
|
+
failures: list[str],
|
|
124
|
+
) -> None:
|
|
125
|
+
allowed = max(1.0, abs(float(expected)) * relative_tolerance)
|
|
126
|
+
if abs(float(actual - expected)) > allowed:
|
|
127
|
+
failures.append(f"{name}: actual={actual}, expected={expected}, allowed_delta={allowed:g}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _compare_vector(
|
|
131
|
+
name: str,
|
|
132
|
+
actual: tuple[float, ...],
|
|
133
|
+
expected: tuple[float, ...],
|
|
134
|
+
relative_tolerance: float,
|
|
135
|
+
absolute_tolerance: float,
|
|
136
|
+
failures: list[str],
|
|
137
|
+
) -> None:
|
|
138
|
+
if len(actual) != len(expected):
|
|
139
|
+
failures.append(f"{name}: actual={actual!r}, expected={expected!r}")
|
|
140
|
+
return
|
|
141
|
+
for index, (actual_value, expected_value) in enumerate(zip(actual, expected, strict=True)):
|
|
142
|
+
_compare_float(
|
|
143
|
+
f"{name}[{index}]",
|
|
144
|
+
actual_value,
|
|
145
|
+
expected_value,
|
|
146
|
+
relative_tolerance,
|
|
147
|
+
absolute_tolerance,
|
|
148
|
+
failures,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _compare_region_volumes(
|
|
153
|
+
actual: tuple[tuple[int, float], ...],
|
|
154
|
+
expected: tuple[tuple[int, float], ...],
|
|
155
|
+
relative_tolerance: float,
|
|
156
|
+
absolute_tolerance: float,
|
|
157
|
+
failures: list[str],
|
|
158
|
+
) -> None:
|
|
159
|
+
if tuple(region for region, _volume in actual) != tuple(region for region, _volume in expected):
|
|
160
|
+
failures.append(f"region volumes ids: actual={actual!r}, expected={expected!r}")
|
|
161
|
+
return
|
|
162
|
+
for (region, actual_volume), (_expected_region, expected_volume) in zip(
|
|
163
|
+
actual, expected, strict=True
|
|
164
|
+
):
|
|
165
|
+
_compare_float(
|
|
166
|
+
f"region_volumes[{region}]",
|
|
167
|
+
actual_volume,
|
|
168
|
+
expected_volume,
|
|
169
|
+
relative_tolerance,
|
|
170
|
+
absolute_tolerance,
|
|
171
|
+
failures,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _compare_float(
|
|
176
|
+
name: str,
|
|
177
|
+
actual: float,
|
|
178
|
+
expected: float,
|
|
179
|
+
relative_tolerance: float,
|
|
180
|
+
absolute_tolerance: float,
|
|
181
|
+
failures: list[str],
|
|
182
|
+
) -> None:
|
|
183
|
+
allowed = max(absolute_tolerance, abs(expected) * relative_tolerance)
|
|
184
|
+
if abs(actual - expected) > allowed:
|
|
185
|
+
failures.append(
|
|
186
|
+
f"{name}: actual={actual:g}, expected={expected:g}, allowed_delta={allowed:g}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _region_ids(summary: MeshMetricSummary) -> tuple[int, ...]:
|
|
191
|
+
return tuple(region for region, _count in summary.region_counts)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Geometry metrics for modernization mesh comparisons."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..backend import RawMesh
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class MeshMetricSummary:
|
|
14
|
+
"""Geometry-oriented mesh summary for modernization parity checks."""
|
|
15
|
+
|
|
16
|
+
dim: int
|
|
17
|
+
point_count: int
|
|
18
|
+
simplex_count: int
|
|
19
|
+
surface_count: int
|
|
20
|
+
link_count: int
|
|
21
|
+
periodic_group_count: int
|
|
22
|
+
region_counts: tuple[tuple[int, int], ...]
|
|
23
|
+
region_volumes: tuple[tuple[int, float], ...]
|
|
24
|
+
bbox_min: tuple[float, ...]
|
|
25
|
+
bbox_max: tuple[float, ...]
|
|
26
|
+
simplex_measure_min: float
|
|
27
|
+
simplex_measure_mean: float
|
|
28
|
+
simplex_measure_max: float
|
|
29
|
+
edge_length_min: float
|
|
30
|
+
edge_length_mean: float
|
|
31
|
+
edge_length_max: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def mesh_metric_summary(raw_mesh: RawMesh) -> MeshMetricSummary:
|
|
35
|
+
"""Return numeric mesh metrics useful for legacy-vs-modern comparisons."""
|
|
36
|
+
|
|
37
|
+
points = np.asarray(raw_mesh.points, dtype=float)
|
|
38
|
+
dim = int(raw_mesh.dim)
|
|
39
|
+
if points.size == 0:
|
|
40
|
+
points = np.empty((0, dim), dtype=float)
|
|
41
|
+
elif points.ndim == 1:
|
|
42
|
+
points = points[np.newaxis, :]
|
|
43
|
+
|
|
44
|
+
simplices = np.asarray(raw_mesh.simplices, dtype=int)
|
|
45
|
+
simplex_measures = _simplex_measures(points, simplices, dim)
|
|
46
|
+
edge_lengths = _edge_lengths(points, raw_mesh.links)
|
|
47
|
+
region_counts = tuple(
|
|
48
|
+
sorted(
|
|
49
|
+
(int(region), int(sum(1 for value in raw_mesh.regions if int(value) == int(region))))
|
|
50
|
+
for region in set(raw_mesh.regions)
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
region_volumes = tuple(
|
|
54
|
+
sorted(
|
|
55
|
+
zip(
|
|
56
|
+
sorted(set(map(int, raw_mesh.regions))),
|
|
57
|
+
map(float, raw_mesh.region_volumes),
|
|
58
|
+
strict=True,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return MeshMetricSummary(
|
|
63
|
+
dim=dim,
|
|
64
|
+
point_count=len(raw_mesh.points),
|
|
65
|
+
simplex_count=len(raw_mesh.simplices),
|
|
66
|
+
surface_count=len(raw_mesh.surfaces),
|
|
67
|
+
link_count=len(raw_mesh.links),
|
|
68
|
+
periodic_group_count=len(raw_mesh.periodic_point_indices),
|
|
69
|
+
region_counts=region_counts,
|
|
70
|
+
region_volumes=region_volumes,
|
|
71
|
+
bbox_min=tuple(np.min(points, axis=0).tolist()) if len(points) else (),
|
|
72
|
+
bbox_max=tuple(np.max(points, axis=0).tolist()) if len(points) else (),
|
|
73
|
+
simplex_measure_min=_safe_min(simplex_measures),
|
|
74
|
+
simplex_measure_mean=_safe_mean(simplex_measures),
|
|
75
|
+
simplex_measure_max=_safe_max(simplex_measures),
|
|
76
|
+
edge_length_min=_safe_min(edge_lengths),
|
|
77
|
+
edge_length_mean=_safe_mean(edge_lengths),
|
|
78
|
+
edge_length_max=_safe_max(edge_lengths),
|
|
79
|
+
)
|
|
80
|
+
def _simplex_measures(points: np.ndarray, simplices: np.ndarray, dim: int) -> np.ndarray:
|
|
81
|
+
if len(simplices) == 0:
|
|
82
|
+
return np.empty(0, dtype=float)
|
|
83
|
+
if dim == 1:
|
|
84
|
+
return np.abs(points[simplices[:, 1], 0] - points[simplices[:, 0], 0])
|
|
85
|
+
matrices = points[simplices[:, 1:]] - points[simplices[:, [0]]]
|
|
86
|
+
return np.abs(np.linalg.det(matrices)) / float(_factorial(dim))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _edge_lengths(points: np.ndarray, links: list[tuple[int, int]]) -> np.ndarray:
|
|
90
|
+
if not links:
|
|
91
|
+
return np.empty(0, dtype=float)
|
|
92
|
+
return np.asarray(
|
|
93
|
+
[np.linalg.norm(points[int(left)] - points[int(right)]) for left, right in links],
|
|
94
|
+
dtype=float,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _factorial(value: int) -> int:
|
|
99
|
+
result = 1
|
|
100
|
+
for item in range(2, value + 1):
|
|
101
|
+
result *= item
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _safe_min(values: np.ndarray) -> float:
|
|
106
|
+
return float(np.min(values)) if len(values) else 0.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _safe_mean(values: np.ndarray) -> float:
|
|
110
|
+
return float(np.mean(values)) if len(values) else 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _safe_max(values: np.ndarray) -> float:
|
|
114
|
+
return float(np.max(values)) if len(values) else 0.0
|
nmesh/mesher/periodic.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Helpers for building periodic-equivalence groups from mesh coordinates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def merge_periodic_groups(groups: Iterable[Iterable[int]]) -> list[list[int]]:
|
|
12
|
+
"""Merge overlapping periodic point groups into disjoint sorted groups."""
|
|
13
|
+
|
|
14
|
+
merged: list[set[int]] = []
|
|
15
|
+
for group in groups:
|
|
16
|
+
candidate = {int(index) for index in group}
|
|
17
|
+
if len(candidate) < 2:
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
overlaps: list[set[int]] = []
|
|
21
|
+
survivors: list[set[int]] = []
|
|
22
|
+
for existing in merged:
|
|
23
|
+
if existing & candidate:
|
|
24
|
+
overlaps.append(existing)
|
|
25
|
+
else:
|
|
26
|
+
survivors.append(existing)
|
|
27
|
+
|
|
28
|
+
for overlap in overlaps:
|
|
29
|
+
candidate.update(overlap)
|
|
30
|
+
|
|
31
|
+
survivors.append(candidate)
|
|
32
|
+
merged = survivors
|
|
33
|
+
|
|
34
|
+
return [sorted(group) for group in merged]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_periodic_groups(
|
|
38
|
+
points: np.ndarray,
|
|
39
|
+
bbox_min: np.ndarray,
|
|
40
|
+
bbox_max: np.ndarray,
|
|
41
|
+
periodic_axes: Iterable[bool | float],
|
|
42
|
+
*,
|
|
43
|
+
tolerance: float,
|
|
44
|
+
) -> list[list[int]]:
|
|
45
|
+
"""Build periodic point groups by matching opposite boundary points."""
|
|
46
|
+
|
|
47
|
+
if points.size == 0:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
periodic_flags = [bool(value) for value in periodic_axes]
|
|
51
|
+
if not any(periodic_flags):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
grouped: defaultdict[tuple[tuple[str, float | int], ...], list[int]] = defaultdict(list)
|
|
55
|
+
for index, point in enumerate(points):
|
|
56
|
+
key = _periodic_equivalence_key(
|
|
57
|
+
point,
|
|
58
|
+
bbox_min,
|
|
59
|
+
bbox_max,
|
|
60
|
+
periodic_flags,
|
|
61
|
+
tolerance,
|
|
62
|
+
)
|
|
63
|
+
if key is None:
|
|
64
|
+
continue
|
|
65
|
+
grouped[key].append(int(index))
|
|
66
|
+
|
|
67
|
+
return [sorted(group) for group in grouped.values() if len(group) >= 2]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _periodic_equivalence_key(
|
|
71
|
+
point: np.ndarray,
|
|
72
|
+
bbox_min: np.ndarray,
|
|
73
|
+
bbox_max: np.ndarray,
|
|
74
|
+
periodic_flags: list[bool],
|
|
75
|
+
tolerance: float,
|
|
76
|
+
) -> tuple[tuple[str, float | int], ...] | None:
|
|
77
|
+
"""Return a canonical key for all periodic copies of one boundary point."""
|
|
78
|
+
|
|
79
|
+
key: list[tuple[str, float | int]] = []
|
|
80
|
+
touches_periodic_boundary = False
|
|
81
|
+
for axis, value in enumerate(point):
|
|
82
|
+
coord = float(value)
|
|
83
|
+
if not periodic_flags[axis]:
|
|
84
|
+
key.append(("coord", coord))
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
on_min = np.isclose(coord, bbox_min[axis], atol=tolerance, rtol=0.0)
|
|
88
|
+
on_max = np.isclose(coord, bbox_max[axis], atol=tolerance, rtol=0.0)
|
|
89
|
+
if on_min or on_max:
|
|
90
|
+
key.append(("periodic", axis))
|
|
91
|
+
touches_periodic_boundary = True
|
|
92
|
+
else:
|
|
93
|
+
key.append(("coord", coord))
|
|
94
|
+
|
|
95
|
+
if not touches_periodic_boundary:
|
|
96
|
+
return None
|
|
97
|
+
return tuple(key)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Relaxation meshing package for the pure-Python section 4 port."""
|
|
2
|
+
|
|
3
|
+
from .density import _compile_density_function as _compile_density_function
|
|
4
|
+
from .engine import RelaxationEngine, mesh_bodies_raw
|
|
5
|
+
from .geometry import FemGeometry, fem_geometry_from_bodies
|
|
6
|
+
from .topology import assemble_raw_mesh
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FemGeometry",
|
|
10
|
+
"RelaxationEngine",
|
|
11
|
+
"assemble_raw_mesh",
|
|
12
|
+
"fem_geometry_from_bodies",
|
|
13
|
+
"mesh_bodies_raw",
|
|
14
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Internal constants for the relaxation meshing package."""
|
|
2
|
+
|
|
3
|
+
from ...utils.constants import BOUNDARY_FUZZ
|
|
4
|
+
|
|
5
|
+
DENSITY_EPSILON = 1.0e-12
|
|
6
|
+
DEFAULT_RNG_SEED = 97
|
|
7
|
+
STATE_FIXED = 0
|
|
8
|
+
STATE_MOBILE = 1
|
|
9
|
+
STATE_BOUNDARY = 2
|
|
10
|
+
STATE_SIMPLE = 3
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BOUNDARY_FUZZ",
|
|
14
|
+
"DEFAULT_RNG_SEED",
|
|
15
|
+
"DENSITY_EPSILON",
|
|
16
|
+
"STATE_BOUNDARY",
|
|
17
|
+
"STATE_FIXED",
|
|
18
|
+
"STATE_MOBILE",
|
|
19
|
+
"STATE_SIMPLE",
|
|
20
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Internal type aliases for the relaxation meshing package."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, TypeAlias
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ...utils.types import FloatArray
|
|
9
|
+
from ..driver import MeshEngineStatus
|
|
10
|
+
|
|
11
|
+
DensityFunction: TypeAlias = Callable[[FloatArray], float]
|
|
12
|
+
RegionFunction: TypeAlias = Callable[[FloatArray], np.ndarray]
|
|
13
|
+
EngineResult: TypeAlias = tuple[MeshEngineStatus, Any]
|
|
14
|
+
|
|
15
|
+
__all__ = ["DensityFunction", "EngineResult", "FloatArray", "RegionFunction"]
|