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,267 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
7
+
8
+ from si.physical import SI
9
+ from simulation.quantity import Quantity
10
+
11
+ log = logging.getLogger("nmag")
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class _SubfieldRequest:
16
+ field_name: str
17
+ prefix: str
18
+ mat_name: str | None = None
19
+
20
+
21
+ class MaterialSource(Protocol):
22
+ name: str
23
+
24
+
25
+ @runtime_checkable
26
+ class SimulationSource(Protocol):
27
+ name: str
28
+ last_spatial_save_timings_seconds: dict[str, float]
29
+ last_maxangle_timings_seconds: dict[str, float]
30
+
31
+ @property
32
+ def id(self) -> int: ...
33
+
34
+ @property
35
+ def step(self) -> int: ...
36
+
37
+ @property
38
+ def stage(self) -> int: ...
39
+
40
+ @property
41
+ def stage_step(self) -> int: ...
42
+
43
+ @property
44
+ def time(self) -> SI: ...
45
+
46
+ @property
47
+ def stage_time(self) -> SI: ...
48
+
49
+ @property
50
+ def real_time(self) -> SI: ...
51
+
52
+ @property
53
+ def last_step_dt(self) -> SI: ...
54
+
55
+ def get_subfield_average(self, subfieldname: str, mat_name: str | None = None) -> Any: ...
56
+
57
+ def get_maxangle_average(self, field_name: str) -> float | None: ...
58
+
59
+ def get_materials_of_field(self, field_name: str) -> list[MaterialSource]: ...
60
+
61
+ def get_all_field_names(self) -> list[str]: ...
62
+
63
+ def save_spatial_fields(self, filename: str, fieldnames: list[str]) -> None: ...
64
+
65
+
66
+ class DataWriterCollectionMixin:
67
+ if TYPE_CHECKING:
68
+
69
+ def __getattr__(self, name: str) -> Any: ...
70
+
71
+ def _base_columns(self, source: SimulationSource) -> list[tuple[str, Any]]:
72
+ lt = time.localtime()
73
+ lt_str = f"{lt[0]:04d}/{lt[1]:02d}/{lt[2]:02d}-{lt[3]:02d}:{lt[4]:02d}:{lt[5]:02d}"
74
+
75
+ return [
76
+ ("id", source.id),
77
+ ("step", source.step),
78
+ ("stage_step", source.stage_step),
79
+ ("stage", source.stage),
80
+ ("last_step_dt", source.last_step_dt),
81
+ ("time", source.time),
82
+ ("stage_time", source.stage_time),
83
+ ("real_time", source.real_time),
84
+ ("unixtime", SI(time.time(), "s")),
85
+ ("localtime", lt_str),
86
+ ]
87
+
88
+ def _append_average_columns(
89
+ self,
90
+ columns: list[tuple[str, Any]],
91
+ current_quantities: list[Quantity] | None,
92
+ quantity: Quantity | None,
93
+ prefix: str,
94
+ average: Any,
95
+ ) -> None:
96
+ if isinstance(average, list):
97
+ for i, comp_value in enumerate(cast(list[Any], average)):
98
+ comp_name = f"{prefix}_{i}"
99
+ columns.append((comp_name, comp_value))
100
+ if current_quantities is not None and quantity is not None:
101
+ current_quantities.append(quantity.sub_quantity(comp_name))
102
+ else:
103
+ columns.append((prefix, average))
104
+ if current_quantities is not None and quantity is not None:
105
+ current_quantities.append(quantity.sub_quantity(prefix))
106
+
107
+ @staticmethod
108
+ def _source_has_subfield(source: SimulationSource, field_name: str) -> bool:
109
+ predicate = getattr(source, "is_subfield_available", None)
110
+ if predicate is None:
111
+ return True
112
+ try:
113
+ return bool(predicate(field_name))
114
+ except KeyError:
115
+ log.debug("Skipping unavailable subfield %s", field_name)
116
+ return False
117
+
118
+ def _gather_data(
119
+ self, source: SimulationSource
120
+ ) -> tuple[list[tuple[str, Any]], list[Quantity]]:
121
+ columns = self._base_columns(source)
122
+ current_quantities: list[Quantity] = [self.quantities_by_name[name] for name, _ in columns]
123
+ subfield_requests: list[_SubfieldRequest] = []
124
+ for quantity in self.quantities:
125
+ self._collect_quantity_subfields(
126
+ source,
127
+ quantity,
128
+ columns,
129
+ current_quantities,
130
+ subfield_requests,
131
+ )
132
+
133
+ writes_maxangle = self._collect_initial_maxangle(
134
+ source,
135
+ columns,
136
+ current_quantities,
137
+ )
138
+
139
+ self._subfield_requests = subfield_requests
140
+ self._writes_maxangle = writes_maxangle
141
+
142
+ return columns, current_quantities
143
+
144
+ def _collect_quantity_subfields(
145
+ self,
146
+ source: SimulationSource,
147
+ quantity: Quantity,
148
+ columns: list[tuple[str, Any]],
149
+ current_quantities: list[Quantity],
150
+ subfield_requests: list[_SubfieldRequest],
151
+ ) -> None:
152
+ field_name = quantity.name
153
+ if quantity.type not in ["field", "pfield"] or not self._source_has_subfield(
154
+ source,
155
+ field_name,
156
+ ):
157
+ return
158
+ if "?" not in (quantity.signature or ""):
159
+ self._collect_subfield_average(
160
+ source,
161
+ field_name,
162
+ field_name,
163
+ quantity,
164
+ columns,
165
+ current_quantities,
166
+ subfield_requests,
167
+ )
168
+ return
169
+ for material in source.get_materials_of_field(field_name):
170
+ self._collect_subfield_average(
171
+ source,
172
+ field_name,
173
+ f"{field_name}_{material.name}",
174
+ quantity,
175
+ columns,
176
+ current_quantities,
177
+ subfield_requests,
178
+ mat_name=material.name,
179
+ )
180
+
181
+ def _collect_subfield_average(
182
+ self,
183
+ source: SimulationSource,
184
+ field_name: str,
185
+ prefix: str,
186
+ quantity: Quantity,
187
+ columns: list[tuple[str, Any]],
188
+ current_quantities: list[Quantity],
189
+ subfield_requests: list[_SubfieldRequest],
190
+ *,
191
+ mat_name: str | None = None,
192
+ ) -> None:
193
+ average = self._subfield_average(source, field_name, prefix, mat_name)
194
+ if average is None:
195
+ return
196
+ self._append_average_columns(columns, current_quantities, quantity, prefix, average)
197
+ subfield_requests.append(_SubfieldRequest(field_name, prefix, mat_name))
198
+
199
+ def _subfield_average(
200
+ self,
201
+ source: SimulationSource,
202
+ field_name: str,
203
+ prefix: str,
204
+ mat_name: str | None = None,
205
+ ) -> Any | None:
206
+ try:
207
+ with self._record_timing(f"average:{prefix}"):
208
+ average = source.get_subfield_average(field_name, mat_name)
209
+ except KeyError:
210
+ log.debug("Skipping unavailable subfield average %s", prefix)
211
+ average = None
212
+ self._record_source_average_timings(source, prefix)
213
+ return average
214
+
215
+ def _collect_initial_maxangle(
216
+ self,
217
+ source: SimulationSource,
218
+ columns: list[tuple[str, Any]],
219
+ current_quantities: list[Quantity],
220
+ ) -> bool:
221
+ maxangle_quantity = self.quantities_by_name.get("maxangle")
222
+ maxangle = self._maxangle_average(source)
223
+ if maxangle_quantity is None or maxangle is None:
224
+ return False
225
+ columns.append(("maxangle_m_Py", maxangle))
226
+ current_quantities.append(maxangle_quantity.sub_quantity("maxangle_m_Py"))
227
+ return True
228
+
229
+ def _maxangle_average(self, source: SimulationSource) -> float | None:
230
+ try:
231
+ with self._record_timing("average:maxangle_m_Py"):
232
+ maxangle = source.get_maxangle_average("m")
233
+ except KeyError:
234
+ log.debug("Skipping unavailable max-angle average for m")
235
+ maxangle = None
236
+ self._record_source_maxangle_timings(source, "maxangle_m_Py")
237
+ return maxangle
238
+
239
+ def _gather_existing_schema_data(self, source: SimulationSource) -> list[tuple[str, Any]]:
240
+ columns = self._base_columns(source)
241
+
242
+ for request in self._subfield_requests:
243
+ try:
244
+ with self._record_timing(f"average:{request.prefix}"):
245
+ avg = source.get_subfield_average(request.field_name, request.mat_name)
246
+ self._record_source_average_timings(source, request.prefix)
247
+ if avg is None:
248
+ continue
249
+ except KeyError:
250
+ self._record_source_average_timings(source, request.prefix)
251
+ log.debug("Skipping unavailable subfield average %s", request.prefix)
252
+ continue
253
+
254
+ self._append_average_columns(
255
+ columns,
256
+ None,
257
+ None,
258
+ request.prefix,
259
+ avg,
260
+ )
261
+
262
+ if self._writes_maxangle:
263
+ maxangle = self._maxangle_average(source)
264
+ if maxangle is not None:
265
+ columns.append(("maxangle_m_Py", maxangle))
266
+
267
+ return columns
@@ -0,0 +1,74 @@
1
+ """
2
+ This file contains the implementation of the hysteresis method
3
+ of the Simulation class.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Protocol
9
+
10
+ from si.physical import SI
11
+ from when import When
12
+
13
+ from .clock import SimulationClock
14
+ from .hysteresis_runner import (
15
+ _update_progress_file as _update_progress_file, # noqa: F401
16
+ )
17
+ from .hysteresis_runner import (
18
+ simulation_hysteresis as simulation_hysteresis,
19
+ )
20
+ from .hysteresis_runner import (
21
+ simulation_relax as simulation_relax,
22
+ )
23
+ from .hysteresis_schedule import (
24
+ Action,
25
+ ScheduleInput,
26
+ )
27
+ from .hysteresis_schedule import (
28
+ _append_x_list as _append_x_list, # noqa: F401
29
+ )
30
+ from .hysteresis_schedule import (
31
+ _join_save_and_do_lists as _join_save_and_do_lists, # noqa: F401
32
+ )
33
+ from .hysteresis_schedule import (
34
+ _string_normalise as _string_normalise, # noqa: F401
35
+ )
36
+
37
+
38
+ class _ConvergenceSource(Protocol):
39
+ def get_log(self) -> str: ...
40
+
41
+
42
+ class HysteresisSimulation(Protocol):
43
+ name: str
44
+ clock: SimulationClock
45
+ convergence: _ConvergenceSource
46
+ action_abbreviations: dict[str, Action]
47
+ _restarting: bool
48
+ max_time_reached: SI
49
+
50
+ def simulation_hysteresis(
51
+ self,
52
+ H_ext_list: list[Any],
53
+ save: ScheduleInput | None = None,
54
+ do: ScheduleInput | None = None,
55
+ convergence_check: When | None = None,
56
+ progress_message_minimum_delay: float = 60.0,
57
+ ) -> None: ...
58
+
59
+ def load_restart_file(self, filename: str | None = None) -> None: ...
60
+
61
+ def do_next_stage(self, stage: int | None = None) -> None: ...
62
+
63
+ def set_H_ext(self, values: Any, unit: SI | None = None) -> None: ...
64
+
65
+ def reinitialise(self, initial_time: float | None = None) -> None: ...
66
+
67
+ def is_converged(self) -> bool: ...
68
+
69
+ def advance_time(
70
+ self,
71
+ target_time: SI,
72
+ max_it: int = -1,
73
+ exact_tstop: bool | None = None,
74
+ ) -> SI: ...
@@ -0,0 +1,286 @@
1
+ """Execution loops for relaxation and multi-stage hysteresis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from typing import Any
8
+
9
+ from si.physical import SI
10
+ from throttler import Throttler
11
+ from when import When, at, every
12
+
13
+ from .clock import SimulationClock
14
+ from .hysteresis_schedule import (
15
+ CounterTriple,
16
+ ScheduleInput,
17
+ _join_save_and_do_lists,
18
+ _next_deltas,
19
+ _next_time,
20
+ )
21
+
22
+ log = logging.getLogger("nmag")
23
+ _progress_throttler = Throttler()
24
+
25
+
26
+ def _update_progress_file(
27
+ sim: Any,
28
+ H_ext: Any,
29
+ progress_file_name: str,
30
+ progress_message_minimum_delay: float,
31
+ ) -> None:
32
+ """
33
+ Writes the current simulation progress to a file, throttled
34
+ to a minimum delay.
35
+ """
36
+ if _progress_throttler.is_allowed("hysteresis_reporting", progress_message_minimum_delay):
37
+ try:
38
+ with open(progress_file_name, "w", encoding="utf-8") as f:
39
+ f.write(f"{time.asctime()}\n")
40
+ f.write(f"{sim.clock}\n")
41
+ f.write(f"{sim.convergence.get_log()}\n")
42
+
43
+ log.info(
44
+ f"it {sim.clock.step}, time {sim.clock.time_reached_si.dens_str()}; "
45
+ f"stage {sim.clock.stage}; H_ext={str(H_ext)}"
46
+ )
47
+ except Exception as e:
48
+ log.warning(f"Could not write progress file '{progress_file_name}'. Error: {e}")
49
+
50
+
51
+ def simulation_relax(
52
+ self: Any,
53
+ H_applied: Any = None,
54
+ save: ScheduleInput | None = None,
55
+ do: ScheduleInput | None = None,
56
+ convergence_check: When | None = None,
57
+ ) -> None:
58
+ """
59
+ This method carries out the time integration of the LLG until
60
+ the system reaches a (metastable) equilibrium.
61
+ Internally, this uses the hysteresis() loop command.
62
+
63
+ (Docstring parameters omitted for brevity, they are unchanged)
64
+ """
65
+ if save is None:
66
+ save = [("averages", "fields", at("stage_end"))]
67
+ if do is None:
68
+ do = []
69
+ if convergence_check is None:
70
+ convergence_check = every("step", 5)
71
+
72
+ log.debug("Entering 'relax'")
73
+ fields = [H_applied]
74
+ log.debug(f"Calling hysteresis({fields})")
75
+ self.simulation_hysteresis(
76
+ fields,
77
+ save=save,
78
+ do=do,
79
+ convergence_check=convergence_check,
80
+ )
81
+
82
+
83
+ def simulation_hysteresis(
84
+ self: Any,
85
+ H_ext_list: list[Any],
86
+ save: ScheduleInput | None = None,
87
+ do: ScheduleInput | None = None,
88
+ convergence_check: When | None = None,
89
+ progress_message_minimum_delay: float = 60.0,
90
+ ) -> None:
91
+ """
92
+ This method executes a simulation where the applied field
93
+ is set in sequence to the values specified in ``H_ext_list``.
94
+
95
+ (Full docstring omitted for brevity, it is unchanged)
96
+ """
97
+ save, do, convergence_check = _hysteresis_defaults(save, do, convergence_check)
98
+ log.debug(
99
+ f"simulation_hysteresis(): Entering with H_ext_list={H_ext_list}, "
100
+ f"save={save}, do={do}, convergence_check={convergence_check}"
101
+ )
102
+
103
+ thing_when_tuples = _scheduled_actions(self, save, do)
104
+ log.debug(f"simulation_hysteresis(): thing_when_tuples={thing_when_tuples}")
105
+ next_save_time = _initial_next_save_times(thing_when_tuples)
106
+ match_tolerances = _hysteresis_match_tolerances()
107
+ progress_file_name = f"{self.name}_progress.txt"
108
+ _prepare_hysteresis_run(self, progress_file_name)
109
+ stage: int | None = int(self.clock.stage)
110
+ self.clock.exit_hysteresis = False
111
+ for H_ext in H_ext_list[stage - 1 :]:
112
+ _prepare_hysteresis_stage(
113
+ self, H_ext, stage, thing_when_tuples, next_save_time, match_tolerances
114
+ )
115
+ stage = None
116
+ if _run_hysteresis_stage(
117
+ self,
118
+ H_ext,
119
+ thing_when_tuples,
120
+ next_save_time,
121
+ convergence_check,
122
+ match_tolerances,
123
+ progress_file_name,
124
+ progress_message_minimum_delay,
125
+ ):
126
+ return
127
+
128
+
129
+ def _hysteresis_defaults(
130
+ save: ScheduleInput | None,
131
+ do: ScheduleInput | None,
132
+ convergence_check: When | None,
133
+ ) -> tuple[ScheduleInput, ScheduleInput, When]:
134
+ return (
135
+ [("averages", "fields", at("stage_end"))] if save is None else save,
136
+ [] if do is None else do,
137
+ every("step", 5) if convergence_check is None else convergence_check,
138
+ )
139
+
140
+
141
+ def _scheduled_actions(self: Any, save: ScheduleInput, do: ScheduleInput) -> list[tuple[Any, When]]:
142
+ return _join_save_and_do_lists(save, do, predefined_actions=self.action_abbreviations)
143
+
144
+
145
+ def _initial_next_save_times(
146
+ actions: list[tuple[Any, When]],
147
+ ) -> dict[str, CounterTriple | None]:
148
+ next_save_time: dict[str, CounterTriple | None] = {}
149
+ for what, _ in actions:
150
+ key = str(what)
151
+ if key in next_save_time:
152
+ raise ValueError(
153
+ "Error in optional argument 'save' or 'do' of method 'hysteresis': "
154
+ "the list of (thing_to_save, when) contains two or more specifications "
155
+ f"for thing_to_save = {key}. You should remove the duplicate entry and "
156
+ "eventually use the operator | (such as in: (thing_to_save, when1 | when2))."
157
+ )
158
+ next_save_time[key] = None
159
+ return next_save_time
160
+
161
+
162
+ def _hysteresis_match_tolerances() -> dict[str, Any]:
163
+ negligible_time = SI(1e-20, "s")
164
+ return {"time": negligible_time, "stage_time": negligible_time}
165
+
166
+
167
+ def _prepare_hysteresis_run(self: Any, progress_file_name: str) -> None:
168
+ if self._restarting:
169
+ log.info("Hysteresis loop: restarting from a previously saved configuration...")
170
+ self.load_restart_file()
171
+ self._restarting = False
172
+ return
173
+ log.info("Hysteresis loop: starting a new simulation.")
174
+ log.info(f"Hysteresis loop: check file '{progress_file_name}' for progress data")
175
+
176
+
177
+ def _next_event_time(
178
+ event: When, clock: SimulationClock, tolerances: dict[str, Any]
179
+ ) -> CounterTriple:
180
+ return _next_time(event, clock, tols=tolerances)
181
+
182
+
183
+ def _next_event_deltas(
184
+ event: When,
185
+ clock: SimulationClock,
186
+ tolerances: dict[str, Any],
187
+ suggest: CounterTriple | None = None,
188
+ ) -> CounterTriple:
189
+ return _next_deltas(event, clock, suggest=suggest, tols=tolerances)
190
+
191
+
192
+ def _prepare_hysteresis_stage(
193
+ self: Any,
194
+ applied_field: Any,
195
+ stage: int | None,
196
+ actions: list[tuple[Any, When]],
197
+ next_save_time: dict[str, CounterTriple | None],
198
+ tolerances: dict[str, Any],
199
+ ) -> None:
200
+ log.info(f"hysteresis: starting new stage: field = {str(applied_field)}")
201
+ self.do_next_stage(stage=stage)
202
+ self.clock.stage_end = False
203
+ if applied_field is not None:
204
+ self.set_H_ext(applied_field)
205
+ self.reinitialise(initial_time=0)
206
+ for what, when in actions:
207
+ key = str(what)
208
+ next_save_time[key] = _next_event_time(when, self.clock, tolerances)
209
+ log.debug(f"hysteresis: will save {what} at {next_save_time[key]}")
210
+
211
+
212
+ def _run_hysteresis_stage(
213
+ self: Any,
214
+ applied_field: Any,
215
+ actions: list[tuple[Any, When]],
216
+ next_save_time: dict[str, CounterTriple | None],
217
+ convergence_check: When,
218
+ tolerances: dict[str, Any],
219
+ progress_file_name: str,
220
+ progress_delay: float,
221
+ ) -> bool:
222
+ while True:
223
+ self.clock.stage_end = converged = self.is_converged()
224
+ deltas = _next_event_deltas(convergence_check, self.clock, tolerances)
225
+ deltas = _dispatch_scheduled_actions(
226
+ self,
227
+ actions,
228
+ next_save_time,
229
+ tolerances,
230
+ deltas,
231
+ )
232
+ if self.clock.exit_hysteresis:
233
+ log.debug(
234
+ "Exit from the hysteresis loop has been forced using the tag 'exit': exiting now!"
235
+ )
236
+ return True
237
+ if self.clock.stage_end:
238
+ log.debug(
239
+ f"Reached end of stage in hysteresis command, converged={converged}, exiting now!"
240
+ )
241
+ return False
242
+ _advance_hysteresis_stage(self, deltas, applied_field, progress_file_name, progress_delay)
243
+
244
+
245
+ def _dispatch_scheduled_actions(
246
+ self: Any,
247
+ actions: list[tuple[Any, When]],
248
+ next_save_time: dict[str, CounterTriple | None],
249
+ tolerances: dict[str, Any],
250
+ deltas: CounterTriple,
251
+ ) -> CounterTriple:
252
+ for what, when in actions:
253
+ key = str(what)
254
+ next_time = _next_event_time(when, self.clock, tolerances)
255
+ if when.match_time(self.clock) or next_time != next_save_time[key]:
256
+ log.info(
257
+ f"hysteresis: saving {what} at id={self.clock.id},step={self.clock.step}.\n{self.clock}"
258
+ )
259
+ what(self)
260
+ next_save_time[key] = next_time
261
+ deltas = _next_event_deltas(when, self.clock, tolerances, suggest=deltas)
262
+ return deltas
263
+
264
+
265
+ def _advance_hysteresis_stage(
266
+ self: Any,
267
+ deltas: CounterTriple,
268
+ applied_field: Any,
269
+ progress_file_name: str,
270
+ progress_delay: float,
271
+ ) -> None:
272
+ delta_step, delta_time, delta_real_time = deltas
273
+ target_time = (
274
+ self.max_time_reached if delta_time is None else self.clock.stage_time + delta_time
275
+ )
276
+ log.debug(
277
+ f"predicted advance: delta_step={delta_step}, delta_time={delta_time}, "
278
+ f"delta_real_time={delta_real_time}"
279
+ )
280
+ time_reached = self.advance_time(target_time, max_it=-1 if delta_step is None else delta_step)
281
+ if time_reached > 0.99 * self.max_time_reached:
282
+ raise RuntimeError(
283
+ f"Simulation time reached {self.max_time_reached}: "
284
+ "are you starting from a zero torque configuration?"
285
+ )
286
+ _update_progress_file(self, applied_field, progress_file_name, progress_delay)