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,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import jax.numpy as jnp
7
+ import numpy as np
8
+ from netCDF4 import Dataset
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class FciMaps:
13
+ """Forward/backward traced-field-line maps in logical index coordinates."""
14
+
15
+ forward_x: jnp.ndarray
16
+ forward_z: jnp.ndarray
17
+ backward_x: jnp.ndarray
18
+ backward_z: jnp.ndarray
19
+ forward_boundary: jnp.ndarray
20
+ backward_boundary: jnp.ndarray
21
+ dphi: float
22
+
23
+ @property
24
+ def shape(self) -> tuple[int, int, int]:
25
+ return tuple(int(value) for value in self.forward_x.shape)
26
+
27
+
28
+ def identity_fci_maps(*, nx: int, ny: int, nz: int, dphi: float = 1.0) -> FciMaps:
29
+ """Build maps whose field lines connect to the same `(x,z)` point."""
30
+
31
+ x = jnp.arange(nx, dtype=jnp.float64)[:, None, None]
32
+ z = jnp.arange(nz, dtype=jnp.float64)[None, None, :]
33
+ forward_x = jnp.broadcast_to(x, (nx, ny, nz))
34
+ forward_z = jnp.broadcast_to(z, (nx, ny, nz))
35
+ boundary = jnp.zeros((nx, ny, nz), dtype=bool)
36
+ return FciMaps(
37
+ forward_x=forward_x,
38
+ forward_z=forward_z,
39
+ backward_x=forward_x,
40
+ backward_z=forward_z,
41
+ forward_boundary=boundary,
42
+ backward_boundary=boundary,
43
+ dphi=float(dphi),
44
+ )
45
+
46
+
47
+ def load_fci_maps_netcdf(path: str | Path) -> FciMaps:
48
+ """Load a compact FCI map bundle from a NetCDF grid file."""
49
+
50
+ resolved = Path(path)
51
+ with Dataset(resolved) as dataset:
52
+ forward_x = _read_variable(dataset, ("forward_xt_prime", "forward_x", "forward_R_index"))
53
+ forward_z = _read_variable(dataset, ("forward_zt_prime", "forward_z", "forward_Z_index"))
54
+ backward_x = _read_variable(dataset, ("backward_xt_prime", "backward_x", "backward_R_index"))
55
+ backward_z = _read_variable(dataset, ("backward_zt_prime", "backward_z", "backward_Z_index"))
56
+ dphi = _infer_dphi(dataset, forward_x.shape[1])
57
+
58
+ forward_boundary = (forward_x < 0.0) | (forward_x > float(forward_x.shape[0] - 1))
59
+ backward_boundary = (backward_x < 0.0) | (backward_x > float(backward_x.shape[0] - 1))
60
+ return FciMaps(
61
+ forward_x=jnp.asarray(forward_x, dtype=jnp.float64),
62
+ forward_z=jnp.asarray(forward_z, dtype=jnp.float64),
63
+ backward_x=jnp.asarray(backward_x, dtype=jnp.float64),
64
+ backward_z=jnp.asarray(backward_z, dtype=jnp.float64),
65
+ forward_boundary=jnp.asarray(forward_boundary),
66
+ backward_boundary=jnp.asarray(backward_boundary),
67
+ dphi=float(dphi),
68
+ )
69
+
70
+
71
+ def _read_variable(dataset: Dataset, candidates: tuple[str, ...]) -> np.ndarray:
72
+ for name in candidates:
73
+ if name in dataset.variables:
74
+ return np.asarray(dataset.variables[name][:], dtype=np.float64)
75
+ raise KeyError(f"None of the FCI map variables were found: {', '.join(candidates)}")
76
+
77
+
78
+ def _infer_dphi(dataset: Dataset, ny: int) -> float:
79
+ if "dy" in dataset.variables:
80
+ dy = np.asarray(dataset.variables["dy"][:], dtype=np.float64)
81
+ if dy.size:
82
+ return float(np.nanmean(dy))
83
+ if "yperiod" in dataset.ncattrs():
84
+ return float(dataset.getncattr("yperiod")) / float(ny)
85
+ return float(2.0 * np.pi / max(ny, 1))
@@ -0,0 +1,291 @@
1
+ """Analytic island-divertor stellarator field (B8).
2
+
3
+ The rotating-ellipse embedding carrying an *island-divertor* magnetic field: the
4
+ rotational transform is sheared, ``iota(x)`` rising from axis to edge, and
5
+ resonant radial field perturbations ``B^x ~ eps sin(m theta - n zeta)`` open
6
+ magnetic island chains at the rational surfaces ``iota = n/m``. With two
7
+ overlapping chains the edge becomes stochastic: edge field lines wander
8
+ radially and strike the wall — genuinely **open** field lines whose endpoint
9
+ masks come from the field-line tracer, not from a hand-placed limiter — while
10
+ the core below the resonances stays closed. This is the analytic island-divertor
11
+ configuration of the B8 benchmark ladder rung.
12
+
13
+ The field components (contravariant, with ``J`` the metric Jacobian):
14
+
15
+ B^zeta = c / J
16
+ B^theta = iota(x) * c / J
17
+ B^x = (c / J) * sum_k eps_k * sin(m_k theta - n_k zeta)
18
+
19
+ so field lines obey ``dx/dzeta = sum_k eps_k sin(m_k theta - n_k zeta)`` and
20
+ ``dtheta/dzeta = iota(x)`` — the standard perturbed-twist-map form whose island
21
+ half-width at a resonance is ``sqrt(eps m / iota')``. The helper
22
+ :func:`island_divertor_field_line_rhs` exposes exactly that reduced system for
23
+ Poincare / connection-length studies, and the full :class:`FciGeometry3D` uses
24
+ the same components so the FCI tracer sees the same field.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+
31
+ import jax
32
+ import jax.numpy as jnp
33
+
34
+ from .embedding import metric_from_position_fn
35
+ from .fci_geometry import (
36
+ BFieldGeometry,
37
+ CellCenteredGrid3D,
38
+ FaceBFieldGeometry,
39
+ FaceMetricGeometry,
40
+ FciGeometry3D,
41
+ FciMaps3D,
42
+ Grid1D,
43
+ MetricGeometry,
44
+ Spacing3D,
45
+ _bmag_from_contravariant_components,
46
+ build_fci_maps_from_b_contravariant,
47
+ logical_grid_from_axis_vectors,
48
+ )
49
+ from .rotating_ellipse import rotating_ellipse_position
50
+
51
+ __all__ = [
52
+ "IslandDivertorField",
53
+ "build_island_divertor_geometry",
54
+ "island_divertor_connection_length",
55
+ "island_divertor_field_line_rhs",
56
+ ]
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class IslandDivertorField:
61
+ """The analytic island-divertor field parameters.
62
+
63
+ ``resonances`` is a tuple of ``(m, n, epsilon)`` triplets; each opens an
64
+ island chain at the surface where ``iota(x) = n / m``. ``iota`` varies
65
+ linearly from ``iota_axis`` at ``x_min`` to ``iota_edge`` at ``x_max``.
66
+ """
67
+
68
+ x_min: float = 0.2
69
+ x_max: float = 1.0
70
+ iota_axis: float = 0.55
71
+ iota_edge: float = 0.82
72
+ resonances: tuple[tuple[int, int, float], ...] = ((3, 2, 0.005), (4, 3, 0.005), (5, 4, 0.005))
73
+
74
+ def iota(self, x):
75
+ x_norm = (x - self.x_min) / (self.x_max - self.x_min)
76
+ return self.iota_axis + (self.iota_edge - self.iota_axis) * x_norm
77
+
78
+ def radial_perturbation(self, theta, zeta):
79
+ perturbation = jnp.zeros_like(jnp.asarray(theta, dtype=jnp.float64) + jnp.asarray(zeta))
80
+ for m, n, epsilon in self.resonances:
81
+ perturbation = perturbation + float(epsilon) * jnp.sin(m * theta - n * zeta)
82
+ return perturbation
83
+
84
+
85
+ def island_divertor_field_line_rhs(field: IslandDivertorField, x, theta, zeta):
86
+ """Field-line equations ``(dx/dzeta, dtheta/dzeta)`` of the reduced system."""
87
+
88
+ return field.radial_perturbation(theta, zeta), field.iota(x)
89
+
90
+
91
+ def island_divertor_connection_length(
92
+ field: IslandDivertorField,
93
+ x: jnp.ndarray,
94
+ theta: jnp.ndarray,
95
+ zeta: jnp.ndarray,
96
+ *,
97
+ direction: int = 1,
98
+ max_transits: int = 40,
99
+ steps_per_transit: int = 64,
100
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
101
+ """Trace field lines and return ``(connection_length_transits, is_open)``.
102
+
103
+ Integrates the reduced field-line system with RK4 from every starting point
104
+ (arrays broadcast together) for up to ``max_transits`` toroidal transits in
105
+ the given ``direction``. A line is **open** if it leaves the radial domain;
106
+ its connection length is the number of transits to the exit (open lines) or
107
+ ``max_transits`` (closed lines). Pure JAX (``vmap``/``scan``), so a full 3D
108
+ grid of starting points traces in one shot.
109
+ """
110
+
111
+ x0, theta0, zeta0 = jnp.broadcast_arrays(
112
+ jnp.asarray(x, dtype=jnp.float64),
113
+ jnp.asarray(theta, dtype=jnp.float64),
114
+ jnp.asarray(zeta, dtype=jnp.float64),
115
+ )
116
+ shape = x0.shape
117
+ dz = float(direction) * 2.0 * jnp.pi / float(steps_per_transit)
118
+
119
+ def rhs(x_value, theta_value, zeta_value):
120
+ return field.radial_perturbation(theta_value, zeta_value), field.iota(x_value)
121
+
122
+ def step(carry, _):
123
+ x_value, theta_value, zeta_value, alive, steps_alive = carry
124
+ k1x, k1t = rhs(x_value, theta_value, zeta_value)
125
+ k2x, k2t = rhs(x_value + 0.5 * dz * k1x, theta_value + 0.5 * dz * k1t, zeta_value + 0.5 * dz)
126
+ k3x, k3t = rhs(x_value + 0.5 * dz * k2x, theta_value + 0.5 * dz * k2t, zeta_value + 0.5 * dz)
127
+ k4x, k4t = rhs(x_value + dz * k3x, theta_value + dz * k3t, zeta_value + dz)
128
+ x_next = x_value + dz / 6.0 * (k1x + 2 * k2x + 2 * k3x + k4x)
129
+ theta_next = theta_value + dz / 6.0 * (k1t + 2 * k2t + 2 * k3t + k4t)
130
+ # Only the outer boundary is material (the divertor wall); the inner
131
+ # boundary is the artificial core cutoff, so reflect there.
132
+ x_next = jnp.maximum(x_next, field.x_min)
133
+ alive_next = alive & (x_next < field.x_max)
134
+ x_next = jnp.where(alive, x_next, x_value)
135
+ theta_next = jnp.where(alive, theta_next, theta_value)
136
+ return (x_next, theta_next, zeta_value + dz, alive_next, steps_alive + alive_next), None
137
+
138
+ initial = (
139
+ x0.ravel(),
140
+ theta0.ravel(),
141
+ zeta0.ravel(),
142
+ jnp.ones(x0.size, dtype=bool),
143
+ jnp.zeros(x0.size, dtype=jnp.int32),
144
+ )
145
+ (final_x, _, _, alive, steps_alive), _ = jax.lax.scan(
146
+ step, initial, None, length=int(max_transits) * int(steps_per_transit)
147
+ )
148
+ del final_x
149
+ connection_transits = steps_alive.astype(jnp.float64) / float(steps_per_transit)
150
+ return connection_transits.reshape(shape), (~alive).reshape(shape)
151
+
152
+
153
+ def build_island_divertor_geometry(
154
+ shape: tuple[int, int, int],
155
+ *,
156
+ field: IslandDivertorField = IslandDivertorField(),
157
+ r0: float = 3.0,
158
+ elongation: float = 0.35,
159
+ n_field_periods: int = 1,
160
+ c_phi: float = 3.0,
161
+ construct_fci_maps: bool = False,
162
+ map_substeps: int = 8,
163
+ open_field_line_masks: bool = False,
164
+ mask_max_transits: int = 30,
165
+ ) -> FciGeometry3D:
166
+ """Build the island-divertor geometry on the rotating-ellipse embedding.
167
+
168
+ With ``open_field_line_masks=True`` the endpoint masks come from
169
+ multi-transit field-line tracing (:func:`island_divertor_connection_length`):
170
+ a cell is an open endpoint if its field line leaves the radial domain within
171
+ ``mask_max_transits`` toroidal transits — the island-divertor scrape-off
172
+ layer *emerges from the field itself* (stochastic-edge lines are open with
173
+ finite connection length, core lines stay closed). ``construct_fci_maps``
174
+ additionally traces the one-cell FCI parallel maps for the operators.
175
+ """
176
+
177
+ nx, ny, nz = shape
178
+ x_faces = jnp.linspace(field.x_min, field.x_max, nx + 1, dtype=jnp.float64)
179
+ theta_faces = jnp.linspace(0.0, 2.0 * jnp.pi, ny + 1, dtype=jnp.float64)
180
+ zeta_faces = jnp.linspace(0.0, 2.0 * jnp.pi, nz + 1, dtype=jnp.float64)
181
+ grid = CellCenteredGrid3D(
182
+ x=Grid1D(centers=0.5 * (x_faces[:-1] + x_faces[1:]), faces=x_faces),
183
+ y=Grid1D(centers=0.5 * (theta_faces[:-1] + theta_faces[1:]), faces=theta_faces),
184
+ z=Grid1D(centers=0.5 * (zeta_faces[:-1] + zeta_faces[1:]), faces=zeta_faces),
185
+ )
186
+ target_shape = grid.shape
187
+
188
+ def _position(u: jax.Array) -> jax.Array:
189
+ return rotating_ellipse_position(
190
+ u[0], u[1], u[2], r0=r0, elongation=elongation, n_field_periods=n_field_periods
191
+ )
192
+
193
+ def _metric(logical_grid: jax.Array) -> MetricGeometry:
194
+ return metric_from_position_fn(_position, logical_grid)
195
+
196
+ def _bfield(logical_grid: jax.Array, metric: MetricGeometry) -> BFieldGeometry:
197
+ x = logical_grid[..., 0]
198
+ theta = logical_grid[..., 1]
199
+ zeta = logical_grid[..., 2]
200
+ toroidal = float(c_phi) / metric.J
201
+ B_contra = jnp.stack(
202
+ (
203
+ field.radial_perturbation(theta, zeta) * toroidal,
204
+ field.iota(x) * toroidal,
205
+ toroidal,
206
+ ),
207
+ axis=-1,
208
+ )
209
+ return BFieldGeometry(
210
+ B_contra=B_contra,
211
+ Bmag=_bmag_from_contravariant_components(B_contra, metric.g_cov),
212
+ )
213
+
214
+ def _logical(x_axis, y_axis, z_axis):
215
+ return logical_grid_from_axis_vectors(x_axis, y_axis, z_axis)
216
+
217
+ cell_logical = _logical(grid.x.centers, grid.y.centers, grid.z.centers)
218
+ cell_metric = _metric(cell_logical)
219
+ cell_bfield = _bfield(cell_logical, cell_metric)
220
+ face_metric = FaceMetricGeometry(
221
+ x=_metric(_logical(grid.x.faces, grid.y.centers, grid.z.centers)),
222
+ y=_metric(_logical(grid.x.centers, grid.y.faces, grid.z.centers)),
223
+ z=_metric(_logical(grid.x.centers, grid.y.centers, grid.z.faces)),
224
+ )
225
+ face_bfield = FaceBFieldGeometry(
226
+ x=_bfield(_logical(grid.x.faces, grid.y.centers, grid.z.centers), face_metric.x),
227
+ y=_bfield(_logical(grid.x.centers, grid.y.faces, grid.z.centers), face_metric.y),
228
+ z=_bfield(_logical(grid.x.centers, grid.y.centers, grid.z.faces), face_metric.z),
229
+ )
230
+
231
+ if construct_fci_maps:
232
+ map_fields = build_fci_maps_from_b_contravariant(
233
+ grid,
234
+ cell_bfield.B_contra,
235
+ cell_bfield.Bmag,
236
+ periodic_axes=(False, True, True),
237
+ substeps=int(map_substeps),
238
+ )
239
+ else:
240
+ ones = jnp.ones(target_shape, dtype=jnp.float64)
241
+ zeros = jnp.zeros(target_shape, dtype=jnp.float64)
242
+ map_fields = {
243
+ "forward_x": zeros,
244
+ "forward_y": zeros,
245
+ "backward_x": zeros,
246
+ "backward_y": zeros,
247
+ "forward_endpoint_x": zeros,
248
+ "forward_endpoint_y": zeros,
249
+ "forward_endpoint_z": zeros,
250
+ "backward_endpoint_x": zeros,
251
+ "backward_endpoint_y": zeros,
252
+ "backward_endpoint_z": zeros,
253
+ "forward_length": ones,
254
+ "backward_length": ones,
255
+ "forward_boundary": zeros.astype(bool),
256
+ "backward_boundary": zeros.astype(bool),
257
+ }
258
+
259
+ if open_field_line_masks:
260
+ forward_transits, forward_open = island_divertor_connection_length(
261
+ field, cell_logical[..., 0], cell_logical[..., 1], cell_logical[..., 2],
262
+ direction=+1, max_transits=mask_max_transits,
263
+ )
264
+ backward_transits, backward_open = island_divertor_connection_length(
265
+ field, cell_logical[..., 0], cell_logical[..., 1], cell_logical[..., 2],
266
+ direction=-1, max_transits=mask_max_transits,
267
+ )
268
+ del forward_transits, backward_transits
269
+ map_fields["forward_boundary"] = jnp.asarray(map_fields["forward_boundary"], dtype=bool) | forward_open
270
+ map_fields["backward_boundary"] = jnp.asarray(map_fields["backward_boundary"], dtype=bool) | backward_open
271
+
272
+ maps = FciMaps3D(**{name: map_fields[name] for name in (
273
+ "forward_x", "forward_y", "backward_x", "backward_y",
274
+ "forward_endpoint_x", "forward_endpoint_y", "forward_endpoint_z",
275
+ "backward_endpoint_x", "backward_endpoint_y", "backward_endpoint_z",
276
+ "forward_length", "backward_length", "forward_boundary", "backward_boundary",
277
+ )})
278
+ spacing = Spacing3D(
279
+ dx=jnp.broadcast_to(grid.x.widths[:, None, None], target_shape),
280
+ dy=jnp.broadcast_to(grid.y.widths[None, :, None], target_shape),
281
+ dz=jnp.broadcast_to(grid.z.widths[None, None, :], target_shape),
282
+ )
283
+ return FciGeometry3D(
284
+ grid=grid,
285
+ maps=maps,
286
+ spacing=spacing,
287
+ cell_metric=cell_metric,
288
+ face_metric=face_metric,
289
+ cell_bfield=cell_bfield,
290
+ face_bfield=face_bfield,
291
+ )
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import jax.numpy as jnp
6
+ import numpy as np
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MetricTensor3D:
11
+ """Complete 3D metric payload used by non-axisymmetric geometry kernels."""
12
+
13
+ dx: jnp.ndarray
14
+ dy: jnp.ndarray
15
+ dz: jnp.ndarray
16
+ J: jnp.ndarray
17
+ Bxy: jnp.ndarray
18
+ g11: jnp.ndarray
19
+ g22: jnp.ndarray
20
+ g33: jnp.ndarray
21
+ g12: jnp.ndarray
22
+ g13: jnp.ndarray
23
+ g23: jnp.ndarray
24
+ g_11: jnp.ndarray
25
+ g_22: jnp.ndarray
26
+ g_33: jnp.ndarray
27
+ g_12: jnp.ndarray
28
+ g_13: jnp.ndarray
29
+ g_23: jnp.ndarray
30
+
31
+ @property
32
+ def shape(self) -> tuple[int, int, int]:
33
+ return tuple(int(value) for value in self.J.shape)
34
+
35
+
36
+ def metric_inverse_residual(metric: MetricTensor3D) -> jnp.ndarray:
37
+ """Return `max(abs(g^ik g_kj - delta^i_j))` over the grid."""
38
+
39
+ contravariant = jnp.stack(
40
+ [
41
+ jnp.stack([metric.g11, metric.g12, metric.g13], axis=-1),
42
+ jnp.stack([metric.g12, metric.g22, metric.g23], axis=-1),
43
+ jnp.stack([metric.g13, metric.g23, metric.g33], axis=-1),
44
+ ],
45
+ axis=-2,
46
+ )
47
+ covariant = jnp.stack(
48
+ [
49
+ jnp.stack([metric.g_11, metric.g_12, metric.g_13], axis=-1),
50
+ jnp.stack([metric.g_12, metric.g_22, metric.g_23], axis=-1),
51
+ jnp.stack([metric.g_13, metric.g_23, metric.g_33], axis=-1),
52
+ ],
53
+ axis=-2,
54
+ )
55
+ product = jnp.einsum("...ik,...kj->...ij", contravariant, covariant)
56
+ identity = jnp.eye(3, dtype=product.dtype)
57
+ return jnp.max(jnp.abs(product - identity))
58
+
59
+
60
+ def build_metric_report(metric: MetricTensor3D) -> dict[str, object]:
61
+ """Build finite/positive/inverse-consistency diagnostics for a metric payload."""
62
+
63
+ report: dict[str, object] = {
64
+ "shape": list(metric.shape),
65
+ "inverse_residual_linf": float(metric_inverse_residual(metric)),
66
+ "fields": {},
67
+ }
68
+ for name in (
69
+ "J",
70
+ "Bxy",
71
+ "g11",
72
+ "g22",
73
+ "g33",
74
+ "g12",
75
+ "g13",
76
+ "g23",
77
+ "g_11",
78
+ "g_22",
79
+ "g_33",
80
+ "g_12",
81
+ "g_13",
82
+ "g_23",
83
+ ):
84
+ values = np.asarray(getattr(metric, name), dtype=np.float64)
85
+ report["fields"][name] = {
86
+ "finite": bool(np.all(np.isfinite(values))),
87
+ "minimum": float(np.nanmin(values)),
88
+ "maximum": float(np.nanmax(values)),
89
+ "mean": float(np.nanmean(values)),
90
+ "std": float(np.nanstd(values)),
91
+ }
92
+ report["passed"] = (
93
+ bool(report["fields"]["J"]["finite"])
94
+ and bool(report["fields"]["Bxy"]["finite"])
95
+ and float(report["fields"]["J"]["minimum"]) > 0.0
96
+ and float(report["fields"]["Bxy"]["minimum"]) > 0.0
97
+ and float(report["inverse_residual_linf"]) < 1.0e-8
98
+ )
99
+ return report
@@ -0,0 +1,150 @@
1
+ """Open-field-line slab SOL flux-tube geometry.
2
+
3
+ This module provides :func:`build_open_slab_geometry`, a Cartesian scrape-off-layer
4
+ (SOL) flux tube with a straight magnetic field along the parallel coordinate
5
+ ``z`` and **open** field lines: every field line terminates on a material target
6
+ plate at ``z = 0`` and ``z = L_parallel``. The endpoint masks
7
+ (``forward_boundary`` at the ``z = L`` target, ``backward_boundary`` at the
8
+ ``z = 0`` target) are exactly the open-field-line target masks the sheath /
9
+ recycling closure (:mod:`drbx.native.fci_sheath_recycling`) consumes.
10
+
11
+ The logical coordinates are ``(x, y, z)`` with ``x`` the radial SOL coordinate,
12
+ ``y`` the periodic binormal, and ``z`` the parallel (connection-length)
13
+ coordinate bounded by the two targets. The metric is Cartesian/identity — the
14
+ point of this geometry is the *open* parallel topology, not curvature — so the
15
+ FCI parallel gradient reduces to ``d/dz`` and the target plates carry the Bohm
16
+ sheath. It is the open-field-line counterpart to the closed flux tubes
17
+ (rotating ellipse, shifted torus) elsewhere in the package.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import jax.numpy as jnp
23
+
24
+ from .fci_geometry import (
25
+ BFieldGeometry,
26
+ CellCenteredGrid3D,
27
+ FaceBFieldGeometry,
28
+ FaceMetricGeometry,
29
+ FciGeometry3D,
30
+ FciMaps3D,
31
+ Grid1D,
32
+ MetricGeometry,
33
+ Spacing3D,
34
+ )
35
+
36
+ __all__ = ["build_open_slab_geometry"]
37
+
38
+
39
+ def _identity_metric(shape: tuple[int, int, int]) -> MetricGeometry:
40
+ ones = jnp.ones(shape, dtype=jnp.float64)
41
+ zeros = jnp.zeros(shape, dtype=jnp.float64)
42
+ return MetricGeometry(
43
+ J=ones,
44
+ g11=ones,
45
+ g22=ones,
46
+ g33=ones,
47
+ g12=zeros,
48
+ g13=zeros,
49
+ g23=zeros,
50
+ g_11=ones,
51
+ g_22=ones,
52
+ g_33=ones,
53
+ g_12=zeros,
54
+ g_13=zeros,
55
+ g_23=zeros,
56
+ )
57
+
58
+
59
+ def _parallel_bfield(shape: tuple[int, int, int]) -> BFieldGeometry:
60
+ return BFieldGeometry(
61
+ B_contra=jnp.broadcast_to(jnp.array([0.0, 0.0, 1.0], dtype=jnp.float64), shape + (3,)),
62
+ Bmag=jnp.ones(shape, dtype=jnp.float64),
63
+ )
64
+
65
+
66
+ def build_open_slab_geometry(
67
+ shape: tuple[int, int, int],
68
+ *,
69
+ radial_extent: float = 1.0,
70
+ binormal_extent: float = 1.0,
71
+ parallel_length: float = 40.0,
72
+ ) -> FciGeometry3D:
73
+ """Build an open-field-line slab SOL flux tube of the requested ``shape``.
74
+
75
+ Args:
76
+ shape: ``(nx, ny, nz)`` cell-centered grid. ``nz`` is the number of cells
77
+ along the field between the two targets; ``nx``/``ny`` are the radial
78
+ and binormal extents (use small values for a flux-tube study).
79
+ radial_extent, binormal_extent: physical sizes of the ``x`` and ``y``
80
+ axes.
81
+ parallel_length: connection length ``L_parallel`` between the ``z = 0``
82
+ and ``z = L`` target plates.
83
+
84
+ Returns:
85
+ A :class:`FciGeometry3D` whose field lines are open: ``forward_boundary``
86
+ is set on the ``z = L`` target plane and ``backward_boundary`` on the
87
+ ``z = 0`` target plane, so :func:`drbx.native.fci_sheath_recycling`.
88
+ `build_fci_target_masks` marks exactly the two target planes.
89
+ """
90
+
91
+ nx, ny, nz = shape
92
+ x_faces = jnp.linspace(0.0, float(radial_extent), nx + 1, dtype=jnp.float64)
93
+ y_faces = jnp.linspace(0.0, float(binormal_extent), ny + 1, dtype=jnp.float64)
94
+ z_faces = jnp.linspace(0.0, float(parallel_length), nz + 1, dtype=jnp.float64)
95
+ grid = CellCenteredGrid3D(
96
+ x=Grid1D(centers=0.5 * (x_faces[:-1] + x_faces[1:]), faces=x_faces),
97
+ y=Grid1D(centers=0.5 * (y_faces[:-1] + y_faces[1:]), faces=y_faces),
98
+ z=Grid1D(centers=0.5 * (z_faces[:-1] + z_faces[1:]), faces=z_faces),
99
+ )
100
+ target_shape = (nx, ny, nz)
101
+ zeros = jnp.zeros(target_shape, dtype=jnp.float64)
102
+ ones = jnp.ones(target_shape, dtype=jnp.float64)
103
+
104
+ cell_metric = _identity_metric(target_shape)
105
+ face_metric = FaceMetricGeometry(
106
+ x=_identity_metric((nx + 1, ny, nz)),
107
+ y=_identity_metric((nx, ny + 1, nz)),
108
+ z=_identity_metric((nx, ny, nz + 1)),
109
+ )
110
+ cell_bfield = _parallel_bfield(target_shape)
111
+ face_bfield = FaceBFieldGeometry(
112
+ x=_parallel_bfield((nx + 1, ny, nz)),
113
+ y=_parallel_bfield((nx, ny + 1, nz)),
114
+ z=_parallel_bfield((nx, ny, nz + 1)),
115
+ )
116
+
117
+ # Open field lines: the forward map exits the domain at the z = L target and
118
+ # the backward map exits at the z = 0 target. Interior planes stay closed.
119
+ forward_boundary = jnp.zeros(target_shape, dtype=bool).at[:, :, -1].set(True)
120
+ backward_boundary = jnp.zeros(target_shape, dtype=bool).at[:, :, 0].set(True)
121
+ maps = FciMaps3D(
122
+ forward_x=zeros,
123
+ forward_y=zeros,
124
+ backward_x=zeros,
125
+ backward_y=zeros,
126
+ forward_endpoint_x=zeros,
127
+ forward_endpoint_y=zeros,
128
+ forward_endpoint_z=zeros,
129
+ backward_endpoint_x=zeros,
130
+ backward_endpoint_y=zeros,
131
+ backward_endpoint_z=zeros,
132
+ forward_length=ones,
133
+ backward_length=ones,
134
+ forward_boundary=forward_boundary,
135
+ backward_boundary=backward_boundary,
136
+ )
137
+ spacing = Spacing3D(
138
+ dx=jnp.broadcast_to(grid.x.widths[:, None, None], target_shape),
139
+ dy=jnp.broadcast_to(grid.y.widths[None, :, None], target_shape),
140
+ dz=jnp.broadcast_to(grid.z.widths[None, None, :], target_shape),
141
+ )
142
+ return FciGeometry3D(
143
+ grid=grid,
144
+ maps=maps,
145
+ spacing=spacing,
146
+ cell_metric=cell_metric,
147
+ face_metric=face_metric,
148
+ cell_bfield=cell_bfield,
149
+ face_bfield=face_bfield,
150
+ )