tsdynamics 2.4.0__tar.gz → 2.5.0__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.4.0 → tsdynamics-2.5.0}/PKG-INFO +1 -1
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/__init__.py +1 -1
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/backends/rustcore.py +91 -19
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/ode_base.py +9 -1
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +31 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/physical_systems.py +40 -1
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/_sampling.py +11 -16
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_diffsol_backend.py +4 -4
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_rustcore.py +112 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_rustcore_translation.py +29 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/.gitignore +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/LICENSE +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/README.md +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/pyproject.toml +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/analysis/fixed_points.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/analysis/lyapunov.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/analysis/orbit_diagram.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/analysis/poincare.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/backends/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/backends/diffsol.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/base.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/dde_base.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/map_base.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/base/protocol.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/ensemble.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/poincare.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/tangent.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/sampling.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/utils/__init__.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/utils/general.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/src/tsdynamics/utils/sagitta_dt.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/conftest.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_analysis.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_base.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_cache_isolation.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_dde_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_derived.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_diffsol_translation.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_jacobians.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_known_values.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_map_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_ode_rhs_symbolic.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_ode_systems.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_protocol.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_registry.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_sampling.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_signatures.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_smoke.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/tests/test_utils.py +0 -0
- {tsdynamics-2.4.0 → tsdynamics-2.5.0}/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.5.0
|
|
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.5.0"
|
|
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.
|
|
@@ -12,15 +12,17 @@ optional accelerator is installed::
|
|
|
12
12
|
|
|
13
13
|
pip install tsdynamics-core # or the [rustcore] extra
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
Solvers (select with ``method=``): fixed-step ``RK4``, adaptive Dormand-Prince
|
|
16
|
+
``RK45`` (default), and an L-stable linearly-implicit ``stiff`` kernel (aliases
|
|
17
|
+
``Rosenbrock``/``LSODA``/``BDF``) that uses the system's analytic Jacobian —
|
|
18
|
+
also lowered into the tape. Limitations (experimental): ODEs only; the RHS must
|
|
19
|
+
use functions the tape VM provides (the same set the DiffSL backend supports).
|
|
20
|
+
Unsupported constructs raise :class:`TapeCompileError`.
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
from __future__ import annotations
|
|
22
24
|
|
|
23
|
-
from dataclasses import dataclass
|
|
25
|
+
from dataclasses import dataclass, field
|
|
24
26
|
from typing import Any
|
|
25
27
|
|
|
26
28
|
import numpy as np
|
|
@@ -101,6 +103,9 @@ class CompiledTape:
|
|
|
101
103
|
n_state: int
|
|
102
104
|
n_param: int
|
|
103
105
|
control_names: list[str]
|
|
106
|
+
# Register index of each Jacobian entry, row-major dim×dim; empty unless the
|
|
107
|
+
# tape was compiled with_jacobian=True (only the stiff solver needs it).
|
|
108
|
+
jac_outputs: np.ndarray = field(default_factory=lambda: np.empty(0, dtype=np.int32))
|
|
104
109
|
|
|
105
110
|
|
|
106
111
|
class _Emitter:
|
|
@@ -191,18 +196,24 @@ class _Emitter:
|
|
|
191
196
|
return self._push(OP_POW, a=base_reg, b=self.emit(exp))
|
|
192
197
|
|
|
193
198
|
|
|
194
|
-
def compile_tape(system: Any) -> CompiledTape:
|
|
199
|
+
def compile_tape(system: Any, *, with_jacobian: bool = False) -> CompiledTape:
|
|
195
200
|
"""
|
|
196
201
|
Lower a :class:`~tsdynamics.base.ContinuousSystem`'s RHS to a tape.
|
|
197
202
|
|
|
198
203
|
Structural parameters are folded to constants; control parameters become
|
|
199
204
|
inputs in ``control_names`` order (the layout the solve-time params vector
|
|
200
205
|
must follow).
|
|
206
|
+
|
|
207
|
+
With ``with_jacobian=True`` the analytic Jacobian ``∂f_k/∂u_j`` is
|
|
208
|
+
differentiated symbolically and emitted into the *same* tape (sharing
|
|
209
|
+
common subexpressions with the RHS) — the stiff solver consumes it.
|
|
201
210
|
"""
|
|
202
211
|
import symengine
|
|
203
212
|
from jitcode import t as t_sym
|
|
204
213
|
from jitcode import y
|
|
205
214
|
|
|
215
|
+
from tsdynamics.base.ode_base import _resolve_derivative_nodes
|
|
216
|
+
|
|
206
217
|
dim = system.dim
|
|
207
218
|
struct_vals = system._structural_vals()
|
|
208
219
|
control_names = list(system._control_params())
|
|
@@ -212,11 +223,22 @@ def compile_tape(system: Any) -> CompiledTape:
|
|
|
212
223
|
if len(exprs) != dim:
|
|
213
224
|
raise ValueError(f"_equations must return {dim} expressions, got {len(exprs)}")
|
|
214
225
|
|
|
215
|
-
|
|
226
|
+
u_syms = [symengine.Symbol(f"u{i}") for i in range(dim)]
|
|
227
|
+
subs = {y(i): u_syms[i] for i in range(dim)}
|
|
216
228
|
subs[t_sym] = symengine.Symbol("t")
|
|
229
|
+
rhs = [symengine.sympify(e).subs(subs) for e in exprs]
|
|
217
230
|
|
|
218
231
|
em = _Emitter()
|
|
219
|
-
outputs = [em.emit(
|
|
232
|
+
outputs = [em.emit(e._sympy_()) for e in rhs]
|
|
233
|
+
jac_outputs: list[int] = []
|
|
234
|
+
if with_jacobian:
|
|
235
|
+
# Row-major dim×dim: ∂f_k/∂u_j. _resolve_derivative_nodes rewrites the
|
|
236
|
+
# unevaluated d|u|/du → sign(u) and d·sign/du → 0 a.e. (the same a.e.
|
|
237
|
+
# convention the jitcode/diffsol Jacobian autogen uses), so systems with
|
|
238
|
+
# abs/sign still lower cleanly.
|
|
239
|
+
for e in rhs:
|
|
240
|
+
for s in u_syms:
|
|
241
|
+
jac_outputs.append(em.emit(_resolve_derivative_nodes(e.diff(s))._sympy_()))
|
|
220
242
|
|
|
221
243
|
return CompiledTape(
|
|
222
244
|
ops=np.asarray(em.ops, dtype=np.int32),
|
|
@@ -227,6 +249,7 @@ def compile_tape(system: Any) -> CompiledTape:
|
|
|
227
249
|
n_state=dim,
|
|
228
250
|
n_param=len(control_names),
|
|
229
251
|
control_names=control_names,
|
|
252
|
+
jac_outputs=np.asarray(jac_outputs, dtype=np.int32),
|
|
230
253
|
)
|
|
231
254
|
|
|
232
255
|
|
|
@@ -253,23 +276,57 @@ def _args(tape: CompiledTape):
|
|
|
253
276
|
return (tape.ops, tape.a, tape.b, tape.imm, tape.outputs, tape.n_state, tape.n_param)
|
|
254
277
|
|
|
255
278
|
|
|
256
|
-
|
|
257
|
-
#
|
|
258
|
-
|
|
279
|
+
def _args_stiff(tape: CompiledTape):
|
|
280
|
+
# Stiff kernels take jac_outputs after outputs (mirrors the Rust signature).
|
|
281
|
+
return (
|
|
282
|
+
tape.ops,
|
|
283
|
+
tape.a,
|
|
284
|
+
tape.b,
|
|
285
|
+
tape.imm,
|
|
286
|
+
tape.outputs,
|
|
287
|
+
tape.jac_outputs,
|
|
288
|
+
tape.n_state,
|
|
289
|
+
tape.n_param,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# Method names → kernel. RK4 = fixed step; RK45/dopri5 = adaptive Dormand-Prince
|
|
294
|
+
# 5(4); stiff = L-stable linearly-implicit solver with the analytic Jacobian.
|
|
295
|
+
# The SciPy-style stiff names (LSODA/BDF/Radau) route to the stiff kernel so
|
|
296
|
+
# `method="LSODA"` does the expected thing rather than silently downgrading.
|
|
259
297
|
_FIXED_RK4 = {"RK4", "rk4"}
|
|
260
298
|
_ADAPTIVE = {"RK45", "rk45", "dopri5", "DP45"}
|
|
261
|
-
|
|
299
|
+
_STIFF = {
|
|
300
|
+
"stiff",
|
|
301
|
+
"Rosenbrock",
|
|
302
|
+
"rosenbrock",
|
|
303
|
+
"ROS1",
|
|
304
|
+
"LSODA",
|
|
305
|
+
"lsoda",
|
|
306
|
+
"BDF",
|
|
307
|
+
"bdf",
|
|
308
|
+
"Radau",
|
|
309
|
+
"radau",
|
|
310
|
+
}
|
|
311
|
+
_METHODS = _FIXED_RK4 | _ADAPTIVE | _STIFF
|
|
262
312
|
|
|
263
313
|
|
|
264
314
|
def _check_method(method: str) -> None:
|
|
265
315
|
if method not in _METHODS:
|
|
266
316
|
raise ValueError(
|
|
267
317
|
f"rustcore: unknown method {method!r}; supported: {sorted(_METHODS)} "
|
|
268
|
-
"(RK4 = fixed step, RK45/dopri5 = adaptive
|
|
269
|
-
"
|
|
318
|
+
"(RK4 = fixed step, RK45/dopri5 = adaptive explicit, "
|
|
319
|
+
"stiff/Rosenbrock/LSODA/BDF = L-stable implicit)."
|
|
270
320
|
)
|
|
271
321
|
|
|
272
322
|
|
|
323
|
+
def _stiff_tape(system: Any, tape: CompiledTape | None) -> CompiledTape:
|
|
324
|
+
"""Return a tape carrying the analytic Jacobian (recompiled if the given one lacks it)."""
|
|
325
|
+
if tape is not None and tape.jac_outputs.size:
|
|
326
|
+
return tape
|
|
327
|
+
return compile_tape(system, with_jacobian=True)
|
|
328
|
+
|
|
329
|
+
|
|
273
330
|
def _as_state(x: Any, dim: int, name: str) -> np.ndarray:
|
|
274
331
|
"""Coerce to a contiguous float64 1-D vector of length ``dim`` (or raise)."""
|
|
275
332
|
a = np.ascontiguousarray(x, dtype=np.float64).ravel()
|
|
@@ -303,14 +360,16 @@ def integrate_dense(
|
|
|
303
360
|
|
|
304
361
|
``method="RK45"`` (default) uses error-controlled adaptive Dormand-Prince
|
|
305
362
|
5(4) with Hermite dense output (``rtol``/``atol`` set the tolerance);
|
|
306
|
-
``method="RK4"`` uses fixed-step RK4 with internal step ``h
|
|
363
|
+
``method="RK4"`` uses fixed-step RK4 with internal step ``h``;
|
|
364
|
+
``method="stiff"`` (aliases ``Rosenbrock``/``LSODA``/``BDF``) uses an
|
|
365
|
+
L-stable linearly-implicit solver with the system's analytic Jacobian.
|
|
307
366
|
|
|
308
367
|
Raises ``RuntimeError`` if the trajectory diverges or the step collapses
|
|
309
368
|
before reaching the final time (matching the jitcode/diffsol backends).
|
|
310
369
|
"""
|
|
311
370
|
_check_method(method)
|
|
312
371
|
core = _require()
|
|
313
|
-
tape = tape or compile_tape(system)
|
|
372
|
+
tape = _stiff_tape(system, tape) if method in _STIFF else (tape or compile_tape(system))
|
|
314
373
|
t_eval = np.ascontiguousarray(t_eval, dtype=np.float64)
|
|
315
374
|
ic = _as_state(ic, tape.n_state, "ic")
|
|
316
375
|
p = _params_vec(system, tape)
|
|
@@ -318,6 +377,12 @@ def integrate_dense(
|
|
|
318
377
|
if h is None:
|
|
319
378
|
h = float(np.min(np.diff(t_eval))) if t_eval.size > 1 else 1e-2
|
|
320
379
|
y = np.asarray(core.integrate_dense_py(*_args(tape), ic, p, t_eval, float(h)))
|
|
380
|
+
elif method in _STIFF:
|
|
381
|
+
y = np.asarray(
|
|
382
|
+
core.integrate_dense_stiff_py(
|
|
383
|
+
*_args_stiff(tape), ic, p, t_eval, float(rtol), float(atol)
|
|
384
|
+
)
|
|
385
|
+
)
|
|
321
386
|
else:
|
|
322
387
|
y = np.asarray(
|
|
323
388
|
core.integrate_dense_rk45_py(*_args(tape), ic, p, t_eval, float(rtol), float(atol))
|
|
@@ -346,8 +411,9 @@ def ensemble_final(
|
|
|
346
411
|
|
|
347
412
|
``u0_batch`` is ``(n, dim)``; each row is integrated from ``t0`` to ``t1``
|
|
348
413
|
and its final state returned as a row of the ``(n, dim)`` result.
|
|
349
|
-
``method`` selects the adaptive (``"RK45"``, default)
|
|
350
|
-
(``"RK4"``) kernel, as in
|
|
414
|
+
``method`` selects the adaptive (``"RK45"``, default), fixed-step
|
|
415
|
+
(``"RK4"``), or stiff (``"stiff"``/``"LSODA"``) kernel, as in
|
|
416
|
+
:func:`integrate_dense`.
|
|
351
417
|
|
|
352
418
|
A trajectory that diverges (escapes to infinity) yields a row of ``NaN``
|
|
353
419
|
rather than aborting the batch — the basin/ensemble caller classifies those
|
|
@@ -355,7 +421,7 @@ def ensemble_final(
|
|
|
355
421
|
"""
|
|
356
422
|
_check_method(method)
|
|
357
423
|
core = _require()
|
|
358
|
-
tape = tape or compile_tape(system)
|
|
424
|
+
tape = _stiff_tape(system, tape) if method in _STIFF else (tape or compile_tape(system))
|
|
359
425
|
u0_batch = np.ascontiguousarray(u0_batch, dtype=np.float64)
|
|
360
426
|
if u0_batch.ndim != 2 or u0_batch.shape[1] != tape.n_state:
|
|
361
427
|
raise ValueError(
|
|
@@ -368,6 +434,12 @@ def ensemble_final(
|
|
|
368
434
|
*_args(tape), u0_batch, p, float(t0), float(t1), float(h)
|
|
369
435
|
)
|
|
370
436
|
)
|
|
437
|
+
if method in _STIFF:
|
|
438
|
+
return np.asarray(
|
|
439
|
+
core.integrate_ensemble_final_stiff_py(
|
|
440
|
+
*_args_stiff(tape), u0_batch, p, float(t0), float(t1), float(rtol), float(atol)
|
|
441
|
+
)
|
|
442
|
+
)
|
|
371
443
|
return np.asarray(
|
|
372
444
|
core.integrate_ensemble_final_rk45_py(
|
|
373
445
|
*_args(tape), u0_batch, p, float(t0), float(t1), float(rtol), float(atol)
|
|
@@ -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.4.0 → tsdynamics-2.5.0}/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
|
|
@@ -47,6 +47,40 @@ class _Twin(ContinuousSystem):
|
|
|
47
47
|
return [y(0), y(0)]
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
class _LinStiff(ContinuousSystem):
|
|
51
|
+
"""y' = -k y — large k stresses L-stability (exact decay to ~0)."""
|
|
52
|
+
|
|
53
|
+
params = {"k": 1000.0}
|
|
54
|
+
dim = 1
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _equations(y, t, *, k):
|
|
58
|
+
return [-k * y(0)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _VdPStiff(ContinuousSystem):
|
|
62
|
+
"""Stiff van der Pol oscillator (μ = 1000)."""
|
|
63
|
+
|
|
64
|
+
params = {"mu": 1000.0}
|
|
65
|
+
dim = 2
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _equations(y, t, *, mu):
|
|
69
|
+
return [y(1), mu * (1 - y(0) ** 2) * y(1) - y(0)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _Robertson(ContinuousSystem):
|
|
73
|
+
"""Robertson's stiff chemical-kinetics problem."""
|
|
74
|
+
|
|
75
|
+
params: dict = {}
|
|
76
|
+
dim = 3
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _equations(y, t):
|
|
80
|
+
a, b, c = y(0), y(1), y(2)
|
|
81
|
+
return [-0.04 * a + 1e4 * b * c, 0.04 * a - 1e4 * b * c - 3e7 * b * b, 3e7 * b * b]
|
|
82
|
+
|
|
83
|
+
|
|
50
84
|
@pytest.mark.parametrize("name", _EVAL_SAMPLE)
|
|
51
85
|
def test_tape_eval_matches_symbolic_rhs(name: str) -> None:
|
|
52
86
|
"""The Rust tape evaluates the exact same RHS as the symbolic core."""
|
|
@@ -214,6 +248,84 @@ def test_empty_t_eval_returns_empty(method: str) -> None:
|
|
|
214
248
|
assert y.shape[0] == 0
|
|
215
249
|
|
|
216
250
|
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Stiff solver — L-stable linearly-implicit kernel with the analytic Jacobian.
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_stiff_is_l_stable() -> None:
|
|
257
|
+
"""y' = -1000 y integrates accurately to ~0 (an explicit method would blow up)."""
|
|
258
|
+
sys = _LinStiff()
|
|
259
|
+
t_eval = np.linspace(0.0, 1.0, 101)
|
|
260
|
+
y = rc.integrate_dense(sys, [1.0], t_eval, method="stiff", rtol=1e-7, atol=1e-9)
|
|
261
|
+
assert np.all(np.isfinite(y))
|
|
262
|
+
np.testing.assert_allclose(y[:, 0], np.exp(-1000.0 * t_eval), atol=1e-6)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@pytest.mark.parametrize(
|
|
266
|
+
("sys_factory", "ic", "T", "ref_method"),
|
|
267
|
+
[
|
|
268
|
+
(_VdPStiff, [2.0, 0.0], 30.0, "Radau"),
|
|
269
|
+
(_Robertson, [1.0, 0.0, 0.0], 40.0, "Radau"),
|
|
270
|
+
],
|
|
271
|
+
)
|
|
272
|
+
def test_stiff_matches_scipy(sys_factory, ic, T, ref_method) -> None:
|
|
273
|
+
"""The stiff kernel reproduces SciPy's implicit solvers on classic stiff problems."""
|
|
274
|
+
from scipy.integrate import solve_ivp
|
|
275
|
+
|
|
276
|
+
sys = sys_factory()
|
|
277
|
+
t_eval = np.linspace(0.0, T, 200)
|
|
278
|
+
y = rc.integrate_dense(sys, ic, t_eval, method="stiff", rtol=1e-7, atol=1e-10)
|
|
279
|
+
assert np.all(np.isfinite(y))
|
|
280
|
+
f = sys._rhs_numeric()
|
|
281
|
+
ref = solve_ivp(
|
|
282
|
+
lambda t, u: f(u, t), (0.0, T), ic, t_eval=t_eval, rtol=1e-11, atol=1e-12, method=ref_method
|
|
283
|
+
).y.T
|
|
284
|
+
np.testing.assert_allclose(y, ref, rtol=1e-3, atol=1e-6)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_stiff_handles_abs_sign_systems() -> None:
|
|
288
|
+
"""A piecewise-linear system (Chua) integrates via the resolved-sign Jacobian."""
|
|
289
|
+
from scipy.integrate import solve_ivp
|
|
290
|
+
|
|
291
|
+
sys = ts.Chua()
|
|
292
|
+
ic = sys.resolve_ic(0.1 * np.ones(sys.dim))
|
|
293
|
+
t_eval = np.linspace(0.0, 4.0, 201)
|
|
294
|
+
y = rc.integrate_dense(sys, ic, t_eval, method="stiff", rtol=1e-7, atol=1e-9)
|
|
295
|
+
assert np.all(np.isfinite(y))
|
|
296
|
+
f = sys._rhs_numeric()
|
|
297
|
+
ref = solve_ivp(
|
|
298
|
+
lambda t, u: f(u, t), (0.0, 4.0), ic, t_eval=t_eval, rtol=1e-10, atol=1e-12, method="LSODA"
|
|
299
|
+
).y.T
|
|
300
|
+
early = t_eval <= 2.0
|
|
301
|
+
np.testing.assert_allclose(y[early], ref[early], atol=1e-3)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_stiff_method_aliases_route_to_stiff_kernel() -> None:
|
|
305
|
+
"""LSODA/BDF names reach the stiff kernel (no silent downgrade to explicit)."""
|
|
306
|
+
sys = _LinStiff()
|
|
307
|
+
t_eval = np.linspace(0.0, 1.0, 51)
|
|
308
|
+
for alias in ("LSODA", "BDF", "Rosenbrock"):
|
|
309
|
+
y = rc.integrate_dense(sys, [1.0], t_eval, method=alias, rtol=1e-7, atol=1e-9)
|
|
310
|
+
np.testing.assert_allclose(y[:, 0], np.exp(-1000.0 * t_eval), atol=1e-6)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_stiff_ensemble_deterministic_and_matches_serial() -> None:
|
|
314
|
+
sys = _VdPStiff()
|
|
315
|
+
rng = np.random.default_rng(0)
|
|
316
|
+
batch = rng.uniform(-2.0, 2.0, size=(200, 2))
|
|
317
|
+
fin = rc.ensemble_final(sys, batch, 0.0, 5.0, method="stiff", rtol=1e-6, atol=1e-8)
|
|
318
|
+
assert fin.shape == batch.shape and np.all(np.isfinite(fin))
|
|
319
|
+
np.testing.assert_array_equal(
|
|
320
|
+
fin, rc.ensemble_final(sys, batch, 0.0, 5.0, method="stiff", rtol=1e-6, atol=1e-8)
|
|
321
|
+
)
|
|
322
|
+
for i in (0, 100, 199):
|
|
323
|
+
serial = rc.integrate_dense(
|
|
324
|
+
sys, batch[i], np.array([0.0, 5.0]), method="stiff", rtol=1e-6, atol=1e-8
|
|
325
|
+
)[-1]
|
|
326
|
+
np.testing.assert_array_equal(fin[i], serial)
|
|
327
|
+
|
|
328
|
+
|
|
217
329
|
def test_version_is_exposed() -> None:
|
|
218
330
|
import tsdynamics_core
|
|
219
331
|
|
|
@@ -47,3 +47,32 @@ def test_tape_coverage_is_total() -> None:
|
|
|
47
47
|
except Exception as exc: # noqa: BLE001 — record, don't abort
|
|
48
48
|
failed.append((e.name, str(exc).splitlines()[0][:60]))
|
|
49
49
|
assert not failed, f"{len(failed)} ODE systems no longer compile to a tape: {failed}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_every_ode_jacobian_compiles_to_tape(ode_entry) -> None:
|
|
53
|
+
"""The analytic Jacobian must lower too (stiff solver) — incl. abs/sign systems."""
|
|
54
|
+
cls = ode_entry.cls
|
|
55
|
+
try:
|
|
56
|
+
tape = compile_tape(cls(), with_jacobian=True)
|
|
57
|
+
except TapeCompileError as exc:
|
|
58
|
+
raise AssertionError(
|
|
59
|
+
f"{ode_entry.name}: Jacobian no longer lowers to a tape — {exc}"
|
|
60
|
+
) from exc
|
|
61
|
+
dim = cls().dim
|
|
62
|
+
# One register per Jacobian entry, row-major dim×dim.
|
|
63
|
+
assert tape.jac_outputs.size == dim * dim
|
|
64
|
+
n = tape.ops.size
|
|
65
|
+
assert tape.jac_outputs.size == 0 or (
|
|
66
|
+
tape.jac_outputs.max() < n and tape.jac_outputs.min() >= 0
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_jacobian_coverage_is_total() -> None:
|
|
71
|
+
"""Every ODE's analytic Jacobian lowers — abs/sign derivatives resolved a.e."""
|
|
72
|
+
failed = []
|
|
73
|
+
for e in registry.all_systems(family="ode"):
|
|
74
|
+
try:
|
|
75
|
+
compile_tape(e.cls(), with_jacobian=True)
|
|
76
|
+
except Exception as exc: # noqa: BLE001 — record, don't abort
|
|
77
|
+
failed.append((e.name, str(exc).splitlines()[0][:60]))
|
|
78
|
+
assert not failed, f"{len(failed)} ODE Jacobians no longer compile to a tape: {failed}"
|
|
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.4.0 → tsdynamics-2.5.0}/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.4.0 → tsdynamics-2.5.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py
RENAMED
|
File without changes
|
{tsdynamics-2.4.0 → tsdynamics-2.5.0}/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
|