nmag-python-3 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,458 @@
1
+ r"""
2
+ Module which defines the SimulationCore class, an abstract class from which
3
+ the real simulation objects are derived (by inheritance).
4
+ Such a design has the goal of separating the parts of the Simulation object
5
+ which depend on the specific discretisation (FD, FE) from the parts which
6
+ do not depend on it (such as the hysteresis logic).
7
+ Here is the structure we have in mind:
8
+
9
+ /----> FDSimulation ----\
10
+ SimulationCore ---| |---> Simulation
11
+ \----> FESimulation ----/
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from abc import ABC, abstractmethod
18
+ from collections.abc import Callable, Sequence
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from nmag.config import NmagConfig
23
+ from nmag.output import prepare_output_files
24
+ from si.physical import SI
25
+
26
+ from . import hysteresis as hysteresis_m
27
+ from .clock import SimulationClock
28
+ from .data_writer import DataWriter
29
+ from .quantity import known_field_quantities, known_quantities, known_quantities_by_name
30
+
31
+ Action = Callable[[Any], Any]
32
+
33
+ log = logging.getLogger("nmag")
34
+
35
+
36
+ class SimulationCore(ABC):
37
+ """
38
+ Abstract base class for simulations.
39
+
40
+ Handles simulation state, clock, data saving, and hysteresis logic
41
+ that is independent of the specific discretisation (FD/FE).
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ name: str | None = None,
47
+ do_demag: bool = True,
48
+ sim_id: str = "Generic Simulation class",
49
+ config: NmagConfig | None = None,
50
+ ) -> None:
51
+
52
+ self.class_id: str = sim_id # String identifying the kind of Simulation class
53
+ self.units: Any | None = None # Simulation units used by this class
54
+ self.do_demag: bool = do_demag # Whether we should include the demag field
55
+ # List of all the materials used by the Simulation object
56
+ self.materials: list[Any] | None = None
57
+
58
+ # Dictionary used by the hysteresis method to find abbreviations for
59
+ # frequently used things to save or do.
60
+ # Example: for ``sim.hysteresis(..., save=[('averages', at(...))])``
61
+ # the string 'averages' needs to be a key in this dictionary.
62
+ # The corresponding value is the function to call.
63
+ self.action_abbreviations: dict[str, Action] = {}
64
+
65
+ # Every quantity the user may want to save needs to be listed here used for data IO
66
+ self.known_quantities = known_quantities
67
+ self.known_quantities_by_name = known_quantities_by_name
68
+ self.known_field_quantities = known_field_quantities
69
+
70
+ self.config = NmagConfig.from_environment() if config is None else config
71
+ self.name = name or self.config.default_name
72
+ log.info(f"Simulation(name={self.name}) object created")
73
+
74
+ self._restarting: bool = False
75
+ data_filenames: list[Path] = [self._ndtfilename(), self._h5filename()]
76
+ self._manage_output_files(data_filenames)
77
+
78
+ self.clock: SimulationClock = SimulationClock()
79
+
80
+ # The advance_time method does not allow to carry on the simulation
81
+ # up to t = infinite. Sometimes we want to simulate for n steps,
82
+ # without any time limits. However we must give a time limit.
83
+ # This is then how we approximate t = infinite.
84
+ # For now, we do not provide any function to set or change it.
85
+ # The user should just use:
86
+ # sim = Simulation()
87
+ # sim.max_time_reached = SI(1000, "s")
88
+ self.max_time_reached: SI = SI(1, "s")
89
+
90
+ # Add abbreviations so that things can be saved just by giving
91
+ # corresponding ID strings.
92
+ # Example: hysteresis(..., save=[('averages', ...)])
93
+ self.add_save_abbrev("save_averages", lambda sim: sim.save_data(avoid_same_step=True))
94
+ self.add_save_abbrev(
95
+ "save_fields", lambda sim: sim.save_data(fields="all", avoid_same_step=True)
96
+ )
97
+ self.add_save_abbrev(
98
+ "save_field_m", lambda sim: sim.save_data(fields=["m"], avoid_same_step=True)
99
+ )
100
+ self.add_save_abbrev("save_restart", lambda sim: sim.save_restart_file())
101
+ self.add_do_abbrev("do_next_stage", SimulationCore.hysteresis_next_stage)
102
+ self.add_do_abbrev("do_exit", SimulationCore.hysteresis_exit)
103
+
104
+ self.writer = DataWriter(
105
+ ndt_filename=self._ndtfilename(),
106
+ h5_filename=self._h5filename(),
107
+ append=self.config.output_policy == "append",
108
+ )
109
+ self.last_spatial_save_timings_seconds: dict[str, float] = {}
110
+ self.last_maxangle_timings_seconds: dict[str, float] = {}
111
+
112
+ # The following list contains a description of the physics components
113
+ # which are included in the physical model For example,
114
+ # ["exch", "demag"] indicates that exchange and demag are included.
115
+ # In this case, spin transfer torque is not. This information
116
+ # is used to understand which fields are relevant and which are not
117
+ # (so that we do not save empty fields). Following the previous
118
+ # example, dm_dcurrent, current_density won't be saved.
119
+ self._components: list[str] | None = None
120
+
121
+ def _manage_output_files(self, data_filenames: list[Path]) -> None:
122
+ """Apply this simulation's explicit NDT/HDF5 output policy."""
123
+
124
+ prepare_output_files(data_filenames, self.config.output_policy)
125
+
126
+ @property
127
+ def id(self) -> int:
128
+ """ID."""
129
+ return self.clock.id
130
+
131
+ @property
132
+ def stage(self) -> int:
133
+ """Stage number."""
134
+ return self.clock.stage
135
+
136
+ @property
137
+ def step(self) -> int:
138
+ """Global step number (always increases)."""
139
+ return self.clock.step
140
+
141
+ @property
142
+ def time(self) -> SI:
143
+ """Global time reached (always increases)."""
144
+ return self.clock.time
145
+
146
+ @property
147
+ def stage_step(self) -> int:
148
+ """Step number counted from the beginning of the current stage."""
149
+ return self.clock.stage_step
150
+
151
+ @property
152
+ def stage_time(self) -> SI:
153
+ """Time reached counted from the beginning of the current stage."""
154
+ return self.clock.stage_time
155
+
156
+ @property
157
+ def real_time(self) -> SI:
158
+ """Time passed in the 'real' world."""
159
+ return self.clock.real_time
160
+
161
+ @property
162
+ def last_step_dt(self) -> SI:
163
+ """Length of the last simulation step."""
164
+ return self.clock.last_step_dt_si
165
+
166
+ @property
167
+ def components(self) -> list[str]:
168
+ """Get the physical components included in the model."""
169
+ if self._components is not None:
170
+ return self._components
171
+
172
+ else:
173
+ components = ["exch"]
174
+ if self.do_demag:
175
+ components.append("demag")
176
+ self._components = components
177
+ return components
178
+
179
+ def get_all_field_names(self) -> list[str]:
180
+ """Get all field names relevant to the enabled components."""
181
+ return [
182
+ q.name
183
+ for q in self.known_field_quantities
184
+ if q.context is None or q.context in self.components
185
+ ]
186
+
187
+ @staticmethod
188
+ def hysteresis_next_stage(sim: SimulationCore) -> None:
189
+ """
190
+ Terminate the current stage of the hysteresis computation
191
+ and start the next one.
192
+ """
193
+ sim.clock.stage_end = True
194
+
195
+ @staticmethod
196
+ def hysteresis_exit(sim: SimulationCore) -> None:
197
+ """
198
+ Exit from the running hysteresis computation.
199
+ """
200
+ sim.clock.exit_hysteresis = True
201
+ sim.clock.stage_end = True
202
+
203
+ simulation_relax = hysteresis_m.simulation_relax
204
+ relax = simulation_relax
205
+
206
+ simulation_hysteresis = hysteresis_m.simulation_hysteresis
207
+ hysteresis = simulation_hysteresis
208
+
209
+ def add_action_abbrev(
210
+ self,
211
+ abbreviation: str,
212
+ function: Action,
213
+ prefix: str | None = None,
214
+ ) -> None:
215
+ """Add an abbreviation for a 'save' or 'do' action."""
216
+ if prefix is None:
217
+ self.action_abbreviations[abbreviation] = function
218
+ return
219
+
220
+ else:
221
+ valid_prefixes = ["save", "do"]
222
+ if prefix not in valid_prefixes:
223
+ raise ValueError(
224
+ f"Valid prefixes for action abbreviations "
225
+ f"are {valid_prefixes}, you gave '{prefix}'!"
226
+ )
227
+
228
+ if abbreviation.startswith(prefix):
229
+ self.action_abbreviations[abbreviation] = function
230
+ else:
231
+ full_abbreviation = f"{prefix}_{abbreviation}"
232
+ self.action_abbreviations[full_abbreviation] = function
233
+
234
+ def add_save_abbrev(self, abbreviation: str, function: Action) -> None:
235
+ """Add an abbreviation to be used in the 'save' argument of the
236
+ hysteresis method. For example, if you use the following:
237
+
238
+ def funky_function(sim): print "Hello, I'm Funky!"
239
+ sim.add_save_abbrev('funky', funky_function)
240
+
241
+ Then you can call:
242
+
243
+ sim.hysteresis(Hs, save=[('funky', at('convergence'))])
244
+
245
+ and this will be equivalent to:
246
+
247
+ sim.hysteresis(Hs, save=[(funky_function, at('convergence')])
248
+ """
249
+ self.add_action_abbrev(abbreviation, function, prefix="save")
250
+
251
+ def add_do_abbrev(self, abbreviation: str, function: Action) -> None:
252
+ """Add an abbreviation for the 'do' argument of hysteresis."""
253
+ self.add_action_abbrev(abbreviation, function, prefix="do")
254
+
255
+ def do_next_stage(self, stage: int | None = None) -> None:
256
+ """Increment the simulation stage."""
257
+ self.clock.inc_stage(stage=stage)
258
+
259
+ def is_converged(self) -> bool:
260
+ """Returns True when convergence has been reached."""
261
+ return self.clock.convergence
262
+
263
+ def _get_filename(self, ext: str) -> Path:
264
+ """Get the full, absolute path for an output file."""
265
+ basename = self.name + ext
266
+ return self.config.output_directory / basename
267
+
268
+ def _ndtfilename(self) -> Path:
269
+ return self._get_filename("_dat.ndt")
270
+
271
+ def _h5filename(self) -> Path:
272
+ return self._get_filename("_dat.h5")
273
+
274
+ def _statfilename(self) -> Path:
275
+ return self._get_filename("_cvode.log")
276
+
277
+ def _tolfilename(self) -> Path:
278
+ return self._get_filename("_tol.log")
279
+
280
+ def get_restart_file_name(self) -> Path:
281
+ """Return the default name for the restart file."""
282
+ return self.config.output_directory / f"{self.name}_restart.h5"
283
+
284
+ def get_materials_of_field(self, field_name: str) -> list[Any]:
285
+ """
286
+ Returns all materials for a per-material field.
287
+ Returns an empty list if the field is not per-material or
288
+ if materials have not been defined yet.
289
+ """
290
+ quantity = self.known_quantities_by_name[field_name]
291
+
292
+ if "?" in (quantity.signature or ""):
293
+ return self.materials if self.materials is not None else []
294
+
295
+ return []
296
+
297
+ @abstractmethod
298
+ def save_spatial_fields(
299
+ self,
300
+ filename: str | None = None,
301
+ fieldnames: list[str] | None = None,
302
+ ) -> None:
303
+ """Abstract method to save spatially-resolved fields."""
304
+ pass
305
+
306
+ def save_data(
307
+ self,
308
+ fields: str | list[str] | None = None,
309
+ avoid_same_step: bool = False,
310
+ ) -> None:
311
+ """
312
+ Save simulation data.
313
+
314
+ - Averages are saved to the *.ndt file (TSV format).
315
+ - Spatially resolved fields are saved to the *.h5 file.
316
+
317
+ :Parameters:
318
+ `fields` : None, 'all' or list of fieldnames
319
+ If None, only saves averages.
320
+ If 'all', saves all available fields.
321
+ If a list (e.g., ['m', 'H_demag']), saves only those fields.
322
+
323
+ `avoid_same_step` : bool
324
+ If True, only save if clock.step has changed since last save.
325
+ This prevents duplicate data points during hysteresis loops.
326
+ """
327
+ self.writer.save(self, fields, avoid_same_step)
328
+
329
+ @abstractmethod
330
+ def save_mesh(self, filename: str) -> None:
331
+ """Save the mesh to a file."""
332
+ pass
333
+
334
+ @abstractmethod
335
+ def load_mesh(
336
+ self,
337
+ filename: str,
338
+ region_names_and_mag_mats: Any,
339
+ unit_length: float,
340
+ do_reorder: bool = False,
341
+ manual_distribution: Any = None,
342
+ ) -> None:
343
+ """Load a mesh from a file."""
344
+ pass
345
+
346
+ @abstractmethod
347
+ def create_mesh(
348
+ self,
349
+ cell_nums: Sequence[int],
350
+ cell_sizes: Sequence[float],
351
+ materials: Any,
352
+ regions: Callable[..., Any] | None = None,
353
+ origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
354
+ ) -> None:
355
+ """Create a new mesh."""
356
+ pass
357
+
358
+ @abstractmethod
359
+ def set_params(
360
+ self,
361
+ stopping_dm_dt: SI | None = None,
362
+ ts_rel_tol: float | None = None,
363
+ ts_abs_tol: float | None = None,
364
+ ) -> None:
365
+ """Set simulation parameters."""
366
+ pass
367
+
368
+ @abstractmethod
369
+ def reinitialise(self, initial_time: float | None = None) -> None:
370
+ """Re-initialise the simulation."""
371
+ pass
372
+
373
+ @abstractmethod
374
+ def set_local_magnetic_coupling(self, mat1: Any, mat2: Any, coupling: Any) -> None:
375
+ """Set local magnetic coupling between materials."""
376
+ pass
377
+
378
+ @abstractmethod
379
+ def set_H_ext(self, values: Any, unit: SI | None = None) -> None:
380
+ """Set the external magnetic field."""
381
+ pass
382
+
383
+ @abstractmethod
384
+ def set_m(self, values: Any, subfieldname: str | None = None) -> None:
385
+ """Set the magnetization."""
386
+ pass
387
+
388
+ @abstractmethod
389
+ def set_pinning(self, values: Any) -> None:
390
+ """Set pinning sites."""
391
+ pass
392
+
393
+ @abstractmethod
394
+ def set_current_density(self, values: Any, unit: SI | None = None) -> None:
395
+ """Set current density."""
396
+ pass
397
+
398
+ @abstractmethod
399
+ def advance_time(
400
+ self,
401
+ target_time: SI,
402
+ max_it: int = -1,
403
+ exact_tstop: bool | None = None,
404
+ ) -> SI:
405
+ """Advance the simulation time."""
406
+ pass
407
+
408
+ @abstractmethod
409
+ def save_restart_file(
410
+ self, filename: str | None = None, fieldnames: list[str] | None = None, all: bool = False
411
+ ) -> None:
412
+ """Save a restart file."""
413
+ pass
414
+
415
+ @abstractmethod
416
+ def load_restart_file(self, filename: str | None = None) -> None:
417
+ """Load a restart file."""
418
+ pass
419
+
420
+ @abstractmethod
421
+ def save_m_to_file(self, filename: str, format: str | None = None) -> None:
422
+ """Save magnetization to a file."""
423
+ pass
424
+
425
+ @abstractmethod
426
+ def load_m_from_file(self, filename: str, format: str | None = None) -> None:
427
+ """Load magnetization from a file."""
428
+ pass
429
+
430
+ @abstractmethod
431
+ def probe_subfield(self, subfieldname: str, pos: SI, unit: SI | None = None) -> Any:
432
+ """Probe a subfield at a specific position."""
433
+ pass
434
+
435
+ @abstractmethod
436
+ def probe_subfield_siv(
437
+ self,
438
+ subfieldname: str,
439
+ pos: list[float],
440
+ unit: SI | None = None,
441
+ ) -> Any:
442
+ """Probe a subfield returning SI values."""
443
+ pass
444
+
445
+ @abstractmethod
446
+ def get_subfield(self, subfieldname: str, units: SI | None = None) -> Any:
447
+ """Get the entire array for a subfield."""
448
+ pass
449
+
450
+ @abstractmethod
451
+ def get_subfield_average(self, subfieldname: str, mat_name: str | None = None) -> Any:
452
+ """Get the average value of a subfield."""
453
+ pass
454
+
455
+ @abstractmethod
456
+ def get_maxangle_average(self, field_name: str) -> float | None:
457
+ """Get the average maximum angle for a field."""
458
+ pass
throttler/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .throttler import Throttler
2
+
3
+ __all__ = ["Throttler"]
throttler/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker for PEP 561 inline type information.
throttler/throttler.py ADDED
@@ -0,0 +1,55 @@
1
+ import time
2
+ from collections.abc import Hashable
3
+
4
+ """
5
+ This replaces the reporttools.py from the original nmag repo.
6
+ https://github.com/nmag-project/nmag-src/blob/master/interface/nsim/reporttools.py.
7
+ It is converted into a class to avoid global variables and uses monotonic time instead.
8
+ """
9
+
10
+
11
+ class Throttler:
12
+ """
13
+ Manages state to allow actions at most every N seconds per key.
14
+
15
+ Attributes:
16
+ last_called (dict[Hashable, float]): A dictionary mapping
17
+ a unique key to the timestamp (from time.monotonic())
18
+ of its last allowed call.
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ """Initializes the throttle state."""
23
+ # Stores the time of the last successful call for each key
24
+ self.last_called: dict[Hashable, float] = {}
25
+
26
+ def is_allowed(self, key: Hashable, report_delay: float) -> bool:
27
+ """
28
+ Checks if an action for a given key should be allowed.
29
+
30
+ Returns True if 'report_delay' seconds have passed since the
31
+ last time this method returned True for the same 'key',
32
+ or if this is the first call for this 'key'.
33
+
34
+ Args:
35
+ key: A unique, hashable identifier for the action
36
+ being rate-limited (e.g., a string, a tuple).
37
+ report_delay: The minimum number of seconds that must
38
+ pass between True returns.
39
+
40
+ Returns:
41
+ True if the action is allowed, False otherwise.
42
+ """
43
+ now = time.monotonic()
44
+
45
+ # Get the last time this key was allowed.
46
+ # Default to 0.0, ensuring the first call always passes
47
+ last_time = self.last_called.get(key, 0.0)
48
+
49
+ if now - last_time >= report_delay:
50
+ # Time has elapsed. Allow the action and update the timestamp.
51
+ self.last_called[key] = now
52
+ return True
53
+
54
+ # Not enough time has passed. Deny the action.
55
+ return False
when/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .when import NextTime, TimeDict, When, at, every, never
2
+
3
+ __all__ = ["NextTime", "TimeDict", "When", "at", "every", "never"]
when/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker for PEP 561 inline type information.