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