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