tsdynamics 5.2.4__tar.gz → 5.2.5__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 (262) hide show
  1. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/PKG-INFO +1 -1
  2. tsdynamics-5.2.5/crates/tsdyn-core/src/bridge/lyapunov.rs +162 -0
  3. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/map.rs +89 -1
  4. tsdynamics-5.2.5/crates/tsdyn-core/src/bridge/map_lyapunov.rs +142 -0
  5. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/mod.rs +5 -1
  6. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/lib.rs +208 -0
  7. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/lib.rs +15 -0
  8. tsdynamics-5.2.5/crates/tsdyn-engine/src/lyapunov.rs +476 -0
  9. tsdynamics-5.2.5/crates/tsdyn-engine/src/map_lyapunov.rs +436 -0
  10. tsdynamics-5.2.5/crates/tsdyn-engine/src/param_sweep.rs +381 -0
  11. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/pyproject.toml +1 -1
  12. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/__init__.py +1 -1
  13. tsdynamics-5.2.5/src/tsdynamics/analysis/fixedpoints/_interval.py +717 -0
  14. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/fixedpoints/fixed.py +107 -6
  15. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/lyapunov/__init__.py +96 -1
  16. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/orbits/orbit_diagram.py +120 -49
  17. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/tangent.py +203 -24
  18. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/compile.py +168 -0
  19. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/run.py +278 -1
  20. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/discrete.py +14 -1
  21. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/LICENSE +0 -0
  22. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/README.md +0 -0
  23. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/Cargo.toml +0 -0
  24. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/Cargo.lock +0 -0
  25. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/Cargo.toml +0 -0
  26. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/basin.rs +0 -0
  27. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/dde.rs +0 -0
  28. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/events.rs +0 -0
  29. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/marshal.rs +0 -0
  30. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/ode.rs +0 -0
  31. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/sde.rs +0 -0
  32. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-core/src/bridge/stepper.rs +0 -0
  33. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/Cargo.toml +0 -0
  34. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/basin.rs +0 -0
  35. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/dde.rs +0 -0
  36. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/ensemble.rs +0 -0
  37. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/event.rs +0 -0
  38. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/integrate.rs +0 -0
  39. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/map.rs +0 -0
  40. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/problem.rs +0 -0
  41. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/rng.rs +0 -0
  42. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/sde.rs +0 -0
  43. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-engine/src/testkit.rs +0 -0
  44. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/Cargo.toml +0 -0
  45. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/builder.rs +0 -0
  46. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/eval.rs +0 -0
  47. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/lib.rs +0 -0
  48. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/op.rs +0 -0
  49. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/reference.rs +0 -0
  50. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/src/tape.rs +0 -0
  51. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/coverage_tape.rs +0 -0
  52. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/CircadianRhythm.txt +0 -0
  53. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/Lorenz.txt +0 -0
  54. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/SanUmSrisuchinwong.txt +0 -0
  55. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/SprottMore.txt +0 -0
  56. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/Torus.txt +0 -0
  57. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/fixtures/YuWang2.txt +0 -0
  58. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/gen_fixtures.py +0 -0
  59. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/golden_fixtures.rs +0 -0
  60. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-ir/tests/roundtrip.rs +0 -0
  61. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/Cargo.toml +0 -0
  62. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/src/codegen.rs +0 -0
  63. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/src/error.rs +0 -0
  64. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/src/evaluator.rs +0 -0
  65. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/src/lib.rs +0 -0
  66. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/src/shims.rs +0 -0
  67. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/tests/compile_latency.rs +0 -0
  68. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/tests/golden_fixtures.rs +0 -0
  69. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-jit/tests/interpreter_equivalence.rs +0 -0
  70. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/Cargo.toml +0 -0
  71. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/caps.rs +0 -0
  72. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/adams.rs +0 -0
  73. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/bs3.rs +0 -0
  74. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/cashkarp.rs +0 -0
  75. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/control.rs +0 -0
  76. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/dop853.rs +0 -0
  77. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/euler.rs +0 -0
  78. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/heun.rs +0 -0
  79. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/heun_euler.rs +0 -0
  80. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/midpoint.rs +0 -0
  81. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/mod.rs +0 -0
  82. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/ralston.rs +0 -0
  83. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/rk4.rs +0 -0
  84. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/rk45.rs +0 -0
  85. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/rk4_38.rs +0 -0
  86. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/rkf45.rs +0 -0
  87. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/ssprk3.rs +0 -0
  88. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/testkit.rs +0 -0
  89. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/explicit/tsit5.rs +0 -0
  90. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/backward_euler.rs +0 -0
  91. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/bdf.rs +0 -0
  92. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/control.rs +0 -0
  93. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/implicit_midpoint.rs +0 -0
  94. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/linalg.rs +0 -0
  95. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/mod.rs +0 -0
  96. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/newton.rs +0 -0
  97. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/rosenbrock.rs +0 -0
  98. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/sdirk2.rs +0 -0
  99. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/trapezoid.rs +0 -0
  100. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/implicit/trbdf2.rs +0 -0
  101. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/lib.rs +0 -0
  102. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/registry.rs +0 -0
  103. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/sde/euler_maruyama.rs +0 -0
  104. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/sde/milstein.rs +0 -0
  105. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/sde/mod.rs +0 -0
  106. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/sde/testkit.rs +0 -0
  107. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/src/solver.rs +0 -0
  108. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/tests/explicit_accuracy.rs +0 -0
  109. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/tests/implicit_stiff.rs +0 -0
  110. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-solvers/tests/registry.rs +0 -0
  111. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-vm/Cargo.toml +0 -0
  112. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-vm/src/interp.rs +0 -0
  113. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-vm/src/lib.rs +0 -0
  114. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-vm/tests/golden_fixtures.rs +0 -0
  115. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/crates/tsdyn-vm/tests/oracle_equivalence.rs +0 -0
  116. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/_result_common.py +0 -0
  117. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/__init__.py +0 -0
  118. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_plotbuilder.py +0 -0
  119. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result.py +0 -0
  120. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_array.py +0 -0
  121. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_base.py +0 -0
  122. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_collection.py +0 -0
  123. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_json.py +0 -0
  124. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_scalar.py +0 -0
  125. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_scaling.py +0 -0
  126. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_result_viz.py +0 -0
  127. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/_tangent.py +0 -0
  128. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/__init__.py +0 -0
  129. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/_common.py +0 -0
  130. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/attractors.py +0 -0
  131. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/basins.py +0 -0
  132. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/continuation.py +0 -0
  133. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/basins/metrics.py +0 -0
  134. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/chaos/__init__.py +0 -0
  135. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/chaos/_common.py +0 -0
  136. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/chaos/expansion.py +0 -0
  137. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/chaos/gali.py +0 -0
  138. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/chaos/zero_one.py +0 -0
  139. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/__init__.py +0 -0
  140. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/_common.py +0 -0
  141. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/_scaling.py +0 -0
  142. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/correlation.py +0 -0
  143. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/fixedmass.py +0 -0
  144. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/dimensions/generalized.py +0 -0
  145. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/embedding/__init__.py +0 -0
  146. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/embedding/_common.py +0 -0
  147. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/embedding/delay.py +0 -0
  148. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/embedding/dimension.py +0 -0
  149. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/embedding/embed.py +0 -0
  150. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/__init__.py +0 -0
  151. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/core.py +0 -0
  152. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/dispersion.py +0 -0
  153. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/lz.py +0 -0
  154. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/multiscale.py +0 -0
  155. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/permutation.py +0 -0
  156. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/entropy/sample.py +0 -0
  157. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/fixedpoints/__init__.py +0 -0
  158. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/fixedpoints/_common.py +0 -0
  159. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/fixedpoints/periodic.py +0 -0
  160. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/lyapunov/from_data.py +0 -0
  161. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/orbits/__init__.py +0 -0
  162. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/orbits/poincare.py +0 -0
  163. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/orbits/return_map.py +0 -0
  164. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/recurrence/__init__.py +0 -0
  165. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/recurrence/_common.py +0 -0
  166. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/recurrence/matrix.py +0 -0
  167. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/recurrence/rqa.py +0 -0
  168. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/recurrence/windowed.py +0 -0
  169. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/sampling/__init__.py +0 -0
  170. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/sampling/sagitta.py +0 -0
  171. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/surrogate/__init__.py +0 -0
  172. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/surrogate/_common.py +0 -0
  173. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/surrogate/generators.py +0 -0
  174. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/surrogate/hypothesis.py +0 -0
  175. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/analysis/surrogate/statistics.py +0 -0
  176. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/data/__init__.py +0 -0
  177. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/data/sampling.py +0 -0
  178. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/data/trajectory.py +0 -0
  179. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/__init__.py +0 -0
  180. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/_base.py +0 -0
  181. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/_crossings.py +0 -0
  182. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/_variational.py +0 -0
  183. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/ensemble.py +0 -0
  184. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/poincare.py +0 -0
  185. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/projected.py +0 -0
  186. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/stroboscopic.py +0 -0
  187. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/derived/wrapped.py +0 -0
  188. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/__init__.py +0 -0
  189. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/_families.py +0 -0
  190. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/events.py +0 -0
  191. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/problem.py +0 -0
  192. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/reference.py +0 -0
  193. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/run_methods.py +0 -0
  194. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/sde_run.py +0 -0
  195. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/stepper.py +0 -0
  196. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/engine/symbols.py +0 -0
  197. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/errors.py +0 -0
  198. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/__init__.py +0 -0
  199. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/_accessors.py +0 -0
  200. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/_dde_lyapunov.py +0 -0
  201. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/_plottable.py +0 -0
  202. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/base.py +0 -0
  203. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/continuous.py +0 -0
  204. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/delay.py +0 -0
  205. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/protocol.py +0 -0
  206. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/stochastic.py +0 -0
  207. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/families/wrapped.py +0 -0
  208. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/plugins.py +0 -0
  209. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/py.typed +0 -0
  210. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/registry.py +0 -0
  211. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/solvers/__init__.py +0 -0
  212. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/solvers/explicit.py +0 -0
  213. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/solvers/implicit.py +0 -0
  214. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/solvers/select.py +0 -0
  215. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/solvers/stochastic.py +0 -0
  216. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/__init__.py +0 -0
  217. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/__init__.py +0 -0
  218. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/chaotic_attractors.py +0 -0
  219. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/chem_bio_systems.py +0 -0
  220. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/climate_geophysics.py +0 -0
  221. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/coupled_systems.py +0 -0
  222. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/delayed_systems.py +0 -0
  223. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/exotic_systems.py +0 -0
  224. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/oscillatory_systems.py +0 -0
  225. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/physical_systems.py +0 -0
  226. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/population_dynamics.py +0 -0
  227. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/continuous/spatial_fields.py +0 -0
  228. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/__init__.py +0 -0
  229. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/chaotic_maps.py +0 -0
  230. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/exotic_maps.py +0 -0
  231. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/geometric_maps.py +0 -0
  232. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/polynomial_maps.py +0 -0
  233. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/systems/discrete/population_maps.py +0 -0
  234. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/__init__.py +0 -0
  235. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/_common.py +0 -0
  236. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/_result.py +0 -0
  237. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/features.py +0 -0
  238. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/preprocessing.py +0 -0
  239. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/transforms/spectral.py +0 -0
  240. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/utils/__init__.py +0 -0
  241. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/utils/grids.py +0 -0
  242. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/__init__.py +0 -0
  243. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/compose.py +0 -0
  244. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/export.py +0 -0
  245. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/producers.py +0 -0
  246. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/__init__.py +0 -0
  247. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/caps.py +0 -0
  248. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/json.py +0 -0
  249. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/mpl/__init__.py +0 -0
  250. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/mpl/_anim.py +0 -0
  251. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/mpl/_core.py +0 -0
  252. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/mpl/_threed.py +0 -0
  253. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/__init__.py +0 -0
  254. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/_anim.py +0 -0
  255. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/_composite.py +0 -0
  256. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/_core.py +0 -0
  257. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/_html.py +0 -0
  258. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/plotly/_threed.py +0 -0
  259. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/threejs/__init__.py +0 -0
  260. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/render/threejs/_lower.py +0 -0
  261. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/spec.py +0 -0
  262. {tsdynamics-5.2.4 → tsdynamics-5.2.5}/src/tsdynamics/viz/style.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsdynamics
3
- Version: 5.2.4
3
+ Version: 5.2.5
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -0,0 +1,162 @@
1
+ //! ODE Lyapunov-spectrum entry point (stream `perf/ode-lyapunov-engine`).
2
+ //!
3
+ //! Thin bridge over the engine's [`tsdyn_engine::lyapunov`] kernel: build the
4
+ //! evaluator (interpreter or JIT) over the **extended** variational tape + resolve
5
+ //! the solver here (the cranelift edge lives in this crate, not the engine), then
6
+ //! run the whole burn-in + averaging Benettin loop in one call. The per-`dt`
7
+ //! integration is byte-for-byte the released `TangentSystem` ODE numerics, so
8
+ //! `interp == jit` and the spectrum matches the Python path to floating-point
9
+ //! tolerance.
10
+ //!
11
+ //! Shared plumbing (evaluator/solver builders, the `EngineError` enum) lives in
12
+ //! [`super::marshal`]; the renormalisation loop and the QR live in
13
+ //! [`tsdyn_engine::lyapunov`].
14
+
15
+ use tsdyn_engine::lyapunov::{lyapunov_spectrum_ode, LyapunovError, LyapunovOutcome};
16
+ use tsdyn_ir::Tape;
17
+ use tsdyn_solvers::Solver;
18
+
19
+ use super::marshal::{
20
+ build_evaluator, build_solver, require_jacobian_if_needed, resolve_solver, EngineError,
21
+ };
22
+
23
+ /// Map a [`LyapunovError`] (a kernel-side shape / divergence failure) to the
24
+ /// bridge's [`EngineError`] so the binding routes it to the matching Python
25
+ /// exception (`ValueError` / `RuntimeError`).
26
+ fn to_engine_err(e: LyapunovError) -> EngineError {
27
+ match e {
28
+ LyapunovError::BadShape(m) => EngineError::BadShape(m),
29
+ LyapunovError::Diverged(m) => EngineError::Diverged(m),
30
+ }
31
+ }
32
+
33
+ /// Run the burn-in + averaging Benettin Lyapunov-spectrum loop over an
34
+ /// **extended variational** ODE tape.
35
+ ///
36
+ /// The `tape` is the `dim*(k+1)`-wide extended variational system (base RHS ⊕ the
37
+ /// `k` tangent-vector RHS blocks) built by the Python
38
+ /// `tsdynamics.derived._variational.build_variational_tape`. `method` resolves
39
+ /// through the solver registry; an implicit kernel needs a Jacobian-carrying tape
40
+ /// (the extended tape carries one — rejected up front exactly as the integrate
41
+ /// path does, for the defence-in-depth message). `dt` is the renormalisation
42
+ /// interval, `burn_in` / `final_time` the two window lengths. `p` is the live
43
+ /// control-parameter vector. Returns the spectrum plus the final extended state.
44
+ #[allow(clippy::too_many_arguments)]
45
+ pub fn lyapunov_spectrum_ode_bridge(
46
+ tape: Tape,
47
+ p: &[f64],
48
+ method: &str,
49
+ rtol: f64,
50
+ atol: f64,
51
+ dim: usize,
52
+ k: usize,
53
+ z0: &[f64],
54
+ t0: f64,
55
+ dt: f64,
56
+ burn_in: f64,
57
+ final_time: f64,
58
+ jit: bool,
59
+ ) -> Result<LyapunovOutcome, EngineError> {
60
+ // The extended tape is `n_state == ev.dim()` (a plain ODE-shaped tape over the
61
+ // stacked state), validated by the kernel against `dim*(k+1)`. Resolve the
62
+ // method + the Jacobian guard exactly like the integrate / basin paths.
63
+ let name = resolve_solver(method)?;
64
+ require_jacobian_if_needed(&tape, name)?;
65
+ let ev = build_evaluator(tape, jit)?;
66
+ // A fresh kernel per `dt` chunk (mirrors `OdeStepper::advance` / the basin
67
+ // march); the name is a registry name, so `build_solver` always succeeds.
68
+ let factory = move || -> Box<dyn Solver> { build_solver(name, rtol, atol) };
69
+ lyapunov_spectrum_ode(&*ev, factory, p, dim, k, z0, t0, dt, burn_in, final_time)
70
+ .map_err(to_engine_err)
71
+ }
72
+
73
+ #[cfg(test)]
74
+ mod tests {
75
+ use super::*;
76
+ use tsdyn_ir::TapeBuilder;
77
+
78
+ /// Extended variational tape of `dx = a x, dy = b y` with `k` tangents (the
79
+ /// Jacobian is the constant `diag(a, b)`). Spectrum is `[max(a,b), min(a,b)]`.
80
+ fn linear_extended(a: f64, b: f64, k: usize) -> Tape {
81
+ let dim = 2;
82
+ let mut bld = TapeBuilder::new();
83
+ let x = bld.state(0);
84
+ let y = bld.state(1);
85
+ let ac = bld.constant(a);
86
+ let bc = bld.constant(b);
87
+ let dx = bld.mul(ac, x);
88
+ let dy = bld.mul(bc, y);
89
+ let mut outs = vec![dx, dy];
90
+ for i in 0..k {
91
+ let base = dim + i * dim;
92
+ let w0 = bld.state(base);
93
+ let w1 = bld.state(base + 1);
94
+ let dw0 = bld.mul(ac, w0);
95
+ let dw1 = bld.mul(bc, w1);
96
+ outs.push(dw0);
97
+ outs.push(dw1);
98
+ }
99
+ let n = dim * (k + 1);
100
+ bld.finish(&outs, &[], n, 0).unwrap()
101
+ }
102
+
103
+ #[test]
104
+ fn bridge_spectrum_matches_analytic_and_interp_equals_jit() {
105
+ let (a, b, k) = (0.5, -2.0, 2);
106
+ let z0 = vec![1.0, 1.0, 1.0, 0.0, 0.0, 1.0];
107
+ let run = |jit| {
108
+ lyapunov_spectrum_ode_bridge(
109
+ linear_extended(a, b, k),
110
+ &[],
111
+ "rk45",
112
+ 1e-9,
113
+ 1e-11,
114
+ 2,
115
+ k,
116
+ &z0,
117
+ 0.0,
118
+ 0.1,
119
+ 5.0,
120
+ 50.0,
121
+ jit,
122
+ )
123
+ .unwrap()
124
+ };
125
+ let interp = run(false);
126
+ let jit = run(true);
127
+ // interp == jit bit-for-bit (both drive the same &dyn Evaluator).
128
+ assert_eq!(interp.spectrum.len(), jit.spectrum.len());
129
+ for (i, (x, y)) in interp.spectrum.iter().zip(jit.spectrum.iter()).enumerate() {
130
+ assert_eq!(
131
+ x.to_bits(),
132
+ y.to_bits(),
133
+ "exponent {i}: jit {y} != interp {x}"
134
+ );
135
+ }
136
+ // and matches the analytic spectrum.
137
+ assert!((interp.spectrum[0] - 0.5).abs() < 1e-4);
138
+ assert!((interp.spectrum[1] - (-2.0)).abs() < 1e-4);
139
+ }
140
+
141
+ #[test]
142
+ fn bridge_rejects_unknown_method() {
143
+ let z0 = vec![1.0, 1.0, 1.0, 0.0, 0.0, 1.0];
144
+ let err = lyapunov_spectrum_ode_bridge(
145
+ linear_extended(0.5, -2.0, 2),
146
+ &[],
147
+ "no-such",
148
+ 1e-6,
149
+ 1e-9,
150
+ 2,
151
+ 2,
152
+ &z0,
153
+ 0.0,
154
+ 0.1,
155
+ 1.0,
156
+ 1.0,
157
+ false,
158
+ )
159
+ .unwrap_err();
160
+ assert!(matches!(err, EngineError::UnknownMethod(_)), "got {err:?}");
161
+ }
162
+ }
@@ -5,7 +5,10 @@
5
5
  //! iteration loop, shape contract and divergence behaviour are unchanged. Shared
6
6
  //! plumbing lives in [`super::marshal`].
7
7
 
8
- use tsdyn_engine::{iterate_dense, iterate_ensemble_final as engine_iterate_ensemble};
8
+ use tsdyn_engine::{
9
+ iterate_dense, iterate_ensemble_final as engine_iterate_ensemble, map_orbit_sweep, SweepError,
10
+ SweepOutcome,
11
+ };
9
12
  use tsdyn_ir::Tape;
10
13
 
11
14
  use super::marshal::{build_evaluator, map_diverge_msg, EngineError};
@@ -126,3 +129,88 @@ pub fn map_ensemble_final(
126
129
  let result = engine_iterate_ensemble(&*ev, ics, &[], steps);
127
130
  Ok(result.states)
128
131
  }
132
+
133
+ /// Map a [`SweepError`] (a kernel-side shape failure) to the bridge's
134
+ /// [`EngineError`] so the binding routes it to the same Python `ValueError` the
135
+ /// other entry points use.
136
+ fn sweep_to_engine_err(e: SweepError) -> EngineError {
137
+ match e {
138
+ SweepError::BadShape(m) => EngineError::BadShape(m),
139
+ }
140
+ }
141
+
142
+ /// Run the whole map orbit-diagram parameter sweep in one call (stream
143
+ /// `perf/param-sweep-kernel`).
144
+ ///
145
+ /// Unlike [`iterate_map`], the tape here keeps the **swept** parameter as its
146
+ /// single runtime `Param` input (`n_param == 1`) — the Python wiring lowers the
147
+ /// map once with the other parameters folded in, so a 1000-value sweep is one FFI
148
+ /// call instead of ~1000 `iterate_map` round-trips. For each value the kernel sets
149
+ /// `p[sweep_index] = value`, runs `transient + n` iterations (carrying the final
150
+ /// state across values when `carry_state`), and records the last `n` asymptotic
151
+ /// states' selected `components`.
152
+ ///
153
+ /// Returns `(points, status)`: `points` is the flat row-major
154
+ /// `(n_values * n, n_components)` recorded buffer, and `status[k]` is `0` for a
155
+ /// value that ran finitely / `1` for a value that diverged (whose block is zero —
156
+ /// the binding drops it to an empty set and warns, mirroring the per-value path).
157
+ /// The per-iterate numerics are byte-for-byte the [`iterate_map`] path (a `Param`
158
+ /// leaf reads the same `f64` a folded `Const` would).
159
+ ///
160
+ /// `jit` selects the evaluator (Cranelift native code vs the interpreter); both
161
+ /// are bit-for-bit identical on `eval`, so the swept diagram is numerically
162
+ /// identical either way.
163
+ #[allow(clippy::too_many_arguments)]
164
+ pub fn map_param_sweep(
165
+ tape: Tape,
166
+ base_params: &[f64],
167
+ sweep_index: usize,
168
+ values: &[f64],
169
+ ic: &[f64],
170
+ components: &[usize],
171
+ transient: usize,
172
+ n_record: usize,
173
+ carry_state: bool,
174
+ jit: bool,
175
+ ) -> Result<(Vec<f64>, Vec<i64>), EngineError> {
176
+ // The sweep tape's next-state dimension equals its state dimension, and it
177
+ // carries exactly the swept parameter as a runtime input. Reject a tape whose
178
+ // shape disagrees (reachable only via a hand-built tape) so a mismatch is a
179
+ // clean error rather than a release-mode out-of-bounds inside the eval loop.
180
+ let dim = tape.dim();
181
+ if tape.n_state() != dim {
182
+ return Err(EngineError::BadShape(format!(
183
+ "map sweep expects n_state == dim (state and next-state share a \
184
+ dimension); got n_state = {} != dim = {dim}",
185
+ tape.n_state()
186
+ )));
187
+ }
188
+ if tape.n_param() != base_params.len() {
189
+ return Err(EngineError::BadShape(format!(
190
+ "map sweep base-parameter vector has length {}, but the tape declares \
191
+ n_param = {}",
192
+ base_params.len(),
193
+ tape.n_param()
194
+ )));
195
+ }
196
+ let ev = build_evaluator(tape, jit)?;
197
+ let SweepOutcome { points, status, .. } = map_orbit_sweep(
198
+ &*ev,
199
+ base_params,
200
+ sweep_index,
201
+ values,
202
+ ic,
203
+ components,
204
+ transient,
205
+ n_record,
206
+ carry_state,
207
+ )
208
+ .map_err(sweep_to_engine_err)?;
209
+ // Flatten the per-value fate to a plain i64 code (0 = ok, 1 = diverged) the
210
+ // FFI returns alongside the points buffer.
211
+ let status: Vec<i64> = status
212
+ .into_iter()
213
+ .map(|s| if s.is_ok() { 0 } else { 1 })
214
+ .collect();
215
+ Ok((points, status))
216
+ }
@@ -0,0 +1,142 @@
1
+ //! Discrete-map Lyapunov-spectrum entry point (stream `perf/map-lyapunov-kernel`).
2
+ //!
3
+ //! Thin bridge over the engine's [`tsdyn_engine::map_lyapunov`] kernel: build the
4
+ //! evaluator (interpreter or JIT) here (the cranelift edge lives in this crate, not
5
+ //! the engine), then run the whole QR tangent-map iteration in one call. The map
6
+ //! tape must carry its Jacobian (`with_jacobian=True`) — the tangent map needs
7
+ //! `∂f/∂u`; a Jacobian-less tape is rejected as the engine error
8
+ //! [`MapLyapunovError::NoJacobian`].
9
+ //!
10
+ //! Driven over the same lowered IR tape, the interpreter and JIT agree bit-for-bit;
11
+ //! against the released Python `TangentSystem._accumulate_map` the result differs
12
+ //! only by the lowered-IR vs pure-Python `_step`/`_jacobian` float order (the
13
+ //! WS-MAPITER caveat) — the same attractor, the same spectrum to tolerance.
14
+
15
+ use tsdyn_engine::map_lyapunov::{map_lyapunov, MapLyapunovError, MapLyapunovOutcome};
16
+ use tsdyn_ir::Tape;
17
+
18
+ use super::marshal::{build_evaluator, EngineError};
19
+
20
+ /// Map a [`MapLyapunovError`] (a kernel-side shape / Jacobian / divergence failure)
21
+ /// to the bridge's [`EngineError`] so the binding layer routes it to the same
22
+ /// Python exception the other entry points use (`ValueError` / `RuntimeError`). The
23
+ /// Jacobian-less case carries the actionable "recompile with a Jacobian" message.
24
+ fn to_engine_err(e: MapLyapunovError) -> EngineError {
25
+ match e {
26
+ MapLyapunovError::BadShape(m) => EngineError::BadShape(m),
27
+ MapLyapunovError::NoJacobian => EngineError::BadShape(
28
+ "map Lyapunov needs the step Jacobian, but the map tape was compiled without one \
29
+ (with_jacobian=False). Recompile the map problem with a Jacobian."
30
+ .to_string(),
31
+ ),
32
+ MapLyapunovError::Diverged(m) => EngineError::Diverged(m),
33
+ }
34
+ }
35
+
36
+ /// Run the discrete-map QR tangent-map Lyapunov spectrum over a **map** tape.
37
+ ///
38
+ /// Builds the evaluator over `tape` (which must carry its step Jacobian), seeds the
39
+ /// deviation frame to the leading `k` identity columns, iterates `steps` map steps
40
+ /// propagating the frame through `J(x_n)` (pre-image convention) with a QR
41
+ /// reorthonormalisation every `reortho_interval` steps, and returns the `k`
42
+ /// time-averaged exponents (largest first) plus the completed-interval count. The
43
+ /// map tape folds its parameters in, so `p` is empty. `jit` selects the Cranelift
44
+ /// evaluator (numerically identical to the interpreter).
45
+ #[allow(clippy::too_many_arguments)]
46
+ pub fn map_lyapunov_bridge(
47
+ tape: Tape,
48
+ p: &[f64],
49
+ ic: &[f64],
50
+ steps: usize,
51
+ k: usize,
52
+ reortho_interval: usize,
53
+ jit: bool,
54
+ ) -> Result<MapLyapunovOutcome, EngineError> {
55
+ let ev = build_evaluator(tape, jit)?;
56
+ map_lyapunov(&*ev, p, ic, steps, k, reortho_interval).map_err(to_engine_err)
57
+ }
58
+
59
+ #[cfg(test)]
60
+ mod tests {
61
+ use super::*;
62
+ use tsdyn_ir::TapeBuilder;
63
+
64
+ /// Hénon `(x, y) ← (1 - a x² + y, b x)` carrying its analytic Jacobian.
65
+ fn henon_jac(a: f64, bcoef: f64) -> Tape {
66
+ let mut b = TapeBuilder::new();
67
+ let x = b.state(0);
68
+ let y = b.state(1);
69
+ let one = b.constant(1.0);
70
+ let ac = b.constant(a);
71
+ let bc = b.constant(bcoef);
72
+ let xx = b.mul(x, x);
73
+ let axx = b.mul(ac, xx);
74
+ let omaxx = b.sub(one, axx);
75
+ let nx = b.add(omaxx, y);
76
+ let ny = b.mul(bc, x);
77
+ let two = b.constant(2.0);
78
+ let twoa = b.mul(two, ac);
79
+ let twoax = b.mul(twoa, x);
80
+ let neg_twoax = b.neg(twoax);
81
+ let zero = b.constant(0.0);
82
+ b.finish(&[nx, ny], &[neg_twoax, one, bc, zero], 2, 0)
83
+ .unwrap()
84
+ }
85
+
86
+ /// A Jacobian-less map tape (the rejected case).
87
+ fn henon_no_jac(a: f64, bcoef: f64) -> Tape {
88
+ let mut b = TapeBuilder::new();
89
+ let x = b.state(0);
90
+ let y = b.state(1);
91
+ let one = b.constant(1.0);
92
+ let ac = b.constant(a);
93
+ let bc = b.constant(bcoef);
94
+ let xx = b.mul(x, x);
95
+ let axx = b.mul(ac, xx);
96
+ let omaxx = b.sub(one, axx);
97
+ let nx = b.add(omaxx, y);
98
+ let ny = b.mul(bc, x);
99
+ b.finish(&[nx, ny], &[], 2, 0).unwrap()
100
+ }
101
+
102
+ #[test]
103
+ fn bridge_interp_equals_jit_bit_for_bit() {
104
+ // The bridge drives both evaluators over the same lowered tape; the
105
+ // iteration is deterministic and eval/eval_jac are bit-identical between
106
+ // interpreter and JIT, so the exponents agree to the last bit.
107
+ let run = |jit| {
108
+ map_lyapunov_bridge(henon_jac(1.4, 0.3), &[], &[0.1, 0.1], 5000, 2, 1, jit)
109
+ .unwrap()
110
+ .exponents
111
+ };
112
+ let interp = run(false);
113
+ let jit = run(true);
114
+ assert_eq!(interp.len(), jit.len());
115
+ for (a, b) in interp.iter().zip(jit.iter()) {
116
+ assert_eq!(a.to_bits(), b.to_bits(), "interp {a} != jit {b}");
117
+ }
118
+ }
119
+
120
+ #[test]
121
+ fn bridge_reproduces_henon_literature() {
122
+ let out = map_lyapunov_bridge(henon_jac(1.4, 0.3), &[], &[0.1, 0.1], 10_000, 2, 1, false)
123
+ .unwrap();
124
+ assert!(
125
+ (out.exponents[0] - 0.419).abs() < 0.05,
126
+ "{:?}",
127
+ out.exponents
128
+ );
129
+ assert!(
130
+ (out.exponents[1] - (-1.623)).abs() < 0.05,
131
+ "{:?}",
132
+ out.exponents
133
+ );
134
+ }
135
+
136
+ #[test]
137
+ fn bridge_rejects_jacobianless_tape() {
138
+ let err = map_lyapunov_bridge(henon_no_jac(1.4, 0.3), &[], &[0.1, 0.1], 100, 2, 1, false)
139
+ .unwrap_err();
140
+ assert!(matches!(err, EngineError::BadShape(_)), "got {err:?}");
141
+ }
142
+ }
@@ -33,7 +33,9 @@
33
33
  pub mod basin;
34
34
  pub mod dde;
35
35
  pub mod events;
36
+ pub mod lyapunov;
36
37
  pub mod map;
38
+ pub mod map_lyapunov;
37
39
  pub mod marshal;
38
40
  pub mod ode;
39
41
  pub mod sde;
@@ -46,7 +48,9 @@ pub mod stepper;
46
48
  pub use basin::{basin_march_flow_bridge, basin_march_map_bridge};
47
49
  pub use dde::integrate_dde_dense;
48
50
  pub use events::integrate_events_dense;
49
- pub use map::{iterate_map, map_ensemble_final};
51
+ pub use lyapunov::lyapunov_spectrum_ode_bridge;
52
+ pub use map::{iterate_map, map_ensemble_final, map_param_sweep};
53
+ pub use map_lyapunov::map_lyapunov_bridge;
50
54
  #[cfg(test)]
51
55
  pub use marshal::build_tape;
52
56
  pub use marshal::EngineError;
@@ -281,6 +281,79 @@ fn iterate_ensemble_final<'py>(
281
281
  PyArray1::from_vec(py, flat).reshape([n_ic, dim])
282
282
  }
283
283
 
284
+ /// Run a whole map orbit-diagram parameter sweep in one call (stream
285
+ /// `perf/param-sweep-kernel`), returning the recorded asymptotic states and each
286
+ /// value's fate.
287
+ ///
288
+ /// The leading tape arrays describe the lowered map with the **swept** parameter
289
+ /// kept as its single runtime `Param` input (`n_param == 1`) — the other
290
+ /// parameters fold into constants — so the sweep needs no per-value re-lowering or
291
+ /// per-value FFI round-trip. `base_params` is the full runtime parameter vector;
292
+ /// `sweep_index` which of its entries each `values[k]` overwrites; `ic` the base
293
+ /// initial condition; `components` the state-component indices to record;
294
+ /// `transient` / `n_record` the discard / record counts; `carry_state` whether
295
+ /// each value resumes from the previous value's final state; `jit` selects the
296
+ /// Cranelift evaluator (numerically identical to the interpreter).
297
+ ///
298
+ /// Returns `(points, status)`: `points` is the `(n_values * n_record,
299
+ /// n_components)` recorded array, and `status` an `(n_values,)` i64 array of `0`
300
+ /// (finite) / `1` (diverged — the value's block is zero, dropped to an empty set
301
+ /// by the Python wiring). The per-iterate numerics are byte-for-byte the
302
+ /// [`iterate_map`] path.
303
+ #[pyfunction]
304
+ #[allow(clippy::too_many_arguments, clippy::type_complexity)]
305
+ fn map_param_sweep<'py>(
306
+ py: Python<'py>,
307
+ ops: PyReadonlyArray1<i32>,
308
+ a: PyReadonlyArray1<i32>,
309
+ b: PyReadonlyArray1<i32>,
310
+ imm: PyReadonlyArray1<f64>,
311
+ outputs: PyReadonlyArray1<i32>,
312
+ jac_outputs: PyReadonlyArray1<i32>,
313
+ n_state: usize,
314
+ n_param: usize,
315
+ base_params: PyReadonlyArray1<f64>,
316
+ sweep_index: usize,
317
+ values: PyReadonlyArray1<f64>,
318
+ ic: PyReadonlyArray1<f64>,
319
+ components: PyReadonlyArray1<i64>,
320
+ transient: usize,
321
+ n_record: usize,
322
+ carry_state: bool,
323
+ jit: bool,
324
+ ) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray1<i64>>)> {
325
+ let tape = OwnedTape::copy_in(&ops, &a, &b, &imm, &outputs, &jac_outputs, n_state, n_param)?;
326
+ let base_params = vec_f64("base_params", &base_params)?;
327
+ let values = vec_f64("values", &values)?;
328
+ let ic = vec_f64("ic", &ic)?;
329
+ let components: Vec<usize> = components
330
+ .as_slice()
331
+ .map_err(|_| PyValueError::new_err("components must be a contiguous int64 array"))?
332
+ .iter()
333
+ .map(|&c| c as usize)
334
+ .collect();
335
+ let n_values = values.len();
336
+ let n_components = components.len();
337
+ let (points, status) = py
338
+ .detach(|| {
339
+ bridge::map_param_sweep(
340
+ tape.build()?,
341
+ &base_params,
342
+ sweep_index,
343
+ &values,
344
+ &ic,
345
+ &components,
346
+ transient,
347
+ n_record,
348
+ carry_state,
349
+ jit,
350
+ )
351
+ })
352
+ .map_err(to_py_err)?;
353
+ let points = PyArray1::from_vec(py, points).reshape([n_values * n_record, n_components])?;
354
+ Ok((points, PyArray1::from_vec(py, status)))
355
+ }
356
+
284
357
  // ---------------------------------------------------------------------------
285
358
  // Integration
286
359
  // ---------------------------------------------------------------------------
@@ -961,6 +1034,138 @@ fn basin_march_map<'py>(
961
1034
  Ok(basin_outcome_to_py(py, out))
962
1035
  }
963
1036
 
1037
+ // ---------------------------------------------------------------------------
1038
+ // Discrete-map Lyapunov spectrum (stream perf/map-lyapunov-kernel)
1039
+ // ---------------------------------------------------------------------------
1040
+
1041
+ use bridge::map_lyapunov_bridge;
1042
+
1043
+ /// Compute a discrete-map Lyapunov spectrum on the engine — the whole QR
1044
+ /// tangent-map iteration (iterate `f`, propagate `k` deviation vectors through
1045
+ /// `J(x_n)` at the pre-image, reorthonormalise, accumulate `log|R_ii|`) in one call.
1046
+ ///
1047
+ /// The leading tape arrays are the usual `Tape.to_arrays()` tuple for the **map**
1048
+ /// step, which **must carry its Jacobian** (`with_jacobian=True`; rejected up front
1049
+ /// otherwise). `ic` is the `(dim,)` start state; `steps` the iterate budget; `k` the
1050
+ /// number of exponents (`1 ≤ k ≤ dim`); `reortho_interval` the QR cadence. The map
1051
+ /// tape folds its parameters in, so there is no parameter vector. `jit` selects the
1052
+ /// Cranelift evaluator (numerically identical to the interpreter).
1053
+ ///
1054
+ /// Returns `(exponents, intervals)`: the `k` time-averaged exponents (largest
1055
+ /// first) and the number of completed reorthonormalisation intervals. A divergence
1056
+ /// before the budget is exhausted raises `RuntimeError` (the "diverge loudly"
1057
+ /// contract); a malformed call / Jacobian-less tape raises `ValueError`.
1058
+ #[pyfunction]
1059
+ #[allow(clippy::too_many_arguments)]
1060
+ fn map_lyapunov_spectrum<'py>(
1061
+ py: Python<'py>,
1062
+ ops: PyReadonlyArray1<i32>,
1063
+ a: PyReadonlyArray1<i32>,
1064
+ b: PyReadonlyArray1<i32>,
1065
+ imm: PyReadonlyArray1<f64>,
1066
+ outputs: PyReadonlyArray1<i32>,
1067
+ jac_outputs: PyReadonlyArray1<i32>,
1068
+ n_state: usize,
1069
+ n_param: usize,
1070
+ ic: PyReadonlyArray1<f64>,
1071
+ steps: usize,
1072
+ k: usize,
1073
+ reortho_interval: usize,
1074
+ jit: bool,
1075
+ ) -> PyResult<(Bound<'py, PyArray1<f64>>, usize)> {
1076
+ let tape = OwnedTape::copy_in(&ops, &a, &b, &imm, &outputs, &jac_outputs, n_state, n_param)?;
1077
+ let ic = vec_f64("ic", &ic)?;
1078
+ let out = py
1079
+ .detach(|| map_lyapunov_bridge(tape.build()?, &[], &ic, steps, k, reortho_interval, jit))
1080
+ .map_err(to_py_err)?;
1081
+ Ok((PyArray1::from_vec(py, out.exponents), out.intervals))
1082
+ }
1083
+
1084
+ // ODE Lyapunov spectrum (stream perf/ode-lyapunov-engine)
1085
+ // ---------------------------------------------------------------------------
1086
+
1087
+ use bridge::lyapunov_spectrum_ode_bridge;
1088
+
1089
+ /// Run the whole burn-in + averaging Benettin Lyapunov-spectrum loop for an ODE
1090
+ /// flow in one engine call.
1091
+ ///
1092
+ /// The leading tape arrays are the usual `Tape.to_arrays()` tuple — here the
1093
+ /// **extended** variational tape (`dim*(k+1)` inputs/outputs: the base RHS stacked
1094
+ /// with the `k` tangent-vector RHS blocks, built by
1095
+ /// `tsdynamics.derived._variational.build_variational_tape`). `p` is the live
1096
+ /// control-parameter vector; `method` resolves through the solver registry
1097
+ /// (an implicit kernel needs a Jacobian-carrying tape — the extended tape carries
1098
+ /// one); `dim` the **base** system dimension; `k` the number of tangent vectors;
1099
+ /// `z0` the extended initial state (`embed_extended`: base IC ⊕ the seed tangent
1100
+ /// frame, column-major); `t0` the start time; `dt` the renormalisation interval;
1101
+ /// `burn_in` / `final_time` the two window lengths; `jit` selects the Cranelift
1102
+ /// evaluator (numerically identical to the interpreter).
1103
+ ///
1104
+ /// Each `dt` chunk is integrated by a *fresh* two-node `integrate_grid([t, t+dt])`
1105
+ /// (byte-for-byte the released `TangentSystem._step_ode_engine` numerics, so
1106
+ /// `interp == jit`), then the `(dim, k)` tangent block is QR-reorthonormalised
1107
+ /// (modified Gram–Schmidt) and `Σ log|diag R|` accumulated over the averaging
1108
+ /// window. Returns `(spectrum, final_state, last_growths)` — the `k` exponents,
1109
+ /// the final extended state, and the last per-step log-stretch contributions. A
1110
+ /// divergence raises (it must not become a plausible-looking spectrum).
1111
+ #[pyfunction]
1112
+ #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1113
+ fn lyapunov_spectrum_ode<'py>(
1114
+ py: Python<'py>,
1115
+ ops: PyReadonlyArray1<i32>,
1116
+ a: PyReadonlyArray1<i32>,
1117
+ b: PyReadonlyArray1<i32>,
1118
+ imm: PyReadonlyArray1<f64>,
1119
+ outputs: PyReadonlyArray1<i32>,
1120
+ jac_outputs: PyReadonlyArray1<i32>,
1121
+ n_state: usize,
1122
+ n_param: usize,
1123
+ p: PyReadonlyArray1<f64>,
1124
+ method: String,
1125
+ rtol: f64,
1126
+ atol: f64,
1127
+ dim: usize,
1128
+ k: usize,
1129
+ z0: PyReadonlyArray1<f64>,
1130
+ t0: f64,
1131
+ dt: f64,
1132
+ burn_in: f64,
1133
+ final_time: f64,
1134
+ jit: bool,
1135
+ ) -> PyResult<(
1136
+ Bound<'py, PyArray1<f64>>,
1137
+ Bound<'py, PyArray1<f64>>,
1138
+ Bound<'py, PyArray1<f64>>,
1139
+ )> {
1140
+ let tape = OwnedTape::copy_in(&ops, &a, &b, &imm, &outputs, &jac_outputs, n_state, n_param)?;
1141
+ let p = vec_f64("p", &p)?;
1142
+ let z0 = vec_f64("z0", &z0)?;
1143
+ let out = py
1144
+ .detach(|| {
1145
+ lyapunov_spectrum_ode_bridge(
1146
+ tape.build()?,
1147
+ &p,
1148
+ &method,
1149
+ rtol,
1150
+ atol,
1151
+ dim,
1152
+ k,
1153
+ &z0,
1154
+ t0,
1155
+ dt,
1156
+ burn_in,
1157
+ final_time,
1158
+ jit,
1159
+ )
1160
+ })
1161
+ .map_err(to_py_err)?;
1162
+ Ok((
1163
+ PyArray1::from_vec(py, out.spectrum),
1164
+ PyArray1::from_vec(py, out.final_state),
1165
+ PyArray1::from_vec(py, out.last_growths),
1166
+ ))
1167
+ }
1168
+
964
1169
  // ---------------------------------------------------------------------------
965
1170
  // Resumable ODE stepper handle (stream WS-STEPPER)
966
1171
  // ---------------------------------------------------------------------------
@@ -1171,6 +1376,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
1171
1376
  m.add_function(wrap_pyfunction!(eval_jac, m)?)?;
1172
1377
  m.add_function(wrap_pyfunction!(iterate_map, m)?)?;
1173
1378
  m.add_function(wrap_pyfunction!(iterate_ensemble_final, m)?)?;
1379
+ m.add_function(wrap_pyfunction!(map_param_sweep, m)?)?;
1174
1380
  m.add_function(wrap_pyfunction!(integrate_dense, m)?)?;
1175
1381
  m.add_function(wrap_pyfunction!(integrate_dde_dense, m)?)?;
1176
1382
  m.add_function(wrap_pyfunction!(integrate_ensemble_final, m)?)?;
@@ -1179,6 +1385,8 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
1179
1385
  m.add_function(wrap_pyfunction!(integrate_events_dense, m)?)?;
1180
1386
  m.add_function(wrap_pyfunction!(basin_march_flow, m)?)?;
1181
1387
  m.add_function(wrap_pyfunction!(basin_march_map, m)?)?;
1388
+ m.add_function(wrap_pyfunction!(map_lyapunov_spectrum, m)?)?;
1389
+ m.add_function(wrap_pyfunction!(lyapunov_spectrum_ode, m)?)?;
1182
1390
  m.add_class::<PyOdeStepper>()?;
1183
1391
  m.add_function(wrap_pyfunction!(solvers, m)?)?;
1184
1392
  m.add_function(wrap_pyfunction!(_version, m)?)?;