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