1.4.0
a flexible 3D MHD equilibrium solver
\[ \newcommand{\deriv}{\mathrm{d}} \newcommand{\pqdx}[2]{\frac{\partial #1}{\partial #2}} \newcommand{\dqdx}[2]{\frac{\deriv #1}{\deriv #2}} \newcommand{\pqdxx}[2]{\frac{\partial^2 #1}{\partial {#2}^2}} \newcommand{\dqdxx}[2]{\frac{\deriv^2 #1}{\deriv {#2}^2}} \newcommand{\pqdxy}[3]{\frac{\partial^2 #1}{\partial #2 \partial #3}} \newcommand{\grad}{\mathbf{\nabla}} \newcommand{\vec}[1]{\mathbf{#1}} \]
Follow along
gitlab.mpcdf.mpg.de/gvec-group/workshop-material → Launch Binder → slides/slides.ipynb
Goal: compute 3D ideal magnetohydrodynamic equilibria
Minimize MHD energy \[ W = \int_\Omega \frac{|\mathbf{B}|^2}{2\mu_0} + \frac{p}{\gamma - 1} \, dV \]
Inspired by VMEC & DESC
Solve for the geometry of the magnetic field
DOI:10.5281/zenodo.15026780 / JOSS paper under review
CMake & scikit-build-core)gvec binary, pygvec scripts & gvec python packageFor a fixed-boundary ideal MHD equilibrium, we need to specify:
Choose cylindrical coordinates:
\[ (x,y,z) := (R\cos\zeta, -R\sin\zeta, Z)\\ R = X^1(\rho,\vartheta,\zeta) \\ Z = X^2(\rho,\vartheta,\zeta) \]
Boundary represented with double-angle Fourier series:
\[ X^i_b(\vartheta,\zeta) = \sum_{m,n} X^i_{\text{b,cos},m,n} \cos(m\vartheta - n N_{FP} \zeta) + X^i_{\text{b,sin},m,n} \sin(m\vartheta - n N_{FP} \zeta) \]
Axisymmetric boundary with elliptical cross-section:
\[ \begin{align} R := X^1_b(\vartheta,\zeta) &= 5.0 + 0.9 \cos\vartheta \\ Z := X^2_b(\vartheta,\zeta) &= 1.1 \sin\vartheta \end{align} \]
GVEC - completed 0/2 stages: |>|.|GVEC - completed 1/2 stages: |=|>|GVEC finished after 0.9 seconds using 2479 iterations (totalIter = 10000) with |force| = 9.97e-07 (minimize_tol = 1.00e-06)
Boundary represented with double-angle Fourier series:
\[ X^i_b(\vartheta,\zeta) = \sum_{m,n} X^i_{\text{b,cos},m,n} \cos(m\vartheta - n N_{FP} \zeta) + X^i_{\text{b,sin},m,n} \sin(m\vartheta - n N_{FP} \zeta) \]
Rotating ellipse with three field periods:
\[ \begin{align} R := X^1_b(\vartheta,\zeta) &= 3.0 + 1.0 \cos\vartheta + 0.4 \cos(\vartheta-3\zeta) \\ Z := X^2_b(\vartheta,\zeta) &= 1.0 \sin\vartheta - 0.4 \sin(\vartheta-3\zeta) - 0.25 \sin(-3\zeta) \end{align} \]
Boundary represented with double-angle Fourier series:
\[ X^i_b(\vartheta,\zeta) = \sum_{m,n} X^i_{\text{b,cos},m,n} \cos(m\vartheta - n N_{FP} \zeta) + X^i_{\text{b,sin},m,n} \sin(m\vartheta - n N_{FP} \zeta) \]
Rotating ellipse with three field periods:
\[ \begin{align} R := X^1_b(\vartheta,\zeta) &= 3.0 + 1.0 \cos\vartheta + 0.4 \cos(\vartheta-3\zeta) \\ Z := X^2_b(\vartheta,\zeta) &= 1.0 \sin\vartheta - 0.4 \sin(\vartheta-3\zeta) - 0.25 \sin(-3\zeta) \end{align} \]


<xarray.Dataset> Size: 1MB
Dimensions: (pol: 20, tor: 50, rad: 2, xyz: 3)
Coordinates:
* theta_B (pol) float64 160B 0.0 0.3142 0.6283 ... 5.341 5.655 5.969
* zeta_B (tor) float64 400B 0.0 0.04189 0.08378 ... 1.969 2.011 2.053
* rho (rad) float64 16B 0.5 1.0
* xyz (xyz) <U1 12B 'x' 'y' 'z'
Dimensions without coordinates: pol, tor, rad
Data variables: (12/55)
theta (rad, pol, tor) float64 16kB 0.0 0.000628 ... 5.797 5.805
zeta (rad, pol, tor) float64 16kB 0.0 0.04117 0.08245 ... 2.041 2.082
LA (rad, pol, tor) float64 16kB 0.0 -0.0008736 ... 0.1855 0.177
dLA_dt (rad, pol, tor) float64 16kB -0.2849 -0.285 ... -0.3648 -0.3673
dLA_dz (rad, pol, tor) float64 16kB -0.01705 -0.01651 ... -0.1325
dLA_dtt (rad, pol, tor) float64 16kB 0.0 -0.004771 ... -0.01721 -0.01356
... ...
e_theta (xyz, rad, pol, tor) float64 48kB 0.0 0.01843 ... 0.5053 0.5261
e_zeta (xyz, rad, pol, tor) float64 48kB 0.0 -0.2213 ... 1.877 1.834
B_contra_t (rad, pol, tor) float64 16kB 0.03825 0.03823 ... 0.04883 0.04903
B_contra_z (rad, pol, tor) float64 16kB 0.0758 0.07586 ... 0.05405 0.05497
B (rad, pol, tor, xyz) float64 48kB 0.0 -0.2846 ... 0.1119 0.1266
mod_B (rad, pol, tor) float64 16kB 0.3048 0.3047 ... 0.2628 0.2657


parameters = dict(
ProjectName="gframe",
which_hmap=21, # G-Frame
hmap_ncfile="../input_surface-Gframe.nc",
getBoundaryFromFile=1,
boundary_filename="../input_surface-Gframe.nc",
X1_mn_max=[3, 4],
X2_mn_max=[3, 4],
LA_mn_max=[3, 4],
X1_sin_cos="_sincos_",
X2_sin_cos="_sincos_",
LA_sin_cos="_sincos_",
X1X2_deg=5,
LA_deg=5,
sgrid=dict(
nElems=5,
),
pres=dict(
type="polynomial",
coefs=[0.0],
),
I_tor=dict(
type="polynomial",
coefs=[0.0],
),
picard_current="auto",
totalIter=10000,
minimize_tol=1e-3,
)GVEC - completed 0/3 stages, restarts in current stage - 0: |>|.|.|GVEC - completed 1/3 stages, restarts in current stage - 0: |=|>|.|GVEC - completed 2/3 stages, restarts in current stage - 0: |=|=|>|GVEC finished after 3.6 seconds using 206 iterations (totalIter = 10000) with |force| = 9.92e-04 (minimize_tol = 1.00e-03)
and rms Δiota = 8.41e-15(iota_tol=1.00e-03)
parameters.toml, pygvec run, pygvec visu …<xarray.Dataset> Size: 1MB
Dimensions: (rad: 2, pol: 50, tor: 33, xyz: 3)
Coordinates:
* rho (rad) float64 16B 0.5 1.0
* theta (pol) float64 400B 0.0 0.1282 0.2565 ... 6.027 6.155 6.283
* zeta (tor) float64 264B 0.0 0.1963 0.3927 0.589 ... 5.89 6.087 6.283
* xyz (xyz) <U1 12B 'x' 'y' 'z'
Dimensions without coordinates: rad, pol, tor
Data variables: (12/44)
X1 (rad, pol, tor) float64 26kB 0.5045 0.4964 ... 0.9758 0.9883
dX1_dr (rad, pol, tor) float64 26kB 0.9732 0.9617 ... 0.9614 0.9681
dX1_dt (rad, pol, tor) float64 26kB 0.008341 0.006512 ... -0.0005496
dX1_dz (rad, pol, tor) float64 26kB 5.451e-15 -0.08317 ... 5.753e-15
dX1_drr (rad, pol, tor) float64 26kB -0.04465 -0.0316 ... 0.02312
dX1_drt (rad, pol, tor) float64 26kB 0.005745 0.004374 ... -0.03973
... ...
Jac (rad, pol, tor) float64 26kB 2.744 2.71 2.529 ... 5.497 5.439
e_theta (xyz, rad, pol, tor) float64 79kB 0.008341 0.006479 ... 1.1
e_zeta (xyz, rad, pol, tor) float64 79kB 1.963e-14 ... -5.255e-15
B_contra_t (rad, pol, tor) float64 26kB 1.001e-16 -0.003144 ... -1.156e-16
B_contra_z (rad, pol, tor) float64 26kB 0.05675 0.05738 ... 0.05264 0.05323
B (rad, pol, tor, xyz) float64 79kB 1.115e-15 ... -4.069e-16
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection="3d", alpha=0.9)
# last surface in rho
ax.plot_surface(*ev.pos.sel(rho=1.0, method="nearest"), alpha=0.5)
# cuts of the boundary
for t in range(0, ev.tor.size, 4):
ax.plot3D(
*ev.pos.isel(rad=-1, tor=t), alpha=0.5, color="k"
)
# flux surface mid-radius
ax.plot_surface(*ev.pos.sel(rho=0.5, method="nearest"), alpha=0.5, color="red")
ev_axis = state.evaluate("pos", rho=0.0, theta=0.0, zeta=np.linspace(0, 2 * np.pi, 201))
ax.plot3D(*ev_axis.pos, color="green")
ax.set(aspect="equal")
ax.view_init(25, 140, 0)
CMake & scikit-build-core (using Find_Package & PkgConfig)f90wrap & numpy-f2pySeLaLib, BLAS, ftimings, OpenMP, MPIxarray, numpy, scipy, matplotlib, plotly, …@register(
requirements=("p", "mod_B", "Jac", "V", "mu0"),
integration=("rho", "theta", "zeta"),
attrs=dict(
long_name="volume-averaged plasma beta",
symbol=r"\overline{\beta}",
),
)
def beta_avg(ds: xr.Dataset):
beta = 2 * ds.mu0 * ds.p / ds.mod_B**2
ds["beta_avg"] = volume_integral(beta * ds.Jac) / ds.Vcutoff_gframe modes
tolerance_outputProjectName : surface_constructed
which_hmap : 21
hmap_ncfile : surface_constructed-Gframe.nc
X1X2_deg : 5
LA_deg : 5
sgrid : {'grid_type': 0, 'nElems': 5}
X1_mn_max : (3, 4)
X2_mn_max : (3, 4)
LA_mn_max : (3, 4)
X1_sin_cos : _sincos_
X2_sin_cos : _sincos_
LA_sin_cos : _sincos_
minimize_tol : 1e-07
totalIter : 100000
logIter : 100
pres : {'type': 'polynomial', 'coefs': [0.0]}
I_tor : {'type': 'polynomial', 'coefs': [0.0]}
picard_current : auto
getBoundaryFromFile : 1
boundary_filename : surface_constructed-Gframe.nc
parameters_constructed["hmap_ncfile"] = "../" + parameters_constructed["hmap_ncfile"]
parameters_constructed["boundary_filename"] = "../" + parameters_constructed["boundary_filename"]
parameters_constructed["minimize_tol"] = 1e-3
runpath = "run_constructed_gframe"
run = gvec.run(parameters_constructed, runpath=runpath)GVEC - completed 0/3 stages, restarts in current stage - 0: |>|.|.|GVEC - completed 1/3 stages, restarts in current stage - 0: |=|>|.|GVEC - completed 2/3 stages, restarts in current stage - 0: |=|=|>|GVEC finished after 3.1 seconds using 157 iterations (totalIter = 100000) with |force| = 9.87e-04 (minimize_tol = 1.00e-03)
and rms Δiota = 2.72e-14(iota_tol=1.00e-03)

Follow along: https://gitlab.mpcdf.mpg.de/gvec-group/workshop-material → Launch Binder → slides/slides.ipynb