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,286 @@
|
|
|
1
|
+
# SFI/inference/parametric_core/objective.py
|
|
2
|
+
"""
|
|
3
|
+
Integrate-engine programs for the overdamped parametric core.
|
|
4
|
+
|
|
5
|
+
Each program declares the sliding-window stream ``X_window:{W}`` and
|
|
6
|
+
returns one value **per (window, particle)** ``(K, N)``; the engine owns
|
|
7
|
+
chunking, masking (``mask_out``), JIT, and the reduction over both window
|
|
8
|
+
centers and particles (``reduce_over_particles=True``). Single particle
|
|
9
|
+
is just ``N=1`` — there is no separate multiparticle path (cf.
|
|
10
|
+
``infer_force_linear``).
|
|
11
|
+
|
|
12
|
+
Per particle the residuals/Jacobians come from :func:`flow_multi.
|
|
13
|
+
multi_step_residuals` (per-particle for non-interacting forces, full-flow
|
|
14
|
+
+ frozen-background Jacobian for interacting ones); the covariance and
|
|
15
|
+
precision kernels are applied per particle.
|
|
16
|
+
|
|
17
|
+
* :class:`ODLossProgram` — frozen-precision quadratic (minimised over θ).
|
|
18
|
+
* :class:`ODCondNLLProgram` — windowed conditional NLL (profiling / joint).
|
|
19
|
+
* :class:`ODDiffNLLProgram` — conditional NLL for state-dependent D(x; θ_D).
|
|
20
|
+
* :class:`ODGramProgram` — GN Gram/score/NLL for the parameter covariance.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
|
|
28
|
+
from .covariance import build_od_blocks
|
|
29
|
+
from .flow_multi import multi_od_instrument, multi_step_residuals, multi_step_residuals_with_psi
|
|
30
|
+
from .precision import (
|
|
31
|
+
center_conditional_nll_contribution,
|
|
32
|
+
center_gram_contribution,
|
|
33
|
+
center_loss_contribution,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ["ODLossProgram", "ODGramProgram", "ODCondNLLProgram", "ODDiffNLLProgram", "unpack_gram"]
|
|
37
|
+
|
|
38
|
+
_BANDWIDTH = 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unpack_gram(packed, n_params):
|
|
42
|
+
"""Unpack a flat ``(G, f, H, nll)`` vector from :class:`ODGramProgram`.
|
|
43
|
+
|
|
44
|
+
``H = ψ_leftᵀPψ_left`` is the estimating-function variance (the sandwich
|
|
45
|
+
meat for the IV path; ``H = G`` on the symmetric path).
|
|
46
|
+
"""
|
|
47
|
+
n2 = n_params * n_params
|
|
48
|
+
G = packed[:n2].reshape(n_params, n_params)
|
|
49
|
+
f = packed[n2:n2 + n_params]
|
|
50
|
+
H = packed[n2 + n_params:2 * n2 + n_params].reshape(n_params, n_params)
|
|
51
|
+
return G, f, H, packed[-1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _center_index(W_res):
|
|
55
|
+
return (W_res - 1) // 2
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _per_particle_loss(r, J, D, Lambda, dt, jitter, jitter_chol):
|
|
59
|
+
"""``½ rᵀ P r`` (center decomposition) per particle → ``(N,)``."""
|
|
60
|
+
c = _center_index(r.shape[0])
|
|
61
|
+
|
|
62
|
+
def _p(rp, Jp):
|
|
63
|
+
blocks = build_od_blocks(Jp, D, Lambda, dt, jitter=jitter)
|
|
64
|
+
return center_loss_contribution(blocks.A, blocks.offdiag, rp, c, jitter_chol, _BANDWIDTH)
|
|
65
|
+
|
|
66
|
+
return jax.vmap(_p, in_axes=(1, 1))(r, J)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _per_particle_condnll(r, J, D, Lambda, dt, jitter, jitter_chol, n_cond):
|
|
70
|
+
"""Windowed conditional NLL per particle (last residual | n_cond past) → ``(N,)``."""
|
|
71
|
+
def _p(rp, Jp):
|
|
72
|
+
blocks = build_od_blocks(Jp, D, Lambda, dt, jitter=jitter)
|
|
73
|
+
return center_conditional_nll_contribution(blocks.A, blocks.offdiag, rp, n_cond, n_cond, jitter_chol)
|
|
74
|
+
|
|
75
|
+
return jax.vmap(_p, in_axes=(1, 1))(r, J)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ODProgramBase:
|
|
79
|
+
def __init__(self, F_psf, *, dt, n_substeps, jitter=1e-7, jitter_chol=1e-10,
|
|
80
|
+
extra_radius=1, integrator="rk4"):
|
|
81
|
+
self._F = F_psf
|
|
82
|
+
self._dt = dt
|
|
83
|
+
self._n_sub = n_substeps
|
|
84
|
+
self._jitter = jitter
|
|
85
|
+
self._jitter_chol = jitter_chol
|
|
86
|
+
self._integrator = integrator
|
|
87
|
+
self._n_params = int(F_psf.template.size)
|
|
88
|
+
W_pos = 2 * _BANDWIDTH + 2 * extra_radius + 2
|
|
89
|
+
self._stream_key = f"X_window:{W_pos}"
|
|
90
|
+
|
|
91
|
+
def require(self):
|
|
92
|
+
req = {self._stream_key}
|
|
93
|
+
if getattr(self._F, "required_extras", None):
|
|
94
|
+
req.add("extras")
|
|
95
|
+
return req
|
|
96
|
+
|
|
97
|
+
def _resid(self, theta, X_w, extras):
|
|
98
|
+
return multi_step_residuals(
|
|
99
|
+
self._F, self._F.unflatten_params(theta), X_w, extras,
|
|
100
|
+
self._dt, self._n_sub, self._integrator)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ODLossProgram(_ODProgramBase):
|
|
104
|
+
"""Per-(window, particle) frozen-precision quadratic ``½ rᵀ P r``."""
|
|
105
|
+
|
|
106
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
107
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
108
|
+
|
|
109
|
+
def batch_call(self, *, params, **streams):
|
|
110
|
+
theta_live, theta_frozen, D, Lambda = params
|
|
111
|
+
X_w = streams[self._stream_key] # (K, W, N, d)
|
|
112
|
+
extras = streams.get("extras")
|
|
113
|
+
|
|
114
|
+
def _window(xw): # (W, N, d)
|
|
115
|
+
r, _ = self._resid(theta_live, xw, extras)
|
|
116
|
+
_, J = self._resid(jax.lax.stop_gradient(theta_frozen), xw, extras)
|
|
117
|
+
J = jax.lax.stop_gradient(J)
|
|
118
|
+
return _per_particle_loss(r, J, D, Lambda, self._dt, self._jitter, self._jitter_chol)
|
|
119
|
+
|
|
120
|
+
return jax.vmap(_window)(X_w) # (K, N)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ODCondNLLProgram(_ODProgramBase):
|
|
124
|
+
"""Per-(window, particle) conditional NLL with (θ, D, Λ) live."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, F_psf, *, n_cond=3, **kw):
|
|
127
|
+
super().__init__(F_psf, **kw)
|
|
128
|
+
self._n_cond = int(n_cond)
|
|
129
|
+
self._stream_key = f"X_window:{n_cond + 2}"
|
|
130
|
+
|
|
131
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
132
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
133
|
+
|
|
134
|
+
def batch_call(self, *, params, **streams):
|
|
135
|
+
theta, D, Lambda = params
|
|
136
|
+
X_w = streams[self._stream_key]
|
|
137
|
+
extras = streams.get("extras")
|
|
138
|
+
|
|
139
|
+
def _window(xw):
|
|
140
|
+
r, J = self._resid(theta, xw, extras)
|
|
141
|
+
return _per_particle_condnll(
|
|
142
|
+
r, J, D, Lambda, self._dt, self._jitter, self._jitter_chol, self._n_cond)
|
|
143
|
+
|
|
144
|
+
return jax.vmap(_window)(X_w)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ODDiffNLLProgram(_ODProgramBase):
|
|
148
|
+
"""Conditional NLL for state-dependent diffusion ``D(x; θ_D)`` (force fixed)."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, F_psf, theta_F, D_psf, *, n_cond=3, **kw):
|
|
151
|
+
super().__init__(F_psf, **kw)
|
|
152
|
+
self._theta_F = theta_F
|
|
153
|
+
self._D_psf = D_psf
|
|
154
|
+
self._n_cond = int(n_cond)
|
|
155
|
+
self._stream_key = f"X_window:{n_cond + 2}"
|
|
156
|
+
|
|
157
|
+
def require(self):
|
|
158
|
+
req = {self._stream_key}
|
|
159
|
+
if getattr(self._F, "required_extras", None) or getattr(self._D_psf, "required_extras", None):
|
|
160
|
+
req.add("extras")
|
|
161
|
+
return req
|
|
162
|
+
|
|
163
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
164
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
165
|
+
|
|
166
|
+
def batch_call(self, *, params, **streams):
|
|
167
|
+
theta_D, Lambda = params
|
|
168
|
+
X_w = streams[self._stream_key]
|
|
169
|
+
extras = streams.get("extras")
|
|
170
|
+
D_struct = self._D_psf.unflatten_params(theta_D)
|
|
171
|
+
|
|
172
|
+
def _D(x):
|
|
173
|
+
return self._D_psf(x[None], params=D_struct, extras=extras)[0]
|
|
174
|
+
|
|
175
|
+
def _window(xw): # (W, N, d)
|
|
176
|
+
r, J = self._resid(self._theta_F, xw, extras) # (W-1,N,d),(W-1,N,d,d)
|
|
177
|
+
n_cond = self._n_cond
|
|
178
|
+
|
|
179
|
+
def _p(rp, Jp, Xp): # (W-1,d),(W-1,d,d),(W,d)
|
|
180
|
+
Dk = jax.vmap(_D)(Xp[:-1]) # (W-1, d, d) state-dependent
|
|
181
|
+
blocks = build_od_blocks(Jp, Dk, Lambda, self._dt, jitter=self._jitter)
|
|
182
|
+
return center_conditional_nll_contribution(
|
|
183
|
+
blocks.A, blocks.offdiag, rp, n_cond, n_cond, self._jitter_chol)
|
|
184
|
+
|
|
185
|
+
return jax.vmap(_p, in_axes=(1, 1, 1))(r, J, xw) # (N,)
|
|
186
|
+
|
|
187
|
+
return jax.vmap(_window)(X_w)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ODGramProgram(_ODProgramBase):
|
|
191
|
+
"""Per-(window, particle) GN Gram/score/NLL packed as ``(G, f, nll)``.
|
|
192
|
+
|
|
193
|
+
The estimating equation is ``⟨ψ_left P r⟩ = 0`` with regressor
|
|
194
|
+
``ψ_right = ∂r/∂θ``. With ``w = 0`` (default) the left factor is the
|
|
195
|
+
regressor — the symmetric MLE Gram, byte-identical to before. With
|
|
196
|
+
``w > 0`` the *center* left factor is blended toward an η-clean instrument,
|
|
197
|
+
|
|
198
|
+
ψ_left,c = (1 − w) ψ_right,c + w ψ_inst,c,
|
|
199
|
+
|
|
200
|
+
which removes the measurement-noise errors-in-variables bias (``w → 1`` is
|
|
201
|
+
the pure instrument). ``w`` is a host-side scalar set from the EIV ratio.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, F_psf, *, w=0.0, extra_radius=1, **kw):
|
|
205
|
+
super().__init__(F_psf, extra_radius=extra_radius, **kw)
|
|
206
|
+
self._w = float(w)
|
|
207
|
+
if self._w > 0.0:
|
|
208
|
+
# The η-clean instrument reserves 1 front position OUTSIDE the
|
|
209
|
+
# residual block the tridiagonal precision couples in (paired against
|
|
210
|
+
# the first block residual); widen the window to hold it. The w=0
|
|
211
|
+
# (MLE) pass keeps the efficient center window.
|
|
212
|
+
self._stream_key = f"X_window:{2 * _BANDWIDTH + 2 * extra_radius + 2 + 1}"
|
|
213
|
+
|
|
214
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
215
|
+
n = self._n_params
|
|
216
|
+
shape = sample_row[self._stream_key].shape # (W, N, d)
|
|
217
|
+
N = int(shape[-2])
|
|
218
|
+
out = N * (2 * n * n + n + 1) * 8
|
|
219
|
+
if getattr(self._F, "particles_input", False):
|
|
220
|
+
# Interacting working set: per-stage dF/dθ jacfwd transient over
|
|
221
|
+
# the pair graph (~N² pair terms × n tangents), 4 RK4 stages ×
|
|
222
|
+
# n_substeps, per window residual. Without this the output-only
|
|
223
|
+
# estimate lets the whole trajectory into one chunk and the
|
|
224
|
+
# working set blows up (FINDINGS #7).
|
|
225
|
+
W = int(shape[-3])
|
|
226
|
+
out += W * 4 * self._n_sub * N * N * max(n, 8) * 8
|
|
227
|
+
return int(out)
|
|
228
|
+
|
|
229
|
+
def batch_call(self, *, params, **streams):
|
|
230
|
+
theta_flat, D, Lambda = params
|
|
231
|
+
X_w = streams[self._stream_key]
|
|
232
|
+
extras = streams.get("extras")
|
|
233
|
+
w = self._w
|
|
234
|
+
interacting = getattr(self._F, "particles_input", False)
|
|
235
|
+
|
|
236
|
+
# When the instrument is active (w>0) the base is the reserved front
|
|
237
|
+
# position, OUTSIDE the tridiagonal-coupled block (η-clean of the whole
|
|
238
|
+
# block), paired against the first block residual.
|
|
239
|
+
skip = w > 0.0
|
|
240
|
+
|
|
241
|
+
def _window(xw):
|
|
242
|
+
xw_res = xw[1:] if skip else xw # block (skip the front position)
|
|
243
|
+
|
|
244
|
+
if interacting:
|
|
245
|
+
# Frozen-background ψ_right (same-particle θ-recursion): the
|
|
246
|
+
# full-flow jacfwd holds n_params tangents alive across the
|
|
247
|
+
# whole window graph — O(N²·n_params) per window — and is the
|
|
248
|
+
# interacting-path memory blow-up (FINDINGS #7).
|
|
249
|
+
r, J, psi = multi_step_residuals_with_psi(
|
|
250
|
+
self._F, theta_flat, xw_res, extras,
|
|
251
|
+
self._dt, self._n_sub, self._integrator)
|
|
252
|
+
else:
|
|
253
|
+
def resid(th):
|
|
254
|
+
r, _ = self._resid(th, xw_res, extras)
|
|
255
|
+
return r # (R, N, d)
|
|
256
|
+
|
|
257
|
+
r = resid(theta_flat)
|
|
258
|
+
psi = jax.jacfwd(resid)(theta_flat) # (R, N, d, n_params) = ψ_right
|
|
259
|
+
_, J = self._resid(theta_flat, xw_res, extras)
|
|
260
|
+
c = 0 if skip else _center_index(r.shape[0])
|
|
261
|
+
|
|
262
|
+
if w > 0.0:
|
|
263
|
+
# instrument base: the reserved front X[0], η-clean of the whole
|
|
264
|
+
# block. Flow-propagated trapeze to the first block residual;
|
|
265
|
+
# interacting models use the same N-body flow as the residual.
|
|
266
|
+
psi_inst = multi_od_instrument(
|
|
267
|
+
self._F, theta_flat, xw[0], extras,
|
|
268
|
+
self._dt, self._n_sub, self._integrator) # (N, d, n_params)
|
|
269
|
+
|
|
270
|
+
def _p(rp, psip, Jp, inst):
|
|
271
|
+
blocks = build_od_blocks(Jp, D, Lambda, self._dt, jitter=self._jitter)
|
|
272
|
+
psi_left = psip.at[c].set((1.0 - w) * psip[c] + w * inst)
|
|
273
|
+
G, f, H, nll = center_gram_contribution(
|
|
274
|
+
blocks.A, blocks.offdiag, rp, psi_left, c, self._jitter_chol, psi_right_w=psip)
|
|
275
|
+
return jnp.concatenate([G.ravel(), f, H.ravel(), nll[None]])
|
|
276
|
+
|
|
277
|
+
def _p_sym(rp, psip, Jp):
|
|
278
|
+
blocks = build_od_blocks(Jp, D, Lambda, self._dt, jitter=self._jitter)
|
|
279
|
+
G, f, H, nll = center_gram_contribution(blocks.A, blocks.offdiag, rp, psip, c, self._jitter_chol)
|
|
280
|
+
return jnp.concatenate([G.ravel(), f, H.ravel(), nll[None]])
|
|
281
|
+
|
|
282
|
+
if w > 0.0:
|
|
283
|
+
return jax.vmap(_p, in_axes=(1, 1, 1, 0))(r, psi, J, psi_inst)
|
|
284
|
+
return jax.vmap(_p_sym, in_axes=(1, 1, 1))(r, psi, J) # (N, n_params²+n_params+1)
|
|
285
|
+
|
|
286
|
+
return jax.vmap(_window)(X_w) # (K, N, packed)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# SFI/inference/parametric_core/objective_ud.py
|
|
2
|
+
"""
|
|
3
|
+
Integrate-engine programs for the underdamped parametric core (bandwidth 2).
|
|
4
|
+
|
|
5
|
+
Mirror of :mod:`objective` (overdamped): each program returns one value
|
|
6
|
+
**per (window, particle)** ``(K, N)`` and runs with
|
|
7
|
+
``reduce_over_particles=True`` (single particle = N=1). Per particle the
|
|
8
|
+
3-point shooting residual + α-propagators come from
|
|
9
|
+
:func:`flow_multi.ud_multi_step_residuals`; the pentadiagonal covariance
|
|
10
|
+
(:func:`covariance.build_ud_blocks`) and the bandwidth-generic precision
|
|
11
|
+
kernels are applied per particle.
|
|
12
|
+
|
|
13
|
+
The force is a velocity-dependent PSF (``needs_v=True``); the unobserved
|
|
14
|
+
velocity is resolved internally by shooting. Interacting underdamped
|
|
15
|
+
multiparticle (multi-particle phase-space shooting) is a follow-up.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import jax
|
|
21
|
+
import jax.numpy as jnp
|
|
22
|
+
|
|
23
|
+
from .covariance import build_ud_blocks
|
|
24
|
+
from .flow_multi import multi_ud_instrument, ud_multi_step_residuals, ud_multi_step_residuals_with_psi
|
|
25
|
+
from .objective import unpack_gram # shared packing
|
|
26
|
+
from .precision import (
|
|
27
|
+
center_conditional_nll_contribution,
|
|
28
|
+
center_gram_contribution,
|
|
29
|
+
center_loss_contribution,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = ["UDLossProgram", "UDCondNLLProgram", "UDGramProgram", "UDDiffNLLProgram", "unpack_gram"]
|
|
33
|
+
|
|
34
|
+
_BANDWIDTH = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ud_force_struct(F_psf, theta):
|
|
38
|
+
return F_psf.unflatten_params(theta)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _UDProgramBase:
|
|
42
|
+
def __init__(self, F_psf, *, dt, n_substeps, jitter=1e-7, jitter_chol=1e-10, integrator="rk4"):
|
|
43
|
+
self._F = F_psf
|
|
44
|
+
self._dt = dt
|
|
45
|
+
self._n_sub = n_substeps
|
|
46
|
+
self._jitter = jitter
|
|
47
|
+
self._jitter_chol = jitter_chol
|
|
48
|
+
self._integrator = integrator
|
|
49
|
+
self._n_params = int(F_psf.template.size)
|
|
50
|
+
|
|
51
|
+
def require(self):
|
|
52
|
+
req = {self._stream_key}
|
|
53
|
+
if getattr(self._F, "required_extras", None):
|
|
54
|
+
req.add("extras")
|
|
55
|
+
return req
|
|
56
|
+
|
|
57
|
+
def _resid(self, theta, Y_w, extras):
|
|
58
|
+
return ud_multi_step_residuals(
|
|
59
|
+
self._F, self._F.unflatten_params(theta), Y_w, extras,
|
|
60
|
+
self._dt, self._n_sub, self._integrator)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UDLossProgram(_UDProgramBase):
|
|
64
|
+
"""Per-(window, particle) frozen-precision quadratic ``½ rᵀ P r`` (bandwidth 2)."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, F_psf, *, w_res=7, **kw):
|
|
67
|
+
super().__init__(F_psf, **kw)
|
|
68
|
+
self._center = w_res // 2
|
|
69
|
+
self._stream_key = f"X_window:{w_res + 2}"
|
|
70
|
+
|
|
71
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
72
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
73
|
+
|
|
74
|
+
def batch_call(self, *, params, **streams):
|
|
75
|
+
theta_live, theta_frozen, D, Lambda = params
|
|
76
|
+
Y_w = streams[self._stream_key]
|
|
77
|
+
extras = streams.get("extras")
|
|
78
|
+
c = self._center
|
|
79
|
+
|
|
80
|
+
def _window(yw):
|
|
81
|
+
r, _, _, _, _ = self._resid(theta_live, yw, extras)
|
|
82
|
+
_, ap, a0, am, _ = self._resid(jax.lax.stop_gradient(theta_frozen), yw, extras)
|
|
83
|
+
ap, a0, am = (jax.lax.stop_gradient(x) for x in (ap, a0, am))
|
|
84
|
+
|
|
85
|
+
def _p(rp, app, a0p, amp):
|
|
86
|
+
blocks = build_ud_blocks(app, a0p, amp, D, Lambda, self._dt, jitter=self._jitter)
|
|
87
|
+
return center_loss_contribution(blocks.A, blocks.offdiag, rp, c, self._jitter_chol, _BANDWIDTH)
|
|
88
|
+
|
|
89
|
+
return jax.vmap(_p, in_axes=(1, 1, 1, 1))(r, ap, a0, am)
|
|
90
|
+
|
|
91
|
+
return jax.vmap(_window)(Y_w)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class UDCondNLLProgram(_UDProgramBase):
|
|
95
|
+
"""Per-(window, particle) conditional NLL — profiling / joint."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, F_psf, *, n_cond=4, **kw):
|
|
98
|
+
super().__init__(F_psf, **kw)
|
|
99
|
+
self._n_cond = int(n_cond)
|
|
100
|
+
self._stream_key = f"X_window:{n_cond + 3}"
|
|
101
|
+
|
|
102
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
103
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
104
|
+
|
|
105
|
+
def batch_call(self, *, params, **streams):
|
|
106
|
+
theta, D, Lambda = params
|
|
107
|
+
Y_w = streams[self._stream_key]
|
|
108
|
+
extras = streams.get("extras")
|
|
109
|
+
n_cond = self._n_cond
|
|
110
|
+
|
|
111
|
+
def _window(yw):
|
|
112
|
+
r, ap, a0, am, _ = self._resid(theta, yw, extras)
|
|
113
|
+
|
|
114
|
+
def _p(rp, app, a0p, amp):
|
|
115
|
+
blocks = build_ud_blocks(app, a0p, amp, D, Lambda, self._dt, jitter=self._jitter)
|
|
116
|
+
return center_conditional_nll_contribution(
|
|
117
|
+
blocks.A, blocks.offdiag, rp, n_cond, n_cond, self._jitter_chol)
|
|
118
|
+
|
|
119
|
+
return jax.vmap(_p, in_axes=(1, 1, 1, 1))(r, ap, a0, am)
|
|
120
|
+
|
|
121
|
+
return jax.vmap(_window)(Y_w)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class UDDiffNLLProgram(_UDProgramBase):
|
|
125
|
+
"""Conditional NLL for state/velocity-dependent diffusion ``D(x, v; θ_D)`` (force fixed)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, F_psf, theta_F, D_psf, *, n_cond=4, **kw):
|
|
128
|
+
super().__init__(F_psf, **kw)
|
|
129
|
+
self._theta_F = theta_F
|
|
130
|
+
self._D_psf = D_psf
|
|
131
|
+
self._n_cond = int(n_cond)
|
|
132
|
+
self._stream_key = f"X_window:{n_cond + 3}"
|
|
133
|
+
|
|
134
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
135
|
+
return int(sample_row[self._stream_key].shape[-2]) * 8
|
|
136
|
+
|
|
137
|
+
def batch_call(self, *, params, **streams):
|
|
138
|
+
theta_D, Lambda = params
|
|
139
|
+
Y_w = streams[self._stream_key]
|
|
140
|
+
extras = streams.get("extras")
|
|
141
|
+
n_cond = self._n_cond
|
|
142
|
+
D_struct = self._D_psf.unflatten_params(theta_D)
|
|
143
|
+
|
|
144
|
+
def _D(x, v):
|
|
145
|
+
return self._D_psf(x[None], v=v[None], params=D_struct, extras=extras)[0]
|
|
146
|
+
|
|
147
|
+
def _window(yw): # (W, N, d)
|
|
148
|
+
r, ap, a0, am, vhat = self._resid(self._theta_F, yw, extras)
|
|
149
|
+
Y_base = yw[1:-1] # (W-2, N, d)
|
|
150
|
+
|
|
151
|
+
def _p(rp, app, a0p, amp, vhatp, Ybp):
|
|
152
|
+
Dk = jax.vmap(_D)(Ybp, vhatp) # (W-2, d, d)
|
|
153
|
+
blocks = build_ud_blocks(app, a0p, amp, Dk, Lambda, self._dt, jitter=self._jitter)
|
|
154
|
+
return center_conditional_nll_contribution(
|
|
155
|
+
blocks.A, blocks.offdiag, rp, n_cond, n_cond, self._jitter_chol)
|
|
156
|
+
|
|
157
|
+
return jax.vmap(_p, in_axes=(1, 1, 1, 1, 1, 1))(r, ap, a0, am, vhat, Y_base)
|
|
158
|
+
|
|
159
|
+
return jax.vmap(_window)(Y_w)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UDGramProgram(_UDProgramBase):
|
|
163
|
+
"""Per-(window, particle) GN Gram/score/NLL packed as ``(G, f, nll)``.
|
|
164
|
+
|
|
165
|
+
``w`` is the EIV instrument blend weight (0 = symmetric MLE). The
|
|
166
|
+
underdamped η-clean instrument is wired in below when ``w > 0``.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, F_psf, *, w_res=7, w=0.0, **kw):
|
|
170
|
+
super().__init__(F_psf, **kw)
|
|
171
|
+
self._center = w_res // 2
|
|
172
|
+
self._w = float(w)
|
|
173
|
+
# The η-clean instrument (w>0) reserves 2 extra front positions for its
|
|
174
|
+
# base (outside the residual block the pentadiagonal precision couples
|
|
175
|
+
# in), so widen the window to keep the block — and hence the precision
|
|
176
|
+
# context / variance — unchanged. The w=0 (MLE) pass uses the narrow window.
|
|
177
|
+
extra = 2 if self._w > 0.0 else 0
|
|
178
|
+
self._stream_key = f"X_window:{w_res + 2 + extra}"
|
|
179
|
+
|
|
180
|
+
def estimate_bytes_per_sample(self, sample_row):
|
|
181
|
+
n = self._n_params
|
|
182
|
+
shape = sample_row[self._stream_key].shape # (W, N, d)
|
|
183
|
+
N = int(shape[-2])
|
|
184
|
+
out = N * (2 * n * n + n + 1) * 8
|
|
185
|
+
if getattr(self._F, "particles_input", False):
|
|
186
|
+
# Interacting working set (see ODGramProgram): per-stage dF/dθ
|
|
187
|
+
# transient over the pair graph, two phase flows per residual.
|
|
188
|
+
W = int(shape[-3])
|
|
189
|
+
out += W * 8 * self._n_sub * N * N * max(n, 8) * 8
|
|
190
|
+
return int(out)
|
|
191
|
+
|
|
192
|
+
def batch_call(self, *, params, **streams):
|
|
193
|
+
theta_flat, D, Lambda = params
|
|
194
|
+
Y_w = streams[self._stream_key]
|
|
195
|
+
extras = streams.get("extras")
|
|
196
|
+
w = self._w
|
|
197
|
+
interacting = getattr(self._F, "particles_input", False)
|
|
198
|
+
# When the instrument is active (w>0) the window is [2 front positions |
|
|
199
|
+
# residual block]: the instrument base pair (the front positions) is then
|
|
200
|
+
# OUTSIDE the whole block's measurement-noise support, so it cannot overlap
|
|
201
|
+
# any residual the pentadiagonal precision couples into the moment (the
|
|
202
|
+
# legacy first-row IV), and is paired against the first block residual.
|
|
203
|
+
# The w=0 (MLE) pass uses the narrow center window.
|
|
204
|
+
skip = w > 0.0
|
|
205
|
+
# center index: first block residual when the instrument is active; middle
|
|
206
|
+
# residual otherwise (derived from the actual window width so it is correct
|
|
207
|
+
# for both the narrow MLE window and the wider instrument one).
|
|
208
|
+
W_pos = Y_w.shape[1]
|
|
209
|
+
c = 0 if skip else (W_pos - 3) // 2
|
|
210
|
+
|
|
211
|
+
def _window(yw):
|
|
212
|
+
yw_res = yw[2:] if skip else yw # block positions (skip the front pair)
|
|
213
|
+
|
|
214
|
+
if interacting:
|
|
215
|
+
# Frozen-background ψ_right (per-particle phase θ-recursion) —
|
|
216
|
+
# see ODGramProgram; the full-flow jacfwd is the interacting
|
|
217
|
+
# memory blow-up.
|
|
218
|
+
r, ap, a0, am, _, psi = ud_multi_step_residuals_with_psi(
|
|
219
|
+
self._F, theta_flat, yw_res, extras,
|
|
220
|
+
self._dt, self._n_sub, self._integrator)
|
|
221
|
+
else:
|
|
222
|
+
def resid(th):
|
|
223
|
+
r, *_ = self._resid(th, yw_res, extras)
|
|
224
|
+
return r # (R, N, d) = ψ_right base
|
|
225
|
+
r = resid(theta_flat)
|
|
226
|
+
psi = jax.jacfwd(resid)(theta_flat) # (R, N, d, n_params) = ψ_right
|
|
227
|
+
_, ap, a0, am, _ = self._resid(theta_flat, yw_res, extras)
|
|
228
|
+
|
|
229
|
+
if w > 0.0:
|
|
230
|
+
# Instrument base pair: the reserved front pair (yw[0], yw[1]),
|
|
231
|
+
# η-clean of the whole block. Flow-propagated to the c-th residual;
|
|
232
|
+
# interacting models use the same N-body flow as the residual.
|
|
233
|
+
psi_inst = multi_ud_instrument(
|
|
234
|
+
self._F, theta_flat, yw[0], yw[1], extras,
|
|
235
|
+
self._dt, self._n_sub, self._integrator) # (N, d, n_params)
|
|
236
|
+
|
|
237
|
+
def _p(rp, psip, app, a0p, amp, inst):
|
|
238
|
+
blocks = build_ud_blocks(app, a0p, amp, D, Lambda, self._dt, jitter=self._jitter)
|
|
239
|
+
psi_left = psip.at[c].set((1.0 - w) * psip[c] + w * inst)
|
|
240
|
+
G, f, H, nll = center_gram_contribution(
|
|
241
|
+
blocks.A, blocks.offdiag, rp, psi_left, c, self._jitter_chol, psi_right_w=psip)
|
|
242
|
+
return jnp.concatenate([G.ravel(), f, H.ravel(), nll[None]])
|
|
243
|
+
|
|
244
|
+
def _p_sym(rp, psip, app, a0p, amp):
|
|
245
|
+
blocks = build_ud_blocks(app, a0p, amp, D, Lambda, self._dt, jitter=self._jitter)
|
|
246
|
+
G, f, H, nll = center_gram_contribution(blocks.A, blocks.offdiag, rp, psip, c, self._jitter_chol)
|
|
247
|
+
return jnp.concatenate([G.ravel(), f, H.ravel(), nll[None]])
|
|
248
|
+
|
|
249
|
+
if w > 0.0:
|
|
250
|
+
return jax.vmap(_p, in_axes=(1, 1, 1, 1, 1, 0))(r, psi, ap, a0, am, psi_inst)
|
|
251
|
+
return jax.vmap(_p_sym, in_axes=(1, 1, 1, 1, 1))(r, psi, ap, a0, am)
|
|
252
|
+
|
|
253
|
+
return jax.vmap(_window)(Y_w)
|