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,318 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
import equinox as eqx
|
|
4
|
+
from jaxtyping import Array
|
|
5
|
+
|
|
6
|
+
from ..memhint import MemHint, SampleMeta, default_leaf_hint, default_op_hint, resolve_P
|
|
7
|
+
from ..params import ParamSuite
|
|
8
|
+
from .contract import _ContractMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Abstract node – every dictionary component subclasses this
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
class BaseNode(_ContractMixin):
|
|
15
|
+
"""
|
|
16
|
+
Single node in the immutable dictionary tree.
|
|
17
|
+
|
|
18
|
+
Required methods
|
|
19
|
+
----------------
|
|
20
|
+
``__call__(x, *, params=None, v=None, mask=None, extras=None) -> Array``
|
|
21
|
+
Returns ``(..., *rank_axes, n_features)``.
|
|
22
|
+
|
|
23
|
+
``flatten() -> (funcs, labels, descriptors)``
|
|
24
|
+
Each func follows the same keyword-only signature as above.
|
|
25
|
+
|
|
26
|
+
``_tree_id() -> int``
|
|
27
|
+
Optional hint for caching/equality.
|
|
28
|
+
|
|
29
|
+
Notes
|
|
30
|
+
-----
|
|
31
|
+
- ``params`` is a ``dict[str, Array]`` for PSF/SF; Basis nodes ignore it.
|
|
32
|
+
- ``extras`` is a plain mapping passed per call. Composite nodes *union*
|
|
33
|
+
children's ``extras_required`` (presence-only).
|
|
34
|
+
- ``particle_extras``: ``tuple[str, ...]`` -- pure metadata. Names of
|
|
35
|
+
extras expected to be per-particle at dispatch time.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# ---------------- static fields ----------------
|
|
40
|
+
extras_required: tuple[str, ...] = eqx.field(static=True, default=())
|
|
41
|
+
|
|
42
|
+
def __call__(self, x: Array, *, params=None, v=None, mask=None, extras=None) -> Array:
|
|
43
|
+
raise NotImplementedError(f"{type(self).__name__} must implement __call__")
|
|
44
|
+
|
|
45
|
+
def flatten(
|
|
46
|
+
self,
|
|
47
|
+
) -> tuple[list[Callable], list[str], list[Any]]: # pragma: no cover
|
|
48
|
+
"""
|
|
49
|
+
Return three parallel lists:
|
|
50
|
+
- callables (each f(x, *, v=None, mask=None, extras=None, params=None) -> Array)
|
|
51
|
+
- labels (human-readable strings)
|
|
52
|
+
- descriptors (arbitrary metadata, JSON-serialisable)
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError(f"{type(self).__name__} must implement flatten")
|
|
55
|
+
|
|
56
|
+
# Optional fast-path identity used by caching layers
|
|
57
|
+
def _tree_id(self) -> int: # pragma: no cover
|
|
58
|
+
return id(self)
|
|
59
|
+
|
|
60
|
+
# ---------------- memory hint API ----------------
|
|
61
|
+
def memory_hint(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
dtype=None, # default float32 when None
|
|
65
|
+
particle_size: int | None = None,
|
|
66
|
+
sample: SampleMeta | None = None,
|
|
67
|
+
mode: str = "forward",
|
|
68
|
+
) -> MemHint:
|
|
69
|
+
"""
|
|
70
|
+
Default conservative rule for LEAF-style nodes: output buffer only.
|
|
71
|
+
|
|
72
|
+
Composite ops override this and add their children.
|
|
73
|
+
``particle_size`` or ``sample.P`` lets callers specify P when this node
|
|
74
|
+
PRODUCES particle axes. If both are None, P=1 is assumed.
|
|
75
|
+
"""
|
|
76
|
+
P = resolve_P(particle_size, sample)
|
|
77
|
+
return default_leaf_hint(self, dtype=dtype, particle_size=P, mode=mode)
|
|
78
|
+
|
|
79
|
+
# Friendly shortcut for callers that need just the number
|
|
80
|
+
def estimate_bytes_per_sample(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
dtype=None,
|
|
84
|
+
particle_size: int | None = None,
|
|
85
|
+
sample: SampleMeta | None = None,
|
|
86
|
+
mode: str = "forward",
|
|
87
|
+
) -> int:
|
|
88
|
+
"""Return ONLY the transient bytes per single sample."""
|
|
89
|
+
return self.memory_hint(dtype=dtype, particle_size=particle_size, sample=sample, mode=mode).per_sample_bytes
|
|
90
|
+
|
|
91
|
+
# ─────────── same-particle diagonal Jacobian protocol ─────────────
|
|
92
|
+
def _same_particle_jacobian(
|
|
93
|
+
self,
|
|
94
|
+
x: Array,
|
|
95
|
+
*,
|
|
96
|
+
var: str = "x",
|
|
97
|
+
v=None,
|
|
98
|
+
params=None,
|
|
99
|
+
mask=None,
|
|
100
|
+
extras=None,
|
|
101
|
+
) -> Array:
|
|
102
|
+
"""Same-particle diagonal Jacobian ∂f_p/∂x_p (or ∂f_p/∂v_p).
|
|
103
|
+
|
|
104
|
+
Returns array in **contract layout**: ``(P, d_jac, rank..., F)``.
|
|
105
|
+
|
|
106
|
+
This is the generic fallback using per-particle AD; cost is O(P²·d).
|
|
107
|
+
Subclasses override for efficiency:
|
|
108
|
+
|
|
109
|
+
- :class:`BaseOpNode` propagates via chain rule through ``_op``.
|
|
110
|
+
- :class:`InteractionDispatcher` uses per-edge ``jacfwd`` + scatter: O(M·d²).
|
|
111
|
+
"""
|
|
112
|
+
import jax
|
|
113
|
+
|
|
114
|
+
from .ops.derivative import _move_deriv_axis, _same_particle_grad
|
|
115
|
+
|
|
116
|
+
J_raw = _same_particle_grad(self, jax.jacfwd, x, v, params, var=var, mask=mask, extras=extras)
|
|
117
|
+
return _move_deriv_axis(
|
|
118
|
+
J_raw,
|
|
119
|
+
rank=int(self.rank),
|
|
120
|
+
pdepth_old=int(self.pdepth),
|
|
121
|
+
particles_input=bool(self.particles_input),
|
|
122
|
+
same_particle=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Generic composite node – all operators with ≥1 child inherit from here
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
class BaseOpNode(BaseNode):
|
|
130
|
+
"""
|
|
131
|
+
Generic **composite** backend node.
|
|
132
|
+
|
|
133
|
+
Unifies boilerplate that every multi-child operator needs:
|
|
134
|
+
|
|
135
|
+
- store/validate ``children``
|
|
136
|
+
- union ``extras_required``
|
|
137
|
+
- union ``particle_extras``
|
|
138
|
+
- merge *static contract* via ``_merge_static()``
|
|
139
|
+
- OR-reduce ``needs_v``
|
|
140
|
+
- single runtime ``__call__`` that evaluates children once and
|
|
141
|
+
delegates combination logic to ``_op(outputs, *, params)``
|
|
142
|
+
- default ``flatten()`` that splices children sequentially
|
|
143
|
+
- deterministic ``._tree_id()`` for JIT-cache keys
|
|
144
|
+
|
|
145
|
+
Sub-classes **must** implement two hooks:
|
|
146
|
+
|
|
147
|
+
``_merge_static(children) -> dict``
|
|
148
|
+
Return at least the keys ``rank, dim, pdepth, n_features, needs_v``.
|
|
149
|
+
|
|
150
|
+
``_op(outputs, *, params) -> jnp.ndarray``
|
|
151
|
+
Combine child outputs into the final tensor.
|
|
152
|
+
|
|
153
|
+
Notes
|
|
154
|
+
-----
|
|
155
|
+
Extras policy: composite nodes **union** children's ``extras_required``
|
|
156
|
+
at construction (presence-only). Shapes/broadcast are not validated here.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
children: tuple[BaseNode, ...] = eqx.field(static=True)
|
|
161
|
+
# Which merge mode to use for this composite node.
|
|
162
|
+
# Subclasses must set one of: 'concat' | 'map' | 'elementwise' | 'einsum'
|
|
163
|
+
CONTRACT_MODE: str | None = None
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
def __init__(self, *children: BaseNode):
|
|
167
|
+
if not children:
|
|
168
|
+
raise ValueError(f"{type(self).__name__}() needs at least one child")
|
|
169
|
+
object.__setattr__(self, "children", tuple(children))
|
|
170
|
+
|
|
171
|
+
# ---------------- merge static contract -----------------------
|
|
172
|
+
contract = self._merge_static(self.children)
|
|
173
|
+
required = {"rank", "dim", "pdepth", "n_features", "needs_v", "particles_input"}
|
|
174
|
+
if missing := required - contract.keys():
|
|
175
|
+
raise RuntimeError(f"{type(self).__name__}._merge_static() missing {missing}")
|
|
176
|
+
for k, v in contract.items():
|
|
177
|
+
object.__setattr__(self, k, v)
|
|
178
|
+
|
|
179
|
+
# --------------- union extras + param template ----------------
|
|
180
|
+
# Presence-only: union of children requirements; ignore wildcards.
|
|
181
|
+
parts: list[set[str]] = []
|
|
182
|
+
for c in self.children:
|
|
183
|
+
ks = getattr(c, "extras_required", ())
|
|
184
|
+
# Normalize: tuples/strings/iterables → iterate, drop wildcard '*'
|
|
185
|
+
if ks == "*":
|
|
186
|
+
continue
|
|
187
|
+
if isinstance(ks, str):
|
|
188
|
+
parts.append({ks})
|
|
189
|
+
continue
|
|
190
|
+
try:
|
|
191
|
+
s = {k for k in ks if k != "*"}
|
|
192
|
+
except TypeError:
|
|
193
|
+
# Non-iterable fallback (shouldn't happen): ignore
|
|
194
|
+
s = set()
|
|
195
|
+
if s:
|
|
196
|
+
parts.append(s)
|
|
197
|
+
req = set().union(*parts) if parts else set()
|
|
198
|
+
object.__setattr__(self, "extras_required", tuple(sorted(req)))
|
|
199
|
+
|
|
200
|
+
# Union of particles_extra:
|
|
201
|
+
decl: set[str] = set()
|
|
202
|
+
for c in self.children:
|
|
203
|
+
pe = getattr(c, "particle_extras", ())
|
|
204
|
+
if pe:
|
|
205
|
+
decl |= set(pe)
|
|
206
|
+
object.__setattr__(self, "particle_extras", tuple(sorted(decl)))
|
|
207
|
+
|
|
208
|
+
suite = ParamSuite.merge_many(*(ch.param_suite for ch in self.children))
|
|
209
|
+
object.__setattr__(self, "param_suite", suite)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# default runtime call
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
def __call__(self, x, *, params=None, v=None, mask=None, extras=None):
|
|
215
|
+
self._assert_inputs(x, v, mask, extras)
|
|
216
|
+
outs = [c(x, params=params, v=v, mask=mask, extras=extras) for c in self.children]
|
|
217
|
+
y = self._op(outs, params=params)
|
|
218
|
+
self._assert_outputs(x, y)
|
|
219
|
+
return y
|
|
220
|
+
|
|
221
|
+
def _merge_static(self, children):
|
|
222
|
+
mode = getattr(self, "CONTRACT_MODE", None)
|
|
223
|
+
if not mode:
|
|
224
|
+
raise NotImplementedError(f"{type(self).__name__} must define CONTRACT_MODE")
|
|
225
|
+
spec = getattr(self, "spec", None) if mode == "einsum" else None
|
|
226
|
+
return _ContractMixin.merge_contract(children, mode=mode, spec=spec)
|
|
227
|
+
|
|
228
|
+
# default flatten: linear splice of children
|
|
229
|
+
def flatten(self):
|
|
230
|
+
funcs, labels, desc = [], [], []
|
|
231
|
+
for ch in self.children:
|
|
232
|
+
f, lab, d = ch.flatten()
|
|
233
|
+
funcs.extend(f)
|
|
234
|
+
labels.extend(lab)
|
|
235
|
+
desc.extend(d)
|
|
236
|
+
return funcs, labels, desc
|
|
237
|
+
|
|
238
|
+
def _tree_id(self):
|
|
239
|
+
return hash((type(self).__name__, tuple(ch._tree_id() for ch in self.children)))
|
|
240
|
+
|
|
241
|
+
# interface stubs ---------------------------------------------------
|
|
242
|
+
def _op(self, outs, *, params): # noqa: D401
|
|
243
|
+
raise NotImplementedError(f"{type(self).__name__} must implement _op")
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
# graph rewriting (see StateExpr.specialize)
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
def with_children(self, new_children):
|
|
249
|
+
"""Rebuild this composite with ``new_children``, recomputing all derived
|
|
250
|
+
static state (contract, extras union, parameter suite) via the
|
|
251
|
+
constructor. Subclasses with extra config must override to preserve it.
|
|
252
|
+
"""
|
|
253
|
+
raise NotImplementedError(
|
|
254
|
+
f"{type(self).__name__}.with_children is not implemented; this node "
|
|
255
|
+
"type is not yet supported by StateExpr.specialize()."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# ─────────── same-particle Jacobian: chain rule through _op ───────
|
|
259
|
+
def _same_particle_jacobian(
|
|
260
|
+
self,
|
|
261
|
+
x: Array,
|
|
262
|
+
*,
|
|
263
|
+
var: str = "x",
|
|
264
|
+
v=None,
|
|
265
|
+
params=None,
|
|
266
|
+
mask=None,
|
|
267
|
+
extras=None,
|
|
268
|
+
) -> Array:
|
|
269
|
+
"""Chain rule: propagate children's Jacobians through ``_op`` via JVP.
|
|
270
|
+
|
|
271
|
+
For each spatial direction *j*, a single ``jax.jvp`` call through
|
|
272
|
+
``_op`` gives the *j*-th column of the composite Jacobian.
|
|
273
|
+
Cost: ``d_jac`` (=2 or 3) lightweight JVP evaluations of ``_op``.
|
|
274
|
+
"""
|
|
275
|
+
import jax
|
|
276
|
+
import jax.numpy as jnp
|
|
277
|
+
|
|
278
|
+
# Fall back if _op is not implemented (DerivativeNode, InteractionDispatcher
|
|
279
|
+
# override __call__ directly and leave _op as the stub).
|
|
280
|
+
if type(self)._op is BaseOpNode._op:
|
|
281
|
+
return super()._same_particle_jacobian(x, var=var, v=v, params=params, mask=mask, extras=extras)
|
|
282
|
+
|
|
283
|
+
kw = dict(v=v, params=params, mask=mask, extras=extras)
|
|
284
|
+
child_vals = [c(x, **kw) for c in self.children]
|
|
285
|
+
child_jacs = [c._same_particle_jacobian(x, var=var, **kw) for c in self.children]
|
|
286
|
+
|
|
287
|
+
d_jac = child_jacs[0].shape[1]
|
|
288
|
+
p = params
|
|
289
|
+
|
|
290
|
+
def op_fn(*outs_tuple):
|
|
291
|
+
return self._op(list(outs_tuple), params=p)
|
|
292
|
+
|
|
293
|
+
primals = tuple(child_vals)
|
|
294
|
+
cols = []
|
|
295
|
+
for j in range(d_jac):
|
|
296
|
+
tangents = tuple(cj[:, j, ...] for cj in child_jacs)
|
|
297
|
+
_, y_dot = jax.jvp(op_fn, primals, tangents)
|
|
298
|
+
cols.append(y_dot)
|
|
299
|
+
|
|
300
|
+
return jnp.stack(cols, axis=1)
|
|
301
|
+
|
|
302
|
+
# ---------------- memory hint for composite ops ----------------
|
|
303
|
+
def memory_hint(
|
|
304
|
+
self,
|
|
305
|
+
*,
|
|
306
|
+
dtype=None, # default float32 when None
|
|
307
|
+
particle_size: int | None = None,
|
|
308
|
+
sample: SampleMeta | None = None,
|
|
309
|
+
mode: str = "forward",
|
|
310
|
+
) -> MemHint:
|
|
311
|
+
"""
|
|
312
|
+
Conservative default for composite nodes.
|
|
313
|
+
|
|
314
|
+
``sum(children.hint) + my output buffer + broadcast overhead``.
|
|
315
|
+
We assume child outputs coexist while this op constructs its result.
|
|
316
|
+
"""
|
|
317
|
+
P = resolve_P(particle_size, sample)
|
|
318
|
+
return default_op_hint(self, children=self.children, dtype=dtype, particle_size=P, mode=mode)
|