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,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ....backends import (
|
|
9
|
+
_load_rust_accelerator,
|
|
10
|
+
_selected_demag_cell_average_backend,
|
|
11
|
+
_selected_demag_nodal_recovery_backend,
|
|
12
|
+
)
|
|
13
|
+
from ...support import _simulation_compatibility_binding
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimulationDemagRecoveryMixin:
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
_demag_nodal_cache: np.ndarray | None
|
|
19
|
+
_demag_cell_field_cache: tuple[np.ndarray, np.ndarray] | None
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
22
|
+
|
|
23
|
+
def _get_demag_nodal_field(self) -> np.ndarray:
|
|
24
|
+
token = self._demag_token()
|
|
25
|
+
if self._demag_cache_token == token and self._demag_nodal_cache is not None:
|
|
26
|
+
return self._demag_nodal_cache
|
|
27
|
+
|
|
28
|
+
points = self._mesh_points()
|
|
29
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
30
|
+
if simplices.size == 0:
|
|
31
|
+
self._demag_cache_token = token
|
|
32
|
+
self._demag_nodal_cache = np.zeros((len(points), 3), dtype=float)
|
|
33
|
+
return self._demag_nodal_cache
|
|
34
|
+
if simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
35
|
+
raise NotImplementedError("The demag MVP currently supports tetrahedral 3D meshes.")
|
|
36
|
+
|
|
37
|
+
cell_h, volumes = self._get_demag_cell_field()
|
|
38
|
+
with self._record_active_subfield_array_timing_block(
|
|
39
|
+
"H_demag_detail:nodal_recovery",
|
|
40
|
+
):
|
|
41
|
+
weights = self._incident_cell_volume_sums(points, simplices, volumes)
|
|
42
|
+
backend = _selected_demag_nodal_recovery_backend(
|
|
43
|
+
len(simplices), getattr(self, "config", None)
|
|
44
|
+
)
|
|
45
|
+
with self._record_active_subfield_array_timing_block(
|
|
46
|
+
f"H_demag_detail:nodal_recovery:{backend}",
|
|
47
|
+
):
|
|
48
|
+
if backend == "rust":
|
|
49
|
+
nodal_h = self._recover_demag_nodal_field_rust(
|
|
50
|
+
simplices,
|
|
51
|
+
volumes,
|
|
52
|
+
cell_h,
|
|
53
|
+
weights,
|
|
54
|
+
len(points),
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
nodal_h = self._recover_demag_nodal_field_python(
|
|
58
|
+
simplices,
|
|
59
|
+
volumes,
|
|
60
|
+
cell_h,
|
|
61
|
+
weights,
|
|
62
|
+
len(points),
|
|
63
|
+
)
|
|
64
|
+
self._demag_cache_token = token
|
|
65
|
+
self._demag_nodal_cache = nodal_h
|
|
66
|
+
return nodal_h
|
|
67
|
+
|
|
68
|
+
def _get_demag_cell_field(self) -> tuple[np.ndarray, np.ndarray]:
|
|
69
|
+
cache_started = time.perf_counter()
|
|
70
|
+
token = self._demag_token()
|
|
71
|
+
if self._demag_cell_cache_token == token and self._demag_cell_field_cache is not None:
|
|
72
|
+
self._record_active_subfield_array_timing(
|
|
73
|
+
"H_demag_detail:cell_field_cache_hit",
|
|
74
|
+
time.perf_counter() - cache_started,
|
|
75
|
+
)
|
|
76
|
+
return self._demag_cell_field_cache
|
|
77
|
+
|
|
78
|
+
points = self._mesh_points()
|
|
79
|
+
simplices = np.asarray(self._require_mesh().simplices, dtype=int)
|
|
80
|
+
if simplices.size == 0:
|
|
81
|
+
cell_h = np.zeros((0, 3), dtype=float)
|
|
82
|
+
volumes = np.zeros(0, dtype=float)
|
|
83
|
+
self._demag_cell_cache_token = token
|
|
84
|
+
self._demag_cell_field_cache = (cell_h, volumes)
|
|
85
|
+
return self._demag_cell_field_cache
|
|
86
|
+
if simplices.ndim != 2 or simplices.shape[1] != 4:
|
|
87
|
+
raise NotImplementedError("The demag MVP currently supports tetrahedral 3D meshes.")
|
|
88
|
+
|
|
89
|
+
with self._record_active_subfield_array_timing_block(
|
|
90
|
+
"H_demag_detail:auxiliary_fields",
|
|
91
|
+
):
|
|
92
|
+
phi, _rho, volumes = self._get_demag_auxiliary_fields()
|
|
93
|
+
with self._record_active_subfield_array_timing_block(
|
|
94
|
+
"H_demag_detail:cell_gradient",
|
|
95
|
+
):
|
|
96
|
+
cell_h = self._cell_demag_from_potential(points, simplices, phi)
|
|
97
|
+
self._demag_cell_cache_token = token
|
|
98
|
+
self._demag_cell_field_cache = (cell_h, volumes)
|
|
99
|
+
return self._demag_cell_field_cache
|
|
100
|
+
|
|
101
|
+
def _demag_cell_field_average(self) -> np.ndarray:
|
|
102
|
+
cell_h, volumes = self._get_demag_cell_field()
|
|
103
|
+
backend = _selected_demag_cell_average_backend(
|
|
104
|
+
len(volumes), getattr(self, "config", None)
|
|
105
|
+
)
|
|
106
|
+
with self._record_active_subfield_array_timing_block(
|
|
107
|
+
"H_demag_detail:cell_average",
|
|
108
|
+
):
|
|
109
|
+
with self._record_active_subfield_array_timing_block(
|
|
110
|
+
f"H_demag_detail:cell_average:{backend}",
|
|
111
|
+
):
|
|
112
|
+
if backend == "rust":
|
|
113
|
+
return self._demag_cell_field_average_rust(cell_h, volumes)
|
|
114
|
+
return self._demag_cell_field_average_python(cell_h, volumes)
|
|
115
|
+
|
|
116
|
+
def _demag_cell_field_average_python(
|
|
117
|
+
self,
|
|
118
|
+
cell_h: np.ndarray,
|
|
119
|
+
volumes: np.ndarray,
|
|
120
|
+
) -> np.ndarray:
|
|
121
|
+
if cell_h.size == 0 or volumes.size == 0:
|
|
122
|
+
return np.zeros(3, dtype=float)
|
|
123
|
+
positive = volumes > 0.0
|
|
124
|
+
if not np.any(positive):
|
|
125
|
+
return np.zeros(3, dtype=float)
|
|
126
|
+
|
|
127
|
+
if np.all(positive):
|
|
128
|
+
total_volume = float(np.sum(volumes))
|
|
129
|
+
if total_volume <= 0.0:
|
|
130
|
+
return np.zeros(3, dtype=float)
|
|
131
|
+
return np.dot(volumes, cell_h) / total_volume
|
|
132
|
+
|
|
133
|
+
total_volume = float(np.sum(volumes[positive]))
|
|
134
|
+
if total_volume <= 0.0:
|
|
135
|
+
return np.zeros(3, dtype=float)
|
|
136
|
+
return np.sum(cell_h[positive] * volumes[positive, np.newaxis], axis=0) / total_volume
|
|
137
|
+
|
|
138
|
+
def _demag_cell_field_average_rust(
|
|
139
|
+
self,
|
|
140
|
+
cell_h: np.ndarray,
|
|
141
|
+
volumes: np.ndarray,
|
|
142
|
+
) -> np.ndarray:
|
|
143
|
+
rust_accel = _simulation_compatibility_binding(
|
|
144
|
+
"_load_rust_accelerator",
|
|
145
|
+
_load_rust_accelerator,
|
|
146
|
+
)("NmagConfig.accelerator['cell_average']")
|
|
147
|
+
try:
|
|
148
|
+
demag_cell_field_average = rust_accel.demag_cell_field_average
|
|
149
|
+
except AttributeError as exc:
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
"NmagConfig.accelerator['cell_average']='rust' requires a nmag_accel build "
|
|
152
|
+
"with demag_cell_field_average support."
|
|
153
|
+
) from exc
|
|
154
|
+
return np.asarray(
|
|
155
|
+
demag_cell_field_average(
|
|
156
|
+
np.asarray(cell_h, dtype=np.float64),
|
|
157
|
+
np.asarray(volumes, dtype=np.float64),
|
|
158
|
+
),
|
|
159
|
+
dtype=float,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _recover_demag_nodal_field_python(
|
|
163
|
+
self,
|
|
164
|
+
simplices: np.ndarray,
|
|
165
|
+
volumes: np.ndarray,
|
|
166
|
+
cell_h: np.ndarray,
|
|
167
|
+
weights: np.ndarray,
|
|
168
|
+
point_count: int,
|
|
169
|
+
) -> np.ndarray:
|
|
170
|
+
nodal_h = np.zeros((point_count, 3), dtype=float)
|
|
171
|
+
positive = volumes > 0.0
|
|
172
|
+
if np.any(positive):
|
|
173
|
+
positive_simplices = simplices[positive]
|
|
174
|
+
weighted_cell_h = cell_h[positive] * volumes[positive, np.newaxis]
|
|
175
|
+
for local_index in range(4):
|
|
176
|
+
np.add.at(
|
|
177
|
+
nodal_h,
|
|
178
|
+
positive_simplices[:, local_index],
|
|
179
|
+
weighted_cell_h,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
present = weights > 0.0
|
|
183
|
+
nodal_h[present] /= weights[present, np.newaxis]
|
|
184
|
+
return nodal_h
|
|
185
|
+
|
|
186
|
+
def _recover_demag_nodal_field_rust(
|
|
187
|
+
self,
|
|
188
|
+
simplices: np.ndarray,
|
|
189
|
+
volumes: np.ndarray,
|
|
190
|
+
cell_h: np.ndarray,
|
|
191
|
+
weights: np.ndarray,
|
|
192
|
+
point_count: int,
|
|
193
|
+
) -> np.ndarray:
|
|
194
|
+
rust_accel = _simulation_compatibility_binding(
|
|
195
|
+
"_load_rust_accelerator",
|
|
196
|
+
_load_rust_accelerator,
|
|
197
|
+
)("NmagConfig.accelerator['nodal_recovery']")
|
|
198
|
+
return np.asarray(
|
|
199
|
+
rust_accel.recover_demag_nodal_field(
|
|
200
|
+
np.asarray(simplices, dtype=np.int64),
|
|
201
|
+
np.asarray(volumes, dtype=np.float64),
|
|
202
|
+
np.asarray(cell_h, dtype=np.float64),
|
|
203
|
+
np.asarray(weights, dtype=np.float64),
|
|
204
|
+
int(point_count),
|
|
205
|
+
),
|
|
206
|
+
dtype=float,
|
|
207
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..exchange import SimulationExchangeMixin
|
|
4
|
+
from .bem import SimulationDemagBemMixin
|
|
5
|
+
from .fem import SimulationDemagFemMixin
|
|
6
|
+
from .fields import SimulationDemagFieldsMixin
|
|
7
|
+
from .fields.probe import SimulationDemagProbeMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SimulationDemagMixin(
|
|
11
|
+
SimulationExchangeMixin,
|
|
12
|
+
SimulationDemagFieldsMixin,
|
|
13
|
+
SimulationDemagFemMixin,
|
|
14
|
+
SimulationDemagBemMixin,
|
|
15
|
+
SimulationDemagProbeMixin,
|
|
16
|
+
):
|
|
17
|
+
"""Compose the exchange, demag field, FEM, BEM, and probe implementations."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
6
|
+
from simulation import hysteresis as hysteresis_module
|
|
7
|
+
|
|
8
|
+
from ...backends import _selected_integrator_backend
|
|
9
|
+
from ..implicit_dynamics import relax_with_diffsol
|
|
10
|
+
from ..support import _simulation_compatibility_binding
|
|
11
|
+
from .integrator import (
|
|
12
|
+
SimulationIntegratorMixin,
|
|
13
|
+
)
|
|
14
|
+
from .integrator import (
|
|
15
|
+
_dop853_class as _dop853_class, # noqa: F401
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimulationDynamicsMixin(SimulationIntegratorMixin):
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
|
|
22
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
23
|
+
|
|
24
|
+
def do_next_stage(self, stage: int | None = None) -> None:
|
|
25
|
+
self.max_dm_dt = None
|
|
26
|
+
self.convergence.reset()
|
|
27
|
+
self.clock.inc_stage(stage=stage)
|
|
28
|
+
self._stage_wall_started = time.perf_counter()
|
|
29
|
+
self._invalidate_integrator()
|
|
30
|
+
|
|
31
|
+
def is_converged(self) -> bool:
|
|
32
|
+
if self.max_dm_dt is None:
|
|
33
|
+
self.clock.convergence = False
|
|
34
|
+
return False
|
|
35
|
+
converged = self.convergence.check(self.step, self.max_dm_dt, self.stopping_dm_dt)
|
|
36
|
+
self.clock.convergence = converged
|
|
37
|
+
return converged
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def hysteresis_next_stage(sim: Any) -> None:
|
|
41
|
+
sim.clock.stage_end = True
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def hysteresis_exit(sim: Any) -> None:
|
|
45
|
+
sim.clock.exit_hysteresis = True
|
|
46
|
+
sim.clock.stage_end = True
|
|
47
|
+
|
|
48
|
+
simulation_relax = hysteresis_module.simulation_relax
|
|
49
|
+
|
|
50
|
+
def relax(
|
|
51
|
+
self,
|
|
52
|
+
H_applied: Any = None,
|
|
53
|
+
save: list[tuple[object, ...]] | None = None,
|
|
54
|
+
do: list[tuple[object, ...]] | None = None,
|
|
55
|
+
convergence_check: Any = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Relax magnetization until the convergence schedule completes.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
H_applied: Optional applied-field value for compatibility with the
|
|
61
|
+
staged relaxation runner.
|
|
62
|
+
save: Scheduled save tuples such as
|
|
63
|
+
``[("averages", every("step", 10))]``. Omit for averages and
|
|
64
|
+
fields at stage end.
|
|
65
|
+
do: Scheduled action tuples. Omit for the default stage lifecycle.
|
|
66
|
+
convergence_check: Custom :class:`when.When` condition. Omit for
|
|
67
|
+
the accepted-step convergence cadence.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
NotImplementedError: If a custom schedule is requested with the
|
|
71
|
+
experimental Diffsol backend.
|
|
72
|
+
"""
|
|
73
|
+
backend = _simulation_compatibility_binding(
|
|
74
|
+
"_selected_integrator_backend", _selected_integrator_backend
|
|
75
|
+
)(getattr(self, "config", None))
|
|
76
|
+
if backend == "scipy":
|
|
77
|
+
hysteresis_module.simulation_relax(
|
|
78
|
+
cast(Any, self),
|
|
79
|
+
H_applied=H_applied,
|
|
80
|
+
save=save,
|
|
81
|
+
do=do,
|
|
82
|
+
convergence_check=convergence_check,
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
if save is not None or do not in (None, []) or convergence_check is not None:
|
|
86
|
+
raise NotImplementedError(
|
|
87
|
+
"The experimental Diffsol backend currently supports only the default "
|
|
88
|
+
"relaxation save and convergence schedule."
|
|
89
|
+
)
|
|
90
|
+
relax_with_diffsol(self, H_applied=H_applied)
|
|
91
|
+
|
|
92
|
+
simulation_hysteresis = hysteresis_module.simulation_hysteresis
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from si.physical import SI
|
|
10
|
+
|
|
11
|
+
from ...dynamics import IntegratorStats
|
|
12
|
+
from ..support import _si_unit
|
|
13
|
+
|
|
14
|
+
FloatArray = NDArray[np.float64]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimulationTimeAdvanceMixin:
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
_integrator: Any
|
|
20
|
+
_integrator_is_stale: bool
|
|
21
|
+
_integrator_rhs_evaluations: int
|
|
22
|
+
_last_integrator_stats: IntegratorStats
|
|
23
|
+
|
|
24
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
25
|
+
|
|
26
|
+
def advance_time(
|
|
27
|
+
self,
|
|
28
|
+
target_time: SI,
|
|
29
|
+
max_it: int = -1,
|
|
30
|
+
exact_tstop: bool | None = None,
|
|
31
|
+
) -> SI:
|
|
32
|
+
"""Advance adaptive LLG integration toward a physical target time.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
target_time: Absolute stage time to reach.
|
|
36
|
+
max_it: Maximum accepted steps, or ``-1`` for no explicit cap.
|
|
37
|
+
exact_tstop: Override whether a step crossing the target is
|
|
38
|
+
reconstructed at exactly the requested time.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The physical time reached. It can precede ``target_time`` when
|
|
42
|
+
``max_it`` limits the operation.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If the target precedes the current time or ``max_it``
|
|
46
|
+
is invalid.
|
|
47
|
+
RuntimeError: If the numerical integrator fails.
|
|
48
|
+
"""
|
|
49
|
+
target_seconds, current_seconds = self._advance_request(target_time, max_it)
|
|
50
|
+
if target_seconds == current_seconds or max_it == 0:
|
|
51
|
+
return SI(current_seconds, "s")
|
|
52
|
+
integrator = self._active_integrator(current_seconds)
|
|
53
|
+
state_before: FloatArray = np.array(integrator.y, dtype=np.float64, copy=True)
|
|
54
|
+
time_before = float(integrator.t)
|
|
55
|
+
rhs_before = self._integrator_rhs_evaluations
|
|
56
|
+
accepted_steps, last_step_seconds = 0, 0.0
|
|
57
|
+
failed = False
|
|
58
|
+
status = "running"
|
|
59
|
+
started = time.perf_counter()
|
|
60
|
+
try:
|
|
61
|
+
integrator, accepted_steps, last_step_seconds = self._advance_integrator(
|
|
62
|
+
integrator,
|
|
63
|
+
target_seconds,
|
|
64
|
+
max_it,
|
|
65
|
+
self._integrator_config.exact_tstop if exact_tstop is None else exact_tstop,
|
|
66
|
+
time_before,
|
|
67
|
+
)
|
|
68
|
+
reached_seconds = self._accept_integrated_state(
|
|
69
|
+
integrator,
|
|
70
|
+
state_before,
|
|
71
|
+
time_before,
|
|
72
|
+
accepted_steps,
|
|
73
|
+
last_step_seconds,
|
|
74
|
+
started,
|
|
75
|
+
)
|
|
76
|
+
status = str(integrator.status)
|
|
77
|
+
return SI(reached_seconds, "s")
|
|
78
|
+
except Exception:
|
|
79
|
+
failed = True
|
|
80
|
+
status = "failed"
|
|
81
|
+
raise
|
|
82
|
+
finally:
|
|
83
|
+
self._last_integrator_stats = IntegratorStats(
|
|
84
|
+
accepted_steps=accepted_steps,
|
|
85
|
+
rhs_evaluations=self._integrator_rhs_evaluations - rhs_before,
|
|
86
|
+
rejected_steps=None,
|
|
87
|
+
failed=failed,
|
|
88
|
+
status=status,
|
|
89
|
+
last_step_seconds=last_step_seconds,
|
|
90
|
+
simulated_seconds=max(
|
|
91
|
+
0.0, float(getattr(integrator, "t", time_before)) - time_before
|
|
92
|
+
),
|
|
93
|
+
wall_seconds=time.perf_counter() - started,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _advance_request(self, target_time: SI, max_it: int) -> tuple[float, float]:
|
|
97
|
+
target_seconds = target_time.in_units_of(_si_unit("s"))
|
|
98
|
+
current_seconds = self.clock.time_reached_si.in_units_of(_si_unit("s"))
|
|
99
|
+
if target_seconds < current_seconds:
|
|
100
|
+
raise ValueError("target_time cannot be earlier than the current stage time.")
|
|
101
|
+
if max_it < -1:
|
|
102
|
+
raise ValueError("max_it must be -1 or a non-negative integer.")
|
|
103
|
+
return target_seconds, current_seconds
|
|
104
|
+
|
|
105
|
+
def _active_integrator(self, current_seconds: float) -> Any:
|
|
106
|
+
if self._integrator is None or self._integrator_is_stale:
|
|
107
|
+
self.reinitialise(initial_time=current_seconds)
|
|
108
|
+
if self._integrator is None:
|
|
109
|
+
raise RuntimeError("The time integrator was not initialised.")
|
|
110
|
+
return self._integrator
|
|
111
|
+
|
|
112
|
+
def _advance_integrator(
|
|
113
|
+
self,
|
|
114
|
+
integrator: Any,
|
|
115
|
+
target_seconds: float,
|
|
116
|
+
max_it: int,
|
|
117
|
+
exact: bool,
|
|
118
|
+
time_before: float,
|
|
119
|
+
) -> tuple[Any, int, float]:
|
|
120
|
+
accepted_steps, last_step_seconds = 0, 0.0
|
|
121
|
+
while self._can_continue_advancing(integrator, target_seconds, max_it, accepted_steps):
|
|
122
|
+
previous_time = float(integrator.t)
|
|
123
|
+
message = integrator.step()
|
|
124
|
+
if integrator.status == "failed":
|
|
125
|
+
raise RuntimeError(message or "DOP853 failed to advance the LLG state.")
|
|
126
|
+
accepted_steps += 1
|
|
127
|
+
last_step_seconds = float(integrator.t) - previous_time
|
|
128
|
+
if exact and float(integrator.t) > target_seconds:
|
|
129
|
+
integrator = self._restart_at_exact_target(
|
|
130
|
+
integrator,
|
|
131
|
+
target_seconds,
|
|
132
|
+
time_before,
|
|
133
|
+
)
|
|
134
|
+
last_step_seconds = target_seconds - previous_time
|
|
135
|
+
break
|
|
136
|
+
return integrator, accepted_steps, last_step_seconds
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _can_continue_advancing(
|
|
140
|
+
integrator: Any,
|
|
141
|
+
target_seconds: float,
|
|
142
|
+
max_it: int,
|
|
143
|
+
accepted_steps: int,
|
|
144
|
+
) -> bool:
|
|
145
|
+
return float(integrator.t) < target_seconds and (max_it < 0 or accepted_steps < max_it)
|
|
146
|
+
|
|
147
|
+
def _restart_at_exact_target(
|
|
148
|
+
self,
|
|
149
|
+
integrator: Any,
|
|
150
|
+
target_seconds: float,
|
|
151
|
+
time_before: float,
|
|
152
|
+
) -> Any:
|
|
153
|
+
accepted_state = np.asarray(integrator.dense_output()(target_seconds), dtype=np.float64)
|
|
154
|
+
self._fields["m"] = np.array(np.reshape(accepted_state, (-1, 3)), copy=True)
|
|
155
|
+
self._invalidate_demag()
|
|
156
|
+
self._integrator_is_stale = True
|
|
157
|
+
self.reinitialise(initial_time=target_seconds)
|
|
158
|
+
if self._integrator is None:
|
|
159
|
+
raise RuntimeError("The time integrator could not be reinitialised.")
|
|
160
|
+
# Reinitialisation positions the solver at the target. Keep the clock at
|
|
161
|
+
# the start of this advance until record_advance accounts for the delta.
|
|
162
|
+
self.clock.stage_time = SI(time_before, "s")
|
|
163
|
+
return self._integrator
|
|
164
|
+
|
|
165
|
+
def _accept_integrated_state(
|
|
166
|
+
self,
|
|
167
|
+
integrator: Any,
|
|
168
|
+
state_before: FloatArray,
|
|
169
|
+
time_before: float,
|
|
170
|
+
accepted_steps: int,
|
|
171
|
+
last_step_seconds: float,
|
|
172
|
+
started: float,
|
|
173
|
+
) -> float:
|
|
174
|
+
accepted_state = np.asarray(integrator.y, dtype=np.float64)
|
|
175
|
+
if not np.all(np.isfinite(accepted_state)):
|
|
176
|
+
raise FloatingPointError("The integrator accepted a non-finite magnetisation state.")
|
|
177
|
+
reached_seconds = float(integrator.t)
|
|
178
|
+
self._fields["m"] = np.array(np.reshape(accepted_state, (-1, 3)), copy=True)
|
|
179
|
+
self._invalidate_demag()
|
|
180
|
+
elapsed_seconds = reached_seconds - time_before
|
|
181
|
+
self.clock.record_advance(
|
|
182
|
+
stage_time_seconds=reached_seconds,
|
|
183
|
+
accepted_steps=accepted_steps,
|
|
184
|
+
last_step_seconds=last_step_seconds,
|
|
185
|
+
wall_seconds=time.perf_counter() - started,
|
|
186
|
+
)
|
|
187
|
+
self._record_max_dm_dt(accepted_state, state_before, elapsed_seconds)
|
|
188
|
+
return reached_seconds
|
|
189
|
+
|
|
190
|
+
def _record_max_dm_dt(
|
|
191
|
+
self,
|
|
192
|
+
accepted_state: FloatArray,
|
|
193
|
+
state_before: FloatArray,
|
|
194
|
+
elapsed_seconds: float,
|
|
195
|
+
) -> None:
|
|
196
|
+
if elapsed_seconds <= 0.0:
|
|
197
|
+
return
|
|
198
|
+
displacement: FloatArray = np.reshape(accepted_state - state_before, (-1, 3))
|
|
199
|
+
self.max_dm_dt = float(np.max(np.linalg.norm(displacement, axis=1)) / elapsed_seconds)
|