Magnetische Koordinatensysteme in Tokamak-Gleichgewichten#
Dieses Tutorial vergleicht vier Flusskoordinatensysteme, die in der magnetischen Fusion verwendet werden: PEST, Boozer, Hamada und Equal-Arc.
Was sind Flusskoordinaten?#
In einem Tokamak liegen magnetische Feldlinien auf verschachtelten toroidalen Flächen, den Flussflächen. Ein Flusskoordinatensystem \((\psi, \theta, \varphi)\) erfüllt:
\(\psi\) beschriftet Flussflächen, z. B. \(\psi_{\rm norm} = 0\) auf der Achse und \(1\) an der LCFS
\(\varphi\) ist der übliche toroidale Winkel
\(\theta\) ist ein poloidaler Winkel mit nützlichen Eigenschaften
Die vier Systeme unterscheiden sich nur in der Wahl von \(\theta\):
System |
Definition des poloidalen Winkels \(\theta\) |
Haupteigenschaft |
|---|---|---|
PEST |
\(\partial(\mathbf{B}\cdot\nabla\theta^*) / \partial\theta^* = 0\): gerade Feldlinien |
Einfachste Gerade-Feldlinien-Koordinaten |
Boozer |
PEST + Jacobi-Determinante ist Flussfunktion: \(\sqrt{g} = \sqrt{g}(\psi)\) |
\(\mathbf{J}\cdot\nabla\varphi = {\rm const}(\psi)\); genutzt in Stellarator-/Tokamak-Codeausgaben |
Hamada |
Gleiche Fläche ab der magnetischen Achse |
\(\mathbf{J}\cdot\nabla\theta = {\rm const}(\psi)\); vereinfacht MHD-Stabilitätsmatrizen |
Equal-Arc |
Bogenlänge entlang der Flussfläche ist gleichmäßig in \(\theta_{\rm ea}\) |
Numerisch robust; einfache Konstruktion |
Warum sind diese Wahlmöglichkeiten wichtig?#
Gerade Feldlinien (PEST, Boozer, Hamada): Ein Modus mit toroidaler Zahl \(n\) und poloidaler Zahl \(m\) erfüllt bei Resonanz \(q = m/n\). Das vereinfacht die Fourier-Analyse von MHD-Moden.
Boozer: Die \(1/B^2\)-Drift ist rein radial, was die neoklassische Transporttheorie und die kinetische Gleichung vereinfacht.
Hamada: Stromlinien sind ebenfalls gerade; das wird in einigen MHD-Stabilitätscodes benötigt.
Equal-Arc: Praktisch für numerische Gitter; löst die Plasmagrenze gut auf.
Einrichtung: analytisches Solov’ev-Gleichgewicht#
Wir verwenden das Solov’ev-Gleichgewicht nach Cerfon & Freidberg (2010), skaliert auf eine Referenz-Tokamakgröße (\(R_0 \approx 1{,}86\) m).
[1]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.gridspec import GridSpec
from pyna.toroidal.equilibrium.Solovev import solovev_iter_like
from pyna.toroidal.coords.PEST import build_PEST_mesh
from pyna.toroidal.coords.EqualArc import build_equal_arc_mesh
from pyna.toroidal.coords.Hamada import build_Hamada_mesh
from pyna.toroidal.coords.Boozer import build_Boozer_mesh
# Create equilibrium (scale=0.3 → referenz-tokamakgroß, R0≈1.86 m)
eq = solovev_iter_like(scale=0.3)
print(f"Gleichgewicht: R0={eq.R0:.2f} m, a={eq.a:.2f} m, B0={eq.B0:.1f} T")
print(f"κ={eq.kappa:.2f}, δ={eq.delta:.2f}, q0={eq.q0:.2f}")
Rmaxis, Zmaxis = eq.magnetic_axis
print(f"Magnetische Achse: R={Rmaxis:.3f} m, Z={Zmaxis:.3f} m")
Gleichgewicht: R0=1.86 m, a=0.60 m, B0=5.3 T
κ=1.70, δ=0.33, q0=1.50
Magnetische Achse: R=1.946 m, Z=0.000 m
[2]:
# Build the background grid for the equilibrium
nR, nZ = 150, 150
R_grid = np.linspace(0.3 * eq.R0, 1.5 * eq.R0, nR)
Z_grid = np.linspace(-eq.a * eq.kappa * 1.3, eq.a * eq.kappa * 1.3, nZ)
Rg, Zg = np.meshgrid(R_grid, Z_grid, indexing='ij')
BR_grid, BZ_grid = eq.BR_BZ(Rg, Zg)
BPhi_grid = eq.Bphi(Rg)
psi_norm_grid = eq.psi(Rg, Zg)
# Build PEST mesh
# ns=40 radial Flächen, ntheta=181 poloidale Punkte
ns = 40
ntheta = 181
S, TET, R_mesh, Z_mesh, q_iS = build_PEST_mesh(
R_grid, Z_grid, BR_grid, BZ_grid, BPhi_grid, psi_norm_grid,
Rmaxis, Zmaxis, ns=ns, ntheta=ntheta
)
print(f"PEST-Gitter aufgebaut: {ns} Flächen, {ntheta} poloidale Punkte")
print(f"S-Bereich: [{S[1]:.3f}, {S[-1]:.3f}]")
print(f"q-Bereich: [{q_iS[1]:.2f}, {q_iS[-1]:.2f}]")
PEST-Gitter aufgebaut: 40 Flächen, 181 poloidale Punkte
S-Bereich: [0.028, 0.972]
q-Bereich: [1.49, 2.39]
Abschnitt 3: Equal-Arc-Koordinaten#
Der Equal-Arc-Winkel \(\theta_{\rm ea}\) parametrisiert jede Flussfläche so, dass die Bogenlänge entlang der Fläche proportional zu \(\theta_{\rm ea}\) ist. Dies ist die einfachste nichttriviale Koordinatentransformation.
[3]:
# Build equal-arc mesh
_, TET_ea, R_ea, Z_ea = build_equal_arc_mesh(S, TET, R_mesh, Z_mesh, n_theta=ntheta)
print(f"Equal-Arc-Gitter aufgebaut: shape {R_ea.shape}")
# Verify arc length uniformity on a mid-radius surface
i_mid = ns // 2
R_s = R_ea[i_mid, :]
Z_s = Z_ea[i_mid, :]
R_loop = R_s[:-1]; Z_loop = Z_s[:-1] # drop endpoint duplicate
R_closed = np.append(R_loop, R_loop[0])
Z_closed = np.append(Z_loop, Z_loop[0])
ds = np.sqrt(np.diff(R_closed)**2 + np.diff(Z_closed)**2)
print(f"Bogenlängensegmente auf Fläche {i_mid}: Mittel={ds.mean()*100:.2f} cm, Std={ds.std()*100:.3f} cm")
print(f" Gleichmäßigkeit (Std/Mittel): {ds.std()/ds.mean():.4f}")
Equal-Arc-Gitter aufgebaut: shape (40, 181)
Bogenlängensegmente auf Fläche 20: Mittel=1.27 cm, Std=0.000 cm
Gleichmäßigkeit (Std/Mittel): 0.0001
[4]:
# Plot equal-arc Koordinaten
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Left: R-Z view of flux Flächen with equal-arc mesh lines
ax = axes[0]
# Plot psi_norm contours as background
cs = ax.contour(Rg, Zg, psi_norm_grid, levels=np.linspace(0, 1, 11),
colors='lightgray', linewidths=0.5)
# Plot every 5th surface in equal-arc
stride = max(1, ns // 10)
colors = cm.plasma(np.linspace(0.2, 0.9, ns // stride + 1))
for k, i in enumerate(range(1, ns, stride)):
ax.plot(R_ea[i, :], Z_ea[i, :], color=colors[k], lw=1.0)
# Plot a few poloidal lines (constant θ_ea)
for j in range(0, ntheta-1, ntheta // 8):
ax.plot(R_ea[1:, j], Z_ea[1:, j], 'b-', lw=0.5, alpha=0.5)
ax.plot(Rmaxis, Zmaxis, 'r+', ms=10, mew=2, label='Achse')
ax.set_xlabel('R (m)')
ax.set_ylabel('Z (m)')
ax.set_title('Equal-Arc-Gitter (R-Z-Ansicht)')
ax.set_aspect('equal')
ax.legend()
# Right: arc-length increments as function of theta_ea
ax = axes[1]
for k, i in enumerate(range(2, ns-1, stride)):
R_s = R_ea[i, :-1]; Z_s = Z_ea[i, :-1]
R_c = np.append(R_s, R_s[0]); Z_c = np.append(Z_s, Z_s[0])
ds_s = np.sqrt(np.diff(R_c)**2 + np.diff(Z_c)**2)
ds_s_norm = ds_s / ds_s.mean()
ax.plot(TET_ea[:-1], ds_s_norm, color=colors[k], lw=0.8,
label=f'S={S[i]:.2f}' if k % 2 == 0 else None)
ax.axhline(1.0, color='k', ls='--', lw=1, label='Ideal (gleichmäßig)')
ax.set_xlabel(r'$\theta_{\rm ea}$ (rad)')
ax.set_ylabel('Bogenlängeninkrement / Mittelwert')
ax.set_title('Bogenlängen-Gleichmäßigkeit in Equal-Arc-Koordinaten')
ax.set_xlim(0, 2*np.pi)
ax.legend(fontsize=8, ncol=2)
plt.tight_layout()
plt.savefig('equal_arc_coords.png', dpi=100, bbox_inches='tight')
plt.show()
print("Equal-Arc: Bogenlängensegmente sind bis auf ~1 % gleichmäßig (Interpolationsdiskretisierung)")
Equal-Arc: Bogenlängensegmente sind bis auf ~1 % gleichmäßig (Interpolationsdiskretisierung)
Abschnitt 4: PEST-Koordinaten mit geraden Feldlinien#
In PEST-Koordinaten erfüllt der Sicherheitsfaktor \(q(\psi)\):
Das bedeutet, dass Feldlinien in der \((\theta^*, \varphi)\)-Ebene als Geraden mit Steigung \(1/q\) erscheinen.
[5]:
# Show q(S) profile from PEST integration
q_valid = q_iS[1:]
S_valid = S[1:]
# Compare with analytic equilibrium q
psi_vals = S_valid**2
q_analytic = eq.q_profile(psi_vals, n_theta=256)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ax = axes[0]
ax.plot(S_valid, q_valid, 'b-o', ms=3, label='PEST q(S)')
finite = np.isfinite(q_analytic)
ax.plot(S_valid[finite], q_analytic[finite], 'r--', label='Analytisches q(S)')
ax.set_xlabel('S = √ψ_norm')
ax.set_ylabel('Sicherheitsfaktor q')
ax.set_title('Sicherheitsfaktorprofil: PEST gegen analytisch')
ax.legend()
ax.grid(True, alpha=0.3)
# PEST mesh in R-Z
ax = axes[1]
cs = ax.contour(Rg, Zg, psi_norm_grid, levels=np.linspace(0, 1, 11),
colors='lightgray', linewidths=0.5)
stride_s = max(1, ns // 10)
colors_pest = cm.viridis(np.linspace(0.2, 0.9, ns // stride_s + 1))
for k, i in enumerate(range(1, ns, stride_s)):
ax.plot(R_mesh[i, :], Z_mesh[i, :], color=colors_pest[k], lw=1.0)
for j in range(0, ntheta-1, ntheta // 8):
ax.plot(R_mesh[1:, j], Z_mesh[1:, j], 'g-', lw=0.5, alpha=0.5)
ax.plot(Rmaxis, Zmaxis, 'r+', ms=10, mew=2)
ax.set_xlabel('R (m)')
ax.set_ylabel('Z (m)')
ax.set_title('PEST-Gitter (R-Z-Ansicht)')
ax.set_aspect('equal')
plt.tight_layout()
plt.savefig('pest_coords.png', dpi=100, bbox_inches='tight')
plt.show()
[6]:
# Demonstrate straight Feldlinien in PEST theta-phi space
# In PEST: a field line traces theta* = phi/q + const
fig, ax = plt.subplots(figsize=(8, 5))
phi_line = np.linspace(0, 4 * np.pi, 200)
surface_indices = [ns // 5, ns // 3, ns // 2, 2 * ns // 3, 4 * ns // 5]
colors_fl = cm.Set1(np.linspace(0, 0.8, len(surface_indices)))
for k, i_s in enumerate(surface_indices):
if i_s >= ns or not np.isfinite(q_iS[i_s]):
continue
q_s = q_iS[i_s]
# Field line: theta* = phi / q (PEST straight field line)
theta_fl = (phi_line / q_s) % (2 * np.pi)
ax.plot(phi_line / np.pi, theta_fl / np.pi, color=colors_fl[k],
label=f'S={S[i_s]:.2f}, q={q_s:.2f}', lw=1.5)
ax.set_xlabel(r'$\varphi / \pi$')
ax.set_ylabel(r'$\theta^* / \pi$ (PEST)')
ax.set_title('Feldlinien in PEST $(\\theta^*, \\varphi)$ space — Geraden mit Steigung $1/q$')
ax.legend(fontsize=9)
ax.set_xlim(0, 4)
ax.set_ylim(0, 2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('pest_fieldlines.png', dpi=100, bbox_inches='tight')
plt.show()
Abschnitt 5: Boozer-Koordinaten#
Boozer-Koordinaten erweitern PEST durch die zusätzliche Forderung, dass die Jacobi-Determinante \(\sqrt{g}\) eine Flussflächen-Größe ist, also nur von \(\psi\) und nicht von \(\theta\) abhängt. Dadurch gilt \(\mathbf{J}\cdot\nabla\varphi = {\rm const}(\psi)\).
Konstruktion: \(\theta_B = \theta^* + \lambda(\psi, \theta^*)\) mit
wobei \(\langle\cdot\rangle\) den Flussflächen-Mittelwert \(\frac{1}{2\pi}\int_0^{2\pi}\cdot\,d\theta^*\) bezeichnet.
[7]:
# Build Boozer mesh
_, TET_B, R_B, Z_B, lambda_corr = build_Boozer_mesh(
S, TET, R_mesh, Z_mesh, q_iS, equilibrium=eq, n_theta=ntheta
)
print(f"Boozer-Gitter aufgebaut: shape {R_B.shape}")
print(f"Max. |λ|-Korrektur: {np.nanmax(np.abs(lambda_corr)):.4f} rad")
# Show the angle correction lambda(psi, theta*)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ax = axes[0]
# lambda correction as a function of theta for several Flächen
for k, i_s in enumerate(range(2, ns-1, max(1, ns // 8))):
lam = lambda_corr[i_s, :]
if np.any(np.isfinite(lam)):
ax.plot(TET / np.pi, lam, label=f'S={S[i_s]:.2f}',
color=cm.plasma(0.1 + 0.8 * i_s / ns))
ax.set_xlabel(r'$\theta^* / \pi$ (PEST)')
ax.set_ylabel(r'$\lambda = \theta_B - \theta^*$ (rad)')
ax.set_title('Boozer-Winkelkorrektur $\\lambda(\\psi, \\theta^*)$')
ax.legend(fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
ax = axes[1]
cs = ax.contour(Rg, Zg, psi_norm_grid, levels=np.linspace(0, 1, 11),
colors='lightgray', linewidths=0.5)
stride_s = max(1, ns // 10)
colors_b = cm.inferno(np.linspace(0.2, 0.9, ns // stride_s + 1))
for k, i in enumerate(range(1, ns, stride_s)):
ax.plot(R_B[i, :], Z_B[i, :], color=colors_b[k], lw=1.0)
for j in range(0, ntheta-1, ntheta // 8):
ax.plot(R_B[1:, j], Z_B[1:, j], 'm-', lw=0.5, alpha=0.5)
ax.plot(Rmaxis, Zmaxis, 'r+', ms=10, mew=2)
ax.set_xlabel('R (m)')
ax.set_ylabel('Z (m)')
ax.set_title('Boozer-Gitter (R-Z-Ansicht)')
ax.set_aspect('equal')
plt.tight_layout()
plt.savefig('boozer_coords.png', dpi=100, bbox_inches='tight')
plt.show()
Boozer-Gitter aufgebaut: shape (40, 181)
Max. |λ|-Korrektur: 0.5429 rad
Abschnitt 6: Hamada-Koordinaten#
Hamada-Koordinaten verlangen, dass sowohl Feldlinien als auch Stromdichtelinien gerade sind:
Für achsensymmetrische Gleichgewichte reduziert sich das darauf, den poloidalen Winkel flächenäquidistant zu machen: Die von der magnetischen Achse bis zum Winkel \(\theta_H\) überstrichene Fläche ist proportional zu \(\theta_H\).
[8]:
# Build Hamada mesh
_, TET_H, R_H, Z_H = build_Hamada_mesh(S, TET, R_mesh, Z_mesh, n_theta=ntheta)
print(f"Hamada-Gitter aufgebaut: shape {R_H.shape}")
# Verify equal-area property
R_ax = R_mesh[0, 0]
Z_ax = Z_mesh[0, 0]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ax = axes[0]
# Show cumulative area vs theta_H for several Flächen
for k, i_s in enumerate(range(2, ns-2, max(1, ns // 8))):
R_s = R_H[i_s, :-1]; Z_s = Z_H[i_s, :-1]
R_c = np.append(R_s, R_s[0]); Z_c = np.append(Z_s, Z_s[0])
dA = 0.5 * ((R_c[:-1] - R_ax) * (Z_c[1:] - Z_ax) -
(R_c[1:] - R_ax) * (Z_c[:-1] - Z_ax))
A_cum = np.cumsum(dA)
A_total = A_cum[-1]
if abs(A_total) > 1e-10:
ax.plot(TET_H[:-1] / np.pi, A_cum / A_total,
color=cm.cool(0.1 + 0.8 * i_s / ns),
label=f'S={S[i_s]:.2f}')
# Ideal line
ax.plot([0, 2], [0, 1], 'k--', lw=2, label='Ideal (linear)')
ax.set_xlabel(r'$\theta_H / \pi$ (Hamada)')
ax.set_ylabel('Kumulative Fläche / Gesamtfläche')
ax.set_title('Hamada: kumulative Fläche ist linear in $\\theta_H$')
ax.legend(fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
ax = axes[1]
cs = ax.contour(Rg, Zg, psi_norm_grid, levels=np.linspace(0, 1, 11),
colors='lightgray', linewidths=0.5)
colors_h = cm.cool(np.linspace(0.2, 0.9, ns // stride_s + 1))
for k, i in enumerate(range(1, ns, stride_s)):
ax.plot(R_H[i, :], Z_H[i, :], color=colors_h[k], lw=1.0)
for j in range(0, ntheta-1, ntheta // 8):
ax.plot(R_H[1:, j], Z_H[1:, j], 'c-', lw=0.5, alpha=0.5)
ax.plot(Rmaxis, Zmaxis, 'r+', ms=10, mew=2)
ax.set_xlabel('R (m)')
ax.set_ylabel('Z (m)')
ax.set_title('Hamada-Gitter (R-Z-Ansicht)')
ax.set_aspect('equal')
plt.tight_layout()
plt.savefig('hamada_coords.png', dpi=100, bbox_inches='tight')
plt.show()
Hamada-Gitter aufgebaut: shape (40, 181)
Abschnitt 7: Vergleichspanel - alle vier Koordinatensysteme#
Hier überlagern wir alle vier Gitter in der R-Z-Ebene für denselben Satz von Flussflächen.
[9]:
fig = plt.figure(figsize=(16, 14))
gs = GridSpec(2, 2, figure=fig, hspace=0.3, wspace=0.3)
meshes = [
('PEST', TET, R_mesh, Z_mesh, 'viridis', 'g'),
('Equal-arc', TET_ea, R_ea, Z_ea, 'plasma', 'b'),
('Boozer', TET_B, R_B, Z_B, 'inferno', 'm'),
('Hamada', TET_H, R_H, Z_H, 'cool', 'c'),
]
stride_s = max(1, ns // 8)
stride_t = ntheta // 12
for idx, (name, tet, R_m, Z_m, cmap, pline_color) in enumerate(meshes):
ax = fig.add_subplot(gs[idx // 2, idx % 2])
# Flux surface contours as background
ax.contour(Rg, Zg, psi_norm_grid, levels=np.linspace(0.05, 0.95, 10),
colors='lightgray', linewidths=0.4)
# Mesh Flächen (constant S)
surface_colors = plt.get_cmap(cmap)(np.linspace(0.2, 0.9, ns // stride_s + 1))
for k, i in enumerate(range(1, ns, stride_s)):
ax.plot(R_m[i, :], Z_m[i, :], color=surface_colors[k], lw=1.2)
# Poloidal lines (constant theta)
for j in range(0, len(tet)-1, stride_t):
ax.plot(R_m[1:, j], Z_m[1:, j], color=pline_color,
lw=0.6, alpha=0.6)
ax.plot(Rmaxis, Zmaxis, 'r+', ms=12, mew=2)
ax.set_xlabel('R (m)', fontsize=11)
ax.set_ylabel('Z (m)', fontsize=11)
ax.set_title(f'{name} Koordinaten', fontsize=13, fontweight='bold')
ax.set_aspect('equal')
# Add annotation
ann = {'PEST': 'Gerade B-Feldlinien',
'Equal-arc': 'Gleichmäßige Bogenlänge',
'Boozer': 'Straight B + uniform Jacobian',
'Hamada': 'Straight B + equal area'}[name]
ax.text(0.05, 0.95, ann, transform=ax.transAxes, fontsize=9,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
fig.suptitle('Vergleich magnetischer Koordinatensysteme\n(farbige Linien = Flussflächen, dünne Linien = poloidales Gitter)',
fontsize=14, y=1.01)
plt.savefig('coords_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
Abschnitt 8: Physikalische Interpretation#
8.1 Fourier-Modenkopplung#
In Gerade-Feldlinien-Koordinaten (PEST, Boozer, Hamada) resoniert eine Störung mit einer einzelnen toroidalen Modenzahl \(n\) auf der Fläche, auf der \(q = m/n\) gilt. Die Fourier-Zerlegung in \(\theta\) besitzt dort eine klare Modenstruktur.
Im Gegensatz dazu erscheint bei geometrischem Winkel oder Equal-Arc ein einzelner \((m,n)\)-Modus als mehrere Fourier-Komponenten und koppelt verschiedene \(m\)-Werte - das sogenannte Fourier-Kopplungsproblem.
[10]:
# Demonstrate: Fourier-Spektrum of R(theta) — 2D heatmap (m vs S) for PEST and equal-arc
m_show = 12 # show Moduss m=0..m_show-1
# Compute FFT of R(theta) - <R> at each surface for both coordinate systems
S_arr = np.linspace(0.1, 0.9, ns) # approx normalised flux label per surface
fft_PEST = np.zeros((ns, m_show))
fft_EA = np.zeros((ns, m_show))
for i in range(ns):
R_pest = R_mesh[i, :-1] - R_mesh[i, :-1].mean()
R_ea_i = R_ea[i, :-1] - R_ea[i, :-1].mean()
n_pts_pest = len(R_pest)
n_pts_ea = len(R_ea_i)
fft_p = np.abs(np.fft.rfft(R_pest))[:m_show]
fft_p = fft_p / (fft_p.max() + 1e-30)
fft_PEST[i, :len(fft_p)] = fft_p[:m_show]
fft_e = np.abs(np.fft.rfft(R_ea_i))[:m_show]
fft_e = fft_e / (fft_e.max() + 1e-30)
fft_EA[i, :len(fft_e)] = fft_e[:m_show]
fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
for ax, data, name in zip(
axes,
[fft_PEST, fft_EA],
['PEST (straight field line)', 'Equal-arc θ'],
):
# log scale heatmap: rows = radial surface index, cols = Modus m
log_data = np.log10(data + 1e-6)
im = ax.imshow(
log_data.T, # shape (m_show, ns): Modus vs surface
origin='lower',
aspect='auto',
cmap='hot_r',
vmin=log_data.max() - 3, # show 3 decades
vmax=log_data.max(),
extent=[S_arr[0], S_arr[-1], -0.5, m_show - 0.5],
interpolation='nearest',
)
cbar = fig.colorbar(im, ax=ax, pad=0.02, shrink=0.85)
cbar.set_label(r'$\log_{10}$ normalised FFT amplitude', fontsize=8)
ax.set_xlabel(r'Normalisiertes Flusslabel $S$', fontsize=10)
ax.set_ylabel('Poloidaler Modus m', fontsize=10)
ax.set_yticks(np.arange(0, m_show, 2))
ax.set_title(f'R(θ) Fourier-Spektrum: {name}', fontsize=11)
# Annotation: PEST sollte sein concentrated at m=1; equal-arc spreads
axes[0].text(0.05, 0.9, 'Spectrum concentrated\nat m=1 (ideal)',
transform=axes[0].transAxes, fontsize=8, color='white',
bbox=dict(boxstyle='round', facecolor='steelblue', alpha=0.5))
axes[1].text(0.05, 0.9, 'Spectrum spreads to\nhigher m (non-ideal)',
transform=axes[1].transAxes, fontsize=8, color='white',
bbox=dict(boxstyle='round', facecolor='tomato', alpha=0.5))
plt.suptitle('Fourier-Inhalt: PEST gegen Equal-Arc-Koordinaten', fontsize=12)
plt.tight_layout()
plt.show()
8.2 Übersichtstabelle#
[11]:
summary = """
╔══════════════╦═══════════════════════╦══════════════════════════════╦══════════════════════════╗
║ Koordinate ║ Definition von θ ║ Hauptvorteil ║ Verwendung ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ PEST ║ Gerade B-Feldlinien ║ Minimale Fourier-Kopplung ║ generische MHD-Codes ║
║ ║ B·∇θ*/B·∇φ = q(ψ) ║ q = m/n bei Resonanz ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Boozer ║ PEST + √g = √g(ψ) ║ 1/B²-Drift ist rein radial ║ Codeausgaben ║
║ ║ (gleichmäßiger Jac.) ║ klarere neoklassische Theorie ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Hamada ║ gleiche Fläche ab Achse║ J·∇θ = const, MHD-Stabilität ║ Stabilitätscodes ║
║ ║ ∝ überstrichene Fläche ║ Matrizen vereinfacht ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Equal-arc ║ gleichmäßige Bogenlänge║ Einfache Konstruktion ║ numerische Gitter, FEM ║
║ ║ ds/dθ_ea = const ║ löst Rand gut auf ║ ║
╚══════════════╩═══════════════════════╩══════════════════════════════╩══════════════════════════╝
"""
print(summary)
╔══════════════╦═══════════════════════╦══════════════════════════════╦══════════════════════════╗
║ Koordinate ║ Definition von θ ║ Hauptvorteil ║ Verwendung ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ PEST ║ Gerade B-Feldlinien ║ Minimale Fourier-Kopplung ║ generische MHD-Codes ║
║ ║ B·∇θ*/B·∇φ = q(ψ) ║ q = m/n bei Resonanz ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Boozer ║ PEST + √g = √g(ψ) ║ 1/B²-Drift ist rein radial ║ Codeausgaben ║
║ ║ (gleichmäßiger Jac.) ║ klarere neoklassische Theorie ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Hamada ║ gleiche Fläche ab Achse║ J·∇θ = const, MHD-Stabilität ║ Stabilitätscodes ║
║ ║ ∝ überstrichene Fläche ║ Matrizen vereinfacht ║ ║
╠══════════════╬═══════════════════════╬══════════════════════════════╬══════════════════════════╣
║ Equal-arc ║ gleichmäßige Bogenlänge║ Einfache Konstruktion ║ numerische Gitter, FEM ║
║ ║ ds/dθ_ea = const ║ löst Rand gut auf ║ ║
╚══════════════╩═══════════════════════╩══════════════════════════════╩══════════════════════════╝
8.3 Beziehung zwischen den Systemen#
Alle vier Systeme sind durch Winkeltransformationen der Form \(\theta_{\rm new} = \theta^* + f(\psi, \theta^*)\) verbunden:
Equal-Arc -> Parametrisierung mit gleicher Bogenlänge (rein geometrisch)
PEST -> gleiche Feldlinien-Windung (erfordert Integration von \(B_R, B_Z\))
Boozer -> PEST + periodische Korrektur zur Glättung der Jacobi-Determinante
Hamada -> Flächentransformation (bezieht die eingeschlossene Fläche ein und entspricht einer \(\sqrt{g}\)-Skalierung proportional zur Gesamtfläche)
Für einen Tokamak sind Boozer und Hamada oft sehr ähnlich, weil beide flussflächen-gemittelten Größen, Jacobi-Determinante und Fläche, durch dieselbe Druckbilanz gesteuert werden.
[12]:
# Final comparison: all four theta angles for a single field line
# Show how theta_PEST, theta_B, theta_H, theta_ea relate on the midradius surface
i_surf = ns // 2
print(f"Vergleiche Darstellungen des poloidalen Winkels auf Fläche S={S[i_surf]:.3f}")
print(f" Sicherheitsfaktor q = {q_iS[i_surf]:.3f}")
# For each coordinate system, compute the geometric angle (atan2(Z-Zax, R-Rax))
fig, ax = plt.subplots(figsize=(10, 5))
for name, tet_arr, R_m, Z_m, color in [
('PEST', TET, R_mesh, Z_mesh, 'blue'),
('Equal-arc', TET_ea, R_ea, Z_ea, 'green'),
('Boozer', TET_B, R_B, Z_B, 'red'),
('Hamada', TET_H, R_H, Z_H, 'purple'),
]:
R_s = R_m[i_surf, :]
Z_s = Z_m[i_surf, :]
theta_geom = np.arctan2(Z_s - Zmaxis, R_s - Rmaxis) % (2 * np.pi)
ax.plot(tet_arr / np.pi, theta_geom / np.pi, label=name, color=color, lw=1.5)
# Diagonal = geometric angle equals coordinate angle
ax.plot([0, 2], [0, 2], 'k--', lw=1, alpha=0.5, label='Identität (geom = coord)')
ax.set_xlabel(r'Koordinatenwinkel $\theta / \pi$')
ax.set_ylabel(r'Geometrischer Winkel $\theta_{\rm geom} / \pi$')
ax.set_title('Geometrischer Winkel gegen Koordinatenwinkel für jedes System\n' +
f'(surface S={S[i_surf]:.2f}, q={q_iS[i_surf]:.2f})')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 2)
ax.set_ylim(0, 2)
plt.tight_layout()
plt.savefig('theta_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
print("Alle vier Systeme unterscheiden sich nur darin, wie θ um die Flussfläche verteilt ist.")
Vergleiche Darstellungen des poloidalen Winkels auf Fläche S=0.474
Sicherheitsfaktor q = 1.638
Alle vier Systeme unterscheiden sich nur darin, wie θ um die Flussfläche verteilt ist.