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
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)
|