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,499 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
import numpy as np
|
|
11
|
+
from netCDF4 import Dataset
|
|
12
|
+
|
|
13
|
+
from .fci_maps import FciMaps
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_REQUIRED_DIMENSIONS = ("nR", "nphi", "nZ")
|
|
17
|
+
_REQUIRED_COORDINATES = ("R", "phi", "Z")
|
|
18
|
+
_REQUIRED_FIELDS = ("BR", "Bphi", "BZ", "absB")
|
|
19
|
+
_ABSB_RTOL = 1.0e-7
|
|
20
|
+
_ABSB_ATOL = 1.0e-10
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@jax.tree_util.register_pytree_node_class
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class VmecExtenderGrid:
|
|
26
|
+
"""Gridded VMEC-extender magnetic field in physical cylindrical coordinates."""
|
|
27
|
+
|
|
28
|
+
R: jax.Array
|
|
29
|
+
phi: jax.Array
|
|
30
|
+
Z: jax.Array
|
|
31
|
+
BR: jax.Array
|
|
32
|
+
Bphi: jax.Array
|
|
33
|
+
BZ: jax.Array
|
|
34
|
+
absB: jax.Array
|
|
35
|
+
nfp: int
|
|
36
|
+
phi_period: float
|
|
37
|
+
metadata: Mapping[str, Any]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def shape(self) -> tuple[int, int, int]:
|
|
41
|
+
return (int(self.R.size), int(self.phi.size), int(self.Z.size))
|
|
42
|
+
|
|
43
|
+
def tree_flatten(self):
|
|
44
|
+
children = (self.R, self.phi, self.Z, self.BR, self.Bphi, self.BZ, self.absB)
|
|
45
|
+
metadata = tuple(sorted((str(key), _static_metadata_value(value)) for key, value in self.metadata.items()))
|
|
46
|
+
aux_data = (int(self.nfp), float(self.phi_period), metadata)
|
|
47
|
+
return children, aux_data
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def tree_unflatten(cls, aux_data, children):
|
|
51
|
+
nfp, phi_period, metadata = aux_data
|
|
52
|
+
R, phi, Z, BR, Bphi, BZ, absB = children
|
|
53
|
+
return cls(
|
|
54
|
+
R=R,
|
|
55
|
+
phi=phi,
|
|
56
|
+
Z=Z,
|
|
57
|
+
BR=BR,
|
|
58
|
+
Bphi=Bphi,
|
|
59
|
+
BZ=BZ,
|
|
60
|
+
absB=absB,
|
|
61
|
+
nfp=int(nfp),
|
|
62
|
+
phi_period=float(phi_period),
|
|
63
|
+
metadata=dict(metadata),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_vmec_extender_grid_netcdf(
|
|
68
|
+
path: str | Path,
|
|
69
|
+
*,
|
|
70
|
+
strict_metadata: bool = True,
|
|
71
|
+
) -> VmecExtenderGrid:
|
|
72
|
+
"""Load a gridded VMEC-extender magnetic field exported as NetCDF.
|
|
73
|
+
|
|
74
|
+
The importer expects physical cylindrical coordinates `(R, phi, Z)` and
|
|
75
|
+
cylindrical field components `(BR, Bphi, BZ)`. File I/O and validation are
|
|
76
|
+
intentionally NumPy/NetCDF operations; the returned interpolation kernels
|
|
77
|
+
operate on JAX arrays.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
resolved = Path(path)
|
|
81
|
+
with Dataset(resolved) as dataset:
|
|
82
|
+
_validate_required_dimensions(dataset)
|
|
83
|
+
metadata = {name: _normalize_metadata_value(dataset.getncattr(name)) for name in dataset.ncattrs()}
|
|
84
|
+
R = _read_1d_variable(dataset, "R")
|
|
85
|
+
phi = _read_1d_variable(dataset, "phi")
|
|
86
|
+
Z = _read_1d_variable(dataset, "Z")
|
|
87
|
+
expected_shape = (R.size, phi.size, Z.size)
|
|
88
|
+
BR = _read_field_variable(dataset, "BR", expected_shape)
|
|
89
|
+
Bphi = _read_field_variable(dataset, "Bphi", expected_shape)
|
|
90
|
+
BZ = _read_field_variable(dataset, "BZ", expected_shape)
|
|
91
|
+
absB = _read_field_variable(dataset, "absB", expected_shape)
|
|
92
|
+
|
|
93
|
+
nfp = _metadata_nfp(metadata, strict_metadata=strict_metadata)
|
|
94
|
+
_validate_metadata(metadata, strict_metadata=strict_metadata)
|
|
95
|
+
_validate_axis("R", R)
|
|
96
|
+
_validate_axis("phi", phi)
|
|
97
|
+
_validate_axis("Z", Z)
|
|
98
|
+
phi_period = _infer_phi_period(metadata, nfp=nfp)
|
|
99
|
+
_validate_phi_coverage(phi, phi_period=phi_period)
|
|
100
|
+
absB_error = _absB_consistency_error(BR=BR, Bphi=Bphi, BZ=BZ, absB=absB)
|
|
101
|
+
if strict_metadata and absB_error > _ABSB_ATOL + _ABSB_RTOL * float(np.max(np.abs(absB))):
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"absB is inconsistent with sqrt(BR**2 + Bphi**2 + BZ**2): "
|
|
104
|
+
f"max_abs_error={absB_error:.6e}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
metadata = {
|
|
108
|
+
**metadata,
|
|
109
|
+
"resolved_path": str(resolved),
|
|
110
|
+
"absB_consistency_max_abs_error": float(absB_error),
|
|
111
|
+
}
|
|
112
|
+
return VmecExtenderGrid(
|
|
113
|
+
R=jnp.asarray(R, dtype=jnp.float64),
|
|
114
|
+
phi=jnp.asarray(phi, dtype=jnp.float64),
|
|
115
|
+
Z=jnp.asarray(Z, dtype=jnp.float64),
|
|
116
|
+
BR=jnp.asarray(BR, dtype=jnp.float64),
|
|
117
|
+
Bphi=jnp.asarray(Bphi, dtype=jnp.float64),
|
|
118
|
+
BZ=jnp.asarray(BZ, dtype=jnp.float64),
|
|
119
|
+
absB=jnp.asarray(absB, dtype=jnp.float64),
|
|
120
|
+
nfp=int(nfp),
|
|
121
|
+
phi_period=float(phi_period),
|
|
122
|
+
metadata=metadata,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def interpolate_vmec_extender_B_cyl(
|
|
127
|
+
grid: VmecExtenderGrid,
|
|
128
|
+
R_phi_Z: jax.Array,
|
|
129
|
+
*,
|
|
130
|
+
strict_bounds: bool = False,
|
|
131
|
+
) -> jax.Array:
|
|
132
|
+
"""Return `(BR, Bphi, BZ)` at target points with physical-phi wrapping.
|
|
133
|
+
|
|
134
|
+
Nonperiodic `R` and `Z` coordinates are clamped at the loaded grid edges by
|
|
135
|
+
the JAX interpolation kernel. Pass `strict_bounds=True` for an eager
|
|
136
|
+
preflight check that raises when any target point lies outside the imported
|
|
137
|
+
`R`/`Z` domain.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
points = _as_points(R_phi_Z)
|
|
141
|
+
if strict_bounds:
|
|
142
|
+
validate_vmec_extender_points_in_bounds(grid, points)
|
|
143
|
+
BR = _interpolate_component(grid, grid.BR, points)
|
|
144
|
+
Bphi = _interpolate_component(grid, grid.Bphi, points)
|
|
145
|
+
BZ = _interpolate_component(grid, grid.BZ, points)
|
|
146
|
+
return jnp.stack((BR, Bphi, BZ), axis=-1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def vmec_extender_absB(
|
|
150
|
+
grid: VmecExtenderGrid,
|
|
151
|
+
R_phi_Z: jax.Array,
|
|
152
|
+
*,
|
|
153
|
+
strict_bounds: bool = False,
|
|
154
|
+
) -> jax.Array:
|
|
155
|
+
"""Return interpolated magnetic-field magnitude at target points."""
|
|
156
|
+
|
|
157
|
+
points = _as_points(R_phi_Z)
|
|
158
|
+
if strict_bounds:
|
|
159
|
+
validate_vmec_extender_points_in_bounds(grid, points)
|
|
160
|
+
return _interpolate_component(grid, grid.absB, points)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def vmec_extender_fieldline_rhs_RZ_phi(
|
|
164
|
+
grid: VmecExtenderGrid,
|
|
165
|
+
R_phi_Z: jax.Array,
|
|
166
|
+
*,
|
|
167
|
+
min_abs_Bphi: float = 1.0e-12,
|
|
168
|
+
strict_bounds: bool = False,
|
|
169
|
+
) -> jax.Array:
|
|
170
|
+
"""Return `(dR/dphi, dZ/dphi)` using `R * BR / Bphi` and `R * BZ / Bphi`.
|
|
171
|
+
|
|
172
|
+
Very small `Bphi` values are bounded with a sign-preserving denominator so
|
|
173
|
+
the function remains JAX-transformable instead of raising inside compiled
|
|
174
|
+
code.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
points = _as_points(R_phi_Z)
|
|
178
|
+
if strict_bounds:
|
|
179
|
+
validate_vmec_extender_points_in_bounds(grid, points)
|
|
180
|
+
B = interpolate_vmec_extender_B_cyl(grid, points)
|
|
181
|
+
R = points[..., 0]
|
|
182
|
+
BR = B[..., 0]
|
|
183
|
+
Bphi = B[..., 1]
|
|
184
|
+
BZ = B[..., 2]
|
|
185
|
+
floor = jnp.asarray(float(min_abs_Bphi), dtype=jnp.float64)
|
|
186
|
+
sign = jnp.where(Bphi < 0.0, -1.0, 1.0)
|
|
187
|
+
safe_Bphi = jnp.where(jnp.abs(Bphi) < floor, sign * floor, Bphi)
|
|
188
|
+
return jnp.stack((R * BR / safe_Bphi, R * BZ / safe_Bphi), axis=-1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def vmec_extender_points_in_bounds(grid: VmecExtenderGrid, R_phi_Z: jax.Array) -> jax.Array:
|
|
192
|
+
"""Return a boolean mask for points inside the imported nonperiodic `R`/`Z` domain."""
|
|
193
|
+
|
|
194
|
+
points = _as_points(R_phi_Z)
|
|
195
|
+
return (
|
|
196
|
+
(points[..., 0] >= grid.R[0])
|
|
197
|
+
& (points[..., 0] <= grid.R[-1])
|
|
198
|
+
& (points[..., 2] >= grid.Z[0])
|
|
199
|
+
& (points[..., 2] <= grid.Z[-1])
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def validate_vmec_extender_points_in_bounds(grid: VmecExtenderGrid, R_phi_Z: jax.Array) -> None:
|
|
204
|
+
"""Raise if any point lies outside the imported nonperiodic `R`/`Z` domain.
|
|
205
|
+
|
|
206
|
+
This is an eager host-side guard intended for setup, tests, and production
|
|
207
|
+
preflight checks. It is deliberately separate from the JAX interpolation
|
|
208
|
+
kernel so the kernel remains usable under `jax.jit`.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
points = np.asarray(R_phi_Z, dtype=np.float64)
|
|
212
|
+
if points.shape == (3,):
|
|
213
|
+
points = points.reshape((1, 3))
|
|
214
|
+
elif points.ndim == 0 or points.shape[-1] != 3:
|
|
215
|
+
raise ValueError("R_phi_Z must have shape (3,) or (..., 3).")
|
|
216
|
+
flat = points.reshape((-1, 3))
|
|
217
|
+
R_min = float(np.asarray(grid.R[0]))
|
|
218
|
+
R_max = float(np.asarray(grid.R[-1]))
|
|
219
|
+
Z_min = float(np.asarray(grid.Z[0]))
|
|
220
|
+
Z_max = float(np.asarray(grid.Z[-1]))
|
|
221
|
+
mask = (
|
|
222
|
+
(flat[:, 0] >= R_min)
|
|
223
|
+
& (flat[:, 0] <= R_max)
|
|
224
|
+
& (flat[:, 2] >= Z_min)
|
|
225
|
+
& (flat[:, 2] <= Z_max)
|
|
226
|
+
)
|
|
227
|
+
if not bool(np.all(mask)):
|
|
228
|
+
first_bad = flat[np.flatnonzero(~mask)[0]]
|
|
229
|
+
raise ValueError(
|
|
230
|
+
"VMEC-extender target point lies outside imported R/Z domain: "
|
|
231
|
+
f"point=(R={first_bad[0]:.6g}, phi={first_bad[1]:.6g}, Z={first_bad[2]:.6g}), "
|
|
232
|
+
f"R_range=({R_min:.6g}, {R_max:.6g}), Z_range=({Z_min:.6g}, {Z_max:.6g})"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def build_vmec_extender_fci_maps(
|
|
237
|
+
grid: VmecExtenderGrid,
|
|
238
|
+
*,
|
|
239
|
+
substeps: int = 8,
|
|
240
|
+
) -> FciMaps:
|
|
241
|
+
"""Build one-plane forward/backward FCI maps from the imported field.
|
|
242
|
+
|
|
243
|
+
Map values are returned in logical `(R_index, Z_index)` coordinates and can
|
|
244
|
+
be passed to the existing compact FCI operator surfaces.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
dphi = float(grid.phi_period / int(grid.phi.size))
|
|
248
|
+
R0, phi0, Z0 = jnp.meshgrid(grid.R, grid.phi, grid.Z, indexing="ij")
|
|
249
|
+
points = jnp.stack((R0, phi0, Z0), axis=-1)
|
|
250
|
+
forward = _rk4_fieldline_step(grid, points, dphi=float(dphi), substeps=int(substeps))
|
|
251
|
+
backward = _rk4_fieldline_step(grid, points, dphi=-float(dphi), substeps=int(substeps))
|
|
252
|
+
|
|
253
|
+
forward_R_index = _axis_to_logical_index(grid.R, forward[..., 0])
|
|
254
|
+
forward_Z_index = _axis_to_logical_index(grid.Z, forward[..., 2])
|
|
255
|
+
backward_R_index = _axis_to_logical_index(grid.R, backward[..., 0])
|
|
256
|
+
backward_Z_index = _axis_to_logical_index(grid.Z, backward[..., 2])
|
|
257
|
+
forward_boundary = _outside_RZ(grid, forward)
|
|
258
|
+
backward_boundary = _outside_RZ(grid, backward)
|
|
259
|
+
return FciMaps(
|
|
260
|
+
forward_x=forward_R_index,
|
|
261
|
+
forward_z=forward_Z_index,
|
|
262
|
+
backward_x=backward_R_index,
|
|
263
|
+
backward_z=backward_Z_index,
|
|
264
|
+
forward_boundary=forward_boundary,
|
|
265
|
+
backward_boundary=backward_boundary,
|
|
266
|
+
dphi=float(dphi),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _rk4_fieldline_step(
|
|
271
|
+
grid: VmecExtenderGrid,
|
|
272
|
+
points: jax.Array,
|
|
273
|
+
*,
|
|
274
|
+
dphi: float,
|
|
275
|
+
substeps: int,
|
|
276
|
+
) -> jax.Array:
|
|
277
|
+
step = float(dphi) / float(max(int(substeps), 1))
|
|
278
|
+
state = jnp.asarray(points, dtype=jnp.float64)
|
|
279
|
+
for _ in range(max(int(substeps), 1)):
|
|
280
|
+
k1 = _fieldline_state_rhs(grid, state)
|
|
281
|
+
k2 = _fieldline_state_rhs(grid, state + 0.5 * step * k1)
|
|
282
|
+
k3 = _fieldline_state_rhs(grid, state + 0.5 * step * k2)
|
|
283
|
+
k4 = _fieldline_state_rhs(grid, state + step * k3)
|
|
284
|
+
state = state + (step / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
|
|
285
|
+
return state
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _fieldline_state_rhs(grid: VmecExtenderGrid, points: jax.Array) -> jax.Array:
|
|
289
|
+
rhs_RZ = vmec_extender_fieldline_rhs_RZ_phi(grid, points)
|
|
290
|
+
ones = jnp.ones_like(rhs_RZ[..., 0])
|
|
291
|
+
return jnp.stack((rhs_RZ[..., 0], ones, rhs_RZ[..., 1]), axis=-1)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _axis_to_logical_index(axis: jax.Array, values: jax.Array) -> jax.Array:
|
|
295
|
+
i0, i1, weight = _bracket_nonperiodic(axis, values)
|
|
296
|
+
return jnp.asarray(i0, dtype=jnp.float64) * (1.0 - weight) + jnp.asarray(i1, dtype=jnp.float64) * weight
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _outside_RZ(grid: VmecExtenderGrid, points: jax.Array) -> jax.Array:
|
|
300
|
+
return (
|
|
301
|
+
(points[..., 0] < grid.R[0])
|
|
302
|
+
| (points[..., 0] > grid.R[-1])
|
|
303
|
+
| (points[..., 2] < grid.Z[0])
|
|
304
|
+
| (points[..., 2] > grid.Z[-1])
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _as_points(R_phi_Z: jax.Array) -> jax.Array:
|
|
309
|
+
points = jnp.asarray(R_phi_Z, dtype=jnp.float64)
|
|
310
|
+
if points.shape == (3,):
|
|
311
|
+
return points
|
|
312
|
+
if len(points.shape) == 0 or points.shape[-1] != 3:
|
|
313
|
+
raise ValueError("R_phi_Z must have shape (3,) or (..., 3).")
|
|
314
|
+
return points
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _interpolate_component(grid: VmecExtenderGrid, values: jax.Array, points: jax.Array) -> jax.Array:
|
|
318
|
+
R_value = points[..., 0]
|
|
319
|
+
phi_value = points[..., 1]
|
|
320
|
+
Z_value = points[..., 2]
|
|
321
|
+
r0, r1, wr = _bracket_nonperiodic(grid.R, R_value)
|
|
322
|
+
p0, p1, wp = _bracket_periodic(grid.phi, phi_value, period=float(grid.phi_period))
|
|
323
|
+
z0, z1, wz = _bracket_nonperiodic(grid.Z, Z_value)
|
|
324
|
+
return _trilinear(values, r0, r1, wr, p0, p1, wp, z0, z1, wz)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _bracket_nonperiodic(axis: jax.Array, values: jax.Array) -> tuple[jax.Array, jax.Array, jax.Array]:
|
|
328
|
+
upper = jnp.searchsorted(axis, values, side="right")
|
|
329
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 2)
|
|
330
|
+
upper = lower + 1
|
|
331
|
+
lower_coord = axis[lower]
|
|
332
|
+
upper_coord = axis[upper]
|
|
333
|
+
weight = (values - lower_coord) / (upper_coord - lower_coord)
|
|
334
|
+
return lower, upper, jnp.clip(weight, 0.0, 1.0)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _bracket_periodic(axis: jax.Array, values: jax.Array, *, period: float) -> tuple[jax.Array, jax.Array, jax.Array]:
|
|
338
|
+
origin = axis[0]
|
|
339
|
+
wrapped = jnp.mod(values - origin, float(period)) + origin
|
|
340
|
+
upper = jnp.searchsorted(axis, wrapped, side="right")
|
|
341
|
+
lower = jnp.clip(upper - 1, 0, int(axis.size) - 1)
|
|
342
|
+
next_index = jnp.mod(lower + 1, int(axis.size))
|
|
343
|
+
lower_coord = axis[lower]
|
|
344
|
+
upper_coord = jnp.where(lower == int(axis.size) - 1, axis[0] + float(period), axis[next_index])
|
|
345
|
+
weight = (wrapped - lower_coord) / (upper_coord - lower_coord)
|
|
346
|
+
return lower, next_index, jnp.clip(weight, 0.0, 1.0)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _trilinear(
|
|
350
|
+
values: jax.Array,
|
|
351
|
+
r0: jax.Array,
|
|
352
|
+
r1: jax.Array,
|
|
353
|
+
wr: jax.Array,
|
|
354
|
+
p0: jax.Array,
|
|
355
|
+
p1: jax.Array,
|
|
356
|
+
wp: jax.Array,
|
|
357
|
+
z0: jax.Array,
|
|
358
|
+
z1: jax.Array,
|
|
359
|
+
wz: jax.Array,
|
|
360
|
+
) -> jax.Array:
|
|
361
|
+
c000 = values[r0, p0, z0]
|
|
362
|
+
c100 = values[r1, p0, z0]
|
|
363
|
+
c010 = values[r0, p1, z0]
|
|
364
|
+
c110 = values[r1, p1, z0]
|
|
365
|
+
c001 = values[r0, p0, z1]
|
|
366
|
+
c101 = values[r1, p0, z1]
|
|
367
|
+
c011 = values[r0, p1, z1]
|
|
368
|
+
c111 = values[r1, p1, z1]
|
|
369
|
+
c00 = c000 * (1.0 - wr) + c100 * wr
|
|
370
|
+
c10 = c010 * (1.0 - wr) + c110 * wr
|
|
371
|
+
c01 = c001 * (1.0 - wr) + c101 * wr
|
|
372
|
+
c11 = c011 * (1.0 - wr) + c111 * wr
|
|
373
|
+
c0 = c00 * (1.0 - wp) + c10 * wp
|
|
374
|
+
c1 = c01 * (1.0 - wp) + c11 * wp
|
|
375
|
+
return c0 * (1.0 - wz) + c1 * wz
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _validate_required_dimensions(dataset: Dataset) -> None:
|
|
379
|
+
for name in _REQUIRED_DIMENSIONS:
|
|
380
|
+
if name not in dataset.dimensions:
|
|
381
|
+
raise ValueError(f"VMEC-extender grid is missing required dimension {name!r}.")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _read_1d_variable(dataset: Dataset, name: str) -> np.ndarray:
|
|
385
|
+
if name not in dataset.variables:
|
|
386
|
+
raise ValueError(f"VMEC-extender grid is missing required coordinate variable {name!r}.")
|
|
387
|
+
values = np.asarray(dataset.variables[name][:], dtype=np.float64)
|
|
388
|
+
if values.ndim != 1:
|
|
389
|
+
raise ValueError(f"Coordinate variable {name!r} must be one-dimensional, got shape {values.shape}.")
|
|
390
|
+
return values
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _read_field_variable(dataset: Dataset, name: str, expected_shape: tuple[int, int, int]) -> np.ndarray:
|
|
394
|
+
if name not in dataset.variables:
|
|
395
|
+
raise ValueError(f"VMEC-extender grid is missing required field variable {name!r}.")
|
|
396
|
+
values = np.asarray(dataset.variables[name][:], dtype=np.float64)
|
|
397
|
+
if values.shape != expected_shape:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"Field variable {name!r} has shape {values.shape}, expected {expected_shape} for dimensions (nR, nphi, nZ)."
|
|
400
|
+
)
|
|
401
|
+
if not np.all(np.isfinite(values)):
|
|
402
|
+
raise ValueError(f"Field variable {name!r} contains non-finite values.")
|
|
403
|
+
return values
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _validate_axis(name: str, values: np.ndarray) -> None:
|
|
407
|
+
if values.size < 2:
|
|
408
|
+
raise ValueError(f"Coordinate axis {name!r} must contain at least two points.")
|
|
409
|
+
if not np.all(np.isfinite(values)):
|
|
410
|
+
raise ValueError(f"Coordinate axis {name!r} contains non-finite values.")
|
|
411
|
+
if not np.all(np.diff(values) > 0.0):
|
|
412
|
+
raise ValueError(f"Coordinate axis {name!r} must be strictly increasing.")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _validate_metadata(metadata: Mapping[str, Any], *, strict_metadata: bool) -> None:
|
|
416
|
+
convention = metadata.get("coordinate_convention")
|
|
417
|
+
if convention is None:
|
|
418
|
+
if strict_metadata:
|
|
419
|
+
raise ValueError("VMEC-extender grid is missing required coordinate_convention metadata.")
|
|
420
|
+
return
|
|
421
|
+
convention_text = str(convention).lower()
|
|
422
|
+
if "zeta" in convention_text:
|
|
423
|
+
raise ValueError("VMEC-extender grid coordinate_convention mentions zeta; expected physical phi.")
|
|
424
|
+
if strict_metadata and "phi" not in convention_text:
|
|
425
|
+
raise ValueError("VMEC-extender grid coordinate_convention must explicitly mention physical phi.")
|
|
426
|
+
components = metadata.get("field_components")
|
|
427
|
+
if components is not None:
|
|
428
|
+
component_text = str(components).replace(" ", "").lower()
|
|
429
|
+
for name in ("br", "bphi", "bz"):
|
|
430
|
+
if name not in component_text:
|
|
431
|
+
raise ValueError("VMEC-extender grid field_components must include BR,Bphi,BZ.")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _metadata_nfp(metadata: Mapping[str, Any], *, strict_metadata: bool) -> int:
|
|
435
|
+
value = metadata.get("nfp")
|
|
436
|
+
if value is None:
|
|
437
|
+
if strict_metadata:
|
|
438
|
+
raise ValueError("VMEC-extender grid is missing required nfp metadata.")
|
|
439
|
+
return 1
|
|
440
|
+
try:
|
|
441
|
+
nfp = int(value)
|
|
442
|
+
except (TypeError, ValueError) as error:
|
|
443
|
+
raise ValueError(f"VMEC-extender grid nfp metadata is not an integer: {value!r}") from error
|
|
444
|
+
if nfp <= 0:
|
|
445
|
+
raise ValueError("VMEC-extender grid nfp metadata must be positive.")
|
|
446
|
+
return nfp
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _infer_phi_period(metadata: Mapping[str, Any], *, nfp: int) -> float:
|
|
450
|
+
if "phi_period" in metadata:
|
|
451
|
+
period = float(metadata["phi_period"])
|
|
452
|
+
if period <= 0.0:
|
|
453
|
+
raise ValueError("VMEC-extender grid phi_period metadata must be positive.")
|
|
454
|
+
return period
|
|
455
|
+
coverage = str(
|
|
456
|
+
metadata.get("phi_coverage", metadata.get("toroidal_coverage", metadata.get("coverage", "")))
|
|
457
|
+
).lower()
|
|
458
|
+
if "2*pi" in coverage or "2pi" in coverage or "full" in coverage:
|
|
459
|
+
return float(2.0 * np.pi)
|
|
460
|
+
return float(2.0 * np.pi / int(nfp))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _validate_phi_coverage(phi: np.ndarray, *, phi_period: float) -> None:
|
|
464
|
+
span = float(phi[-1] - phi[0])
|
|
465
|
+
if span >= phi_period:
|
|
466
|
+
raise ValueError(
|
|
467
|
+
"Physical phi grid must not include a duplicated periodic endpoint; "
|
|
468
|
+
f"axis span={span:.6e}, phi_period={phi_period:.6e}."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _absB_consistency_error(*, BR: np.ndarray, Bphi: np.ndarray, BZ: np.ndarray, absB: np.ndarray) -> float:
|
|
473
|
+
expected = np.sqrt(BR * BR + Bphi * Bphi + BZ * BZ)
|
|
474
|
+
return float(np.max(np.abs(absB - expected)))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _normalize_metadata_value(value: Any) -> Any:
|
|
478
|
+
if isinstance(value, bytes):
|
|
479
|
+
return value.decode("utf-8")
|
|
480
|
+
if isinstance(value, np.generic):
|
|
481
|
+
return value.item()
|
|
482
|
+
if isinstance(value, np.ndarray):
|
|
483
|
+
if value.shape == ():
|
|
484
|
+
return _normalize_metadata_value(value.item())
|
|
485
|
+
return tuple(_normalize_metadata_value(item) for item in value.tolist())
|
|
486
|
+
if isinstance(value, (list, tuple)):
|
|
487
|
+
return tuple(_normalize_metadata_value(item) for item in value)
|
|
488
|
+
return value
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _static_metadata_value(value: Any) -> Any:
|
|
492
|
+
value = _normalize_metadata_value(value)
|
|
493
|
+
if isinstance(value, list):
|
|
494
|
+
return tuple(_static_metadata_value(item) for item in value)
|
|
495
|
+
if isinstance(value, tuple):
|
|
496
|
+
return tuple(_static_metadata_value(item) for item in value)
|
|
497
|
+
if isinstance(value, dict):
|
|
498
|
+
return tuple(sorted((str(key), _static_metadata_value(item)) for key, item in value.items()))
|
|
499
|
+
return value
|