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,166 @@
1
+ """Sector types for the structured dimensions layer.
2
+
3
+ Defines :class:`ScalarSector`, :class:`VectorSector`,
4
+ :class:`SymTensorSector`, :class:`TensorSector` and the convenience
5
+ type alias :data:`Sector`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+
13
+ # =====================================================================
14
+ # Sector types
15
+ # =====================================================================
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class ScalarSector:
20
+ """A single scalar field occupying one data index.
21
+
22
+ ``sdims = ()``, ``n_data = 1``.
23
+ """
24
+
25
+ indices: tuple[int, ...]
26
+
27
+ def __post_init__(self) -> None:
28
+ object.__setattr__(self, "indices", tuple(self.indices))
29
+ if len(self.indices) != 1:
30
+ raise ValueError(f"ScalarSector requires exactly 1 index, got {len(self.indices)}: {self.indices}")
31
+
32
+ @property
33
+ def sdims(self) -> tuple[int, ...]:
34
+ return ()
35
+
36
+ @property
37
+ def n_data(self) -> int:
38
+ return 1
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class VectorSector:
43
+ """A vector field with *sdim* components.
44
+
45
+ ``sdims = (sdim,)``, ``n_data = sdim``.
46
+
47
+ Parameters
48
+ ----------
49
+ spatial : bool
50
+ When ``True``, declares that this sector's components correspond
51
+ to spatial coordinates. Layouts with ``ndim`` validate that
52
+ ``sdim == ndim``.
53
+ """
54
+
55
+ indices: tuple[int, ...]
56
+ sdim: int
57
+ spatial: bool = False
58
+
59
+ def __post_init__(self) -> None:
60
+ object.__setattr__(self, "indices", tuple(self.indices))
61
+ if self.sdim < 1:
62
+ raise ValueError(f"sdim must be >= 1, got {self.sdim}")
63
+ if len(self.indices) != self.sdim:
64
+ raise ValueError(f"VectorSector(sdim={self.sdim}) requires {self.sdim} indices, got {len(self.indices)}")
65
+
66
+ @property
67
+ def sdims(self) -> tuple[int, ...]:
68
+ return (self.sdim,)
69
+
70
+ @property
71
+ def n_data(self) -> int:
72
+ return self.sdim
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class SymTensorSector:
77
+ """A symmetric tensor field, Voigt-packed in data space.
78
+
79
+ ``sdims = (sdim, sdim)``.
80
+
81
+ When ``traceless=False`` (default), ``n_data = sdim * (sdim + 1) // 2``
82
+ (full Voigt packing). When ``traceless=True``,
83
+ ``n_data = sdim * (sdim + 1) // 2 - 1`` (the trace degree of freedom
84
+ is removed: the diagonal is constrained so that
85
+ :math:`Q_{00} + Q_{11} + \\ldots = 0`).
86
+
87
+ Parameters
88
+ ----------
89
+ traceless : bool
90
+ When ``True``, the tensor is both symmetric *and* traceless.
91
+ Only the independent components are stored in data space;
92
+ the last diagonal entry is reconstructed as minus the sum
93
+ of the other diagonal entries.
94
+ """
95
+
96
+ indices: tuple[int, ...]
97
+ sdim: int
98
+ traceless: bool = False
99
+
100
+ def __post_init__(self) -> None:
101
+ object.__setattr__(self, "indices", tuple(self.indices))
102
+ expected = self._n_independent
103
+ if len(self.indices) != expected:
104
+ kind = "traceless symmetric" if self.traceless else "symmetric"
105
+ raise ValueError(
106
+ f"SymTensorSector(sdim={self.sdim}, traceless={self.traceless}) "
107
+ f"requires {expected} indices ({kind} packing), "
108
+ f"got {len(self.indices)}"
109
+ )
110
+
111
+ @property
112
+ def _n_independent(self) -> int:
113
+ """Number of independent components stored in data space."""
114
+ n_voigt = self.sdim * (self.sdim + 1) // 2
115
+ return n_voigt - 1 if self.traceless else n_voigt
116
+
117
+ @property
118
+ def sdims(self) -> tuple[int, ...]:
119
+ return (self.sdim, self.sdim)
120
+
121
+ @property
122
+ def n_data(self) -> int:
123
+ return self._n_independent
124
+
125
+ @property
126
+ def voigt_pairs(self) -> list[tuple[int, int]]:
127
+ """Upper-triangle ``(i, j)`` pairs matching the index order.
128
+
129
+ When ``traceless=True`` the last diagonal pair
130
+ ``(sdim-1, sdim-1)`` is omitted (it is reconstructed from the
131
+ other diagonal entries).
132
+ """
133
+ pairs: list[tuple[int, int]] = []
134
+ for i in range(self.sdim):
135
+ for j in range(i, self.sdim):
136
+ pairs.append((i, j))
137
+ if self.traceless:
138
+ # Remove the last diagonal entry (sdim-1, sdim-1)
139
+ pairs = [p for p in pairs if p != (self.sdim - 1, self.sdim - 1)]
140
+ return pairs
141
+
142
+
143
+ @dataclass(frozen=True, slots=True)
144
+ class TensorSector:
145
+ """A general tensor field with arbitrary ``sdims``.
146
+
147
+ ``n_data = prod(sdims)``.
148
+ """
149
+
150
+ indices: tuple[int, ...]
151
+ sdims: tuple[int, ...]
152
+
153
+ def __post_init__(self) -> None:
154
+ object.__setattr__(self, "indices", tuple(self.indices))
155
+ object.__setattr__(self, "sdims", tuple(self.sdims))
156
+ expected = math.prod(self.sdims)
157
+ if len(self.indices) != expected:
158
+ raise ValueError(f"TensorSector(sdims={self.sdims}) requires {expected} indices, got {len(self.indices)}")
159
+
160
+ @property
161
+ def n_data(self) -> int:
162
+ return math.prod(self.sdims)
163
+
164
+
165
+ # Convenience type alias
166
+ Sector = ScalarSector | VectorSector | SymTensorSector | TensorSector
@@ -0,0 +1,222 @@
1
+ # SFI/statefunc/memhint.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Iterable, Optional, Protocol
6
+
7
+ import jax.numpy as jnp
8
+
9
+ # ──────────────────────────────────────────────────────────────────────────────
10
+ # Core types
11
+ # ──────────────────────────────────────────────────────────────────────────────
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class MemHint:
16
+ """
17
+ Conservative memory footprint per SINGLE sample.
18
+ - per_sample_bytes scales with the number of samples.
19
+ - persistent_bytes is model state / CSR / weights, does NOT scale with samples.
20
+ """
21
+
22
+ per_sample_bytes: int = 0
23
+ persistent_bytes: int = 0
24
+
25
+ def __add__(self, other: "MemHint") -> "MemHint":
26
+ return MemHint(
27
+ self.per_sample_bytes + other.per_sample_bytes,
28
+ self.persistent_bytes + other.persistent_bytes,
29
+ )
30
+
31
+ def scaled(self, k: int) -> "MemHint":
32
+ """Scale only the transient part (useful if you ever convert to total bytes)."""
33
+ return MemHint(self.per_sample_bytes * int(k), self.persistent_bytes)
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class SampleMeta:
38
+ """
39
+ Optional single-sample context to refine estimates.
40
+
41
+ P: number of particles in ONE sample (for nodes with particle axes, pdepth>0).
42
+ K: arity for interaction gathers when fixed/known (pairs=2, etc.).
43
+ has_v: whether a velocity block will be present.
44
+ has_mask: whether a boolean mask participates in the call.
45
+ """
46
+
47
+ P: Optional[int] = None
48
+ K: Optional[int] = None
49
+ has_v: Optional[bool] = None
50
+ has_mask: Optional[bool] = None
51
+
52
+ @staticmethod
53
+ def from_arrays(x=None, v=None, mask=None) -> "SampleMeta":
54
+ """
55
+ Try to recover P from provided single-sample arrays without allocating anything.
56
+ Heuristics:
57
+
58
+ - If x has at least 2 dims and looks like (..., dim) with a leading particle axis,
59
+ we guess P from the axis before `dim`. If ambiguous, we leave P=None.
60
+ - v and mask only toggle flags; they don't change P.
61
+ """
62
+ P = None
63
+ if x is not None and hasattr(x, "shape"):
64
+ # super conservative: if it looks like (..., P, dim) with small dim
65
+ if x.ndim >= 2:
66
+ dim_guess = int(x.shape[-1])
67
+ if 1 <= dim_guess <= 16:
68
+ # take the previous axis as P
69
+ P = int(x.shape[-2])
70
+ return SampleMeta(
71
+ P=P,
72
+ K=None,
73
+ has_v=(v is not None),
74
+ has_mask=(mask is not None),
75
+ )
76
+
77
+
78
+ # ──────────────────────────────────────────────────────────────────────────────
79
+ # Helpers
80
+ # ──────────────────────────────────────────────────────────────────────────────
81
+
82
+
83
+ def itemsize_of(dtype) -> int:
84
+ """Return dtype itemsize as an int; defaults to float32 when dtype is None."""
85
+ return int(jnp.dtype(jnp.float32 if dtype is None else dtype).itemsize)
86
+
87
+
88
+ class _HasContract(Protocol):
89
+ # Minimal surface we read from nodes (already present on your BaseNode/ops).
90
+ rank: int
91
+ dim: Optional[int]
92
+ pdepth: int
93
+ n_features: int
94
+
95
+
96
+ def resolve_P(particle_size: Optional[int], sample) -> Optional[int]:
97
+ """Pick P from explicit particle_size, else from SampleMeta or array, else None."""
98
+ if particle_size is not None:
99
+ return int(particle_size)
100
+ if sample is None:
101
+ return None
102
+
103
+ # Accept either a SampleMeta or an array-like (single-sample x)
104
+ if isinstance(sample, SampleMeta):
105
+ sm = sample
106
+ else:
107
+ # best-effort duck-typing: treat it as x and try to read P
108
+ try:
109
+ sm = SampleMeta.from_arrays(x=sample)
110
+ except Exception:
111
+ sm = None
112
+
113
+ return None if (sm is None or sm.P is None) else int(sm.P)
114
+
115
+
116
+ def output_elems_per_sample(node: _HasContract, *, particle_size: Optional[int]) -> int:
117
+ """
118
+ Count output elements of a node for ONE sample from its static contract.
119
+
120
+ Output shape suffix is ``(*particle_axes, *rank_axes, n_features)``
121
+ with ``particle_axes = (P,) * pdepth`` and ``rank_axes = (dim,) * rank``.
122
+
123
+ If ``particle_size`` is None, treat ``P = 1`` (safe lower bound for
124
+ batch picking).
125
+ """
126
+ n_features = int(getattr(node, "n_features", 0) or 0)
127
+ if n_features <= 0:
128
+ return 0
129
+
130
+ dim = getattr(node, "dim", None)
131
+ rank = int(getattr(node, "rank", 0) or 0)
132
+ if dim is None and rank > 0:
133
+ return 0 # cannot infer rank block without dim
134
+
135
+ pdepth = int(getattr(node, "pdepth", 0) or 0)
136
+ P = 1 if particle_size is None else int(particle_size)
137
+
138
+ elems = (dim or 1) ** rank
139
+ if pdepth:
140
+ elems *= P**pdepth
141
+ elems *= n_features
142
+ return int(elems)
143
+
144
+
145
+ def output_bytes_per_sample(node: _HasContract, *, dtype, particle_size: Optional[int]) -> int:
146
+ """Translate element count to bytes using dtype itemsize."""
147
+ return output_elems_per_sample(node, particle_size=particle_size) * itemsize_of(dtype)
148
+
149
+
150
+ def default_leaf_hint(node: _HasContract, *, dtype, particle_size: Optional[int], mode: str) -> MemHint:
151
+ """
152
+ Default for leaf-like nodes: count only the output buffer per sample.
153
+ Composite nodes will also include their children.
154
+ """
155
+ return MemHint(per_sample_bytes=output_bytes_per_sample(node, dtype=dtype, particle_size=particle_size))
156
+
157
+
158
+ def broadcast_extra_bytes_for_children(*, children: Iterable, dtype, particle_size: Optional[int]) -> int:
159
+ """
160
+ When children have different particle depths, lower-depth outputs are broadcast
161
+ to match the max. Materializing that broadcast costs memory. We conservatively
162
+ account for an additional slab equal to (P^Δ - 1) times the child's OUTPUT bytes.
163
+ """
164
+ # target = max pdepth among children
165
+ target = 0
166
+ pd = []
167
+ for ch in children:
168
+ p = int(getattr(ch, "pdepth", 0) or 0)
169
+ pd.append(p)
170
+ if p > target:
171
+ target = p
172
+ if target == 0:
173
+ return 0
174
+
175
+ extra = 0
176
+ P = 1 if particle_size is None else int(particle_size)
177
+ for ch in children:
178
+ p = int(getattr(ch, "pdepth", 0) or 0)
179
+ if p < target:
180
+ delta = target - p
181
+ # only the OUTPUT slab needs broadcasting; not the child's internal working set
182
+ base_out = output_bytes_per_sample(ch, dtype=dtype, particle_size=particle_size)
183
+ factor = P**delta - 1
184
+ if factor > 0 and base_out > 0:
185
+ extra += base_out * factor
186
+ return int(extra)
187
+
188
+
189
+ def default_op_hint(
190
+ node: _HasContract,
191
+ *,
192
+ children: Iterable,
193
+ dtype,
194
+ particle_size: Optional[int],
195
+ mode: str,
196
+ ) -> MemHint:
197
+ """
198
+ Default for composite ops: sum(children.hint) + my output buffer + broadcast overhead.
199
+ We assume child outputs coexist while the op constructs its result.
200
+ """
201
+ hint = MemHint()
202
+ for ch in children:
203
+ hint = hint + ch.memory_hint(dtype=dtype, particle_size=particle_size, mode=mode)
204
+ # add broadcast materialization if children pdepth differ
205
+ hint = MemHint(
206
+ hint.per_sample_bytes
207
+ + broadcast_extra_bytes_for_children(children=children, dtype=dtype, particle_size=particle_size),
208
+ hint.persistent_bytes,
209
+ )
210
+ # add this op's own output
211
+ return hint + default_leaf_hint(node, dtype=dtype, particle_size=particle_size, mode=mode)
212
+
213
+
214
+ def inflate_for_grad(hint: MemHint, *, factor: float = 2.0) -> MemHint:
215
+ """
216
+ Blanket inflation for gradient-mode nodes to account for tangents/tapes.
217
+ Keep it simple and conservative; adjust locally if you get better bounds.
218
+ """
219
+ return MemHint(
220
+ per_sample_bytes=int(hint.per_sample_bytes * factor),
221
+ persistent_bytes=hint.persistent_bytes,
222
+ )
@@ -0,0 +1,56 @@
1
+ # SFI/statefunc/nodes/__init__.py
2
+ """
3
+ Developer-facing node classes.
4
+
5
+ Most users should import from `SFI.statefunc` (Basis/PSF/SF).
6
+ This module exposes the node types for advanced composition.
7
+ """
8
+
9
+ from .base import BaseNode, BaseOpNode
10
+ from .contract import Rank
11
+ from .leaf import InteractionLeaf, SimpleLeaf
12
+ from .ops import (
13
+ CoeffNode,
14
+ ConcatNode,
15
+ DenseNode,
16
+ DerivativeNode,
17
+ EinsumNode,
18
+ MapNNode,
19
+ ReshapeRankNode,
20
+ SliceFeaturesNode,
21
+ )
22
+
23
+ __all__ = [
24
+ "BaseNode",
25
+ "BaseOpNode",
26
+ "SimpleLeaf",
27
+ "ConcatNode",
28
+ "MapNNode",
29
+ "EinsumNode",
30
+ "DenseNode",
31
+ "CoeffNode",
32
+ "SliceFeaturesNode",
33
+ "DerivativeNode",
34
+ "ReshapeRankNode",
35
+ "Rank",
36
+ ]
37
+
38
+
39
+ from .interactions import (
40
+ AutoPairs,
41
+ FromExtrasPairsCSR,
42
+ HyperCSR,
43
+ HyperFixed,
44
+ InteractionDispatcher,
45
+ PairsCSR,
46
+ )
47
+
48
+ __all__ += [
49
+ "InteractionDispatcher",
50
+ "PairsCSR",
51
+ "HyperFixed",
52
+ "HyperCSR",
53
+ "AutoPairs",
54
+ "FromExtrasPairsCSR",
55
+ "InteractionLeaf",
56
+ ]