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/pairs.py
ADDED
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SFI.bases.pairs
|
|
3
|
+
===============
|
|
4
|
+
|
|
5
|
+
Generic building blocks for multi-particle (pair-interaction) systems.
|
|
6
|
+
|
|
7
|
+
This module provides:
|
|
8
|
+
|
|
9
|
+
- **PBC utilities**: minimum-image displacement in arbitrary dimension.
|
|
10
|
+
- **Kernel families**: pre-built 1-D radial kernel functions.
|
|
11
|
+
- **Radial / scalar pair bases**: build Interactor objects from kernel
|
|
12
|
+
families, ready for dispatch over neighbor lists.
|
|
13
|
+
- **Angular coupling bases**: weighted orientation-coupling interactors.
|
|
14
|
+
- **Heading vector**: single-particle heading vector from an angle coordinate.
|
|
15
|
+
- **Tensor pair features**: rank-2 dyadic basis for diffusion tensors, nematic order, etc.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Callable, Sequence
|
|
22
|
+
|
|
23
|
+
import jax.numpy as jnp
|
|
24
|
+
|
|
25
|
+
from ..statefunc import Basis, Interactor, make_basis, make_interactor
|
|
26
|
+
|
|
27
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
28
|
+
# PBC UTILITIES
|
|
29
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pbc_displacement(xj, xi, box):
|
|
33
|
+
"""Minimum-image displacement ``xj - xi`` under periodic boundaries.
|
|
34
|
+
|
|
35
|
+
Works in any dimension. All inputs are plain JAX arrays of shape
|
|
36
|
+
``(d,)`` (or broadcastable).
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
xj, xi : array, shape ``(d,)``
|
|
41
|
+
Positions (or sub-positions) of two particles.
|
|
42
|
+
box : array, shape ``(d,)``
|
|
43
|
+
Box lengths along each axis.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
dx : array, shape ``(d,)``
|
|
48
|
+
``xj - xi`` folded via minimum-image convention.
|
|
49
|
+
"""
|
|
50
|
+
dx = xj - xi
|
|
51
|
+
return dx - box * jnp.round(dx / box)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def wrap_angle(a):
|
|
55
|
+
"""Wrap angle(s) to ``(-π, π]``."""
|
|
56
|
+
return (a + jnp.pi) % (2.0 * jnp.pi) - jnp.pi
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _pairwise_dr(XK, *, box=None, spatial_dims=None, eps=1e-12):
|
|
60
|
+
"""Compute displacement, distance and unit vector for a K=2 pair.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
XK : array, shape ``(2, dim)``
|
|
65
|
+
Stacked pair ``[xi, xj]``.
|
|
66
|
+
box : array or None
|
|
67
|
+
PBC box lengths (applied to ``spatial_dims`` only).
|
|
68
|
+
spatial_dims : slice or index array, optional
|
|
69
|
+
Which dimensions of state vector are spatial coordinates.
|
|
70
|
+
Default: all.
|
|
71
|
+
eps : float
|
|
72
|
+
Regularisation to avoid division by zero.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
dx : array, shape ``(d_spatial,)``
|
|
77
|
+
r : scalar
|
|
78
|
+
rhat : array, shape ``(d_spatial,)``
|
|
79
|
+
"""
|
|
80
|
+
xi, xj = XK[0], XK[1]
|
|
81
|
+
if spatial_dims is not None:
|
|
82
|
+
xi_s, xj_s = xi[spatial_dims], xj[spatial_dims]
|
|
83
|
+
else:
|
|
84
|
+
xi_s, xj_s = xi, xj
|
|
85
|
+
if box is not None:
|
|
86
|
+
dx = pbc_displacement(xj_s, xi_s, box)
|
|
87
|
+
else:
|
|
88
|
+
dx = xj_s - xi_s
|
|
89
|
+
r = jnp.sqrt(jnp.sum(dx**2) + eps)
|
|
90
|
+
rhat = dx / r
|
|
91
|
+
return dx, r, rhat
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
95
|
+
# KERNEL FAMILIES
|
|
96
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def exp_poly_kernels(degrees, lengths):
|
|
100
|
+
r"""Radial kernels :math:`\phi_{k,L}(r) = r^k \exp(-r/L)`.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
degrees : sequence of int
|
|
105
|
+
Polynomial degrees *k*.
|
|
106
|
+
lengths : sequence of float
|
|
107
|
+
Exponential decay lengths *L*.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
list of (callable, str)
|
|
112
|
+
Each entry is ``(phi, label)`` where ``phi(r) -> scalar``.
|
|
113
|
+
"""
|
|
114
|
+
out = []
|
|
115
|
+
for k in degrees:
|
|
116
|
+
for L in lengths:
|
|
117
|
+
k_, L_ = int(k), float(L)
|
|
118
|
+
|
|
119
|
+
def _phi(r, _k=k_, _L=L_):
|
|
120
|
+
return (r**_k) * jnp.exp(-r / _L)
|
|
121
|
+
|
|
122
|
+
out.append((_phi, f"r^{k_}·exp(-r/{L_:g})"))
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def gaussian_kernels(sigmas):
|
|
127
|
+
r"""Radial kernels :math:`\phi_\sigma(r) = \exp(-r^2 / 2\sigma^2)`.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
sigmas : sequence of float
|
|
132
|
+
Gaussian widths.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
list of (callable, str)
|
|
137
|
+
"""
|
|
138
|
+
out = []
|
|
139
|
+
for s in sigmas:
|
|
140
|
+
s_ = float(s)
|
|
141
|
+
|
|
142
|
+
def _phi(r, _s=s_):
|
|
143
|
+
return jnp.exp(-(r**2) / (2.0 * _s**2))
|
|
144
|
+
|
|
145
|
+
out.append((_phi, f"r^0·gauss(σ={s_:g})"))
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def power_kernels(degrees):
|
|
150
|
+
r"""Radial kernels :math:`\phi_k(r) = r^k`.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
degrees : sequence of int
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
list of (callable, str)
|
|
159
|
+
"""
|
|
160
|
+
out = []
|
|
161
|
+
for k in degrees:
|
|
162
|
+
k_ = int(k)
|
|
163
|
+
|
|
164
|
+
def _phi(r, _k=k_):
|
|
165
|
+
return r**_k
|
|
166
|
+
|
|
167
|
+
out.append((_phi, f"r^{k_}"))
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compact_kernels(degrees, cutoff):
|
|
172
|
+
r"""Compactly-supported kernels :math:`r^k (1 - r/r_c)^2` for :math:`r < r_c`.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
degrees : sequence of int
|
|
177
|
+
cutoff : float
|
|
178
|
+
Support radius *r_c*.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
list of (callable, str)
|
|
183
|
+
"""
|
|
184
|
+
rc = float(cutoff)
|
|
185
|
+
out = []
|
|
186
|
+
for k in degrees:
|
|
187
|
+
k_ = int(k)
|
|
188
|
+
|
|
189
|
+
def _phi(r, _k=k_, _rc=rc):
|
|
190
|
+
w = jnp.where(r < _rc, (1.0 - r / _rc) ** 2, 0.0)
|
|
191
|
+
return (r**_k) * w
|
|
192
|
+
|
|
193
|
+
out.append((_phi, f"r^{k_}·comp(rc={rc:g})"))
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
198
|
+
# RADIAL / SCALAR PAIR BASES
|
|
199
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def radial_pair_basis(
|
|
203
|
+
kernels: Sequence[tuple[Callable, str]],
|
|
204
|
+
*,
|
|
205
|
+
dim: int,
|
|
206
|
+
box: Any = None,
|
|
207
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
208
|
+
embed_dim: int | None = None,
|
|
209
|
+
embed_axes: Sequence[int] | None = None,
|
|
210
|
+
labels: Sequence[str] | None = None,
|
|
211
|
+
) -> Interactor:
|
|
212
|
+
r"""Build a rank-1 pair Interactor with radial-kernel features.
|
|
213
|
+
|
|
214
|
+
Each feature is :math:`\phi_\alpha(r_{ij})\,\hat{\mathbf{r}}_{ij}`
|
|
215
|
+
where :math:`r_{ij}` is the pairwise distance (optionally with PBC).
|
|
216
|
+
|
|
217
|
+
.. physics:: Radial pair interaction basis
|
|
218
|
+
:label: radial-pair-basis
|
|
219
|
+
:category: Basis functions
|
|
220
|
+
|
|
221
|
+
.. math::
|
|
222
|
+
|
|
223
|
+
f_\alpha(\mathbf{r}_{ij})
|
|
224
|
+
= \phi_\alpha(r_{ij})\;\hat{\mathbf{r}}_{ij}
|
|
225
|
+
|
|
226
|
+
Scalar radial kernel :math:`\phi_\alpha` times the unit displacement
|
|
227
|
+
vector. Available kernel families: exponential-polynomial,
|
|
228
|
+
Gaussian, power-law, and compactly supported.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
kernels : list of (callable, str)
|
|
233
|
+
1-D kernel functions and their labels, as returned by
|
|
234
|
+
:func:`exp_poly_kernels`, :func:`gaussian_kernels`, etc.
|
|
235
|
+
dim : int
|
|
236
|
+
Full state-vector dimension per particle.
|
|
237
|
+
box : array, ``"extras"``, or None
|
|
238
|
+
PBC box lengths. ``None`` (default) = free-space, no periodic
|
|
239
|
+
boundaries. Pass an array for a static box captured in the
|
|
240
|
+
closure, or ``"extras"`` to read the box from
|
|
241
|
+
``extras["box"]`` at evaluation time. The box is applied over
|
|
242
|
+
``spatial_dims`` only.
|
|
243
|
+
spatial_dims : slice or index array, optional
|
|
244
|
+
Which axes of the state vector are spatial coordinates (the ones
|
|
245
|
+
over which distances are computed and the output vector is
|
|
246
|
+
defined). Default: ``slice(None)`` (all axes are spatial).
|
|
247
|
+
embed_dim : int, optional
|
|
248
|
+
If the output should be embedded into a larger vector (e.g., the
|
|
249
|
+
displacement lives in 2D but the state vector is 3D with an
|
|
250
|
+
angle), set ``embed_dim`` to the output vector size. Spatial
|
|
251
|
+
components are placed at ``embed_axes`` indices; remaining indices
|
|
252
|
+
are zero.
|
|
253
|
+
embed_axes : sequence of int, optional
|
|
254
|
+
Indices into the ``embed_dim``-length output where spatial
|
|
255
|
+
components are placed. Required when ``embed_dim is not None``.
|
|
256
|
+
labels : sequence of str, optional
|
|
257
|
+
Override labels (one per kernel).
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
Interactor
|
|
262
|
+
Rank-1 (vector) interactor with ``K=2``, ``n_features=len(kernels)``.
|
|
263
|
+
Call ``.dispatch_pairs(...)`` to stream over neighbour lists.
|
|
264
|
+
"""
|
|
265
|
+
n_feat = len(kernels)
|
|
266
|
+
if labels is None:
|
|
267
|
+
labels = [lab for _, lab in kernels]
|
|
268
|
+
fns = [fn for fn, _ in kernels]
|
|
269
|
+
|
|
270
|
+
# Determine output vector size
|
|
271
|
+
if embed_dim is not None:
|
|
272
|
+
out_d = embed_dim
|
|
273
|
+
if embed_axes is None:
|
|
274
|
+
raise ValueError("embed_axes required when embed_dim is set")
|
|
275
|
+
_embed_axes = jnp.array(embed_axes, dtype=jnp.int32)
|
|
276
|
+
if len(embed_axes) != (
|
|
277
|
+
dim if spatial_dims is None
|
|
278
|
+
else len(range(*spatial_dims.indices(dim))) if isinstance(spatial_dims, slice)
|
|
279
|
+
else len(spatial_dims)
|
|
280
|
+
):
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"len(embed_axes)={len(embed_axes)} must equal the number of spatial "
|
|
283
|
+
f"dimensions selected by spatial_dims."
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
out_d = (
|
|
287
|
+
dim
|
|
288
|
+
if spatial_dims is None
|
|
289
|
+
else len(range(*spatial_dims.indices(dim)))
|
|
290
|
+
if isinstance(spatial_dims, slice)
|
|
291
|
+
else len(spatial_dims)
|
|
292
|
+
)
|
|
293
|
+
_embed_axes = None
|
|
294
|
+
|
|
295
|
+
# Resolve box mode
|
|
296
|
+
_use_extras_box = box == "extras"
|
|
297
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
298
|
+
|
|
299
|
+
def _pair_local(XK, *, extras=None):
|
|
300
|
+
# Resolve box
|
|
301
|
+
b = _box
|
|
302
|
+
if b is None and _use_extras_box and extras is not None:
|
|
303
|
+
b = extras["box"]
|
|
304
|
+
if spatial_dims is not None:
|
|
305
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
306
|
+
dx, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
307
|
+
|
|
308
|
+
# Evaluate kernels → (n_feat,)
|
|
309
|
+
vals = jnp.stack([fn(r) for fn in fns]) # (F,)
|
|
310
|
+
|
|
311
|
+
# Build output: (out_d, F) = rhat[:, None] * vals[None, :]
|
|
312
|
+
if _embed_axes is not None:
|
|
313
|
+
# Embed into larger vector
|
|
314
|
+
if len(embed_axes) != rhat.shape[0]:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"embed_axes length ({len(embed_axes)}) must equal the number of "
|
|
317
|
+
f"selected spatial dims ({rhat.shape[0]}) from spatial_dims."
|
|
318
|
+
)
|
|
319
|
+
full = jnp.zeros((out_d, n_feat), dtype=XK.dtype)
|
|
320
|
+
for k, ax in enumerate(embed_axes):
|
|
321
|
+
full = full.at[ax, :].set(rhat[k] * vals)
|
|
322
|
+
return full
|
|
323
|
+
else:
|
|
324
|
+
return rhat[:, None] * vals[None, :] # (d_spatial, F)
|
|
325
|
+
|
|
326
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
327
|
+
return make_interactor(
|
|
328
|
+
_pair_local,
|
|
329
|
+
dim=dim,
|
|
330
|
+
rank=1,
|
|
331
|
+
K=2,
|
|
332
|
+
n_features=n_feat,
|
|
333
|
+
extras_keys=extras_keys,
|
|
334
|
+
labels=list(labels),
|
|
335
|
+
descriptor="radial-pair-basis",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def scalar_pair_basis(
|
|
340
|
+
kernels: Sequence[tuple[Callable, str]],
|
|
341
|
+
*,
|
|
342
|
+
dim: int,
|
|
343
|
+
box: Any = None,
|
|
344
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
345
|
+
labels: Sequence[str] | None = None,
|
|
346
|
+
) -> Interactor:
|
|
347
|
+
r"""Build a rank-0 pair Interactor with scalar radial-kernel features.
|
|
348
|
+
|
|
349
|
+
Each feature is :math:`\phi_\alpha(r_{ij})` — the raw kernel value
|
|
350
|
+
without the directional :math:`\hat{r}` factor.
|
|
351
|
+
|
|
352
|
+
Use this for energy-like quantities, as radial weights for angular
|
|
353
|
+
coupling, or as building blocks for tensor pair features composed
|
|
354
|
+
via the ``*`` operator.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
kernels, dim, box, spatial_dims, labels
|
|
359
|
+
Same as :func:`radial_pair_basis`.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
Interactor
|
|
364
|
+
Rank-0 (scalar) interactor with ``K=2``.
|
|
365
|
+
"""
|
|
366
|
+
n_feat = len(kernels)
|
|
367
|
+
if labels is None:
|
|
368
|
+
labels = [lab for _, lab in kernels]
|
|
369
|
+
fns = [fn for fn, _ in kernels]
|
|
370
|
+
|
|
371
|
+
_use_extras_box = box == "extras"
|
|
372
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
373
|
+
|
|
374
|
+
def _pair_local(XK, *, extras=None):
|
|
375
|
+
b = _box
|
|
376
|
+
if b is None and _use_extras_box and extras is not None:
|
|
377
|
+
b = extras["box"]
|
|
378
|
+
if spatial_dims is not None:
|
|
379
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
380
|
+
_, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
381
|
+
return jnp.stack([fn(r) for fn in fns]) # (F,)
|
|
382
|
+
|
|
383
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
384
|
+
return make_interactor(
|
|
385
|
+
_pair_local,
|
|
386
|
+
dim=dim,
|
|
387
|
+
rank=0,
|
|
388
|
+
K=2,
|
|
389
|
+
n_features=n_feat,
|
|
390
|
+
extras_keys=extras_keys,
|
|
391
|
+
labels=list(labels),
|
|
392
|
+
descriptor="scalar-pair-basis",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
397
|
+
# ANGULAR / ORIENTATION COUPLING
|
|
398
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def angular_pair_basis(
|
|
402
|
+
kernels: Sequence[tuple[Callable, str]],
|
|
403
|
+
coupling_fn: Callable,
|
|
404
|
+
*,
|
|
405
|
+
dim: int,
|
|
406
|
+
angle_index: int,
|
|
407
|
+
output_index: int | None = None,
|
|
408
|
+
box: Any = None,
|
|
409
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
410
|
+
coupling_label: str = "g",
|
|
411
|
+
labels: Sequence[str] | None = None,
|
|
412
|
+
) -> Interactor:
|
|
413
|
+
r"""Build a rank-1 pair Interactor for orientation coupling.
|
|
414
|
+
|
|
415
|
+
Each feature computes
|
|
416
|
+
:math:`\phi_\alpha(r_{ij})\,g(\theta_j - \theta_i)` and embeds the
|
|
417
|
+
result along ``output_index`` in a ``dim``-d output vector.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
kernels : list of (callable, str)
|
|
422
|
+
Radial weight functions (same format as other kernel factories).
|
|
423
|
+
coupling_fn : callable
|
|
424
|
+
Scalar function of the angle difference, e.g. ``jnp.sin`` for
|
|
425
|
+
alignment, ``lambda a: jnp.cos(2*a)`` for nematic coupling.
|
|
426
|
+
dim : int
|
|
427
|
+
Full state-vector dimension.
|
|
428
|
+
angle_index : int
|
|
429
|
+
Index of the angle coordinate in the state vector.
|
|
430
|
+
output_index : int, optional
|
|
431
|
+
Index along which the coupled output is placed. Defaults to
|
|
432
|
+
``angle_index``.
|
|
433
|
+
box, spatial_dims
|
|
434
|
+
PBC and spatial-dimension controls (same as :func:`radial_pair_basis`).
|
|
435
|
+
coupling_label : str
|
|
436
|
+
Short label for the coupling (appears in feature labels).
|
|
437
|
+
labels : list of str, optional
|
|
438
|
+
Override labels.
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
Interactor
|
|
443
|
+
Rank-1 (vector) interactor with ``K=2``.
|
|
444
|
+
"""
|
|
445
|
+
n_feat = len(kernels)
|
|
446
|
+
if output_index is None:
|
|
447
|
+
output_index = angle_index
|
|
448
|
+
if labels is None:
|
|
449
|
+
labels = [f"{coupling_label}·{lab}" for _, lab in kernels]
|
|
450
|
+
fns = [fn for fn, _ in kernels]
|
|
451
|
+
|
|
452
|
+
_use_extras_box = box == "extras"
|
|
453
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
454
|
+
|
|
455
|
+
def _pair_local(XK, *, extras=None):
|
|
456
|
+
xi, xj = XK[0], XK[1]
|
|
457
|
+
# distance
|
|
458
|
+
b = _box
|
|
459
|
+
if b is None and _use_extras_box and extras is not None:
|
|
460
|
+
b = extras["box"]
|
|
461
|
+
if spatial_dims is not None:
|
|
462
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
463
|
+
_, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
464
|
+
# angle coupling
|
|
465
|
+
dth = wrap_angle(xj[angle_index] - xi[angle_index])
|
|
466
|
+
g = coupling_fn(dth)
|
|
467
|
+
vals = jnp.stack([fn(r) * g for fn in fns]) # (F,)
|
|
468
|
+
# embed along output_index
|
|
469
|
+
out = jnp.zeros((dim, n_feat), dtype=XK.dtype)
|
|
470
|
+
out = out.at[output_index, :].set(vals)
|
|
471
|
+
return out # (dim, F)
|
|
472
|
+
|
|
473
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
474
|
+
return make_interactor(
|
|
475
|
+
_pair_local,
|
|
476
|
+
dim=dim,
|
|
477
|
+
rank=1,
|
|
478
|
+
K=2,
|
|
479
|
+
n_features=n_feat,
|
|
480
|
+
extras_keys=extras_keys,
|
|
481
|
+
labels=list(labels),
|
|
482
|
+
descriptor="angular-pair-basis",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
487
|
+
# SINGLE-PARTICLE: HEADING VECTOR
|
|
488
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def heading_vector(dim: int, angle_index: int, *, spatial_axes: tuple[int, ...] | None = None) -> Basis:
|
|
492
|
+
r"""Single-particle heading vector from an angle coordinate.
|
|
493
|
+
|
|
494
|
+
Returns a rank-1 Basis whose single feature is the unit vector
|
|
495
|
+
:math:`(\cos\theta, \sin\theta)` embedded in a ``dim``-d vector,
|
|
496
|
+
with the cosine and sine placed at ``spatial_axes[0]`` and
|
|
497
|
+
``spatial_axes[1]`` respectively.
|
|
498
|
+
|
|
499
|
+
Parameters
|
|
500
|
+
----------
|
|
501
|
+
dim : int
|
|
502
|
+
State-vector dimension.
|
|
503
|
+
angle_index : int
|
|
504
|
+
Index of the angle coordinate :math:`\theta`.
|
|
505
|
+
spatial_axes : (int, int), optional
|
|
506
|
+
Indices for (cos θ, sin θ) in the output.
|
|
507
|
+
Default: ``(0, 1)`` — i.e. the first two axes.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
Basis
|
|
512
|
+
Rank-1, 1-feature heading-vector basis.
|
|
513
|
+
"""
|
|
514
|
+
if spatial_axes is None:
|
|
515
|
+
spatial_axes = (0, 1)
|
|
516
|
+
ax_cos, ax_sin = spatial_axes[0], spatial_axes[1]
|
|
517
|
+
|
|
518
|
+
def _f(x, *, mask=None):
|
|
519
|
+
th = x[angle_index]
|
|
520
|
+
out = jnp.zeros(dim, dtype=x.dtype)
|
|
521
|
+
out = out.at[ax_cos].set(jnp.cos(th))
|
|
522
|
+
out = out.at[ax_sin].set(jnp.sin(th))
|
|
523
|
+
return out[:, None] # (dim, 1) — rank-1, 1 feature
|
|
524
|
+
|
|
525
|
+
return make_basis(
|
|
526
|
+
_f,
|
|
527
|
+
dim=dim,
|
|
528
|
+
rank=1,
|
|
529
|
+
n_features=1,
|
|
530
|
+
labels=("e_heading",),
|
|
531
|
+
descriptor="heading-vector",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
536
|
+
# TENSOR PAIR FEATURES
|
|
537
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def dyadic_pair_basis(
|
|
541
|
+
kernels: Sequence[tuple[Callable, str]],
|
|
542
|
+
*,
|
|
543
|
+
dim: int,
|
|
544
|
+
box: Any = None,
|
|
545
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
546
|
+
labels: Sequence[str] | None = None,
|
|
547
|
+
) -> Interactor:
|
|
548
|
+
r"""Build a rank-2 (tensor) pair Interactor: :math:`\phi(r)\,\hat{r}\otimes\hat{r}`.
|
|
549
|
+
|
|
550
|
+
Each feature is the outer product of the unit displacement vector
|
|
551
|
+
with itself, weighted by a radial kernel. Useful for directional
|
|
552
|
+
diffusion tensors, nematic order parameters, etc.
|
|
553
|
+
|
|
554
|
+
Parameters
|
|
555
|
+
----------
|
|
556
|
+
kernels, dim, box, spatial_dims, labels
|
|
557
|
+
Same as :func:`radial_pair_basis`.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
Interactor
|
|
562
|
+
Rank-2 (matrix) interactor with ``K=2``.
|
|
563
|
+
"""
|
|
564
|
+
n_feat = len(kernels)
|
|
565
|
+
if labels is None:
|
|
566
|
+
labels = [f"rr·{lab}" for _, lab in kernels]
|
|
567
|
+
fns = [fn for fn, _ in kernels]
|
|
568
|
+
|
|
569
|
+
_use_extras_box = box == "extras"
|
|
570
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
571
|
+
|
|
572
|
+
def _pair_local(XK, *, extras=None):
|
|
573
|
+
b = _box
|
|
574
|
+
if b is None and _use_extras_box and extras is not None:
|
|
575
|
+
b = extras["box"]
|
|
576
|
+
if spatial_dims is not None:
|
|
577
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
578
|
+
_, r, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
579
|
+
vals = jnp.stack([fn(r) for fn in fns]) # (F,)
|
|
580
|
+
# rhat ⊗ rhat: (d, d) then weight by each kernel → (d, d, F)
|
|
581
|
+
rr = jnp.outer(rhat, rhat) # (d, d)
|
|
582
|
+
return rr[:, :, None] * vals[None, None, :] # (d, d, F)
|
|
583
|
+
|
|
584
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
585
|
+
return make_interactor(
|
|
586
|
+
_pair_local,
|
|
587
|
+
dim=dim,
|
|
588
|
+
rank=2,
|
|
589
|
+
K=2,
|
|
590
|
+
n_features=n_feat,
|
|
591
|
+
extras_keys=extras_keys,
|
|
592
|
+
labels=list(labels),
|
|
593
|
+
descriptor="dyadic-pair-basis",
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
598
|
+
# COMPOSABLE GEOMETRIC PRIMITIVES
|
|
599
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
600
|
+
#
|
|
601
|
+
# Single-feature Interactors designed for composition via ``*``
|
|
602
|
+
# (element-wise spatial multiplication with feature Cartesian product).
|
|
603
|
+
# Combine a direction (rank-1), a scalar gate (rank-0), and a
|
|
604
|
+
# parametric radial kernel (rank-0) to build rich pair forces.
|
|
605
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def pair_direction(
|
|
609
|
+
*,
|
|
610
|
+
dim: int,
|
|
611
|
+
box: Any = None,
|
|
612
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
613
|
+
embed_dim: int | None = None,
|
|
614
|
+
embed_axes: Sequence[int] | None = None,
|
|
615
|
+
) -> Interactor:
|
|
616
|
+
r"""Unit displacement vector :math:`\hat{r}_{ij}` as a rank-1 Interactor.
|
|
617
|
+
|
|
618
|
+
Returns a single-feature, rank-1 pair Interactor whose output is the
|
|
619
|
+
unit vector pointing from particle *i* to particle *j*.
|
|
620
|
+
|
|
621
|
+
Parameters
|
|
622
|
+
----------
|
|
623
|
+
dim : int
|
|
624
|
+
Full state-vector dimension per particle.
|
|
625
|
+
box : array, ``"extras"``, or None
|
|
626
|
+
PBC box lengths (same semantics as :func:`radial_pair_basis`).
|
|
627
|
+
spatial_dims : slice or index array, optional
|
|
628
|
+
Which axes are spatial coordinates.
|
|
629
|
+
embed_dim : int, optional
|
|
630
|
+
Embed into a larger output vector (e.g. 2-D displacement in 3-D state).
|
|
631
|
+
embed_axes : sequence of int, optional
|
|
632
|
+
Indices for embedding (required when ``embed_dim`` is set).
|
|
633
|
+
|
|
634
|
+
Returns
|
|
635
|
+
-------
|
|
636
|
+
Interactor
|
|
637
|
+
Rank-1, 1-feature interactor with ``K=2``.
|
|
638
|
+
"""
|
|
639
|
+
if embed_dim is not None:
|
|
640
|
+
out_d = embed_dim
|
|
641
|
+
if embed_axes is None:
|
|
642
|
+
raise ValueError("embed_axes required when embed_dim is set")
|
|
643
|
+
else:
|
|
644
|
+
out_d = (
|
|
645
|
+
dim
|
|
646
|
+
if spatial_dims is None
|
|
647
|
+
else (len(range(*spatial_dims.indices(dim))) if isinstance(spatial_dims, slice) else len(spatial_dims))
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
_use_extras_box = box == "extras"
|
|
651
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
652
|
+
|
|
653
|
+
def _pair_local(XK, *, extras=None):
|
|
654
|
+
b = _box
|
|
655
|
+
if b is None and _use_extras_box and extras is not None:
|
|
656
|
+
b = extras["box"]
|
|
657
|
+
if spatial_dims is not None:
|
|
658
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
659
|
+
_, _, rhat = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
660
|
+
if embed_dim is not None:
|
|
661
|
+
full = jnp.zeros(out_d, dtype=XK.dtype)
|
|
662
|
+
for k, ax in enumerate(embed_axes):
|
|
663
|
+
full = full.at[ax].set(rhat[k])
|
|
664
|
+
return full[:, None] # (out_d, 1)
|
|
665
|
+
return rhat[:, None] # (d_spatial, 1)
|
|
666
|
+
|
|
667
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
668
|
+
return make_interactor(
|
|
669
|
+
_pair_local,
|
|
670
|
+
dim=dim,
|
|
671
|
+
rank=1,
|
|
672
|
+
K=2,
|
|
673
|
+
n_features=1,
|
|
674
|
+
extras_keys=extras_keys,
|
|
675
|
+
labels=("r̂_ij",),
|
|
676
|
+
descriptor="pair-direction",
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def angle_coupling(
|
|
681
|
+
coupling_fn: Callable,
|
|
682
|
+
*,
|
|
683
|
+
dim: int,
|
|
684
|
+
angle_index: int,
|
|
685
|
+
output_index: int | None = None,
|
|
686
|
+
label: str = "g",
|
|
687
|
+
) -> Interactor:
|
|
688
|
+
r"""Scalar orientation coupling embedded as a rank-1 Interactor.
|
|
689
|
+
|
|
690
|
+
Computes ``coupling_fn(θ_j − θ_i)`` and places the result along
|
|
691
|
+
``output_index`` in a ``dim``-d output vector.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
coupling_fn : callable
|
|
696
|
+
Scalar function of the wrapped angle difference, e.g. ``jnp.sin``.
|
|
697
|
+
dim : int
|
|
698
|
+
Full state-vector dimension.
|
|
699
|
+
angle_index : int
|
|
700
|
+
Index of the angle coordinate in the state vector.
|
|
701
|
+
output_index : int, optional
|
|
702
|
+
Output axis for the coupling value. Defaults to ``angle_index``.
|
|
703
|
+
label : str
|
|
704
|
+
Short label used in feature names.
|
|
705
|
+
|
|
706
|
+
Returns
|
|
707
|
+
-------
|
|
708
|
+
Interactor
|
|
709
|
+
Rank-1, 1-feature interactor with ``K=2``.
|
|
710
|
+
"""
|
|
711
|
+
if output_index is None:
|
|
712
|
+
output_index = angle_index
|
|
713
|
+
|
|
714
|
+
def _pair_local(XK, *, extras=None):
|
|
715
|
+
xi, xj = XK[0], XK[1]
|
|
716
|
+
dth = wrap_angle(xj[angle_index] - xi[angle_index])
|
|
717
|
+
g = coupling_fn(dth)
|
|
718
|
+
out = jnp.zeros(dim, dtype=XK.dtype)
|
|
719
|
+
out = out.at[output_index].set(g)
|
|
720
|
+
return out[:, None] # (dim, 1)
|
|
721
|
+
|
|
722
|
+
return make_interactor(
|
|
723
|
+
_pair_local,
|
|
724
|
+
dim=dim,
|
|
725
|
+
rank=1,
|
|
726
|
+
K=2,
|
|
727
|
+
n_features=1,
|
|
728
|
+
labels=(label,),
|
|
729
|
+
descriptor="angle-coupling",
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def particle_heading(
|
|
734
|
+
which: int,
|
|
735
|
+
*,
|
|
736
|
+
dim: int,
|
|
737
|
+
angle_index: int,
|
|
738
|
+
spatial_axes: tuple[int, ...] | None = None,
|
|
739
|
+
) -> Interactor:
|
|
740
|
+
r"""Heading vector of one particle in a pair, as a rank-1 Interactor.
|
|
741
|
+
|
|
742
|
+
Returns :math:`(\cos\theta, \sin\theta)` of the selected particle
|
|
743
|
+
(``which=0`` for the focal particle, ``which=1`` for the neighbor),
|
|
744
|
+
embedded in a ``dim``-d output vector.
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
which : int
|
|
749
|
+
``0`` for the focal particle, ``1`` for the neighbor.
|
|
750
|
+
dim : int
|
|
751
|
+
Full state-vector dimension.
|
|
752
|
+
angle_index : int
|
|
753
|
+
Index of the angle coordinate.
|
|
754
|
+
spatial_axes : (int, int), optional
|
|
755
|
+
Indices for (cos θ, sin θ) in the output. Default: ``(0, 1)``.
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
-------
|
|
759
|
+
Interactor
|
|
760
|
+
Rank-1, 1-feature interactor with ``K=2``.
|
|
761
|
+
"""
|
|
762
|
+
if spatial_axes is None:
|
|
763
|
+
spatial_axes = (0, 1)
|
|
764
|
+
ax_cos, ax_sin = spatial_axes[0], spatial_axes[1]
|
|
765
|
+
|
|
766
|
+
def _pair_local(XK, *, extras=None):
|
|
767
|
+
th = XK[which][angle_index]
|
|
768
|
+
out = jnp.zeros(dim, dtype=XK.dtype)
|
|
769
|
+
out = out.at[ax_cos].set(jnp.cos(th))
|
|
770
|
+
out = out.at[ax_sin].set(jnp.sin(th))
|
|
771
|
+
return out[:, None] # (dim, 1)
|
|
772
|
+
|
|
773
|
+
return make_interactor(
|
|
774
|
+
_pair_local,
|
|
775
|
+
dim=dim,
|
|
776
|
+
rank=1,
|
|
777
|
+
K=2,
|
|
778
|
+
n_features=1,
|
|
779
|
+
labels=(f"ê_θ[{which}]",),
|
|
780
|
+
descriptor="particle-heading",
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def vision_gate(
|
|
785
|
+
gate_fn: Callable,
|
|
786
|
+
*,
|
|
787
|
+
dim: int,
|
|
788
|
+
angle_index: int,
|
|
789
|
+
box: Any = None,
|
|
790
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
791
|
+
) -> Interactor:
|
|
792
|
+
r"""Scalar vision-cone gate as a rank-0 Interactor.
|
|
793
|
+
|
|
794
|
+
Computes the bearing angle :math:`\delta` from the focal particle's
|
|
795
|
+
heading to the displacement toward the neighbor, then returns
|
|
796
|
+
``gate_fn(δ)``. This makes interactions *nonreciprocal*: the gate
|
|
797
|
+
value depends on whether the neighbor is "in front" or "behind" the
|
|
798
|
+
focal particle.
|
|
799
|
+
|
|
800
|
+
Parameters
|
|
801
|
+
----------
|
|
802
|
+
gate_fn : callable
|
|
803
|
+
Scalar function of the bearing angle, e.g.
|
|
804
|
+
``lambda d: (1 + jnp.cos(d)) / 2`` for a cosine vision cone.
|
|
805
|
+
dim : int
|
|
806
|
+
Full state-vector dimension.
|
|
807
|
+
angle_index : int
|
|
808
|
+
Index of the angle coordinate.
|
|
809
|
+
box : array, ``"extras"``, or None
|
|
810
|
+
PBC box (same semantics as :func:`radial_pair_basis`).
|
|
811
|
+
spatial_dims : slice or index array, optional
|
|
812
|
+
Which axes are spatial coordinates (for the displacement).
|
|
813
|
+
Must select exactly 2 dimensions — bearing angle is 2-D only.
|
|
814
|
+
|
|
815
|
+
Returns
|
|
816
|
+
-------
|
|
817
|
+
Interactor
|
|
818
|
+
Rank-0, 1-feature interactor with ``K=2``.
|
|
819
|
+
"""
|
|
820
|
+
_use_extras_box = box == "extras"
|
|
821
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
822
|
+
|
|
823
|
+
def _pair_local(XK, *, extras=None):
|
|
824
|
+
xi, _ = XK[0], XK[1]
|
|
825
|
+
# Spatial displacement
|
|
826
|
+
b = _box
|
|
827
|
+
if b is None and _use_extras_box and extras is not None:
|
|
828
|
+
b = extras["box"]
|
|
829
|
+
if spatial_dims is not None:
|
|
830
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
831
|
+
dx, _, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
832
|
+
if dx.shape[0] != 2:
|
|
833
|
+
raise ValueError(
|
|
834
|
+
f"vision_gate requires exactly 2 spatial dimensions, got {dx.shape[0]}. "
|
|
835
|
+
"Use spatial_dims to select 2 axes from a higher-dimensional state vector."
|
|
836
|
+
)
|
|
837
|
+
# Bearing angle: direction of neighbour relative to heading
|
|
838
|
+
phi_ij = jnp.arctan2(dx[1], dx[0])
|
|
839
|
+
theta_i = xi[angle_index]
|
|
840
|
+
delta = wrap_angle(phi_ij - theta_i)
|
|
841
|
+
return gate_fn(delta)[None] # (1,) — rank-0, 1 feature
|
|
842
|
+
|
|
843
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
844
|
+
return make_interactor(
|
|
845
|
+
_pair_local,
|
|
846
|
+
dim=dim,
|
|
847
|
+
rank=0,
|
|
848
|
+
K=2,
|
|
849
|
+
n_features=1,
|
|
850
|
+
extras_keys=extras_keys,
|
|
851
|
+
labels=("vision",),
|
|
852
|
+
descriptor="vision-gate",
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def parametric_radial_kernel(
|
|
857
|
+
kernel_fn: Callable,
|
|
858
|
+
*,
|
|
859
|
+
params: dict,
|
|
860
|
+
dim: int,
|
|
861
|
+
box: Any = None,
|
|
862
|
+
spatial_dims: slice | Sequence[int] | None = None,
|
|
863
|
+
) -> Interactor:
|
|
864
|
+
r"""Parametric scalar radial kernel as a rank-0 Interactor.
|
|
865
|
+
|
|
866
|
+
Wraps a user-supplied function ``kernel_fn(r, params)`` into a
|
|
867
|
+
rank-0 pair Interactor with learnable parameters.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
kernel_fn : callable
|
|
872
|
+
``kernel_fn(r, params) -> scalar`` where *r* is the inter-particle
|
|
873
|
+
distance and *params* is a dict of JAX arrays.
|
|
874
|
+
params : dict
|
|
875
|
+
Parameter specification passed to :func:`make_interactor`, e.g.
|
|
876
|
+
``{"eps": (), "R0": ()}`` for two scalar parameters.
|
|
877
|
+
dim : int
|
|
878
|
+
Full state-vector dimension.
|
|
879
|
+
box : array, ``"extras"``, or None
|
|
880
|
+
PBC box (same semantics as :func:`radial_pair_basis`).
|
|
881
|
+
spatial_dims : slice or index array, optional
|
|
882
|
+
Which axes are spatial coordinates.
|
|
883
|
+
|
|
884
|
+
Returns
|
|
885
|
+
-------
|
|
886
|
+
Interactor
|
|
887
|
+
Rank-0, 1-feature parametric interactor with ``K=2``.
|
|
888
|
+
"""
|
|
889
|
+
_use_extras_box = box == "extras"
|
|
890
|
+
_box = None if (box is None or _use_extras_box) else jnp.asarray(box)
|
|
891
|
+
|
|
892
|
+
def _pair_local(XK, *, params=None, extras=None):
|
|
893
|
+
b = _box
|
|
894
|
+
if b is None and _use_extras_box and extras is not None:
|
|
895
|
+
b = extras["box"]
|
|
896
|
+
if spatial_dims is not None:
|
|
897
|
+
b = b[spatial_dims] if b.ndim > 0 else b
|
|
898
|
+
_, r, _ = _pairwise_dr(XK, box=b, spatial_dims=spatial_dims)
|
|
899
|
+
return kernel_fn(r, params)[None] # (1,) — rank-0, 1 feature
|
|
900
|
+
|
|
901
|
+
extras_keys = ("box",) if _use_extras_box else ()
|
|
902
|
+
return make_interactor(
|
|
903
|
+
_pair_local,
|
|
904
|
+
dim=dim,
|
|
905
|
+
rank=0,
|
|
906
|
+
K=2,
|
|
907
|
+
n_features=1,
|
|
908
|
+
params=params,
|
|
909
|
+
extras_keys=extras_keys,
|
|
910
|
+
labels=("k(r)",),
|
|
911
|
+
descriptor="parametric-radial-kernel",
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
916
|
+
# VELOCITY-DEPENDENT PAIR BASES
|
|
917
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def pair_velocity_difference(
|
|
921
|
+
*,
|
|
922
|
+
dim: int,
|
|
923
|
+
) -> Interactor:
|
|
924
|
+
r"""Velocity difference :math:`\mathbf{v}_j - \mathbf{v}_i` as a rank-1 Interactor.
|
|
925
|
+
|
|
926
|
+
Returns a single-feature, rank-1 pair Interactor whose output is the
|
|
927
|
+
velocity difference between neighbor and focal particle. Designed
|
|
928
|
+
for composition with scalar pair Interactors via the ``*`` operator,
|
|
929
|
+
e.g.::
|
|
930
|
+
|
|
931
|
+
scalar_pair_basis(kernels, dim=d) * pair_velocity_difference(dim=d)
|
|
932
|
+
|
|
933
|
+
Parameters
|
|
934
|
+
----------
|
|
935
|
+
dim : int
|
|
936
|
+
State-vector dimension per particle.
|
|
937
|
+
|
|
938
|
+
Returns
|
|
939
|
+
-------
|
|
940
|
+
Interactor
|
|
941
|
+
Rank-1, 1-feature interactor with ``K=2``, ``needs_v=True``.
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
def _pair_local(XK, *, v, extras=None):
|
|
945
|
+
dv = v[1] - v[0] # (dim,)
|
|
946
|
+
return dv[:, None] # (dim, 1)
|
|
947
|
+
|
|
948
|
+
return make_interactor(
|
|
949
|
+
_pair_local,
|
|
950
|
+
dim=dim,
|
|
951
|
+
rank=1,
|
|
952
|
+
K=2,
|
|
953
|
+
n_features=1,
|
|
954
|
+
needs_v=True,
|
|
955
|
+
labels=("Δv_ij",),
|
|
956
|
+
descriptor="pair-velocity-difference",
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def particle_velocity(
|
|
961
|
+
which: int,
|
|
962
|
+
*,
|
|
963
|
+
dim: int,
|
|
964
|
+
) -> Interactor:
|
|
965
|
+
r"""Velocity of one particle in a pair, as a rank-1 Interactor.
|
|
966
|
+
|
|
967
|
+
Returns the velocity of either the focal particle (``which=0``) or
|
|
968
|
+
the neighbor (``which=1``) as a rank-1 Interactor. Designed for
|
|
969
|
+
composition with scalar pair Interactors via the ``*`` operator, e.g.::
|
|
970
|
+
|
|
971
|
+
scalar_pair_basis(kernels, dim=d) * particle_velocity(which=1, dim=d)
|
|
972
|
+
|
|
973
|
+
Parameters
|
|
974
|
+
----------
|
|
975
|
+
which : int
|
|
976
|
+
``0`` for the focal particle's velocity, ``1`` for the neighbor's.
|
|
977
|
+
dim : int
|
|
978
|
+
State-vector dimension per particle.
|
|
979
|
+
|
|
980
|
+
Returns
|
|
981
|
+
-------
|
|
982
|
+
Interactor
|
|
983
|
+
Rank-1, 1-feature interactor with ``K=2``, ``needs_v=True``.
|
|
984
|
+
"""
|
|
985
|
+
|
|
986
|
+
def _pair_local(XK, *, v, extras=None):
|
|
987
|
+
return v[which][:, None] # (dim, 1)
|
|
988
|
+
|
|
989
|
+
return make_interactor(
|
|
990
|
+
_pair_local,
|
|
991
|
+
dim=dim,
|
|
992
|
+
rank=1,
|
|
993
|
+
K=2,
|
|
994
|
+
n_features=1,
|
|
995
|
+
needs_v=True,
|
|
996
|
+
labels=(f"v[{which}]",),
|
|
997
|
+
descriptor="particle-velocity",
|
|
998
|
+
)
|