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
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)