StochasticForceInference 2.0.0__py3-none-any.whl
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.
- SFI/__init__.py +64 -0
- SFI/bases/__init__.py +85 -0
- SFI/bases/constants.py +492 -0
- SFI/bases/linear.py +325 -0
- SFI/bases/monomials.py +218 -0
- SFI/bases/pairs.py +998 -0
- SFI/bases/spde.py +1537 -0
- SFI/diagnostics/__init__.py +60 -0
- SFI/diagnostics/assess.py +87 -0
- SFI/diagnostics/dynamics_order.py +621 -0
- SFI/diagnostics/plotting.py +226 -0
- SFI/diagnostics/report.py +238 -0
- SFI/diagnostics/residual_tests.py +395 -0
- SFI/diagnostics/residuals.py +688 -0
- SFI/inference/__init__.py +58 -0
- SFI/inference/base.py +1460 -0
- SFI/inference/optimizers.py +200 -0
- SFI/inference/overdamped.py +1214 -0
- SFI/inference/parametric_core/__init__.py +34 -0
- SFI/inference/parametric_core/covariance.py +232 -0
- SFI/inference/parametric_core/driver.py +272 -0
- SFI/inference/parametric_core/flow.py +149 -0
- SFI/inference/parametric_core/flow_multi.py +362 -0
- SFI/inference/parametric_core/flow_ud.py +168 -0
- SFI/inference/parametric_core/jacobians.py +540 -0
- SFI/inference/parametric_core/objective.py +286 -0
- SFI/inference/parametric_core/objective_ud.py +253 -0
- SFI/inference/parametric_core/precision.py +229 -0
- SFI/inference/parametric_core/solve.py +763 -0
- SFI/inference/result.py +362 -0
- SFI/inference/serialization.py +245 -0
- SFI/inference/sparse/__init__.py +67 -0
- SFI/inference/sparse/base.py +43 -0
- SFI/inference/sparse/beam.py +303 -0
- SFI/inference/sparse/greedy.py +151 -0
- SFI/inference/sparse/hillclimb.py +307 -0
- SFI/inference/sparse/lasso.py +178 -0
- SFI/inference/sparse/metrics.py +78 -0
- SFI/inference/sparse/result.py +278 -0
- SFI/inference/sparse/scorer.py +323 -0
- SFI/inference/sparse/stlsq.py +165 -0
- SFI/inference/sparsity.py +40 -0
- SFI/inference/underdamped.py +1355 -0
- SFI/integrate/__init__.py +38 -0
- SFI/integrate/api.py +920 -0
- SFI/integrate/integrand.py +402 -0
- SFI/integrate/rk4.py +156 -0
- SFI/integrate/timeops.py +174 -0
- SFI/langevin/__init__.py +29 -0
- SFI/langevin/base.py +863 -0
- SFI/langevin/chunked.py +225 -0
- SFI/langevin/noise.py +446 -0
- SFI/langevin/overdamped.py +560 -0
- SFI/langevin/underdamped.py +448 -0
- SFI/statefunc/__init__.py +49 -0
- SFI/statefunc/basis.py +87 -0
- SFI/statefunc/core/runtime.py +47 -0
- SFI/statefunc/factory.py +346 -0
- SFI/statefunc/interactor.py +90 -0
- SFI/statefunc/layout/__init__.py +29 -0
- SFI/statefunc/layout/_base.py +186 -0
- SFI/statefunc/layout/_eval_compiler.py +453 -0
- SFI/statefunc/layout/_fd_atoms.py +196 -0
- SFI/statefunc/layout/_grid.py +878 -0
- SFI/statefunc/layout/_sectors.py +166 -0
- SFI/statefunc/memhint.py +222 -0
- SFI/statefunc/nodes/__init__.py +56 -0
- SFI/statefunc/nodes/base.py +318 -0
- SFI/statefunc/nodes/contract.py +422 -0
- SFI/statefunc/nodes/interactions/__init__.py +23 -0
- SFI/statefunc/nodes/interactions/dispatcher.py +1365 -0
- SFI/statefunc/nodes/interactions/prepare.py +161 -0
- SFI/statefunc/nodes/interactions/specs.py +362 -0
- SFI/statefunc/nodes/interactions/stencils.py +718 -0
- SFI/statefunc/nodes/leaf.py +530 -0
- SFI/statefunc/nodes/ops/__init__.py +27 -0
- SFI/statefunc/nodes/ops/concat.py +28 -0
- SFI/statefunc/nodes/ops/derivative.py +447 -0
- SFI/statefunc/nodes/ops/einsum.py +120 -0
- SFI/statefunc/nodes/ops/linear.py +153 -0
- SFI/statefunc/nodes/ops/mapn.py +138 -0
- SFI/statefunc/nodes/ops/reshape_rank.py +158 -0
- SFI/statefunc/nodes/ops/slice.py +83 -0
- SFI/statefunc/params.py +309 -0
- SFI/statefunc/psf.py +112 -0
- SFI/statefunc/sf.py +104 -0
- SFI/statefunc/stateexpr.py +1243 -0
- SFI/statefunc/structexpr.py +1003 -0
- SFI/trajectory/__init__.py +31 -0
- SFI/trajectory/collection.py +1122 -0
- SFI/trajectory/dataset.py +1164 -0
- SFI/trajectory/degrade.py +1108 -0
- SFI/trajectory/io.py +1027 -0
- SFI/trajectory/reserved_extras.py +129 -0
- SFI/utils/__init__.py +17 -0
- SFI/utils/formatting.py +308 -0
- SFI/utils/maths.py +185 -0
- SFI/utils/neighbors.py +162 -0
- SFI/utils/plotting.py +1936 -0
- stochasticforceinference-2.0.0.dist-info/METADATA +203 -0
- stochasticforceinference-2.0.0.dist-info/RECORD +104 -0
- stochasticforceinference-2.0.0.dist-info/WHEEL +5 -0
- stochasticforceinference-2.0.0.dist-info/licenses/LICENSE +21 -0
- stochasticforceinference-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# SFI/inference/parametric_core/flow_multi.py
|
|
2
|
+
"""
|
|
3
|
+
Multiparticle residuals — one function for both the non-interacting and
|
|
4
|
+
interacting cases, dispatched on the force model's particle contract.
|
|
5
|
+
|
|
6
|
+
The covariance/precision machinery is **per particle**, so all this layer
|
|
7
|
+
does is produce per-particle residuals ``r`` and flow Jacobians ``J`` from
|
|
8
|
+
a window of multiparticle positions:
|
|
9
|
+
|
|
10
|
+
* **non-interacting** (``particles_input=False``): the same force is
|
|
11
|
+
applied to each particle independently — ``vmap`` the single-particle
|
|
12
|
+
:func:`flow.od_window_residuals` over the particle axis.
|
|
13
|
+
* **interacting** (``particles_input=True``): the displacement uses the
|
|
14
|
+
*full* multiparticle force (couplings matter), while the per-particle
|
|
15
|
+
flow Jacobian uses the frozen-background same-particle derivative
|
|
16
|
+
``F_psf.d_x(same_particle=True)`` — via
|
|
17
|
+
:func:`jacobians.rk4_composed_jacobian` (O(N) per window).
|
|
18
|
+
|
|
19
|
+
Single particle is just ``N=1``; the integrate engine reduces + masks
|
|
20
|
+
over the particle axis (``reduce_over_particles``), so there is no
|
|
21
|
+
separate multiparticle code path in the solver — exactly like
|
|
22
|
+
``infer_force_linear``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import jax
|
|
28
|
+
import jax.numpy as jnp
|
|
29
|
+
|
|
30
|
+
from .flow import flow_displacement, od_instrument, od_window_residuals
|
|
31
|
+
from .flow_ud import _phase_field, ud_instrument, ud_window_residuals
|
|
32
|
+
|
|
33
|
+
__all__ = ["multi_step_residuals", "multi_step_residuals_with_psi",
|
|
34
|
+
"ud_multi_step_residuals", "ud_multi_step_residuals_with_psi",
|
|
35
|
+
"multi_od_instrument", "multi_ud_instrument"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _interacting_jpb(F_psf, theta_flat, extras, dt, n_substeps, integrator):
|
|
39
|
+
r"""Frame → ``(J, Φ, B)`` closure for an interacting force (frozen background).
|
|
40
|
+
|
|
41
|
+
``B = ∂Φ/∂θ`` per particle via :func:`jacobians.rk4_composed_jacobian_theta`
|
|
42
|
+
— the legacy same-particle trick: O(N·d·n_params) carry instead of
|
|
43
|
+
forward-mode tangents through the full N-body window flow. ``∂F/∂θ`` at a
|
|
44
|
+
fixed frame is exact and per-particle (jacfwd of one force evaluation);
|
|
45
|
+
only the O(h²) cross-particle substep feedback is dropped, consistently
|
|
46
|
+
with the frozen-background ``J``.
|
|
47
|
+
"""
|
|
48
|
+
from .jacobians import rk4_composed_jacobian_theta
|
|
49
|
+
|
|
50
|
+
struct = F_psf.unflatten_params(theta_flat)
|
|
51
|
+
dFdx = F_psf.d_x(same_particle=True)
|
|
52
|
+
n_params = int(theta_flat.shape[-1])
|
|
53
|
+
|
|
54
|
+
def dFdth(X):
|
|
55
|
+
return jax.jacfwd(
|
|
56
|
+
lambda th: F_psf(X, params=F_psf.unflatten_params(th), extras=extras)
|
|
57
|
+
)(theta_flat) # (N, d, n_params)
|
|
58
|
+
|
|
59
|
+
def jpb(X_start):
|
|
60
|
+
return rk4_composed_jacobian_theta(
|
|
61
|
+
F_psf, dFdx, dFdth, n_params, X_start, struct, None, extras,
|
|
62
|
+
dt, n_substeps, integrator)
|
|
63
|
+
|
|
64
|
+
return jpb
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def multi_step_residuals_with_psi(F_psf, theta_flat, X_w, extras, dt, n_substeps,
|
|
68
|
+
integrator="rk4"):
|
|
69
|
+
r"""Interacting residuals + flow Jacobians + frozen-background ``ψ_right``.
|
|
70
|
+
|
|
71
|
+
Same residuals/Jacobians as the interacting branch of
|
|
72
|
+
:func:`multi_step_residuals`, additionally returning the per-particle
|
|
73
|
+
regressor ``ψ_right = ∂r/∂θ = −B`` from the same-particle θ-recursion —
|
|
74
|
+
the memory-scalable replacement for ``jax.jacfwd`` through the full
|
|
75
|
+
N-body flow (which holds n_params tangents alive across the whole
|
|
76
|
+
window graph and scales O(K·N²·n_params) per chunk).
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
r : ``(W-1, N, d)``, J : ``(W-1, N, d, d)``, psi : ``(W-1, N, d, n_params)``
|
|
81
|
+
"""
|
|
82
|
+
jpb = _interacting_jpb(F_psf, theta_flat, extras, dt, n_substeps, integrator)
|
|
83
|
+
|
|
84
|
+
def _step(X_start, X_end):
|
|
85
|
+
J, Phi, B = jpb(X_start)
|
|
86
|
+
return X_end - X_start - Phi, J, -B
|
|
87
|
+
|
|
88
|
+
return jax.vmap(_step)(X_w[:-1], X_w[1:])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def multi_step_residuals(F_psf, theta_struct, X_w, extras, dt, n_substeps, integrator="rk4"):
|
|
92
|
+
r"""Per-particle residuals and flow Jacobians over a multiparticle window.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
F_psf : PSF
|
|
97
|
+
theta_struct : structured parameters (from ``F_psf.unflatten_params``).
|
|
98
|
+
X_w : ``(W, N, d)`` window of multiparticle positions.
|
|
99
|
+
extras : dict or None
|
|
100
|
+
dt, n_substeps, integrator : flow settings.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
r : ``(W-1, N, d)``
|
|
105
|
+
J : ``(W-1, N, d, d)``
|
|
106
|
+
"""
|
|
107
|
+
if getattr(F_psf, "particles_input", False):
|
|
108
|
+
from .jacobians import rk4_composed_jacobian
|
|
109
|
+
|
|
110
|
+
dFdx = F_psf.d_x(same_particle=True)
|
|
111
|
+
|
|
112
|
+
def _step(X_start, X_end): # (N, d), (N, d)
|
|
113
|
+
J, Phi = rk4_composed_jacobian(
|
|
114
|
+
F_psf, dFdx, X_start, theta_struct, None, extras, dt, n_substeps, integrator)
|
|
115
|
+
return X_end - X_start - Phi, J
|
|
116
|
+
|
|
117
|
+
return jax.vmap(_step)(X_w[:-1], X_w[1:]) # (W-1, N, d), (W-1, N, d, d)
|
|
118
|
+
|
|
119
|
+
def _drift(x):
|
|
120
|
+
return F_psf(x[None], params=theta_struct, extras=extras)[0]
|
|
121
|
+
|
|
122
|
+
def _per_particle(Xp): # (W, d)
|
|
123
|
+
return od_window_residuals(_drift, Xp, dt, n_substeps, integrator)
|
|
124
|
+
|
|
125
|
+
r, J = jax.vmap(_per_particle, in_axes=1)(X_w) # (N, W-1, d), (N, W-1, d, d)
|
|
126
|
+
return jnp.swapaxes(r, 0, 1), jnp.swapaxes(J, 0, 1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ud_multi_step_residuals(F_psf, theta_struct, Y_w, extras, dt, n_substeps, integrator="rk4"):
|
|
130
|
+
r"""Per-particle underdamped 3-point residuals + α-propagators + ``v̂``.
|
|
131
|
+
|
|
132
|
+
* *Non-interacting* (``particles_input=False``): ``vmap`` the
|
|
133
|
+
single-particle :func:`flow_ud.ud_window_residuals` over particles.
|
|
134
|
+
* *Interacting* (``particles_input=True``): multi-particle phase-space
|
|
135
|
+
shooting (full force drives the flow) + frozen-background per-particle
|
|
136
|
+
phase Jacobian via ``rk4_composed_jacobian_phase`` — the underdamped
|
|
137
|
+
analogue of the overdamped interacting path.
|
|
138
|
+
|
|
139
|
+
Returns ``r, α₊, α₀, α₋, v̂`` each shaped ``(W-2, N, ...)``.
|
|
140
|
+
"""
|
|
141
|
+
if getattr(F_psf, "particles_input", False):
|
|
142
|
+
return _ud_multi_interacting(F_psf, theta_struct, Y_w, extras, dt, n_substeps, integrator)
|
|
143
|
+
|
|
144
|
+
def _force(x, v):
|
|
145
|
+
return F_psf(x[None], v=v[None], params=theta_struct, extras=extras)[0]
|
|
146
|
+
|
|
147
|
+
def _per_particle(Yp): # (W, d)
|
|
148
|
+
r, ap, a0, am, _, vhat = ud_window_residuals(_force, Yp, dt, n_substeps, integrator)
|
|
149
|
+
return r, ap, a0, am, vhat
|
|
150
|
+
|
|
151
|
+
r, ap, a0, am, vhat = jax.vmap(_per_particle, in_axes=1)(Y_w) # each (N, W-2, ...)
|
|
152
|
+
|
|
153
|
+
def sw(x):
|
|
154
|
+
return jnp.swapaxes(x, 0, 1)
|
|
155
|
+
|
|
156
|
+
return sw(r), sw(ap), sw(a0), sw(am), sw(vhat)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def multi_od_instrument(F_psf, theta, X_base, extras, dt, n_substeps, integrator="rk4"):
|
|
160
|
+
r"""η-clean overdamped instrument per particle — ``(N, d, n_params)``.
|
|
161
|
+
|
|
162
|
+
Multiparticle dispatcher for :func:`flow.od_instrument`, mirroring
|
|
163
|
+
:func:`multi_step_residuals`:
|
|
164
|
+
|
|
165
|
+
* **non-interacting**: the single-particle instrument, ``vmap``-ed over
|
|
166
|
+
the particle axis of the base frame.
|
|
167
|
+
* **interacting** (``particles_input=True``): the instrument flow must be
|
|
168
|
+
the *same N-body flow* the residual uses — the sensitivity is taken of
|
|
169
|
+
the full-frame displacement ``Φ(X;θ)``, seeded at the η-clean base frame.
|
|
170
|
+
Evaluating the force on isolated single-particle frames instead zeroes
|
|
171
|
+
(or crashes) every pair-feature column, which makes the IV Gram
|
|
172
|
+
structurally singular on the interaction parameters — the ABP-port
|
|
173
|
+
plateau. ``od_instrument`` is shape-agnostic, so the frame-level call
|
|
174
|
+
only needs a frame-level drift closure.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
F_psf : PSF
|
|
179
|
+
theta : ``(n_params,)`` flat parameters.
|
|
180
|
+
X_base : ``(N, d)`` η-clean base frame (the reserved front position).
|
|
181
|
+
extras : dict or None
|
|
182
|
+
dt, n_substeps, integrator : flow settings.
|
|
183
|
+
"""
|
|
184
|
+
if getattr(F_psf, "particles_input", False):
|
|
185
|
+
# Frozen-background trapeze: ψ_inst = −½(B(X_base) + B(X_base + Φ)),
|
|
186
|
+
# the same-particle θ-sensitivity at the η-clean frame and its forward
|
|
187
|
+
# image — O(N·d·n_params) memory, vs jacfwd through the N-body flow.
|
|
188
|
+
jpb = _interacting_jpb(F_psf, theta, extras, dt, n_substeps, integrator)
|
|
189
|
+
_, Phi0, B0 = jpb(X_base)
|
|
190
|
+
_, _, B1 = jpb(jax.lax.stop_gradient(X_base + Phi0))
|
|
191
|
+
return -0.5 * (B0 + B1)
|
|
192
|
+
|
|
193
|
+
def drift_of_theta(th):
|
|
194
|
+
struct = F_psf.unflatten_params(th)
|
|
195
|
+
return lambda y: F_psf(y[None], params=struct, extras=extras)[0]
|
|
196
|
+
|
|
197
|
+
return jax.vmap(
|
|
198
|
+
lambda xb: od_instrument(drift_of_theta, theta, xb, dt, n_substeps, integrator)
|
|
199
|
+
)(X_base)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def multi_ud_instrument(F_psf, theta, Y_a, Y_b, extras, dt, n_substeps,
|
|
203
|
+
integrator="rk4", n_predict=2):
|
|
204
|
+
r"""η-clean underdamped instrument per particle — ``(N, d, n_params)``.
|
|
205
|
+
|
|
206
|
+
Multiparticle dispatcher for :func:`flow_ud.ud_instrument`:
|
|
207
|
+
|
|
208
|
+
* **non-interacting**: the single-particle instrument, ``vmap``-ed over
|
|
209
|
+
the particle axis of the lagged pair ``(Y_a, Y_b)``.
|
|
210
|
+
* **interacting** (``particles_input=True``): frame-level analogue built
|
|
211
|
+
from the same protocols as the interacting residual path — shooting at
|
|
212
|
+
``(Y_a → Y_b)`` with the frozen-background per-particle ``J^{xv}``
|
|
213
|
+
(:func:`jacobians.rk4_composed_jacobian_phase`, as in the residual's
|
|
214
|
+
``v̂``), phase-flowing the full frame forward ``n_predict`` intervals,
|
|
215
|
+
then differentiating the full-force one-step position flow in θ at the
|
|
216
|
+
clean (stop-gradient) phase point. Everything is a function of the two
|
|
217
|
+
front frames only, so the instrument stays η-clean of the whole
|
|
218
|
+
residual block.
|
|
219
|
+
"""
|
|
220
|
+
if getattr(F_psf, "particles_input", False):
|
|
221
|
+
from .jacobians import rk4_composed_jacobian_phase
|
|
222
|
+
|
|
223
|
+
struct0 = F_psf.unflatten_params(theta)
|
|
224
|
+
dFdx = F_psf.d_x(same_particle=True)
|
|
225
|
+
dFdv = F_psf.d_v(same_particle=True)
|
|
226
|
+
N = Y_a.shape[0]
|
|
227
|
+
|
|
228
|
+
def frame_phase_field(th_struct):
|
|
229
|
+
def force(X, V):
|
|
230
|
+
return F_psf(X, v=V, params=th_struct, extras=extras)
|
|
231
|
+
return _phase_field(force, N)
|
|
232
|
+
|
|
233
|
+
# 1. shooting at (Y_a → Y_b): arrival velocity at Y_b (residual protocol).
|
|
234
|
+
V0 = (Y_b - Y_a) / dt
|
|
235
|
+
Phi_x0, Phi_v0, _Jxx, Jxv, _Jvx, Jvv = rk4_composed_jacobian_phase(
|
|
236
|
+
F_psf, dFdx, dFdv, Y_a, V0, struct0, None, extras, dt, n_substeps, integrator)
|
|
237
|
+
dV = jnp.linalg.solve(Jxv, (Y_b - Phi_x0)[..., None]).squeeze(-1)
|
|
238
|
+
v_b = Phi_v0 + jnp.einsum("nij,nj->ni", Jvv, dV)
|
|
239
|
+
|
|
240
|
+
# 2. full-force phase flow to the center base (θ fixed, then "data").
|
|
241
|
+
z = jnp.concatenate([Y_b, v_b], axis=0) # (2N, d) lifted frame
|
|
242
|
+
g0 = frame_phase_field(struct0)
|
|
243
|
+
for _ in range(n_predict):
|
|
244
|
+
z = z + flow_displacement(g0, z, dt, n_substeps, integrator)
|
|
245
|
+
z_c = jax.lax.stop_gradient(z)
|
|
246
|
+
|
|
247
|
+
# 3. ψ_inst = −∂Φˣ/∂θ at the clean phase point — frozen-background
|
|
248
|
+
# per-particle phase θ-sensitivity (Bx), O(N·d·n_params) memory.
|
|
249
|
+
jpb = _interacting_phase_jpb(F_psf, theta, extras, dt, n_substeps, integrator)
|
|
250
|
+
x_c, v_c = z_c[:N], z_c[N:]
|
|
251
|
+
Bx = jpb(x_c, v_c)[6]
|
|
252
|
+
return -Bx
|
|
253
|
+
|
|
254
|
+
def force_of_theta(th):
|
|
255
|
+
struct = F_psf.unflatten_params(th)
|
|
256
|
+
return lambda x, v: F_psf(x[None], v=v[None], params=struct, extras=extras)[0]
|
|
257
|
+
|
|
258
|
+
return jax.vmap(
|
|
259
|
+
lambda a, b: ud_instrument(force_of_theta, theta, a, b, dt, n_substeps,
|
|
260
|
+
integrator, n_predict=n_predict)
|
|
261
|
+
)(Y_a, Y_b)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _interacting_phase_jpb(F_psf, theta_flat, extras, dt, n_substeps, integrator):
|
|
265
|
+
"""Phase frame → (Φx, Φv, Jxx, Jxv, Jvx, Jvv, Bx, Bv) closure (frozen background)."""
|
|
266
|
+
from .jacobians import rk4_composed_jacobian_phase_theta
|
|
267
|
+
|
|
268
|
+
struct = F_psf.unflatten_params(theta_flat)
|
|
269
|
+
dFdx = F_psf.d_x(same_particle=True)
|
|
270
|
+
dFdv = F_psf.d_v(same_particle=True)
|
|
271
|
+
n_params = int(theta_flat.shape[-1])
|
|
272
|
+
|
|
273
|
+
def dFdth(X, V):
|
|
274
|
+
return jax.jacfwd(
|
|
275
|
+
lambda th: F_psf(X, v=V, params=F_psf.unflatten_params(th), extras=extras)
|
|
276
|
+
)(theta_flat) # (N, d, n_params)
|
|
277
|
+
|
|
278
|
+
def jpb(X, V):
|
|
279
|
+
return rk4_composed_jacobian_phase_theta(
|
|
280
|
+
F_psf, dFdx, dFdv, dFdth, n_params, X, V, struct, None, extras,
|
|
281
|
+
dt, n_substeps, integrator)
|
|
282
|
+
|
|
283
|
+
return jpb
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def ud_multi_step_residuals_with_psi(F_psf, theta_flat, Y_w, extras, dt, n_substeps,
|
|
287
|
+
integrator="rk4"):
|
|
288
|
+
r"""Interacting UD residuals + α-propagators + frozen-background ``ψ_right``.
|
|
289
|
+
|
|
290
|
+
Same shooting protocol as :func:`_ud_multi_interacting`, additionally
|
|
291
|
+
returning ``ψ_right = ∂r/∂θ`` from the per-particle phase θ-recursion:
|
|
292
|
+
|
|
293
|
+
.. math::
|
|
294
|
+
|
|
295
|
+
\partial\hat v/\partial\theta = B^v_{\rm in}
|
|
296
|
+
- J^{vv} (J^{xv})^{-1} B^x_{\rm in},\qquad
|
|
297
|
+
ψ = -\bigl(B^x_{\rm out} + J^{xv}_{\rm out}\,
|
|
298
|
+
\partial\hat v/\partial\theta\bigr),
|
|
299
|
+
|
|
300
|
+
treating the (O(h)) Jacobian blocks as θ-independent — the same
|
|
301
|
+
second-order cross terms the frozen-background blocks already drop.
|
|
302
|
+
|
|
303
|
+
Returns ``r, α₊, α₀, α₋, v̂, ψ`` with ``ψ : (W-2, N, d, n_params)``.
|
|
304
|
+
"""
|
|
305
|
+
jpb = _interacting_phase_jpb(F_psf, theta_flat, extras, dt, n_substeps, integrator)
|
|
306
|
+
d = Y_w.shape[-1]
|
|
307
|
+
I_d = jnp.eye(d, dtype=Y_w.dtype)
|
|
308
|
+
|
|
309
|
+
def _step(X_prev, X_cur, X_next): # each (N, d)
|
|
310
|
+
V0 = (X_cur - X_prev) / dt
|
|
311
|
+
Phi_x0, Phi_v0, Jxx_in, Jxv_in, Jvx_in, Jvv_in, Bx_in, Bv_in = jpb(X_prev, V0)
|
|
312
|
+
Jxv_in_inv = jnp.linalg.inv(Jxv_in)
|
|
313
|
+
dV = jnp.einsum("nij,nj->ni", Jxv_in_inv, X_cur - Phi_x0)
|
|
314
|
+
v_hat = Phi_v0 + jnp.einsum("nij,nj->ni", Jvv_in, dV)
|
|
315
|
+
dvhat_dth = Bv_in - jnp.einsum("nij,njk->nik", Jvv_in,
|
|
316
|
+
jnp.einsum("nij,njk->nik", Jxv_in_inv, Bx_in))
|
|
317
|
+
|
|
318
|
+
Phi_x_out, _, Jxx_out, Jxv_out, _, _, Bx_out, _ = jpb(X_cur, v_hat)
|
|
319
|
+
r = X_next - Phi_x_out
|
|
320
|
+
psi = -(Bx_out + jnp.einsum("nij,njk->nik", Jxv_out, dvhat_dth))
|
|
321
|
+
|
|
322
|
+
M = Jvx_in - Jvv_in @ Jxv_in_inv @ Jxx_in
|
|
323
|
+
Nmat = Jvv_in @ Jxv_in_inv
|
|
324
|
+
alpha_p1 = jnp.broadcast_to(I_d, Jxx_out.shape)
|
|
325
|
+
alpha_0 = -Jxx_out - Jxv_out @ Nmat
|
|
326
|
+
alpha_m1 = -Jxv_out @ M
|
|
327
|
+
return r, alpha_p1, alpha_0, alpha_m1, v_hat, psi
|
|
328
|
+
|
|
329
|
+
return jax.vmap(_step)(Y_w[:-2], Y_w[1:-1], Y_w[2:])
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _ud_multi_interacting(F_psf, theta_struct, Y_w, extras, dt, n_substeps, integrator):
|
|
333
|
+
"""Interacting underdamped: per-particle shooting + frozen-background phase Jacobian."""
|
|
334
|
+
from .jacobians import rk4_composed_jacobian_phase
|
|
335
|
+
|
|
336
|
+
dFdx = F_psf.d_x(same_particle=True)
|
|
337
|
+
dFdv = F_psf.d_v(same_particle=True)
|
|
338
|
+
d = Y_w.shape[-1]
|
|
339
|
+
I_d = jnp.eye(d, dtype=Y_w.dtype)
|
|
340
|
+
|
|
341
|
+
def _phase(X, V):
|
|
342
|
+
return rk4_composed_jacobian_phase(
|
|
343
|
+
F_psf, dFdx, dFdv, X, V, theta_struct, None, extras, dt, n_substeps, integrator)
|
|
344
|
+
|
|
345
|
+
def _step(X_prev, X_cur, X_next): # each (N, d)
|
|
346
|
+
V0 = (X_cur - X_prev) / dt
|
|
347
|
+
Phi_x0, Phi_v0, Jxx_in, Jxv_in, Jvx_in, Jvv_in = _phase(X_prev, V0)
|
|
348
|
+
dV = jnp.linalg.solve(Jxv_in, (X_cur - Phi_x0)[..., None]).squeeze(-1) # (N, d)
|
|
349
|
+
v_hat = Phi_v0 + jnp.einsum("nij,nj->ni", Jvv_in, dV)
|
|
350
|
+
|
|
351
|
+
Phi_x_out, _, Jxx_out, Jxv_out, _, _ = _phase(X_cur, v_hat)
|
|
352
|
+
r = X_next - Phi_x_out
|
|
353
|
+
|
|
354
|
+
Jxv_in_inv = jnp.linalg.inv(Jxv_in)
|
|
355
|
+
M = Jvx_in - Jvv_in @ Jxv_in_inv @ Jxx_in
|
|
356
|
+
Nmat = Jvv_in @ Jxv_in_inv
|
|
357
|
+
alpha_p1 = jnp.broadcast_to(I_d, Jxx_out.shape)
|
|
358
|
+
alpha_0 = -Jxx_out - Jxv_out @ Nmat
|
|
359
|
+
alpha_m1 = -Jxv_out @ M
|
|
360
|
+
return r, alpha_p1, alpha_0, alpha_m1, v_hat
|
|
361
|
+
|
|
362
|
+
return jax.vmap(_step)(Y_w[:-2], Y_w[1:-1], Y_w[2:]) # each (W-2, N, ...)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SFI/inference/parametric_core/flow_ud.py
|
|
2
|
+
"""
|
|
3
|
+
Underdamped (2nd-order) flow: phase-space propagation, shooting velocity,
|
|
4
|
+
and the 3-point position residual with its measurement-noise propagators.
|
|
5
|
+
|
|
6
|
+
The velocity is unobserved, so the residual is built from three
|
|
7
|
+
consecutive positions:
|
|
8
|
+
|
|
9
|
+
1. **Shooting** — resolve the velocity ``v̂_n`` that carries ``Y_{n-1}``
|
|
10
|
+
to ``Y_n`` by one Newton step from the secant velocity (the position
|
|
11
|
+
block of the phase-space flow is inverted via ``J^{xv}``).
|
|
12
|
+
2. **Residual** — propagate ``(Y_n, v̂_n)`` one step and compare to the
|
|
13
|
+
next observed position: ``r_n = Y_{n+1} − Φˣ(Y_n, v̂_n; θ)``.
|
|
14
|
+
3. **α-propagators** — how the measurement noise at the three samples
|
|
15
|
+
enters ``r_n`` (``α₊ = I``, ``α₀``, ``α₋``); these set the bandwidth-2
|
|
16
|
+
(pentadiagonal) covariance structure.
|
|
17
|
+
|
|
18
|
+
The phase-space flow is just :func:`flow.flow_step` applied to the lifted
|
|
19
|
+
field ``ż = (v, F(x, v))`` on ``ℝ^{2d}`` — RK4/Euler and the Jacobian come
|
|
20
|
+
for free.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
|
|
28
|
+
from .flow import flow_step
|
|
29
|
+
|
|
30
|
+
__all__ = ["phase_space_flow_jac", "shooting_velocity", "ud_step_quantities",
|
|
31
|
+
"ud_window_residuals", "ud_instrument"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _phase_field(force_fn, d):
|
|
35
|
+
def g(z):
|
|
36
|
+
return jnp.concatenate([z[d:], force_fn(z[:d], z[d:])])
|
|
37
|
+
return g
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def phase_space_flow_jac(force_fn, x, v, dt, n_substeps, integrator="rk4"):
|
|
41
|
+
r"""Phase-space arrival state and ``2d×2d`` flow Jacobian blocks.
|
|
42
|
+
|
|
43
|
+
Returns ``Φ_x, Φ_v`` (arrival position, velocity) and the four
|
|
44
|
+
``d×d`` blocks ``J^{xx}, J^{xv}, J^{vx}, J^{vv}`` of ``∂(x',v')/∂(x,v)``.
|
|
45
|
+
"""
|
|
46
|
+
d = x.shape[0]
|
|
47
|
+
z0 = jnp.concatenate([x, v])
|
|
48
|
+
Phi_z, J = flow_step(_phase_field(force_fn, d), z0, dt, n_substeps, integrator)
|
|
49
|
+
z_final = z0 + Phi_z
|
|
50
|
+
return (
|
|
51
|
+
z_final[:d], z_final[d:],
|
|
52
|
+
J[:d, :d], J[:d, d:], J[d:, :d], J[d:, d:],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def shooting_velocity(force_fn, Y_prev, Y_cur, dt, n_substeps, integrator="rk4"):
|
|
57
|
+
r"""Velocity at ``Y_prev`` that flows to ``Y_cur`` (one Newton step).
|
|
58
|
+
|
|
59
|
+
Returns ``v̂`` (the *arrival* velocity at ``Y_cur``) and the incoming
|
|
60
|
+
phase-space Jacobian blocks evaluated at ``(Y_prev, v₀)``.
|
|
61
|
+
"""
|
|
62
|
+
v0 = (Y_cur - Y_prev) / dt
|
|
63
|
+
Phi_x0, Phi_v0, Jxx, Jxv, Jvx, Jvv = phase_space_flow_jac(force_fn, Y_prev, v0, dt, n_substeps, integrator)
|
|
64
|
+
dv = jnp.linalg.solve(Jxv, Y_cur - Phi_x0)
|
|
65
|
+
v_hat = Phi_v0 + Jvv @ dv
|
|
66
|
+
return v_hat, Jxx, Jxv, Jvx, Jvv
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ud_step_quantities(force_fn, Y_prev, Y_cur, Y_next, dt, n_substeps, integrator="rk4"):
|
|
70
|
+
r"""Residual and α-propagators for one interior position ``Y_cur``.
|
|
71
|
+
|
|
72
|
+
Returns ``r, α₊, α₀, α₋, J^{vv}_out, v̂`` where ``r = Y_next − Φˣ(Y_cur,
|
|
73
|
+
v̂)``, the α's propagate the measurement noise at ``Y_{n-1}, Y_n,
|
|
74
|
+
Y_{n+1}`` into ``r``, and ``v̂`` is the shooting velocity at ``Y_cur``
|
|
75
|
+
(used to evaluate a velocity-dependent diffusion ``D(x, v)``).
|
|
76
|
+
"""
|
|
77
|
+
d = Y_cur.shape[0]
|
|
78
|
+
I_d = jnp.eye(d, dtype=Y_cur.dtype)
|
|
79
|
+
|
|
80
|
+
v_hat, Jxx_in, Jxv_in, Jvx_in, Jvv_in = shooting_velocity(force_fn, Y_prev, Y_cur, dt, n_substeps, integrator)
|
|
81
|
+
Phi_x_out, _, Jxx_out, Jxv_out, _, Jvv_out = phase_space_flow_jac(
|
|
82
|
+
force_fn, Y_cur, v_hat, dt, n_substeps, integrator
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
r = Y_next - Phi_x_out
|
|
86
|
+
|
|
87
|
+
Jxv_in_inv = jnp.linalg.inv(Jxv_in)
|
|
88
|
+
M = Jvx_in - Jvv_in @ Jxv_in_inv @ Jxx_in
|
|
89
|
+
N = Jvv_in @ Jxv_in_inv
|
|
90
|
+
|
|
91
|
+
alpha_p1 = I_d
|
|
92
|
+
alpha_0 = -Jxx_out - Jxv_out @ N
|
|
93
|
+
alpha_m1 = -Jxv_out @ M
|
|
94
|
+
return r, alpha_p1, alpha_0, alpha_m1, Jvv_out, v_hat
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ud_window_residuals(force_fn, Y_w, dt, n_substeps, integrator="rk4"):
|
|
98
|
+
r"""3-point residuals and α-propagators over a position window.
|
|
99
|
+
|
|
100
|
+
For ``W`` consecutive positions returns the ``W-2`` interior residuals
|
|
101
|
+
and their propagators.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
r : ``(W-2, d)``
|
|
106
|
+
alpha_plus, alpha_zero, alpha_minus : ``(W-2, d, d)``
|
|
107
|
+
Jvv_out : ``(W-2, d, d)``
|
|
108
|
+
v_hat : ``(W-2, d)`` shooting velocities (for velocity-dependent D)
|
|
109
|
+
"""
|
|
110
|
+
def _step(Yp, Yc, Yn):
|
|
111
|
+
return ud_step_quantities(force_fn, Yp, Yc, Yn, dt, n_substeps, integrator)
|
|
112
|
+
|
|
113
|
+
return jax.vmap(_step)(Y_w[:-2], Y_w[1:-1], Y_w[2:])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ud_instrument(force_of_theta, theta, Y_a, Y_b, dt, n_substeps, integrator="rk4", n_predict=2):
|
|
117
|
+
r"""η-clean instrument (left test function) for one underdamped residual.
|
|
118
|
+
|
|
119
|
+
The center 3-point residual ``r_c = Y_{c+2} − Φˣ(Y_{c+1}, v̂)`` is
|
|
120
|
+
contaminated by measurement noise at ``Y_c, Y_{c+1}, Y_{c+2}`` through the
|
|
121
|
+
shooting velocity ``v̂`` (reconstructed from ``Y_c, Y_{c+1}``) — the
|
|
122
|
+
errors-in-variables source that is *amplified* in the underdamped case
|
|
123
|
+
because ``v̂ ∼ ΔY/Δt`` divides the position noise by ``Δt``.
|
|
124
|
+
|
|
125
|
+
The instrument is the regressor ``∂r/∂θ = −∂Φˣ(Y_{c+1}, v̂)/∂θ`` evaluated
|
|
126
|
+
at a **clean phase point** instead of the noisy ``(Y_{c+1}, v̂)``. The clean
|
|
127
|
+
phase point is reconstructed from the lagged pair ``(Y_a, Y_b) =
|
|
128
|
+
(Y_{c-2}, Y_{c-1})`` — whose measurement noise is independent of the
|
|
129
|
+
residual's — by shooting the velocity at ``Y_b`` and phase-flowing forward
|
|
130
|
+
``n_predict`` intervals to the center base; that point is held fixed
|
|
131
|
+
(stop-gradient, "data"), and only the final one-step position displacement
|
|
132
|
+
is differentiated in θ:
|
|
133
|
+
|
|
134
|
+
.. math:: ψ_{\mathrm{inst}} = -\left.\frac{∂Φˣ(x,v;θ)}{∂θ}\right|_{(\tilde x_{c+1},\,\tilde v_{c+1})}.
|
|
135
|
+
|
|
136
|
+
Because it is the *same* one-step displacement sensitivity as the regressor
|
|
137
|
+
(only the evaluation phase point is clean), every parameter column — in
|
|
138
|
+
particular the velocity-damping (``γ``) column, which depends on the
|
|
139
|
+
reconstructed velocity — keeps the regressor's scale, so the asymmetric
|
|
140
|
+
Gram stays well-conditioned. It is the underdamped analogue of the
|
|
141
|
+
overdamped lag-1 shift instrument :func:`flow.od_instrument`.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
force_of_theta : callable ``θ_flat → (force_fn : (x, v) → accel)``
|
|
146
|
+
theta : array ``(n_params,)``
|
|
147
|
+
Y_a, Y_b : arrays ``(d,)`` clean lagged pair ``(Y_{c-2}, Y_{c-1})``.
|
|
148
|
+
n_predict : int phase-flow steps from ``Y_b`` to the center base
|
|
149
|
+
(``= 2`` for the centered 7-residual window: ``Y_{c-1} → Y_{c+1}``).
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
psi_inst : array ``(d, n_params)``
|
|
154
|
+
"""
|
|
155
|
+
force0 = force_of_theta(theta)
|
|
156
|
+
v_b, *_ = shooting_velocity(force0, Y_a, Y_b, dt, n_substeps, integrator)
|
|
157
|
+
x, v = Y_b, v_b
|
|
158
|
+
for _ in range(n_predict):
|
|
159
|
+
x, v, *_ = phase_space_flow_jac(force0, x, v, dt, n_substeps, integrator)
|
|
160
|
+
x_c = jax.lax.stop_gradient(x)
|
|
161
|
+
v_c = jax.lax.stop_gradient(v)
|
|
162
|
+
|
|
163
|
+
def disp_x(th):
|
|
164
|
+
x_next, _, *_ = phase_space_flow_jac(
|
|
165
|
+
force_of_theta(th), x_c, v_c, dt, n_substeps, integrator)
|
|
166
|
+
return x_next
|
|
167
|
+
|
|
168
|
+
return -jax.jacfwd(disp_x)(theta)
|