drbx 2.0.0.dev0__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.
- drbx/__init__.py +20 -0
- drbx/__main__.py +5 -0
- drbx/cli.py +586 -0
- drbx/config/__init__.py +20 -0
- drbx/config/boutinp.py +556 -0
- drbx/config/model.py +34 -0
- drbx/config/normalization.py +66 -0
- drbx/data/atomic_rates/__init__.py +1 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
- drbx/geometry/__init__.py +207 -0
- drbx/geometry/embedding.py +56 -0
- drbx/geometry/essos_import.py +1385 -0
- drbx/geometry/fci_geometry.py +4622 -0
- drbx/geometry/fci_maps.py +85 -0
- drbx/geometry/island_divertor.py +291 -0
- drbx/geometry/metric_tensor.py +99 -0
- drbx/geometry/open_slab.py +150 -0
- drbx/geometry/rotating_ellipse.py +253 -0
- drbx/geometry/shifted_torus.py +225 -0
- drbx/geometry/stellarator.py +287 -0
- drbx/geometry/vmec_extender_import.py +499 -0
- drbx/geometry/vmec_jax_import.py +306 -0
- drbx/linear/__init__.py +37 -0
- drbx/linear/dispersion.py +138 -0
- drbx/linear/eigen.py +91 -0
- drbx/native/__init__.py +224 -0
- drbx/native/array_backend.py +64 -0
- drbx/native/deck_runner.py +779 -0
- drbx/native/electromagnetic.py +250 -0
- drbx/native/expression.py +173 -0
- drbx/native/fci.py +295 -0
- drbx/native/fci_2_field_rhs.py +182 -0
- drbx/native/fci_4_field_rhs.py +1267 -0
- drbx/native/fci_boundaries.py +2494 -0
- drbx/native/fci_differentiable_case.py +304 -0
- drbx/native/fci_drb_EB_rhs.py +1243 -0
- drbx/native/fci_drb_rhs.py +190 -0
- drbx/native/fci_halo.py +1575 -0
- drbx/native/fci_helpers.py +350 -0
- drbx/native/fci_model.py +294 -0
- drbx/native/fci_neutral.py +139 -0
- drbx/native/fci_operators.py +4081 -0
- drbx/native/fci_sharding.py +597 -0
- drbx/native/fci_sheath_recycling.py +206 -0
- drbx/native/fci_time_integrator.py +96 -0
- drbx/native/fci_vorticity.py +198 -0
- drbx/native/fluid_1d.py +330 -0
- drbx/native/hasegawa_wakatani.py +196 -0
- drbx/native/limiters.py +57 -0
- drbx/native/mesh.py +238 -0
- drbx/native/metrics.py +234 -0
- drbx/native/neutrals/__init__.py +58 -0
- drbx/native/neutrals/atomic_rates.py +134 -0
- drbx/native/neutrals/detachment_sol_model.py +221 -0
- drbx/native/neutrals/reactions.py +164 -0
- drbx/native/neutrals/recycling_sol_model.py +197 -0
- drbx/native/sol_flux_tube.py +133 -0
- drbx/native/stellarator_turbulence.py +343 -0
- drbx/native/transport.py +134 -0
- drbx/native/units.py +32 -0
- drbx/native/vorticity.py +252 -0
- drbx/runtime/__init__.py +53 -0
- drbx/runtime/artifacts.py +161 -0
- drbx/runtime/memory.py +144 -0
- drbx/runtime/output.py +374 -0
- drbx/runtime/paths.py +9 -0
- drbx/runtime/performance.py +161 -0
- drbx/runtime/run_config.py +184 -0
- drbx/runtime/scheduler.py +99 -0
- drbx/runtime/state.py +40 -0
- drbx/validation/__init__.py +424 -0
- drbx/validation/autodiff_diffusion.py +329 -0
- drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
- drbx/validation/diverted_tokamak_movie.py +558 -0
- drbx/validation/essos_fieldline_import_campaign.py +181 -0
- drbx/validation/essos_imported_artifact_audit.py +535 -0
- drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
- drbx/validation/essos_imported_fci_campaign.py +5241 -0
- drbx/validation/essos_imported_pytree_campaign.py +406 -0
- drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
- drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
- drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
- drbx/validation/fluid_1d_mms_convergence.py +250 -0
- drbx/validation/geometry_lineouts.py +136 -0
- drbx/validation/geometry_slices.py +178 -0
- drbx/validation/publication_plotting.py +91 -0
- drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
- drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
- drbx/validation/stellarator_fci_operator_campaign.py +304 -0
- drbx/validation/stellarator_fci_suite_campaign.py +264 -0
- drbx/validation/stellarator_metric_mms_campaign.py +289 -0
- drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
- drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
- drbx/validation/stellarator_sol_showcase.py +628 -0
- drbx/validation/stellarator_vorticity_campaign.py +304 -0
- drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
- drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
- drbx-2.0.0.dev0.dist-info/METADATA +380 -0
- drbx-2.0.0.dev0.dist-info/RECORD +106 -0
- drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
- drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,4622 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, fields
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Callable
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
from jax import lax
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_pytree_base = jax.tree_util.register_pytree_node_class
|
|
12
|
+
|
|
13
|
+
def _normalize_same_shape_fields(instance, field_names: tuple[str, ...], *, expected_shape: tuple[int, ...], label: str) -> None:
|
|
14
|
+
for name in field_names:
|
|
15
|
+
value = jnp.asarray(getattr(instance, name), dtype=jnp.float64)
|
|
16
|
+
if value.shape != expected_shape:
|
|
17
|
+
raise ValueError(f"{label}.{name} must have shape {expected_shape}, got {value.shape}")
|
|
18
|
+
object.__setattr__(instance, name, value)
|
|
19
|
+
|
|
20
|
+
class _DataclassPyTreeMixin:
|
|
21
|
+
"""Generic PyTree support for frozen dataclasses.
|
|
22
|
+
All dataclass fields with init=True are treated as dynamic PyTree children.
|
|
23
|
+
Computed fields with init=False are rebuilt in __post_init__.
|
|
24
|
+
"""
|
|
25
|
+
def tree_flatten(self):
|
|
26
|
+
children = tuple(getattr(self, f.name) for f in fields(self) if f.init)
|
|
27
|
+
return children, None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def tree_unflatten(cls, aux_data, children):
|
|
31
|
+
init_names = [f.name for f in fields(cls) if f.init]
|
|
32
|
+
return cls(**dict(zip(init_names, children)))
|
|
33
|
+
|
|
34
|
+
def _as_float_array(value, name: str):
|
|
35
|
+
return jnp.asarray(value, dtype=jnp.float64)
|
|
36
|
+
|
|
37
|
+
def _require_shape(value, expected_shape: tuple[int, ...], name: str):
|
|
38
|
+
arr = jnp.asarray(value)
|
|
39
|
+
if arr.shape != expected_shape:
|
|
40
|
+
raise ValueError(f"{name} must have shape {expected_shape}, got {arr.shape}")
|
|
41
|
+
return arr
|
|
42
|
+
|
|
43
|
+
def _require_float_shape(value, expected_shape: tuple[int, ...], name: str):
|
|
44
|
+
arr = jnp.asarray(value, dtype=jnp.float64)
|
|
45
|
+
if arr.shape != expected_shape:
|
|
46
|
+
raise ValueError(f"{name} must have shape {expected_shape}, got {arr.shape}")
|
|
47
|
+
return arr
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize_periodic_axes(
|
|
51
|
+
periodic_axes: tuple[bool | None, bool | None, bool | None] | None,
|
|
52
|
+
*,
|
|
53
|
+
default: tuple[bool, bool, bool] = (False, True, True),
|
|
54
|
+
) -> tuple[bool, bool, bool]:
|
|
55
|
+
if periodic_axes is None:
|
|
56
|
+
periodic_axes = default
|
|
57
|
+
if len(periodic_axes) != 3:
|
|
58
|
+
raise ValueError(f"periodic_axes must have length 3, got {periodic_axes}")
|
|
59
|
+
return tuple(False if axis is None else bool(axis) for axis in periodic_axes)
|
|
60
|
+
|
|
61
|
+
def _metric_from_components(
|
|
62
|
+
g11: jnp.ndarray,
|
|
63
|
+
g22: jnp.ndarray,
|
|
64
|
+
g33: jnp.ndarray,
|
|
65
|
+
g12: jnp.ndarray,
|
|
66
|
+
g13: jnp.ndarray,
|
|
67
|
+
g23: jnp.ndarray,
|
|
68
|
+
) -> jnp.ndarray:
|
|
69
|
+
return jnp.stack(
|
|
70
|
+
[
|
|
71
|
+
jnp.stack([g11, g12, g13], axis=-1),
|
|
72
|
+
jnp.stack([g12, g22, g23], axis=-1),
|
|
73
|
+
jnp.stack([g13, g23, g33], axis=-1),
|
|
74
|
+
],
|
|
75
|
+
axis=-2,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _bmag_from_contravariant_components(
|
|
79
|
+
B_contra: jnp.ndarray,
|
|
80
|
+
g_cov: jnp.ndarray,
|
|
81
|
+
) -> jnp.ndarray:
|
|
82
|
+
bmag_sq = jnp.einsum("...i,...ij,...j->...", B_contra, g_cov, B_contra)
|
|
83
|
+
return jnp.sqrt(jnp.maximum(bmag_sq, 0.0))
|
|
84
|
+
|
|
85
|
+
def logical_grid_from_axis_vectors(
|
|
86
|
+
x_axis: jnp.ndarray,
|
|
87
|
+
y_axis: jnp.ndarray,
|
|
88
|
+
z_axis: jnp.ndarray,
|
|
89
|
+
) -> jnp.ndarray:
|
|
90
|
+
x = jnp.asarray(x_axis, dtype=jnp.float64)
|
|
91
|
+
y = jnp.asarray(y_axis, dtype=jnp.float64)
|
|
92
|
+
z = jnp.asarray(z_axis, dtype=jnp.float64)
|
|
93
|
+
xx = jnp.broadcast_to(x[:, None, None], (x.size, y.size, z.size))
|
|
94
|
+
yy = jnp.broadcast_to(y[None, :, None], (x.size, y.size, z.size))
|
|
95
|
+
zz = jnp.broadcast_to(z[None, None, :], (x.size, y.size, z.size))
|
|
96
|
+
return jnp.stack((xx, yy, zz), axis=-1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@_pytree_base
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class Grid1D(_DataclassPyTreeMixin):
|
|
102
|
+
centers: jnp.ndarray # (n,)
|
|
103
|
+
faces: jnp.ndarray # (n + 1,)
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
centers = _as_float_array(self.centers, "centers")
|
|
106
|
+
faces = _as_float_array(self.faces, "faces")
|
|
107
|
+
if centers.ndim != 1:
|
|
108
|
+
raise ValueError(f"centers must be one-dimensional, got {centers.shape}")
|
|
109
|
+
if faces.ndim != 1:
|
|
110
|
+
raise ValueError(f"faces must be one-dimensional, got {faces.shape}")
|
|
111
|
+
if faces.size != centers.size + 1:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"faces must have length centers.size + 1; got centers={centers.shape}, faces={faces.shape}"
|
|
114
|
+
)
|
|
115
|
+
object.__setattr__(self, "centers", centers)
|
|
116
|
+
object.__setattr__(self, "faces", faces)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_centers(cls, centers: jnp.ndarray) -> "Grid1D":
|
|
120
|
+
centers = jnp.asarray(centers, dtype=jnp.float64)
|
|
121
|
+
if centers.ndim != 1:
|
|
122
|
+
raise ValueError(f"centers must be one-dimensional, got {centers.shape}")
|
|
123
|
+
if centers.size == 0:
|
|
124
|
+
raise ValueError("centers must contain at least one point")
|
|
125
|
+
if centers.size == 1:
|
|
126
|
+
spacing = jnp.asarray(1.0, dtype=jnp.float64)
|
|
127
|
+
faces = jnp.array([centers[0] - 0.5 * spacing, centers[0] + 0.5 * spacing], dtype=jnp.float64)
|
|
128
|
+
else:
|
|
129
|
+
faces = jnp.empty(centers.size + 1, dtype=jnp.float64)
|
|
130
|
+
faces = faces.at[1:-1].set(0.5 * (centers[:-1] + centers[1:]))
|
|
131
|
+
faces = faces.at[0].set(centers[0] - 0.5 * (centers[1] - centers[0]))
|
|
132
|
+
faces = faces.at[-1].set(centers[-1] + 0.5 * (centers[-1] - centers[-2]))
|
|
133
|
+
return cls(centers=centers, faces=faces)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def n(self) -> int:
|
|
137
|
+
return int(self.centers.size)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def widths(self) -> jnp.ndarray:
|
|
141
|
+
return self.faces[1:] - self.faces[:-1]
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def center_deltas(self) -> jnp.ndarray:
|
|
145
|
+
return self.centers[1:] - self.centers[:-1]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def lower_center_to_face(self):
|
|
149
|
+
return self.centers[0] - self.faces[0]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def upper_center_to_face(self):
|
|
153
|
+
return self.faces[-1] - self.centers[-1]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class HaloLayout3D:
|
|
158
|
+
"""Shared halo metadata for shard-local 3D geometry."""
|
|
159
|
+
|
|
160
|
+
owned_shape: tuple[int, int, int]
|
|
161
|
+
halo_width: int
|
|
162
|
+
|
|
163
|
+
def __post_init__(self) -> None:
|
|
164
|
+
owned_shape = tuple(int(v) for v in self.owned_shape)
|
|
165
|
+
if len(owned_shape) != 3:
|
|
166
|
+
raise ValueError(f"HaloLayout3D.owned_shape must have length 3, got {owned_shape}")
|
|
167
|
+
if any(size <= 0 for size in owned_shape):
|
|
168
|
+
raise ValueError(f"HaloLayout3D.owned_shape must contain positive integers, got {owned_shape}")
|
|
169
|
+
halo_width = int(self.halo_width)
|
|
170
|
+
if halo_width < 0:
|
|
171
|
+
raise ValueError(f"HaloLayout3D.halo_width must be non-negative, got {halo_width}")
|
|
172
|
+
object.__setattr__(self, "owned_shape", owned_shape)
|
|
173
|
+
object.__setattr__(self, "halo_width", halo_width)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def cell_halo_shape(self) -> tuple[int, int, int]:
|
|
177
|
+
h = self.halo_width
|
|
178
|
+
nx, ny, nz = self.owned_shape
|
|
179
|
+
return nx + 2 * h, ny + 2 * h, nz + 2 * h
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def owned_slices_cell(self) -> tuple[slice, slice, slice]:
|
|
183
|
+
h = self.halo_width
|
|
184
|
+
nx, ny, nz = self.owned_shape
|
|
185
|
+
return (
|
|
186
|
+
slice(h, h + nx),
|
|
187
|
+
slice(h, h + ny),
|
|
188
|
+
slice(h, h + nz),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def face_halo_shape(self, axis: int) -> tuple[int, int, int]:
|
|
192
|
+
axis = int(axis)
|
|
193
|
+
if axis < 0 or axis > 2:
|
|
194
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
195
|
+
shape = list(self.cell_halo_shape)
|
|
196
|
+
shape[axis] += 1
|
|
197
|
+
return tuple(shape)
|
|
198
|
+
|
|
199
|
+
def face_control_shape(self, axis: int) -> tuple[int, int, int]:
|
|
200
|
+
axis = int(axis)
|
|
201
|
+
if axis < 0 or axis > 2:
|
|
202
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
203
|
+
nx, ny, nz = self.owned_shape
|
|
204
|
+
shape = [nx, ny, nz]
|
|
205
|
+
shape[axis] += 1
|
|
206
|
+
return tuple(shape)
|
|
207
|
+
|
|
208
|
+
def face_control_slices(self, axis: int) -> tuple[slice, slice, slice]:
|
|
209
|
+
axis = int(axis)
|
|
210
|
+
if axis < 0 or axis > 2:
|
|
211
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
212
|
+
h = self.halo_width
|
|
213
|
+
nx, ny, nz = self.owned_shape
|
|
214
|
+
extents = [nx, ny, nz]
|
|
215
|
+
extents[axis] += 1
|
|
216
|
+
return tuple(slice(h, h + n) for n in extents)
|
|
217
|
+
|
|
218
|
+
def location_halo_shape(self, location: str) -> tuple[int, int, int]:
|
|
219
|
+
if location == "cell":
|
|
220
|
+
return self.cell_halo_shape
|
|
221
|
+
if location == "x_face":
|
|
222
|
+
return self.face_halo_shape(0)
|
|
223
|
+
if location == "y_face":
|
|
224
|
+
return self.face_halo_shape(1)
|
|
225
|
+
if location == "z_face":
|
|
226
|
+
return self.face_halo_shape(2)
|
|
227
|
+
raise ValueError(
|
|
228
|
+
'location must be one of "cell", "x_face", "y_face", or "z_face", '
|
|
229
|
+
f"got {location!r}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def location_owned_slices(self, location: str) -> tuple[slice, slice, slice]:
|
|
233
|
+
if location == "cell":
|
|
234
|
+
return self.owned_slices_cell
|
|
235
|
+
if location == "x_face":
|
|
236
|
+
return self.face_control_slices(0)
|
|
237
|
+
if location == "y_face":
|
|
238
|
+
return self.face_control_slices(1)
|
|
239
|
+
if location == "z_face":
|
|
240
|
+
return self.face_control_slices(2)
|
|
241
|
+
raise ValueError(
|
|
242
|
+
'location must be one of "cell", "x_face", "y_face", or "z_face", '
|
|
243
|
+
f"got {location!r}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def location_owned_shape(self, location: str) -> tuple[int, int, int]:
|
|
247
|
+
if location == "cell":
|
|
248
|
+
return self.owned_shape
|
|
249
|
+
if location == "x_face":
|
|
250
|
+
return self.face_control_shape(0)
|
|
251
|
+
if location == "y_face":
|
|
252
|
+
return self.face_control_shape(1)
|
|
253
|
+
if location == "z_face":
|
|
254
|
+
return self.face_control_shape(2)
|
|
255
|
+
raise ValueError(
|
|
256
|
+
'location must be one of "cell", "x_face", "y_face", or "z_face", '
|
|
257
|
+
f"got {location!r}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@_pytree_base
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class LocalGrid1D:
|
|
264
|
+
"""Shard-local 1D grid with owned and halo coordinate storage."""
|
|
265
|
+
|
|
266
|
+
layout: HaloLayout3D
|
|
267
|
+
axis: int
|
|
268
|
+
centers_halo: jnp.ndarray
|
|
269
|
+
faces_halo: jnp.ndarray
|
|
270
|
+
owned_start_global: int
|
|
271
|
+
owned_stop_global: int
|
|
272
|
+
|
|
273
|
+
def __post_init__(self) -> None:
|
|
274
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
275
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
276
|
+
axis = int(self.axis)
|
|
277
|
+
if axis < 0 or axis > 2:
|
|
278
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
279
|
+
centers_halo = _as_float_array(self.centers_halo, "centers_halo")
|
|
280
|
+
faces_halo = _as_float_array(self.faces_halo, "faces_halo")
|
|
281
|
+
if centers_halo.ndim != 1:
|
|
282
|
+
raise ValueError(f"centers_halo must be one-dimensional, got {centers_halo.shape}")
|
|
283
|
+
if faces_halo.ndim != 1:
|
|
284
|
+
raise ValueError(f"faces_halo must be one-dimensional, got {faces_halo.shape}")
|
|
285
|
+
|
|
286
|
+
owned_start_global = int(self.owned_start_global)
|
|
287
|
+
owned_stop_global = int(self.owned_stop_global)
|
|
288
|
+
halo_width = self.layout.halo_width
|
|
289
|
+
if owned_start_global < 0:
|
|
290
|
+
raise ValueError(f"owned_start_global must be non-negative, got {owned_start_global}")
|
|
291
|
+
if owned_stop_global < owned_start_global:
|
|
292
|
+
raise ValueError(
|
|
293
|
+
"owned_stop_global must be greater than or equal to owned_start_global, "
|
|
294
|
+
f"got start={owned_start_global}, stop={owned_stop_global}"
|
|
295
|
+
)
|
|
296
|
+
owned_size = self.layout.owned_shape[axis]
|
|
297
|
+
if owned_stop_global - owned_start_global != owned_size:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
"owned_start_global and owned_stop_global must span the owned size from the layout; "
|
|
300
|
+
f"axis={axis}, expected size={owned_size}, got start={owned_start_global}, stop={owned_stop_global}"
|
|
301
|
+
)
|
|
302
|
+
expected_center_size = owned_size + 2 * halo_width
|
|
303
|
+
expected_face_size = expected_center_size + 1
|
|
304
|
+
if centers_halo.size != expected_center_size:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
"centers_halo must contain owned cells plus both halo layers; "
|
|
307
|
+
f"expected {expected_center_size}, got {centers_halo.size}"
|
|
308
|
+
)
|
|
309
|
+
if faces_halo.size != expected_face_size:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
"faces_halo must contain one more entry than centers_halo; "
|
|
312
|
+
f"expected {expected_face_size}, got {faces_halo.size}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
object.__setattr__(self, "centers_halo", centers_halo)
|
|
316
|
+
object.__setattr__(self, "faces_halo", faces_halo)
|
|
317
|
+
object.__setattr__(self, "axis", axis)
|
|
318
|
+
object.__setattr__(self, "owned_start_global", owned_start_global)
|
|
319
|
+
object.__setattr__(self, "owned_stop_global", owned_stop_global)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def centers(self) -> jnp.ndarray:
|
|
323
|
+
return self.centers_halo
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def faces(self) -> jnp.ndarray:
|
|
327
|
+
return self.faces_halo
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def n(self) -> int:
|
|
331
|
+
return self.n_local
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def n_owned(self) -> int:
|
|
335
|
+
return self.layout.owned_shape[self.axis]
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def n_halo(self) -> int:
|
|
339
|
+
return self.layout.halo_width
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def n_local(self) -> int:
|
|
343
|
+
return int(self.centers_halo.size)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def shape(self) -> tuple[int]:
|
|
347
|
+
return (self.n_local,)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def halo_start_global(self) -> int:
|
|
351
|
+
return self.owned_start_global - self.layout.halo_width
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def halo_stop_global(self) -> int:
|
|
355
|
+
return self.owned_stop_global + self.layout.halo_width
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def owned_center_slice(self) -> slice:
|
|
359
|
+
h = self.layout.halo_width
|
|
360
|
+
return slice(h, h + self.n_owned)
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def owned_face_slice(self) -> slice:
|
|
364
|
+
h = self.layout.halo_width
|
|
365
|
+
return slice(h, h + self.n_owned + 1)
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def centers_owned(self) -> jnp.ndarray:
|
|
369
|
+
return self.centers_halo[self.owned_center_slice]
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def faces_owned(self) -> jnp.ndarray:
|
|
373
|
+
return self.faces_halo[self.owned_face_slice]
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def widths(self) -> jnp.ndarray:
|
|
377
|
+
return self.faces_halo[1:] - self.faces_halo[:-1]
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def center_deltas(self) -> jnp.ndarray:
|
|
381
|
+
return self.centers_halo[1:] - self.centers_halo[:-1]
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def lower_center_to_face(self):
|
|
385
|
+
return self.centers_halo[0] - self.faces_halo[0]
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def upper_center_to_face(self):
|
|
389
|
+
return self.faces_halo[-1] - self.centers_halo[-1]
|
|
390
|
+
|
|
391
|
+
def tree_flatten(self):
|
|
392
|
+
return (
|
|
393
|
+
(self.centers_halo, self.faces_halo),
|
|
394
|
+
(self.layout, self.axis, self.owned_start_global, self.owned_stop_global),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
def tree_unflatten(cls, aux_data, children):
|
|
399
|
+
layout, axis, owned_start_global, owned_stop_global = aux_data
|
|
400
|
+
centers_halo, faces_halo = children
|
|
401
|
+
return cls(
|
|
402
|
+
layout=layout,
|
|
403
|
+
axis=axis,
|
|
404
|
+
centers_halo=centers_halo,
|
|
405
|
+
faces_halo=faces_halo,
|
|
406
|
+
owned_start_global=owned_start_global,
|
|
407
|
+
owned_stop_global=owned_stop_global,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@_pytree_base
|
|
411
|
+
@dataclass(frozen=True)
|
|
412
|
+
class CellCenteredGrid3D(_DataclassPyTreeMixin):
|
|
413
|
+
x: Grid1D
|
|
414
|
+
y: Grid1D
|
|
415
|
+
z: Grid1D
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def shape(self) -> tuple[int, int, int]:
|
|
419
|
+
return self.x.n, self.y.n, self.z.n
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def x_centers(self) -> jnp.ndarray:
|
|
423
|
+
return self.x.centers
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def y_centers(self) -> jnp.ndarray:
|
|
427
|
+
return self.y.centers
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def z_centers(self) -> jnp.ndarray:
|
|
431
|
+
return self.z.centers
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def x_faces(self) -> jnp.ndarray:
|
|
435
|
+
return self.x.faces
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def y_faces(self) -> jnp.ndarray:
|
|
439
|
+
return self.y.faces
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def z_faces(self) -> jnp.ndarray:
|
|
443
|
+
return self.z.faces
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def logical_axis_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
447
|
+
return self.x.centers, self.y.centers, self.z.centers
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def logical_face_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
451
|
+
return self.x.faces, self.y.faces, self.z.faces
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@_pytree_base
|
|
455
|
+
@dataclass(frozen=True)
|
|
456
|
+
class LocalCellCenteredGrid3D(_DataclassPyTreeMixin):
|
|
457
|
+
"""Shard-local cell-centered grid with halo-aware 1D axes."""
|
|
458
|
+
|
|
459
|
+
layout: HaloLayout3D
|
|
460
|
+
x: LocalGrid1D
|
|
461
|
+
y: LocalGrid1D
|
|
462
|
+
z: LocalGrid1D
|
|
463
|
+
|
|
464
|
+
def __post_init__(self) -> None:
|
|
465
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
466
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
467
|
+
if not isinstance(self.x, LocalGrid1D):
|
|
468
|
+
raise TypeError("x must be a LocalGrid1D instance")
|
|
469
|
+
if not isinstance(self.y, LocalGrid1D):
|
|
470
|
+
raise TypeError("y must be a LocalGrid1D instance")
|
|
471
|
+
if not isinstance(self.z, LocalGrid1D):
|
|
472
|
+
raise TypeError("z must be a LocalGrid1D instance")
|
|
473
|
+
if self.x.layout != self.layout or self.y.layout != self.layout or self.z.layout != self.layout:
|
|
474
|
+
raise ValueError("LocalCellCenteredGrid3D axes must share the same HaloLayout3D")
|
|
475
|
+
if self.x.axis != 0 or self.y.axis != 1 or self.z.axis != 2:
|
|
476
|
+
raise ValueError("LocalCellCenteredGrid3D axes must be ordered as x=0, y=1, z=2")
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def shape(self) -> tuple[int, int, int]:
|
|
480
|
+
return self.layout.cell_halo_shape
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
484
|
+
return self.layout.owned_shape
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
488
|
+
return self.shape
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
492
|
+
return self.owned_shape
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def owned_slices_in_halo(self) -> tuple[slice, slice, slice]:
|
|
496
|
+
return self.layout.owned_slices_cell
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
500
|
+
return self.shape
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def x_centers(self) -> jnp.ndarray:
|
|
504
|
+
return self.x.centers
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def y_centers(self) -> jnp.ndarray:
|
|
508
|
+
return self.y.centers
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def z_centers(self) -> jnp.ndarray:
|
|
512
|
+
return self.z.centers
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def x_faces(self) -> jnp.ndarray:
|
|
516
|
+
return self.x.faces
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def y_faces(self) -> jnp.ndarray:
|
|
520
|
+
return self.y.faces
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def z_faces(self) -> jnp.ndarray:
|
|
524
|
+
return self.z.faces
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def logical_axis_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
528
|
+
return self.x.centers, self.y.centers, self.z.centers
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def logical_face_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
532
|
+
return self.x.faces, self.y.faces, self.z.faces
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def x_centers_owned(self) -> jnp.ndarray:
|
|
536
|
+
return self.x.centers_owned
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def y_centers_owned(self) -> jnp.ndarray:
|
|
540
|
+
return self.y.centers_owned
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def z_centers_owned(self) -> jnp.ndarray:
|
|
544
|
+
return self.z.centers_owned
|
|
545
|
+
|
|
546
|
+
@_pytree_base
|
|
547
|
+
@dataclass(frozen=True)
|
|
548
|
+
class FciMaps3D(_DataclassPyTreeMixin):
|
|
549
|
+
# Fractional interpolation indices into cell-centered field arrays.
|
|
550
|
+
# These are meaningful for non-boundary traces. For boundary traces,
|
|
551
|
+
# use the boundary mask and endpoint coordinates instead.
|
|
552
|
+
forward_x: jnp.ndarray
|
|
553
|
+
forward_y: jnp.ndarray
|
|
554
|
+
backward_x: jnp.ndarray
|
|
555
|
+
backward_y: jnp.ndarray
|
|
556
|
+
|
|
557
|
+
# Logical endpoint coordinates of the trace.
|
|
558
|
+
# If boundary=False: endpoint is on the target toroidal plane.
|
|
559
|
+
# If boundary=True: endpoint is the estimated physical boundary hit point.
|
|
560
|
+
forward_endpoint_x: jnp.ndarray
|
|
561
|
+
forward_endpoint_y: jnp.ndarray
|
|
562
|
+
forward_endpoint_z: jnp.ndarray
|
|
563
|
+
backward_endpoint_x: jnp.ndarray
|
|
564
|
+
backward_endpoint_y: jnp.ndarray
|
|
565
|
+
backward_endpoint_z: jnp.ndarray
|
|
566
|
+
|
|
567
|
+
# Physical arclengths from cell center to endpoint.
|
|
568
|
+
forward_length: jnp.ndarray
|
|
569
|
+
backward_length: jnp.ndarray
|
|
570
|
+
|
|
571
|
+
# True if the trace hit/exited a nonperiodic physical boundary before
|
|
572
|
+
# reaching the target toroidal plane.
|
|
573
|
+
forward_boundary: jnp.ndarray
|
|
574
|
+
backward_boundary: jnp.ndarray
|
|
575
|
+
|
|
576
|
+
def __post_init__(self) -> None:
|
|
577
|
+
forward_x = jnp.asarray(self.forward_x, dtype=jnp.float64)
|
|
578
|
+
shape = tuple(int(v) for v in forward_x.shape)
|
|
579
|
+
|
|
580
|
+
if len(shape) != 3:
|
|
581
|
+
raise ValueError(f"FciMaps3D fields must have shape (nx, ny, nz), got {shape}")
|
|
582
|
+
|
|
583
|
+
object.__setattr__(self, "forward_x", forward_x)
|
|
584
|
+
|
|
585
|
+
float_fields = (
|
|
586
|
+
"forward_y",
|
|
587
|
+
"backward_x",
|
|
588
|
+
"backward_y",
|
|
589
|
+
"forward_endpoint_x",
|
|
590
|
+
"forward_endpoint_y",
|
|
591
|
+
"forward_endpoint_z",
|
|
592
|
+
"backward_endpoint_x",
|
|
593
|
+
"backward_endpoint_y",
|
|
594
|
+
"backward_endpoint_z",
|
|
595
|
+
"forward_length",
|
|
596
|
+
"backward_length",
|
|
597
|
+
)
|
|
598
|
+
bool_fields = (
|
|
599
|
+
"forward_boundary",
|
|
600
|
+
"backward_boundary",
|
|
601
|
+
)
|
|
602
|
+
for name in float_fields:
|
|
603
|
+
value = _require_float_shape(getattr(self, name), shape, f"FciMaps3D.{name}")
|
|
604
|
+
object.__setattr__(self, name, value)
|
|
605
|
+
for name in bool_fields:
|
|
606
|
+
value = jnp.asarray(getattr(self, name), dtype=bool)
|
|
607
|
+
if value.shape != shape:
|
|
608
|
+
raise ValueError(f"FciMaps3D.{name} must have shape {shape}, got {value.shape}")
|
|
609
|
+
object.__setattr__(self, name, value)
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def shape(self) -> tuple[int, int, int]:
|
|
613
|
+
return tuple(int(v) for v in self.forward_x.shape)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# Dependency kinds are shared by local and remote FCI dependency metadata.
|
|
617
|
+
FCI_DEP_INVALID = 0
|
|
618
|
+
FCI_DEP_FIELD_INTERIOR = 1
|
|
619
|
+
FCI_DEP_PHYSICAL_BOUNDARY = 2
|
|
620
|
+
FCI_DEP_CUT_WALL = 3
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@_pytree_base
|
|
624
|
+
@dataclass(frozen=True)
|
|
625
|
+
class LocalFciLocalDependencyTable(_DataclassPyTreeMixin):
|
|
626
|
+
"""Sparse interpolation rows that can be satisfied locally.
|
|
627
|
+
|
|
628
|
+
Interior rows read from the local field halo. Boundary and cut-wall rows
|
|
629
|
+
may instead use a prepared value identified by ``value_slot``. The rows
|
|
630
|
+
are padded to a fixed maximum length so the object stays JAX compilation
|
|
631
|
+
friendly. Only the ``active`` rows participate in interpolation.
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
target_flat: jnp.ndarray # (max_entries,)
|
|
635
|
+
source_i: jnp.ndarray # (max_entries,)
|
|
636
|
+
source_j: jnp.ndarray # (max_entries,)
|
|
637
|
+
source_k: jnp.ndarray # (max_entries,)
|
|
638
|
+
weight: jnp.ndarray # (max_entries,)
|
|
639
|
+
active: jnp.ndarray # (max_entries,)
|
|
640
|
+
dependency_kind: jnp.ndarray | None = None # (max_entries,), int32
|
|
641
|
+
value_slot: jnp.ndarray | None = None # (max_entries,), int32
|
|
642
|
+
|
|
643
|
+
def __post_init__(self) -> None:
|
|
644
|
+
target_flat = jnp.asarray(self.target_flat, dtype=jnp.int32)
|
|
645
|
+
shape = tuple(int(v) for v in target_flat.shape)
|
|
646
|
+
if target_flat.ndim != 1:
|
|
647
|
+
raise ValueError(f"LocalFciLocalDependencyTable.target_flat must be 1D, got {target_flat.shape}")
|
|
648
|
+
object.__setattr__(self, "target_flat", target_flat)
|
|
649
|
+
for name in ("source_i", "source_j", "source_k"):
|
|
650
|
+
object.__setattr__(self, name, _require_shape(getattr(self, name), shape, f"LocalFciLocalDependencyTable.{name}"))
|
|
651
|
+
object.__setattr__(self, "weight", _require_float_shape(self.weight, shape, "LocalFciLocalDependencyTable.weight"))
|
|
652
|
+
active = jnp.asarray(self.active, dtype=bool)
|
|
653
|
+
if active.shape != shape:
|
|
654
|
+
raise ValueError(f"LocalFciLocalDependencyTable.active must have shape {shape}, got {active.shape}")
|
|
655
|
+
object.__setattr__(self, "active", active)
|
|
656
|
+
|
|
657
|
+
if self.dependency_kind is None:
|
|
658
|
+
dependency_kind = jnp.full(shape, FCI_DEP_FIELD_INTERIOR, dtype=jnp.int32)
|
|
659
|
+
else:
|
|
660
|
+
dependency_kind = jnp.asarray(self.dependency_kind, dtype=jnp.int32)
|
|
661
|
+
if dependency_kind.shape != shape:
|
|
662
|
+
raise ValueError(
|
|
663
|
+
"LocalFciLocalDependencyTable.dependency_kind must have "
|
|
664
|
+
f"shape {shape}, got {dependency_kind.shape}"
|
|
665
|
+
)
|
|
666
|
+
object.__setattr__(self, "dependency_kind", dependency_kind)
|
|
667
|
+
|
|
668
|
+
if self.value_slot is None:
|
|
669
|
+
value_slot = jnp.zeros(shape, dtype=jnp.int32)
|
|
670
|
+
else:
|
|
671
|
+
value_slot = jnp.asarray(self.value_slot, dtype=jnp.int32)
|
|
672
|
+
if value_slot.shape != shape:
|
|
673
|
+
raise ValueError(
|
|
674
|
+
"LocalFciLocalDependencyTable.value_slot must have "
|
|
675
|
+
f"shape {shape}, got {value_slot.shape}"
|
|
676
|
+
)
|
|
677
|
+
object.__setattr__(self, "value_slot", value_slot)
|
|
678
|
+
|
|
679
|
+
@property
|
|
680
|
+
def max_entries(self) -> int:
|
|
681
|
+
return int(self.target_flat.size)
|
|
682
|
+
|
|
683
|
+
@_pytree_base
|
|
684
|
+
@dataclass(frozen=True)
|
|
685
|
+
class LocalFciRemoteDependencyTable(_DataclassPyTreeMixin):
|
|
686
|
+
"""Sparse FCI interpolation rows satisfied by remote value exchange.
|
|
687
|
+
|
|
688
|
+
This table has two logical parts:
|
|
689
|
+
|
|
690
|
+
1. Interpolation rows, length max_entries
|
|
691
|
+
These are used by LocalFciStencilBuilder.
|
|
692
|
+
|
|
693
|
+
endpoint[target_flat[r]] += weight[r] * remote_values[receive_slot[r]]
|
|
694
|
+
|
|
695
|
+
2. Request rows, length max_receive_values
|
|
696
|
+
These are used by RemoteFciDependencyExchange.
|
|
697
|
+
|
|
698
|
+
remote_values[q] = value requested by request row q
|
|
699
|
+
|
|
700
|
+
Consolidation convention
|
|
701
|
+
------------------------
|
|
702
|
+
|
|
703
|
+
The request row index is the receive slot:
|
|
704
|
+
|
|
705
|
+
remote_values[q] contains the scalar returned for request row q
|
|
706
|
+
|
|
707
|
+
Therefore:
|
|
708
|
+
|
|
709
|
+
receive_slot[r] points directly to one request row q.
|
|
710
|
+
|
|
711
|
+
This lets multiple interpolation rows reuse the same remote scalar by sharing
|
|
712
|
+
the same receive_slot.
|
|
713
|
+
|
|
714
|
+
Request dependency kinds tell the owner/source shard what kind of scalar to
|
|
715
|
+
return:
|
|
716
|
+
|
|
717
|
+
FCI_DEP_FIELD_INTERIOR:
|
|
718
|
+
return a field value from the owner shard's local/halo field data.
|
|
719
|
+
|
|
720
|
+
FCI_DEP_PHYSICAL_BOUNDARY:
|
|
721
|
+
return a prepared physical-boundary value from the owner shard.
|
|
722
|
+
|
|
723
|
+
FCI_DEP_CUT_WALL:
|
|
724
|
+
return a prepared cut-wall value from the owner shard.
|
|
725
|
+
|
|
726
|
+
The stencil builder does not interpret request_dependency_kind. It only uses
|
|
727
|
+
target_flat, weight, receive_slot, and active. The exchange object uses the
|
|
728
|
+
request_* arrays.
|
|
729
|
+
"""
|
|
730
|
+
|
|
731
|
+
# -------------------------------------------------------------------------
|
|
732
|
+
# Interpolation rows on the requesting shard.
|
|
733
|
+
# Shape: (max_entries,)
|
|
734
|
+
# Used by LocalFciStencilBuilder.
|
|
735
|
+
# -------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
target_flat: jnp.ndarray
|
|
738
|
+
weight: jnp.ndarray
|
|
739
|
+
receive_slot: jnp.ndarray
|
|
740
|
+
active: jnp.ndarray
|
|
741
|
+
|
|
742
|
+
# -------------------------------------------------------------------------
|
|
743
|
+
# Request rows.
|
|
744
|
+
# Shape: (max_receive_values,)
|
|
745
|
+
# Used by RemoteFciDependencyExchange.
|
|
746
|
+
#
|
|
747
|
+
# Consolidation convention:
|
|
748
|
+
#
|
|
749
|
+
# remote_values[q] corresponds to request row q
|
|
750
|
+
#
|
|
751
|
+
# Therefore active interpolation rows must satisfy:
|
|
752
|
+
#
|
|
753
|
+
# 0 <= receive_slot[r] < max_receive_values
|
|
754
|
+
# -------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
request_active: jnp.ndarray
|
|
757
|
+
request_dependency_kind: jnp.ndarray
|
|
758
|
+
|
|
759
|
+
request_source_global_i: jnp.ndarray
|
|
760
|
+
request_source_global_j: jnp.ndarray
|
|
761
|
+
request_source_global_k: jnp.ndarray
|
|
762
|
+
|
|
763
|
+
request_source_shard_index: jnp.ndarray # (max_receive_values, 3)
|
|
764
|
+
request_source_shard_linear: jnp.ndarray
|
|
765
|
+
|
|
766
|
+
request_source_owner_local_i: jnp.ndarray
|
|
767
|
+
request_source_owner_local_j: jnp.ndarray
|
|
768
|
+
request_source_owner_local_k: jnp.ndarray
|
|
769
|
+
|
|
770
|
+
# Used for PHYSICAL_BOUNDARY / CUT_WALL requests.
|
|
771
|
+
# Dummy zero for FIELD_INTERIOR requests.
|
|
772
|
+
request_value_slot: jnp.ndarray
|
|
773
|
+
|
|
774
|
+
def __post_init__(self) -> None:
|
|
775
|
+
# ---------------------------------------------------------------------
|
|
776
|
+
# Interpolation-row arrays.
|
|
777
|
+
# ---------------------------------------------------------------------
|
|
778
|
+
target_flat = jnp.asarray(self.target_flat, dtype=jnp.int32)
|
|
779
|
+
row_shape = tuple(int(v) for v in target_flat.shape)
|
|
780
|
+
|
|
781
|
+
if target_flat.ndim != 1:
|
|
782
|
+
raise ValueError(
|
|
783
|
+
"LocalFciRemoteDependencyTable.target_flat must be 1D, "
|
|
784
|
+
f"got {target_flat.shape}"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
object.__setattr__(self, "target_flat", target_flat)
|
|
788
|
+
|
|
789
|
+
object.__setattr__(
|
|
790
|
+
self,
|
|
791
|
+
"weight",
|
|
792
|
+
_require_float_shape(
|
|
793
|
+
self.weight,
|
|
794
|
+
row_shape,
|
|
795
|
+
"LocalFciRemoteDependencyTable.weight",
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
object.__setattr__(
|
|
800
|
+
self,
|
|
801
|
+
"receive_slot",
|
|
802
|
+
_require_shape(
|
|
803
|
+
self.receive_slot,
|
|
804
|
+
row_shape,
|
|
805
|
+
"LocalFciRemoteDependencyTable.receive_slot",
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
active = jnp.asarray(self.active, dtype=bool)
|
|
810
|
+
if active.shape != row_shape:
|
|
811
|
+
raise ValueError(
|
|
812
|
+
"LocalFciRemoteDependencyTable.active must have shape "
|
|
813
|
+
f"{row_shape}, got {active.shape}"
|
|
814
|
+
)
|
|
815
|
+
object.__setattr__(self, "active", active)
|
|
816
|
+
|
|
817
|
+
# ---------------------------------------------------------------------
|
|
818
|
+
# Request-row arrays.
|
|
819
|
+
# ---------------------------------------------------------------------
|
|
820
|
+
request_active = jnp.asarray(self.request_active, dtype=bool)
|
|
821
|
+
request_shape = tuple(int(v) for v in request_active.shape)
|
|
822
|
+
|
|
823
|
+
if request_active.ndim != 1:
|
|
824
|
+
raise ValueError(
|
|
825
|
+
"LocalFciRemoteDependencyTable.request_active must be 1D, "
|
|
826
|
+
f"got {request_active.shape}"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
object.__setattr__(self, "request_active", request_active)
|
|
830
|
+
|
|
831
|
+
object.__setattr__(
|
|
832
|
+
self,
|
|
833
|
+
"request_dependency_kind",
|
|
834
|
+
_require_shape(
|
|
835
|
+
self.request_dependency_kind,
|
|
836
|
+
request_shape,
|
|
837
|
+
"LocalFciRemoteDependencyTable.request_dependency_kind",
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
for name in (
|
|
842
|
+
"request_source_global_i",
|
|
843
|
+
"request_source_global_j",
|
|
844
|
+
"request_source_global_k",
|
|
845
|
+
"request_source_shard_linear",
|
|
846
|
+
"request_source_owner_local_i",
|
|
847
|
+
"request_source_owner_local_j",
|
|
848
|
+
"request_source_owner_local_k",
|
|
849
|
+
"request_value_slot",
|
|
850
|
+
):
|
|
851
|
+
object.__setattr__(
|
|
852
|
+
self,
|
|
853
|
+
name,
|
|
854
|
+
_require_shape(
|
|
855
|
+
getattr(self, name),
|
|
856
|
+
request_shape,
|
|
857
|
+
f"LocalFciRemoteDependencyTable.{name}",
|
|
858
|
+
),
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
request_source_shard_index = jnp.asarray(
|
|
862
|
+
self.request_source_shard_index,
|
|
863
|
+
dtype=jnp.int32,
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if (
|
|
867
|
+
request_source_shard_index.ndim != 2
|
|
868
|
+
or request_source_shard_index.shape[1] != 3
|
|
869
|
+
):
|
|
870
|
+
raise ValueError(
|
|
871
|
+
"LocalFciRemoteDependencyTable.request_source_shard_index "
|
|
872
|
+
"must have shape (max_receive_values, 3), got "
|
|
873
|
+
f"{request_source_shard_index.shape}"
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
if int(request_source_shard_index.shape[0]) != request_shape[0]:
|
|
877
|
+
raise ValueError(
|
|
878
|
+
"LocalFciRemoteDependencyTable.request_source_shard_index "
|
|
879
|
+
"must match request_active length; got "
|
|
880
|
+
f"{request_source_shard_index.shape[0]}, "
|
|
881
|
+
f"expected {request_shape[0]}"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
object.__setattr__(
|
|
885
|
+
self,
|
|
886
|
+
"request_source_shard_index",
|
|
887
|
+
request_source_shard_index,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
@property
|
|
891
|
+
def max_entries(self) -> int:
|
|
892
|
+
"""Maximum number of interpolation rows."""
|
|
893
|
+
return int(self.target_flat.size)
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def max_receive_values(self) -> int:
|
|
897
|
+
"""Maximum number of requested/received scalar values."""
|
|
898
|
+
return int(self.request_active.size)
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def has_requests(self) -> bool:
|
|
902
|
+
"""Static-size table may still contain no active requests at runtime."""
|
|
903
|
+
return self.max_receive_values > 0
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
@_pytree_base
|
|
908
|
+
@dataclass(frozen=True)
|
|
909
|
+
class LocalFciDirectionMap(_DataclassPyTreeMixin):
|
|
910
|
+
"""One directional FCI dependency map for owned target cells."""
|
|
911
|
+
|
|
912
|
+
layout: HaloLayout3D
|
|
913
|
+
local: LocalFciLocalDependencyTable
|
|
914
|
+
remote: LocalFciRemoteDependencyTable | None = None
|
|
915
|
+
target_valid: jnp.ndarray | None = None # (nx_owned, ny_owned, nz_owned)
|
|
916
|
+
connection_length: jnp.ndarray | None = None # (nx_owned, ny_owned, nz_owned)
|
|
917
|
+
endpoint_kind: jnp.ndarray | None = None # (nx_owned, ny_owned, nz_owned), int32
|
|
918
|
+
|
|
919
|
+
def __post_init__(self) -> None:
|
|
920
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
921
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
922
|
+
if not isinstance(self.local, LocalFciLocalDependencyTable):
|
|
923
|
+
raise TypeError("local must be a LocalFciLocalDependencyTable instance")
|
|
924
|
+
if self.remote is not None and not isinstance(self.remote, LocalFciRemoteDependencyTable):
|
|
925
|
+
raise TypeError("remote must be a LocalFciRemoteDependencyTable instance or None")
|
|
926
|
+
if self.target_valid is None:
|
|
927
|
+
target_valid = jnp.ones(self.layout.owned_shape, dtype=bool)
|
|
928
|
+
else:
|
|
929
|
+
target_valid = jnp.asarray(self.target_valid, dtype=bool)
|
|
930
|
+
if target_valid.shape != self.layout.owned_shape:
|
|
931
|
+
raise ValueError(
|
|
932
|
+
"LocalFciDirectionMap.target_valid must match layout.owned_shape; "
|
|
933
|
+
f"got {target_valid.shape}, expected {self.layout.owned_shape}"
|
|
934
|
+
)
|
|
935
|
+
object.__setattr__(self, "target_valid", target_valid)
|
|
936
|
+
|
|
937
|
+
if self.endpoint_kind is None:
|
|
938
|
+
endpoint_kind = jnp.where(
|
|
939
|
+
target_valid,
|
|
940
|
+
FCI_DEP_FIELD_INTERIOR,
|
|
941
|
+
FCI_DEP_INVALID,
|
|
942
|
+
).astype(jnp.int32)
|
|
943
|
+
else:
|
|
944
|
+
endpoint_kind = jnp.asarray(self.endpoint_kind, dtype=jnp.int32)
|
|
945
|
+
if endpoint_kind.shape != self.layout.owned_shape:
|
|
946
|
+
raise ValueError(
|
|
947
|
+
"LocalFciDirectionMap.endpoint_kind must match "
|
|
948
|
+
f"layout.owned_shape; got {endpoint_kind.shape}, expected "
|
|
949
|
+
f"{self.layout.owned_shape}"
|
|
950
|
+
)
|
|
951
|
+
object.__setattr__(self, "endpoint_kind", endpoint_kind)
|
|
952
|
+
|
|
953
|
+
if self.connection_length is not None:
|
|
954
|
+
connection_length = _require_float_shape(
|
|
955
|
+
self.connection_length,
|
|
956
|
+
self.layout.owned_shape,
|
|
957
|
+
"LocalFciDirectionMap.connection_length",
|
|
958
|
+
)
|
|
959
|
+
object.__setattr__(self, "connection_length", connection_length)
|
|
960
|
+
|
|
961
|
+
@property
|
|
962
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
963
|
+
return self.layout.owned_shape
|
|
964
|
+
|
|
965
|
+
@property
|
|
966
|
+
def has_remote_dependencies(self) -> bool:
|
|
967
|
+
return self.remote is not None
|
|
968
|
+
|
|
969
|
+
@property
|
|
970
|
+
def has_local_dependencies(self) -> jnp.ndarray:
|
|
971
|
+
return jnp.any(self.local.active)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@_pytree_base
|
|
975
|
+
@dataclass(frozen=True)
|
|
976
|
+
class LocalFciMaps3D(_DataclassPyTreeMixin):
|
|
977
|
+
"""Shard-local FCI dependency maps over owned target cells.
|
|
978
|
+
|
|
979
|
+
This is a static description of interpolation dependencies only.
|
|
980
|
+
It does not perform communication and does not own field data.
|
|
981
|
+
"""
|
|
982
|
+
|
|
983
|
+
layout: HaloLayout3D
|
|
984
|
+
forward: LocalFciDirectionMap
|
|
985
|
+
backward: LocalFciDirectionMap
|
|
986
|
+
mode: str = "remote_dependencies"
|
|
987
|
+
|
|
988
|
+
def __post_init__(self) -> None:
|
|
989
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
990
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
991
|
+
if not isinstance(self.forward, LocalFciDirectionMap):
|
|
992
|
+
raise TypeError("forward must be a LocalFciDirectionMap instance")
|
|
993
|
+
if not isinstance(self.backward, LocalFciDirectionMap):
|
|
994
|
+
raise TypeError("backward must be a LocalFciDirectionMap instance")
|
|
995
|
+
if self.forward.layout != self.layout or self.backward.layout != self.layout:
|
|
996
|
+
raise ValueError("LocalFciMaps3D directions must share the same HaloLayout3D")
|
|
997
|
+
mode = str(self.mode)
|
|
998
|
+
if mode not in ("local_halo_only", "remote_dependencies"):
|
|
999
|
+
raise ValueError(
|
|
1000
|
+
'mode must be either "local_halo_only" or "remote_dependencies", '
|
|
1001
|
+
f"got {mode!r}"
|
|
1002
|
+
)
|
|
1003
|
+
if mode == "local_halo_only" and (self.forward.remote is not None or self.backward.remote is not None):
|
|
1004
|
+
raise ValueError("local_halo_only mode cannot include remote dependency tables")
|
|
1005
|
+
object.__setattr__(self, "mode", mode)
|
|
1006
|
+
|
|
1007
|
+
@property
|
|
1008
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1009
|
+
return self.layout.owned_shape
|
|
1010
|
+
|
|
1011
|
+
@property
|
|
1012
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
1013
|
+
return self.layout.owned_shape
|
|
1014
|
+
|
|
1015
|
+
@property
|
|
1016
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
1017
|
+
return self.layout.cell_halo_shape
|
|
1018
|
+
|
|
1019
|
+
@property
|
|
1020
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
1021
|
+
return self.layout.owned_shape
|
|
1022
|
+
|
|
1023
|
+
@property
|
|
1024
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
1025
|
+
return self.layout.cell_halo_shape
|
|
1026
|
+
|
|
1027
|
+
@property
|
|
1028
|
+
def local_halo_only(self) -> bool:
|
|
1029
|
+
return self.mode == "local_halo_only"
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@_pytree_base
|
|
1033
|
+
@dataclass(frozen=True)
|
|
1034
|
+
class LocalFciGeometry3D(_DataclassPyTreeMixin):
|
|
1035
|
+
"""Shard-local FCI geometry bundle.
|
|
1036
|
+
|
|
1037
|
+
This is the local counterpart to `FciGeometry3D`.
|
|
1038
|
+
It carries the shard-owned cell grid, the halo-padded local geometry
|
|
1039
|
+
fields, the local FCI dependency maps, and the owned-only conservative
|
|
1040
|
+
measures used by downstream operators.
|
|
1041
|
+
"""
|
|
1042
|
+
|
|
1043
|
+
layout: HaloLayout3D
|
|
1044
|
+
grid: LocalCellCenteredGrid3D
|
|
1045
|
+
maps: LocalFciMaps3D
|
|
1046
|
+
spacing: LocalSpacing3D
|
|
1047
|
+
cell_metric: LocalMetricGeometry
|
|
1048
|
+
face_metric: LocalFaceMetricGeometry
|
|
1049
|
+
cell_bfield: LocalBFieldGeometry
|
|
1050
|
+
face_bfield: LocalFaceBFieldGeometry
|
|
1051
|
+
regular_face_geometry: LocalRegularFaceGeometry3D
|
|
1052
|
+
cell_volume_geometry: LocalCellVolumeGeometry3D
|
|
1053
|
+
|
|
1054
|
+
def __post_init__(self) -> None:
|
|
1055
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1056
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1057
|
+
if not isinstance(self.grid, LocalCellCenteredGrid3D):
|
|
1058
|
+
raise TypeError("grid must be a LocalCellCenteredGrid3D instance")
|
|
1059
|
+
if not isinstance(self.maps, LocalFciMaps3D):
|
|
1060
|
+
raise TypeError("maps must be a LocalFciMaps3D instance")
|
|
1061
|
+
if not isinstance(self.spacing, LocalSpacing3D):
|
|
1062
|
+
raise TypeError("spacing must be a LocalSpacing3D instance")
|
|
1063
|
+
if not isinstance(self.cell_metric, LocalMetricGeometry):
|
|
1064
|
+
raise TypeError("cell_metric must be a LocalMetricGeometry instance")
|
|
1065
|
+
if not isinstance(self.face_metric, LocalFaceMetricGeometry):
|
|
1066
|
+
raise TypeError("face_metric must be a LocalFaceMetricGeometry instance")
|
|
1067
|
+
if not isinstance(self.cell_bfield, LocalBFieldGeometry):
|
|
1068
|
+
raise TypeError("cell_bfield must be a LocalBFieldGeometry instance")
|
|
1069
|
+
if not isinstance(self.face_bfield, LocalFaceBFieldGeometry):
|
|
1070
|
+
raise TypeError("face_bfield must be a LocalFaceBFieldGeometry instance")
|
|
1071
|
+
if not isinstance(self.regular_face_geometry, LocalRegularFaceGeometry3D):
|
|
1072
|
+
raise TypeError("regular_face_geometry must be a LocalRegularFaceGeometry3D instance")
|
|
1073
|
+
if not isinstance(self.cell_volume_geometry, LocalCellVolumeGeometry3D):
|
|
1074
|
+
raise TypeError("cell_volume_geometry must be a LocalCellVolumeGeometry3D instance")
|
|
1075
|
+
|
|
1076
|
+
for name, value in (
|
|
1077
|
+
("grid", self.grid.layout),
|
|
1078
|
+
("maps", self.maps.layout),
|
|
1079
|
+
("spacing", self.spacing.layout),
|
|
1080
|
+
("cell_metric", self.cell_metric.layout),
|
|
1081
|
+
("face_metric", self.face_metric.layout),
|
|
1082
|
+
("cell_bfield", self.cell_bfield.layout),
|
|
1083
|
+
("face_bfield", self.face_bfield.layout),
|
|
1084
|
+
("regular_face_geometry", self.regular_face_geometry.layout),
|
|
1085
|
+
("cell_volume_geometry", self.cell_volume_geometry.layout),
|
|
1086
|
+
):
|
|
1087
|
+
if value != self.layout:
|
|
1088
|
+
raise ValueError(f"LocalFciGeometry3D.{name} must share the same HaloLayout3D")
|
|
1089
|
+
|
|
1090
|
+
@property
|
|
1091
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1092
|
+
return self.layout.owned_shape
|
|
1093
|
+
|
|
1094
|
+
@property
|
|
1095
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
1096
|
+
return self.layout.owned_shape
|
|
1097
|
+
|
|
1098
|
+
@property
|
|
1099
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
1100
|
+
return self.layout.cell_halo_shape
|
|
1101
|
+
|
|
1102
|
+
@property
|
|
1103
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
1104
|
+
return self.layout.owned_shape
|
|
1105
|
+
|
|
1106
|
+
@property
|
|
1107
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
1108
|
+
return self.layout.cell_halo_shape
|
|
1109
|
+
|
|
1110
|
+
@property
|
|
1111
|
+
def x_centers(self) -> jnp.ndarray:
|
|
1112
|
+
return self.grid.x_centers
|
|
1113
|
+
|
|
1114
|
+
@property
|
|
1115
|
+
def y_centers(self) -> jnp.ndarray:
|
|
1116
|
+
return self.grid.y_centers
|
|
1117
|
+
|
|
1118
|
+
@property
|
|
1119
|
+
def z_centers(self) -> jnp.ndarray:
|
|
1120
|
+
return self.grid.z_centers
|
|
1121
|
+
|
|
1122
|
+
@property
|
|
1123
|
+
def x_faces(self) -> jnp.ndarray:
|
|
1124
|
+
return self.grid.x_faces
|
|
1125
|
+
|
|
1126
|
+
@property
|
|
1127
|
+
def y_faces(self) -> jnp.ndarray:
|
|
1128
|
+
return self.grid.y_faces
|
|
1129
|
+
|
|
1130
|
+
@property
|
|
1131
|
+
def z_faces(self) -> jnp.ndarray:
|
|
1132
|
+
return self.grid.z_faces
|
|
1133
|
+
|
|
1134
|
+
@property
|
|
1135
|
+
def logical_axis_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
1136
|
+
return self.grid.logical_axis_vectors
|
|
1137
|
+
|
|
1138
|
+
@property
|
|
1139
|
+
def logical_face_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
1140
|
+
return self.grid.logical_face_vectors
|
|
1141
|
+
|
|
1142
|
+
@property
|
|
1143
|
+
def x_centers_owned(self) -> jnp.ndarray:
|
|
1144
|
+
return self.grid.x_centers_owned
|
|
1145
|
+
|
|
1146
|
+
@property
|
|
1147
|
+
def y_centers_owned(self) -> jnp.ndarray:
|
|
1148
|
+
return self.grid.y_centers_owned
|
|
1149
|
+
|
|
1150
|
+
@property
|
|
1151
|
+
def z_centers_owned(self) -> jnp.ndarray:
|
|
1152
|
+
return self.grid.z_centers_owned
|
|
1153
|
+
|
|
1154
|
+
@property
|
|
1155
|
+
def cell_volume(self) -> LocalCellVolumeGeometry3D:
|
|
1156
|
+
return self.cell_volume_geometry
|
|
1157
|
+
|
|
1158
|
+
@property
|
|
1159
|
+
def regular_face(self) -> LocalRegularFaceGeometry3D:
|
|
1160
|
+
return self.regular_face_geometry
|
|
1161
|
+
|
|
1162
|
+
NeighborIndex3D = tuple[int, int, int]
|
|
1163
|
+
OptionalNeighborIndex3D = NeighborIndex3D | None
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
# Meaning of a global lower/upper side in a ShardSpec3D. These values are
|
|
1167
|
+
# metadata for later halo stages; HaloExchange3D only owns regular-neighbor
|
|
1168
|
+
# and SIDE_SIMPLE_PERIODIC data.
|
|
1169
|
+
SIDE_PHYSICAL = 1
|
|
1170
|
+
SIDE_SIMPLE_PERIODIC = 2
|
|
1171
|
+
SIDE_AXIS_REGULAR = 3
|
|
1172
|
+
SIDE_TOPOLOGY_MAPPED = 4
|
|
1173
|
+
SIDE_UNUSED = 5
|
|
1174
|
+
_VALID_SIDE_KINDS = frozenset(
|
|
1175
|
+
{
|
|
1176
|
+
SIDE_PHYSICAL,
|
|
1177
|
+
SIDE_SIMPLE_PERIODIC,
|
|
1178
|
+
SIDE_AXIS_REGULAR,
|
|
1179
|
+
SIDE_TOPOLOGY_MAPPED,
|
|
1180
|
+
SIDE_UNUSED,
|
|
1181
|
+
}
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
@_pytree_base
|
|
1186
|
+
@dataclass(frozen=True)
|
|
1187
|
+
class ShardSpec3D(_DataclassPyTreeMixin):
|
|
1188
|
+
"""Static metadata describing one shard's owned-cell block."""
|
|
1189
|
+
|
|
1190
|
+
global_shape: tuple[int, int, int]
|
|
1191
|
+
owned_start: tuple[int, int, int]
|
|
1192
|
+
owned_stop: tuple[int, int, int]
|
|
1193
|
+
shard_index: tuple[int, int, int]
|
|
1194
|
+
shard_counts: tuple[int, int, int]
|
|
1195
|
+
periodic_axes: tuple[bool, bool, bool]
|
|
1196
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False)
|
|
1197
|
+
halo_width: int = 1
|
|
1198
|
+
side_kind_lower: tuple[int, int, int] | None = None
|
|
1199
|
+
side_kind_upper: tuple[int, int, int] | None = None
|
|
1200
|
+
|
|
1201
|
+
def __post_init__(self) -> None:
|
|
1202
|
+
global_shape = tuple(int(v) for v in self.global_shape)
|
|
1203
|
+
owned_start = tuple(int(v) for v in self.owned_start)
|
|
1204
|
+
owned_stop = tuple(int(v) for v in self.owned_stop)
|
|
1205
|
+
shard_index = tuple(int(v) for v in self.shard_index)
|
|
1206
|
+
shard_counts = tuple(int(v) for v in self.shard_counts)
|
|
1207
|
+
periodic_axes = tuple(bool(v) for v in self.periodic_axes)
|
|
1208
|
+
axis_regular_axes = tuple(bool(v) for v in self.axis_regular_axes)
|
|
1209
|
+
halo_width = int(self.halo_width)
|
|
1210
|
+
|
|
1211
|
+
if self.side_kind_lower is None:
|
|
1212
|
+
side_kind_lower = tuple(
|
|
1213
|
+
SIDE_SIMPLE_PERIODIC if periodic else SIDE_PHYSICAL
|
|
1214
|
+
for periodic in periodic_axes
|
|
1215
|
+
)
|
|
1216
|
+
else:
|
|
1217
|
+
side_kind_lower = tuple(int(v) for v in self.side_kind_lower)
|
|
1218
|
+
if self.side_kind_upper is None:
|
|
1219
|
+
side_kind_upper = tuple(
|
|
1220
|
+
SIDE_SIMPLE_PERIODIC if periodic else SIDE_PHYSICAL
|
|
1221
|
+
for periodic in periodic_axes
|
|
1222
|
+
)
|
|
1223
|
+
else:
|
|
1224
|
+
side_kind_upper = tuple(int(v) for v in self.side_kind_upper)
|
|
1225
|
+
|
|
1226
|
+
for name, value in (
|
|
1227
|
+
("global_shape", global_shape),
|
|
1228
|
+
("owned_start", owned_start),
|
|
1229
|
+
("owned_stop", owned_stop),
|
|
1230
|
+
("shard_index", shard_index),
|
|
1231
|
+
("shard_counts", shard_counts),
|
|
1232
|
+
):
|
|
1233
|
+
if len(value) != 3:
|
|
1234
|
+
raise ValueError(f"ShardSpec3D.{name} must have length 3, got {value}")
|
|
1235
|
+
if len(periodic_axes) != 3:
|
|
1236
|
+
raise ValueError(f"ShardSpec3D.periodic_axes must have length 3, got {periodic_axes}")
|
|
1237
|
+
if len(axis_regular_axes) != 3:
|
|
1238
|
+
raise ValueError(f"ShardSpec3D.axis_regular_axes must have length 3, got {axis_regular_axes}")
|
|
1239
|
+
if len(side_kind_lower) != 3:
|
|
1240
|
+
raise ValueError(f"ShardSpec3D.side_kind_lower must have length 3, got {side_kind_lower}")
|
|
1241
|
+
if len(side_kind_upper) != 3:
|
|
1242
|
+
raise ValueError(f"ShardSpec3D.side_kind_upper must have length 3, got {side_kind_upper}")
|
|
1243
|
+
if any(kind not in _VALID_SIDE_KINDS for kind in side_kind_lower + side_kind_upper):
|
|
1244
|
+
raise ValueError(
|
|
1245
|
+
"ShardSpec3D side kinds must be one of "
|
|
1246
|
+
f"{sorted(_VALID_SIDE_KINDS)}, got lower={side_kind_lower}, "
|
|
1247
|
+
f"upper={side_kind_upper}"
|
|
1248
|
+
)
|
|
1249
|
+
for axis, periodic in enumerate(periodic_axes):
|
|
1250
|
+
if periodic and (
|
|
1251
|
+
side_kind_lower[axis] != SIDE_SIMPLE_PERIODIC
|
|
1252
|
+
or side_kind_upper[axis] != SIDE_SIMPLE_PERIODIC
|
|
1253
|
+
):
|
|
1254
|
+
raise ValueError(
|
|
1255
|
+
"periodic_axes requires SIDE_SIMPLE_PERIODIC on both global "
|
|
1256
|
+
f"sides; axis={axis}, lower={side_kind_lower[axis]}, "
|
|
1257
|
+
f"upper={side_kind_upper[axis]}"
|
|
1258
|
+
)
|
|
1259
|
+
if any(size <= 0 for size in global_shape):
|
|
1260
|
+
raise ValueError(f"ShardSpec3D.global_shape must contain positive integers, got {global_shape}")
|
|
1261
|
+
if any(start < 0 for start in owned_start):
|
|
1262
|
+
raise ValueError(f"ShardSpec3D.owned_start must be non-negative, got {owned_start}")
|
|
1263
|
+
if any(stop <= start for start, stop in zip(owned_start, owned_stop)):
|
|
1264
|
+
raise ValueError(
|
|
1265
|
+
"ShardSpec3D.owned_stop must be strictly greater than owned_start on every axis; "
|
|
1266
|
+
f"got start={owned_start}, stop={owned_stop}"
|
|
1267
|
+
)
|
|
1268
|
+
if any(stop > size for stop, size in zip(owned_stop, global_shape)):
|
|
1269
|
+
raise ValueError(
|
|
1270
|
+
"ShardSpec3D.owned_stop must not exceed global_shape; "
|
|
1271
|
+
f"got stop={owned_stop}, global_shape={global_shape}"
|
|
1272
|
+
)
|
|
1273
|
+
if any(count <= 0 for count in shard_counts):
|
|
1274
|
+
raise ValueError(f"ShardSpec3D.shard_counts must contain positive integers, got {shard_counts}")
|
|
1275
|
+
if any(index < 0 or index >= count for index, count in zip(shard_index, shard_counts)):
|
|
1276
|
+
raise ValueError(
|
|
1277
|
+
"ShardSpec3D.shard_index must lie within shard_counts; "
|
|
1278
|
+
f"got shard_index={shard_index}, shard_counts={shard_counts}"
|
|
1279
|
+
)
|
|
1280
|
+
if halo_width < 0:
|
|
1281
|
+
raise ValueError(f"ShardSpec3D.halo_width must be non-negative, got {halo_width}")
|
|
1282
|
+
|
|
1283
|
+
object.__setattr__(self, "global_shape", global_shape)
|
|
1284
|
+
object.__setattr__(self, "owned_start", owned_start)
|
|
1285
|
+
object.__setattr__(self, "owned_stop", owned_stop)
|
|
1286
|
+
object.__setattr__(self, "shard_index", shard_index)
|
|
1287
|
+
object.__setattr__(self, "shard_counts", shard_counts)
|
|
1288
|
+
object.__setattr__(self, "periodic_axes", periodic_axes)
|
|
1289
|
+
object.__setattr__(self, "axis_regular_axes", axis_regular_axes)
|
|
1290
|
+
object.__setattr__(self, "halo_width", halo_width)
|
|
1291
|
+
object.__setattr__(self, "side_kind_lower", side_kind_lower)
|
|
1292
|
+
object.__setattr__(self, "side_kind_upper", side_kind_upper)
|
|
1293
|
+
|
|
1294
|
+
@property
|
|
1295
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
1296
|
+
return (
|
|
1297
|
+
self.owned_stop[0] - self.owned_start[0],
|
|
1298
|
+
self.owned_stop[1] - self.owned_start[1],
|
|
1299
|
+
self.owned_stop[2] - self.owned_start[2],
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
def touches_lower(self, axis: int) -> bool:
|
|
1303
|
+
"""Static host/debug check for a per-shard domain description."""
|
|
1304
|
+
return self.owned_start[int(axis)] == 0
|
|
1305
|
+
|
|
1306
|
+
def touches_upper(self, axis: int) -> bool:
|
|
1307
|
+
"""Static host/debug check for a per-shard domain description."""
|
|
1308
|
+
return self.owned_stop[int(axis)] == self.global_shape[int(axis)]
|
|
1309
|
+
|
|
1310
|
+
@staticmethod
|
|
1311
|
+
def _check_axis(axis: int) -> int:
|
|
1312
|
+
axis = int(axis)
|
|
1313
|
+
if axis < 0 or axis > 2:
|
|
1314
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
1315
|
+
return axis
|
|
1316
|
+
|
|
1317
|
+
def lower_side_kind(self, axis: int) -> int:
|
|
1318
|
+
return int(self.side_kind_lower[self._check_axis(axis)])
|
|
1319
|
+
|
|
1320
|
+
def upper_side_kind(self, axis: int) -> int:
|
|
1321
|
+
return int(self.side_kind_upper[self._check_axis(axis)])
|
|
1322
|
+
|
|
1323
|
+
def has_physical_lower(self, axis: int) -> bool:
|
|
1324
|
+
"""Static host/debug helper; not runtime SPMD ownership."""
|
|
1325
|
+
axis = int(axis)
|
|
1326
|
+
return self.touches_lower(axis) and self.lower_side_kind(axis) == SIDE_PHYSICAL
|
|
1327
|
+
|
|
1328
|
+
def has_physical_upper(self, axis: int) -> bool:
|
|
1329
|
+
"""Static host/debug helper; not runtime SPMD ownership."""
|
|
1330
|
+
axis = int(axis)
|
|
1331
|
+
return self.touches_upper(axis) and self.upper_side_kind(axis) == SIDE_PHYSICAL
|
|
1332
|
+
|
|
1333
|
+
def allows_regular_exchange_lower(self, axis: int) -> bool:
|
|
1334
|
+
axis = self._check_axis(axis)
|
|
1335
|
+
return not self.touches_lower(axis) or self.lower_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
1336
|
+
|
|
1337
|
+
def allows_regular_exchange_upper(self, axis: int) -> bool:
|
|
1338
|
+
axis = self._check_axis(axis)
|
|
1339
|
+
return not self.touches_upper(axis) or self.upper_side_kind(axis) == SIDE_SIMPLE_PERIODIC
|
|
1340
|
+
|
|
1341
|
+
def has_topology_lower(self, axis: int) -> bool:
|
|
1342
|
+
return self.touches_lower(axis) and self.lower_side_kind(axis) in (
|
|
1343
|
+
SIDE_SIMPLE_PERIODIC,
|
|
1344
|
+
SIDE_AXIS_REGULAR,
|
|
1345
|
+
SIDE_TOPOLOGY_MAPPED,
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
def has_topology_upper(self, axis: int) -> bool:
|
|
1349
|
+
return self.touches_upper(axis) and self.upper_side_kind(axis) in (
|
|
1350
|
+
SIDE_SIMPLE_PERIODIC,
|
|
1351
|
+
SIDE_AXIS_REGULAR,
|
|
1352
|
+
SIDE_TOPOLOGY_MAPPED,
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
def tree_flatten(self):
|
|
1356
|
+
return (), (
|
|
1357
|
+
self.global_shape,
|
|
1358
|
+
self.owned_start,
|
|
1359
|
+
self.owned_stop,
|
|
1360
|
+
self.shard_index,
|
|
1361
|
+
self.shard_counts,
|
|
1362
|
+
self.periodic_axes,
|
|
1363
|
+
self.axis_regular_axes,
|
|
1364
|
+
self.side_kind_lower,
|
|
1365
|
+
self.side_kind_upper,
|
|
1366
|
+
self.halo_width,
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
@classmethod
|
|
1370
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1371
|
+
del children
|
|
1372
|
+
(
|
|
1373
|
+
global_shape,
|
|
1374
|
+
owned_start,
|
|
1375
|
+
owned_stop,
|
|
1376
|
+
shard_index,
|
|
1377
|
+
shard_counts,
|
|
1378
|
+
periodic_axes,
|
|
1379
|
+
axis_regular_axes,
|
|
1380
|
+
side_kind_lower,
|
|
1381
|
+
side_kind_upper,
|
|
1382
|
+
halo_width,
|
|
1383
|
+
) = aux_data
|
|
1384
|
+
return cls(
|
|
1385
|
+
global_shape=global_shape,
|
|
1386
|
+
owned_start=owned_start,
|
|
1387
|
+
owned_stop=owned_stop,
|
|
1388
|
+
shard_index=shard_index,
|
|
1389
|
+
shard_counts=shard_counts,
|
|
1390
|
+
periodic_axes=periodic_axes,
|
|
1391
|
+
axis_regular_axes=axis_regular_axes,
|
|
1392
|
+
halo_width=halo_width,
|
|
1393
|
+
side_kind_lower=side_kind_lower,
|
|
1394
|
+
side_kind_upper=side_kind_upper,
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
@_pytree_base
|
|
1399
|
+
@dataclass(frozen=True)
|
|
1400
|
+
class NeighborMap3D(_DataclassPyTreeMixin):
|
|
1401
|
+
"""Shard-adjacency metadata for one local 3D domain."""
|
|
1402
|
+
|
|
1403
|
+
minus: tuple[OptionalNeighborIndex3D, OptionalNeighborIndex3D, OptionalNeighborIndex3D]
|
|
1404
|
+
plus: tuple[OptionalNeighborIndex3D, OptionalNeighborIndex3D, OptionalNeighborIndex3D]
|
|
1405
|
+
|
|
1406
|
+
def __post_init__(self) -> None:
|
|
1407
|
+
minus = tuple(self.minus)
|
|
1408
|
+
plus = tuple(self.plus)
|
|
1409
|
+
if len(minus) != 3:
|
|
1410
|
+
raise ValueError(f"NeighborMap3D.minus must have length 3, got {minus}")
|
|
1411
|
+
if len(plus) != 3:
|
|
1412
|
+
raise ValueError(f"NeighborMap3D.plus must have length 3, got {plus}")
|
|
1413
|
+
normalized_minus = []
|
|
1414
|
+
normalized_plus = []
|
|
1415
|
+
for name, side, normalized in (
|
|
1416
|
+
("minus", minus, normalized_minus),
|
|
1417
|
+
("plus", plus, normalized_plus),
|
|
1418
|
+
):
|
|
1419
|
+
for entry in side:
|
|
1420
|
+
if entry is None:
|
|
1421
|
+
normalized.append(None)
|
|
1422
|
+
continue
|
|
1423
|
+
if len(entry) != 3:
|
|
1424
|
+
raise ValueError(f"NeighborMap3D.{name} entries must be length-3 tuples or None, got {entry}")
|
|
1425
|
+
normalized.append(tuple(int(v) for v in entry))
|
|
1426
|
+
object.__setattr__(self, "minus", tuple(normalized_minus))
|
|
1427
|
+
object.__setattr__(self, "plus", tuple(normalized_plus))
|
|
1428
|
+
|
|
1429
|
+
def tree_flatten(self):
|
|
1430
|
+
return (), (self.minus, self.plus)
|
|
1431
|
+
|
|
1432
|
+
@classmethod
|
|
1433
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1434
|
+
del children
|
|
1435
|
+
minus, plus = aux_data
|
|
1436
|
+
return cls(minus=minus, plus=plus)
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
@_pytree_base
|
|
1440
|
+
@dataclass(frozen=True)
|
|
1441
|
+
class LocalDomain3D(_DataclassPyTreeMixin):
|
|
1442
|
+
"""Metadata for one local shard/domain.
|
|
1443
|
+
|
|
1444
|
+
``mesh_axis_names`` describes the execution mesh used by SPMD-facing
|
|
1445
|
+
helpers. It is deliberately kept on ``LocalDomain3D`` rather than
|
|
1446
|
+
``ShardSpec3D`` because collective axis names are execution metadata, not
|
|
1447
|
+
geometric metadata.
|
|
1448
|
+
|
|
1449
|
+
The existing ``touches_*`` and ``has_*`` methods are host/debug helpers
|
|
1450
|
+
based on the static per-shard metadata in ``ShardSpec3D``. Code executing
|
|
1451
|
+
inside ``pmap``/``shard_map`` should use the ``runtime_*`` methods below.
|
|
1452
|
+
"""
|
|
1453
|
+
|
|
1454
|
+
shard_spec: ShardSpec3D
|
|
1455
|
+
layout: HaloLayout3D
|
|
1456
|
+
neighbor_map: NeighborMap3D | None = None
|
|
1457
|
+
mesh_axis_names: tuple[str | None, str | None, str | None] = (
|
|
1458
|
+
None,
|
|
1459
|
+
None,
|
|
1460
|
+
None,
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
def __post_init__(self) -> None:
|
|
1464
|
+
if not isinstance(self.shard_spec, ShardSpec3D):
|
|
1465
|
+
raise TypeError("shard_spec must be a ShardSpec3D instance")
|
|
1466
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1467
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1468
|
+
if self.layout.owned_shape != self.shard_spec.owned_shape:
|
|
1469
|
+
raise ValueError(
|
|
1470
|
+
"LocalDomain3D.layout.owned_shape must match shard_spec.owned_shape; "
|
|
1471
|
+
f"got layout={self.layout.owned_shape}, shard_spec={self.shard_spec.owned_shape}"
|
|
1472
|
+
)
|
|
1473
|
+
if self.layout.halo_width != self.shard_spec.halo_width:
|
|
1474
|
+
raise ValueError(
|
|
1475
|
+
"LocalDomain3D.layout.halo_width must match shard_spec.halo_width; "
|
|
1476
|
+
f"got layout={self.layout.halo_width}, shard_spec={self.shard_spec.halo_width}"
|
|
1477
|
+
)
|
|
1478
|
+
if self.neighbor_map is not None and not isinstance(self.neighbor_map, NeighborMap3D):
|
|
1479
|
+
raise TypeError("neighbor_map must be a NeighborMap3D instance or None")
|
|
1480
|
+
mesh_axis_names = tuple(self.mesh_axis_names)
|
|
1481
|
+
if len(mesh_axis_names) != 3:
|
|
1482
|
+
raise ValueError(
|
|
1483
|
+
"LocalDomain3D.mesh_axis_names must have length 3, "
|
|
1484
|
+
f"got {mesh_axis_names}"
|
|
1485
|
+
)
|
|
1486
|
+
for axis, name in enumerate(mesh_axis_names):
|
|
1487
|
+
if name is not None and not isinstance(name, str):
|
|
1488
|
+
raise TypeError(
|
|
1489
|
+
"LocalDomain3D.mesh_axis_names entries must be strings or None; "
|
|
1490
|
+
f"axis={axis}, value={name!r}"
|
|
1491
|
+
)
|
|
1492
|
+
object.__setattr__(self, "mesh_axis_names", mesh_axis_names)
|
|
1493
|
+
|
|
1494
|
+
@property
|
|
1495
|
+
def periodic_axes(self) -> tuple[bool, bool, bool]:
|
|
1496
|
+
return self.shard_spec.periodic_axes
|
|
1497
|
+
|
|
1498
|
+
@property
|
|
1499
|
+
def axis_regular_axes(self) -> tuple[bool, bool, bool]:
|
|
1500
|
+
return self.shard_spec.axis_regular_axes
|
|
1501
|
+
|
|
1502
|
+
@property
|
|
1503
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
1504
|
+
return self.shard_spec.owned_shape
|
|
1505
|
+
|
|
1506
|
+
def has_physical_lower(self, axis: int) -> bool:
|
|
1507
|
+
"""Static host/debug helper; use ``runtime_has_physical_lower`` in SPMD."""
|
|
1508
|
+
return self.shard_spec.has_physical_lower(axis)
|
|
1509
|
+
|
|
1510
|
+
def has_physical_upper(self, axis: int) -> bool:
|
|
1511
|
+
"""Static host/debug helper; use ``runtime_has_physical_upper`` in SPMD."""
|
|
1512
|
+
return self.shard_spec.has_physical_upper(axis)
|
|
1513
|
+
|
|
1514
|
+
def allows_regular_exchange_lower(self, axis: int) -> bool:
|
|
1515
|
+
return self.shard_spec.allows_regular_exchange_lower(axis)
|
|
1516
|
+
|
|
1517
|
+
def allows_regular_exchange_upper(self, axis: int) -> bool:
|
|
1518
|
+
return self.shard_spec.allows_regular_exchange_upper(axis)
|
|
1519
|
+
|
|
1520
|
+
def has_topology_lower(self, axis: int) -> bool:
|
|
1521
|
+
return self.shard_spec.has_topology_lower(axis)
|
|
1522
|
+
|
|
1523
|
+
def has_topology_upper(self, axis: int) -> bool:
|
|
1524
|
+
return self.shard_spec.has_topology_upper(axis)
|
|
1525
|
+
|
|
1526
|
+
def runtime_shard_id(self, axis: int) -> int | jnp.ndarray:
|
|
1527
|
+
"""Return the current SPMD shard index for a logical axis.
|
|
1528
|
+
|
|
1529
|
+
An axis without a configured mesh name is treated as undecomposed and
|
|
1530
|
+
returns the Python integer ``0``. A configured name must be valid in
|
|
1531
|
+
the surrounding ``pmap``/``shard_map`` context.
|
|
1532
|
+
"""
|
|
1533
|
+
|
|
1534
|
+
axis = int(axis)
|
|
1535
|
+
if axis < 0 or axis > 2:
|
|
1536
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
1537
|
+
name = self.mesh_axis_names[axis]
|
|
1538
|
+
if name is None:
|
|
1539
|
+
return 0
|
|
1540
|
+
return lax.axis_index(name)
|
|
1541
|
+
|
|
1542
|
+
def runtime_touches_lower(self, axis: int) -> bool | jnp.ndarray:
|
|
1543
|
+
axis = int(axis)
|
|
1544
|
+
return self.runtime_shard_id(axis) == 0
|
|
1545
|
+
|
|
1546
|
+
def runtime_touches_upper(self, axis: int) -> bool | jnp.ndarray:
|
|
1547
|
+
axis = int(axis)
|
|
1548
|
+
return self.runtime_shard_id(axis) == self.shard_spec.shard_counts[axis] - 1
|
|
1549
|
+
|
|
1550
|
+
def runtime_has_physical_lower(self, axis: int) -> bool | jnp.ndarray:
|
|
1551
|
+
axis = int(axis)
|
|
1552
|
+
return self.runtime_touches_lower(axis) & (
|
|
1553
|
+
self.shard_spec.lower_side_kind(axis) == SIDE_PHYSICAL
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
def runtime_has_physical_upper(self, axis: int) -> bool | jnp.ndarray:
|
|
1557
|
+
axis = int(axis)
|
|
1558
|
+
return self.runtime_touches_upper(axis) & (
|
|
1559
|
+
self.shard_spec.upper_side_kind(axis) == SIDE_PHYSICAL
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
def runtime_has_axis_regular_lower(self, axis: int) -> bool | jnp.ndarray:
|
|
1563
|
+
axis = int(axis)
|
|
1564
|
+
return self.runtime_touches_lower(axis) & (
|
|
1565
|
+
self.shard_spec.lower_side_kind(axis) == SIDE_AXIS_REGULAR
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
def runtime_has_axis_regular_upper(self, axis: int) -> bool | jnp.ndarray:
|
|
1569
|
+
axis = int(axis)
|
|
1570
|
+
return self.runtime_touches_upper(axis) & (
|
|
1571
|
+
self.shard_spec.upper_side_kind(axis) == SIDE_AXIS_REGULAR
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1574
|
+
def runtime_has_side_kind_lower(
|
|
1575
|
+
self,
|
|
1576
|
+
axis: int,
|
|
1577
|
+
side_kind: int,
|
|
1578
|
+
) -> bool | jnp.ndarray:
|
|
1579
|
+
axis = int(axis)
|
|
1580
|
+
return self.runtime_touches_lower(axis) & (
|
|
1581
|
+
self.shard_spec.lower_side_kind(axis) == int(side_kind)
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
def runtime_has_side_kind_upper(
|
|
1585
|
+
self,
|
|
1586
|
+
axis: int,
|
|
1587
|
+
side_kind: int,
|
|
1588
|
+
) -> bool | jnp.ndarray:
|
|
1589
|
+
axis = int(axis)
|
|
1590
|
+
return self.runtime_touches_upper(axis) & (
|
|
1591
|
+
self.shard_spec.upper_side_kind(axis) == int(side_kind)
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
def tree_flatten(self):
|
|
1595
|
+
return (), (
|
|
1596
|
+
self.shard_spec,
|
|
1597
|
+
self.layout,
|
|
1598
|
+
self.neighbor_map,
|
|
1599
|
+
self.mesh_axis_names,
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
@classmethod
|
|
1603
|
+
def tree_unflatten(cls, aux_data, children):
|
|
1604
|
+
del children
|
|
1605
|
+
shard_spec, layout, neighbor_map, mesh_axis_names = aux_data
|
|
1606
|
+
return cls(
|
|
1607
|
+
shard_spec=shard_spec,
|
|
1608
|
+
layout=layout,
|
|
1609
|
+
neighbor_map=neighbor_map,
|
|
1610
|
+
mesh_axis_names=mesh_axis_names,
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
@_pytree_base
|
|
1615
|
+
@dataclass(frozen=True)
|
|
1616
|
+
class StencilBuilderContext(_DataclassPyTreeMixin):
|
|
1617
|
+
layout: HaloLayout3D
|
|
1618
|
+
domain: LocalDomain3D | None = None
|
|
1619
|
+
cut_wall_geometry: "LocalCutWallGeometry3D | None" = None
|
|
1620
|
+
cut_wall_bc: "LocalCutWallBC3D | None" = None
|
|
1621
|
+
cut_wall_value_reconstructor: "LocalCutWallValueReconstructor3D | None" = None
|
|
1622
|
+
cut_wall_normal_derivative_constructor: (
|
|
1623
|
+
"LocalCutWallNormalDerivativeConstructor3D | None"
|
|
1624
|
+
) = None
|
|
1625
|
+
|
|
1626
|
+
def __post_init__(self) -> None:
|
|
1627
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1628
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1629
|
+
if self.domain is not None and self.domain.layout != self.layout:
|
|
1630
|
+
raise ValueError("StencilBuilderContext.domain must share the same layout")
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
# Backward-compatible aliases. Both historical context names now refer to the
|
|
1634
|
+
# same canonical PyTree type.
|
|
1635
|
+
LocalStencilBuilderContext = StencilBuilderContext
|
|
1636
|
+
ConservativeStencilBuilderContext = StencilBuilderContext
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
@_pytree_base
|
|
1640
|
+
@dataclass(frozen=True)
|
|
1641
|
+
class Spacing3D(_DataclassPyTreeMixin):
|
|
1642
|
+
"""Logical spacings evaluated at active cell centers.
|
|
1643
|
+
These are usually broadcast arrays with shape (nx, ny, nz)."""
|
|
1644
|
+
dx: jnp.ndarray
|
|
1645
|
+
dy: jnp.ndarray
|
|
1646
|
+
dz: jnp.ndarray
|
|
1647
|
+
def __post_init__(self) -> None:
|
|
1648
|
+
dx = jnp.asarray(self.dx, dtype=jnp.float64)
|
|
1649
|
+
if dx.ndim != 3:
|
|
1650
|
+
raise ValueError(f"Spacing3D.dx must have shape (nx, ny, nz), got {dx.shape}")
|
|
1651
|
+
shape = tuple(int(v) for v in dx.shape)
|
|
1652
|
+
object.__setattr__(self, "dx", dx)
|
|
1653
|
+
object.__setattr__(self, "dy", _require_float_shape(self.dy, shape, "Spacing3D.dy"))
|
|
1654
|
+
object.__setattr__(self, "dz", _require_float_shape(self.dz, shape, "Spacing3D.dz"))
|
|
1655
|
+
@property
|
|
1656
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1657
|
+
return tuple(int(v) for v in self.dx.shape)
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
@_pytree_base
|
|
1661
|
+
@dataclass(frozen=True)
|
|
1662
|
+
class LocalSpacing3D(_DataclassPyTreeMixin):
|
|
1663
|
+
"""Shard-local logical spacings with halo and owned views."""
|
|
1664
|
+
|
|
1665
|
+
layout: HaloLayout3D
|
|
1666
|
+
dx_halo: jnp.ndarray # (nx + 2*h, ny + 2*h, nz + 2*h)
|
|
1667
|
+
dy_halo: jnp.ndarray # (nx + 2*h, ny + 2*h, nz + 2*h)
|
|
1668
|
+
dz_halo: jnp.ndarray # (nx + 2*h, ny + 2*h, nz + 2*h)
|
|
1669
|
+
|
|
1670
|
+
def __post_init__(self) -> None:
|
|
1671
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1672
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1673
|
+
dx_halo = jnp.asarray(self.dx_halo, dtype=jnp.float64)
|
|
1674
|
+
if dx_halo.ndim != 3:
|
|
1675
|
+
raise ValueError(f"LocalSpacing3D.dx_halo must have shape (nx, ny, nz), got {dx_halo.shape}")
|
|
1676
|
+
shape = tuple(int(v) for v in dx_halo.shape)
|
|
1677
|
+
if shape != self.layout.cell_halo_shape:
|
|
1678
|
+
raise ValueError(
|
|
1679
|
+
"LocalSpacing3D.dx_halo must match layout.cell_halo_shape; "
|
|
1680
|
+
f"got {shape}, expected {self.layout.cell_halo_shape}"
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
dy_halo = _require_float_shape(self.dy_halo, shape, "LocalSpacing3D.dy_halo")
|
|
1684
|
+
dz_halo = _require_float_shape(self.dz_halo, shape, "LocalSpacing3D.dz_halo")
|
|
1685
|
+
|
|
1686
|
+
object.__setattr__(self, "dx_halo", dx_halo)
|
|
1687
|
+
object.__setattr__(self, "dy_halo", dy_halo)
|
|
1688
|
+
object.__setattr__(self, "dz_halo", dz_halo)
|
|
1689
|
+
|
|
1690
|
+
@property
|
|
1691
|
+
def dx(self) -> jnp.ndarray:
|
|
1692
|
+
return self.dx_halo
|
|
1693
|
+
|
|
1694
|
+
@property
|
|
1695
|
+
def dy(self) -> jnp.ndarray:
|
|
1696
|
+
return self.dy_halo
|
|
1697
|
+
|
|
1698
|
+
@property
|
|
1699
|
+
def dz(self) -> jnp.ndarray:
|
|
1700
|
+
return self.dz_halo
|
|
1701
|
+
|
|
1702
|
+
@property
|
|
1703
|
+
def dx_owned(self) -> jnp.ndarray:
|
|
1704
|
+
return self.dx_halo[self.owned_slices_in_halo]
|
|
1705
|
+
|
|
1706
|
+
@property
|
|
1707
|
+
def dy_owned(self) -> jnp.ndarray:
|
|
1708
|
+
return self.dy_halo[self.owned_slices_in_halo]
|
|
1709
|
+
|
|
1710
|
+
@property
|
|
1711
|
+
def dz_owned(self) -> jnp.ndarray:
|
|
1712
|
+
return self.dz_halo[self.owned_slices_in_halo]
|
|
1713
|
+
|
|
1714
|
+
@property
|
|
1715
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1716
|
+
return tuple(int(v) for v in self.dx_halo.shape)
|
|
1717
|
+
|
|
1718
|
+
@property
|
|
1719
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
1720
|
+
return self.shape
|
|
1721
|
+
|
|
1722
|
+
@property
|
|
1723
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
1724
|
+
return self.layout.owned_shape
|
|
1725
|
+
|
|
1726
|
+
@property
|
|
1727
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
1728
|
+
return self.shape
|
|
1729
|
+
|
|
1730
|
+
@property
|
|
1731
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
1732
|
+
return self.owned_shape
|
|
1733
|
+
|
|
1734
|
+
@property
|
|
1735
|
+
def owned_slices_in_halo(self) -> tuple[slice, slice, slice]:
|
|
1736
|
+
return self.layout.owned_slices_cell
|
|
1737
|
+
|
|
1738
|
+
@_pytree_base
|
|
1739
|
+
@dataclass(frozen=True)
|
|
1740
|
+
class LocalMetricGeometry(_DataclassPyTreeMixin):
|
|
1741
|
+
"""Metric coefficients on a local halo-padded cell or face grid.
|
|
1742
|
+
|
|
1743
|
+
These arrays are local arrays, not views into a global MetricGeometry.
|
|
1744
|
+
The `location` metadata determines which local shape convention applies:
|
|
1745
|
+
cell-centered or one of the three face families.
|
|
1746
|
+
"""
|
|
1747
|
+
#field_halo shaped arrays
|
|
1748
|
+
layout: HaloLayout3D
|
|
1749
|
+
J_halo: jnp.ndarray
|
|
1750
|
+
g11_halo: jnp.ndarray
|
|
1751
|
+
g22_halo: jnp.ndarray
|
|
1752
|
+
g33_halo: jnp.ndarray
|
|
1753
|
+
g12_halo: jnp.ndarray
|
|
1754
|
+
g13_halo: jnp.ndarray
|
|
1755
|
+
g23_halo: jnp.ndarray
|
|
1756
|
+
g_11_halo: jnp.ndarray
|
|
1757
|
+
g_22_halo: jnp.ndarray
|
|
1758
|
+
g_33_halo: jnp.ndarray
|
|
1759
|
+
g_12_halo: jnp.ndarray
|
|
1760
|
+
g_13_halo: jnp.ndarray
|
|
1761
|
+
g_23_halo: jnp.ndarray
|
|
1762
|
+
location: str # "cell", "x_face", "y_face", or "z_face"
|
|
1763
|
+
|
|
1764
|
+
def __post_init__(self) -> None:
|
|
1765
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
1766
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
1767
|
+
location = str(self.location)
|
|
1768
|
+
expected_shape = self.layout.location_halo_shape(location)
|
|
1769
|
+
J_halo = jnp.asarray(self.J_halo, dtype=jnp.float64)
|
|
1770
|
+
if J_halo.ndim != 3:
|
|
1771
|
+
raise ValueError(f"LocalMetricGeometry.J_halo must be 3D, got {J_halo.shape}")
|
|
1772
|
+
if tuple(int(v) for v in J_halo.shape) != expected_shape:
|
|
1773
|
+
raise ValueError(
|
|
1774
|
+
"LocalMetricGeometry.J_halo must match the expected halo shape for the location; "
|
|
1775
|
+
f"got {J_halo.shape}, expected {expected_shape} for location={location!r}"
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
object.__setattr__(self, "J_halo", J_halo)
|
|
1779
|
+
for name in (
|
|
1780
|
+
"g11_halo",
|
|
1781
|
+
"g22_halo",
|
|
1782
|
+
"g33_halo",
|
|
1783
|
+
"g12_halo",
|
|
1784
|
+
"g13_halo",
|
|
1785
|
+
"g23_halo",
|
|
1786
|
+
"g_11_halo",
|
|
1787
|
+
"g_22_halo",
|
|
1788
|
+
"g_33_halo",
|
|
1789
|
+
"g_12_halo",
|
|
1790
|
+
"g_13_halo",
|
|
1791
|
+
"g_23_halo",
|
|
1792
|
+
):
|
|
1793
|
+
object.__setattr__(self, name, _require_float_shape(getattr(self, name), expected_shape, f"LocalMetricGeometry.{name}"))
|
|
1794
|
+
object.__setattr__(self, "location", location)
|
|
1795
|
+
|
|
1796
|
+
@property
|
|
1797
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1798
|
+
return tuple(int(v) for v in self.J_halo.shape)
|
|
1799
|
+
|
|
1800
|
+
@property
|
|
1801
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
1802
|
+
return self.shape
|
|
1803
|
+
|
|
1804
|
+
@property
|
|
1805
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
1806
|
+
return self.shape
|
|
1807
|
+
|
|
1808
|
+
@property
|
|
1809
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
1810
|
+
return self.layout.location_owned_shape(self.location)
|
|
1811
|
+
|
|
1812
|
+
@property
|
|
1813
|
+
def owned_slices_in_halo(self) -> tuple[slice, slice, slice]:
|
|
1814
|
+
return self.layout.location_owned_slices(self.location)
|
|
1815
|
+
|
|
1816
|
+
@property
|
|
1817
|
+
def J(self) -> jnp.ndarray:
|
|
1818
|
+
return self.J_halo
|
|
1819
|
+
|
|
1820
|
+
@property
|
|
1821
|
+
def g11(self) -> jnp.ndarray:
|
|
1822
|
+
return self.g11_halo
|
|
1823
|
+
|
|
1824
|
+
@property
|
|
1825
|
+
def g22(self) -> jnp.ndarray:
|
|
1826
|
+
return self.g22_halo
|
|
1827
|
+
|
|
1828
|
+
@property
|
|
1829
|
+
def g33(self) -> jnp.ndarray:
|
|
1830
|
+
return self.g33_halo
|
|
1831
|
+
|
|
1832
|
+
@property
|
|
1833
|
+
def g12(self) -> jnp.ndarray:
|
|
1834
|
+
return self.g12_halo
|
|
1835
|
+
|
|
1836
|
+
@property
|
|
1837
|
+
def g13(self) -> jnp.ndarray:
|
|
1838
|
+
return self.g13_halo
|
|
1839
|
+
|
|
1840
|
+
@property
|
|
1841
|
+
def g23(self) -> jnp.ndarray:
|
|
1842
|
+
return self.g23_halo
|
|
1843
|
+
|
|
1844
|
+
@property
|
|
1845
|
+
def g_11(self) -> jnp.ndarray:
|
|
1846
|
+
return self.g_11_halo
|
|
1847
|
+
|
|
1848
|
+
@property
|
|
1849
|
+
def g_22(self) -> jnp.ndarray:
|
|
1850
|
+
return self.g_22_halo
|
|
1851
|
+
|
|
1852
|
+
@property
|
|
1853
|
+
def g_33(self) -> jnp.ndarray:
|
|
1854
|
+
return self.g_33_halo
|
|
1855
|
+
|
|
1856
|
+
@property
|
|
1857
|
+
def g_12(self) -> jnp.ndarray:
|
|
1858
|
+
return self.g_12_halo
|
|
1859
|
+
|
|
1860
|
+
@property
|
|
1861
|
+
def g_13(self) -> jnp.ndarray:
|
|
1862
|
+
return self.g_13_halo
|
|
1863
|
+
|
|
1864
|
+
@property
|
|
1865
|
+
def g_23(self) -> jnp.ndarray:
|
|
1866
|
+
return self.g_23_halo
|
|
1867
|
+
|
|
1868
|
+
@property
|
|
1869
|
+
def g_contra(self) -> jnp.ndarray:
|
|
1870
|
+
return _metric_from_components(self.g11_halo, self.g22_halo, self.g33_halo, self.g12_halo, self.g13_halo, self.g23_halo)
|
|
1871
|
+
|
|
1872
|
+
@property
|
|
1873
|
+
def g_cov(self) -> jnp.ndarray:
|
|
1874
|
+
return _metric_from_components(self.g_11_halo, self.g_22_halo, self.g_33_halo, self.g_12_halo, self.g_13_halo, self.g_23_halo)
|
|
1875
|
+
|
|
1876
|
+
@property
|
|
1877
|
+
def g_contra_owned(self) -> jnp.ndarray:
|
|
1878
|
+
s = self.owned_slices_in_halo
|
|
1879
|
+
return _metric_from_components(
|
|
1880
|
+
self.g11_halo[s],
|
|
1881
|
+
self.g22_halo[s],
|
|
1882
|
+
self.g33_halo[s],
|
|
1883
|
+
self.g12_halo[s],
|
|
1884
|
+
self.g13_halo[s],
|
|
1885
|
+
self.g23_halo[s],
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
@property
|
|
1889
|
+
def g_cov_owned(self) -> jnp.ndarray:
|
|
1890
|
+
s = self.owned_slices_in_halo
|
|
1891
|
+
return _metric_from_components(
|
|
1892
|
+
self.g_11_halo[s],
|
|
1893
|
+
self.g_22_halo[s],
|
|
1894
|
+
self.g_33_halo[s],
|
|
1895
|
+
self.g_12_halo[s],
|
|
1896
|
+
self.g_13_halo[s],
|
|
1897
|
+
self.g_23_halo[s],
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
@property
|
|
1901
|
+
def J_owned(self) -> jnp.ndarray:
|
|
1902
|
+
return self.J_halo[self.owned_slices_in_halo]
|
|
1903
|
+
|
|
1904
|
+
@property
|
|
1905
|
+
def g11_owned(self) -> jnp.ndarray:
|
|
1906
|
+
return self.g11_halo[self.owned_slices_in_halo]
|
|
1907
|
+
|
|
1908
|
+
@property
|
|
1909
|
+
def g22_owned(self) -> jnp.ndarray:
|
|
1910
|
+
return self.g22_halo[self.owned_slices_in_halo]
|
|
1911
|
+
|
|
1912
|
+
@property
|
|
1913
|
+
def g33_owned(self) -> jnp.ndarray:
|
|
1914
|
+
return self.g33_halo[self.owned_slices_in_halo]
|
|
1915
|
+
|
|
1916
|
+
@property
|
|
1917
|
+
def g12_owned(self) -> jnp.ndarray:
|
|
1918
|
+
return self.g12_halo[self.owned_slices_in_halo]
|
|
1919
|
+
|
|
1920
|
+
@property
|
|
1921
|
+
def g13_owned(self) -> jnp.ndarray:
|
|
1922
|
+
return self.g13_halo[self.owned_slices_in_halo]
|
|
1923
|
+
|
|
1924
|
+
@property
|
|
1925
|
+
def g23_owned(self) -> jnp.ndarray:
|
|
1926
|
+
return self.g23_halo[self.owned_slices_in_halo]
|
|
1927
|
+
|
|
1928
|
+
@property
|
|
1929
|
+
def g_11_owned(self) -> jnp.ndarray:
|
|
1930
|
+
return self.g_11_halo[self.owned_slices_in_halo]
|
|
1931
|
+
|
|
1932
|
+
@property
|
|
1933
|
+
def g_22_owned(self) -> jnp.ndarray:
|
|
1934
|
+
return self.g_22_halo[self.owned_slices_in_halo]
|
|
1935
|
+
|
|
1936
|
+
@property
|
|
1937
|
+
def g_33_owned(self) -> jnp.ndarray:
|
|
1938
|
+
return self.g_33_halo[self.owned_slices_in_halo]
|
|
1939
|
+
|
|
1940
|
+
@property
|
|
1941
|
+
def g_12_owned(self) -> jnp.ndarray:
|
|
1942
|
+
return self.g_12_halo[self.owned_slices_in_halo]
|
|
1943
|
+
|
|
1944
|
+
@property
|
|
1945
|
+
def g_13_owned(self) -> jnp.ndarray:
|
|
1946
|
+
return self.g_13_halo[self.owned_slices_in_halo]
|
|
1947
|
+
|
|
1948
|
+
@property
|
|
1949
|
+
def g_23_owned(self) -> jnp.ndarray:
|
|
1950
|
+
return self.g_23_halo[self.owned_slices_in_halo]
|
|
1951
|
+
|
|
1952
|
+
@_pytree_base
|
|
1953
|
+
@dataclass(frozen=True)
|
|
1954
|
+
class MetricGeometry(_DataclassPyTreeMixin):
|
|
1955
|
+
"""Metric/Jacobian data on one grid location family.
|
|
1956
|
+
This class is used both for cell centers and for each face family."""
|
|
1957
|
+
J: jnp.ndarray
|
|
1958
|
+
g11: jnp.ndarray
|
|
1959
|
+
g22: jnp.ndarray
|
|
1960
|
+
g33: jnp.ndarray
|
|
1961
|
+
g12: jnp.ndarray
|
|
1962
|
+
g13: jnp.ndarray
|
|
1963
|
+
g23: jnp.ndarray
|
|
1964
|
+
g_11: jnp.ndarray
|
|
1965
|
+
g_22: jnp.ndarray
|
|
1966
|
+
g_33: jnp.ndarray
|
|
1967
|
+
g_12: jnp.ndarray
|
|
1968
|
+
g_13: jnp.ndarray
|
|
1969
|
+
g_23: jnp.ndarray
|
|
1970
|
+
def __post_init__(self) -> None:
|
|
1971
|
+
J = jnp.asarray(self.J, dtype=jnp.float64)
|
|
1972
|
+
if J.ndim != 3:
|
|
1973
|
+
raise ValueError(f"MetricGeometry.J must have shape (a, b, c), got {J.shape}")
|
|
1974
|
+
shape = tuple(int(v) for v in J.shape)
|
|
1975
|
+
object.__setattr__(self, "J", J)
|
|
1976
|
+
for name in ("g11", "g22", "g33", "g12", "g13", "g23", "g_11", "g_22", "g_33", "g_12", "g_13", "g_23"):
|
|
1977
|
+
value = _require_float_shape(getattr(self, name), shape, f"MetricGeometry.{name}")
|
|
1978
|
+
object.__setattr__(self, name, value)
|
|
1979
|
+
@property
|
|
1980
|
+
def shape(self) -> tuple[int, int, int]:
|
|
1981
|
+
return tuple(int(v) for v in self.J.shape)
|
|
1982
|
+
@property
|
|
1983
|
+
def g_contra(self) -> jnp.ndarray:
|
|
1984
|
+
return _metric_from_components(self.g11, self.g22, self.g33, self.g12, self.g13, self.g23)
|
|
1985
|
+
@property
|
|
1986
|
+
def g_cov(self) -> jnp.ndarray:
|
|
1987
|
+
return _metric_from_components(self.g_11, self.g_22, self.g_33, self.g_12, self.g_13, self.g_23)
|
|
1988
|
+
|
|
1989
|
+
@_pytree_base
|
|
1990
|
+
@dataclass(frozen=True)
|
|
1991
|
+
class FaceMetricGeometry(_DataclassPyTreeMixin):
|
|
1992
|
+
x: MetricGeometry # (nx + 1, ny, nz)
|
|
1993
|
+
y: MetricGeometry # (nx, ny + 1, nz)
|
|
1994
|
+
z: MetricGeometry # (nx, ny, nz + 1)
|
|
1995
|
+
@property
|
|
1996
|
+
def axes(self) -> tuple[MetricGeometry, MetricGeometry, MetricGeometry]:
|
|
1997
|
+
return self.x, self.y, self.z
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
@_pytree_base
|
|
2001
|
+
@dataclass(frozen=True)
|
|
2002
|
+
class LocalFaceMetricGeometry(_DataclassPyTreeMixin):
|
|
2003
|
+
"""Local metric bundles on the x/y/z face families.
|
|
2004
|
+
|
|
2005
|
+
Each field stores a halo-padded local metric object for that face family.
|
|
2006
|
+
The shape annotations below describe the expected local array extent
|
|
2007
|
+
when the underlying face family is built from a shard-local owned region
|
|
2008
|
+
with halo width `h`.
|
|
2009
|
+
"""
|
|
2010
|
+
|
|
2011
|
+
layout: HaloLayout3D
|
|
2012
|
+
x: LocalMetricGeometry # (nx_owned + 2*h + 1, ny_owned + 2*h, nz_owned + 2*h)
|
|
2013
|
+
y: LocalMetricGeometry # (nx_owned + 2*h, ny_owned + 2*h + 1, nz_owned + 2*h)
|
|
2014
|
+
z: LocalMetricGeometry # (nx_owned + 2*h, ny_owned + 2*h, nz_owned + 2*h + 1)
|
|
2015
|
+
|
|
2016
|
+
def __post_init__(self) -> None:
|
|
2017
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
2018
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
2019
|
+
if not isinstance(self.x, LocalMetricGeometry):
|
|
2020
|
+
raise TypeError("x must be a LocalMetricGeometry instance")
|
|
2021
|
+
if not isinstance(self.y, LocalMetricGeometry):
|
|
2022
|
+
raise TypeError("y must be a LocalMetricGeometry instance")
|
|
2023
|
+
if not isinstance(self.z, LocalMetricGeometry):
|
|
2024
|
+
raise TypeError("z must be a LocalMetricGeometry instance")
|
|
2025
|
+
if self.x.layout != self.layout or self.y.layout != self.layout or self.z.layout != self.layout:
|
|
2026
|
+
raise ValueError("LocalFaceMetricGeometry axes must share the same HaloLayout3D")
|
|
2027
|
+
if self.x.location != "x_face":
|
|
2028
|
+
raise ValueError(f"LocalFaceMetricGeometry.x.location must be 'x_face', got {self.x.location!r}")
|
|
2029
|
+
if self.y.location != "y_face":
|
|
2030
|
+
raise ValueError(f"LocalFaceMetricGeometry.y.location must be 'y_face', got {self.y.location!r}")
|
|
2031
|
+
if self.z.location != "z_face":
|
|
2032
|
+
raise ValueError(f"LocalFaceMetricGeometry.z.location must be 'z_face', got {self.z.location!r}")
|
|
2033
|
+
|
|
2034
|
+
@property
|
|
2035
|
+
def axes(self) -> tuple[LocalMetricGeometry, LocalMetricGeometry, LocalMetricGeometry]:
|
|
2036
|
+
return self.x, self.y, self.z
|
|
2037
|
+
|
|
2038
|
+
@property
|
|
2039
|
+
def shape(self) -> tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]:
|
|
2040
|
+
return self.x.shape, self.y.shape, self.z.shape
|
|
2041
|
+
|
|
2042
|
+
@_pytree_base
|
|
2043
|
+
@dataclass(frozen=True)
|
|
2044
|
+
class BFieldGeometry(_DataclassPyTreeMixin):
|
|
2045
|
+
B_contra: jnp.ndarray
|
|
2046
|
+
Bmag: jnp.ndarray
|
|
2047
|
+
def __post_init__(self) -> None:
|
|
2048
|
+
B_contra = jnp.asarray(self.B_contra, dtype=jnp.float64)
|
|
2049
|
+
if B_contra.ndim != 4 or B_contra.shape[-1] != 3:
|
|
2050
|
+
raise ValueError(f"BFieldGeometry.B_contra must have shape (a, b, c, 3), got {B_contra.shape}")
|
|
2051
|
+
shape = tuple(int(v) for v in B_contra.shape[:-1])
|
|
2052
|
+
Bmag = _require_float_shape(self.Bmag, shape, "BFieldGeometry.Bmag")
|
|
2053
|
+
object.__setattr__(self, "B_contra", B_contra)
|
|
2054
|
+
object.__setattr__(self, "Bmag", Bmag)
|
|
2055
|
+
|
|
2056
|
+
@property
|
|
2057
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2058
|
+
return tuple(int(v) for v in self.Bmag.shape)
|
|
2059
|
+
|
|
2060
|
+
@property
|
|
2061
|
+
def b_contra(self) -> jnp.ndarray:
|
|
2062
|
+
return self.B_contra / self.Bmag[..., None]
|
|
2063
|
+
|
|
2064
|
+
@_pytree_base
|
|
2065
|
+
@dataclass(frozen=True)
|
|
2066
|
+
class FaceBFieldGeometry(_DataclassPyTreeMixin):
|
|
2067
|
+
x: BFieldGeometry # (nx + 1, ny, nz, 3)
|
|
2068
|
+
y: BFieldGeometry # (nx, ny + 1, nz, 3)
|
|
2069
|
+
z: BFieldGeometry # (nx, ny, nz + 1, 3)
|
|
2070
|
+
|
|
2071
|
+
@property
|
|
2072
|
+
def axes(self) -> tuple[BFieldGeometry, BFieldGeometry, BFieldGeometry]:
|
|
2073
|
+
return self.x, self.y, self.z
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
@_pytree_base
|
|
2077
|
+
@dataclass(frozen=True)
|
|
2078
|
+
class LocalBFieldGeometry(_DataclassPyTreeMixin):
|
|
2079
|
+
"""Local magnetic field bundle on a halo-padded cell or face grid."""
|
|
2080
|
+
|
|
2081
|
+
layout: HaloLayout3D
|
|
2082
|
+
B_contra_halo: jnp.ndarray # (..., 3) with leading halo_shape / face_halo_shape
|
|
2083
|
+
Bmag_halo: jnp.ndarray # halo_shape / face_halo_shape
|
|
2084
|
+
location: str # "cell", "x_face", "y_face", or "z_face"
|
|
2085
|
+
|
|
2086
|
+
def __post_init__(self) -> None:
|
|
2087
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
2088
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
2089
|
+
location = str(self.location)
|
|
2090
|
+
expected_shape = self.layout.location_halo_shape(location)
|
|
2091
|
+
B_contra_halo = jnp.asarray(self.B_contra_halo, dtype=jnp.float64)
|
|
2092
|
+
if B_contra_halo.ndim != 4 or B_contra_halo.shape[-1] != 3:
|
|
2093
|
+
raise ValueError(
|
|
2094
|
+
"LocalBFieldGeometry.B_contra_halo must have shape "
|
|
2095
|
+
f"{expected_shape + (3,)}, got {B_contra_halo.shape}"
|
|
2096
|
+
)
|
|
2097
|
+
if tuple(int(v) for v in B_contra_halo.shape[:-1]) != expected_shape:
|
|
2098
|
+
raise ValueError(
|
|
2099
|
+
"LocalBFieldGeometry.B_contra_halo must match the expected halo shape for the location; "
|
|
2100
|
+
f"got {B_contra_halo.shape[:-1]}, expected {expected_shape} for location={location!r}"
|
|
2101
|
+
)
|
|
2102
|
+
|
|
2103
|
+
object.__setattr__(self, "B_contra_halo", B_contra_halo)
|
|
2104
|
+
object.__setattr__(self, "Bmag_halo", _require_float_shape(self.Bmag_halo, expected_shape, "LocalBFieldGeometry.Bmag_halo"))
|
|
2105
|
+
object.__setattr__(self, "location", location)
|
|
2106
|
+
|
|
2107
|
+
@property
|
|
2108
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2109
|
+
return tuple(int(v) for v in self.Bmag_halo.shape)
|
|
2110
|
+
|
|
2111
|
+
@property
|
|
2112
|
+
def halo_shape(self) -> tuple[int, int, int]:
|
|
2113
|
+
return self.shape
|
|
2114
|
+
|
|
2115
|
+
@property
|
|
2116
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
2117
|
+
return self.shape
|
|
2118
|
+
|
|
2119
|
+
@property
|
|
2120
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
2121
|
+
return self.layout.location_owned_shape(self.location)
|
|
2122
|
+
|
|
2123
|
+
@property
|
|
2124
|
+
def owned_slices_in_halo(self) -> tuple[slice, slice, slice]:
|
|
2125
|
+
return self.layout.location_owned_slices(self.location)
|
|
2126
|
+
|
|
2127
|
+
@property
|
|
2128
|
+
def B_contra(self) -> jnp.ndarray:
|
|
2129
|
+
return self.B_contra_halo
|
|
2130
|
+
|
|
2131
|
+
@property
|
|
2132
|
+
def Bmag(self) -> jnp.ndarray:
|
|
2133
|
+
return self.Bmag_halo
|
|
2134
|
+
|
|
2135
|
+
@property
|
|
2136
|
+
def b_contra(self) -> jnp.ndarray:
|
|
2137
|
+
return self.B_contra_halo / self.Bmag_halo[..., None]
|
|
2138
|
+
|
|
2139
|
+
@property
|
|
2140
|
+
def B_contra_owned(self) -> jnp.ndarray:
|
|
2141
|
+
return self.B_contra_halo[self.owned_slices_in_halo]
|
|
2142
|
+
|
|
2143
|
+
@property
|
|
2144
|
+
def Bmag_owned(self) -> jnp.ndarray:
|
|
2145
|
+
return self.Bmag_halo[self.owned_slices_in_halo]
|
|
2146
|
+
|
|
2147
|
+
|
|
2148
|
+
@_pytree_base
|
|
2149
|
+
@dataclass(frozen=True)
|
|
2150
|
+
class LocalFaceBFieldGeometry(_DataclassPyTreeMixin):
|
|
2151
|
+
"""Local B-field bundles on the x/y/z face families.
|
|
2152
|
+
|
|
2153
|
+
Shape annotations:
|
|
2154
|
+
x: (nx_owned + 2*h + 1, ny_owned + 2*h, nz_owned + 2*h, 3)
|
|
2155
|
+
y: (nx_owned + 2*h, ny_owned + 2*h + 1, nz_owned + 2*h, 3)
|
|
2156
|
+
z: (nx_owned + 2*h, ny_owned + 2*h, nz_owned + 2*h + 1, 3)
|
|
2157
|
+
"""
|
|
2158
|
+
|
|
2159
|
+
layout: HaloLayout3D
|
|
2160
|
+
x: LocalBFieldGeometry # (nx_owned + 2*h + 1, ny_owned + 2*h, nz_owned + 2*h, 3)
|
|
2161
|
+
y: LocalBFieldGeometry # (nx_owned + 2*h, ny_owned + 2*h + 1, nz_owned + 2*h, 3)
|
|
2162
|
+
z: LocalBFieldGeometry # (nx_owned + 2*h, ny_owned + 2*h, nz_owned + 2*h + 1, 3)
|
|
2163
|
+
|
|
2164
|
+
def __post_init__(self) -> None:
|
|
2165
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
2166
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
2167
|
+
if not isinstance(self.x, LocalBFieldGeometry):
|
|
2168
|
+
raise TypeError("x must be a LocalBFieldGeometry instance")
|
|
2169
|
+
if not isinstance(self.y, LocalBFieldGeometry):
|
|
2170
|
+
raise TypeError("y must be a LocalBFieldGeometry instance")
|
|
2171
|
+
if not isinstance(self.z, LocalBFieldGeometry):
|
|
2172
|
+
raise TypeError("z must be a LocalBFieldGeometry instance")
|
|
2173
|
+
if self.x.layout != self.layout or self.y.layout != self.layout or self.z.layout != self.layout:
|
|
2174
|
+
raise ValueError("LocalFaceBFieldGeometry axes must share the same HaloLayout3D")
|
|
2175
|
+
if self.x.location != "x_face":
|
|
2176
|
+
raise ValueError(f"LocalFaceBFieldGeometry.x.location must be 'x_face', got {self.x.location!r}")
|
|
2177
|
+
if self.y.location != "y_face":
|
|
2178
|
+
raise ValueError(f"LocalFaceBFieldGeometry.y.location must be 'y_face', got {self.y.location!r}")
|
|
2179
|
+
if self.z.location != "z_face":
|
|
2180
|
+
raise ValueError(f"LocalFaceBFieldGeometry.z.location must be 'z_face', got {self.z.location!r}")
|
|
2181
|
+
|
|
2182
|
+
@property
|
|
2183
|
+
def axes(self) -> tuple[LocalBFieldGeometry, LocalBFieldGeometry, LocalBFieldGeometry]:
|
|
2184
|
+
return self.x, self.y, self.z
|
|
2185
|
+
|
|
2186
|
+
@property
|
|
2187
|
+
def shape(self) -> tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]:
|
|
2188
|
+
return self.x.shape, self.y.shape, self.z.shape
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
@_pytree_base
|
|
2192
|
+
@dataclass(frozen=True)
|
|
2193
|
+
class LocalRegularFaceGeometry3D(_DataclassPyTreeMixin):
|
|
2194
|
+
"""Shard-local regular face measures for conservative fluxes.
|
|
2195
|
+
|
|
2196
|
+
Design notes
|
|
2197
|
+
------------
|
|
2198
|
+
Unlike the reconstruction and metric objects, this is intentionally
|
|
2199
|
+
owned-face only. Conservative flux operators only need the faces that
|
|
2200
|
+
belong to the shard, so we do not store halo-padded face measures here.
|
|
2201
|
+
The shared `HaloLayout3D` still lives on the object so the owned face
|
|
2202
|
+
shapes remain tied to the shard decomposition in one place.
|
|
2203
|
+
"""
|
|
2204
|
+
|
|
2205
|
+
layout: HaloLayout3D
|
|
2206
|
+
x_area: jnp.ndarray # (nx_owned + 1, ny_owned, nz_owned)
|
|
2207
|
+
y_area: jnp.ndarray # (nx_owned, ny_owned + 1, nz_owned)
|
|
2208
|
+
z_area: jnp.ndarray # (nx_owned, ny_owned, nz_owned + 1)
|
|
2209
|
+
x_area_fraction: jnp.ndarray # (nx_owned + 1, ny_owned, nz_owned)
|
|
2210
|
+
y_area_fraction: jnp.ndarray # (nx_owned, ny_owned + 1, nz_owned)
|
|
2211
|
+
z_area_fraction: jnp.ndarray # (nx_owned, ny_owned, nz_owned + 1)
|
|
2212
|
+
x_open_mask: jnp.ndarray # (nx_owned + 1, ny_owned, nz_owned)
|
|
2213
|
+
y_open_mask: jnp.ndarray # (nx_owned, ny_owned + 1, nz_owned)
|
|
2214
|
+
z_open_mask: jnp.ndarray # (nx_owned, ny_owned, nz_owned + 1)
|
|
2215
|
+
|
|
2216
|
+
def __post_init__(self) -> None:
|
|
2217
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
2218
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
2219
|
+
|
|
2220
|
+
expected_x = self.layout.face_control_shape(0)
|
|
2221
|
+
expected_y = self.layout.face_control_shape(1)
|
|
2222
|
+
expected_z = self.layout.face_control_shape(2)
|
|
2223
|
+
|
|
2224
|
+
x_area = jnp.asarray(self.x_area, dtype=jnp.float64)
|
|
2225
|
+
y_area = jnp.asarray(self.y_area, dtype=jnp.float64)
|
|
2226
|
+
z_area = jnp.asarray(self.z_area, dtype=jnp.float64)
|
|
2227
|
+
if x_area.shape != expected_x or y_area.shape != expected_y or z_area.shape != expected_z:
|
|
2228
|
+
raise ValueError(
|
|
2229
|
+
"LocalRegularFaceGeometry3D face shapes must match the owned face layout; "
|
|
2230
|
+
f"expected x={expected_x}, y={expected_y}, z={expected_z}, got "
|
|
2231
|
+
f"x={x_area.shape}, y={y_area.shape}, z={z_area.shape}"
|
|
2232
|
+
)
|
|
2233
|
+
|
|
2234
|
+
object.__setattr__(self, "x_area", x_area)
|
|
2235
|
+
object.__setattr__(self, "y_area", y_area)
|
|
2236
|
+
object.__setattr__(self, "z_area", z_area)
|
|
2237
|
+
object.__setattr__(self, "x_area_fraction", _require_float_shape(self.x_area_fraction, expected_x, "LocalRegularFaceGeometry3D.x_area_fraction"))
|
|
2238
|
+
object.__setattr__(self, "y_area_fraction", _require_float_shape(self.y_area_fraction, expected_y, "LocalRegularFaceGeometry3D.y_area_fraction"))
|
|
2239
|
+
object.__setattr__(self, "z_area_fraction", _require_float_shape(self.z_area_fraction, expected_z, "LocalRegularFaceGeometry3D.z_area_fraction"))
|
|
2240
|
+
for name, shape in (("x_open_mask", expected_x), ("y_open_mask", expected_y), ("z_open_mask", expected_z)):
|
|
2241
|
+
value = jnp.asarray(getattr(self, name), dtype=bool)
|
|
2242
|
+
if value.shape != shape:
|
|
2243
|
+
raise ValueError(f"LocalRegularFaceGeometry3D.{name} must have shape {shape}, got {value.shape}")
|
|
2244
|
+
object.__setattr__(self, name, value)
|
|
2245
|
+
|
|
2246
|
+
@property
|
|
2247
|
+
def axes(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
2248
|
+
return self.x_area, self.y_area, self.z_area
|
|
2249
|
+
|
|
2250
|
+
@property
|
|
2251
|
+
def shape(self) -> tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]:
|
|
2252
|
+
return self.x_area.shape, self.y_area.shape, self.z_area.shape
|
|
2253
|
+
|
|
2254
|
+
@property
|
|
2255
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
2256
|
+
return self.layout.owned_shape
|
|
2257
|
+
|
|
2258
|
+
@property
|
|
2259
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
2260
|
+
return self.layout.cell_halo_shape
|
|
2261
|
+
|
|
2262
|
+
|
|
2263
|
+
@_pytree_base
|
|
2264
|
+
@dataclass(frozen=True)
|
|
2265
|
+
class RegularFaceGeometry3D(_DataclassPyTreeMixin):
|
|
2266
|
+
"""Regular coordinate-face measures for conservative fluxes."""
|
|
2267
|
+
|
|
2268
|
+
x_area: jnp.ndarray
|
|
2269
|
+
y_area: jnp.ndarray
|
|
2270
|
+
z_area: jnp.ndarray
|
|
2271
|
+
x_area_fraction: jnp.ndarray
|
|
2272
|
+
y_area_fraction: jnp.ndarray
|
|
2273
|
+
z_area_fraction: jnp.ndarray
|
|
2274
|
+
x_open_mask: jnp.ndarray
|
|
2275
|
+
y_open_mask: jnp.ndarray
|
|
2276
|
+
z_open_mask: jnp.ndarray
|
|
2277
|
+
|
|
2278
|
+
def __post_init__(self) -> None:
|
|
2279
|
+
x_area = jnp.asarray(self.x_area, dtype=jnp.float64)
|
|
2280
|
+
y_area = jnp.asarray(self.y_area, dtype=jnp.float64)
|
|
2281
|
+
z_area = jnp.asarray(self.z_area, dtype=jnp.float64)
|
|
2282
|
+
if x_area.ndim != 3 or y_area.ndim != 3 or z_area.ndim != 3:
|
|
2283
|
+
raise ValueError(
|
|
2284
|
+
"RegularFaceGeometry3D areas must be 3D arrays with face-grid shapes"
|
|
2285
|
+
)
|
|
2286
|
+
|
|
2287
|
+
x_shape = tuple(int(v) for v in x_area.shape)
|
|
2288
|
+
y_shape = tuple(int(v) for v in y_area.shape)
|
|
2289
|
+
z_shape = tuple(int(v) for v in z_area.shape)
|
|
2290
|
+
cell_shape = (x_shape[0] - 1, y_shape[1] - 1, z_shape[2] - 1)
|
|
2291
|
+
expected_x = (cell_shape[0] + 1, cell_shape[1], cell_shape[2])
|
|
2292
|
+
expected_y = (cell_shape[0], cell_shape[1] + 1, cell_shape[2])
|
|
2293
|
+
expected_z = (cell_shape[0], cell_shape[1], cell_shape[2] + 1)
|
|
2294
|
+
if x_shape != expected_x or y_shape != expected_y or z_shape != expected_z:
|
|
2295
|
+
raise ValueError(
|
|
2296
|
+
"RegularFaceGeometry3D face shapes must match the face-grid layout; "
|
|
2297
|
+
f"expected x={expected_x}, y={expected_y}, z={expected_z}, got "
|
|
2298
|
+
f"x={x_shape}, y={y_shape}, z={z_shape}"
|
|
2299
|
+
)
|
|
2300
|
+
|
|
2301
|
+
object.__setattr__(self, "x_area", x_area)
|
|
2302
|
+
object.__setattr__(self, "y_area", y_area)
|
|
2303
|
+
object.__setattr__(self, "z_area", z_area)
|
|
2304
|
+
object.__setattr__(self, "x_area_fraction", _require_float_shape(self.x_area_fraction, x_shape, "RegularFaceGeometry3D.x_area_fraction"))
|
|
2305
|
+
object.__setattr__(self, "y_area_fraction", _require_float_shape(self.y_area_fraction, y_shape, "RegularFaceGeometry3D.y_area_fraction"))
|
|
2306
|
+
object.__setattr__(self, "z_area_fraction", _require_float_shape(self.z_area_fraction, z_shape, "RegularFaceGeometry3D.z_area_fraction"))
|
|
2307
|
+
for name, shape in (("x_open_mask", x_shape), ("y_open_mask", y_shape), ("z_open_mask", z_shape)):
|
|
2308
|
+
value = jnp.asarray(getattr(self, name), dtype=bool)
|
|
2309
|
+
if value.shape != shape:
|
|
2310
|
+
raise ValueError(f"RegularFaceGeometry3D.{name} must have shape {shape}, got {value.shape}")
|
|
2311
|
+
object.__setattr__(self, name, value)
|
|
2312
|
+
|
|
2313
|
+
@classmethod
|
|
2314
|
+
def unit(cls, geometry: "FciGeometry3D") -> "RegularFaceGeometry3D":
|
|
2315
|
+
shape = geometry.shape
|
|
2316
|
+
x_shape = (shape[0] + 1, shape[1], shape[2])
|
|
2317
|
+
y_shape = (shape[0], shape[1] + 1, shape[2])
|
|
2318
|
+
z_shape = (shape[0], shape[1], shape[2] + 1)
|
|
2319
|
+
return cls(
|
|
2320
|
+
x_area=jnp.ones(x_shape, dtype=jnp.float64),
|
|
2321
|
+
y_area=jnp.ones(y_shape, dtype=jnp.float64),
|
|
2322
|
+
z_area=jnp.ones(z_shape, dtype=jnp.float64),
|
|
2323
|
+
x_area_fraction=jnp.ones(x_shape, dtype=jnp.float64),
|
|
2324
|
+
y_area_fraction=jnp.ones(y_shape, dtype=jnp.float64),
|
|
2325
|
+
z_area_fraction=jnp.ones(z_shape, dtype=jnp.float64),
|
|
2326
|
+
x_open_mask=jnp.ones(x_shape, dtype=bool),
|
|
2327
|
+
y_open_mask=jnp.ones(y_shape, dtype=bool),
|
|
2328
|
+
z_open_mask=jnp.ones(z_shape, dtype=bool),
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
@property
|
|
2332
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2333
|
+
return (int(self.x_area.shape[0] - 1), int(self.y_area.shape[1] - 1), int(self.z_area.shape[2] - 1))
|
|
2334
|
+
|
|
2335
|
+
|
|
2336
|
+
@_pytree_base
|
|
2337
|
+
@dataclass(frozen=True)
|
|
2338
|
+
class CellVolumeGeometry3D(_DataclassPyTreeMixin):
|
|
2339
|
+
"""Effective cell-volume measure for conservative operators."""
|
|
2340
|
+
|
|
2341
|
+
volume: jnp.ndarray
|
|
2342
|
+
volume_fraction: jnp.ndarray
|
|
2343
|
+
|
|
2344
|
+
def __post_init__(self) -> None:
|
|
2345
|
+
volume = jnp.asarray(self.volume, dtype=jnp.float64)
|
|
2346
|
+
if volume.ndim != 3:
|
|
2347
|
+
raise ValueError(f"CellVolumeGeometry3D.volume must be 3D, got {volume.shape}")
|
|
2348
|
+
shape = tuple(int(v) for v in volume.shape)
|
|
2349
|
+
object.__setattr__(self, "volume", volume)
|
|
2350
|
+
object.__setattr__(self, "volume_fraction", _require_float_shape(self.volume_fraction, shape, "CellVolumeGeometry3D.volume_fraction"))
|
|
2351
|
+
|
|
2352
|
+
@classmethod
|
|
2353
|
+
def unit(cls, geometry: "FciGeometry3D") -> "CellVolumeGeometry3D":
|
|
2354
|
+
volume = jnp.asarray(geometry.cell_metric.J, dtype=jnp.float64)
|
|
2355
|
+
return cls(volume=volume, volume_fraction=jnp.ones_like(volume, dtype=jnp.float64))
|
|
2356
|
+
|
|
2357
|
+
@property
|
|
2358
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2359
|
+
return tuple(int(v) for v in self.volume.shape)
|
|
2360
|
+
|
|
2361
|
+
|
|
2362
|
+
@_pytree_base
|
|
2363
|
+
@dataclass(frozen=True)
|
|
2364
|
+
class LocalCellVolumeGeometry3D(_DataclassPyTreeMixin):
|
|
2365
|
+
"""Shard-local cell-volume measure for conservative operators.
|
|
2366
|
+
|
|
2367
|
+
Design notes
|
|
2368
|
+
------------
|
|
2369
|
+
This object is intentionally owned-cell only.
|
|
2370
|
+
Conservative operators use it to normalize or weight the cells that the
|
|
2371
|
+
shard owns; halo exchange is handled separately by the stencil/field
|
|
2372
|
+
objects, not by the volume measure itself.
|
|
2373
|
+
"""
|
|
2374
|
+
|
|
2375
|
+
layout: HaloLayout3D
|
|
2376
|
+
volume: jnp.ndarray # (nx_owned, ny_owned, nz_owned)
|
|
2377
|
+
volume_fraction: jnp.ndarray # (nx_owned, ny_owned, nz_owned)
|
|
2378
|
+
|
|
2379
|
+
def __post_init__(self) -> None:
|
|
2380
|
+
if not isinstance(self.layout, HaloLayout3D):
|
|
2381
|
+
raise TypeError("layout must be a HaloLayout3D instance")
|
|
2382
|
+
expected_shape = self.layout.owned_shape
|
|
2383
|
+
volume = jnp.asarray(self.volume, dtype=jnp.float64)
|
|
2384
|
+
if volume.ndim != 3:
|
|
2385
|
+
raise ValueError(f"LocalCellVolumeGeometry3D.volume must be 3D, got {volume.shape}")
|
|
2386
|
+
if tuple(int(v) for v in volume.shape) != expected_shape:
|
|
2387
|
+
raise ValueError(
|
|
2388
|
+
"LocalCellVolumeGeometry3D.volume must match layout.owned_shape; "
|
|
2389
|
+
f"got {volume.shape}, expected {expected_shape}"
|
|
2390
|
+
)
|
|
2391
|
+
object.__setattr__(self, "volume", volume)
|
|
2392
|
+
object.__setattr__(
|
|
2393
|
+
self,
|
|
2394
|
+
"volume_fraction",
|
|
2395
|
+
_require_float_shape(self.volume_fraction, expected_shape, "LocalCellVolumeGeometry3D.volume_fraction"),
|
|
2396
|
+
)
|
|
2397
|
+
|
|
2398
|
+
@property
|
|
2399
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2400
|
+
return self.layout.owned_shape
|
|
2401
|
+
|
|
2402
|
+
@property
|
|
2403
|
+
def owned_shape(self) -> tuple[int, int, int]:
|
|
2404
|
+
return self.layout.owned_shape
|
|
2405
|
+
|
|
2406
|
+
@property
|
|
2407
|
+
def local_owned_shape(self) -> tuple[int, int, int]:
|
|
2408
|
+
return self.layout.owned_shape
|
|
2409
|
+
|
|
2410
|
+
@property
|
|
2411
|
+
def local_halo_shape(self) -> tuple[int, int, int]:
|
|
2412
|
+
return self.layout.cell_halo_shape
|
|
2413
|
+
|
|
2414
|
+
@property
|
|
2415
|
+
def volume_owned(self) -> jnp.ndarray:
|
|
2416
|
+
return self.volume
|
|
2417
|
+
|
|
2418
|
+
@property
|
|
2419
|
+
def volume_fraction_owned(self) -> jnp.ndarray:
|
|
2420
|
+
return self.volume_fraction
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
@_pytree_base
|
|
2424
|
+
@dataclass(frozen=True)
|
|
2425
|
+
class FciGeometry3D(_DataclassPyTreeMixin):
|
|
2426
|
+
"""Centralized cell-centered FCI geometry payload.
|
|
2427
|
+
Geometry is stored only on active cell centers and physical faces.
|
|
2428
|
+
Computational ghost cells are not part of this object.
|
|
2429
|
+
Native FCI coordinates:
|
|
2430
|
+
(x, y, z) = (radial, poloidal, toroidal)
|
|
2431
|
+
"""
|
|
2432
|
+
grid: CellCenteredGrid3D
|
|
2433
|
+
maps: FciMaps3D
|
|
2434
|
+
spacing: Spacing3D
|
|
2435
|
+
cell_metric: MetricGeometry
|
|
2436
|
+
face_metric: FaceMetricGeometry
|
|
2437
|
+
cell_bfield: BFieldGeometry
|
|
2438
|
+
face_bfield: FaceBFieldGeometry
|
|
2439
|
+
|
|
2440
|
+
def __post_init__(self) -> None:
|
|
2441
|
+
shape = self.grid.shape
|
|
2442
|
+
xface_shape = (shape[0] + 1, shape[1], shape[2])
|
|
2443
|
+
yface_shape = (shape[0], shape[1] + 1, shape[2])
|
|
2444
|
+
zface_shape = (shape[0], shape[1], shape[2] + 1)
|
|
2445
|
+
if self.maps.shape != shape:
|
|
2446
|
+
raise ValueError(f"maps shape must be {shape}, got {self.maps.shape}")
|
|
2447
|
+
if self.spacing.shape != shape:
|
|
2448
|
+
raise ValueError(f"spacing shape must be {shape}, got {self.spacing.shape}")
|
|
2449
|
+
if self.cell_metric.shape != shape:
|
|
2450
|
+
raise ValueError(f"cell_metric shape must be {shape}, got {self.cell_metric.shape}")
|
|
2451
|
+
if self.cell_bfield.shape != shape:
|
|
2452
|
+
raise ValueError(f"cell_bfield shape must be {shape}, got {self.cell_bfield.shape}")
|
|
2453
|
+
if self.face_metric.x.shape != xface_shape:
|
|
2454
|
+
raise ValueError(f"face_metric.x shape must be {xface_shape}, got {self.face_metric.x.shape}")
|
|
2455
|
+
if self.face_metric.y.shape != yface_shape:
|
|
2456
|
+
raise ValueError(f"face_metric.y shape must be {yface_shape}, got {self.face_metric.y.shape}")
|
|
2457
|
+
if self.face_metric.z.shape != zface_shape:
|
|
2458
|
+
raise ValueError(f"face_metric.z shape must be {zface_shape}, got {self.face_metric.z.shape}")
|
|
2459
|
+
if self.face_bfield.x.shape != xface_shape:
|
|
2460
|
+
raise ValueError(f"face_bfield.x shape must be {xface_shape}, got {self.face_bfield.x.shape}")
|
|
2461
|
+
if self.face_bfield.y.shape != yface_shape:
|
|
2462
|
+
raise ValueError(f"face_bfield.y shape must be {yface_shape}, got {self.face_bfield.y.shape}")
|
|
2463
|
+
if self.face_bfield.z.shape != zface_shape:
|
|
2464
|
+
raise ValueError(f"face_bfield.z shape must be {zface_shape}, got {self.face_bfield.z.shape}")
|
|
2465
|
+
|
|
2466
|
+
@property
|
|
2467
|
+
def shape(self) -> tuple[int, int, int]:
|
|
2468
|
+
return self.grid.shape
|
|
2469
|
+
|
|
2470
|
+
@property
|
|
2471
|
+
def logical_axis_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
2472
|
+
return self.grid.logical_axis_vectors
|
|
2473
|
+
|
|
2474
|
+
@property
|
|
2475
|
+
def logical_face_vectors(self) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
2476
|
+
return self.grid.logical_face_vectors
|
|
2477
|
+
|
|
2478
|
+
|
|
2479
|
+
@lru_cache(maxsize=1)
|
|
2480
|
+
def _stencil_types():
|
|
2481
|
+
from ..native.fci_boundaries import (
|
|
2482
|
+
FaceGradientStencil3D,
|
|
2483
|
+
ConservativeStencil3D,
|
|
2484
|
+
LocalStencil1D,
|
|
2485
|
+
LocalStencil3D,
|
|
2486
|
+
)
|
|
2487
|
+
|
|
2488
|
+
return ConservativeStencil3D, FaceGradientStencil3D, LocalStencil1D, LocalStencil3D
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
def _shift_owned_slices(layout: HaloLayout3D, axis: int, offset: int) -> tuple[slice, slice, slice]:
|
|
2492
|
+
h = layout.halo_width
|
|
2493
|
+
nx, ny, nz = layout.owned_shape
|
|
2494
|
+
extents = [nx, ny, nz]
|
|
2495
|
+
start = h + offset
|
|
2496
|
+
stop = start + extents[axis]
|
|
2497
|
+
slices = [slice(h, h + ext) for ext in extents]
|
|
2498
|
+
slices[axis] = slice(start, stop)
|
|
2499
|
+
return tuple(slices)
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
def _local_axis_stencil_from_halo(
|
|
2503
|
+
values_halo: jnp.ndarray,
|
|
2504
|
+
geometry: LocalFciGeometry3D,
|
|
2505
|
+
*,
|
|
2506
|
+
axis: int,
|
|
2507
|
+
) -> "LocalStencil1D":
|
|
2508
|
+
ConservativeStencil3D, _, LocalStencil1D, _ = _stencil_types()
|
|
2509
|
+
del ConservativeStencil3D
|
|
2510
|
+
|
|
2511
|
+
values_halo = jnp.asarray(values_halo, dtype=jnp.float64)
|
|
2512
|
+
if values_halo.shape != geometry.halo_shape:
|
|
2513
|
+
raise ValueError(
|
|
2514
|
+
"field_halo must have shape geometry.halo_shape; "
|
|
2515
|
+
f"got {values_halo.shape}, expected {geometry.halo_shape}"
|
|
2516
|
+
)
|
|
2517
|
+
|
|
2518
|
+
h = geometry.layout.halo_width
|
|
2519
|
+
owned_shape = geometry.owned_shape
|
|
2520
|
+
if h < 1:
|
|
2521
|
+
raise ValueError("local stencil reconstruction requires halo_width >= 1")
|
|
2522
|
+
|
|
2523
|
+
center = values_halo[geometry.layout.owned_slices_cell]
|
|
2524
|
+
minus = values_halo[_shift_owned_slices(geometry.layout, axis, -1)]
|
|
2525
|
+
plus = values_halo[_shift_owned_slices(geometry.layout, axis, +1)]
|
|
2526
|
+
|
|
2527
|
+
grid_axis = (geometry.grid.x, geometry.grid.y, geometry.grid.z)[axis]
|
|
2528
|
+
centers_halo = jnp.asarray(grid_axis.centers_halo, dtype=jnp.float64)
|
|
2529
|
+
owned_slice = grid_axis.owned_center_slice
|
|
2530
|
+
owned_centers = centers_halo[owned_slice]
|
|
2531
|
+
lower_centers = centers_halo[slice(owned_slice.start - 1, owned_slice.stop - 1)]
|
|
2532
|
+
upper_centers = centers_halo[slice(owned_slice.start + 1, owned_slice.stop + 1)]
|
|
2533
|
+
|
|
2534
|
+
lower_width_1d = owned_centers - lower_centers
|
|
2535
|
+
upper_width_1d = upper_centers - owned_centers
|
|
2536
|
+
|
|
2537
|
+
if axis == 0:
|
|
2538
|
+
dx_min = jnp.broadcast_to(lower_width_1d[:, None, None], owned_shape)
|
|
2539
|
+
dx_plus = jnp.broadcast_to(upper_width_1d[:, None, None], owned_shape)
|
|
2540
|
+
elif axis == 1:
|
|
2541
|
+
dx_min = jnp.broadcast_to(lower_width_1d[None, :, None], owned_shape)
|
|
2542
|
+
dx_plus = jnp.broadcast_to(upper_width_1d[None, :, None], owned_shape)
|
|
2543
|
+
else:
|
|
2544
|
+
dx_min = jnp.broadcast_to(lower_width_1d[None, None, :], owned_shape)
|
|
2545
|
+
dx_plus = jnp.broadcast_to(upper_width_1d[None, None, :], owned_shape)
|
|
2546
|
+
|
|
2547
|
+
return LocalStencil1D(center=center, minus=minus, plus=plus, dx_min=dx_min, dx_plus=dx_plus)
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
def _lift_cell_field_to_faces(field: jnp.ndarray, *, axis: int, periodic: bool) -> jnp.ndarray:
|
|
2551
|
+
"""Map a cell-centered field onto the corresponding face grid along one axis."""
|
|
2552
|
+
|
|
2553
|
+
values_3d = jnp.asarray(field, dtype=jnp.float64)
|
|
2554
|
+
axis_n = values_3d.shape[axis]
|
|
2555
|
+
face_shape = list(values_3d.shape)
|
|
2556
|
+
face_shape[axis] += 1
|
|
2557
|
+
|
|
2558
|
+
if axis_n == 1:
|
|
2559
|
+
return jnp.broadcast_to(values_3d, tuple(face_shape))
|
|
2560
|
+
|
|
2561
|
+
first = jnp.take(values_3d, 0, axis=axis)
|
|
2562
|
+
second = jnp.take(values_3d, 1, axis=axis)
|
|
2563
|
+
last = jnp.take(values_3d, -1, axis=axis)
|
|
2564
|
+
penultimate = jnp.take(values_3d, -2, axis=axis)
|
|
2565
|
+
|
|
2566
|
+
if periodic:
|
|
2567
|
+
lower_ghost = last
|
|
2568
|
+
upper_ghost = first
|
|
2569
|
+
elif axis_n >= 3:
|
|
2570
|
+
# Quadratic (three-point) ghost extrapolation keeps the boundary-face
|
|
2571
|
+
# average reconstruction second-order accurate for curved profiles.
|
|
2572
|
+
third = jnp.take(values_3d, 2, axis=axis)
|
|
2573
|
+
antepenultimate = jnp.take(values_3d, -3, axis=axis)
|
|
2574
|
+
lower_ghost = 3.0 * first - 3.0 * second + third
|
|
2575
|
+
upper_ghost = 3.0 * last - 3.0 * penultimate + antepenultimate
|
|
2576
|
+
else:
|
|
2577
|
+
lower_ghost = 2.0 * first - second
|
|
2578
|
+
upper_ghost = 2.0 * last - penultimate
|
|
2579
|
+
|
|
2580
|
+
ext = jnp.concatenate(
|
|
2581
|
+
(
|
|
2582
|
+
jnp.expand_dims(lower_ghost, axis=axis),
|
|
2583
|
+
values_3d,
|
|
2584
|
+
jnp.expand_dims(upper_ghost, axis=axis),
|
|
2585
|
+
),
|
|
2586
|
+
axis=axis,
|
|
2587
|
+
)
|
|
2588
|
+
return 0.5 * (
|
|
2589
|
+
jnp.take(ext, jnp.arange(axis_n + 1), axis=axis)
|
|
2590
|
+
+ jnp.take(ext, jnp.arange(1, axis_n + 2), axis=axis)
|
|
2591
|
+
)
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
def _global_axis_stencil_from_field(
|
|
2595
|
+
field: jnp.ndarray,
|
|
2596
|
+
geometry: FciGeometry3D,
|
|
2597
|
+
*,
|
|
2598
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
2599
|
+
) -> "ConservativeStencil3D":
|
|
2600
|
+
ConservativeStencil3D, FaceGradientStencil3D, LocalStencil1D, _ = _stencil_types()
|
|
2601
|
+
|
|
2602
|
+
values = jnp.asarray(field, dtype=jnp.float64)
|
|
2603
|
+
if values.shape != geometry.shape:
|
|
2604
|
+
raise ValueError(f"field must have shape {geometry.shape}, got {values.shape}")
|
|
2605
|
+
|
|
2606
|
+
periodic_axes = _normalize_periodic_axes(periodic_axes)
|
|
2607
|
+
|
|
2608
|
+
def _face_spacing(field_spacing: jnp.ndarray, *, face_axis: int) -> jnp.ndarray:
|
|
2609
|
+
return _lift_cell_field_to_faces(field_spacing, axis=face_axis, periodic=periodic_axes[face_axis])
|
|
2610
|
+
|
|
2611
|
+
def _face_gradient_for_axis(face_axis: int) -> jnp.ndarray:
|
|
2612
|
+
face_values = _lift_cell_field_to_faces(values, axis=face_axis, periodic=periodic_axes[face_axis])
|
|
2613
|
+
face_spacings = (
|
|
2614
|
+
_face_spacing(geometry.spacing.dx, face_axis=face_axis),
|
|
2615
|
+
_face_spacing(geometry.spacing.dy, face_axis=face_axis),
|
|
2616
|
+
_face_spacing(geometry.spacing.dz, face_axis=face_axis),
|
|
2617
|
+
)
|
|
2618
|
+
components = tuple(
|
|
2619
|
+
_first_derivative_3d(
|
|
2620
|
+
face_values,
|
|
2621
|
+
face_spacings[component],
|
|
2622
|
+
axis=component,
|
|
2623
|
+
periodic=periodic_axes[component],
|
|
2624
|
+
)
|
|
2625
|
+
for component in range(3)
|
|
2626
|
+
)
|
|
2627
|
+
return jnp.stack(components, axis=-1)
|
|
2628
|
+
|
|
2629
|
+
def _axis_stencil(axis: int, grid_axis, periodic: bool) -> LocalStencil1D:
|
|
2630
|
+
axis_n = values.shape[axis]
|
|
2631
|
+
if axis_n == 1:
|
|
2632
|
+
repeated = jnp.broadcast_to(values, geometry.shape)
|
|
2633
|
+
width = jnp.asarray(grid_axis.faces[-1] - grid_axis.faces[0], dtype=jnp.float64)
|
|
2634
|
+
width = jnp.broadcast_to(width, geometry.shape)
|
|
2635
|
+
return LocalStencil1D(center=values, minus=repeated, plus=repeated, dx_min=width, dx_plus=width)
|
|
2636
|
+
|
|
2637
|
+
if periodic:
|
|
2638
|
+
minus = jnp.concatenate(
|
|
2639
|
+
(
|
|
2640
|
+
jnp.expand_dims(jnp.take(values, -1, axis=axis), axis=axis),
|
|
2641
|
+
jnp.take(values, jnp.arange(axis_n - 1), axis=axis),
|
|
2642
|
+
),
|
|
2643
|
+
axis=axis,
|
|
2644
|
+
)
|
|
2645
|
+
plus = jnp.concatenate(
|
|
2646
|
+
(
|
|
2647
|
+
jnp.take(values, jnp.arange(1, axis_n), axis=axis),
|
|
2648
|
+
jnp.expand_dims(jnp.take(values, 0, axis=axis), axis=axis),
|
|
2649
|
+
),
|
|
2650
|
+
axis=axis,
|
|
2651
|
+
)
|
|
2652
|
+
period = jnp.asarray(grid_axis.faces[-1] - grid_axis.faces[0], dtype=jnp.float64)
|
|
2653
|
+
deltas = jnp.asarray(grid_axis.centers, dtype=jnp.float64)
|
|
2654
|
+
dx_min_1d = jnp.concatenate((jnp.asarray([deltas[0] - (deltas[-1] - period)], dtype=jnp.float64), deltas[1:] - deltas[:-1]))
|
|
2655
|
+
dx_plus_1d = jnp.concatenate((deltas[1:] - deltas[:-1], jnp.expand_dims((deltas[0] + period) - deltas[-1], axis=0)))
|
|
2656
|
+
else:
|
|
2657
|
+
first = jnp.take(values, 0, axis=axis)
|
|
2658
|
+
second = jnp.take(values, 1, axis=axis)
|
|
2659
|
+
last = jnp.take(values, -1, axis=axis)
|
|
2660
|
+
penultimate = jnp.take(values, -2, axis=axis)
|
|
2661
|
+
minus = jnp.concatenate((jnp.expand_dims(2.0 * first - second, axis=axis), jnp.take(values, jnp.arange(axis_n - 1), axis=axis)), axis=axis)
|
|
2662
|
+
plus = jnp.concatenate((jnp.take(values, jnp.arange(1, axis_n), axis=axis), jnp.expand_dims(2.0 * last - penultimate, axis=axis)), axis=axis)
|
|
2663
|
+
deltas = jnp.asarray(grid_axis.centers, dtype=jnp.float64)
|
|
2664
|
+
lower_width = 2.0 * jnp.asarray(grid_axis.lower_center_to_face, dtype=jnp.float64)
|
|
2665
|
+
upper_width = 2.0 * jnp.asarray(grid_axis.upper_center_to_face, dtype=jnp.float64)
|
|
2666
|
+
dx_min_1d = jnp.concatenate((jnp.expand_dims(lower_width, axis=0), deltas[1:] - deltas[:-1]))
|
|
2667
|
+
dx_plus_1d = jnp.concatenate((deltas[1:] - deltas[:-1], jnp.expand_dims(upper_width, axis=0)))
|
|
2668
|
+
|
|
2669
|
+
if axis == 0:
|
|
2670
|
+
dx_min = jnp.broadcast_to(dx_min_1d[:, None, None], geometry.shape)
|
|
2671
|
+
dx_plus = jnp.broadcast_to(dx_plus_1d[:, None, None], geometry.shape)
|
|
2672
|
+
elif axis == 1:
|
|
2673
|
+
dx_min = jnp.broadcast_to(dx_min_1d[None, :, None], geometry.shape)
|
|
2674
|
+
dx_plus = jnp.broadcast_to(dx_plus_1d[None, :, None], geometry.shape)
|
|
2675
|
+
else:
|
|
2676
|
+
dx_min = jnp.broadcast_to(dx_min_1d[None, None, :], geometry.shape)
|
|
2677
|
+
dx_plus = jnp.broadcast_to(dx_plus_1d[None, None, :], geometry.shape)
|
|
2678
|
+
|
|
2679
|
+
return LocalStencil1D(center=values, minus=minus, plus=plus, dx_min=dx_min, dx_plus=dx_plus)
|
|
2680
|
+
|
|
2681
|
+
return ConservativeStencil3D(
|
|
2682
|
+
x=_axis_stencil(0, geometry.grid.x, periodic_axes[0]),
|
|
2683
|
+
y=_axis_stencil(1, geometry.grid.y, periodic_axes[1]),
|
|
2684
|
+
z=_axis_stencil(2, geometry.grid.z, periodic_axes[2]),
|
|
2685
|
+
face_grad=FaceGradientStencil3D(
|
|
2686
|
+
x=_face_gradient_for_axis(0),
|
|
2687
|
+
y=_face_gradient_for_axis(1),
|
|
2688
|
+
z=_face_gradient_for_axis(2),
|
|
2689
|
+
),
|
|
2690
|
+
)
|
|
2691
|
+
|
|
2692
|
+
|
|
2693
|
+
def _one_sided_physical_global_axis_stencil(
|
|
2694
|
+
stencil: "LocalStencil1D",
|
|
2695
|
+
values: jnp.ndarray,
|
|
2696
|
+
grid_axis: Grid1D,
|
|
2697
|
+
*,
|
|
2698
|
+
axis: int,
|
|
2699
|
+
) -> "LocalStencil1D":
|
|
2700
|
+
"""Patch global physical side planes with nonuniform three-point formulas."""
|
|
2701
|
+
|
|
2702
|
+
axis = int(axis)
|
|
2703
|
+
axis_n = int(values.shape[axis])
|
|
2704
|
+
if axis_n < 3:
|
|
2705
|
+
return stencil
|
|
2706
|
+
|
|
2707
|
+
centers = jnp.asarray(grid_axis.centers, dtype=jnp.float64)
|
|
2708
|
+
|
|
2709
|
+
minus = jnp.asarray(stencil.minus, dtype=jnp.float64)
|
|
2710
|
+
center = jnp.asarray(stencil.center, dtype=jnp.float64)
|
|
2711
|
+
plus = jnp.asarray(stencil.plus, dtype=jnp.float64)
|
|
2712
|
+
dx_min = jnp.asarray(stencil.dx_min, dtype=jnp.float64)
|
|
2713
|
+
dx_plus = jnp.asarray(stencil.dx_plus, dtype=jnp.float64)
|
|
2714
|
+
c_minus = jnp.asarray(stencil.derivative_minus_weight, dtype=jnp.float64)
|
|
2715
|
+
c_center = jnp.asarray(stencil.derivative_center_weight, dtype=jnp.float64)
|
|
2716
|
+
c_plus = jnp.asarray(stencil.derivative_plus_weight, dtype=jnp.float64)
|
|
2717
|
+
|
|
2718
|
+
lower_plane = _axis_index_nd(axis, 0, values.ndim)
|
|
2719
|
+
lower_f0 = jnp.take(values, 0, axis=axis)
|
|
2720
|
+
lower_f1 = jnp.take(values, 1, axis=axis)
|
|
2721
|
+
lower_f2 = jnp.take(values, 2, axis=axis)
|
|
2722
|
+
lower_weights = _three_point_first_derivative_weights(
|
|
2723
|
+
centers[0],
|
|
2724
|
+
centers[1],
|
|
2725
|
+
centers[2],
|
|
2726
|
+
)
|
|
2727
|
+
minus = minus.at[lower_plane].set(lower_f2)
|
|
2728
|
+
center = center.at[lower_plane].set(lower_f0)
|
|
2729
|
+
plus = plus.at[lower_plane].set(lower_f1)
|
|
2730
|
+
c_minus = c_minus.at[lower_plane].set(jnp.full_like(lower_f0, lower_weights[2]))
|
|
2731
|
+
c_center = c_center.at[lower_plane].set(jnp.full_like(lower_f0, lower_weights[0]))
|
|
2732
|
+
c_plus = c_plus.at[lower_plane].set(jnp.full_like(lower_f0, lower_weights[1]))
|
|
2733
|
+
dx_min = dx_min.at[lower_plane].set(jnp.full_like(lower_f0, jnp.abs(centers[2] - centers[0])))
|
|
2734
|
+
dx_plus = dx_plus.at[lower_plane].set(jnp.full_like(lower_f0, jnp.abs(centers[1] - centers[0])))
|
|
2735
|
+
|
|
2736
|
+
upper_plane = _axis_index_nd(axis, axis_n - 1, values.ndim)
|
|
2737
|
+
upper_f0 = jnp.take(values, axis_n - 1, axis=axis)
|
|
2738
|
+
upper_f1 = jnp.take(values, axis_n - 2, axis=axis)
|
|
2739
|
+
upper_f2 = jnp.take(values, axis_n - 3, axis=axis)
|
|
2740
|
+
upper_weights = _three_point_first_derivative_weights(
|
|
2741
|
+
centers[axis_n - 1],
|
|
2742
|
+
centers[axis_n - 2],
|
|
2743
|
+
centers[axis_n - 3],
|
|
2744
|
+
)
|
|
2745
|
+
minus = minus.at[upper_plane].set(upper_f1)
|
|
2746
|
+
center = center.at[upper_plane].set(upper_f0)
|
|
2747
|
+
plus = plus.at[upper_plane].set(upper_f2)
|
|
2748
|
+
c_minus = c_minus.at[upper_plane].set(jnp.full_like(upper_f0, upper_weights[1]))
|
|
2749
|
+
c_center = c_center.at[upper_plane].set(jnp.full_like(upper_f0, upper_weights[0]))
|
|
2750
|
+
c_plus = c_plus.at[upper_plane].set(jnp.full_like(upper_f0, upper_weights[2]))
|
|
2751
|
+
dx_min = dx_min.at[upper_plane].set(jnp.full_like(upper_f0, jnp.abs(centers[axis_n - 2] - centers[axis_n - 1])))
|
|
2752
|
+
dx_plus = dx_plus.at[upper_plane].set(jnp.full_like(upper_f0, jnp.abs(centers[axis_n - 3] - centers[axis_n - 1])))
|
|
2753
|
+
|
|
2754
|
+
return stencil.replace(
|
|
2755
|
+
minus=minus,
|
|
2756
|
+
center=center,
|
|
2757
|
+
plus=plus,
|
|
2758
|
+
dx_min=dx_min,
|
|
2759
|
+
dx_plus=dx_plus,
|
|
2760
|
+
derivative_minus_weight=c_minus,
|
|
2761
|
+
derivative_center_weight=c_center,
|
|
2762
|
+
derivative_plus_weight=c_plus,
|
|
2763
|
+
)
|
|
2764
|
+
|
|
2765
|
+
|
|
2766
|
+
def _build_conservative_stencil_from_field(
|
|
2767
|
+
field_halo: jnp.ndarray,
|
|
2768
|
+
geometry: LocalFciGeometry3D | FciGeometry3D,
|
|
2769
|
+
context: StencilBuilderContext | tuple[bool | None, bool | None, bool | None] | None = None,
|
|
2770
|
+
face_bc: object | None = None,
|
|
2771
|
+
cut_wall_geometry: object | None = None,
|
|
2772
|
+
cut_wall_bc: object | None = None,
|
|
2773
|
+
*,
|
|
2774
|
+
periodic_axes: tuple[bool | None, bool | None, bool | None] | None = None,
|
|
2775
|
+
) -> "ConservativeStencil3D":
|
|
2776
|
+
ConservativeStencil3D, FaceGradientStencil3D, _, _ = _stencil_types()
|
|
2777
|
+
if isinstance(geometry, FciGeometry3D):
|
|
2778
|
+
# Global single-device path (legacy call convention:
|
|
2779
|
+
# ``(field, geometry, periodic_axes, face_bc[, cut_wall...])``).
|
|
2780
|
+
# Boundary conditions are applied by the downstream flux/operator
|
|
2781
|
+
# assembly, not during stencil reconstruction.
|
|
2782
|
+
del face_bc, cut_wall_geometry, cut_wall_bc
|
|
2783
|
+
if periodic_axes is None:
|
|
2784
|
+
periodic_axes = context
|
|
2785
|
+
return _global_axis_stencil_from_field(
|
|
2786
|
+
field_halo,
|
|
2787
|
+
geometry,
|
|
2788
|
+
periodic_axes=_normalize_periodic_axes(periodic_axes),
|
|
2789
|
+
)
|
|
2790
|
+
if not isinstance(geometry, LocalFciGeometry3D):
|
|
2791
|
+
raise TypeError("geometry must be a LocalFciGeometry3D instance")
|
|
2792
|
+
if not isinstance(context, StencilBuilderContext):
|
|
2793
|
+
raise TypeError("context must be a StencilBuilderContext instance")
|
|
2794
|
+
if context.layout != geometry.layout:
|
|
2795
|
+
raise ValueError("geometry and context must share the same HaloLayout3D")
|
|
2796
|
+
if context.domain is None:
|
|
2797
|
+
raise ValueError("context.domain is required for the local conservative stencil builder")
|
|
2798
|
+
field_halo = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2799
|
+
if field_halo.shape != geometry.halo_shape:
|
|
2800
|
+
raise ValueError(
|
|
2801
|
+
"field_halo must match geometry.halo_shape; "
|
|
2802
|
+
f"got {field_halo.shape}, expected {geometry.halo_shape}"
|
|
2803
|
+
)
|
|
2804
|
+
return ConservativeStencil3D(
|
|
2805
|
+
x=_local_axis_stencil_from_halo(field_halo, geometry, axis=0),
|
|
2806
|
+
y=_local_axis_stencil_from_halo(field_halo, geometry, axis=1),
|
|
2807
|
+
z=_local_axis_stencil_from_halo(field_halo, geometry, axis=2),
|
|
2808
|
+
face_grad=_build_local_face_gradient_from_halo(
|
|
2809
|
+
field_halo,
|
|
2810
|
+
geometry,
|
|
2811
|
+
context.domain,
|
|
2812
|
+
),
|
|
2813
|
+
)
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
def _build_local_stencil_from_field(
|
|
2817
|
+
field_halo: jnp.ndarray,
|
|
2818
|
+
geometry: LocalFciGeometry3D | FciGeometry3D,
|
|
2819
|
+
context: StencilBuilderContext | tuple[bool | None, bool | None, bool | None] | None = None,
|
|
2820
|
+
face_bc: object | None = None,
|
|
2821
|
+
cut_wall_geometry: object | None = None,
|
|
2822
|
+
cut_wall_bc: object | None = None,
|
|
2823
|
+
*,
|
|
2824
|
+
periodic_axes: tuple[bool | None, bool | None, bool | None] | None = None,
|
|
2825
|
+
) -> "LocalStencil3D":
|
|
2826
|
+
_, _, _, LocalStencil3D = _stencil_types()
|
|
2827
|
+
|
|
2828
|
+
if isinstance(geometry, FciGeometry3D):
|
|
2829
|
+
# Global single-device path (legacy call convention:
|
|
2830
|
+
# ``(field, geometry, periodic_axes, face_bc[, cut_wall...])``).
|
|
2831
|
+
# Boundary conditions are applied by the downstream operators, not
|
|
2832
|
+
# during stencil reconstruction. Physical (non-periodic) side planes
|
|
2833
|
+
# use nonuniform second-order one-sided derivative stencils, matching
|
|
2834
|
+
# the shard-local one-sided physical closure.
|
|
2835
|
+
del face_bc, cut_wall_geometry, cut_wall_bc
|
|
2836
|
+
if periodic_axes is None:
|
|
2837
|
+
periodic_axes = context
|
|
2838
|
+
normalized_periodic_axes = _normalize_periodic_axes(periodic_axes)
|
|
2839
|
+
conservative = _global_axis_stencil_from_field(
|
|
2840
|
+
field_halo,
|
|
2841
|
+
geometry,
|
|
2842
|
+
periodic_axes=normalized_periodic_axes,
|
|
2843
|
+
)
|
|
2844
|
+
values = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2845
|
+
axis_stencils = [conservative.x, conservative.y, conservative.z]
|
|
2846
|
+
grid_axes = (geometry.grid.x, geometry.grid.y, geometry.grid.z)
|
|
2847
|
+
for axis in range(3):
|
|
2848
|
+
if not normalized_periodic_axes[axis]:
|
|
2849
|
+
axis_stencils[axis] = _one_sided_physical_global_axis_stencil(
|
|
2850
|
+
axis_stencils[axis],
|
|
2851
|
+
values,
|
|
2852
|
+
grid_axes[axis],
|
|
2853
|
+
axis=axis,
|
|
2854
|
+
)
|
|
2855
|
+
return LocalStencil3D(
|
|
2856
|
+
x=axis_stencils[0],
|
|
2857
|
+
y=axis_stencils[1],
|
|
2858
|
+
z=axis_stencils[2],
|
|
2859
|
+
)
|
|
2860
|
+
if not isinstance(geometry, LocalFciGeometry3D):
|
|
2861
|
+
raise TypeError("geometry must be a LocalFciGeometry3D instance")
|
|
2862
|
+
if not isinstance(context, StencilBuilderContext):
|
|
2863
|
+
raise TypeError("context must be a StencilBuilderContext instance")
|
|
2864
|
+
if context.layout != geometry.layout:
|
|
2865
|
+
raise ValueError("geometry and context must share the same HaloLayout3D")
|
|
2866
|
+
field_halo = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2867
|
+
if field_halo.shape != geometry.halo_shape:
|
|
2868
|
+
raise ValueError(
|
|
2869
|
+
"field_halo must match geometry.halo_shape; "
|
|
2870
|
+
f"got {field_halo.shape}, expected {geometry.halo_shape}"
|
|
2871
|
+
)
|
|
2872
|
+
return LocalStencil3D(
|
|
2873
|
+
x=_local_axis_stencil_from_halo(field_halo, geometry, axis=0),
|
|
2874
|
+
y=_local_axis_stencil_from_halo(field_halo, geometry, axis=1),
|
|
2875
|
+
z=_local_axis_stencil_from_halo(field_halo, geometry, axis=2),
|
|
2876
|
+
)
|
|
2877
|
+
|
|
2878
|
+
|
|
2879
|
+
@_pytree_base
|
|
2880
|
+
@dataclass(frozen=True)
|
|
2881
|
+
class ConservativeStencilBuilder(_DataclassPyTreeMixin):
|
|
2882
|
+
"""Callable adapter that delegates conservative-stencil construction to an injected function."""
|
|
2883
|
+
|
|
2884
|
+
build_fn: Callable[
|
|
2885
|
+
[
|
|
2886
|
+
jnp.ndarray,
|
|
2887
|
+
"LocalFciGeometry3D",
|
|
2888
|
+
"StencilBuilderContext",
|
|
2889
|
+
],
|
|
2890
|
+
"ConservativeStencil3D",
|
|
2891
|
+
]
|
|
2892
|
+
|
|
2893
|
+
def __call__(self, *args: object, **kwargs: object) -> "ConservativeStencil3D":
|
|
2894
|
+
# Signature-agnostic delegation: the shard-local halo contract is
|
|
2895
|
+
# ``(field_halo, geometry, context)`` while the global single-device
|
|
2896
|
+
# contract is ``(field, geometry, periodic_axes, face_bc[, cut_wall...])``.
|
|
2897
|
+
return self.build_fn(*args, **kwargs)
|
|
2898
|
+
|
|
2899
|
+
def tree_flatten(self):
|
|
2900
|
+
return (), self.build_fn
|
|
2901
|
+
|
|
2902
|
+
@classmethod
|
|
2903
|
+
def tree_unflatten(cls, aux_data, children):
|
|
2904
|
+
return cls(aux_data)
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
build_conservative_stencil_from_field = ConservativeStencilBuilder(_build_conservative_stencil_from_field)
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
@_pytree_base
|
|
2911
|
+
@dataclass(frozen=True)
|
|
2912
|
+
class LocalStencilBuilder(_DataclassPyTreeMixin):
|
|
2913
|
+
"""Callable adapter that delegates local-stencil construction to an injected function."""
|
|
2914
|
+
|
|
2915
|
+
build_fn: Callable[
|
|
2916
|
+
[
|
|
2917
|
+
jnp.ndarray,
|
|
2918
|
+
"LocalFciGeometry3D",
|
|
2919
|
+
"StencilBuilderContext",
|
|
2920
|
+
],
|
|
2921
|
+
"LocalStencil3D",
|
|
2922
|
+
]
|
|
2923
|
+
|
|
2924
|
+
def __call__(self, *args: object, **kwargs: object) -> "LocalStencil3D":
|
|
2925
|
+
# Signature-agnostic delegation: the shard-local halo contract is
|
|
2926
|
+
# ``(field_halo, geometry, context)`` while the global single-device
|
|
2927
|
+
# contract is ``(field, geometry, periodic_axes, face_bc[, cut_wall...])``.
|
|
2928
|
+
return self.build_fn(*args, **kwargs)
|
|
2929
|
+
|
|
2930
|
+
def tree_flatten(self):
|
|
2931
|
+
return (), self.build_fn
|
|
2932
|
+
|
|
2933
|
+
@classmethod
|
|
2934
|
+
def tree_unflatten(cls, aux_data, children):
|
|
2935
|
+
return cls(aux_data)
|
|
2936
|
+
|
|
2937
|
+
|
|
2938
|
+
build_local_stencil_from_field = LocalStencilBuilder(_build_local_stencil_from_field)
|
|
2939
|
+
|
|
2940
|
+
|
|
2941
|
+
def _build_local_face_gradient_from_halo(
|
|
2942
|
+
field_halo: jnp.ndarray,
|
|
2943
|
+
geometry: LocalFciGeometry3D,
|
|
2944
|
+
domain: LocalDomain3D,
|
|
2945
|
+
) -> "FaceGradientStencil3D":
|
|
2946
|
+
_, FaceGradientStencil3D, _, _ = _stencil_types()
|
|
2947
|
+
|
|
2948
|
+
field_halo = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
2949
|
+
if field_halo.shape != geometry.halo_shape:
|
|
2950
|
+
raise ValueError(
|
|
2951
|
+
"field_halo must match geometry.halo_shape; "
|
|
2952
|
+
f"got {field_halo.shape}, expected {geometry.halo_shape}"
|
|
2953
|
+
)
|
|
2954
|
+
if not isinstance(domain, LocalDomain3D):
|
|
2955
|
+
raise TypeError(
|
|
2956
|
+
"domain must be a LocalDomain3D instance, "
|
|
2957
|
+
f"got {type(domain).__name__}"
|
|
2958
|
+
)
|
|
2959
|
+
if domain.layout != geometry.layout:
|
|
2960
|
+
raise ValueError("geometry and domain must share the same HaloLayout3D")
|
|
2961
|
+
|
|
2962
|
+
face_locations = ("x_face", "y_face", "z_face")
|
|
2963
|
+
expected_face_shapes = tuple(
|
|
2964
|
+
geometry.layout.location_owned_shape(location) for location in face_locations
|
|
2965
|
+
)
|
|
2966
|
+
spacing_fields = (
|
|
2967
|
+
geometry.spacing.dx_halo,
|
|
2968
|
+
geometry.spacing.dy_halo,
|
|
2969
|
+
geometry.spacing.dz_halo,
|
|
2970
|
+
)
|
|
2971
|
+
|
|
2972
|
+
def _build_for_face_axis(face_axis: int) -> jnp.ndarray:
|
|
2973
|
+
face_values = _lift_cell_field_to_faces(
|
|
2974
|
+
field_halo,
|
|
2975
|
+
axis=face_axis,
|
|
2976
|
+
periodic=domain.periodic_axes[face_axis],
|
|
2977
|
+
)
|
|
2978
|
+
face_slices = geometry.layout.location_owned_slices(face_locations[face_axis])
|
|
2979
|
+
components = tuple(
|
|
2980
|
+
_first_derivative_3d(
|
|
2981
|
+
face_values,
|
|
2982
|
+
_lift_cell_field_to_faces(
|
|
2983
|
+
spacing_fields[component],
|
|
2984
|
+
axis=face_axis,
|
|
2985
|
+
periodic=domain.periodic_axes[face_axis],
|
|
2986
|
+
),
|
|
2987
|
+
axis=component,
|
|
2988
|
+
periodic=domain.periodic_axes[component],
|
|
2989
|
+
)[face_slices]
|
|
2990
|
+
for component in range(3)
|
|
2991
|
+
)
|
|
2992
|
+
return jnp.stack(components, axis=-1)
|
|
2993
|
+
|
|
2994
|
+
face_grad = FaceGradientStencil3D(
|
|
2995
|
+
x=_build_for_face_axis(0),
|
|
2996
|
+
y=_build_for_face_axis(1),
|
|
2997
|
+
z=_build_for_face_axis(2),
|
|
2998
|
+
)
|
|
2999
|
+
if face_grad.x.shape[:-1] != expected_face_shapes[0]:
|
|
3000
|
+
raise ValueError(
|
|
3001
|
+
f"face_grad.x must have shape {expected_face_shapes[0] + (3,)}, got {face_grad.x.shape}"
|
|
3002
|
+
)
|
|
3003
|
+
if face_grad.y.shape[:-1] != expected_face_shapes[1]:
|
|
3004
|
+
raise ValueError(
|
|
3005
|
+
f"face_grad.y must have shape {expected_face_shapes[1] + (3,)}, got {face_grad.y.shape}"
|
|
3006
|
+
)
|
|
3007
|
+
if face_grad.z.shape[:-1] != expected_face_shapes[2]:
|
|
3008
|
+
raise ValueError(
|
|
3009
|
+
f"face_grad.z must have shape {expected_face_shapes[2] + (3,)}, got {face_grad.z.shape}"
|
|
3010
|
+
)
|
|
3011
|
+
return face_grad
|
|
3012
|
+
|
|
3013
|
+
|
|
3014
|
+
@_pytree_base
|
|
3015
|
+
@dataclass(frozen=True)
|
|
3016
|
+
class LocalConservativeStencilBuilder(_DataclassPyTreeMixin):
|
|
3017
|
+
"""Callable adapter that delegates local conservative-stencil construction."""
|
|
3018
|
+
|
|
3019
|
+
build_fn: Callable[
|
|
3020
|
+
[
|
|
3021
|
+
jnp.ndarray,
|
|
3022
|
+
"LocalFciGeometry3D",
|
|
3023
|
+
"StencilBuilderContext",
|
|
3024
|
+
],
|
|
3025
|
+
"ConservativeStencil3D",
|
|
3026
|
+
]
|
|
3027
|
+
|
|
3028
|
+
def __call__(
|
|
3029
|
+
self,
|
|
3030
|
+
field_halo: jnp.ndarray,
|
|
3031
|
+
geometry: "LocalFciGeometry3D",
|
|
3032
|
+
context: "StencilBuilderContext",
|
|
3033
|
+
) -> "ConservativeStencil3D":
|
|
3034
|
+
return self.build_fn(field_halo, geometry, context)
|
|
3035
|
+
|
|
3036
|
+
def tree_flatten(self):
|
|
3037
|
+
return (), self.build_fn
|
|
3038
|
+
|
|
3039
|
+
@classmethod
|
|
3040
|
+
def tree_unflatten(cls, aux_data, children):
|
|
3041
|
+
return cls(aux_data)
|
|
3042
|
+
|
|
3043
|
+
|
|
3044
|
+
build_local_conservative_stencil_from_field = LocalConservativeStencilBuilder(
|
|
3045
|
+
_build_conservative_stencil_from_field
|
|
3046
|
+
)
|
|
3047
|
+
|
|
3048
|
+
|
|
3049
|
+
def _local_axis_plane_slice(axis: int, index: int | slice) -> tuple[object, object, object]:
|
|
3050
|
+
"""Return a 3D slice tuple with ``index`` applied along one axis."""
|
|
3051
|
+
|
|
3052
|
+
axis = int(axis)
|
|
3053
|
+
if axis == 0:
|
|
3054
|
+
return index, slice(None), slice(None)
|
|
3055
|
+
if axis == 1:
|
|
3056
|
+
return slice(None), index, slice(None)
|
|
3057
|
+
if axis == 2:
|
|
3058
|
+
return slice(None), slice(None), index
|
|
3059
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
def _local_halo_axis_slice(
|
|
3063
|
+
layout: HaloLayout3D,
|
|
3064
|
+
axis: int,
|
|
3065
|
+
owned_axis_offset: int,
|
|
3066
|
+
) -> tuple[object, object, object]:
|
|
3067
|
+
"""Slice a local halo field at an owned-axis-relative cell offset."""
|
|
3068
|
+
|
|
3069
|
+
axis = int(axis)
|
|
3070
|
+
if axis < 0 or axis > 2:
|
|
3071
|
+
raise ValueError(f"axis must be 0, 1, or 2, got {axis}")
|
|
3072
|
+
|
|
3073
|
+
h = int(layout.halo_width)
|
|
3074
|
+
slices: list[object] = [
|
|
3075
|
+
slice(h, h + layout.owned_shape[0]),
|
|
3076
|
+
slice(h, h + layout.owned_shape[1]),
|
|
3077
|
+
slice(h, h + layout.owned_shape[2]),
|
|
3078
|
+
]
|
|
3079
|
+
slices[axis] = h + int(owned_axis_offset)
|
|
3080
|
+
return tuple(slices)
|
|
3081
|
+
|
|
3082
|
+
|
|
3083
|
+
def _three_point_first_derivative_weights(
|
|
3084
|
+
target: jnp.ndarray,
|
|
3085
|
+
first: jnp.ndarray,
|
|
3086
|
+
second: jnp.ndarray,
|
|
3087
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
3088
|
+
"""Return first-derivative weights at ``target`` for three coordinates."""
|
|
3089
|
+
|
|
3090
|
+
target = jnp.asarray(target, dtype=jnp.float64)
|
|
3091
|
+
first = jnp.asarray(first, dtype=jnp.float64)
|
|
3092
|
+
second = jnp.asarray(second, dtype=jnp.float64)
|
|
3093
|
+
|
|
3094
|
+
w_target = (2.0 * target - first - second) / (
|
|
3095
|
+
(target - first) * (target - second)
|
|
3096
|
+
)
|
|
3097
|
+
w_first = (target - second) / ((first - target) * (first - second))
|
|
3098
|
+
w_second = (target - first) / ((second - target) * (second - first))
|
|
3099
|
+
return w_target, w_first, w_second
|
|
3100
|
+
|
|
3101
|
+
|
|
3102
|
+
def _patch_local_physical_one_sided_axis_stencil(
|
|
3103
|
+
stencil: "LocalStencil1D",
|
|
3104
|
+
field_halo: jnp.ndarray,
|
|
3105
|
+
geometry: LocalFciGeometry3D,
|
|
3106
|
+
domain: LocalDomain3D,
|
|
3107
|
+
*,
|
|
3108
|
+
axis: int,
|
|
3109
|
+
) -> "LocalStencil1D":
|
|
3110
|
+
"""Patch physical side planes with nonuniform three-point formulas."""
|
|
3111
|
+
|
|
3112
|
+
ConservativeStencil3D, _, LocalStencil1D, _ = _stencil_types()
|
|
3113
|
+
del ConservativeStencil3D
|
|
3114
|
+
|
|
3115
|
+
axis = int(axis)
|
|
3116
|
+
layout = domain.layout
|
|
3117
|
+
if layout != geometry.layout:
|
|
3118
|
+
raise ValueError("geometry and domain must share the same HaloLayout3D")
|
|
3119
|
+
|
|
3120
|
+
n_axis = int(layout.owned_shape[axis])
|
|
3121
|
+
if n_axis < 3:
|
|
3122
|
+
has_physical_side = (
|
|
3123
|
+
domain.shard_spec.lower_side_kind(axis) == SIDE_PHYSICAL
|
|
3124
|
+
or domain.shard_spec.upper_side_kind(axis) == SIDE_PHYSICAL
|
|
3125
|
+
)
|
|
3126
|
+
if has_physical_side:
|
|
3127
|
+
raise ValueError(
|
|
3128
|
+
"second-order one-sided derivative requires at least 3 owned "
|
|
3129
|
+
f"cells along physical axis {axis}; got {n_axis}"
|
|
3130
|
+
)
|
|
3131
|
+
return stencil
|
|
3132
|
+
|
|
3133
|
+
field_halo = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
3134
|
+
if field_halo.shape != layout.cell_halo_shape:
|
|
3135
|
+
raise ValueError(
|
|
3136
|
+
"field_halo must match domain.layout.cell_halo_shape; "
|
|
3137
|
+
f"got {field_halo.shape}, expected {layout.cell_halo_shape}"
|
|
3138
|
+
)
|
|
3139
|
+
|
|
3140
|
+
minus = jnp.asarray(stencil.minus, dtype=jnp.float64)
|
|
3141
|
+
center = jnp.asarray(stencil.center, dtype=jnp.float64)
|
|
3142
|
+
plus = jnp.asarray(stencil.plus, dtype=jnp.float64)
|
|
3143
|
+
dx_min = jnp.asarray(stencil.dx_min, dtype=jnp.float64)
|
|
3144
|
+
dx_plus = jnp.asarray(stencil.dx_plus, dtype=jnp.float64)
|
|
3145
|
+
c_minus = jnp.asarray(stencil.derivative_minus_weight, dtype=jnp.float64)
|
|
3146
|
+
c_center = jnp.asarray(stencil.derivative_center_weight, dtype=jnp.float64)
|
|
3147
|
+
c_plus = jnp.asarray(stencil.derivative_plus_weight, dtype=jnp.float64)
|
|
3148
|
+
|
|
3149
|
+
grid_axis = (geometry.grid.x, geometry.grid.y, geometry.grid.z)[axis]
|
|
3150
|
+
centers_halo = jnp.asarray(grid_axis.centers_halo, dtype=jnp.float64)
|
|
3151
|
+
h = int(layout.halo_width)
|
|
3152
|
+
|
|
3153
|
+
lower_target = centers_halo[h]
|
|
3154
|
+
lower_first = centers_halo[h + 1]
|
|
3155
|
+
lower_second = centers_halo[h + 2]
|
|
3156
|
+
lower_weights = _three_point_first_derivative_weights(
|
|
3157
|
+
lower_target,
|
|
3158
|
+
lower_first,
|
|
3159
|
+
lower_second,
|
|
3160
|
+
)
|
|
3161
|
+
|
|
3162
|
+
lower_plane = _local_axis_plane_slice(axis, 0)
|
|
3163
|
+
lower_f0 = field_halo[
|
|
3164
|
+
_local_halo_axis_slice(layout, axis, 0)
|
|
3165
|
+
]
|
|
3166
|
+
lower_f1 = field_halo[
|
|
3167
|
+
_local_halo_axis_slice(layout, axis, 1)
|
|
3168
|
+
]
|
|
3169
|
+
lower_f2 = field_halo[
|
|
3170
|
+
_local_halo_axis_slice(layout, axis, 2)
|
|
3171
|
+
]
|
|
3172
|
+
do_lower = domain.runtime_has_physical_lower(axis)
|
|
3173
|
+
|
|
3174
|
+
minus = minus.at[lower_plane].set(
|
|
3175
|
+
jnp.where(do_lower, lower_f2, minus[lower_plane])
|
|
3176
|
+
)
|
|
3177
|
+
center = center.at[lower_plane].set(
|
|
3178
|
+
jnp.where(do_lower, lower_f0, center[lower_plane])
|
|
3179
|
+
)
|
|
3180
|
+
plus = plus.at[lower_plane].set(
|
|
3181
|
+
jnp.where(do_lower, lower_f1, plus[lower_plane])
|
|
3182
|
+
)
|
|
3183
|
+
c_minus = c_minus.at[lower_plane].set(
|
|
3184
|
+
jnp.where(do_lower, lower_weights[2], c_minus[lower_plane])
|
|
3185
|
+
)
|
|
3186
|
+
c_center = c_center.at[lower_plane].set(
|
|
3187
|
+
jnp.where(do_lower, lower_weights[0], c_center[lower_plane])
|
|
3188
|
+
)
|
|
3189
|
+
c_plus = c_plus.at[lower_plane].set(
|
|
3190
|
+
jnp.where(do_lower, lower_weights[1], c_plus[lower_plane])
|
|
3191
|
+
)
|
|
3192
|
+
dx_min = dx_min.at[lower_plane].set(
|
|
3193
|
+
jnp.where(
|
|
3194
|
+
do_lower,
|
|
3195
|
+
jnp.abs(lower_second - lower_target),
|
|
3196
|
+
dx_min[lower_plane],
|
|
3197
|
+
)
|
|
3198
|
+
)
|
|
3199
|
+
dx_plus = dx_plus.at[lower_plane].set(
|
|
3200
|
+
jnp.where(
|
|
3201
|
+
do_lower,
|
|
3202
|
+
jnp.abs(lower_first - lower_target),
|
|
3203
|
+
dx_plus[lower_plane],
|
|
3204
|
+
)
|
|
3205
|
+
)
|
|
3206
|
+
|
|
3207
|
+
upper_target = centers_halo[h + n_axis - 1]
|
|
3208
|
+
upper_first = centers_halo[h + n_axis - 2]
|
|
3209
|
+
upper_second = centers_halo[h + n_axis - 3]
|
|
3210
|
+
upper_weights = _three_point_first_derivative_weights(
|
|
3211
|
+
upper_target,
|
|
3212
|
+
upper_first,
|
|
3213
|
+
upper_second,
|
|
3214
|
+
)
|
|
3215
|
+
|
|
3216
|
+
upper_plane = _local_axis_plane_slice(axis, n_axis - 1)
|
|
3217
|
+
upper_f0 = field_halo[
|
|
3218
|
+
_local_halo_axis_slice(layout, axis, n_axis - 1)
|
|
3219
|
+
]
|
|
3220
|
+
upper_f1 = field_halo[
|
|
3221
|
+
_local_halo_axis_slice(layout, axis, n_axis - 2)
|
|
3222
|
+
]
|
|
3223
|
+
upper_f2 = field_halo[
|
|
3224
|
+
_local_halo_axis_slice(layout, axis, n_axis - 3)
|
|
3225
|
+
]
|
|
3226
|
+
do_upper = domain.runtime_has_physical_upper(axis)
|
|
3227
|
+
|
|
3228
|
+
minus = minus.at[upper_plane].set(
|
|
3229
|
+
jnp.where(do_upper, upper_f1, minus[upper_plane])
|
|
3230
|
+
)
|
|
3231
|
+
center = center.at[upper_plane].set(
|
|
3232
|
+
jnp.where(do_upper, upper_f0, center[upper_plane])
|
|
3233
|
+
)
|
|
3234
|
+
plus = plus.at[upper_plane].set(
|
|
3235
|
+
jnp.where(do_upper, upper_f2, plus[upper_plane])
|
|
3236
|
+
)
|
|
3237
|
+
c_minus = c_minus.at[upper_plane].set(
|
|
3238
|
+
jnp.where(do_upper, upper_weights[1], c_minus[upper_plane])
|
|
3239
|
+
)
|
|
3240
|
+
c_center = c_center.at[upper_plane].set(
|
|
3241
|
+
jnp.where(do_upper, upper_weights[0], c_center[upper_plane])
|
|
3242
|
+
)
|
|
3243
|
+
c_plus = c_plus.at[upper_plane].set(
|
|
3244
|
+
jnp.where(do_upper, upper_weights[2], c_plus[upper_plane])
|
|
3245
|
+
)
|
|
3246
|
+
dx_min = dx_min.at[upper_plane].set(
|
|
3247
|
+
jnp.where(
|
|
3248
|
+
do_upper,
|
|
3249
|
+
jnp.abs(upper_first - upper_target),
|
|
3250
|
+
dx_min[upper_plane],
|
|
3251
|
+
)
|
|
3252
|
+
)
|
|
3253
|
+
dx_plus = dx_plus.at[upper_plane].set(
|
|
3254
|
+
jnp.where(
|
|
3255
|
+
do_upper,
|
|
3256
|
+
jnp.abs(upper_second - upper_target),
|
|
3257
|
+
dx_plus[upper_plane],
|
|
3258
|
+
)
|
|
3259
|
+
)
|
|
3260
|
+
|
|
3261
|
+
return LocalStencil1D(
|
|
3262
|
+
center=center,
|
|
3263
|
+
minus=minus,
|
|
3264
|
+
plus=plus,
|
|
3265
|
+
dx_min=dx_min,
|
|
3266
|
+
dx_plus=dx_plus,
|
|
3267
|
+
derivative_minus_weight=c_minus,
|
|
3268
|
+
derivative_center_weight=c_center,
|
|
3269
|
+
derivative_plus_weight=c_plus,
|
|
3270
|
+
)
|
|
3271
|
+
|
|
3272
|
+
|
|
3273
|
+
def build_local_direct_stencil_one_sided_physical_from_halo(
|
|
3274
|
+
field_halo: jnp.ndarray,
|
|
3275
|
+
geometry: LocalFciGeometry3D,
|
|
3276
|
+
context: StencilBuilderContext,
|
|
3277
|
+
) -> "LocalStencil3D":
|
|
3278
|
+
"""Build function for a one-sided physical-boundary local stencil.
|
|
3279
|
+
|
|
3280
|
+
This follows the ``LocalStencilBuilder`` call signature so callers can
|
|
3281
|
+
construct the intermediate builder explicitly. The domain is taken from
|
|
3282
|
+
``context.domain``.
|
|
3283
|
+
|
|
3284
|
+
Interior, shard-interface, and topology-side cells use the normal local
|
|
3285
|
+
centered stencil. True regular-coordinate physical side planes are
|
|
3286
|
+
replaced by three-point one-sided formulas whose weights are computed from
|
|
3287
|
+
the local coordinate-center positions. This is intended for intermediate
|
|
3288
|
+
fields such as ``q = grad_parallel(f)`` after halo exchange and topology
|
|
3289
|
+
filling, but before physical ghost filling.
|
|
3290
|
+
"""
|
|
3291
|
+
|
|
3292
|
+
_, _, _, LocalStencil3D = _stencil_types()
|
|
3293
|
+
|
|
3294
|
+
if not isinstance(geometry, LocalFciGeometry3D):
|
|
3295
|
+
raise TypeError(
|
|
3296
|
+
"build_local_direct_stencil_one_sided_physical_from_halo requires "
|
|
3297
|
+
f"LocalFciGeometry3D, got {type(geometry).__name__}"
|
|
3298
|
+
)
|
|
3299
|
+
if not isinstance(context, StencilBuilderContext):
|
|
3300
|
+
raise TypeError(
|
|
3301
|
+
"build_local_direct_stencil_one_sided_physical_from_halo requires "
|
|
3302
|
+
"StencilBuilderContext, "
|
|
3303
|
+
f"got {type(context).__name__}"
|
|
3304
|
+
)
|
|
3305
|
+
domain = context.domain
|
|
3306
|
+
if domain is None:
|
|
3307
|
+
raise ValueError(
|
|
3308
|
+
"build_local_direct_stencil_one_sided_physical_from_halo requires "
|
|
3309
|
+
"context.domain"
|
|
3310
|
+
)
|
|
3311
|
+
if domain.layout != geometry.layout:
|
|
3312
|
+
raise ValueError("geometry and domain must share the same HaloLayout3D")
|
|
3313
|
+
|
|
3314
|
+
field_halo = jnp.asarray(field_halo, dtype=jnp.float64)
|
|
3315
|
+
if field_halo.shape != geometry.halo_shape:
|
|
3316
|
+
raise ValueError(
|
|
3317
|
+
"field_halo must match geometry.halo_shape; "
|
|
3318
|
+
f"got {field_halo.shape}, expected {geometry.halo_shape}"
|
|
3319
|
+
)
|
|
3320
|
+
|
|
3321
|
+
centered = tuple(
|
|
3322
|
+
_local_axis_stencil_from_halo(field_halo, geometry, axis=axis)
|
|
3323
|
+
for axis in range(3)
|
|
3324
|
+
)
|
|
3325
|
+
patched = tuple(
|
|
3326
|
+
_patch_local_physical_one_sided_axis_stencil(
|
|
3327
|
+
centered[axis],
|
|
3328
|
+
field_halo,
|
|
3329
|
+
geometry,
|
|
3330
|
+
domain,
|
|
3331
|
+
axis=axis,
|
|
3332
|
+
)
|
|
3333
|
+
for axis in range(3)
|
|
3334
|
+
)
|
|
3335
|
+
return LocalStencil3D(x=patched[0], y=patched[1], z=patched[2])
|
|
3336
|
+
|
|
3337
|
+
|
|
3338
|
+
def _axis_index_nd(axis: int, index: int, ndim: int) -> tuple[object, ...]:
|
|
3339
|
+
slices: list[object] = [slice(None)] * ndim
|
|
3340
|
+
slices[axis] = index
|
|
3341
|
+
return tuple(slices)
|
|
3342
|
+
|
|
3343
|
+
|
|
3344
|
+
def _first_derivative_3d(
|
|
3345
|
+
values: jnp.ndarray,
|
|
3346
|
+
spacing: jnp.ndarray | float,
|
|
3347
|
+
*,
|
|
3348
|
+
axis: int,
|
|
3349
|
+
periodic: bool,
|
|
3350
|
+
) -> jnp.ndarray:
|
|
3351
|
+
"""Centered first derivative with periodic or second-order edge treatment."""
|
|
3352
|
+
|
|
3353
|
+
values = jnp.asarray(values, dtype=jnp.float64)
|
|
3354
|
+
h = jnp.asarray(spacing, dtype=jnp.float64)
|
|
3355
|
+
if h.ndim == 0:
|
|
3356
|
+
h = jnp.ones_like(values) * h
|
|
3357
|
+
centered = (jnp.roll(values, -1, axis=axis) - jnp.roll(values, 1, axis=axis)) / jnp.maximum(2.0 * h, 1.0e-30)
|
|
3358
|
+
if periodic:
|
|
3359
|
+
return centered
|
|
3360
|
+
|
|
3361
|
+
first = _axis_index_nd(axis, 0, values.ndim)
|
|
3362
|
+
second = _axis_index_nd(axis, 1, values.ndim)
|
|
3363
|
+
third = _axis_index_nd(axis, 2, values.ndim)
|
|
3364
|
+
last = _axis_index_nd(axis, -1, values.ndim)
|
|
3365
|
+
penultimate = _axis_index_nd(axis, -2, values.ndim)
|
|
3366
|
+
antepenultimate = _axis_index_nd(axis, -3, values.ndim)
|
|
3367
|
+
forward = (-3.0 * values[first] + 4.0 * values[second] - values[third]) / jnp.maximum(2.0 * h[first], 1.0e-30)
|
|
3368
|
+
backward = (3.0 * values[last] - 4.0 * values[penultimate] + values[antepenultimate]) / jnp.maximum(2.0 * h[last], 1.0e-30)
|
|
3369
|
+
return centered.at[first].set(forward).at[last].set(backward)
|
|
3370
|
+
|
|
3371
|
+
|
|
3372
|
+
def build_curvature_coefficients(
|
|
3373
|
+
geometry: "FciGeometry3D",
|
|
3374
|
+
*,
|
|
3375
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
3376
|
+
axis_regular_axes: tuple[bool, bool, bool] = (False, False, False),
|
|
3377
|
+
b_floor: float = 1.0e-30,
|
|
3378
|
+
jacobian_floor: float = 1.0e-30,
|
|
3379
|
+
) -> jnp.ndarray:
|
|
3380
|
+
"""Build geometry-dependent curvature coefficients for a given geometry."""
|
|
3381
|
+
|
|
3382
|
+
periodic_axes = tuple(bool(value) for value in periodic_axes)
|
|
3383
|
+
axis_regular_axes = tuple(bool(value) for value in axis_regular_axes)
|
|
3384
|
+
if any(periodic and axis_regular for periodic, axis_regular in zip(periodic_axes, axis_regular_axes)):
|
|
3385
|
+
raise ValueError(
|
|
3386
|
+
"periodic_axes and axis_regular_axes cannot both be True on the same axis; "
|
|
3387
|
+
f"got periodic_axes={periodic_axes}, axis_regular_axes={axis_regular_axes}"
|
|
3388
|
+
)
|
|
3389
|
+
if axis_regular_axes[1] or axis_regular_axes[2]:
|
|
3390
|
+
raise NotImplementedError(
|
|
3391
|
+
"axis_regular_axes currently only supports the lower x axis for curvature coefficients; "
|
|
3392
|
+
f"got axis_regular_axes={axis_regular_axes}"
|
|
3393
|
+
)
|
|
3394
|
+
if axis_regular_axes[0] and geometry.shape[1] % 2:
|
|
3395
|
+
raise ValueError("axis-regular lower-x curvature coefficients require an even poloidal grid")
|
|
3396
|
+
|
|
3397
|
+
def _covariant_field(metric: MetricGeometry, bfield: BFieldGeometry) -> jnp.ndarray:
|
|
3398
|
+
b = jnp.asarray(bfield.B_contra, dtype=jnp.float64)
|
|
3399
|
+
bmag = jnp.maximum(jnp.asarray(bfield.Bmag, dtype=jnp.float64), float(b_floor))
|
|
3400
|
+
b_unit = b / bmag[..., None]
|
|
3401
|
+
return jnp.einsum("...ij,...j->...i", metric.g_cov, b_unit) / bmag[..., None]
|
|
3402
|
+
|
|
3403
|
+
def _boundary_corrected_derivative(
|
|
3404
|
+
values: jnp.ndarray,
|
|
3405
|
+
spacing: jnp.ndarray,
|
|
3406
|
+
*,
|
|
3407
|
+
axis: int,
|
|
3408
|
+
component: int,
|
|
3409
|
+
periodic: bool,
|
|
3410
|
+
lower_face_value: jnp.ndarray,
|
|
3411
|
+
upper_face_value: jnp.ndarray,
|
|
3412
|
+
lower_center_to_face: float,
|
|
3413
|
+
upper_center_to_face: float,
|
|
3414
|
+
lower_center_to_center: float,
|
|
3415
|
+
upper_center_to_center: float,
|
|
3416
|
+
axis_regular_lower_parity: float | None = None,
|
|
3417
|
+
) -> jnp.ndarray:
|
|
3418
|
+
deriv = _first_derivative_3d(values, spacing, axis=axis, periodic=periodic)
|
|
3419
|
+
if periodic:
|
|
3420
|
+
return deriv
|
|
3421
|
+
|
|
3422
|
+
if values.shape[axis] < 3:
|
|
3423
|
+
raise ValueError("curvature coefficient construction requires at least 3 cells along each axis")
|
|
3424
|
+
|
|
3425
|
+
lower_center = values[_axis_index_nd(axis, 0, values.ndim)]
|
|
3426
|
+
if axis == 0 and axis_regular_axes[0]:
|
|
3427
|
+
half_turn = values.shape[1] // 2
|
|
3428
|
+
component_parity = float(axis_regular_lower_parity) if axis_regular_lower_parity is not None else (-1.0 if int(component) == 0 else 1.0)
|
|
3429
|
+
lower_ghost = component_parity * jnp.roll(lower_center, shift=-half_turn, axis=0)
|
|
3430
|
+
else:
|
|
3431
|
+
lower_ghost = 2.0 * lower_face_value - lower_center
|
|
3432
|
+
upper_ghost = 2.0 * upper_face_value - values[_axis_index_nd(axis, -1, values.ndim)]
|
|
3433
|
+
|
|
3434
|
+
lower_dx_min = jnp.asarray(2.0 * lower_center_to_face, dtype=jnp.float64)
|
|
3435
|
+
upper_dx_plus = jnp.asarray(2.0 * upper_center_to_face, dtype=jnp.float64)
|
|
3436
|
+
lower_dx_plus = jnp.asarray(lower_center_to_center, dtype=jnp.float64)
|
|
3437
|
+
upper_dx_min = jnp.asarray(upper_center_to_center, dtype=jnp.float64)
|
|
3438
|
+
|
|
3439
|
+
def _fd(minus: jnp.ndarray, center: jnp.ndarray, plus: jnp.ndarray, dx_min: jnp.ndarray, dx_plus: jnp.ndarray) -> jnp.ndarray:
|
|
3440
|
+
denom = jnp.maximum(dx_min * dx_plus * (dx_min + dx_plus), 1.0e-30)
|
|
3441
|
+
c_minus = -dx_plus * dx_plus / denom
|
|
3442
|
+
c_center = (dx_plus * dx_plus - dx_min * dx_min) / denom
|
|
3443
|
+
c_plus = dx_min * dx_min / denom
|
|
3444
|
+
return c_minus * minus + c_center * center + c_plus * plus
|
|
3445
|
+
|
|
3446
|
+
lower_deriv = _fd(
|
|
3447
|
+
lower_ghost,
|
|
3448
|
+
lower_center,
|
|
3449
|
+
values[_axis_index_nd(axis, 1, values.ndim)],
|
|
3450
|
+
lower_dx_min,
|
|
3451
|
+
lower_dx_plus,
|
|
3452
|
+
)
|
|
3453
|
+
upper_deriv = _fd(
|
|
3454
|
+
values[_axis_index_nd(axis, -2, values.ndim)],
|
|
3455
|
+
values[_axis_index_nd(axis, -1, values.ndim)],
|
|
3456
|
+
upper_ghost,
|
|
3457
|
+
upper_dx_min,
|
|
3458
|
+
upper_dx_plus,
|
|
3459
|
+
)
|
|
3460
|
+
return deriv.at[_axis_index_nd(axis, 0, values.ndim)].set(lower_deriv).at[_axis_index_nd(axis, -1, values.ndim)].set(upper_deriv)
|
|
3461
|
+
|
|
3462
|
+
metric = geometry.cell_metric
|
|
3463
|
+
cell_bfield = geometry.cell_bfield
|
|
3464
|
+
bmag = jnp.maximum(jnp.asarray(cell_bfield.Bmag, dtype=jnp.float64), float(b_floor))
|
|
3465
|
+
covariant_field = _covariant_field(metric, cell_bfield)
|
|
3466
|
+
|
|
3467
|
+
face_covariant_x = _covariant_field(geometry.face_metric.x, geometry.face_bfield.x)
|
|
3468
|
+
face_covariant_y = _covariant_field(geometry.face_metric.y, geometry.face_bfield.y)
|
|
3469
|
+
face_covariant_z = _covariant_field(geometry.face_metric.z, geometry.face_bfield.z)
|
|
3470
|
+
if axis_regular_axes[0]:
|
|
3471
|
+
# The collapsed lower-x face is topological rather than physical. It is
|
|
3472
|
+
# not used by the axis-regular lower derivative, but overwriting it keeps
|
|
3473
|
+
# singular face geometry from lingering in the traced computation graph.
|
|
3474
|
+
face_covariant_x = face_covariant_x.at[0].set(jnp.zeros_like(face_covariant_x[0]))
|
|
3475
|
+
|
|
3476
|
+
x_lower_center_to_face = jnp.asarray(geometry.grid.x.lower_center_to_face, dtype=jnp.float64)
|
|
3477
|
+
x_upper_center_to_face = jnp.asarray(geometry.grid.x.upper_center_to_face, dtype=jnp.float64)
|
|
3478
|
+
x_lower_center_to_center = jnp.asarray(geometry.grid.x.center_deltas[0], dtype=jnp.float64)
|
|
3479
|
+
x_upper_center_to_center = jnp.asarray(geometry.grid.x.center_deltas[-1], dtype=jnp.float64)
|
|
3480
|
+
y_lower_center_to_face = jnp.asarray(geometry.grid.y.lower_center_to_face, dtype=jnp.float64)
|
|
3481
|
+
y_upper_center_to_face = jnp.asarray(geometry.grid.y.upper_center_to_face, dtype=jnp.float64)
|
|
3482
|
+
y_lower_center_to_center = jnp.asarray(geometry.grid.y.center_deltas[0], dtype=jnp.float64)
|
|
3483
|
+
y_upper_center_to_center = jnp.asarray(geometry.grid.y.center_deltas[-1], dtype=jnp.float64)
|
|
3484
|
+
z_lower_center_to_face = jnp.asarray(geometry.grid.z.lower_center_to_face, dtype=jnp.float64)
|
|
3485
|
+
z_upper_center_to_face = jnp.asarray(geometry.grid.z.upper_center_to_face, dtype=jnp.float64)
|
|
3486
|
+
z_lower_center_to_center = jnp.asarray(geometry.grid.z.center_deltas[0], dtype=jnp.float64)
|
|
3487
|
+
z_upper_center_to_center = jnp.asarray(geometry.grid.z.center_deltas[-1], dtype=jnp.float64)
|
|
3488
|
+
|
|
3489
|
+
dcov_dx = jnp.stack(
|
|
3490
|
+
[
|
|
3491
|
+
_boundary_corrected_derivative(
|
|
3492
|
+
covariant_field[..., 0],
|
|
3493
|
+
geometry.spacing.dx,
|
|
3494
|
+
axis=0,
|
|
3495
|
+
component=0,
|
|
3496
|
+
periodic=periodic_axes[0],
|
|
3497
|
+
lower_face_value=face_covariant_x[0, ..., 0],
|
|
3498
|
+
upper_face_value=face_covariant_x[-1, ..., 0],
|
|
3499
|
+
lower_center_to_face=x_lower_center_to_face,
|
|
3500
|
+
upper_center_to_face=x_upper_center_to_face,
|
|
3501
|
+
lower_center_to_center=x_lower_center_to_center,
|
|
3502
|
+
upper_center_to_center=x_upper_center_to_center,
|
|
3503
|
+
),
|
|
3504
|
+
_boundary_corrected_derivative(
|
|
3505
|
+
covariant_field[..., 1],
|
|
3506
|
+
geometry.spacing.dx,
|
|
3507
|
+
axis=0,
|
|
3508
|
+
component=1,
|
|
3509
|
+
periodic=periodic_axes[0],
|
|
3510
|
+
lower_face_value=face_covariant_x[0, ..., 1],
|
|
3511
|
+
upper_face_value=face_covariant_x[-1, ..., 1],
|
|
3512
|
+
lower_center_to_face=x_lower_center_to_face,
|
|
3513
|
+
upper_center_to_face=x_upper_center_to_face,
|
|
3514
|
+
lower_center_to_center=x_lower_center_to_center,
|
|
3515
|
+
upper_center_to_center=x_upper_center_to_center,
|
|
3516
|
+
),
|
|
3517
|
+
_boundary_corrected_derivative(
|
|
3518
|
+
covariant_field[..., 2],
|
|
3519
|
+
geometry.spacing.dx,
|
|
3520
|
+
axis=0,
|
|
3521
|
+
component=2,
|
|
3522
|
+
periodic=periodic_axes[0],
|
|
3523
|
+
lower_face_value=face_covariant_x[0, ..., 2],
|
|
3524
|
+
upper_face_value=face_covariant_x[-1, ..., 2],
|
|
3525
|
+
lower_center_to_face=x_lower_center_to_face,
|
|
3526
|
+
upper_center_to_face=x_upper_center_to_face,
|
|
3527
|
+
lower_center_to_center=x_lower_center_to_center,
|
|
3528
|
+
upper_center_to_center=x_upper_center_to_center,
|
|
3529
|
+
),
|
|
3530
|
+
],
|
|
3531
|
+
axis=-1,
|
|
3532
|
+
)
|
|
3533
|
+
dcov_dy = jnp.stack(
|
|
3534
|
+
[
|
|
3535
|
+
_boundary_corrected_derivative(
|
|
3536
|
+
covariant_field[..., 0],
|
|
3537
|
+
geometry.spacing.dy,
|
|
3538
|
+
axis=1,
|
|
3539
|
+
component=0,
|
|
3540
|
+
periodic=periodic_axes[1],
|
|
3541
|
+
lower_face_value=face_covariant_y[:, 0, ..., 0],
|
|
3542
|
+
upper_face_value=face_covariant_y[:, -1, ..., 0],
|
|
3543
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3544
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3545
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3546
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3547
|
+
),
|
|
3548
|
+
_boundary_corrected_derivative(
|
|
3549
|
+
covariant_field[..., 1],
|
|
3550
|
+
geometry.spacing.dy,
|
|
3551
|
+
axis=1,
|
|
3552
|
+
component=1,
|
|
3553
|
+
periodic=periodic_axes[1],
|
|
3554
|
+
lower_face_value=face_covariant_y[:, 0, ..., 1],
|
|
3555
|
+
upper_face_value=face_covariant_y[:, -1, ..., 1],
|
|
3556
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3557
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3558
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3559
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3560
|
+
),
|
|
3561
|
+
_boundary_corrected_derivative(
|
|
3562
|
+
covariant_field[..., 2],
|
|
3563
|
+
geometry.spacing.dy,
|
|
3564
|
+
axis=1,
|
|
3565
|
+
component=2,
|
|
3566
|
+
periodic=periodic_axes[1],
|
|
3567
|
+
lower_face_value=face_covariant_y[:, 0, ..., 2],
|
|
3568
|
+
upper_face_value=face_covariant_y[:, -1, ..., 2],
|
|
3569
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3570
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3571
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3572
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3573
|
+
),
|
|
3574
|
+
],
|
|
3575
|
+
axis=-1,
|
|
3576
|
+
)
|
|
3577
|
+
dcov_dz = jnp.stack(
|
|
3578
|
+
[
|
|
3579
|
+
_boundary_corrected_derivative(
|
|
3580
|
+
covariant_field[..., 0],
|
|
3581
|
+
geometry.spacing.dz,
|
|
3582
|
+
axis=2,
|
|
3583
|
+
component=0,
|
|
3584
|
+
periodic=periodic_axes[2],
|
|
3585
|
+
lower_face_value=face_covariant_z[:, :, 0, 0],
|
|
3586
|
+
upper_face_value=face_covariant_z[:, :, -1, 0],
|
|
3587
|
+
lower_center_to_face=z_lower_center_to_face,
|
|
3588
|
+
upper_center_to_face=z_upper_center_to_face,
|
|
3589
|
+
lower_center_to_center=z_lower_center_to_center,
|
|
3590
|
+
upper_center_to_center=z_upper_center_to_center,
|
|
3591
|
+
),
|
|
3592
|
+
_boundary_corrected_derivative(
|
|
3593
|
+
covariant_field[..., 1],
|
|
3594
|
+
geometry.spacing.dz,
|
|
3595
|
+
axis=2,
|
|
3596
|
+
component=1,
|
|
3597
|
+
periodic=periodic_axes[2],
|
|
3598
|
+
lower_face_value=face_covariant_z[:, :, 0, 1],
|
|
3599
|
+
upper_face_value=face_covariant_z[:, :, -1, 1],
|
|
3600
|
+
lower_center_to_face=z_lower_center_to_face,
|
|
3601
|
+
upper_center_to_face=z_upper_center_to_face,
|
|
3602
|
+
lower_center_to_center=z_lower_center_to_center,
|
|
3603
|
+
upper_center_to_center=z_upper_center_to_center,
|
|
3604
|
+
),
|
|
3605
|
+
_boundary_corrected_derivative(
|
|
3606
|
+
covariant_field[..., 2],
|
|
3607
|
+
geometry.spacing.dz,
|
|
3608
|
+
axis=2,
|
|
3609
|
+
component=2,
|
|
3610
|
+
periodic=periodic_axes[2],
|
|
3611
|
+
lower_face_value=face_covariant_z[:, :, 0, 2],
|
|
3612
|
+
upper_face_value=face_covariant_z[:, :, -1, 2],
|
|
3613
|
+
lower_center_to_face=z_lower_center_to_face,
|
|
3614
|
+
upper_center_to_face=z_upper_center_to_face,
|
|
3615
|
+
lower_center_to_center=z_lower_center_to_center,
|
|
3616
|
+
upper_center_to_center=z_upper_center_to_center,
|
|
3617
|
+
),
|
|
3618
|
+
],
|
|
3619
|
+
axis=-1,
|
|
3620
|
+
)
|
|
3621
|
+
|
|
3622
|
+
curl = jnp.stack(
|
|
3623
|
+
(
|
|
3624
|
+
dcov_dy[..., 2] - dcov_dz[..., 1],
|
|
3625
|
+
dcov_dz[..., 0] - dcov_dx[..., 2],
|
|
3626
|
+
dcov_dx[..., 1] - dcov_dy[..., 0],
|
|
3627
|
+
),
|
|
3628
|
+
axis=-1,
|
|
3629
|
+
)
|
|
3630
|
+
coefficient = bmag / (2.0 * jnp.maximum(jnp.asarray(metric.J, dtype=jnp.float64), float(jacobian_floor)))
|
|
3631
|
+
curvature_coefficients = coefficient[..., None] * curl
|
|
3632
|
+
|
|
3633
|
+
if axis_regular_axes[0]:
|
|
3634
|
+
rho = jnp.asarray(geometry.grid.x.centers, dtype=jnp.float64)
|
|
3635
|
+
theta = jnp.asarray(geometry.grid.y.centers, dtype=jnp.float64)
|
|
3636
|
+
rho_values = rho[:, None, None]
|
|
3637
|
+
rho_safe = jnp.maximum(rho_values, 1.0e-30)
|
|
3638
|
+
theta_values = theta[None, :, None]
|
|
3639
|
+
cos_theta = jnp.cos(theta_values)
|
|
3640
|
+
sin_theta = jnp.sin(theta_values)
|
|
3641
|
+
|
|
3642
|
+
A_rho = covariant_field[..., 0]
|
|
3643
|
+
A_theta = covariant_field[..., 1]
|
|
3644
|
+
A_zeta = covariant_field[..., 2]
|
|
3645
|
+
|
|
3646
|
+
A_X = A_rho * cos_theta - A_theta * sin_theta / rho_safe
|
|
3647
|
+
A_Y = A_rho * sin_theta + A_theta * cos_theta / rho_safe
|
|
3648
|
+
A_Z = A_zeta
|
|
3649
|
+
|
|
3650
|
+
x_upper_face_rho = jnp.asarray(geometry.grid.x.faces[-1], dtype=jnp.float64)
|
|
3651
|
+
x_upper_face_rho_safe = jnp.maximum(x_upper_face_rho, 1.0e-30)
|
|
3652
|
+
x_upper_A_X = face_covariant_x[-1, ..., 0] * cos_theta[0] - face_covariant_x[-1, ..., 1] * sin_theta[0] / x_upper_face_rho_safe
|
|
3653
|
+
x_upper_A_Y = face_covariant_x[-1, ..., 0] * sin_theta[0] + face_covariant_x[-1, ..., 1] * cos_theta[0] / x_upper_face_rho_safe
|
|
3654
|
+
x_upper_A_Z = face_covariant_x[-1, ..., 2]
|
|
3655
|
+
|
|
3656
|
+
def _axis_regular_cartesian_x_derivative(values: jnp.ndarray, upper_face_value: jnp.ndarray) -> jnp.ndarray:
|
|
3657
|
+
return _boundary_corrected_derivative(
|
|
3658
|
+
values,
|
|
3659
|
+
geometry.spacing.dx,
|
|
3660
|
+
axis=0,
|
|
3661
|
+
component=0,
|
|
3662
|
+
periodic=False,
|
|
3663
|
+
lower_face_value=jnp.zeros_like(upper_face_value),
|
|
3664
|
+
upper_face_value=upper_face_value,
|
|
3665
|
+
lower_center_to_face=x_lower_center_to_face,
|
|
3666
|
+
upper_center_to_face=x_upper_center_to_face,
|
|
3667
|
+
lower_center_to_center=x_lower_center_to_center,
|
|
3668
|
+
upper_center_to_center=x_upper_center_to_center,
|
|
3669
|
+
axis_regular_lower_parity=1.0,
|
|
3670
|
+
)
|
|
3671
|
+
|
|
3672
|
+
dA_X_drho = _axis_regular_cartesian_x_derivative(A_X, x_upper_A_X)
|
|
3673
|
+
dA_Y_drho = _axis_regular_cartesian_x_derivative(A_Y, x_upper_A_Y)
|
|
3674
|
+
dA_Z_drho = _axis_regular_cartesian_x_derivative(A_Z, x_upper_A_Z)
|
|
3675
|
+
|
|
3676
|
+
rho_xz_safe = jnp.maximum(rho[:, None], 1.0e-30)
|
|
3677
|
+
y_lower_theta = jnp.asarray(geometry.grid.y.faces[0], dtype=jnp.float64)
|
|
3678
|
+
y_upper_theta = jnp.asarray(geometry.grid.y.faces[-1], dtype=jnp.float64)
|
|
3679
|
+
y_lower_cos_theta = jnp.cos(y_lower_theta)
|
|
3680
|
+
y_lower_sin_theta = jnp.sin(y_lower_theta)
|
|
3681
|
+
y_upper_cos_theta = jnp.cos(y_upper_theta)
|
|
3682
|
+
y_upper_sin_theta = jnp.sin(y_upper_theta)
|
|
3683
|
+
y_lower_A_X = face_covariant_y[:, 0, :, 0] * y_lower_cos_theta - face_covariant_y[:, 0, :, 1] * y_lower_sin_theta / rho_xz_safe
|
|
3684
|
+
y_upper_A_X = face_covariant_y[:, -1, :, 0] * y_upper_cos_theta - face_covariant_y[:, -1, :, 1] * y_upper_sin_theta / rho_xz_safe
|
|
3685
|
+
y_lower_A_Y = face_covariant_y[:, 0, :, 0] * y_lower_sin_theta + face_covariant_y[:, 0, :, 1] * y_lower_cos_theta / rho_xz_safe
|
|
3686
|
+
y_upper_A_Y = face_covariant_y[:, -1, :, 0] * y_upper_sin_theta + face_covariant_y[:, -1, :, 1] * y_upper_cos_theta / rho_xz_safe
|
|
3687
|
+
y_lower_A_Z = face_covariant_y[:, 0, :, 2]
|
|
3688
|
+
y_upper_A_Z = face_covariant_y[:, -1, :, 2]
|
|
3689
|
+
|
|
3690
|
+
dA_X_dtheta = _boundary_corrected_derivative(
|
|
3691
|
+
A_X,
|
|
3692
|
+
geometry.spacing.dy,
|
|
3693
|
+
axis=1,
|
|
3694
|
+
component=0,
|
|
3695
|
+
periodic=periodic_axes[1],
|
|
3696
|
+
lower_face_value=y_lower_A_X,
|
|
3697
|
+
upper_face_value=y_upper_A_X,
|
|
3698
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3699
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3700
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3701
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3702
|
+
)
|
|
3703
|
+
dA_Y_dtheta = _boundary_corrected_derivative(
|
|
3704
|
+
A_Y,
|
|
3705
|
+
geometry.spacing.dy,
|
|
3706
|
+
axis=1,
|
|
3707
|
+
component=1,
|
|
3708
|
+
periodic=periodic_axes[1],
|
|
3709
|
+
lower_face_value=y_lower_A_Y,
|
|
3710
|
+
upper_face_value=y_upper_A_Y,
|
|
3711
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3712
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3713
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3714
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3715
|
+
)
|
|
3716
|
+
dA_Z_dtheta = _boundary_corrected_derivative(
|
|
3717
|
+
A_Z,
|
|
3718
|
+
geometry.spacing.dy,
|
|
3719
|
+
axis=1,
|
|
3720
|
+
component=2,
|
|
3721
|
+
periodic=periodic_axes[1],
|
|
3722
|
+
lower_face_value=y_lower_A_Z,
|
|
3723
|
+
upper_face_value=y_upper_A_Z,
|
|
3724
|
+
lower_center_to_face=y_lower_center_to_face,
|
|
3725
|
+
upper_center_to_face=y_upper_center_to_face,
|
|
3726
|
+
lower_center_to_center=y_lower_center_to_center,
|
|
3727
|
+
upper_center_to_center=y_upper_center_to_center,
|
|
3728
|
+
)
|
|
3729
|
+
|
|
3730
|
+
rho_xy_safe = jnp.maximum(rho[:, None], 1.0e-30)
|
|
3731
|
+
theta_xy = theta[None, :]
|
|
3732
|
+
cos_theta_xy = jnp.cos(theta_xy)
|
|
3733
|
+
sin_theta_xy = jnp.sin(theta_xy)
|
|
3734
|
+
z_lower_A_X = face_covariant_z[:, :, 0, 0] * cos_theta_xy - face_covariant_z[:, :, 0, 1] * sin_theta_xy / rho_xy_safe
|
|
3735
|
+
z_upper_A_X = face_covariant_z[:, :, -1, 0] * cos_theta_xy - face_covariant_z[:, :, -1, 1] * sin_theta_xy / rho_xy_safe
|
|
3736
|
+
z_lower_A_Y = face_covariant_z[:, :, 0, 0] * sin_theta_xy + face_covariant_z[:, :, 0, 1] * cos_theta_xy / rho_xy_safe
|
|
3737
|
+
z_upper_A_Y = face_covariant_z[:, :, -1, 0] * sin_theta_xy + face_covariant_z[:, :, -1, 1] * cos_theta_xy / rho_xy_safe
|
|
3738
|
+
z_lower_A_Z = face_covariant_z[:, :, 0, 2]
|
|
3739
|
+
z_upper_A_Z = face_covariant_z[:, :, -1, 2]
|
|
3740
|
+
|
|
3741
|
+
dA_X_dzeta = _boundary_corrected_derivative(
|
|
3742
|
+
A_X,
|
|
3743
|
+
geometry.spacing.dz,
|
|
3744
|
+
axis=2,
|
|
3745
|
+
component=0,
|
|
3746
|
+
periodic=periodic_axes[2],
|
|
3747
|
+
lower_face_value=z_lower_A_X,
|
|
3748
|
+
upper_face_value=z_upper_A_X,
|
|
3749
|
+
lower_center_to_face=z_lower_center_to_face,
|
|
3750
|
+
upper_center_to_face=z_upper_center_to_face,
|
|
3751
|
+
lower_center_to_center=z_lower_center_to_center,
|
|
3752
|
+
upper_center_to_center=z_upper_center_to_center,
|
|
3753
|
+
)
|
|
3754
|
+
dA_Y_dzeta = _boundary_corrected_derivative(
|
|
3755
|
+
A_Y,
|
|
3756
|
+
geometry.spacing.dz,
|
|
3757
|
+
axis=2,
|
|
3758
|
+
component=1,
|
|
3759
|
+
periodic=periodic_axes[2],
|
|
3760
|
+
lower_face_value=z_lower_A_Y,
|
|
3761
|
+
upper_face_value=z_upper_A_Y,
|
|
3762
|
+
lower_center_to_face=z_lower_center_to_face,
|
|
3763
|
+
upper_center_to_face=z_upper_center_to_face,
|
|
3764
|
+
lower_center_to_center=z_lower_center_to_center,
|
|
3765
|
+
upper_center_to_center=z_upper_center_to_center,
|
|
3766
|
+
)
|
|
3767
|
+
inv_rho = 1.0 / rho_safe
|
|
3768
|
+
dA_X_dY = sin_theta * dA_X_drho + cos_theta * inv_rho * dA_X_dtheta
|
|
3769
|
+
dA_Y_dX = cos_theta * dA_Y_drho - sin_theta * inv_rho * dA_Y_dtheta
|
|
3770
|
+
dA_Z_dX = cos_theta * dA_Z_drho - sin_theta * inv_rho * dA_Z_dtheta
|
|
3771
|
+
dA_Z_dY = sin_theta * dA_Z_drho + cos_theta * inv_rho * dA_Z_dtheta
|
|
3772
|
+
|
|
3773
|
+
cartesian_curl = jnp.stack(
|
|
3774
|
+
(
|
|
3775
|
+
dA_Z_dY - dA_Y_dzeta,
|
|
3776
|
+
dA_X_dzeta - dA_Z_dX,
|
|
3777
|
+
dA_Y_dX - dA_X_dY,
|
|
3778
|
+
),
|
|
3779
|
+
axis=-1,
|
|
3780
|
+
)
|
|
3781
|
+
cartesian_coefficient = (
|
|
3782
|
+
bmag
|
|
3783
|
+
* rho_safe
|
|
3784
|
+
/ (2.0 * jnp.maximum(jnp.asarray(metric.J, dtype=jnp.float64), float(jacobian_floor)))
|
|
3785
|
+
)
|
|
3786
|
+
C_X = cartesian_coefficient * cartesian_curl[..., 0]
|
|
3787
|
+
C_Y = cartesian_coefficient * cartesian_curl[..., 1]
|
|
3788
|
+
C_Z = cartesian_coefficient * cartesian_curl[..., 2]
|
|
3789
|
+
axis_regular_lower_coefficients = jnp.stack(
|
|
3790
|
+
(
|
|
3791
|
+
C_X * cos_theta + C_Y * sin_theta,
|
|
3792
|
+
(-C_X * sin_theta + C_Y * cos_theta) / rho_safe,
|
|
3793
|
+
C_Z,
|
|
3794
|
+
),
|
|
3795
|
+
axis=-1,
|
|
3796
|
+
)
|
|
3797
|
+
curvature_coefficients = curvature_coefficients.at[0].set(axis_regular_lower_coefficients[0])
|
|
3798
|
+
|
|
3799
|
+
return curvature_coefficients
|
|
3800
|
+
|
|
3801
|
+
|
|
3802
|
+
def _physical_domain_valid_mask(
|
|
3803
|
+
grid: CellCenteredGrid3D,
|
|
3804
|
+
x: jnp.ndarray,
|
|
3805
|
+
y: jnp.ndarray,
|
|
3806
|
+
z: jnp.ndarray,
|
|
3807
|
+
*,
|
|
3808
|
+
periodic_axes: tuple[bool, bool, bool],
|
|
3809
|
+
) -> jnp.ndarray:
|
|
3810
|
+
valid_x = jnp.isfinite(x) if periodic_axes[0] else (jnp.isfinite(x) & (x >= grid.x.faces[0]) & (x <= grid.x.faces[-1]))
|
|
3811
|
+
valid_y = jnp.isfinite(y) if periodic_axes[1] else (jnp.isfinite(y) & (y >= grid.y.faces[0]) & (y <= grid.y.faces[-1]))
|
|
3812
|
+
valid_z = jnp.isfinite(z) if periodic_axes[2] else (jnp.isfinite(z) & (z >= grid.z.faces[0]) & (z <= grid.z.faces[-1]))
|
|
3813
|
+
return valid_x & valid_y & valid_z
|
|
3814
|
+
|
|
3815
|
+
|
|
3816
|
+
def _extend_axis_with_ghost_cells(grid_axis: Grid1D) -> jnp.ndarray:
|
|
3817
|
+
centers = grid_axis.centers
|
|
3818
|
+
faces = grid_axis.faces
|
|
3819
|
+
lower = 2.0 * faces[0] - centers[0]
|
|
3820
|
+
upper = 2.0 * faces[-1] - centers[-1]
|
|
3821
|
+
return jnp.concatenate([
|
|
3822
|
+
jnp.asarray([lower], dtype=jnp.float64),
|
|
3823
|
+
centers,
|
|
3824
|
+
jnp.asarray([upper], dtype=jnp.float64),
|
|
3825
|
+
])
|
|
3826
|
+
|
|
3827
|
+
def _extend_values_x(values: jnp.ndarray, grid_axis: Grid1D) -> jnp.ndarray:
|
|
3828
|
+
centers = grid_axis.centers
|
|
3829
|
+
faces = grid_axis.faces
|
|
3830
|
+
xg_lower = 2.0 * faces[0] - centers[0]
|
|
3831
|
+
xg_upper = 2.0 * faces[-1] - centers[-1]
|
|
3832
|
+
|
|
3833
|
+
if values.shape[0] == 1:
|
|
3834
|
+
lower = values[0:1, :, :]
|
|
3835
|
+
upper = values[-1:, :, :]
|
|
3836
|
+
else:
|
|
3837
|
+
lower_slope = (values[1, :, :] - values[0, :, :]) / (centers[1] - centers[0])
|
|
3838
|
+
upper_slope = (values[-1, :, :] - values[-2, :, :]) / (centers[-1] - centers[-2])
|
|
3839
|
+
|
|
3840
|
+
lower = (values[0, :, :] + (xg_lower - centers[0]) * lower_slope)[None, :, :]
|
|
3841
|
+
upper = (values[-1, :, :] + (xg_upper - centers[-1]) * upper_slope)[None, :, :]
|
|
3842
|
+
|
|
3843
|
+
return jnp.concatenate([lower, values, upper], axis=0)
|
|
3844
|
+
|
|
3845
|
+
|
|
3846
|
+
def _extend_values_y(values: jnp.ndarray, grid_axis: Grid1D) -> jnp.ndarray:
|
|
3847
|
+
centers = grid_axis.centers
|
|
3848
|
+
faces = grid_axis.faces
|
|
3849
|
+
yg_lower = 2.0 * faces[0] - centers[0]
|
|
3850
|
+
yg_upper = 2.0 * faces[-1] - centers[-1]
|
|
3851
|
+
|
|
3852
|
+
if values.shape[1] == 1:
|
|
3853
|
+
lower = values[:, 0:1, :]
|
|
3854
|
+
upper = values[:, -1:, :]
|
|
3855
|
+
else:
|
|
3856
|
+
lower_slope = (values[:, 1, :] - values[:, 0, :]) / (centers[1] - centers[0])
|
|
3857
|
+
upper_slope = (values[:, -1, :] - values[:, -2, :]) / (centers[-1] - centers[-2])
|
|
3858
|
+
|
|
3859
|
+
lower = (values[:, 0, :] + (yg_lower - centers[0]) * lower_slope)[:, None, :]
|
|
3860
|
+
upper = (values[:, -1, :] + (yg_upper - centers[-1]) * upper_slope)[:, None, :]
|
|
3861
|
+
|
|
3862
|
+
return jnp.concatenate([lower, values, upper], axis=1)
|
|
3863
|
+
|
|
3864
|
+
|
|
3865
|
+
def _extend_values_z(values: jnp.ndarray, grid_axis: Grid1D) -> jnp.ndarray:
|
|
3866
|
+
centers = grid_axis.centers
|
|
3867
|
+
faces = grid_axis.faces
|
|
3868
|
+
zg_lower = 2.0 * faces[0] - centers[0]
|
|
3869
|
+
zg_upper = 2.0 * faces[-1] - centers[-1]
|
|
3870
|
+
|
|
3871
|
+
if values.shape[2] == 1:
|
|
3872
|
+
lower = values[:, :, 0:1]
|
|
3873
|
+
upper = values[:, :, -1:]
|
|
3874
|
+
else:
|
|
3875
|
+
lower_slope = (values[:, :, 1] - values[:, :, 0]) / (centers[1] - centers[0])
|
|
3876
|
+
upper_slope = (values[:, :, -1] - values[:, :, -2]) / (centers[-1] - centers[-2])
|
|
3877
|
+
|
|
3878
|
+
lower = (values[:, :, 0] + (zg_lower - centers[0]) * lower_slope)[:, :, None]
|
|
3879
|
+
upper = (values[:, :, -1] + (zg_upper - centers[-1]) * upper_slope)[:, :, None]
|
|
3880
|
+
|
|
3881
|
+
return jnp.concatenate([lower, values, upper], axis=2)
|
|
3882
|
+
|
|
3883
|
+
def _interpolate_scalar_cell_centered(
|
|
3884
|
+
values: jnp.ndarray,
|
|
3885
|
+
x: jnp.ndarray,
|
|
3886
|
+
y: jnp.ndarray,
|
|
3887
|
+
z: jnp.ndarray,
|
|
3888
|
+
*,
|
|
3889
|
+
grid: CellCenteredGrid3D,
|
|
3890
|
+
periodic_axes: tuple[bool, bool, bool],
|
|
3891
|
+
boundary_value: float,
|
|
3892
|
+
) -> jnp.ndarray:
|
|
3893
|
+
values = jnp.asarray(values, dtype=jnp.float64)
|
|
3894
|
+
x = jnp.asarray(x, dtype=jnp.float64)
|
|
3895
|
+
y = jnp.asarray(y, dtype=jnp.float64)
|
|
3896
|
+
z = jnp.asarray(z, dtype=jnp.float64)
|
|
3897
|
+
|
|
3898
|
+
if values.shape != grid.shape:
|
|
3899
|
+
raise ValueError(f"values must have shape {grid.shape}, got {values.shape}")
|
|
3900
|
+
|
|
3901
|
+
valid = _physical_domain_valid_mask(
|
|
3902
|
+
grid,
|
|
3903
|
+
x,
|
|
3904
|
+
y,
|
|
3905
|
+
z,
|
|
3906
|
+
periodic_axes=periodic_axes,
|
|
3907
|
+
)
|
|
3908
|
+
|
|
3909
|
+
if periodic_axes[0]:
|
|
3910
|
+
x_axis = grid.x.centers
|
|
3911
|
+
x0, x1, wx, _ = _bracket_axis(x_axis, x, periodic=True)
|
|
3912
|
+
else:
|
|
3913
|
+
values = _extend_values_x(values, grid.x)
|
|
3914
|
+
x_axis = _extend_axis_with_ghost_cells(grid.x)
|
|
3915
|
+
x0, x1, wx, _ = _bracket_axis(x_axis, x, periodic=False)
|
|
3916
|
+
|
|
3917
|
+
if periodic_axes[1]:
|
|
3918
|
+
y_axis = grid.y.centers
|
|
3919
|
+
y0, y1, wy, _ = _bracket_axis(y_axis, y, periodic=True)
|
|
3920
|
+
else:
|
|
3921
|
+
values = _extend_values_y(values, grid.y)
|
|
3922
|
+
y_axis = _extend_axis_with_ghost_cells(grid.y)
|
|
3923
|
+
y0, y1, wy, _ = _bracket_axis(y_axis, y, periodic=False)
|
|
3924
|
+
|
|
3925
|
+
if periodic_axes[2]:
|
|
3926
|
+
z_axis = grid.z.centers
|
|
3927
|
+
z0, z1, wz, _ = _bracket_axis(z_axis, z, periodic=True)
|
|
3928
|
+
else:
|
|
3929
|
+
values = _extend_values_z(values, grid.z)
|
|
3930
|
+
z_axis = _extend_axis_with_ghost_cells(grid.z)
|
|
3931
|
+
z0, z1, wz, _ = _bracket_axis(z_axis, z, periodic=False)
|
|
3932
|
+
|
|
3933
|
+
interpolated = _trilinear_sample(values, x0, x1, wx, y0, y1, wy, z0, z1, wz)
|
|
3934
|
+
return jnp.where(
|
|
3935
|
+
valid,
|
|
3936
|
+
interpolated,
|
|
3937
|
+
jnp.asarray(boundary_value, dtype=jnp.float64),
|
|
3938
|
+
)
|
|
3939
|
+
|
|
3940
|
+
|
|
3941
|
+
def _interpolate_B_contravariant_cell_centered(
|
|
3942
|
+
grid: CellCenteredGrid3D,
|
|
3943
|
+
B_contra_cell: jnp.ndarray,
|
|
3944
|
+
points: jnp.ndarray,
|
|
3945
|
+
*,
|
|
3946
|
+
periodic_axes: tuple[bool, bool, bool],
|
|
3947
|
+
boundary_value: float,
|
|
3948
|
+
) -> jnp.ndarray:
|
|
3949
|
+
sampled_points = jnp.asarray(points, dtype=jnp.float64)
|
|
3950
|
+
if sampled_points.shape == (3,):
|
|
3951
|
+
sampled_points = sampled_points[None, :]
|
|
3952
|
+
squeeze = True
|
|
3953
|
+
else:
|
|
3954
|
+
if sampled_points.ndim < 1 or sampled_points.shape[-1] != 3:
|
|
3955
|
+
raise ValueError(f"points must have shape (3,) or (..., 3), got {sampled_points.shape}")
|
|
3956
|
+
squeeze = False
|
|
3957
|
+
|
|
3958
|
+
B_contra_cell = jnp.asarray(B_contra_cell, dtype=jnp.float64)
|
|
3959
|
+
if B_contra_cell.shape != grid.shape + (3,):
|
|
3960
|
+
raise ValueError(f"B_contra_cell must have shape {grid.shape + (3,)}, got {B_contra_cell.shape}")
|
|
3961
|
+
|
|
3962
|
+
samples = []
|
|
3963
|
+
for component in range(3):
|
|
3964
|
+
samples.append(
|
|
3965
|
+
_interpolate_scalar_cell_centered(
|
|
3966
|
+
B_contra_cell[..., component],
|
|
3967
|
+
sampled_points[..., 0],
|
|
3968
|
+
sampled_points[..., 1],
|
|
3969
|
+
sampled_points[..., 2],
|
|
3970
|
+
grid=grid,
|
|
3971
|
+
periodic_axes=periodic_axes,
|
|
3972
|
+
boundary_value=boundary_value,
|
|
3973
|
+
)
|
|
3974
|
+
)
|
|
3975
|
+
result = jnp.stack(samples, axis=-1)
|
|
3976
|
+
return result[0] if squeeze else result
|
|
3977
|
+
|
|
3978
|
+
|
|
3979
|
+
def _rk4_step_cell_centered(
|
|
3980
|
+
grid: CellCenteredGrid3D,
|
|
3981
|
+
B_contra_cell: jnp.ndarray,
|
|
3982
|
+
point: jnp.ndarray,
|
|
3983
|
+
step: float,
|
|
3984
|
+
*,
|
|
3985
|
+
periodic_axes: tuple[bool, bool, bool],
|
|
3986
|
+
min_abs_bz: float,
|
|
3987
|
+
boundary_value: float,
|
|
3988
|
+
) -> jnp.ndarray:
|
|
3989
|
+
state = jnp.asarray(point, dtype=jnp.float64)
|
|
3990
|
+
if state.shape != (3,):
|
|
3991
|
+
raise ValueError(f"point must have shape (3,), got {state.shape}")
|
|
3992
|
+
h = float(step)
|
|
3993
|
+
|
|
3994
|
+
def rhs(value: jnp.ndarray) -> jnp.ndarray:
|
|
3995
|
+
b = _interpolate_B_contravariant_cell_centered(
|
|
3996
|
+
grid,
|
|
3997
|
+
B_contra_cell,
|
|
3998
|
+
value,
|
|
3999
|
+
periodic_axes=periodic_axes,
|
|
4000
|
+
boundary_value=boundary_value,
|
|
4001
|
+
)
|
|
4002
|
+
bz = jnp.asarray(b[2], dtype=jnp.float64)
|
|
4003
|
+
safe_bz = jnp.where(jnp.abs(bz) < min_abs_bz, jnp.where(bz < 0.0, -1.0, 1.0) * (min_abs_bz), bz)
|
|
4004
|
+
return jnp.stack((b[0] / safe_bz, b[1] / safe_bz, jnp.array(1.0, dtype=jnp.float64)))
|
|
4005
|
+
|
|
4006
|
+
k1 = rhs(state)
|
|
4007
|
+
k2 = rhs(state + 0.5 * h * k1)
|
|
4008
|
+
k3 = rhs(state + 0.5 * h * k2)
|
|
4009
|
+
k4 = rhs(state + h * k3)
|
|
4010
|
+
return state + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
|
|
4011
|
+
|
|
4012
|
+
|
|
4013
|
+
def _trace_fieldline_to_plane_cell_centered(
|
|
4014
|
+
grid: CellCenteredGrid3D,
|
|
4015
|
+
B_contra_cell: jnp.ndarray,
|
|
4016
|
+
Bmag_cell: jnp.ndarray,
|
|
4017
|
+
seed_points: jnp.ndarray,
|
|
4018
|
+
*,
|
|
4019
|
+
step: float,
|
|
4020
|
+
substeps: int,
|
|
4021
|
+
periodic_axes: tuple[bool, bool, bool],
|
|
4022
|
+
min_abs_bz: float,
|
|
4023
|
+
boundary_value: float,
|
|
4024
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
4025
|
+
points = jnp.asarray(seed_points, dtype=jnp.float64)
|
|
4026
|
+
if points.ndim != 2 or points.shape[-1] != 3:
|
|
4027
|
+
raise ValueError(f"seed_points must have shape (n, 3), got {points.shape}")
|
|
4028
|
+
|
|
4029
|
+
nseed = int(points.shape[0])
|
|
4030
|
+
step_size = float(step) / float(max(int(substeps), 1))
|
|
4031
|
+
length = jnp.zeros(nseed, dtype=jnp.float64)
|
|
4032
|
+
alive = jnp.ones(nseed, dtype=bool)
|
|
4033
|
+
state = points
|
|
4034
|
+
|
|
4035
|
+
def _speed(sampled_b: jnp.ndarray, sampled_bmag: jnp.ndarray) -> jnp.ndarray:
|
|
4036
|
+
bz = sampled_b[..., 2]
|
|
4037
|
+
safe_bz = jnp.where(
|
|
4038
|
+
jnp.abs(bz) < min_abs_bz,
|
|
4039
|
+
jnp.where(bz < 0.0, -1.0, 1.0) * min_abs_bz,
|
|
4040
|
+
bz,
|
|
4041
|
+
)
|
|
4042
|
+
return jnp.asarray(sampled_bmag, dtype=jnp.float64) / jnp.maximum(jnp.abs(safe_bz), 1.0e-30)
|
|
4043
|
+
|
|
4044
|
+
for _ in range(max(int(substeps), 1)):
|
|
4045
|
+
b0 = jax.vmap(
|
|
4046
|
+
lambda point: _interpolate_B_contravariant_cell_centered(
|
|
4047
|
+
grid,
|
|
4048
|
+
B_contra_cell,
|
|
4049
|
+
point,
|
|
4050
|
+
periodic_axes=periodic_axes,
|
|
4051
|
+
boundary_value=boundary_value,
|
|
4052
|
+
)
|
|
4053
|
+
)(state)
|
|
4054
|
+
bmag0 = _interpolate_scalar_cell_centered(
|
|
4055
|
+
Bmag_cell,
|
|
4056
|
+
state[:, 0],
|
|
4057
|
+
state[:, 1],
|
|
4058
|
+
state[:, 2],
|
|
4059
|
+
grid=grid,
|
|
4060
|
+
periodic_axes=periodic_axes,
|
|
4061
|
+
boundary_value=boundary_value,
|
|
4062
|
+
)
|
|
4063
|
+
next_state = jax.vmap(
|
|
4064
|
+
lambda point: _rk4_step_cell_centered(
|
|
4065
|
+
grid,
|
|
4066
|
+
B_contra_cell,
|
|
4067
|
+
point,
|
|
4068
|
+
step_size,
|
|
4069
|
+
periodic_axes=periodic_axes,
|
|
4070
|
+
min_abs_bz=min_abs_bz,
|
|
4071
|
+
boundary_value=boundary_value,
|
|
4072
|
+
)
|
|
4073
|
+
)(state)
|
|
4074
|
+
bmag1 = _interpolate_scalar_cell_centered(
|
|
4075
|
+
Bmag_cell,
|
|
4076
|
+
next_state[:, 0],
|
|
4077
|
+
next_state[:, 1],
|
|
4078
|
+
next_state[:, 2],
|
|
4079
|
+
grid=grid,
|
|
4080
|
+
periodic_axes=periodic_axes,
|
|
4081
|
+
boundary_value=boundary_value,
|
|
4082
|
+
)
|
|
4083
|
+
b1 = jax.vmap(
|
|
4084
|
+
lambda point: _interpolate_B_contravariant_cell_centered(
|
|
4085
|
+
grid,
|
|
4086
|
+
B_contra_cell,
|
|
4087
|
+
point,
|
|
4088
|
+
periodic_axes=periodic_axes,
|
|
4089
|
+
boundary_value=boundary_value,
|
|
4090
|
+
)
|
|
4091
|
+
)(next_state)
|
|
4092
|
+
finite = jnp.all(jnp.isfinite(next_state), axis=-1)
|
|
4093
|
+
valid = _physical_domain_valid_mask(
|
|
4094
|
+
grid,
|
|
4095
|
+
next_state[:, 0],
|
|
4096
|
+
next_state[:, 1],
|
|
4097
|
+
next_state[:, 2],
|
|
4098
|
+
periodic_axes=periodic_axes,
|
|
4099
|
+
)
|
|
4100
|
+
increment = 0.5 * abs(step_size) * (_speed(b0, bmag0) + _speed(b1, bmag1))
|
|
4101
|
+
increment = jnp.where(alive & finite & valid, increment, 0.0)
|
|
4102
|
+
length = length + increment
|
|
4103
|
+
state = jnp.where((alive & finite & valid)[..., None], next_state, state)
|
|
4104
|
+
alive = alive & finite & valid
|
|
4105
|
+
|
|
4106
|
+
return state, length, ~alive
|
|
4107
|
+
|
|
4108
|
+
|
|
4109
|
+
def build_fci_maps_from_b_contravariant(
|
|
4110
|
+
grid: CellCenteredGrid3D,
|
|
4111
|
+
B_contra_cell: jnp.ndarray,
|
|
4112
|
+
Bmag_cell: jnp.ndarray,
|
|
4113
|
+
*,
|
|
4114
|
+
substeps: int = 4,
|
|
4115
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
4116
|
+
min_abs_bz: float = 1.0e-30,
|
|
4117
|
+
boundary_value: float = jnp.nan,
|
|
4118
|
+
) -> dict[str, jnp.ndarray]:
|
|
4119
|
+
"""JIT-compatible FCI map builder for a cell-centered grid.
|
|
4120
|
+
|
|
4121
|
+
Improved boundary behavior:
|
|
4122
|
+
- Traces start from active cell centers.
|
|
4123
|
+
- If a trace reaches the target toroidal plane, normal FCI maps are stored.
|
|
4124
|
+
- If a trace exits a nonperiodic physical boundary first, the boundary
|
|
4125
|
+
mask is set and the endpoint is estimated at the boundary face.
|
|
4126
|
+
- forward_length/backward_length are distances to the actual endpoint:
|
|
4127
|
+
target plane if no boundary hit,
|
|
4128
|
+
boundary hit point if boundary=True.
|
|
4129
|
+
|
|
4130
|
+
For jitting:
|
|
4131
|
+
|
|
4132
|
+
build_maps_jit = jax.jit(
|
|
4133
|
+
build_fci_maps_from_b_contravariant,
|
|
4134
|
+
static_argnames=("substeps", "periodic_axes"),
|
|
4135
|
+
)
|
|
4136
|
+
"""
|
|
4137
|
+
|
|
4138
|
+
shape = grid.shape
|
|
4139
|
+
nx, ny, nz = shape
|
|
4140
|
+
nxy = nx * ny
|
|
4141
|
+
|
|
4142
|
+
B_contra_cell = jnp.asarray(B_contra_cell, dtype=jnp.float64)
|
|
4143
|
+
Bmag_cell = jnp.asarray(Bmag_cell, dtype=jnp.float64)
|
|
4144
|
+
|
|
4145
|
+
if B_contra_cell.shape != shape + (3,):
|
|
4146
|
+
raise ValueError(
|
|
4147
|
+
f"B_contra_cell must have shape {shape + (3,)}, got {B_contra_cell.shape}"
|
|
4148
|
+
)
|
|
4149
|
+
if Bmag_cell.shape != shape:
|
|
4150
|
+
raise ValueError(f"Bmag_cell must have shape {shape}, got {Bmag_cell.shape}")
|
|
4151
|
+
|
|
4152
|
+
n_substeps = int(substeps)
|
|
4153
|
+
if n_substeps < 1:
|
|
4154
|
+
raise ValueError(f"substeps must be >= 1, got {substeps}")
|
|
4155
|
+
|
|
4156
|
+
x_axis = grid.x.centers
|
|
4157
|
+
y_axis = grid.y.centers
|
|
4158
|
+
z_axis = grid.z.centers
|
|
4159
|
+
|
|
4160
|
+
xx = jnp.broadcast_to(x_axis[:, None], (nx, ny)).reshape(-1)
|
|
4161
|
+
yy = jnp.broadcast_to(y_axis[None, :], (nx, ny)).reshape(-1)
|
|
4162
|
+
|
|
4163
|
+
periodic_x = bool(periodic_axes[0])
|
|
4164
|
+
periodic_y = bool(periodic_axes[1])
|
|
4165
|
+
periodic_z = bool(periodic_axes[2])
|
|
4166
|
+
|
|
4167
|
+
min_bz = jnp.asarray(min_abs_bz, dtype=jnp.float64)
|
|
4168
|
+
|
|
4169
|
+
def _wrap_periodic_coord(coord: jnp.ndarray, axis: Grid1D, periodic: bool) -> jnp.ndarray:
|
|
4170
|
+
if periodic:
|
|
4171
|
+
period = axis.faces[-1] - axis.faces[0]
|
|
4172
|
+
return jnp.mod(coord - axis.faces[0], period) + axis.faces[0]
|
|
4173
|
+
return coord
|
|
4174
|
+
|
|
4175
|
+
def _wrap_points(points: jnp.ndarray) -> jnp.ndarray:
|
|
4176
|
+
return jnp.stack(
|
|
4177
|
+
(
|
|
4178
|
+
_wrap_periodic_coord(points[..., 0], grid.x, periodic_x),
|
|
4179
|
+
_wrap_periodic_coord(points[..., 1], grid.y, periodic_y),
|
|
4180
|
+
_wrap_periodic_coord(points[..., 2], grid.z, periodic_z),
|
|
4181
|
+
),
|
|
4182
|
+
axis=-1,
|
|
4183
|
+
)
|
|
4184
|
+
|
|
4185
|
+
def _plane_step_jit(k: jnp.ndarray, direction: int) -> jnp.ndarray:
|
|
4186
|
+
"""Signed z step from plane k to neighboring plane."""
|
|
4187
|
+
|
|
4188
|
+
if nz < 2:
|
|
4189
|
+
return jnp.asarray(direction, dtype=jnp.float64)
|
|
4190
|
+
|
|
4191
|
+
dz_lower = z_axis[1] - z_axis[0]
|
|
4192
|
+
dz_upper = z_axis[-1] - z_axis[-2]
|
|
4193
|
+
period = (z_axis[-1] - z_axis[0]) + dz_lower
|
|
4194
|
+
|
|
4195
|
+
if direction >= 0:
|
|
4196
|
+
k_next = jnp.minimum(k + 1, nz - 1)
|
|
4197
|
+
interior_step = z_axis[k_next] - z_axis[k]
|
|
4198
|
+
|
|
4199
|
+
if periodic_z:
|
|
4200
|
+
boundary_step = (z_axis[0] + period) - z_axis[-1]
|
|
4201
|
+
else:
|
|
4202
|
+
boundary_step = dz_upper
|
|
4203
|
+
|
|
4204
|
+
return jnp.where(k < nz - 1, interior_step, boundary_step)
|
|
4205
|
+
|
|
4206
|
+
k_prev = jnp.maximum(k - 1, 0)
|
|
4207
|
+
interior_step = -(z_axis[k] - z_axis[k_prev])
|
|
4208
|
+
|
|
4209
|
+
if periodic_z:
|
|
4210
|
+
boundary_step = (z_axis[-1] - period) - z_axis[0]
|
|
4211
|
+
else:
|
|
4212
|
+
boundary_step = -dz_lower
|
|
4213
|
+
|
|
4214
|
+
return jnp.where(k > 0, interior_step, boundary_step)
|
|
4215
|
+
|
|
4216
|
+
def _interp_B(points: jnp.ndarray) -> jnp.ndarray:
|
|
4217
|
+
return _interpolate_B_contravariant_cell_centered(
|
|
4218
|
+
grid,
|
|
4219
|
+
B_contra_cell,
|
|
4220
|
+
points,
|
|
4221
|
+
periodic_axes=periodic_axes,
|
|
4222
|
+
boundary_value=boundary_value,
|
|
4223
|
+
)
|
|
4224
|
+
|
|
4225
|
+
def _interp_Bmag(points: jnp.ndarray) -> jnp.ndarray:
|
|
4226
|
+
return _interpolate_scalar_cell_centered(
|
|
4227
|
+
Bmag_cell,
|
|
4228
|
+
points[..., 0],
|
|
4229
|
+
points[..., 1],
|
|
4230
|
+
points[..., 2],
|
|
4231
|
+
grid=grid,
|
|
4232
|
+
periodic_axes=periodic_axes,
|
|
4233
|
+
boundary_value=boundary_value,
|
|
4234
|
+
)
|
|
4235
|
+
|
|
4236
|
+
def _safe_bz(bz: jnp.ndarray) -> jnp.ndarray:
|
|
4237
|
+
return jnp.where(
|
|
4238
|
+
jnp.abs(bz) < min_bz,
|
|
4239
|
+
jnp.where(bz < 0.0, -1.0, 1.0) * min_bz,
|
|
4240
|
+
bz,
|
|
4241
|
+
)
|
|
4242
|
+
|
|
4243
|
+
def _rhs(points: jnp.ndarray) -> jnp.ndarray:
|
|
4244
|
+
b = _interp_B(points)
|
|
4245
|
+
bz = _safe_bz(b[..., 2])
|
|
4246
|
+
return jnp.stack(
|
|
4247
|
+
(
|
|
4248
|
+
b[..., 0] / bz,
|
|
4249
|
+
b[..., 1] / bz,
|
|
4250
|
+
jnp.ones_like(bz),
|
|
4251
|
+
),
|
|
4252
|
+
axis=-1,
|
|
4253
|
+
)
|
|
4254
|
+
|
|
4255
|
+
def _rk4_batch(points: jnp.ndarray, h: jnp.ndarray) -> jnp.ndarray:
|
|
4256
|
+
k1 = _rhs(points)
|
|
4257
|
+
k2 = _rhs(points + 0.5 * h * k1)
|
|
4258
|
+
k3 = _rhs(points + 0.5 * h * k2)
|
|
4259
|
+
k4 = _rhs(points + h * k3)
|
|
4260
|
+
return points + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
|
|
4261
|
+
|
|
4262
|
+
def _speed(sampled_b: jnp.ndarray, sampled_bmag: jnp.ndarray) -> jnp.ndarray:
|
|
4263
|
+
bz = _safe_bz(sampled_b[..., 2])
|
|
4264
|
+
return sampled_bmag / jnp.maximum(jnp.abs(bz), 1.0e-30)
|
|
4265
|
+
|
|
4266
|
+
def _axis_crossing_fraction(
|
|
4267
|
+
old: jnp.ndarray,
|
|
4268
|
+
new: jnp.ndarray,
|
|
4269
|
+
lower: jnp.ndarray,
|
|
4270
|
+
upper: jnp.ndarray,
|
|
4271
|
+
periodic: bool,
|
|
4272
|
+
) -> jnp.ndarray:
|
|
4273
|
+
"""Fraction along old->new where a nonperiodic axis hits a boundary.
|
|
4274
|
+
|
|
4275
|
+
Returns inf if this axis does not produce a boundary crossing.
|
|
4276
|
+
"""
|
|
4277
|
+
|
|
4278
|
+
if periodic:
|
|
4279
|
+
return jnp.full_like(old, jnp.inf, dtype=jnp.float64)
|
|
4280
|
+
|
|
4281
|
+
denom = new - old
|
|
4282
|
+
safe_denom = jnp.where(jnp.abs(denom) < 1.0e-300, 1.0, denom)
|
|
4283
|
+
|
|
4284
|
+
crosses_lower = new < lower
|
|
4285
|
+
crosses_upper = new > upper
|
|
4286
|
+
|
|
4287
|
+
t_lower = (lower - old) / safe_denom
|
|
4288
|
+
t_upper = (upper - old) / safe_denom
|
|
4289
|
+
|
|
4290
|
+
t = jnp.where(crosses_lower, t_lower, jnp.inf)
|
|
4291
|
+
t = jnp.minimum(t, jnp.where(crosses_upper, t_upper, jnp.inf))
|
|
4292
|
+
|
|
4293
|
+
valid_t = (t >= 0.0) & (t <= 1.0)
|
|
4294
|
+
return jnp.where(valid_t, t, jnp.inf)
|
|
4295
|
+
|
|
4296
|
+
def _boundary_hit_state(
|
|
4297
|
+
old_state: jnp.ndarray,
|
|
4298
|
+
new_state: jnp.ndarray,
|
|
4299
|
+
finite_new: jnp.ndarray,
|
|
4300
|
+
valid_new: jnp.ndarray,
|
|
4301
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
4302
|
+
"""Estimate physical boundary hit location between old_state and new_state.
|
|
4303
|
+
|
|
4304
|
+
Assumes old_state is inside the domain. If new_state leaves the domain,
|
|
4305
|
+
compute the first face intersection along the straight RK substep chord.
|
|
4306
|
+
"""
|
|
4307
|
+
|
|
4308
|
+
tx = _axis_crossing_fraction(
|
|
4309
|
+
old_state[:, 0],
|
|
4310
|
+
new_state[:, 0],
|
|
4311
|
+
grid.x.faces[0],
|
|
4312
|
+
grid.x.faces[-1],
|
|
4313
|
+
periodic_x,
|
|
4314
|
+
)
|
|
4315
|
+
ty = _axis_crossing_fraction(
|
|
4316
|
+
old_state[:, 1],
|
|
4317
|
+
new_state[:, 1],
|
|
4318
|
+
grid.y.faces[0],
|
|
4319
|
+
grid.y.faces[-1],
|
|
4320
|
+
periodic_y,
|
|
4321
|
+
)
|
|
4322
|
+
tz = _axis_crossing_fraction(
|
|
4323
|
+
old_state[:, 2],
|
|
4324
|
+
new_state[:, 2],
|
|
4325
|
+
grid.z.faces[0],
|
|
4326
|
+
grid.z.faces[-1],
|
|
4327
|
+
periodic_z,
|
|
4328
|
+
)
|
|
4329
|
+
|
|
4330
|
+
t_hit = jnp.minimum(jnp.minimum(tx, ty), tz)
|
|
4331
|
+
has_hit = finite_new & (~valid_new) & jnp.isfinite(t_hit)
|
|
4332
|
+
|
|
4333
|
+
t_used = jnp.where(has_hit, t_hit, 1.0)
|
|
4334
|
+
hit_state = old_state + t_used[:, None] * (new_state - old_state)
|
|
4335
|
+
|
|
4336
|
+
# Clamp nonperiodic hit coordinates exactly to the face-bounded domain.
|
|
4337
|
+
hx = hit_state[:, 0] if periodic_x else jnp.clip(hit_state[:, 0], grid.x.faces[0], grid.x.faces[-1])
|
|
4338
|
+
hy = hit_state[:, 1] if periodic_y else jnp.clip(hit_state[:, 1], grid.y.faces[0], grid.y.faces[-1])
|
|
4339
|
+
hz = hit_state[:, 2] if periodic_z else jnp.clip(hit_state[:, 2], grid.z.faces[0], grid.z.faces[-1])
|
|
4340
|
+
|
|
4341
|
+
hit_state = jnp.stack((hx, hy, hz), axis=-1)
|
|
4342
|
+
hit_state = _wrap_points(hit_state)
|
|
4343
|
+
|
|
4344
|
+
return hit_state, jnp.where(has_hit, t_hit, 1.0)
|
|
4345
|
+
|
|
4346
|
+
def _trace_to_plane(seed_points: jnp.ndarray, step: jnp.ndarray):
|
|
4347
|
+
step_size = step / jnp.asarray(n_substeps, dtype=jnp.float64)
|
|
4348
|
+
|
|
4349
|
+
init_state = seed_points
|
|
4350
|
+
init_length = jnp.zeros(seed_points.shape[0], dtype=jnp.float64)
|
|
4351
|
+
init_alive = jnp.ones(seed_points.shape[0], dtype=bool)
|
|
4352
|
+
init_boundary = jnp.zeros(seed_points.shape[0], dtype=bool)
|
|
4353
|
+
|
|
4354
|
+
def substep_body(carry, _):
|
|
4355
|
+
state, length, alive, boundary = carry
|
|
4356
|
+
|
|
4357
|
+
b0 = _interp_B(state)
|
|
4358
|
+
bmag0 = _interp_Bmag(state)
|
|
4359
|
+
speed0 = _speed(b0, bmag0)
|
|
4360
|
+
|
|
4361
|
+
raw_next_state = _rk4_batch(state, step_size)
|
|
4362
|
+
finite_next = jnp.all(jnp.isfinite(raw_next_state), axis=-1)
|
|
4363
|
+
|
|
4364
|
+
# For nonfinite results, keep the old state to avoid NaN pollution.
|
|
4365
|
+
next_state_finite = jnp.where(finite_next[:, None], raw_next_state, state)
|
|
4366
|
+
|
|
4367
|
+
valid_next = _physical_domain_valid_mask(
|
|
4368
|
+
grid,
|
|
4369
|
+
next_state_finite[:, 0],
|
|
4370
|
+
next_state_finite[:, 1],
|
|
4371
|
+
next_state_finite[:, 2],
|
|
4372
|
+
periodic_axes=periodic_axes,
|
|
4373
|
+
)
|
|
4374
|
+
|
|
4375
|
+
active_full = alive & finite_next & valid_next
|
|
4376
|
+
active_exit = alive & finite_next & (~valid_next)
|
|
4377
|
+
active_bad = alive & (~finite_next)
|
|
4378
|
+
|
|
4379
|
+
hit_state, t_hit = _boundary_hit_state(
|
|
4380
|
+
state,
|
|
4381
|
+
next_state_finite,
|
|
4382
|
+
finite_next,
|
|
4383
|
+
valid_next,
|
|
4384
|
+
)
|
|
4385
|
+
|
|
4386
|
+
b1 = _interp_B(next_state_finite)
|
|
4387
|
+
bmag1 = _interp_Bmag(next_state_finite)
|
|
4388
|
+
speed1 = _speed(b1, bmag1)
|
|
4389
|
+
|
|
4390
|
+
b_hit = _interp_B(hit_state)
|
|
4391
|
+
bmag_hit = _interp_Bmag(hit_state)
|
|
4392
|
+
speed_hit = _speed(b_hit, bmag_hit)
|
|
4393
|
+
|
|
4394
|
+
full_increment = 0.5 * jnp.abs(step_size) * (speed0 + speed1)
|
|
4395
|
+
hit_increment = 0.5 * jnp.abs(step_size) * t_hit * (speed0 + speed_hit)
|
|
4396
|
+
|
|
4397
|
+
increment = jnp.where(active_full, full_increment, 0.0)
|
|
4398
|
+
increment = increment + jnp.where(active_exit, hit_increment, 0.0)
|
|
4399
|
+
|
|
4400
|
+
new_length = length + increment
|
|
4401
|
+
|
|
4402
|
+
endpoint_state = jnp.where(active_full[:, None], next_state_finite, state)
|
|
4403
|
+
endpoint_state = jnp.where(active_exit[:, None], hit_state, endpoint_state)
|
|
4404
|
+
endpoint_state = _wrap_points(endpoint_state)
|
|
4405
|
+
|
|
4406
|
+
new_alive = alive & finite_next & valid_next
|
|
4407
|
+
new_boundary = boundary | active_exit | active_bad
|
|
4408
|
+
|
|
4409
|
+
return (endpoint_state, new_length, new_alive, new_boundary), None
|
|
4410
|
+
|
|
4411
|
+
final_state, final_length, final_alive, final_boundary = jax.lax.scan(
|
|
4412
|
+
substep_body,
|
|
4413
|
+
(init_state, init_length, init_alive, init_boundary),
|
|
4414
|
+
xs=None,
|
|
4415
|
+
length=n_substeps,
|
|
4416
|
+
)[0]
|
|
4417
|
+
|
|
4418
|
+
del final_alive
|
|
4419
|
+
return final_state, final_length, final_boundary
|
|
4420
|
+
|
|
4421
|
+
def plane_body(_carry, k):
|
|
4422
|
+
z_k = jnp.full((nxy,), z_axis[k], dtype=jnp.float64)
|
|
4423
|
+
seed_points = jnp.stack((xx, yy, z_k), axis=-1)
|
|
4424
|
+
|
|
4425
|
+
forward_step = _plane_step_jit(k, direction=1)
|
|
4426
|
+
backward_step = _plane_step_jit(k, direction=-1)
|
|
4427
|
+
|
|
4428
|
+
forward_points, forward_length, forward_boundary = _trace_to_plane(
|
|
4429
|
+
seed_points,
|
|
4430
|
+
forward_step,
|
|
4431
|
+
)
|
|
4432
|
+
backward_points, backward_length, backward_boundary = _trace_to_plane(
|
|
4433
|
+
seed_points,
|
|
4434
|
+
backward_step,
|
|
4435
|
+
)
|
|
4436
|
+
|
|
4437
|
+
forward_x = _logical_coordinate_to_index(
|
|
4438
|
+
x_axis,
|
|
4439
|
+
forward_points[:, 0],
|
|
4440
|
+
periodic=periodic_x,
|
|
4441
|
+
)
|
|
4442
|
+
forward_y = _logical_coordinate_to_index(
|
|
4443
|
+
y_axis,
|
|
4444
|
+
forward_points[:, 1],
|
|
4445
|
+
periodic=periodic_y,
|
|
4446
|
+
)
|
|
4447
|
+
backward_x = _logical_coordinate_to_index(
|
|
4448
|
+
x_axis,
|
|
4449
|
+
backward_points[:, 0],
|
|
4450
|
+
periodic=periodic_x,
|
|
4451
|
+
)
|
|
4452
|
+
backward_y = _logical_coordinate_to_index(
|
|
4453
|
+
y_axis,
|
|
4454
|
+
backward_points[:, 1],
|
|
4455
|
+
periodic=periodic_y,
|
|
4456
|
+
)
|
|
4457
|
+
|
|
4458
|
+
dz_plane = jnp.full((nxy,), jnp.abs(forward_step), dtype=jnp.float64)
|
|
4459
|
+
|
|
4460
|
+
outputs = (
|
|
4461
|
+
forward_x,
|
|
4462
|
+
forward_y,
|
|
4463
|
+
backward_x,
|
|
4464
|
+
backward_y,
|
|
4465
|
+
forward_points[:, 0],
|
|
4466
|
+
forward_points[:, 1],
|
|
4467
|
+
forward_points[:, 2],
|
|
4468
|
+
backward_points[:, 0],
|
|
4469
|
+
backward_points[:, 1],
|
|
4470
|
+
backward_points[:, 2],
|
|
4471
|
+
forward_length,
|
|
4472
|
+
backward_length,
|
|
4473
|
+
forward_boundary,
|
|
4474
|
+
backward_boundary,
|
|
4475
|
+
dz_plane,
|
|
4476
|
+
)
|
|
4477
|
+
|
|
4478
|
+
return None, outputs
|
|
4479
|
+
|
|
4480
|
+
_, scanned = jax.lax.scan(
|
|
4481
|
+
plane_body,
|
|
4482
|
+
None,
|
|
4483
|
+
jnp.arange(nz),
|
|
4484
|
+
)
|
|
4485
|
+
|
|
4486
|
+
(
|
|
4487
|
+
forward_x_k,
|
|
4488
|
+
forward_y_k,
|
|
4489
|
+
backward_x_k,
|
|
4490
|
+
backward_y_k,
|
|
4491
|
+
forward_endpoint_x_k,
|
|
4492
|
+
forward_endpoint_y_k,
|
|
4493
|
+
forward_endpoint_z_k,
|
|
4494
|
+
backward_endpoint_x_k,
|
|
4495
|
+
backward_endpoint_y_k,
|
|
4496
|
+
backward_endpoint_z_k,
|
|
4497
|
+
forward_length_k,
|
|
4498
|
+
backward_length_k,
|
|
4499
|
+
forward_boundary_k,
|
|
4500
|
+
backward_boundary_k,
|
|
4501
|
+
dz_k,
|
|
4502
|
+
) = scanned
|
|
4503
|
+
|
|
4504
|
+
def _planes_to_grid(arr_k_nxy: jnp.ndarray) -> jnp.ndarray:
|
|
4505
|
+
return jnp.swapaxes(arr_k_nxy, 0, 1).reshape(shape)
|
|
4506
|
+
|
|
4507
|
+
return {
|
|
4508
|
+
"forward_x": _planes_to_grid(forward_x_k),
|
|
4509
|
+
"forward_y": _planes_to_grid(forward_y_k),
|
|
4510
|
+
"backward_x": _planes_to_grid(backward_x_k),
|
|
4511
|
+
"backward_y": _planes_to_grid(backward_y_k),
|
|
4512
|
+
"forward_endpoint_x": _planes_to_grid(forward_endpoint_x_k),
|
|
4513
|
+
"forward_endpoint_y": _planes_to_grid(forward_endpoint_y_k),
|
|
4514
|
+
"forward_endpoint_z": _planes_to_grid(forward_endpoint_z_k),
|
|
4515
|
+
"backward_endpoint_x": _planes_to_grid(backward_endpoint_x_k),
|
|
4516
|
+
"backward_endpoint_y": _planes_to_grid(backward_endpoint_y_k),
|
|
4517
|
+
"backward_endpoint_z": _planes_to_grid(backward_endpoint_z_k),
|
|
4518
|
+
"forward_length": _planes_to_grid(forward_length_k),
|
|
4519
|
+
"backward_length": _planes_to_grid(backward_length_k),
|
|
4520
|
+
"forward_boundary": _planes_to_grid(forward_boundary_k),
|
|
4521
|
+
"backward_boundary": _planes_to_grid(backward_boundary_k),
|
|
4522
|
+
"dz": _planes_to_grid(dz_k),
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
def _bracket_axis(
|
|
4526
|
+
axis: jnp.ndarray,
|
|
4527
|
+
values: jnp.ndarray,
|
|
4528
|
+
*,
|
|
4529
|
+
periodic: bool,
|
|
4530
|
+
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
|
4531
|
+
axis = jnp.asarray(axis, dtype=jnp.float64)
|
|
4532
|
+
values = jnp.asarray(values, dtype=jnp.float64)
|
|
4533
|
+
if axis.size == 1:
|
|
4534
|
+
zero = jnp.zeros_like(values, dtype=jnp.int32)
|
|
4535
|
+
return zero, zero, jnp.zeros_like(values, dtype=jnp.float64), jnp.isfinite(values)
|
|
4536
|
+
if axis.size < 1:
|
|
4537
|
+
raise ValueError("Each logical axis must contain at least one point for interpolation.")
|
|
4538
|
+
|
|
4539
|
+
if periodic:
|
|
4540
|
+
spacing = axis[1] - axis[0]
|
|
4541
|
+
period = (axis[-1] - axis[0]) + spacing
|
|
4542
|
+
wrapped = jnp.mod(values - axis[0], period) + axis[0]
|
|
4543
|
+
upper = jnp.searchsorted(axis, wrapped, side="right")
|
|
4544
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 1)
|
|
4545
|
+
next_index = jnp.mod(lower + 1, int(axis.size))
|
|
4546
|
+
lower_coord = axis[lower]
|
|
4547
|
+
upper_coord = jnp.where(lower == int(axis.size) - 1, axis[0] + period, axis[next_index])
|
|
4548
|
+
weight = (wrapped - lower_coord) / (upper_coord - lower_coord)
|
|
4549
|
+
valid = jnp.isfinite(values)
|
|
4550
|
+
return lower, next_index, jnp.clip(weight, 0.0, 1.0), valid
|
|
4551
|
+
|
|
4552
|
+
upper = jnp.searchsorted(axis, values, side="right")
|
|
4553
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 2)
|
|
4554
|
+
upper = lower + 1
|
|
4555
|
+
lower_coord = axis[lower]
|
|
4556
|
+
upper_coord = axis[upper]
|
|
4557
|
+
weight = (values - lower_coord) / (upper_coord - lower_coord)
|
|
4558
|
+
valid = (values >= axis[0]) & (values <= axis[-1]) & jnp.isfinite(values)
|
|
4559
|
+
return lower, upper, jnp.clip(weight, 0.0, 1.0), valid
|
|
4560
|
+
|
|
4561
|
+
|
|
4562
|
+
def _trilinear_sample(
|
|
4563
|
+
values: jnp.ndarray,
|
|
4564
|
+
x0: jnp.ndarray,
|
|
4565
|
+
x1: jnp.ndarray,
|
|
4566
|
+
wx: jnp.ndarray,
|
|
4567
|
+
y0: jnp.ndarray,
|
|
4568
|
+
y1: jnp.ndarray,
|
|
4569
|
+
wy: jnp.ndarray,
|
|
4570
|
+
z0: jnp.ndarray,
|
|
4571
|
+
z1: jnp.ndarray,
|
|
4572
|
+
wz: jnp.ndarray,
|
|
4573
|
+
) -> jnp.ndarray:
|
|
4574
|
+
c000 = values[x0, y0, z0]
|
|
4575
|
+
c100 = values[x1, y0, z0]
|
|
4576
|
+
c010 = values[x0, y1, z0]
|
|
4577
|
+
c110 = values[x1, y1, z0]
|
|
4578
|
+
c001 = values[x0, y0, z1]
|
|
4579
|
+
c101 = values[x1, y0, z1]
|
|
4580
|
+
c011 = values[x0, y1, z1]
|
|
4581
|
+
c111 = values[x1, y1, z1]
|
|
4582
|
+
c00 = c000 * (1.0 - wx) + c100 * wx
|
|
4583
|
+
c10 = c010 * (1.0 - wx) + c110 * wx
|
|
4584
|
+
c01 = c001 * (1.0 - wx) + c101 * wx
|
|
4585
|
+
c11 = c011 * (1.0 - wx) + c111 * wx
|
|
4586
|
+
c0 = c00 * (1.0 - wy) + c10 * wy
|
|
4587
|
+
c1 = c01 * (1.0 - wy) + c11 * wy
|
|
4588
|
+
return c0 * (1.0 - wz) + c1 * wz
|
|
4589
|
+
|
|
4590
|
+
|
|
4591
|
+
def _logical_coordinate_to_index(
|
|
4592
|
+
axis: jnp.ndarray,
|
|
4593
|
+
values: jnp.ndarray,
|
|
4594
|
+
*,
|
|
4595
|
+
periodic: bool,
|
|
4596
|
+
) -> jnp.ndarray:
|
|
4597
|
+
"""Convert logical coordinate values to fractional grid indices."""
|
|
4598
|
+
|
|
4599
|
+
axis = jnp.asarray(axis, dtype=jnp.float64)
|
|
4600
|
+
values = jnp.asarray(values, dtype=jnp.float64)
|
|
4601
|
+
if axis.size < 2:
|
|
4602
|
+
return jnp.zeros_like(values, dtype=jnp.float64)
|
|
4603
|
+
|
|
4604
|
+
if periodic:
|
|
4605
|
+
spacing = axis[1] - axis[0]
|
|
4606
|
+
period = (axis[-1] - axis[0]) + spacing
|
|
4607
|
+
wrapped = jnp.mod(values - axis[0], period) + axis[0]
|
|
4608
|
+
upper = jnp.searchsorted(axis, wrapped, side="right")
|
|
4609
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 1)
|
|
4610
|
+
next_index = jnp.mod(lower + 1, int(axis.size))
|
|
4611
|
+
lower_coord = axis[lower]
|
|
4612
|
+
upper_coord = jnp.where(lower == int(axis.size) - 1, axis[0] + period, axis[next_index])
|
|
4613
|
+
weight = (wrapped - lower_coord) / (upper_coord - lower_coord)
|
|
4614
|
+
return jnp.asarray(lower, dtype=jnp.float64) + jnp.clip(weight, 0.0, 1.0)
|
|
4615
|
+
|
|
4616
|
+
upper = jnp.searchsorted(axis, values, side="right")
|
|
4617
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 2)
|
|
4618
|
+
upper = lower + 1
|
|
4619
|
+
lower_coord = axis[lower]
|
|
4620
|
+
upper_coord = axis[upper]
|
|
4621
|
+
weight = (values - lower_coord) / (upper_coord - lower_coord)
|
|
4622
|
+
return jnp.asarray(lower, dtype=jnp.float64) + jnp.clip(weight, 0.0, 1.0)
|