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,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from si.physical import SI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulationFieldProbeMixin:
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
|
|
15
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
16
|
+
|
|
17
|
+
def probe_subfield_siv(
|
|
18
|
+
self,
|
|
19
|
+
subfieldname: str,
|
|
20
|
+
pos: Sequence[float],
|
|
21
|
+
unit: SI | None = None,
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""Probe a field at a position expressed in metres.
|
|
24
|
+
|
|
25
|
+
Demagnetization fields are linearly interpolated in the containing
|
|
26
|
+
tetrahedron. Magnetization uses the nearest mesh node, and a homogeneous
|
|
27
|
+
applied field is returned directly.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
subfieldname: ``H_demag``, ``H_ext``, or ``m``.
|
|
31
|
+
pos: Three physical coordinates in metres.
|
|
32
|
+
unit: Reserved compatibility argument. Returned values use the
|
|
33
|
+
field's standard SI representation.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Ordinary numeric components in SI, or ``None`` outside the mesh.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
KeyError: If the field name is not supported by this probe API.
|
|
40
|
+
"""
|
|
41
|
+
if subfieldname == "H_demag":
|
|
42
|
+
total_started = time.perf_counter()
|
|
43
|
+
try:
|
|
44
|
+
started = time.perf_counter()
|
|
45
|
+
within_bounds = self._probe_is_within_mesh_bounds(pos)
|
|
46
|
+
self._record_probe_timing(
|
|
47
|
+
"bounds_check",
|
|
48
|
+
time.perf_counter() - started,
|
|
49
|
+
)
|
|
50
|
+
if not within_bounds:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
started = time.perf_counter()
|
|
54
|
+
points = self._mesh_points()
|
|
55
|
+
probe = np.asarray(pos, dtype=float)
|
|
56
|
+
self._record_probe_timing(
|
|
57
|
+
"prepare_probe",
|
|
58
|
+
time.perf_counter() - started,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
started = time.perf_counter()
|
|
62
|
+
legacy_null_vertex = self._probe_is_legacy_null_vertex(probe, points)
|
|
63
|
+
self._record_probe_timing(
|
|
64
|
+
"legacy_null_vertex_check",
|
|
65
|
+
time.perf_counter() - started,
|
|
66
|
+
)
|
|
67
|
+
if legacy_null_vertex:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
started = time.perf_counter()
|
|
71
|
+
nodal_field = self._get_demag_nodal_field()
|
|
72
|
+
self._record_probe_timing(
|
|
73
|
+
"demag_nodal_field",
|
|
74
|
+
time.perf_counter() - started,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
started = time.perf_counter()
|
|
78
|
+
result = self._probe_tetrahedral_field(probe, nodal_field)
|
|
79
|
+
self._record_probe_timing(
|
|
80
|
+
"tetrahedral_interpolation",
|
|
81
|
+
time.perf_counter() - started,
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
finally:
|
|
85
|
+
self._record_probe_timing(
|
|
86
|
+
"total",
|
|
87
|
+
time.perf_counter() - total_started,
|
|
88
|
+
)
|
|
89
|
+
if subfieldname == "H_ext":
|
|
90
|
+
return self._fields.get("H_ext", np.zeros(3)).tolist()
|
|
91
|
+
if subfieldname != "m":
|
|
92
|
+
raise KeyError(f"Unknown or unset subfield '{subfieldname}'.")
|
|
93
|
+
if self.mesh is None or "m" not in self._fields:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
points = np.asarray(self.mesh.points, dtype=float)
|
|
97
|
+
if points.size == 0:
|
|
98
|
+
return None
|
|
99
|
+
probe = np.asarray(pos, dtype=float)
|
|
100
|
+
nearest_index = int(np.argmin(np.linalg.norm(points - probe, axis=1)))
|
|
101
|
+
return np.asarray(self._fields["m"][nearest_index], dtype=float).tolist()
|
|
102
|
+
|
|
103
|
+
def _record_probe_timing(self, name: str, value: float) -> None:
|
|
104
|
+
self.last_probe_timings_seconds[name] = (
|
|
105
|
+
self.last_probe_timings_seconds.get(name, 0.0) + value
|
|
106
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from ..backends import _load_rust_accelerator
|
|
12
|
+
from ..dynamics import IntegratorStats
|
|
13
|
+
from ..resources import available_memory_bytes as _available_memory_bytes
|
|
14
|
+
from .support import _si_unit, _simulation_compatibility_binding
|
|
15
|
+
|
|
16
|
+
FloatArray = NDArray[np.float64]
|
|
17
|
+
DIFFSOL_DENSE_MEMORY_LIMIT_ENV = "NMAG_DIFFSOL_DENSE_MEMORY_LIMIT_GIB"
|
|
18
|
+
DIFFSOL_DENSE_MATRIX_COPIES_ESTIMATE = 6
|
|
19
|
+
DIFFSOL_AVAILABLE_MEMORY_FRACTION = 0.8
|
|
20
|
+
DEFAULT_BDF_INITIAL_STEP_SECONDS = 1.0e-15
|
|
21
|
+
DEFAULT_BDF_MAXIMUM_STEPS = 1_000_000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _BdfResult(Protocol):
|
|
25
|
+
state: list[float]
|
|
26
|
+
reached_time: float
|
|
27
|
+
accepted_steps: int
|
|
28
|
+
rejected_steps: int
|
|
29
|
+
rhs_evaluations: int
|
|
30
|
+
jacobian_vector_evaluations: int
|
|
31
|
+
nonlinear_iterations: int
|
|
32
|
+
nonlinear_failures: int
|
|
33
|
+
linear_solver_setups: int
|
|
34
|
+
last_step: float
|
|
35
|
+
max_dm_dt: float
|
|
36
|
+
converged: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _estimated_dense_bdf_peak_bytes(state_size: int) -> int:
|
|
40
|
+
if state_size < 1:
|
|
41
|
+
raise ValueError("state_size must be positive.")
|
|
42
|
+
matrix_bytes = state_size * state_size * np.dtype(np.float64).itemsize
|
|
43
|
+
return DIFFSOL_DENSE_MATRIX_COPIES_ESTIMATE * matrix_bytes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _diffsol_dense_memory_budget() -> tuple[int | None, str]:
|
|
47
|
+
configured = os.environ.get(DIFFSOL_DENSE_MEMORY_LIMIT_ENV)
|
|
48
|
+
if configured is not None:
|
|
49
|
+
value = configured.strip().lower()
|
|
50
|
+
if value == "unlimited":
|
|
51
|
+
return None, "explicit unlimited override"
|
|
52
|
+
try:
|
|
53
|
+
gibibytes = float(value)
|
|
54
|
+
except ValueError as error:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"{DIFFSOL_DENSE_MEMORY_LIMIT_ENV} must be a positive number of GiB "
|
|
57
|
+
f"or 'unlimited', got {configured!r}."
|
|
58
|
+
) from error
|
|
59
|
+
if not math.isfinite(gibibytes) or gibibytes <= 0.0:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"{DIFFSOL_DENSE_MEMORY_LIMIT_ENV} must be a positive number of GiB "
|
|
62
|
+
f"or 'unlimited', got {configured!r}."
|
|
63
|
+
)
|
|
64
|
+
return int(gibibytes * 1024**3), "explicit environment budget"
|
|
65
|
+
|
|
66
|
+
available = _available_memory_bytes()
|
|
67
|
+
if available is None:
|
|
68
|
+
return None, "available memory could not be determined"
|
|
69
|
+
return int(available * DIFFSOL_AVAILABLE_MEMORY_FRACTION), "80% of available memory"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _validate_diffsol_dense_memory(state_size: int) -> None:
|
|
73
|
+
estimated_bytes = _estimated_dense_bdf_peak_bytes(state_size)
|
|
74
|
+
budget_bytes, budget_source = _diffsol_dense_memory_budget()
|
|
75
|
+
if budget_bytes is None or estimated_bytes <= budget_bytes:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
estimated_gib = estimated_bytes / 1024**3
|
|
79
|
+
budget_gib = budget_bytes / 1024**3
|
|
80
|
+
raise MemoryError(
|
|
81
|
+
f"The dense Diffsol solve for {state_size} state values is estimated to require "
|
|
82
|
+
f"approximately {estimated_gib:.2f} GiB, exceeding the {budget_gib:.2f} GiB "
|
|
83
|
+
f"budget ({budget_source}). Set {DIFFSOL_DENSE_MEMORY_LIMIT_ENV} to a reviewed "
|
|
84
|
+
"GiB budget or 'unlimited' to proceed. There is no fixed state-count limit."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_affine_field_operator(simulation: Any) -> FloatArray:
|
|
89
|
+
"""Build the dense linear map from nodal m to demag plus exchange field."""
|
|
90
|
+
mesh_token = simulation._mesh_geometry_token()
|
|
91
|
+
cached = simulation._llg_affine_operator_cache
|
|
92
|
+
if cached is not None and cached[0] == mesh_token:
|
|
93
|
+
return cached[1]
|
|
94
|
+
|
|
95
|
+
original_m = np.asarray(simulation._fields["m"], dtype=np.float64)
|
|
96
|
+
state_size = int(original_m.size)
|
|
97
|
+
_validate_diffsol_dense_memory(state_size)
|
|
98
|
+
|
|
99
|
+
def linear_field(state: FloatArray) -> FloatArray:
|
|
100
|
+
simulation._fields["m"] = np.reshape(state, (-1, 3))
|
|
101
|
+
simulation._invalidate_demag()
|
|
102
|
+
field = np.asarray(simulation._get_exchange_nodal_field(), dtype=np.float64)
|
|
103
|
+
if simulation.do_demag:
|
|
104
|
+
field = field + np.asarray(simulation._get_demag_nodal_field(), dtype=np.float64)
|
|
105
|
+
return np.ravel(field)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
zero = np.zeros(state_size, dtype=np.float64)
|
|
109
|
+
baseline = linear_field(zero)
|
|
110
|
+
operator = np.empty((state_size, state_size), dtype=np.float64)
|
|
111
|
+
basis = np.zeros(state_size, dtype=np.float64)
|
|
112
|
+
for column in range(state_size):
|
|
113
|
+
basis[column] = 1.0
|
|
114
|
+
operator[:, column] = linear_field(basis) - baseline
|
|
115
|
+
basis[column] = 0.0
|
|
116
|
+
finally:
|
|
117
|
+
simulation._fields["m"] = np.array(original_m, copy=True)
|
|
118
|
+
simulation._invalidate_demag()
|
|
119
|
+
|
|
120
|
+
operator = np.ascontiguousarray(operator)
|
|
121
|
+
simulation._llg_affine_operator_cache = (mesh_token, operator)
|
|
122
|
+
return operator
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def relax_with_diffsol(simulation: Any, H_applied: Any = None) -> None:
|
|
126
|
+
if simulation.mesh is None or "m" not in simulation._fields:
|
|
127
|
+
raise RuntimeError("A mesh and magnetisation are required before relaxation.")
|
|
128
|
+
if not simulation._anisotropy_is_zero_by_construction():
|
|
129
|
+
raise NotImplementedError(
|
|
130
|
+
"The experimental Diffsol backend does not support dynamic anisotropy."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
simulation.do_next_stage(stage=simulation.clock.stage)
|
|
134
|
+
simulation.clock.stage_end = False
|
|
135
|
+
if H_applied is not None:
|
|
136
|
+
simulation.set_H_ext(H_applied)
|
|
137
|
+
|
|
138
|
+
started = time.perf_counter()
|
|
139
|
+
try:
|
|
140
|
+
operator = build_affine_field_operator(simulation)
|
|
141
|
+
point_count = len(simulation._fields["m"])
|
|
142
|
+
h_ext = np.asarray(simulation._fields.get("H_ext", np.zeros(3)), dtype=np.float64)
|
|
143
|
+
constant_field = np.ascontiguousarray(np.tile(h_ext, point_count))
|
|
144
|
+
pin = np.ascontiguousarray(simulation._subfield_array("pin"), dtype=np.float64)
|
|
145
|
+
c1, c2, c3 = simulation._llg_coefficients()
|
|
146
|
+
config = simulation.integrator_config
|
|
147
|
+
rust_accel = _simulation_compatibility_binding(
|
|
148
|
+
"_load_rust_accelerator", _load_rust_accelerator
|
|
149
|
+
)("NmagConfig(integrator_backend='diffsol')")
|
|
150
|
+
try:
|
|
151
|
+
integrate_llg_bdf = rust_accel.integrate_llg_bdf
|
|
152
|
+
except AttributeError as exc:
|
|
153
|
+
raise RuntimeError(
|
|
154
|
+
"NmagConfig(integrator_backend='diffsol') requires an nmag_accel build with the "
|
|
155
|
+
"Diffsol backend. Rebuild it with `./scripts/build-accelerator.sh --release`."
|
|
156
|
+
) from exc
|
|
157
|
+
|
|
158
|
+
result: _BdfResult = integrate_llg_bdf(
|
|
159
|
+
np.ascontiguousarray(np.ravel(simulation._fields["m"]), dtype=np.float64),
|
|
160
|
+
operator,
|
|
161
|
+
constant_field,
|
|
162
|
+
pin,
|
|
163
|
+
c1,
|
|
164
|
+
c2,
|
|
165
|
+
c3,
|
|
166
|
+
0.0,
|
|
167
|
+
simulation.max_time_reached.in_units_of(_si_unit("s")),
|
|
168
|
+
config.relative_tolerance,
|
|
169
|
+
config.absolute_tolerance,
|
|
170
|
+
min(DEFAULT_BDF_INITIAL_STEP_SECONDS, config.maximum_step_seconds),
|
|
171
|
+
config.maximum_step_seconds,
|
|
172
|
+
simulation.stopping_dm_dt,
|
|
173
|
+
5,
|
|
174
|
+
simulation.convergence.required_checks,
|
|
175
|
+
DEFAULT_BDF_MAXIMUM_STEPS,
|
|
176
|
+
)
|
|
177
|
+
except Exception:
|
|
178
|
+
simulation._last_integrator_stats = IntegratorStats(
|
|
179
|
+
failed=True,
|
|
180
|
+
status="failed",
|
|
181
|
+
wall_seconds=time.perf_counter() - started,
|
|
182
|
+
)
|
|
183
|
+
raise
|
|
184
|
+
wall_seconds = time.perf_counter() - started
|
|
185
|
+
accepted_state = np.asarray(result.state, dtype=np.float64)
|
|
186
|
+
if not np.all(np.isfinite(accepted_state)):
|
|
187
|
+
raise FloatingPointError("Diffsol returned a non-finite magnetisation state.")
|
|
188
|
+
|
|
189
|
+
simulation._fields["m"] = np.reshape(accepted_state, (-1, 3))
|
|
190
|
+
simulation._invalidate_demag()
|
|
191
|
+
simulation._invalidate_integrator()
|
|
192
|
+
simulation.max_dm_dt = float(result.max_dm_dt)
|
|
193
|
+
simulation.clock.record_advance(
|
|
194
|
+
stage_time_seconds=float(result.reached_time),
|
|
195
|
+
accepted_steps=int(result.accepted_steps),
|
|
196
|
+
last_step_seconds=float(result.last_step),
|
|
197
|
+
wall_seconds=wall_seconds,
|
|
198
|
+
)
|
|
199
|
+
simulation.clock.convergence = bool(result.converged)
|
|
200
|
+
simulation.clock.stage_end = bool(result.converged)
|
|
201
|
+
simulation._last_integrator_stats = IntegratorStats(
|
|
202
|
+
accepted_steps=int(result.accepted_steps),
|
|
203
|
+
rhs_evaluations=int(result.rhs_evaluations),
|
|
204
|
+
rejected_steps=int(result.rejected_steps),
|
|
205
|
+
failed=False,
|
|
206
|
+
status="converged" if result.converged else "maximum_time_reached",
|
|
207
|
+
last_step_seconds=float(result.last_step),
|
|
208
|
+
simulated_seconds=float(result.reached_time),
|
|
209
|
+
wall_seconds=wall_seconds,
|
|
210
|
+
jacobian_vector_evaluations=int(result.jacobian_vector_evaluations),
|
|
211
|
+
nonlinear_iterations=int(result.nonlinear_iterations),
|
|
212
|
+
nonlinear_failures=int(result.nonlinear_failures),
|
|
213
|
+
linear_solver_setups=int(result.linear_solver_setups),
|
|
214
|
+
)
|
|
215
|
+
if not result.converged:
|
|
216
|
+
raise RuntimeError(
|
|
217
|
+
f"Diffsol reached {simulation.max_time_reached} without satisfying convergence."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
simulation.save_data()
|
|
221
|
+
simulation.save_data(fields="all")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Mesh-operation compatibility façade composed from focused mixins."""
|
|
2
|
+
|
|
3
|
+
from .geometry import SimulationMeshGeometryMixin
|
|
4
|
+
from .materials import SimulationMeshMaterialMixin
|
|
5
|
+
from .probe import SimulationMeshProbeMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulationMeshMixin(
|
|
9
|
+
SimulationMeshGeometryMixin,
|
|
10
|
+
SimulationMeshProbeMixin,
|
|
11
|
+
SimulationMeshMaterialMixin,
|
|
12
|
+
):
|
|
13
|
+
"""Provide mesh caches, probing, and material-aware averaging."""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..support import _MESH_EDGE_SET_THRESHOLD
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SimulationMeshGeometryMixin:
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
|
|
13
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
14
|
+
|
|
15
|
+
def _invalidate_demag(self, *, clear_geometry: bool = False) -> None:
|
|
16
|
+
self._demag_cache_token = None
|
|
17
|
+
self._demag_dipoles = None
|
|
18
|
+
self._demag_nodal_cache = None
|
|
19
|
+
self._demag_cell_cache_token = None
|
|
20
|
+
self._demag_cell_field_cache = None
|
|
21
|
+
self._demag_aux_cache_token = None
|
|
22
|
+
self._demag_phi_cache = None
|
|
23
|
+
self._demag_rho_cache = None
|
|
24
|
+
self._demag_volumes_cache = None
|
|
25
|
+
self.last_demag_solve_diagnostics = {}
|
|
26
|
+
self.last_demag_internal_vectors = {}
|
|
27
|
+
self._exchange_cache_token = None
|
|
28
|
+
self._exchange_nodal_cache = None
|
|
29
|
+
self._anisotropy_fields_cache = None
|
|
30
|
+
if clear_geometry:
|
|
31
|
+
self._demag_geometry_cache_token = None
|
|
32
|
+
self._demag_boundary_faces_cache = None
|
|
33
|
+
self._demag_bem_cache = None
|
|
34
|
+
self._last_bem_operator_stats = None
|
|
35
|
+
self._demag_fem_geometry_cache = None
|
|
36
|
+
self._demag_gauge_factorization_cache = None
|
|
37
|
+
self._demag_dirichlet_factorization_cache = None
|
|
38
|
+
self._demag_ms_values_cache = None
|
|
39
|
+
self._demag_volume_charge_scales_cache = None
|
|
40
|
+
self._nodal_ms_values_cache = None
|
|
41
|
+
self._mesh_points_cache = None
|
|
42
|
+
self._mesh_bounds_cache = None
|
|
43
|
+
self._mesh_edge_cache = None
|
|
44
|
+
self._simplex_volume_cache = None
|
|
45
|
+
self._volume_average_node_weights_cache = None
|
|
46
|
+
self._incident_cell_volume_sums_cache = None
|
|
47
|
+
self._exchange_spectral_bound_cache = None
|
|
48
|
+
self._nodal_material_coefficients_cache = None
|
|
49
|
+
self._nodal_anisotropy_groups_cache = None
|
|
50
|
+
self._anisotropy_fields_cache = None
|
|
51
|
+
self._llg_affine_operator_cache = None
|
|
52
|
+
self._probe_geometry_cache_token = None
|
|
53
|
+
self._probe_tetrahedral_cache = None
|
|
54
|
+
|
|
55
|
+
def _mesh_points(self) -> np.ndarray:
|
|
56
|
+
token = self._mesh_geometry_token()
|
|
57
|
+
if self._mesh_points_cache is not None:
|
|
58
|
+
cached_token, cached_points = self._mesh_points_cache
|
|
59
|
+
if cached_token == token:
|
|
60
|
+
return cached_points
|
|
61
|
+
|
|
62
|
+
points = np.asarray(self._require_mesh().points, dtype=float)
|
|
63
|
+
if points.ndim != 2 or points.shape[1] != 3:
|
|
64
|
+
raise NotImplementedError("The demag MVP currently supports only 3D meshes.")
|
|
65
|
+
self._mesh_points_cache = (token, points)
|
|
66
|
+
return points
|
|
67
|
+
|
|
68
|
+
def _mesh_bounds(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
69
|
+
token = self._mesh_geometry_token()
|
|
70
|
+
if self._mesh_bounds_cache is not None:
|
|
71
|
+
cached_token, lower, upper, span = self._mesh_bounds_cache
|
|
72
|
+
if cached_token == token:
|
|
73
|
+
return lower, upper, span
|
|
74
|
+
|
|
75
|
+
points = self._mesh_points()
|
|
76
|
+
lower = np.min(points, axis=0)
|
|
77
|
+
upper = np.max(points, axis=0)
|
|
78
|
+
span = np.maximum(upper - lower, np.ones(3))
|
|
79
|
+
self._mesh_bounds_cache = (token, lower, upper, span)
|
|
80
|
+
return lower, upper, span
|
|
81
|
+
|
|
82
|
+
def _mesh_edges(self) -> np.ndarray:
|
|
83
|
+
token = self._mesh_geometry_token()
|
|
84
|
+
if self._mesh_edge_cache is not None:
|
|
85
|
+
cached_token, cached_edges = self._mesh_edge_cache
|
|
86
|
+
if cached_token == token:
|
|
87
|
+
return cached_edges
|
|
88
|
+
|
|
89
|
+
mesh = self._require_mesh()
|
|
90
|
+
simplices = np.asarray(mesh.simplices, dtype=int)
|
|
91
|
+
if simplices.ndim != 2 or simplices.shape[1] < 2 or len(simplices) == 0:
|
|
92
|
+
edges = np.zeros((0, 2), dtype=int)
|
|
93
|
+
else:
|
|
94
|
+
local_pairs = np.asarray(
|
|
95
|
+
[
|
|
96
|
+
(left, right)
|
|
97
|
+
for left in range(simplices.shape[1])
|
|
98
|
+
for right in range(left + 1, simplices.shape[1])
|
|
99
|
+
],
|
|
100
|
+
dtype=int,
|
|
101
|
+
)
|
|
102
|
+
candidate_edge_count = len(simplices) * len(local_pairs)
|
|
103
|
+
if candidate_edge_count <= _MESH_EDGE_SET_THRESHOLD:
|
|
104
|
+
seen_edges: set[tuple[int, int]] = set()
|
|
105
|
+
for simplex in simplices:
|
|
106
|
+
for left_position, left in enumerate(simplex):
|
|
107
|
+
left_index = int(left)
|
|
108
|
+
for right in simplex[left_position + 1 :]:
|
|
109
|
+
right_index = int(right)
|
|
110
|
+
if left_index <= right_index:
|
|
111
|
+
seen_edges.add((left_index, right_index))
|
|
112
|
+
else:
|
|
113
|
+
seen_edges.add((right_index, left_index))
|
|
114
|
+
edges = np.asarray(sorted(seen_edges), dtype=int)
|
|
115
|
+
else:
|
|
116
|
+
expanded_edges = np.asarray(simplices[:, local_pairs], dtype=np.int64)
|
|
117
|
+
flat_edges = expanded_edges.flatten()
|
|
118
|
+
edges = np.column_stack((flat_edges[0::2], flat_edges[1::2]))
|
|
119
|
+
edges.sort(axis=1)
|
|
120
|
+
node_count = max(int(np.max(simplices)) + 1, len(mesh.points))
|
|
121
|
+
edge_codes = edges[:, 0].astype(np.int64, copy=False) * node_count + edges[
|
|
122
|
+
:, 1
|
|
123
|
+
].astype(np.int64, copy=False)
|
|
124
|
+
unique_codes = np.unique(edge_codes)
|
|
125
|
+
edges = np.empty((len(unique_codes), 2), dtype=int)
|
|
126
|
+
edges[:, 0] = unique_codes // node_count
|
|
127
|
+
edges[:, 1] = unique_codes % node_count
|
|
128
|
+
|
|
129
|
+
self._mesh_edge_cache = (token, edges)
|
|
130
|
+
return edges
|
|
131
|
+
|
|
132
|
+
def _mesh_geometry_token(self) -> int:
|
|
133
|
+
return id(self._require_mesh().raw_mesh)
|
|
134
|
+
|
|
135
|
+
def _require_mesh(self) -> Any:
|
|
136
|
+
if self.mesh is None:
|
|
137
|
+
raise RuntimeError("A mesh must be loaded before using simulation fields.")
|
|
138
|
+
return self.mesh
|
|
139
|
+
|
|
140
|
+
def _ensure_demag_geometry_cache_token(self, token: int) -> None:
|
|
141
|
+
if self._demag_geometry_cache_token == token:
|
|
142
|
+
return
|
|
143
|
+
self._demag_geometry_cache_token = token
|
|
144
|
+
self._demag_boundary_faces_cache = None
|
|
145
|
+
self._demag_bem_cache = None
|
|
146
|
+
self._last_bem_operator_stats = None
|
|
147
|
+
self._demag_fem_geometry_cache = None
|
|
148
|
+
self._demag_ms_values_cache = None
|
|
149
|
+
self._demag_volume_charge_scales_cache = None
|
|
150
|
+
self._nodal_ms_values_cache = None
|
|
151
|
+
self._nodal_material_coefficients_cache = None
|
|
152
|
+
self._nodal_anisotropy_groups_cache = None
|
|
153
|
+
self._anisotropy_fields_cache = None
|