Iota Calculation, Fourier Surface Extraction, and Perturbations (Wendelstein 7-X)

This example shows how to calculate the rotational transform for Wendelstein 7-X.

import fusionsc as fsc
from fusionsc.devices import w7x
import numpy as np
import matplotlib.pyplot as plt

First, we need to perform some setup to make sure W7-X data are available.

Note: The W7-X geometry is currently protected by the W7-X data access agreement. Therefore, the file referenced below is available to W7-X team members (including external collaborators who signed this agreement) upon request.

field = w7x.standard()

grid = w7x.defaultGrid()
grid.nR = 32
grid.nZ = 32
grid.nPhi = 32

field = field.compute(grid)
await field

Finally, we need to decide in which phi planes we want to evaluate our iota and start our surfaces from.

#rStart = np.concatenate([np.linspace(6.1, 6.18, 5), np.linspace(6.225, 6.245, 5)])[None, :]
#phiStart = np.linspace(0, 2 * np.pi, 5, endpoint = False)[:,None]
rStart = np.asarray([6.19] + [6.225, 6.235, 6.245] * 5)
phiStart = np.asarray(
    [0] +
    [0 * 2 * np.pi / 5] * 3 + [1 * 2 * np.pi / 5] * 3 + [2 * 2 * np.pi / 5] * 3 + [3 * 2 * np.pi / 5] * 3 + [4 * 2 * np.pi / 5] * 3
)


xStart = rStart * np.cos(phiStart)
yStart = rStart * np.sin(phiStart)
zStart = 0 * xStart

Now it’s time to run our calculation.

pos, ax = fsc.flt.findAxis(field, startPoint = [xStart, yStart, zStart], islandM = 5, targetError = 1e-4, minStepSize = 1e-3, nTurns = 50)
iotas = fsc.flt.calculateIota(
    field, [xStart, yStart, zStart],
    200, # Turn count
    unwrapEvery = 10, distanceLimit = 1e4, 
    targetError = 1e-4, minStepSize = 1e-3,
    islandM = 5, axis = ax
)

The iota profile is returned in a shape matching the start point shape, and can be easily plotted.

print(iotas.shape, rStart.shape)
(16,) (16,)
plt.scatter(rStart + 0 * phiStart, iotas)
<matplotlib.collections.PathCollection at 0x2631eaadfc0>
../_images/ca8439cbc2cc974f7db2fa1c9f153dab232accdd6faad2f293744a4b05bcf8e6.png

Additionally, the field line tracer can also extract the Fourier decomposition of the magnetic surfaces from the field line.

modes = fsc.flt.calculateFourierModes(
    field, [xStart, yStart, zStart],
    30, # Turn count
    nMax = 15, mMax = 5, toroidalSymmetry = 5,
    unwrapEvery = 10, recordEvery = 1,
    targetError = 1e-4, distanceLimit = 1e5, maxStepSize = 0.01,
    stellaratorSymmetric = False, aliasThreshold = 0.05, # The island surfaces are not Stellarator symmetric
    islandM = 5
)

The modes are returned with a dict containing rotational transform and the Fourier expansion of the modes, as well as an encoding of the result as a FourierSurfaces object that can also be used in VMEC inputs.

modes.keys()
dict_keys(['surfaces', 'iota', 'theta', 'rCos', 'zSin', 'mPol', 'nTor', 'rSin', 'zCos'])

Of particular interest are two components: The first one is the ‘iota’ array, which returns the rotational transforms.

modes["iota"]
array([0.94893875, 1.08870193, 1.09037153, 1.09210265, 1.08833397,
       1.09001014, 1.09174102, 1.087973  , 1.08964504, 1.09137735,
       1.0876151 , 1.08928435, 1.09101242, 1.0872471 , 1.08891643,
       1.09065221])

Secondarily relevant is the ‘surfaces’ element, which contains the magnetic surfaces. This is an instance of the class fusionsc.magnetics.SurfaceArray, which can be sliced, added, and multiplied similar to a regular NumPy array.

modes["theta"]
array([-0.01311462,  3.08886114,  3.08667328, -0.01027982, -1.86227137,
       -1.8612186 ,  1.32774406, -0.53052922, -0.52729869,  2.66455488,
        0.80054195,  0.80604205, -2.28296756,  2.1296124 ,  2.13764443,
       -0.94786024])

To plot the modes, we need to multiply the Fourier coefficients with the appropriate angles.

surfaces = modes["surfaces"]
def plotCut(phi):
    plt.figure()
    surfaces.asGeometry().plotCut(np.radians(phi))
    plt.axis('equal')
    plt.xlim(5, 6.5)
    plt.ylim(-1.2, 1.2)

plotCut(0)
plotCut(18)
plotCut(36)
../_images/2b32d18fbb25ee0308b5510af55a6a0005d3a2c675fd4b64b6f8d69c8f370851.png ../_images/a7b88fca267ee7f81ed5d6209db45448ca902dcd1491c965a13c9ccf1398ab8d.png ../_images/b898611033e629b0be0c3bd1049a32190258ea69682d6b8ebdc373c585061056.png
surf = surfaces[0]

fieldModes = field.calculateRadialModes(surf, field, nSym = 5, mMax = 15, nMax = 15, nTheta = 400, nPhi = 400)
cc = fieldModes["cosCoeffs"]
sc = fieldModes["sinCoeffs"]

m = fieldModes["mPol"]
n = fieldModes["nTor"]

phi = fieldModes["phi"]
theta = fieldModes["theta"]

tot = np.sqrt(cc**2 + sc**2)

plt.figure(figsize=(16,9))
plt.imshow(fieldModes["radialValues"].T, origin='lower', extent = [0, 360, 0, 360])
plt.colorbar()
plt.title("Brad / B")
plt.xlabel('Phi [deg]')
plt.ylabel('Theta [deg]')

plt.figure(figsize=(16,9))
plt.imshow(tot.T, origin = "lower", norm = "log")
#plt.xlabel(f"n[0, {n[1]:.0f} ... {max(n):.0f}, {min(n):.0f}, ..., {n[-1]:.0f}]")
plt.xlabel("n")
plt.ylabel("m")
plt.xticks(range(len(n)), n)
plt.title("Fourier components of Brad / B")
plt.colorbar()
plt.show()
C:\Users\Knieps\Documents\repos\fsc\src\python\fusionsc\_api_markers.py:15: UserWarning: The function fusionsc.magnetics.MagneticConfig.calculateRadialModes is part of the unstable API. It might change or get removed in the near future. While unlikely, it might also not be compatible across client/server versions.
  warnings.warn(f"The function {f.__module__}.{f.__qualname__} is part of the unstable API. It might change or get removed in the near future. While unlikely, it might also not be compatible across client/server versions.")
C:\Users\Knieps\Documents\repos\fsc\src\python\fusionsc\magnetics.py:402: UserWarning: calculateRadialModes can only use the FFT fast path if nPhi == 2 * nMax + 1 and nTheta == 2 * mMax + 1. Other values are not recommended
  warnings.warn("calculateRadialModes can only use the FFT fast path if nPhi == 2 * nMax + 1 and nTheta == 2 * mMax + 1. Other values are not recommended")
../_images/af529b7bc9b7bded0a84aa518bda64d7eaff9d94e14fc5a371849a6dadcfb1d7.png ../_images/7511de830b3009db676873c8193d0309b2d5f1a13f0c27295b172e03a95d516a.png

The calculated surfaces can also be converted into geometries to plot.

asPv = surfaces[0].asGeometry(nPhi = 360, nTheta = 360).asPyvista()
asPv.plot(jupyter_backend = 'static')
../_images/309b2326d2c8935ecda36fe56a839fbd7d33d9e633f609077d4138e82a61c74a.png