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,253 @@
|
|
|
1
|
+
"""Rotating-ellipse non-axisymmetric FCI geometry constructor.
|
|
2
|
+
|
|
3
|
+
This module provides :func:`build_rotating_ellipse_geometry`, a self-contained
|
|
4
|
+
constructor for a genuinely non-axisymmetric flux-coordinate-independent (FCI)
|
|
5
|
+
geometry: a torus whose elliptical cross-section *rotates* as it is followed
|
|
6
|
+
around toroidally. This is the classical rotating-ellipse (``l = 2``)
|
|
7
|
+
configuration and the canonical minimal non-axisymmetric field for exercising
|
|
8
|
+
FCI parallel operators, because the flux surfaces genuinely change orientation
|
|
9
|
+
with the toroidal angle (the metric depends on all three logical coordinates).
|
|
10
|
+
|
|
11
|
+
The logical coordinates are ``(x, theta, zeta)`` with a minor-radius label ``x``
|
|
12
|
+
and periodic poloidal / toroidal angles ``theta`` / ``zeta``. The physical
|
|
13
|
+
embedding into Cartesian space is
|
|
14
|
+
|
|
15
|
+
p0 = (1 + delta) * x * cos(theta)
|
|
16
|
+
q0 = (1 - delta) * x * sin(theta)
|
|
17
|
+
lam = n_field_periods * zeta # ellipse orientation
|
|
18
|
+
p = cos(lam) * p0 - sin(lam) * q0
|
|
19
|
+
q = sin(lam) * p0 + cos(lam) * q0
|
|
20
|
+
R = r0 + p
|
|
21
|
+
X, Y, Z = R cos(zeta), R sin(zeta), q
|
|
22
|
+
|
|
23
|
+
so a surface ``x = const`` is an ellipse of elongation ``(1 + delta)/(1 - delta)``
|
|
24
|
+
whose major axis rotates ``n_field_periods`` times per toroidal turn. The metric
|
|
25
|
+
is obtained **by automatic differentiation** of this embedding
|
|
26
|
+
(``g_ij = d_i X . d_j X``) rather than by hand, which keeps the construction
|
|
27
|
+
exact and differentiable with respect to the shape parameters (elongation,
|
|
28
|
+
rotation) themselves.
|
|
29
|
+
|
|
30
|
+
The magnetic field is helical on the flux surfaces,
|
|
31
|
+
``B^x = 0``, ``B^theta = iota * c_phi / J``, ``B^zeta = c_phi / J``, so its field
|
|
32
|
+
lines wind with rotational transform ``iota`` and never leave a surface. Pass
|
|
33
|
+
``construct_fci_maps=True`` to trace the parallel FCI maps from this field.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import jax
|
|
39
|
+
import jax.numpy as jnp
|
|
40
|
+
|
|
41
|
+
from .embedding import metric_from_position_fn
|
|
42
|
+
from .fci_geometry import (
|
|
43
|
+
BFieldGeometry,
|
|
44
|
+
CellCenteredGrid3D,
|
|
45
|
+
FaceBFieldGeometry,
|
|
46
|
+
FaceMetricGeometry,
|
|
47
|
+
FciGeometry3D,
|
|
48
|
+
FciMaps3D,
|
|
49
|
+
Grid1D,
|
|
50
|
+
MetricGeometry,
|
|
51
|
+
Spacing3D,
|
|
52
|
+
_bmag_from_contravariant_components,
|
|
53
|
+
build_fci_maps_from_b_contravariant,
|
|
54
|
+
logical_grid_from_axis_vectors,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = ["build_rotating_ellipse_geometry", "rotating_ellipse_position"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rotating_ellipse_position(
|
|
61
|
+
x: jax.Array,
|
|
62
|
+
theta: jax.Array,
|
|
63
|
+
zeta: jax.Array,
|
|
64
|
+
*,
|
|
65
|
+
r0: float = 3.0,
|
|
66
|
+
elongation: float = 0.35,
|
|
67
|
+
n_field_periods: int = 1,
|
|
68
|
+
) -> jax.Array:
|
|
69
|
+
"""Cartesian ``(X, Y, Z)`` position of the rotating-ellipse embedding.
|
|
70
|
+
|
|
71
|
+
``elongation`` is the ellipse deformation ``delta`` (the semi-axes are
|
|
72
|
+
``(1 +/- delta) * x``); ``n_field_periods`` is how many times the ellipse
|
|
73
|
+
rotates per toroidal turn. Broadcasts over array-valued arguments and
|
|
74
|
+
returns the Cartesian components stacked on the last axis. ``r0`` and
|
|
75
|
+
``elongation`` are kept traceable (no ``float`` cast), so this map is
|
|
76
|
+
differentiable with respect to the shape parameters themselves.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
lam = float(n_field_periods) * zeta
|
|
80
|
+
cos_lam = jnp.cos(lam)
|
|
81
|
+
sin_lam = jnp.sin(lam)
|
|
82
|
+
p0 = (1.0 + elongation) * x * jnp.cos(theta)
|
|
83
|
+
q0 = (1.0 - elongation) * x * jnp.sin(theta)
|
|
84
|
+
p = cos_lam * p0 - sin_lam * q0
|
|
85
|
+
q = sin_lam * p0 + cos_lam * q0
|
|
86
|
+
major_radius = float(r0) + p
|
|
87
|
+
return jnp.stack(
|
|
88
|
+
(major_radius * jnp.cos(zeta), major_radius * jnp.sin(zeta), q),
|
|
89
|
+
axis=-1,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_rotating_ellipse_geometry(
|
|
94
|
+
shape: tuple[int, int, int],
|
|
95
|
+
*,
|
|
96
|
+
r0: float = 3.0,
|
|
97
|
+
x_min: float = 0.2,
|
|
98
|
+
x_max: float = 1.0,
|
|
99
|
+
elongation: float = 0.35,
|
|
100
|
+
n_field_periods: int = 1,
|
|
101
|
+
iota: float = 0.9,
|
|
102
|
+
c_phi: float = 3.0,
|
|
103
|
+
construct_fci_maps: bool = False,
|
|
104
|
+
map_substeps: int = 8,
|
|
105
|
+
limiter_radius: float | None = None,
|
|
106
|
+
) -> FciGeometry3D:
|
|
107
|
+
"""Build a rotating-ellipse non-axisymmetric FCI geometry.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
shape: ``(nx, ny, nz)`` cell-centered grid resolution.
|
|
111
|
+
r0: major-radius offset of the torus.
|
|
112
|
+
x_min, x_max: minor-radius (``x``) label bounds.
|
|
113
|
+
elongation: ellipse deformation ``delta`` in ``[0, 1)``; ``0`` recovers
|
|
114
|
+
a circular (still non-axisymmetric only if rotated) cross-section,
|
|
115
|
+
larger values elongate it. The cross-section aspect ratio is
|
|
116
|
+
``(1 + delta) / (1 - delta)``.
|
|
117
|
+
n_field_periods: ellipse rotations per toroidal turn (the field
|
|
118
|
+
periodicity). Must be a positive integer for a ``zeta``-periodic
|
|
119
|
+
geometry.
|
|
120
|
+
iota: rotational transform of the helical field.
|
|
121
|
+
c_phi: toroidal-field scaling constant.
|
|
122
|
+
construct_fci_maps: if ``True``, trace the parallel FCI maps from the
|
|
123
|
+
contravariant field; otherwise install identity/placeholder maps.
|
|
124
|
+
map_substeps: field-line-tracer substeps per toroidal cell when
|
|
125
|
+
``construct_fci_maps`` is ``True``.
|
|
126
|
+
limiter_radius: if set, a toroidal limiter at the ``zeta = 0`` plane
|
|
127
|
+
opens every field line with ``x > limiter_radius``: those cells'
|
|
128
|
+
forward maps exit on the last toroidal plane and their backward maps
|
|
129
|
+
on the first, so the endpoint masks mark a scrape-off layer while
|
|
130
|
+
``x <= limiter_radius`` remains a closed core. ``None`` (default)
|
|
131
|
+
keeps every field line closed.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A fully populated :class:`FciGeometry3D` of the requested ``shape``.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
nx, ny, nz = shape
|
|
138
|
+
x_faces = jnp.linspace(float(x_min), float(x_max), nx + 1, dtype=jnp.float64)
|
|
139
|
+
theta_faces = jnp.linspace(0.0, 2.0 * jnp.pi, ny + 1, dtype=jnp.float64)
|
|
140
|
+
zeta_faces = jnp.linspace(0.0, 2.0 * jnp.pi, nz + 1, dtype=jnp.float64)
|
|
141
|
+
grid = CellCenteredGrid3D(
|
|
142
|
+
x=Grid1D(centers=0.5 * (x_faces[:-1] + x_faces[1:]), faces=x_faces),
|
|
143
|
+
y=Grid1D(centers=0.5 * (theta_faces[:-1] + theta_faces[1:]), faces=theta_faces),
|
|
144
|
+
z=Grid1D(centers=0.5 * (zeta_faces[:-1] + zeta_faces[1:]), faces=zeta_faces),
|
|
145
|
+
)
|
|
146
|
+
target_shape = grid.shape
|
|
147
|
+
|
|
148
|
+
def _position(u: jax.Array) -> jax.Array:
|
|
149
|
+
return rotating_ellipse_position(
|
|
150
|
+
u[0], u[1], u[2],
|
|
151
|
+
r0=r0, elongation=elongation, n_field_periods=n_field_periods,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _metric(logical_grid: jax.Array) -> MetricGeometry:
|
|
155
|
+
return metric_from_position_fn(_position, logical_grid)
|
|
156
|
+
|
|
157
|
+
def _bfield(metric: MetricGeometry) -> BFieldGeometry:
|
|
158
|
+
jacobian = metric.J
|
|
159
|
+
B_contra = jnp.stack(
|
|
160
|
+
(
|
|
161
|
+
jnp.zeros_like(jacobian),
|
|
162
|
+
float(iota) * float(c_phi) / jacobian,
|
|
163
|
+
float(c_phi) / jacobian,
|
|
164
|
+
),
|
|
165
|
+
axis=-1,
|
|
166
|
+
)
|
|
167
|
+
Bmag = _bmag_from_contravariant_components(B_contra, metric.g_cov)
|
|
168
|
+
return BFieldGeometry(B_contra=B_contra, Bmag=Bmag)
|
|
169
|
+
|
|
170
|
+
def _logical(x_axis: jax.Array, y_axis: jax.Array, z_axis: jax.Array) -> jax.Array:
|
|
171
|
+
return logical_grid_from_axis_vectors(x_axis, y_axis, z_axis)
|
|
172
|
+
|
|
173
|
+
cell_metric = _metric(_logical(grid.x.centers, grid.y.centers, grid.z.centers))
|
|
174
|
+
cell_bfield = _bfield(cell_metric)
|
|
175
|
+
face_metric = FaceMetricGeometry(
|
|
176
|
+
x=_metric(_logical(grid.x.faces, grid.y.centers, grid.z.centers)),
|
|
177
|
+
y=_metric(_logical(grid.x.centers, grid.y.faces, grid.z.centers)),
|
|
178
|
+
z=_metric(_logical(grid.x.centers, grid.y.centers, grid.z.faces)),
|
|
179
|
+
)
|
|
180
|
+
face_bfield = FaceBFieldGeometry(
|
|
181
|
+
x=_bfield(face_metric.x),
|
|
182
|
+
y=_bfield(face_metric.y),
|
|
183
|
+
z=_bfield(face_metric.z),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if construct_fci_maps:
|
|
187
|
+
map_fields = build_fci_maps_from_b_contravariant(
|
|
188
|
+
grid,
|
|
189
|
+
cell_bfield.B_contra,
|
|
190
|
+
cell_bfield.Bmag,
|
|
191
|
+
periodic_axes=(False, True, True),
|
|
192
|
+
substeps=int(map_substeps),
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
ones = jnp.ones(target_shape, dtype=jnp.float64)
|
|
196
|
+
zeros = jnp.zeros(target_shape, dtype=jnp.float64)
|
|
197
|
+
map_fields = {
|
|
198
|
+
"forward_x": zeros,
|
|
199
|
+
"forward_y": zeros,
|
|
200
|
+
"backward_x": zeros,
|
|
201
|
+
"backward_y": zeros,
|
|
202
|
+
"forward_endpoint_x": zeros,
|
|
203
|
+
"forward_endpoint_y": zeros,
|
|
204
|
+
"forward_endpoint_z": zeros,
|
|
205
|
+
"backward_endpoint_x": zeros,
|
|
206
|
+
"backward_endpoint_y": zeros,
|
|
207
|
+
"backward_endpoint_z": zeros,
|
|
208
|
+
"forward_length": ones,
|
|
209
|
+
"backward_length": ones,
|
|
210
|
+
"forward_boundary": zeros.astype(bool),
|
|
211
|
+
"backward_boundary": zeros.astype(bool),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if limiter_radius is not None:
|
|
215
|
+
# Toroidal limiter at the zeta = 0 plane: scrape-off-layer field lines
|
|
216
|
+
# (x > limiter_radius) strike it — forward traces exit on the last
|
|
217
|
+
# toroidal plane, backward traces on the first. The core stays closed.
|
|
218
|
+
sol_mask = grid.x.centers > float(limiter_radius)
|
|
219
|
+
forward_boundary = jnp.asarray(map_fields["forward_boundary"], dtype=bool)
|
|
220
|
+
backward_boundary = jnp.asarray(map_fields["backward_boundary"], dtype=bool)
|
|
221
|
+
map_fields["forward_boundary"] = forward_boundary.at[:, :, -1].set(forward_boundary[:, :, -1] | sol_mask[:, None])
|
|
222
|
+
map_fields["backward_boundary"] = backward_boundary.at[:, :, 0].set(backward_boundary[:, :, 0] | sol_mask[:, None])
|
|
223
|
+
|
|
224
|
+
maps = FciMaps3D(
|
|
225
|
+
forward_x=map_fields["forward_x"],
|
|
226
|
+
forward_y=map_fields["forward_y"],
|
|
227
|
+
backward_x=map_fields["backward_x"],
|
|
228
|
+
backward_y=map_fields["backward_y"],
|
|
229
|
+
forward_endpoint_x=map_fields["forward_endpoint_x"],
|
|
230
|
+
forward_endpoint_y=map_fields["forward_endpoint_y"],
|
|
231
|
+
forward_endpoint_z=map_fields["forward_endpoint_z"],
|
|
232
|
+
backward_endpoint_x=map_fields["backward_endpoint_x"],
|
|
233
|
+
backward_endpoint_y=map_fields["backward_endpoint_y"],
|
|
234
|
+
backward_endpoint_z=map_fields["backward_endpoint_z"],
|
|
235
|
+
forward_length=map_fields["forward_length"],
|
|
236
|
+
backward_length=map_fields["backward_length"],
|
|
237
|
+
forward_boundary=map_fields["forward_boundary"],
|
|
238
|
+
backward_boundary=map_fields["backward_boundary"],
|
|
239
|
+
)
|
|
240
|
+
spacing = Spacing3D(
|
|
241
|
+
dx=jnp.broadcast_to(grid.x.widths[:, None, None], target_shape),
|
|
242
|
+
dy=jnp.broadcast_to(grid.y.widths[None, :, None], target_shape),
|
|
243
|
+
dz=jnp.broadcast_to(grid.z.widths[None, None, :], target_shape),
|
|
244
|
+
)
|
|
245
|
+
return FciGeometry3D(
|
|
246
|
+
grid=grid,
|
|
247
|
+
maps=maps,
|
|
248
|
+
spacing=spacing,
|
|
249
|
+
cell_metric=cell_metric,
|
|
250
|
+
face_metric=face_metric,
|
|
251
|
+
cell_bfield=cell_bfield,
|
|
252
|
+
face_bfield=face_bfield,
|
|
253
|
+
)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Shifted-torus non-axisymmetric FCI geometry constructor.
|
|
2
|
+
|
|
3
|
+
This module provides :func:`build_shifted_torus_geometry`, a self-contained
|
|
4
|
+
constructor for a non-axisymmetric flux-coordinate-independent (FCI) metric on a
|
|
5
|
+
shifted-torus flux tube. The logical coordinates are ``(x, theta, zeta)`` with a
|
|
6
|
+
physical radial coordinate ``x`` and periodic poloidal/toroidal angles. The
|
|
7
|
+
poloidal angle is sheared by a radial shift ``Theta = theta + sigma * (x - x_mid)``
|
|
8
|
+
so that, for ``sigma != 0``, the metric acquires genuine off-diagonal (``g12`` /
|
|
9
|
+
``g_12``) cross terms and the geometry is non-axisymmetric.
|
|
10
|
+
|
|
11
|
+
The construction was verified as a manufactured-solution scaffold in
|
|
12
|
+
``tests/test_mms_shifted_torus_2_field.py`` (the two-field FCI MMS convergence
|
|
13
|
+
harness). It is promoted here so the reduced drift-reduced FCI model can be
|
|
14
|
+
exercised and differentiated on non-axisymmetric geometry from the package
|
|
15
|
+
itself.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import jax.numpy as jnp
|
|
21
|
+
|
|
22
|
+
from .fci_geometry import (
|
|
23
|
+
BFieldGeometry,
|
|
24
|
+
CellCenteredGrid3D,
|
|
25
|
+
FaceBFieldGeometry,
|
|
26
|
+
FaceMetricGeometry,
|
|
27
|
+
FciGeometry3D,
|
|
28
|
+
FciMaps3D,
|
|
29
|
+
Grid1D,
|
|
30
|
+
MetricGeometry,
|
|
31
|
+
Spacing3D,
|
|
32
|
+
build_fci_maps_from_b_contravariant,
|
|
33
|
+
logical_grid_from_axis_vectors,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ["build_shifted_torus_geometry"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_shifted_torus_geometry(
|
|
40
|
+
shape: tuple[int, int, int],
|
|
41
|
+
*,
|
|
42
|
+
x_min: float = 0.15,
|
|
43
|
+
x_max: float = 1.0,
|
|
44
|
+
r0: float = 3.0,
|
|
45
|
+
alpha_value: float = 0.25,
|
|
46
|
+
iota: float = 1.1,
|
|
47
|
+
c_phi: float = 3.0,
|
|
48
|
+
sigma: float = 0.0,
|
|
49
|
+
construct_fci_maps: bool = False,
|
|
50
|
+
B_contravariant: jnp.ndarray | None = None,
|
|
51
|
+
) -> FciGeometry3D:
|
|
52
|
+
"""Build a shifted-torus non-axisymmetric FCI geometry.
|
|
53
|
+
|
|
54
|
+
The logical coordinates are ``(x, theta, zeta)`` with periodic ``theta`` and
|
|
55
|
+
``zeta``. The helper follows the same `FciGeometry3D` construction pattern used
|
|
56
|
+
in `test_fci_operators.py`, but uses the physical radial coordinate directly and
|
|
57
|
+
a shifted poloidal angle ``Theta = theta + sigma * (x - x_mid)``. A nonzero
|
|
58
|
+
``sigma`` makes the metric non-axisymmetric (it introduces the off-diagonal
|
|
59
|
+
``g12`` / ``g_12`` terms below).
|
|
60
|
+
|
|
61
|
+
Parameters mirror the module-level constants used by the two-field MMS
|
|
62
|
+
convergence harness; the defaults reproduce that verified geometry.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
shape: ``(nx, ny, nz)`` cell-centered grid resolution.
|
|
66
|
+
x_min, x_max: physical radial coordinate bounds.
|
|
67
|
+
r0: major-radius offset of the shifted torus.
|
|
68
|
+
alpha_value: Shafranov-like shift amplitude.
|
|
69
|
+
iota: rotational transform used to build the contravariant field.
|
|
70
|
+
c_phi: toroidal field scaling constant.
|
|
71
|
+
sigma: poloidal shear; ``sigma != 0`` yields a non-axisymmetric metric.
|
|
72
|
+
construct_fci_maps: if ``True``, trace the parallel FCI maps from the
|
|
73
|
+
contravariant field; otherwise install identity/placeholder maps.
|
|
74
|
+
B_contravariant: optional override for the contravariant field
|
|
75
|
+
components; when ``None`` the analytic field is used.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A fully populated :class:`FciGeometry3D` of the requested ``shape``.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
nx, ny, nz = shape
|
|
82
|
+
x_centers = jnp.linspace(float(x_min), float(x_max), nx, dtype=jnp.float64)
|
|
83
|
+
theta_centers = jnp.linspace(0.0, 2.0 * jnp.pi, ny, endpoint=False, dtype=jnp.float64)
|
|
84
|
+
zeta_centers = jnp.linspace(0.0, 2.0 * jnp.pi, nz, endpoint=False, dtype=jnp.float64)
|
|
85
|
+
grid = CellCenteredGrid3D(
|
|
86
|
+
x=Grid1D.from_centers(x_centers),
|
|
87
|
+
y=Grid1D.from_centers(theta_centers),
|
|
88
|
+
z=Grid1D.from_centers(zeta_centers),
|
|
89
|
+
)
|
|
90
|
+
target_shape = grid.shape
|
|
91
|
+
|
|
92
|
+
def _logical_grid(x_axis: jnp.ndarray, y_axis: jnp.ndarray, z_axis: jnp.ndarray) -> jnp.ndarray:
|
|
93
|
+
return logical_grid_from_axis_vectors(x_axis, y_axis, z_axis)
|
|
94
|
+
|
|
95
|
+
def _metric(logical_grid: jnp.ndarray) -> MetricGeometry:
|
|
96
|
+
x = logical_grid[..., 0]
|
|
97
|
+
theta = logical_grid[..., 1]
|
|
98
|
+
x_mid = 0.5 * (float(x_min) + float(x_max))
|
|
99
|
+
theta_shift = theta + float(sigma) * (x - x_mid)
|
|
100
|
+
cos_theta = jnp.cos(theta_shift)
|
|
101
|
+
sin_theta = jnp.sin(theta_shift)
|
|
102
|
+
R = float(r0) + float(alpha_value) * x + x * cos_theta
|
|
103
|
+
jacobian = R * x * (1.0 + float(alpha_value) * cos_theta)
|
|
104
|
+
jacobian = jnp.where(jnp.abs(jacobian) < 1.0e-14, 1.0e-14, jacobian)
|
|
105
|
+
g11 = 1.0 / (1.0 + float(alpha_value) * cos_theta) ** 2
|
|
106
|
+
g12 = float(alpha_value) * sin_theta / (x * (1.0 + float(alpha_value) * cos_theta) ** 2)
|
|
107
|
+
g13 = jnp.zeros_like(x)
|
|
108
|
+
g22 = (1.0 + 2.0 * float(alpha_value) * cos_theta + float(alpha_value) ** 2) / (x**2 * (1.0 + float(alpha_value) * cos_theta) ** 2)
|
|
109
|
+
g23 = jnp.zeros_like(x)
|
|
110
|
+
g33 = 1.0 / (R**2)
|
|
111
|
+
g_11 = 1.0 + 2.0 * float(alpha_value) * cos_theta + float(alpha_value) ** 2
|
|
112
|
+
g_12 = -float(alpha_value) * x * sin_theta
|
|
113
|
+
g_13 = jnp.zeros_like(x)
|
|
114
|
+
g_22 = x**2
|
|
115
|
+
g_23 = jnp.zeros_like(x)
|
|
116
|
+
g_33 = R**2
|
|
117
|
+
return MetricGeometry(
|
|
118
|
+
J=jacobian,
|
|
119
|
+
g11=g11,
|
|
120
|
+
g22=g22,
|
|
121
|
+
g33=g33,
|
|
122
|
+
g12=g12,
|
|
123
|
+
g13=g13,
|
|
124
|
+
g23=g23,
|
|
125
|
+
g_11=g_11,
|
|
126
|
+
g_22=g_22,
|
|
127
|
+
g_33=g_33,
|
|
128
|
+
g_12=g_12,
|
|
129
|
+
g_13=g_13,
|
|
130
|
+
g_23=g_23,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _bfield(logical_grid: jnp.ndarray, metric: MetricGeometry) -> BFieldGeometry:
|
|
134
|
+
x = logical_grid[..., 0]
|
|
135
|
+
theta = logical_grid[..., 1]
|
|
136
|
+
x_mid = 0.5 * (float(x_min) + float(x_max))
|
|
137
|
+
theta_shift = theta + float(sigma) * (x - x_mid)
|
|
138
|
+
cos_theta = jnp.cos(theta_shift)
|
|
139
|
+
R = float(r0) + float(alpha_value) * x + x * cos_theta
|
|
140
|
+
jacobian = metric.J
|
|
141
|
+
if B_contravariant is None:
|
|
142
|
+
B_contra = jnp.stack(
|
|
143
|
+
(
|
|
144
|
+
jnp.zeros_like(jacobian),
|
|
145
|
+
float(iota) * float(c_phi) / jacobian,
|
|
146
|
+
float(c_phi) / jacobian,
|
|
147
|
+
),
|
|
148
|
+
axis=-1,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
B_contra = jnp.asarray(B_contravariant, dtype=jnp.float64)
|
|
152
|
+
Bmag = jnp.sqrt((float(iota) ** 2) * x**2 + R**2) * float(c_phi) / jacobian
|
|
153
|
+
return BFieldGeometry(B_contra=B_contra, Bmag=Bmag)
|
|
154
|
+
|
|
155
|
+
cell_logical_grid = _logical_grid(grid.x.centers, grid.y.centers, grid.z.centers)
|
|
156
|
+
cell_metric = _metric(cell_logical_grid)
|
|
157
|
+
cell_bfield = _bfield(cell_logical_grid, cell_metric)
|
|
158
|
+
face_metric = FaceMetricGeometry(
|
|
159
|
+
x=_metric(_logical_grid(grid.x.faces, grid.y.centers, grid.z.centers)),
|
|
160
|
+
y=_metric(_logical_grid(grid.x.centers, grid.y.faces, grid.z.centers)),
|
|
161
|
+
z=_metric(_logical_grid(grid.x.centers, grid.y.centers, grid.z.faces)),
|
|
162
|
+
)
|
|
163
|
+
face_bfield = FaceBFieldGeometry(
|
|
164
|
+
x=_bfield(_logical_grid(grid.x.faces, grid.y.centers, grid.z.centers), face_metric.x),
|
|
165
|
+
y=_bfield(_logical_grid(grid.x.centers, grid.y.faces, grid.z.centers), face_metric.y),
|
|
166
|
+
z=_bfield(_logical_grid(grid.x.centers, grid.y.centers, grid.z.faces), face_metric.z),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if construct_fci_maps:
|
|
170
|
+
map_fields = build_fci_maps_from_b_contravariant(
|
|
171
|
+
grid,
|
|
172
|
+
cell_bfield.B_contra,
|
|
173
|
+
cell_bfield.Bmag,
|
|
174
|
+
periodic_axes=(False, True, True),
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
ones = jnp.ones(target_shape, dtype=jnp.float64)
|
|
178
|
+
zeros = jnp.zeros(target_shape, dtype=jnp.float64)
|
|
179
|
+
map_fields = {
|
|
180
|
+
"forward_x": zeros,
|
|
181
|
+
"forward_y": zeros,
|
|
182
|
+
"backward_x": zeros,
|
|
183
|
+
"backward_y": zeros,
|
|
184
|
+
"forward_endpoint_x": zeros,
|
|
185
|
+
"forward_endpoint_y": zeros,
|
|
186
|
+
"forward_endpoint_z": zeros,
|
|
187
|
+
"backward_endpoint_x": zeros,
|
|
188
|
+
"backward_endpoint_y": zeros,
|
|
189
|
+
"backward_endpoint_z": zeros,
|
|
190
|
+
"forward_length": ones,
|
|
191
|
+
"backward_length": ones,
|
|
192
|
+
"forward_boundary": zeros.astype(bool),
|
|
193
|
+
"backward_boundary": zeros.astype(bool),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
maps = FciMaps3D(
|
|
197
|
+
forward_x=map_fields["forward_x"],
|
|
198
|
+
forward_y=map_fields["forward_y"],
|
|
199
|
+
backward_x=map_fields["backward_x"],
|
|
200
|
+
backward_y=map_fields["backward_y"],
|
|
201
|
+
forward_endpoint_x=map_fields["forward_endpoint_x"],
|
|
202
|
+
forward_endpoint_y=map_fields["forward_endpoint_y"],
|
|
203
|
+
forward_endpoint_z=map_fields["forward_endpoint_z"],
|
|
204
|
+
backward_endpoint_x=map_fields["backward_endpoint_x"],
|
|
205
|
+
backward_endpoint_y=map_fields["backward_endpoint_y"],
|
|
206
|
+
backward_endpoint_z=map_fields["backward_endpoint_z"],
|
|
207
|
+
forward_length=map_fields["forward_length"],
|
|
208
|
+
backward_length=map_fields["backward_length"],
|
|
209
|
+
forward_boundary=map_fields["forward_boundary"],
|
|
210
|
+
backward_boundary=map_fields["backward_boundary"],
|
|
211
|
+
)
|
|
212
|
+
spacing = Spacing3D(
|
|
213
|
+
dx=jnp.broadcast_to(grid.x.widths[:, None, None], target_shape),
|
|
214
|
+
dy=jnp.broadcast_to(grid.y.widths[None, :, None], target_shape),
|
|
215
|
+
dz=jnp.broadcast_to(grid.z.widths[None, None, :], target_shape),
|
|
216
|
+
)
|
|
217
|
+
return FciGeometry3D(
|
|
218
|
+
grid=grid,
|
|
219
|
+
maps=maps,
|
|
220
|
+
spacing=spacing,
|
|
221
|
+
cell_metric=cell_metric,
|
|
222
|
+
face_metric=face_metric,
|
|
223
|
+
cell_bfield=cell_bfield,
|
|
224
|
+
face_bfield=face_bfield,
|
|
225
|
+
)
|