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,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
|
simulation/hysteresis.py
ADDED
|
@@ -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)
|