tsdynamics 5.2.0__tar.gz → 5.2.2__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-5.2.0 → tsdynamics-5.2.2}/PKG-INFO +1 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/Cargo.toml +16 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/Cargo.toml +24 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/marshal.rs +108 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/mod.rs +4 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/lib.rs +22 -9
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/ensemble.rs +70 -16
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/map.rs +58 -14
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/sde.rs +128 -16
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/eval.rs +71 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/tape.rs +92 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/src/codegen.rs +147 -37
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/adams.rs +31 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/control.rs +12 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/tsit5.rs +31 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/bdf.rs +10 -4
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/newton.rs +295 -8
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-vm/src/interp.rs +47 -55
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/pyproject.toml +1 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/__init__.py +1 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result.py +3 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_array.py +50 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_base.py +17 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_collection.py +5 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_json.py +9 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_scalar.py +8 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_viz.py +68 -25
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/attractors.py +38 -11
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/basins.py +12 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/continuation.py +25 -4
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/metrics.py +65 -9
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/chaos/expansion.py +36 -8
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/chaos/gali.py +42 -15
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/chaos/zero_one.py +20 -6
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/_scaling.py +41 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/generalized.py +31 -4
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/embedding/delay.py +53 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/embedding/dimension.py +44 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/embedding/embed.py +11 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/core.py +19 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/dispersion.py +9 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/multiscale.py +21 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/sample.py +21 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/fixedpoints/_common.py +64 -24
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/fixedpoints/fixed.py +9 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/fixedpoints/periodic.py +55 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/lyapunov/__init__.py +59 -14
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/lyapunov/from_data.py +49 -14
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/orbits/orbit_diagram.py +67 -8
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/recurrence/rqa.py +47 -12
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/sampling/sagitta.py +123 -170
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/surrogate/_common.py +12 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/surrogate/generators.py +44 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/surrogate/hypothesis.py +35 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/surrogate/statistics.py +21 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/data/sampling.py +175 -16
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/_crossings.py +9 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/_variational.py +17 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/ensemble.py +26 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/poincare.py +55 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/stroboscopic.py +48 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/tangent.py +25 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/compile.py +79 -13
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/events.py +124 -15
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/problem.py +10 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/reference.py +15 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/run.py +70 -11
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/_dde_lyapunov.py +9 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/base.py +73 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/continuous.py +53 -6
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/discrete.py +54 -2
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/protocol.py +25 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/stochastic.py +17 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/wrapped.py +71 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/solvers/__init__.py +21 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/solvers/select.py +49 -12
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/_common.py +23 -4
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/_result.py +14 -5
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/features.py +52 -7
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/preprocessing.py +17 -1
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/spectral.py +22 -3
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/__init__.py +28 -37
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/LICENSE +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/README.md +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/Cargo.lock +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/dde.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/events.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/map.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/ode.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/sde.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-core/src/bridge/stepper.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/Cargo.toml +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/dde.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/event.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/integrate.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/lib.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/problem.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/rng.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-engine/src/testkit.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/Cargo.toml +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/builder.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/lib.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/op.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/src/reference.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/coverage_tape.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/CircadianRhythm.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/Lorenz.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/SanUmSrisuchinwong.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/SprottMore.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/Torus.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/fixtures/YuWang2.txt +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/gen_fixtures.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/golden_fixtures.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-ir/tests/roundtrip.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/Cargo.toml +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/src/error.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/src/evaluator.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/src/lib.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/src/shims.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/tests/compile_latency.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/tests/golden_fixtures.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-jit/tests/interpreter_equivalence.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/Cargo.toml +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/caps.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/bs3.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/cashkarp.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/dop853.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/euler.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/heun.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/heun_euler.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/midpoint.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/mod.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/ralston.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/rk4.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/rk45.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/rk4_38.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/rkf45.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/ssprk3.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/explicit/testkit.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/backward_euler.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/control.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/implicit_midpoint.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/linalg.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/mod.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/rosenbrock.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/sdirk2.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/trapezoid.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/implicit/trbdf2.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/lib.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/registry.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/sde/euler_maruyama.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/sde/milstein.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/sde/mod.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/sde/testkit.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/src/solver.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/tests/explicit_accuracy.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/tests/implicit_stiff.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-solvers/tests/registry.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-vm/Cargo.toml +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-vm/src/lib.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-vm/tests/golden_fixtures.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/crates/tsdyn-vm/tests/oracle_equivalence.rs +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/_result_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_result_scaling.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/_tangent.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/basins/_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/chaos/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/chaos/_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/correlation.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/dimensions/fixedmass.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/embedding/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/embedding/_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/lz.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/entropy/permutation.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/fixedpoints/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/orbits/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/orbits/poincare.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/orbits/return_map.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/recurrence/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/recurrence/_common.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/recurrence/matrix.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/recurrence/windowed.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/sampling/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/analysis/surrogate/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/data/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/data/trajectory.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/_base.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/projected.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/derived/wrapped.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/engine/symbols.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/errors.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/_accessors.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/_plottable.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/families/delay.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/plugins.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/py.typed +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/registry.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/solvers/explicit.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/solvers/implicit.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/solvers/stochastic.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/continuous/spatial_fields.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/transforms/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/utils/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/utils/grids.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/compose.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/export.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/producers.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/caps.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/json.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/mpl/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/mpl/_anim.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/mpl/_core.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/mpl/_threed.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/_anim.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/_composite.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/_core.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/_html.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/plotly/_threed.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/threejs/__init__.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/render/threejs/_lower.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/spec.py +0 -0
- {tsdynamics-5.2.0 → tsdynamics-5.2.2}/src/tsdynamics/viz/style.py +0 -0
|
@@ -49,6 +49,22 @@ repository = "https://github.com/El3ssar/TSDynamics"
|
|
|
49
49
|
# path here; external engine/binding deps (pyo3, numpy, rayon) are pre-pinned to
|
|
50
50
|
# the versions already proven in `tsdynamics-core`. Solver-/JIT-specific deps
|
|
51
51
|
# (e.g. cranelift for tsdyn-jit) are declared by their owning crate, not here.
|
|
52
|
+
# Release-profile tuning for the workspace members (`tsdyn-ir`/`-vm`/`-jit`/
|
|
53
|
+
# `-solvers`/`-engine`). Profiles are resolved from the root manifest of the
|
|
54
|
+
# build, so this block governs any `cargo build --release` / benchmark over the
|
|
55
|
+
# workspace (e.g. `rust-workspace.yml`); the wheel build does NOT pass through
|
|
56
|
+
# here — it roots at the excluded `tsdyn-core` manifest, which carries its own
|
|
57
|
+
# matching `[profile.release]`. `lto = "thin"` enables cross-crate inlining of
|
|
58
|
+
# the hot RK / interpreter loops; `codegen-units = 1` hands each crate to the
|
|
59
|
+
# optimizer as a single unit. Conventional release tuning for a numerical
|
|
60
|
+
# engine, and **answer-preserving** — it changes only code generation, never the
|
|
61
|
+
# IEEE-754 results, so `interp == jit`, the golden fixtures and parallel==serial
|
|
62
|
+
# ensembles are bit-for-bit unchanged. Affects the release build only; `cargo
|
|
63
|
+
# test` (debug) is untouched.
|
|
64
|
+
[profile.release]
|
|
65
|
+
lto = "thin"
|
|
66
|
+
codegen-units = 1
|
|
67
|
+
|
|
52
68
|
[workspace.dependencies]
|
|
53
69
|
tsdyn-ir = { path = "tsdyn-ir" }
|
|
54
70
|
tsdyn-vm = { path = "tsdyn-vm" }
|
|
@@ -48,3 +48,27 @@ tsdyn-engine = { path = "../tsdyn-engine" }
|
|
|
48
48
|
# building the shipped extension; left OFF for `cargo test` so the unit-test
|
|
49
49
|
# harness links libpython and runs. Kept off the default set on purpose.
|
|
50
50
|
extension-module = ["pyo3/extension-module"]
|
|
51
|
+
|
|
52
|
+
# Release-profile tuning for the shipped wheel.
|
|
53
|
+
#
|
|
54
|
+
# This crate is the maturin build root (the project's `pyproject.toml` points
|
|
55
|
+
# `manifest-path` here) and is **excluded** from `crates/Cargo.toml`'s workspace,
|
|
56
|
+
# so it does NOT inherit any workspace `[profile.release]` — Cargo resolves the
|
|
57
|
+
# profile from the root manifest of whatever is being built, and for the wheel
|
|
58
|
+
# that root is *this* manifest. Without these keys the wheel built with cargo's
|
|
59
|
+
# defaults (`lto = false`, `codegen-units = 16`), which leaves the hot RK /
|
|
60
|
+
# interpreter loops in `tsdyn-vm` / `tsdyn-solvers` un-inlined across crate
|
|
61
|
+
# boundaries.
|
|
62
|
+
#
|
|
63
|
+
# `lto = "thin"` enables cross-crate inlining of the tape-evaluation and
|
|
64
|
+
# solver-step hot paths (thin LTO keeps build times reasonable vs. `"fat"`);
|
|
65
|
+
# `codegen-units = 1` gives the optimizer the whole crate as one unit. Both are
|
|
66
|
+
# the conventional release tuning for a tight numerical kernel. They affect ONLY
|
|
67
|
+
# the release build — `cargo test` (debug) is untouched — and are
|
|
68
|
+
# **answer-preserving**: LTO/codegen-unit changes alter only how the same
|
|
69
|
+
# floating-point operations are scheduled into machine code, never the IEEE-754
|
|
70
|
+
# result, so `interp == jit`, the golden fixtures, and the parallel==serial
|
|
71
|
+
# ensembles are unchanged.
|
|
72
|
+
[profile.release]
|
|
73
|
+
lto = "thin"
|
|
74
|
+
codegen-units = 1
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//! unchanged.
|
|
10
10
|
|
|
11
11
|
use tsdyn_engine::{EventDirection, IntegrateError, MapError, SdeError};
|
|
12
|
-
use tsdyn_ir::{Evaluator, Tape};
|
|
12
|
+
use tsdyn_ir::{Evaluator, Op, Tape};
|
|
13
13
|
use tsdyn_jit::JitEvaluator;
|
|
14
14
|
use tsdyn_solvers::explicit::{Bs3, CashKarp, Dop853, HeunEuler, Rk45, Rkf45, Tsit5};
|
|
15
15
|
use tsdyn_solvers::implicit::{
|
|
@@ -162,6 +162,7 @@ pub(super) fn build_evaluator_send(
|
|
|
162
162
|
/// Mirrors the argument order of the Python `Tape.to_arrays()`:
|
|
163
163
|
/// `(ops, a, b, imm, outputs, jac_outputs, n_state, n_param)`. A malformed tape
|
|
164
164
|
/// is rejected here, at the boundary, as [`EngineError::BadTape`].
|
|
165
|
+
#[cfg(test)] // production uses the no-copy `build_tape_owned`; kept for the cross-check test.
|
|
165
166
|
#[allow(clippy::too_many_arguments)] // the eight arrays are the tape contract.
|
|
166
167
|
pub fn build_tape(
|
|
167
168
|
ops: &[i32],
|
|
@@ -177,6 +178,39 @@ pub fn build_tape(
|
|
|
177
178
|
.map_err(|e| EngineError::BadTape(e.to_string()))
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
/// Build a validated [`Tape`] from **owned** wire arrays — the move (no-copy)
|
|
182
|
+
/// FFI ingestion path.
|
|
183
|
+
///
|
|
184
|
+
/// Identical in result to `build_tape`, but takes its arrays by value: it
|
|
185
|
+
/// decodes `ops` (the raw wire opcodes) into [`Op`]s **in place** (rejecting an
|
|
186
|
+
/// unknown opcode as [`EngineError::BadTape`], exactly as the slice path does),
|
|
187
|
+
/// then moves the already-owned `a`/`b`/`imm`/`outputs`/`jac_outputs` `Vec`s
|
|
188
|
+
/// straight into [`Tape::from_parts`]. Because the caller (`crate::OwnedTape`)
|
|
189
|
+
/// already owns these `Vec`s (it copied the NumPy views once so they could
|
|
190
|
+
/// cross [`Python::detach`]), this avoids the second
|
|
191
|
+
/// allocation `Tape::from_arrays` makes when it `to_vec`s every slice. The tape
|
|
192
|
+
/// is byte-identical — `from_parts` runs the same structural validation
|
|
193
|
+
/// `from_arrays` runs after its copy.
|
|
194
|
+
///
|
|
195
|
+
/// [`Python::detach`]: pyo3::Python::detach
|
|
196
|
+
#[allow(clippy::too_many_arguments)] // the eight arrays are the tape contract.
|
|
197
|
+
pub fn build_tape_owned(
|
|
198
|
+
ops: Vec<i32>,
|
|
199
|
+
a: Vec<i32>,
|
|
200
|
+
b: Vec<i32>,
|
|
201
|
+
imm: Vec<f64>,
|
|
202
|
+
outputs: Vec<i32>,
|
|
203
|
+
jac_outputs: Vec<i32>,
|
|
204
|
+
n_state: usize,
|
|
205
|
+
n_param: usize,
|
|
206
|
+
) -> Result<Tape, EngineError> {
|
|
207
|
+
// Decode the wire opcodes to `Op`s; an unknown opcode is a malformed tape.
|
|
208
|
+
let decoded: Result<Vec<Op>, _> = ops.into_iter().map(Op::from_i32).collect();
|
|
209
|
+
let decoded = decoded.map_err(|e| EngineError::BadTape(e.to_string()))?;
|
|
210
|
+
Tape::from_parts(decoded, a, b, imm, outputs, jac_outputs, n_state, n_param)
|
|
211
|
+
.map_err(|e| EngineError::BadTape(e.to_string()))
|
|
212
|
+
}
|
|
213
|
+
|
|
180
214
|
// ---------------------------------------------------------------------------
|
|
181
215
|
// Solver resolution
|
|
182
216
|
// ---------------------------------------------------------------------------
|
|
@@ -379,3 +413,76 @@ pub(super) fn map_diverge_msg(e: &MapError) -> String {
|
|
|
379
413
|
pub(super) fn sde_diverge_msg(e: &SdeError) -> String {
|
|
380
414
|
format!("SDE integration diverged before reaching the final time: {e}")
|
|
381
415
|
}
|
|
416
|
+
|
|
417
|
+
#[cfg(test)]
|
|
418
|
+
mod tests {
|
|
419
|
+
use super::*;
|
|
420
|
+
|
|
421
|
+
// A minimal valid wire tape: f(u) = u0 * p0 (dim 1, n_state 1, n_param 1).
|
|
422
|
+
// Wire opcodes: State=1, Param=2, Mul=12 (the v2 contract values pinned in
|
|
423
|
+
// `tsdyn_ir`'s tape tests).
|
|
424
|
+
#[allow(clippy::type_complexity)]
|
|
425
|
+
fn ok_wire() -> (Vec<i32>, Vec<i32>, Vec<i32>, Vec<f64>, Vec<i32>) {
|
|
426
|
+
let ops = vec![1, 2, 12];
|
|
427
|
+
let a = vec![0, 0, 0];
|
|
428
|
+
let b = vec![0, 0, 1];
|
|
429
|
+
let imm = vec![0.0, 0.0, 0.0];
|
|
430
|
+
let outputs = vec![2];
|
|
431
|
+
(ops, a, b, imm, outputs)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// The move path ([`build_tape_owned`]) and the slice/copy path
|
|
435
|
+
/// ([`build_tape`]) must produce a byte-identical [`Tape`] — the no-copy
|
|
436
|
+
/// optimisation is answer-preserving.
|
|
437
|
+
#[test]
|
|
438
|
+
fn owned_path_equals_slice_path() {
|
|
439
|
+
let (ops, a, b, imm, outputs) = ok_wire();
|
|
440
|
+
let jac: Vec<i32> = vec![];
|
|
441
|
+
|
|
442
|
+
let by_slice =
|
|
443
|
+
build_tape(&ops, &a, &b, &imm, &outputs, &jac, 1, 1).expect("slice path builds");
|
|
444
|
+
let by_move = build_tape_owned(
|
|
445
|
+
ops.clone(),
|
|
446
|
+
a.clone(),
|
|
447
|
+
b.clone(),
|
|
448
|
+
imm.clone(),
|
|
449
|
+
outputs.clone(),
|
|
450
|
+
jac.clone(),
|
|
451
|
+
1,
|
|
452
|
+
1,
|
|
453
|
+
)
|
|
454
|
+
.expect("move path builds");
|
|
455
|
+
|
|
456
|
+
// `Tape` derives `PartialEq`, so this is a full structural equality.
|
|
457
|
+
assert_eq!(by_slice, by_move);
|
|
458
|
+
assert_eq!(by_move.n_reg(), 3);
|
|
459
|
+
assert_eq!(by_move.dim(), 1);
|
|
460
|
+
assert!(!by_move.has_jacobian());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// The move path decodes opcodes in place and must reject an unknown one as
|
|
464
|
+
/// [`EngineError::BadTape`], exactly as the slice path does.
|
|
465
|
+
#[test]
|
|
466
|
+
fn owned_path_rejects_unknown_opcode() {
|
|
467
|
+
let (mut ops, a, b, imm, outputs) = ok_wire();
|
|
468
|
+
ops[2] = 99; // not a known opcode
|
|
469
|
+
let err = build_tape_owned(ops, a, b, imm, outputs, vec![], 1, 1).expect_err("must reject");
|
|
470
|
+
assert!(matches!(err, EngineError::BadTape(_)));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// A malformed (out-of-range output) wire tape is rejected identically on the
|
|
474
|
+
/// move path and the slice path.
|
|
475
|
+
#[test]
|
|
476
|
+
fn owned_path_rejects_bad_shape_like_slice_path() {
|
|
477
|
+
let (ops, a, b, imm, _outputs) = ok_wire();
|
|
478
|
+
let bad_outputs = vec![9]; // output register 9 with only 3 instructions
|
|
479
|
+
|
|
480
|
+
let slice_err = build_tape(&ops, &a, &b, &imm, &bad_outputs, &[], 1, 1)
|
|
481
|
+
.expect_err("slice path rejects");
|
|
482
|
+
let move_err = build_tape_owned(ops, a, b, imm, bad_outputs, vec![], 1, 1)
|
|
483
|
+
.expect_err("move path rejects");
|
|
484
|
+
|
|
485
|
+
assert!(matches!(slice_err, EngineError::BadTape(_)));
|
|
486
|
+
assert_eq!(slice_err, move_err);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//! the test suite) see exactly the same `bridge::*` surface as before:
|
|
16
16
|
//!
|
|
17
17
|
//! - [`marshal`] — the shared FFI plumbing: the [`EngineError`] enum, the
|
|
18
|
-
//! [`VmEvaluator`] adapter + evaluator builders,
|
|
18
|
+
//! [`VmEvaluator`] adapter + evaluator builders, `build_tape` ingestion,
|
|
19
19
|
//! [`resolve_solver`] / [`build_solver`], the input/grid/shape guards and the
|
|
20
20
|
//! divergence-message helpers. Every family entry point is built on it.
|
|
21
21
|
//! - [`ode`] — [`eval_rhs`] / [`eval_jac`] / [`integrate_dense`] /
|
|
@@ -45,7 +45,9 @@ pub mod stepper;
|
|
|
45
45
|
pub use dde::integrate_dde_dense;
|
|
46
46
|
pub use events::integrate_events_dense;
|
|
47
47
|
pub use map::{iterate_map, map_ensemble_final};
|
|
48
|
-
|
|
48
|
+
#[cfg(test)]
|
|
49
|
+
pub use marshal::build_tape;
|
|
50
|
+
pub use marshal::EngineError;
|
|
49
51
|
pub use ode::{ensemble_final, eval_jac, eval_rhs, integrate_dense};
|
|
50
52
|
pub use sde::{sde_ensemble_final, sde_integrate_dense};
|
|
51
53
|
pub use stepper::OdeStepper;
|
|
@@ -125,15 +125,28 @@ impl OwnedTape {
|
|
|
125
125
|
self.outputs.len()
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/// Build the validated [`tsdyn_ir::Tape`] (inside the GIL release)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
/// Build the validated [`tsdyn_ir::Tape`] (inside the GIL release),
|
|
129
|
+
/// **consuming** the owned wire arrays.
|
|
130
|
+
///
|
|
131
|
+
/// `OwnedTape` already holds owned `Vec`s (copied once, in
|
|
132
|
+
/// [`copy_in`](OwnedTape::copy_in), so they could cross the
|
|
133
|
+
/// [`Python::detach`] GIL release). Handing those `Vec`s **by value** to the
|
|
134
|
+
/// engine — decoding `ops` to [`tsdyn_ir::Op`]s in place and moving every
|
|
135
|
+
/// array into [`tsdyn_ir::Tape::from_parts`] — avoids the second allocation
|
|
136
|
+
/// the slice path ([`tsdyn_ir::Tape::from_arrays`], which re-`to_vec`s each
|
|
137
|
+
/// array) would make. The resulting `Tape` is byte-identical: `from_parts`
|
|
138
|
+
/// runs the same validation `from_arrays` does after its copy. `build` takes
|
|
139
|
+
/// `self` because each caller uses the `OwnedTape` exactly once (the system
|
|
140
|
+
/// dimension is read via [`dim`](OwnedTape::dim) *before* the GIL release, and
|
|
141
|
+
/// `build` is the tape's last use, inside the detached closure).
|
|
142
|
+
fn build(self) -> Result<tsdyn_ir::Tape, EngineError> {
|
|
143
|
+
bridge::marshal::build_tape_owned(
|
|
144
|
+
self.ops,
|
|
145
|
+
self.a,
|
|
146
|
+
self.b,
|
|
147
|
+
self.imm,
|
|
148
|
+
self.outputs,
|
|
149
|
+
self.jac_outputs,
|
|
137
150
|
self.n_state,
|
|
138
151
|
self.n_param,
|
|
139
152
|
)
|
|
@@ -120,26 +120,33 @@ where
|
|
|
120
120
|
);
|
|
121
121
|
let n_ic = u0_batch.len() / dim;
|
|
122
122
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// Preallocate the contiguous output and write each trajectory DIRECTLY into
|
|
124
|
+
// its own row slice in parallel. Zipping `states.par_chunks_mut(dim)` with
|
|
125
|
+
// `status.par_iter_mut()` partitions the buffers into disjoint, index-aligned
|
|
126
|
+
// pieces, so worker `i` only ever touches row `i` — no per-trajectory heap
|
|
127
|
+
// `Vec` and no second copy. The partition is positional, so the output is
|
|
128
|
+
// still in input order regardless of how rayon schedules the workers
|
|
129
|
+
// (parallel == serial bit-for-bit, the determinism contract above).
|
|
130
|
+
let mut states = vec![0.0; n_ic * dim];
|
|
131
|
+
let mut status = vec![TrajStatus::Ok; n_ic];
|
|
132
|
+
states
|
|
133
|
+
.par_chunks_mut(dim)
|
|
134
|
+
.zip(status.par_iter_mut())
|
|
135
|
+
.enumerate()
|
|
136
|
+
.for_each(|(i, (row, st))| {
|
|
128
137
|
let u0 = &u0_batch[i * dim..(i + 1) * dim];
|
|
129
138
|
let mut solver = make_solver(i);
|
|
130
139
|
match integrate_final(ev, &mut *solver, u0, p, t0, t1, cfg) {
|
|
131
|
-
Ok(uf) =>
|
|
132
|
-
|
|
140
|
+
Ok(uf) => {
|
|
141
|
+
row.copy_from_slice(&uf);
|
|
142
|
+
*st = TrajStatus::Ok;
|
|
143
|
+
}
|
|
144
|
+
Err(e) => {
|
|
145
|
+
row.fill(f64::NAN);
|
|
146
|
+
*st = TrajStatus::Failed(e);
|
|
147
|
+
}
|
|
133
148
|
}
|
|
134
|
-
})
|
|
135
|
-
.collect();
|
|
136
|
-
|
|
137
|
-
let mut states = Vec::with_capacity(n_ic * dim);
|
|
138
|
-
let mut status = Vec::with_capacity(n_ic);
|
|
139
|
-
for (uf, s) in per_traj {
|
|
140
|
-
states.extend_from_slice(&uf);
|
|
141
|
-
status.push(s);
|
|
142
|
-
}
|
|
149
|
+
});
|
|
143
150
|
EnsembleFinal {
|
|
144
151
|
dim,
|
|
145
152
|
states,
|
|
@@ -300,4 +307,51 @@ mod tests {
|
|
|
300
307
|
assert_eq!(ens.n_ic(), 0);
|
|
301
308
|
assert!(ens.states.is_empty());
|
|
302
309
|
}
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
fn direct_row_write_preserves_index_order_with_interleaved_failures() {
|
|
313
|
+
// Regression for the direct-into-row-slice ensemble write (no per-traj
|
|
314
|
+
// Vec + second copy): an interleaved ok/fail/ok/fail pattern must land
|
|
315
|
+
// each trajectory in its OWN row in input order, with NaN confined to the
|
|
316
|
+
// failed rows — even under a forced multi-thread pool where workers finish
|
|
317
|
+
// out of order. blowup() diverges for x0 > 0 and decays for x0 < 0.
|
|
318
|
+
let ev = VmEval::new(blowup());
|
|
319
|
+
let cfg = IntegrateConfig::new(0.01);
|
|
320
|
+
let n_ic = 64;
|
|
321
|
+
// Even index → +1 (blows up before t=2), odd index → -1 (stays finite).
|
|
322
|
+
let u0: Vec<f64> = (0..n_ic)
|
|
323
|
+
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
|
|
324
|
+
.collect();
|
|
325
|
+
|
|
326
|
+
let pool = rayon::ThreadPoolBuilder::new()
|
|
327
|
+
.num_threads(8)
|
|
328
|
+
.build()
|
|
329
|
+
.unwrap();
|
|
330
|
+
let ens = pool
|
|
331
|
+
.install(|| ensemble_final(&ev, |_| Box::new(Rk4::new()), &u0, &[], 0.0, 2.0, &cfg));
|
|
332
|
+
|
|
333
|
+
assert_eq!(ens.n_ic(), n_ic);
|
|
334
|
+
assert_eq!(ens.n_failed(), n_ic / 2);
|
|
335
|
+
for i in 0..n_ic {
|
|
336
|
+
if i % 2 == 0 {
|
|
337
|
+
assert!(
|
|
338
|
+
matches!(ens.status[i], TrajStatus::Failed(_)),
|
|
339
|
+
"row {i} should have diverged"
|
|
340
|
+
);
|
|
341
|
+
assert!(ens.row(i)[0].is_nan(), "diverged row {i} must be NaN");
|
|
342
|
+
} else {
|
|
343
|
+
assert!(ens.status[i].is_ok(), "row {i} should be finite");
|
|
344
|
+
assert!(
|
|
345
|
+
ens.row(i)[0].is_finite(),
|
|
346
|
+
"finite row {i} must not be NaN-smeared by a neighbour"
|
|
347
|
+
);
|
|
348
|
+
// -1 decaying under dx/dt = x²: x(t) = -1/(1+t), so x(2) = -1/3.
|
|
349
|
+
assert!(
|
|
350
|
+
(ens.row(i)[0] - (-1.0 / 3.0)).abs() < 1e-6,
|
|
351
|
+
"row {i}: {} vs -1/3",
|
|
352
|
+
ens.row(i)[0]
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
303
357
|
}
|
|
@@ -230,23 +230,31 @@ pub fn iterate_ensemble_final(
|
|
|
230
230
|
);
|
|
231
231
|
let n_ic = u0_batch.len() / dim;
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
// Preallocate the contiguous output and write each trajectory DIRECTLY into
|
|
234
|
+
// its own row slice in parallel: `states.par_chunks_mut(dim)` zipped with
|
|
235
|
+
// `status.par_iter_mut()` gives worker `i` exclusive access to row `i`, so
|
|
236
|
+
// there is no per-trajectory heap `Vec` and no second copy. The partition is
|
|
237
|
+
// positional, so the result stays in input order regardless of thread count
|
|
238
|
+
// (maps carry no per-step randomness, so this is identical run to run).
|
|
239
|
+
let mut states = vec![0.0; n_ic * dim];
|
|
240
|
+
let mut status = vec![MapTrajStatus::Ok; n_ic];
|
|
241
|
+
states
|
|
242
|
+
.par_chunks_mut(dim)
|
|
243
|
+
.zip(status.par_iter_mut())
|
|
244
|
+
.enumerate()
|
|
245
|
+
.for_each(|(i, (row, st))| {
|
|
236
246
|
let u0 = &u0_batch[i * dim..(i + 1) * dim];
|
|
237
247
|
match iterate_final(ev, u0, p, n_steps) {
|
|
238
|
-
Ok(uf) =>
|
|
239
|
-
|
|
248
|
+
Ok(uf) => {
|
|
249
|
+
row.copy_from_slice(&uf);
|
|
250
|
+
*st = MapTrajStatus::Ok;
|
|
251
|
+
}
|
|
252
|
+
Err(e) => {
|
|
253
|
+
row.fill(f64::NAN);
|
|
254
|
+
*st = MapTrajStatus::Failed(e);
|
|
255
|
+
}
|
|
240
256
|
}
|
|
241
|
-
})
|
|
242
|
-
.collect();
|
|
243
|
-
|
|
244
|
-
let mut states = Vec::with_capacity(n_ic * dim);
|
|
245
|
-
let mut status = Vec::with_capacity(n_ic);
|
|
246
|
-
for (uf, s) in per_traj {
|
|
247
|
-
states.extend_from_slice(&uf);
|
|
248
|
-
status.push(s);
|
|
249
|
-
}
|
|
257
|
+
});
|
|
250
258
|
MapEnsembleFinal {
|
|
251
259
|
dim,
|
|
252
260
|
states,
|
|
@@ -521,4 +529,40 @@ mod tests {
|
|
|
521
529
|
assert_eq!(ens.n_ic(), 0);
|
|
522
530
|
assert!(ens.states.is_empty());
|
|
523
531
|
}
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
fn direct_row_write_preserves_index_order_with_interleaved_failures() {
|
|
535
|
+
// Regression for the direct-into-row-slice ensemble write (no per-traj
|
|
536
|
+
// Vec + second copy): an interleaved finite/diverging pattern must land
|
|
537
|
+
// each trajectory in its OWN row in input order with NaN confined to the
|
|
538
|
+
// diverged rows, even under a forced multi-thread pool. doubling() x←2x
|
|
539
|
+
// keeps x0=0 fixed and overflows any nonzero x0 over this horizon.
|
|
540
|
+
let ev = VmEval::new(doubling());
|
|
541
|
+
let n_ic = 64;
|
|
542
|
+
// Even index → 0 (stays at the fixed point), odd index → 1 (overflows).
|
|
543
|
+
let u0: Vec<f64> = (0..n_ic)
|
|
544
|
+
.map(|i| if i % 2 == 0 { 0.0 } else { 1.0 })
|
|
545
|
+
.collect();
|
|
546
|
+
|
|
547
|
+
let pool = rayon::ThreadPoolBuilder::new()
|
|
548
|
+
.num_threads(8)
|
|
549
|
+
.build()
|
|
550
|
+
.unwrap();
|
|
551
|
+
let ens = pool.install(|| iterate_ensemble_final(&ev, &u0, &[], 100_000));
|
|
552
|
+
|
|
553
|
+
assert_eq!(ens.n_ic(), n_ic);
|
|
554
|
+
assert_eq!(ens.n_failed(), n_ic / 2);
|
|
555
|
+
for i in 0..n_ic {
|
|
556
|
+
if i % 2 == 0 {
|
|
557
|
+
assert!(ens.status[i].is_ok(), "fixed-point row {i} should survive");
|
|
558
|
+
assert_eq!(ens.row(i), &[0.0], "fixed-point row {i} must stay 0");
|
|
559
|
+
} else {
|
|
560
|
+
assert!(
|
|
561
|
+
matches!(ens.status[i], MapTrajStatus::Failed(_)),
|
|
562
|
+
"row {i} should have diverged"
|
|
563
|
+
);
|
|
564
|
+
assert!(ens.row(i)[0].is_nan(), "diverged row {i} must be NaN");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
524
568
|
}
|
|
@@ -368,29 +368,36 @@ where
|
|
|
368
368
|
);
|
|
369
369
|
let n_ic = u0_batch.len() / dim;
|
|
370
370
|
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
// Preallocate the contiguous output and write each trajectory DIRECTLY into
|
|
372
|
+
// its own row slice in parallel: `states.par_chunks_mut(dim)` zipped with
|
|
373
|
+
// `status.par_iter_mut()` gives worker `i` exclusive access to row `i`, so
|
|
374
|
+
// there is no per-trajectory heap `Vec` and no second copy. The partition is
|
|
375
|
+
// positional and each worker's RNG is seeded only from its index `i`
|
|
376
|
+
// (`seed_for(base_seed, i)`), so the result depends only on the index, never
|
|
377
|
+
// on thread count — parallel == serial bit-for-bit (the determinism contract).
|
|
378
|
+
let mut states = vec![0.0; n_ic * dim];
|
|
379
|
+
let mut status = vec![SdeTrajStatus::Ok; n_ic];
|
|
380
|
+
states
|
|
381
|
+
.par_chunks_mut(dim)
|
|
382
|
+
.zip(status.par_iter_mut())
|
|
383
|
+
.enumerate()
|
|
384
|
+
.for_each(|(i, (row, st))| {
|
|
376
385
|
let u0 = &u0_batch[i * dim..(i + 1) * dim];
|
|
377
386
|
let mut kernel = make_kernel();
|
|
378
387
|
// Randomness keyed by trajectory index — the determinism contract.
|
|
379
388
|
let mut rng = SplitMix64::new(seed_for(base_seed, i as u64));
|
|
380
389
|
match sde_integrate_final(drift, diffusion, &mut *kernel, &mut rng, u0, p, t0, t1, cfg)
|
|
381
390
|
{
|
|
382
|
-
Ok(uf) =>
|
|
383
|
-
|
|
391
|
+
Ok(uf) => {
|
|
392
|
+
row.copy_from_slice(&uf);
|
|
393
|
+
*st = SdeTrajStatus::Ok;
|
|
394
|
+
}
|
|
395
|
+
Err(e) => {
|
|
396
|
+
row.fill(f64::NAN);
|
|
397
|
+
*st = SdeTrajStatus::Failed(e);
|
|
398
|
+
}
|
|
384
399
|
}
|
|
385
|
-
})
|
|
386
|
-
.collect();
|
|
387
|
-
|
|
388
|
-
let mut states = Vec::with_capacity(n_ic * dim);
|
|
389
|
-
let mut status = Vec::with_capacity(n_ic);
|
|
390
|
-
for (uf, s) in per_traj {
|
|
391
|
-
states.extend_from_slice(&uf);
|
|
392
|
-
status.push(s);
|
|
393
|
-
}
|
|
400
|
+
});
|
|
394
401
|
SdeEnsembleFinal {
|
|
395
402
|
dim,
|
|
396
403
|
states,
|
|
@@ -1105,4 +1112,109 @@ mod tests {
|
|
|
1105
1112
|
assert_eq!(ens.n_ic(), 0);
|
|
1106
1113
|
assert!(ens.states.is_empty());
|
|
1107
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
#[test]
|
|
1117
|
+
fn direct_row_write_is_index_keyed_and_parallel_equals_serial() {
|
|
1118
|
+
// Regression for the direct-into-row-slice ensemble write (no per-traj
|
|
1119
|
+
// Vec + second copy). Each worker writes its OWN row and seeds its RNG
|
|
1120
|
+
// only from its index, so the multi-thread result must match a serial
|
|
1121
|
+
// per-index loop bit-for-bit AND each row must equal the standalone
|
|
1122
|
+
// single-trajectory integration of that index's seed.
|
|
1123
|
+
let drift = gbm_drift(0.1);
|
|
1124
|
+
let diffusion = gbm_diffusion(0.5);
|
|
1125
|
+
let cfg = SdeConfig::new(0.01);
|
|
1126
|
+
let base = 0xD15EA5E;
|
|
1127
|
+
let n = 128;
|
|
1128
|
+
let u0 = vec![1.0; n];
|
|
1129
|
+
|
|
1130
|
+
let pool = rayon::ThreadPoolBuilder::new()
|
|
1131
|
+
.num_threads(8)
|
|
1132
|
+
.build()
|
|
1133
|
+
.unwrap();
|
|
1134
|
+
let parallel = pool.install(|| {
|
|
1135
|
+
sde_ensemble_final(
|
|
1136
|
+
&drift,
|
|
1137
|
+
&diffusion,
|
|
1138
|
+
|| Box::new(Milstein::new()),
|
|
1139
|
+
base,
|
|
1140
|
+
&u0,
|
|
1141
|
+
&[],
|
|
1142
|
+
0.0,
|
|
1143
|
+
1.0,
|
|
1144
|
+
&cfg,
|
|
1145
|
+
)
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
for i in 0..n {
|
|
1149
|
+
let mut kernel = Milstein::new();
|
|
1150
|
+
let mut rng = SplitMix64::new(seed_for(base, i as u64));
|
|
1151
|
+
let serial = sde_integrate_final(
|
|
1152
|
+
&drift,
|
|
1153
|
+
&diffusion,
|
|
1154
|
+
&mut kernel,
|
|
1155
|
+
&mut rng,
|
|
1156
|
+
&[1.0],
|
|
1157
|
+
&[],
|
|
1158
|
+
0.0,
|
|
1159
|
+
1.0,
|
|
1160
|
+
&cfg,
|
|
1161
|
+
)
|
|
1162
|
+
.unwrap();
|
|
1163
|
+
assert!(parallel.status[i].is_ok());
|
|
1164
|
+
assert_eq!(
|
|
1165
|
+
parallel.row(i)[0].to_bits(),
|
|
1166
|
+
serial[0].to_bits(),
|
|
1167
|
+
"row {i}: direct-write ensemble != serial single-trajectory",
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
#[test]
|
|
1173
|
+
fn direct_row_write_isolates_interleaved_failures_in_order() {
|
|
1174
|
+
// f(x) = x², g = 0 ⇒ deterministic finite-time blow-up for x0 = 1 and a
|
|
1175
|
+
// finite decay for x0 = -1. An interleaved pattern must confine NaN to the
|
|
1176
|
+
// diverged rows in input order under a forced multi-thread pool.
|
|
1177
|
+
let mut b = TapeBuilder::new();
|
|
1178
|
+
let x = b.state(0);
|
|
1179
|
+
let f = b.mul(x, x);
|
|
1180
|
+
let drift = VmEval::new(Interpreter::new(b.finish(&[f], &[], 1, 0).unwrap()));
|
|
1181
|
+
let diffusion = const_diffusion(0.0);
|
|
1182
|
+
let cfg = SdeConfig::new(0.01);
|
|
1183
|
+
let n = 32;
|
|
1184
|
+
let u0: Vec<f64> = (0..n)
|
|
1185
|
+
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
|
|
1186
|
+
.collect();
|
|
1187
|
+
|
|
1188
|
+
let pool = rayon::ThreadPoolBuilder::new()
|
|
1189
|
+
.num_threads(8)
|
|
1190
|
+
.build()
|
|
1191
|
+
.unwrap();
|
|
1192
|
+
let ens = pool.install(|| {
|
|
1193
|
+
sde_ensemble_final(
|
|
1194
|
+
&drift,
|
|
1195
|
+
&diffusion,
|
|
1196
|
+
|| Box::new(EulerMaruyama::new()),
|
|
1197
|
+
0,
|
|
1198
|
+
&u0,
|
|
1199
|
+
&[],
|
|
1200
|
+
0.0,
|
|
1201
|
+
2.0,
|
|
1202
|
+
&cfg,
|
|
1203
|
+
)
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
assert_eq!(ens.n_failed(), n / 2);
|
|
1207
|
+
for i in 0..n {
|
|
1208
|
+
if i % 2 == 0 {
|
|
1209
|
+
assert!(
|
|
1210
|
+
matches!(ens.status[i], SdeTrajStatus::Failed(_)),
|
|
1211
|
+
"row {i} should have diverged"
|
|
1212
|
+
);
|
|
1213
|
+
assert!(ens.row(i)[0].is_nan(), "diverged row {i} must be NaN");
|
|
1214
|
+
} else {
|
|
1215
|
+
assert!(ens.status[i].is_ok(), "row {i} should stay finite");
|
|
1216
|
+
assert!(ens.row(i)[0].is_finite());
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1108
1220
|
}
|