StochasticForceInference 2.0.3__tar.gz → 2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/AGENTS.md +12 -1
  2. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/PKG-INFO +11 -3
  3. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/README.md +9 -2
  4. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/pairs.py +60 -13
  5. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/__init__.py +8 -1
  6. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/assess.py +9 -1
  7. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/dynamics_order.py +66 -70
  8. stochasticforceinference-2.0.4/SFI/diagnostics/finite_dt.py +183 -0
  9. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/plotting.py +22 -9
  10. stochasticforceinference-2.0.4/SFI/diagnostics/psd.py +93 -0
  11. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/report.py +82 -7
  12. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/residual_tests.py +57 -18
  13. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/diagnostics/residuals.py +320 -4
  14. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/base.py +712 -70
  15. stochasticforceinference-2.0.4/SFI/inference/graph_diffusion.py +509 -0
  16. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/overdamped.py +427 -129
  17. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/driver.py +26 -4
  18. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/flow_multi.py +143 -33
  19. stochasticforceinference-2.0.4/SFI/inference/parametric_core/frame_extras.py +112 -0
  20. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/precompute.py +58 -26
  21. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/runner.py +365 -81
  22. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/solve.py +103 -22
  23. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/result.py +18 -5
  24. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/result.py +28 -9
  25. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/underdamped.py +546 -172
  26. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/integrate/__init__.py +11 -1
  27. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/integrate/api.py +391 -43
  28. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/integrate/integrand.py +213 -18
  29. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/integrate/timeops.py +23 -0
  30. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/base.py +115 -8
  31. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/chunked.py +2 -3
  32. stochasticforceinference-2.0.4/SFI/langevin/graph_noise.py +498 -0
  33. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/noise.py +9 -0
  34. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/overdamped.py +43 -23
  35. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/underdamped.py +63 -39
  36. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/factory.py +19 -0
  37. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/base.py +16 -0
  38. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/interactions/__init__.py +2 -0
  39. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/interactions/dispatcher.py +351 -337
  40. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/interactions/specs.py +93 -3
  41. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/leaf.py +56 -3
  42. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/derivative.py +41 -12
  43. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/slice.py +42 -3
  44. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/params.py +25 -9
  45. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/stateexpr.py +27 -3
  46. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/dataset.py +95 -55
  47. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/degrade.py +201 -64
  48. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/reserved_extras.py +29 -5
  49. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/utils/formatting.py +32 -4
  50. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/utils/neighbors.py +11 -3
  51. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/utils/plotting.py +245 -51
  52. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/StochasticForceInference.egg-info/PKG-INFO +11 -3
  53. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/StochasticForceInference.egg-info/SOURCES.txt +7 -0
  54. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/StochasticForceInference.egg-info/requires.txt +1 -0
  55. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/abp.py +7 -3
  56. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/coarse_grain.py +26 -16
  57. stochasticforceinference-2.0.4/examples/_gallery_utils/janus_colloids.py +475 -0
  58. stochasticforceinference-2.0.4/examples/experimental_data/generate_tweezer_csv.py +53 -0
  59. stochasticforceinference-2.0.4/examples/gallery/abp_align_demo.py +459 -0
  60. stochasticforceinference-2.0.4/examples/gallery/abp_nonreciprocal_demo.py +605 -0
  61. stochasticforceinference-2.0.4/examples/gallery/abp_to_spde_demo.py +831 -0
  62. stochasticforceinference-2.0.4/examples/gallery/advanced/flocking_3d_demo.py +537 -0
  63. stochasticforceinference-2.0.4/examples/gallery/advanced/multi_experiment_demo.py +389 -0
  64. stochasticforceinference-2.0.4/examples/gallery/advanced/nn_force_demo.py +405 -0
  65. stochasticforceinference-2.0.4/examples/gallery/anisotropic_diffusion_demo.py +380 -0
  66. stochasticforceinference-2.0.4/examples/gallery/custom_basis_demo.py +329 -0
  67. stochasticforceinference-2.0.4/examples/gallery/diagnostics_demo.py +303 -0
  68. stochasticforceinference-2.0.4/examples/gallery/dynamics_order_demo.py +291 -0
  69. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/gallery/entropy_production_demo.py +153 -135
  70. stochasticforceinference-2.0.4/examples/gallery/entropy_production_underdamped_demo.py +479 -0
  71. stochasticforceinference-2.0.4/examples/gallery/experimental_workflow_demo.py +415 -0
  72. stochasticforceinference-2.0.4/examples/gallery/gray_scott_demo.py +715 -0
  73. stochasticforceinference-2.0.4/examples/gallery/home_range_demo.py +377 -0
  74. stochasticforceinference-2.0.4/examples/gallery/janus_colloids_demo.py +457 -0
  75. stochasticforceinference-2.0.4/examples/gallery/limitcycle_demo.py +328 -0
  76. stochasticforceinference-2.0.4/examples/gallery/lorenz_demo.py +370 -0
  77. stochasticforceinference-2.0.4/examples/gallery/lotka_volterra_demo.py +595 -0
  78. stochasticforceinference-2.0.4/examples/gallery/multiplicative_diffusion_demo.py +359 -0
  79. stochasticforceinference-2.0.4/examples/gallery/ou_demo.py +431 -0
  80. stochasticforceinference-2.0.4/examples/gallery/time_dependent_forcing_demo.py +269 -0
  81. stochasticforceinference-2.0.4/examples/gallery/time_fourier_demo.py +341 -0
  82. stochasticforceinference-2.0.4/examples/gallery/van_der_pol_demo.py +576 -0
  83. stochasticforceinference-2.0.4/examples/gallery/velocity_dependent_noise_demo.py +373 -0
  84. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/pyproject.toml +2 -2
  85. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/tests/test_parametric_mask_abp.py +1 -0
  86. stochasticforceinference-2.0.3/examples/experimental_data/generate_tweezer_csv.py +0 -35
  87. stochasticforceinference-2.0.3/examples/gallery/abp_align_demo.py +0 -574
  88. stochasticforceinference-2.0.3/examples/gallery/abp_nonreciprocal_demo.py +0 -529
  89. stochasticforceinference-2.0.3/examples/gallery/abp_to_spde_demo.py +0 -602
  90. stochasticforceinference-2.0.3/examples/gallery/advanced/flocking_3d_demo.py +0 -314
  91. stochasticforceinference-2.0.3/examples/gallery/advanced/multi_experiment_demo.py +0 -347
  92. stochasticforceinference-2.0.3/examples/gallery/advanced/nn_force_demo.py +0 -521
  93. stochasticforceinference-2.0.3/examples/gallery/anisotropic_diffusion_demo.py +0 -234
  94. stochasticforceinference-2.0.3/examples/gallery/custom_basis_demo.py +0 -254
  95. stochasticforceinference-2.0.3/examples/gallery/diagnostics_demo.py +0 -150
  96. stochasticforceinference-2.0.3/examples/gallery/dynamics_order_demo.py +0 -271
  97. stochasticforceinference-2.0.3/examples/gallery/entropy_production_underdamped_demo.py +0 -464
  98. stochasticforceinference-2.0.3/examples/gallery/experimental_workflow_demo.py +0 -216
  99. stochasticforceinference-2.0.3/examples/gallery/gray_scott_demo.py +0 -574
  100. stochasticforceinference-2.0.3/examples/gallery/home_range_demo.py +0 -394
  101. stochasticforceinference-2.0.3/examples/gallery/limitcycle_demo.py +0 -213
  102. stochasticforceinference-2.0.3/examples/gallery/lorenz_demo.py +0 -219
  103. stochasticforceinference-2.0.3/examples/gallery/lotka_volterra_demo.py +0 -319
  104. stochasticforceinference-2.0.3/examples/gallery/multiplicative_diffusion_demo.py +0 -240
  105. stochasticforceinference-2.0.3/examples/gallery/ou_demo.py +0 -413
  106. stochasticforceinference-2.0.3/examples/gallery/time_dependent_forcing_demo.py +0 -156
  107. stochasticforceinference-2.0.3/examples/gallery/time_fourier_demo.py +0 -234
  108. stochasticforceinference-2.0.3/examples/gallery/van_der_pol_demo.py +0 -245
  109. stochasticforceinference-2.0.3/examples/gallery/velocity_dependent_noise_demo.py +0 -202
  110. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/CONTRIBUTING.md +0 -0
  111. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/GALLERY_STYLE_GUIDE.md +0 -0
  112. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/LICENSE +0 -0
  113. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/MANIFEST.in +0 -0
  114. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/__init__.py +0 -0
  115. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/__init__.py +0 -0
  116. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/constants.py +0 -0
  117. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/linear.py +0 -0
  118. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/monomials.py +0 -0
  119. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/bases/spde.py +0 -0
  120. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/__init__.py +0 -0
  121. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/optimizers.py +0 -0
  122. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/__init__.py +0 -0
  123. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/banded.py +0 -0
  124. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/covariance.py +0 -0
  125. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/flow.py +0 -0
  126. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/flow_ud.py +0 -0
  127. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/gram.py +0 -0
  128. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/parametric_core/jacobians.py +0 -0
  129. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/serialization.py +0 -0
  130. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/__init__.py +0 -0
  131. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/base.py +0 -0
  132. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/beam.py +0 -0
  133. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/greedy.py +0 -0
  134. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/hillclimb.py +0 -0
  135. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/lasso.py +0 -0
  136. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/metrics.py +0 -0
  137. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/scorer.py +0 -0
  138. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparse/stlsq.py +0 -0
  139. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/inference/sparsity.py +0 -0
  140. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/integrate/rk4.py +0 -0
  141. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/langevin/__init__.py +0 -0
  142. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/__init__.py +0 -0
  143. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/basis.py +0 -0
  144. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/core/runtime.py +0 -0
  145. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/interactor.py +0 -0
  146. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/__init__.py +0 -0
  147. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/_base.py +0 -0
  148. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/_eval_compiler.py +0 -0
  149. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/_fd_atoms.py +0 -0
  150. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/_grid.py +0 -0
  151. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/layout/_sectors.py +0 -0
  152. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/memhint.py +0 -0
  153. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/__init__.py +0 -0
  154. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/contract.py +0 -0
  155. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/interactions/prepare.py +0 -0
  156. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/interactions/stencils.py +0 -0
  157. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/__init__.py +0 -0
  158. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/concat.py +0 -0
  159. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/einsum.py +0 -0
  160. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/linear.py +0 -0
  161. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/mapn.py +0 -0
  162. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/nodes/ops/reshape_rank.py +0 -0
  163. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/psf.py +0 -0
  164. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/sf.py +0 -0
  165. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/statefunc/structexpr.py +0 -0
  166. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/__init__.py +0 -0
  167. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/collection.py +0 -0
  168. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/trajectory/io.py +0 -0
  169. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/utils/__init__.py +0 -0
  170. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/SFI/utils/maths.py +0 -0
  171. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/StochasticForceInference.egg-info/dependency_links.txt +0 -0
  172. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/StochasticForceInference.egg-info/top_level.txt +0 -0
  173. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/__init__.py +0 -0
  174. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/bench_utils.py +0 -0
  175. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/helpers.py +0 -0
  176. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/_gallery_utils/scenario_picker.py +0 -0
  177. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/experimental_data/_analyze_results.py +0 -0
  178. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/experimental_data/_expanded_basis.py +0 -0
  179. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_align_demo.py +0 -0
  180. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_largescale_config.py +0 -0
  181. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_largescale_demo.py +0 -0
  182. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_largescale_infer.py +0 -0
  183. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_largescale_plot.py +0 -0
  184. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_largescale_simulate.py +0 -0
  185. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/examples/manual_examples/abp_pursuit_demo.py +0 -0
  186. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/setup.cfg +0 -0
  187. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/tests/test_dual_mask.py +0 -0
  188. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/tests/test_parametric_mask.py +0 -0
  189. {stochasticforceinference-2.0.3 → stochasticforceinference-2.0.4}/tests/test_same_particle_jacobian.py +0 -0
@@ -45,6 +45,7 @@ SFI has mature, well-tested abstractions for every common operation.
45
45
  | Wrap a user function as a state function | `SFI.statefunc.make_basis` / `make_sf` / `make_psf` |
46
46
  | Run force / diffusion inference | `SFI.OverdampedLangevinInference` / `UnderdampedLangevinInference` |
47
47
  | Select a sparse model | `inf.sparsify_force(criterion="PASTIS", p=0.1)` |
48
+ | Select a sparse diffusion model | `inf.sparsify_diffusion(criterion="PASTIS", p=0.05)` |
48
49
  | Compare to ground truth | `inf.compare_to_exact(model_exact=...)` |
49
50
  | Plot in the SFI style | `SFI.utils.plotting.SFI_COLORS`, `dark_fig`, `phase2d` |
50
51
  | Save / load a fitted model | `SFI.inference.save_model` / `load_model` |
@@ -78,6 +79,16 @@ from SFI import (
78
79
  The dynamics-order classifier (`classify_dynamics`, returning a
79
80
  `DynamicsOrderReport`) decides overdamped vs underdamped from raw
80
81
  positions; see `SFI.diagnostics` and `examples/gallery/dynamics_order_demo.py`.
82
+ `SFI.diagnostics.finite_dt_scale(inf)` reports the per-step drift-gradient
83
+ scale Δt‖∇F̂‖ of a fitted force (the size of the linear estimators' O(Δt)
84
+ biases) with a `fine` / `coarse` / `unreliable` verdict; `assess(inf)` includes
85
+ it and flags the coarse regimes. Both linear engines also record the force
86
+ Gram condition number (`metadata["force_gram_cond"]`) and warn when the
87
+ dictionary is ill-conditioned on the data.
88
+ `SFI.diagnostics.psd_fraction(D_sf, coll)` reports the fraction of data
89
+ points where a diffusion field is not positive semi-definite;
90
+ `simulate_bootstrapped_trajectory` runs it on the fitted diffusion and warns
91
+ (the simulator clips negative eigenvalues silently).
81
92
 
82
93
  ### 4.2 `SFI.statefunc` — composable state functions
83
94
 
@@ -89,7 +100,7 @@ positions; see `SFI.diagnostics` and `examples/gallery/dynamics_order_demo.py`.
89
100
  | `make_sf(func, dim, rank, ...)` | Wrap a Python callable as an `SF` |
90
101
  | `make_psf(func, dim, rank, ...)` | Wrap a callable with parameters as `PSF` |
91
102
  | `make_basis(func, dim, ...)` | Wrap a vector-valued callable as `Basis` |
92
- | `Interactor`, `make_interactor` | Multi-scale interaction graphs |
103
+ | `Interactor`, `make_interactor` | Multi-scale interaction graphs (pairs, hyperedges; `per_slot=True` for many-body forces) |
93
104
 
94
105
  ### 4.3 `SFI.inference` — inference engines
95
106
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: StochasticForceInference
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Stochastic Force Inference for Langevin SDEs
5
5
  Author-email: Pierre Ronceray <pierre.ronceray@univ-amu.fr>
6
6
  License: MIT
@@ -42,6 +42,7 @@ Requires-Dist: sphinxcontrib-video; extra == "docs"
42
42
  Requires-Dist: sphinxcontrib-mermaid; extra == "docs"
43
43
  Provides-Extra: dev
44
44
  Requires-Dist: pytest; extra == "dev"
45
+ Requires-Dist: pytest-xdist; extra == "dev"
45
46
  Requires-Dist: ruff; extra == "dev"
46
47
  Requires-Dist: pyright; extra == "dev"
47
48
  Requires-Dist: StochasticForceInference[docs]; extra == "dev"
@@ -57,6 +58,7 @@ Dynamic: license-file
57
58
  [![Documentation](https://readthedocs.org/projects/stochasticforceinference/badge/?version=latest)](https://stochasticforceinference.readthedocs.io/en/latest/)
58
59
  [![PyPI](https://img.shields.io/pypi/v/StochasticForceInference.svg)](https://pypi.org/project/StochasticForceInference/)
59
60
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ronceray/StochasticForceInference/blob/main/LICENSE)
61
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21532842.svg)](https://doi.org/10.5281/zenodo.21532842)
60
62
 
61
63
  **Infer force and diffusion fields from stochastic trajectory data.**
62
64
 
@@ -197,8 +199,14 @@ guidance.
197
199
  ## Citation
198
200
 
199
201
  If you use SFI, please cite the software (see [`CITATION.cff`](CITATION.cff))
200
- together with the method paper(s) relevant to the features you use. See
201
- **[How to cite](https://stochasticforceinference.readthedocs.io/en/latest/#how-to-cite)** in the
202
+ together with the method paper(s) relevant to the features you use.
203
+
204
+ The software is archived on Zenodo. Cite the concept DOI
205
+ [10.5281/zenodo.21532842](https://doi.org/10.5281/zenodo.21532842), which always
206
+ resolves to the latest release; each version also has its own DOI (v2.0.3:
207
+ [10.5281/zenodo.21532843](https://doi.org/10.5281/zenodo.21532843)).
208
+
209
+ See **[How to cite](https://stochasticforceinference.readthedocs.io/en/latest/#how-to-cite)** in the
202
210
  documentation for the full, feature-specific list (overdamped, underdamped/ULI,
203
211
  trapeze, and PASTIS references).
204
212
 
@@ -3,6 +3,7 @@
3
3
  [![Documentation](https://readthedocs.org/projects/stochasticforceinference/badge/?version=latest)](https://stochasticforceinference.readthedocs.io/en/latest/)
4
4
  [![PyPI](https://img.shields.io/pypi/v/StochasticForceInference.svg)](https://pypi.org/project/StochasticForceInference/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ronceray/StochasticForceInference/blob/main/LICENSE)
6
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21532842.svg)](https://doi.org/10.5281/zenodo.21532842)
6
7
 
7
8
  **Infer force and diffusion fields from stochastic trajectory data.**
8
9
 
@@ -143,8 +144,14 @@ guidance.
143
144
  ## Citation
144
145
 
145
146
  If you use SFI, please cite the software (see [`CITATION.cff`](CITATION.cff))
146
- together with the method paper(s) relevant to the features you use. See
147
- **[How to cite](https://stochasticforceinference.readthedocs.io/en/latest/#how-to-cite)** in the
147
+ together with the method paper(s) relevant to the features you use.
148
+
149
+ The software is archived on Zenodo. Cite the concept DOI
150
+ [10.5281/zenodo.21532842](https://doi.org/10.5281/zenodo.21532842), which always
151
+ resolves to the latest release; each version also has its own DOI (v2.0.3:
152
+ [10.5281/zenodo.21532843](https://doi.org/10.5281/zenodo.21532843)).
153
+
154
+ See **[How to cite](https://stochasticforceinference.readthedocs.io/en/latest/#how-to-cite)** in the
148
155
  documentation for the full, feature-specific list (overdamped, underdamped/ULI,
149
156
  trapeze, and PASTIS references).
150
157
 
@@ -29,8 +29,8 @@ from ..statefunc import Basis, Interactor, make_basis, make_interactor
29
29
  # ═══════════════════════════════════════════════════════════════════════
30
30
 
31
31
 
32
- def pbc_displacement(xj, xi, box):
33
- """Minimum-image displacement ``xj - xi`` under periodic boundaries.
32
+ def pbc_displacement(xj, xi, box, ref=None):
33
+ r"""Minimum-image displacement ``xj - xi`` under periodic boundaries.
34
34
 
35
35
  Works in any dimension. All inputs are plain JAX arrays of shape
36
36
  ``(d,)`` (or broadcastable).
@@ -41,14 +41,49 @@ def pbc_displacement(xj, xi, box):
41
41
  Positions (or sub-positions) of two particles.
42
42
  box : array, shape ``(d,)``
43
43
  Box lengths along each axis.
44
+ ref : tuple ``(ref_j, ref_i)`` of arrays, optional
45
+ Reference positions of the same two particles (same axes as
46
+ ``xj``, ``xi``). The periodic image is the minimum image of
47
+ ``ref_j - ref_i`` instead of ``xj - xi``. Re-picking the image at
48
+ every frame makes direction-carrying features such as
49
+ :math:`\phi(r)\,\hat r_{ij}` jump when a pair crosses the cut
50
+ ``|Δx| = L/2``, and a program combining a feature at several
51
+ frames (Stratonovich trapezoid, shifted Gram, ULI) integrates the
52
+ jumps into an :math:`O(1)` bias. The integrator therefore hands
53
+ such programs one reference frame per row (a frame preceding every
54
+ increment of the row, see
55
+ :meth:`~SFI.integrate.Integrand.pbc_reference_in`) as the reserved
56
+ per-particle extra ``extras["pbc_ref"]``. Hand-written pair
57
+ interactors declare ``particle_extras=("pbc_ref",)``, receive it
58
+ with shape ``(K, d)`` and pass ``ref=(pr[1], pr[0])`` (sliced to
59
+ the spatial axes) when present.
44
60
 
45
61
  Returns
46
62
  -------
47
63
  dx : array, shape ``(d,)``
48
- ``xj - xi`` folded via minimum-image convention.
64
+ ``xj - xi`` folded via minimum-image convention (of ``ref``
65
+ when given).
49
66
  """
50
67
  dx = xj - xi
51
- return dx - box * jnp.round(dx / box)
68
+ d_img = dx if ref is None else ref[0] - ref[1]
69
+ return dx - box * jnp.round(d_img / box)
70
+
71
+
72
+ # Reserved per-particle extra: reference positions fixing the periodic image
73
+ # of every pair (see :func:`pbc_displacement`); SFI.integrate.integrand.PBC_REF_KEY.
74
+ _PBC_REF_KEY = "pbc_ref"
75
+
76
+
77
+ def _pbc_ref(extras):
78
+ """The ``pbc_ref`` reference positions ``(2, d)`` of a pair, or None."""
79
+ if not extras:
80
+ return None
81
+ return extras.get(_PBC_REF_KEY)
82
+
83
+
84
+ def _pbc_particle_extras(box):
85
+ """Periodic pair primitives gather the optional image reference per pair."""
86
+ return (_PBC_REF_KEY,) if box is not None else ()
52
87
 
53
88
 
54
89
  def wrap_angle(a):
@@ -56,7 +91,7 @@ def wrap_angle(a):
56
91
  return (a + jnp.pi) % (2.0 * jnp.pi) - jnp.pi
57
92
 
58
93
 
59
- def _pairwise_dr(XK, *, box=None, spatial_dims=None, eps=1e-12):
94
+ def _pairwise_dr(XK, *, box=None, spatial_dims=None, eps=1e-12, ref=None):
60
95
  """Compute displacement, distance and unit vector for a K=2 pair.
61
96
 
62
97
  Parameters
@@ -70,6 +105,9 @@ def _pairwise_dr(XK, *, box=None, spatial_dims=None, eps=1e-12):
70
105
  Default: all.
71
106
  eps : float
72
107
  Regularisation to avoid division by zero.
108
+ ref : array, shape ``(2, dim)``, optional
109
+ Reference states of the pair fixing the periodic image (see
110
+ :func:`pbc_displacement`).
73
111
 
74
112
  Returns
75
113
  -------
@@ -83,7 +121,9 @@ def _pairwise_dr(XK, *, box=None, spatial_dims=None, eps=1e-12):
83
121
  else:
84
122
  xi_s, xj_s = xi, xj
85
123
  if box is not None:
86
- dx = pbc_displacement(xj_s, xi_s, box)
124
+ if ref is not None:
125
+ ref = (ref[1], ref[0]) if spatial_dims is None else (ref[1][spatial_dims], ref[0][spatial_dims])
126
+ dx = pbc_displacement(xj_s, xi_s, box, ref=ref)
87
127
  else:
88
128
  dx = xj_s - xi_s
89
129
  r = jnp.sqrt(jnp.sum(dx**2) + eps)
@@ -303,7 +343,7 @@ def radial_pair_basis(
303
343
  b = extras["box"]
304
344
  if spatial_dims is not None:
305
345
  b = b[spatial_dims] if b.ndim > 0 else b
306
- dx, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
346
+ dx, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
307
347
 
308
348
  # Evaluate kernels → (n_feat,)
309
349
  vals = jnp.stack([fn(r) for fn in fns]) # (F,)
@@ -331,6 +371,7 @@ def radial_pair_basis(
331
371
  K=2,
332
372
  n_features=n_feat,
333
373
  extras_keys=extras_keys,
374
+ particle_extras=_pbc_particle_extras(box),
334
375
  labels=list(labels),
335
376
  descriptor="radial-pair-basis",
336
377
  )
@@ -377,7 +418,7 @@ def scalar_pair_basis(
377
418
  b = extras["box"]
378
419
  if spatial_dims is not None:
379
420
  b = b[spatial_dims] if b.ndim > 0 else b
380
- _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
421
+ _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
381
422
  return jnp.stack([fn(r) for fn in fns]) # (F,)
382
423
 
383
424
  extras_keys = ("box",) if _use_extras_box else ()
@@ -388,6 +429,7 @@ def scalar_pair_basis(
388
429
  K=2,
389
430
  n_features=n_feat,
390
431
  extras_keys=extras_keys,
432
+ particle_extras=_pbc_particle_extras(box),
391
433
  labels=list(labels),
392
434
  descriptor="scalar-pair-basis",
393
435
  )
@@ -460,7 +502,7 @@ def angular_pair_basis(
460
502
  b = extras["box"]
461
503
  if spatial_dims is not None:
462
504
  b = b[spatial_dims] if b.ndim > 0 else b
463
- _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
505
+ _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
464
506
  # angle coupling
465
507
  dth = wrap_angle(xj[angle_index] - xi[angle_index])
466
508
  g = coupling_fn(dth)
@@ -478,6 +520,7 @@ def angular_pair_basis(
478
520
  K=2,
479
521
  n_features=n_feat,
480
522
  extras_keys=extras_keys,
523
+ particle_extras=_pbc_particle_extras(box),
481
524
  labels=list(labels),
482
525
  descriptor="angular-pair-basis",
483
526
  )
@@ -575,7 +618,7 @@ def dyadic_pair_basis(
575
618
  b = extras["box"]
576
619
  if spatial_dims is not None:
577
620
  b = b[spatial_dims] if b.ndim > 0 else b
578
- _, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
621
+ _, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
579
622
  vals = jnp.stack([fn(r) for fn in fns]) # (F,)
580
623
  # rhat ⊗ rhat: (d, d) then weight by each kernel → (d, d, F)
581
624
  rr = jnp.outer(rhat, rhat) # (d, d)
@@ -589,6 +632,7 @@ def dyadic_pair_basis(
589
632
  K=2,
590
633
  n_features=n_feat,
591
634
  extras_keys=extras_keys,
635
+ particle_extras=_pbc_particle_extras(box),
592
636
  labels=list(labels),
593
637
  descriptor="dyadic-pair-basis",
594
638
  )
@@ -656,7 +700,7 @@ def pair_direction(
656
700
  b = extras["box"]
657
701
  if spatial_dims is not None:
658
702
  b = b[spatial_dims] if b.ndim > 0 else b
659
- _, _, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
703
+ _, _, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
660
704
  if embed_dim is not None:
661
705
  full = jnp.zeros(out_d, dtype=XK.dtype)
662
706
  for k, ax in enumerate(embed_axes):
@@ -672,6 +716,7 @@ def pair_direction(
672
716
  K=2,
673
717
  n_features=1,
674
718
  extras_keys=extras_keys,
719
+ particle_extras=_pbc_particle_extras(box),
675
720
  labels=("r̂_ij",),
676
721
  descriptor="pair-direction",
677
722
  )
@@ -828,7 +873,7 @@ def vision_gate(
828
873
  b = extras["box"]
829
874
  if spatial_dims is not None:
830
875
  b = b[spatial_dims] if b.ndim > 0 else b
831
- dx, _, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
876
+ dx, _, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
832
877
  if dx.shape[0] != 2:
833
878
  raise ValueError(
834
879
  f"vision_gate requires exactly 2 spatial dimensions, got {dx.shape[0]}. "
@@ -848,6 +893,7 @@ def vision_gate(
848
893
  K=2,
849
894
  n_features=1,
850
895
  extras_keys=extras_keys,
896
+ particle_extras=_pbc_particle_extras(box),
851
897
  labels=("vision",),
852
898
  descriptor="vision-gate",
853
899
  )
@@ -895,7 +941,7 @@ def parametric_radial_kernel(
895
941
  b = extras["box"]
896
942
  if spatial_dims is not None:
897
943
  b = b[spatial_dims] if b.ndim > 0 else b
898
- _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
944
+ _, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims, ref=_pbc_ref(extras))
899
945
  return kernel_fn(r, params)[None] # (1,) — rank-0, 1 feature
900
946
 
901
947
  extras_keys = ("box",) if _use_extras_box else ()
@@ -907,6 +953,7 @@ def parametric_radial_kernel(
907
953
  n_features=1,
908
954
  params=params,
909
955
  extras_keys=extras_keys,
956
+ particle_extras=_pbc_particle_extras(box),
910
957
  labels=("k(r)",),
911
958
  descriptor="parametric-radial-kernel",
912
959
  )
@@ -15,7 +15,10 @@ like an independent ``N(0, 1)`` sample:
15
15
  * Ljung--Box autocorrelation of the residuals and their squares;
16
16
  * Kolmogorov--Smirnov normality against ``N(0, 1)``;
17
17
  * predicted-vs-realised normalised mean squared error of the force
18
- (a sampling-noise-aware chi-square check).
18
+ (a sampling-noise-aware chi-square check);
19
+ * the finite sampling-interval scale ``dt*|grad F|`` of the fitted force
20
+ (:func:`finite_dt_scale`), the size of the O(dt) biases of the linear
21
+ estimators.
19
22
 
20
23
  See :mod:`SFI.diagnostics.residual_tests`. Two preset levels are exposed:
21
24
 
@@ -36,6 +39,8 @@ Ljung & Box (1978); Diebold, Gunther & Tay (1998).
36
39
 
37
40
  from .assess import assess
38
41
  from .dynamics_order import DynamicsOrderReport, classify_dynamics
42
+ from .finite_dt import finite_dt_scale
43
+ from .psd import psd_fraction
39
44
  from .plotting import (
40
45
  plot_dynamics_order,
41
46
  plot_qq,
@@ -51,10 +56,12 @@ __all__ = [
51
56
  "classify_dynamics",
52
57
  "DiagnosticsReport",
53
58
  "DynamicsOrderReport",
59
+ "finite_dt_scale",
54
60
  "parametric_four_point_diagnostic",
55
61
  "plot_dynamics_order",
56
62
  "plot_qq",
57
63
  "plot_residual_acf",
58
64
  "plot_residual_histogram",
59
65
  "plot_summary",
66
+ "psd_fraction",
60
67
  ]
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  from typing import Optional
9
9
 
10
10
  from .report import DiagnosticsReport
11
+ from .finite_dt import finite_dt_scale
11
12
  from .residual_tests import (
12
13
  autocorrelation_tests,
13
14
  mse_consistency,
@@ -74,6 +75,13 @@ def assess(
74
75
  residuals["normality"] = normality_test(bundle)
75
76
  residuals["mse_consistency"] = mse_consistency(inferer, bundle)
76
77
 
78
+ finite_dt: dict = {}
79
+ if level == "standard":
80
+ try:
81
+ finite_dt = finite_dt_scale(inferer)
82
+ except Exception as exc: # never let the guard break the residual report
83
+ finite_dt = {"available": False, "reason": f"{type(exc).__name__}: {exc}", "verdict": "unavailable"}
84
+
77
85
  meta = {
78
86
  "backend": bundle.backend,
79
87
  "regime": bundle.regime,
@@ -84,4 +92,4 @@ def assess(
84
92
  "inferer_class": type(inferer).__name__,
85
93
  }
86
94
 
87
- return DiagnosticsReport(residuals=residuals, meta=meta)
95
+ return DiagnosticsReport(residuals=residuals, meta=meta, finite_dt=finite_dt)
@@ -29,71 +29,70 @@ cross-check (:func:`_cross_check`) into an OD / UD / inconclusive verdict.
29
29
  from __future__ import annotations
30
30
 
31
31
  import json
32
+ import warnings
32
33
  from dataclasses import dataclass, field
33
34
  from typing import Optional
34
35
 
35
- import jax.numpy as jnp
36
36
  import numpy as np
37
37
  from scipy.optimize import least_squares
38
38
 
39
39
  from SFI.diagnostics.report import _to_jsonable
40
- from SFI.integrate import Integrand, Term, TimeOperand, integrate, timeop
41
40
 
42
41
  # --------------------------------------------------------------------------- #
43
- # Lag-0/1/2 displacement-covariance time-operators.
42
+ # Lag-0/1/2 displacement covariances.
44
43
  #
45
- # Each returns a per-particle (..., N, d, d) tensor, mirroring the diffusion /
46
- # measurement-noise estimators in SFI.inference.overdamped (``_Lambda`` etc.).
47
- # The dataset stream layer supplies the increments:
48
- # dX = X[t+1] - X[t] (offset 0..+1)
49
- # dX_minus = X[t] - X[t-1] (offset -1..0)
50
- # dX_plus = X[t+2] - X[t+1] (offset +1..+2)
51
- # so <dX . dX_minus> is the lag-1 covariance and <dX_minus . dX_plus> is the
52
- # lag-2 covariance (displacements one step apart).
44
+ # Plain moment sums over the increments dX[t] = X[t+1] - X[t], with the same
45
+ # row semantics as ``integrate(..., reduce="mean")`` on the dataset streams
46
+ # (dX_minus = dX[t-1], dX_plus = dX[t+1]): a row (t, i) counts when the
47
+ # dynamic mask holds at t and the static mask at every other frame its
48
+ # increments touch; rows are weighted by dt and the dataset weight. Computed
49
+ # in NumPy: each coarse-graining stride has its own length, and a JAX path
50
+ # recompiles for every one of them.
53
51
  # --------------------------------------------------------------------------- #
54
52
 
55
-
56
- @timeop(name="dyn_cov_lag0", batch_safe=True)
57
- def _cov_lag0(**streams):
58
- """Lag-0 displacement covariance ``dX (x) dX``."""
59
- dX = streams["dX"]
60
- return _outer(dX, dX)
61
-
62
-
63
- @timeop(name="dyn_cov_lag1", batch_safe=True)
64
- def _cov_lag1(**streams):
65
- """Symmetrised lag-1 covariance ``1/2 (dX (x) dX^- + dX^- (x) dX)``."""
66
- dX = streams["dX"]
67
- dXm = streams["dX_minus"]
68
- return 0.5 * (_outer(dX, dXm) + _outer(dXm, dX))
69
-
70
-
71
- @timeop(name="dyn_cov_lag2", batch_safe=True)
72
- def _cov_lag2(**streams):
73
- """Symmetrised lag-2 covariance ``1/2 (dX^- (x) dX^+ + dX^+ (x) dX^-)``."""
74
- dXm = streams["dX_minus"]
75
- dXp = streams["dX_plus"]
76
- return 0.5 * (_outer(dXm, dXp) + _outer(dXp, dXm))
77
-
78
-
79
- _cov_lag0._requires = frozenset({"dX"}) # type: ignore[attr-defined]
80
- _cov_lag1._requires = frozenset({"dX", "dX_minus"}) # type: ignore[attr-defined]
81
- _cov_lag2._requires = frozenset({"dX_minus", "dX_plus"}) # type: ignore[attr-defined]
82
-
83
-
84
- def _outer(a, b):
85
- """Outer product over the trailing spatial axis: ``a_m b_n``."""
86
- return jnp.einsum("...m,...n->...mn", a, b)
87
-
88
-
89
- # --------------------------------------------------------------------------- #
90
- # Covariance backbone
91
- # --------------------------------------------------------------------------- #
92
- def _integrate_tensor(coll, op, alias):
93
- """Time- and particle-averaged ``(d, d)`` value of a covariance time-op."""
94
- operand = TimeOperand(op, alias=alias)
95
- prog = Integrand(times=[operand], terms=[Term(eq="imn->imn", ops=(alias,))])
96
- return np.asarray(integrate(coll, prog, reduce="mean"))
53
+ # rows t in [first, T - tail), increments (dX[t+a], dX[t+b]), static-mask frames t+off
54
+ _LAGS = {
55
+ "C0": (0, 1, (0, 0), (1,)),
56
+ "C1": (1, 1, (0, -1), (-1, 1)),
57
+ "C2": (1, 2, (-1, 1), (-1, 1, 2)),
58
+ }
59
+
60
+
61
+ def _dataset_steps(ds, T):
62
+ """Per-interval steps ``(T-1,)`` of a dataset (``t[k+1] - t[k]``)."""
63
+ if ds.t is not None:
64
+ return np.diff(np.asarray(ds.t, dtype=float))
65
+ if ds.dt is None:
66
+ raise ValueError("Both 't' and 'dt' are None; one must be provided.")
67
+ dt = np.asarray(ds.dt, dtype=float)
68
+ return np.broadcast_to(dt, (T - 1,)) if dt.ndim == 0 else dt[: T - 1]
69
+
70
+
71
+ def _lag_covariances(coll):
72
+ """Time- and particle-averaged ``(d, d)`` lag-0/1/2 covariances of ``coll``."""
73
+ acc = dict.fromkeys(_LAGS, 0.0)
74
+ norm = dict.fromkeys(_LAGS, 0.0)
75
+ for ds, w in zip(coll.datasets, np.asarray(coll.weights, dtype=float)):
76
+ X = np.asarray(ds._X3d(), dtype=float) # (T, N, d)
77
+ M = np.asarray(ds._M2d()) # static mask (T, N)
78
+ Mdyn = np.asarray(ds._dynamic_M2d())
79
+ T = X.shape[0]
80
+ steps = _dataset_steps(ds, T)
81
+ dX = np.diff(X, axis=0)
82
+ for key, (first, tail, (a, b), frames) in _LAGS.items():
83
+ t = np.arange(first, T - tail)
84
+ if t.size == 0:
85
+ continue
86
+ m = Mdyn[t].copy()
87
+ for off in frames:
88
+ m &= M[t + off]
89
+ u, v = dX[t + a], dX[t + b]
90
+ # NaN-safe masking: masked frames may hold NaN placeholders.
91
+ uv = np.einsum("kim,kin->kimn", u, v)
92
+ uv = np.where(m[..., None, None], 0.5 * (uv + np.swapaxes(uv, -1, -2)), 0.0)
93
+ acc[key] = acc[key] + w * np.einsum("k,kimn->mn", steps[t], uv)
94
+ norm[key] = norm[key] + w * float(np.sum(steps[t] * m.sum(axis=1)))
95
+ return {k: acc[k] / norm[k] if norm[k] > 0 else np.asarray(0.0) for k in _LAGS}
97
96
 
98
97
 
99
98
  def _mean_dt(coll):
@@ -112,13 +111,9 @@ def _mean_dt(coll):
112
111
  return float(np.mean(dts))
113
112
 
114
113
 
115
- def _n_eff(coll, require):
116
- """Number of valid (masked) increment samples available for ``require``."""
117
- n = 0
118
- for ds in coll.datasets:
119
- idx = ds.valid_indices(frozenset(require))
120
- n += int(np.asarray(idx).size) * int(ds.N)
121
- return n
114
+ def _n_eff(coll):
115
+ """Number of lag-2 increment pairs (rows ``t = 1 .. T-3``, all particles)."""
116
+ return sum(max(int(ds.T) - 3, 0) * int(ds.N) for ds in coll.datasets)
122
117
 
123
118
 
124
119
  def _increment_covariances(coll):
@@ -135,15 +130,13 @@ def _increment_covariances(coll):
135
130
  ``C0``, ``C1``, ``C2`` : ``(d, d)`` displacement covariance tensors;
136
131
  ``dt`` : representative scalar step; ``n_eff`` : valid sample count.
137
132
  """
138
- C0 = _integrate_tensor(coll, _cov_lag0, "c0")
139
- C1 = _integrate_tensor(coll, _cov_lag1, "c1")
140
- C2 = _integrate_tensor(coll, _cov_lag2, "c2")
133
+ cov = _lag_covariances(coll)
141
134
  return {
142
- "C0": C0,
143
- "C1": C1,
144
- "C2": C2,
135
+ "C0": cov["C0"],
136
+ "C1": cov["C1"],
137
+ "C2": cov["C2"],
145
138
  "dt": _mean_dt(coll),
146
- "n_eff": _n_eff(coll, {"dX_minus", "dX_plus"}),
139
+ "n_eff": _n_eff(coll),
147
140
  }
148
141
 
149
142
 
@@ -432,7 +425,6 @@ def _cross_check(data, *, force_order: int = 3):
432
425
  quick fit fails for any reason.
433
426
  """
434
427
  try:
435
- from SFI.bases.constants import unit_vector_basis
436
428
  from SFI.bases.monomials import monomials_up_to
437
429
  from SFI.diagnostics.assess import assess
438
430
  from SFI.inference.overdamped import OverdampedLangevinInference
@@ -441,8 +433,12 @@ def _cross_check(data, *, force_order: int = 3):
441
433
  inf = OverdampedLangevinInference(data)
442
434
  inf.compute_diffusion_constant(method="auto")
443
435
  basis = monomials_up_to(order=force_order, dim=d, include_constant=True, include_x=True, include_v=False)
444
- inf.infer_force_linear(basis * unit_vector_basis(d))
445
- inf.compute_force_error()
436
+ with warnings.catch_warnings():
437
+ # A corroboration fit on a deliberately misfit model: its Gram
438
+ # conditioning is not the user's concern.
439
+ warnings.simplefilter("ignore", UserWarning)
440
+ inf.infer_force_linear(basis.vectorize(d))
441
+ inf.compute_force_error()
446
442
  report = assess(inf, level="standard")
447
443
  lb = report.residuals.get("autocorr", {}).get("ljung_box", {})
448
444
  return {"ljung_box_pvalue": lb.get("pvalue"), "ljung_box_stat": lb.get("statistic")}