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
when/when.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Time specification utilities for specifying when to do things
|
|
3
|
+
(such as saving fields or averages).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import abc
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimeDict(Protocol):
|
|
12
|
+
def __getitem__(self, key: str, /) -> Any: ...
|
|
13
|
+
|
|
14
|
+
def get(self, key: str, default: Any = None, /) -> Any: ...
|
|
15
|
+
|
|
16
|
+
def copy(self) -> dict[str, Any]: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
NextTime = Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _float_is_integer(f: float) -> bool:
|
|
23
|
+
return math.isclose(f, round(f))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- Abstract Base Class for Specification Logic ---
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _WhenSpec(abc.ABC):
|
|
30
|
+
"""
|
|
31
|
+
Abstract base class for a time specification.
|
|
32
|
+
This is an internal implementation detail. The user interacts
|
|
33
|
+
with the 'When' class, which wraps this.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abc.abstractmethod
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
"""Return the string representation."""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
43
|
+
"""Check if the current time matches this specification."""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
48
|
+
"""Calculate the next matching time for the given identifier."""
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- Concrete Specification Implementations ---
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _AtSpec(_WhenSpec):
|
|
56
|
+
"""Specification for a single point in time."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, identifier: str, value: Any) -> None:
|
|
59
|
+
self.identifier = identifier
|
|
60
|
+
self.value = value
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
return f"at({self.identifier!r}, {self.value!r})"
|
|
64
|
+
|
|
65
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
66
|
+
return this_time.get(self.identifier) == self.value
|
|
67
|
+
|
|
68
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
69
|
+
if self.identifier != identifier:
|
|
70
|
+
return True # Irrelevant to this identifier, so don't block
|
|
71
|
+
|
|
72
|
+
t = this_time.get(identifier)
|
|
73
|
+
if isinstance(t, bool):
|
|
74
|
+
if self.value is True:
|
|
75
|
+
return True
|
|
76
|
+
else:
|
|
77
|
+
return t == self.value
|
|
78
|
+
else:
|
|
79
|
+
if t < self.value:
|
|
80
|
+
return self.value
|
|
81
|
+
else:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _EverySpec(_WhenSpec):
|
|
86
|
+
"""Specification for a periodic event."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
identifier: str,
|
|
91
|
+
delta: Any | None,
|
|
92
|
+
first: Any,
|
|
93
|
+
last: Any | None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self.identifier = identifier
|
|
96
|
+
self.delta = delta
|
|
97
|
+
self.first = first
|
|
98
|
+
self.last = last
|
|
99
|
+
|
|
100
|
+
def __repr__(self) -> str:
|
|
101
|
+
opts = ""
|
|
102
|
+
if self.first != 0:
|
|
103
|
+
opts += f", first={self.first}"
|
|
104
|
+
if self.last is not None:
|
|
105
|
+
opts += f", last={self.last}"
|
|
106
|
+
|
|
107
|
+
delta_str = str(self.delta) if self.delta is not None else "None"
|
|
108
|
+
return f"every({delta_str}, {self.identifier!r}{opts})"
|
|
109
|
+
|
|
110
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
111
|
+
t = this_time.get(self.identifier)
|
|
112
|
+
if t is None:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
if t < self.first:
|
|
116
|
+
return False
|
|
117
|
+
if self.last is not None and t > self.last:
|
|
118
|
+
return False
|
|
119
|
+
if self.delta is None:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
if self.delta <= 0:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
pos = float((t - self.first) / self.delta)
|
|
126
|
+
return _float_is_integer(pos)
|
|
127
|
+
|
|
128
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
129
|
+
if self.identifier != identifier:
|
|
130
|
+
return self.match_time(this_time)
|
|
131
|
+
|
|
132
|
+
t = this_time.get(identifier)
|
|
133
|
+
if t is None:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
if self.last is not None and t >= self.last:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
if t < self.first and self.delta is not None:
|
|
140
|
+
return self.first
|
|
141
|
+
|
|
142
|
+
if self.delta is None:
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
if self.delta <= 0:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
pos = float((t - self.first) / self.delta)
|
|
149
|
+
|
|
150
|
+
if _float_is_integer(pos):
|
|
151
|
+
next_pos = int(round(pos)) + 1
|
|
152
|
+
else:
|
|
153
|
+
next_pos = int(pos) + 1
|
|
154
|
+
|
|
155
|
+
next_t = self.delta * next_pos + self.first
|
|
156
|
+
|
|
157
|
+
if self.last is not None and next_t > self.last:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
return next_t
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _OrSpec(_WhenSpec):
|
|
164
|
+
"""Specification for a logical OR of two specifications."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, spec1: _WhenSpec, spec2: _WhenSpec) -> None:
|
|
167
|
+
self.spec1 = spec1
|
|
168
|
+
self.spec2 = spec2
|
|
169
|
+
|
|
170
|
+
def __repr__(self) -> str:
|
|
171
|
+
return f"({self.spec1!r} | {self.spec2!r})"
|
|
172
|
+
|
|
173
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
174
|
+
return self.spec1.match_time(this_time) or self.spec2.match_time(this_time)
|
|
175
|
+
|
|
176
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
177
|
+
nt1 = self.spec1.next_time(identifier, this_time)
|
|
178
|
+
nt2 = self.spec2.next_time(identifier, this_time)
|
|
179
|
+
|
|
180
|
+
nt1_is_bool = isinstance(nt1, bool)
|
|
181
|
+
nt2_is_bool = isinstance(nt2, bool)
|
|
182
|
+
|
|
183
|
+
if nt1_is_bool and nt2_is_bool:
|
|
184
|
+
return nt1 or nt2
|
|
185
|
+
|
|
186
|
+
if nt1 is False:
|
|
187
|
+
return nt2
|
|
188
|
+
if nt2 is False:
|
|
189
|
+
return nt1
|
|
190
|
+
|
|
191
|
+
if nt1 is True:
|
|
192
|
+
return True
|
|
193
|
+
if nt2 is True:
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
return min(nt1, nt2)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class _AndSpec(_WhenSpec):
|
|
200
|
+
"""Specification for a logical AND of two specifications."""
|
|
201
|
+
|
|
202
|
+
def __init__(self, spec1: _WhenSpec, spec2: _WhenSpec) -> None:
|
|
203
|
+
self.spec1 = spec1
|
|
204
|
+
self.spec2 = spec2
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return f"({self.spec1!r} & {self.spec2!r})"
|
|
208
|
+
|
|
209
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
210
|
+
return self.spec1.match_time(this_time) and self.spec2.match_time(this_time)
|
|
211
|
+
|
|
212
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
213
|
+
# Use a copy to avoid side effects on the original time dict
|
|
214
|
+
temp_time = this_time.copy()
|
|
215
|
+
save_t = temp_time.get(identifier)
|
|
216
|
+
if save_t is None:
|
|
217
|
+
return False # Can't calculate if identifier is missing
|
|
218
|
+
|
|
219
|
+
ntmax: int | float | None = None
|
|
220
|
+
|
|
221
|
+
both_match = False
|
|
222
|
+
while not both_match:
|
|
223
|
+
nt1 = self.spec1.next_time(identifier, temp_time)
|
|
224
|
+
nt2 = self.spec2.next_time(identifier, temp_time)
|
|
225
|
+
|
|
226
|
+
if nt1 is False or nt2 is False:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
if nt1 is True:
|
|
230
|
+
return nt2
|
|
231
|
+
if nt2 is True:
|
|
232
|
+
return nt1
|
|
233
|
+
|
|
234
|
+
if nt1 > nt2:
|
|
235
|
+
ntmax, argmin = nt1, self.spec2
|
|
236
|
+
else:
|
|
237
|
+
ntmax, argmin = nt2, self.spec1
|
|
238
|
+
|
|
239
|
+
temp_time[identifier] = ntmax
|
|
240
|
+
both_match = argmin.match_time(temp_time)
|
|
241
|
+
|
|
242
|
+
assert ntmax is not None, "Logic error: 'and' loop exited without setting ntmax"
|
|
243
|
+
|
|
244
|
+
return ntmax
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class _NeverSpec(_WhenSpec):
|
|
248
|
+
"""Specification that never matches."""
|
|
249
|
+
|
|
250
|
+
def __repr__(self) -> str:
|
|
251
|
+
return "never"
|
|
252
|
+
|
|
253
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def next_time(self, identifier: str, this_time: TimeDict) -> NextTime:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# --- Public-Facing 'When' Class ---
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class When:
|
|
264
|
+
"""Combine a schedule condition with ``|`` or ``&`` operators.
|
|
265
|
+
|
|
266
|
+
Users normally create instances with :func:`at` and :func:`every` rather
|
|
267
|
+
than calling this constructor directly.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, spec: _WhenSpec) -> None:
|
|
271
|
+
self.spec = spec
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
return f"<When: {self.spec!r}>"
|
|
275
|
+
|
|
276
|
+
def __str__(self) -> str:
|
|
277
|
+
return repr(self.spec)
|
|
278
|
+
|
|
279
|
+
def match_time(self, this_time: TimeDict) -> bool:
|
|
280
|
+
"""Return whether this condition matches a simulation clock mapping."""
|
|
281
|
+
return self.spec.match_time(this_time)
|
|
282
|
+
|
|
283
|
+
def next_time(
|
|
284
|
+
self, identifier: str, this_time: TimeDict, tols: dict[str, Any] | None = None
|
|
285
|
+
) -> NextTime:
|
|
286
|
+
"""Return the next match for one clock identifier.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
identifier: Clock key such as ``step``, ``time``, or ``stage_time``.
|
|
290
|
+
this_time: Current clock-like mapping.
|
|
291
|
+
tols: Optional per-identifier tolerance preventing a floating-point
|
|
292
|
+
boundary from triggering repeatedly.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Next matching value or a boolean sentinel used by schedule merging.
|
|
296
|
+
"""
|
|
297
|
+
nt = self.spec.next_time(identifier, this_time)
|
|
298
|
+
|
|
299
|
+
# Apply tolerance logic from the original class
|
|
300
|
+
if tols is not None and identifier in tols and type(nt) is not bool:
|
|
301
|
+
tol = tols[identifier]
|
|
302
|
+
tt = this_time[identifier]
|
|
303
|
+
|
|
304
|
+
if tol > 0.0 and abs(nt - tt) < tol:
|
|
305
|
+
# We are too close to the current time.
|
|
306
|
+
# Advance time slightly and recalculate.
|
|
307
|
+
temp_time = this_time.copy()
|
|
308
|
+
temp_time[identifier] = nt + tol
|
|
309
|
+
nt = self.spec.next_time(identifier, temp_time)
|
|
310
|
+
|
|
311
|
+
return nt
|
|
312
|
+
|
|
313
|
+
def __or__(self, other: object) -> "When":
|
|
314
|
+
"""Combines two 'When' objects with a logical OR."""
|
|
315
|
+
if not isinstance(other, When):
|
|
316
|
+
return NotImplemented
|
|
317
|
+
return When(_OrSpec(self.spec, other.spec))
|
|
318
|
+
|
|
319
|
+
def __and__(self, other: object) -> "When":
|
|
320
|
+
"""
|
|
321
|
+
Combines two 'When' objects with a logical AND.
|
|
322
|
+
|
|
323
|
+
WARNING: As in the original, this can lead to infinite loops
|
|
324
|
+
if the two conditions are mutually exclusive (e.g.,
|
|
325
|
+
every('step', 2) & every('step', 2, first=1)).
|
|
326
|
+
Use with care.
|
|
327
|
+
"""
|
|
328
|
+
if not isinstance(other, When):
|
|
329
|
+
return NotImplemented
|
|
330
|
+
return When(_AndSpec(self.spec, other.spec))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# --- Factory Functions (Public API) ---
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def at(identifier: str, value: Any = True) -> When:
|
|
337
|
+
"""Create a condition matching one exact clock value or event.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
identifier: Clock key or event such as ``"step"``, ``"time"``,
|
|
341
|
+
``"convergence"``, or ``"stage_end"``.
|
|
342
|
+
value: Exact value to match. Boolean events default to true.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Schedule condition, for example ``at("step", 10)``.
|
|
346
|
+
"""
|
|
347
|
+
return When(_AtSpec(identifier, value))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def every(
|
|
351
|
+
arg1: Any,
|
|
352
|
+
arg2: Any | None = None,
|
|
353
|
+
first: Any = 0.0,
|
|
354
|
+
last: Any | None = None,
|
|
355
|
+
) -> When:
|
|
356
|
+
"""Create a periodic clock condition.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
arg1: Preferred clock identifier, such as ``"step"`` or ``"time"``.
|
|
360
|
+
A legacy delta-first call is also accepted.
|
|
361
|
+
arg2: Positive interval in clock units, or the identifier in the legacy
|
|
362
|
+
argument order.
|
|
363
|
+
first: First value eligible to match.
|
|
364
|
+
last: Optional final eligible value; it must exceed ``first``.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Periodic condition, for example ``every("step", 10)``.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ValueError: If no identifier is supplied, the interval is non-positive,
|
|
371
|
+
or the requested range is invalid.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
identifier: str | None = None
|
|
375
|
+
delta: Any | None = None
|
|
376
|
+
|
|
377
|
+
# Handle swapped arguments with explicit type-checking
|
|
378
|
+
if isinstance(arg1, str):
|
|
379
|
+
identifier = arg1
|
|
380
|
+
if arg2 is not None and not isinstance(arg2, str):
|
|
381
|
+
delta = arg2
|
|
382
|
+
elif arg2 is None:
|
|
383
|
+
delta = None # e.g., every('step', first=10)
|
|
384
|
+
# If arg2 is a str, we let it fail validation below
|
|
385
|
+
|
|
386
|
+
else:
|
|
387
|
+
delta = arg1
|
|
388
|
+
if isinstance(arg2, str):
|
|
389
|
+
identifier = arg2
|
|
390
|
+
# If arg2 is numeric or None, we let it fail validation below
|
|
391
|
+
|
|
392
|
+
# --- Validation ---
|
|
393
|
+
|
|
394
|
+
# 1. Check if identifier was found and is a string
|
|
395
|
+
if not isinstance(identifier, str):
|
|
396
|
+
raise ValueError(
|
|
397
|
+
"Bad usage of 'every': you must specify an identifier (string). "
|
|
398
|
+
"Example: every('step', 10)"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# 2. Check 'last' vs 'first'
|
|
402
|
+
if last is not None and last <= first:
|
|
403
|
+
raise ValueError(
|
|
404
|
+
"Bad usage of 'every': 'last' must be greater than 'first'. "
|
|
405
|
+
f"Got first={first}, last={last}"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# 3. Check delta
|
|
409
|
+
if delta is not None and delta <= 0:
|
|
410
|
+
raise ValueError(f"Bad usage of 'every': delta must be positive. Got delta={delta}")
|
|
411
|
+
|
|
412
|
+
return When(_EverySpec(identifier, delta, first, last))
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# A singleton instance for the 'never' specification
|
|
416
|
+
never = When(_NeverSpec())
|