tsdynamics 2.1.0__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.
Files changed (72) hide show
  1. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/PKG-INFO +1 -1
  2. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/__init__.py +1 -1
  3. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/ode_base.py +18 -6
  4. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +7 -0
  5. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/exotic_systems.py +1 -0
  6. tsdynamics-2.2.0/tests/test_diffsol_backend.py +169 -0
  7. tsdynamics-2.2.0/tests/test_diffsol_translation.py +42 -0
  8. tsdynamics-2.1.0/tests/test_diffsol_backend.py +0 -76
  9. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/.gitignore +0 -0
  10. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/LICENSE +0 -0
  11. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/README.md +0 -0
  12. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/pyproject.toml +0 -0
  13. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/analysis/__init__.py +0 -0
  14. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/analysis/fixed_points.py +0 -0
  15. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/analysis/lyapunov.py +0 -0
  16. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/analysis/orbit_diagram.py +0 -0
  17. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/analysis/poincare.py +0 -0
  18. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/backends/__init__.py +0 -0
  19. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/backends/diffsol.py +0 -0
  20. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/__init__.py +0 -0
  21. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/base.py +0 -0
  22. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/dde_base.py +0 -0
  23. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/map_base.py +0 -0
  24. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/base/protocol.py +0 -0
  25. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/__init__.py +0 -0
  26. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/_base.py +0 -0
  27. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/ensemble.py +0 -0
  28. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/poincare.py +0 -0
  29. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/projected.py +0 -0
  30. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
  31. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/tangent.py +0 -0
  32. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/derived/wrapped.py +0 -0
  33. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/py.typed +0 -0
  34. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/registry.py +0 -0
  35. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/sampling.py +0 -0
  36. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/__init__.py +0 -0
  37. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
  38. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
  39. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
  40. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
  41. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
  42. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
  43. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
  44. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
  45. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/__init__.py +0 -0
  46. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
  47. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
  48. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
  49. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
  50. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
  51. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/utils/__init__.py +0 -0
  52. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/utils/general.py +0 -0
  53. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/src/tsdynamics/utils/sagitta_dt.py +0 -0
  54. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/_sampling.py +0 -0
  55. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/conftest.py +0 -0
  56. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_analysis.py +0 -0
  57. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_base.py +0 -0
  58. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_cache_isolation.py +0 -0
  59. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_dde_systems.py +0 -0
  60. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_derived.py +0 -0
  61. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_jacobians.py +0 -0
  62. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_known_values.py +0 -0
  63. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_map_systems.py +0 -0
  64. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_ode_rhs_symbolic.py +0 -0
  65. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_ode_systems.py +0 -0
  66. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_protocol.py +0 -0
  67. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_registry.py +0 -0
  68. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_sampling.py +0 -0
  69. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_signatures.py +0 -0
  70. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_smoke.py +0 -0
  71. {tsdynamics-2.1.0 → tsdynamics-2.2.0}/tests/test_utils.py +0 -0
  72. {tsdynamics-2.1.0 → 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.1.0
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.1.0"
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 via C; ``"diffsol"`` is the
723
- experimental Rust solver suite
724
- (``pip install tsdynamics[diffsol]``,
725
- see :mod:`tsdynamics.backends.diffsol`).
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 'diffsol'.")
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)
@@ -568,6 +568,7 @@ class Duffing(ContinuousSystem):
568
568
  class RabinovichFabrikant(ContinuousSystem):
569
569
  params = {"a": 1.1, "g": 0.87}
570
570
  dim = 3
571
+ default_ic = [-1.0, 0.0, 0.5] # random U[0,1)^3 escapes the basin
571
572
 
572
573
  @staticmethod
573
574
  def _equations(Y, t, *, a, g):
@@ -718,6 +719,7 @@ class SprottC(ContinuousSystem):
718
719
  class SprottD(ContinuousSystem):
719
720
  params = {}
720
721
  dim = 3
722
+ default_ic = [0.1, 0.05, 0.05] # random U[0,1)^3 escapes the basin
721
723
 
722
724
  @staticmethod
723
725
  def _equations(Y, t):
@@ -823,6 +825,7 @@ class SprottH(ContinuousSystem):
823
825
  class SprottI(ContinuousSystem):
824
826
  params = {"a": 0.2}
825
827
  dim = 3
828
+ default_ic = [0.1, 0.05, 0.05] # random U[0,1)^3 escapes the basin
826
829
 
827
830
  @staticmethod
828
831
  def _equations(Y, t, *, a):
@@ -907,6 +910,7 @@ class SprottL(ContinuousSystem):
907
910
  class SprottM(ContinuousSystem):
908
911
  params = {"a": 1.7}
909
912
  dim = 3
913
+ default_ic = [0.1, 0.05, 0.05] # random U[0,1)^3 escapes the basin
910
914
 
911
915
  @staticmethod
912
916
  def _equations(Y, t, *, a):
@@ -949,6 +953,7 @@ class SprottN(ContinuousSystem):
949
953
  class SprottO(ContinuousSystem):
950
954
  params = {"a": 2.7}
951
955
  dim = 3
956
+ default_ic = [0.1, 0.05, 0.05] # random U[0,1)^3 escapes the basin
952
957
 
953
958
  @staticmethod
954
959
  def _equations(Y, t, *, a):
@@ -1141,6 +1146,7 @@ class Rucklidge(ContinuousSystem):
1141
1146
  class HyperRossler(ContinuousSystem):
1142
1147
  params = {"a": 0.25, "b": 3.0, "c": 0.5, "d": 0.05}
1143
1148
  dim = 4
1149
+ default_ic = [-10.0, -6.0, 0.0, 10.0] # random U[0,1)^4 escapes the basin
1144
1150
 
1145
1151
  @staticmethod
1146
1152
  def _equations(Y, t, *, a, b, c, d):
@@ -1219,6 +1225,7 @@ class GuckenheimerHolmes(ContinuousSystem):
1219
1225
  class HenonHeiles(ContinuousSystem):
1220
1226
  params = {"lam": 1}
1221
1227
  dim = 4
1228
+ default_ic = [0.1, 0.1, 0.1, 0.1] # low-energy bounded orbit; random U[0,1)^4 can be unbound
1222
1229
 
1223
1230
  @staticmethod
1224
1231
  def _equations(Y, t, *, lam):
@@ -78,6 +78,7 @@ class HyperJha(ContinuousSystem):
78
78
  class HyperQi(ContinuousSystem):
79
79
  params = {"a": 50, "b": 24, "c": 13, "d": 8, "e": 33, "f": 30}
80
80
  dim = 4
81
+ default_ic = [1.0, 2.0, 1.0, 1.0] # random U[0,1)^4 escapes the basin
81
82
 
82
83
  @staticmethod
83
84
  def _equations(Y, t, *, a, b, c, d, e, f):
@@ -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