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
si/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 inline type information.
|
simulation/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Simulation support modules for the nmag Python 3 rewrite."""
|
simulation/clock.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from math import isfinite
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from tabulate import tabulate
|
|
8
|
+
|
|
9
|
+
from si.physical import SI
|
|
10
|
+
|
|
11
|
+
_zero_seconds_cache: SI | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _zero_seconds() -> SI:
|
|
15
|
+
global _zero_seconds_cache
|
|
16
|
+
if _zero_seconds_cache is None:
|
|
17
|
+
_zero_seconds_cache = SI(0.0, "s")
|
|
18
|
+
return SI(_zero_seconds_cache._quantity)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fmt_time(t: SI, fmt_ps: str = ".2f", fmt_ns: str = ".2f") -> str:
|
|
22
|
+
"""Formats an SI time object into picoseconds or nanoseconds."""
|
|
23
|
+
t_ps = float(t / SI(1e-12, "s"))
|
|
24
|
+
|
|
25
|
+
ps_str = f"{t_ps:{fmt_ps}}"
|
|
26
|
+
ns_str = f"{(t_ps / 1000.0):{fmt_ns}}"
|
|
27
|
+
|
|
28
|
+
return f"{ps_str} ps" if t_ps < 100.0 else f"{ns_str} ns"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SimulationClock:
|
|
33
|
+
"""
|
|
34
|
+
This object specifies all the parameters which define the current time
|
|
35
|
+
in the simulation, such as the simulation time, step number, ...
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
id: Unique identifier for data saved. Incremented on save.
|
|
39
|
+
stage: Stage number. Increments when the external field changes.
|
|
40
|
+
step: Total number of steps performed (always increases).
|
|
41
|
+
stage_step: Step number from the beginning of the current stage.
|
|
42
|
+
zero_stage_step: The value of 'step' at the beginning of the stage.
|
|
43
|
+
time: Total simulation time (always increases).
|
|
44
|
+
stage_time: The simulation time from the beginning of the stage.
|
|
45
|
+
zero_stage_time: The value of 'time' at the beginning of the stage.
|
|
46
|
+
real_time: The real world time used for advancing time.
|
|
47
|
+
last_step_dt_si: Last time step's length in SI units.
|
|
48
|
+
convergence: Flag indicating if convergence is reached.
|
|
49
|
+
stage_end: Flag indicating the end of a stage.
|
|
50
|
+
exit_hysteresis: Flag to signal exit from hysteresis loop.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
id: int = -1
|
|
54
|
+
stage: int = 1
|
|
55
|
+
step: int = 0
|
|
56
|
+
time: SI = field(default_factory=_zero_seconds)
|
|
57
|
+
stage_step: int = 0
|
|
58
|
+
stage_time: SI = field(default_factory=_zero_seconds)
|
|
59
|
+
real_time: SI = field(default_factory=_zero_seconds)
|
|
60
|
+
stage_end: bool = False
|
|
61
|
+
convergence: bool = False
|
|
62
|
+
exit_hysteresis: bool = False
|
|
63
|
+
zero_stage_time: SI = field(default_factory=_zero_seconds)
|
|
64
|
+
zero_stage_step: int = 0
|
|
65
|
+
time_reached_su: float = 0.0
|
|
66
|
+
time_reached_si: SI = field(default_factory=_zero_seconds)
|
|
67
|
+
last_step_dt_su: float = 0.0
|
|
68
|
+
last_step_dt_si: SI = field(default_factory=_zero_seconds)
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, key: str, /) -> Any:
|
|
71
|
+
if key not in self.__dataclass_fields__:
|
|
72
|
+
raise KeyError(key)
|
|
73
|
+
return getattr(self, key)
|
|
74
|
+
|
|
75
|
+
def get(self, key: str, default: Any = None, /) -> Any:
|
|
76
|
+
try:
|
|
77
|
+
return self[key]
|
|
78
|
+
except KeyError:
|
|
79
|
+
return default
|
|
80
|
+
|
|
81
|
+
def copy(self) -> dict[str, Any]:
|
|
82
|
+
return asdict(self)
|
|
83
|
+
|
|
84
|
+
def checkpoint_state(self) -> dict[str, int | float | bool]:
|
|
85
|
+
"""Return a JSON-safe representation of the simulation clock."""
|
|
86
|
+
return {
|
|
87
|
+
"id": self.id,
|
|
88
|
+
"stage": self.stage,
|
|
89
|
+
"step": self.step,
|
|
90
|
+
"stage_step": self.stage_step,
|
|
91
|
+
"zero_stage_step": self.zero_stage_step,
|
|
92
|
+
"time_seconds": self.time.in_units_of(SI(1.0, "s")),
|
|
93
|
+
"stage_time_seconds": self.stage_time.in_units_of(SI(1.0, "s")),
|
|
94
|
+
"real_time_seconds": self.real_time.in_units_of(SI(1.0, "s")),
|
|
95
|
+
"zero_stage_time_seconds": self.zero_stage_time.in_units_of(SI(1.0, "s")),
|
|
96
|
+
"time_reached_seconds": self.time_reached_si.in_units_of(SI(1.0, "s")),
|
|
97
|
+
"last_step_seconds": self.last_step_dt_si.in_units_of(SI(1.0, "s")),
|
|
98
|
+
"stage_end": self.stage_end,
|
|
99
|
+
"convergence": self.convergence,
|
|
100
|
+
"exit_hysteresis": self.exit_hysteresis,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_checkpoint_state(cls, state: dict[str, object]) -> SimulationClock:
|
|
105
|
+
"""Build a validated clock from :meth:`checkpoint_state` data."""
|
|
106
|
+
integer_names = ("id", "stage", "step", "stage_step", "zero_stage_step")
|
|
107
|
+
seconds_names = (
|
|
108
|
+
"time_seconds",
|
|
109
|
+
"stage_time_seconds",
|
|
110
|
+
"real_time_seconds",
|
|
111
|
+
"zero_stage_time_seconds",
|
|
112
|
+
"time_reached_seconds",
|
|
113
|
+
"last_step_seconds",
|
|
114
|
+
)
|
|
115
|
+
boolean_names = ("stage_end", "convergence", "exit_hysteresis")
|
|
116
|
+
required = {*integer_names, *seconds_names, *boolean_names}
|
|
117
|
+
missing = sorted(required.difference(state))
|
|
118
|
+
if missing:
|
|
119
|
+
raise ValueError(f"Checkpoint clock state is missing {', '.join(missing)}.")
|
|
120
|
+
|
|
121
|
+
integers: dict[str, int] = {}
|
|
122
|
+
for name in integer_names:
|
|
123
|
+
value = state[name]
|
|
124
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
125
|
+
raise ValueError(f"Checkpoint clock {name} must be an integer.")
|
|
126
|
+
integers[name] = value
|
|
127
|
+
if integers["stage"] < 1 or any(
|
|
128
|
+
integers[name] < 0 for name in ("step", "stage_step", "zero_stage_step")
|
|
129
|
+
):
|
|
130
|
+
raise ValueError("Checkpoint clock counters must be non-negative and stage at least one.")
|
|
131
|
+
|
|
132
|
+
seconds: dict[str, float] = {}
|
|
133
|
+
for name in seconds_names:
|
|
134
|
+
value = state[name]
|
|
135
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
136
|
+
raise ValueError(f"Checkpoint clock {name} must be a finite number.")
|
|
137
|
+
converted = float(value)
|
|
138
|
+
if not isfinite(converted) or converted < 0.0:
|
|
139
|
+
raise ValueError(f"Checkpoint clock {name} must be a non-negative finite number.")
|
|
140
|
+
seconds[name] = converted
|
|
141
|
+
|
|
142
|
+
booleans: dict[str, bool] = {}
|
|
143
|
+
for name in boolean_names:
|
|
144
|
+
value = state[name]
|
|
145
|
+
if not isinstance(value, bool):
|
|
146
|
+
raise ValueError(f"Checkpoint clock {name} must be a boolean.")
|
|
147
|
+
booleans[name] = value
|
|
148
|
+
|
|
149
|
+
return cls(
|
|
150
|
+
id=integers["id"],
|
|
151
|
+
stage=integers["stage"],
|
|
152
|
+
step=integers["step"],
|
|
153
|
+
stage_step=integers["stage_step"],
|
|
154
|
+
zero_stage_step=integers["zero_stage_step"],
|
|
155
|
+
time=SI(seconds["time_seconds"], "s"),
|
|
156
|
+
stage_time=SI(seconds["stage_time_seconds"], "s"),
|
|
157
|
+
real_time=SI(seconds["real_time_seconds"], "s"),
|
|
158
|
+
zero_stage_time=SI(seconds["zero_stage_time_seconds"], "s"),
|
|
159
|
+
time_reached_su=seconds["time_reached_seconds"],
|
|
160
|
+
time_reached_si=SI(seconds["time_reached_seconds"], "s"),
|
|
161
|
+
last_step_dt_su=seconds["last_step_seconds"],
|
|
162
|
+
last_step_dt_si=SI(seconds["last_step_seconds"], "s"),
|
|
163
|
+
stage_end=booleans["stage_end"],
|
|
164
|
+
convergence=booleans["convergence"],
|
|
165
|
+
exit_hysteresis=booleans["exit_hysteresis"],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# __init__ and __repr__ are GONE (auto-generated)
|
|
169
|
+
|
|
170
|
+
def inc_stage(self, stage: int | None = None) -> None:
|
|
171
|
+
"""Advance the clock to the next stage."""
|
|
172
|
+
if stage is None:
|
|
173
|
+
self.stage += 1
|
|
174
|
+
else:
|
|
175
|
+
self.stage = stage
|
|
176
|
+
self.stage_step = 0
|
|
177
|
+
self.stage_time = SI(0.0, "s")
|
|
178
|
+
self.convergence = False
|
|
179
|
+
self.zero_stage_step = self.step
|
|
180
|
+
self.zero_stage_time = self.time
|
|
181
|
+
|
|
182
|
+
def record_advance(
|
|
183
|
+
self,
|
|
184
|
+
*,
|
|
185
|
+
stage_time_seconds: float,
|
|
186
|
+
accepted_steps: int,
|
|
187
|
+
last_step_seconds: float,
|
|
188
|
+
wall_seconds: float,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Atomically record an accepted group of integration steps."""
|
|
191
|
+
previous_stage_seconds = self.stage_time.in_units_of(SI(1.0, "s"))
|
|
192
|
+
delta_seconds = stage_time_seconds - previous_stage_seconds
|
|
193
|
+
if delta_seconds < 0.0:
|
|
194
|
+
raise ValueError("stage time cannot move backwards.")
|
|
195
|
+
if accepted_steps < 0:
|
|
196
|
+
raise ValueError("accepted_steps cannot be negative.")
|
|
197
|
+
|
|
198
|
+
self.step += accepted_steps
|
|
199
|
+
self.stage_step += accepted_steps
|
|
200
|
+
self.time += SI(delta_seconds, "s")
|
|
201
|
+
self.stage_time = SI(stage_time_seconds, "s")
|
|
202
|
+
self.time_reached_su = stage_time_seconds
|
|
203
|
+
self.time_reached_si = SI(stage_time_seconds, "s")
|
|
204
|
+
self.last_step_dt_su = last_step_seconds
|
|
205
|
+
self.last_step_dt_si = SI(last_step_seconds, "s")
|
|
206
|
+
self.real_time += SI(max(0.0, wall_seconds), "s")
|
|
207
|
+
|
|
208
|
+
# This method was updated to use tabulate, the format of the data printed out might
|
|
209
|
+
# look slightly different, but there is a lot less manual formatting code here now.
|
|
210
|
+
def __str__(self) -> str:
|
|
211
|
+
ft = fmt_time
|
|
212
|
+
|
|
213
|
+
rows = [
|
|
214
|
+
[
|
|
215
|
+
f"ID={self.id}",
|
|
216
|
+
f"Step={self.step}",
|
|
217
|
+
f"Time={ft(self.time)}",
|
|
218
|
+
f"Last step size={ft(self.last_step_dt_si)}",
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
"",
|
|
222
|
+
f"Stage={self.stage}",
|
|
223
|
+
f"Stage-step={self.stage_step}",
|
|
224
|
+
f"Stage-time={ft(self.stage_time)}",
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"",
|
|
228
|
+
f"Convergence={self.convergence}",
|
|
229
|
+
f"Stage-end={self.stage_end}",
|
|
230
|
+
f"Exit hysteresis={self.exit_hysteresis}",
|
|
231
|
+
],
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
table = tabulate(rows, tablefmt="pipe")
|
|
235
|
+
|
|
236
|
+
sep_line = "=" * (len(table.splitlines()[0]))
|
|
237
|
+
return f"{sep_line}\n{table}\n{sep_line}"
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from si.physical import SI
|
|
12
|
+
from simulation.data_writer_collection import (
|
|
13
|
+
DataWriterCollectionMixin,
|
|
14
|
+
SimulationSource,
|
|
15
|
+
_SubfieldRequest,
|
|
16
|
+
)
|
|
17
|
+
from simulation.quantity import Quantity, known_quantities
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger("nmag")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataWriter(DataWriterCollectionMixin):
|
|
23
|
+
def __init__(self, ndt_filename: Path, h5_filename: Path, *, append: bool = False) -> None:
|
|
24
|
+
"""
|
|
25
|
+
:param ndt_filename: Path to the tabular output (.ndt).
|
|
26
|
+
:param h5_filename: Path to the spatial output (.h5).
|
|
27
|
+
"""
|
|
28
|
+
self.ndt_filename = ndt_filename
|
|
29
|
+
self.h5_filename = h5_filename
|
|
30
|
+
self.quantities = known_quantities
|
|
31
|
+
|
|
32
|
+
self.quantities_by_name: dict[str, Quantity] = {q.name: q for q in known_quantities}
|
|
33
|
+
|
|
34
|
+
self._header_written: bool = False
|
|
35
|
+
self._column_names: list[str] | None = None
|
|
36
|
+
self._column_index_by_name: dict[str, int] | None = None
|
|
37
|
+
self._column_units: dict[str, str] | None = None
|
|
38
|
+
self._subfield_requests: list[_SubfieldRequest] = []
|
|
39
|
+
self._writes_maxangle: bool = False
|
|
40
|
+
self._last_saved_step: int = -1
|
|
41
|
+
self.last_save_timings_seconds: dict[str, float] = {}
|
|
42
|
+
self._active_save_timings: dict[str, float] | None = None
|
|
43
|
+
self._append_schema_pending = False
|
|
44
|
+
if append and self.ndt_filename.exists():
|
|
45
|
+
self._recover_ndt_schema()
|
|
46
|
+
self._append_schema_pending = True
|
|
47
|
+
|
|
48
|
+
def _recover_ndt_schema(self) -> None:
|
|
49
|
+
"""Recover the schema and last step from an NDT file opened for append."""
|
|
50
|
+
|
|
51
|
+
with open(self.ndt_filename, newline="", encoding="utf-8") as stream:
|
|
52
|
+
rows = [row for row in csv.reader(stream, delimiter="\t") if row]
|
|
53
|
+
if len(rows) < 2 or not rows[0][0].startswith("# Simulation:"):
|
|
54
|
+
raise ValueError(f"Cannot append to malformed NDT output: {self.ndt_filename}.")
|
|
55
|
+
column_names = rows[1]
|
|
56
|
+
if len(column_names) != len(set(column_names)):
|
|
57
|
+
raise ValueError(f"Cannot append to NDT output with duplicate columns: {self.ndt_filename}.")
|
|
58
|
+
self._column_names = column_names
|
|
59
|
+
self._column_index_by_name = {name: index for index, name in enumerate(column_names)}
|
|
60
|
+
self._column_units = {}
|
|
61
|
+
self._header_written = True
|
|
62
|
+
step_index = self._column_index_by_name.get("step")
|
|
63
|
+
if step_index is not None:
|
|
64
|
+
for row in reversed(rows[2:]):
|
|
65
|
+
if len(row) != len(column_names):
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
self._last_saved_step = int(float(row[step_index]))
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
def save(
|
|
74
|
+
self,
|
|
75
|
+
source: SimulationSource,
|
|
76
|
+
fields: str | list[str] | None = None,
|
|
77
|
+
avoid_same_step: bool = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
|
|
80
|
+
current_step = source.step
|
|
81
|
+
if avoid_same_step and current_step == self._last_saved_step:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
timings: dict[str, float] = {}
|
|
85
|
+
self._active_save_timings = timings
|
|
86
|
+
total_started = time.perf_counter()
|
|
87
|
+
try:
|
|
88
|
+
with self._record_timing("write_ndt_row"):
|
|
89
|
+
self._write_ndt_row(source)
|
|
90
|
+
|
|
91
|
+
if fields is not None:
|
|
92
|
+
with self._record_timing("save_spatial_fields"):
|
|
93
|
+
self._trigger_field_save(source, fields)
|
|
94
|
+
finally:
|
|
95
|
+
timings["total"] = time.perf_counter() - total_started
|
|
96
|
+
self.last_save_timings_seconds = dict(sorted(timings.items()))
|
|
97
|
+
self._active_save_timings = None
|
|
98
|
+
|
|
99
|
+
self._last_saved_step = current_step
|
|
100
|
+
|
|
101
|
+
@contextmanager
|
|
102
|
+
def _record_timing(self, name: str) -> Generator[None]:
|
|
103
|
+
started = time.perf_counter()
|
|
104
|
+
try:
|
|
105
|
+
yield
|
|
106
|
+
finally:
|
|
107
|
+
timings = self._active_save_timings
|
|
108
|
+
if timings is not None:
|
|
109
|
+
timings[name] = timings.get(name, 0.0) + time.perf_counter() - started
|
|
110
|
+
|
|
111
|
+
def _trigger_field_save(self, source: SimulationSource, fields: str | list[str]) -> None:
|
|
112
|
+
with self._record_timing("spatial_detail:field_selection"):
|
|
113
|
+
field_names_to_save: list[str] = []
|
|
114
|
+
|
|
115
|
+
if fields == "all":
|
|
116
|
+
field_names_to_save = source.get_all_field_names()
|
|
117
|
+
elif isinstance(fields, list):
|
|
118
|
+
field_names_to_save = fields
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError(f"Invalid fields argument: {fields}")
|
|
121
|
+
|
|
122
|
+
if field_names_to_save:
|
|
123
|
+
with self._record_timing("spatial_detail:write_dispatch"):
|
|
124
|
+
source.save_spatial_fields(
|
|
125
|
+
filename=str(self.h5_filename), fieldnames=field_names_to_save
|
|
126
|
+
)
|
|
127
|
+
self._record_source_spatial_save_timings(source)
|
|
128
|
+
|
|
129
|
+
def _write_ndt_row(self, source: SimulationSource) -> None:
|
|
130
|
+
quantities: list[Quantity] = []
|
|
131
|
+
with self._record_timing("gather_ndt_columns"):
|
|
132
|
+
if self._append_schema_pending:
|
|
133
|
+
with self._record_timing("gather_ndt_append_schema_columns"):
|
|
134
|
+
columns, _quantities = self._gather_data(source)
|
|
135
|
+
elif self._header_written:
|
|
136
|
+
with self._record_timing("gather_ndt_existing_schema_columns"):
|
|
137
|
+
columns = self._gather_existing_schema_data(source)
|
|
138
|
+
quantities: list[Quantity] = []
|
|
139
|
+
else:
|
|
140
|
+
with self._record_timing("gather_ndt_initial_schema_columns"):
|
|
141
|
+
columns, quantities = self._gather_data(source)
|
|
142
|
+
|
|
143
|
+
if not self._header_written:
|
|
144
|
+
with self._record_timing("build_ndt_header_schema"):
|
|
145
|
+
col_names: list[str] = []
|
|
146
|
+
col_units: dict[str, str] = {}
|
|
147
|
+
|
|
148
|
+
for (name, _), qty in zip(columns, quantities, strict=True):
|
|
149
|
+
col_names.append(name)
|
|
150
|
+
if qty.units:
|
|
151
|
+
col_units[name] = qty.units.dens_str()
|
|
152
|
+
else:
|
|
153
|
+
col_units[name] = "-"
|
|
154
|
+
|
|
155
|
+
column_index_by_name = {name: index for index, name in enumerate(col_names)}
|
|
156
|
+
with self._record_timing("format_ndt_values"):
|
|
157
|
+
row_values: list[Any] = [self._ndt_cell_value(value) for _, value in columns]
|
|
158
|
+
|
|
159
|
+
with open(self.ndt_filename, "w", newline="", encoding="utf-8") as f:
|
|
160
|
+
writer = csv.writer(f, delimiter="\t")
|
|
161
|
+
with self._record_timing("write_ndt_header"):
|
|
162
|
+
f.write(f"# Simulation: {source.name}\n")
|
|
163
|
+
writer.writerow(col_names)
|
|
164
|
+
|
|
165
|
+
with self._record_timing("write_ndt_values"):
|
|
166
|
+
writer.writerow(row_values)
|
|
167
|
+
|
|
168
|
+
self._column_names = col_names
|
|
169
|
+
self._column_index_by_name = column_index_by_name
|
|
170
|
+
self._column_units = col_units
|
|
171
|
+
self._header_written = True
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if self._column_names is None:
|
|
175
|
+
log.error("Column names not initialized.")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
with self._record_timing("validate_ndt_schema"):
|
|
179
|
+
actual_names = [name for name, _ in columns]
|
|
180
|
+
if actual_names != self._column_names:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"Cannot append output with a different NDT schema. "
|
|
183
|
+
f"Expected {self._column_names!r}, got {actual_names!r}."
|
|
184
|
+
)
|
|
185
|
+
self._append_schema_pending = False
|
|
186
|
+
|
|
187
|
+
with self._record_timing("format_ndt_values"):
|
|
188
|
+
row_values = self._existing_schema_row_values(columns)
|
|
189
|
+
|
|
190
|
+
with self._record_timing("write_ndt_values"):
|
|
191
|
+
with open(self.ndt_filename, "a", newline="", encoding="utf-8") as f:
|
|
192
|
+
writer = csv.writer(f, delimiter="\t")
|
|
193
|
+
writer.writerow(row_values)
|
|
194
|
+
|
|
195
|
+
def _existing_schema_row_values(
|
|
196
|
+
self,
|
|
197
|
+
columns: list[tuple[str, Any]],
|
|
198
|
+
) -> list[Any]:
|
|
199
|
+
if self._column_names is None:
|
|
200
|
+
return []
|
|
201
|
+
if self._column_index_by_name is None:
|
|
202
|
+
self._column_index_by_name = {
|
|
203
|
+
name: index for index, name in enumerate(self._column_names)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
row_values: list[Any] = [None] * len(self._column_names)
|
|
207
|
+
for name, value in columns:
|
|
208
|
+
index = self._column_index_by_name.get(name)
|
|
209
|
+
if index is not None:
|
|
210
|
+
row_values[index] = self._ndt_cell_value(value)
|
|
211
|
+
return row_values
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _ndt_cell_value(value: Any) -> Any:
|
|
215
|
+
if isinstance(value, SI):
|
|
216
|
+
return value.magnitude
|
|
217
|
+
return value
|
|
218
|
+
|
|
219
|
+
def _record_source_average_timings(
|
|
220
|
+
self,
|
|
221
|
+
source: SimulationSource,
|
|
222
|
+
prefix: str,
|
|
223
|
+
) -> None:
|
|
224
|
+
timings = self._active_save_timings
|
|
225
|
+
if timings is None:
|
|
226
|
+
return
|
|
227
|
+
source_timings = getattr(
|
|
228
|
+
source,
|
|
229
|
+
"last_subfield_average_timings_seconds",
|
|
230
|
+
None,
|
|
231
|
+
)
|
|
232
|
+
if not isinstance(source_timings, dict):
|
|
233
|
+
return
|
|
234
|
+
for name, seconds in cast(dict[object, object], source_timings).items():
|
|
235
|
+
if isinstance(seconds, (int, float)):
|
|
236
|
+
timing_name = f"average_detail:{prefix}:{name!s}"
|
|
237
|
+
timings[timing_name] = timings.get(timing_name, 0.0) + float(seconds)
|
|
238
|
+
|
|
239
|
+
def _record_source_spatial_save_timings(self, source: SimulationSource) -> None:
|
|
240
|
+
timings = self._active_save_timings
|
|
241
|
+
if timings is None:
|
|
242
|
+
return
|
|
243
|
+
source_timings = getattr(
|
|
244
|
+
source,
|
|
245
|
+
"last_spatial_save_timings_seconds",
|
|
246
|
+
None,
|
|
247
|
+
)
|
|
248
|
+
if not isinstance(source_timings, dict):
|
|
249
|
+
return
|
|
250
|
+
for name, seconds in cast(dict[object, object], source_timings).items():
|
|
251
|
+
if isinstance(seconds, (int, float)):
|
|
252
|
+
timing_name = f"spatial_detail:{name!s}"
|
|
253
|
+
timings[timing_name] = timings.get(timing_name, 0.0) + float(seconds)
|
|
254
|
+
|
|
255
|
+
def _record_source_maxangle_timings(
|
|
256
|
+
self,
|
|
257
|
+
source: SimulationSource,
|
|
258
|
+
prefix: str,
|
|
259
|
+
) -> None:
|
|
260
|
+
timings = self._active_save_timings
|
|
261
|
+
if timings is None:
|
|
262
|
+
return
|
|
263
|
+
source_timings = getattr(
|
|
264
|
+
source,
|
|
265
|
+
"last_maxangle_timings_seconds",
|
|
266
|
+
None,
|
|
267
|
+
)
|
|
268
|
+
if not isinstance(source_timings, dict):
|
|
269
|
+
return
|
|
270
|
+
for name, seconds in cast(dict[object, object], source_timings).items():
|
|
271
|
+
if isinstance(seconds, (int, float)):
|
|
272
|
+
timing_name = f"average_detail:{prefix}:{name!s}"
|
|
273
|
+
timings[timing_name] = timings.get(timing_name, 0.0) + float(seconds)
|