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.
Files changed (106) hide show
  1. drbx/__init__.py +20 -0
  2. drbx/__main__.py +5 -0
  3. drbx/cli.py +586 -0
  4. drbx/config/__init__.py +20 -0
  5. drbx/config/boutinp.py +556 -0
  6. drbx/config/model.py +34 -0
  7. drbx/config/normalization.py +66 -0
  8. drbx/data/atomic_rates/__init__.py +1 -0
  9. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
  10. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
  11. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
  12. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
  13. drbx/geometry/__init__.py +207 -0
  14. drbx/geometry/embedding.py +56 -0
  15. drbx/geometry/essos_import.py +1385 -0
  16. drbx/geometry/fci_geometry.py +4622 -0
  17. drbx/geometry/fci_maps.py +85 -0
  18. drbx/geometry/island_divertor.py +291 -0
  19. drbx/geometry/metric_tensor.py +99 -0
  20. drbx/geometry/open_slab.py +150 -0
  21. drbx/geometry/rotating_ellipse.py +253 -0
  22. drbx/geometry/shifted_torus.py +225 -0
  23. drbx/geometry/stellarator.py +287 -0
  24. drbx/geometry/vmec_extender_import.py +499 -0
  25. drbx/geometry/vmec_jax_import.py +306 -0
  26. drbx/linear/__init__.py +37 -0
  27. drbx/linear/dispersion.py +138 -0
  28. drbx/linear/eigen.py +91 -0
  29. drbx/native/__init__.py +224 -0
  30. drbx/native/array_backend.py +64 -0
  31. drbx/native/deck_runner.py +779 -0
  32. drbx/native/electromagnetic.py +250 -0
  33. drbx/native/expression.py +173 -0
  34. drbx/native/fci.py +295 -0
  35. drbx/native/fci_2_field_rhs.py +182 -0
  36. drbx/native/fci_4_field_rhs.py +1267 -0
  37. drbx/native/fci_boundaries.py +2494 -0
  38. drbx/native/fci_differentiable_case.py +304 -0
  39. drbx/native/fci_drb_EB_rhs.py +1243 -0
  40. drbx/native/fci_drb_rhs.py +190 -0
  41. drbx/native/fci_halo.py +1575 -0
  42. drbx/native/fci_helpers.py +350 -0
  43. drbx/native/fci_model.py +294 -0
  44. drbx/native/fci_neutral.py +139 -0
  45. drbx/native/fci_operators.py +4081 -0
  46. drbx/native/fci_sharding.py +597 -0
  47. drbx/native/fci_sheath_recycling.py +206 -0
  48. drbx/native/fci_time_integrator.py +96 -0
  49. drbx/native/fci_vorticity.py +198 -0
  50. drbx/native/fluid_1d.py +330 -0
  51. drbx/native/hasegawa_wakatani.py +196 -0
  52. drbx/native/limiters.py +57 -0
  53. drbx/native/mesh.py +238 -0
  54. drbx/native/metrics.py +234 -0
  55. drbx/native/neutrals/__init__.py +58 -0
  56. drbx/native/neutrals/atomic_rates.py +134 -0
  57. drbx/native/neutrals/detachment_sol_model.py +221 -0
  58. drbx/native/neutrals/reactions.py +164 -0
  59. drbx/native/neutrals/recycling_sol_model.py +197 -0
  60. drbx/native/sol_flux_tube.py +133 -0
  61. drbx/native/stellarator_turbulence.py +343 -0
  62. drbx/native/transport.py +134 -0
  63. drbx/native/units.py +32 -0
  64. drbx/native/vorticity.py +252 -0
  65. drbx/runtime/__init__.py +53 -0
  66. drbx/runtime/artifacts.py +161 -0
  67. drbx/runtime/memory.py +144 -0
  68. drbx/runtime/output.py +374 -0
  69. drbx/runtime/paths.py +9 -0
  70. drbx/runtime/performance.py +161 -0
  71. drbx/runtime/run_config.py +184 -0
  72. drbx/runtime/scheduler.py +99 -0
  73. drbx/runtime/state.py +40 -0
  74. drbx/validation/__init__.py +424 -0
  75. drbx/validation/autodiff_diffusion.py +329 -0
  76. drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
  77. drbx/validation/diverted_tokamak_movie.py +558 -0
  78. drbx/validation/essos_fieldline_import_campaign.py +181 -0
  79. drbx/validation/essos_imported_artifact_audit.py +535 -0
  80. drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
  81. drbx/validation/essos_imported_fci_campaign.py +5241 -0
  82. drbx/validation/essos_imported_pytree_campaign.py +406 -0
  83. drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
  84. drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
  85. drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
  86. drbx/validation/fluid_1d_mms_convergence.py +250 -0
  87. drbx/validation/geometry_lineouts.py +136 -0
  88. drbx/validation/geometry_slices.py +178 -0
  89. drbx/validation/publication_plotting.py +91 -0
  90. drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
  91. drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
  92. drbx/validation/stellarator_fci_operator_campaign.py +304 -0
  93. drbx/validation/stellarator_fci_suite_campaign.py +264 -0
  94. drbx/validation/stellarator_metric_mms_campaign.py +289 -0
  95. drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
  96. drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
  97. drbx/validation/stellarator_sol_showcase.py +628 -0
  98. drbx/validation/stellarator_vorticity_campaign.py +304 -0
  99. drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
  100. drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
  101. drbx-2.0.0.dev0.dist-info/METADATA +380 -0
  102. drbx-2.0.0.dev0.dist-info/RECORD +106 -0
  103. drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
  104. drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
  105. drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  106. 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
+ ]