tsdynamics 2.2.2__tar.gz → 2.4.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.2.2 → tsdynamics-2.4.0}/.gitignore +8 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/PKG-INFO +1 -1
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/__init__.py +1 -1
- tsdynamics-2.4.0/src/tsdynamics/backends/rustcore.py +375 -0
- tsdynamics-2.4.0/tests/test_rustcore.py +220 -0
- tsdynamics-2.4.0/tests/test_rustcore_translation.py +49 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/LICENSE +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/README.md +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/pyproject.toml +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/analysis/fixed_points.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/analysis/lyapunov.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/analysis/orbit_diagram.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/analysis/poincare.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/backends/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/backends/diffsol.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/dde_base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/map_base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/ode_base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/base/protocol.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/ensemble.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/poincare.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/tangent.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/sampling.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/utils/__init__.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/utils/general.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/utils/sagitta_dt.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/_sampling.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/conftest.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_analysis.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_base.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_cache_isolation.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_dde_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_derived.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_diffsol_backend.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_diffsol_translation.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_jacobians.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_known_values.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_map_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_ode_rhs_symbolic.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_ode_systems.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_protocol.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_registry.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_sampling.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_signatures.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_smoke.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_utils.py +0 -0
- {tsdynamics-2.2.2 → tsdynamics-2.4.0}/tests/test_wrapped.py +0 -0
|
@@ -175,3 +175,11 @@ src/tsdynamics/_version.py
|
|
|
175
175
|
# Generated docs artifacts (figures render at build time, cache in .cache/)
|
|
176
176
|
docs/assets/figures/systems/
|
|
177
177
|
site/
|
|
178
|
+
|
|
179
|
+
# Rust core crate build artifacts (Cargo.lock IS committed for reproducible
|
|
180
|
+
# wheel builds — the crate ships a distributable artifact).
|
|
181
|
+
crates/*/target/
|
|
182
|
+
crates/*/dist/
|
|
183
|
+
crates/*/.venv/
|
|
184
|
+
|
|
185
|
+
crates/*/uv.lock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsdynamics
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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.4.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.
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Experimental Rust-backed RHS evaluation and ensemble integration (``tsdynamics-core``).
|
|
2
|
+
|
|
3
|
+
The same symbolic ``_equations`` that feed the DiffSL backend are lowered here
|
|
4
|
+
to a flat *instruction tape* — single-static-assignment primitives over a
|
|
5
|
+
register file — which the Rust crate evaluates with no Python callbacks and no
|
|
6
|
+
runtime compiler. That makes ensemble integration GIL-free and rayon-parallel
|
|
7
|
+
(the basin/Monte-Carlo primitive), and is the foundation the future Rust SDE
|
|
8
|
+
and DDE solvers build on.
|
|
9
|
+
|
|
10
|
+
``tsdynamics`` itself stays pure-Python; this module is inert unless the
|
|
11
|
+
optional accelerator is installed::
|
|
12
|
+
|
|
13
|
+
pip install tsdynamics-core # or the [rustcore] extra
|
|
14
|
+
|
|
15
|
+
Limitations (experimental): ODEs only; fixed-step explicit RK4 (no stiff
|
|
16
|
+
support — use the ``jitcode``/``diffsol`` backends for stiff systems); the RHS
|
|
17
|
+
must use functions the tape VM provides (the same set the DiffSL backend
|
|
18
|
+
supports). Unsupported constructs raise :class:`TapeCompileError`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"TapeCompileError",
|
|
30
|
+
"available",
|
|
31
|
+
"compile_tape",
|
|
32
|
+
"ensemble_final",
|
|
33
|
+
"eval_rhs",
|
|
34
|
+
"integrate_dense",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Opcodes — MUST stay in sync with crates/tsdynamics-core/src/vm.rs
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
OP_CONST = 0
|
|
41
|
+
OP_STATE = 1
|
|
42
|
+
OP_PARAM = 2
|
|
43
|
+
OP_TIME = 3
|
|
44
|
+
OP_ADD = 10
|
|
45
|
+
OP_SUB = 11
|
|
46
|
+
OP_MUL = 12
|
|
47
|
+
OP_DIV = 13
|
|
48
|
+
OP_POW = 14 # regs[a] ** regs[b]
|
|
49
|
+
OP_POWI = 15 # regs[a] ** b (b is the integer exponent)
|
|
50
|
+
OP_NEG = 20
|
|
51
|
+
OP_RECIP = 21
|
|
52
|
+
_FUNC_OPS = {
|
|
53
|
+
"sin": 30,
|
|
54
|
+
"cos": 31,
|
|
55
|
+
"tan": 32,
|
|
56
|
+
"exp": 33,
|
|
57
|
+
"log": 34,
|
|
58
|
+
"sqrt": 35,
|
|
59
|
+
"Abs": 36,
|
|
60
|
+
"sign": 37,
|
|
61
|
+
"sinh": 38,
|
|
62
|
+
"cosh": 39,
|
|
63
|
+
"tanh": 40,
|
|
64
|
+
"asin": 41,
|
|
65
|
+
"acos": 42,
|
|
66
|
+
"atan": 43,
|
|
67
|
+
"asinh": 44,
|
|
68
|
+
"acosh": 45,
|
|
69
|
+
"atanh": 46,
|
|
70
|
+
}
|
|
71
|
+
_OP_SQRT = 35
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TapeCompileError(NotImplementedError):
|
|
75
|
+
"""The system's RHS uses a construct the tape VM cannot express."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def available() -> bool:
|
|
79
|
+
"""Whether the optional ``tsdynamics-core`` accelerator is installed."""
|
|
80
|
+
try:
|
|
81
|
+
import tsdynamics_core # noqa: F401
|
|
82
|
+
except ImportError:
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Symbolic RHS → instruction tape
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class CompiledTape:
|
|
94
|
+
"""A system RHS lowered to flat arrays for the Rust tape VM."""
|
|
95
|
+
|
|
96
|
+
ops: np.ndarray # int32 (n_instr,)
|
|
97
|
+
a: np.ndarray # int32
|
|
98
|
+
b: np.ndarray # int32
|
|
99
|
+
imm: np.ndarray # float64
|
|
100
|
+
outputs: np.ndarray # int32 (dim,) — register holding each derivative
|
|
101
|
+
n_state: int
|
|
102
|
+
n_param: int
|
|
103
|
+
control_names: list[str]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _Emitter:
|
|
107
|
+
"""Lower a sympy expression DAG to SSA instructions, sharing subexpressions."""
|
|
108
|
+
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
self.ops: list[int] = []
|
|
111
|
+
self.a: list[int] = []
|
|
112
|
+
self.b: list[int] = []
|
|
113
|
+
self.imm: list[float] = []
|
|
114
|
+
self._cache: dict[Any, int] = {}
|
|
115
|
+
|
|
116
|
+
def _push(self, op: int, a: int = 0, b: int = 0, imm: float = 0.0) -> int:
|
|
117
|
+
idx = len(self.ops)
|
|
118
|
+
self.ops.append(op)
|
|
119
|
+
self.a.append(a)
|
|
120
|
+
self.b.append(b)
|
|
121
|
+
self.imm.append(imm)
|
|
122
|
+
return idx
|
|
123
|
+
|
|
124
|
+
def emit(self, expr: Any) -> int:
|
|
125
|
+
cached = self._cache.get(expr)
|
|
126
|
+
if cached is not None:
|
|
127
|
+
return cached
|
|
128
|
+
idx = self._emit(expr)
|
|
129
|
+
self._cache[expr] = idx
|
|
130
|
+
return idx
|
|
131
|
+
|
|
132
|
+
def _emit(self, expr: Any) -> int:
|
|
133
|
+
import sympy
|
|
134
|
+
|
|
135
|
+
# Any symbol-free subexpression (numbers, pi, e, constant folds).
|
|
136
|
+
if not expr.free_symbols:
|
|
137
|
+
return self._push(OP_CONST, imm=float(expr))
|
|
138
|
+
|
|
139
|
+
if isinstance(expr, sympy.Symbol):
|
|
140
|
+
name = expr.name
|
|
141
|
+
if name == "t":
|
|
142
|
+
return self._push(OP_TIME)
|
|
143
|
+
if name.startswith("u"):
|
|
144
|
+
return self._push(OP_STATE, a=int(name[1:]))
|
|
145
|
+
if name.startswith("p"):
|
|
146
|
+
return self._push(OP_PARAM, a=int(name[1:]))
|
|
147
|
+
raise TapeCompileError(f"unexpected symbol {name!r} in RHS")
|
|
148
|
+
|
|
149
|
+
if isinstance(expr, sympy.Add):
|
|
150
|
+
args = expr.args
|
|
151
|
+
acc = self.emit(args[0])
|
|
152
|
+
for term in args[1:]:
|
|
153
|
+
acc = self._push(OP_ADD, a=acc, b=self.emit(term))
|
|
154
|
+
return acc
|
|
155
|
+
|
|
156
|
+
if isinstance(expr, sympy.Mul):
|
|
157
|
+
args = expr.args
|
|
158
|
+
acc = self.emit(args[0])
|
|
159
|
+
for fac in args[1:]:
|
|
160
|
+
acc = self._push(OP_MUL, a=acc, b=self.emit(fac))
|
|
161
|
+
return acc
|
|
162
|
+
|
|
163
|
+
if isinstance(expr, sympy.Pow):
|
|
164
|
+
return self._emit_pow(expr)
|
|
165
|
+
|
|
166
|
+
name = type(expr).__name__
|
|
167
|
+
op = _FUNC_OPS.get(name)
|
|
168
|
+
if op is not None:
|
|
169
|
+
if len(expr.args) != 1:
|
|
170
|
+
raise TapeCompileError(f"function {name!r} expects 1 argument")
|
|
171
|
+
return self._push(op, a=self.emit(expr.args[0]))
|
|
172
|
+
|
|
173
|
+
raise TapeCompileError(f"tape VM has no equivalent for {name!r}.")
|
|
174
|
+
|
|
175
|
+
def _emit_pow(self, expr: Any) -> int:
|
|
176
|
+
import sympy
|
|
177
|
+
|
|
178
|
+
base, exp = expr.base, expr.exp
|
|
179
|
+
base_reg = self.emit(base)
|
|
180
|
+
if isinstance(exp, sympy.Integer):
|
|
181
|
+
e = int(exp)
|
|
182
|
+
if e == -1:
|
|
183
|
+
return self._push(OP_RECIP, a=base_reg)
|
|
184
|
+
return self._push(OP_POWI, a=base_reg, b=e)
|
|
185
|
+
if exp == sympy.Rational(1, 2):
|
|
186
|
+
return self._push(_OP_SQRT, a=base_reg)
|
|
187
|
+
if exp == sympy.Rational(-1, 2):
|
|
188
|
+
sqrt_reg = self._push(_OP_SQRT, a=base_reg)
|
|
189
|
+
return self._push(OP_RECIP, a=sqrt_reg)
|
|
190
|
+
# general exponent: a constant non-integer power, or a symbolic exponent
|
|
191
|
+
return self._push(OP_POW, a=base_reg, b=self.emit(exp))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def compile_tape(system: Any) -> CompiledTape:
|
|
195
|
+
"""
|
|
196
|
+
Lower a :class:`~tsdynamics.base.ContinuousSystem`'s RHS to a tape.
|
|
197
|
+
|
|
198
|
+
Structural parameters are folded to constants; control parameters become
|
|
199
|
+
inputs in ``control_names`` order (the layout the solve-time params vector
|
|
200
|
+
must follow).
|
|
201
|
+
"""
|
|
202
|
+
import symengine
|
|
203
|
+
from jitcode import t as t_sym
|
|
204
|
+
from jitcode import y
|
|
205
|
+
|
|
206
|
+
dim = system.dim
|
|
207
|
+
struct_vals = system._structural_vals()
|
|
208
|
+
control_names = list(system._control_params())
|
|
209
|
+
control_syms = {k: symengine.Symbol(f"p{i}") for i, k in enumerate(control_names)}
|
|
210
|
+
|
|
211
|
+
exprs = list(type(system)._equations(y, t_sym, **{**struct_vals, **control_syms}))
|
|
212
|
+
if len(exprs) != dim:
|
|
213
|
+
raise ValueError(f"_equations must return {dim} expressions, got {len(exprs)}")
|
|
214
|
+
|
|
215
|
+
subs = {y(i): symengine.Symbol(f"u{i}") for i in range(dim)}
|
|
216
|
+
subs[t_sym] = symengine.Symbol("t")
|
|
217
|
+
|
|
218
|
+
em = _Emitter()
|
|
219
|
+
outputs = [em.emit(symengine.sympify(e).subs(subs)._sympy_()) for e in exprs]
|
|
220
|
+
|
|
221
|
+
return CompiledTape(
|
|
222
|
+
ops=np.asarray(em.ops, dtype=np.int32),
|
|
223
|
+
a=np.asarray(em.a, dtype=np.int32),
|
|
224
|
+
b=np.asarray(em.b, dtype=np.int32),
|
|
225
|
+
imm=np.asarray(em.imm, dtype=np.float64),
|
|
226
|
+
outputs=np.asarray(outputs, dtype=np.int32),
|
|
227
|
+
n_state=dim,
|
|
228
|
+
n_param=len(control_names),
|
|
229
|
+
control_names=control_names,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# Thin wrappers over the Rust kernels
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _require():
|
|
239
|
+
if not available():
|
|
240
|
+
raise ImportError(
|
|
241
|
+
"the Rust core is not installed: pip install tsdynamics-core (or the 'rustcore' extra)"
|
|
242
|
+
)
|
|
243
|
+
import tsdynamics_core
|
|
244
|
+
|
|
245
|
+
return tsdynamics_core
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _params_vec(system: Any, tape: CompiledTape) -> np.ndarray:
|
|
249
|
+
return np.asarray([float(system.params[k]) for k in tape.control_names], dtype=np.float64)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _args(tape: CompiledTape):
|
|
253
|
+
return (tape.ops, tape.a, tape.b, tape.imm, tape.outputs, tape.n_state, tape.n_param)
|
|
254
|
+
|
|
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
|
+
|
|
281
|
+
def eval_rhs(
|
|
282
|
+
system: Any, u: Any, t: float = 0.0, *, tape: CompiledTape | None = None
|
|
283
|
+
) -> np.ndarray:
|
|
284
|
+
"""Evaluate ``du/dt`` once in Rust — used to cross-check the tape."""
|
|
285
|
+
core = _require()
|
|
286
|
+
tape = tape or compile_tape(system)
|
|
287
|
+
u = _as_state(u, tape.n_state, "u")
|
|
288
|
+
return np.asarray(core.eval_rhs(*_args(tape), u, _params_vec(system, tape), float(t)))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def integrate_dense(
|
|
292
|
+
system: Any,
|
|
293
|
+
ic: Any,
|
|
294
|
+
t_eval: Any,
|
|
295
|
+
*,
|
|
296
|
+
method: str = "RK45",
|
|
297
|
+
rtol: float = 1e-6,
|
|
298
|
+
atol: float = 1e-9,
|
|
299
|
+
h: float | None = None,
|
|
300
|
+
tape: CompiledTape | None = None,
|
|
301
|
+
) -> np.ndarray:
|
|
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)
|
|
312
|
+
core = _require()
|
|
313
|
+
tape = tape or compile_tape(system)
|
|
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
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def ensemble_final(
|
|
334
|
+
system: Any,
|
|
335
|
+
u0_batch: Any,
|
|
336
|
+
t0: float,
|
|
337
|
+
t1: float,
|
|
338
|
+
*,
|
|
339
|
+
method: str = "RK45",
|
|
340
|
+
rtol: float = 1e-6,
|
|
341
|
+
atol: float = 1e-9,
|
|
342
|
+
h: float = 1e-2,
|
|
343
|
+
tape: CompiledTape | None = None,
|
|
344
|
+
) -> np.ndarray:
|
|
345
|
+
"""Integrate a batch of initial conditions in parallel; return final states.
|
|
346
|
+
|
|
347
|
+
``u0_batch`` is ``(n, dim)``; each row is integrated from ``t0`` to ``t1``
|
|
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.
|
|
355
|
+
"""
|
|
356
|
+
_check_method(method)
|
|
357
|
+
core = _require()
|
|
358
|
+
tape = tape or compile_tape(system)
|
|
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
|
+
)
|
|
371
|
+
return np.asarray(
|
|
372
|
+
core.integrate_ensemble_final_rk45_py(
|
|
373
|
+
*_args(tape), u0_batch, p, float(t0), float(t1), float(rtol), float(atol)
|
|
374
|
+
)
|
|
375
|
+
)
|
|
@@ -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)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rust-core *tape compiler* coverage — runs without the ``tsdynamics-core`` extra.
|
|
3
|
+
|
|
4
|
+
``compile_tape`` only needs SymEngine + the jitcode symbols, so this guards the
|
|
5
|
+
symbolic→tape lowering for the whole ODE catalogue on every CI run, regardless
|
|
6
|
+
of whether the Rust accelerator is installed. The numeric cross-validation
|
|
7
|
+
against the symbolic RHS (and the RK4/ensemble kernels) lives in
|
|
8
|
+
``test_rustcore.py``, which needs the compiled crate and is skipped when absent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from tsdynamics import registry
|
|
16
|
+
from tsdynamics.backends.rustcore import TapeCompileError, compile_tape
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_every_ode_compiles_to_tape(ode_entry) -> None:
|
|
20
|
+
"""Every built-in ODE must lower to a well-formed instruction tape."""
|
|
21
|
+
try:
|
|
22
|
+
tape = compile_tape(ode_entry.cls())
|
|
23
|
+
except TapeCompileError as exc:
|
|
24
|
+
raise AssertionError(f"{ode_entry.name}: no longer compiles to a tape — {exc}") from exc
|
|
25
|
+
|
|
26
|
+
n = tape.ops.size
|
|
27
|
+
# Parallel instruction arrays agree in length.
|
|
28
|
+
assert tape.a.size == n and tape.b.size == n and tape.imm.size == n
|
|
29
|
+
# One output register per state component.
|
|
30
|
+
assert tape.outputs.size == ode_entry.cls().dim == tape.n_state
|
|
31
|
+
# Every register reference points to an already-emitted instruction.
|
|
32
|
+
assert np.all(tape.outputs < n) and np.all(tape.outputs >= 0)
|
|
33
|
+
# control inputs = the non-structural params (structural ones are folded
|
|
34
|
+
# to constants in the tape, mirroring the DiffSL backend).
|
|
35
|
+
cls = ode_entry.cls
|
|
36
|
+
structural = getattr(cls, "_structural_params", frozenset())
|
|
37
|
+
expected = [k for k in cls().params if k not in structural]
|
|
38
|
+
assert list(tape.control_names) == expected
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_tape_coverage_is_total() -> None:
|
|
42
|
+
"""The whole ODE catalogue lowers to a tape — the Rust-core coverage fact."""
|
|
43
|
+
failed = []
|
|
44
|
+
for e in registry.all_systems(family="ode"):
|
|
45
|
+
try:
|
|
46
|
+
compile_tape(e.cls())
|
|
47
|
+
except Exception as exc: # noqa: BLE001 — record, don't abort
|
|
48
|
+
failed.append((e.name, str(exc).splitlines()[0][:60]))
|
|
49
|
+
assert not failed, f"{len(failed)} ODE systems 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.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py
RENAMED
|
File without changes
|
|
File without changes
|
{tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/climate_geophysics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsdynamics-2.2.2 → tsdynamics-2.4.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py
RENAMED
|
File without changes
|
|
File without changes
|
{tsdynamics-2.2.2 → tsdynamics-2.4.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
|
|
File without changes
|
|
File without changes
|