tsdynamics 2.1.1__tar.gz → 2.2.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.1.1 → tsdynamics-2.2.0}/PKG-INFO +1 -1
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/__init__.py +1 -1
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/ode_base.py +18 -6
- tsdynamics-2.2.0/tests/test_diffsol_backend.py +169 -0
- tsdynamics-2.2.0/tests/test_diffsol_translation.py +42 -0
- tsdynamics-2.1.1/tests/test_diffsol_backend.py +0 -76
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/.gitignore +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/LICENSE +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/README.md +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/pyproject.toml +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/analysis/fixed_points.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/analysis/lyapunov.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/analysis/orbit_diagram.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/analysis/poincare.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/backends/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/backends/diffsol.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/base.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/dde_base.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/map_base.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/base/protocol.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/ensemble.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/poincare.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/tangent.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/sampling.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/utils/__init__.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/utils/general.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/utils/sagitta_dt.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/_sampling.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/conftest.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_analysis.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_base.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_cache_isolation.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_dde_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_derived.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_jacobians.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_known_values.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_map_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_ode_rhs_symbolic.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_ode_systems.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_protocol.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_registry.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_sampling.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_signatures.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_smoke.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.0}/tests/test_utils.py +0 -0
- {tsdynamics-2.1.1 → tsdynamics-2.2.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.2.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.2.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.
|
|
@@ -718,11 +718,14 @@ class ContinuousSystem(SystemBase, ABC):
|
|
|
718
718
|
``tsit45`` / ``bdf`` / ``tr_bdf2`` / ``esdirk34``.
|
|
719
719
|
rtol, atol : float
|
|
720
720
|
Solver tolerances (default 1e-6 / 1e-9).
|
|
721
|
-
backend : {"jitcode", "diffsol"}
|
|
722
|
-
``"jitcode"`` (default) compiles
|
|
723
|
-
|
|
724
|
-
(``pip install tsdynamics[diffsol]
|
|
725
|
-
|
|
721
|
+
backend : {"jitcode", "diffsol", "auto"}
|
|
722
|
+
``"jitcode"`` (default) compiles the RHS to C; ``"diffsol"`` uses
|
|
723
|
+
the Rust solver suite via LLVM JIT — no C compiler, prebuilt
|
|
724
|
+
wheels (``pip install tsdynamics[diffsol]``), ~10× faster on small
|
|
725
|
+
chaotic systems, and validated against JiTCODE across the whole
|
|
726
|
+
ODE catalogue (see :mod:`tsdynamics.backends.diffsol`). ``"auto"``
|
|
727
|
+
picks ``"diffsol"`` when it is installed, else ``"jitcode"`` —
|
|
728
|
+
the recommended zero-compiler fast path.
|
|
726
729
|
**integrator_kwargs
|
|
727
730
|
Forwarded to ``jitcode.set_integrator`` (e.g. ``max_step``).
|
|
728
731
|
|
|
@@ -731,6 +734,15 @@ class ContinuousSystem(SystemBase, ABC):
|
|
|
731
734
|
Trajectory
|
|
732
735
|
Supports tuple-unpacking: ``t, y = sys.integrate(...)``.
|
|
733
736
|
"""
|
|
737
|
+
if backend == "auto":
|
|
738
|
+
# Prefer the zero-compiler Rust path when its optional dependency
|
|
739
|
+
# is installed; otherwise fall back to the always-available
|
|
740
|
+
# JiTCODE path. Lets `tsdynamics[diffsol]` users get the fast
|
|
741
|
+
# backend without naming it, with no surprise for everyone else.
|
|
742
|
+
from tsdynamics.backends import diffsol as _diffsol
|
|
743
|
+
|
|
744
|
+
backend = "diffsol" if _diffsol.available() else "jitcode"
|
|
745
|
+
|
|
734
746
|
if backend == "diffsol":
|
|
735
747
|
from tsdynamics.backends import diffsol as _diffsol
|
|
736
748
|
|
|
@@ -754,7 +766,7 @@ class ContinuousSystem(SystemBase, ABC):
|
|
|
754
766
|
),
|
|
755
767
|
)
|
|
756
768
|
if backend != "jitcode":
|
|
757
|
-
raise ValueError(f"Unknown backend {backend!r}; use 'jitcode' or '
|
|
769
|
+
raise ValueError(f"Unknown backend {backend!r}; use 'jitcode', 'diffsol', or 'auto'.")
|
|
758
770
|
|
|
759
771
|
method = method or self._default_method
|
|
760
772
|
integ_name = _INTEGRATOR_MAP.get(method, method)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-validation of the experimental diffsol backend against JiTCODE.
|
|
3
|
+
|
|
4
|
+
Skipped entirely when the ``tsdynamics[diffsol]`` extra is not installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
pytest.importorskip("pydiffsol")
|
|
13
|
+
|
|
14
|
+
import tsdynamics as ts
|
|
15
|
+
from tsdynamics.backends import diffsol as dsl
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Translator (fast)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_to_diffsl_lorenz_structure() -> None:
|
|
23
|
+
code, control = dsl.to_diffsl(ts.Lorenz())
|
|
24
|
+
assert control == ["sigma", "rho", "beta"]
|
|
25
|
+
assert "in_i {" in code and "u_i {" in code and "F_i {" in code
|
|
26
|
+
assert "ic0" in code and "ic2" in code
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_unknown_backend_rejected() -> None:
|
|
30
|
+
with pytest.raises(ValueError, match="Unknown backend"):
|
|
31
|
+
ts.Lorenz().integrate(final_time=1.0, dt=0.1, backend="quantum")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_auto_backend_uses_diffsol_when_available() -> None:
|
|
35
|
+
"""With pydiffsol installed, backend='auto' routes to diffsol."""
|
|
36
|
+
traj = ts.Lorenz().integrate(final_time=1.0, dt=0.1, ic=[1.0, 1.0, 1.0], backend="auto")
|
|
37
|
+
assert traj.meta["backend"] == "diffsol"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Solving + cross-validation (no C compiler needed — pure LLVM JIT)
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_lorenz_diffsol_short_integration() -> None:
|
|
46
|
+
traj = ts.Lorenz().integrate(
|
|
47
|
+
final_time=2.0, dt=0.01, ic=[1.0, 1.0, 1.0], backend="diffsol", rtol=1e-9, atol=1e-11
|
|
48
|
+
)
|
|
49
|
+
assert traj.y.shape == (201, 3) # grid includes the endpoint
|
|
50
|
+
assert np.all(np.isfinite(traj.y))
|
|
51
|
+
assert traj.meta["backend"] == "diffsol"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_param_change_reuses_compiled_module() -> None:
|
|
55
|
+
a = ts.Lorenz().integrate(final_time=1.0, dt=0.1, ic=[1.0, 1.0, 1.0], backend="diffsol")
|
|
56
|
+
before = dict(dsl._ODE_CACHE)
|
|
57
|
+
b = ts.Lorenz(params={"rho": 35.0}).integrate(
|
|
58
|
+
final_time=1.0, dt=0.1, ic=[1.0, 1.0, 1.0], backend="diffsol"
|
|
59
|
+
)
|
|
60
|
+
assert dict(dsl._ODE_CACHE) == before # no recompilation
|
|
61
|
+
assert not np.allclose(a.y[-1], b.y[-1]) # but different dynamics
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.slow
|
|
65
|
+
@pytest.mark.parametrize("name,ic", [("Lorenz", [1.0, 1.0, 1.0]), ("Rossler", [1.0, 0.0, 0.0])])
|
|
66
|
+
def test_cross_validation_against_jitcode(name: str, ic: list) -> None:
|
|
67
|
+
"""Both backends must agree to tight tolerance over a short window."""
|
|
68
|
+
sys_a = getattr(ts, name)()
|
|
69
|
+
sys_b = getattr(ts, name)()
|
|
70
|
+
kw = dict(final_time=5.0, dt=0.01, ic=ic, rtol=1e-10, atol=1e-12)
|
|
71
|
+
ref = sys_a.integrate(method="dop853", **kw)
|
|
72
|
+
alt = sys_b.integrate(backend="diffsol", **kw)
|
|
73
|
+
np.testing.assert_allclose(alt.y, ref.y, rtol=1e-5, atol=1e-6)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.slow
|
|
77
|
+
def test_cross_validation_over_sample() -> None:
|
|
78
|
+
"""
|
|
79
|
+
diffsol (BDF) reproduces JiTCODE (dop853) across the curated ODE sample.
|
|
80
|
+
|
|
81
|
+
Short horizon + tight tolerances keep chaotic sensitivity from masking a
|
|
82
|
+
real translator/solver discrepancy; max abs deviation must stay < 1e-3.
|
|
83
|
+
"""
|
|
84
|
+
import sys
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
|
|
87
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
88
|
+
from _sampling import INTEGRATION_SAMPLE
|
|
89
|
+
|
|
90
|
+
from tsdynamics import registry
|
|
91
|
+
|
|
92
|
+
bad = []
|
|
93
|
+
for name in INTEGRATION_SAMPLE:
|
|
94
|
+
cls = registry.get(name).cls
|
|
95
|
+
ic = cls().resolve_ic(None)
|
|
96
|
+
yj = (
|
|
97
|
+
cls()
|
|
98
|
+
.integrate(ic=ic, final_time=1.5, dt=0.03, method="dop853", rtol=1e-10, atol=1e-12)
|
|
99
|
+
.y
|
|
100
|
+
)
|
|
101
|
+
yd = (
|
|
102
|
+
cls()
|
|
103
|
+
.integrate(
|
|
104
|
+
ic=ic,
|
|
105
|
+
final_time=1.5,
|
|
106
|
+
dt=0.03,
|
|
107
|
+
backend="diffsol",
|
|
108
|
+
method="LSODA",
|
|
109
|
+
rtol=1e-10,
|
|
110
|
+
atol=1e-12,
|
|
111
|
+
)
|
|
112
|
+
.y
|
|
113
|
+
)
|
|
114
|
+
n = min(len(yj), len(yd))
|
|
115
|
+
dev = float(np.max(np.abs(yj[:n] - yd[:n])))
|
|
116
|
+
if dev >= 1e-3:
|
|
117
|
+
bad.append((name, dev))
|
|
118
|
+
assert not bad, f"diffsol disagrees with jitcode on: {bad}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.slow
|
|
122
|
+
def test_stiff_solver_path() -> None:
|
|
123
|
+
"""The BDF mapping handles a stiff-ish problem."""
|
|
124
|
+
traj = ts.Lorenz().integrate(
|
|
125
|
+
final_time=2.0, dt=0.01, ic=[1.0, 1.0, 1.0], backend="diffsol", method="LSODA"
|
|
126
|
+
)
|
|
127
|
+
assert np.all(np.isfinite(traj.y))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.full
|
|
131
|
+
def test_cross_validation_full_catalogue() -> None:
|
|
132
|
+
"""
|
|
133
|
+
The gate for flipping the default backend to diffsol: *every* built-in ODE
|
|
134
|
+
must integrate on diffsol (BDF) and agree with JiTCODE (dop853) to <1e-3
|
|
135
|
+
over a short window. Runs nightly (``-m full``) with the diffsol extra.
|
|
136
|
+
"""
|
|
137
|
+
from tsdynamics import registry
|
|
138
|
+
|
|
139
|
+
bad = []
|
|
140
|
+
for e in registry.all_systems(family="ode"):
|
|
141
|
+
cls = e.cls
|
|
142
|
+
try:
|
|
143
|
+
ic = cls().resolve_ic(None)
|
|
144
|
+
yj = (
|
|
145
|
+
cls()
|
|
146
|
+
.integrate(ic=ic, final_time=1.0, dt=0.02, method="dop853", rtol=1e-10, atol=1e-12)
|
|
147
|
+
.y
|
|
148
|
+
)
|
|
149
|
+
yd = (
|
|
150
|
+
cls()
|
|
151
|
+
.integrate(
|
|
152
|
+
ic=ic,
|
|
153
|
+
final_time=1.0,
|
|
154
|
+
dt=0.02,
|
|
155
|
+
backend="diffsol",
|
|
156
|
+
method="LSODA",
|
|
157
|
+
rtol=1e-10,
|
|
158
|
+
atol=1e-12,
|
|
159
|
+
)
|
|
160
|
+
.y
|
|
161
|
+
)
|
|
162
|
+
except Exception as exc: # noqa: BLE001 — record which system & why
|
|
163
|
+
bad.append((e.name, f"error: {str(exc).splitlines()[-1][:50]}"))
|
|
164
|
+
continue
|
|
165
|
+
n = min(len(yj), len(yd))
|
|
166
|
+
dev = float(np.max(np.abs(yj[:n] - yd[:n])))
|
|
167
|
+
if dev >= 1e-3:
|
|
168
|
+
bad.append((e.name, round(dev, 5)))
|
|
169
|
+
assert not bad, f"{len(bad)}/118 ODEs disagree or error on diffsol: {bad}"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diffsol *translator* coverage — runs without the pydiffsol extra.
|
|
3
|
+
|
|
4
|
+
``to_diffsl`` only needs SymEngine + the jitcode symbols, so this guards the
|
|
5
|
+
SymEngine→DiffSL translation for the whole ODE catalogue on every CI run,
|
|
6
|
+
independently of whether the Rust solver itself is installed. The numeric
|
|
7
|
+
cross-validation against JiTCODE lives in ``test_diffsol_backend.py`` (which
|
|
8
|
+
needs pydiffsol and is therefore skipped when the extra is absent).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from tsdynamics import registry
|
|
14
|
+
from tsdynamics.backends.diffsol import DiffSLTranslationError, to_diffsl
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_every_ode_translates_to_diffsl(ode_entry) -> None:
|
|
18
|
+
"""Every built-in ODE must translate to a well-formed DiffSL module."""
|
|
19
|
+
try:
|
|
20
|
+
code, control_names = to_diffsl(ode_entry.cls())
|
|
21
|
+
except DiffSLTranslationError as exc:
|
|
22
|
+
# A genuine "DiffSL can't express this" is allowed but must be visible:
|
|
23
|
+
# fail loudly so the coverage number can't silently regress unnoticed.
|
|
24
|
+
raise AssertionError(f"{ode_entry.name}: no longer translates to DiffSL — {exc}") from exc
|
|
25
|
+
assert "in_i {" in code and "u_i {" in code and "F_i {" in code
|
|
26
|
+
# control inputs = the non-structural params (structural ones, e.g.
|
|
27
|
+
# Lorenz96's N, are baked into the generated module, not solve-time inputs).
|
|
28
|
+
cls = ode_entry.cls
|
|
29
|
+
structural = getattr(cls, "_structural_params", frozenset())
|
|
30
|
+
expected = [k for k in cls().params if k not in structural]
|
|
31
|
+
assert list(control_names) == expected
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_translation_coverage_is_total() -> None:
|
|
35
|
+
"""The whole ODE catalogue translates — the headline Phase-2 coverage fact."""
|
|
36
|
+
failed = []
|
|
37
|
+
for e in registry.all_systems(family="ode"):
|
|
38
|
+
try:
|
|
39
|
+
to_diffsl(e.cls())
|
|
40
|
+
except Exception as exc: # noqa: BLE001 — record, don't abort
|
|
41
|
+
failed.append((e.name, str(exc).splitlines()[0][:60]))
|
|
42
|
+
assert not failed, f"{len(failed)} ODE systems no longer translate: {failed}"
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Cross-validation of the experimental diffsol backend against JiTCODE.
|
|
3
|
-
|
|
4
|
-
Skipped entirely when the ``tsdynamics[diffsol]`` extra is not installed.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
import pytest
|
|
11
|
-
|
|
12
|
-
pytest.importorskip("pydiffsol")
|
|
13
|
-
|
|
14
|
-
import tsdynamics as ts
|
|
15
|
-
from tsdynamics.backends import diffsol as dsl
|
|
16
|
-
|
|
17
|
-
# ---------------------------------------------------------------------------
|
|
18
|
-
# Translator (fast)
|
|
19
|
-
# ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_to_diffsl_lorenz_structure() -> None:
|
|
23
|
-
code, control = dsl.to_diffsl(ts.Lorenz())
|
|
24
|
-
assert control == ["sigma", "rho", "beta"]
|
|
25
|
-
assert "in_i {" in code and "u_i {" in code and "F_i {" in code
|
|
26
|
-
assert "ic0" in code and "ic2" in code
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_unknown_backend_rejected() -> None:
|
|
30
|
-
with pytest.raises(ValueError, match="Unknown backend"):
|
|
31
|
-
ts.Lorenz().integrate(final_time=1.0, dt=0.1, backend="quantum")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
# Solving + cross-validation (no C compiler needed — pure LLVM JIT)
|
|
36
|
-
# ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_lorenz_diffsol_short_integration() -> None:
|
|
40
|
-
traj = ts.Lorenz().integrate(
|
|
41
|
-
final_time=2.0, dt=0.01, ic=[1.0, 1.0, 1.0], backend="diffsol", rtol=1e-9, atol=1e-11
|
|
42
|
-
)
|
|
43
|
-
assert traj.y.shape == (201, 3) # grid includes the endpoint
|
|
44
|
-
assert np.all(np.isfinite(traj.y))
|
|
45
|
-
assert traj.meta["backend"] == "diffsol"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_param_change_reuses_compiled_module() -> None:
|
|
49
|
-
a = ts.Lorenz().integrate(final_time=1.0, dt=0.1, ic=[1.0, 1.0, 1.0], backend="diffsol")
|
|
50
|
-
before = dict(dsl._ODE_CACHE)
|
|
51
|
-
b = ts.Lorenz(params={"rho": 35.0}).integrate(
|
|
52
|
-
final_time=1.0, dt=0.1, ic=[1.0, 1.0, 1.0], backend="diffsol"
|
|
53
|
-
)
|
|
54
|
-
assert dict(dsl._ODE_CACHE) == before # no recompilation
|
|
55
|
-
assert not np.allclose(a.y[-1], b.y[-1]) # but different dynamics
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@pytest.mark.slow
|
|
59
|
-
@pytest.mark.parametrize("name,ic", [("Lorenz", [1.0, 1.0, 1.0]), ("Rossler", [1.0, 0.0, 0.0])])
|
|
60
|
-
def test_cross_validation_against_jitcode(name: str, ic: list) -> None:
|
|
61
|
-
"""Both backends must agree to tight tolerance over a short window."""
|
|
62
|
-
sys_a = getattr(ts, name)()
|
|
63
|
-
sys_b = getattr(ts, name)()
|
|
64
|
-
kw = dict(final_time=5.0, dt=0.01, ic=ic, rtol=1e-10, atol=1e-12)
|
|
65
|
-
ref = sys_a.integrate(method="dop853", **kw)
|
|
66
|
-
alt = sys_b.integrate(backend="diffsol", **kw)
|
|
67
|
-
np.testing.assert_allclose(alt.y, ref.y, rtol=1e-5, atol=1e-6)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.slow
|
|
71
|
-
def test_stiff_solver_path() -> None:
|
|
72
|
-
"""The BDF mapping handles a stiff-ish problem."""
|
|
73
|
-
traj = ts.Lorenz().integrate(
|
|
74
|
-
final_time=2.0, dt=0.01, ic=[1.0, 1.0, 1.0], backend="diffsol", method="LSODA"
|
|
75
|
-
)
|
|
76
|
-
assert np.all(np.isfinite(traj.y))
|
|
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.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py
RENAMED
|
File without changes
|
|
File without changes
|
{tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/climate_geophysics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsdynamics-2.1.1 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py
RENAMED
|
File without changes
|
|
File without changes
|
{tsdynamics-2.1.1 → tsdynamics-2.2.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
|