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,878 @@
|
|
|
1
|
+
"""GridLayout — structured-dimensions layout for SPDE systems.
|
|
2
|
+
|
|
3
|
+
Provides :class:`GridLayout`, a layout for regular-grid SPDE systems that
|
|
4
|
+
carries ``ndim`` (spatial dimension) and stencil parameters (``bc``, ``order``).
|
|
5
|
+
Differential operators are exposed as methods (``grad``, ``lap``, ``div``,
|
|
6
|
+
``biharmonic``, etc.). The :meth:`~GridLayout.embed` method compiles
|
|
7
|
+
inner-world :class:`~SFI.statefunc.structexpr.StructuredExpr` trees into
|
|
8
|
+
outer-world :class:`~SFI.statefunc.stateexpr.StateExpr` objects.
|
|
9
|
+
|
|
10
|
+
Generic stencil composition
|
|
11
|
+
---------------------------
|
|
12
|
+
Any expression tree — including nested differential operators like
|
|
13
|
+
``layout.div(Q * layout.grad(phi))`` — compiles to a *single* fat-stencil
|
|
14
|
+
gather + locally-unrolled evaluation function. The stencil footprint is
|
|
15
|
+
computed via Minkowski sums of per-operator template offsets (see
|
|
16
|
+
:mod:`._fd_atoms`), and the evaluation function is recursively compiled
|
|
17
|
+
with trace-time offset resolution (see :mod:`._eval_compiler`).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Callable, Literal
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
|
|
28
|
+
from ..nodes.contract import Rank
|
|
29
|
+
from ..structexpr import (
|
|
30
|
+
StructuredExpr,
|
|
31
|
+
_BinaryOp,
|
|
32
|
+
_ConcatOp,
|
|
33
|
+
_ConstNode,
|
|
34
|
+
_DiffOpNode,
|
|
35
|
+
_EinsumOp,
|
|
36
|
+
_SectorLeaf,
|
|
37
|
+
_SliceOp,
|
|
38
|
+
_StackOp,
|
|
39
|
+
_StructNode,
|
|
40
|
+
_UnaryOp,
|
|
41
|
+
)
|
|
42
|
+
from ._base import _BaseLayout
|
|
43
|
+
from ._sectors import (
|
|
44
|
+
ScalarSector,
|
|
45
|
+
Sector,
|
|
46
|
+
SymTensorSector,
|
|
47
|
+
TensorSector,
|
|
48
|
+
VectorSector,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# =====================================================================
|
|
52
|
+
# StencilMeta — opaque metadata attached to _DiffOpNode
|
|
53
|
+
# =====================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class StencilMeta:
|
|
58
|
+
"""Metadata for a finite-difference differential operator node.
|
|
59
|
+
|
|
60
|
+
Stored on ``_DiffOpNode.engine_meta`` and consumed by the embed
|
|
61
|
+
compiler to wire stencil dispatch.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
op_name: str
|
|
65
|
+
ndim: int
|
|
66
|
+
bc: str
|
|
67
|
+
order: str
|
|
68
|
+
key_grid_shape: str
|
|
69
|
+
key_dx: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =====================================================================
|
|
73
|
+
# Voigt helpers
|
|
74
|
+
# =====================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _voigt_expand(raw: jax.Array, sector: SymTensorSector) -> jax.Array:
|
|
78
|
+
"""Expand Voigt-packed ``(n_data,)`` to full ``(sdim, sdim)`` tensor.
|
|
79
|
+
|
|
80
|
+
For traceless sectors, the last diagonal is reconstructed as
|
|
81
|
+
``-sum(other diagonals)``.
|
|
82
|
+
"""
|
|
83
|
+
sdim = sector.sdim
|
|
84
|
+
pairs = sector.voigt_pairs
|
|
85
|
+
|
|
86
|
+
# Build full symmetric matrix
|
|
87
|
+
mat = jnp.zeros((sdim, sdim), dtype=raw.dtype)
|
|
88
|
+
for k, (i, j) in enumerate(pairs):
|
|
89
|
+
mat = mat.at[i, j].set(raw[k])
|
|
90
|
+
if i != j:
|
|
91
|
+
mat = mat.at[j, i].set(raw[k])
|
|
92
|
+
|
|
93
|
+
if sector.traceless:
|
|
94
|
+
# Reconstruct last diagonal: -(sum of other diags)
|
|
95
|
+
diag_sum = jnp.zeros((), dtype=raw.dtype)
|
|
96
|
+
for d in range(sdim - 1):
|
|
97
|
+
diag_sum = diag_sum + mat[d, d]
|
|
98
|
+
mat = mat.at[sdim - 1, sdim - 1].set(-diag_sum)
|
|
99
|
+
|
|
100
|
+
return mat
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _voigt_pack(mat: jax.Array, sector: SymTensorSector) -> jax.Array:
|
|
104
|
+
"""Extract independent Voigt components from ``(sdim, sdim)`` tensor."""
|
|
105
|
+
return jnp.array([mat[i, j] for i, j in sector.voigt_pairs])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _sector_pack(val: jax.Array, sector: Sector) -> jax.Array:
|
|
109
|
+
"""Pack a structured value to flat ``(n_data,)`` for a sector.
|
|
110
|
+
|
|
111
|
+
Handles Voigt packing for SymTensorSector, flat reshape for others.
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(sector, SymTensorSector):
|
|
114
|
+
return _voigt_pack(val, sector)
|
|
115
|
+
elif isinstance(sector, ScalarSector):
|
|
116
|
+
return val.reshape(1)
|
|
117
|
+
else:
|
|
118
|
+
# VectorSector, TensorSector: flat reshape
|
|
119
|
+
return val.reshape(-1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _sector_expand(raw: jax.Array, sector: Sector) -> jax.Array:
|
|
123
|
+
"""Expand flat ``(n_data,)`` to structured ``(*sdims)``."""
|
|
124
|
+
if isinstance(sector, SymTensorSector):
|
|
125
|
+
return _voigt_expand(raw, sector)
|
|
126
|
+
elif isinstance(sector, ScalarSector):
|
|
127
|
+
return raw[0]
|
|
128
|
+
elif isinstance(sector, VectorSector):
|
|
129
|
+
return raw
|
|
130
|
+
elif isinstance(sector, TensorSector):
|
|
131
|
+
return raw.reshape(sector.sdims)
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError(f"Unknown sector type: {type(sector)}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# =====================================================================
|
|
137
|
+
# Pointwise tree compiler
|
|
138
|
+
# =====================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _compile_pointwise_fn(
|
|
142
|
+
node: _StructNode,
|
|
143
|
+
sectors: dict[str, Sector],
|
|
144
|
+
) -> Callable:
|
|
145
|
+
"""Compile a pointwise _StructNode tree to ``fn(x) -> (*sdims) array``.
|
|
146
|
+
|
|
147
|
+
The resulting function takes a single state vector ``x: (dim,)`` and
|
|
148
|
+
returns the structured value with shape ``(*sdims)``.
|
|
149
|
+
|
|
150
|
+
Only used for *pure-pointwise* paths (no ``_DiffOpNode`` in tree).
|
|
151
|
+
Trees with stencil nodes go through :func:`._eval_compiler.compile_eval`.
|
|
152
|
+
"""
|
|
153
|
+
if isinstance(node, _SectorLeaf):
|
|
154
|
+
indices = jnp.array(node.indices)
|
|
155
|
+
sector = sectors[node.sector_name]
|
|
156
|
+
if isinstance(sector, SymTensorSector):
|
|
157
|
+
|
|
158
|
+
def _fn(x):
|
|
159
|
+
return _voigt_expand(x[indices], sector)
|
|
160
|
+
|
|
161
|
+
return _fn
|
|
162
|
+
elif isinstance(sector, ScalarSector):
|
|
163
|
+
idx0 = node.indices[0]
|
|
164
|
+
|
|
165
|
+
def _fn(x):
|
|
166
|
+
return x[idx0]
|
|
167
|
+
|
|
168
|
+
return _fn
|
|
169
|
+
else:
|
|
170
|
+
# VectorSector or TensorSector
|
|
171
|
+
def _fn(x):
|
|
172
|
+
raw = x[indices]
|
|
173
|
+
if node.sdims:
|
|
174
|
+
return raw.reshape(node.sdims)
|
|
175
|
+
return raw
|
|
176
|
+
|
|
177
|
+
return _fn
|
|
178
|
+
|
|
179
|
+
if isinstance(node, _ConstNode):
|
|
180
|
+
val = node.value
|
|
181
|
+
if isinstance(val, (int, float)):
|
|
182
|
+
arr = jnp.array(val, dtype=jnp.float32)
|
|
183
|
+
|
|
184
|
+
def _fn(x):
|
|
185
|
+
return arr.astype(x.dtype)
|
|
186
|
+
|
|
187
|
+
return _fn
|
|
188
|
+
else:
|
|
189
|
+
arr = jnp.asarray(val)
|
|
190
|
+
|
|
191
|
+
def _fn(x):
|
|
192
|
+
return arr
|
|
193
|
+
|
|
194
|
+
return _fn
|
|
195
|
+
|
|
196
|
+
if isinstance(node, _BinaryOp):
|
|
197
|
+
left_fn = _compile_pointwise_fn(node.left, sectors)
|
|
198
|
+
right_fn = _compile_pointwise_fn(node.right, sectors)
|
|
199
|
+
op_map = {
|
|
200
|
+
"+": jnp.add,
|
|
201
|
+
"-": jnp.subtract,
|
|
202
|
+
"*": jnp.multiply,
|
|
203
|
+
"/": jnp.true_divide,
|
|
204
|
+
"**": jnp.power,
|
|
205
|
+
}
|
|
206
|
+
op_fn = op_map[node.op]
|
|
207
|
+
|
|
208
|
+
def _fn(x):
|
|
209
|
+
return op_fn(left_fn(x), right_fn(x))
|
|
210
|
+
|
|
211
|
+
return _fn
|
|
212
|
+
|
|
213
|
+
if isinstance(node, _EinsumOp):
|
|
214
|
+
child_fns = [_compile_pointwise_fn(c, sectors) for c in node.children]
|
|
215
|
+
spec = node.spec
|
|
216
|
+
|
|
217
|
+
def _fn(x):
|
|
218
|
+
operands = [cfn(x) for cfn in child_fns]
|
|
219
|
+
return jnp.einsum(spec, *operands)
|
|
220
|
+
|
|
221
|
+
return _fn
|
|
222
|
+
|
|
223
|
+
if isinstance(node, _UnaryOp):
|
|
224
|
+
child_fn = _compile_pointwise_fn(node.child, sectors)
|
|
225
|
+
if node.op == "neg":
|
|
226
|
+
|
|
227
|
+
def _fn(x):
|
|
228
|
+
return -child_fn(x)
|
|
229
|
+
|
|
230
|
+
return _fn
|
|
231
|
+
elif node.op == "T":
|
|
232
|
+
|
|
233
|
+
def _fn(x):
|
|
234
|
+
v = child_fn(x)
|
|
235
|
+
if v.ndim >= 2:
|
|
236
|
+
return jnp.swapaxes(v, -2, -1)
|
|
237
|
+
return v
|
|
238
|
+
|
|
239
|
+
return _fn
|
|
240
|
+
elif node.op == "ew":
|
|
241
|
+
ew = node.fn
|
|
242
|
+
assert ew is not None
|
|
243
|
+
|
|
244
|
+
def _fn(x):
|
|
245
|
+
return ew(child_fn(x))
|
|
246
|
+
|
|
247
|
+
return _fn
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(f"Unknown unary op: {node.op!r}")
|
|
250
|
+
|
|
251
|
+
if isinstance(node, _StackOp):
|
|
252
|
+
child_fns = [_compile_pointwise_fn(c, sectors) for c in node.children]
|
|
253
|
+
|
|
254
|
+
def _fn(x):
|
|
255
|
+
return jnp.stack([cfn(x) for cfn in child_fns], axis=0)
|
|
256
|
+
|
|
257
|
+
return _fn
|
|
258
|
+
|
|
259
|
+
if isinstance(node, _SliceOp):
|
|
260
|
+
child_fn = _compile_pointwise_fn(node.child, sectors)
|
|
261
|
+
idx = node.idx
|
|
262
|
+
|
|
263
|
+
def _fn(x):
|
|
264
|
+
v = child_fn(x)
|
|
265
|
+
return v[..., idx]
|
|
266
|
+
|
|
267
|
+
return _fn
|
|
268
|
+
|
|
269
|
+
raise NotImplementedError(f"Cannot compile {type(node).__name__} as pointwise.")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# =====================================================================
|
|
273
|
+
# GridLayout
|
|
274
|
+
# =====================================================================
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class GridLayout(_BaseLayout):
|
|
278
|
+
"""Layout for SPDE systems on regular Cartesian grids.
|
|
279
|
+
|
|
280
|
+
Provides differential operator methods and an ``embed()`` compiler
|
|
281
|
+
that turns inner-world :class:`~SFI.statefunc.structexpr.StructuredExpr`
|
|
282
|
+
trees into outer-world ``Basis`` / ``PSF`` objects.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
ndim : int
|
|
287
|
+
Number of spatial dimensions.
|
|
288
|
+
bc : {``"pbc"``, ``"noflux"``}
|
|
289
|
+
Boundary condition for stencil operators.
|
|
290
|
+
order : str
|
|
291
|
+
Grid flattening order (``"C"`` default).
|
|
292
|
+
key_grid_shape : str
|
|
293
|
+
Extras key for the grid shape array.
|
|
294
|
+
key_dx : str
|
|
295
|
+
Extras key for the grid spacing.
|
|
296
|
+
**sectors : Sector
|
|
297
|
+
Named sectors (keyword arguments).
|
|
298
|
+
|
|
299
|
+
Examples
|
|
300
|
+
--------
|
|
301
|
+
>>> layout = GridLayout(
|
|
302
|
+
... velocity=VectorSector([0, 1], sdim=2, spatial=True),
|
|
303
|
+
... Q=SymTensorSector([2, 3], sdim=2, traceless=True),
|
|
304
|
+
... dim=4, ndim=2, bc="pbc",
|
|
305
|
+
... )
|
|
306
|
+
>>> v = layout.velocity
|
|
307
|
+
>>> Q = layout.Q
|
|
308
|
+
>>> force = layout.embed(rank=1,
|
|
309
|
+
... velocity=layout.lap(v),
|
|
310
|
+
... Q=layout.lap(Q),
|
|
311
|
+
... )
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
dim: int,
|
|
318
|
+
ndim: int,
|
|
319
|
+
bc: Literal["pbc", "noflux"] = "pbc",
|
|
320
|
+
order: Literal["C", "F"] = "C",
|
|
321
|
+
key_grid_shape: str = "box/grid_shape",
|
|
322
|
+
key_dx: str = "box/dx",
|
|
323
|
+
**sectors: Sector,
|
|
324
|
+
) -> None:
|
|
325
|
+
self._ndim = int(ndim)
|
|
326
|
+
self._bc: Literal["pbc", "noflux"] = bc
|
|
327
|
+
self._order: Literal["C", "F"] = order
|
|
328
|
+
self._key_grid_shape = key_grid_shape
|
|
329
|
+
self._key_dx = key_dx
|
|
330
|
+
super().__init__(dim=dim, **sectors)
|
|
331
|
+
self._validate_spatial_sectors()
|
|
332
|
+
|
|
333
|
+
# --- validation ---------------------------------------------------
|
|
334
|
+
|
|
335
|
+
def _validate_spatial_sectors(self) -> None:
|
|
336
|
+
for name, sector in self._sectors.items():
|
|
337
|
+
if isinstance(sector, VectorSector) and sector.spatial:
|
|
338
|
+
if sector.sdim != self._ndim:
|
|
339
|
+
raise ValueError(f"Sector '{name}' is spatial but sdim={sector.sdim} != ndim={self._ndim}")
|
|
340
|
+
|
|
341
|
+
# --- properties ---------------------------------------------------
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def ndim(self) -> int:
|
|
345
|
+
"""Number of spatial dimensions."""
|
|
346
|
+
return self._ndim
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def bc(self) -> Literal["pbc", "noflux"]:
|
|
350
|
+
"""Boundary condition."""
|
|
351
|
+
return self._bc
|
|
352
|
+
|
|
353
|
+
# --- private: create StencilMeta ----------------------------------
|
|
354
|
+
|
|
355
|
+
def _make_meta(self, op_name: str) -> StencilMeta:
|
|
356
|
+
return StencilMeta(
|
|
357
|
+
op_name=op_name,
|
|
358
|
+
ndim=self._ndim,
|
|
359
|
+
bc=self._bc,
|
|
360
|
+
order=self._order,
|
|
361
|
+
key_grid_shape=self._key_grid_shape,
|
|
362
|
+
key_dx=self._key_dx,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# --- private: validate expression belongs to this layout ----------
|
|
366
|
+
|
|
367
|
+
def _check_layout(self, expr: StructuredExpr, method: str) -> None:
|
|
368
|
+
if expr._layout_id != self._layout_id:
|
|
369
|
+
from ..structexpr import _FREE_LAYOUT
|
|
370
|
+
|
|
371
|
+
if expr._layout_id != _FREE_LAYOUT:
|
|
372
|
+
raise TypeError(
|
|
373
|
+
f"{method}: expression belongs to a different layout (id {expr._layout_id} vs {self._layout_id})"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# ==================================================================
|
|
377
|
+
# Differential operator methods
|
|
378
|
+
# ==================================================================
|
|
379
|
+
|
|
380
|
+
# ------------------------------------------------------------------
|
|
381
|
+
# Internal label helper
|
|
382
|
+
# ------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _op_label(prefix: str, expr: StructuredExpr, suffix: str = "") -> tuple:
|
|
386
|
+
"""Return a single-element label tuple for a unary differential op.
|
|
387
|
+
|
|
388
|
+
Uses the operand's existing single label if available; otherwise
|
|
389
|
+
returns an empty tuple so the fallback in ``embed`` is used.
|
|
390
|
+
"""
|
|
391
|
+
if expr.labels and len(expr.labels) == 1:
|
|
392
|
+
return (f"{prefix}{expr.labels[0]}{suffix}",)
|
|
393
|
+
return ()
|
|
394
|
+
|
|
395
|
+
def grad(self, expr: StructuredExpr) -> StructuredExpr:
|
|
396
|
+
"""Spatial gradient. Adds an ``(ndim,)`` trailing axis.
|
|
397
|
+
|
|
398
|
+
Input sdims ``S`` → output sdims ``S + (ndim,)``.
|
|
399
|
+
"""
|
|
400
|
+
self._check_layout(expr, "grad")
|
|
401
|
+
out_sdims = expr.sdims + (self._ndim,)
|
|
402
|
+
return expr._new(
|
|
403
|
+
sdims=out_sdims,
|
|
404
|
+
n_features=expr.n_features,
|
|
405
|
+
labels=self._op_label("∇", expr),
|
|
406
|
+
node=_DiffOpNode(
|
|
407
|
+
op_name="grad",
|
|
408
|
+
child=expr._node,
|
|
409
|
+
engine_meta=self._make_meta("grad"),
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def lap(self, expr: StructuredExpr) -> StructuredExpr:
|
|
414
|
+
"""Laplacian ∇². Rank-preserving (same sdims as input)."""
|
|
415
|
+
self._check_layout(expr, "lap")
|
|
416
|
+
return expr._new(
|
|
417
|
+
sdims=expr.sdims,
|
|
418
|
+
n_features=expr.n_features,
|
|
419
|
+
labels=self._op_label("∇²", expr),
|
|
420
|
+
node=_DiffOpNode(
|
|
421
|
+
op_name="lap",
|
|
422
|
+
child=expr._node,
|
|
423
|
+
engine_meta=self._make_meta("lap"),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def div(self, expr: StructuredExpr) -> StructuredExpr:
|
|
428
|
+
"""Divergence. Contracts the last axis (must be ``ndim``).
|
|
429
|
+
|
|
430
|
+
Input sdims ``(*S, ndim)`` → output sdims ``S``.
|
|
431
|
+
"""
|
|
432
|
+
self._check_layout(expr, "div")
|
|
433
|
+
if not expr.sdims or expr.sdims[-1] != self._ndim:
|
|
434
|
+
raise ValueError(f"div requires last sdim == ndim={self._ndim}, got sdims={expr.sdims}")
|
|
435
|
+
out_sdims = expr.sdims[:-1]
|
|
436
|
+
return expr._new(
|
|
437
|
+
sdims=out_sdims,
|
|
438
|
+
n_features=expr.n_features,
|
|
439
|
+
labels=self._op_label("∇·", expr),
|
|
440
|
+
node=_DiffOpNode(
|
|
441
|
+
op_name="div",
|
|
442
|
+
child=expr._node,
|
|
443
|
+
engine_meta=self._make_meta("div"),
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def biharmonic(self, expr: StructuredExpr) -> StructuredExpr:
|
|
448
|
+
"""Biharmonic ∇⁴. Rank-preserving. Requires ``ndim >= 2``."""
|
|
449
|
+
self._check_layout(expr, "biharmonic")
|
|
450
|
+
if self._ndim < 2:
|
|
451
|
+
raise ValueError("biharmonic requires ndim >= 2")
|
|
452
|
+
return expr._new(
|
|
453
|
+
sdims=expr.sdims,
|
|
454
|
+
n_features=expr.n_features,
|
|
455
|
+
labels=self._op_label("∇⁴", expr),
|
|
456
|
+
node=_DiffOpNode(
|
|
457
|
+
op_name="biharmonic",
|
|
458
|
+
child=expr._node,
|
|
459
|
+
engine_meta=self._make_meta("biharmonic"),
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def lap_of_grad_sq(self, expr: StructuredExpr) -> StructuredExpr:
|
|
464
|
+
r"""∇²(\|∇f\|²) — the Active Model B+ term.
|
|
465
|
+
|
|
466
|
+
Convenience sugar for ``self.lap(self.grad(expr).dot(self.grad(expr)))``.
|
|
467
|
+
Compiles to the biharmonic-radius stencil automatically via footprint
|
|
468
|
+
computation. Requires ``ndim >= 2``.
|
|
469
|
+
"""
|
|
470
|
+
self._check_layout(expr, "lap_of_grad_sq")
|
|
471
|
+
if self._ndim < 2:
|
|
472
|
+
raise ValueError("lap_of_grad_sq requires ndim >= 2")
|
|
473
|
+
g = self.grad(expr)
|
|
474
|
+
return self.lap(g.dot(g))
|
|
475
|
+
|
|
476
|
+
# ------------------------------------------------------------------
|
|
477
|
+
# Convenience differential operator methods
|
|
478
|
+
# ------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
def advection_by(
|
|
481
|
+
self,
|
|
482
|
+
velocity: StructuredExpr,
|
|
483
|
+
transported: StructuredExpr,
|
|
484
|
+
) -> StructuredExpr:
|
|
485
|
+
r"""Advection :math:`(\mathbf{v}\cdot\nabla)\phi`.
|
|
486
|
+
|
|
487
|
+
*velocity* must have sdims ``(…, ndim)`` and *transported* is
|
|
488
|
+
arbitrary. Contracts the spatial gradient with the velocity
|
|
489
|
+
vector.
|
|
490
|
+
"""
|
|
491
|
+
self._check_layout(velocity, "advection_by")
|
|
492
|
+
self._check_layout(transported, "advection_by")
|
|
493
|
+
v_lbl = velocity.labels[0] if len(velocity.labels) == 1 else "v"
|
|
494
|
+
f_lbl = transported.labels[0] if len(transported.labels) == 1 else "f"
|
|
495
|
+
g = self.grad(transported) # sdims: (*T, ndim)
|
|
496
|
+
# Contract the trailing ndim axis of grad with trailing ndim of v.
|
|
497
|
+
result = g.dot(velocity)
|
|
498
|
+
return result._new(
|
|
499
|
+
labels=(f"({v_lbl}·∇){f_lbl}",),
|
|
500
|
+
node=result._node,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def strain_rate(self, v_expr: StructuredExpr) -> StructuredExpr:
|
|
504
|
+
r"""Symmetric strain rate :math:`E = (\nabla v + (\nabla v)^T)/2`.
|
|
505
|
+
|
|
506
|
+
Input: vector ``(ndim,)``. Output: symmetric tensor ``(ndim, ndim)``.
|
|
507
|
+
"""
|
|
508
|
+
self._check_layout(v_expr, "strain_rate")
|
|
509
|
+
v_lbl = v_expr.labels[0] if len(v_expr.labels) == 1 else "v"
|
|
510
|
+
gv = self.grad(v_expr) # (ndim, ndim)
|
|
511
|
+
result = (gv + gv.T) * 0.5
|
|
512
|
+
return result._new(labels=(f"E[{v_lbl}]",), node=result._node)
|
|
513
|
+
|
|
514
|
+
def vorticity(self, v_expr: StructuredExpr) -> StructuredExpr:
|
|
515
|
+
r"""Antisymmetric vorticity :math:`\Omega = (\nabla v - (\nabla v)^T)/2`.
|
|
516
|
+
|
|
517
|
+
Input: vector ``(ndim,)``. Output: antisymmetric tensor ``(ndim, ndim)``.
|
|
518
|
+
"""
|
|
519
|
+
self._check_layout(v_expr, "vorticity")
|
|
520
|
+
gv = self.grad(v_expr) # (ndim, ndim)
|
|
521
|
+
return (gv - gv.T) * 0.5
|
|
522
|
+
|
|
523
|
+
# ==================================================================
|
|
524
|
+
# embed() — the compilation boundary
|
|
525
|
+
# ==================================================================
|
|
526
|
+
|
|
527
|
+
def embed(
|
|
528
|
+
self,
|
|
529
|
+
rank: int = 1,
|
|
530
|
+
**named_fields: StructuredExpr,
|
|
531
|
+
) -> Any:
|
|
532
|
+
"""Compile inner-world expressions into an outer-world Basis/PSF.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
rank : int
|
|
537
|
+
Output rank (1 for forces, 2 for diffusion matrices).
|
|
538
|
+
**named_fields
|
|
539
|
+
``sector_name=expression`` pairs. Each expression's sdims
|
|
540
|
+
must match the corresponding sector's sdims.
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
Basis
|
|
545
|
+
Outer-world expression ready for inference.
|
|
546
|
+
"""
|
|
547
|
+
if rank not in (1, 2):
|
|
548
|
+
raise ValueError(f"Only rank=1 and rank=2 are supported, got {rank}")
|
|
549
|
+
|
|
550
|
+
# Validate sector names and sdims
|
|
551
|
+
for name, expr in named_fields.items():
|
|
552
|
+
if name not in self._sectors:
|
|
553
|
+
raise ValueError(f"Unknown sector '{name}'. Available: {list(self._sectors.keys())}")
|
|
554
|
+
sector = self._sectors[name]
|
|
555
|
+
if expr.sdims != sector.sdims:
|
|
556
|
+
raise ValueError(f"Sector '{name}' has sdims={sector.sdims}, but expression has sdims={expr.sdims}")
|
|
557
|
+
|
|
558
|
+
# Compile each sector
|
|
559
|
+
sector_bases = []
|
|
560
|
+
for name, expr in named_fields.items():
|
|
561
|
+
sector = self._sectors[name]
|
|
562
|
+
compiled = self._compile_sector(expr._node, name, sector, rank, labels=list(expr.labels))
|
|
563
|
+
sector_bases.append(compiled)
|
|
564
|
+
|
|
565
|
+
# Combine all sectors with feature concatenation
|
|
566
|
+
if not sector_bases:
|
|
567
|
+
raise ValueError("At least one sector expression is required")
|
|
568
|
+
|
|
569
|
+
result = sector_bases[0]
|
|
570
|
+
for sb in sector_bases[1:]:
|
|
571
|
+
result = result & sb
|
|
572
|
+
return result
|
|
573
|
+
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
# Sector compilation — footprint-based dispatch
|
|
576
|
+
# ------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
def _compile_sector(
|
|
579
|
+
self,
|
|
580
|
+
node: _StructNode,
|
|
581
|
+
sector_name: str,
|
|
582
|
+
sector: Sector,
|
|
583
|
+
rank: int,
|
|
584
|
+
labels: list | None = None,
|
|
585
|
+
) -> Any:
|
|
586
|
+
"""Compile a single sector's _StructNode tree to outer StateExpr.
|
|
587
|
+
|
|
588
|
+
Uses footprint analysis: if the tree reads only the local site
|
|
589
|
+
(footprint = {origin}), compile as pure-pointwise. Otherwise
|
|
590
|
+
compile the *entire tree* — including nested stencil ops,
|
|
591
|
+
nonlinear algebra, etc. — as a single fat-stencil dispatch.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
labels : list[str] or None
|
|
596
|
+
Human-readable term labels propagated from the outer
|
|
597
|
+
``StructuredExpr.labels``. One label per feature (i.e. per
|
|
598
|
+
term after ``&``-concatenation). ``None`` / empty list falls
|
|
599
|
+
back to ``"stencil(<sector_name>)"``.
|
|
600
|
+
"""
|
|
601
|
+
from ._fd_atoms import _origin, compute_footprint
|
|
602
|
+
|
|
603
|
+
labels = labels or []
|
|
604
|
+
|
|
605
|
+
# Handle _ConcatOp at top level (feature concatenation)
|
|
606
|
+
if isinstance(node, _ConcatOp):
|
|
607
|
+
parts = []
|
|
608
|
+
for i, c in enumerate(node.children):
|
|
609
|
+
child_lbl = [labels[i]] if i < len(labels) else []
|
|
610
|
+
parts.append(self._compile_sector(c, sector_name, sector, rank, labels=child_lbl))
|
|
611
|
+
result = parts[0]
|
|
612
|
+
for p in parts[1:]:
|
|
613
|
+
result = result & p
|
|
614
|
+
return result
|
|
615
|
+
|
|
616
|
+
footprint = compute_footprint(node, self._ndim)
|
|
617
|
+
origin = _origin(self._ndim)
|
|
618
|
+
|
|
619
|
+
if footprint == origin:
|
|
620
|
+
# Pure pointwise — no stencil needed
|
|
621
|
+
return self._compile_pointwise_term(node, sector_name, sector, rank, labels=labels)
|
|
622
|
+
else:
|
|
623
|
+
# Stencil tree (may include nested diff ops + algebra)
|
|
624
|
+
return self._compile_stencil_tree(node, footprint, sector_name, sector, rank, labels=labels)
|
|
625
|
+
|
|
626
|
+
# ------------------------------------------------------------------
|
|
627
|
+
# Stencil tree compilation (generic, supports nesting)
|
|
628
|
+
# ------------------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
def _compile_stencil_tree(
|
|
631
|
+
self,
|
|
632
|
+
node: _StructNode,
|
|
633
|
+
footprint,
|
|
634
|
+
sector_name: str,
|
|
635
|
+
sector: Sector,
|
|
636
|
+
rank: int,
|
|
637
|
+
labels: list | None = None,
|
|
638
|
+
) -> Any:
|
|
639
|
+
"""Compile an arbitrary tree with stencil nodes to outer Basis.
|
|
640
|
+
|
|
641
|
+
1. Convert footprint → sorted offset array + o2i dict.
|
|
642
|
+
2. Recursively compile the tree via ``_eval_compiler.compile_eval``.
|
|
643
|
+
3. Wrap the eval function with sector pack + scatter.
|
|
644
|
+
4. Dispatch via ``make_interactor`` + ``PreparedSquareStencilFromBox``.
|
|
645
|
+
"""
|
|
646
|
+
from ..factory import make_interactor
|
|
647
|
+
from ..nodes.interactions.stencils import PreparedSquareStencilFromBox
|
|
648
|
+
from ._eval_compiler import compile_eval, wrap_as_local_fn
|
|
649
|
+
from ._fd_atoms import build_offset_to_idx, offsets_to_sorted_array
|
|
650
|
+
|
|
651
|
+
ndim = self._ndim
|
|
652
|
+
offsets_array = offsets_to_sorted_array(footprint)
|
|
653
|
+
o2i = build_offset_to_idx(offsets_array)
|
|
654
|
+
|
|
655
|
+
# Build the recursive eval function (trace-time offset resolution)
|
|
656
|
+
eval_fn = compile_eval(node, o2i, self._sectors, ndim, self._key_dx)
|
|
657
|
+
|
|
658
|
+
# Wrap: mask pre-processing + evaluation at origin
|
|
659
|
+
raw_local_fn = wrap_as_local_fn(eval_fn, ndim, o2i)
|
|
660
|
+
|
|
661
|
+
# Determine output sdims
|
|
662
|
+
out_sdims = _node_sdims(node, self._sectors)
|
|
663
|
+
|
|
664
|
+
# Wrap with sector pack + scatter to dim-wide output
|
|
665
|
+
local_fn = self._wrap_scatter(raw_local_fn, out_sdims, sector, rank)
|
|
666
|
+
|
|
667
|
+
K = int(offsets_array.shape[0])
|
|
668
|
+
|
|
669
|
+
# Determine stencil cache prefix from the footprint size
|
|
670
|
+
# (The prefix is only needed for the hyper-table cache key)
|
|
671
|
+
prefix = f"_cache/stencil/composed_{ndim}d_{self._bc}_K{K}"
|
|
672
|
+
|
|
673
|
+
labels = labels or []
|
|
674
|
+
label_str = labels[0] if labels else f"stencil({sector_name})"
|
|
675
|
+
inter = make_interactor(
|
|
676
|
+
local_fn,
|
|
677
|
+
dim=self._dim,
|
|
678
|
+
rank=Rank(rank),
|
|
679
|
+
K=K,
|
|
680
|
+
n_features=1,
|
|
681
|
+
labels=[label_str],
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
spec = PreparedSquareStencilFromBox(
|
|
685
|
+
offsets=jnp.asarray(offsets_array),
|
|
686
|
+
bc=self._bc,
|
|
687
|
+
order=self._order,
|
|
688
|
+
key_grid_shape=self._key_grid_shape,
|
|
689
|
+
key_prefix=prefix,
|
|
690
|
+
include_slot_extras_offset=True,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
return inter.dispatch(
|
|
694
|
+
spec,
|
|
695
|
+
owners="focal",
|
|
696
|
+
focal_index=0,
|
|
697
|
+
reducer="sum",
|
|
698
|
+
return_as="basis",
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# ------------------------------------------------------------------
|
|
702
|
+
# Pointwise term compilation
|
|
703
|
+
# ------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
def _compile_pointwise_term(
|
|
706
|
+
self,
|
|
707
|
+
node: _StructNode,
|
|
708
|
+
sector_name: str,
|
|
709
|
+
sector: Sector,
|
|
710
|
+
rank: int,
|
|
711
|
+
labels: list | None = None,
|
|
712
|
+
) -> Any:
|
|
713
|
+
"""Compile a pure pointwise _StructNode to outer Basis."""
|
|
714
|
+
from ..factory import make_basis
|
|
715
|
+
|
|
716
|
+
labels = labels or []
|
|
717
|
+
label_str = labels[0] if labels else sector_name
|
|
718
|
+
|
|
719
|
+
pw_fn = _compile_pointwise_fn(node, self._sectors)
|
|
720
|
+
dim = self._dim
|
|
721
|
+
indices = jnp.array(sector.indices)
|
|
722
|
+
|
|
723
|
+
if rank == 1:
|
|
724
|
+
|
|
725
|
+
def fn(x):
|
|
726
|
+
val = pw_fn(x) # (*sdims)
|
|
727
|
+
packed = _sector_pack(val, sector) # (n_data,)
|
|
728
|
+
out = jnp.zeros(dim, dtype=x.dtype)
|
|
729
|
+
out = out.at[indices].set(packed)
|
|
730
|
+
return out # (dim,) → rank=1, n_features=1
|
|
731
|
+
|
|
732
|
+
return make_basis(
|
|
733
|
+
fn,
|
|
734
|
+
dim=self._dim,
|
|
735
|
+
rank=1,
|
|
736
|
+
n_features=1,
|
|
737
|
+
labels=[label_str],
|
|
738
|
+
)
|
|
739
|
+
elif rank == 2:
|
|
740
|
+
# Block-diagonal embedding
|
|
741
|
+
def fn(x):
|
|
742
|
+
val = pw_fn(x) # (*sdims)
|
|
743
|
+
packed = _sector_pack(val, sector) # (n_data,)
|
|
744
|
+
out = jnp.zeros((dim, dim), dtype=x.dtype)
|
|
745
|
+
# Place as diagonal block
|
|
746
|
+
for i_local, i_global in enumerate(sector.indices):
|
|
747
|
+
out = out.at[i_global, i_global].set(packed[i_local] if i_local < len(packed) else 0.0)
|
|
748
|
+
return out # (dim, dim) → rank=2
|
|
749
|
+
|
|
750
|
+
return make_basis(
|
|
751
|
+
fn,
|
|
752
|
+
dim=self._dim,
|
|
753
|
+
rank=2,
|
|
754
|
+
n_features=1,
|
|
755
|
+
labels=[label_str],
|
|
756
|
+
)
|
|
757
|
+
else:
|
|
758
|
+
raise ValueError(f"Unsupported rank={rank}")
|
|
759
|
+
|
|
760
|
+
# ------------------------------------------------------------------
|
|
761
|
+
# Helpers
|
|
762
|
+
# ------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
def _wrap_scatter(
|
|
765
|
+
self,
|
|
766
|
+
fd_fn: Callable,
|
|
767
|
+
out_sdims: tuple[int, ...],
|
|
768
|
+
sector: Sector,
|
|
769
|
+
rank: int,
|
|
770
|
+
) -> Callable:
|
|
771
|
+
"""Wrap an FD local function with sector scatter to dim-wide output."""
|
|
772
|
+
dim = self._dim
|
|
773
|
+
indices = jnp.array(sector.indices)
|
|
774
|
+
|
|
775
|
+
if rank == 1:
|
|
776
|
+
|
|
777
|
+
def local_fn(Xk, *, mask=None, extras=None):
|
|
778
|
+
fd_result = fd_fn(Xk, mask=mask, extras=extras) # (n_flat_out,)
|
|
779
|
+
# Reshape to structured
|
|
780
|
+
structured = fd_result.reshape(out_sdims) if out_sdims else fd_result
|
|
781
|
+
# Pack for sector
|
|
782
|
+
packed = _sector_pack(structured, sector) # (n_data,)
|
|
783
|
+
# Scatter to dim-wide
|
|
784
|
+
out = jnp.zeros(dim, dtype=fd_result.dtype)
|
|
785
|
+
out = out.at[indices].set(packed)
|
|
786
|
+
return out # (dim,) → rank=1, n_features=1
|
|
787
|
+
|
|
788
|
+
return local_fn
|
|
789
|
+
elif rank == 2:
|
|
790
|
+
|
|
791
|
+
def local_fn(Xk, *, mask=None, extras=None):
|
|
792
|
+
fd_result = fd_fn(Xk, mask=mask, extras=extras)
|
|
793
|
+
structured = fd_result.reshape(out_sdims) if out_sdims else fd_result
|
|
794
|
+
packed = _sector_pack(structured, sector)
|
|
795
|
+
out = jnp.zeros((dim, dim), dtype=fd_result.dtype)
|
|
796
|
+
for i_local, i_global in enumerate(sector.indices):
|
|
797
|
+
out = out.at[i_global, i_global].set(packed[i_local] if i_local < len(packed) else 0.0)
|
|
798
|
+
return out # (dim, dim)
|
|
799
|
+
|
|
800
|
+
return local_fn
|
|
801
|
+
else:
|
|
802
|
+
raise ValueError(f"Unsupported rank={rank}")
|
|
803
|
+
|
|
804
|
+
def __repr__(self) -> str:
|
|
805
|
+
parts = [f"dim={self._dim}", f"ndim={self._ndim}", f"bc={self._bc!r}"]
|
|
806
|
+
for name, sector in self._sectors.items():
|
|
807
|
+
parts.append(f"{name}={sector!r}")
|
|
808
|
+
return f"GridLayout({', '.join(parts)})"
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# =====================================================================
|
|
812
|
+
# Tree utilities
|
|
813
|
+
# =====================================================================
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _node_sdims(
|
|
817
|
+
node: _StructNode,
|
|
818
|
+
sectors: dict[str, Sector],
|
|
819
|
+
) -> tuple[int, ...]:
|
|
820
|
+
"""Infer sdims of a node from the tree structure."""
|
|
821
|
+
if isinstance(node, _SectorLeaf):
|
|
822
|
+
return node.sdims
|
|
823
|
+
if isinstance(node, _ConstNode):
|
|
824
|
+
return node.sdims
|
|
825
|
+
if isinstance(node, _BinaryOp):
|
|
826
|
+
left_s = _node_sdims(node.left, sectors)
|
|
827
|
+
right_s = _node_sdims(node.right, sectors)
|
|
828
|
+
# Broadcasting: scalar with anything -> anything
|
|
829
|
+
if left_s == ():
|
|
830
|
+
return right_s
|
|
831
|
+
if right_s == ():
|
|
832
|
+
return left_s
|
|
833
|
+
return left_s # should match
|
|
834
|
+
if isinstance(node, _UnaryOp):
|
|
835
|
+
child_s = _node_sdims(node.child, sectors)
|
|
836
|
+
if node.op == "T" and len(child_s) >= 2:
|
|
837
|
+
return child_s[:-2] + (child_s[-1], child_s[-2])
|
|
838
|
+
return child_s
|
|
839
|
+
if isinstance(node, _EinsumOp):
|
|
840
|
+
# Parse the spec to get output sdims
|
|
841
|
+
from ..structexpr import _parse_einsum
|
|
842
|
+
|
|
843
|
+
operand_specs, rhs = _parse_einsum(node.spec)
|
|
844
|
+
letter_size: dict[str, int] = {}
|
|
845
|
+
for spec_str, child in zip(operand_specs, node.children):
|
|
846
|
+
child_sdims = _node_sdims(child, sectors)
|
|
847
|
+
for ch, sz in zip(spec_str, child_sdims):
|
|
848
|
+
letter_size[ch] = sz
|
|
849
|
+
return tuple(letter_size[ch] for ch in rhs)
|
|
850
|
+
if isinstance(node, _StackOp):
|
|
851
|
+
return (node.sdim,) + _node_sdims(node.children[0], sectors)
|
|
852
|
+
if isinstance(node, _SliceOp):
|
|
853
|
+
return _node_sdims(node.child, sectors)[:-1]
|
|
854
|
+
if isinstance(node, _ConcatOp):
|
|
855
|
+
return _node_sdims(node.children[0], sectors)
|
|
856
|
+
if isinstance(node, _DiffOpNode):
|
|
857
|
+
child_s = _node_sdims(node.child, sectors)
|
|
858
|
+
meta = node.engine_meta
|
|
859
|
+
return _op_output_sdims(node.op_name, child_s, meta.ndim)
|
|
860
|
+
return ()
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _op_output_sdims(
|
|
864
|
+
op_name: str,
|
|
865
|
+
child_sdims: tuple[int, ...],
|
|
866
|
+
ndim: int,
|
|
867
|
+
) -> tuple[int, ...]:
|
|
868
|
+
"""Compute output sdims for a differential operator."""
|
|
869
|
+
if op_name == "lap":
|
|
870
|
+
return child_sdims
|
|
871
|
+
elif op_name == "grad":
|
|
872
|
+
return child_sdims + (ndim,)
|
|
873
|
+
elif op_name == "div":
|
|
874
|
+
return child_sdims[:-1]
|
|
875
|
+
elif op_name == "biharmonic":
|
|
876
|
+
return child_sdims
|
|
877
|
+
else:
|
|
878
|
+
raise ValueError(f"Unknown op: {op_name!r}")
|