tsdynamics 5.2.0__tar.gz → 5.2.2__tar.gz

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