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.
Files changed (210) hide show
  1. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/PKG-INFO +2 -2
  2. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/README.md +1 -1
  3. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/src/bridge.rs +16 -4
  4. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/sde.rs +101 -11
  5. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/adams.rs +453 -0
  6. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/bs3.rs +189 -0
  7. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/cashkarp.rs +205 -0
  8. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/control.rs +1 -0
  9. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/euler.rs +99 -0
  10. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/heun.rs +101 -0
  11. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/heun_euler.rs +175 -0
  12. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/midpoint.rs +99 -0
  13. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/mod.rs +22 -0
  14. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/ralston.rs +100 -0
  15. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/rk4_38.rs +106 -0
  16. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/rkf45.rs +203 -0
  17. tsdynamics-3.2.0/crates/tsdyn-solvers/src/explicit/ssprk3.rs +104 -0
  18. tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/backward_euler.rs +161 -0
  19. tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/implicit_midpoint.rs +179 -0
  20. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/mod.rs +9 -0
  21. tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/newton.rs +175 -0
  22. tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/sdirk2.rs +228 -0
  23. tsdynamics-3.2.0/crates/tsdyn-solvers/src/implicit/trapezoid.rs +160 -0
  24. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/trbdf2.rs +5 -83
  25. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/explicit_accuracy.rs +40 -2
  26. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/implicit_stiff.rs +36 -0
  27. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/pyproject.toml +9 -6
  28. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/__init__.py +11 -3
  29. tsdynamics-3.2.0/src/tsdynamics/analysis/_tangent.py +155 -0
  30. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/_common.py +5 -3
  31. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/attractors.py +15 -3
  32. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/basins.py +12 -3
  33. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/continuation.py +6 -4
  34. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/metrics.py +3 -2
  35. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/_common.py +19 -130
  36. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/expansion.py +4 -3
  37. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/gali.py +63 -29
  38. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/zero_one.py +5 -2
  39. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/_common.py +14 -5
  40. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/correlation.py +24 -4
  41. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/generalized.py +2 -0
  42. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/_common.py +14 -0
  43. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/delay.py +6 -5
  44. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/dimension.py +1 -10
  45. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/embed.py +5 -10
  46. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/core.py +0 -4
  47. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/lz.py +3 -2
  48. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/_common.py +32 -126
  49. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/fixed.py +3 -3
  50. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/periodic.py +6 -4
  51. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/lyapunov/__init__.py +1 -3
  52. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/return_map.py +4 -1
  53. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/generators.py +7 -7
  54. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/hypothesis.py +1 -1
  55. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/trajectory.py +9 -4
  56. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/__init__.py +6 -1
  57. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_variational.py +4 -4
  58. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/compile.py +13 -13
  59. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/problem.py +9 -4
  60. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/run.py +83 -20
  61. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/errors.py +7 -4
  62. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/continuous.py +5 -1
  63. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/delay.py +39 -0
  64. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/discrete.py +12 -8
  65. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/stochastic.py +94 -22
  66. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/explicit.py +18 -2
  67. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/implicit.py +4 -0
  68. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/select.py +38 -2
  69. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/chaotic_attractors.py +31 -14
  70. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/chem_bio_systems.py +5 -3
  71. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/coupled_systems.py +19 -0
  72. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/exotic_systems.py +4 -6
  73. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -3
  74. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/chaotic_maps.py +25 -20
  75. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/exotic_maps.py +27 -15
  76. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/geometric_maps.py +11 -12
  77. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/polynomial_maps.py +8 -7
  78. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/population_maps.py +7 -7
  79. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/__init__.py +0 -2
  80. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/sagitta_dt.py +32 -18
  81. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/viz/spec.py +296 -8
  82. tsdynamics-3.1.3/src/tsdynamics/utils/general.py +0 -26
  83. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/LICENSE +0 -0
  84. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/Cargo.toml +0 -0
  85. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/Cargo.lock +0 -0
  86. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/Cargo.toml +0 -0
  87. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-core/src/lib.rs +0 -0
  88. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/Cargo.toml +0 -0
  89. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/dde.rs +0 -0
  90. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/ensemble.rs +0 -0
  91. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/event.rs +0 -0
  92. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/integrate.rs +0 -0
  93. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/lib.rs +0 -0
  94. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/map.rs +0 -0
  95. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/problem.rs +0 -0
  96. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/rng.rs +0 -0
  97. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-engine/src/testkit.rs +0 -0
  98. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/Cargo.toml +0 -0
  99. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/builder.rs +0 -0
  100. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/eval.rs +0 -0
  101. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/lib.rs +0 -0
  102. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/op.rs +0 -0
  103. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/reference.rs +0 -0
  104. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/src/tape.rs +0 -0
  105. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/coverage_tape.rs +0 -0
  106. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/CircadianRhythm.txt +0 -0
  107. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/Lorenz.txt +0 -0
  108. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/SanUmSrisuchinwong.txt +0 -0
  109. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/SprottMore.txt +0 -0
  110. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/Torus.txt +0 -0
  111. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/fixtures/YuWang2.txt +0 -0
  112. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/gen_fixtures.py +0 -0
  113. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/golden_fixtures.rs +0 -0
  114. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-ir/tests/roundtrip.rs +0 -0
  115. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/Cargo.toml +0 -0
  116. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/codegen.rs +0 -0
  117. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/error.rs +0 -0
  118. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/evaluator.rs +0 -0
  119. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/lib.rs +0 -0
  120. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/src/shims.rs +0 -0
  121. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/compile_latency.rs +0 -0
  122. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/golden_fixtures.rs +0 -0
  123. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-jit/tests/interpreter_equivalence.rs +0 -0
  124. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/Cargo.toml +0 -0
  125. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/caps.rs +0 -0
  126. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/dop853.rs +0 -0
  127. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/rk4.rs +0 -0
  128. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/rk45.rs +0 -0
  129. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/testkit.rs +0 -0
  130. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/explicit/tsit5.rs +0 -0
  131. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/bdf.rs +0 -0
  132. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/control.rs +0 -0
  133. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/linalg.rs +0 -0
  134. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/implicit/rosenbrock.rs +0 -0
  135. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/lib.rs +0 -0
  136. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/registry.rs +0 -0
  137. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/euler_maruyama.rs +0 -0
  138. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/milstein.rs +0 -0
  139. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/mod.rs +0 -0
  140. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/sde/testkit.rs +0 -0
  141. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/src/solver.rs +0 -0
  142. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-solvers/tests/registry.rs +0 -0
  143. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/Cargo.toml +0 -0
  144. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/src/interp.rs +0 -0
  145. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/src/lib.rs +0 -0
  146. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/tests/golden_fixtures.rs +0 -0
  147. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/crates/tsdyn-vm/tests/oracle_equivalence.rs +0 -0
  148. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/__init__.py +0 -0
  149. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/_result.py +0 -0
  150. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/basins/__init__.py +0 -0
  151. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/chaos/__init__.py +0 -0
  152. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/__init__.py +0 -0
  153. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/_scaling.py +0 -0
  154. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/dimensions/fixedmass.py +0 -0
  155. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/embedding/__init__.py +0 -0
  156. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/__init__.py +0 -0
  157. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/dispersion.py +0 -0
  158. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/multiscale.py +0 -0
  159. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/permutation.py +0 -0
  160. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/entropy/sample.py +0 -0
  161. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/fixedpoints/__init__.py +0 -0
  162. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/lyapunov/from_data.py +0 -0
  163. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/__init__.py +0 -0
  164. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/orbit_diagram.py +0 -0
  165. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/orbits/poincare.py +0 -0
  166. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/__init__.py +0 -0
  167. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/_common.py +0 -0
  168. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/matrix.py +0 -0
  169. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/rqa.py +0 -0
  170. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/recurrence/windowed.py +0 -0
  171. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/__init__.py +0 -0
  172. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/_common.py +0 -0
  173. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/analysis/surrogate/statistics.py +0 -0
  174. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/__init__.py +0 -0
  175. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/data/sampling.py +0 -0
  176. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_base.py +0 -0
  177. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/_crossings.py +0 -0
  178. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/ensemble.py +0 -0
  179. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/poincare.py +0 -0
  180. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/projected.py +0 -0
  181. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/stroboscopic.py +0 -0
  182. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/tangent.py +0 -0
  183. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/derived/wrapped.py +0 -0
  184. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/__init__.py +0 -0
  185. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/engine/symbols.py +0 -0
  186. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/__init__.py +0 -0
  187. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/_accessors.py +0 -0
  188. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/_dde_lyapunov.py +0 -0
  189. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/base.py +0 -0
  190. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/protocol.py +0 -0
  191. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/families/wrapped.py +0 -0
  192. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/plugins.py +0 -0
  193. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/py.typed +0 -0
  194. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/registry.py +0 -0
  195. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/__init__.py +0 -0
  196. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/solvers/stochastic.py +0 -0
  197. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/__init__.py +0 -0
  198. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/__init__.py +0 -0
  199. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
  200. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
  201. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
  202. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
  203. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/systems/discrete/__init__.py +2 -2
  204. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/__init__.py +0 -0
  205. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/_common.py +0 -0
  206. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/features.py +0 -0
  207. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/preprocessing.py +0 -0
  208. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/transforms/spectral.py +0 -0
  209. {tsdynamics-3.1.3 → tsdynamics-3.2.0}/src/tsdynamics/utils/grids.py +0 -0
  210. {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.1.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), steps=500)
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), steps=500)
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::{Bdf, RosenbrockW, TrBdf2};
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
- // The fixed-step `rk4` (no tolerances) and any out-of-tree plugin kernel
231
- // build through the registry factory. `name` came from the registry
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 not on the `dt` lattice is
31
- //! produced by a shorter final sub-step whose increment is drawn `~ N(0, h)` for
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. As with the ODE driver, a non-finite
35
- //! state is reported, never returned as plausible data ([`SdeError`]).
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
- /// A landing step (when `dt` would reach/pass `t_end`) is shortened to the
115
- /// remaining span and its increment drawn `~ N(0, h)` for that `h`; the time is
116
- /// then pinned exactly to `t_end` so grid points never drift. `dw` is a caller-
117
- /// owned scratch of length `dim`, reused across steps.
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
- let landing = cfg.dt >= remaining;
143
- let h = if landing { remaining } else { cfg.dt };
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
+ }