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,540 @@
|
|
|
1
|
+
# SFI/inference/parametric_core/jacobians.py
|
|
2
|
+
"""
|
|
3
|
+
RK4-composed per-particle Jacobians for interacting (multi-particle) systems.
|
|
4
|
+
|
|
5
|
+
Moved verbatim from the legacy parametric package (``_od_precompute`` /
|
|
6
|
+
``_underdamped``) — the frozen-background approximation: the state update
|
|
7
|
+
uses the full multi-particle force, while the tangent is propagated per
|
|
8
|
+
particle from the same-particle derivatives, keeping the flow Jacobian
|
|
9
|
+
block-diagonal in the particle axis and the cost O(N) per window.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from jax import lax
|
|
16
|
+
|
|
17
|
+
__all__ = ["rk4_composed_jacobian", "rk4_composed_jacobian_theta",
|
|
18
|
+
"rk4_composed_jacobian_phase", "rk4_composed_jacobian_phase_theta"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rk4_composed_jacobian_theta(F_psf, dFdx_expr, dFdth_fn, n_params, X_start, theta_struct,
|
|
22
|
+
mask, extras, dt, n_substeps, integrator="rk4"):
|
|
23
|
+
r"""Per-particle flow Jacobian, displacement, **and θ-sensitivity** ``B``.
|
|
24
|
+
|
|
25
|
+
Same frozen-background RK4 composition as :func:`rk4_composed_jacobian`,
|
|
26
|
+
additionally carrying the per-particle parameter sensitivity
|
|
27
|
+
|
|
28
|
+
.. math:: B_p = \partial \Phi_p / \partial \theta \in (N, d, n_\theta)
|
|
29
|
+
|
|
30
|
+
through the classic variation-of-constants recursion (the legacy
|
|
31
|
+
``_compute_jb_single_particle`` trick, RK4-composed): per stage
|
|
32
|
+
``dk_s/dθ = Θ_s + c_s h A_s dk_{s-1}/dθ`` with ``Θ = ∂F/∂θ`` at the
|
|
33
|
+
*fixed* stage frame (exact — the force's direct θ-dependence is
|
|
34
|
+
per-particle), and per substep ``B ← J_step B + B_step``. The dropped
|
|
35
|
+
cross-particle paths (θ wiggling a *neighbour's* substep motion, which
|
|
36
|
+
feeds back within one Δt) are the same O(h²·coupling) terms the
|
|
37
|
+
frozen-background ``J`` already drops. Peak memory is
|
|
38
|
+
O(N·d·n_θ) instead of forward-mode AD's n_θ tangents through the full
|
|
39
|
+
N-body window flow.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
dFdth_fn : callable ``(N, d) frame → (N, d, n_params)``
|
|
44
|
+
Direct parameter derivative of the force at a fixed frame
|
|
45
|
+
(e.g. ``jax.jacfwd`` over θ of the frame-level force).
|
|
46
|
+
n_params : int
|
|
47
|
+
Number of flat parameters (sets the ``B`` carry width).
|
|
48
|
+
Other parameters as in :func:`rk4_composed_jacobian`.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
J : ``(N, d, d)``, Phi : ``(N, d)``, B : ``(N, d, n_params)``
|
|
53
|
+
"""
|
|
54
|
+
if integrator != "rk4":
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"rk4_composed_jacobian_theta only supports integrator='rk4', got {integrator!r}"
|
|
57
|
+
)
|
|
58
|
+
h = dt / n_substeps
|
|
59
|
+
d = X_start.shape[-1]
|
|
60
|
+
I_d = jnp.eye(d)
|
|
61
|
+
|
|
62
|
+
def _force(X):
|
|
63
|
+
return F_psf(X, params=theta_struct, mask=mask, extras=extras)
|
|
64
|
+
|
|
65
|
+
def _dFdx(X):
|
|
66
|
+
raw = dFdx_expr(X, params=theta_struct, mask=mask, extras=extras)
|
|
67
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
68
|
+
|
|
69
|
+
def _mm(A, B):
|
|
70
|
+
return jnp.einsum("...ij,...jk->...ik", A, B)
|
|
71
|
+
|
|
72
|
+
def _rk4_step(carry, _):
|
|
73
|
+
X, J_acc, B_acc = carry
|
|
74
|
+
|
|
75
|
+
k1 = _force(X)
|
|
76
|
+
A1 = _dFdx(X)
|
|
77
|
+
T1 = dFdth_fn(X)
|
|
78
|
+
X2 = X + (h / 2) * k1
|
|
79
|
+
k2 = _force(X2)
|
|
80
|
+
A2 = _dFdx(X2)
|
|
81
|
+
T2 = dFdth_fn(X2)
|
|
82
|
+
X3 = X + (h / 2) * k2
|
|
83
|
+
k3 = _force(X3)
|
|
84
|
+
A3 = _dFdx(X3)
|
|
85
|
+
T3 = dFdth_fn(X3)
|
|
86
|
+
X4 = X + h * k3
|
|
87
|
+
k4 = _force(X4)
|
|
88
|
+
A4 = _dFdx(X4)
|
|
89
|
+
T4 = dFdth_fn(X4)
|
|
90
|
+
|
|
91
|
+
X_new = X + (h / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
92
|
+
|
|
93
|
+
# x-tangent chain (as in rk4_composed_jacobian)
|
|
94
|
+
S1 = I_d + (h / 2) * A1
|
|
95
|
+
A2S1 = _mm(A2, S1)
|
|
96
|
+
S2 = I_d + (h / 2) * A2S1
|
|
97
|
+
A3S2 = _mm(A3, S2)
|
|
98
|
+
S3 = I_d + h * A3S2
|
|
99
|
+
J_step = I_d + (h / 6) * (A1 + 2 * A2S1 + 2 * A3S2 + _mm(A4, S3))
|
|
100
|
+
|
|
101
|
+
# θ-tangent chain: dk_s = Θ_s + c_s·h·A_s·dk_{s-1}
|
|
102
|
+
dk1 = T1
|
|
103
|
+
dk2 = T2 + (h / 2) * _mm(A2, dk1)
|
|
104
|
+
dk3 = T3 + (h / 2) * _mm(A3, dk2)
|
|
105
|
+
dk4 = T4 + h * _mm(A4, dk3)
|
|
106
|
+
B_step = (h / 6) * (dk1 + 2 * dk2 + 2 * dk3 + dk4)
|
|
107
|
+
|
|
108
|
+
J_acc_new = _mm(J_step, J_acc)
|
|
109
|
+
B_acc_new = _mm(J_step, B_acc) + B_step
|
|
110
|
+
return (X_new, J_acc_new, B_acc_new), None
|
|
111
|
+
|
|
112
|
+
J0 = jnp.broadcast_to(I_d, X_start.shape[:-1] + (d, d))
|
|
113
|
+
B0 = jnp.zeros(X_start.shape[:-1] + (d, n_params), dtype=X_start.dtype)
|
|
114
|
+
(X_final, J_composed, B_composed), _ = lax.scan(
|
|
115
|
+
_rk4_step, (X_start, J0, B0), None, length=n_substeps)
|
|
116
|
+
return J_composed, X_final - X_start, B_composed
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def rk4_composed_jacobian(F_psf, dFdx_expr, X_start, theta_struct, mask, extras, dt, n_substeps, integrator="rk4"):
|
|
120
|
+
r"""Exact RK4-composed per-particle diagonal Jacobian and displacement.
|
|
121
|
+
|
|
122
|
+
Computes the per-particle flow Jacobian :math:`\partial x_{end}^p /
|
|
123
|
+
\partial x_{start}^p` and displacement :math:`\Phi^p = x_{end}^p -
|
|
124
|
+
x_{start}^p` by composing exact RK4 Jacobians across substeps using
|
|
125
|
+
``dFdx_expr = F_psf.d_x(same_particle=True)``.
|
|
126
|
+
|
|
127
|
+
The frozen-background approximation is used: cross-particle
|
|
128
|
+
derivatives are ignored (other particles held fixed at their
|
|
129
|
+
observed positions within each substep).
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
F_psf : PSF object (``particles_input=True``)
|
|
134
|
+
dFdx_expr : DerivativeNode
|
|
135
|
+
From ``F_psf.d_x(same_particle=True)``. Evaluated as
|
|
136
|
+
``dFdx_expr(X, params=..., mask=..., extras=...)``
|
|
137
|
+
returning ``(N, d, d)`` in the SFI convention (derivative-dim
|
|
138
|
+
first: ``[p, j, i] = dF_p^i / dx_p^j``).
|
|
139
|
+
X_start : ``(N, d)``
|
|
140
|
+
theta_struct : structured parameters (from ``F_psf.unflatten_params``)
|
|
141
|
+
mask : ``(N,)`` boolean or None
|
|
142
|
+
extras : dict or None
|
|
143
|
+
dt : float
|
|
144
|
+
n_substeps : int
|
|
145
|
+
integrator : {"rk4"}, optional
|
|
146
|
+
Accepted for API symmetry with the single-particle
|
|
147
|
+
:func:`._underdamped._phase_space_jacobian`. Only ``"rk4"`` is
|
|
148
|
+
supported here — the substep Jacobian is the hand-rolled RK4
|
|
149
|
+
chain rule below. Passing anything else raises ``ValueError`` so
|
|
150
|
+
callers don't silently get RK4 when they asked for Euler.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
J : ``(N, d, d)``
|
|
155
|
+
Flow Jacobian in standard convention: ``J[p, i, j] =
|
|
156
|
+
dx_{end,p}^i / dx_{start,p}^j``.
|
|
157
|
+
Phi : ``(N, d)``
|
|
158
|
+
Displacement ``x_{end} - x_{start}``.
|
|
159
|
+
"""
|
|
160
|
+
if integrator != "rk4":
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"rk4_composed_jacobian only supports integrator='rk4', got {integrator!r}"
|
|
163
|
+
)
|
|
164
|
+
h = dt / n_substeps
|
|
165
|
+
d = X_start.shape[-1]
|
|
166
|
+
I_d = jnp.eye(d)
|
|
167
|
+
|
|
168
|
+
def _force(X):
|
|
169
|
+
return F_psf(X, params=theta_struct, mask=mask, extras=extras)
|
|
170
|
+
|
|
171
|
+
def _dFdx(X):
|
|
172
|
+
# SFI convention → standard: swap last two axes
|
|
173
|
+
raw = dFdx_expr(X, params=theta_struct, mask=mask, extras=extras)
|
|
174
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
175
|
+
|
|
176
|
+
def _mm(A, B):
|
|
177
|
+
"""Batched (N, d, d) matmul."""
|
|
178
|
+
return jnp.einsum("...ij,...jk->...ik", A, B)
|
|
179
|
+
|
|
180
|
+
def _rk4_step(carry, _):
|
|
181
|
+
X, J_acc = carry
|
|
182
|
+
|
|
183
|
+
k1 = _force(X)
|
|
184
|
+
A1 = _dFdx(X)
|
|
185
|
+
|
|
186
|
+
k2 = _force(X + (h / 2) * k1)
|
|
187
|
+
A2 = _dFdx(X + (h / 2) * k1)
|
|
188
|
+
|
|
189
|
+
k3 = _force(X + (h / 2) * k2)
|
|
190
|
+
A3 = _dFdx(X + (h / 2) * k2)
|
|
191
|
+
|
|
192
|
+
k4 = _force(X + h * k3)
|
|
193
|
+
A4 = _dFdx(X + h * k3)
|
|
194
|
+
|
|
195
|
+
# State update
|
|
196
|
+
X_new = X + (h / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
197
|
+
|
|
198
|
+
# RK4 Jacobian chain rule (all per-particle, (N, d, d))
|
|
199
|
+
# S_i = sensitivity of stage i input w.r.t. X_start
|
|
200
|
+
S1 = I_d + (h / 2) * A1
|
|
201
|
+
A2S1 = _mm(A2, S1)
|
|
202
|
+
S2 = I_d + (h / 2) * A2S1
|
|
203
|
+
A3S2 = _mm(A3, S2)
|
|
204
|
+
S3 = I_d + h * A3S2
|
|
205
|
+
|
|
206
|
+
J_step = I_d + (h / 6) * (A1 + 2 * A2S1 + 2 * A3S2 + _mm(A4, S3))
|
|
207
|
+
J_acc_new = _mm(J_step, J_acc)
|
|
208
|
+
|
|
209
|
+
return (X_new, J_acc_new), None
|
|
210
|
+
|
|
211
|
+
J0 = jnp.broadcast_to(I_d, X_start.shape[:-1] + (d, d))
|
|
212
|
+
(X_final, J_composed), _ = lax.scan(
|
|
213
|
+
_rk4_step,
|
|
214
|
+
(X_start, J0),
|
|
215
|
+
None,
|
|
216
|
+
length=n_substeps,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
Phi = X_final - X_start
|
|
220
|
+
return J_composed, Phi
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def rk4_composed_jacobian_phase(
|
|
224
|
+
F_psf,
|
|
225
|
+
dFdx_expr,
|
|
226
|
+
dFdv_expr,
|
|
227
|
+
X_start,
|
|
228
|
+
V_start,
|
|
229
|
+
theta_struct,
|
|
230
|
+
mask,
|
|
231
|
+
extras,
|
|
232
|
+
dt,
|
|
233
|
+
n_substeps,
|
|
234
|
+
integrator="rk4",
|
|
235
|
+
):
|
|
236
|
+
r"""RK4-composed per-particle phase-space Jacobian and flow.
|
|
237
|
+
|
|
238
|
+
Multi-particle analogue of :func:`_phase_space_jacobian` for the
|
|
239
|
+
flocking class: the state update uses the full multi-particle
|
|
240
|
+
force :math:`F(X, V)` (so inter-particle couplings drive the
|
|
241
|
+
dynamics), while the tangent is propagated **per particle** using
|
|
242
|
+
the per-edge derivatives
|
|
243
|
+
:math:`\partial F_p/\partial x_p` and :math:`\partial F_p/\partial v_p`
|
|
244
|
+
obtained from ``F_psf.d_x(same_particle=True)`` and
|
|
245
|
+
``F_psf.d_v(same_particle=True)``. This is the frozen-background
|
|
246
|
+
approximation lifted to phase space: cross-particle Jacobian blocks
|
|
247
|
+
are dropped, keeping the per-particle flow Jacobian block-diagonal
|
|
248
|
+
in the particle axis and the cost O(N) per window.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
F_psf : PSF (``particles_input=True``, ``needs_v=True``)
|
|
253
|
+
dFdx_expr : ``F_psf.d_x(same_particle=True)``
|
|
254
|
+
dFdv_expr : ``F_psf.d_v(same_particle=True)``
|
|
255
|
+
X_start, V_start : ``(N, d)``
|
|
256
|
+
theta_struct : structured parameters (from ``F_psf.unflatten_params``)
|
|
257
|
+
mask : ``(N,)`` boolean or None
|
|
258
|
+
extras : dict or None
|
|
259
|
+
dt : float
|
|
260
|
+
n_substeps : int
|
|
261
|
+
|
|
262
|
+
integrator : {"rk4"}, optional
|
|
263
|
+
Accepted for API symmetry with the single-particle
|
|
264
|
+
:func:`_phase_space_jacobian`. Only ``"rk4"`` is supported here —
|
|
265
|
+
the substep Jacobian is the hand-rolled RK4 chain rule below.
|
|
266
|
+
Passing anything else raises ``ValueError`` so callers don't
|
|
267
|
+
silently get RK4 when they asked for Euler.
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
Phi_x, Phi_v : ``(N, d)``
|
|
272
|
+
End-state position and velocity after time ``dt`` (matches
|
|
273
|
+
single-particle :func:`_phase_space_jacobian` convention).
|
|
274
|
+
Jxx, Jxv, Jvx, Jvv : ``(N, d, d)``
|
|
275
|
+
Per-particle phase-space Jacobian blocks in standard
|
|
276
|
+
convention: ``Jxx[p, i, j] = ∂x_{end,p}^i/∂x_{start,p}^j``, etc.
|
|
277
|
+
"""
|
|
278
|
+
if integrator != "rk4":
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"rk4_composed_jacobian_phase only supports integrator='rk4', got {integrator!r}"
|
|
281
|
+
)
|
|
282
|
+
import jax.lax as lax
|
|
283
|
+
|
|
284
|
+
h = dt / n_substeps
|
|
285
|
+
d = X_start.shape[-1]
|
|
286
|
+
N = X_start.shape[-2]
|
|
287
|
+
I_d = jnp.eye(d)
|
|
288
|
+
I_2d = jnp.eye(2 * d)
|
|
289
|
+
|
|
290
|
+
def _force(X, V):
|
|
291
|
+
return F_psf(
|
|
292
|
+
X,
|
|
293
|
+
v=V,
|
|
294
|
+
params=theta_struct,
|
|
295
|
+
mask=mask,
|
|
296
|
+
extras=extras,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def _dFdx(X, V):
|
|
300
|
+
# SFI convention ``[p, j, i] = dF_p^i / dx_p^j`` →
|
|
301
|
+
# standard ``[p, i, j] = dF_p^i / dx_p^j`` via swap.
|
|
302
|
+
raw = dFdx_expr(
|
|
303
|
+
X,
|
|
304
|
+
v=V,
|
|
305
|
+
params=theta_struct,
|
|
306
|
+
mask=mask,
|
|
307
|
+
extras=extras,
|
|
308
|
+
)
|
|
309
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
310
|
+
|
|
311
|
+
def _dFdv(X, V):
|
|
312
|
+
raw = dFdv_expr(
|
|
313
|
+
X,
|
|
314
|
+
v=V,
|
|
315
|
+
params=theta_struct,
|
|
316
|
+
mask=mask,
|
|
317
|
+
extras=extras,
|
|
318
|
+
)
|
|
319
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
320
|
+
|
|
321
|
+
def _mm(A, B):
|
|
322
|
+
"""Batched ``(N, a, b) @ (N, b, c) → (N, a, c)`` matmul."""
|
|
323
|
+
return jnp.einsum("...ij,...jk->...ik", A, B)
|
|
324
|
+
|
|
325
|
+
def _M_stack(Ax, Av):
|
|
326
|
+
"""Build per-particle stage Jacobian
|
|
327
|
+
|
|
328
|
+
M = [[0, I_d], [A_x, A_v]] shape (N, 2d, 2d)
|
|
329
|
+
"""
|
|
330
|
+
zero = jnp.zeros_like(Ax)
|
|
331
|
+
top = jnp.concatenate([zero, jnp.broadcast_to(I_d, Ax.shape)], axis=-1)
|
|
332
|
+
bot = jnp.concatenate([Ax, Av], axis=-1)
|
|
333
|
+
return jnp.concatenate([top, bot], axis=-2)
|
|
334
|
+
|
|
335
|
+
def _rk4_step(carry, _):
|
|
336
|
+
X, V, J_acc = carry
|
|
337
|
+
|
|
338
|
+
# Stage 1
|
|
339
|
+
A1_x = _dFdx(X, V)
|
|
340
|
+
A1_v = _dFdv(X, V)
|
|
341
|
+
k1_x = V
|
|
342
|
+
k1_v = _force(X, V)
|
|
343
|
+
M1 = _M_stack(A1_x, A1_v) # (N, 2d, 2d)
|
|
344
|
+
T1 = M1 # = M1 @ I = M1
|
|
345
|
+
|
|
346
|
+
# Stage 2
|
|
347
|
+
X2 = X + (h / 2) * k1_x
|
|
348
|
+
V2 = V + (h / 2) * k1_v
|
|
349
|
+
A2_x = _dFdx(X2, V2)
|
|
350
|
+
A2_v = _dFdv(X2, V2)
|
|
351
|
+
k2_x = V2
|
|
352
|
+
k2_v = _force(X2, V2)
|
|
353
|
+
M2 = _M_stack(A2_x, A2_v)
|
|
354
|
+
S2 = I_2d + (h / 2) * T1
|
|
355
|
+
T2 = _mm(M2, S2)
|
|
356
|
+
|
|
357
|
+
# Stage 3
|
|
358
|
+
X3 = X + (h / 2) * k2_x
|
|
359
|
+
V3 = V + (h / 2) * k2_v
|
|
360
|
+
A3_x = _dFdx(X3, V3)
|
|
361
|
+
A3_v = _dFdv(X3, V3)
|
|
362
|
+
k3_x = V3
|
|
363
|
+
k3_v = _force(X3, V3)
|
|
364
|
+
M3 = _M_stack(A3_x, A3_v)
|
|
365
|
+
S3 = I_2d + (h / 2) * T2
|
|
366
|
+
T3 = _mm(M3, S3)
|
|
367
|
+
|
|
368
|
+
# Stage 4
|
|
369
|
+
X4 = X + h * k3_x
|
|
370
|
+
V4 = V + h * k3_v
|
|
371
|
+
A4_x = _dFdx(X4, V4)
|
|
372
|
+
A4_v = _dFdv(X4, V4)
|
|
373
|
+
k4_x = V4
|
|
374
|
+
k4_v = _force(X4, V4)
|
|
375
|
+
M4 = _M_stack(A4_x, A4_v)
|
|
376
|
+
S4 = I_2d + h * T3
|
|
377
|
+
T4 = _mm(M4, S4)
|
|
378
|
+
|
|
379
|
+
# State update (full multi-particle force)
|
|
380
|
+
X_new = X + (h / 6) * (k1_x + 2 * k2_x + 2 * k3_x + k4_x)
|
|
381
|
+
V_new = V + (h / 6) * (k1_v + 2 * k2_v + 2 * k3_v + k4_v)
|
|
382
|
+
|
|
383
|
+
# Per-particle 2d×2d substep Jacobian + chain compose
|
|
384
|
+
J_step = I_2d + (h / 6) * (T1 + 2 * T2 + 2 * T3 + T4)
|
|
385
|
+
J_acc_new = _mm(J_step, J_acc)
|
|
386
|
+
|
|
387
|
+
return (X_new, V_new, J_acc_new), None
|
|
388
|
+
|
|
389
|
+
J0 = jnp.broadcast_to(I_2d, (N, 2 * d, 2 * d))
|
|
390
|
+
(X_final, V_final, J_composed), _ = lax.scan(
|
|
391
|
+
_rk4_step,
|
|
392
|
+
(X_start, V_start, J0),
|
|
393
|
+
None,
|
|
394
|
+
length=n_substeps,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
Phi_x = X_final
|
|
398
|
+
Phi_v = V_final
|
|
399
|
+
Jxx = J_composed[:, :d, :d]
|
|
400
|
+
Jxv = J_composed[:, :d, d:]
|
|
401
|
+
Jvx = J_composed[:, d:, :d]
|
|
402
|
+
Jvv = J_composed[:, d:, d:]
|
|
403
|
+
return Phi_x, Phi_v, Jxx, Jxv, Jvx, Jvv
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def rk4_composed_jacobian_phase_theta(
|
|
407
|
+
F_psf,
|
|
408
|
+
dFdx_expr,
|
|
409
|
+
dFdv_expr,
|
|
410
|
+
dFdth_fn,
|
|
411
|
+
n_params,
|
|
412
|
+
X_start,
|
|
413
|
+
V_start,
|
|
414
|
+
theta_struct,
|
|
415
|
+
mask,
|
|
416
|
+
extras,
|
|
417
|
+
dt,
|
|
418
|
+
n_substeps,
|
|
419
|
+
integrator="rk4",
|
|
420
|
+
):
|
|
421
|
+
r"""Phase-space per-particle Jacobian blocks, flow, **and θ-sensitivity**.
|
|
422
|
+
|
|
423
|
+
Phase-space analogue of :func:`rk4_composed_jacobian_theta`: alongside
|
|
424
|
+
the frozen-background blocks of :func:`rk4_composed_jacobian_phase`,
|
|
425
|
+
carries the per-particle lifted parameter sensitivity
|
|
426
|
+
|
|
427
|
+
.. math:: B_p = \partial (x', v')_p / \partial\theta \in (N, 2d, n_\theta)
|
|
428
|
+
|
|
429
|
+
through the RK4 chain with lifted stage tangents
|
|
430
|
+
``Θ̃ = [0; ∂F/∂θ]`` (the position field has no direct θ-dependence) and
|
|
431
|
+
``dk_s/dθ = Θ̃_s + c_s h M_s dk_{s-1}/dθ``; per substep
|
|
432
|
+
``B ← J_step B + B_step``. Memory O(N·2d·n_θ).
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
dFdth_fn : callable ``((N, d), (N, d)) → (N, d, n_params)``
|
|
437
|
+
Direct parameter derivative of the force at a fixed phase frame.
|
|
438
|
+
n_params : int
|
|
439
|
+
Other parameters as in :func:`rk4_composed_jacobian_phase`.
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
Phi_x, Phi_v : ``(N, d)`` end-state position / velocity.
|
|
444
|
+
Jxx, Jxv, Jvx, Jvv : ``(N, d, d)`` frozen-background blocks.
|
|
445
|
+
Bx, Bv : ``(N, d, n_params)`` θ-sensitivity of the end state.
|
|
446
|
+
"""
|
|
447
|
+
if integrator != "rk4":
|
|
448
|
+
raise ValueError(
|
|
449
|
+
f"rk4_composed_jacobian_phase_theta only supports integrator='rk4', got {integrator!r}"
|
|
450
|
+
)
|
|
451
|
+
import jax.lax as lax
|
|
452
|
+
|
|
453
|
+
h = dt / n_substeps
|
|
454
|
+
d = X_start.shape[-1]
|
|
455
|
+
N = X_start.shape[-2]
|
|
456
|
+
I_d = jnp.eye(d)
|
|
457
|
+
I_2d = jnp.eye(2 * d)
|
|
458
|
+
|
|
459
|
+
def _force(X, V):
|
|
460
|
+
return F_psf(X, v=V, params=theta_struct, mask=mask, extras=extras)
|
|
461
|
+
|
|
462
|
+
def _dFdx(X, V):
|
|
463
|
+
raw = dFdx_expr(X, v=V, params=theta_struct, mask=mask, extras=extras)
|
|
464
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
465
|
+
|
|
466
|
+
def _dFdv(X, V):
|
|
467
|
+
raw = dFdv_expr(X, v=V, params=theta_struct, mask=mask, extras=extras)
|
|
468
|
+
return jnp.swapaxes(raw, -1, -2)
|
|
469
|
+
|
|
470
|
+
def _mm(A, B):
|
|
471
|
+
return jnp.einsum("...ij,...jk->...ik", A, B)
|
|
472
|
+
|
|
473
|
+
def _M_stack(Ax, Av):
|
|
474
|
+
zero = jnp.zeros_like(Ax)
|
|
475
|
+
top = jnp.concatenate([zero, jnp.broadcast_to(I_d, Ax.shape)], axis=-1)
|
|
476
|
+
bot = jnp.concatenate([Ax, Av], axis=-1)
|
|
477
|
+
return jnp.concatenate([top, bot], axis=-2)
|
|
478
|
+
|
|
479
|
+
def _lift(T):
|
|
480
|
+
# (N, d, n) → (N, 2d, n): zero position block on top
|
|
481
|
+
return jnp.concatenate([jnp.zeros_like(T), T], axis=-2)
|
|
482
|
+
|
|
483
|
+
def _rk4_step(carry, _):
|
|
484
|
+
X, V, J_acc, B_acc = carry
|
|
485
|
+
|
|
486
|
+
A1x, A1v = _dFdx(X, V), _dFdv(X, V)
|
|
487
|
+
k1_x, k1_v = V, _force(X, V)
|
|
488
|
+
M1 = _M_stack(A1x, A1v)
|
|
489
|
+
T1 = _lift(dFdth_fn(X, V))
|
|
490
|
+
|
|
491
|
+
X2, V2 = X + (h / 2) * k1_x, V + (h / 2) * k1_v
|
|
492
|
+
A2x, A2v = _dFdx(X2, V2), _dFdv(X2, V2)
|
|
493
|
+
k2_x, k2_v = V2, _force(X2, V2)
|
|
494
|
+
M2 = _M_stack(A2x, A2v)
|
|
495
|
+
T2 = _lift(dFdth_fn(X2, V2))
|
|
496
|
+
|
|
497
|
+
X3, V3 = X + (h / 2) * k2_x, V + (h / 2) * k2_v
|
|
498
|
+
A3x, A3v = _dFdx(X3, V3), _dFdv(X3, V3)
|
|
499
|
+
k3_x, k3_v = V3, _force(X3, V3)
|
|
500
|
+
M3 = _M_stack(A3x, A3v)
|
|
501
|
+
T3 = _lift(dFdth_fn(X3, V3))
|
|
502
|
+
|
|
503
|
+
X4, V4 = X + h * k3_x, V + h * k3_v
|
|
504
|
+
A4x, A4v = _dFdx(X4, V4), _dFdv(X4, V4)
|
|
505
|
+
k4_x, k4_v = V4, _force(X4, V4)
|
|
506
|
+
M4 = _M_stack(A4x, A4v)
|
|
507
|
+
T4 = _lift(dFdth_fn(X4, V4))
|
|
508
|
+
|
|
509
|
+
# x-tangent chain (identical to rk4_composed_jacobian_phase)
|
|
510
|
+
S2 = I_2d + (h / 2) * M1
|
|
511
|
+
T2_ = _mm(M2, S2)
|
|
512
|
+
S3 = I_2d + (h / 2) * T2_
|
|
513
|
+
T3_ = _mm(M3, S3)
|
|
514
|
+
S4 = I_2d + h * T3_
|
|
515
|
+
T4_ = _mm(M4, S4)
|
|
516
|
+
J_step = I_2d + (h / 6) * (M1 + 2 * T2_ + 2 * T3_ + T4_)
|
|
517
|
+
|
|
518
|
+
# θ-tangent chain on the lifted system
|
|
519
|
+
dk1 = T1
|
|
520
|
+
dk2 = T2 + (h / 2) * _mm(M2, dk1)
|
|
521
|
+
dk3 = T3 + (h / 2) * _mm(M3, dk2)
|
|
522
|
+
dk4 = T4 + h * _mm(M4, dk3)
|
|
523
|
+
B_step = (h / 6) * (dk1 + 2 * dk2 + 2 * dk3 + dk4)
|
|
524
|
+
|
|
525
|
+
X_new = X + (h / 6) * (k1_x + 2 * k2_x + 2 * k3_x + k4_x)
|
|
526
|
+
V_new = V + (h / 6) * (k1_v + 2 * k2_v + 2 * k3_v + k4_v)
|
|
527
|
+
J_acc_new = _mm(J_step, J_acc)
|
|
528
|
+
B_acc_new = _mm(J_step, B_acc) + B_step
|
|
529
|
+
return (X_new, V_new, J_acc_new, B_acc_new), None
|
|
530
|
+
|
|
531
|
+
J0 = jnp.broadcast_to(I_2d, (N, 2 * d, 2 * d))
|
|
532
|
+
B0 = jnp.zeros((N, 2 * d, n_params), dtype=X_start.dtype)
|
|
533
|
+
(X_final, V_final, J_composed, B_composed), _ = lax.scan(
|
|
534
|
+
_rk4_step, (X_start, V_start, J0, B0), None, length=n_substeps)
|
|
535
|
+
return (
|
|
536
|
+
X_final, V_final,
|
|
537
|
+
J_composed[:, :d, :d], J_composed[:, :d, d:],
|
|
538
|
+
J_composed[:, d:, :d], J_composed[:, d:, d:],
|
|
539
|
+
B_composed[:, :d, :], B_composed[:, d:, :],
|
|
540
|
+
)
|