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,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
|