Theory and numerical implementation
Theory
The code is based on an unpublished 1995 note by Steve Hirshman,
"Transformation from VMEC to Boozer coordinates".
(The method is not specific to VMEC, and can be applied to other equilibrium representations.)
The calculation has two steps. In the first step, the difference is
computed between the original toroidal and poloidal angles and the Boozer angles.
In the second step, this information is used to transform other quantities such as
\(R\), \(Z\), and \(B\) that are known as functions of the original
angles to functions of the new angles.
1. Determining the toroidal angle difference
We assume that at the start, each flux surface is parameterized by a poloidal angle \(\theta_0\) and a toroidal angle \(\zeta_0\), which need not be straight-field-line angles. The toroidal angle need not be the standard toroidal angle (although it typically is). We assume that we know the quantity \(\lambda\) by which the poloidal angle can be shifted to obtain the straight-field-line angle \(\theta^*=\theta_0 + \lambda\) compatible with \(\zeta_0\). Thus, the magnetic field \(\vec{B}\) can be written as
where \(2 \pi \psi\) is the toroidal flux, and \(\iota\) is the rotational transform. The Boozer poloidal angle \(\theta_B\) and toroidal angle \(\zeta_B\) are also straight field line angles, so
Moreover, Boozer coordinates satisfy
We define \(\nu\) to be the difference between the two toroidal angles:
Plugging this expression into (2) and equating the result with (1) gives \(\nabla\psi\times\nabla\left( \theta_B - \theta_0^* - \iota \nu \right) = 0\). Therefore \(\theta_B - \theta_0^* - \iota \nu = y(\psi)\) for some flux function \(y(\psi)\). We are free to choose \(y(\psi)=0\), which amounts to specifying the origin of \(\theta_B\) on each surface. Thus,
Note
In Hirshman’s note “Transformation from VMEC to Boozer coordinates”, the angle difference
is defined as \(p = \zeta_B - \zeta_0\). However in the fortran booz_xform
code,
a minus sign appears on line 83 of boozer.f
that reverses the sign, so the p
quantity
saved in boozmn_*.nc
files is in fact \(\zeta_0 - \zeta_B\). In the C++/python booz_xform
code, the sign convention \(\nu = \zeta_B - \zeta_0\) is used consistently throughout the
code and output files.
Plugging (4)-(5) into the covariant Boozer representation (3) gives
Note that the covariant components of \(\vec{B}\) in the original coordinates are
where \(\vec{r}\) is the position vector, and the dual relations have been used to get the right expressions from the central ones. Plugging (6) into these expressions gives
and
Equations (8) and (9) determine \(\nu\) up to a flux function (i.e. a constant on each surface). From (4), it can be seen that this constant effectively determines the origin of the \(\zeta_B\) coordinate. We are free to fix this constant by requiring that the average of \(\nu\) over \(\theta_0\) and \(\zeta_0\) be zero. We can write a double Fourier series for \(\nu\),
with analogous series for \(\lambda\), \(B_{\theta_0}\), and \(B_{\zeta_0}\). The \(m=n=0\) modes of (8)-(9) then give
allowing the flux functions \(I\) and \(G\) to be computed from the input data. When at least one of \(m\) or \(n\) is nonzero, the \(\cos(m \theta_0 - n \zeta_0)\) modes of (8)-(9) are
and
and the \(\sin(m \theta_0 - n \zeta_0)\) modes of (8)-(9) are
and
Equations (12)-(15) enable \(\nu\) to be calculated from the input data. To see that (12)-(13) are consistent with each other, the curl of \(\vec{B} = B_\psi\nabla\psi + B_{\theta_0}\nabla\theta_0 + B_{\zeta_0}\nabla\zeta_0\) can be plugged into the MHD equilibrium property \(\vec{J}\cdot\nabla\psi=0\), yielding \(\partial B_{\zeta_0}/\partial\theta_0 - \partial B_{\theta_0}/\partial\zeta_0=0\). In Fourier space, then,
The modes of \(\nu\) with \(m \ne 0\) can be computed from (12) and (14). The modes of \(\nu\) with \(n \ne 0\) can be computed from (13) and (15). For modes of \(\nu\) with both \(n \ne 0\) and \(m \ne 0\), we can use either (12) or (13), since the expressions are equivalent by (16); for the same reason we can use either (14) or (15). The mode of \(\nu\) with \(m=n=0\) can be set to zero, for as described above, this choice amounts to specifying the origin of \(\zeta_B\).
The results (12)-(15) can be summarized as
where \(w\) has a Fourier series analogous to (10), with \(\hat{w}_{0,0}^c=0\) (assuming \(\lambda_{0,0}^c=0\)) and
2. Transforming other quantities
Now that \(\nu\) is determined, the remaining task is to express other scalar quantities like \(B\) as functions of the new angles instead of the old angles. We let \(\Omega\) denote any scalar quantity that we wish to transform in this way, including the cylindrical coordinates \(R\) and \(Z\), and \(\nu\) itself. We write a double Fourier expansion, using a bar instead of hat to denote that the independent variables are now the Boozer angles:
The amplitudes satisfy
Changing the integration variables to the old angles,
where (4)-(5) have been applied. The Jacobian appearing in (21) is
It can be seen that (21) and the last expression in (22) express the new Fourier amplitudes we seek in terms of the old angles and known quantities on these angles.
Implementation details
Consistent with the discussion following (16), the modes
of \(\nu\) are computed via \(w\) as follows. If \(m \ne 0\),
(12) and (14) are used, equivalent to the first equality
on each line of (18).
If \(m = 0\) but \(n \ne 0\),
(13) and (15) are used, equivalent to the last expression
on each line of (18). This logic appears in surface_solve.cpp
.
The integrals in (21) are computed using a uniform tensor-product grid in \(\theta_0\) and \(\zeta_0\).