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,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from math import isfinite
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
FloatArray = NDArray[np.float64]
|
|
12
|
+
|
|
13
|
+
DEFAULT_STOPPING_DM_DT = float(np.deg2rad(1.0) * 1.0e9)
|
|
14
|
+
DOP853_EXCHANGE_RESOLUTION_RADIUS = 3.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class NodalMaterialCoefficients:
|
|
19
|
+
exchange_prefactor: FloatArray
|
|
20
|
+
precession: FloatArray
|
|
21
|
+
damping: FloatArray
|
|
22
|
+
normalisation: FloatArray
|
|
23
|
+
stt_adiabatic: FloatArray
|
|
24
|
+
stt_nonadiabatic: FloatArray
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class IntegratorConfig:
|
|
29
|
+
"""Snapshot of effective public DOP853 controls.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
relative_tolerance: Relative local-error tolerance.
|
|
33
|
+
absolute_tolerance: Absolute local-error tolerance.
|
|
34
|
+
maximum_step_seconds: Configured upper step bound in seconds.
|
|
35
|
+
exact_tstop: Whether fixed-time requests normally stop exactly.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
relative_tolerance: float = 1.0e-6
|
|
39
|
+
absolute_tolerance: float = 1.0e-6
|
|
40
|
+
maximum_step_seconds: float = 1.0e-12
|
|
41
|
+
exact_tstop: bool = True
|
|
42
|
+
|
|
43
|
+
def validate(self) -> None:
|
|
44
|
+
for name, value in (
|
|
45
|
+
("relative_tolerance", self.relative_tolerance),
|
|
46
|
+
("absolute_tolerance", self.absolute_tolerance),
|
|
47
|
+
("maximum_step_seconds", self.maximum_step_seconds),
|
|
48
|
+
):
|
|
49
|
+
if not np.isfinite(value) or value <= 0.0:
|
|
50
|
+
raise ValueError(f"{name} must be a positive finite number, got {value!r}.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class IntegratorStats:
|
|
55
|
+
"""Measurements from the most recent integration operation.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
accepted_steps: Accepted adaptive steps.
|
|
59
|
+
rhs_evaluations: Landau-Lifshitz-Gilbert right-hand-side evaluations.
|
|
60
|
+
rejected_steps: Rejected steps when the backend reports them.
|
|
61
|
+
failed: Whether the operation raised a numerical failure.
|
|
62
|
+
status: Backend status at completion.
|
|
63
|
+
last_step_seconds: Size of the final accepted step.
|
|
64
|
+
simulated_seconds: Physical time advanced by the operation.
|
|
65
|
+
wall_seconds: Elapsed wall-clock time.
|
|
66
|
+
jacobian_vector_evaluations: Optional implicit-backend measurement.
|
|
67
|
+
nonlinear_iterations: Optional implicit-backend Newton iterations.
|
|
68
|
+
nonlinear_failures: Optional implicit-backend failure count.
|
|
69
|
+
linear_solver_setups: Optional implicit-backend setup count.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
accepted_steps: int = 0
|
|
73
|
+
rhs_evaluations: int = 0
|
|
74
|
+
rejected_steps: int | None = None
|
|
75
|
+
failed: bool = False
|
|
76
|
+
status: str = "not_started"
|
|
77
|
+
last_step_seconds: float = 0.0
|
|
78
|
+
simulated_seconds: float = 0.0
|
|
79
|
+
wall_seconds: float = 0.0
|
|
80
|
+
jacobian_vector_evaluations: int | None = None
|
|
81
|
+
nonlinear_iterations: int | None = None
|
|
82
|
+
nonlinear_failures: int | None = None
|
|
83
|
+
linear_solver_setups: int | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class ConvergenceTracker:
|
|
88
|
+
required_checks: int = 2
|
|
89
|
+
statistics_window: int = 5
|
|
90
|
+
_remaining_checks: int = field(init=False)
|
|
91
|
+
_history: deque[tuple[int, float, float, float | None]] = field(
|
|
92
|
+
default_factory=lambda: deque(maxlen=50),
|
|
93
|
+
init=False,
|
|
94
|
+
)
|
|
95
|
+
_previous_max_dm_dt: float | None = field(default=None, init=False)
|
|
96
|
+
_window_changes: list[float] = field(default_factory=lambda: list[float](), init=False)
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
if self.required_checks < 1:
|
|
100
|
+
raise ValueError("required_checks must be at least one.")
|
|
101
|
+
if self.statistics_window < 1:
|
|
102
|
+
raise ValueError("statistics_window must be at least one.")
|
|
103
|
+
self._remaining_checks = self.required_checks
|
|
104
|
+
|
|
105
|
+
def reset(self) -> None:
|
|
106
|
+
self._remaining_checks = self.required_checks
|
|
107
|
+
self._previous_max_dm_dt = None
|
|
108
|
+
self._window_changes.clear()
|
|
109
|
+
|
|
110
|
+
def check(self, step: int, max_dm_dt: float, stopping_dm_dt: float) -> bool:
|
|
111
|
+
if max_dm_dt < stopping_dm_dt:
|
|
112
|
+
self._remaining_checks -= 1
|
|
113
|
+
if self._remaining_checks == 0:
|
|
114
|
+
self.reset()
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
self._remaining_checks = self.required_checks
|
|
119
|
+
quality = self._quality(max_dm_dt)
|
|
120
|
+
self._history.append((step, max_dm_dt, stopping_dm_dt, quality))
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def _quality(self, max_dm_dt: float) -> float | None:
|
|
124
|
+
if self._previous_max_dm_dt is None:
|
|
125
|
+
self._previous_max_dm_dt = max_dm_dt
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
self._window_changes.append(max_dm_dt - self._previous_max_dm_dt)
|
|
129
|
+
self._previous_max_dm_dt = max_dm_dt
|
|
130
|
+
if len(self._window_changes) < self.statistics_window:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
signed = abs(sum(self._window_changes))
|
|
134
|
+
absolute = sum(abs(change) for change in self._window_changes)
|
|
135
|
+
self._window_changes.clear()
|
|
136
|
+
return signed / absolute if absolute > 0.0 else None
|
|
137
|
+
|
|
138
|
+
def get_log(self) -> str:
|
|
139
|
+
lines = ["# Convergence log", "# step, max dm/dt, stopping dm/dt, quality"]
|
|
140
|
+
lines.extend(
|
|
141
|
+
f"{step} {max_dm_dt} {stopping_dm_dt} {quality}"
|
|
142
|
+
for step, max_dm_dt, stopping_dm_dt, quality in self._history
|
|
143
|
+
)
|
|
144
|
+
return "\n".join(lines) + "\n"
|
|
145
|
+
|
|
146
|
+
def checkpoint_state(self) -> dict[str, object]:
|
|
147
|
+
"""Return the convergence cadence state without exposing mutable internals."""
|
|
148
|
+
return {
|
|
149
|
+
"required_checks": self.required_checks,
|
|
150
|
+
"statistics_window": self.statistics_window,
|
|
151
|
+
"remaining_checks": self._remaining_checks,
|
|
152
|
+
"history": [list(entry) for entry in self._history],
|
|
153
|
+
"previous_max_dm_dt": self._previous_max_dm_dt,
|
|
154
|
+
"window_changes": list(self._window_changes),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_checkpoint_state(cls, state: dict[str, object]) -> ConvergenceTracker:
|
|
159
|
+
cls._validate_checkpoint_keys(state)
|
|
160
|
+
required_checks, statistics_window, remaining_checks = cls._checkpoint_counters(state)
|
|
161
|
+
tracker = cls(
|
|
162
|
+
required_checks=required_checks,
|
|
163
|
+
statistics_window=statistics_window,
|
|
164
|
+
)
|
|
165
|
+
if not 1 <= remaining_checks <= tracker.required_checks:
|
|
166
|
+
raise ValueError("Checkpoint remaining convergence checks are invalid.")
|
|
167
|
+
tracker._remaining_checks = remaining_checks
|
|
168
|
+
tracker._history = cls._checkpoint_history(state["history"])
|
|
169
|
+
tracker._previous_max_dm_dt = cls._optional_finite_checkpoint_value(
|
|
170
|
+
state["previous_max_dm_dt"],
|
|
171
|
+
"Checkpoint previous convergence rate must be finite or null.",
|
|
172
|
+
)
|
|
173
|
+
tracker._window_changes = cls._finite_checkpoint_values(
|
|
174
|
+
state["window_changes"],
|
|
175
|
+
"Checkpoint convergence window changes must be finite numbers.",
|
|
176
|
+
)
|
|
177
|
+
return tracker
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _validate_checkpoint_keys(state: dict[str, object]) -> None:
|
|
181
|
+
required = {
|
|
182
|
+
"required_checks",
|
|
183
|
+
"statistics_window",
|
|
184
|
+
"remaining_checks",
|
|
185
|
+
"history",
|
|
186
|
+
"previous_max_dm_dt",
|
|
187
|
+
"window_changes",
|
|
188
|
+
}
|
|
189
|
+
missing = sorted(required.difference(state))
|
|
190
|
+
if missing:
|
|
191
|
+
raise ValueError(f"Checkpoint convergence state is missing {', '.join(missing)}.")
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _checkpoint_counters(state: dict[str, object]) -> tuple[int, int, int]:
|
|
195
|
+
values = tuple(
|
|
196
|
+
state[name] for name in ("required_checks", "statistics_window", "remaining_checks")
|
|
197
|
+
)
|
|
198
|
+
if any(isinstance(value, bool) or not isinstance(value, int) for value in values):
|
|
199
|
+
raise ValueError("Checkpoint convergence counters must be integers.")
|
|
200
|
+
return cast(tuple[int, int, int], values)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def _checkpoint_history(cls, value: object) -> deque[tuple[int, float, float, float | None]]:
|
|
204
|
+
if not isinstance(value, list):
|
|
205
|
+
raise ValueError("Checkpoint convergence history must be a list.")
|
|
206
|
+
history: deque[tuple[int, float, float, float | None]] = deque(maxlen=50)
|
|
207
|
+
for raw_entry in cast(list[object], value):
|
|
208
|
+
history.append(cls._checkpoint_history_entry(raw_entry))
|
|
209
|
+
return history
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def _checkpoint_history_entry(cls, value: object) -> tuple[int, float, float, float | None]:
|
|
213
|
+
if not isinstance(value, list):
|
|
214
|
+
raise ValueError("Checkpoint convergence history entries must have four values.")
|
|
215
|
+
entry = cast(list[object], value)
|
|
216
|
+
if len(entry) != 4:
|
|
217
|
+
raise ValueError("Checkpoint convergence history entries must have four values.")
|
|
218
|
+
step, maximum, stopping, quality = entry
|
|
219
|
+
if isinstance(step, bool) or not isinstance(step, int):
|
|
220
|
+
raise ValueError("Checkpoint convergence history steps must be integers.")
|
|
221
|
+
rates = cls._finite_checkpoint_values(
|
|
222
|
+
[maximum, stopping],
|
|
223
|
+
"Checkpoint convergence history rates must be finite.",
|
|
224
|
+
)
|
|
225
|
+
return (
|
|
226
|
+
step,
|
|
227
|
+
rates[0],
|
|
228
|
+
rates[1],
|
|
229
|
+
cls._optional_finite_checkpoint_value(
|
|
230
|
+
quality,
|
|
231
|
+
"Checkpoint convergence history quality must be finite or null.",
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _optional_finite_checkpoint_value(value: object, message: str) -> float | None:
|
|
237
|
+
if value is None:
|
|
238
|
+
return None
|
|
239
|
+
values = ConvergenceTracker._finite_checkpoint_values([value], message)
|
|
240
|
+
return values[0]
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _finite_checkpoint_values(value: object, message: str) -> list[float]:
|
|
244
|
+
if not isinstance(value, list):
|
|
245
|
+
raise ValueError(message)
|
|
246
|
+
parsed: list[float] = []
|
|
247
|
+
for raw_value in cast(list[object], value):
|
|
248
|
+
if (
|
|
249
|
+
isinstance(raw_value, bool)
|
|
250
|
+
or not isinstance(raw_value, (int, float))
|
|
251
|
+
or not isfinite(float(raw_value))
|
|
252
|
+
):
|
|
253
|
+
raise ValueError(message)
|
|
254
|
+
parsed.append(float(raw_value))
|
|
255
|
+
return parsed
|
nmag/output.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Output-file lifecycle helpers shared by simulation implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import OutputPolicy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def prepare_output_files(paths: Iterable[Path], policy: OutputPolicy) -> None:
|
|
12
|
+
"""Create parents and apply an explicit output lifecycle policy."""
|
|
13
|
+
|
|
14
|
+
destinations = tuple(paths)
|
|
15
|
+
for destination in destinations:
|
|
16
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
existing = tuple(destination for destination in destinations if destination.exists())
|
|
19
|
+
if policy == "error" and existing:
|
|
20
|
+
rendered = ", ".join(str(destination) for destination in existing)
|
|
21
|
+
raise FileExistsError(
|
|
22
|
+
f"Output file(s) already exist: {rendered}. "
|
|
23
|
+
"Use NmagConfig(output_policy='replace' or 'append') explicitly."
|
|
24
|
+
)
|
|
25
|
+
if policy == "replace":
|
|
26
|
+
for destination in existing:
|
|
27
|
+
destination.unlink()
|
nmag/parallel.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .backends import _load_rust_accelerator, _rust_accelerator_available
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ParallelRuntimeInfo:
|
|
11
|
+
"""Shared-memory execution settings visible to the current process.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
logical_cpus: Logical processors reported by the operating system.
|
|
15
|
+
rust_available: Whether a compatible native extension is installed.
|
|
16
|
+
rust_worker_threads: Rayon worker count when Rust is available.
|
|
17
|
+
rust_parallel_min_items: Operation-size crossover for parallel kernels.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
logical_cpus: int
|
|
21
|
+
rust_available: bool
|
|
22
|
+
rust_worker_threads: int | None
|
|
23
|
+
rust_parallel_min_items: int | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parallel_runtime_info() -> ParallelRuntimeInfo:
|
|
27
|
+
"""Return effective Rust worker and crossover settings without changing them."""
|
|
28
|
+
logical_cpus = os.cpu_count() or 1
|
|
29
|
+
if not _rust_accelerator_available():
|
|
30
|
+
return ParallelRuntimeInfo(logical_cpus, False, None, None)
|
|
31
|
+
|
|
32
|
+
accelerator = _load_rust_accelerator("NmagConfig.accelerator")
|
|
33
|
+
try:
|
|
34
|
+
worker_threads, minimum_items = accelerator.parallel_runtime_info()
|
|
35
|
+
except AttributeError as exc:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"The installed nmag_accel does not expose parallel runtime metadata. "
|
|
38
|
+
"Rebuild it with ./scripts/build-accelerator.sh."
|
|
39
|
+
) from exc
|
|
40
|
+
return ParallelRuntimeInfo(
|
|
41
|
+
logical_cpus=logical_cpus,
|
|
42
|
+
rust_available=True,
|
|
43
|
+
rust_worker_threads=int(worker_threads),
|
|
44
|
+
rust_parallel_min_items=int(minimum_items),
|
|
45
|
+
)
|
nmag/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 inline type information.
|
nmag/resources.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _read_integer(path: Path) -> int | None:
|
|
8
|
+
try:
|
|
9
|
+
raw_value = path.read_text(encoding="ascii").strip()
|
|
10
|
+
return None if raw_value == "max" else int(raw_value)
|
|
11
|
+
except (OSError, ValueError):
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def available_memory_bytes() -> int | None:
|
|
16
|
+
"""Return the tightest host or cgroup estimate of currently available memory."""
|
|
17
|
+
candidates: list[int] = []
|
|
18
|
+
try:
|
|
19
|
+
page_size = int(os.sysconf("SC_PAGE_SIZE"))
|
|
20
|
+
available_pages = int(os.sysconf("SC_AVPHYS_PAGES"))
|
|
21
|
+
if page_size > 0 and available_pages > 0:
|
|
22
|
+
candidates.append(page_size * available_pages)
|
|
23
|
+
except (KeyError, OSError, ValueError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
for limit_path, usage_path in (
|
|
27
|
+
(Path("/sys/fs/cgroup/memory.max"), Path("/sys/fs/cgroup/memory.current")),
|
|
28
|
+
(
|
|
29
|
+
Path("/sys/fs/cgroup/memory/memory.limit_in_bytes"),
|
|
30
|
+
Path("/sys/fs/cgroup/memory/memory.usage_in_bytes"),
|
|
31
|
+
),
|
|
32
|
+
):
|
|
33
|
+
limit = _read_integer(limit_path)
|
|
34
|
+
usage = _read_integer(usage_path)
|
|
35
|
+
if limit is not None and usage is not None and limit > usage:
|
|
36
|
+
candidates.append(limit - usage)
|
|
37
|
+
|
|
38
|
+
return min(candidates) if candidates else None
|