tsdynamics 2.3.0__tar.gz → 2.4.1__tar.gz
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.
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/PKG-INFO +1 -1
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/__init__.py +1 -1
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/backends/rustcore.py +80 -12
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/ode_base.py +9 -1
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/climate_geophysics.py +31 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/physical_systems.py +40 -1
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/_sampling.py +11 -16
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_diffsol_backend.py +4 -4
- tsdynamics-2.4.1/tests/test_rustcore.py +220 -0
- tsdynamics-2.3.0/tests/test_rustcore.py +0 -81
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/.gitignore +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/LICENSE +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/README.md +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/pyproject.toml +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/analysis/fixed_points.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/analysis/lyapunov.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/analysis/orbit_diagram.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/analysis/poincare.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/backends/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/backends/diffsol.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/base.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/dde_base.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/map_base.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/base/protocol.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/ensemble.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/poincare.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/stroboscopic.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/tangent.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/sampling.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/utils/__init__.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/utils/general.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/utils/sagitta_dt.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/conftest.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_analysis.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_base.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_cache_isolation.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_dde_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_derived.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_diffsol_translation.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_jacobians.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_known_values.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_map_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_ode_rhs_symbolic.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_ode_systems.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_protocol.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_registry.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_rustcore_translation.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_sampling.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_signatures.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_smoke.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_utils.py +0 -0
- {tsdynamics-2.3.0 → tsdynamics-2.4.1}/tests/test_wrapped.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsdynamics
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Compiled ODE/DDE integration, discrete maps, and chaos analysis for dynamical systems.
|
|
5
5
|
Project-URL: Source, https://github.com/El3ssar/TSDynamics
|
|
6
6
|
Project-URL: Documentation, https://el3ssar.github.io/TSDynamics/
|
|
@@ -48,7 +48,7 @@ from .systems import continuous as _continuous
|
|
|
48
48
|
from .systems import discrete as _discrete
|
|
49
49
|
|
|
50
50
|
# Single source of truth for the package version; rewritten by python-semantic-release.
|
|
51
|
-
__version__ = "2.
|
|
51
|
+
__version__ = "2.4.1"
|
|
52
52
|
|
|
53
53
|
# Re-export every system class at the top level so ``from tsdynamics import Lorenz``
|
|
54
54
|
# works without users having to remember which submodule a system lives in.
|
|
@@ -253,13 +253,38 @@ def _args(tape: CompiledTape):
|
|
|
253
253
|
return (tape.ops, tape.a, tape.b, tape.imm, tape.outputs, tape.n_state, tape.n_param)
|
|
254
254
|
|
|
255
255
|
|
|
256
|
+
# Method names: fixed-step RK4, or the adaptive Dormand-Prince 5(4) kernel.
|
|
257
|
+
# These are the only kernels the crate implements; an unknown name is rejected
|
|
258
|
+
# (rather than silently downgraded) — there is no DOP853/LSODA kernel to reach.
|
|
259
|
+
_FIXED_RK4 = {"RK4", "rk4"}
|
|
260
|
+
_ADAPTIVE = {"RK45", "rk45", "dopri5", "DP45"}
|
|
261
|
+
_METHODS = _FIXED_RK4 | _ADAPTIVE
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _check_method(method: str) -> None:
|
|
265
|
+
if method not in _METHODS:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"rustcore: unknown method {method!r}; supported: {sorted(_METHODS)} "
|
|
268
|
+
"(RK4 = fixed step, RK45/dopri5 = adaptive). For stiff systems use the "
|
|
269
|
+
"jitcode/diffsol backends."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _as_state(x: Any, dim: int, name: str) -> np.ndarray:
|
|
274
|
+
"""Coerce to a contiguous float64 1-D vector of length ``dim`` (or raise)."""
|
|
275
|
+
a = np.ascontiguousarray(x, dtype=np.float64).ravel()
|
|
276
|
+
if a.size != dim:
|
|
277
|
+
raise ValueError(f"rustcore: {name} has length {a.size}, expected dim={dim}")
|
|
278
|
+
return a
|
|
279
|
+
|
|
280
|
+
|
|
256
281
|
def eval_rhs(
|
|
257
282
|
system: Any, u: Any, t: float = 0.0, *, tape: CompiledTape | None = None
|
|
258
283
|
) -> np.ndarray:
|
|
259
284
|
"""Evaluate ``du/dt`` once in Rust — used to cross-check the tape."""
|
|
260
285
|
core = _require()
|
|
261
286
|
tape = tape or compile_tape(system)
|
|
262
|
-
u =
|
|
287
|
+
u = _as_state(u, tape.n_state, "u")
|
|
263
288
|
return np.asarray(core.eval_rhs(*_args(tape), u, _params_vec(system, tape), float(t)))
|
|
264
289
|
|
|
265
290
|
|
|
@@ -268,19 +293,41 @@ def integrate_dense(
|
|
|
268
293
|
ic: Any,
|
|
269
294
|
t_eval: Any,
|
|
270
295
|
*,
|
|
296
|
+
method: str = "RK45",
|
|
297
|
+
rtol: float = 1e-6,
|
|
298
|
+
atol: float = 1e-9,
|
|
271
299
|
h: float | None = None,
|
|
272
300
|
tape: CompiledTape | None = None,
|
|
273
301
|
) -> np.ndarray:
|
|
274
|
-
"""
|
|
302
|
+
"""Integrate a trajectory and sample it at ``t_eval``.
|
|
303
|
+
|
|
304
|
+
``method="RK45"`` (default) uses error-controlled adaptive Dormand-Prince
|
|
305
|
+
5(4) with Hermite dense output (``rtol``/``atol`` set the tolerance);
|
|
306
|
+
``method="RK4"`` uses fixed-step RK4 with internal step ``h``.
|
|
307
|
+
|
|
308
|
+
Raises ``RuntimeError`` if the trajectory diverges or the step collapses
|
|
309
|
+
before reaching the final time (matching the jitcode/diffsol backends).
|
|
310
|
+
"""
|
|
311
|
+
_check_method(method)
|
|
275
312
|
core = _require()
|
|
276
313
|
tape = tape or compile_tape(system)
|
|
277
|
-
t_eval = np.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
314
|
+
t_eval = np.ascontiguousarray(t_eval, dtype=np.float64)
|
|
315
|
+
ic = _as_state(ic, tape.n_state, "ic")
|
|
316
|
+
p = _params_vec(system, tape)
|
|
317
|
+
if method in _FIXED_RK4:
|
|
318
|
+
if h is None:
|
|
319
|
+
h = float(np.min(np.diff(t_eval))) if t_eval.size > 1 else 1e-2
|
|
320
|
+
y = np.asarray(core.integrate_dense_py(*_args(tape), ic, p, t_eval, float(h)))
|
|
321
|
+
else:
|
|
322
|
+
y = np.asarray(
|
|
323
|
+
core.integrate_dense_rk45_py(*_args(tape), ic, p, t_eval, float(rtol), float(atol))
|
|
324
|
+
)
|
|
325
|
+
if not np.all(np.isfinite(y)):
|
|
326
|
+
raise RuntimeError(
|
|
327
|
+
f"{type(system).__name__}: rustcore integration diverged or the step "
|
|
328
|
+
"collapsed before reaching the final time."
|
|
329
|
+
)
|
|
330
|
+
return y
|
|
284
331
|
|
|
285
332
|
|
|
286
333
|
def ensemble_final(
|
|
@@ -289,19 +336,40 @@ def ensemble_final(
|
|
|
289
336
|
t0: float,
|
|
290
337
|
t1: float,
|
|
291
338
|
*,
|
|
339
|
+
method: str = "RK45",
|
|
340
|
+
rtol: float = 1e-6,
|
|
341
|
+
atol: float = 1e-9,
|
|
292
342
|
h: float = 1e-2,
|
|
293
343
|
tape: CompiledTape | None = None,
|
|
294
344
|
) -> np.ndarray:
|
|
295
345
|
"""Integrate a batch of initial conditions in parallel; return final states.
|
|
296
346
|
|
|
297
347
|
``u0_batch`` is ``(n, dim)``; each row is integrated from ``t0`` to ``t1``
|
|
298
|
-
and its final state returned as row of the ``(n, dim)`` result.
|
|
348
|
+
and its final state returned as a row of the ``(n, dim)`` result.
|
|
349
|
+
``method`` selects the adaptive (``"RK45"``, default) or fixed-step
|
|
350
|
+
(``"RK4"``) kernel, as in :func:`integrate_dense`.
|
|
351
|
+
|
|
352
|
+
A trajectory that diverges (escapes to infinity) yields a row of ``NaN``
|
|
353
|
+
rather than aborting the batch — the basin/ensemble caller classifies those
|
|
354
|
+
initial conditions as escaped.
|
|
299
355
|
"""
|
|
356
|
+
_check_method(method)
|
|
300
357
|
core = _require()
|
|
301
358
|
tape = tape or compile_tape(system)
|
|
302
359
|
u0_batch = np.ascontiguousarray(u0_batch, dtype=np.float64)
|
|
360
|
+
if u0_batch.ndim != 2 or u0_batch.shape[1] != tape.n_state:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"rustcore: u0_batch must be (n, {tape.n_state}); got shape {u0_batch.shape}"
|
|
363
|
+
)
|
|
364
|
+
p = _params_vec(system, tape)
|
|
365
|
+
if method in _FIXED_RK4:
|
|
366
|
+
return np.asarray(
|
|
367
|
+
core.integrate_ensemble_final_py(
|
|
368
|
+
*_args(tape), u0_batch, p, float(t0), float(t1), float(h)
|
|
369
|
+
)
|
|
370
|
+
)
|
|
303
371
|
return np.asarray(
|
|
304
|
-
core.
|
|
305
|
-
*_args(tape), u0_batch,
|
|
372
|
+
core.integrate_ensemble_final_rk45_py(
|
|
373
|
+
*_args(tape), u0_batch, p, float(t0), float(t1), float(rtol), float(atol)
|
|
306
374
|
)
|
|
307
375
|
)
|
|
@@ -153,6 +153,14 @@ class ContinuousSystem(SystemBase, ABC):
|
|
|
153
153
|
|
|
154
154
|
_default_method: ClassVar[str] = "RK45"
|
|
155
155
|
|
|
156
|
+
#: Whether JiTCODE should run SymEngine's ``simplify(ratio=1)`` on each RHS
|
|
157
|
+
#: expression before emitting C. ``None`` keeps JiTCODE's own default
|
|
158
|
+
#: (enabled for ``dim <= 10``). Set ``False`` on systems whose RHS is a
|
|
159
|
+
#: large rational expression: the simplify pass is super-linear and can
|
|
160
|
+
#: effectively hang at compile time, while the C compiler optimises the
|
|
161
|
+
#: unsimplified code just as well (see ``BlinkingRotlet``).
|
|
162
|
+
_compile_simplify: ClassVar[bool | None] = None
|
|
163
|
+
|
|
156
164
|
#: Parameters whose values affect the symbolic *structure* of _equations
|
|
157
165
|
#: (e.g. integer loop bounds). These are baked in at compile time.
|
|
158
166
|
_structural_params: ClassVar[frozenset[str]] = frozenset()
|
|
@@ -390,7 +398,7 @@ class ContinuousSystem(SystemBase, ABC):
|
|
|
390
398
|
ode = cls_jitc(
|
|
391
399
|
f_sym, n=self.dim, control_pars=control_par_list, verbose=False, **lyap_kwargs
|
|
392
400
|
)
|
|
393
|
-
ode.generate_f_C()
|
|
401
|
+
ode.generate_f_C(simplify=type(self)._compile_simplify)
|
|
394
402
|
so = pathlib.Path(ode.save_compiled(destination=str(dest), overwrite=True))
|
|
395
403
|
|
|
396
404
|
if for_lyap:
|
{tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/climate_geophysics.py
RENAMED
|
@@ -75,6 +75,28 @@ class DoubleGyre(ContinuousSystem):
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class BlinkingRotlet(ContinuousSystem):
|
|
78
|
+
r"""Blinking-rotlet flow — a model of chaotic advection in Stokes mixing.
|
|
79
|
+
|
|
80
|
+
A passive tracer in a circular cell stirred by two off-centre rotlets at
|
|
81
|
+
radius ``+b`` and ``-b`` that alternate being active with period ``tau``,
|
|
82
|
+
the switch being a steep ``tanh`` ramp. The periodic blinking folds and
|
|
83
|
+
stretches material lines, producing chaotic advection (Aref-type *blinking*
|
|
84
|
+
flow). State is ``(r, theta, t)`` in polar coordinates with an explicit
|
|
85
|
+
clock; ``dtheta`` carries a ``1/r`` factor singular at the cell centre, so
|
|
86
|
+
``default_ic`` sits on a bounded orbit away from ``r = 0``.
|
|
87
|
+
|
|
88
|
+
Compile note
|
|
89
|
+
------------
|
|
90
|
+
The rotlet velocity field is a large rational expression. JiTCODE's
|
|
91
|
+
default ``simplify(ratio=1)`` codegen pass is super-linear in expression
|
|
92
|
+
size and effectively hangs while *compiling* this RHS — which is what made
|
|
93
|
+
the system look un-integrable. ``_compile_simplify = False`` skips that
|
|
94
|
+
pass (the C compiler optimises the unsimplified code), after which the flow
|
|
95
|
+
integrates in well under a millisecond at every tolerance, even at the
|
|
96
|
+
original steep ``tanh`` switch.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
reference = "Meleshko & Aref (1996), Phys. Fluids 8, 3215-3217"
|
|
78
100
|
params = {
|
|
79
101
|
"a": 1.0,
|
|
80
102
|
"b": 0.5298833894399929,
|
|
@@ -83,6 +105,15 @@ class BlinkingRotlet(ContinuousSystem):
|
|
|
83
105
|
"tau": 3.0,
|
|
84
106
|
}
|
|
85
107
|
dim = 3
|
|
108
|
+
variables = ("r", "theta", "t")
|
|
109
|
+
#: On a bounded orbit (``r`` stays in ``[0.8, 0.91]``) away from the
|
|
110
|
+
#: ``r = 0`` angular singularity, so default sweeps stay bounded.
|
|
111
|
+
default_ic = [0.8, 4.887, 0.0]
|
|
112
|
+
|
|
113
|
+
# The rotlet RHS is a large rational expression; JiTCODE's default
|
|
114
|
+
# simplify(ratio=1) pass is super-linear on it and effectively hangs the
|
|
115
|
+
# compile. Skip it — the C compiler optimises the unsimplified code.
|
|
116
|
+
_compile_simplify = False
|
|
86
117
|
|
|
87
118
|
@staticmethod
|
|
88
119
|
def _rotlet(r, theta, a, b, bc):
|
|
@@ -140,13 +140,52 @@ class JerkCircuit(ContinuousSystem):
|
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
class WindmiReduced(ContinuousSystem):
|
|
143
|
+
r"""Reduced WINDMI model of the solar-wind–magnetosphere–ionosphere coupling.
|
|
144
|
+
|
|
145
|
+
Three-variable reduction (field-aligned current ``i``, voltage ``v``,
|
|
146
|
+
pressure ``p``) of the WINDMI energy-conserving model. The pressure-loss
|
|
147
|
+
term carries a fractional power ``p**(5/4)`` gated by a steep
|
|
148
|
+
``tanh(d1*(i-1))`` switch (``d1 = 2200`` makes it a near-step at ``i = 1``,
|
|
149
|
+
the substorm-onset threshold).
|
|
150
|
+
|
|
151
|
+
Numerical guards (no change to the dynamics)
|
|
152
|
+
--------------------------------------------
|
|
153
|
+
Two robustness guards let every backend reproduce the same orbit that
|
|
154
|
+
JiTCODE's explicit dopri5 already handles:
|
|
155
|
+
|
|
156
|
+
* ``abs(p)`` under the fractional power. On the attractor ``p >= 0``, but
|
|
157
|
+
adaptive and implicit solvers probe trial states with ``p < 0`` where the
|
|
158
|
+
real ``p**(5/4)`` is complex. ``abs(p)`` is identical to ``p`` on the
|
|
159
|
+
physical trajectory and real, ``C^1`` everywhere.
|
|
160
|
+
|
|
161
|
+
* The ``tanh`` argument is clamped to ``[-_TANH_CLAMP, _TANH_CLAMP]``.
|
|
162
|
+
``tanh`` saturates to ``±1`` to machine precision past ``|arg| ≈ 20``, so
|
|
163
|
+
clamping leaves the switch value unchanged, but it stops the diffsol Rust
|
|
164
|
+
kernels' automatic differentiation from overflowing on ``exp`` of an
|
|
165
|
+
enormous ``d1*(i-1)`` (≈ ±1500 on the orbit) — the actual cause of their
|
|
166
|
+
Newton/error-test failures. The clamp uses ``abs`` only, which both the
|
|
167
|
+
JiTCODE and DiffSL paths support.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
reference = "Horton, Weigel & Sprott (2001), Phys. Plasmas 8, 2946-2952"
|
|
143
171
|
params = {"a1": 0.247, "b1": 10.8, "b2": 0.0752, "b3": 1.06, "d1": 2200, "vsw": 5}
|
|
144
172
|
dim = 3
|
|
173
|
+
variables = ("i", "v", "p")
|
|
174
|
+
|
|
175
|
+
#: Bound on the ``tanh`` switch argument; ``tanh(±20)`` already equals ``±1``
|
|
176
|
+
#: to ~1e-18, so clamping is dynamically invisible but autodiff-safe.
|
|
177
|
+
_TANH_CLAMP = 25.0
|
|
145
178
|
|
|
146
179
|
@staticmethod
|
|
147
180
|
def _equations(Y, t, *, a1, b1, b2, b3, d1, vsw):
|
|
148
181
|
i, v, p = Y(0), Y(1), Y(2)
|
|
182
|
+
# Clamp d1*(i-1) to [-C, C] via the abs identity
|
|
183
|
+
# clamp(z, -C, C) = (|z + C| - |z - C|) / 2 — smooth-enough (abs only)
|
|
184
|
+
# and translatable by both the JiTCODE and DiffSL backends.
|
|
185
|
+
c = WindmiReduced._TANH_CLAMP
|
|
186
|
+
z = d1 * (i - 1)
|
|
187
|
+
z_clamped = (abs(z + c) - abs(z - c)) / 2
|
|
149
188
|
idot = a1 * (vsw - v)
|
|
150
189
|
vdot = b1 * i - b2 * p**1 / 2 - b3 * v
|
|
151
|
-
pdot = vsw**2 - p ** (5 / 4) * vsw ** (1 / 2) * (1 + tanh(
|
|
190
|
+
pdot = vsw**2 - abs(p) ** (5 / 4) * vsw ** (1 / 2) * (1 + tanh(z_clamped)) / 2
|
|
152
191
|
return idot, vdot, pdot
|
|
@@ -56,25 +56,20 @@ INTEGRATION_SAMPLE: list[str] = [
|
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
# --- ODE systems excluded from the exhaustive integration sweeps ------------
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"needs a dedicated integration recipe — tracked separately"
|
|
66
|
-
),
|
|
67
|
-
}
|
|
59
|
+
# Systems that cannot be integrated by adaptive solvers in bounded time would
|
|
60
|
+
# hang the nightly full sweep rather than fail it, so they are skipped here.
|
|
61
|
+
# Currently empty: ``BlinkingRotlet`` (the former entry) only *looked*
|
|
62
|
+
# un-integrable because JiTCODE's simplify(ratio=1) codegen pass hangs on its
|
|
63
|
+
# large rational RHS — fixed with ``_compile_simplify = False`` on the class.
|
|
64
|
+
HARD_TO_INTEGRATE: dict[str, str] = {}
|
|
68
65
|
|
|
69
66
|
# Systems the diffsol backend specifically can't integrate (the default JiTCODE
|
|
70
67
|
# backend handles them fine). Excluded from the diffsol full-catalogue sweep.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
),
|
|
77
|
-
}
|
|
68
|
+
# Currently empty: ``WindmiReduced`` (the former entry) now integrates on every
|
|
69
|
+
# diffsol solver after guarding the fractional power with ``abs(p)`` and
|
|
70
|
+
# clamping the steep ``tanh`` argument (autodiff-safe) — both no-ops on the
|
|
71
|
+
# physical trajectory.
|
|
72
|
+
DIFFSOL_SKIP: dict[str, str] = {}
|
|
78
73
|
|
|
79
74
|
# --- Maps excluded from Lyapunov-spectrum shape tests -----------------------
|
|
80
75
|
MAP_LYAPUNOV_EXCLUDE: dict[str, str] = {
|
|
@@ -135,10 +135,10 @@ def test_diffsol_integrates_full_catalogue() -> None:
|
|
|
135
135
|
(``-m full``) with the diffsol extra.
|
|
136
136
|
|
|
137
137
|
Numeric agreement with JiTCODE is checked separately on the curated sample
|
|
138
|
-
(``test_cross_validation_over_sample``)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
(``test_cross_validation_over_sample``); here we only assert each diffsol
|
|
139
|
+
trajectory is finite. ``HARD_TO_INTEGRATE | DIFFSOL_SKIP`` is honoured as
|
|
140
|
+
an escape hatch for systems that genuinely cannot be integrated, but both
|
|
141
|
+
sets are currently empty — the whole ODE catalogue integrates on diffsol.
|
|
142
142
|
"""
|
|
143
143
|
import sys
|
|
144
144
|
import zlib
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numeric validation of the ``tsdynamics-core`` Rust kernels.
|
|
3
|
+
|
|
4
|
+
Skipped entirely unless the optional accelerator is installed. These tests
|
|
5
|
+
prove the tape VM reproduces the symbolic RHS, that the RK4 stepper matches a
|
|
6
|
+
high-accuracy SciPy reference, and that the rayon ensemble runner is
|
|
7
|
+
deterministic and race-free (parallel == serial, bit for bit).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import tsdynamics as ts
|
|
16
|
+
from tsdynamics.backends import rustcore as rc
|
|
17
|
+
from tsdynamics.base.ode_base import ContinuousSystem
|
|
18
|
+
|
|
19
|
+
pytestmark = pytest.mark.skipif(
|
|
20
|
+
not rc.available(), reason="tsdynamics-core (Rust accelerator) is not installed"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_EVAL_SAMPLE = ["Lorenz", "Rossler", "Chen", "Thomas", "Halvorsen", "SprottA"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Tiny user-defined systems for the robustness/regression tests below. They
|
|
27
|
+
# register as non-builtin, so the builtin-only catalogue sweeps never see them.
|
|
28
|
+
class _Blowup(ContinuousSystem):
|
|
29
|
+
"""du/dt = u² — finite-time blow-up from u0 = 1 near t = 1."""
|
|
30
|
+
|
|
31
|
+
params: dict = {}
|
|
32
|
+
dim = 1
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _equations(y, t):
|
|
36
|
+
return [y(0) ** 2]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Twin(ContinuousSystem):
|
|
40
|
+
"""Both components are the bare symbol u0 → the tape CSEs to n_reg = 1 < dim."""
|
|
41
|
+
|
|
42
|
+
params: dict = {}
|
|
43
|
+
dim = 2
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _equations(y, t):
|
|
47
|
+
return [y(0), y(0)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize("name", _EVAL_SAMPLE)
|
|
51
|
+
def test_tape_eval_matches_symbolic_rhs(name: str) -> None:
|
|
52
|
+
"""The Rust tape evaluates the exact same RHS as the symbolic core."""
|
|
53
|
+
sys = getattr(ts, name)()
|
|
54
|
+
tape = rc.compile_tape(sys)
|
|
55
|
+
f = sys._rhs_numeric()
|
|
56
|
+
rng = np.random.default_rng(abs(hash(name)) % (2**32))
|
|
57
|
+
for _ in range(8):
|
|
58
|
+
u = rng.standard_normal(sys.dim)
|
|
59
|
+
t = float(rng.uniform(0.0, 5.0))
|
|
60
|
+
got = rc.eval_rhs(sys, u, t, tape=tape)
|
|
61
|
+
ref = f(u, t)
|
|
62
|
+
np.testing.assert_allclose(got, ref, rtol=1e-9, atol=1e-9)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
("name", "ic"),
|
|
67
|
+
[("Lorenz", [1.0, 1.0, 1.0]), ("Rossler", [0.1, 0.0, 0.0])],
|
|
68
|
+
)
|
|
69
|
+
def test_rk4_matches_scipy(name: str, ic: list[float]) -> None:
|
|
70
|
+
"""Fixed-step RK4 agrees with a high-accuracy SciPy reference."""
|
|
71
|
+
from scipy.integrate import solve_ivp
|
|
72
|
+
|
|
73
|
+
sys = getattr(ts, name)()
|
|
74
|
+
t_eval = np.linspace(0.0, 8.0, 801)
|
|
75
|
+
y = rc.integrate_dense(sys, ic, t_eval, method="RK4", h=1e-3)
|
|
76
|
+
assert y.shape == (t_eval.size, sys.dim)
|
|
77
|
+
f = sys._rhs_numeric()
|
|
78
|
+
sol = solve_ivp(lambda t, u: f(u, t), (0.0, 8.0), ic, t_eval=t_eval, rtol=1e-11, atol=1e-13)
|
|
79
|
+
# Chaotic divergence over 8 t.u. is bounded for matched truncation; require
|
|
80
|
+
# tight early agreement and finiteness throughout.
|
|
81
|
+
assert np.all(np.isfinite(y))
|
|
82
|
+
early = t_eval <= 3.0
|
|
83
|
+
np.testing.assert_allclose(y[early], sol.y.T[early], atol=1e-4)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize(
|
|
87
|
+
("name", "ic"),
|
|
88
|
+
[("Lorenz", [1.0, 1.0, 1.0]), ("Rossler", [1.0, 1.0, 1.0]), ("Thomas", [0.1, 0.0, 0.0])],
|
|
89
|
+
)
|
|
90
|
+
def test_rk45_matches_scipy(name: str, ic: list[float]) -> None:
|
|
91
|
+
"""Adaptive Dormand-Prince agrees with SciPy's RK45 at the same tolerance."""
|
|
92
|
+
from scipy.integrate import solve_ivp
|
|
93
|
+
|
|
94
|
+
sys = getattr(ts, name)()
|
|
95
|
+
rtol, atol = 1e-8, 1e-10
|
|
96
|
+
t_eval = np.linspace(0.0, 5.0, 501)
|
|
97
|
+
y = rc.integrate_dense(sys, ic, t_eval, method="RK45", rtol=rtol, atol=atol)
|
|
98
|
+
assert y.shape == (t_eval.size, sys.dim)
|
|
99
|
+
assert np.all(np.isfinite(y))
|
|
100
|
+
f = sys._rhs_numeric()
|
|
101
|
+
ref = solve_ivp(
|
|
102
|
+
lambda t, u: f(u, t), (0.0, 5.0), ic, t_eval=t_eval, rtol=rtol, atol=atol, method="RK45"
|
|
103
|
+
).y.T
|
|
104
|
+
# Two correct integrators on a chaotic flow diverge slowly; require tight
|
|
105
|
+
# agreement in the early window where Lyapunov amplification is small.
|
|
106
|
+
early = t_eval <= 2.0
|
|
107
|
+
np.testing.assert_allclose(y[early], ref[early], atol=1e-3)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_rk45_converges_with_tolerance() -> None:
|
|
111
|
+
"""Tightening the tolerance drives the error toward a high-accuracy reference."""
|
|
112
|
+
from scipy.integrate import solve_ivp
|
|
113
|
+
|
|
114
|
+
sys = ts.Lorenz()
|
|
115
|
+
ic = [1.0, 1.0, 1.0]
|
|
116
|
+
t_eval = np.linspace(0.0, 3.0, 301)
|
|
117
|
+
f = sys._rhs_numeric()
|
|
118
|
+
ref = solve_ivp(
|
|
119
|
+
lambda t, u: f(u, t), (0.0, 3.0), ic, t_eval=t_eval, rtol=1e-12, atol=1e-14, method="DOP853"
|
|
120
|
+
).y.T
|
|
121
|
+
err_loose = np.max(np.abs(rc.integrate_dense(sys, ic, t_eval, rtol=1e-6, atol=1e-9) - ref))
|
|
122
|
+
err_tight = np.max(np.abs(rc.integrate_dense(sys, ic, t_eval, rtol=1e-10, atol=1e-12) - ref))
|
|
123
|
+
assert err_tight < err_loose
|
|
124
|
+
assert err_tight < 1e-4 # adaptive at rtol=1e-10 tracks the reference closely
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_adaptive_ensemble_deterministic_and_matches_serial() -> None:
|
|
128
|
+
"""Adaptive ensemble == serial adaptive, bit for bit, and is reproducible."""
|
|
129
|
+
sys = ts.Lorenz()
|
|
130
|
+
rng = np.random.default_rng(3)
|
|
131
|
+
batch = rng.uniform(-12.0, 12.0, size=(400, 3))
|
|
132
|
+
fin = rc.ensemble_final(sys, batch, 0.0, 4.0, method="RK45", rtol=1e-8, atol=1e-10)
|
|
133
|
+
assert fin.shape == batch.shape and np.all(np.isfinite(fin))
|
|
134
|
+
np.testing.assert_array_equal(
|
|
135
|
+
fin, rc.ensemble_final(sys, batch, 0.0, 4.0, method="RK45", rtol=1e-8, atol=1e-10)
|
|
136
|
+
)
|
|
137
|
+
for i in (0, 200, 399):
|
|
138
|
+
serial = rc.integrate_dense(
|
|
139
|
+
sys, batch[i], np.array([0.0, 4.0]), method="RK45", rtol=1e-8, atol=1e-10
|
|
140
|
+
)[-1]
|
|
141
|
+
np.testing.assert_array_equal(fin[i], serial)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_ensemble_is_deterministic_and_race_free() -> None:
|
|
145
|
+
"""Fixed-step parallel ensemble == serial per-trajectory, bit for bit; all finite."""
|
|
146
|
+
sys = ts.Lorenz()
|
|
147
|
+
rng = np.random.default_rng(0)
|
|
148
|
+
batch = rng.uniform(-12.0, 12.0, size=(500, 3))
|
|
149
|
+
fin = rc.ensemble_final(sys, batch, 0.0, 4.0, method="RK4", h=2e-3)
|
|
150
|
+
assert fin.shape == batch.shape
|
|
151
|
+
assert np.all(np.isfinite(fin))
|
|
152
|
+
# Re-running must be identical (no nondeterministic reduction).
|
|
153
|
+
again = rc.ensemble_final(sys, batch, 0.0, 4.0, method="RK4", h=2e-3)
|
|
154
|
+
np.testing.assert_array_equal(fin, again)
|
|
155
|
+
# And it must equal the serial single-trajectory result for sampled rows.
|
|
156
|
+
for i in (0, 17, 250, 499):
|
|
157
|
+
serial = rc.integrate_dense(sys, batch[i], np.array([0.0, 4.0]), method="RK4", h=2e-3)[-1]
|
|
158
|
+
np.testing.assert_array_equal(fin[i], serial)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Robustness / regression — failure modes an adversarial review surfaced.
|
|
163
|
+
# Each must terminate (no infinite loop) and signal cleanly, not crash or lie.
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_divergent_trajectory_raises_not_hangs() -> None:
|
|
168
|
+
"""A blow-up must raise (not spin forever on a NaN error norm)."""
|
|
169
|
+
with pytest.raises(RuntimeError):
|
|
170
|
+
rc.integrate_dense(_Blowup(), [1.0], np.linspace(0.0, 5.0, 101), method="RK45")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_ensemble_divergent_ics_yield_nan_rows() -> None:
|
|
174
|
+
"""Diverging ICs become NaN rows; the batch finishes and finite ICs survive."""
|
|
175
|
+
batch = np.array([[0.1], [0.2], [5.0], [10.0]])
|
|
176
|
+
fin = rc.ensemble_final(_Blowup(), batch, 0.0, 3.0, method="RK45")
|
|
177
|
+
assert np.isfinite(fin[0, 0]) and np.isfinite(fin[1, 0])
|
|
178
|
+
assert np.isnan(fin[2, 0]) and np.isnan(fin[3, 0])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_degenerate_tape_nreg_below_dim_does_not_panic() -> None:
|
|
182
|
+
"""A tape with fewer registers than state components must integrate, not panic."""
|
|
183
|
+
y = rc.integrate_dense(_Twin(), [1.0, 1.0], np.linspace(0.0, 1.0, 11), method="RK45")
|
|
184
|
+
assert np.all(np.isfinite(y))
|
|
185
|
+
# Analytic: u0 = e^t, u1 = 1 + (e^t − 1) = e^t.
|
|
186
|
+
np.testing.assert_allclose(y[-1], [np.e, np.e], rtol=1e-4)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_noncontiguous_inputs_are_accepted() -> None:
|
|
190
|
+
"""Strided (non-C-contiguous) ic / t_eval must be coerced, not panic."""
|
|
191
|
+
lor = ts.Lorenz()
|
|
192
|
+
t_eval = np.linspace(0.0, 2.0, 41)[::2] # strided view
|
|
193
|
+
ic = np.array([1.0, 1.0, 1.0, 9.0])[:3] # strided-ish view
|
|
194
|
+
y = rc.integrate_dense(lor, ic, t_eval, method="RK45")
|
|
195
|
+
assert y.shape == (t_eval.size, 3) and np.all(np.isfinite(y))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_unknown_method_raises() -> None:
|
|
199
|
+
with pytest.raises(ValueError, match="unknown method"):
|
|
200
|
+
rc.integrate_dense(ts.Lorenz(), [1.0, 1.0, 1.0], np.linspace(0, 1, 5), method="DOP853")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_dimension_mismatch_raises() -> None:
|
|
204
|
+
lor = ts.Lorenz()
|
|
205
|
+
with pytest.raises(ValueError):
|
|
206
|
+
rc.integrate_dense(lor, [1.0, 1.0], np.linspace(0, 1, 5))
|
|
207
|
+
with pytest.raises(ValueError):
|
|
208
|
+
rc.ensemble_final(lor, np.zeros((5, 2)), 0.0, 1.0)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("method", ["RK45", "RK4"])
|
|
212
|
+
def test_empty_t_eval_returns_empty(method: str) -> None:
|
|
213
|
+
y = rc.integrate_dense(ts.Lorenz(), [1.0, 1.0, 1.0], np.array([]), method=method)
|
|
214
|
+
assert y.shape[0] == 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_version_is_exposed() -> None:
|
|
218
|
+
import tsdynamics_core
|
|
219
|
+
|
|
220
|
+
assert isinstance(tsdynamics_core._version(), str)
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Numeric validation of the ``tsdynamics-core`` Rust kernels.
|
|
3
|
-
|
|
4
|
-
Skipped entirely unless the optional accelerator is installed. These tests
|
|
5
|
-
prove the tape VM reproduces the symbolic RHS, that the RK4 stepper matches a
|
|
6
|
-
high-accuracy SciPy reference, and that the rayon ensemble runner is
|
|
7
|
-
deterministic and race-free (parallel == serial, bit for bit).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import numpy as np
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
|
-
import tsdynamics as ts
|
|
16
|
-
from tsdynamics.backends import rustcore as rc
|
|
17
|
-
|
|
18
|
-
pytestmark = pytest.mark.skipif(
|
|
19
|
-
not rc.available(), reason="tsdynamics-core (Rust accelerator) is not installed"
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
_EVAL_SAMPLE = ["Lorenz", "Rossler", "Chen", "Thomas", "Halvorsen", "SprottA"]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@pytest.mark.parametrize("name", _EVAL_SAMPLE)
|
|
26
|
-
def test_tape_eval_matches_symbolic_rhs(name: str) -> None:
|
|
27
|
-
"""The Rust tape evaluates the exact same RHS as the symbolic core."""
|
|
28
|
-
sys = getattr(ts, name)()
|
|
29
|
-
tape = rc.compile_tape(sys)
|
|
30
|
-
f = sys._rhs_numeric()
|
|
31
|
-
rng = np.random.default_rng(abs(hash(name)) % (2**32))
|
|
32
|
-
for _ in range(8):
|
|
33
|
-
u = rng.standard_normal(sys.dim)
|
|
34
|
-
t = float(rng.uniform(0.0, 5.0))
|
|
35
|
-
got = rc.eval_rhs(sys, u, t, tape=tape)
|
|
36
|
-
ref = f(u, t)
|
|
37
|
-
np.testing.assert_allclose(got, ref, rtol=1e-9, atol=1e-9)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@pytest.mark.parametrize(
|
|
41
|
-
("name", "ic"),
|
|
42
|
-
[("Lorenz", [1.0, 1.0, 1.0]), ("Rossler", [0.1, 0.0, 0.0])],
|
|
43
|
-
)
|
|
44
|
-
def test_rk4_matches_scipy(name: str, ic: list[float]) -> None:
|
|
45
|
-
"""Fixed-step RK4 agrees with a high-accuracy SciPy reference."""
|
|
46
|
-
from scipy.integrate import solve_ivp
|
|
47
|
-
|
|
48
|
-
sys = getattr(ts, name)()
|
|
49
|
-
t_eval = np.linspace(0.0, 8.0, 801)
|
|
50
|
-
y = rc.integrate_dense(sys, ic, t_eval, h=1e-3)
|
|
51
|
-
assert y.shape == (t_eval.size, sys.dim)
|
|
52
|
-
f = sys._rhs_numeric()
|
|
53
|
-
sol = solve_ivp(lambda t, u: f(u, t), (0.0, 8.0), ic, t_eval=t_eval, rtol=1e-11, atol=1e-13)
|
|
54
|
-
# Chaotic divergence over 8 t.u. is bounded for matched truncation; require
|
|
55
|
-
# tight early agreement and finiteness throughout.
|
|
56
|
-
assert np.all(np.isfinite(y))
|
|
57
|
-
early = t_eval <= 3.0
|
|
58
|
-
np.testing.assert_allclose(y[early], sol.y.T[early], atol=1e-4)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_ensemble_is_deterministic_and_race_free() -> None:
|
|
62
|
-
"""Parallel ensemble == serial per-trajectory, bit for bit; all finite."""
|
|
63
|
-
sys = ts.Lorenz()
|
|
64
|
-
rng = np.random.default_rng(0)
|
|
65
|
-
batch = rng.uniform(-12.0, 12.0, size=(500, 3))
|
|
66
|
-
fin = rc.ensemble_final(sys, batch, 0.0, 4.0, h=2e-3)
|
|
67
|
-
assert fin.shape == batch.shape
|
|
68
|
-
assert np.all(np.isfinite(fin))
|
|
69
|
-
# Re-running must be identical (no nondeterministic reduction).
|
|
70
|
-
again = rc.ensemble_final(sys, batch, 0.0, 4.0, h=2e-3)
|
|
71
|
-
np.testing.assert_array_equal(fin, again)
|
|
72
|
-
# And it must equal the serial single-trajectory result for sampled rows.
|
|
73
|
-
for i in (0, 17, 250, 499):
|
|
74
|
-
serial = rc.integrate_dense(sys, batch[i], np.array([0.0, 4.0]), h=2e-3)[-1]
|
|
75
|
-
np.testing.assert_array_equal(fin[i], serial)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_version_is_exposed() -> None:
|
|
79
|
-
import tsdynamics_core
|
|
80
|
-
|
|
81
|
-
assert isinstance(tsdynamics_core._version(), str)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/chaotic_attractors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/oscillatory_systems.py
RENAMED
|
File without changes
|
{tsdynamics-2.3.0 → tsdynamics-2.4.1}/src/tsdynamics/systems/continuous/population_dynamics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|