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,422 @@
1
+ # SFI/statefunc/contract.py
2
+ # --------------------------
3
+
4
+ from enum import IntEnum
5
+ from typing import Any, Optional, Sequence
6
+
7
+ import equinox as eqx
8
+ from jaxtyping import Array
9
+
10
+ from ..params import ParamSuite
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Enums - ease of reading & cheap comparisons
15
+ # ---------------------------------------------------------------------------
16
+ class Rank(IntEnum):
17
+ SCALAR = 0
18
+ VECTOR = 1
19
+ MATRIX = 2
20
+ TENSOR3 = 3
21
+ TENSOR4 = 4 # extend if/when needed
22
+
23
+
24
+ # ---------------------------------------------------------------------
25
+ # Contract mix-in (rank | dim | pdepth | … + runtime guards)
26
+ # ---------------------------------------------------------------------
27
+ class _ContractMixin(eqx.Module):
28
+ """
29
+ Static contract shared by every StateExpr node.
30
+
31
+ Parameters are stored as *eqx.field(static=True)* so they never flow into AD.
32
+
33
+ Attributes
34
+ ----------
35
+ rank : Rank enum
36
+ Tensor rank of each basis element (0=scalar, 1=vector, ...).
37
+ dim : int | None
38
+ Spatial dimension (None = agnostic).
39
+ needs_v : bool
40
+ Dictionary requires velocity input?
41
+ pdepth : int
42
+ Particle depth: how many particle axes the node's **output** carries.
43
+ Inputs with `particles_input=True` *consume* one particle axis from `x`.
44
+ n_features : int
45
+ Number of basis functions/features carried by this node (feature-last).
46
+ param_suite : ParamSuite | None
47
+ - None → deterministic node
48
+ - ParamSuite → parametric node (no broadcasting of params).
49
+ extras_required : tuple[str, ...]
50
+ Presence-only requirements for the `extras` mapping. **No broadcasting is
51
+ validated here.** Dispatcher/leaves decide how extras are consumed.
52
+ particles_input : bool
53
+ If True, `x` is expected to have a particle axis: layout `batch · P · dim`.
54
+ sdims : tuple[int, ...] | None
55
+ Spatial dimensions of the node's output. None = agnostic. Useful for structured
56
+ data like grids, where each rank axis may have a different size. If provided, it
57
+ is used for validating outputs and merging contracts of composite nodes (e.g.
58
+ concatenation requires matching sdims on the concatenated axis). For leaf nodes,
59
+ if sdims is None but dim is provided, we treat all rank axes as having size `dim`.
60
+ For composite nodes, sdims must be explicitly provided if any child has sdims; we
61
+ do not attempt to infer or broadcast sdims from dim in that case.
62
+ """
63
+
64
+ # ---------- static metadata --------------------------------------
65
+ rank: "Rank" = eqx.field(static=True)
66
+ dim: Optional[int] = eqx.field(static=True, default=None)
67
+ needs_v: bool = eqx.field(static=True, default=False)
68
+ pdepth: int = eqx.field(static=True, default=0)
69
+ n_features: int = eqx.field(static=True, default=1)
70
+ param_suite: ParamSuite | None = eqx.field(static=True, default=None)
71
+ extras_required: tuple[str, ...] = eqx.field(static=True, default=())
72
+ particles_input: bool = eqx.field(static=True, default=False)
73
+ sdims: tuple[int, ...] | None = eqx.field(static=True, default=None)
74
+
75
+ @staticmethod
76
+ def inherit_contract(child: "_ContractMixin", **overrides) -> dict:
77
+ """
78
+ Start from child's static contract and override only what changes.
79
+ Keys: rank, dim, pdepth, n_features, needs_v, particles_input, sdims
80
+ """
81
+ out = dict(
82
+ rank=child.rank,
83
+ dim=child.dim,
84
+ pdepth=child.pdepth,
85
+ n_features=child.n_features,
86
+ needs_v=child.needs_v,
87
+ particles_input=child.particles_input,
88
+ sdims=child.sdims,
89
+ )
90
+ out.update(overrides)
91
+ return out
92
+
93
+ # -----------------------------------------------------------------
94
+ # merge helper used by composite nodes
95
+ # -----------------------------------------------------------------
96
+ @classmethod
97
+ def merge_contract(
98
+ cls,
99
+ children: Sequence["_ContractMixin"],
100
+ *,
101
+ mode: str,
102
+ spec: str | None = None,
103
+ ) -> dict:
104
+ """
105
+ Single source of truth for merging static contracts of composite nodes.
106
+
107
+ Parameters
108
+ ----------
109
+ children : sequence of nodes
110
+ The operands to merge.
111
+ mode : {'concat','map','einsum'}
112
+ Semantic of the operation.
113
+ ``'concat'``: feature-axis concatenation.
114
+ ``'map'``: elementwise Hadamard-like map (identical shapes/features).
115
+ ``'einsum'``: spatial contraction; requires ``spec``.
116
+ spec : str | None
117
+ Einstein notation for 'einsum' mode, lowercase letters only
118
+ on spatial axes. Example: "m,n->mn", "m,m->", ",n->n".
119
+
120
+ Returns
121
+ -------
122
+ dict with keys: rank, dim, pdepth, n_features, needs_v, particles_input
123
+
124
+ Notes
125
+ -----
126
+ - Particle-depth and particles_input are merged with the broadcast rule:
127
+ Allowed mixes:
128
+ - identical (pdepth, particles_input) for all children
129
+ - OR (pdepth=0, particles_input=False) combined with (pdepth=1, particles_input=True)
130
+ nodes. In that case the input particle axis is treated as broadcast by the False node,
131
+ which aligns and conceptually matches the particle axis in the True node.
132
+ The merged depth is the **maximum**; particles_input is True if any child has it.
133
+ - This function is purposely strict on error messages to make debugging easy.
134
+ - Extras policy: composite nodes **union** children’s `extras_required` at construction
135
+ (presence-only). This function does not validate extras shapes or broadcasting.
136
+ """
137
+ if not children:
138
+ raise ValueError("merge_contract(mode=%r): needs at least one child" % mode)
139
+
140
+ # ---------------- helpers ----------------
141
+ def _merge_pdepth(nodes: Sequence["_ContractMixin"]) -> tuple[int, bool]:
142
+ pd, flag = nodes[0].pdepth, nodes[0].particles_input
143
+ for ch in nodes[1:]:
144
+ if (ch.pdepth, ch.particles_input) == (pd, flag):
145
+ continue
146
+ # benign broadcast case
147
+ if (pd, flag) == (0, False) and ch.particles_input and ch.pdepth == 1:
148
+ pd, flag = ch.pdepth, True
149
+ continue
150
+ if (ch.pdepth, ch.particles_input) == (0, False) and flag and pd == 1:
151
+ # keep current pd/flag
152
+ continue
153
+ raise ValueError(
154
+ "merge_contract(%s): incompatible particle-depth mix: "
155
+ "A(pdepth=%d, particles_input=%s) vs B(pdepth=%d, particles_input=%s)"
156
+ % (mode, pd, flag, ch.pdepth, ch.particles_input)
157
+ )
158
+ return int(pd), bool(flag)
159
+
160
+ def _unify_dim(nodes: Sequence["_ContractMixin"]) -> int | None:
161
+ dims = [n.dim for n in nodes]
162
+ concrete = [d for d in dims if d is not None]
163
+ if not concrete:
164
+ return None
165
+ d0 = concrete[0]
166
+ if any(d != d0 for d in concrete[1:]):
167
+ raise ValueError("merge_contract(%s): children disagree on `dim`: %s" % (mode, tuple(dims)))
168
+ return d0
169
+
170
+ def _all_equal(attrs: tuple[str, ...]) -> tuple[bool, dict]:
171
+ """Check equality across children for the given attributes."""
172
+ vals = {a: tuple(getattr(ch, a) for ch in children) for a in attrs}
173
+ ok = all(len(set(vals[a])) == 1 for a in attrs)
174
+ return ok, vals
175
+
176
+ def _unify_sdims(nodes: Sequence["_ContractMixin"]) -> tuple[int, ...] | None:
177
+ """Merge sdims: all must agree, or all be None."""
178
+ all_sd = [n.sdims for n in nodes]
179
+ if all(s is None for s in all_sd):
180
+ return None
181
+ if any(s is None for s in all_sd):
182
+ raise ValueError(
183
+ "merge_contract(%s): some children have sdims=None while "
184
+ "others have explicit sdims: %s" % (mode, all_sd)
185
+ )
186
+ s0 = all_sd[0]
187
+ if any(s != s0 for s in all_sd[1:]):
188
+ raise ValueError("merge_contract(%s): children disagree on sdims: %s" % (mode, all_sd))
189
+ return s0
190
+
191
+ # -------------- fields independent of mode --------------
192
+ needs_v = any(ch.needs_v for ch in children)
193
+ pdepth, pflag = _merge_pdepth(children)
194
+ dim = _unify_dim(children)
195
+
196
+ # -------------- mode-specific logic ---------------------
197
+ mode = mode.lower()
198
+ if mode == "concat":
199
+ ok, vals = _all_equal(("rank",))
200
+ if not ok:
201
+ raise ValueError(f"merge_contract(concat): children must share rank; got rank={vals['rank']}")
202
+ n_features = sum(int(ch.n_features) for ch in children)
203
+ rank = int(children[0].rank)
204
+ sdims = _unify_sdims(children)
205
+
206
+ elif mode in {"map"}:
207
+ ok, vals = _all_equal(("rank", "n_features"))
208
+ if not ok:
209
+ raise ValueError(
210
+ f"merge_contract({mode}): children must share rank and n_features; "
211
+ f"got rank={vals['rank']}, n_features={vals['n_features']}"
212
+ )
213
+ n_features = int(children[0].n_features)
214
+ rank = int(children[0].rank)
215
+ sdims = _unify_sdims(children)
216
+
217
+ elif mode == "einsum":
218
+ if not spec or "->" not in spec:
219
+ raise ValueError(
220
+ "merge_contract(einsum): a valid `spec` like 'i,j->ij' is required"
221
+ " (with RHS; no ellipses; batch is implicit)"
222
+ )
223
+ if "..." in spec:
224
+ raise ValueError(
225
+ "merge_contract(einsum): ellipsis '...' is reserved for batch and must NOT appear in specs"
226
+ )
227
+
228
+ lhs, rhs = spec.replace(" ", "").split("->")
229
+ lhs_ops = lhs.split(",")
230
+
231
+ if len(lhs_ops) != len(children):
232
+ raise ValueError(
233
+ "merge_contract(einsum): spec lists %d operands but %d children were provided"
234
+ % (len(lhs_ops), len(children))
235
+ )
236
+
237
+ def _letters_only(op: str) -> str:
238
+ # Only lowercase letters; no dots allowed at all.
239
+ if "." in op:
240
+ raise ValueError(f"merge_contract(einsum): malformed operand {op!r} (dots/ellipsis not allowed)")
241
+ bad = [c for c in op if not c.islower()]
242
+ if bad:
243
+ raise ValueError(f"merge_contract(einsum): operand {op!r} contains non-lowercase symbols: {bad}")
244
+ return op
245
+
246
+ # LHS validation and rank compatibility (spatial rank == len(token))
247
+ lhs_clean = []
248
+ for idx, (ch, op) in enumerate(zip(children, lhs_ops)):
249
+ core = _letters_only(op)
250
+ if len(core) != int(ch.rank):
251
+ raise ValueError(
252
+ "merge_contract(einsum): rank mismatch for child %d: child.rank=%d, "
253
+ "operand %r has %d spatial letters" % (idx, int(ch.rank), op, len(core))
254
+ )
255
+ lhs_clean.append(core)
256
+
257
+ # RHS validation: subset of union of LHS letters
258
+ rhs_core = _letters_only(rhs)
259
+ if set(rhs_core) - set("".join(lhs_clean)):
260
+ raise ValueError("merge_contract(einsum): RHS contains letters not present on LHS")
261
+
262
+ rank = len(rhs_core)
263
+
264
+ # Feature axes: Cartesian product
265
+ from functools import reduce
266
+ from operator import mul
267
+
268
+ n_features = reduce(mul, (int(ch.n_features) for ch in children), 1)
269
+
270
+ # --- sdims for einsum: build per-letter size map ---
271
+ any_has_sdims = any(ch.sdims is not None for ch in children)
272
+ if any_has_sdims:
273
+ letter_size: dict[str, int] = {}
274
+ for ch, op in zip(children, lhs_clean):
275
+ ch_sdims = ch.sdims
276
+ if ch_sdims is None:
277
+ # Uniform dim: all rank axes are dim-sized
278
+ if ch.dim is None:
279
+ raise ValueError(
280
+ "merge_contract(einsum): child has sdims=None and "
281
+ "dim=None but other children have sdims; cannot "
282
+ "determine letter sizes"
283
+ )
284
+ ch_sdims = (ch.dim,) * int(ch.rank)
285
+ for letter, size in zip(op, ch_sdims):
286
+ if letter in letter_size:
287
+ if letter_size[letter] != size:
288
+ raise ValueError(
289
+ f"merge_contract(einsum): letter {letter!r} has "
290
+ f"inconsistent sizes: {letter_size[letter]} vs {size}"
291
+ )
292
+ else:
293
+ letter_size[letter] = size
294
+ sdims = tuple(letter_size[c] for c in rhs_core) if rhs_core else ()
295
+ else:
296
+ sdims = None
297
+
298
+ else:
299
+ raise ValueError("merge_contract: unknown mode %r" % mode)
300
+
301
+ return dict(
302
+ rank=Rank(rank),
303
+ dim=dim,
304
+ sdims=sdims,
305
+ pdepth=int(pdepth),
306
+ n_features=int(n_features),
307
+ needs_v=bool(needs_v),
308
+ particles_input=bool(pflag),
309
+ )
310
+
311
+ def _assert_inputs(
312
+ self,
313
+ x: Array,
314
+ v: Optional[Array],
315
+ mask: Optional[Array],
316
+ extras: Any | None = None,
317
+ ):
318
+ # ---------- spatial dimension ---------------------------------
319
+ if (self.dim is not None) and (x.shape[-1] != self.dim):
320
+ raise ValueError(f"[Node] expected x.shape[-1]=={self.dim}, got {x.shape[-1]}")
321
+
322
+ # ---------- minimal rank of x (batch · [P?] · dim) ------------
323
+ min_ndim = 2 if self.particles_input else 1
324
+ if x.ndim < min_ndim:
325
+ need = "batch·P·dim" if self.particles_input else "batch·dim"
326
+ raise ValueError(f"[Node] x.ndim={x.ndim} < required for {need}")
327
+
328
+ # Prefixes used for later checks
329
+ prefix_shape = x.shape[:-1] # everything except spatial dim (includes P if present)
330
+
331
+ # ---------- velocity ------------------------------------------
332
+ if self.needs_v:
333
+ if v is None:
334
+ raise ValueError("[Node] velocity `v` required but not provided")
335
+ if v.shape != x.shape:
336
+ raise ValueError("[Node] `v` must have same shape as `x`")
337
+
338
+ # ---------- helpers -------------------------------------------
339
+ def _strip_trailing_ones(shp: tuple[int, ...]) -> tuple[int, ...]:
340
+ k = len(shp)
341
+ while k > 0 and shp[k - 1] == 1:
342
+ k -= 1
343
+ return shp[:k]
344
+
345
+ def _broadcastable(dst: tuple[int, ...], src: tuple[int, ...]) -> bool:
346
+ # NumPy broadcasting: align from the right
347
+ for a, b in zip(dst[::-1], src[::-1]):
348
+ if b not in (1, a):
349
+ return False
350
+ if len(src) > len(dst):
351
+ lead = src[: len(src) - len(dst)]
352
+ return all(d == 1 for d in lead)
353
+ return True
354
+
355
+ # ---------- mask ----------------------------------------------
356
+ if mask is not None:
357
+ # Allow boolean OR numeric masks. Require broadcast to prefix_shape (batch·[P]),
358
+ # without stripping trailing ones (so (B,1) can broadcast to (B,P)).
359
+ if hasattr(mask, "shape"):
360
+ mshape = tuple(mask.shape)
361
+
362
+ if not _broadcastable(prefix_shape, mshape):
363
+ raise ValueError(
364
+ "[Node] mask must broadcast to x's batch/particle prefix: "
365
+ f"expected {prefix_shape}, got {mask.shape}"
366
+ )
367
+ else:
368
+ # Non-array (e.g. Python float) is treated as scalar broadcast.
369
+ pass
370
+
371
+ # ----- extras: presence-only contract (no broadcasting checks here) -----
372
+ req = tuple(getattr(self, "extras_required", ()) or ())
373
+
374
+ if req:
375
+ if extras is None:
376
+ # Named keys were declared but no mapping was provided at all.
377
+ raise KeyError(f"[Node] missing extras keys: {req}")
378
+ missing = [k for k in req if k not in extras]
379
+ if missing:
380
+ raise KeyError(f"[Node] missing extras keys: {tuple(missing)}")
381
+ # (If extras is provided, we do not validate shapes here—dispatcher/leaf runtime handles that.)
382
+
383
+ # -----------------------------------------------------------------
384
+ def _assert_outputs(self, x: Array, y: Array):
385
+ # -------- feature axis size -----------------------------------------
386
+ if y.shape[-1] != self.n_features:
387
+ raise ValueError(f"[Node] expected last axis length {self.n_features}, got {y.shape[-1]}")
388
+
389
+ # -------- rank axes size ---------------------------------------------
390
+ r_axes = int(self.rank) # 0 = scalar, 1 = vector, etc.
391
+ rank_shape = () if r_axes == 0 else y.shape[-(r_axes + 1) : -1]
392
+ if r_axes > 0:
393
+ if self.sdims is not None:
394
+ # Structured dimensions: per-axis sizes from sdims
395
+ expected_rank = self.sdims
396
+ elif self.dim is not None:
397
+ # Uniform dimensions: all rank axes are dim-sized
398
+ expected_rank = (self.dim,) * r_axes
399
+ else:
400
+ expected_rank = None
401
+ if expected_rank is not None and rank_shape != expected_rank:
402
+ raise ValueError(f"[Node] rank axes shape {rank_shape} ≠ expected {expected_rank}")
403
+
404
+ # -------- batch / particle prefix --------------------
405
+ # Layouts:
406
+ # x : batch · [P_in?] · dim
407
+ # y : batch · [P_out^pdepth] · (rank) · feature
408
+ y_prefix = y.shape[: -(r_axes + 1)]
409
+ x_batch = x.shape[:-2] if self.particles_input else x.shape[:-1]
410
+
411
+ if self.particles_input:
412
+ P_in = x.shape[-2]
413
+ expected = x_batch + ((P_in,) * int(self.pdepth))
414
+ else:
415
+ expected = x_batch if int(self.pdepth) == 0 else None
416
+
417
+ if expected is None or y_prefix != expected:
418
+ raise ValueError(
419
+ "[Node] output prefix incompatible: "
420
+ f"y_prefix={y_prefix}, expected={expected}, "
421
+ f"particles_input={self.particles_input}, pdepth={self.pdepth}"
422
+ )
@@ -0,0 +1,23 @@
1
+ from .dispatcher import InteractionDispatcher
2
+ from .specs import (
3
+ AutoPairs,
4
+ CachedRule,
5
+ FromExtrasHyperFixed,
6
+ FromExtrasPairsCSR,
7
+ HyperCSR,
8
+ HyperFixed,
9
+ PairsCSR,
10
+ SpecRule,
11
+ )
12
+
13
+ __all__ = [
14
+ "PairsCSR",
15
+ "HyperFixed",
16
+ "HyperCSR",
17
+ "SpecRule",
18
+ "CachedRule",
19
+ "FromExtrasHyperFixed",
20
+ "AutoPairs",
21
+ "FromExtrasPairsCSR",
22
+ "InteractionDispatcher",
23
+ ]