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
nmag/checkpoint.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Native HDF5 checkpoints for the supported Python 3 simulation state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
import h5py
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from anisotropy import anisotropy_signature_values
|
|
16
|
+
from si.physical import SI
|
|
17
|
+
from simulation.clock import SimulationClock
|
|
18
|
+
|
|
19
|
+
from .dynamics import ConvergenceTracker, IntegratorConfig
|
|
20
|
+
|
|
21
|
+
CHECKPOINT_FORMAT = "nmag-python-3-checkpoint"
|
|
22
|
+
CHECKPOINT_VERSION = 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class CheckpointContents:
|
|
27
|
+
magnetisation: np.ndarray
|
|
28
|
+
pinning: np.ndarray
|
|
29
|
+
external_field: np.ndarray
|
|
30
|
+
current_density: np.ndarray | None
|
|
31
|
+
metadata: dict[str, object]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class RestartRuntimeState:
|
|
36
|
+
clock: SimulationClock
|
|
37
|
+
integrator_config: IntegratorConfig
|
|
38
|
+
stopping_dm_dt: float
|
|
39
|
+
maximum_time_seconds: float
|
|
40
|
+
maximum_dm_dt: float | None
|
|
41
|
+
convergence: ConvergenceTracker
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _array_fingerprint(digest: Any, label: str, values: object, dtype: Any) -> None:
|
|
45
|
+
array = np.ascontiguousarray(np.asarray(values, dtype=dtype))
|
|
46
|
+
digest.update(label.encode("ascii"))
|
|
47
|
+
digest.update(str(array.shape).encode("ascii"))
|
|
48
|
+
digest.update(str(array.dtype).encode("ascii"))
|
|
49
|
+
digest.update(array.tobytes())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mesh_fingerprint(simulation: Any) -> str:
|
|
53
|
+
"""Return a stable hash of the loaded physical mesh and its topology."""
|
|
54
|
+
mesh = simulation._require_mesh()
|
|
55
|
+
simplices = np.asarray(mesh.simplices, dtype=np.int64)
|
|
56
|
+
regions_raw = mesh.regions
|
|
57
|
+
regions = np.asarray(
|
|
58
|
+
[1] * len(simplices) if regions_raw is None else regions_raw,
|
|
59
|
+
dtype=np.int64,
|
|
60
|
+
)
|
|
61
|
+
digest = hashlib.sha256()
|
|
62
|
+
_array_fingerprint(digest, "points", simulation._mesh_points(), np.dtype(np.float64))
|
|
63
|
+
_array_fingerprint(digest, "simplices", simplices, np.dtype(np.int64))
|
|
64
|
+
_array_fingerprint(digest, "regions", regions, np.dtype(np.int64))
|
|
65
|
+
return digest.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def material_fingerprint(simulation: Any) -> str:
|
|
69
|
+
"""Hash mesh-region material values that influence supported dynamics.
|
|
70
|
+
|
|
71
|
+
Saturation magnetisation is represented per simplex here rather than by
|
|
72
|
+
volume-averaged nodal recovery. The latter can differ by harmless last-bit
|
|
73
|
+
geometry-kernel roundoff after a run, which must not make an otherwise
|
|
74
|
+
compatible checkpoint unloadable.
|
|
75
|
+
"""
|
|
76
|
+
coefficients = simulation._nodal_material_coefficients()
|
|
77
|
+
mesh = simulation._require_mesh()
|
|
78
|
+
regions = np.asarray(mesh.regions or [1] * len(mesh.simplices), dtype=np.int64)
|
|
79
|
+
digest = hashlib.sha256()
|
|
80
|
+
for name, values in (
|
|
81
|
+
("simplex_ms", simulation._simplex_material_ms_values(regions)),
|
|
82
|
+
("volume_charge_scale", simulation._simplex_volume_charge_scales(regions)),
|
|
83
|
+
("exchange_prefactor", coefficients.exchange_prefactor),
|
|
84
|
+
("precession", coefficients.precession),
|
|
85
|
+
("damping", coefficients.damping),
|
|
86
|
+
("normalisation", coefficients.normalisation),
|
|
87
|
+
("stt_adiabatic", coefficients.stt_adiabatic),
|
|
88
|
+
("stt_nonadiabatic", coefficients.stt_nonadiabatic),
|
|
89
|
+
):
|
|
90
|
+
_array_fingerprint(digest, name, values, np.dtype(np.float64))
|
|
91
|
+
for region in np.unique(regions):
|
|
92
|
+
material = simulation._simplex_material(int(region))
|
|
93
|
+
signature = anisotropy_signature_values(
|
|
94
|
+
material.anisotropy,
|
|
95
|
+
material.anisotropy_order,
|
|
96
|
+
)
|
|
97
|
+
_array_fingerprint(
|
|
98
|
+
digest,
|
|
99
|
+
f"anisotropy:{int(region)}",
|
|
100
|
+
signature,
|
|
101
|
+
np.dtype(np.float64),
|
|
102
|
+
)
|
|
103
|
+
return digest.hexdigest()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _finite_array(name: str, values: object, shape: tuple[int, ...]) -> np.ndarray:
|
|
107
|
+
array = np.ascontiguousarray(np.asarray(values, dtype=np.float64))
|
|
108
|
+
if array.shape != shape:
|
|
109
|
+
raise ValueError(f"Checkpoint {name} must have shape {shape}, got {array.shape}.")
|
|
110
|
+
if not np.all(np.isfinite(array)):
|
|
111
|
+
raise ValueError(f"Checkpoint {name} values must be finite.")
|
|
112
|
+
return array
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _finite_number(name: str, value: object, *, positive: bool = False) -> float:
|
|
116
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
117
|
+
raise ValueError(f"Checkpoint {name} must be a finite number.")
|
|
118
|
+
number = float(value)
|
|
119
|
+
if not np.isfinite(number) or (positive and number <= 0.0):
|
|
120
|
+
qualifier = "positive finite" if positive else "finite"
|
|
121
|
+
raise ValueError(f"Checkpoint {name} must be a {qualifier} number.")
|
|
122
|
+
return number
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _snapshot(simulation: Any) -> CheckpointContents:
|
|
126
|
+
point_count = len(simulation._mesh_points())
|
|
127
|
+
if "m" not in simulation._fields:
|
|
128
|
+
raise RuntimeError("Magnetisation must be set before saving a restart checkpoint.")
|
|
129
|
+
|
|
130
|
+
magnetisation = _finite_array("magnetisation", simulation._fields["m"], (point_count, 3))
|
|
131
|
+
pinning = _finite_array(
|
|
132
|
+
"pinning",
|
|
133
|
+
simulation._fields.get("pin", np.ones(point_count, dtype=float)),
|
|
134
|
+
(point_count,),
|
|
135
|
+
)
|
|
136
|
+
external_field = _finite_array(
|
|
137
|
+
"external field",
|
|
138
|
+
simulation._fields.get("H_ext", np.zeros(3, dtype=float)),
|
|
139
|
+
(3,),
|
|
140
|
+
)
|
|
141
|
+
current_density = None
|
|
142
|
+
if "current_density" in simulation._fields:
|
|
143
|
+
current_density = _finite_array(
|
|
144
|
+
"current density",
|
|
145
|
+
simulation._fields["current_density"],
|
|
146
|
+
(point_count, 3),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
config = simulation.integrator_config
|
|
150
|
+
config.validate()
|
|
151
|
+
max_dm_dt = simulation.max_dm_dt
|
|
152
|
+
if max_dm_dt is not None:
|
|
153
|
+
max_dm_dt = _finite_number("maximum dm/dt", max_dm_dt)
|
|
154
|
+
dynamics_state: dict[str, object] = {
|
|
155
|
+
"relative_tolerance": config.relative_tolerance,
|
|
156
|
+
"absolute_tolerance": config.absolute_tolerance,
|
|
157
|
+
"maximum_step_seconds": config.maximum_step_seconds,
|
|
158
|
+
"exact_tstop": config.exact_tstop,
|
|
159
|
+
"stopping_dm_dt": _finite_number(
|
|
160
|
+
"stopping dm/dt", simulation.stopping_dm_dt, positive=True
|
|
161
|
+
),
|
|
162
|
+
"maximum_time_seconds": simulation.max_time_reached.in_units_of(SI(1.0, "s")),
|
|
163
|
+
"maximum_dm_dt": max_dm_dt,
|
|
164
|
+
"convergence": simulation.convergence.checkpoint_state(),
|
|
165
|
+
}
|
|
166
|
+
_finite_number("maximum time", dynamics_state["maximum_time_seconds"], positive=True)
|
|
167
|
+
metadata: dict[str, object] = {
|
|
168
|
+
"mesh_fingerprint": mesh_fingerprint(simulation),
|
|
169
|
+
"material_fingerprint": material_fingerprint(simulation),
|
|
170
|
+
"do_demag": bool(simulation.do_demag),
|
|
171
|
+
"clock": simulation.clock.checkpoint_state(),
|
|
172
|
+
"dynamics": dynamics_state,
|
|
173
|
+
}
|
|
174
|
+
return CheckpointContents(magnetisation, pinning, external_field, current_density, metadata)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def save_checkpoint(simulation: Any, destination: Path) -> Path:
|
|
178
|
+
"""Atomically persist a complete native checkpoint and return its path."""
|
|
179
|
+
contents = _snapshot(simulation)
|
|
180
|
+
destination = destination.expanduser()
|
|
181
|
+
temporary = destination.with_name(f".{destination.name}.tmp")
|
|
182
|
+
try:
|
|
183
|
+
with h5py.File(str(temporary), "w") as h5:
|
|
184
|
+
h5.attrs["format"] = CHECKPOINT_FORMAT
|
|
185
|
+
h5.attrs["version"] = CHECKPOINT_VERSION
|
|
186
|
+
h5.attrs["metadata"] = json.dumps(contents.metadata, allow_nan=False, sort_keys=True)
|
|
187
|
+
state = h5.create_group("state")
|
|
188
|
+
state.create_dataset("m", data=contents.magnetisation, dtype=np.float64)
|
|
189
|
+
state.create_dataset("pin", data=contents.pinning, dtype=np.float64)
|
|
190
|
+
state.create_dataset("H_ext", data=contents.external_field, dtype=np.float64)
|
|
191
|
+
if contents.current_density is not None:
|
|
192
|
+
state.create_dataset(
|
|
193
|
+
"current_density",
|
|
194
|
+
data=contents.current_density,
|
|
195
|
+
dtype=np.float64,
|
|
196
|
+
)
|
|
197
|
+
h5.flush()
|
|
198
|
+
with temporary.open("rb") as checkpoint_file:
|
|
199
|
+
os.fsync(checkpoint_file.fileno())
|
|
200
|
+
os.replace(temporary, destination)
|
|
201
|
+
except Exception:
|
|
202
|
+
temporary.unlink(missing_ok=True)
|
|
203
|
+
raise
|
|
204
|
+
return destination
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _attribute_text(value: object, name: str) -> str:
|
|
208
|
+
if isinstance(value, bytes):
|
|
209
|
+
return value.decode("utf-8")
|
|
210
|
+
if isinstance(value, str):
|
|
211
|
+
return value
|
|
212
|
+
raise ValueError(f"Checkpoint {name} attribute must be text.")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def read_checkpoint(filename: Path, point_count: int) -> CheckpointContents:
|
|
216
|
+
"""Read and fully validate native checkpoint data without mutating a simulation."""
|
|
217
|
+
try:
|
|
218
|
+
with h5py.File(str(filename), "r") as h5:
|
|
219
|
+
if _attribute_text(h5.attrs.get("format"), "format") != CHECKPOINT_FORMAT:
|
|
220
|
+
raise ValueError("File is not an nmag-python-3 native checkpoint.")
|
|
221
|
+
version = cast(object, h5.attrs.get("version"))
|
|
222
|
+
if isinstance(version, bool) or not isinstance(version, (int, np.integer)):
|
|
223
|
+
raise ValueError("Checkpoint version must be an integer.")
|
|
224
|
+
if str(cast(object, version)) != str(CHECKPOINT_VERSION):
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Unsupported checkpoint version {version}; expected {CHECKPOINT_VERSION}."
|
|
227
|
+
)
|
|
228
|
+
try:
|
|
229
|
+
decoded_metadata = json.loads(_attribute_text(h5.attrs.get("metadata"), "metadata"))
|
|
230
|
+
except json.JSONDecodeError as exc:
|
|
231
|
+
raise ValueError("Checkpoint metadata is not valid JSON.") from exc
|
|
232
|
+
if not isinstance(decoded_metadata, dict):
|
|
233
|
+
raise ValueError("Checkpoint metadata must be an object.")
|
|
234
|
+
metadata = cast(dict[str, object], decoded_metadata)
|
|
235
|
+
try:
|
|
236
|
+
state = h5["state"]
|
|
237
|
+
if not isinstance(state, h5py.Group):
|
|
238
|
+
raise ValueError("Checkpoint state must be an HDF5 group.")
|
|
239
|
+
magnetisation = _finite_array("magnetisation", state["m"], (point_count, 3))
|
|
240
|
+
pinning = _finite_array("pinning", state["pin"], (point_count,))
|
|
241
|
+
external_field = _finite_array("external field", state["H_ext"], (3,))
|
|
242
|
+
current_density = (
|
|
243
|
+
_finite_array("current density", state["current_density"], (point_count, 3))
|
|
244
|
+
if "current_density" in state
|
|
245
|
+
else None
|
|
246
|
+
)
|
|
247
|
+
except KeyError as exc:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"Checkpoint is missing required state dataset {exc.args[0]!r}."
|
|
250
|
+
) from exc
|
|
251
|
+
except OSError as exc:
|
|
252
|
+
raise ValueError(f"Unable to read checkpoint {filename}: {exc}") from exc
|
|
253
|
+
return CheckpointContents(magnetisation, pinning, external_field, current_density, metadata)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def validate_mesh(simulation: Any, contents: CheckpointContents) -> None:
|
|
257
|
+
saved = contents.metadata.get("mesh_fingerprint")
|
|
258
|
+
if not isinstance(saved, str):
|
|
259
|
+
raise ValueError("Checkpoint metadata is missing a mesh fingerprint.")
|
|
260
|
+
if saved != mesh_fingerprint(simulation):
|
|
261
|
+
raise ValueError("Checkpoint mesh does not match the loaded simulation mesh.")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def runtime_state(simulation: Any, contents: CheckpointContents) -> RestartRuntimeState:
|
|
265
|
+
"""Validate full-restart compatibility and parse all non-array runtime state."""
|
|
266
|
+
validate_mesh(simulation, contents)
|
|
267
|
+
saved_material = contents.metadata.get("material_fingerprint")
|
|
268
|
+
if not isinstance(saved_material, str):
|
|
269
|
+
raise ValueError("Checkpoint metadata is missing a material fingerprint.")
|
|
270
|
+
if saved_material != material_fingerprint(simulation):
|
|
271
|
+
raise ValueError("Checkpoint materials do not match the loaded simulation.")
|
|
272
|
+
saved_demag = contents.metadata.get("do_demag")
|
|
273
|
+
if not isinstance(saved_demag, bool) or saved_demag != simulation.do_demag:
|
|
274
|
+
raise ValueError("Checkpoint demagnetization mode does not match the loaded simulation.")
|
|
275
|
+
|
|
276
|
+
clock_state = contents.metadata.get("clock")
|
|
277
|
+
dynamics = contents.metadata.get("dynamics")
|
|
278
|
+
if not isinstance(clock_state, dict) or not isinstance(dynamics, dict):
|
|
279
|
+
raise ValueError("Checkpoint metadata is missing clock or dynamics state.")
|
|
280
|
+
parsed_clock_state = cast(dict[str, object], clock_state)
|
|
281
|
+
parsed_dynamics = cast(dict[str, object], dynamics)
|
|
282
|
+
clock = SimulationClock.from_checkpoint_state(parsed_clock_state)
|
|
283
|
+
try:
|
|
284
|
+
exact_tstop = parsed_dynamics["exact_tstop"]
|
|
285
|
+
if not isinstance(exact_tstop, bool):
|
|
286
|
+
raise ValueError("Checkpoint exact_tstop must be a boolean.")
|
|
287
|
+
config = IntegratorConfig(
|
|
288
|
+
relative_tolerance=_finite_number(
|
|
289
|
+
"relative tolerance", parsed_dynamics["relative_tolerance"], positive=True
|
|
290
|
+
),
|
|
291
|
+
absolute_tolerance=_finite_number(
|
|
292
|
+
"absolute tolerance", parsed_dynamics["absolute_tolerance"], positive=True
|
|
293
|
+
),
|
|
294
|
+
maximum_step_seconds=_finite_number(
|
|
295
|
+
"maximum step", parsed_dynamics["maximum_step_seconds"], positive=True
|
|
296
|
+
),
|
|
297
|
+
exact_tstop=exact_tstop,
|
|
298
|
+
)
|
|
299
|
+
config.validate()
|
|
300
|
+
stopping_dm_dt = _finite_number(
|
|
301
|
+
"stopping dm/dt", parsed_dynamics["stopping_dm_dt"], positive=True
|
|
302
|
+
)
|
|
303
|
+
maximum_time = _finite_number(
|
|
304
|
+
"maximum time", parsed_dynamics["maximum_time_seconds"], positive=True
|
|
305
|
+
)
|
|
306
|
+
maximum_dm_dt_raw = parsed_dynamics["maximum_dm_dt"]
|
|
307
|
+
maximum_dm_dt = (
|
|
308
|
+
None
|
|
309
|
+
if maximum_dm_dt_raw is None
|
|
310
|
+
else _finite_number("maximum dm/dt", maximum_dm_dt_raw)
|
|
311
|
+
)
|
|
312
|
+
convergence_state = parsed_dynamics["convergence"]
|
|
313
|
+
except KeyError as exc:
|
|
314
|
+
raise ValueError(f"Checkpoint dynamics state is missing {exc.args[0]!r}.") from exc
|
|
315
|
+
if not isinstance(convergence_state, dict):
|
|
316
|
+
raise ValueError("Checkpoint convergence state must be an object.")
|
|
317
|
+
convergence = ConvergenceTracker.from_checkpoint_state(
|
|
318
|
+
cast(dict[str, object], convergence_state)
|
|
319
|
+
)
|
|
320
|
+
return RestartRuntimeState(
|
|
321
|
+
clock,
|
|
322
|
+
config,
|
|
323
|
+
stopping_dm_dt,
|
|
324
|
+
maximum_time,
|
|
325
|
+
maximum_dm_dt,
|
|
326
|
+
convergence,
|
|
327
|
+
)
|
nmag/config.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Typed runtime configuration for supported Nmag Python workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from numbers import Real
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import MappingProxyType
|
|
13
|
+
from typing import Literal, cast
|
|
14
|
+
|
|
15
|
+
AcceleratorMode = Literal["auto", "off", "rust"]
|
|
16
|
+
OutputPolicy = Literal["error", "replace", "append"]
|
|
17
|
+
IntegratorBackend = Literal["scipy", "diffsol"]
|
|
18
|
+
DemagBemStorage = Literal["auto", "dense", "hierarchical", "matrix-free"]
|
|
19
|
+
|
|
20
|
+
ACCELERATOR_ENV = "NMAG_ACCELERATOR"
|
|
21
|
+
DEMAG_BEM_STORAGE_ENV = "NMAG_DEMAG_BEM_STORAGE_BACKEND"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class HierarchicalBemConfig:
|
|
26
|
+
"""Control accuracy and resources for compressed Lindholm BEM.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
relative_tolerance: Requested relative compression/certification error.
|
|
30
|
+
admissibility_eta: Geometric separation threshold for low-rank blocks.
|
|
31
|
+
leaf_size: Maximum cluster leaf size.
|
|
32
|
+
max_rank: Maximum accepted low-rank block rank.
|
|
33
|
+
validation_vectors: Random vectors used to certify completed action.
|
|
34
|
+
validation_rows: Exact rows sampled during certification.
|
|
35
|
+
memory_fraction: Fraction of currently available memory allowed for the
|
|
36
|
+
completed hierarchy.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
relative_tolerance: float = 1.0e-6
|
|
40
|
+
admissibility_eta: float = 2.0
|
|
41
|
+
leaf_size: int = 32
|
|
42
|
+
max_rank: int = 128
|
|
43
|
+
validation_vectors: int = 4
|
|
44
|
+
validation_rows: int = 64
|
|
45
|
+
memory_fraction: float = 0.20
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
for name in ("relative_tolerance", "admissibility_eta", "memory_fraction"):
|
|
49
|
+
raw_value = getattr(self, name)
|
|
50
|
+
if type(raw_value) is bool or not isinstance(raw_value, Real):
|
|
51
|
+
raise TypeError(f"{name} must be a real number.")
|
|
52
|
+
value = float(raw_value)
|
|
53
|
+
if not math.isfinite(value) or value <= 0.0:
|
|
54
|
+
raise ValueError(f"{name} must be finite and positive.")
|
|
55
|
+
object.__setattr__(self, name, value)
|
|
56
|
+
if self.memory_fraction > 1.0:
|
|
57
|
+
raise ValueError("memory_fraction must not exceed 1.0.")
|
|
58
|
+
for name in ("leaf_size", "max_rank", "validation_vectors", "validation_rows"):
|
|
59
|
+
value = getattr(self, name)
|
|
60
|
+
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
|
61
|
+
raise ValueError(f"{name} must be a positive integer.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RustKernel(str, Enum):
|
|
65
|
+
"""Rust-accelerated calculation families accepted as override keys."""
|
|
66
|
+
|
|
67
|
+
LINDHOLM_BEM = "lindholm_bem"
|
|
68
|
+
PROBE_GEOMETRY = "probe_geometry"
|
|
69
|
+
FEM_GEOMETRY = "fem_geometry"
|
|
70
|
+
BOUNDARY_FACES = "boundary_faces"
|
|
71
|
+
FEM_ASSEMBLY = "fem_assembly"
|
|
72
|
+
NODAL_RECOVERY = "nodal_recovery"
|
|
73
|
+
CELL_AVERAGE = "cell_average"
|
|
74
|
+
LLG = "llg"
|
|
75
|
+
MAXANGLE = "maxangle"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_mode(value: object, *, field_name: str) -> AcceleratorMode:
|
|
79
|
+
if not isinstance(value, str):
|
|
80
|
+
raise TypeError(f"{field_name} must be a string mode.")
|
|
81
|
+
if value not in {"auto", "off", "rust"}:
|
|
82
|
+
raise ValueError(f"{field_name} must be one of 'auto', 'off', or 'rust', got {value!r}.")
|
|
83
|
+
return cast(AcceleratorMode, value)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _empty_overrides() -> Mapping[RustKernel, AcceleratorMode]:
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class NmagConfig:
|
|
92
|
+
"""Define immutable output, acceleration, storage, and integrator policy.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
default_name: Simulation name used when ``Simulation(name=...)`` is
|
|
96
|
+
omitted.
|
|
97
|
+
output_directory: Directory for NDT, HDF5, and default checkpoint files.
|
|
98
|
+
The directory must exist before output is written.
|
|
99
|
+
output_policy: ``"error"` protects old output, ``"replace"`` starts it
|
|
100
|
+
again, and ``"append"`` validates and extends the NDT schema.
|
|
101
|
+
accelerator: Global ``"auto"``, ``"off"``, or strict ``"rust"`` mode.
|
|
102
|
+
accelerator_overrides: Per-:class:`RustKernel` mode overrides.
|
|
103
|
+
integrator_backend: ``"scipy"`` or experimental ``"diffsol"``.
|
|
104
|
+
demag_bem_storage: ``"auto"``, ``"dense"``, ``"hierarchical"``, or
|
|
105
|
+
``"matrix-free"`` boundary-operator storage.
|
|
106
|
+
hierarchical_bem: Accuracy and resource settings for hierarchy builds.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
default_name: str = "nmag_simulation"
|
|
110
|
+
output_directory: Path = Path(".")
|
|
111
|
+
output_policy: OutputPolicy = "error"
|
|
112
|
+
accelerator: AcceleratorMode = "auto"
|
|
113
|
+
accelerator_overrides: Mapping[RustKernel, AcceleratorMode] = field(
|
|
114
|
+
default_factory=_empty_overrides
|
|
115
|
+
)
|
|
116
|
+
integrator_backend: IntegratorBackend = "scipy"
|
|
117
|
+
demag_bem_storage: DemagBemStorage = "auto"
|
|
118
|
+
hierarchical_bem: HierarchicalBemConfig = field(default_factory=HierarchicalBemConfig)
|
|
119
|
+
|
|
120
|
+
def __post_init__(self) -> None:
|
|
121
|
+
if not self.default_name:
|
|
122
|
+
raise ValueError("default_name must not be empty.")
|
|
123
|
+
if self.output_policy not in {"error", "replace", "append"}:
|
|
124
|
+
raise ValueError("output_policy must be 'error', 'replace', or 'append'.")
|
|
125
|
+
if self.integrator_backend not in {"scipy", "diffsol"}:
|
|
126
|
+
raise ValueError("integrator_backend must be 'scipy' or 'diffsol'.")
|
|
127
|
+
if self.demag_bem_storage not in {"auto", "dense", "hierarchical", "matrix-free"}:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"demag_bem_storage must be 'auto', 'dense', 'hierarchical', or 'matrix-free'."
|
|
130
|
+
)
|
|
131
|
+
if not isinstance(cast(object, self.hierarchical_bem), HierarchicalBemConfig):
|
|
132
|
+
raise TypeError("hierarchical_bem must be a HierarchicalBemConfig instance.")
|
|
133
|
+
object.__setattr__(self, "output_directory", Path(self.output_directory).expanduser())
|
|
134
|
+
object.__setattr__(
|
|
135
|
+
self,
|
|
136
|
+
"accelerator",
|
|
137
|
+
_validate_mode(self.accelerator, field_name="accelerator"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
validated_overrides: dict[RustKernel, AcceleratorMode] = {}
|
|
141
|
+
raw_overrides = cast(Mapping[object, object], self.accelerator_overrides)
|
|
142
|
+
for raw_kernel, raw_mode in raw_overrides.items():
|
|
143
|
+
if not isinstance(raw_kernel, RustKernel):
|
|
144
|
+
raise TypeError("accelerator_overrides keys must be RustKernel values.")
|
|
145
|
+
validated_overrides[raw_kernel] = _validate_mode(
|
|
146
|
+
raw_mode,
|
|
147
|
+
field_name=f"accelerator_overrides[{raw_kernel.value!r}]",
|
|
148
|
+
)
|
|
149
|
+
object.__setattr__(
|
|
150
|
+
self,
|
|
151
|
+
"accelerator_overrides",
|
|
152
|
+
MappingProxyType(validated_overrides),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_environment(cls) -> NmagConfig:
|
|
157
|
+
"""Build default configuration from supported process-level selectors.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Configuration using ``NMAG_ACCELERATOR`` and
|
|
161
|
+
``NMAG_DEMAG_BEM_STORAGE_BACKEND`` when set.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
accelerator = os.environ.get(ACCELERATOR_ENV, "auto").strip().lower()
|
|
165
|
+
bem_storage = os.environ.get(DEMAG_BEM_STORAGE_ENV, "auto").strip().lower()
|
|
166
|
+
return cls(
|
|
167
|
+
accelerator=_validate_mode(accelerator, field_name=ACCELERATOR_ENV),
|
|
168
|
+
demag_bem_storage=cast(DemagBemStorage, bem_storage),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def accelerator_mode_for(self, kernel: RustKernel) -> AcceleratorMode:
|
|
172
|
+
"""Return a kernel override, falling back to the global accelerator mode."""
|
|
173
|
+
|
|
174
|
+
return self.accelerator_overrides.get(kernel, self.accelerator)
|
nmag/demag/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from . import geometry as _geometry
|
|
6
|
+
from . import lindholm as _lindholm
|
|
7
|
+
from . import lindholm_fast as _fast
|
|
8
|
+
from . import linear as _linear
|
|
9
|
+
|
|
10
|
+
_HELPER_MODULES = (_geometry, _lindholm, _fast, _linear)
|
|
11
|
+
|
|
12
|
+
# Preserve the established patch points used by downstream tests and tooling.
|
|
13
|
+
np = _linear.np
|
|
14
|
+
_scipy_linalg = _linear._scipy_linalg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _solve_gauge_fixed(*args: Any, **kwargs: Any) -> Any:
|
|
18
|
+
"""Forward to the solver while preserving facade-level monkeypatches."""
|
|
19
|
+
|
|
20
|
+
_linear._scipy_linalg = _scipy_linalg
|
|
21
|
+
return _linear._solve_gauge_fixed(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _solve_linear_system(*args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
"""Forward to the solver while preserving facade-level monkeypatches."""
|
|
26
|
+
|
|
27
|
+
_linear._scipy_linalg = _scipy_linalg
|
|
28
|
+
return _linear._solve_linear_system(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str) -> Any:
|
|
32
|
+
"""Resolve compatibility exports from the focused demag modules."""
|
|
33
|
+
|
|
34
|
+
for module in _HELPER_MODULES:
|
|
35
|
+
try:
|
|
36
|
+
return getattr(module, name)
|
|
37
|
+
except AttributeError:
|
|
38
|
+
continue
|
|
39
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .lindholm_fast import _apply_lindholm_bem_matrix_free_fast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class BemOperatorStats:
|
|
13
|
+
"""Describe construction and storage of the active BEM operator.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
requested_backend: Storage mode requested by configuration.
|
|
17
|
+
effective_backend: Storage implementation actually constructed.
|
|
18
|
+
fallback_reason: Explanation when construction changed modes.
|
|
19
|
+
boundary_nodes: Number of boundary degrees of freedom.
|
|
20
|
+
boundary_faces: Number of oriented surface triangles.
|
|
21
|
+
setup_seconds: Operator construction time.
|
|
22
|
+
storage_bytes: Bytes retained by the effective operator.
|
|
23
|
+
dense_equivalent_bytes: Bytes required by an equivalent dense matrix.
|
|
24
|
+
dense_blocks: Exact blocks in a hierarchy.
|
|
25
|
+
low_rank_blocks: Compressed blocks in a hierarchy.
|
|
26
|
+
maximum_rank: Largest compressed-block rank.
|
|
27
|
+
mean_rank: Mean compressed-block rank.
|
|
28
|
+
sampled_relative_error: Certification error measured during setup.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
requested_backend: str
|
|
32
|
+
effective_backend: str
|
|
33
|
+
fallback_reason: str | None
|
|
34
|
+
boundary_nodes: int
|
|
35
|
+
boundary_faces: int
|
|
36
|
+
setup_seconds: float
|
|
37
|
+
storage_bytes: int
|
|
38
|
+
dense_equivalent_bytes: int
|
|
39
|
+
dense_blocks: int = 0
|
|
40
|
+
low_rank_blocks: int = 0
|
|
41
|
+
maximum_rank: int = 0
|
|
42
|
+
mean_rank: float = 0.0
|
|
43
|
+
sampled_relative_error: float = 0.0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def compression_ratio(self) -> float:
|
|
47
|
+
if self.dense_equivalent_bytes == 0:
|
|
48
|
+
return 0.0
|
|
49
|
+
return self.storage_bytes / self.dense_equivalent_bytes
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class MatrixFreeLindholmBemOperator:
|
|
54
|
+
"""Exact Lindholm BEM action without storing the dense boundary matrix."""
|
|
55
|
+
|
|
56
|
+
points: np.ndarray
|
|
57
|
+
simplices: np.ndarray
|
|
58
|
+
boundary_faces: np.ndarray
|
|
59
|
+
face_points: np.ndarray
|
|
60
|
+
zeta_vectors: np.ndarray
|
|
61
|
+
eta_vectors: np.ndarray
|
|
62
|
+
edge_lengths: np.ndarray
|
|
63
|
+
corner_cosines: np.ndarray
|
|
64
|
+
denominator_factors: np.ndarray
|
|
65
|
+
boundary_nodes: np.ndarray
|
|
66
|
+
local_index_by_point: np.ndarray
|
|
67
|
+
solid_angles: np.ndarray
|
|
68
|
+
rust_accelerator: Any | None = None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def shape(self) -> tuple[int, int]:
|
|
72
|
+
size = len(self.boundary_nodes)
|
|
73
|
+
return size, size
|
|
74
|
+
|
|
75
|
+
def __matmul__(self, values: np.ndarray) -> np.ndarray:
|
|
76
|
+
vector = np.asarray(values, dtype=np.float64)
|
|
77
|
+
expected = (len(self.boundary_nodes),)
|
|
78
|
+
if vector.shape != expected:
|
|
79
|
+
raise ValueError(f"BEM input must have shape {expected}, got {vector.shape}.")
|
|
80
|
+
if self.rust_accelerator is not None:
|
|
81
|
+
return np.asarray(
|
|
82
|
+
self.rust_accelerator.apply_lindholm_bem_matrix_free(
|
|
83
|
+
self.points,
|
|
84
|
+
self.simplices,
|
|
85
|
+
self.boundary_faces,
|
|
86
|
+
self.boundary_nodes,
|
|
87
|
+
self.local_index_by_point,
|
|
88
|
+
vector,
|
|
89
|
+
),
|
|
90
|
+
dtype=float,
|
|
91
|
+
)
|
|
92
|
+
return np.asarray(
|
|
93
|
+
_apply_lindholm_bem_matrix_free_fast(
|
|
94
|
+
self.points,
|
|
95
|
+
self.boundary_faces,
|
|
96
|
+
self.face_points,
|
|
97
|
+
self.zeta_vectors,
|
|
98
|
+
self.eta_vectors,
|
|
99
|
+
self.edge_lengths,
|
|
100
|
+
self.corner_cosines,
|
|
101
|
+
self.denominator_factors,
|
|
102
|
+
self.boundary_nodes,
|
|
103
|
+
self.local_index_by_point,
|
|
104
|
+
self.solid_angles,
|
|
105
|
+
vector,
|
|
106
|
+
),
|
|
107
|
+
dtype=float,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def storage_bytes(self) -> int:
|
|
112
|
+
arrays = (
|
|
113
|
+
self.points,
|
|
114
|
+
self.simplices,
|
|
115
|
+
self.boundary_faces,
|
|
116
|
+
self.face_points,
|
|
117
|
+
self.zeta_vectors,
|
|
118
|
+
self.eta_vectors,
|
|
119
|
+
self.edge_lengths,
|
|
120
|
+
self.corner_cosines,
|
|
121
|
+
self.denominator_factors,
|
|
122
|
+
self.boundary_nodes,
|
|
123
|
+
self.local_index_by_point,
|
|
124
|
+
self.solid_angles,
|
|
125
|
+
)
|
|
126
|
+
return sum(int(array.nbytes) for array in arrays)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True, slots=True)
|
|
130
|
+
class HierarchicalLindholmBemOperator:
|
|
131
|
+
"""Certified compressed Lindholm operator owned by the Rust extension."""
|
|
132
|
+
|
|
133
|
+
rust_operator: Any
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def shape(self) -> tuple[int, int]:
|
|
137
|
+
size = int(self.rust_operator.size)
|
|
138
|
+
return size, size
|
|
139
|
+
|
|
140
|
+
def __matmul__(self, values: np.ndarray) -> np.ndarray:
|
|
141
|
+
vector = np.asarray(values, dtype=np.float64)
|
|
142
|
+
expected = (self.shape[1],)
|
|
143
|
+
if vector.shape != expected:
|
|
144
|
+
raise ValueError(f"BEM input must have shape {expected}, got {vector.shape}.")
|
|
145
|
+
return np.asarray(self.rust_operator.matvec(vector), dtype=np.float64)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def storage_bytes(self) -> int:
|
|
149
|
+
return int(self.rust_operator.storage_bytes)
|