StochasticForceInference 2.0.0.post1__tar.gz → 2.0.1__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 (165) hide show
  1. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/AGENTS.md +29 -12
  2. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/PKG-INFO +1 -1
  3. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/base.py +8 -1
  4. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/overdamped.py +317 -54
  5. stochasticforceinference-2.0.1/SFI/inference/parametric_core/__init__.py +32 -0
  6. stochasticforceinference-2.0.1/SFI/inference/parametric_core/banded.py +279 -0
  7. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/covariance.py +105 -14
  8. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/driver.py +55 -5
  9. stochasticforceinference-2.0.1/SFI/inference/parametric_core/flow_multi.py +494 -0
  10. stochasticforceinference-2.0.1/SFI/inference/parametric_core/gram.py +21 -0
  11. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/jacobians.py +191 -49
  12. stochasticforceinference-2.0.1/SFI/inference/parametric_core/precompute.py +221 -0
  13. stochasticforceinference-2.0.1/SFI/inference/parametric_core/runner.py +853 -0
  14. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/solve.py +421 -142
  15. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/underdamped.py +510 -26
  16. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/integrate/rk4.py +1 -1
  17. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/underdamped.py +121 -20
  18. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/derivative.py +11 -0
  19. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/reshape_rank.py +5 -0
  20. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/slice.py +5 -0
  21. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/dataset.py +14 -10
  22. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/StochasticForceInference.egg-info/PKG-INFO +1 -1
  23. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/StochasticForceInference.egg-info/SOURCES.txt +7 -5
  24. stochasticforceinference-2.0.1/examples/_gallery_utils/bench_utils.py +27 -0
  25. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/_gallery_utils/helpers.py +11 -0
  26. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/abp_align_demo.py +3 -3
  27. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/abp_nonreciprocal_demo.py +11 -6
  28. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/abp_to_spde_demo.py +3 -3
  29. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/advanced/flocking_3d_demo.py +4 -4
  30. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/advanced/multi_experiment_demo.py +2 -2
  31. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/advanced/nn_force_demo.py +161 -70
  32. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/anisotropic_diffusion_demo.py +2 -3
  33. stochasticforceinference-2.0.1/examples/gallery/entropy_production_demo.py +561 -0
  34. stochasticforceinference-2.0.1/examples/gallery/entropy_production_underdamped_demo.py +464 -0
  35. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/experimental_workflow_demo.py +2 -2
  36. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/gray_scott_demo.py +2 -2
  37. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/home_range_demo.py +2 -2
  38. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/lorenz_demo.py +0 -1
  39. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/lotka_volterra_demo.py +7 -7
  40. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/multiplicative_diffusion_demo.py +2 -4
  41. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/velocity_dependent_noise_demo.py +2 -3
  42. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/pyproject.toml +1 -1
  43. stochasticforceinference-2.0.0.post1/SFI/inference/parametric_core/__init__.py +0 -34
  44. stochasticforceinference-2.0.0.post1/SFI/inference/parametric_core/flow_multi.py +0 -362
  45. stochasticforceinference-2.0.0.post1/SFI/inference/parametric_core/objective.py +0 -286
  46. stochasticforceinference-2.0.0.post1/SFI/inference/parametric_core/objective_ud.py +0 -253
  47. stochasticforceinference-2.0.0.post1/SFI/inference/parametric_core/precision.py +0 -229
  48. stochasticforceinference-2.0.0.post1/examples/convert_active_nematic_mat.py +0 -409
  49. stochasticforceinference-2.0.0.post1/examples/gallery/active_nematic_demo.py +0 -910
  50. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/CONTRIBUTING.md +0 -0
  51. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/GALLERY_STYLE_GUIDE.md +0 -0
  52. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/LICENSE +0 -0
  53. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/MANIFEST.in +0 -0
  54. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/README.md +0 -0
  55. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/__init__.py +0 -0
  56. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/__init__.py +0 -0
  57. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/constants.py +0 -0
  58. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/linear.py +0 -0
  59. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/monomials.py +0 -0
  60. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/pairs.py +0 -0
  61. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/bases/spde.py +0 -0
  62. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/__init__.py +0 -0
  63. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/assess.py +0 -0
  64. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/dynamics_order.py +0 -0
  65. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/plotting.py +0 -0
  66. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/report.py +0 -0
  67. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/residual_tests.py +0 -0
  68. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/diagnostics/residuals.py +0 -0
  69. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/__init__.py +0 -0
  70. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/optimizers.py +0 -0
  71. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/flow.py +0 -0
  72. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/parametric_core/flow_ud.py +0 -0
  73. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/result.py +0 -0
  74. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/serialization.py +0 -0
  75. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/__init__.py +0 -0
  76. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/base.py +0 -0
  77. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/beam.py +0 -0
  78. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/greedy.py +0 -0
  79. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/hillclimb.py +0 -0
  80. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/lasso.py +0 -0
  81. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/metrics.py +0 -0
  82. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/result.py +0 -0
  83. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/scorer.py +0 -0
  84. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparse/stlsq.py +0 -0
  85. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/inference/sparsity.py +0 -0
  86. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/integrate/__init__.py +0 -0
  87. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/integrate/api.py +0 -0
  88. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/integrate/integrand.py +0 -0
  89. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/integrate/timeops.py +0 -0
  90. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/__init__.py +0 -0
  91. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/base.py +0 -0
  92. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/chunked.py +0 -0
  93. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/noise.py +0 -0
  94. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/langevin/overdamped.py +0 -0
  95. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/__init__.py +0 -0
  96. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/basis.py +0 -0
  97. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/core/runtime.py +0 -0
  98. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/factory.py +0 -0
  99. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/interactor.py +0 -0
  100. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/__init__.py +0 -0
  101. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/_base.py +0 -0
  102. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/_eval_compiler.py +0 -0
  103. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/_fd_atoms.py +0 -0
  104. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/_grid.py +0 -0
  105. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/layout/_sectors.py +0 -0
  106. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/memhint.py +0 -0
  107. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/__init__.py +0 -0
  108. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/base.py +0 -0
  109. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/contract.py +0 -0
  110. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/interactions/__init__.py +0 -0
  111. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/interactions/dispatcher.py +0 -0
  112. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/interactions/prepare.py +0 -0
  113. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/interactions/specs.py +0 -0
  114. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/interactions/stencils.py +0 -0
  115. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/leaf.py +0 -0
  116. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/__init__.py +0 -0
  117. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/concat.py +0 -0
  118. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/einsum.py +0 -0
  119. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/linear.py +0 -0
  120. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/nodes/ops/mapn.py +0 -0
  121. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/params.py +0 -0
  122. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/psf.py +0 -0
  123. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/sf.py +0 -0
  124. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/stateexpr.py +0 -0
  125. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/statefunc/structexpr.py +0 -0
  126. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/__init__.py +0 -0
  127. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/collection.py +0 -0
  128. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/degrade.py +0 -0
  129. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/io.py +0 -0
  130. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/trajectory/reserved_extras.py +0 -0
  131. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/utils/__init__.py +0 -0
  132. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/utils/formatting.py +0 -0
  133. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/utils/maths.py +0 -0
  134. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/utils/neighbors.py +0 -0
  135. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/SFI/utils/plotting.py +0 -0
  136. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/StochasticForceInference.egg-info/dependency_links.txt +0 -0
  137. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/StochasticForceInference.egg-info/requires.txt +0 -0
  138. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/StochasticForceInference.egg-info/top_level.txt +0 -0
  139. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/_gallery_utils/__init__.py +0 -0
  140. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/_gallery_utils/abp.py +0 -0
  141. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/_gallery_utils/coarse_grain.py +0 -0
  142. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/_gallery_utils/scenario_picker.py +0 -0
  143. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/experimental_data/_analyze_results.py +0 -0
  144. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/experimental_data/_expanded_basis.py +0 -0
  145. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/experimental_data/generate_tweezer_csv.py +0 -0
  146. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/custom_basis_demo.py +0 -0
  147. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/diagnostics_demo.py +0 -0
  148. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/dynamics_order_demo.py +0 -0
  149. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/limitcycle_demo.py +0 -0
  150. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/ou_demo.py +0 -0
  151. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/time_dependent_forcing_demo.py +0 -0
  152. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/time_fourier_demo.py +0 -0
  153. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/gallery/van_der_pol_demo.py +0 -0
  154. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_align_demo.py +0 -0
  155. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_largescale_config.py +0 -0
  156. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_largescale_demo.py +0 -0
  157. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_largescale_infer.py +0 -0
  158. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_largescale_plot.py +0 -0
  159. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_largescale_simulate.py +0 -0
  160. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/examples/manual_examples/abp_pursuit_demo.py +0 -0
  161. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/setup.cfg +0 -0
  162. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/tests/test_dual_mask.py +0 -0
  163. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/tests/test_parametric_mask.py +0 -0
  164. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/tests/test_parametric_mask_abp.py +0 -0
  165. {stochasticforceinference-2.0.0.post1 → stochasticforceinference-2.0.1}/tests/test_same_particle_jacobian.py +0 -0
@@ -110,7 +110,8 @@ compute for robustness; route by data regime, not by habit:
110
110
  | Quick exploration / huge dataset | linear | closed form, no initial guess |
111
111
  | Measurement (localization) noise | parametric | profiles Λ; EIV instrument removes the noise bias |
112
112
  | Coarse sampling (large Δt) | parametric | RK4 flow step replaces the Euler secant |
113
- | Model nonlinear in θ (NN drift, gated PSF) | parametric | the L-BFGS path is the only option |
113
+ | Non-uniform sampling (irregular `t` / per-step `dt`) | parametric | per-interval flow steps + process covariances, no resampling |
114
+ | Model nonlinear in θ (NN drift, gated PSF) | parametric | the damped-GN / L-BFGS paths are the only option |
114
115
  | Multi-particle / interacting | either | linear for speed; parametric when noise/coarse Δt too |
115
116
  | SPDE / grid fields | linear | the (experimental) SPDE toolbox is linear-only |
116
117
 
@@ -133,30 +134,42 @@ agreement is itself a diagnostic.
133
134
  | `inf.print_report()` / `inf.report_dict()` | Human / machine summary |
134
135
  | `inf.compare_to_exact(model_exact=...)` | Validate against a known model |
135
136
  | `inf.holdout_score(test)` | Held-out NMSE on `coll.split_time(...)` test data (data-abundant scenarios only) |
137
+ | `inf.compute_entropy_production()` | Dissipation of the observed dynamics (after a linear force fit; both engines): rate ± error + fluctuation bias and the AIC-debiased rate (`Sdot_debiased`), in k_B. Underdamped: positions-only via the even/odd split; `coefficients=` enables cross-fitting |
138
+ | `inf.time_reversal_split()` | Underdamped only: the fitted field's reversible/irreversible parts `(F_even, F_odd)` as callables |
136
139
 
137
140
  * **Parametric estimators (robust, flexible; compute-intensive).**
138
141
  Robust to measurement noise
139
142
  and finite Δt. Methods `infer_force` and `infer_diffusion`
140
143
  (implemented in `SFI.inference.parametric_core`). Takes a `Basis`
141
144
  (linear-in-θ; PASTIS sparsification wired) or a `PSF` (general
142
- parametric family): a single RK4 flow step per observation interval
143
- defines the residual, the banded residual covariance gives a
144
- windowed-precision NLL, and `(D, Λ)` are profiled natively
145
- (moment-estimator init + one conditional-NLL refinement). Two inner
145
+ parametric family): an RK4 flow step per observation interval
146
+ defines the residual, the block-banded residual covariance is
147
+ whitened *exactly* (reverse-time banded-innovations NLL no window
148
+ truncation, no tuning knobs), and `(D, Λ)` are profiled natively
149
+ (moment-estimator init + an exact-NLL refinement on cached fixed-θ
150
+ tensors). Non-uniform sampling (a `t` vector or per-step `dt`
151
+ array) fits directly — force and diffusion, both engines. Two inner
146
152
  solvers, selected by `inner="auto"`:
147
153
 
148
- * `inner="gn"` — direct Gauss–Newton on the windowed Gram (the
154
+ * `inner="gn"` — direct Gauss–Newton on the whitened Gram (the
149
155
  linear-in-θ fast path) with the skip-trick errors-in-variables
150
156
  instrument (`eiv=True`, consistent under measurement noise).
157
+ Nonlinear-in-θ PSFs run the same loop as damped Gauss–Newton,
158
+ instrument active.
151
159
  * `inner="lbfgs"` — frozen-precision L-BFGS for non-linear-in-θ
152
- families (neural-net drift, gated PSFs, …); raise `inner_maxiter`
153
- for large models. Worked example:
160
+ overdamped families (neural-net drift, gated PSFs, …); raise
161
+ `inner_maxiter` for large models (underdamped redirects to the
162
+ damped-GN path). Worked example:
154
163
  [`examples/gallery/advanced/nn_force_demo.py`](examples/gallery/advanced/nn_force_demo.py).
155
164
 
165
+ Caveat: for (near-)clean data at coarse effective sampling pass
166
+ `eiv=False, n_substeps=2` — `eiv="auto"` does not inspect the noise
167
+ level, and the `(D, Λ)` profile can fit a spurious Λ there.
168
+
156
169
  Multi-particle / interacting systems are supported in both overdamped
157
170
  and underdamped regimes (the multi-particle path uses the per-edge
158
171
  `d_x(same_particle=True)` / `d_v(same_particle=True)` Jacobian
159
- protocol; O(N) per window).
172
+ protocol; O(N) per residual).
160
173
 
161
174
  See
162
175
  [`docs/source/inference/user_guide.rst`](docs/source/inference/user_guide.rst)
@@ -171,8 +184,9 @@ inf.infer_force_linear(basis, preset="auto") # 2. force ("auto"/"robust"
171
184
  inf.infer_diffusion_linear() # 3. optional: constant sym D (default basis)
172
185
  inf.compute_force_error() # 4. error analysis
173
186
  inf.sparsify_force(criterion="PASTIS", p=0.1) # 5. optional: sparsity
174
- inf.print_report() # 6. summary
175
- inf.compare_to_exact(model_exact=process) # 7. optional: validate
187
+ inf.compute_entropy_production() # 6. optional: dissipation (OD & UD)
188
+ inf.print_report() # 7. summary
189
+ inf.compare_to_exact(model_exact=process) # 8. optional: validate
176
190
  ```
177
191
 
178
192
  **Parametric-estimator sequence:**
@@ -336,7 +350,10 @@ Use `coll.T` (frames) and `coll.d` (dimension) instead of
336
350
  - `*.bak` files, if you ever see one, are stale snapshots — ignore them.
337
351
 
338
352
  Deprecated / soon-to-be-removed symbols (do not use in new code):
339
- - (none currently)
353
+ - `infer_force(..., extra_radius=)` / `infer_force(..., n_cond=)` —
354
+ deprecated tuning knobs, accepted but **inert** (a
355
+ `DeprecationWarning` fires when set). The banded likelihood has no
356
+ window truncation to tune.
340
357
 
341
358
  ## 6. Testing, building, environment
342
359
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: StochasticForceInference
3
- Version: 2.0.0.post1
3
+ Version: 2.0.1
4
4
  Summary: Stochastic Force Inference for Langevin SDEs
5
5
  Author-email: Pierre Ronceray <pierre.ronceray@univ-amu.fr>
6
6
  License: MIT
@@ -526,10 +526,15 @@ class BaseLangevinInference(ABC):
526
526
  # Entropy production
527
527
  if hasattr(self, "DeltaS"):
528
528
  print(
529
- "Entropy production: inferred/bootstrapped error:",
529
+ "Entropy production (total Delta S, estimated error):",
530
530
  self.DeltaS,
531
531
  self.error_DeltaS,
532
532
  )
533
+ if hasattr(self, "DeltaS_debiased"):
534
+ print(
535
+ "Entropy production, fluctuation-bias subtracted (AIC):",
536
+ self.DeltaS_debiased,
537
+ )
533
538
 
534
539
  # Force inference metrics
535
540
  if hasattr(self, "force_predicted_MSE"):
@@ -649,6 +654,8 @@ class BaseLangevinInference(ABC):
649
654
  if hasattr(self, "DeltaS"):
650
655
  d["DeltaS"] = float(self.DeltaS)
651
656
  d["error_DeltaS"] = float(self.error_DeltaS)
657
+ if hasattr(self, "DeltaS_debiased"):
658
+ d["DeltaS_debiased"] = float(self.DeltaS_debiased)
652
659
 
653
660
  if hasattr(self, "force_coefficients"):
654
661
  d["force_coefficients"] = np.asarray(self.force_coefficients)
@@ -16,7 +16,7 @@ from SFI.integrate.integrand import (
16
16
  )
17
17
  from SFI.integrate.timeops import stream, timeop, velocity
18
18
  from SFI.statefunc import PSF, SF, Basis
19
- from SFI.utils.maths import sqrtm_psd
19
+ from SFI.utils.maths import sqrtm_psd, stable_pinv
20
20
 
21
21
  from .result import InferenceResultSF # fitted SF with param_cov/meta
22
22
  from .sparse import SparseScorer
@@ -403,6 +403,187 @@ class OverdampedLangevinInference(BaseLangevinInference):
403
403
  self.diffusion_scorer = SparseScorer(M=self.diffusion_moments, G=self.diffusion_G_full)
404
404
  self._update_diffusion_coefficients(self.diffusion_scorer.total_C)
405
405
 
406
+ def compute_entropy_production(self, *, support: str = "current") -> dict:
407
+ r"""Estimate the entropy production of the observed dynamics.
408
+
409
+ Projects the phase-space velocity (mean local velocity) of the data
410
+ onto the force basis and contracts it with the inverse average
411
+ diffusion — the estimator of Frishman & Ronceray, Phys. Rev. X 10,
412
+ 021009 (2020), reconnected to the v2 moment engine. Requires a
413
+ prior :meth:`infer_force_linear` fit (any preset); the Stratonovich
414
+ velocity moments are reused from the fit when available and computed
415
+ on demand otherwise.
416
+
417
+ .. physics:: Entropy production estimator (overdamped, linear basis)
418
+ :label: entropy-production-inference-overdamped
419
+ :category: Observable
420
+
421
+ .. math::
422
+
423
+ \Delta\hat S = 2\, M_v^\top G^{-1} M_v,
424
+ \qquad
425
+ M_{v,a} = \sum_t \mathrm{d}X_t \circ A^{-1}\, b_a,
426
+ \qquad A = 2\bar D,
427
+
428
+ .. math::
429
+
430
+ \hat{\dot S} = \frac{\Delta\hat S}{\tau_N},
431
+ \qquad
432
+ \dot S_{\mathrm{bias}} = \frac{2 N_b}{\tau_N},
433
+ \qquad
434
+ \delta \Delta S = \sqrt{2\,\Delta\hat S + (2 N_b)^2}.
435
+
436
+ :math:`\hat v = G^{-1} M_v` is the basis projection of the
437
+ phase-space velocity :math:`v = F - D\nabla\ln\rho`, :math:`N_b`
438
+ the number of basis degrees of freedom, and :math:`\tau_N` the
439
+ total particle-time. The projection makes :math:`\Delta\hat S` a
440
+ *lower bound* on the entropy produced in the observed phase
441
+ space, up to the :math:`+2N_b` fluctuation bias. The bias is
442
+ reported both ways: the raw plug-in value keeps it, and
443
+ ``DeltaS_debiased`` :math:`= \Delta\hat S - 2N_b` subtracts it.
444
+ This is exactly the **AIC complexity correction** applied to the
445
+ trajectory information: :math:`\Delta\hat S` is four times the
446
+ plug-in information carried by the projected velocity field, and
447
+ each fitted degree of freedom inflates a maximized (plug-in)
448
+ information by :math:`+\tfrac12` — the same counting that powers
449
+ the ``AIC`` criterion of ``sparsify_force``.
450
+
451
+ Parameters
452
+ ----------
453
+ support : {"current", "full"}
454
+ ``"current"`` (default) restricts the projection to the active
455
+ basis support — the full basis before sparsification, the
456
+ selected support after :meth:`sparsify_force` (shrinking the
457
+ :math:`2 N_b/\tau_N` bias). ``"full"`` always uses the complete
458
+ basis.
459
+
460
+ Returns
461
+ -------
462
+ dict
463
+ ``{"Sdot", "Sdot_error", "Sdot_bias", "Sdot_debiased", "DeltaS",
464
+ "error_DeltaS", "DeltaS_debiased", "tauN", "Nb"}`` — the
465
+ per-particle entropy production rate, its estimated error and
466
+ fluctuation bias, the bias-subtracted (AIC-corrected) rate, the
467
+ total entropy :math:`\Delta\hat S = \hat{\dot S}\,\tau_N` with
468
+ error and its debiased value, the total particle-time and the
469
+ basis size. All plain floats, in units of :math:`k_B` (nats).
470
+
471
+ Notes
472
+ -----
473
+ - The estimate assumes constant diffusion: the contraction uses the
474
+ average :math:`\bar D` even when a state-dependent field was
475
+ fitted (a warning is emitted in that case). Extension point: a
476
+ trajectory integral of :math:`\hat v \cdot D(x)^{-1} \hat v`.
477
+ - ``Sdot_debiased`` can come out negative near equilibrium — that is
478
+ the expected fluctuation of an unbiased estimate around zero, not
479
+ an inconsistency. Use the raw ``Sdot`` when a conservative
480
+ plug-in value is wanted, the debiased one for point estimates and
481
+ detection decisions.
482
+ - **Independent measurement noise does not bias the estimator at
483
+ leading order.** The Stratonovich velocity moments are *odd*
484
+ under time reversal while i.i.d. (signal-independent) localization
485
+ noise is *even*, so the naive :math:`O(\Lambda/\Delta t)`
486
+ noise–increment correlation cancels exactly — per step it
487
+ telescopes to a boundary term. The surviving effects are second
488
+ order and *not* included in the error bar: an :math:`O(\Lambda)`
489
+ basis-smoothing distortion (the moments see the
490
+ :math:`\Lambda`-blurred basis functions) and a noise-inflated
491
+ variance that raises the effective fluctuation bias. A warning is
492
+ emitted when the estimated noise dominates the per-step diffusive
493
+ displacement (:math:`\mathrm{Tr}\,\Lambda \gtrsim 50\%` of
494
+ :math:`\mathrm{Tr}\,\bar D\,\Delta t`). Noise *correlated with
495
+ the motion* — motion blur, exposure-time averaging — breaks the
496
+ parity cancellation and does bias the estimate: correct or
497
+ deconvolve it first (see :doc:`/inference/noise_and_sampling`).
498
+ - Parametric fits (:meth:`infer_force`) are not supported yet. The
499
+ natural extension is the plug-in Stratonovich sum
500
+ :math:`\sum_t \mathrm{d}X_t \circ \bar D^{-1}\,
501
+ \tfrac12[\hat F(X_t)+\hat F(X_{t+1})]`.
502
+ - With weighted multi-dataset collections the normalization by the
503
+ unweighted :math:`\tau_N` is approximate (same caveat as
504
+ ``force_information``).
505
+ """
506
+ if self.metadata.get("force_method") != "linear":
507
+ raise RuntimeError(
508
+ "compute_entropy_production() requires a prior infer_force_linear() "
509
+ "fit; parametric fits (infer_force) are not supported yet."
510
+ )
511
+ lam = float(getattr(self, "Lambda_trace", 0.0))
512
+ if lam > 0.0:
513
+ # The leading O(Λ/Δt) noise bias cancels by time-reversal parity
514
+ # for signal-independent noise (the Stratonovich moments are odd,
515
+ # i.i.d. noise is even); what survives is second order. Warn only
516
+ # when noise dominates the per-step diffusive displacement.
517
+ dt_val = float(self.data.peek_row(require={"dt"})["dt"])
518
+ diffusive_scale = float(jnp.trace(self.diffusion_average)) * dt_val
519
+ if lam > 0.5 * diffusive_scale:
520
+ logger.warning(
521
+ "Strong measurement noise (Tr Lambda = %.3g, %.0f%% of "
522
+ "Tr(D)*dt). Independent noise does not bias the entropy "
523
+ "estimate at leading order (parity cancellation), but "
524
+ "basis smoothing and noise-inflated variance (second "
525
+ "order) are NOT in the error bar — and noise correlated "
526
+ "with the motion (blur) does not cancel.",
527
+ lam, 100.0 * lam / max(diffusive_scale, 1e-300),
528
+ )
529
+ if self._diffusion_looks_state_dependent():
530
+ logger.warning(
531
+ "A state-dependent diffusion field was inferred; the entropy "
532
+ "estimator contracts with the constant average diffusion and is "
533
+ "only exact for constant D."
534
+ )
535
+
536
+ # Stratonovich v-moments: by-product of a Strato fit, else on demand.
537
+ if not hasattr(self, "force_v_moments"):
538
+ with self._structural_scope(self.force_basis):
539
+ self.force_v_moments = self._strato_v_moments()
540
+
541
+ if support == "current":
542
+ S = self.force_support
543
+ G_pinv = self.force_G_pinv
544
+ elif support == "full":
545
+ S = jnp.arange(self.force_v_moments.shape[0])
546
+ G_pinv = stable_pinv(self.force_G_full)
547
+ else:
548
+ raise ValueError(f"support must be 'current' or 'full', got {support!r}")
549
+
550
+ Mv = self.force_v_moments[S]
551
+ Nb = int(S.shape[0])
552
+ # A carries one factor of A_inv in both M_v and G, so the A-scaling
553
+ # cancels in G^{-1} M_v; the leading 2 restores D̄^{-1} = 2 A^{-1}.
554
+ DeltaS = float(2.0 * (Mv @ (G_pinv @ Mv))) if Nb > 0 else 0.0
555
+ tauN = float(self.data.Teff({"X", "X_plus", "dX", "dt"}))
556
+
557
+ self.DeltaS = DeltaS
558
+ self.error_DeltaS = float((2.0 * DeltaS + (2.0 * Nb) ** 2) ** 0.5)
559
+ self.DeltaS_debiased = DeltaS - 2.0 * Nb
560
+ self.Sdot = DeltaS / tauN
561
+ self.Sdot_bias = 2.0 * Nb / tauN
562
+ self.Sdot_debiased = self.DeltaS_debiased / tauN
563
+ self.Sdot_error = self.error_DeltaS / tauN
564
+ self.metadata["entropy_production"] = {
565
+ "support": support,
566
+ "Nb": Nb,
567
+ "tauN": tauN,
568
+ }
569
+ logger.info(
570
+ "Entropy production: DeltaS = %.4g +- %.4g "
571
+ "(rate %.4g +- %.4g, fluctuation bias %.4g, debiased %.4g)",
572
+ DeltaS, self.error_DeltaS, self.Sdot, self.Sdot_error,
573
+ self.Sdot_bias, self.Sdot_debiased,
574
+ )
575
+ return {
576
+ "Sdot": self.Sdot,
577
+ "Sdot_error": self.Sdot_error,
578
+ "Sdot_bias": self.Sdot_bias,
579
+ "Sdot_debiased": self.Sdot_debiased,
580
+ "DeltaS": self.DeltaS,
581
+ "error_DeltaS": self.error_DeltaS,
582
+ "DeltaS_debiased": self.DeltaS_debiased,
583
+ "tauN": tauN,
584
+ "Nb": Nb,
585
+ }
586
+
406
587
  def simulate_bootstrapped_trajectory(self, key, oversampling=1, simulate=True, dataset=0):
407
588
  """
408
589
  Simulate an overdamped Langevin trajectory with the inferred force and diffusion fields.
@@ -461,7 +642,7 @@ class OverdampedLangevinInference(BaseLangevinInference):
461
642
  return data_bootstrap, bootstrapped_process
462
643
  return bootstrapped_process
463
644
 
464
- # ── Parametric (windowed) force inference ──
645
+ # ── Parametric (exact-core) force inference ──
465
646
 
466
647
  def infer_force(
467
648
  self,
@@ -478,18 +659,23 @@ class OverdampedLangevinInference(BaseLangevinInference):
478
659
  inner_maxiter: int = 80,
479
660
  extra_radius: int = 1,
480
661
  ) -> None:
481
- r"""Infer the force field with the minimal parametric estimator.
482
-
483
- Built on ``SFI.inference.parametric_core``: a single RK4 flow step
484
- per observation interval defines the residual, the residual
485
- covariance gives a windowed-precision NLL, and the parameters are
486
- found by direct Gauss–Newton (linear-in-θ ``Basis``, with the
487
- skip-trick errors-in-variables instrument) or frozen-precision
488
- L-BFGS (nonlinear-in-θ ``PSF``). ``(D, Λ)`` are profiled
489
- natively: moment-estimator init, then one windowed
490
- conditional-NLL refinement at the fitted θ.
491
-
492
- .. physics:: Parametric windowed force inference (overdamped)
662
+ r"""Infer the force field with the exact parametric estimator.
663
+
664
+ Built on ``SFI.inference.parametric_core``: an RK4 flow step per
665
+ observation interval defines the residual, the block-tridiagonal
666
+ residual covariance is whitened *exactly* by a reverse-time
667
+ block-LDLᵀ innovations recursion (no window truncation, no tuning
668
+ knobs), and the parameters are found by direct Gauss–Newton
669
+ (linear-in-θ ``Basis``, with the skip-trick errors-in-variables
670
+ instrument) or L-BFGS (nonlinear-in-θ ``PSF``). ``(D, Λ)`` are
671
+ profiled natively: moment-estimator init, then an exact banded-NLL
672
+ refinement at the fitted θ on cached fixed-θ tensors (zero basis
673
+ evaluations per profile iteration). Non-uniform sampling is
674
+ supported end-to-end: datasets carrying an absolute time vector
675
+ ``t`` or a per-step ``dt`` array fit directly, with per-interval
676
+ flow steps and process covariances.
677
+
678
+ .. physics:: Parametric force inference (overdamped)
493
679
  :label: parametric-force-overdamped
494
680
  :category: Inference
495
681
 
@@ -500,10 +686,11 @@ class OverdampedLangevinInference(BaseLangevinInference):
500
686
  :math:`\Phi(x;\theta) = z(\Delta t) - x`
501
687
  (one RK4 step by default) defines the residual
502
688
  :math:`r_t = y_{t+1} - y_t - \Phi(y_t;\theta)`.
503
- Residuals follow a banded Gaussian whose local precision
504
- weights the Gauss–Newton normal equations; under
505
- measurement noise the left factor is replaced by the
506
- η-clean *skip* instrument
689
+ Residuals follow a block-tridiagonal Gaussian that is
690
+ whitened exactly (reverse-time innovations), and the
691
+ whitened sensitivities enter the Gauss–Newton normal
692
+ equations; under measurement noise the left factor is
693
+ replaced by the η-clean *skip* instrument
507
694
  :math:`\psi_{\rm inst} = \partial\Phi/\partial\theta`
508
695
  evaluated at the lagged clean point (``eiv=True``),
509
696
  giving a consistent estimating equation.
@@ -526,27 +713,36 @@ class OverdampedLangevinInference(BaseLangevinInference):
526
713
  Flow predictor (default ``"rk4"``, a single 4th-order step).
527
714
  n_substeps : int
528
715
  Integrator micro-steps per observation interval (default 1 —
529
- the single-step minimal estimator).
716
+ the single-step minimal estimator). At coarse effective
717
+ sampling (fast dynamics per interval) raise to 2 — the RK4
718
+ flow model saturates at ``n_substeps=2`` on typical chaotic
719
+ benchmarks. With per-step ``dt`` the substep is
720
+ ``h_k = dt_k / n_substeps``.
530
721
  inner : {"auto", "gn", "lbfgs"}
531
- Inner solver. ``"auto"`` → direct Gauss–Newton for a linear
532
- ``Basis``, L-BFGS for a ``PSF``.
722
+ Inner solver. ``"auto"`` → Gauss–Newton (direct for a
723
+ linear ``Basis``, damped for a nonlinear ``PSF`` — the
724
+ instrument stays active); ``"lbfgs"`` forces the
725
+ frozen-precision quadratic route.
533
726
  eiv : {"auto", True, False, float}
534
727
  Measurement-noise errors-in-variables instrument. ``"auto"``
535
728
  (default) → ``True`` for all models (interacting models use
536
729
  the same N-body flow for the instrument as for the residual);
537
730
  ``True`` forces the η-clean skip instrument (consistent under
538
731
  noise); ``False`` is the plain MLE; a float in ``[0, 1]``
539
- blends. Active on the GN path only.
732
+ blends. ``"auto"`` does *not* inspect the noise level: for
733
+ (near-)clean data at coarse effective sampling pass
734
+ ``eiv=False`` — the ``(D, Λ)`` profile cannot separate a
735
+ Δt-independent Λ from the process increment structure there
736
+ and can fit a spurious measurement noise (see
737
+ :ref:`parametric-algorithm`).
540
738
  max_outer : int
541
739
  Outer Gauss–Newton / IRLS iterations (default 5).
542
740
  inner_maxiter : int
543
741
  Inner L-BFGS iterations per outer step on the PSF path
544
742
  (default 80; raise for large nonlinear families, e.g. NNs).
545
743
  extra_radius : int
546
- Precision-window padding beyond the covariance bandwidth
547
- (default 1). Raise to 2–3 in the noise-dominated regime
548
- β = Tr(Λ)/Tr(2DΔt) ≫ 1, where the windowed precision
549
- decays slowly and the default window under-resolves it.
744
+ Deprecated, no effect (a ``DeprecationWarning`` fires when
745
+ a non-default value is passed).
550
746
 
551
747
  Updates
552
748
  -------
@@ -587,7 +783,14 @@ class OverdampedLangevinInference(BaseLangevinInference):
587
783
  F_psf = _as_psf(F)
588
784
 
589
785
  with self._structural_scope(F_psf):
590
- dt_val = float(self.data.peek_row(require={"dt"})["dt"])
786
+ import numpy as _np
787
+
788
+ from SFI.inference.parametric_core.solve import _dt_steps
789
+
790
+ # representative Δt for logging and the reported β — the median
791
+ # step (identical to the single step at uniform sampling)
792
+ dt_val = float(_np.median(_np.concatenate(
793
+ [_dt_steps(ds) for ds in self.data.datasets])))
591
794
 
592
795
  logger.info(
593
796
  "[infer_force] OD minimal parametric solve (n_params=%d, dt=%.4g, %s·n%d, basis_mode=%s)...",
@@ -600,6 +803,7 @@ class OverdampedLangevinInference(BaseLangevinInference):
600
803
  integrator=integrator, n_substeps=n_substeps,
601
804
  inner=inner, eiv=eiv, max_outer=max_outer,
602
805
  inner_maxiter=inner_maxiter, extra_radius=extra_radius,
806
+ chunk_target_bytes=self._chunk_target_bytes,
603
807
  )
604
808
  t_elapsed = _time.perf_counter() - t0
605
809
 
@@ -689,10 +893,13 @@ class OverdampedLangevinInference(BaseLangevinInference):
689
893
  r"""Infer state-dependent diffusion D(x) from parametric residuals.
690
894
 
691
895
  Requires a prior parametric :meth:`infer_force` call. Holds the
692
- fitted force fixed and minimises the windowed conditional NLL over
693
- the diffusion parameters (the log-det term makes the diffusion
694
- level identifiable), reusing the same flow residuals and integrate
695
- engine as the force solve.
896
+ fitted force fixed and minimises the exact banded NLL over the
897
+ diffusion parameters (the log-det term makes the diffusion level
898
+ identifiable) on cached fixed-θ tensors: the flow residuals and
899
+ stage Jacobians are computed once at :math:`\hat\theta`, so each
900
+ L-BFGS iteration costs linear algebra only — zero force or basis
901
+ evaluations. Non-uniform sampling (``t`` vector or per-step
902
+ ``dt``) is supported.
696
903
 
697
904
  .. physics:: State-dependent diffusion inference (overdamped)
698
905
  :label: parametric-diffusion-overdamped
@@ -700,7 +907,7 @@ class OverdampedLangevinInference(BaseLangevinInference):
700
907
 
701
908
  With the force :math:`\hat F` held fixed, the state-dependent
702
909
  diffusion :math:`D(x;\theta_D)` is optimised by minimising the
703
- windowed conditional negative log-likelihood; :math:`\Lambda`
910
+ exact banded negative log-likelihood; :math:`\Lambda`
704
911
  from the force inference is held fixed. A rank-2 basis gives
705
912
  :math:`D(x) = \sum_j (\theta_D)_j\, d_j(x)`; a PSF is evaluated
706
913
  directly.
@@ -756,6 +963,7 @@ class OverdampedLangevinInference(BaseLangevinInference):
756
963
  self.data, self.force_psf, self.force_coefficients_full, D_psf,
757
964
  Lambda=Lambda, theta_D0=theta_D0,
758
965
  n_substeps=n_substeps, integrator=integrator, maxiter=maxiter,
966
+ chunk_target_bytes=self._chunk_target_bytes,
759
967
  )
760
968
  theta_D = res.theta_D
761
969
  self.diffusion_coefficients = theta_D
@@ -786,6 +994,61 @@ class OverdampedLangevinInference(BaseLangevinInference):
786
994
  b_right = self.force_basis @ self.A_inv
787
995
  return self.__G_matrix__(b_left, b_right, self.__force_G_mode__, "ima,imb->iab")
788
996
 
997
+ def _strato_v_moments(self) -> jnp.ndarray:
998
+ r"""Stratonovich (trapezoid) phase-space velocity moments.
999
+
1000
+ Extensive, dt-weighted, mask-aware sums
1001
+
1002
+ .. math::
1003
+
1004
+ M_{v,a} = \sum_t \mathrm{d}X_t \cdot A^{-1} \cdot
1005
+ \tfrac12\bigl[b_a(X_t) + b_a(X_{t+1})\bigr]
1006
+
1007
+ — the by-product of Stratonovich force fitting and the raw material
1008
+ of :meth:`compute_entropy_production`.
1009
+ """
1010
+ A = ConstOperand(self.A_inv, alias="A")
1011
+ V = TimeOperand(velocity("dX", "dt"), alias="V")
1012
+ B0 = ExprOperand(expr=self.force_basis, x=stream("X"), alias="B0")
1013
+ Bp = ExprOperand(expr=self.force_basis, x=stream("X_plus"), alias="Bp")
1014
+ prog_B0 = Integrand(
1015
+ exprs=[B0],
1016
+ times=[V],
1017
+ consts=[A],
1018
+ terms=[Term(eq="im,mn,ina->ia", ops=("V", "A", "B0"))],
1019
+ )
1020
+ prog_Bp = Integrand(
1021
+ exprs=[Bp],
1022
+ times=[V],
1023
+ consts=[A],
1024
+ terms=[Term(eq="im,mn,ina->ia", ops=("V", "A", "Bp"))],
1025
+ )
1026
+ prog_strato_vb = 0.5 * (prog_B0 + prog_Bp)
1027
+ return integrate(
1028
+ self.data, prog_strato_vb, reduce="sum", chunk_target_bytes=self._chunk_target_bytes
1029
+ )
1030
+
1031
+ def _diffusion_looks_state_dependent(self) -> bool:
1032
+ """Best-effort probe: does ``diffusion_inferred`` vary with x?
1033
+
1034
+ Used by :meth:`compute_entropy_production` to warn when a
1035
+ state-dependent diffusion field was fitted (the estimator contracts
1036
+ with the constant average). Conservative: unprobeable fields count
1037
+ as state-dependent.
1038
+ """
1039
+ D_sf = getattr(self, "diffusion_inferred", None)
1040
+ if D_sf is None:
1041
+ return False
1042
+ try:
1043
+ x0 = jnp.asarray(self.data.datasets[0].X[0][:1]) # (1, d)
1044
+ x1 = x0 + 1.0 + 0.5 * jnp.abs(x0)
1045
+ D_a = jnp.asarray(D_sf(x0))
1046
+ D_b = jnp.asarray(D_sf(x1))
1047
+ scale = 1.0 + float(jnp.max(jnp.abs(D_a)))
1048
+ return bool(float(jnp.max(jnp.abs(D_a - D_b))) > 1e-6 * scale)
1049
+ except Exception: # pragma: no cover — conservative fallback
1050
+ return True
1051
+
789
1052
  def _force_moments(self):
790
1053
  r"""
791
1054
  Compute force moments ⟨ v · A_inv · b ⟩ with Ito / Ito-shift / Strato flavors.
@@ -869,24 +1132,7 @@ class OverdampedLangevinInference(BaseLangevinInference):
869
1132
  logger.debug("Computing Strato force coefficients.")
870
1133
 
871
1134
  # v ∘ b via trapezoid on basis
872
- B0 = ExprOperand(expr=self.force_basis, x=stream("X"), alias="B0")
873
- Bp = ExprOperand(expr=self.force_basis, x=stream("X_plus"), alias="Bp")
874
- prog_B0 = Integrand(
875
- exprs=[B0],
876
- times=[V],
877
- consts=[A],
878
- terms=[Term(eq="im,mn,ina->ia", ops=("V", "A", "B0"))],
879
- )
880
- prog_Bp = Integrand(
881
- exprs=[Bp],
882
- times=[V],
883
- consts=[A],
884
- terms=[Term(eq="im,mn,ina->ia", ops=("V", "A", "Bp"))],
885
- )
886
- prog_strato_vb = 0.5 * (prog_B0 + prog_Bp)
887
- self.force_v_moments = integrate(
888
- self.data, prog_strato_vb, reduce="sum", chunk_target_bytes=self._chunk_target_bytes
889
- )
1135
+ self.force_v_moments = self._strato_v_moments()
890
1136
 
891
1137
  logger.debug("Computing Strato gradient term.")
892
1138
 
@@ -1133,13 +1379,22 @@ def _D_noisy(**streams):
1133
1379
 
1134
1380
  Two-point estimator robust to measurement noise
1135
1381
  (CL Vestergaard, PC Blainey, H Flyvbjerg - Physical Review E, 2014).
1382
+
1383
+ Non-uniform sampling: the stencil expectation is
1384
+ :math:`2D(\mathrm{d}t + \mathrm{d}t^-)` with the localization terms
1385
+ cancelling at any spacing, so the denominator is the two-interval
1386
+ mean :math:`(\mathrm{d}t + \mathrm{d}t^-)/2` — identical to
1387
+ :math:`\mathrm{d}t` (bitwise) at uniform sampling, exactly unbiased
1388
+ under per-step ``dt``.
1136
1389
  """
1137
1390
  dX = streams["dX"]
1138
1391
  dXm = streams["dX_minus"]
1139
1392
  dt = streams["dt"]
1393
+ dtm = streams["dt_minus"]
1140
1394
  while dt.ndim < dX.ndim:
1141
1395
  dt = dt[..., jnp.newaxis]
1142
- invdt = 1.0 / dt
1396
+ dtm = dtm[..., jnp.newaxis]
1397
+ invdt = 1.0 / (0.5 * (dt + dtm))
1143
1398
  a = jnp.einsum("...m,...n->...mn", dX, dX * invdt)
1144
1399
  b = jnp.einsum("...m,...n->...mn", dX, dXm * invdt)
1145
1400
  c = jnp.einsum("...m,...n->...mn", dXm, dX * invdt)
@@ -1147,7 +1402,7 @@ def _D_noisy(**streams):
1147
1402
  return 0.25 * (a + 2 * b + 2 * c + d)
1148
1403
 
1149
1404
 
1150
- _D_noisy._requires = frozenset({"dX", "dX_minus", "dt"}) # type: ignore[attr-defined]
1405
+ _D_noisy._requires = frozenset({"dX", "dX_minus", "dt", "dt_minus"}) # type: ignore[attr-defined]
1151
1406
 
1152
1407
 
1153
1408
  @timeop(name="D_weaknoise", batch_safe=True)
@@ -1169,19 +1424,27 @@ def _D_weaknoise(**streams):
1169
1424
 
1170
1425
  Uses successive-displacement differences; suitable when localization
1171
1426
  noise is negligible.
1427
+
1428
+ Non-uniform sampling: ``E[(dX − dX⁻)⊗(dX − dX⁻)] = 2D(dt + dt⁻)``
1429
+ for clean data at any spacing, so the denominator is the
1430
+ two-interval mean ``(dt + dt⁻)/2`` — bitwise-identical to ``dt``
1431
+ at uniform sampling, exactly unbiased per-step (same
1432
+ generalization as the noisy estimator above).
1172
1433
  """
1173
1434
  dX = streams["dX"]
1174
1435
  dXm = streams["dX_minus"]
1175
1436
  dt = streams["dt"]
1437
+ dtm = streams["dt_minus"]
1176
1438
  while dt.ndim < dX.ndim:
1177
1439
  dt = dt[..., jnp.newaxis]
1178
- invdt = 1.0 / dt
1440
+ dtm = dtm[..., jnp.newaxis]
1441
+ invdt = 1.0 / (0.5 * (dt + dtm))
1179
1442
  ddx = dX - dXm
1180
1443
  dv = dX * invdt - dXm * invdt
1181
1444
  return 0.25 * jnp.einsum("...m,...n->...mn", ddx, dv)
1182
1445
 
1183
1446
 
1184
- _D_weaknoise._requires = frozenset({"dX", "dX_minus", "dt"}) # type: ignore[attr-defined]
1447
+ _D_weaknoise._requires = frozenset({"dX", "dX_minus", "dt", "dt_minus"}) # type: ignore[attr-defined]
1185
1448
 
1186
1449
 
1187
1450
  @timeop(name="Lambda_meas_noise", batch_safe=True)
@@ -0,0 +1,32 @@
1
+ # SFI/inference/parametric_core/ — the exact parametric estimator.
2
+ """
3
+ The parametric force/diffusion estimator: RK4-flow residuals, block-banded
4
+ residual covariance whitened *exactly* by a reverse-time block-LDLᵀ
5
+ innovations recursion (:mod:`banded` / :mod:`runner`), errors-in-variables
6
+ instrument, and native ``(D, Λ)`` profiling on cached fixed-θ tensors.
7
+ Per-interval (non-uniform) dt is supported end-to-end.
8
+
9
+ A single, paper-ready objective with a handful of parameters, integrating
10
+ like ``infer_*_linear`` (Basis/PSF in → ``InferenceResultSF`` out,
11
+ multi-particle aware).
12
+
13
+ Docs: ``docs/source/inference/parametric_concept.rst`` (foundations) and
14
+ ``docs/source/inference/parametric_algorithm.rst`` (implementation).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .solve import (
20
+ DiffusionSolveResult,
21
+ ForceSolveResult,
22
+ solve_diffusion_od,
23
+ solve_diffusion_ud,
24
+ solve_force_od,
25
+ solve_force_ud,
26
+ )
27
+
28
+ __all__ = [
29
+ "solve_force_od", "solve_force_ud",
30
+ "solve_diffusion_od", "solve_diffusion_ud",
31
+ "ForceSolveResult", "DiffusionSolveResult",
32
+ ]