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,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
@@ -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"]