tsdynamics 6.0.2__tar.gz → 6.0.4__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 (288) hide show
  1. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/PKG-INFO +1 -1
  2. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/lyapunov.rs +19 -3
  3. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/map_lyapunov.rs +2 -2
  4. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/marshal.rs +6 -0
  5. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/mod.rs +17 -0
  6. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/lib.rs +3 -1
  7. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/lib.rs +2 -0
  8. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/lyapunov.rs +116 -1
  9. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/map_lyapunov.rs +4 -7
  10. tsdynamics-6.0.4/crates/tsdyn-engine/src/tangent_frame.rs +82 -0
  11. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/pyproject.toml +1 -1
  12. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/__init__.py +1 -1
  13. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/run.py +5 -0
  14. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/run_methods.py +4 -2
  15. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_solvers/__init__.py +7 -9
  16. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_solvers/select.py +2 -1
  17. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/escape.py +14 -6
  18. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/__init__.py +18 -3
  19. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_common.py +33 -1
  20. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_discovery.py +133 -2
  21. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_public.py +1 -1
  22. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_tangent.py +35 -1
  23. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/__init__.py +4 -0
  24. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/_common.py +8 -0
  25. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/attractors.py +17 -15
  26. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/basins.py +2 -0
  27. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/metrics.py +19 -19
  28. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/chaos/__init__.py +18 -8
  29. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/chaos/_common.py +4 -4
  30. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/chaos/expansion.py +19 -20
  31. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/chaos/gali.py +50 -87
  32. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/_scaling.py +4 -4
  33. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/fixedmass.py +13 -11
  34. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/generalized.py +18 -5
  35. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/embedding/dimension.py +44 -13
  36. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/embedding/embed.py +16 -12
  37. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/fixedpoints/__init__.py +9 -0
  38. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/fixedpoints/_interval.py +4 -4
  39. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/fixedpoints/fixed.py +64 -16
  40. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/fixedpoints/periodic.py +22 -8
  41. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/lyapunov/__init__.py +115 -34
  42. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/lyapunov/from_data.py +2 -32
  43. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/orbits/__init__.py +6 -3
  44. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/orbits/poincare.py +6 -2
  45. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/orbits/return_map.py +29 -15
  46. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/planar.py +97 -47
  47. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/recurrence/rqa.py +4 -2
  48. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/recurrence/windowed.py +15 -10
  49. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/data/sampling.py +57 -27
  50. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/_crossings.py +1 -1
  51. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/tangent.py +75 -15
  52. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/continuous.py +6 -5
  53. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/delay.py +1 -2
  54. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/discrete.py +1 -2
  55. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/stochastic.py +24 -8
  56. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/chem_bio_systems.py +4 -5
  57. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/exotic_systems.py +13 -6
  58. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/oscillatory_systems.py +27 -14
  59. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/exotic_maps.py +22 -12
  60. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/export.py +105 -13
  61. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/__init__.py +34 -2
  62. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/caps.py +20 -2
  63. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/mpl/_anim.py +206 -24
  64. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/mpl/_core.py +34 -18
  65. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/mpl/_threed.py +49 -21
  66. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/plotly/__init__.py +9 -0
  67. tsdynamics-6.0.4/src/tsdynamics/viz/render/plotly/_anim.py +666 -0
  68. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/plotly/_composite.py +3 -18
  69. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/plotly/_core.py +49 -18
  70. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/plotly/_threed.py +20 -37
  71. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/threejs/__init__.py +23 -1
  72. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/threejs/_assets/tsdyn-threejs-loader.js +155 -319
  73. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/threejs/_lower.py +94 -32
  74. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/spec.py +79 -46
  75. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/style.py +83 -14
  76. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/_base.py +35 -8
  77. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/_data.py +40 -9
  78. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/_frontdoor.py +15 -2
  79. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/_registry.py +4 -3
  80. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/fields.py +13 -14
  81. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/hilbert.py +33 -9
  82. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/planar.py +39 -22
  83. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/results.py +35 -9
  84. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/series.py +22 -10
  85. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/spectra.py +136 -41
  86. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/stability.py +30 -12
  87. tsdynamics-6.0.2/src/tsdynamics/viz/render/plotly/_anim.py +0 -751
  88. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/LICENSE +0 -0
  89. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/README.md +0 -0
  90. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/Cargo.toml +0 -0
  91. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/Cargo.lock +0 -0
  92. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/Cargo.toml +0 -0
  93. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/basin.rs +0 -0
  94. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/dde.rs +0 -0
  95. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/events.rs +0 -0
  96. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/map.rs +0 -0
  97. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/ode.rs +0 -0
  98. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/sde.rs +0 -0
  99. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-core/src/bridge/stepper.rs +0 -0
  100. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/Cargo.toml +0 -0
  101. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/benches/engine.rs +0 -0
  102. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/alloc.rs +0 -0
  103. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/basin.rs +0 -0
  104. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/dde.rs +0 -0
  105. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/ensemble.rs +0 -0
  106. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/event.rs +0 -0
  107. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/integrate.rs +0 -0
  108. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/interrupt.rs +0 -0
  109. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/map.rs +0 -0
  110. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/param_sweep.rs +0 -0
  111. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/pool.rs +0 -0
  112. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/problem.rs +0 -0
  113. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/rng.rs +0 -0
  114. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/sde.rs +0 -0
  115. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-engine/src/testkit.rs +0 -0
  116. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/Cargo.toml +0 -0
  117. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/builder.rs +0 -0
  118. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/eval.rs +0 -0
  119. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/lib.rs +0 -0
  120. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/op.rs +0 -0
  121. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/reference.rs +0 -0
  122. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/src/tape.rs +0 -0
  123. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/coverage_tape.rs +0 -0
  124. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/CircadianRhythm.txt +0 -0
  125. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/Lorenz.txt +0 -0
  126. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/SanUmSrisuchinwong.txt +0 -0
  127. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/SprottMore.txt +0 -0
  128. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/Torus.txt +0 -0
  129. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/fixtures/YuWang2.txt +0 -0
  130. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/gen_fixtures.py +0 -0
  131. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/golden_fixtures.rs +0 -0
  132. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-ir/tests/roundtrip.rs +0 -0
  133. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/Cargo.toml +0 -0
  134. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/benches/compile.rs +0 -0
  135. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/cache.rs +0 -0
  136. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/codegen.rs +0 -0
  137. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/error.rs +0 -0
  138. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/evaluator.rs +0 -0
  139. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/lib.rs +0 -0
  140. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/src/shims.rs +0 -0
  141. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/tests/compile_latency.rs +0 -0
  142. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/tests/golden_fixtures.rs +0 -0
  143. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-jit/tests/interpreter_equivalence.rs +0 -0
  144. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/Cargo.toml +0 -0
  145. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/benches/kernels.rs +0 -0
  146. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/caps.rs +0 -0
  147. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/adams.rs +0 -0
  148. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/bs3.rs +0 -0
  149. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/cashkarp.rs +0 -0
  150. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/control.rs +0 -0
  151. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/dop853.rs +0 -0
  152. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/euler.rs +0 -0
  153. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/heun.rs +0 -0
  154. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/heun_euler.rs +0 -0
  155. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/midpoint.rs +0 -0
  156. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/mod.rs +0 -0
  157. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/ralston.rs +0 -0
  158. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/rk4.rs +0 -0
  159. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/rk45.rs +0 -0
  160. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/rk4_38.rs +0 -0
  161. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/rkf45.rs +0 -0
  162. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/ssprk3.rs +0 -0
  163. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/testkit.rs +0 -0
  164. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/explicit/tsit5.rs +0 -0
  165. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/backward_euler.rs +0 -0
  166. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/bdf.rs +0 -0
  167. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/control.rs +0 -0
  168. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/implicit_midpoint.rs +0 -0
  169. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/linalg.rs +0 -0
  170. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/mod.rs +0 -0
  171. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/newton.rs +0 -0
  172. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/rosenbrock.rs +0 -0
  173. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/sdirk2.rs +0 -0
  174. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/trapezoid.rs +0 -0
  175. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/implicit/trbdf2.rs +0 -0
  176. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/lib.rs +0 -0
  177. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/registry.rs +0 -0
  178. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/sde/euler_maruyama.rs +0 -0
  179. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/sde/milstein.rs +0 -0
  180. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/sde/mod.rs +0 -0
  181. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/sde/testkit.rs +0 -0
  182. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/src/solver.rs +0 -0
  183. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/tests/explicit_accuracy.rs +0 -0
  184. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/tests/implicit_stiff.rs +0 -0
  185. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-solvers/tests/registry.rs +0 -0
  186. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/Cargo.toml +0 -0
  187. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/benches/interp.rs +0 -0
  188. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/src/interp.rs +0 -0
  189. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/src/lib.rs +0 -0
  190. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/tests/golden_fixtures.rs +0 -0
  191. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/crates/tsdyn-vm/tests/oracle_equivalence.rs +0 -0
  192. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/__init__.py +0 -0
  193. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/_families.py +0 -0
  194. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/compile.py +0 -0
  195. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/events.py +0 -0
  196. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/problem.py +0 -0
  197. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/reference.py +0 -0
  198. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/sde_run.py +0 -0
  199. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/stepper.py +0 -0
  200. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_engine/symbols.py +0 -0
  201. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_redirects.py +0 -0
  202. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_solvers/explicit.py +0 -0
  203. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_solvers/implicit.py +0 -0
  204. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_solvers/stochastic.py +0 -0
  205. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/__init__.py +0 -0
  206. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/grids.py +0 -0
  207. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/lookup.py +0 -0
  208. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/plot_namespace.py +0 -0
  209. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/_utils/tolerances.py +0 -0
  210. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_plotbuilder.py +0 -0
  211. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result.py +0 -0
  212. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_array.py +0 -0
  213. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_base.py +0 -0
  214. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_collection.py +0 -0
  215. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_json.py +0 -0
  216. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_scalar.py +0 -0
  217. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_scaling.py +0 -0
  218. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/_result_viz.py +0 -0
  219. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/basins/continuation.py +0 -0
  220. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/chaos/zero_one.py +0 -0
  221. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/__init__.py +0 -0
  222. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/_common.py +0 -0
  223. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/dimensions/correlation.py +0 -0
  224. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/embedding/__init__.py +0 -0
  225. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/embedding/_common.py +0 -0
  226. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/embedding/delay.py +0 -0
  227. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/fixedpoints/_common.py +0 -0
  228. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/orbits/orbit_diagram.py +0 -0
  229. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/recurrence/__init__.py +0 -0
  230. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/recurrence/_common.py +0 -0
  231. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/recurrence/matrix.py +0 -0
  232. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/results.py +0 -0
  233. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/sampling/__init__.py +0 -0
  234. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/analysis/sampling/sagitta.py +0 -0
  235. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/data/__init__.py +0 -0
  236. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/data/trajectory.py +0 -0
  237. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/__init__.py +0 -0
  238. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/_base.py +0 -0
  239. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/_variational.py +0 -0
  240. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/ensemble.py +0 -0
  241. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/poincare.py +0 -0
  242. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/projected.py +0 -0
  243. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/stroboscopic.py +0 -0
  244. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/derived/wrapped.py +0 -0
  245. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/errors.py +0 -0
  246. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/__init__.py +0 -0
  247. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_dde_lyapunov.py +0 -0
  248. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_derive.py +0 -0
  249. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_hidden.py +0 -0
  250. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_info.py +0 -0
  251. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_kwargs.py +0 -0
  252. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_params.py +0 -0
  253. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/_plottable.py +0 -0
  254. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/base.py +0 -0
  255. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/protocol.py +0 -0
  256. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/families/wrapped.py +0 -0
  257. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/plugins.py +0 -0
  258. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/py.typed +0 -0
  259. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/registry.py +0 -0
  260. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/__init__.py +0 -0
  261. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/_declared_params.py +0 -0
  262. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/__init__.py +0 -0
  263. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
  264. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
  265. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
  266. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
  267. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
  268. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
  269. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/spatial_fields.py +0 -0
  270. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/continuous/stochastic_systems.py +0 -0
  271. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/__init__.py +0 -0
  272. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
  273. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
  274. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
  275. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
  276. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/__init__.py +0 -0
  277. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/_frames.py +0 -0
  278. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/_resample.py +0 -0
  279. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/_tweaks.py +0 -0
  280. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/_visibility.py +0 -0
  281. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/compose.py +0 -0
  282. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/producers.py +0 -0
  283. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/json.py +0 -0
  284. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/mpl/__init__.py +0 -0
  285. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/plotly/_html.py +0 -0
  286. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/render/threejs/_page.py +0 -0
  287. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/__init__.py +0 -0
  288. {tsdynamics-6.0.2 → tsdynamics-6.0.4}/src/tsdynamics/viz/transforms/_primitives.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsdynamics
3
- Version: 6.0.2
3
+ Version: 6.0.4
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -12,7 +12,7 @@
12
12
  //! [`super::marshal`]; the renormalisation loop and the QR live in
13
13
  //! [`tsdyn_engine::lyapunov`].
14
14
 
15
- use tsdyn_engine::lyapunov::{lyapunov_spectrum_ode, LyapunovError, LyapunovOutcome};
15
+ use tsdyn_engine::lyapunov::{lyapunov_spectrum_ode_guarded, LyapunovError, LyapunovOutcome};
16
16
  use tsdyn_ir::Tape;
17
17
  use tsdyn_solvers::Solver;
18
18
 
@@ -59,6 +59,7 @@ pub fn lyapunov_spectrum_ode_bridge(
59
59
  burn_in: f64,
60
60
  final_time: f64,
61
61
  jit: bool,
62
+ escape_limit: f64,
62
63
  ) -> Result<LyapunovOutcome, EngineError> {
63
64
  // The extended tape is `n_state == ev.dim()` (a plain ODE-shaped tape over the
64
65
  // stacked state), validated by the kernel against `dim*(k+1)`. Resolve the
@@ -71,8 +72,20 @@ pub fn lyapunov_spectrum_ode_bridge(
71
72
  // march); the name is a registry name and the tolerances are validated, so
72
73
  // `build_solver` always succeeds.
73
74
  let factory = move || -> Box<dyn Solver> { build_solver(name, tol) };
74
- lyapunov_spectrum_ode(&*ev, factory, p, dim, k, z0, t0, dt, burn_in, final_time)
75
- .map_err(to_engine_err)
75
+ lyapunov_spectrum_ode_guarded(
76
+ &*ev,
77
+ factory,
78
+ p,
79
+ dim,
80
+ k,
81
+ z0,
82
+ t0,
83
+ dt,
84
+ burn_in,
85
+ final_time,
86
+ escape_limit,
87
+ )
88
+ .map_err(to_engine_err)
76
89
  }
77
90
 
78
91
  #[cfg(test)]
@@ -124,6 +137,7 @@ mod tests {
124
137
  5.0,
125
138
  50.0,
126
139
  jit,
140
+ f64::INFINITY,
127
141
  )
128
142
  .unwrap()
129
143
  };
@@ -160,6 +174,7 @@ mod tests {
160
174
  1.0,
161
175
  1.0,
162
176
  false,
177
+ f64::INFINITY,
163
178
  )
164
179
  .unwrap_err();
165
180
  assert!(matches!(err, EngineError::UnknownMethod(_)), "got {err:?}");
@@ -186,6 +201,7 @@ mod tests {
186
201
  1.0,
187
202
  1.0,
188
203
  false,
204
+ f64::INFINITY,
189
205
  )
190
206
  .unwrap_err();
191
207
  assert!(
@@ -37,10 +37,10 @@ fn to_engine_err(e: MapLyapunovError) -> EngineError {
37
37
  /// Run the discrete-map QR tangent-map Lyapunov spectrum over a **map** tape.
38
38
  ///
39
39
  /// Builds the evaluator over `tape` (which must carry its step Jacobian), seeds the
40
- /// deviation frame to the leading `k` identity columns, iterates `steps` map steps
40
+ /// deviation frame to a deterministic mixed orthonormal frame, iterates `steps` map steps
41
41
  /// propagating the frame through `J(x_n)` (pre-image convention) with a QR
42
42
  /// reorthonormalisation every `reortho_interval` steps, and returns the `k`
43
- /// time-averaged exponents (largest first) plus the completed-interval count. The
43
+ /// time-averaged exponents in QR order plus the completed-interval count. The
44
44
  /// map tape folds its parameters in, so `p` is empty. `jit` selects the Cranelift
45
45
  /// evaluator (numerically identical to the interpreter).
46
46
  #[allow(clippy::too_many_arguments)]
@@ -544,6 +544,12 @@ pub(super) fn validate_grid(t_eval: &[f64]) -> Result<(), EngineError> {
544
544
  w[0]
545
545
  )));
546
546
  }
547
+ if !(w[1] - w[0]).is_finite() {
548
+ return Err(EngineError::InvalidParameter(format!(
549
+ "t_eval gap {i} is not finite: {} - {} exceeds float64 range",
550
+ w[1], w[0]
551
+ )));
552
+ }
547
553
  }
548
554
  Ok(())
549
555
  }
@@ -730,6 +730,23 @@ mod tests {
730
730
  );
731
731
  }
732
732
 
733
+ #[test]
734
+ fn integrate_dense_rejects_overflowing_finite_grid_gap() {
735
+ let err = integrate_dense(
736
+ decay_tape(),
737
+ &[1.0],
738
+ &[2.0],
739
+ &[-f64::MAX, f64::MAX],
740
+ "rk45",
741
+ 1e-6,
742
+ 1e-9,
743
+ false,
744
+ )
745
+ .unwrap_err();
746
+ assert!(matches!(err, EngineError::InvalidParameter(_)), "{err:?}");
747
+ assert!(err.to_string().contains("gap"));
748
+ }
749
+
733
750
  #[test]
734
751
  fn ensemble_rejects_non_finite_times() {
735
752
  let err = ensemble_final(
@@ -1245,7 +1245,7 @@ use bridge::lyapunov_spectrum_ode_bridge;
1245
1245
  /// window. Returns `(spectrum, final_state, last_growths)` — the `k` exponents,
1246
1246
  /// the final extended state, and the last per-step log-stretch contributions. A
1247
1247
  /// divergence raises (it must not become a plausible-looking spectrum).
1248
- #[pyfunction]
1248
+ #[pyfunction(signature = (ops, a, b, imm, outputs, jac_outputs, n_state, n_param, p, method, rtol, atol, dim, k, z0, t0, dt, burn_in, final_time, jit, escape_limit=None))]
1249
1249
  #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1250
1250
  fn lyapunov_spectrum_ode<'py>(
1251
1251
  py: Python<'py>,
@@ -1269,6 +1269,7 @@ fn lyapunov_spectrum_ode<'py>(
1269
1269
  burn_in: f64,
1270
1270
  final_time: f64,
1271
1271
  jit: bool,
1272
+ escape_limit: Option<f64>,
1272
1273
  ) -> PyResult<(
1273
1274
  Bound<'py, PyArray1<f64>>,
1274
1275
  Bound<'py, PyArray1<f64>>,
@@ -1292,6 +1293,7 @@ fn lyapunov_spectrum_ode<'py>(
1292
1293
  burn_in,
1293
1294
  final_time,
1294
1295
  jit,
1296
+ escape_limit.unwrap_or(f64::INFINITY),
1295
1297
  )
1296
1298
  })
1297
1299
  .map_err(to_py_err)?;
@@ -62,6 +62,8 @@ pub mod alloc;
62
62
  pub mod interrupt;
63
63
  pub mod pool;
64
64
 
65
+ mod tangent_frame;
66
+
65
67
  #[cfg(test)]
66
68
  mod testkit;
67
69
 
@@ -188,7 +188,7 @@ pub struct LyapunovOutcome {
188
188
  /// - `dim` — the **base** system dimension.
189
189
  /// - `k` — the number of tangent vectors (`1 ≤ k ≤ dim`).
190
190
  /// - `z0` — the initial extended state (length `dim*(k+1)`): base IC ⊕ the seed
191
- /// tangent frame (`I[:, :k]` column-major), exactly the Python `embed_extended`.
191
+ /// tangent frame (column-major), exactly the Python `embed_extended`.
192
192
  /// - `t0` — the start time.
193
193
  /// - `dt` — the renormalisation interval (the Python `dt`).
194
194
  /// - `burn_in` — discard this much time before accumulating (`max(0, burn_in)`).
@@ -213,6 +213,44 @@ pub fn lyapunov_spectrum_ode<F>(
213
213
  burn_in: f64,
214
214
  final_time: f64,
215
215
  ) -> Result<LyapunovOutcome, LyapunovError>
216
+ where
217
+ F: Fn() -> Box<dyn Solver>,
218
+ {
219
+ lyapunov_spectrum_ode_guarded(
220
+ ev,
221
+ solver_factory,
222
+ p,
223
+ dim,
224
+ k,
225
+ z0,
226
+ t0,
227
+ dt,
228
+ burn_in,
229
+ final_time,
230
+ f64::INFINITY,
231
+ )
232
+ }
233
+
234
+ /// The Benettin loop with a caller-supplied base-orbit escape limit.
235
+ ///
236
+ /// Only the first `dim` state components are checked: a growing tangent vector
237
+ /// is not an escaping base orbit. Python derives this limit from its shared
238
+ /// scale-and-growth policy; the unguarded Rust entry point remains available
239
+ /// for callers measuring non-attractor finite-time growth.
240
+ #[allow(clippy::too_many_arguments)]
241
+ pub fn lyapunov_spectrum_ode_guarded<F>(
242
+ ev: &dyn Evaluator,
243
+ solver_factory: F,
244
+ p: &[f64],
245
+ dim: usize,
246
+ k: usize,
247
+ z0: &[f64],
248
+ t0: f64,
249
+ dt: f64,
250
+ burn_in: f64,
251
+ final_time: f64,
252
+ escape_limit: f64,
253
+ ) -> Result<LyapunovOutcome, LyapunovError>
216
254
  where
217
255
  F: Fn() -> Box<dyn Solver>,
218
256
  {
@@ -257,6 +295,11 @@ where
257
295
  "final_time must be finite and positive, got {final_time}"
258
296
  )));
259
297
  }
298
+ if escape_limit.is_nan() || escape_limit <= 0.0 {
299
+ return Err(LyapunovError::BadShape(
300
+ "escape_limit must be positive (or infinite)".to_string(),
301
+ ));
302
+ }
260
303
 
261
304
  let mut z = z0.to_vec();
262
305
  let mut t = t0;
@@ -277,6 +320,7 @@ where
277
320
  while t < t_burn - EPS {
278
321
  let h = dt.min(t_burn - t);
279
322
  advance_chunk(ev, &solver_factory, &mut z, t, h, p, &mut poll).map_err(classify)?;
323
+ check_base_orbit(&z[..dim], t + h, escape_limit)?;
280
324
  if !renorm_step(&mut z, dim, k, &mut last_growths)? {
281
325
  return Err(LyapunovError::Diverged(
282
326
  "extended variational state went non-finite during burn-in".to_string(),
@@ -291,6 +335,7 @@ where
291
335
  while t < t_end - EPS {
292
336
  let h = dt.min(t_end - t);
293
337
  advance_chunk(ev, &solver_factory, &mut z, t, h, p, &mut poll).map_err(classify)?;
338
+ check_base_orbit(&z[..dim], t + h, escape_limit)?;
294
339
  if !renorm_step(&mut z, dim, k, &mut last_growths)? {
295
340
  return Err(LyapunovError::Diverged(
296
341
  "extended variational state went non-finite during the averaging window"
@@ -317,6 +362,20 @@ where
317
362
  })
318
363
  }
319
364
 
365
+ fn check_base_orbit(state: &[f64], t: f64, limit: f64) -> Result<(), LyapunovError> {
366
+ let peak = state
367
+ .iter()
368
+ .fold(0.0_f64, |peak, value| peak.max(value.abs()));
369
+ if peak >= limit {
370
+ return Err(LyapunovError::Diverged(format!(
371
+ "Lyapunov base orbit escaped at t={t:.6}: state magnitude {peak:.6e} \
372
+ reached the scale-and-growth limit {limit:.6e}. Use an initial condition \
373
+ on a bounded orbit before estimating attractor exponents."
374
+ )));
375
+ }
376
+ Ok(())
377
+ }
378
+
320
379
  /// Unpack the tangent block of the extended state `z`, QR-reorthonormalise it in
321
380
  /// place, and re-embed the orthonormal frame — the per-chunk renormalisation.
322
381
  ///
@@ -460,6 +519,62 @@ mod tests {
460
519
  assert!((out.spectrum[0] - 0.5).abs() < 1e-4, "{}", out.spectrum[0]);
461
520
  }
462
521
 
522
+ #[test]
523
+ fn guarded_spectrum_refuses_base_escape_but_not_growing_tangents() {
524
+ let ev = VmEval::new(linear_extended(200.0, -2.0, 1));
525
+ let bounded = lyapunov_spectrum_ode_guarded(
526
+ &ev,
527
+ rk45_factory,
528
+ &[],
529
+ 2,
530
+ 1,
531
+ &[0.0, 0.0, 1.0, 0.0],
532
+ 0.0,
533
+ 0.1,
534
+ 0.0,
535
+ 0.2,
536
+ 10.0,
537
+ )
538
+ .unwrap();
539
+ assert!((bounded.spectrum[0] - 200.0).abs() < 1e-4);
540
+ let err = lyapunov_spectrum_ode_guarded(
541
+ &ev,
542
+ rk45_factory,
543
+ &[],
544
+ 2,
545
+ 1,
546
+ &[1.0, 0.0, 1.0, 0.0],
547
+ 0.0,
548
+ 0.1,
549
+ 0.0,
550
+ 0.2,
551
+ 10.0,
552
+ )
553
+ .unwrap_err();
554
+ assert!(matches!(err, LyapunovError::Diverged(_)));
555
+ assert!(err.to_string().contains("base orbit escaped at t="));
556
+ }
557
+
558
+ #[test]
559
+ fn guarded_spectrum_accepts_large_units_with_a_relative_limit() {
560
+ let ev = VmEval::new(linear_extended(0.0, 0.0, 1));
561
+ let out = lyapunov_spectrum_ode_guarded(
562
+ &ev,
563
+ rk45_factory,
564
+ &[],
565
+ 2,
566
+ 1,
567
+ &[1e12, -1e12, 1.0, 0.0],
568
+ 0.0,
569
+ 0.1,
570
+ 0.0,
571
+ 1.0,
572
+ 1e15,
573
+ )
574
+ .unwrap();
575
+ assert_eq!(out.spectrum, vec![0.0]);
576
+ }
577
+
463
578
  #[test]
464
579
  fn rejects_bad_k_and_shapes() {
465
580
  let ev = VmEval::new(linear_extended(0.5, -2.0, 2));
@@ -92,7 +92,7 @@ impl std::error::Error for MapLyapunovError {}
92
92
  /// The accumulated spectrum a [`map_lyapunov`] run returns.
93
93
  #[derive(Clone, Debug, PartialEq)]
94
94
  pub struct MapLyapunovOutcome {
95
- /// The `k` Lyapunov exponents, largest first (QR order) — the time-averaged
95
+ /// The `k` Lyapunov estimates in QR order — the time-averaged
96
96
  /// `log|R_ii|`.
97
97
  pub exponents: Vec<f64>,
98
98
  /// The number of reorthonormalisation intervals completed (the `_elapsed` the
@@ -215,12 +215,9 @@ pub fn map_lyapunov(
215
215
  // Live state and the next-state buffer (a map's RHS writes the next state).
216
216
  let mut x = ic[..dim].to_vec();
217
217
  let mut x_next = vec![0.0; dim];
218
- // The deviation frame, column-major dim × k, seeded to the leading k columns of
219
- // the identity (matching the released `np.eye(dim)[:, :k]`).
220
- let mut w = vec![0.0; dim * k];
221
- for j in 0..k {
222
- w[j * dim + j] = 1.0;
223
- }
218
+ // A mixed frame avoids getting stuck in a coordinate-aligned contracting
219
+ // subspace. The Python reference uses the identical fixed stream and QR.
220
+ let mut w = crate::tangent_frame::initial_frame(dim, k);
224
221
  // The propagated frame W' = J · W (column-major dim × k), a scratch buffer.
225
222
  let mut w_prop = vec![0.0; dim * k];
226
223
  let mut jac = vec![0.0; dim * dim];
@@ -0,0 +1,82 @@
1
+ //! Deterministic, mixed initial tangent frames (mirrored in Python tangent.py).
2
+
3
+ use crate::rng::SplitMix64;
4
+
5
+ /// Column-major orthonormal frame with no privileged coordinate direction.
6
+ ///
7
+ /// Identity columns can remain forever inside a contracting invariant subspace,
8
+ /// missing a maximal exponent in another component. A fixed, local SplitMix64
9
+ /// stream supplies a generic dense frame without changing global RNG state or
10
+ /// introducing run-to-run variation. Two-pass modified Gram--Schmidt controls
11
+ /// orthogonality even when many directions are requested.
12
+ pub(crate) fn initial_frame(dim: usize, k: usize) -> Vec<f64> {
13
+ let mut rng = SplitMix64::new(0x5441_4e47_454e_5453);
14
+ let mut w = vec![0.0; dim * k];
15
+ for j in 0..k {
16
+ for d in 0..dim {
17
+ w[j * dim + d] = 2.0 * rng.next_f64() - 1.0;
18
+ }
19
+ let mut norm = orthogonalise(&mut w, dim, j);
20
+ if norm <= f64::EPSILON {
21
+ // A numerically dependent draw: choose the coordinate least covered
22
+ // by the existing columns, then project it into their complement.
23
+ let mut best = 0;
24
+ let mut least = f64::INFINITY;
25
+ for d in 0..dim {
26
+ let used: f64 = (0..j).map(|i| w[i * dim + d].powi(2)).sum();
27
+ if used < least {
28
+ least = used;
29
+ best = d;
30
+ }
31
+ }
32
+ w[j * dim..(j + 1) * dim].fill(0.0);
33
+ w[j * dim + best] = 1.0;
34
+ norm = orthogonalise(&mut w, dim, j);
35
+ }
36
+ for d in 0..dim {
37
+ w[j * dim + d] /= norm;
38
+ }
39
+ }
40
+ w
41
+ }
42
+
43
+ fn orthogonalise(w: &mut [f64], dim: usize, j: usize) -> f64 {
44
+ for _ in 0..2 {
45
+ for i in 0..j {
46
+ let mut dot = 0.0;
47
+ for d in 0..dim {
48
+ dot += w[i * dim + d] * w[j * dim + d];
49
+ }
50
+ for d in 0..dim {
51
+ w[j * dim + d] -= dot * w[i * dim + d];
52
+ }
53
+ }
54
+ }
55
+ let mut norm2 = 0.0;
56
+ for d in 0..dim {
57
+ norm2 += w[j * dim + d] * w[j * dim + d];
58
+ }
59
+ norm2.sqrt()
60
+ }
61
+
62
+ #[cfg(test)]
63
+ mod tests {
64
+ use super::*;
65
+
66
+ #[test]
67
+ fn mixed_frame_is_reproducible_and_orthonormal() {
68
+ for dim in [1, 2, 3, 10, 64] {
69
+ let w = initial_frame(dim, dim);
70
+ assert_eq!(w, initial_frame(dim, dim));
71
+ assert_eq!(&w[..dim], initial_frame(dim, 1).as_slice());
72
+ for i in 0..dim {
73
+ for j in 0..dim {
74
+ let dot: f64 = (0..dim).map(|d| w[i * dim + d] * w[j * dim + d]).sum();
75
+ let expected = if i == j { 1.0 } else { 0.0 };
76
+ assert!((dot - expected).abs() < 1e-13);
77
+ }
78
+ }
79
+ assert!(w[..dim].iter().all(|value| *value != 0.0));
80
+ }
81
+ }
82
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tsdynamics"
7
- version = "6.0.2"
7
+ version = "6.0.4"
8
8
  description = "Rust-backed ODE/DDE/SDE integration, discrete maps, and chaos analysis for dynamical systems."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<4.0"
@@ -190,7 +190,7 @@ from .families import (
190
190
  )
191
191
 
192
192
  # Single source of truth for the package version; rewritten by python-semantic-release.
193
- __version__ = "6.0.2"
193
+ __version__ = "6.0.4"
194
194
 
195
195
  #: The curated top level. **Sorted, one name per line** — the same mechanical
196
196
  #: policy every public package in this library follows, because a list grouped by
@@ -1182,6 +1182,7 @@ def lyapunov_spectrum_ode(
1182
1182
  rtol: float,
1183
1183
  atol: float,
1184
1184
  jit: bool,
1185
+ escape_limit: float = float("inf"),
1185
1186
  ) -> dict[str, Any]:
1186
1187
  """Run the whole ODE Benettin Lyapunov-spectrum loop in the Rust engine.
1187
1188
 
@@ -1226,6 +1227,9 @@ def lyapunov_spectrum_ode(
1226
1227
  Adaptive-kernel tolerances.
1227
1228
  jit : bool
1228
1229
  Select the Cranelift evaluator.
1230
+ escape_limit : float, optional
1231
+ Upper bound on base-state magnitude, derived by the caller from its
1232
+ scale-and-growth escape policy. Tangent components are not checked.
1229
1233
 
1230
1234
  Returns
1231
1235
  -------
@@ -1256,6 +1260,7 @@ def lyapunov_spectrum_ode(
1256
1260
  float(burn_in),
1257
1261
  float(final_time),
1258
1262
  bool(jit),
1263
+ float(escape_limit),
1259
1264
  )
1260
1265
  return {
1261
1266
  "spectrum": np.asarray(spectrum, dtype=np.float64),
@@ -142,7 +142,9 @@ def _resolve_method_and_prepare(
142
142
  ``rebuild_extra`` carries entry-point-specific build kwargs (``integrate``
143
143
  passes ``ic=``; ``ensemble`` seeds each trajectory later, so it passes none).
144
144
 
145
- Returns the canonical method name and the (possibly rebuilt) problem.
145
+ Returns the implementation kernel name and the (possibly rebuilt) problem.
146
+ A plugin's selectable name can differ from its ``SolverSpec.kernel``;
147
+ the compiled engine dispatches the implementation, not that Python name.
146
148
  """
147
149
  from .run import _as_problem
148
150
 
@@ -157,7 +159,7 @@ def _resolve_method_and_prepare(
157
159
  problem = _as_problem(
158
160
  system_or_problem, with_jacobian=True, **rebuild_extra, **build_kwargs
159
161
  )
160
- return resolution.name, problem
162
+ return resolution.kernel, problem
161
163
 
162
164
 
163
165
  def __dir__() -> list[str]:
@@ -1,9 +1,9 @@
1
- """The Python solver registry — the discovery half of the pluggable engine (D4).
1
+ """Solver metadata, capability checks, and installed solver discovery.
2
2
 
3
3
  .. note::
4
4
  **Advanced / internal.** Reachable as :mod:`tsdynamics._solvers` to inspect or
5
5
  extend the available integrators, but most users only ever pass a solver by
6
- name (``integrate(method="rk45")``) rather than importing this package.
6
+ name (``system.run(10.0, solver="rk45")``) rather than importing this package.
7
7
 
8
8
  This package is the Python-side mirror of the Rust link-time solver registry in
9
9
  the ``tsdyn-solvers`` crate. It holds a ``name -> SolverSpec`` table of solver
@@ -16,12 +16,10 @@ auto-populates it from two sources at import time:
16
16
  2. **out-of-tree plugins** declared under the ``tsdynamics._solvers`` entry-point
17
17
  group (see :mod:`tsdynamics.plugins`).
18
18
 
19
- Stream **F2** owns this *mechanism* only. It ships no built-in solvers — those
20
- arrive with the Rust kernels (streams E3/E4/E-SDE) and the selection layer
21
- (``method=`` resolution + auto-stiffness) is stream **C-SOLV**, which builds on
22
- top of this registry. A :class:`SolverSpec` therefore carries no Python
23
- implementation; its :attr:`SolverSpec.kernel` names the Rust kernel to dispatch
24
- to once the engine bindings (stream E7) land.
19
+ A :class:`SolverSpec` carries metadata rather than an implementation. Its
20
+ :attr:`SolverSpec.kernel` names the executable kernel selected by the engine.
21
+ An installed plugin may expose an existing kernel under a new name; adding an
22
+ algorithm requires implementing and rebuilding the compiled engine as well.
25
23
  """
26
24
 
27
25
  from __future__ import annotations
@@ -44,7 +42,7 @@ _VALID_KINDS = frozenset({"explicit", "implicit"})
44
42
  class SolverCaps:
45
43
  """A solver's capabilities — the Python mirror of ``tsdyn_solvers::Caps``.
46
44
 
47
- Drives ``method=`` resolution and the auto-stiffness layer (stream C-SOLV).
45
+ Drives solver resolution and the auto-stiffness layer.
48
46
  The four flags are how the selection layer reasons about a kernel without
49
47
  running it: ``kind`` is the auto-stiffness axis (:func:`~select.select`
50
48
  returns an ``"implicit"`` kernel only on a stiff RHS), ``needs_jacobian``
@@ -187,7 +187,8 @@ class Resolution:
187
187
  Attributes
188
188
  ----------
189
189
  name : str
190
- The canonical kernel name (the resolved ``method=`` the engine receives).
190
+ The registered solver name. Plugins can select an implementation with a
191
+ different name through :attr:`kernel`.
191
192
  spec : SolverSpec
192
193
  The registered spec for :attr:`name`.
193
194
  family : str or None
@@ -111,17 +111,25 @@ def detect_unbounded(y: np.ndarray) -> Unbounded | None:
111
111
  )
112
112
 
113
113
 
114
- def escaped(peak: float | None) -> bool:
115
- """Whether a recorded state magnitude is past :data:`ESCAPE_SCALE`.
114
+ def escaped(peak: float | None, *, start: float | None = None) -> bool:
115
+ """Whether recorded orbit magnitudes satisfy the shared escape policy.
116
116
 
117
- The one-number form, for a caller that holds only the landed state (a
118
- Lyapunov estimator) rather than the whole orbit. ``None`` — nothing was
119
- recorded — is *not* an escape: a family that cannot answer says nothing.
117
+ With a recorded starting scale, both :data:`ESCAPE_SCALE` and
118
+ :data:`ESCAPE_GROWTH` must be reached. The legacy one-number form retains
119
+ its absolute-scale check when no starting scale was recorded. ``None`` for
120
+ the peak means there is no evidence of escape.
120
121
  """
121
122
  if peak is None:
122
123
  return False
123
124
  value = float(peak)
124
- return not np.isfinite(value) or value >= ESCAPE_SCALE
125
+ if not np.isfinite(value):
126
+ return True
127
+ if start is None:
128
+ return value >= ESCAPE_SCALE
129
+ initial = float(start)
130
+ return not np.isfinite(initial) or (
131
+ value >= ESCAPE_SCALE and value / max(initial, 1.0) >= ESCAPE_GROWTH
132
+ )
125
133
 
126
134
 
127
135
  def __dir__() -> list[str]:
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
  from typing import Any as _Any
16
16
 
17
17
  from .. import registry as _registry
18
+ from ..families import ContinuousSystem as _ContinuousSystem
18
19
  from ..plugins import ANALYSES_GROUP, register_entry_points
19
20
  from . import _discovery
20
21
 
@@ -145,7 +146,14 @@ globals()["basins"] = _basins_fn
145
146
  # picture) had no door at all.
146
147
  for _name, _subjects, _area, _keywords in PLANAR_ANALYSES:
147
148
  register(
148
- getattr(planar, _name), subjects=_subjects, area=_area, keywords=_keywords, replace=True
149
+ getattr(planar, _name),
150
+ subjects=_subjects,
151
+ area=_area,
152
+ keywords=_keywords,
153
+ families=("ode",) if "flow" in _subjects else None,
154
+ system_types=(_ContinuousSystem,) if "flow" in _subjects else None,
155
+ min_dim=2 if "flow" in _subjects else None,
156
+ replace=True,
149
157
  )
150
158
  del _name, _subjects, _area, _keywords
151
159
 
@@ -215,6 +223,8 @@ def find(what: _Any = None, /) -> AnalysisList:
215
223
  ``what`` is a **string** (free-text search over the name, area, keywords and
216
224
  summary), or a **subject** — a system, a :class:`~tsdynamics.data.Trajectory`,
217
225
  an array, a result, or any of their classes — or omitted, for all of them.
226
+ System instances are filtered by the registered family, state dimension and
227
+ concrete autonomy restrictions. A class query cannot check parameter values.
218
228
  Returns a plain ``list`` of the analysis *functions*, whose repr is the
219
229
  grouped table.
220
230
 
@@ -224,7 +234,7 @@ def find(what: _Any = None, /) -> AnalysisList:
224
234
  >>> sorted(f.__name__ for f in ts.analysis.find("multistability"))[:3]
225
235
  ['attractors', 'basin_fractions', 'basins']
226
236
  >>> len(ts.analysis.find(ts.systems.Henon())) # a map has no vector field
227
- 13
237
+ 11
228
238
  >>> len(ts.analysis.find(ts.systems.Lorenz()))
229
239
  20
230
240
  """
@@ -246,7 +256,12 @@ def find(what: _Any = None, /) -> AnalysisList:
246
256
  f" ts.analysis.find(traj) # a subject\n"
247
257
  f" ts.analysis.find() # everything"
248
258
  )
249
- hits = [e for e in entries if set(tokens) & set(e.metadata.get("subjects", ()))]
259
+ hits = [
260
+ e
261
+ for e in entries
262
+ if set(tokens) & set(e.metadata.get("subjects", ()))
263
+ and _discovery.supports_subject(e, what)
264
+ ]
250
265
  subject = what.__name__ if isinstance(what, type) else type(what).__name__
251
266
  return AnalysisList(hits, f"{len(hits)} analyses take a {subject}")
252
267