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,597 @@
|
|
|
1
|
+
"""Multi-device sharded execution helpers for the FCI stack.
|
|
2
|
+
|
|
3
|
+
This module promotes the proven ``shard_map`` harness patterns from
|
|
4
|
+
``tests/test_fci_operators_domain_decomp.py`` into a small library API and
|
|
5
|
+
extends them from operator-level tests to a full two-field RHS + RK4 step:
|
|
6
|
+
|
|
7
|
+
- :func:`make_shard_mesh` builds the three-axis execution mesh.
|
|
8
|
+
- :func:`build_local_fci_geometries` converts a global :class:`FciGeometry3D`
|
|
9
|
+
into the per-shard representation the ``shard_map`` kernel consumes: a
|
|
10
|
+
``LocalDomain3D`` plus a stacked bundle of cell-centered geometry fields
|
|
11
|
+
that is partitioned with ``PartitionSpec("x", "y", "z")``.
|
|
12
|
+
- :func:`assemble_local_fci_geometry` runs inside ``shard_map`` and assembles
|
|
13
|
+
a :class:`LocalFciGeometry3D` from one shard's owned geometry block using
|
|
14
|
+
halo exchange, periodic topology filling, and the runtime shard index.
|
|
15
|
+
- :func:`make_sharded_2field_step` returns a jitted RK4 step for the reduced
|
|
16
|
+
two-field model where every stage prepares state halos (exchange plus
|
|
17
|
+
periodic topology fill) before evaluating the RHS on local geometry.
|
|
18
|
+
|
|
19
|
+
The reduced two-field direct stencil path closes physical sides with
|
|
20
|
+
one-sided derivative stencils and consumes no face-BC payload, matching the
|
|
21
|
+
global single-device path exactly; ``boundary_conditions`` entries are
|
|
22
|
+
forwarded verbatim to :func:`compute_2field_rhs` for both paths.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
import jax
|
|
31
|
+
import jax.numpy as jnp
|
|
32
|
+
import numpy as np
|
|
33
|
+
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
|
|
34
|
+
|
|
35
|
+
from ..geometry import (
|
|
36
|
+
FciGeometry3D,
|
|
37
|
+
HaloLayout3D,
|
|
38
|
+
LocalBFieldGeometry,
|
|
39
|
+
LocalCellCenteredGrid3D,
|
|
40
|
+
LocalCellVolumeGeometry3D,
|
|
41
|
+
LocalDomain3D,
|
|
42
|
+
LocalFaceBFieldGeometry,
|
|
43
|
+
LocalFaceMetricGeometry,
|
|
44
|
+
LocalFciDirectionMap,
|
|
45
|
+
LocalFciGeometry3D,
|
|
46
|
+
LocalFciLocalDependencyTable,
|
|
47
|
+
LocalFciMaps3D,
|
|
48
|
+
LocalGrid1D,
|
|
49
|
+
LocalMetricGeometry,
|
|
50
|
+
LocalRegularFaceGeometry3D,
|
|
51
|
+
LocalSpacing3D,
|
|
52
|
+
LocalStencilBuilder,
|
|
53
|
+
SIDE_PHYSICAL,
|
|
54
|
+
SIDE_SIMPLE_PERIODIC,
|
|
55
|
+
ShardSpec3D,
|
|
56
|
+
StencilBuilderContext,
|
|
57
|
+
build_curvature_coefficients,
|
|
58
|
+
build_local_direct_stencil_one_sided_physical_from_halo,
|
|
59
|
+
)
|
|
60
|
+
from .fci_2_field_rhs import Fci2FieldRhsParameters, Fci2FieldState, compute_2field_rhs
|
|
61
|
+
from .fci_halo import HaloExchange3D, LocalPeriodicTopologyRule3D, TopologyHaloFiller3D
|
|
62
|
+
from .fci_model import inject_owned_field_to_halo, inject_owned_vector_field_to_halo
|
|
63
|
+
from .fci_time_integrator import rk4_step
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_MESH_AXIS_NAMES = ("x", "y", "z")
|
|
67
|
+
_METRIC_NAMES = (
|
|
68
|
+
"J",
|
|
69
|
+
"g11",
|
|
70
|
+
"g22",
|
|
71
|
+
"g33",
|
|
72
|
+
"g12",
|
|
73
|
+
"g13",
|
|
74
|
+
"g23",
|
|
75
|
+
"g_11",
|
|
76
|
+
"g_22",
|
|
77
|
+
"g_33",
|
|
78
|
+
"g_12",
|
|
79
|
+
"g_13",
|
|
80
|
+
"g_23",
|
|
81
|
+
)
|
|
82
|
+
_CELL_FIELD_NAMES = _METRIC_NAMES + (
|
|
83
|
+
"Bmag",
|
|
84
|
+
"B_contra_x",
|
|
85
|
+
"B_contra_y",
|
|
86
|
+
"B_contra_z",
|
|
87
|
+
"dx",
|
|
88
|
+
"dy",
|
|
89
|
+
"dz",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def make_shard_mesh(shard_counts: tuple[int, int, int]) -> Mesh:
|
|
94
|
+
"""Build the ``("x", "y", "z")`` execution mesh for the requested layout."""
|
|
95
|
+
|
|
96
|
+
shard_counts = tuple(int(value) for value in shard_counts)
|
|
97
|
+
if len(shard_counts) != 3 or any(value <= 0 for value in shard_counts):
|
|
98
|
+
raise ValueError(f"shard_counts must contain three positive integers, got {shard_counts}")
|
|
99
|
+
|
|
100
|
+
ndevices = math.prod(shard_counts)
|
|
101
|
+
devices = np.asarray(jax.devices()[:ndevices], dtype=object)
|
|
102
|
+
if devices.size < ndevices:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"shard_counts={shard_counts} requires {ndevices} devices, "
|
|
105
|
+
f"but only {devices.size} are available"
|
|
106
|
+
)
|
|
107
|
+
return Mesh(devices.reshape(shard_counts), _MESH_AXIS_NAMES)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _assert_shape_divisible_by_shards(
|
|
111
|
+
shape: tuple[int, int, int],
|
|
112
|
+
shard_counts: tuple[int, int, int],
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Require equal-sized local blocks on every mesh axis."""
|
|
115
|
+
|
|
116
|
+
for axis, (size, count) in enumerate(zip(shape, shard_counts)):
|
|
117
|
+
if int(size) % int(count):
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"global shape axis {axis} with size {size} is not divisible by "
|
|
120
|
+
f"shard count {count}; shape={shape}, shard_counts={shard_counts}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class _UniformAxisMeta:
|
|
126
|
+
"""Static uniform-axis coordinate metadata for one logical axis."""
|
|
127
|
+
|
|
128
|
+
center0: float
|
|
129
|
+
face0: float
|
|
130
|
+
spacing: float
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _uniform_axis_meta(grid_axis, *, axis: int) -> _UniformAxisMeta:
|
|
134
|
+
centers = np.asarray(grid_axis.centers, dtype=np.float64)
|
|
135
|
+
faces = np.asarray(grid_axis.faces, dtype=np.float64)
|
|
136
|
+
if centers.size < 2:
|
|
137
|
+
raise ValueError(f"sharded axis {axis} requires at least two cells, got {centers.size}")
|
|
138
|
+
spacing = float((centers[-1] - centers[0]) / (centers.size - 1))
|
|
139
|
+
deltas = np.diff(centers)
|
|
140
|
+
tolerance = 1.0e-12 * max(1.0, abs(spacing))
|
|
141
|
+
if np.max(np.abs(deltas - spacing)) > tolerance:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"build_local_fci_geometries requires uniformly spaced grid axes; "
|
|
144
|
+
f"axis {axis} center spacings deviate by "
|
|
145
|
+
f"{float(np.max(np.abs(deltas - spacing))):.3e}"
|
|
146
|
+
)
|
|
147
|
+
return _UniformAxisMeta(
|
|
148
|
+
center0=float(centers[0]),
|
|
149
|
+
face0=float(faces[0]),
|
|
150
|
+
spacing=spacing,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class ShardedFciGeometry3D:
|
|
156
|
+
"""Per-shard geometry description consumed by the ``shard_map`` kernel.
|
|
157
|
+
|
|
158
|
+
``cell_fields`` is a global ``(nx, ny, nz, len(_CELL_FIELD_NAMES))`` array
|
|
159
|
+
of cell-centered geometry values. It is partitioned on the mesh with
|
|
160
|
+
``PartitionSpec("x", "y", "z")`` so each kernel instance receives its
|
|
161
|
+
owned block; :func:`assemble_local_fci_geometry` fills the halos through
|
|
162
|
+
the same exchange/topology pipeline used for state fields.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
domain: LocalDomain3D
|
|
166
|
+
cell_fields: jnp.ndarray
|
|
167
|
+
axis_meta: tuple[_UniformAxisMeta, _UniformAxisMeta, _UniformAxisMeta]
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def global_shape(self) -> tuple[int, int, int]:
|
|
171
|
+
return self.domain.shard_spec.global_shape
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def shard_counts(self) -> tuple[int, int, int]:
|
|
175
|
+
return self.domain.shard_spec.shard_counts
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def halo_width(self) -> int:
|
|
179
|
+
return self.domain.layout.halo_width
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def build_local_fci_geometries(
|
|
183
|
+
geometry: FciGeometry3D,
|
|
184
|
+
shard_counts: tuple[int, int, int],
|
|
185
|
+
*,
|
|
186
|
+
halo_width: int = 1,
|
|
187
|
+
periodic_axes: tuple[bool, bool, bool] = (False, True, True),
|
|
188
|
+
) -> ShardedFciGeometry3D:
|
|
189
|
+
"""Convert a global FCI geometry into the shard-local kernel inputs."""
|
|
190
|
+
|
|
191
|
+
if not isinstance(geometry, FciGeometry3D):
|
|
192
|
+
raise TypeError(f"geometry must be an FciGeometry3D instance, got {type(geometry).__name__}")
|
|
193
|
+
shard_counts = tuple(int(value) for value in shard_counts)
|
|
194
|
+
global_shape = geometry.shape
|
|
195
|
+
_assert_shape_divisible_by_shards(global_shape, shard_counts)
|
|
196
|
+
owned_shape = tuple(
|
|
197
|
+
int(size) // int(count) for size, count in zip(global_shape, shard_counts)
|
|
198
|
+
)
|
|
199
|
+
halo_width = int(halo_width)
|
|
200
|
+
for axis, (extent, count) in enumerate(zip(owned_shape, shard_counts)):
|
|
201
|
+
if count > 1 and halo_width > extent:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"halo exchange requires halo_width no larger than the owned "
|
|
204
|
+
f"extent on decomposed axis {axis}; got halo_width={halo_width}, "
|
|
205
|
+
f"owned extent={extent}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
layout = HaloLayout3D(owned_shape, halo_width)
|
|
209
|
+
side_kinds = tuple(
|
|
210
|
+
SIDE_SIMPLE_PERIODIC if periodic else SIDE_PHYSICAL for periodic in periodic_axes
|
|
211
|
+
)
|
|
212
|
+
spec = ShardSpec3D(
|
|
213
|
+
global_shape=global_shape,
|
|
214
|
+
# Local-array coordinates; runtime boundary ownership is decided by
|
|
215
|
+
# LocalDomain3D.runtime_* predicates inside shard_map.
|
|
216
|
+
owned_start=(0, 0, 0),
|
|
217
|
+
owned_stop=owned_shape,
|
|
218
|
+
shard_index=(0, 0, 0),
|
|
219
|
+
shard_counts=shard_counts,
|
|
220
|
+
periodic_axes=tuple(bool(value) for value in periodic_axes),
|
|
221
|
+
halo_width=halo_width,
|
|
222
|
+
side_kind_lower=side_kinds,
|
|
223
|
+
side_kind_upper=side_kinds,
|
|
224
|
+
)
|
|
225
|
+
domain = LocalDomain3D(shard_spec=spec, layout=layout, mesh_axis_names=_MESH_AXIS_NAMES)
|
|
226
|
+
|
|
227
|
+
metric = geometry.cell_metric
|
|
228
|
+
bfield = geometry.cell_bfield
|
|
229
|
+
spacing = geometry.spacing
|
|
230
|
+
channels = [getattr(metric, name) for name in _METRIC_NAMES]
|
|
231
|
+
channels.append(bfield.Bmag)
|
|
232
|
+
channels.extend(bfield.B_contra[..., component] for component in range(3))
|
|
233
|
+
channels.extend((spacing.dx, spacing.dy, spacing.dz))
|
|
234
|
+
cell_fields = jnp.stack(
|
|
235
|
+
[jnp.asarray(channel, dtype=jnp.float64) for channel in channels],
|
|
236
|
+
axis=-1,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
axis_meta = tuple(
|
|
240
|
+
_uniform_axis_meta(grid_axis, axis=axis)
|
|
241
|
+
for axis, grid_axis in enumerate((geometry.grid.x, geometry.grid.y, geometry.grid.z))
|
|
242
|
+
)
|
|
243
|
+
return ShardedFciGeometry3D(domain=domain, cell_fields=cell_fields, axis_meta=axis_meta)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _empty_local_fci_maps(layout: HaloLayout3D) -> LocalFciMaps3D:
|
|
247
|
+
"""Inactive local-halo-only FCI maps for models that use direct stencils."""
|
|
248
|
+
|
|
249
|
+
empty = LocalFciLocalDependencyTable(
|
|
250
|
+
target_flat=jnp.zeros((1,), dtype=jnp.int32),
|
|
251
|
+
source_i=jnp.zeros((1,), dtype=jnp.int32),
|
|
252
|
+
source_j=jnp.zeros((1,), dtype=jnp.int32),
|
|
253
|
+
source_k=jnp.zeros((1,), dtype=jnp.int32),
|
|
254
|
+
weight=jnp.zeros((1,), dtype=jnp.float64),
|
|
255
|
+
active=jnp.zeros((1,), dtype=bool),
|
|
256
|
+
)
|
|
257
|
+
direction = LocalFciDirectionMap(
|
|
258
|
+
layout=layout,
|
|
259
|
+
local=empty,
|
|
260
|
+
target_valid=jnp.ones(layout.owned_shape, dtype=bool),
|
|
261
|
+
connection_length=jnp.ones(layout.owned_shape, dtype=jnp.float64),
|
|
262
|
+
)
|
|
263
|
+
return LocalFciMaps3D(
|
|
264
|
+
layout=layout,
|
|
265
|
+
forward=direction,
|
|
266
|
+
backward=direction,
|
|
267
|
+
mode="local_halo_only",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _local_axis_grid(
|
|
272
|
+
layout: HaloLayout3D,
|
|
273
|
+
*,
|
|
274
|
+
axis: int,
|
|
275
|
+
meta: _UniformAxisMeta,
|
|
276
|
+
shard_id,
|
|
277
|
+
) -> LocalGrid1D:
|
|
278
|
+
local_size = layout.owned_shape[axis]
|
|
279
|
+
h = layout.halo_width
|
|
280
|
+
start = jnp.asarray(shard_id, dtype=jnp.int32) * local_size
|
|
281
|
+
center_indices = start + jnp.arange(-h, local_size + h)
|
|
282
|
+
face_indices = start + jnp.arange(-h, local_size + h + 1)
|
|
283
|
+
return LocalGrid1D(
|
|
284
|
+
layout=layout,
|
|
285
|
+
axis=axis,
|
|
286
|
+
centers_halo=meta.center0 + center_indices * meta.spacing,
|
|
287
|
+
faces_halo=meta.face0 + face_indices * meta.spacing,
|
|
288
|
+
owned_start_global=0,
|
|
289
|
+
owned_stop_global=local_size,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _axis_slice(values: jnp.ndarray, axis: int, start: int | None, stop: int | None) -> jnp.ndarray:
|
|
294
|
+
index = [slice(None)] * values.ndim
|
|
295
|
+
index[axis] = slice(start, stop)
|
|
296
|
+
return values[tuple(index)]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _lift_cell_halo_to_faces(values: jnp.ndarray, *, axis: int) -> jnp.ndarray:
|
|
300
|
+
"""Midpoint-interpolate a halo-shaped cell array onto one face family."""
|
|
301
|
+
|
|
302
|
+
lower = _axis_slice(values, axis, 0, 1)
|
|
303
|
+
upper = _axis_slice(values, axis, values.shape[axis] - 1, None)
|
|
304
|
+
interior = 0.5 * (_axis_slice(values, axis, 0, -1) + _axis_slice(values, axis, 1, None))
|
|
305
|
+
return jnp.concatenate((lower, interior, upper), axis=axis)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def assemble_local_fci_geometry(
|
|
309
|
+
sharded_geometry: ShardedFciGeometry3D,
|
|
310
|
+
cell_fields_owned: jnp.ndarray,
|
|
311
|
+
) -> LocalFciGeometry3D:
|
|
312
|
+
"""Assemble one shard's ``LocalFciGeometry3D`` inside ``shard_map``.
|
|
313
|
+
|
|
314
|
+
The owned geometry block is injected into a halo-shaped array, exchanged
|
|
315
|
+
across shard interfaces, and topology-filled on undecomposed periodic
|
|
316
|
+
sides. Physical-side geometry halos are left unfilled; the two-field
|
|
317
|
+
operators only consume owned geometry values, and physical field planes
|
|
318
|
+
are closed with one-sided stencils. Face-family geometry is midpoint
|
|
319
|
+
interpolation of the cell values and is unused by the direct two-field
|
|
320
|
+
operators.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
domain = sharded_geometry.domain
|
|
324
|
+
layout = domain.layout
|
|
325
|
+
expected_shape = layout.owned_shape + (len(_CELL_FIELD_NAMES),)
|
|
326
|
+
if tuple(cell_fields_owned.shape) != expected_shape:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"cell_fields_owned must have shape {expected_shape}, got {cell_fields_owned.shape}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
fields_halo = inject_owned_vector_field_to_halo(
|
|
332
|
+
jnp.asarray(cell_fields_owned, dtype=jnp.float64),
|
|
333
|
+
layout,
|
|
334
|
+
)
|
|
335
|
+
fields_halo = HaloExchange3D()(fields_halo, domain)
|
|
336
|
+
fields_halo = TopologyHaloFiller3D(rules=(LocalPeriodicTopologyRule3D(),))(fields_halo, domain)
|
|
337
|
+
channel = {
|
|
338
|
+
name: fields_halo[..., index] for index, name in enumerate(_CELL_FIELD_NAMES)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
cell_metric = LocalMetricGeometry(
|
|
342
|
+
layout=layout,
|
|
343
|
+
location="cell",
|
|
344
|
+
**{f"{name}_halo": channel[name] for name in _METRIC_NAMES},
|
|
345
|
+
)
|
|
346
|
+
face_locations = ("x_face", "y_face", "z_face")
|
|
347
|
+
face_metric = LocalFaceMetricGeometry(
|
|
348
|
+
layout=layout,
|
|
349
|
+
**{
|
|
350
|
+
axis_name: LocalMetricGeometry(
|
|
351
|
+
layout=layout,
|
|
352
|
+
location=face_locations[axis],
|
|
353
|
+
**{
|
|
354
|
+
f"{name}_halo": _lift_cell_halo_to_faces(channel[name], axis=axis)
|
|
355
|
+
for name in _METRIC_NAMES
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
for axis, axis_name in enumerate(("x", "y", "z"))
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
B_contra_halo = jnp.stack(
|
|
363
|
+
(channel["B_contra_x"], channel["B_contra_y"], channel["B_contra_z"]),
|
|
364
|
+
axis=-1,
|
|
365
|
+
)
|
|
366
|
+
cell_bfield = LocalBFieldGeometry(
|
|
367
|
+
layout=layout,
|
|
368
|
+
B_contra_halo=B_contra_halo,
|
|
369
|
+
Bmag_halo=channel["Bmag"],
|
|
370
|
+
location="cell",
|
|
371
|
+
)
|
|
372
|
+
face_bfield = LocalFaceBFieldGeometry(
|
|
373
|
+
layout=layout,
|
|
374
|
+
**{
|
|
375
|
+
axis_name: LocalBFieldGeometry(
|
|
376
|
+
layout=layout,
|
|
377
|
+
B_contra_halo=_lift_cell_halo_to_faces(B_contra_halo, axis=axis),
|
|
378
|
+
Bmag_halo=_lift_cell_halo_to_faces(channel["Bmag"], axis=axis),
|
|
379
|
+
location=face_locations[axis],
|
|
380
|
+
)
|
|
381
|
+
for axis, axis_name in enumerate(("x", "y", "z"))
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
spacing = LocalSpacing3D(
|
|
386
|
+
layout=layout,
|
|
387
|
+
dx_halo=channel["dx"],
|
|
388
|
+
dy_halo=channel["dy"],
|
|
389
|
+
dz_halo=channel["dz"],
|
|
390
|
+
)
|
|
391
|
+
grid = LocalCellCenteredGrid3D(
|
|
392
|
+
layout=layout,
|
|
393
|
+
x=_local_axis_grid(layout, axis=0, meta=sharded_geometry.axis_meta[0], shard_id=domain.runtime_shard_id(0)),
|
|
394
|
+
y=_local_axis_grid(layout, axis=1, meta=sharded_geometry.axis_meta[1], shard_id=domain.runtime_shard_id(1)),
|
|
395
|
+
z=_local_axis_grid(layout, axis=2, meta=sharded_geometry.axis_meta[2], shard_id=domain.runtime_shard_id(2)),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
face_shapes = tuple(layout.face_control_shape(axis) for axis in range(3))
|
|
399
|
+
regular = LocalRegularFaceGeometry3D(
|
|
400
|
+
layout=layout,
|
|
401
|
+
x_area=jnp.ones(face_shapes[0]),
|
|
402
|
+
y_area=jnp.ones(face_shapes[1]),
|
|
403
|
+
z_area=jnp.ones(face_shapes[2]),
|
|
404
|
+
x_area_fraction=jnp.ones(face_shapes[0]),
|
|
405
|
+
y_area_fraction=jnp.ones(face_shapes[1]),
|
|
406
|
+
z_area_fraction=jnp.ones(face_shapes[2]),
|
|
407
|
+
x_open_mask=jnp.ones(face_shapes[0], dtype=bool),
|
|
408
|
+
y_open_mask=jnp.ones(face_shapes[1], dtype=bool),
|
|
409
|
+
z_open_mask=jnp.ones(face_shapes[2], dtype=bool),
|
|
410
|
+
)
|
|
411
|
+
return LocalFciGeometry3D(
|
|
412
|
+
layout=layout,
|
|
413
|
+
grid=grid,
|
|
414
|
+
maps=_empty_local_fci_maps(layout),
|
|
415
|
+
spacing=spacing,
|
|
416
|
+
cell_metric=cell_metric,
|
|
417
|
+
face_metric=face_metric,
|
|
418
|
+
cell_bfield=cell_bfield,
|
|
419
|
+
face_bfield=face_bfield,
|
|
420
|
+
regular_face_geometry=regular,
|
|
421
|
+
cell_volume_geometry=LocalCellVolumeGeometry3D(
|
|
422
|
+
layout=layout,
|
|
423
|
+
volume=jnp.ones(layout.owned_shape),
|
|
424
|
+
volume_fraction=jnp.ones(layout.owned_shape),
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _make_prepared_local_stencil_builder(
|
|
430
|
+
domain: LocalDomain3D,
|
|
431
|
+
context: StencilBuilderContext,
|
|
432
|
+
) -> LocalStencilBuilder:
|
|
433
|
+
"""Wrap halo preparation plus the one-sided physical local stencil build.
|
|
434
|
+
|
|
435
|
+
The returned builder follows the ``compute_2field_rhs`` stencil-builder
|
|
436
|
+
call convention ``(field, geometry, periodic_axes=..., face_bc=..., ...)``
|
|
437
|
+
but receives shard-owned fields: it injects them into halo arrays,
|
|
438
|
+
exchanges shard-interface halos, topology-fills undecomposed periodic
|
|
439
|
+
sides, and closes physical side planes with one-sided derivative
|
|
440
|
+
stencils. Like the global direct path, it consumes no face-BC payload.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
halo_exchange = HaloExchange3D()
|
|
444
|
+
topology_filler = TopologyHaloFiller3D(rules=(LocalPeriodicTopologyRule3D(),))
|
|
445
|
+
|
|
446
|
+
def _build(
|
|
447
|
+
field_owned: jnp.ndarray,
|
|
448
|
+
geometry: LocalFciGeometry3D,
|
|
449
|
+
_context: object | None = None,
|
|
450
|
+
face_bc: object | None = None,
|
|
451
|
+
cut_wall_geometry: object | None = None,
|
|
452
|
+
cut_wall_bc: object | None = None,
|
|
453
|
+
*,
|
|
454
|
+
periodic_axes: object | None = None,
|
|
455
|
+
):
|
|
456
|
+
del _context, face_bc, cut_wall_geometry, cut_wall_bc, periodic_axes
|
|
457
|
+
field_halo = inject_owned_field_to_halo(
|
|
458
|
+
jnp.asarray(field_owned, dtype=jnp.float64),
|
|
459
|
+
domain.layout,
|
|
460
|
+
)
|
|
461
|
+
field_halo = halo_exchange(field_halo, domain)
|
|
462
|
+
field_halo = topology_filler(field_halo, domain)
|
|
463
|
+
return build_local_direct_stencil_one_sided_physical_from_halo(
|
|
464
|
+
field_halo,
|
|
465
|
+
geometry,
|
|
466
|
+
context,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
return LocalStencilBuilder(_build)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@dataclass(frozen=True)
|
|
473
|
+
class Sharded2FieldStepInfo:
|
|
474
|
+
"""Static sharding facts about a sharded two-field RK4 step."""
|
|
475
|
+
|
|
476
|
+
mesh: Mesh
|
|
477
|
+
partition_spec: P
|
|
478
|
+
state_sharding: NamedSharding
|
|
479
|
+
domain: LocalDomain3D
|
|
480
|
+
geometry: ShardedFciGeometry3D
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def make_sharded_2field_step(
|
|
484
|
+
geometry: FciGeometry3D,
|
|
485
|
+
shard_counts: tuple[int, int, int],
|
|
486
|
+
parameters: Fci2FieldRhsParameters,
|
|
487
|
+
boundary_conditions: dict[str, object] | None = None,
|
|
488
|
+
*,
|
|
489
|
+
dt: float,
|
|
490
|
+
halo_width: int = 1,
|
|
491
|
+
) -> tuple[object, Sharded2FieldStepInfo]:
|
|
492
|
+
"""Build a jitted sharded RK4 step for the reduced two-field model.
|
|
493
|
+
|
|
494
|
+
Returns ``(step_fn, info)`` where ``step_fn(state)`` advances a global
|
|
495
|
+
:class:`Fci2FieldState` by one RK4 step under ``shard_map`` with in/out
|
|
496
|
+
partition spec ``P("x", "y", "z")`` on every state field. Each of the
|
|
497
|
+
four stage RHS evaluations prepares fresh state halos (exchange plus
|
|
498
|
+
periodic topology fill) before building stencils.
|
|
499
|
+
|
|
500
|
+
``boundary_conditions`` maps field names (``"density"``, ``"phi"``,
|
|
501
|
+
``"v_parallel"``) to face-BC payloads forwarded to
|
|
502
|
+
:func:`compute_2field_rhs`; the two-field direct stencil path consumes no
|
|
503
|
+
face-BC payload, so ``None`` matches the single-device behavior.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
shard_counts = tuple(int(value) for value in shard_counts)
|
|
507
|
+
boundary_conditions = dict(boundary_conditions or {})
|
|
508
|
+
known_fields = ("density", "phi", "v_parallel")
|
|
509
|
+
unknown = sorted(set(boundary_conditions) - set(known_fields))
|
|
510
|
+
if unknown:
|
|
511
|
+
raise ValueError(f"boundary_conditions has unknown fields {unknown}; expected {known_fields}")
|
|
512
|
+
face_bcs = {name: boundary_conditions.get(name) for name in known_fields}
|
|
513
|
+
|
|
514
|
+
mesh = make_shard_mesh(shard_counts)
|
|
515
|
+
sharded_geometry = build_local_fci_geometries(geometry, shard_counts, halo_width=halo_width)
|
|
516
|
+
domain = sharded_geometry.domain
|
|
517
|
+
partition_spec = P(*_MESH_AXIS_NAMES)
|
|
518
|
+
state_sharding = NamedSharding(mesh, partition_spec)
|
|
519
|
+
|
|
520
|
+
curvature_coefficients = build_curvature_coefficients(
|
|
521
|
+
geometry,
|
|
522
|
+
periodic_axes=domain.periodic_axes,
|
|
523
|
+
)
|
|
524
|
+
curvature_sharded = jax.device_put(curvature_coefficients, state_sharding)
|
|
525
|
+
cell_fields_sharded = jax.device_put(sharded_geometry.cell_fields, state_sharding)
|
|
526
|
+
timestep = jnp.asarray(dt, dtype=jnp.float64)
|
|
527
|
+
|
|
528
|
+
def _kernel(density, v_parallel, density_background, curvature_owned, cell_fields_owned):
|
|
529
|
+
local_geometry = assemble_local_fci_geometry(sharded_geometry, cell_fields_owned)
|
|
530
|
+
context = StencilBuilderContext(layout=domain.layout, domain=domain)
|
|
531
|
+
stencil_builder = _make_prepared_local_stencil_builder(domain, context)
|
|
532
|
+
state = Fci2FieldState(
|
|
533
|
+
density=density,
|
|
534
|
+
v_parallel=v_parallel,
|
|
535
|
+
density_background=density_background,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _rhs_fn(stage_state, stage_time, carry):
|
|
539
|
+
del stage_time
|
|
540
|
+
result, _timings = compute_2field_rhs(
|
|
541
|
+
stage_state,
|
|
542
|
+
geometry=local_geometry,
|
|
543
|
+
stencil_builder=stencil_builder,
|
|
544
|
+
parameters=parameters,
|
|
545
|
+
curvature_coefficients=curvature_owned,
|
|
546
|
+
density_face_bc=face_bcs["density"],
|
|
547
|
+
phi_face_bc=face_bcs["phi"],
|
|
548
|
+
v_parallel_face_bc=face_bcs["v_parallel"],
|
|
549
|
+
)
|
|
550
|
+
return result.rhs, carry, None
|
|
551
|
+
|
|
552
|
+
step = rk4_step(state, time=0.0, timestep=timestep, rhs_fn=_rhs_fn, carry=None)
|
|
553
|
+
next_state = step.state
|
|
554
|
+
return next_state.density, next_state.v_parallel, next_state.density_background
|
|
555
|
+
|
|
556
|
+
sharded_kernel = jax.jit(
|
|
557
|
+
jax.shard_map(
|
|
558
|
+
_kernel,
|
|
559
|
+
mesh=mesh,
|
|
560
|
+
in_specs=(partition_spec,) * 5,
|
|
561
|
+
out_specs=(partition_spec,) * 3,
|
|
562
|
+
check_vma=False,
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def step_fn(state: Fci2FieldState) -> Fci2FieldState:
|
|
567
|
+
density, v_parallel, density_background = sharded_kernel(
|
|
568
|
+
jax.device_put(jnp.asarray(state.density, dtype=jnp.float64), state_sharding),
|
|
569
|
+
jax.device_put(jnp.asarray(state.v_parallel, dtype=jnp.float64), state_sharding),
|
|
570
|
+
jax.device_put(jnp.asarray(state.density_background, dtype=jnp.float64), state_sharding),
|
|
571
|
+
curvature_sharded,
|
|
572
|
+
cell_fields_sharded,
|
|
573
|
+
)
|
|
574
|
+
return Fci2FieldState(
|
|
575
|
+
density=density,
|
|
576
|
+
v_parallel=v_parallel,
|
|
577
|
+
density_background=density_background,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
info = Sharded2FieldStepInfo(
|
|
581
|
+
mesh=mesh,
|
|
582
|
+
partition_spec=partition_spec,
|
|
583
|
+
state_sharding=state_sharding,
|
|
584
|
+
domain=domain,
|
|
585
|
+
geometry=sharded_geometry,
|
|
586
|
+
)
|
|
587
|
+
return step_fn, info
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
__all__ = [
|
|
591
|
+
"Sharded2FieldStepInfo",
|
|
592
|
+
"ShardedFciGeometry3D",
|
|
593
|
+
"assemble_local_fci_geometry",
|
|
594
|
+
"build_local_fci_geometries",
|
|
595
|
+
"make_shard_mesh",
|
|
596
|
+
"make_sharded_2field_step",
|
|
597
|
+
]
|