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