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
SFI/bases/linear.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# SFI.bases.linear
|
|
2
|
+
# =================
|
|
3
|
+
# Lightweight linear utilities for building first-order bases in x and v.
|
|
4
|
+
#
|
|
5
|
+
# Contract assumed here:
|
|
6
|
+
# - User functions are called on a SINGLE SAMPLE (no leading batch axes).
|
|
7
|
+
# - For rank=VECTOR leaves, x and v arrive as shape (dim,).
|
|
8
|
+
# - For rank=SCALAR leaves, the function may return () or (k,) where k=n_features.
|
|
9
|
+
# - Feature axis must be present in the final leaf output; for n_features=1 on
|
|
10
|
+
# a scalar leaf, BasisLeaf will auto-insert it from a scalar return.
|
|
11
|
+
#
|
|
12
|
+
# pdepth=0 (non-interacting) everywhere in this helper module.
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Optional, Sequence
|
|
17
|
+
|
|
18
|
+
import jax.numpy as jnp
|
|
19
|
+
|
|
20
|
+
from ..statefunc import Basis, Rank, make_basis
|
|
21
|
+
from .monomials import monomials_degree # degree-specific builder
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"linear_basis",
|
|
25
|
+
"X",
|
|
26
|
+
"V",
|
|
27
|
+
"x_coordinate",
|
|
28
|
+
"x_coordinates",
|
|
29
|
+
"v_coordinate",
|
|
30
|
+
"v_coordinates",
|
|
31
|
+
"x_components",
|
|
32
|
+
"v_components",
|
|
33
|
+
"unit_axes",
|
|
34
|
+
"frame",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def linear_basis(dim: int, *, include_x: bool = True, include_v: bool = False):
|
|
39
|
+
"""
|
|
40
|
+
Degree-1 monomial basis in (x, v).
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
dim : int
|
|
45
|
+
Spatial dimension.
|
|
46
|
+
include_x : bool
|
|
47
|
+
Include linear x terms.
|
|
48
|
+
include_v : bool
|
|
49
|
+
Include linear v terms.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
Basis
|
|
54
|
+
Rank-1 (vector) basis concatenating requested degree-1 monomials.
|
|
55
|
+
"""
|
|
56
|
+
return monomials_degree(1, dim=dim, include_x=include_x, include_v=include_v)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def X(dim: int, *, label: Optional[str] = None) -> Basis:
|
|
60
|
+
"""
|
|
61
|
+
Identity in x with an explicit feature axis.
|
|
62
|
+
|
|
63
|
+
Input : x ∈ R^dim
|
|
64
|
+
Output: Y ∈ R^{dim×1}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def _eval(x):
|
|
68
|
+
return x[:, None] # (dim, 1)
|
|
69
|
+
|
|
70
|
+
label = "x" if label is None else label
|
|
71
|
+
return make_basis(
|
|
72
|
+
func=_eval,
|
|
73
|
+
dim=dim,
|
|
74
|
+
rank=Rank.VECTOR,
|
|
75
|
+
n_features=1,
|
|
76
|
+
needs_v=False,
|
|
77
|
+
labels=[label],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def V(dim: int, *, label: Optional[str] = None) -> Basis:
|
|
82
|
+
"""
|
|
83
|
+
Identity in v with an explicit feature axis.
|
|
84
|
+
|
|
85
|
+
Input : v ∈ R^dim (provided via keyword v=...)
|
|
86
|
+
Output: Y ∈ R^{dim×1}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def _eval(x, v):
|
|
90
|
+
return v[:, None] # (dim, 1)
|
|
91
|
+
|
|
92
|
+
label = "v" if label is None else label
|
|
93
|
+
return make_basis(
|
|
94
|
+
func=_eval,
|
|
95
|
+
dim=dim,
|
|
96
|
+
rank=Rank.VECTOR,
|
|
97
|
+
n_features=1,
|
|
98
|
+
needs_v=True,
|
|
99
|
+
labels=[label],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def x_coordinate(index: int, *, dim: int, label: Optional[str] = None) -> Basis:
|
|
104
|
+
"""
|
|
105
|
+
Single x-coordinate as a scalar feature.
|
|
106
|
+
|
|
107
|
+
Input : x ∈ R^dim
|
|
108
|
+
Return: scalar (); BasisLeaf will auto-insert feature axis → (1,)
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def _eval(x):
|
|
112
|
+
return x[index] # ()
|
|
113
|
+
|
|
114
|
+
label = f"x{index}" if label is None else label
|
|
115
|
+
return make_basis(
|
|
116
|
+
func=_eval,
|
|
117
|
+
dim=dim,
|
|
118
|
+
rank=Rank.SCALAR,
|
|
119
|
+
n_features=1,
|
|
120
|
+
needs_v=False,
|
|
121
|
+
labels=[label],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def field_component(index: int, *, n_fields: int, label: Optional[str] = None) -> Basis:
|
|
126
|
+
"""Extract a single field component from an SPDE state vector.
|
|
127
|
+
|
|
128
|
+
Alias for :func:`x_coordinate` with SPDE-oriented naming.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
index : int
|
|
133
|
+
Zero-based index of the field component to extract.
|
|
134
|
+
n_fields : int
|
|
135
|
+
Total number of field components per grid site (= ``dim``).
|
|
136
|
+
label : str, optional
|
|
137
|
+
Human-readable label; defaults to ``"field[{index}]"``.
|
|
138
|
+
"""
|
|
139
|
+
if label is None:
|
|
140
|
+
label = f"field[{index}]"
|
|
141
|
+
return x_coordinate(index, dim=n_fields, label=label)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def x_coordinates(indices: Sequence[int], *, dim: int, labels: Optional[Sequence[str]] = None) -> Basis:
|
|
145
|
+
"""
|
|
146
|
+
Multiple x-coordinates as scalar features.
|
|
147
|
+
|
|
148
|
+
Input : x ∈ R^dim
|
|
149
|
+
Output: y ∈ R^{k} with k=len(indices)
|
|
150
|
+
"""
|
|
151
|
+
indices = jnp.array(indices)
|
|
152
|
+
|
|
153
|
+
def _eval(x):
|
|
154
|
+
return x[indices] # (k,)
|
|
155
|
+
|
|
156
|
+
if labels is None:
|
|
157
|
+
labels = [f"x{i}" for i in indices]
|
|
158
|
+
elif len(labels) != len(indices):
|
|
159
|
+
raise ValueError("x_coordinates: labels length must match number of indices")
|
|
160
|
+
|
|
161
|
+
return make_basis(
|
|
162
|
+
func=_eval,
|
|
163
|
+
dim=dim,
|
|
164
|
+
rank=Rank.SCALAR,
|
|
165
|
+
n_features=len(indices),
|
|
166
|
+
needs_v=False,
|
|
167
|
+
labels=list(labels),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def v_coordinate(index: int, *, dim: int, label: Optional[str] = None) -> Basis:
|
|
172
|
+
"""
|
|
173
|
+
Single v-coordinate as a scalar feature.
|
|
174
|
+
|
|
175
|
+
Input : v ∈ R^dim (provided via keyword v=...)
|
|
176
|
+
Return: scalar (); BasisLeaf will auto-insert feature axis → (1,)
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def _eval(x, v):
|
|
180
|
+
return v[index] # ()
|
|
181
|
+
|
|
182
|
+
label = f"v{index}" if label is None else label
|
|
183
|
+
return make_basis(
|
|
184
|
+
func=_eval,
|
|
185
|
+
dim=dim,
|
|
186
|
+
rank=Rank.SCALAR,
|
|
187
|
+
n_features=1,
|
|
188
|
+
needs_v=True,
|
|
189
|
+
labels=[label],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def v_coordinates(indices: Sequence[int], *, dim: int, labels: Optional[Sequence[str]] = None) -> Basis:
|
|
194
|
+
"""
|
|
195
|
+
Multiple v-coordinates as scalar features.
|
|
196
|
+
|
|
197
|
+
Input : v ∈ R^dim (provided via keyword v=...)
|
|
198
|
+
Output: y ∈ R^{k} with k=len(indices)
|
|
199
|
+
"""
|
|
200
|
+
indices = jnp.array(indices)
|
|
201
|
+
|
|
202
|
+
def _eval(x, v):
|
|
203
|
+
return v[indices] # (k,)
|
|
204
|
+
|
|
205
|
+
if labels is None:
|
|
206
|
+
labels = [f"v{i}" for i in indices]
|
|
207
|
+
elif len(labels) != len(indices):
|
|
208
|
+
raise ValueError("v_coordinates: labels length must match number of indices")
|
|
209
|
+
|
|
210
|
+
return make_basis(
|
|
211
|
+
func=_eval,
|
|
212
|
+
dim=dim,
|
|
213
|
+
rank=Rank.SCALAR,
|
|
214
|
+
n_features=len(indices),
|
|
215
|
+
needs_v=True,
|
|
216
|
+
labels=list(labels),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Component / axis unpackers
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
_DEFAULT_X_LABELS = ("x", "y", "z", "w")
|
|
224
|
+
_DEFAULT_V_LABELS = ("vx", "vy", "vz", "vw")
|
|
225
|
+
_DEFAULT_E_LABELS = ("ex", "ey", "ez", "ew")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _auto_labels(dim: int, defaults: Sequence[str], prefix: str) -> list[str]:
|
|
229
|
+
if dim <= len(defaults):
|
|
230
|
+
return [defaults[i] for i in range(dim)]
|
|
231
|
+
return [f"{prefix}{i}" for i in range(dim)]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def x_components(dim: int, *, labels: Optional[Sequence[str]] = None) -> tuple[Basis, ...]:
|
|
235
|
+
"""Unpack scalar x-coordinate bases, one per axis.
|
|
236
|
+
|
|
237
|
+
>>> x, y, z = x_components(3)
|
|
238
|
+
|
|
239
|
+
Each returned basis is rank-0 with one feature. Labels default to
|
|
240
|
+
``("x", "y", "z", "w")`` for ``dim <= 4`` and ``("x0", "x1", ...)`` otherwise.
|
|
241
|
+
"""
|
|
242
|
+
if labels is None:
|
|
243
|
+
labels = _auto_labels(dim, _DEFAULT_X_LABELS, "x")
|
|
244
|
+
elif len(labels) != dim:
|
|
245
|
+
raise ValueError(f"x_components: labels length ({len(labels)}) must equal dim ({dim})")
|
|
246
|
+
return tuple(x_coordinate(i, dim=dim, label=labels[i]) for i in range(dim))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def v_components(dim: int, *, labels: Optional[Sequence[str]] = None) -> tuple[Basis, ...]:
|
|
250
|
+
"""Unpack scalar v-coordinate bases, one per axis.
|
|
251
|
+
|
|
252
|
+
>>> vx, vy, vz = v_components(3)
|
|
253
|
+
"""
|
|
254
|
+
if labels is None:
|
|
255
|
+
labels = _auto_labels(dim, _DEFAULT_V_LABELS, "v")
|
|
256
|
+
elif len(labels) != dim:
|
|
257
|
+
raise ValueError(f"v_components: labels length ({len(labels)}) must equal dim ({dim})")
|
|
258
|
+
return tuple(v_coordinate(i, dim=dim, label=labels[i]) for i in range(dim))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def unit_axes(dim: int, *, labels: Optional[Sequence[str]] = None) -> tuple[Basis, ...]:
|
|
262
|
+
"""Unpack unit-vector bases (one per spatial axis).
|
|
263
|
+
|
|
264
|
+
>>> ex, ey, ez = unit_axes(3)
|
|
265
|
+
|
|
266
|
+
Each returned basis is rank-1 with a single feature carrying the unit
|
|
267
|
+
vector along that axis. Labels default to ``("ex", "ey", "ez", "ew")``
|
|
268
|
+
for ``dim <= 4`` and ``("e0", "e1", ...)`` otherwise.
|
|
269
|
+
"""
|
|
270
|
+
# local import to avoid a circular import at module load
|
|
271
|
+
from .constants import unit_vector_basis
|
|
272
|
+
|
|
273
|
+
if labels is None:
|
|
274
|
+
labels = _auto_labels(dim, _DEFAULT_E_LABELS, "e")
|
|
275
|
+
elif len(labels) != dim:
|
|
276
|
+
raise ValueError(f"unit_axes: labels length ({len(labels)}) must equal dim ({dim})")
|
|
277
|
+
|
|
278
|
+
out = []
|
|
279
|
+
for i in range(dim):
|
|
280
|
+
e = unit_vector_basis(dim, axes=[i])
|
|
281
|
+
# Override the leaf's label with the friendly default. Bypass the
|
|
282
|
+
# Equinox freeze to patch labels after construction; fragile if
|
|
283
|
+
# Equinox changes its freeze mechanism — prefer constructing with
|
|
284
|
+
# labels set.
|
|
285
|
+
leaf = e.root
|
|
286
|
+
object.__setattr__(leaf, "labels", (labels[i],))
|
|
287
|
+
out.append(e)
|
|
288
|
+
return tuple(out)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def frame(
|
|
292
|
+
dim: int,
|
|
293
|
+
*,
|
|
294
|
+
velocity: bool = False,
|
|
295
|
+
x_labels: Optional[Sequence[str]] = None,
|
|
296
|
+
v_labels: Optional[Sequence[str]] = None,
|
|
297
|
+
e_labels: Optional[Sequence[str]] = None,
|
|
298
|
+
) -> tuple[Basis, ...]:
|
|
299
|
+
"""Default compositional frame: constant ``1`` + coordinate scalars + unit axes.
|
|
300
|
+
|
|
301
|
+
Overdamped (``velocity=False``)::
|
|
302
|
+
|
|
303
|
+
one, *x_components(dim), *unit_axes(dim)
|
|
304
|
+
|
|
305
|
+
Underdamped (``velocity=True``)::
|
|
306
|
+
|
|
307
|
+
one, *x_components(dim), *v_components(dim), *unit_axes(dim)
|
|
308
|
+
|
|
309
|
+
Examples
|
|
310
|
+
--------
|
|
311
|
+
>>> one, x, y, z, ex, ey, ez = frame(3)
|
|
312
|
+
>>> one, x, y, z, vx, vy, vz, ex, ey, ez = frame(3, velocity=True)
|
|
313
|
+
|
|
314
|
+
Custom labels (useful for ``dim > 4``):
|
|
315
|
+
|
|
316
|
+
>>> bundle = frame(5, x_labels=["q0","q1","q2","q3","q4"])
|
|
317
|
+
"""
|
|
318
|
+
from .constants import ones_basis
|
|
319
|
+
|
|
320
|
+
pieces: list[Basis] = [ones_basis(dim)]
|
|
321
|
+
pieces.extend(x_components(dim, labels=x_labels))
|
|
322
|
+
if velocity:
|
|
323
|
+
pieces.extend(v_components(dim, labels=v_labels))
|
|
324
|
+
pieces.extend(unit_axes(dim, labels=e_labels))
|
|
325
|
+
return tuple(pieces)
|
SFI/bases/monomials.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import itertools
|
|
3
|
+
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from ..statefunc import Basis, Rank, make_basis
|
|
7
|
+
|
|
8
|
+
_RANK_ALIASES = {
|
|
9
|
+
"scalar": None,
|
|
10
|
+
"vector": "vector",
|
|
11
|
+
"matrix": "symmetric",
|
|
12
|
+
"symmetric_matrix": "symmetric",
|
|
13
|
+
"identity_matrix": "identity",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _lift(B: Basis, rank: str, dim: int):
|
|
18
|
+
"""Lift a scalar Basis to higher rank via Cartesian product with a structural basis."""
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
from .constants import identity_matrix_basis, symmetric_matrix_basis, unit_vector_basis
|
|
22
|
+
|
|
23
|
+
if rank not in _RANK_ALIASES:
|
|
24
|
+
raise ValueError(f"Unknown rank={rank!r}. Choose from: {list(_RANK_ALIASES.keys())}")
|
|
25
|
+
mode = _RANK_ALIASES[rank]
|
|
26
|
+
if mode is None:
|
|
27
|
+
return B
|
|
28
|
+
# Suppress the Cartesian-product warning — this is intentional lifting.
|
|
29
|
+
with warnings.catch_warnings():
|
|
30
|
+
warnings.filterwarnings("ignore", ".*Cartesian product.*")
|
|
31
|
+
if mode == "vector":
|
|
32
|
+
return B * unit_vector_basis(dim)
|
|
33
|
+
if mode == "identity":
|
|
34
|
+
return B * identity_matrix_basis(dim)
|
|
35
|
+
if mode == "symmetric":
|
|
36
|
+
return B * symmetric_matrix_basis(dim)
|
|
37
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@functools.lru_cache(maxsize=64)
|
|
41
|
+
def _exponent_mats(dim: int, degree: int, include_x: bool, include_v: bool):
|
|
42
|
+
"""Return (ex_x, ex_v, labels) for all monomials of exact total degree."""
|
|
43
|
+
if not include_x and not include_v:
|
|
44
|
+
raise ValueError("At least one of include_x / include_v must be True.")
|
|
45
|
+
L = (dim if include_x else 0) + (dim if include_v else 0)
|
|
46
|
+
|
|
47
|
+
# stars-and-bars enumeration for total degree == degree
|
|
48
|
+
if degree == 0:
|
|
49
|
+
gammas = [tuple(0 for _ in range(L))]
|
|
50
|
+
else:
|
|
51
|
+
gammas = []
|
|
52
|
+
for stars in itertools.combinations_with_replacement(range(L), degree):
|
|
53
|
+
arr = [0] * L
|
|
54
|
+
for s in stars:
|
|
55
|
+
arr[s] += 1
|
|
56
|
+
gammas.append(tuple(arr))
|
|
57
|
+
|
|
58
|
+
ex_x, ex_v, labels = [], [], []
|
|
59
|
+
for g in gammas:
|
|
60
|
+
alpha = g[:dim] if include_x else (0,) * dim
|
|
61
|
+
beta = g[-dim:] if include_v else (0,) * dim
|
|
62
|
+
|
|
63
|
+
parts = []
|
|
64
|
+
for k, p in enumerate(alpha):
|
|
65
|
+
if p:
|
|
66
|
+
parts.append(f"x{k}^{p}" if p > 1 else f"x{k}")
|
|
67
|
+
for k, p in enumerate(beta):
|
|
68
|
+
if p:
|
|
69
|
+
parts.append(f"v{k}^{p}" if p > 1 else f"v{k}")
|
|
70
|
+
lab = "1" if not parts else "·".join(parts)
|
|
71
|
+
|
|
72
|
+
ex_x.append(alpha)
|
|
73
|
+
ex_v.append(beta)
|
|
74
|
+
labels.append(lab)
|
|
75
|
+
|
|
76
|
+
ex_x = jnp.array(ex_x, dtype=jnp.int32) # (F, dim)
|
|
77
|
+
ex_v = jnp.array(ex_v, dtype=jnp.int32) # (F, dim)
|
|
78
|
+
return ex_x, ex_v, tuple(labels)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def monomials_degree(
|
|
82
|
+
degree: int,
|
|
83
|
+
*,
|
|
84
|
+
dim: int,
|
|
85
|
+
include_x: bool = True,
|
|
86
|
+
include_v: bool = False,
|
|
87
|
+
rank: str = "scalar",
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
All monomials of **exact** total degree in x and/or v.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
degree : int
|
|
95
|
+
Exact total polynomial degree.
|
|
96
|
+
dim : int
|
|
97
|
+
Spatial dimension.
|
|
98
|
+
include_x, include_v : bool
|
|
99
|
+
Which variables to include.
|
|
100
|
+
rank : str
|
|
101
|
+
Output rank. ``'scalar'`` (default) returns a scalar Basis with
|
|
102
|
+
``F`` features. ``'vector'`` lifts to rank-1 via Cartesian product
|
|
103
|
+
with ``unit_vector_basis(dim)`` (F × dim features).
|
|
104
|
+
``'matrix'`` / ``'symmetric_matrix'`` lifts to rank-2 via
|
|
105
|
+
``symmetric_matrix_basis(dim)`` (F × dim(dim+1)/2 features).
|
|
106
|
+
``'identity_matrix'`` lifts via ``identity_matrix_basis(dim)``
|
|
107
|
+
(F × 1 features, isotropic).
|
|
108
|
+
"""
|
|
109
|
+
ex_x, ex_v, labels = _exponent_mats(dim, degree, include_x, include_v)
|
|
110
|
+
F = int(ex_x.shape[0])
|
|
111
|
+
use_x, use_v = bool(include_x), bool(include_v)
|
|
112
|
+
|
|
113
|
+
# Precompute static stuff outside eval (captured in the closure)
|
|
114
|
+
Dx = int(ex_x.max()) if use_x else 0 # max degree for x
|
|
115
|
+
Dv = int(ex_v.max()) if use_v else 0 # max degree for v
|
|
116
|
+
Ix = ex_x.T if use_x else None # (dim, F) gather indices
|
|
117
|
+
Iv = ex_v.T if use_v else None # (dim, F)
|
|
118
|
+
|
|
119
|
+
def _power_table_fixed(z, D):
|
|
120
|
+
"""
|
|
121
|
+
z: (dim,) -> (dim, D+1) = [1, z, z^2, ..., z^D]
|
|
122
|
+
|
|
123
|
+
Important: avoid jnp.power with float exponents because its JVP/VJP uses log(z)
|
|
124
|
+
and yields NaNs at z=0 even for integer powers (e.g. 0^0 pathways).
|
|
125
|
+
This iterative construction is polynomial and differentiable everywhere.
|
|
126
|
+
"""
|
|
127
|
+
dim = z.shape[0]
|
|
128
|
+
one = jnp.ones((dim, 1), dtype=z.dtype)
|
|
129
|
+
if D == 0:
|
|
130
|
+
return one
|
|
131
|
+
# powers[:, k] = z^k
|
|
132
|
+
powers = [one, z[:, None]]
|
|
133
|
+
for _ in range(2, D + 1):
|
|
134
|
+
powers.append(powers[-1] * z[:, None])
|
|
135
|
+
return jnp.concatenate(powers, axis=1)
|
|
136
|
+
|
|
137
|
+
if use_x and use_v:
|
|
138
|
+
|
|
139
|
+
def _eval(x, *, v=None, mask=None):
|
|
140
|
+
# x,v: (dim,) ; returns (F,)
|
|
141
|
+
if v is None:
|
|
142
|
+
v = jnp.zeros_like(x)
|
|
143
|
+
Tx = _power_table_fixed(x, Dx) # (dim, Dx+1)
|
|
144
|
+
Tv = _power_table_fixed(v, Dv) # (dim, Dv+1)
|
|
145
|
+
vx = jnp.take_along_axis(Tx, Ix, axis=1) # (dim, F)
|
|
146
|
+
vv = jnp.take_along_axis(Tv, Iv, axis=1) # (dim, F)
|
|
147
|
+
return jnp.prod(vx * vv, axis=0) # (F,)
|
|
148
|
+
elif use_x:
|
|
149
|
+
|
|
150
|
+
def _eval(x, *, v=None, mask=None):
|
|
151
|
+
Tx = _power_table_fixed(x, Dx) # (dim, Dx+1)
|
|
152
|
+
vx = jnp.take_along_axis(Tx, Ix, axis=1) # (dim, F)
|
|
153
|
+
return jnp.prod(vx, axis=0) # (F,)
|
|
154
|
+
else: # use_v only
|
|
155
|
+
|
|
156
|
+
def _eval(x, *, v=None, mask=None):
|
|
157
|
+
Tv = _power_table_fixed(v if v is not None else jnp.zeros_like(x), Dv)
|
|
158
|
+
vv = jnp.take_along_axis(Tv, Iv, axis=1) # (dim, F)
|
|
159
|
+
return jnp.prod(vv, axis=0) # (F,)
|
|
160
|
+
|
|
161
|
+
B = make_basis(
|
|
162
|
+
func=_eval,
|
|
163
|
+
dim=dim,
|
|
164
|
+
rank=Rank.SCALAR,
|
|
165
|
+
n_features=F,
|
|
166
|
+
needs_v=use_v,
|
|
167
|
+
labels=list(labels),
|
|
168
|
+
)
|
|
169
|
+
return _lift(B, rank, dim)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def monomials_up_to(
|
|
173
|
+
order: int,
|
|
174
|
+
*,
|
|
175
|
+
dim: int,
|
|
176
|
+
include_constant: bool = True,
|
|
177
|
+
include_x: bool = True,
|
|
178
|
+
include_v: bool = False,
|
|
179
|
+
rank: str = "scalar",
|
|
180
|
+
):
|
|
181
|
+
r"""
|
|
182
|
+
Concatenate degree-wise monomial bases for degrees 0..order (ascending).
|
|
183
|
+
|
|
184
|
+
.. physics:: Multivariate polynomial basis
|
|
185
|
+
:label: polynomial-basis
|
|
186
|
+
:category: Basis functions
|
|
187
|
+
|
|
188
|
+
.. math::
|
|
189
|
+
|
|
190
|
+
f_\\alpha(x) = \\prod_{k=1}^{d} x_k^{\\alpha_k},
|
|
191
|
+
\\qquad |\\alpha| \\le \\texttt{order}
|
|
192
|
+
|
|
193
|
+
Full polynomial dictionary up to a given total degree, optionally
|
|
194
|
+
including velocity monomials and lifted to vector or matrix rank.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
order : int
|
|
199
|
+
Maximum total polynomial degree.
|
|
200
|
+
dim : int
|
|
201
|
+
Spatial dimension.
|
|
202
|
+
include_constant : bool
|
|
203
|
+
If False, skip degree-0 (constant) term.
|
|
204
|
+
include_x, include_v : bool
|
|
205
|
+
Which variables to include.
|
|
206
|
+
rank : str
|
|
207
|
+
Output tensor rank. See :func:`monomials_degree` for allowed
|
|
208
|
+
values: ``'scalar'``, ``'vector'``, ``'matrix'`` /
|
|
209
|
+
``'symmetric_matrix'``, ``'identity_matrix'``.
|
|
210
|
+
|
|
211
|
+
For force inference, use ``rank='vector'`` (the most common choice).
|
|
212
|
+
"""
|
|
213
|
+
start = 0 if include_constant else 1
|
|
214
|
+
if start > order:
|
|
215
|
+
raise ValueError("No monomials to include: set include_constant=True or order>=1.")
|
|
216
|
+
Bs = [monomials_degree(d, dim=dim, include_x=include_x, include_v=include_v) for d in range(start, order + 1)]
|
|
217
|
+
B = Basis.stack(Bs)
|
|
218
|
+
return _lift(B, rank, dim)
|