Tutorial 1: Loss fraction from a VMEC equilibrium¶
This tutorial computes the collisionless alpha-particle loss fraction of the quasi-axisymmetric (QA) and quasi-helically symmetric (QH) reactor-scale configurations of Landreman and Paul (2022), and its statistical error. Both VMEC equilibria are public simsopt test files with a mean field of about 5.9 T. Runtime: one to two minutes per configuration on 16 threads.
Step 1: Input data¶
mkdir -p tutorial1/qa tutorial1/qh
cd tutorial1
wget https://github.com/hiddenSymmetries/simsopt/raw/master/tests/test_files/wout_LandremanPaul2021_QA_reactorScale_lowres_reference.nc \
-O qa/wout.nc
wget https://github.com/hiddenSymmetries/simsopt/raw/master/tests/test_files/wout_LandremanPaul2021_QH_reactorScale_lowres_reference.nc \
-O qh/wout.nc
Step 2: Input file¶
Write tutorial1/simple.in and copy it to both directories
(cp simple.in qa/ && cp simple.in qh/):
&config
netcdffile = 'wout.nc' ! VMEC equilibrium
isw_field_type = 2 ! Boozer canonical coordinates
integmode = 1 ! symplectic Euler
trace_time = 1d-1 ! s
sbeg = 0.6d0 ! start surface, normalised toroidal flux
ntestpart = 256 ! number of alpha particles
deterministic = .true. ! reproducible sampling
output_results_netcdf = .true.
/
trace_time = 0.1Without collisions (the default) the result is the collisionless loss fraction up to this time. Losses of an optimised configuration occur mostly within the first 10 to 100 ms; the slowing-down time of 3.5 MeV alphas in a reactor is 0.1 to 1 s.
sbeg = 0.6Markers start on the surface \(s = 0.6\) along one field line (
startmode = 1, the default) with isotropic pitch. Largersbegprobes the outer region, where losses are larger. Other start distributions: Running SIMPLE.ntestpart = 256Sets the statistical error (below). Production runs use several thousand markers.
Step 3: Run¶
(cd qa && ../../build/simple.x simple.in > simple.log)
(cd qh && ../../build/simple.x simple.in > simple.log)
The relative path assumes tutorial1 is in the repository root.
OMP_NUM_THREADS sets the number of threads.
Step 4: Loss fraction¶
The last line of confined_fraction.dat holds time, the confined fractions
of passing and trapped markers, and the number of numerically resolved
markers:
tail -n 1 qa/confined_fraction.dat
tail -n 1 qh/confined_fraction.dat
0.10000000000000001 0.70703125000000000 0.17187500000000000 256
0.10000000000000001 0.67968750000000000 0.32031250000000000 256
In QA the confined fraction is \(0.707 + 0.172 = 0.879\): 12.1 % of the markers are lost within 0.1 s. In QH none is lost.
orbit_exit_code.dat gives the reason each orbit ended (second column;
all codes in Outputs):
awk '{print $2}' qa/orbit_exit_code.dat | sort | uniq -c
95 0
31 1
130 3
Code 0 is traced to trace_time, code 1 crossed the last closed flux
surface, code 3 was not traced: markers with trapping parameter below
contr_pp (default \(-1\)) are strongly passing and counted as confined
without tracing. Set contr_pp = -1d10 to trace all markers. Codes of 101
and above are numerically unresolved orbits; they are excluded from the
confined fractions and reported in unresolved_fraction.dat.
Step 5: Statistical error¶
Each marker is lost or confined independently, so the number of losses \(k\) out of \(N\) resolved markers is binomial. The estimate \(\hat f = k/N\) has standard error
This plug-in error vanishes at \(k = 0\) and then says nothing. For zero observed losses the one-sided 95 % upper bound is
which for \(N = 256\) is 1.2 %. A loss fraction quoted as zero means “below \(3/N\)”, not zero. For small \(k\) use the Wilson interval (Plotting results).
Save as tutorial1/plot_confined.py and run it in tutorial1:
import matplotlib.pyplot as plt
import numpy as np
for case in ("qa", "qh"):
t, passing, trapped, resolved = np.loadtxt(
f"{case}/confined_fraction.dat", unpack=True)
loss = 1.0 - (passing + trapped)
n = resolved[-1]
error = np.sqrt(loss[-1] * (1.0 - loss[-1]) / n)
bound = 1.0 - 0.05 ** (1.0 / n)
print(f"{case}: loss {loss[-1]:.3f} +- {error:.3f}"
f" (zero-loss 95% bound {bound:.3f})")
plt.semilogx(t, loss, label=case.upper())
plt.xlabel("time (s)")
plt.ylabel("loss fraction")
plt.legend()
plt.savefig("loss_fraction.png", dpi=150)
qa: loss 0.121 +- 0.020 (zero-loss 95% bound 0.012)
qh: loss 0.000 +- 0.000 (zero-loss 95% bound 0.012)
The QA/QH difference (12.1 ± 2.0 % against < 1.2 %) is significant. The plot shows when the losses happen.
Step 6: Loss times of individual markers¶
times_lost.dat has one line per marker (columns in
Outputs): index, loss time in s, trapping parameter
\(\kappa\), start \(s\), … Confined markers have loss time
trace_time, skipped markers -1. The five earliest losses in QA:
awk '$2 > 0 && $2 < 0.1' qa/times_lost.dat | LC_ALL=C sort -g -k2 | head -5
\(\kappa\) (third column) is negative for passing and in \((0,1)\) for trapped markers (Guiding-centre model). In this QA run 28 of the 31 lost markers are trapped, and the first losses occur after about 20 µs.
What next¶
Vary
sbeg,ntestpartandtrace_timeand watch the loss fraction and its error.Add collisions with
swcoll = .true.and background profiles (Running SIMPLE).Tutorial 2: Wall heat loads turns the QA
results.ncinto a heat-load pattern.