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,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
|
SFI/statefunc/memhint.py
ADDED
|
@@ -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
|
+
]
|