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,153 @@
|
|
|
1
|
+
# SFI/statefunc/nodes/ops/linear.py
|
|
2
|
+
"""Linear algebra operator nodes: Dense and Coeff."""
|
|
3
|
+
|
|
4
|
+
import equinox as eqx
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
from ...params import ParamSpec, ParamSuite
|
|
8
|
+
from ..base import BaseNode, BaseOpNode
|
|
9
|
+
from ..contract import _ContractMixin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
# DenseNode – linear map on the feature axis of one child
|
|
14
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
class DenseNode(BaseOpNode):
|
|
16
|
+
"""
|
|
17
|
+
Affine map along the feature axis of a single child.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
n_out : int - output feature dimension
|
|
22
|
+
weight : str = "W" - parameter, shape ``(n_in, n_out)``
|
|
23
|
+
bias : str | None = "b" - parameter, shape ``(n_out,)`` or None
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
weight_name: str = eqx.field(static=True)
|
|
27
|
+
bias_name: str | None = eqx.field(static=True)
|
|
28
|
+
_n_out: int = eqx.field(static=True)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
def __init__(self, child: BaseNode, *, n_out: int, weight: str = "W", bias: str | None = "b"):
|
|
32
|
+
self.weight_name = weight
|
|
33
|
+
self.bias_name = bias
|
|
34
|
+
self._n_out = int(n_out)
|
|
35
|
+
super().__init__(child)
|
|
36
|
+
|
|
37
|
+
# own parameter template
|
|
38
|
+
W_spec = ParamSpec(weight, (child.n_features, n_out))
|
|
39
|
+
specs = [W_spec]
|
|
40
|
+
if bias is not None:
|
|
41
|
+
specs.append(ParamSpec(bias, (n_out,)))
|
|
42
|
+
own_suite = ParamSuite.from_specs(*specs)
|
|
43
|
+
|
|
44
|
+
merged = child.param_suite.merge(own_suite) if child.param_suite else own_suite
|
|
45
|
+
object.__setattr__(self, "param_suite", merged)
|
|
46
|
+
|
|
47
|
+
def with_children(self, new_children):
|
|
48
|
+
return DenseNode(
|
|
49
|
+
new_children[0],
|
|
50
|
+
n_out=self._n_out,
|
|
51
|
+
weight=self.weight_name,
|
|
52
|
+
bias=self.bias_name,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
def _merge_static(self, children):
|
|
57
|
+
ch = children[0]
|
|
58
|
+
return _ContractMixin.inherit_contract(ch, n_features=self._n_out)
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
def _op(self, outs, *, params):
|
|
62
|
+
y_in = outs[0]
|
|
63
|
+
W = params[self.weight_name]
|
|
64
|
+
b = params[self.bias_name] if self.bias_name else None
|
|
65
|
+
y = jnp.tensordot(y_in, W, axes=[-1, 0]) # "...i,ij->...j"
|
|
66
|
+
return y + b if b is not None else y
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# Feature-wise flatten
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
def flatten(self):
|
|
72
|
+
"""
|
|
73
|
+
Return (funcs, labels, descs) where
|
|
74
|
+
|
|
75
|
+
* ``funcs[j](...)`` calls the *DenseNode* itself and picks the
|
|
76
|
+
j-th feature slice → keeps maintenance trivial.
|
|
77
|
+
* Label is ``"dense{j}"`` as there's no practical way to propagate
|
|
78
|
+
previous labels through a Dense node.
|
|
79
|
+
* Descriptors are inherited from the **child** – the linear map
|
|
80
|
+
does not alter the mathematical identity, only its coefficients.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
child_funcs, child_labels, child_descs = self.children[0].flatten()
|
|
84
|
+
n_out = self.n_features
|
|
85
|
+
|
|
86
|
+
# --- per-feature callables -----------------------------------
|
|
87
|
+
def _slice_feature(j):
|
|
88
|
+
# j is captured as default arg to avoid late binding
|
|
89
|
+
return lambda x, *, v=None, mask=None, extras=None, params=None, _j=j: (
|
|
90
|
+
self(x, v=v, mask=mask, extras=extras, params=params)[..., _j]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
funcs = tuple(_slice_feature(j) for j in range(n_out))
|
|
94
|
+
|
|
95
|
+
# --- labels ---------------------------------------------------
|
|
96
|
+
labels = tuple(f"dense{j}" for j in range(n_out))
|
|
97
|
+
|
|
98
|
+
# --- descriptors (inherit) ------------------------------------
|
|
99
|
+
descs = tuple({"dense_of": tuple(child_descs), "index": j} for j in range(n_out))
|
|
100
|
+
|
|
101
|
+
return funcs, labels, descs
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
# CoeffNode – “Dense-with-θ-vector” (n_out = 1, bias = None)
|
|
106
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
class CoeffNode(DenseNode):
|
|
108
|
+
"""
|
|
109
|
+
Special-case of :class:`DenseNode` that stores its weights as a **vector**
|
|
110
|
+
θ ∈ ℝⁿ rather than a (n_in × 1) column matrix:
|
|
111
|
+
|
|
112
|
+
F(x; θ) = θ · f(x) # dot on feature axis, returns scalar
|
|
113
|
+
|
|
114
|
+
The output therefore has a **singleton feature axis** (n_features = 1).
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
child : BaseNode
|
|
119
|
+
Basis/dictionary to be linearly combined.
|
|
120
|
+
coeff_key : str, default ``"coeff"``
|
|
121
|
+
Parameter-dict name under which θ is stored.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
_basis_labels: tuple[str, ...] = eqx.field(static=True, default=())
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
def __init__(self, child: BaseNode, *, coeff_key: str = "coeff"):
|
|
128
|
+
# call DenseNode constructor with n_out = 1, bias = None
|
|
129
|
+
super().__init__(child, n_out=1, weight=coeff_key, bias=None)
|
|
130
|
+
|
|
131
|
+
# Override the weight spec to a **vector** (n_in,) instead of matrix,
|
|
132
|
+
# with a default of ones so bare `Basis → .to_psf()` simulates out of
|
|
133
|
+
# the box (equivalent to "sum all features").
|
|
134
|
+
theta = ParamSpec(coeff_key, (child.n_features,), default=jnp.ones((child.n_features,)))
|
|
135
|
+
own = ParamSuite.from_specs(theta)
|
|
136
|
+
merged = child.param_suite.merge(own) if child.param_suite else own
|
|
137
|
+
object.__setattr__(self, "param_suite", merged)
|
|
138
|
+
|
|
139
|
+
# Preserve the original basis labels for downstream reporting
|
|
140
|
+
_, child_labels, _ = child.flatten()
|
|
141
|
+
object.__setattr__(self, "_basis_labels", tuple(child_labels))
|
|
142
|
+
|
|
143
|
+
def with_children(self, new_children):
|
|
144
|
+
return CoeffNode(new_children[0], coeff_key=self.weight_name)
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# DenseNode._op expects W shaped (n_in, 1); we override to use a vector.
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
def _op(self, outs, *, params):
|
|
150
|
+
y_in = outs[0] # (..., n_feat)
|
|
151
|
+
theta = params[self.weight_name] # (n_feat,)
|
|
152
|
+
y = jnp.einsum("...b,b->...", y_in, theta)[..., None] # (..., 1)
|
|
153
|
+
return y
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SFI/statefunc/nodes/ops/mapn.py
|
|
2
|
+
"""MapN: broadcast and apply N-ary function across children."""
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
import equinox as eqx
|
|
7
|
+
|
|
8
|
+
from ...params import ParamSuite
|
|
9
|
+
from ..base import BaseNode, BaseOpNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
# MapNNode – elementwise map of N children (with optional parameters)
|
|
14
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
class MapNNode(BaseOpNode):
|
|
16
|
+
"""
|
|
17
|
+
Apply an arbitrary **element-wise** callable to *N* child bases.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
mapper : Callable
|
|
22
|
+
A function that will be called as::
|
|
23
|
+
|
|
24
|
+
mapper(*arrays[, theta]) -> array
|
|
25
|
+
|
|
26
|
+
The *arrays* (one per child) have **identical shapes**, including feature axis.
|
|
27
|
+
If a ``mapper_param_suite`` is supplied, a θ-dict with **only** those names
|
|
28
|
+
is appended as the *last positional argument*; otherwise no parameter payload
|
|
29
|
+
is passed to the mapper.
|
|
30
|
+
|
|
31
|
+
mapper_param_suite : ParamSuite | None
|
|
32
|
+
Parameter template **owned by the mapper itself** (not the children).
|
|
33
|
+
|
|
34
|
+
Contract rules
|
|
35
|
+
--------------
|
|
36
|
+
- Children must match in (rank, n_features). ``dim`` is unified if concrete.
|
|
37
|
+
- Particle depth follows broadcast rules: you may mix (pdepth=0, particles_input=False)
|
|
38
|
+
with (pdepth=1, True). The output uses the latter, and non-P children treat P as part
|
|
39
|
+
of the batch prefix.
|
|
40
|
+
- The mapper **must** return a tensor with the same last-axis size (``n_features``),
|
|
41
|
+
otherwise ``_assert_outputs`` will trigger.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
This node's full ``param_suite`` is the **shared-by-name** merge of
|
|
46
|
+
``(children's suites) ∪ (mapper_param_suite)``.
|
|
47
|
+
|
|
48
|
+
Reusing the same name across children/mapper **ties** that parameter provided
|
|
49
|
+
the shape/dtype match exactly; incompatible duplicates raise an error.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
CONTRACT_MODE = "map"
|
|
53
|
+
|
|
54
|
+
mapper: Callable = eqx.field(static=True)
|
|
55
|
+
mapper_param_suite: ParamSuite | None = eqx.field(static=True)
|
|
56
|
+
label_fn: Callable[[str], str] | None = eqx.field(static=True, default=None, repr=False)
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
mapper: Callable,
|
|
62
|
+
*children: BaseNode,
|
|
63
|
+
mapper_param_suite: ParamSuite | None = None,
|
|
64
|
+
label_fn: Callable[[str], str] | None = None,
|
|
65
|
+
):
|
|
66
|
+
object.__setattr__(self, "mapper", mapper)
|
|
67
|
+
object.__setattr__(self, "mapper_param_suite", mapper_param_suite)
|
|
68
|
+
object.__setattr__(self, "label_fn", label_fn)
|
|
69
|
+
|
|
70
|
+
# Build base composite first: this sets `param_suite` from children
|
|
71
|
+
super().__init__(*children)
|
|
72
|
+
|
|
73
|
+
# Now incorporate the mapper's own params using shared-by-name merge
|
|
74
|
+
# (allows parameter tying across children and mapper when specs match).
|
|
75
|
+
merged = ParamSuite.merge_many(self.param_suite, self.mapper_param_suite)
|
|
76
|
+
object.__setattr__(self, "param_suite", merged)
|
|
77
|
+
|
|
78
|
+
def with_children(self, new_children):
|
|
79
|
+
return MapNNode(
|
|
80
|
+
self.mapper,
|
|
81
|
+
*new_children,
|
|
82
|
+
mapper_param_suite=self.mapper_param_suite,
|
|
83
|
+
label_fn=self.label_fn,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
def _op(self, outs, *, params):
|
|
88
|
+
"""
|
|
89
|
+
Combine child outputs element-wise via `mapper`.
|
|
90
|
+
|
|
91
|
+
The `params` dict passed here is the full suite; we slice out only the
|
|
92
|
+
mapper's subset (if any) and append it as the final positional argument.
|
|
93
|
+
"""
|
|
94
|
+
if self.mapper_param_suite:
|
|
95
|
+
theta = {n: params[n] for n in self.mapper_param_suite._lookup}
|
|
96
|
+
return self.mapper(*outs, theta)
|
|
97
|
+
return self.mapper(*outs)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Feature-aware flatten (N children, element-wise map)
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
def flatten(self):
|
|
103
|
+
"""
|
|
104
|
+
Build (funcs, labels, descs) where *n_features* matches children exactly.
|
|
105
|
+
|
|
106
|
+
- funcs[i](x, ...) := mapper( child0_i(x), child1_i(x), …, θ? )
|
|
107
|
+
- labels are produced by `label_fn` if given, otherwise
|
|
108
|
+
`"{mapper_name}(" + ",".join(child_labels) + ")"`.
|
|
109
|
+
- descs = tuple of children’s desc[i] (one per child) for traceability.
|
|
110
|
+
"""
|
|
111
|
+
# ---- gather children ----
|
|
112
|
+
child_flat = [ch.flatten() for ch in self.children]
|
|
113
|
+
funcs_lists, labels_lists, descs_lists = zip(*child_flat)
|
|
114
|
+
n_feat = len(labels_lists[0]) # already validated in _merge_static
|
|
115
|
+
|
|
116
|
+
# --- per-feature callables -----------------------------------
|
|
117
|
+
def _slice_feature(j):
|
|
118
|
+
# j is captured as default arg to avoid late binding
|
|
119
|
+
return lambda x, *, v=None, mask=None, extras=None, params=None, _j=j: (
|
|
120
|
+
self(x, v=v, mask=mask, extras=extras, params=params)[..., _j]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
funcs = tuple(_slice_feature(j) for j in range(n_feat))
|
|
124
|
+
|
|
125
|
+
# ---- labels ----
|
|
126
|
+
if self.label_fn is not None:
|
|
127
|
+
labels = tuple(self.label_fn(*(lablist[i] for lablist in labels_lists)) for i in range(n_feat))
|
|
128
|
+
else:
|
|
129
|
+
mname = self.mapper.__name__ if getattr(self.mapper, "__name__", "") else "map"
|
|
130
|
+
labels = tuple(f"{mname}(" + ",".join(lab_tuple) + ")" for lab_tuple in zip(*labels_lists))
|
|
131
|
+
|
|
132
|
+
# ---- descriptors ----
|
|
133
|
+
descs = [
|
|
134
|
+
tuple(dl[i] for dl in descs_lists) # one tuple per child
|
|
135
|
+
for i in range(n_feat)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return funcs, labels, descs
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# SFI/statefunc/nodes/ops/reshape_rank.py
|
|
2
|
+
"""Lossless reshape between spatial (rank) axes and the feature axis."""
|
|
3
|
+
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from operator import mul
|
|
6
|
+
|
|
7
|
+
import equinox as eqx
|
|
8
|
+
import jax.numpy as jnp
|
|
9
|
+
|
|
10
|
+
from ..base import BaseNode, BaseOpNode
|
|
11
|
+
from ..contract import Rank, _ContractMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _rank_axis_sizes(node: _ContractMixin) -> tuple[int, ...]:
|
|
15
|
+
"""Return per-axis sizes for rank axes: sdims if present, else (dim,)^rank."""
|
|
16
|
+
if node.sdims is not None:
|
|
17
|
+
return node.sdims
|
|
18
|
+
if node.dim is None:
|
|
19
|
+
raise ValueError("ReshapeRankNode requires known axis sizes (dim or sdims); got dim=None, sdims=None.")
|
|
20
|
+
return (node.dim,) * int(node.rank)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReshapeRankNode(BaseOpNode):
|
|
24
|
+
"""Lossless reshape that moves data between rank (spatial) axes and features.
|
|
25
|
+
|
|
26
|
+
Given a child with output layout::
|
|
27
|
+
|
|
28
|
+
batch · (dim,)^source_rank · n_features
|
|
29
|
+
|
|
30
|
+
this node reshapes to::
|
|
31
|
+
|
|
32
|
+
batch · (dim,)^target_rank · n_features'
|
|
33
|
+
|
|
34
|
+
where ``n_features' = n_features x dim^(source_rank - target_rank)``
|
|
35
|
+
(folding when *target_rank < source_rank*) or
|
|
36
|
+
``n_features' = n_features / dim^(target_rank - source_rank)``
|
|
37
|
+
(unfolding when *target_rank > source_rank*).
|
|
38
|
+
|
|
39
|
+
The reshape uses C-order so that folding followed by unfolding back
|
|
40
|
+
to the original rank is the identity — the pair is invertible.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
child : BaseNode
|
|
45
|
+
Source node (must have known ``dim``).
|
|
46
|
+
target_rank : int or Rank
|
|
47
|
+
Desired output rank.
|
|
48
|
+
|
|
49
|
+
Notes
|
|
50
|
+
-----
|
|
51
|
+
* Folding merges the *innermost* spatial axes into the feature axis.
|
|
52
|
+
* Unfolding splits the feature axis to create new *innermost* spatial axes.
|
|
53
|
+
* The child's ``dim`` must be known (not ``None``).
|
|
54
|
+
* For unfolding, ``n_features`` must be divisible by ``dim^Δrank``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_target_rank: Rank = eqx.field(static=True)
|
|
58
|
+
|
|
59
|
+
def __init__(self, child: BaseNode, *, target_rank: int | Rank):
|
|
60
|
+
object.__setattr__(self, "_target_rank", Rank(int(target_rank)))
|
|
61
|
+
|
|
62
|
+
if child.dim is None and child.sdims is None:
|
|
63
|
+
raise ValueError("ReshapeRankNode requires a child with known dim or sdims; got dim=None, sdims=None.")
|
|
64
|
+
src = int(child.rank)
|
|
65
|
+
tgt = int(self._target_rank)
|
|
66
|
+
|
|
67
|
+
if tgt > src:
|
|
68
|
+
# Unfolding: need to split the feature axis. Determine how many
|
|
69
|
+
# elements need to be carved out. For uniform dim this is dim^(tgt-src).
|
|
70
|
+
# For sdims, the target rank axes beyond the source's sdims are unknown
|
|
71
|
+
# at this point — we require uniform dim for unfolding (sdims=None or
|
|
72
|
+
# all dims equal).
|
|
73
|
+
if child.sdims is not None:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"ReshapeRankNode: unfolding with non-uniform sdims is not "
|
|
76
|
+
"supported. Use explicit reshape or einsum instead."
|
|
77
|
+
)
|
|
78
|
+
assert child.dim is not None # guaranteed: sdims is None and guard on L62 ensures one is non-None
|
|
79
|
+
factor = child.dim ** (tgt - src)
|
|
80
|
+
if child.n_features % factor != 0:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Cannot unfold {child.n_features} features into rank-{tgt} "
|
|
83
|
+
f"with dim={child.dim}: {child.n_features} is not divisible "
|
|
84
|
+
f"by dim^{tgt - src} = {factor}."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
super().__init__(child)
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
def _merge_static(self, children):
|
|
91
|
+
ch = children[0]
|
|
92
|
+
src = int(ch.rank)
|
|
93
|
+
tgt = int(self._target_rank)
|
|
94
|
+
axis_sizes = _rank_axis_sizes(ch)
|
|
95
|
+
|
|
96
|
+
if tgt < src:
|
|
97
|
+
# Folding: innermost (src − tgt) rank axes → features
|
|
98
|
+
factor = reduce(mul, axis_sizes[tgt:], 1)
|
|
99
|
+
n_out = ch.n_features * factor
|
|
100
|
+
out_sdims = axis_sizes[:tgt] if ch.sdims is not None else None
|
|
101
|
+
elif tgt > src:
|
|
102
|
+
# Unfolding: features → new innermost rank axes (uniform dim only)
|
|
103
|
+
dim = ch.dim
|
|
104
|
+
factor = dim ** (tgt - src)
|
|
105
|
+
n_out = ch.n_features // factor
|
|
106
|
+
# Uniform unfolding: new axes are all dim-sized
|
|
107
|
+
out_sdims = axis_sizes + (dim,) * (tgt - src) if ch.sdims is not None else None
|
|
108
|
+
else:
|
|
109
|
+
n_out = ch.n_features
|
|
110
|
+
out_sdims = ch.sdims
|
|
111
|
+
|
|
112
|
+
return _ContractMixin.inherit_contract(ch, rank=self._target_rank, n_features=n_out, sdims=out_sdims)
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
def _op(self, outs, *, params):
|
|
116
|
+
y = outs[0]
|
|
117
|
+
src = int(self.children[0].rank)
|
|
118
|
+
tgt = int(self._target_rank)
|
|
119
|
+
|
|
120
|
+
if tgt == src:
|
|
121
|
+
return y
|
|
122
|
+
|
|
123
|
+
if tgt < src:
|
|
124
|
+
# Fold innermost (src − tgt) spatial axes + feature axis into one.
|
|
125
|
+
n_fold = src - tgt
|
|
126
|
+
new_shape = y.shape[: -(n_fold + 1)] + (-1,)
|
|
127
|
+
return jnp.reshape(y, new_shape)
|
|
128
|
+
else:
|
|
129
|
+
# Unfold feature axis into (tgt − src) new spatial axes + features.
|
|
130
|
+
n_unfold = tgt - src
|
|
131
|
+
dim = self.children[0].dim
|
|
132
|
+
n_feat_out = self.n_features
|
|
133
|
+
new_shape = y.shape[:-1] + (dim,) * n_unfold + (n_feat_out,)
|
|
134
|
+
return jnp.reshape(y, new_shape)
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
def flatten(self):
|
|
138
|
+
child_funcs, child_labels, child_descs = self.children[0].flatten()
|
|
139
|
+
|
|
140
|
+
n_out = self.n_features
|
|
141
|
+
|
|
142
|
+
def _slice_feature(j):
|
|
143
|
+
return lambda x, *, v=None, mask=None, extras=None, params=None, _j=j: (
|
|
144
|
+
self(x, v=v, mask=mask, extras=extras, params=params)[..., _j]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
src = int(self.children[0].rank)
|
|
148
|
+
tgt = int(self._target_rank)
|
|
149
|
+
tag = "fold" if tgt < src else "unfold"
|
|
150
|
+
|
|
151
|
+
funcs = [_slice_feature(j) for j in range(n_out)]
|
|
152
|
+
labels = [f"{tag}_{j}" for j in range(n_out)]
|
|
153
|
+
descs = [{f"{tag}_of": [d for d in child_descs], "index": j} for j in range(n_out)]
|
|
154
|
+
return funcs, labels, descs
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
def _tree_id(self):
|
|
158
|
+
return hash(("ReshapeRankNode", int(self._target_rank), self.children[0]._tree_id()))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# SFI/statefunc/nodes/ops/slice.py
|
|
2
|
+
"""Slice features along the feature axis."""
|
|
3
|
+
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
import equinox as eqx
|
|
7
|
+
|
|
8
|
+
from ..base import BaseNode, BaseOpNode
|
|
9
|
+
from ..contract import _ContractMixin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------
|
|
13
|
+
# Helper node – slice the **feature axis**
|
|
14
|
+
# ---------------------------------------------------------------------
|
|
15
|
+
class SliceFeaturesNode(BaseOpNode):
|
|
16
|
+
"""Select or reorder *feature* entries of a child basis.
|
|
17
|
+
|
|
18
|
+
The node simply indexes the last axis (feature axis) of its child
|
|
19
|
+
while preserving every spatial axis. Supported *idx* forms are
|
|
20
|
+
|
|
21
|
+
* ``int`` - keep a single feature → ``n_features = 1``
|
|
22
|
+
* ``slice`` - standard Python slice
|
|
23
|
+
* ``Sequence[int]`` - fancy indexing/ordering
|
|
24
|
+
* ``Sequence[bool]`` - boolean mask (length must equal ``n_features``)
|
|
25
|
+
|
|
26
|
+
Notes
|
|
27
|
+
-----
|
|
28
|
+
- Rank, dim, pdepth & needs_v are *identical* to the child - only
|
|
29
|
+
``n_features`` changes.
|
|
30
|
+
- No parameters or extras are consumed, so the node is deterministic.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
idx: int | slice | Sequence[int] | Sequence[bool] = eqx.field(static=True)
|
|
34
|
+
|
|
35
|
+
# ---------------- constructor ----------------
|
|
36
|
+
def __init__(self, child: BaseNode, idx):
|
|
37
|
+
object.__setattr__(self, "idx", idx)
|
|
38
|
+
super().__init__(child)
|
|
39
|
+
object.__setattr__(self, "param_suite", child.param_suite)
|
|
40
|
+
|
|
41
|
+
# ------------- static contract merge ----------
|
|
42
|
+
def _merge_static(self, children):
|
|
43
|
+
ch = children[0]
|
|
44
|
+
n_out: int
|
|
45
|
+
if isinstance(self.idx, int):
|
|
46
|
+
n_out = 1
|
|
47
|
+
elif isinstance(self.idx, slice):
|
|
48
|
+
n_out = len(range(*self.idx.indices(ch.n_features)))
|
|
49
|
+
else: # fancy or boolean indexing
|
|
50
|
+
n_out = len(self.idx)
|
|
51
|
+
return _ContractMixin.inherit_contract(ch, n_features=n_out)
|
|
52
|
+
|
|
53
|
+
# ---------------- combine ---------------------
|
|
54
|
+
def _op(self, outs, *, params):
|
|
55
|
+
y = outs[0]
|
|
56
|
+
if isinstance(self.idx, int):
|
|
57
|
+
# Keep the feature axis (size 1), including for negative indices.
|
|
58
|
+
return y[..., [self.idx]]
|
|
59
|
+
return y[..., self.idx]
|
|
60
|
+
|
|
61
|
+
# ---------------- flatten (label-aware) --------
|
|
62
|
+
def flatten(self):
|
|
63
|
+
"""Select the labels/funcs/descriptors matching the sliced features."""
|
|
64
|
+
child_funcs, child_labels, child_descs = self.children[0].flatten()
|
|
65
|
+
|
|
66
|
+
# Resolve idx to a concrete list of integer indices
|
|
67
|
+
n_child = len(child_funcs)
|
|
68
|
+
if isinstance(self.idx, int):
|
|
69
|
+
indices = [self.idx % n_child]
|
|
70
|
+
elif isinstance(self.idx, slice):
|
|
71
|
+
indices = list(range(*self.idx.indices(n_child)))
|
|
72
|
+
else:
|
|
73
|
+
seq = list(self.idx)
|
|
74
|
+
if seq and isinstance(seq[0], bool):
|
|
75
|
+
# boolean mask
|
|
76
|
+
indices = [i for i, b in enumerate(seq) if b]
|
|
77
|
+
else:
|
|
78
|
+
indices = [int(i) % n_child for i in seq]
|
|
79
|
+
|
|
80
|
+
funcs = [child_funcs[i] for i in indices]
|
|
81
|
+
labels = [child_labels[i] for i in indices]
|
|
82
|
+
descs = [child_descs[i] for i in indices]
|
|
83
|
+
return funcs, labels, descs
|