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,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from si.physical import SI
|
|
12
|
+
|
|
13
|
+
from ...backends import _selected_llg_backend
|
|
14
|
+
from ...dynamics import (
|
|
15
|
+
DEFAULT_STOPPING_DM_DT,
|
|
16
|
+
ConvergenceTracker,
|
|
17
|
+
IntegratorConfig,
|
|
18
|
+
IntegratorStats,
|
|
19
|
+
)
|
|
20
|
+
from ..support import _si_unit, _simulation_compatibility_binding
|
|
21
|
+
from .advance import SimulationTimeAdvanceMixin
|
|
22
|
+
|
|
23
|
+
FloatArray = NDArray[np.float64]
|
|
24
|
+
Action = Callable[[Any], Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _DenseOutput(Protocol):
|
|
28
|
+
def __call__(self, time_seconds: float) -> FloatArray: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _OdeIntegrator(Protocol):
|
|
32
|
+
t: float
|
|
33
|
+
y: FloatArray
|
|
34
|
+
status: str
|
|
35
|
+
|
|
36
|
+
def step(self) -> str | None: ...
|
|
37
|
+
|
|
38
|
+
def dense_output(self) -> _DenseOutput: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _dop853_class() -> Callable[..., _OdeIntegrator]:
|
|
42
|
+
from scipy.integrate import DOP853
|
|
43
|
+
|
|
44
|
+
return cast(Callable[..., _OdeIntegrator], DOP853)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _dop853_constructor() -> Callable[..., _OdeIntegrator]:
|
|
48
|
+
"""Honor the historical module-level test and extension override."""
|
|
49
|
+
dynamics_module = sys.modules.get("nmag.simulation.dynamics")
|
|
50
|
+
return getattr(dynamics_module, "_dop853_class", _dop853_class)()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SimulationIntegratorSetupMixin:
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
_integrator: _OdeIntegrator | None
|
|
56
|
+
_integrator_is_stale: bool
|
|
57
|
+
_integrator_rhs_evaluations: int
|
|
58
|
+
_integrator_config: IntegratorConfig
|
|
59
|
+
_integrator_effective_max_step_seconds: float
|
|
60
|
+
_last_integrator_stats: IntegratorStats
|
|
61
|
+
_stage_wall_started: float
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
64
|
+
|
|
65
|
+
def _initialise_dynamics(self) -> None:
|
|
66
|
+
self._integrator = None
|
|
67
|
+
self._integrator_is_stale = True
|
|
68
|
+
self._integrator_rhs_evaluations = 0
|
|
69
|
+
self._integrator_config = IntegratorConfig()
|
|
70
|
+
self._integrator_effective_max_step_seconds = self._integrator_config.maximum_step_seconds
|
|
71
|
+
self._last_integrator_stats = IntegratorStats()
|
|
72
|
+
self._stage_wall_started = time.perf_counter()
|
|
73
|
+
self.stopping_dm_dt = DEFAULT_STOPPING_DM_DT
|
|
74
|
+
self.max_dm_dt: float | None = None
|
|
75
|
+
self.convergence = ConvergenceTracker()
|
|
76
|
+
self.max_time_reached = SI(1.0, "s")
|
|
77
|
+
self._restarting = False
|
|
78
|
+
self.action_abbreviations: dict[str, Action] = {
|
|
79
|
+
# Legacy relaxation emits separate average and field rows at stage end.
|
|
80
|
+
"save_averages": lambda sim: sim.save_data(),
|
|
81
|
+
"save_fields": lambda sim: sim.save_data(fields="all"),
|
|
82
|
+
"save_restart": lambda sim: sim.save_restart_file(),
|
|
83
|
+
"do_next_stage": self.hysteresis_next_stage,
|
|
84
|
+
"do_exit": self.hysteresis_exit,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def last_integrator_stats(self) -> IntegratorStats:
|
|
89
|
+
return self._last_integrator_stats
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def integrator_config(self) -> IntegratorConfig:
|
|
93
|
+
config = self._integrator_config
|
|
94
|
+
return IntegratorConfig(
|
|
95
|
+
relative_tolerance=config.relative_tolerance,
|
|
96
|
+
absolute_tolerance=config.absolute_tolerance,
|
|
97
|
+
maximum_step_seconds=config.maximum_step_seconds,
|
|
98
|
+
exact_tstop=config.exact_tstop,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def effective_integrator_max_step(self) -> SI:
|
|
103
|
+
return SI(self._integrator_effective_max_step_seconds, "s")
|
|
104
|
+
|
|
105
|
+
def _invalidate_integrator(self) -> None:
|
|
106
|
+
self._integrator_is_stale = True
|
|
107
|
+
|
|
108
|
+
def set_params(
|
|
109
|
+
self,
|
|
110
|
+
stopping_dm_dt: SI | float | None = None,
|
|
111
|
+
ts_rel_tol: float | None = None,
|
|
112
|
+
ts_abs_tol: float | None = None,
|
|
113
|
+
exact_tstop: bool | None = None,
|
|
114
|
+
ts_max_step: SI | float | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Update convergence and DOP853 integration controls.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
stopping_dm_dt: Positive convergence rate in 1/s.
|
|
120
|
+
ts_rel_tol: Positive relative error tolerance.
|
|
121
|
+
ts_abs_tol: Positive absolute error tolerance.
|
|
122
|
+
exact_tstop: Whether :meth:`advance_time` normally reconstructs
|
|
123
|
+
state exactly at its requested target.
|
|
124
|
+
ts_max_step: Positive configured maximum step in seconds. Exchange
|
|
125
|
+
stability can impose a smaller effective maximum.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If a supplied rate, tolerance, or step is not finite
|
|
129
|
+
and positive.
|
|
130
|
+
"""
|
|
131
|
+
if stopping_dm_dt is not None:
|
|
132
|
+
value = (
|
|
133
|
+
stopping_dm_dt.in_units_of(_si_unit("1/s"))
|
|
134
|
+
if isinstance(stopping_dm_dt, SI)
|
|
135
|
+
else float(stopping_dm_dt)
|
|
136
|
+
)
|
|
137
|
+
if not np.isfinite(value) or value <= 0.0:
|
|
138
|
+
raise ValueError("stopping_dm_dt must be a positive finite rate.")
|
|
139
|
+
self.stopping_dm_dt = value
|
|
140
|
+
if ts_rel_tol is not None:
|
|
141
|
+
self._integrator_config.relative_tolerance = float(ts_rel_tol)
|
|
142
|
+
if ts_abs_tol is not None:
|
|
143
|
+
self._integrator_config.absolute_tolerance = float(ts_abs_tol)
|
|
144
|
+
if exact_tstop is not None:
|
|
145
|
+
self._integrator_config.exact_tstop = bool(exact_tstop)
|
|
146
|
+
if ts_max_step is not None:
|
|
147
|
+
self._integrator_config.maximum_step_seconds = (
|
|
148
|
+
ts_max_step.in_units_of(_si_unit("s"))
|
|
149
|
+
if isinstance(ts_max_step, SI)
|
|
150
|
+
else float(ts_max_step)
|
|
151
|
+
)
|
|
152
|
+
self._integrator_config.validate()
|
|
153
|
+
self._invalidate_integrator()
|
|
154
|
+
|
|
155
|
+
def _llg_time_derivative(self, _time_seconds: float, state: FloatArray) -> FloatArray:
|
|
156
|
+
self._integrator_rhs_evaluations += 1
|
|
157
|
+
m: FloatArray = np.reshape(np.asarray(state, dtype=np.float64), (-1, 3))
|
|
158
|
+
if not np.all(np.isfinite(m)):
|
|
159
|
+
raise FloatingPointError("The integrator supplied a non-finite magnetisation state.")
|
|
160
|
+
|
|
161
|
+
self._fields["m"] = m
|
|
162
|
+
self._invalidate_demag()
|
|
163
|
+
h_total = self._subfield_array("H_total")
|
|
164
|
+
pin = self._subfield_array("pin")
|
|
165
|
+
unit_scale = np.ones(len(m), dtype=float)
|
|
166
|
+
coefficients = self._nodal_material_coefficients()
|
|
167
|
+
dm_dcurrent = self._get_dm_dcurrent() if "current_density" in self._fields else None
|
|
168
|
+
backend = _simulation_compatibility_binding("_selected_llg_backend", _selected_llg_backend)(
|
|
169
|
+
len(m), getattr(self, "config", None)
|
|
170
|
+
)
|
|
171
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
172
|
+
if backend == "rust":
|
|
173
|
+
derivative = self._llg_rhs_rust(
|
|
174
|
+
m,
|
|
175
|
+
h_total,
|
|
176
|
+
pin,
|
|
177
|
+
unit_scale,
|
|
178
|
+
coefficients.precession,
|
|
179
|
+
coefficients.damping,
|
|
180
|
+
coefficients.normalisation,
|
|
181
|
+
dm_dcurrent,
|
|
182
|
+
coefficients.stt_adiabatic,
|
|
183
|
+
coefficients.stt_nonadiabatic,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
derivative = self._llg_rhs_python(
|
|
187
|
+
m,
|
|
188
|
+
h_total,
|
|
189
|
+
pin,
|
|
190
|
+
unit_scale,
|
|
191
|
+
coefficients.precession,
|
|
192
|
+
coefficients.damping,
|
|
193
|
+
coefficients.normalisation,
|
|
194
|
+
dm_dcurrent,
|
|
195
|
+
coefficients.stt_adiabatic,
|
|
196
|
+
coefficients.stt_nonadiabatic,
|
|
197
|
+
)
|
|
198
|
+
if not np.all(np.isfinite(derivative)):
|
|
199
|
+
raise FloatingPointError("The LLG right-hand side produced non-finite values.")
|
|
200
|
+
return np.ravel(np.asarray(derivative, dtype=np.float64))
|
|
201
|
+
|
|
202
|
+
def reinitialise(
|
|
203
|
+
self,
|
|
204
|
+
rel_tolerance: float | None = None,
|
|
205
|
+
abs_tolerance: float | None = None,
|
|
206
|
+
initial_time: SI | float | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Rebuild the adaptive integrator from the current physical state.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
rel_tolerance: Optional new relative tolerance.
|
|
212
|
+
abs_tolerance: Optional new absolute tolerance.
|
|
213
|
+
initial_time: Non-negative physical start time. Omit it to retain
|
|
214
|
+
the simulation clock.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
RuntimeError: If mesh or magnetization has not been set.
|
|
218
|
+
ValueError: If the time or tolerances are invalid.
|
|
219
|
+
"""
|
|
220
|
+
if rel_tolerance is not None or abs_tolerance is not None:
|
|
221
|
+
self.set_params(ts_rel_tol=rel_tolerance, ts_abs_tol=abs_tolerance)
|
|
222
|
+
if self.mesh is None or "m" not in self._fields:
|
|
223
|
+
raise RuntimeError("A mesh and magnetisation are required before time integration.")
|
|
224
|
+
|
|
225
|
+
if initial_time is None:
|
|
226
|
+
initial_seconds = self.clock.time_reached_si.in_units_of(_si_unit("s"))
|
|
227
|
+
elif isinstance(initial_time, SI):
|
|
228
|
+
initial_seconds = initial_time.in_units_of(_si_unit("s"))
|
|
229
|
+
else:
|
|
230
|
+
initial_seconds = float(initial_time)
|
|
231
|
+
if not np.isfinite(initial_seconds) or initial_seconds < 0.0:
|
|
232
|
+
raise ValueError("initial_time must be a finite non-negative time.")
|
|
233
|
+
|
|
234
|
+
self._integrator_config.validate()
|
|
235
|
+
state: FloatArray = np.array(
|
|
236
|
+
np.ravel(np.asarray(self._fields["m"], dtype=np.float64)),
|
|
237
|
+
dtype=np.float64,
|
|
238
|
+
copy=True,
|
|
239
|
+
)
|
|
240
|
+
upper_bound = self.max_time_reached.in_units_of(_si_unit("s"))
|
|
241
|
+
if upper_bound <= initial_seconds:
|
|
242
|
+
upper_bound = float(np.nextafter(initial_seconds, np.inf))
|
|
243
|
+
self._integrator_effective_max_step_seconds = min(
|
|
244
|
+
self._integrator_config.maximum_step_seconds,
|
|
245
|
+
self._exchange_explicit_step_limit_seconds(),
|
|
246
|
+
)
|
|
247
|
+
self._integrator = _dop853_constructor()(
|
|
248
|
+
self._llg_time_derivative,
|
|
249
|
+
initial_seconds,
|
|
250
|
+
state,
|
|
251
|
+
upper_bound,
|
|
252
|
+
rtol=self._integrator_config.relative_tolerance,
|
|
253
|
+
atol=self._integrator_config.absolute_tolerance,
|
|
254
|
+
max_step=self._integrator_effective_max_step_seconds,
|
|
255
|
+
)
|
|
256
|
+
self.clock.time_reached_su = initial_seconds
|
|
257
|
+
self.clock.time_reached_si = SI(initial_seconds, "s")
|
|
258
|
+
self.clock.stage_time = SI(initial_seconds, "s")
|
|
259
|
+
self._integrator_is_stale = False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class SimulationIntegratorMixin(SimulationIntegratorSetupMixin, SimulationTimeAdvanceMixin):
|
|
263
|
+
"""Compose adaptive-integrator setup with its stepping lifecycle."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exchange and LLG compatibility façade composed from focused mixins."""
|
|
2
|
+
|
|
3
|
+
from .coefficients import SimulationExchangeCoefficientMixin
|
|
4
|
+
from .fields import SimulationExchangeFieldMixin
|
|
5
|
+
from .llg_rhs import SimulationLlgRhsMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulationExchangeMixin(
|
|
9
|
+
SimulationExchangeCoefficientMixin,
|
|
10
|
+
SimulationLlgRhsMixin,
|
|
11
|
+
SimulationExchangeFieldMixin,
|
|
12
|
+
):
|
|
13
|
+
"""Provide material coefficients, LLG kernels, and exchange/STT fields."""
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from si.physical import SI
|
|
8
|
+
|
|
9
|
+
from ...dynamics import DOP853_EXCHANGE_RESOLUTION_RADIUS, NodalMaterialCoefficients
|
|
10
|
+
from ..support import MU0, _si_dimensionless, _si_unit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SimulationExchangeCoefficientMixin:
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
_exchange_spectral_bound_cache: tuple[int, float] | None
|
|
16
|
+
_nodal_material_coefficients_cache: tuple[int, NodalMaterialCoefficients] | None
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _material_dynamic_coefficients(
|
|
22
|
+
material: Any,
|
|
23
|
+
) -> tuple[float, float, float, float, float, float]:
|
|
24
|
+
exchange = material.exchange_coupling.in_units_of(_si_unit("J/m"))
|
|
25
|
+
ms = material.Ms.in_units_of(_si_unit("A/m"))
|
|
26
|
+
exchange_prefactor = 0.0 if ms == 0.0 else 2.0 * exchange / (MU0 * ms)
|
|
27
|
+
gamma = material.llg_gamma_G.in_units_of(_si_unit("m/A/s"))
|
|
28
|
+
damping = (
|
|
29
|
+
material.llg_damping.in_units_of(_si_dimensionless())
|
|
30
|
+
if isinstance(material.llg_damping, SI)
|
|
31
|
+
else float(material.llg_damping)
|
|
32
|
+
)
|
|
33
|
+
gilbert_to_ll = 1.0 / (1.0 + damping * damping)
|
|
34
|
+
precession = -gamma * gilbert_to_ll
|
|
35
|
+
damping_coeff = precession * damping
|
|
36
|
+
if material.do_precession is False:
|
|
37
|
+
precession = 0.0
|
|
38
|
+
normalisation = material.llg_normalisationfactor.in_units_of(_si_unit("1/s"))
|
|
39
|
+
stt_adiabatic = material.su_llg_stt_adiab.in_units_of(_si_unit("m^3/A/s"))
|
|
40
|
+
stt_nonadiabatic = material.su_llg_stt_nadiab.in_units_of(_si_unit("m^3/A/s"))
|
|
41
|
+
return (
|
|
42
|
+
exchange_prefactor,
|
|
43
|
+
precession,
|
|
44
|
+
damping_coeff,
|
|
45
|
+
normalisation,
|
|
46
|
+
stt_adiabatic,
|
|
47
|
+
stt_nonadiabatic,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _nodal_material_coefficients(self) -> NodalMaterialCoefficients:
|
|
51
|
+
token = self._mesh_geometry_token()
|
|
52
|
+
cached = self._nodal_material_coefficients_cache
|
|
53
|
+
if cached is not None and cached[0] == token:
|
|
54
|
+
return cached[1]
|
|
55
|
+
|
|
56
|
+
point_count = len(self._mesh_points())
|
|
57
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
58
|
+
regions = np.asarray(self._require_mesh().regions or [1] * len(simplices), dtype=int)
|
|
59
|
+
if len(regions) != len(simplices):
|
|
60
|
+
raise ValueError("Mesh regions must contain one entry per simplex.")
|
|
61
|
+
|
|
62
|
+
region_coefficients = {
|
|
63
|
+
int(region): self._material_dynamic_coefficients(self._simplex_material(int(region)))
|
|
64
|
+
for region in np.unique(regions)
|
|
65
|
+
}
|
|
66
|
+
if not region_coefficients:
|
|
67
|
+
defaults = (
|
|
68
|
+
self._material_dynamic_coefficients(self.materials[0])
|
|
69
|
+
if self.materials
|
|
70
|
+
else (
|
|
71
|
+
0.0,
|
|
72
|
+
0.0,
|
|
73
|
+
0.0,
|
|
74
|
+
0.0,
|
|
75
|
+
0.0,
|
|
76
|
+
0.0,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
arrays = tuple(np.full(point_count, value, dtype=float) for value in defaults)
|
|
80
|
+
else:
|
|
81
|
+
nodal_regions = np.full(point_count, -1, dtype=int)
|
|
82
|
+
for region, coefficients in region_coefficients.items():
|
|
83
|
+
region_nodes = np.unique(simplices[regions == region].ravel())
|
|
84
|
+
assigned = nodal_regions[region_nodes]
|
|
85
|
+
for previous_region in np.unique(assigned[assigned >= 0]):
|
|
86
|
+
previous = region_coefficients[int(previous_region)]
|
|
87
|
+
if not np.allclose(previous, coefficients, rtol=1.0e-12, atol=0.0):
|
|
88
|
+
conflict_nodes = region_nodes[assigned == previous_region]
|
|
89
|
+
raise NotImplementedError(
|
|
90
|
+
"A mesh node shared by regions with different exchange or LLG "
|
|
91
|
+
f"coefficients requires material-specific magnetisation DOFs; node "
|
|
92
|
+
f"{int(conflict_nodes[0])} belongs to regions {int(previous_region)} "
|
|
93
|
+
f"and {region}."
|
|
94
|
+
)
|
|
95
|
+
nodal_regions[region_nodes[assigned < 0]] = region
|
|
96
|
+
|
|
97
|
+
first_region = next(iter(region_coefficients))
|
|
98
|
+
nodal_regions[nodal_regions < 0] = first_region
|
|
99
|
+
arrays = tuple(
|
|
100
|
+
np.asarray(
|
|
101
|
+
[region_coefficients[int(region)][component] for region in nodal_regions],
|
|
102
|
+
dtype=float,
|
|
103
|
+
)
|
|
104
|
+
for component in range(6)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
coefficients = NodalMaterialCoefficients(*arrays)
|
|
108
|
+
self._nodal_material_coefficients_cache = (token, coefficients)
|
|
109
|
+
return coefficients
|
|
110
|
+
|
|
111
|
+
def _exchange_prefactor(self) -> float:
|
|
112
|
+
if not self.materials:
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
prefactors = [
|
|
116
|
+
self._material_dynamic_coefficients(material)[0] for material in self.materials
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
first = prefactors[0]
|
|
120
|
+
if any(
|
|
121
|
+
abs(prefactor - first) > 1.0e-12 * max(1.0, abs(first)) for prefactor in prefactors[1:]
|
|
122
|
+
):
|
|
123
|
+
raise NotImplementedError("The exchange MVP currently supports one exchange prefactor.")
|
|
124
|
+
return float(first)
|
|
125
|
+
|
|
126
|
+
def _llg_coefficients(self) -> tuple[float, float, float]:
|
|
127
|
+
if not self.materials:
|
|
128
|
+
return 0.0, 0.0, 0.0
|
|
129
|
+
|
|
130
|
+
coefficients = [
|
|
131
|
+
self._material_dynamic_coefficients(material)[1:] for material in self.materials
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
first = coefficients[0]
|
|
135
|
+
for coeffs in coefficients[1:]:
|
|
136
|
+
if any(
|
|
137
|
+
abs(value - reference) > 1.0e-12 * max(1.0, abs(reference))
|
|
138
|
+
for value, reference in zip(coeffs, first, strict=True)
|
|
139
|
+
):
|
|
140
|
+
raise NotImplementedError(
|
|
141
|
+
"The dmdt MVP currently supports one LLG coefficient set."
|
|
142
|
+
)
|
|
143
|
+
return (float(first[0]), float(first[1]), float(first[2]))
|
|
144
|
+
|
|
145
|
+
def _exchange_laplacian_spectral_bound(self) -> float:
|
|
146
|
+
"""Return a cached upper bound for the lumped FEM Laplacian spectrum."""
|
|
147
|
+
token = self._mesh_geometry_token()
|
|
148
|
+
if self._exchange_spectral_bound_cache is not None:
|
|
149
|
+
cached_token, cached_bound = self._exchange_spectral_bound_cache
|
|
150
|
+
if cached_token == token:
|
|
151
|
+
return cached_bound
|
|
152
|
+
|
|
153
|
+
points = self._mesh_points()
|
|
154
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
155
|
+
if simplices.size == 0:
|
|
156
|
+
bound = 0.0
|
|
157
|
+
else:
|
|
158
|
+
stiffness, _gradients_by_cell, volumes = self._demag_fem_geometry_for_mesh(
|
|
159
|
+
points, simplices
|
|
160
|
+
)
|
|
161
|
+
lumped_volumes = self._incident_cell_volume_sums(points, simplices, volumes) / 4.0
|
|
162
|
+
present = lumped_volumes > 0.0
|
|
163
|
+
if not np.any(present):
|
|
164
|
+
bound = 0.0
|
|
165
|
+
else:
|
|
166
|
+
absolute_row_sums = np.asarray(
|
|
167
|
+
np.abs(stiffness[present]).sum(axis=1),
|
|
168
|
+
).ravel()
|
|
169
|
+
bound = float(np.max(absolute_row_sums / lumped_volumes[present]))
|
|
170
|
+
|
|
171
|
+
self._exchange_spectral_bound_cache = (token, bound)
|
|
172
|
+
return bound
|
|
173
|
+
|
|
174
|
+
def _exchange_explicit_step_limit_seconds(self) -> float:
|
|
175
|
+
"""Resolve stiff exchange modes accurately with the explicit integrator."""
|
|
176
|
+
spectral_bound = self._exchange_laplacian_spectral_bound()
|
|
177
|
+
coefficients = self._nodal_material_coefficients()
|
|
178
|
+
dynamic_coefficients = np.hypot(coefficients.precession, coefficients.damping)
|
|
179
|
+
maximum_rate = (
|
|
180
|
+
float(np.max(np.abs(coefficients.exchange_prefactor) * dynamic_coefficients))
|
|
181
|
+
* spectral_bound
|
|
182
|
+
)
|
|
183
|
+
if maximum_rate <= 0.0:
|
|
184
|
+
return float("inf")
|
|
185
|
+
return DOP853_EXCHANGE_RESOLUTION_RADIUS / maximum_rate
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulationExchangeFieldMixin:
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
|
|
11
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
12
|
+
|
|
13
|
+
def _get_dm_dcurrent(self) -> np.ndarray:
|
|
14
|
+
if "current_density" not in self._fields:
|
|
15
|
+
return np.zeros((len(self._mesh_points()), 3), dtype=float)
|
|
16
|
+
points = self._mesh_points()
|
|
17
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
18
|
+
if simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
19
|
+
raise NotImplementedError("Spin-transfer torque currently supports tetrahedral meshes.")
|
|
20
|
+
stiffness, gradients_by_cell, volumes = self._demag_fem_geometry_for_mesh(
|
|
21
|
+
points,
|
|
22
|
+
simplices,
|
|
23
|
+
)
|
|
24
|
+
del stiffness
|
|
25
|
+
m = np.asarray(self._fields["m"], dtype=float)
|
|
26
|
+
cell_gradients = np.einsum(
|
|
27
|
+
"cla,clj->caj",
|
|
28
|
+
m[simplices],
|
|
29
|
+
gradients_by_cell,
|
|
30
|
+
optimize=True,
|
|
31
|
+
)
|
|
32
|
+
nodal_gradients = np.zeros((len(points), 3, 3), dtype=float)
|
|
33
|
+
weighted_gradients = cell_gradients * volumes[:, np.newaxis, np.newaxis]
|
|
34
|
+
for local_index in range(4):
|
|
35
|
+
np.add.at(
|
|
36
|
+
nodal_gradients,
|
|
37
|
+
simplices[:, local_index],
|
|
38
|
+
weighted_gradients,
|
|
39
|
+
)
|
|
40
|
+
weights = self._incident_cell_volume_sums(points, simplices, volumes)
|
|
41
|
+
present = weights > 0.0
|
|
42
|
+
nodal_gradients[present] /= weights[present, np.newaxis, np.newaxis]
|
|
43
|
+
current_density = np.asarray(self._fields["current_density"], dtype=float)
|
|
44
|
+
if current_density.shape != (len(points), 3):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"current_density must contain one 3-vector per mesh point; "
|
|
47
|
+
f"got {current_density.shape}."
|
|
48
|
+
)
|
|
49
|
+
return np.einsum(
|
|
50
|
+
"nij,nj->ni",
|
|
51
|
+
nodal_gradients,
|
|
52
|
+
current_density,
|
|
53
|
+
optimize=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _get_exchange_nodal_field(self) -> np.ndarray:
|
|
57
|
+
token = self._demag_token()
|
|
58
|
+
if self._exchange_cache_token == token:
|
|
59
|
+
return self._exchange_nodal_cache
|
|
60
|
+
|
|
61
|
+
points = self._mesh_points()
|
|
62
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
63
|
+
exchange = np.zeros((len(points), 3), dtype=float)
|
|
64
|
+
if simplices.size == 0:
|
|
65
|
+
self._exchange_cache_token = token
|
|
66
|
+
self._exchange_nodal_cache = exchange
|
|
67
|
+
return exchange
|
|
68
|
+
if simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
69
|
+
raise NotImplementedError("The exchange MVP currently supports tetrahedral 3D meshes.")
|
|
70
|
+
if self._exchange_is_zero_by_construction():
|
|
71
|
+
self._exchange_cache_token = token
|
|
72
|
+
self._exchange_nodal_cache = exchange
|
|
73
|
+
return exchange
|
|
74
|
+
|
|
75
|
+
stiffness, _gradients_by_cell, volumes = self._demag_fem_geometry_for_mesh(
|
|
76
|
+
points, simplices
|
|
77
|
+
)
|
|
78
|
+
lumped_volumes = (
|
|
79
|
+
self._incident_cell_volume_sums(
|
|
80
|
+
points,
|
|
81
|
+
simplices,
|
|
82
|
+
volumes,
|
|
83
|
+
)
|
|
84
|
+
/ 4.0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
present = lumped_volumes > 0.0
|
|
88
|
+
if np.any(present):
|
|
89
|
+
cofield = stiffness @ np.asarray(self._fields["m"], dtype=float)
|
|
90
|
+
prefactors = self._nodal_material_coefficients().exchange_prefactor
|
|
91
|
+
exchange[present] = -(
|
|
92
|
+
prefactors[present, np.newaxis]
|
|
93
|
+
* cofield[present]
|
|
94
|
+
/ lumped_volumes[present, np.newaxis]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._exchange_cache_token = token
|
|
98
|
+
self._exchange_nodal_cache = exchange
|
|
99
|
+
return exchange
|
|
100
|
+
|
|
101
|
+
def _demag_token(self) -> tuple[int, int]:
|
|
102
|
+
if self.mesh is None:
|
|
103
|
+
raise RuntimeError("A mesh must be loaded before using demag fields.")
|
|
104
|
+
if "m" not in self._fields:
|
|
105
|
+
raise RuntimeError("Magnetisation must be set before using demag fields.")
|
|
106
|
+
return (id(self._require_mesh().raw_mesh), id(self._fields["m"]))
|