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.
Files changed (104) hide show
  1. SFI/__init__.py +64 -0
  2. SFI/bases/__init__.py +85 -0
  3. SFI/bases/constants.py +492 -0
  4. SFI/bases/linear.py +325 -0
  5. SFI/bases/monomials.py +218 -0
  6. SFI/bases/pairs.py +998 -0
  7. SFI/bases/spde.py +1537 -0
  8. SFI/diagnostics/__init__.py +60 -0
  9. SFI/diagnostics/assess.py +87 -0
  10. SFI/diagnostics/dynamics_order.py +621 -0
  11. SFI/diagnostics/plotting.py +226 -0
  12. SFI/diagnostics/report.py +238 -0
  13. SFI/diagnostics/residual_tests.py +395 -0
  14. SFI/diagnostics/residuals.py +688 -0
  15. SFI/inference/__init__.py +58 -0
  16. SFI/inference/base.py +1460 -0
  17. SFI/inference/optimizers.py +200 -0
  18. SFI/inference/overdamped.py +1214 -0
  19. SFI/inference/parametric_core/__init__.py +34 -0
  20. SFI/inference/parametric_core/covariance.py +232 -0
  21. SFI/inference/parametric_core/driver.py +272 -0
  22. SFI/inference/parametric_core/flow.py +149 -0
  23. SFI/inference/parametric_core/flow_multi.py +362 -0
  24. SFI/inference/parametric_core/flow_ud.py +168 -0
  25. SFI/inference/parametric_core/jacobians.py +540 -0
  26. SFI/inference/parametric_core/objective.py +286 -0
  27. SFI/inference/parametric_core/objective_ud.py +253 -0
  28. SFI/inference/parametric_core/precision.py +229 -0
  29. SFI/inference/parametric_core/solve.py +763 -0
  30. SFI/inference/result.py +362 -0
  31. SFI/inference/serialization.py +245 -0
  32. SFI/inference/sparse/__init__.py +67 -0
  33. SFI/inference/sparse/base.py +43 -0
  34. SFI/inference/sparse/beam.py +303 -0
  35. SFI/inference/sparse/greedy.py +151 -0
  36. SFI/inference/sparse/hillclimb.py +307 -0
  37. SFI/inference/sparse/lasso.py +178 -0
  38. SFI/inference/sparse/metrics.py +78 -0
  39. SFI/inference/sparse/result.py +278 -0
  40. SFI/inference/sparse/scorer.py +323 -0
  41. SFI/inference/sparse/stlsq.py +165 -0
  42. SFI/inference/sparsity.py +40 -0
  43. SFI/inference/underdamped.py +1355 -0
  44. SFI/integrate/__init__.py +38 -0
  45. SFI/integrate/api.py +920 -0
  46. SFI/integrate/integrand.py +402 -0
  47. SFI/integrate/rk4.py +156 -0
  48. SFI/integrate/timeops.py +174 -0
  49. SFI/langevin/__init__.py +29 -0
  50. SFI/langevin/base.py +863 -0
  51. SFI/langevin/chunked.py +225 -0
  52. SFI/langevin/noise.py +446 -0
  53. SFI/langevin/overdamped.py +560 -0
  54. SFI/langevin/underdamped.py +448 -0
  55. SFI/statefunc/__init__.py +49 -0
  56. SFI/statefunc/basis.py +87 -0
  57. SFI/statefunc/core/runtime.py +47 -0
  58. SFI/statefunc/factory.py +346 -0
  59. SFI/statefunc/interactor.py +90 -0
  60. SFI/statefunc/layout/__init__.py +29 -0
  61. SFI/statefunc/layout/_base.py +186 -0
  62. SFI/statefunc/layout/_eval_compiler.py +453 -0
  63. SFI/statefunc/layout/_fd_atoms.py +196 -0
  64. SFI/statefunc/layout/_grid.py +878 -0
  65. SFI/statefunc/layout/_sectors.py +166 -0
  66. SFI/statefunc/memhint.py +222 -0
  67. SFI/statefunc/nodes/__init__.py +56 -0
  68. SFI/statefunc/nodes/base.py +318 -0
  69. SFI/statefunc/nodes/contract.py +422 -0
  70. SFI/statefunc/nodes/interactions/__init__.py +23 -0
  71. SFI/statefunc/nodes/interactions/dispatcher.py +1365 -0
  72. SFI/statefunc/nodes/interactions/prepare.py +161 -0
  73. SFI/statefunc/nodes/interactions/specs.py +362 -0
  74. SFI/statefunc/nodes/interactions/stencils.py +718 -0
  75. SFI/statefunc/nodes/leaf.py +530 -0
  76. SFI/statefunc/nodes/ops/__init__.py +27 -0
  77. SFI/statefunc/nodes/ops/concat.py +28 -0
  78. SFI/statefunc/nodes/ops/derivative.py +447 -0
  79. SFI/statefunc/nodes/ops/einsum.py +120 -0
  80. SFI/statefunc/nodes/ops/linear.py +153 -0
  81. SFI/statefunc/nodes/ops/mapn.py +138 -0
  82. SFI/statefunc/nodes/ops/reshape_rank.py +158 -0
  83. SFI/statefunc/nodes/ops/slice.py +83 -0
  84. SFI/statefunc/params.py +309 -0
  85. SFI/statefunc/psf.py +112 -0
  86. SFI/statefunc/sf.py +104 -0
  87. SFI/statefunc/stateexpr.py +1243 -0
  88. SFI/statefunc/structexpr.py +1003 -0
  89. SFI/trajectory/__init__.py +31 -0
  90. SFI/trajectory/collection.py +1122 -0
  91. SFI/trajectory/dataset.py +1164 -0
  92. SFI/trajectory/degrade.py +1108 -0
  93. SFI/trajectory/io.py +1027 -0
  94. SFI/trajectory/reserved_extras.py +129 -0
  95. SFI/utils/__init__.py +17 -0
  96. SFI/utils/formatting.py +308 -0
  97. SFI/utils/maths.py +185 -0
  98. SFI/utils/neighbors.py +162 -0
  99. SFI/utils/plotting.py +1936 -0
  100. stochasticforceinference-2.0.0.dist-info/METADATA +203 -0
  101. stochasticforceinference-2.0.0.dist-info/RECORD +104 -0
  102. stochasticforceinference-2.0.0.dist-info/WHEEL +5 -0
  103. stochasticforceinference-2.0.0.dist-info/licenses/LICENSE +21 -0
  104. 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)