tsdynamics 3.1.3__tar.gz → 3.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-3.1.3 → tsdynamics-3.2.0}/PKG-INFO +2 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/README.md +1 -1
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/src/bridge.rs +16 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/sde.rs +101 -11
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/adams.rs +453 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/bs3.rs +189 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/cashkarp.rs +205 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/control.rs +1 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/euler.rs +99 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/heun.rs +101 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/heun_euler.rs +175 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/midpoint.rs +99 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/mod.rs +22 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/ralston.rs +100 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/rk4_38.rs +106 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/rkf45.rs +203 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/ssprk3.rs +104 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/backward_euler.rs +161 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/implicit_midpoint.rs +179 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/mod.rs +9 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/newton.rs +175 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/sdirk2.rs +228 -0
- tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/trapezoid.rs +160 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/trbdf2.rs +5 -83
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/explicit_accuracy.rs +40 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/implicit_stiff.rs +36 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/pyproject.toml +9 -6
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/__init__.py +11 -3
- tsdynamics-3.2.0/src/tsdynamics/analysis/_tangent.py +155 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/_common.py +5 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/attractors.py +15 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/basins.py +12 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/continuation.py +6 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/metrics.py +3 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/_common.py +19 -130
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/expansion.py +4 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/gali.py +63 -29
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/zero_one.py +5 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/_common.py +14 -5
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/correlation.py +24 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/generalized.py +2 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/_common.py +14 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/delay.py +6 -5
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/dimension.py +1 -10
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/embed.py +5 -10
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/core.py +0 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/lz.py +3 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/_common.py +32 -126
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/fixed.py +3 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/periodic.py +6 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/lyapunov/__init__.py +1 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/return_map.py +4 -1
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/generators.py +7 -7
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/hypothesis.py +1 -1
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/trajectory.py +9 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/__init__.py +6 -1
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_variational.py +4 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/compile.py +13 -13
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/problem.py +9 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/run.py +83 -20
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/errors.py +7 -4
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/continuous.py +5 -1
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/delay.py +39 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/discrete.py +12 -8
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/stochastic.py +94 -22
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/explicit.py +18 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/implicit.py +4 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/select.py +38 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +31 -14
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +5 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/coupled_systems.py +19 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/exotic_systems.py +4 -6
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -3
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +25 -20
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/exotic_maps.py +27 -15
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/geometric_maps.py +11 -12
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +8 -7
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/population_maps.py +7 -7
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/__init__.py +0 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/sagitta_dt.py +32 -18
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/viz/spec.py +296 -8
- tsdynamics-3.1.3/src/tsdynamics/utils/general.py +0 -26
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/LICENSE +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/Cargo.lock +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/dde.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/ensemble.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/event.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/integrate.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/map.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/problem.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/rng.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/testkit.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/builder.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/eval.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/op.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/reference.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/tape.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/coverage_tape.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/CircadianRhythm.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/Lorenz.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/SanUmSrisuchinwong.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/SprottMore.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/Torus.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/YuWang2.txt +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/gen_fixtures.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/golden_fixtures.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/roundtrip.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/codegen.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/error.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/evaluator.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/shims.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/compile_latency.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/golden_fixtures.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/interpreter_equivalence.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/caps.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/dop853.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/rk4.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/rk45.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/testkit.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/tsit5.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/bdf.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/control.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/linalg.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/rosenbrock.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/registry.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/euler_maruyama.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/milstein.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/mod.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/testkit.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/solver.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/registry.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/Cargo.toml +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/src/interp.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/src/lib.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/tests/golden_fixtures.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/tests/oracle_equivalence.rs +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/_result.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/_scaling.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/fixedmass.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/dispersion.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/multiscale.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/permutation.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/sample.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/lyapunov/from_data.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/orbit_diagram.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/poincare.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/_common.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/matrix.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/rqa.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/windowed.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/_common.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/statistics.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/sampling.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_crossings.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/ensemble.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/poincare.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/tangent.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/symbols.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/_accessors.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/_dde_lyapunov.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/base.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/protocol.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/wrapped.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/plugins.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/stochastic.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/__init__.py +2 -2
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/__init__.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/_common.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/features.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/preprocessing.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/spectral.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/grids.py +0 -0
- {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/viz/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsdynamics
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Operating System :: OS Independent
|
|
@@ -124,7 +124,7 @@ od = ts.orbit_diagram(ts.Logistic(), "r", np.linspace(2.5, 4.0, 600))
|
|
|
124
124
|
x, y = od.flat()
|
|
125
125
|
|
|
126
126
|
# Poincaré section of the Rössler attractor (root-refined crossings)
|
|
127
|
-
section = ts.poincare_section(ts.Rossler(), plane=(1, 0.0),
|
|
127
|
+
section = ts.poincare_section(ts.Rossler(), plane=(1, 0.0), n=500)
|
|
128
128
|
|
|
129
129
|
# Fixed points of the Hénon map, with stability
|
|
130
130
|
ts.fixed_points(ts.Henon())
|
|
@@ -93,7 +93,7 @@ od = ts.orbit_diagram(ts.Logistic(), "r", np.linspace(2.5, 4.0, 600))
|
|
|
93
93
|
x, y = od.flat()
|
|
94
94
|
|
|
95
95
|
# Poincaré section of the Rössler attractor (root-refined crossings)
|
|
96
|
-
section = ts.poincare_section(ts.Rossler(), plane=(1, 0.0),
|
|
96
|
+
section = ts.poincare_section(ts.Rossler(), plane=(1, 0.0), n=500)
|
|
97
97
|
|
|
98
98
|
# Fixed points of the Hénon map, with stability
|
|
99
99
|
ts.fixed_points(ts.Henon())
|
|
@@ -35,8 +35,10 @@ use tsdyn_engine::{
|
|
|
35
35
|
};
|
|
36
36
|
use tsdyn_ir::{Evaluator, Tape};
|
|
37
37
|
use tsdyn_jit::JitEvaluator;
|
|
38
|
-
use tsdyn_solvers::explicit::{Dop853, Rk45, Tsit5};
|
|
39
|
-
use tsdyn_solvers::implicit::{
|
|
38
|
+
use tsdyn_solvers::explicit::{Bs3, CashKarp, Dop853, HeunEuler, Rk45, Rkf45, Tsit5};
|
|
39
|
+
use tsdyn_solvers::implicit::{
|
|
40
|
+
BackwardEuler, Bdf, ImplicitMidpoint, RosenbrockW, Sdirk2, TrBdf2, Trapezoid,
|
|
41
|
+
};
|
|
40
42
|
use tsdyn_solvers::sde::{self, SdeKernel};
|
|
41
43
|
use tsdyn_solvers::{Solver, SolverKind};
|
|
42
44
|
use tsdyn_vm::Interpreter;
|
|
@@ -224,11 +226,21 @@ pub fn build_solver(name: &'static str, rtol: f64, atol: f64) -> Box<dyn Solver>
|
|
|
224
226
|
"rk45" => Box::new(Rk45::with_tolerances(rtol, atol)),
|
|
225
227
|
"tsit5" => Box::new(Tsit5::with_tolerances(rtol, atol)),
|
|
226
228
|
"dop853" => Box::new(Dop853::with_tolerances(rtol, atol)),
|
|
229
|
+
"heun_euler" => Box::new(HeunEuler::with_tolerances(rtol, atol)),
|
|
230
|
+
"bs3" => Box::new(Bs3::with_tolerances(rtol, atol)),
|
|
231
|
+
"rkf45" => Box::new(Rkf45::with_tolerances(rtol, atol)),
|
|
232
|
+
"cashkarp" => Box::new(CashKarp::with_tolerances(rtol, atol)),
|
|
227
233
|
"rosenbrock" => Box::new(RosenbrockW::with_tolerances(rtol, atol)),
|
|
228
234
|
"trbdf2" => Box::new(TrBdf2::with_tolerances(rtol, atol)),
|
|
229
235
|
"bdf" => Box::new(Bdf::with_tolerances(rtol, atol)),
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
"backward_euler" => Box::new(BackwardEuler::with_tolerances(rtol, atol)),
|
|
237
|
+
"implicit_midpoint" => Box::new(ImplicitMidpoint::with_tolerances(rtol, atol)),
|
|
238
|
+
"trapezoid" => Box::new(Trapezoid::with_tolerances(rtol, atol)),
|
|
239
|
+
"sdirk2" => Box::new(Sdirk2::with_tolerances(rtol, atol)),
|
|
240
|
+
// The fixed-step kernels (`euler`/`midpoint`/`heun`/`ralston`/`rk4`/
|
|
241
|
+
// `rk4_38`/`ssprk3`), the Adams multistep kernels (`ab3`/`ab4`/`abm4`,
|
|
242
|
+
// which take no tolerances), and any out-of-tree plugin kernel build
|
|
243
|
+
// through the registry factory. `name` came from the registry
|
|
232
244
|
// (via `resolve_solver`), so `make` is guaranteed `Some`.
|
|
233
245
|
other => tsdyn_solvers::make(other)
|
|
234
246
|
.expect("a name returned by resolve_solver is always registered"),
|
|
@@ -27,12 +27,15 @@
|
|
|
27
27
|
//! # Fixed step, sub-stepping to the grid
|
|
28
28
|
//!
|
|
29
29
|
//! These schemes are fixed-step: the step size *is* `dt`, and `dt` sets the noise
|
|
30
|
-
//! scale `√dt`. Output at a requested time that is
|
|
31
|
-
//! produced by a shorter final sub-step whose increment is drawn `~ N(0, h)`
|
|
32
|
-
//! that `h` — a valid Euler–Maruyama/Milstein step on a non-uniform partition
|
|
30
|
+
//! scale `√dt`. Output at a requested time that is genuinely off the `dt` lattice
|
|
31
|
+
//! is produced by a shorter final sub-step whose increment is drawn `~ N(0, h)`
|
|
32
|
+
//! for that `h` — a valid Euler–Maruyama/Milstein step on a non-uniform partition
|
|
33
33
|
//! (Brownian increments over sub-intervals are independent and sum correctly), so
|
|
34
|
-
//! the discretization stays consistent.
|
|
35
|
-
//!
|
|
34
|
+
//! the discretization stays consistent. A nominally `dt`-wide segment whose width
|
|
35
|
+
//! only *appears* to exceed `dt` by floating-point roundoff is absorbed into one
|
|
36
|
+
//! canonical `dt` step (a tolerant landing, [`LANDING_REL_TOL`]) rather than
|
|
37
|
+
//! spawning a spurious sub-ULP sliver. As with the ODE driver, a non-finite state
|
|
38
|
+
//! is reported, never returned as plausible data ([`SdeError`]).
|
|
36
39
|
|
|
37
40
|
use rayon::prelude::*;
|
|
38
41
|
use tsdyn_ir::Evaluator;
|
|
@@ -42,6 +45,20 @@ use tsdyn_solvers::{SolverState, StepOutcome};
|
|
|
42
45
|
use crate::integrate::DEFAULT_MAX_STEPS;
|
|
43
46
|
use crate::rng::{fill_wiener, seed_for, SplitMix64};
|
|
44
47
|
|
|
48
|
+
/// Relative tolerance for the fixed-step *landing* decision (mirror of
|
|
49
|
+
/// `_LANDING_REL_TOL` in `src/tsdynamics/families/stochastic.py` — keep the two
|
|
50
|
+
/// in lock-step so the engine and the pure-Python reference draw their Wiener
|
|
51
|
+
/// streams in identical order).
|
|
52
|
+
///
|
|
53
|
+
/// Output grids are uniform in `dt`, but floating-point roundoff makes a
|
|
54
|
+
/// nominally `dt`-wide segment compute a `remaining = t_end - t` that is `dt`
|
|
55
|
+
/// plus a few ULP. The naive `dt >= remaining` test then takes a full `dt` step
|
|
56
|
+
/// and a **spurious sub-ULP sliver step** — drawing an extra Wiener increment
|
|
57
|
+
/// (desyncing the noise stream) and adding a meaningless `N(0, ~ULP)` kick.
|
|
58
|
+
/// Absorbing that roundoff into a single canonical `dt` step makes the
|
|
59
|
+
/// discretisation exactly "one `N(0, dt)` increment per step".
|
|
60
|
+
const LANDING_REL_TOL: f64 = 1e-9;
|
|
61
|
+
|
|
45
62
|
/// Knobs for the fixed-step SDE loop.
|
|
46
63
|
///
|
|
47
64
|
/// Build with [`SdeConfig::new`] (the step `dt` plus the default step cap) and
|
|
@@ -111,10 +128,13 @@ impl std::error::Error for SdeError {}
|
|
|
111
128
|
/// Advance `st` from its current time to `t_end` with fixed step `cfg.dt`,
|
|
112
129
|
/// drawing one diagonal Wiener increment per step from `rng` into `dw`.
|
|
113
130
|
///
|
|
114
|
-
///
|
|
115
|
-
/// remaining
|
|
116
|
-
///
|
|
117
|
-
///
|
|
131
|
+
/// The landing (final) step of the span reuses the canonical `cfg.dt` when
|
|
132
|
+
/// `remaining` is within [`LANDING_REL_TOL`] of it (so a uniform-`dt` grid takes
|
|
133
|
+
/// exactly one `N(0, dt)` increment per segment and never a spurious sub-ULP
|
|
134
|
+
/// sliver), and only a genuinely shorter span draws its increment `~ N(0, h)`
|
|
135
|
+
/// for that smaller `h`; the time is then pinned exactly to `t_end` so grid
|
|
136
|
+
/// points never drift. `dw` is a caller-owned scratch of length `dim`, reused
|
|
137
|
+
/// across steps.
|
|
118
138
|
#[allow(clippy::too_many_arguments)] // drift + diffusion + the state/rng/dw/time/cfg the loop threads.
|
|
119
139
|
fn sde_advance_to(
|
|
120
140
|
drift: &dyn Evaluator,
|
|
@@ -139,8 +159,21 @@ fn sde_advance_to(
|
|
|
139
159
|
return Err(SdeError::StepLimit { t: st.t, steps });
|
|
140
160
|
}
|
|
141
161
|
let remaining = t_end - st.t;
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
// Tolerant landing: absorb a roundoff-scale overshoot so a uniform-dt grid
|
|
163
|
+
// takes exactly one canonical dt step per segment (no spurious sub-ULP
|
|
164
|
+
// sliver step). Only a genuinely shorter final span uses `remaining`. See
|
|
165
|
+
// LANDING_REL_TOL; the Python reference mirrors this predicate so the two
|
|
166
|
+
// draw their Wiener streams in identical order.
|
|
167
|
+
let landing = remaining <= cfg.dt * (1.0 + LANDING_REL_TOL);
|
|
168
|
+
let h = if landing {
|
|
169
|
+
if remaining >= cfg.dt * (1.0 - LANDING_REL_TOL) {
|
|
170
|
+
cfg.dt
|
|
171
|
+
} else {
|
|
172
|
+
remaining
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
cfg.dt
|
|
176
|
+
};
|
|
144
177
|
steps += 1;
|
|
145
178
|
|
|
146
179
|
fill_wiener(rng, h, dw);
|
|
@@ -900,6 +933,63 @@ mod tests {
|
|
|
900
933
|
assert!(out.iter().all(|x| x.is_finite()));
|
|
901
934
|
}
|
|
902
935
|
|
|
936
|
+
#[test]
|
|
937
|
+
fn tolerant_landing_kills_the_spurious_substep_on_a_roundoff_grid() {
|
|
938
|
+
// FIX-SDE-WIENER regression. A uniform grid t_eval[k] = k·dt built the way
|
|
939
|
+
// NumPy's arange does (k as f64 * dt) makes many segments compute a width a
|
|
940
|
+
// few ULP above dt. The old `dt >= remaining` landing then took a full dt
|
|
941
|
+
// step *plus a spurious sub-ULP sliver* — an extra Wiener draw that desynced
|
|
942
|
+
// the stream. With the tolerant landing the grid takes exactly one clean dt
|
|
943
|
+
// step per segment, so marching the grid and marching the whole span in one
|
|
944
|
+
// shot draw the identical N increments and reach the SAME final state
|
|
945
|
+
// bit-for-bit (GBM is autonomous, so the pinned-vs-accumulated time is moot).
|
|
946
|
+
let drift = gbm_drift(0.12);
|
|
947
|
+
let diffusion = gbm_diffusion(0.3);
|
|
948
|
+
let dt = 0.02;
|
|
949
|
+
let n = 50usize;
|
|
950
|
+
let cfg = SdeConfig::new(dt);
|
|
951
|
+
let t_eval: Vec<f64> = (0..=n).map(|k| k as f64 * dt).collect();
|
|
952
|
+
// At least one segment must actually overshoot dt, else the test is vacuous.
|
|
953
|
+
let overshoot = (1..=n).filter(|&k| t_eval[k] - t_eval[k - 1] > dt).count();
|
|
954
|
+
assert!(overshoot > 0, "grid did not provoke the roundoff overshoot");
|
|
955
|
+
|
|
956
|
+
let mut kernel_g = EulerMaruyama::new();
|
|
957
|
+
let mut rng_g = SplitMix64::new(20240614);
|
|
958
|
+
let grid = sde_integrate_grid(
|
|
959
|
+
&drift,
|
|
960
|
+
&diffusion,
|
|
961
|
+
&mut kernel_g,
|
|
962
|
+
&mut rng_g,
|
|
963
|
+
&[1.0],
|
|
964
|
+
&[],
|
|
965
|
+
&t_eval,
|
|
966
|
+
&cfg,
|
|
967
|
+
)
|
|
968
|
+
.unwrap();
|
|
969
|
+
let grid_final = grid[grid.len() - 1];
|
|
970
|
+
|
|
971
|
+
let mut kernel_f = EulerMaruyama::new();
|
|
972
|
+
let mut rng_f = SplitMix64::new(20240614);
|
|
973
|
+
let one_shot = sde_integrate_final(
|
|
974
|
+
&drift,
|
|
975
|
+
&diffusion,
|
|
976
|
+
&mut kernel_f,
|
|
977
|
+
&mut rng_f,
|
|
978
|
+
&[1.0],
|
|
979
|
+
&[],
|
|
980
|
+
0.0,
|
|
981
|
+
t_eval[n],
|
|
982
|
+
&cfg,
|
|
983
|
+
)
|
|
984
|
+
.unwrap();
|
|
985
|
+
|
|
986
|
+
assert_eq!(
|
|
987
|
+
grid_final.to_bits(),
|
|
988
|
+
one_shot[0].to_bits(),
|
|
989
|
+
"grid march != one-shot march — a spurious sliver desynced the noise stream"
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
903
993
|
#[test]
|
|
904
994
|
fn divergence_is_reported_not_silently_returned() {
|
|
905
995
|
// f(x) = x², g = 0 ⇒ deterministic finite-time blow-up past t = 1 from
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
//! The explicit linear-multistep (Adams) family: Adams–Bashforth and the
|
|
2
|
+
//! Adams–Bashforth–Moulton predictor–corrector.
|
|
3
|
+
//!
|
|
4
|
+
//! Unlike a Runge–Kutta method, a multistep method reuses the right-hand-side
|
|
5
|
+
//! values it already computed at *previous* steps, so it advances with only **one**
|
|
6
|
+
//! (Adams–Bashforth) or **two** (predictor–corrector) new RHS evaluations per step
|
|
7
|
+
//! regardless of order. That makes them efficient on non-stiff problems with an
|
|
8
|
+
//! expensive `f` — e.g. the large ODE systems that arise from
|
|
9
|
+
//! method-of-lines/spectral discretisations of PDEs. They are fixed-step here and
|
|
10
|
+
//! self-start with RK4 until enough history has accumulated.
|
|
11
|
+
//!
|
|
12
|
+
//! | name | method | order | RHS/step |
|
|
13
|
+
//! |--------|------------------------------------------|-------|----------|
|
|
14
|
+
//! | `ab3` | Adams–Bashforth 3-step | 3 | 1 |
|
|
15
|
+
//! | `ab4` | Adams–Bashforth 4-step | 4 | 1 |
|
|
16
|
+
//! | `abm4` | Adams–Bashforth–Moulton (PECE) | 4 | 2 |
|
|
17
|
+
//!
|
|
18
|
+
//! References: F. Bashforth & J. C. Adams, *An Attempt to Test the Theories of
|
|
19
|
+
//! Capillary Action…* (Cambridge Univ. Press, 1883) — the Adams–Bashforth and
|
|
20
|
+
//! Adams–Moulton formulas; E. Hairer, S. P. Nørsett & G. Wanner, *Solving
|
|
21
|
+
//! Ordinary Differential Equations I*, 2nd ed. (1993), §III.1 (Adams methods and
|
|
22
|
+
//! predictor–corrector pairs).
|
|
23
|
+
//!
|
|
24
|
+
//! # Why fixed step (and how variable `h` is handled)
|
|
25
|
+
//!
|
|
26
|
+
//! The classic Adams coefficients assume a *uniform* past mesh. In the engine's
|
|
27
|
+
//! grid integration each output segment is one step of the running `h`, so the
|
|
28
|
+
//! mesh is uniform; the kernel detects a change in `h` (the final landing step of
|
|
29
|
+
//! a non-commensurate grid, or a `step()` driven at a new `dt`) and transparently
|
|
30
|
+
//! **restarts** the history with an RK4 step until `k−1` uniform slopes have
|
|
31
|
+
//! re-accumulated. Variable-coefficient Adams methods are a possible future
|
|
32
|
+
//! refinement; they would slot in behind this same kernel without an interface
|
|
33
|
+
//! change.
|
|
34
|
+
|
|
35
|
+
use std::collections::VecDeque;
|
|
36
|
+
|
|
37
|
+
use crate::{
|
|
38
|
+
register_solver, Caps, Evaluator, ProblemKind, ProblemKinds, Solver, SolverState, StepOutcome,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
use super::control::{fixed_step, RkWork};
|
|
42
|
+
|
|
43
|
+
// Classic RK4 tableau for the self-start / history-restart bootstrap.
|
|
44
|
+
const RK4_C: &[f64] = &[0.0, 0.5, 0.5, 1.0];
|
|
45
|
+
const RK4_A: &[&[f64]] = &[&[], &[0.5], &[0.0, 0.5], &[0.0, 0.0, 1.0]];
|
|
46
|
+
const RK4_B: &[f64] = &[1.0 / 6.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 6.0];
|
|
47
|
+
|
|
48
|
+
/// Relative tolerance for the "is `h` the same as the previous step?" check, so
|
|
49
|
+
/// ULP-level grid roundoff does not spuriously restart the history every step.
|
|
50
|
+
const UNIFORM_REL_TOL: f64 = 1e-9;
|
|
51
|
+
|
|
52
|
+
/// Shared multistep state: the ring buffer of past slopes (most-recent first),
|
|
53
|
+
/// per-step scratch, and the RK4 bootstrap work. Grown to the system dimension on
|
|
54
|
+
/// first use and reused with no per-step allocation after warm-up.
|
|
55
|
+
#[derive(Default)]
|
|
56
|
+
struct AdamsState {
|
|
57
|
+
/// Past slopes `f_{n-1}, f_{n-2}, …` (most recent at the front).
|
|
58
|
+
hist: VecDeque<Vec<f64>>,
|
|
59
|
+
/// `f` at the current point, recomputed each step.
|
|
60
|
+
fc: Vec<f64>,
|
|
61
|
+
/// `f` at the predicted point (predictor–corrector only).
|
|
62
|
+
fp: Vec<f64>,
|
|
63
|
+
/// Proposed next state.
|
|
64
|
+
u_new: Vec<f64>,
|
|
65
|
+
/// RK4 bootstrap buffers.
|
|
66
|
+
rk: RkWork,
|
|
67
|
+
/// The step size of the previous accepted step (`NaN` until the first).
|
|
68
|
+
h_prev: f64,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl AdamsState {
|
|
72
|
+
fn new() -> Self {
|
|
73
|
+
AdamsState {
|
|
74
|
+
h_prev: f64::NAN,
|
|
75
|
+
..AdamsState::default()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn ensure(&mut self, n: usize) {
|
|
80
|
+
if self.fc.len() != n {
|
|
81
|
+
self.fc = vec![0.0; n];
|
|
82
|
+
self.fp = vec![0.0; n];
|
|
83
|
+
self.u_new = vec![0.0; n];
|
|
84
|
+
self.hist.clear();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn uniform(&self, h: f64) -> bool {
|
|
89
|
+
self.h_prev.is_finite() && (h - self.h_prev).abs() <= UNIFORM_REL_TOL * (1.0 + h.abs())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Push the current slope `fc` to the front of the history, capping the buffer
|
|
93
|
+
/// at `keep` entries (reusing the evicted buffer to avoid per-step allocation).
|
|
94
|
+
fn record(&mut self, keep: usize) {
|
|
95
|
+
let mut buf = if self.hist.len() >= keep {
|
|
96
|
+
self.hist
|
|
97
|
+
.pop_back()
|
|
98
|
+
.unwrap_or_else(|| vec![0.0; self.fc.len()])
|
|
99
|
+
} else {
|
|
100
|
+
vec![0.0; self.fc.len()]
|
|
101
|
+
};
|
|
102
|
+
buf.copy_from_slice(&self.fc);
|
|
103
|
+
self.hist.push_front(buf);
|
|
104
|
+
while self.hist.len() > keep {
|
|
105
|
+
self.hist.pop_back();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// One explicit-multistep step. `ab` are the Adams–Bashforth weights applied to
|
|
111
|
+
/// `[f_c, f_{n-1}, …]` (the predictor); `corrector`, when present, are the
|
|
112
|
+
/// Adams–Moulton weights applied to `[f_pred, f_c, f_{n-1}, …]` (the PECE
|
|
113
|
+
/// corrector). `keep = order − 1` past slopes are retained. The history restarts
|
|
114
|
+
/// with an RK4 step whenever it is too short or `h` changed.
|
|
115
|
+
fn adams_step(
|
|
116
|
+
ev: &dyn Evaluator,
|
|
117
|
+
st: &mut SolverState,
|
|
118
|
+
h: f64,
|
|
119
|
+
state: &mut AdamsState,
|
|
120
|
+
keep: usize,
|
|
121
|
+
ab: &[f64],
|
|
122
|
+
corrector: Option<&[f64]>,
|
|
123
|
+
) -> StepOutcome {
|
|
124
|
+
let n = st.u.len();
|
|
125
|
+
state.ensure(n);
|
|
126
|
+
|
|
127
|
+
// f at the current point.
|
|
128
|
+
ev.eval(&st.u, &st.p, st.t, &mut st.scratch, &mut state.fc);
|
|
129
|
+
if !state.fc.iter().all(|x| x.is_finite()) {
|
|
130
|
+
return StepOutcome::Failed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if state.hist.len() < keep || !state.uniform(h) {
|
|
134
|
+
// A step-size *change* invalidates the uniform-mesh history (the classic
|
|
135
|
+
// Adams coefficients assume a uniform past spacing): discard it so the
|
|
136
|
+
// `hist.len() < keep` guard forces `keep` consecutive RK4 starter steps at
|
|
137
|
+
// the new `h` before the Adams formula resumes — without this, the next
|
|
138
|
+
// step would apply uniform-`h` weights to mixed-spacing slopes. A genuine
|
|
139
|
+
// bootstrap (`h_prev` not yet finite) or an ongoing refill leaves the
|
|
140
|
+
// (already new-`h`) history intact.
|
|
141
|
+
if state.h_prev.is_finite() && !state.uniform(h) {
|
|
142
|
+
state.hist.clear();
|
|
143
|
+
}
|
|
144
|
+
// Self-start / restart: one RK4 step. `fixed_step` advances st (and is the
|
|
145
|
+
// exact RK4 used by the `rk4` kernel, so the bootstrap is 4th-order).
|
|
146
|
+
match fixed_step(ev, st, h, RK4_C, RK4_A, RK4_B, &mut state.rk) {
|
|
147
|
+
StepOutcome::Accepted { .. } => {}
|
|
148
|
+
other => return other,
|
|
149
|
+
}
|
|
150
|
+
state.record(keep);
|
|
151
|
+
state.h_prev = h;
|
|
152
|
+
return StepOutcome::Accepted { h_next: h };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Adams–Bashforth predictor: u_p = u + h·(ab[0]·f_c + Σ_j ab[1+j]·f_{n-1-j}).
|
|
156
|
+
for d in 0..n {
|
|
157
|
+
let mut acc = ab[0] * state.fc[d];
|
|
158
|
+
for (j, hslot) in state.hist.iter().enumerate() {
|
|
159
|
+
acc += ab[1 + j] * hslot[d];
|
|
160
|
+
}
|
|
161
|
+
state.u_new[d] = st.u[d] + h * acc;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if let Some(am) = corrector {
|
|
165
|
+
// Evaluate at the predicted point, then correct (PECE).
|
|
166
|
+
ev.eval(
|
|
167
|
+
&state.u_new,
|
|
168
|
+
&st.p,
|
|
169
|
+
st.t + h,
|
|
170
|
+
&mut st.scratch,
|
|
171
|
+
&mut state.fp,
|
|
172
|
+
);
|
|
173
|
+
if !state.fp.iter().all(|x| x.is_finite()) {
|
|
174
|
+
return StepOutcome::Rejected { h_next: 0.5 * h };
|
|
175
|
+
}
|
|
176
|
+
for d in 0..n {
|
|
177
|
+
// u_{n+1} = u + h·(am[0]·f_p + am[1]·f_c + Σ_j am[2+j]·f_{n-1-j}).
|
|
178
|
+
let mut acc = am[0] * state.fp[d] + am[1] * state.fc[d];
|
|
179
|
+
for (j, hslot) in state.hist.iter().enumerate() {
|
|
180
|
+
if 2 + j >= am.len() {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
acc += am[2 + j] * hslot[d];
|
|
184
|
+
}
|
|
185
|
+
state.u_new[d] = st.u[d] + h * acc;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if !state.u_new.iter().all(|x| x.is_finite()) {
|
|
190
|
+
return StepOutcome::Rejected { h_next: 0.5 * h };
|
|
191
|
+
}
|
|
192
|
+
st.u.copy_from_slice(&state.u_new);
|
|
193
|
+
st.t += h;
|
|
194
|
+
state.record(keep);
|
|
195
|
+
state.h_prev = h;
|
|
196
|
+
StepOutcome::Accepted { h_next: h }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Adams–Bashforth weights [f_c, f_{n-1}, …].
|
|
200
|
+
const AB3: &[f64] = &[23.0 / 12.0, -16.0 / 12.0, 5.0 / 12.0];
|
|
201
|
+
const AB4: &[f64] = &[55.0 / 24.0, -59.0 / 24.0, 37.0 / 24.0, -9.0 / 24.0];
|
|
202
|
+
// Adams–Moulton-4 corrector weights [f_pred, f_c, f_{n-1}, f_{n-2}].
|
|
203
|
+
const AM4: &[f64] = &[9.0 / 24.0, 19.0 / 24.0, -5.0 / 24.0, 1.0 / 24.0];
|
|
204
|
+
|
|
205
|
+
/// Adams–Bashforth 3-step explicit multistep kernel (order 3).
|
|
206
|
+
#[derive(Default)]
|
|
207
|
+
pub struct Ab3 {
|
|
208
|
+
state: AdamsState,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl Ab3 {
|
|
212
|
+
/// A fresh kernel with empty history (self-starts with RK4).
|
|
213
|
+
pub fn new() -> Self {
|
|
214
|
+
Ab3 {
|
|
215
|
+
state: AdamsState::new(),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
impl Solver for Ab3 {
|
|
221
|
+
fn name(&self) -> &'static str {
|
|
222
|
+
"ab3"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn caps(&self) -> Caps {
|
|
226
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn step(&mut self, ev: &dyn Evaluator, st: &mut SolverState, h: f64) -> StepOutcome {
|
|
230
|
+
adams_step(ev, st, h, &mut self.state, 2, AB3, None)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
register_solver!(
|
|
235
|
+
"ab3",
|
|
236
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode)),
|
|
237
|
+
|| Box::new(Ab3::new())
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/// Adams–Bashforth 4-step explicit multistep kernel (order 4).
|
|
241
|
+
#[derive(Default)]
|
|
242
|
+
pub struct Ab4 {
|
|
243
|
+
state: AdamsState,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
impl Ab4 {
|
|
247
|
+
/// A fresh kernel with empty history (self-starts with RK4).
|
|
248
|
+
pub fn new() -> Self {
|
|
249
|
+
Ab4 {
|
|
250
|
+
state: AdamsState::new(),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
impl Solver for Ab4 {
|
|
256
|
+
fn name(&self) -> &'static str {
|
|
257
|
+
"ab4"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn caps(&self) -> Caps {
|
|
261
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn step(&mut self, ev: &dyn Evaluator, st: &mut SolverState, h: f64) -> StepOutcome {
|
|
265
|
+
adams_step(ev, st, h, &mut self.state, 3, AB4, None)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
register_solver!(
|
|
270
|
+
"ab4",
|
|
271
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode)),
|
|
272
|
+
|| Box::new(Ab4::new())
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/// Adams–Bashforth–Moulton predictor–corrector kernel (PECE, order 4).
|
|
276
|
+
#[derive(Default)]
|
|
277
|
+
pub struct Abm4 {
|
|
278
|
+
state: AdamsState,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
impl Abm4 {
|
|
282
|
+
/// A fresh kernel with empty history (self-starts with RK4).
|
|
283
|
+
pub fn new() -> Self {
|
|
284
|
+
Abm4 {
|
|
285
|
+
state: AdamsState::new(),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
impl Solver for Abm4 {
|
|
291
|
+
fn name(&self) -> &'static str {
|
|
292
|
+
"abm4"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn caps(&self) -> Caps {
|
|
296
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fn step(&mut self, ev: &dyn Evaluator, st: &mut SolverState, h: f64) -> StepOutcome {
|
|
300
|
+
adams_step(ev, st, h, &mut self.state, 3, AB4, Some(AM4))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
register_solver!(
|
|
305
|
+
"abm4",
|
|
306
|
+
Caps::explicit(ProblemKinds::of(ProblemKind::Ode)),
|
|
307
|
+
|| Box::new(Abm4::new())
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
#[cfg(test)]
|
|
311
|
+
mod tests {
|
|
312
|
+
use super::*;
|
|
313
|
+
use crate::explicit::testkit::{max_abs_diff, HarmonicEval};
|
|
314
|
+
|
|
315
|
+
/// Drive a kernel through a uniform fixed-step loop to `t_final`.
|
|
316
|
+
fn integrate_uniform(
|
|
317
|
+
solver: &mut dyn Solver,
|
|
318
|
+
ev: &dyn Evaluator,
|
|
319
|
+
u0: Vec<f64>,
|
|
320
|
+
t_final: f64,
|
|
321
|
+
h: f64,
|
|
322
|
+
) -> Vec<f64> {
|
|
323
|
+
let mut st = SolverState::for_evaluator(ev, u0, 0.0, vec![]);
|
|
324
|
+
let n = (t_final / h).round() as usize;
|
|
325
|
+
for _ in 0..n {
|
|
326
|
+
match solver.step(ev, &mut st, h) {
|
|
327
|
+
StepOutcome::Accepted { .. } => {}
|
|
328
|
+
other => panic!("unexpected outcome {other:?}"),
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
st.u
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Empirical convergence order from two final-state errors under h-halving.
|
|
335
|
+
fn measure_order(make: impl Fn() -> Box<dyn Solver>, hs: &[f64]) -> f64 {
|
|
336
|
+
let ev = HarmonicEval { omega: 1.0 };
|
|
337
|
+
let t_final = 5.0;
|
|
338
|
+
let mut errs = Vec::new();
|
|
339
|
+
for &h in hs {
|
|
340
|
+
let got = integrate_uniform(&mut *make(), &ev, vec![1.0, 0.0], t_final, h);
|
|
341
|
+
let exact = vec![t_final.cos(), -t_final.sin()];
|
|
342
|
+
errs.push((h, max_abs_diff(&got, &exact)));
|
|
343
|
+
}
|
|
344
|
+
let (h1, e1) = errs[errs.len() - 2];
|
|
345
|
+
let (h2, e2) = errs[errs.len() - 1];
|
|
346
|
+
(e1 / e2).ln() / (h1 / h2).ln()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn caps_are_explicit_non_adaptive_ode() {
|
|
351
|
+
for s in [
|
|
352
|
+
Box::new(Ab3::new()) as Box<dyn Solver>,
|
|
353
|
+
Box::new(Ab4::new()),
|
|
354
|
+
Box::new(Abm4::new()),
|
|
355
|
+
] {
|
|
356
|
+
assert!(!s.caps().adaptive);
|
|
357
|
+
assert!(!s.caps().needs_jacobian);
|
|
358
|
+
assert!(s.caps().supports(ProblemKind::Ode));
|
|
359
|
+
}
|
|
360
|
+
assert_eq!(Ab3::new().name(), "ab3");
|
|
361
|
+
assert_eq!(Ab4::new().name(), "ab4");
|
|
362
|
+
assert_eq!(Abm4::new().name(), "abm4");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn ab3_is_third_order() {
|
|
367
|
+
let order = measure_order(|| Box::new(Ab3::new()), &[0.04, 0.02, 0.01]);
|
|
368
|
+
assert!((order - 3.0).abs() < 0.4, "measured AB3 order {order}");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#[test]
|
|
372
|
+
fn ab4_is_fourth_order() {
|
|
373
|
+
let order = measure_order(|| Box::new(Ab4::new()), &[0.04, 0.02, 0.01]);
|
|
374
|
+
assert!((order - 4.0).abs() < 0.4, "measured AB4 order {order}");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn abm4_is_fourth_order() {
|
|
379
|
+
let order = measure_order(|| Box::new(Abm4::new()), &[0.04, 0.02, 0.01]);
|
|
380
|
+
assert!((order - 4.0).abs() < 0.4, "measured ABM4 order {order}");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn ab4_matches_analytic_harmonic_solution() {
|
|
385
|
+
// A wrong coefficient breaks accuracy against the exact cos/sin solution.
|
|
386
|
+
let ev = HarmonicEval { omega: 1.0 };
|
|
387
|
+
let got = integrate_uniform(&mut Ab4::new(), &ev, vec![1.0, 0.0], 6.0, 0.005);
|
|
388
|
+
let exact = vec![(6.0_f64).cos(), -(6.0_f64).sin()];
|
|
389
|
+
assert!(
|
|
390
|
+
max_abs_diff(&got, &exact) < 1e-6,
|
|
391
|
+
"AB4 error {}",
|
|
392
|
+
max_abs_diff(&got, &exact)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn step_size_change_discards_stale_history() {
|
|
398
|
+
// After an h-change the next `keep` steps must be pure RK4 restarts over a
|
|
399
|
+
// freshly-rebuilt uniform mesh — NOT Adams steps over a contaminated
|
|
400
|
+
// mixed-spacing history. We assert the post-change AB4 steps are
|
|
401
|
+
// bit-for-bit identical to a standalone RK4 from the same state, which only
|
|
402
|
+
// holds if the stale h1 history was discarded (regression for the restart
|
|
403
|
+
// bug: record() alone never shrank the history below `keep`).
|
|
404
|
+
use crate::explicit::Rk4;
|
|
405
|
+
let ev = HarmonicEval { omega: 1.0 };
|
|
406
|
+
let mut ab = Ab4::new();
|
|
407
|
+
let mut st = SolverState::for_evaluator(&ev, vec![1.0, 0.0], 0.0, vec![]);
|
|
408
|
+
for _ in 0..50 {
|
|
409
|
+
ab.step(&ev, &mut st, 0.01); // fill a full uniform-h1 history
|
|
410
|
+
}
|
|
411
|
+
// Branch a standalone RK4 from the exact current state.
|
|
412
|
+
let mut rk = Rk4::new();
|
|
413
|
+
let mut st_rk = SolverState::for_evaluator(&ev, st.u.clone(), st.t, vec![]);
|
|
414
|
+
let h2 = 0.017; // a genuine step-size change
|
|
415
|
+
for _ in 0..3 {
|
|
416
|
+
// keep = 3 for AB4
|
|
417
|
+
ab.step(&ev, &mut st, h2);
|
|
418
|
+
rk.step(&ev, &mut st_rk, h2);
|
|
419
|
+
}
|
|
420
|
+
assert_eq!(
|
|
421
|
+
st.u, st_rk.u,
|
|
422
|
+
"post-h-change AB4 steps must be exact RK4 restarts (stale history discarded)"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[test]
|
|
427
|
+
fn restarts_cleanly_when_step_size_changes() {
|
|
428
|
+
// Drive with one h, then a different h: the kernel must restart its history
|
|
429
|
+
// (RK4) and still integrate accurately rather than corrupt the multistep
|
|
430
|
+
// formula with a non-uniform mesh.
|
|
431
|
+
let ev = HarmonicEval { omega: 1.0 };
|
|
432
|
+
let mut s = Ab4::new();
|
|
433
|
+
let mut st = SolverState::for_evaluator(&ev, vec![1.0, 0.0], 0.0, vec![]);
|
|
434
|
+
for _ in 0..100 {
|
|
435
|
+
assert!(matches!(
|
|
436
|
+
s.step(&ev, &mut st, 0.01),
|
|
437
|
+
StepOutcome::Accepted { .. }
|
|
438
|
+
));
|
|
439
|
+
}
|
|
440
|
+
for _ in 0..100 {
|
|
441
|
+
assert!(matches!(
|
|
442
|
+
s.step(&ev, &mut st, 0.013),
|
|
443
|
+
StepOutcome::Accepted { .. }
|
|
444
|
+
));
|
|
445
|
+
}
|
|
446
|
+
let t = st.t;
|
|
447
|
+
let exact = vec![t.cos(), -t.sin()];
|
|
448
|
+
assert!(
|
|
449
|
+
max_abs_diff(&st.u, &exact) < 1e-4,
|
|
450
|
+
"post-restart error too large"
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|