Fixed-Boundary Equilibrium Tutorial

In a fixed-boundary equilibrium calculation, the outer plasma boundary shape is known and held fixed. You prescribe the outermost shape of the plasma using Fourier coefficients, along with radial profiles for pressure and rotational transform or toroidal current. VMEC++ then computes the nested flux surfaces inside that boundary that satisfy ideal magnetohydrodynamic (ideal-MHD) force balance:

\[ \mathbf{J} \times \mathbf{B} = \nabla p \]

This tutorial walks through setting up and running a fixed-boundary case, followed by recommended settings for accuracy and convergence.

Basic Workflow

1. Running from an Input File

The most common way to run a fixed-boundary equilibrium is from a classic Fortran input.* file or a VMEC++ JSON file:

import vmecpp

# Load input configuration from file
vmec_input = vmecpp.VmecInput.from_file("examples/data/w7x.json")

# Ensure fixed-boundary mode is active (default is False)
vmec_input.lfreeb = False

# Run the solver
output = vmecpp.run(vmec_input)

# Check convergence and inspect results
print(f"Force residual reached: {output.wout.fsqr}")
print(f"Plasma volume: {output.wout.volume:.4f} m^3")

# Save the standard NetCDF output file
output.wout.save("wout_w7x.nc")

2. Building an Input Object Programmatically

You can also construct or modify the VmecInput object directly in Python:

import numpy as np
import vmecpp

# Create input with basic grid sizes and profiles
vmec_input = vmecpp.VmecInput(
    nfp=1,  # Number of field periods (e.g. 1 for tokamak)
    mpol=6,  # Number of poloidal Fourier modes
    ntor=0,  # Number of toroidal Fourier modes (0 for axisymmetric)
    ns_array=np.array([17, 33, 65]),  # Multigrid radial resolutions
    niter_array=np.array([500, 1000, 2000]),  # Max iterations per grid
    ftol_array=np.array([1e-6, 1e-8, 1e-10]),  # Target force tolerance
    am=np.array([1.0, -1.0]),  # Pressure profile: p(s) = p0 * (1 - s)
    pres_scale=1.0e4,  # Pressure scaling factor in Pa
    ai=np.array([1.0, -0.5]),  # Iota profile: iota(s) = 1.0 - 0.5 * s
    lfreeb=False,  # Fixed boundary
)

# Set the boundary Fourier coefficients (R ~ cos, Z ~ sin)
vmec_input.rbc = vmecpp.VmecInput.resize_2d_coeff(
    np.array([[3.0], [1.0]]),  # Major radius R0=3.0, minor radius r=1.0
    vmec_input.mpol,
    vmec_input.ntor,
)
vmec_input.zbs = vmecpp.VmecInput.resize_2d_coeff(
    np.array([[0.0], [1.0]]),  # Circular cross-section
    vmec_input.mpol,
    vmec_input.ntor,
)

# Set the initial guess for the magnetic axis
vmec_input.raxis_c = np.array([3.0])
vmec_input.zaxis_s = np.array([0.0])

# Run VMEC++
output = vmecpp.run(vmec_input)
print(f"Solved successfully in {output.wout.itfsq} iterations.")