Equilibrium representations
A tokamak equilibrium reaches an analysis as $\psi(R,Z)$, a $q(\psi)$ profile, a $\rho_{tor,N}$ grid, a set of Miller numbers, Fourier boundary coefficients, a PEST grid, camera pixels, a field-line trajectory, an $m/n$ helical perturbation or a local transport input. These forms are related, but they are not interchangeable. This page names the five kinds of object VAFT derives from an equilibrium, maps the hierarchy of issue #1201 onto the functions that implement it, and lists what is not yet implemented.
Every branch starts from one canonical record, vaft.data.EquilibriumData, built by
vaft.process.as_equilibrium from an ODS, a GEQDSK or an analytic generator. The record keeps the
source’s convention and does not convert it on construction. What follows is always
canonical state + explicit transformation = derived object (with provenance)
and never one object that holds every coordinate, projection and approximation at once.
The five categories
| Category | Answers | VAFT examples | Is not |
|---|---|---|---|
| coordinate | where a quantity is parameterized | $\psi_N$, $\rho_{pol,N}$, $\rho_{tor,N}$, PEST $\theta^*$ | $\rho_{tor,N} = \sqrt{\psi_N}$ |
| projection | where the same object is viewed | R–Z plane, top view, 3-D embedding, camera pixels | a camera overlay as a 2-D equilibrium |
| representation | how the state is encoded or approximated | Miller surface, FourierSurface, Solov’ev fit, prescribed island |
Miller as an arbitrary LCFS |
| derived quantity | what physics follows from the state | $q$, shear, $a/L_T$, rational surfaces, shaping | $a/L_T$ as a radial coordinate |
| solver convention | how an external code stores it | COCOS index, Wb vs Wb/rad, DCON/GPEC Jacobian, TGLF units | a Fourier contour angle as a magnetic angle |
Coordinate
A coordinate labels positions in the plasma. A quantity is a function of it, and changing the coordinate changes the abscissa but not the physics. Radial coordinates label flux surfaces. Poloidal coordinates label points on one surface. Two coordinates can share the range $[0,1]$ or $[0,2\pi)$ and still label different points. The definition, not the range or the name, makes a coordinate.
vaft.process.derive_radial_coordinates(eq)returnspsi_n,rho_pol_nandrho_tor_nasDerivedValuerecords. Each record gives its definition, for examplesqrt(int_axis^psi q dpsi / int_axis^boundary q dpsi)for $\rho_{tor,N}$. When the record cannot form $\rho_{tor,N}$, it gives a reason and does not substitute $\sqrt{\psi_N}$.vaft.process.MappedPositionsfromequilibrium_mapping_thomson_scatteringandequilibrium_mapping_charge_exchangegives each kinetic channel in every radial coordinate the equilibrium supports (Equilibrium and kinetic profiles).vaft.process.straight_field_line_mapreturns aStraightFieldLineMapthat evaluates the PEST angle $\theta^*$ and $\psi_N$ at any $(R,Z)$ and tabulatesthetaandtheta_staron a surface.vaft.formula.generalized_straight_field_line_anglegives the PEST, Boozer, Hamada and equal-arc angles of one surface.vaft.formula.sfl_toroidal_angle_shiftgives the matching toroidal shift $\nu$. The family is drawn byvaft.diagram.sfl_coordinate_taxonomy(Scientific diagrams).
Is not: $\rho_{tor,N}$ is not $\sqrt{\psi_N}$, and the geometric angle
$\theta = \mathrm{atan2}(Z-Z_a, R-R_a)$ is not the PEST $\theta^*$. The packaged 39915 ODS stores
$\sqrt{\psi_N}$ under profiles_1d.rho_tor_norm, so the stored name does not tell you the
definition (see Gaps).
Projection
A projection shows the same physical object in another space: the poloidal plane, the machine $X$–$Y$ plane, a 3-D scene or a detector. It creates no new equilibrium, fits nothing and loses no physics that the viewing geometry does not remove. It can add calibration: a camera projection depends on a calibrated camera model as well as on the equilibrium.
- The R–Z flux map, drawn by the
equilibrium_field_psiplot recipe from avaft.plot.Field2Dmodel. - The top view,
vaft.plot.equilibrium_geometry_topview. - The camera view:
vaft.omas.camera_projection_forreturns a calibratedvaft.process.camera_geometry.CameraProjection, andvaft.omas.compute_camera_visible_efit_overlayprojects the LCFS and flux surfaces into pixels without writing anything back. - The 3-D axisymmetric embedding:
vaft.process.camera_geometry.sweep_toroidalsweeps a contour toroidally, andvaft.plot.Geometry3DLayers/vaft.plot.render_geometry_3d_layersdraw it.
Is not: a camera overlay is not a 2-D equilibrium, and a toroidal sweep of an axisymmetric surface is not a 3-D equilibrium.
Representation
A representation encodes the state, or part of it, in another form, usually with fewer degrees of freedom. It is lossy, so it has to report how well it reproduces what it replaced. A representation fitted to the state describes it only within its fit validity.
vaft.process.fit_miller_surfacereturns aMillerFitResultwithrms_error,normalized_rms_error,max_error,hausdorff_distance,acceptedandreason. It refuses surfaces near an active X-point.vaft.process.fit_fourier_surface/evaluate_fourier_surface/fit_fourier_surface_sequencework withvaft.data.FourierSurface, which records its curve parameter inangle_convention("arc_length").- The analytic ladder:
vaft.process.solovev_to_equilibriumandsolovev_example(topology=...),vaft.process.guazzotto_freidberg_to_equilibriumfor Guazzotto–Freidberg parts 1 and 2, and the inverse fitvaft.process.fit_solovev. All of them return or consumeEquilibriumData, so an analytic state follows the same downstream paths as a reconstruction. - A prescribed island:
vaft.process.MagneticIslandSpecandmagnetic_island_topology.
Is not: a Miller shape is not an arbitrary LCFS. A fit that works on the nearly up-down
symmetric VEST limited boundary says nothing about beans, racetracks or diverted shapes
(notebooks/edge_and_boundary_representation.ipynb).
Derived quantity
A derived quantity is physics computed from the state: a number or profile that the equilibrium implies, not a new label for positions. It is plotted against a coordinate. Its value can depend on the coordinate and length scale used to define it, and then that choice is part of the quantity.
- $q$, magnetic shear and $\alpha$ on a Miller surface (
MillerSurface.q,.magnetic_shear,.alpha), and $q$ recomputed from the PEST map as $F\oint dl/(R|\nabla\psi|)$. - Shaping observables from
vaft.process.contour_shaping_observables, and the boundary topology record fromvaft.process.derive_boundary_representation(Topology, X-points, strike points, gaps). - Normalized gradients:
vaft.process.profile_gradients.profile_gradient, which keeps the plot coordinate, the gradient coordinate and the reference length apart and records all three (see Profile gradients below). The older kernelsvaft.formula.normalized_gradient_scale_length(x, y, a)andSyntheticKineticProfiles.a_over_L(channel, coordinate)take no equilibrium map.
Is not: $a/L_T$ is not a radial coordinate. On one $T_e$ profile at $\rho_{tor,N}=0.6$, the reference notebook gets 3.0, 2.95, 2.07 and 2.95 from four gradient conventions. A gradient is defined only once its derivative coordinate and its reference length are stated.
Profile gradients
vaft.process.profile_gradients (#551) computes $-L\,d\ln f/dx_g$ with four separate roles:
| argument | role | values |
|---|---|---|
coordinate |
the abscissa the result is placed on | any RADIAL_COORDINATES name |
gradient_coordinate |
what the derivative is taken with respect to | any RADIAL_COORDINATES name; r_minor or r_outboard for a normalized result |
reference_length |
the length that multiplies the derivative | None, a_minor, R_major_axis, R_major_surface, L_ref |
convention |
a code preset resolving the two above | tglf, cgyro, gs2, gkw, gene |
radial_coordinate_map(eq, time) gives every coordinate of the registry on one slice’s
$\psi$ grid, each as a DerivedValue: $\psi_N$, $\rho_{pol,N}$, $\rho_{tor,N}$ (from $q$, never the
$\sqrt{\psi_N}$ proxy), the midplane crossings r_inboard/r_outboard at the axis height,
r_center, r_minor $=(R_{out}-R_{in})/2$ and r_minor_norm $= r_{minor}/a$. Here $a$ is
r_minor on the LCFS. That is not the contour half-width minor_radius of
derive_global_descriptors, and r_minor is never $R_{out}-R_{axis}$.
radial_coordinate_map_from_arrays builds the same map from stored radii, such as an
input.gacode.
The profile is differentiated on its own grid. The chain rule $d/dx_g = (dx_p/dx_g)\,d/dx_p$ then carries it through the map, and only then is it interpolated, which is GACODE’s order.
from vaft.process.profile_gradients import profile_gradient, radial_coordinate_map
cmap = radial_coordinate_map(ods, time=0.32) # one equilibrium slice, chosen by time
psi_norm = np.linspace(0.0, 1.0, 101)
te = 200.0 * (1.0 - 0.9 * psi_norm) ** 1.5 + 10.0 # an analytic T_e [eV] on psi_norm
rho = np.linspace(0.2, 0.8, 4)
a_lt = profile_gradient(te, psi_norm, "psi_norm", equilibrium=cmap,
coordinate="rho_tor_norm", convention="tglf", at=rho)
r_lt = profile_gradient(te, psi_norm, "psi_norm", equilibrium=cmap, coordinate="rho_tor_norm",
gradient_coordinate="r_minor", reference_length="R_major_axis", at=rho)
r_lt.values / a_lt.values # R0/a at every point: the same physical derivative
a_lt.metadata["mathematical_definition"] # '-a * d(log(f)) / d(r_minor)'
Each result carries quantity, source_quantity, plot_coordinate, gradient_coordinate,
reference_length (definition, symbol, value, unit, provenance), requested_convention, the
resolved convention and mathematical_definition.
Presets
tglfandcgyroresolve tor_minoranda_minor, the GACODE definition. On the locpargen 48224 fixture,tglfreproducesRLTS_*/RLNS_*.gs2needs the run’sirhoandlocal_eq:irho = 2with a numerical equilibrium resolves toa_minor;irho = 2with Miller (local_eq) resolves to the run’s ownL_ref;- other
irhovalues are refused.
gkwneedsgeom_type:circresolves toR_major_axis;s-alpharesolves to the run’sL_ref;- shaped geometries are refused, because their $\psi$ uses the surface’s global $R$ extremes.
genetakes the run’s own gradient coordinate andL_refand is never a fixed major radius.
Whatever cannot be formed is refused with ValueError, never extrapolated:
- a non-monotonic map;
- a point outside the radial support;
- a profile value that is not positive;
- an unavailable coordinate or reference length.
The *_profile_gradient plot views are built on this layer; see
Normalized gradients.
Solver convention
A solver convention is how a code, a file format or an IMAS version stores a state: the COCOS index, the sign and direction choices it implies, the flux unit (Wb or Wb/rad), the poloidal-angle Jacobian of a solver mesh, and a code’s normalizations. It changes numbers without changing physics. Quantities from two sources are comparable only after their conventions are converted to a common one.
vaft.data.EquilibriumConvention(cocos,psi_per_radian, sign fields,source, andcontradictedwhen the data disagree with the declaration).vaft.process.convert_cocos(eq, target)converts it explicitly. Process reference: cocos.vaft.process.straight_field_line_tablesreads the angle tables of a DCON/GPEC mesh and requiresjacobian=to be named.lab_to_straight_field_lineapplies them.TGLFNormalisationinvaft.code.gacode.tglf.inputsgives TGLF’s $a$, $c_s$, $\rho_s$ and $B_\mathrm{unit}$ from explicit definitions, checked againstlocpargen.
Is not: a Fourier curve parameter is not a magnetic poloidal angle. FourierSurface uses an
arc-length angle, which differs from both the geometric and the PEST angle on the same surface.
Reading a solver’s angle (straight_field_line_tables) is also different from constructing one
from the equilibrium (straight_field_line_map).
The hierarchy in VAFT
EquilibriumData is a single-slice, axisymmetric working model for numerical
algorithms, not a persistence format. GEQDSK, ODS, and native IDS remain the
storage and interchange records. A DerivedValue records its SI unit,
definition, source fields, convention, method, tolerances, and quality; missing
or ambiguous inputs produce an unavailable value with a reason. VAFT does not
guess one COCOS index when observable signs admit several.
The normalized radial coordinates are
$\psi_N=(\psi-\psi_{axis})/(\psi_{boundary}-\psi_{axis})$,
$\rho_{pol,N}=\sqrt{\psi_N}$, and
$\rho_{tor,N}=\sqrt{\int q\,d\psi/\int_{boundary}q\,d\psi}$.
A non-monotonic toroidal-flux mapping is reported, rather than repaired by
taking absolute values. Shape descriptors use
major_radius=(R_out+R_in)/2; the LCFS area centroid is recorded separately
because it is the radius needed for Pappus’s volume theorem. Poloidal-field
calculations honor the COCOS e_Bp factor, so beta_p and li agree across
Wb and Wb/rad representations.
Local Miller fits report RMS, maximum, and Hausdorff contour errors. Fits near
the separatrix (psi_n >= 0.995) or within 0.05a of an X-point are flagged
because the local form is unreliable there. The analytic Solov’ev model assumes
axisymmetric constant-p' and constant-FF' solutions; it is an example and
regression model rather than a general experimental equilibrium solver. Edge
dRsep is the upper-minus-lower X-point flux-surface radius at the outboard
midplane, and is reported only for diverted configurations.
Boundary topology is inferred from the flux map without machine-specific
geometry. Stationary points are classified by the Hessian determinant; a
saddle becomes an active X-point only if its flux matches the boundary within
a curvature- and grid-dependent window and the confined region reaches it.
Irrelevant numerical saddles remain in x_points with active=False.
A diverted boundary yields upper single-null, lower single-null, or
double-null topology. A wall-contacting LCFS without an active X-point is
limited; a clipped region, missing wall, or indeterminate boundary is
AMBIGUOUS with a reason.
The branches of #1201 §2, their category, and where each starts in the code. Status follows the acceptance audit posted on #1201: met means a public API plus a test or documentation demonstrates it.
| Branch | Category | Canonical entry points | Status |
|---|---|---|---|
| Radial coordinates | coordinate | vaft.process.derive_radial_coordinates, vaft.process.MappedPositions, vaft.process.psi_to_radial (midplane radii) |
partly met |
| Poloidal / magnetic coordinates | coordinate | vaft.process.straight_field_line_map (native PEST), vaft.process.straight_field_line_tables (read DCON/GPEC), vaft.formula.generalized_straight_field_line_angle, vaft.formula.sfl_toroidal_angle_shift |
partly met |
| Physical-space views | projection | Profile1D and Field2D in vaft.plot, vaft.plot.equilibrium_geometry_topview, vaft.plot.Geometry3DLayers |
partly met |
| Diagnostic projections | projection | vaft.omas.camera_projection_for, vaft.omas.compute_camera_visible_efit_overlay |
partly met |
| Compact geometric representations | representation | vaft.process.fit_miller_surface, vaft.process.fit_fourier_surface, vaft.process.evaluate_fourier_surface, vaft.data.MXHChebyshevRepresentation |
Miller and Fourier met |
| Analytic hierarchy | representation | vaft.process.solovev_to_equilibrium, vaft.process.solovev_example, vaft.process.guazzotto_freidberg_to_equilibrium, vaft.process.fit_solovev |
partly met |
| Kinetic / gradient representations | derived quantity | vaft.process.profile_gradients.profile_gradient, vaft.process.profile_gradients.radial_coordinate_map, vaft.formula.normalized_gradient_scale_length, vaft.data.SyntheticKineticProfiles, vaft.data.GradientProfile |
met: process layer and *_profile_gradient views |
| Boundary / topology | derived quantity | vaft.process.derive_boundary_representation, vaft.data.BoundaryRepresentation, vaft.data.Topology, vaft.process.contour_shaping_observables |
shape vs topology met |
| Perturbed / helical | representation | vaft.process.MagneticIslandSpec, vaft.process.magnetic_island_topology, GpecCylindricalOutput in vaft.code.gpec |
partly met |
| Conventions (all branches) | solver convention | vaft.data.EquilibriumConvention, vaft.process.convert_cocos, vaft.process.make_equilibrium_field_interpolator |
partly met |
The per-function pages are under Process reference, Formula reference and the Plot reference.
The provenance thread
Every object derived from an equilibrium should point back to the same state. Five things identify that state, and each has a place in the code:
| Thread | Where it lives |
|---|---|
| source | EquilibriumData.metadata, EquilibriumConvention.source; DerivationProvenance.source_type and .source_fields |
| time | EquilibriumData.time; DerivationProvenance.source_time |
| COCOS and flux unit | EquilibriumData.convention.cocos and .psi_per_radian; DerivationProvenance.convention |
| method | DerivationProvenance.method, .interpolation, .fit_range, .radial_coordinate |
| residual | DerivedValue.quality; MillerFitResult / FourierFitResult errors; SolovevFit.metrics; camera reproj_mean_px |
Same shot and time does not mean the same equilibrium. The package also ships a standalone 39915 g-file at 319 ms, which is a different EFIT run stored per radian. The source record, not the timestamp, identifies the state.
The check below runs on the packaged sample. It asserts the COCOS instead of trusting it, then confirms that each derived object still names the same time and convention.
index = int(np.argmin(np.abs(np.asarray(ods["equilibrium.time"]) - 0.319)))
eq = vaft.process.as_equilibrium(ods, time_index=index, convention=11)
assert eq.convention.cocos == 11 and eq.convention.psi_per_radian is False # psi in Wb
coords = vaft.process.derive_radial_coordinates(eq)
for name, item in coords.items():
p = item.provenance
assert item.available, item.reason
assert abs(p.source_time - eq.time) < 5e-4 and p.convention.cocos == eq.convention.cocos
print(f"{name:10s} {item.definition:55s} [{p.method}]")
miller = vaft.process.fit_miller_surface(eq.lcfs)
fourier = vaft.process.fit_fourier_surface(eq.lcfs, modes=6)
print("Miller nRMS", round(miller.normalized_rms_error, 4), "accepted", miller.accepted)
print("Fourier nRMS", round(fourier.normalized_rms_error, 4), "angle", fourier.surface.angle_convention)
print("Miller provenance carries a convention:", miller.provenance.convention is not None) # False: #604
eq_rad = vaft.process.convert_cocos(eq, 1) # same state, psi per radian
print("flux-span ratio", (eq.psi_boundary - eq.psi_axis) / (eq_rad.psi_boundary - eq_rad.psi_axis)) # 2*pi
The last lines show where the thread breaks today. A Miller fit of a bare contour carries no time or convention (#604), so the caller has to keep them.
The whole chain is notebooks/equilibrium_representation_reference.ipynb. It takes the 39915
slice at 0.319 s through the four radial coordinates, the R–Z map, a PEST grid with a $q$
residual, Miller and Fourier fits, $a/L_T$ in four conventions, rational surfaces, a prescribed
island with a field-line check, a 3-D embedding and a camera projection, and checks source, time,
COCOS and flux unit at each step. The GPEC response step is missing because no GPEC output is
packaged.
Gaps
These are the items the #1201 audit grades not met or partly met, with the issue that owns each. “Unowned” means no issue other than #1201 tracks it.
Partly met
- Radial coordinates:
- The process layer names coordinates
psi_n/rho_*_n, while profiles and plots use*_norm. - The packaged 39915 ODS stores $\sqrt{\psi_N}$ under
profiles_1d.rho_tor_norm. The plot recipe detects this withvaft.data._derived.is_rho_pol_proxy, but a direct reader of the leaf does not (unowned).
- The process layer names coordinates
- Geometric radii.
r_minor,r_minor_normandr_centerare defined invaft.process.profile_gradients.RADIAL_COORDINATES(#551).- The older tuples in
vaft.plotandvaft.process.profileare not unified with it. vaft/plot/onedim.pystill uses $r - r_{axis}$ (#479).
- Flux coordinates. The native map is PEST only. There is no direct inverse $(\psi,\theta^*)\to(R,Z)$, no Jacobian output, no native Boozer or Hamada map (only the per-surface formulas), and no closure or Jacobian residual on the map (#472).
- Angle identity on the read path:
straight_field_line_tablesreturns bare(lab, sfl)tuples.vaft/machine_mapping/mhd_linear.pydefaults an unrecorded Jacobian to"hamada"(unowned).
- On-surface view. There is no first-class $f(\theta \mid \psi)$ view; the notebook builds one
from
StraightFieldLineMap.surface()(#1201 phase E). - Projections:
- There is no projection abstraction and no general 3-D embedding.
sweep_toroidalis a camera helper that works in centimetres and calls the toroidal angletheta. - The camera overlay reads raw ODS paths instead of going through
as_equilibrium. - The camera’s toroidal frame is not reconciled with the port frame (#746).
- There is no projection abstraction and no general 3-D embedding.
- Compact representations:
MillerSurfacemixes geometry with $q$, shear and $\alpha$.BoundaryRepresentation.fourier_coefficientsandMXHChebyshevRepresentationdo not record their angle convention (#942).
- Flow equilibria.
EquilibriumDatahas no governing-equation field. A Guazzotto–Freidberg flow case is kept distinct only because exporting it raises (#1149). - Code presets. TGLF and CGYRO resolve fixed. GS2 and GKW resolve only the configurations their manuals fix, and GENE resolves only from the run’s own metadata (#551).
- Advanced divertors. Snowflake, X-divertor and Super-X exist only as a TES input (#950).
- Islands and perturbations (unowned; #886 covers the prescribed island):
- The solver-derived island separatrix is reconstructed only inside the plot backend.
- No island record says whether it is prescribed or solver-derived.
- There is no public real-space or diagnostic-space projection of a complex GPEC perturbation.
- Conventions:
- A default-constructed record falls back to Wb/rad (#603).
- Miller provenance carries no convention (#604).
- Discovery. No API lists the available coordinates, projections and representations with their required source data (#1201 phase H).
Two audit findings have been fixed since the audit:
- The kinetic plot recipes drew $\sqrt{\psi_N}$ under a $\psi_N$ label (#1314).
make_equilibrium_field_interpolatorassumed Wb/rad when no COCOS was given. It now reads the convention from the record (#1313).
See also
- Equilibrium and kinetic profiles: radial mapping of kinetic channels.
- Geometric approximations: slab, cylinder and torus as model geometries, which are a different axis from the representations here.
- Scientific diagrams: the tokamak-geometry and straight-field-line sections.
- Notebooks:
equilibrium_representation_reference.ipynb,edge_and_boundary_representation.ipynb,compact_equilibrium_representation.ipynb,local_miller_equilibrium_fitting.ipynb,analytic_solovev_equilibrium.ipynb,analytic_island_model_and_synthetic_response_model.ipynb.