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
drbx/native/fci.py ADDED
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import jax.numpy as jnp
4
+
5
+ from ..geometry import FciMaps, MetricTensor3D
6
+
7
+
8
+ def interpolate_fci_plane(
9
+ field: jnp.ndarray,
10
+ x_prime: jnp.ndarray,
11
+ z_prime: jnp.ndarray,
12
+ *,
13
+ y_offset: int,
14
+ boundary_value: float = 0.0,
15
+ ) -> jnp.ndarray:
16
+ """Bilinearly interpolate a 3D field on traced FCI intersections."""
17
+
18
+ values = jnp.asarray(field, dtype=jnp.float64)
19
+ nx, ny, nz = values.shape
20
+ y = (jnp.arange(ny)[None, :, None] + int(y_offset)) % ny
21
+ y = jnp.broadcast_to(y, values.shape)
22
+
23
+ x = jnp.asarray(x_prime, dtype=jnp.float64)
24
+ z = jnp.mod(jnp.asarray(z_prime, dtype=jnp.float64), float(nz))
25
+ valid = (x >= 0.0) & (x <= float(nx - 1))
26
+ x_clipped = jnp.clip(x, 0.0, float(nx - 1))
27
+ x0 = jnp.floor(x_clipped).astype(jnp.int32)
28
+ x1 = jnp.clip(x0 + 1, 0, nx - 1)
29
+ z0 = jnp.floor(z).astype(jnp.int32) % nz
30
+ z1 = (z0 + 1) % nz
31
+ wx = x_clipped - x0.astype(jnp.float64)
32
+ wz = z - jnp.floor(z)
33
+
34
+ f00 = values[x0, y, z0]
35
+ f10 = values[x1, y, z0]
36
+ f01 = values[x0, y, z1]
37
+ f11 = values[x1, y, z1]
38
+ interpolated = (
39
+ (1.0 - wx) * (1.0 - wz) * f00
40
+ + wx * (1.0 - wz) * f10
41
+ + (1.0 - wx) * wz * f01
42
+ + wx * wz * f11
43
+ )
44
+ return jnp.where(valid, interpolated, jnp.asarray(boundary_value, dtype=jnp.float64))
45
+
46
+
47
+ def fci_yup(field: jnp.ndarray, maps: FciMaps, *, boundary_value: float = 0.0) -> jnp.ndarray:
48
+ return interpolate_fci_plane(
49
+ field,
50
+ maps.forward_x,
51
+ maps.forward_z,
52
+ y_offset=1,
53
+ boundary_value=boundary_value,
54
+ )
55
+
56
+
57
+ def fci_ydown(field: jnp.ndarray, maps: FciMaps, *, boundary_value: float = 0.0) -> jnp.ndarray:
58
+ return interpolate_fci_plane(
59
+ field,
60
+ maps.backward_x,
61
+ maps.backward_z,
62
+ y_offset=-1,
63
+ boundary_value=boundary_value,
64
+ )
65
+
66
+
67
+ def grad_parallel_fci(field: jnp.ndarray, maps: FciMaps, *, boundary_value: float = 0.0) -> jnp.ndarray:
68
+ """Centered FCI parallel derivative per toroidal angle."""
69
+
70
+ up = fci_yup(field, maps, boundary_value=boundary_value)
71
+ down = fci_ydown(field, maps, boundary_value=boundary_value)
72
+ return (up - down) / (2.0 * maps.dphi)
73
+
74
+
75
+ def laplace_parallel_fci(field: jnp.ndarray, maps: FciMaps, *, boundary_value: float = 0.0) -> jnp.ndarray:
76
+ """Second centered FCI derivative along traced field-line maps."""
77
+
78
+ values = jnp.asarray(field, dtype=jnp.float64)
79
+ up = fci_yup(values, maps, boundary_value=boundary_value)
80
+ down = fci_ydown(values, maps, boundary_value=boundary_value)
81
+ return (up - 2.0 * values + down) / (maps.dphi * maps.dphi)
82
+
83
+
84
+ def conservative_parallel_diffusion_fci(
85
+ field: jnp.ndarray,
86
+ coefficient: jnp.ndarray,
87
+ maps: FciMaps,
88
+ *,
89
+ jacobian: jnp.ndarray | float = 1.0,
90
+ boundary_mode: str = "zero_flux",
91
+ ) -> jnp.ndarray:
92
+ """Return ``J^-1 div_parallel(J K grad_parallel(f))`` on FCI maps.
93
+
94
+ The stencil computes mapped forward/backward face gradients, arithmetic
95
+ face averages for ``J`` and ``K``, and a conservative difference of the two
96
+ mapped fluxes. ``boundary_mode='zero_flux'`` turns map exits into vanishing
97
+ target-normal diffusive fluxes, which is the stable manufactured-solution
98
+ and neutral-diffusion gate used before wall-distance target losses are
99
+ promoted.
100
+ """
101
+
102
+ values = jnp.asarray(field, dtype=jnp.float64)
103
+ coef = jnp.asarray(coefficient, dtype=jnp.float64)
104
+ jac = jnp.asarray(jacobian, dtype=jnp.float64)
105
+ if jac.ndim == 0:
106
+ jac = jnp.ones_like(values) * jac
107
+ forward_valid = ~jnp.asarray(maps.forward_boundary, dtype=bool)
108
+ backward_valid = ~jnp.asarray(maps.backward_boundary, dtype=bool)
109
+ up = fci_yup(values, maps, boundary_value=0.0)
110
+ down = fci_ydown(values, maps, boundary_value=0.0)
111
+ coef_up = fci_yup(coef, maps, boundary_value=0.0)
112
+ coef_down = fci_ydown(coef, maps, boundary_value=0.0)
113
+ jac_up = fci_yup(jac, maps, boundary_value=0.0)
114
+ jac_down = fci_ydown(jac, maps, boundary_value=0.0)
115
+ if boundary_mode == "zero_flux":
116
+ up = jnp.where(forward_valid, up, values)
117
+ down = jnp.where(backward_valid, down, values)
118
+ coef_up = jnp.where(forward_valid, coef_up, coef)
119
+ coef_down = jnp.where(backward_valid, coef_down, coef)
120
+ jac_up = jnp.where(forward_valid, jac_up, jac)
121
+ jac_down = jnp.where(backward_valid, jac_down, jac)
122
+ elif boundary_mode != "dirichlet_zero":
123
+ raise ValueError(f"Unsupported FCI conservative boundary_mode={boundary_mode!r}")
124
+ forward_face = 0.25 * (jac + jac_up) * (coef + coef_up)
125
+ backward_face = 0.25 * (jac + jac_down) * (coef + coef_down)
126
+ forward_flux = forward_face * (up - values) / maps.dphi
127
+ backward_flux = backward_face * (values - down) / maps.dphi
128
+ return (forward_flux - backward_flux) / (jnp.maximum(jac, 1.0e-30) * maps.dphi)
129
+
130
+
131
+ def laplace_perp_xz(field: jnp.ndarray, *, dx: float, dz: float) -> jnp.ndarray:
132
+ """Simple perpendicular benchmark Laplacian in `(x,z)` logical space."""
133
+
134
+ values = jnp.asarray(field, dtype=jnp.float64)
135
+ x_part = jnp.zeros_like(values)
136
+ x_part = x_part.at[1:-1, :, :].set((values[2:, :, :] - 2.0 * values[1:-1, :, :] + values[:-2, :, :]) / (dx * dx))
137
+ x_part = x_part.at[0, :, :].set((values[1, :, :] - values[0, :, :]) / (dx * dx))
138
+ x_part = x_part.at[-1, :, :].set((values[-2, :, :] - values[-1, :, :]) / (dx * dx))
139
+ z_part = (jnp.roll(values, -1, axis=2) - 2.0 * values + jnp.roll(values, 1, axis=2)) / (dz * dz)
140
+ return x_part + z_part
141
+
142
+
143
+ def conservative_perp_diffusion_xz(
144
+ field: jnp.ndarray,
145
+ coefficient: jnp.ndarray,
146
+ metric: MetricTensor3D,
147
+ ) -> jnp.ndarray:
148
+ """Metric-weighted conservative perpendicular diffusion in logical ``x-z``.
149
+
150
+ This first production-facing operator uses the diagonal perpendicular
151
+ metric terms ``g11`` and ``g33`` with zero radial flux and periodic
152
+ poloidal/toroidal-binormal flux. Cross-metric terms are deliberately left
153
+ for the next manufactured-solution gate so this operator remains symmetric,
154
+ compact, and directly testable.
155
+ """
156
+
157
+ values = jnp.asarray(field, dtype=jnp.float64)
158
+ coef = jnp.asarray(coefficient, dtype=jnp.float64)
159
+ jac = jnp.asarray(metric.J, dtype=jnp.float64)
160
+ dx = jnp.asarray(metric.dx, dtype=jnp.float64)
161
+ dz = jnp.asarray(metric.dz, dtype=jnp.float64)
162
+ kx = jac * coef * jnp.asarray(metric.g11, dtype=jnp.float64)
163
+ kz = jac * coef * jnp.asarray(metric.g33, dtype=jnp.float64)
164
+
165
+ dx_face = 0.5 * (dx[1:, :, :] + dx[:-1, :, :])
166
+ kx_face = 0.5 * (kx[1:, :, :] + kx[:-1, :, :])
167
+ flux_x_internal = kx_face * (values[1:, :, :] - values[:-1, :, :]) / jnp.maximum(dx_face, 1.0e-30)
168
+ flux_x_plus = jnp.zeros_like(values).at[:-1, :, :].set(flux_x_internal)
169
+ flux_x_minus = jnp.zeros_like(values).at[1:, :, :].set(flux_x_internal)
170
+ div_x = (flux_x_plus - flux_x_minus) / jnp.maximum(dx, 1.0e-30)
171
+
172
+ values_plus_z = jnp.roll(values, -1, axis=2)
173
+ kz_plus = jnp.roll(kz, -1, axis=2)
174
+ dz_plus = 0.5 * (dz + jnp.roll(dz, -1, axis=2))
175
+ kz_face = 0.5 * (kz + kz_plus)
176
+ flux_z_plus = kz_face * (values_plus_z - values) / jnp.maximum(dz_plus, 1.0e-30)
177
+ flux_z_minus = jnp.roll(flux_z_plus, 1, axis=2)
178
+ div_z = (flux_z_plus - flux_z_minus) / jnp.maximum(dz, 1.0e-30)
179
+ return (div_x + div_z) / jnp.maximum(jac, 1.0e-30)
180
+
181
+
182
+ def logical_exb_bracket_xz(
183
+ potential: jnp.ndarray,
184
+ field: jnp.ndarray,
185
+ metric: MetricTensor3D,
186
+ *,
187
+ periodic_x: bool = False,
188
+ periodic_z: bool = True,
189
+ b_floor: float = 1.0e-30,
190
+ ) -> jnp.ndarray:
191
+ """Return the logical perpendicular ``E x B`` bracket ``{phi, f}``.
192
+
193
+ This is the geometry-facing nonlinear advection seam for reduced
194
+ non-axisymmetric tests. In the logical radial/poloidal plane it evaluates
195
+
196
+ ``(d_z phi d_x f - d_x phi d_z f) / B``
197
+
198
+ with the grid spacing stored in ``metric``. The toroidal/FCI direction is
199
+ not differentiated here; field-line coupling enters through the FCI
200
+ operators and the potential/vorticity solve.
201
+ """
202
+
203
+ phi = jnp.asarray(potential, dtype=jnp.float64)
204
+ values = jnp.asarray(field, dtype=jnp.float64)
205
+ dphi_dx = _first_derivative_3d(phi, metric.dx, axis=0, periodic=periodic_x)
206
+ dphi_dz = _first_derivative_3d(phi, metric.dz, axis=2, periodic=periodic_z)
207
+ df_dx = _first_derivative_3d(values, metric.dx, axis=0, periodic=periodic_x)
208
+ df_dz = _first_derivative_3d(values, metric.dz, axis=2, periodic=periodic_z)
209
+ return (dphi_dz * df_dx - dphi_dx * df_dz) / jnp.maximum(
210
+ jnp.asarray(metric.Bxy, dtype=jnp.float64),
211
+ float(b_floor),
212
+ )
213
+
214
+
215
+ def metric_weighted_scalar_laplacian_3d(
216
+ field: jnp.ndarray,
217
+ metric: MetricTensor3D,
218
+ coefficient: jnp.ndarray | float = 1.0,
219
+ *,
220
+ periodic_axes: tuple[bool, bool, bool] = (False, True, True),
221
+ ) -> jnp.ndarray:
222
+ """Return ``J^-1 d_i(J K g^ij d_j f)`` on a logical 3D grid.
223
+
224
+ This is the full contravariant-metric scalar diffusion form used by the
225
+ non-axisymmetric manufactured-solution gate. It includes all cross-metric
226
+ terms and is written only with JAX array operations, so it can be used under
227
+ ``jit``, ``vmap``, ``jvp``, and ``linearize``. The default boundary choice
228
+ treats the radial direction as open/one-sided and the field-line/toroidal
229
+ directions as periodic; analytic verification tests can request fully
230
+ periodic axes.
231
+ """
232
+
233
+ values = jnp.asarray(field, dtype=jnp.float64)
234
+ coef = jnp.asarray(coefficient, dtype=jnp.float64)
235
+ if coef.ndim == 0:
236
+ coef = jnp.ones_like(values) * coef
237
+ jac = jnp.asarray(metric.J, dtype=jnp.float64)
238
+ weighted = jac * coef
239
+ df_dx = _first_derivative_3d(values, metric.dx, axis=0, periodic=periodic_axes[0])
240
+ df_dy = _first_derivative_3d(values, metric.dy, axis=1, periodic=periodic_axes[1])
241
+ df_dz = _first_derivative_3d(values, metric.dz, axis=2, periodic=periodic_axes[2])
242
+
243
+ flux_x = weighted * (
244
+ jnp.asarray(metric.g11, dtype=jnp.float64) * df_dx
245
+ + jnp.asarray(metric.g12, dtype=jnp.float64) * df_dy
246
+ + jnp.asarray(metric.g13, dtype=jnp.float64) * df_dz
247
+ )
248
+ flux_y = weighted * (
249
+ jnp.asarray(metric.g12, dtype=jnp.float64) * df_dx
250
+ + jnp.asarray(metric.g22, dtype=jnp.float64) * df_dy
251
+ + jnp.asarray(metric.g23, dtype=jnp.float64) * df_dz
252
+ )
253
+ flux_z = weighted * (
254
+ jnp.asarray(metric.g13, dtype=jnp.float64) * df_dx
255
+ + jnp.asarray(metric.g23, dtype=jnp.float64) * df_dy
256
+ + jnp.asarray(metric.g33, dtype=jnp.float64) * df_dz
257
+ )
258
+
259
+ divergence = (
260
+ _first_derivative_3d(flux_x, metric.dx, axis=0, periodic=periodic_axes[0])
261
+ + _first_derivative_3d(flux_y, metric.dy, axis=1, periodic=periodic_axes[1])
262
+ + _first_derivative_3d(flux_z, metric.dz, axis=2, periodic=periodic_axes[2])
263
+ )
264
+ return divergence / jnp.maximum(jac, 1.0e-30)
265
+
266
+
267
+ def _first_derivative_3d(
268
+ values: jnp.ndarray,
269
+ spacing: jnp.ndarray | float,
270
+ *,
271
+ axis: int,
272
+ periodic: bool,
273
+ ) -> jnp.ndarray:
274
+ """Centered first derivative with periodic or one-sided edge treatment."""
275
+
276
+ h = jnp.asarray(spacing, dtype=jnp.float64)
277
+ if h.ndim == 0:
278
+ h = jnp.ones_like(values) * h
279
+ centered = (jnp.roll(values, -1, axis=axis) - jnp.roll(values, 1, axis=axis)) / jnp.maximum(2.0 * h, 1.0e-30)
280
+ if periodic:
281
+ return centered
282
+
283
+ first = _axis_index(axis, 0)
284
+ second = _axis_index(axis, 1)
285
+ last = _axis_index(axis, -1)
286
+ penultimate = _axis_index(axis, -2)
287
+ forward = (values[second] - values[first]) / jnp.maximum(h[first], 1.0e-30)
288
+ backward = (values[last] - values[penultimate]) / jnp.maximum(h[last], 1.0e-30)
289
+ return centered.at[first].set(forward).at[last].set(backward)
290
+
291
+
292
+ def _axis_index(axis: int, index: int) -> tuple[object, object, object]:
293
+ slices: list[object] = [slice(None), slice(None), slice(None)]
294
+ slices[axis] = index
295
+ return tuple(slices)
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import time as time_module
4
+ from dataclasses import dataclass
5
+
6
+ import jax
7
+ import jax.numpy as jnp
8
+
9
+ from ..geometry import (
10
+ FciGeometry3D,
11
+ LocalFciGeometry3D,
12
+ build_local_stencil_from_field,
13
+ LocalStencilBuilder,
14
+ )
15
+ from .fci_model import FciModelState
16
+ from .fci_boundaries import BoundaryFaceBC3D, CutWallBC3D, CutWallGeometry3D
17
+ from .fci_operators import (
18
+ curvature_op,
19
+ grad_parallel_op_direct,
20
+ local_curvature_op,
21
+ local_grad_parallel_op_direct,
22
+ local_poisson_bracket_op,
23
+ poisson_bracket_op,
24
+ )
25
+
26
+
27
+ @jax.tree_util.register_pytree_node_class
28
+ @dataclass(frozen=True)
29
+ class Fci2FieldState(FciModelState):
30
+ density: jax.Array
31
+ v_parallel: jax.Array
32
+ density_background: jax.Array
33
+
34
+
35
+ @jax.tree_util.register_pytree_node_class
36
+ @dataclass(frozen=True)
37
+ class Fci2FieldRhsParameters:
38
+ """Placeholder parameter bundle for the reduced two-field FCI model."""
39
+
40
+ rho_star: float = 1.0
41
+
42
+ def tree_flatten(self):
43
+ return ((self.rho_star,), None)
44
+
45
+ @classmethod
46
+ def tree_unflatten(cls, _aux_data, children):
47
+ (rho_star,) = children
48
+ return cls(rho_star=rho_star)
49
+
50
+
51
+ @jax.tree_util.register_pytree_node_class
52
+ @dataclass(frozen=True)
53
+ class Fci2FieldRhsResult:
54
+ rhs: Fci2FieldState
55
+
56
+ def tree_flatten(self):
57
+ return ((self.rhs,), None)
58
+
59
+ @classmethod
60
+ def tree_unflatten(cls, _aux_data, children):
61
+ (rhs,) = children
62
+ return cls(rhs=rhs)
63
+
64
+
65
+ def compute_2field_rhs(
66
+ state: Fci2FieldState,
67
+ *,
68
+ geometry: FciGeometry3D,
69
+ stencil_builder: LocalStencilBuilder = build_local_stencil_from_field,
70
+ parameters: Fci2FieldRhsParameters = Fci2FieldRhsParameters(),
71
+ curvature_coefficients: jax.Array,
72
+ density_face_bc: BoundaryFaceBC3D,
73
+ phi_face_bc: BoundaryFaceBC3D,
74
+ v_parallel_face_bc: BoundaryFaceBC3D,
75
+ periodic_axes: tuple[bool, bool, bool] = (False, True, True),
76
+ density_cut_wall_geometry: CutWallGeometry3D | None = None,
77
+ density_cut_wall_bc: CutWallBC3D | None = None,
78
+ phi_cut_wall_geometry: CutWallGeometry3D | None = None,
79
+ phi_cut_wall_bc: CutWallBC3D | None = None,
80
+ v_parallel_cut_wall_geometry: CutWallGeometry3D | None = None,
81
+ v_parallel_cut_wall_bc: CutWallBC3D | None = None,
82
+ density_source: jax.Array | None = None,
83
+ v_parallel_source: jax.Array | None = None,
84
+ with_diagnostics: bool = False,
85
+ ) -> tuple[Fci2FieldRhsResult, jnp.ndarray | None]:
86
+ """Assemble the reduced two-field FCI RHS with Neumann stencil reconstruction.
87
+
88
+ ``geometry`` may be a global ``FciGeometry3D`` or a shard-local
89
+ ``LocalFciGeometry3D``; the local path evaluates the owned-cell operator
90
+ variants and expects an injected ``stencil_builder`` that owns halo
91
+ preparation, mirroring the geometry-type dispatch used by the shared
92
+ stencil builders.
93
+ """
94
+
95
+ is_local = isinstance(geometry, LocalFciGeometry3D)
96
+ rho_star = jnp.asarray(parameters.rho_star, dtype=jnp.float64)
97
+ magnetic_field = jnp.maximum(
98
+ jnp.asarray(
99
+ geometry.cell_bfield.Bmag_owned if is_local else geometry.cell_bfield.Bmag,
100
+ dtype=jnp.float64,
101
+ ),
102
+ 1.0e-30,
103
+ )
104
+
105
+ density = jnp.asarray(state.density, dtype=jnp.float64)
106
+ v_parallel = jnp.asarray(state.v_parallel, dtype=jnp.float64)
107
+ density_background = jnp.asarray(state.density_background, dtype=jnp.float64)
108
+ phi = jnp.log(jnp.maximum(density, 1.0e-30) / jnp.maximum(density_background, 1.0e-30))
109
+
110
+ stencil_start = time_module.perf_counter() if with_diagnostics else 0.0
111
+ density_stencil = stencil_builder(
112
+ density,
113
+ geometry,
114
+ periodic_axes=periodic_axes,
115
+ face_bc=density_face_bc,
116
+ cut_wall_geometry=density_cut_wall_geometry,
117
+ cut_wall_bc=density_cut_wall_bc,
118
+ )
119
+ phi_stencil = stencil_builder(
120
+ phi,
121
+ geometry,
122
+ periodic_axes=periodic_axes,
123
+ face_bc=phi_face_bc,
124
+ cut_wall_geometry=phi_cut_wall_geometry,
125
+ cut_wall_bc=phi_cut_wall_bc,
126
+ )
127
+ v_parallel_stencil = stencil_builder(
128
+ v_parallel,
129
+ geometry,
130
+ periodic_axes=periodic_axes,
131
+ face_bc=v_parallel_face_bc,
132
+ cut_wall_geometry=v_parallel_cut_wall_geometry,
133
+ cut_wall_bc=v_parallel_cut_wall_bc,
134
+ )
135
+ if with_diagnostics:
136
+ # Host-synced stage timings for the validation harnesses; the default
137
+ # path stays sync-free so the whole RHS can run inside jit.
138
+ jax.block_until_ready(density_stencil.x.center)
139
+ jax.block_until_ready(phi_stencil.x.center)
140
+ jax.block_until_ready(v_parallel_stencil.x.center)
141
+ stencil_time = (time_module.perf_counter() - stencil_start) if with_diagnostics else 0.0
142
+
143
+ operator_start = time_module.perf_counter() if with_diagnostics else 0.0
144
+ if is_local:
145
+ poisson_density = local_poisson_bracket_op(phi_stencil, density_stencil, geometry)
146
+ curvature_density = local_curvature_op(density_stencil, geometry, curvature_coefficients=curvature_coefficients)
147
+ curvature_phi = local_curvature_op(phi_stencil, geometry, curvature_coefficients=curvature_coefficients)
148
+ parallel_velocity_gradient = local_grad_parallel_op_direct(v_parallel_stencil, geometry)
149
+ poisson_v_parallel = local_poisson_bracket_op(phi_stencil, v_parallel_stencil, geometry)
150
+ else:
151
+ poisson_density = poisson_bracket_op(phi_stencil, density_stencil, geometry)
152
+ curvature_density = curvature_op(density_stencil, geometry, curvature_coefficients=curvature_coefficients)
153
+ curvature_phi = curvature_op(phi_stencil, geometry, curvature_coefficients=curvature_coefficients)
154
+ parallel_velocity_gradient = grad_parallel_op_direct(v_parallel_stencil, geometry)
155
+ poisson_v_parallel = poisson_bracket_op(phi_stencil, v_parallel_stencil, geometry)
156
+
157
+ density_rhs = (
158
+ -(poisson_density / (rho_star * magnetic_field))
159
+ + (2.0 / magnetic_field) * curvature_density
160
+ - (2.0 * density / magnetic_field) * curvature_phi
161
+ - density * parallel_velocity_gradient
162
+ )
163
+ v_parallel_rhs = -(poisson_v_parallel / (rho_star * magnetic_field))
164
+
165
+ if density_source is not None:
166
+ density_rhs = density_rhs + jnp.asarray(density_source, dtype=jnp.float64)
167
+ if v_parallel_source is not None:
168
+ v_parallel_rhs = v_parallel_rhs + jnp.asarray(v_parallel_source, dtype=jnp.float64)
169
+ rhs_density = jnp.asarray(density_rhs, dtype=jnp.float64)
170
+ rhs_v_parallel = jnp.asarray(v_parallel_rhs, dtype=jnp.float64)
171
+
172
+ rhs = Fci2FieldState(
173
+ density=rhs_density,
174
+ v_parallel=rhs_v_parallel,
175
+ density_background=jnp.zeros_like(density_background),
176
+ )
177
+ if not with_diagnostics:
178
+ return Fci2FieldRhsResult(rhs=rhs), None
179
+ jax.block_until_ready(rhs_density)
180
+ jax.block_until_ready(rhs_v_parallel)
181
+ operator_time = time_module.perf_counter() - operator_start
182
+ return Fci2FieldRhsResult(rhs=rhs), jnp.asarray([stencil_time, operator_time], dtype=jnp.float64)