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,170 @@
1
+ """Density-function parsing and compilation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+
11
+ from ._constants import DENSITY_EPSILON
12
+ from ._types import DensityFunction, FloatArray
13
+
14
+
15
+ def _strip_c_comments(source: str) -> str:
16
+ """Remove C-style block comments from a legacy density script."""
17
+
18
+ return re.sub(r"/\*.*?\*/", "", source, flags=re.S)
19
+
20
+
21
+ def _translate_density_source(source: str) -> str:
22
+ """Translate a small subset of legacy C-like density syntax into Python."""
23
+
24
+ cleaned = _strip_c_comments(source)
25
+ cleaned = cleaned.replace("\r\n", "\n")
26
+ cleaned = cleaned.replace("{", "\n{\n").replace("}", "\n}\n").replace(";", ";\n")
27
+ lines = [line.strip() for line in cleaned.splitlines() if line.strip()]
28
+
29
+ py_lines = ["def __compiled_density(x):", " density = 1.0"]
30
+ indent = 1
31
+
32
+ for line in lines:
33
+ if line == "{":
34
+ indent += 1
35
+ continue
36
+ if line == "}":
37
+ indent = max(1, indent - 1)
38
+ continue
39
+
40
+ translated = line.rstrip(";").strip()
41
+ translated = re.sub(r"\bdouble\s+", "", translated)
42
+ translated = translated.replace("&&", " and ").replace("||", " or ")
43
+ translated = re.sub(r"(?<![<>=!])!(?!=)", " not ", translated)
44
+ translated = re.sub(r"\bexp\s*\(", "math.exp(", translated)
45
+ translated = re.sub(r"\bsqrt\s*\(", "math.sqrt(", translated)
46
+ translated = re.sub(r"\bsin\s*\(", "math.sin(", translated)
47
+ translated = re.sub(r"\bcos\s*\(", "math.cos(", translated)
48
+ translated = re.sub(r"\btan\s*\(", "math.tan(", translated)
49
+ translated = re.sub(r"\bpow\s*\(", "math.pow(", translated)
50
+
51
+ if translated.startswith("if "):
52
+ condition = translated[2:].strip()
53
+ if condition.startswith("(") and condition.endswith(")"):
54
+ condition = condition[1:-1].strip()
55
+ py_lines.append(f"{' ' * indent}if {condition}:")
56
+ continue
57
+
58
+ if translated == "else":
59
+ py_lines.append(f"{' ' * indent}else:")
60
+ continue
61
+
62
+ py_lines.append(f"{' ' * indent}{translated}")
63
+
64
+ py_lines.append(" return float(density)")
65
+ return "\n".join(py_lines)
66
+
67
+
68
+ def _clamp_density_value(value: float) -> float:
69
+ """Clamp a density sample to the smallest supported positive value."""
70
+
71
+ return max(float(value), DENSITY_EPSILON)
72
+
73
+
74
+ def _expression_density_scope(point: FloatArray) -> dict[str, object]:
75
+ """Build the safe eval scope for single-expression density snippets."""
76
+
77
+ x = np.asarray(point, dtype=float)
78
+ return {
79
+ "x": x,
80
+ "math": math,
81
+ "exp": math.exp,
82
+ "sqrt": math.sqrt,
83
+ "sin": math.sin,
84
+ "cos": math.cos,
85
+ "tan": math.tan,
86
+ "abs": abs,
87
+ "min": min,
88
+ "max": max,
89
+ }
90
+
91
+
92
+ def _build_callable_density(density: DensityFunction) -> DensityFunction:
93
+ """Wrap a user callable with array coercion and positive clamping."""
94
+
95
+ def density_wrapper(point: FloatArray) -> float:
96
+ """Evaluate a user density callable safely."""
97
+
98
+ return _clamp_density_value(float(density(np.asarray(point, dtype=float))))
99
+
100
+ return density_wrapper
101
+
102
+
103
+ def _compile_expression_density(source: str) -> DensityFunction | None:
104
+ """Compile a single-expression density assignment when available."""
105
+
106
+ expression = re.search(r"density\s*=\s*(.+?)\s*;", source, flags=re.S)
107
+ if expression is None or "if" in source or "{" in source:
108
+ return None
109
+
110
+ code = compile(expression.group(1), "<density>", "eval")
111
+
112
+ def expression_density(point: FloatArray) -> float:
113
+ """Evaluate a single-expression density snippet."""
114
+
115
+ value = float(eval(code, {"__builtins__": {}}, _expression_density_scope(point)))
116
+ return _clamp_density_value(value)
117
+
118
+ return expression_density
119
+
120
+
121
+ def _script_density_namespace() -> dict[str, object]:
122
+ """Return the restricted execution namespace for translated density scripts."""
123
+
124
+ return {
125
+ "__builtins__": {},
126
+ "math": math,
127
+ "float": float,
128
+ "abs": abs,
129
+ "min": min,
130
+ "max": max,
131
+ }
132
+
133
+
134
+ def _compile_script_density(source: str) -> DensityFunction:
135
+ """Compile a translated multi-line legacy density script."""
136
+
137
+ translated = _translate_density_source(source)
138
+ namespace = _script_density_namespace()
139
+ exec(translated, namespace, namespace)
140
+ compiled = namespace["__compiled_density"]
141
+ if not callable(compiled):
142
+ raise TypeError("Translated density script did not define a callable")
143
+
144
+ def script_density(point: FloatArray) -> float:
145
+ """Evaluate a translated multi-line density script."""
146
+
147
+ x = np.asarray(point, dtype=float)
148
+ result: Any = compiled(x)
149
+ return _clamp_density_value(float(result))
150
+
151
+ return script_density
152
+
153
+
154
+ def _compile_density_function(density: str | DensityFunction | None) -> DensityFunction:
155
+ """Compile a legacy density string or callable into a normalized density function."""
156
+
157
+ if density is None or density == "":
158
+ return lambda _point: 1.0
159
+
160
+ if callable(density):
161
+ return _build_callable_density(density)
162
+
163
+ source = str(density).strip()
164
+ try:
165
+ expression_density = _compile_expression_density(source)
166
+ if expression_density is not None:
167
+ return expression_density
168
+ return _compile_script_density(source)
169
+ except Exception as exc:
170
+ raise ValueError(f"Could not compile density function: {source!r}") from exc
@@ -0,0 +1,18 @@
1
+ """Public relaxation-engine façade composed from focused implementation phases."""
2
+
3
+ from .state import RelaxationEngineStateMixin
4
+ from .steps import (
5
+ RelaxationEngineStepMixin,
6
+ )
7
+ from .steps import (
8
+ mesh_bodies_raw as mesh_bodies_raw, # noqa: F401
9
+ )
10
+ from .topology import RelaxationEngineTopologyMixin
11
+
12
+
13
+ class RelaxationEngine(
14
+ RelaxationEngineStateMixin,
15
+ RelaxationEngineTopologyMixin,
16
+ RelaxationEngineStepMixin,
17
+ ):
18
+ """Iterative meshing engine that exposes the legacy driver protocol."""
@@ -0,0 +1,155 @@
1
+ """State initialization and configuration access for relaxation meshing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import math
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import numpy as np
10
+
11
+ from .._constants import BOUNDARY_FUZZ, DEFAULT_RNG_SEED, STATE_BOUNDARY, STATE_MOBILE
12
+ from .._types import FloatArray
13
+ from ..geometry import FemGeometry
14
+ from ..seeding import _prepare_initial_points
15
+ from ..topology import assemble_raw_mesh
16
+ from ..topology.finalize import snap_final_boundary_points
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class RelaxationEngineStateMixin:
22
+ if TYPE_CHECKING:
23
+
24
+ def __getattr__(self, name: str) -> Any: ...
25
+
26
+ def __init__(
27
+ self,
28
+ geometry: FemGeometry,
29
+ mesher: dict[str, Any],
30
+ a0: float,
31
+ fixed_points: FloatArray,
32
+ mobile_points: FloatArray,
33
+ simply_points: FloatArray,
34
+ periodic: list[float] | list[bool],
35
+ *,
36
+ rng: np.random.Generator | None = None,
37
+ ) -> None:
38
+ """Initialize the engine state from geometry, seeds, and mesher parameters."""
39
+
40
+ self.geometry = geometry
41
+ self.params = dict(mesher.get("parameters", {}))
42
+ self.a0 = float(a0)
43
+ self.periodic = list(periodic)
44
+ self.rng = rng or np.random.default_rng(DEFAULT_RNG_SEED)
45
+ self.points, self.states = _prepare_initial_points(
46
+ geometry,
47
+ self.a0,
48
+ fixed_points,
49
+ mobile_points,
50
+ simply_points,
51
+ self.periodic,
52
+ self.rng,
53
+ self.params,
54
+ )
55
+ self.step = 0
56
+ self.finished = False
57
+ self.last_addition_deletion_step = 0
58
+ self.last_max_displacement = math.inf
59
+ self.last_max_effective_force = math.inf
60
+ self.last_point_density = np.zeros(len(self.points), dtype=float)
61
+ self.max_rel_movement_since_last_triangulation = 0.0
62
+ self.current_simplices: np.ndarray | None = None
63
+ self.points_at_last_triangulation = np.array(self.points, copy=True)
64
+ self._refresh_boundary_states()
65
+ self.cached_raw_mesh = assemble_raw_mesh(
66
+ self.points,
67
+ self.geometry,
68
+ self.periodic,
69
+ states=self.states,
70
+ params=self.params,
71
+ )
72
+
73
+ @property
74
+ def max_steps(self) -> int:
75
+ """Return the configured maximum relaxation-step budget."""
76
+
77
+ return int(self.params.get("controller_step_limit_max", 1000))
78
+
79
+ @property
80
+ def tolerated_rel_move(self) -> float:
81
+ """Return the relative movement threshold used for convergence."""
82
+
83
+ return float(self.params.get("controller_tolerated_rel_movement", 0.002))
84
+
85
+ @property
86
+ def time_step_scale(self) -> float:
87
+ """Return the scale factor applied to each relaxation displacement."""
88
+
89
+ return float(self.params.get("controller_time_step_scale", 0.1))
90
+
91
+ @property
92
+ def max_time_step(self) -> float:
93
+ """Return the largest time-step magnitude allowed by the controller."""
94
+
95
+ return float(self.params.get("controller_max_time_step", 10.0))
96
+
97
+ @property
98
+ def boundary_drift_tolerance(self) -> float:
99
+ """Return the tolerated increase in boundary distance for boundary points.
100
+
101
+ Legacy boundary points are only allowed to move when they do not drift
102
+ farther from the boundary. The Python port keeps only the global
103
+ floating-point boundary fuzz here; it does not scale this tolerance with
104
+ ``a0`` because that would turn a numerical guard into different meshing
105
+ behavior.
106
+ """
107
+
108
+ return BOUNDARY_FUZZ
109
+
110
+ @property
111
+ def min_equilibrium_steps(self) -> int:
112
+ """Return the minimum number of steps before equilibrium can stop the loop."""
113
+
114
+ return int(self.params.get("controller_step_limit_min", 500))
115
+
116
+ @property
117
+ def post_change_settling_steps(self) -> int:
118
+ """Return the legacy settling window after point add/delete checks."""
119
+
120
+ return 50
121
+
122
+ def _rebuild_mesh(self, *, final: bool = False) -> None:
123
+ """Refresh the cached ``RawMesh`` snapshot from the current point cloud."""
124
+
125
+ points, states = (
126
+ self._final_output_points_and_states() if final else (self.points, self.states)
127
+ )
128
+ self.cached_raw_mesh = assemble_raw_mesh(
129
+ points,
130
+ self.geometry,
131
+ self.periodic,
132
+ states=states,
133
+ params=self.params,
134
+ )
135
+
136
+ def _final_output_points_and_states(self) -> tuple[FloatArray, np.ndarray]:
137
+ """Return the final point cloud after the legacy high-density cleanup."""
138
+
139
+ if len(self.last_point_density) != len(self.points):
140
+ return self.points, self.states
141
+ dynamic_mask = np.isin(self.states, [STATE_MOBILE, STATE_BOUNDARY])
142
+ keep = (~dynamic_mask) | (self.last_point_density < 100.0)
143
+ if np.all(keep):
144
+ points = self.points
145
+ states = self.states
146
+ else:
147
+ points = self.points[keep]
148
+ states = self.states[keep]
149
+ dynamic_mask = np.isin(states, [STATE_MOBILE, STATE_BOUNDARY])
150
+ domain_mask = self.geometry.classify_points(points) >= 0
151
+ keep_domain = (~dynamic_mask) | domain_mask
152
+ if not np.all(keep_domain):
153
+ points = points[keep_domain]
154
+ states = states[keep_domain]
155
+ return snap_final_boundary_points(points, states, self.geometry, self.a0, self.params)
@@ -0,0 +1,248 @@
1
+ """Relaxation displacement, convergence, and command execution phases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import math
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import numpy as np
10
+
11
+ from ....backend import RawMesh
12
+ from ....geometry.primitives import Body
13
+ from ...driver import MeshEngineCommand, MeshEngineStatus
14
+ from .._constants import BOUNDARY_FUZZ, DEFAULT_RNG_SEED, STATE_BOUNDARY, STATE_MOBILE
15
+ from .._types import DensityFunction, EngineResult, FloatArray
16
+ from ..forces import ForceSummary, compute_forces
17
+ from ..geometry import FemGeometry, fem_geometry_from_bodies
18
+ from ..seeding import _as_float_array
19
+ from ..topology import _callback_mesh_info
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ class RelaxationEngineStepMixin:
25
+ if TYPE_CHECKING:
26
+ geometry: FemGeometry
27
+ params: dict[str, Any]
28
+ a0: float
29
+ periodic: list[float] | list[bool]
30
+ points: FloatArray
31
+ states: np.ndarray[Any, Any]
32
+ step: int
33
+ finished: bool
34
+ last_addition_deletion_step: int
35
+ last_max_displacement: float
36
+ last_max_effective_force: float
37
+ last_point_density: FloatArray
38
+ max_rel_movement_since_last_triangulation: float
39
+ current_simplices: np.ndarray[Any, Any] | None
40
+ points_at_last_triangulation: FloatArray
41
+ cached_raw_mesh: RawMesh
42
+
43
+ def __getattr__(self, name: str) -> Any: ...
44
+
45
+ def _compute_constrained_displacement(
46
+ self,
47
+ point: FloatArray,
48
+ state: int,
49
+ displacement: FloatArray,
50
+ max_norm: float,
51
+ ) -> FloatArray:
52
+ """Return a clipped, boundary-aware candidate position for one point."""
53
+
54
+ bounded_displacement = np.array(displacement, copy=True)
55
+ if state == STATE_BOUNDARY:
56
+ normal = self.geometry.boundary_normal(point)
57
+ normal_norm = float(np.linalg.norm(normal))
58
+ if normal_norm > BOUNDARY_FUZZ:
59
+ bounded_displacement -= float(np.dot(bounded_displacement, normal)) * (
60
+ normal / normal_norm
61
+ )
62
+
63
+ disp_norm = float(np.linalg.norm(bounded_displacement))
64
+ if disp_norm > max_norm > 0.0:
65
+ bounded_displacement *= max_norm / disp_norm
66
+
67
+ candidate = point + bounded_displacement
68
+ if state == STATE_BOUNDARY:
69
+ old_boundary_distance = self.geometry.boundary_distance(point)
70
+ new_boundary_distance = self.geometry.boundary_distance(candidate)
71
+ if new_boundary_distance > old_boundary_distance + self.boundary_drift_tolerance:
72
+ return np.asarray(point, dtype=float)
73
+
74
+ if self.geometry.classify_points(candidate[np.newaxis, :])[0] < 0:
75
+ candidate = self.geometry.project_segment_to_domain(point, candidate)
76
+
77
+ if self.geometry.classify_points(candidate[np.newaxis, :])[0] >= 0:
78
+ return np.asarray(candidate, dtype=float)
79
+ return np.asarray(point, dtype=float)
80
+
81
+ def _apply_relaxation_displacements(
82
+ self,
83
+ force_summary: ForceSummary,
84
+ time_step: float,
85
+ ) -> float:
86
+ """Move dynamic points and return the largest density-scaled relative step."""
87
+
88
+ new_points = np.array(self.points, copy=True)
89
+ max_relative_displacement = 0.0
90
+ max_norm = math.inf
91
+ for index, state in enumerate(self.states):
92
+ if state not in (STATE_MOBILE, STATE_BOUNDARY):
93
+ continue
94
+
95
+ point = self.points[index]
96
+ displacement = np.array(force_summary.total_forces[index], copy=True) * time_step
97
+ candidate = self._compute_constrained_displacement(
98
+ point, int(state), displacement, max_norm
99
+ )
100
+ new_points[index] = candidate
101
+ density_scale = self.geometry.density_at(candidate) ** (1.0 / max(self.geometry.dim, 1))
102
+ relative_displacement = (
103
+ float(np.linalg.norm(candidate - point))
104
+ * density_scale
105
+ / max(self.a0, BOUNDARY_FUZZ)
106
+ )
107
+ max_relative_displacement = max(max_relative_displacement, relative_displacement)
108
+ self.points = new_points
109
+ return max_relative_displacement
110
+
111
+ def _check_convergence(self) -> None:
112
+ """Update the engine state when a convergence condition is satisfied."""
113
+
114
+ if (
115
+ self.step > self.min_equilibrium_steps
116
+ and self.last_max_displacement <= self.tolerated_rel_move
117
+ ):
118
+ log.debug(
119
+ "Relaxation finished by movement convergence at step %d (rel_move=%g, effective_force=%g)",
120
+ self.step,
121
+ self.last_max_displacement,
122
+ self.last_max_effective_force,
123
+ )
124
+ self.finished = True
125
+ return
126
+
127
+ if self.step >= 2 and self.last_max_effective_force <= BOUNDARY_FUZZ:
128
+ log.debug(
129
+ "Relaxation finished by effective-force equilibrium at step %d (effective_force=%g, rel_move=%g)",
130
+ self.step,
131
+ self.last_max_effective_force,
132
+ self.last_max_displacement,
133
+ )
134
+ self.finished = True
135
+
136
+ def _step_once(self) -> None:
137
+ """Execute one relaxation step over all currently mobile or boundary points."""
138
+
139
+ if len(self.points) < self.geometry.dim + 1:
140
+ self.finished = True
141
+ return
142
+
143
+ force_summary = compute_forces(
144
+ self.points,
145
+ self.states,
146
+ self.geometry,
147
+ self.a0,
148
+ self.params,
149
+ self.step,
150
+ simplices=self.current_simplices,
151
+ )
152
+ self._record_triangulation(force_summary)
153
+ self.last_point_density = np.asarray(force_summary.point_density, dtype=float)
154
+ time_step = self._effective_time_step(force_summary)
155
+ self.last_max_displacement = self._apply_relaxation_displacements(force_summary, time_step)
156
+ self.last_max_effective_force = force_summary.max_effective_force
157
+ self._refresh_boundary_states()
158
+ self._update_topology_movement()
159
+ self._refresh_topology_if_needed()
160
+
161
+ if self._should_attempt_point_change():
162
+ self.last_addition_deletion_step = self.step
163
+ self._attempt_add_delete_points(force_summary)
164
+
165
+ self._check_convergence()
166
+
167
+ def callback_payload(self) -> list[list[Any]]:
168
+ """Return the callback payload for the engine's current mesh snapshot."""
169
+
170
+ return _callback_mesh_info(self.cached_raw_mesh)
171
+
172
+ def run(self, command: MeshEngineCommand) -> EngineResult:
173
+ """Handle one driver command and return the next engine status tuple."""
174
+
175
+ if command == MeshEngineCommand.DO_EXTRACT:
176
+ self._rebuild_mesh()
177
+ return MeshEngineStatus.PRODUCED_INTERMEDIATE_MESH, (
178
+ self.callback_payload(),
179
+ self.run,
180
+ )
181
+
182
+ if command != MeshEngineCommand.DO_STEP:
183
+ raise ValueError(f"Unsupported mesh engine command: {command!r}")
184
+
185
+ if self.finished or self._step_limit_reached():
186
+ self._rebuild_mesh(final=True)
187
+ return MeshEngineStatus.FINISHED_STEP_LIMIT_REACHED, None
188
+
189
+ self.step += 1
190
+ self._step_once()
191
+
192
+ if self.finished:
193
+ self._rebuild_mesh(final=True)
194
+ return MeshEngineStatus.FINISHED_FORCE_EQUILIBRIUM_REACHED, None
195
+
196
+ if self._step_limit_reached():
197
+ self._rebuild_mesh(final=True)
198
+ return MeshEngineStatus.FINISHED_STEP_LIMIT_REACHED, None
199
+
200
+ return MeshEngineStatus.CAN_CONTINUE, self.run
201
+
202
+
203
+ def mesh_bodies_raw(
204
+ gendriver: Any,
205
+ mesher: dict[str, Any],
206
+ bb_min: list[float],
207
+ bb_max: list[float],
208
+ mesh_ext: int,
209
+ objects: list[Body],
210
+ a0: float,
211
+ density: str | DensityFunction | None,
212
+ fixed: list[list[float]],
213
+ mobile: list[list[float]],
214
+ simply: list[list[float]],
215
+ periodic: list[float] | list[bool],
216
+ hints: list[list[Any]],
217
+ ) -> RawMesh:
218
+ """Public backend entry point for meshing bodies with the Python engine."""
219
+
220
+ geometry = fem_geometry_from_bodies(
221
+ (np.asarray(bb_min, dtype=float), np.asarray(bb_max, dtype=float)),
222
+ list(objects),
223
+ list(hints),
224
+ density=density,
225
+ mesh_exterior=bool(mesh_ext),
226
+ )
227
+
228
+ # Import lazily so the public façade can compose the engine mixins first.
229
+ from . import RelaxationEngine
230
+
231
+ engine = RelaxationEngine(
232
+ geometry,
233
+ mesher,
234
+ float(a0),
235
+ _as_float_array(fixed, dim=geometry.dim),
236
+ _as_float_array(mobile, dim=geometry.dim),
237
+ _as_float_array(simply, dim=geometry.dim),
238
+ list(periodic),
239
+ rng=np.random.default_rng(DEFAULT_RNG_SEED),
240
+ )
241
+
242
+ if callable(gendriver):
243
+ gendriver(engine.run)
244
+ else:
245
+ engine.run(MeshEngineCommand.DO_STEP)
246
+
247
+ engine._rebuild_mesh(final=True)
248
+ return engine.cached_raw_mesh