Basic API¶
The API you use daily: the lazily imported vmex top-level exports
(import vmex as vj) plus the three modules behind gradient-based work.
Everything else — the per-module solver internals and the mirror lane — is
in Advanced API.
Top-level package¶
Every name in vmex.__all__ is listed below by group, each linked to the
module that documents it; this page is an index, not a second copy of every
docstring.
vmex: a JAX implementation of VMEC2000 for fixed and free-boundary equilibria.
Every name below is a lazy attribute of the top-level package
(import vmex as vj): the owning module is imported on first access, and
vmex.__all__ lists exactly these names plus __version__. Each entry
links to the module that documents it.
Inputs and run controls
VmecInput— INDATA / VMEC++-JSON input pytreeRunOptions/InputRequest/read_input_request()—!@VMEXdirectives and the JSON_vmexsection (execution metadata, never physics)
Solvers
solve()— single-grid fixed-boundary solvesolve_multigrid()— NS_ARRAY ladder (runvmec.f)solve_file()— run a deck the way the CLI does (directives honored,wout_<case>.ncwritten)solve_free_boundary()— NESTOR free boundarysolve_free_boundary_multigrid()— free-boundary laddermake_free_boundary_config()/solve_free_boundary_implicit()/solve_free_boundary_implicit_status()— coupled NESTOR/VMEC implicit derivativestate_from_wout()/restart_state()— hot restart from any wout (alsosolve*(..., restart_from=...))
Outputs and scaling
gk_fieldline_geometry_from_wout()— GK field-line geometry from any compatible wout, without a solvescale_input()/scale_wout()/scale_mgrid()— dimensional similarity transforms
High-order reconstruction and strong-force certificate
HighOrderEquilibriumState/high_order_state_from_wout()/lift_high_order_state()— axis-regular continuous reconstructionevaluate_high_order_fields()→HighOrderFieldSamples;evaluate_high_order_surface()→HighOrderSurfaceSamplesevaluate_strong_force()→StrongForceSamples;certify_strong_force()→StrongForceReport(carrying twoForceErrorNormalizations, whole-domain and windowed) /plot_strong_force_report()— independent strong-force certificateboozer_spectrum_state()/boozer_spectrum_high_order()— Boozer|B|spectrum without a sampled radial mesh
Force-balance polishing
PolishConfig/PolishContext/PolishResult/PolishReport— strong-root correction (solve*(..., polish=...))PolishLinearConfig/collocation_polish_tangent()/collocation_polish_adjoint()/implicit_collocation_polished_state()— derivatives through a polished root
Optimization
vmex.optimize— objectives + least-squares driver (module)vmex.implicit— implicit differentiation of the equilibrium (module)vmex.parallel— concurrent ensembles of independent solves (module)VmecProblem/FunctionProblem/Evaluation— optimizer-neutral value, residual, and derivative callablesOptimizationMonitor/OptimizationRecord— accepted iterations;EquilibriumReporter— compact diagnostics
Post-processing and plotting
run_booz_xform()— Boozer transform (booz_xform_jax)epsilon_effective_from_wout()/epsilon_effective_from_boozer()— optional NEO_JAX effective-ripple profilegamma_c_from_wout()— fast-ionGamma_cprofile from any compatible wout, without a solveessos_vmec_field()— hand a solved equilibrium to ESSOS as anessos.fields.Vmec(optional ESSOS dependency)trace_alphas()→AlphaTracingResult/plot_tracing()— optional ESSOS alpha-particle tracing (exact loss fraction; alsovmex --trace)plot_wout()/plot_boozmn()/plot_bootstrap_current()/plot_optimization_movie()/plot_optimization_objects()— wout, boozmn, bootstrap, optimization-history, and surfaces-and-coils plots
External fields
MgridData/MgridField/read_mgrid()/write_mgrid()/tabulate_cartesian_field()— mgrid or tabulated direct field (MgridField.from_coilstabulates an ESSOS coil set)MagneticField— base JAX field with explicit and stored-point evaluation;VmecInteriorField— field inside the plasma;VmecExtender— field outside the plasma surfacePlasmaVacuumInterface/surface_field_data_from_state()/surface_field_data_from_high_order()/surface_field_data_from_wout()— virtual-casing diagnostics on a prescribed plasma-vacuum interface
Errors, diagnostics, and modules
vmex.errors— typed zero-crash exceptions (module); also exported directly:VmecError,VmecInputError,VmecJacobianError,VmecConvergenceError,VmecNumericalError,MgridNotFoundError,StrongForceContinuationError,StrongForceCertificationError,StrongForceLinearSolveErrorvmex.doctor— installation diagnostics behindvmex --doctor(module)vmex.core— the solver internals (module)
The vmex console entry point (vmec is an alias) lives in
vmex.core.cli.
Inputs¶
VMEC input handling: the &INDATA namelist and structured JSON.
VMEC2000 counterparts: LIBSTELL/Sources/Modules/vmec_input.f
(read_indata_namelist: variable set and defaults) and readin.f
(post-read normalizations). The JSON schema follows VMEC++
(vmecpp.VmecInput): identical key names, boundary coefficients as sparse
{"m": int, "n": int, "value": float} lists, dense axis arrays, and
adiabatic_index accepted as an alias for gamma.
VmecInput is a frozen dataclass holding the INDATA content this code
base actually consumes, with VMEC2000 defaults. Parsing is host-side NumPy
code (nothing here needs JAX). Controls which would change the mathematical
problem or iteration contract but are not implemented are rejected by
UnsupportedInputModeError; they are never silently converted into an
ordinary fixed-/free-boundary solve.
Normalizations applied on construction (all from VMEC2000):
read_indata_namelist:raxis_s[0] = 0andzaxis_s[0] = 0; the obsoleteRAXIS/ZAXISarrays overrideRAXIS_CC/ZAXIS_CSwhere nonzero;niter_arrayfalls back toNITERwhen absent.readin.f: the explicit legacyNS_ARRAY(1)=0form expands to[max(3, NSIN), 31];lfreebis forcedFalsewhenmgrid_file == 'NONE';nvacskip <= 0falls back tonfp.Boundary coefficients outside
|n| <= ntor,0 <= m < mpolare dropped (VMEC2000 reads them into oversized arrays but never uses them).
Index conventions: rbc/zbs/rbs/zbc are dense 2D arrays of shape
(2*ntor + 1, mpol) indexed [n + ntor, m], i.e. rbc[n + ntor, m]
is the INDATA coefficient RBC(n, m).
- exception vmex.core.input.UnsupportedInputModeError(code: str, control: str, reason: str)¶
An input requests semantics which VMEX does not implement.
codeis a stable, value-free diagnostic code suitable for the privacy-preserving input checker.controlnames the INDATA/JSON control without echoing its value or any equilibrium data.
- class vmex.core.input.VmecInput(lasym: bool = False, nfp: int = 1, mpol: int = 6, ntor: int = 0, ntheta: int = 0, nzeta: int = 0, ns_array: Any = None, ftol_array: Any = None, niter_array: Any = None, delt: float = 1.0, tcon0: float = 1.0, lforbal: bool = False, lmove_axis: bool = True, lfull3d1out: bool = False, aphi: Any = None, phiedge: float = 1.0, nstep: int = 10, time_slice: float = 0.0, pmass_type: str = 'power_series', am: Any = None, am_aux_s: Any = None, am_aux_f: Any = None, pres_scale: float = 1.0, gamma: float = 0.0, spres_ped: float = 1.0, ncurr: int = 0, pcurr_type: str = 'power_series', ac: Any = None, ac_aux_s: Any = None, ac_aux_f: Any = None, curtor: float = 0.0, piota_type: str = 'power_series', ai: Any = None, ai_aux_s: Any = None, ai_aux_f: Any = None, bloat: float = 1.0, raxis_c: Any = None, zaxis_s: Any = None, raxis_s: Any = None, zaxis_c: Any = None, rbc: Any = None, zbs: Any = None, rbs: Any = None, zbc: Any = None, lfreeb: bool = True, mgrid_file: str = 'NONE', extcur: Any = None, nvacskip: int = 1, mfilter_fbdy: int = -1, nfilter_fbdy: int = -1, precon_type: str = 'NONE', prec2d_threshold: float = 1e-30, restart_wout: str = '')¶
Full
&INDATAcontent with VMEC2000 semantics and defaults.Defaults are the initializations in
read_indata_namelist(vmec_input.f), after thereadin.fnormalizations documented in the module docstring. Array fields are NumPy arrays;Nonedefaults are resolved in__post_init__(they depend onmpol/ntor).- change_resolution(*, mpol: int, ntor: int, ntheta: int | None = None, nzeta: int | None = None) VmecInput¶
Return a copy at the requested Fourier and real-space resolution.
Fourier and axis coefficients present at both resolutions are copied; newly added modes are zero.
nthetaandnzetakeep their current values when omitted, including0for VMEC’s automatic grid choice. This method applies no optimization policy: callers choose every resolution explicitly.
- classmethod from_file(path: str | Path) VmecInput¶
Read a VMEC input file, auto-detecting INDATA vs JSON format.
Files whose first non-whitespace character is
{(or with a.jsonsuffix) are parsed as structured JSON; everything else as a classic&INDATAFortran namelist (VMEC2000readin.f).
- classmethod from_indata_text(text: str) VmecInput¶
Build from
&INDATAnamelist text (VMEC2000 read_indata_namelist).
- classmethod from_json_text(text: str) VmecInput¶
Build from structured JSON text (plan Appendix C / vmecpp.VmecInput).
Same key names as the dataclass fields;
adiabatic_indexis accepted as an alias forgamma;rbc/zbs/rbs/zbcare sparse{"m", "n", "value"}lists; axis arrays are dense. The VMEC++free_boundary_method="nestor"spelling is accepted. Other free-boundary methods and unknown keys fail explicitly instead of being silently ignored.
- to_json(path: str | Path) Path¶
Write VMEC++-schema JSON that round-trips through
from_file().Boundary coefficients are written as sparse
{"m","n","value"}lists (nonzero entries only); axis and profile arrays are dense.
DESC input bridge; WOUT is produced by the ordinary VMEX solve.
- vmex.core.desc.is_desc_file(path: Path) bool¶
Recognize DESC outputs or native text decks without importing DESC.
- vmex.core.desc.write_desc_input(source: Path, outdir: Path | None = None, *, tolerance: float = 0.01) Path¶
Read a DESC equilibrium without DESC and write a compact VMEC input.
The tolerance bounds discarded boundary position and angular derivatives, not the solved magnetic field. Zero retains all nonzero boundary modes.
Run directives (!@VMEX comment lines and the JSON _vmex section) are
execution metadata and never become VmecInput
fields; they are parsed and resolved here. The precedence rule is stated
with the polishing options in Advanced API.
VMEX execution metadata carried outside the VMEC physics schema.
VMEX-only run controls travel in places every legacy reader ignores: comment
directives in &INDATA text and a reserved _vmex section in structured
JSON. VMEC2000 treats the directive lines as comments and solves the ordinary
input; a VMEC++-compatible JSON reader sees the physics schema once _vmex
is removed. VmecInput stays a pure physics object —
these options control execution, not equilibrium physics, so they never
become dataclass fields on it.
Two directive spellings are accepted, both VMEC-safe comments:
!@VMEX POLISH = AUTO
!@VMEX POLISH_TOL = 1.0E-8
!@VMEX POLISH_FAIL = ERROR
!@VMEX POLISH_DEGREE = 5
!@VMEX POLISH_MAX_ITER = 40
!@VMEX POLISH_SPANS = 16
!@VMEX POLISH_BUDGET = 3600
and the original single-flag form from the polishing integration:
! VMEX: POLISH_FORCE_BALANCE = .TRUE.
Precedence, resolved by resolve_run_options(), is exactly:
CLI option > explicit Python keyword > file directive > package default.
- class vmex.core.run_options.InputRequest(input: VmecInput, options: RunOptions, source: Path)¶
One parsed input file: the physics and how to run it.
The result of
read_input_request(), which is the only place VMEX builds one. It exists because the two halves of a deck have different lifetimes: the physics goes on to the solver unchanged, while the execution options are merged with CLI flags and Python keywords byresolve_run_options()before anything runs.- input¶
The parsed physics deck, a
VmecInput. It carries no execution metadata:!@VMEXdirective lines are Fortran comments to the&INDATAparser, and the JSON_vmexsection is discarded byVmecInput.from_json_text.- Type:
- options¶
The
RunOptionsread from those same directives or from the JSON_vmexsection. A deck with no directives yieldsRunOptions(), i.e. every package default.- Type:
- source¶
The path the deck was read from, kept so a caller can name outputs or resolve paths relative to the deck. VMEX itself only records it.
- Type:
Path
- class vmex.core.run_options.RunOptions(polish: bool | str = False, polish_tol: float | None = None, polish_fail: str = 'error', polish_degree: int | None = None, polish_max_iter: int | None = None, polish_spans: int | None = None, polish_budget: float | None = None)¶
How to execute a solve; never what equilibrium to solve.
polishisFalse,True, or"auto"(polish only when the legacy solve converged and the physics is in the supported set).polish_tol/polish_degree/polish_max_iter/polish_spansoverride the matchingPolishConfigfields (tolerance,radial_degree,max_nonlinear_iterations,radial_spans) when set; only knobs that exist on the driver config are exposed here.polish_failmaps onto the driver’s fail policy:"error"raises,"fallback"returns the unpolished state silently,"warn"returns it with aRuntimeWarning.polish_budgetis the wall-clock ceiling in seconds thatPOLISH = AUTOwill commit to (auto_budget_seconds); it raises or lowers the cost at which AUTO declines to polish and has no effect onPOLISH = ON, which never consults it.
- vmex.core.run_options.format_indata_directives(options: RunOptions) str¶
Serialize non-default options as canonical directive lines.
Only the
!@VMEX KEY = VALUEspelling is written; the legacy! VMEX: POLISH_FORCE_BALANCEform is read but never emitted. Round trips throughparse_indata_run_options().- Parameters:
options – The options to serialize. A field equal to its
RunOptionsdefault (polish,polish_fail) or left atNone(the four numeric overrides) is omitted, so an unmodifiedRunOptions()produces nothing.- Returns:
The directive lines as one string terminated by a newline, or the empty
string when every option is at its default. Prepend it to
&INDATAtext (VMEC2000 reads the lines as comments.)
- vmex.core.run_options.parse_indata_run_options(text: str) RunOptions¶
Read every VMEX directive from raw
&INDATAtext.Both spellings are read; a repeated key must repeat the same value, and a conflicting repetition is an error rather than a silent last-one-wins. Ordinary Fortran comments and quoted
!characters are unaffected: the directive patterns anchor on comment lines only, and VMEC2000 discards those lines entirely.
- vmex.core.run_options.read_input_request(path: str | Path) InputRequest¶
Read one input file into physics plus execution options.
VmecInput.from_fileremains physics-only; this is the entry point the CLI andsolve_file()share.The format is chosen from the content, not only the name: a
.jsonsuffix or a first non-blank character of{selects the structured JSON reader, anything else the Fortran&INDATAreader. The file is read once as UTF-8 and both halves come from that same text, so the physics and the directives can never disagree about which revision was parsed.- Parameters:
path – Filesystem path to an
&INDATAdeck or a VMEC++-compatible JSON input.- Returns:
An
InputRequestholding the physics input, theRunOptionsparsed from the file, andpath. Pass theoptions to
resolve_run_options()to apply CLI and Pythonoverrides; a malformed or unknown directive raises
VmecInputErrorhere rather than silentlyrunning with defaults.
- vmex.core.run_options.resolve_run_options(file_options: RunOptions | None, *, polish: bool | str | None = None, polish_tol: float | None = None, polish_fail: str | None = None, polish_degree: int | None = None, polish_max_iter: int | None = None, polish_spans: int | None = None, polish_budget: float | None = None) tuple[RunOptions, dict[str, str]]¶
Apply the documented precedence and record where each value came from.
Explicit Python keywords override the file; the CLI passes its flags through the same keywords, so
CLI > Python > file > defaultreduces to one merge. The source map ("python","file","default"per field) goes into the run report so a surprising activation is traceable.- Parameters:
file_options – Options parsed from the deck, normally
InputRequest.options.Noneis treated asRunOptions().polish – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.polish_tol – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.polish_fail – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.polish_degree – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.polish_max_iter – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.polish_spans – Explicit overrides, each with the meaning of the matching
RunOptionsfield.Nonemeans “not specified” and leaves the file or default value in place, so an override cannot be used to reset a field back toNone.
- Returns:
``(options, source)`` (the merged
RunOptions, and a mapping)from every
RunOptionsfield name to"python","file",or
"default"saying which layer supplied it. A field is creditedto
"file"only when the deck’s value differs from the packagedefault.
- vmex.core.run_options.strip_vmex_json(data: Mapping[str, Any]) tuple[dict[str, Any], RunOptions]¶
Split a structured-JSON mapping into physics data and run options.
The physics schema remains VMEC++ compatible after
_vmexis removed. Unknown_vmexkeys fail explicitly — a typo must not silently run with defaults.
Differentiation and optimization¶
A converged vmex.core.optimize.Equilibrium exposes
equilibrium.solution (the spectral equilibrium arrays) and
equilibrium.solver_context (read-only grids, profiles, and constants).
The shorter state and runtime attribute names remain compatible with
existing code; runtime never means elapsed wall-clock time.
Implicit differentiation of the fixed-boundary equilibrium (§6).
The converged equilibrium is a root of F(x, p) = 0 with x the
SpectralState and p the differentiable
run parameters (ImplicitParams: dense INDATA boundary arrays,
phiedge, pres_scale, curtor and the am/ai/ac profile
coefficients). solve_implicit() wraps the opaque host solver
(vmex.core.solver.solve() / solve_multigrid) in
jax.custom_vjp; the backward pass solves the adjoint linear system
(dF/dx)^T lambda = g_x
exactly: at the root the gradient needs only the multiplier of the raw force
residual, whose Jacobian is block tridiagonal in radius, so one transposed
block-Thomas solve refined once against an independent pullback, staged as one
reusable per-config executable (_adjoint_block_core), returns
g_p - mu^T dF_raw/dp with one more VJP — O(1) memory in the forward
iteration count. Recycling GCROT(m, k) on the preconditioned transpose
(_adjoint_gcrot_core) is the host fallback when that certificate fails.
The adjoint solve executes host-eagerly on the caller’s
thread and is bound to the config’s carried device on its own (see the
“Adjoint execution site” section note); its convergence is enforced — a
missed certificate raises the typed
AdjointSolveError instead of silently returning a
plausible-but-wrong gradient — and setting VMEX_ADJOINT_DEBUG=1 prints
per-stage device/norm lines for hardware placement triage.
Residual formulation (documented choice)¶
F is the self-consistently preconditioned force gc of a single
fresh evaluate_forces() pass (cache=None):
the bcovar.f preconditioner/force norms/tcon are recomputed from the
current (x, p) rather than frozen at the converged cache. This makes
F a fixed, smooth function of (x, p) — required by the implicit
function theorem — while remaining exactly as well-conditioned as VMEC’s own
preconditioned iteration. Correctness: gc = M(x, p) f(x, p) with f
the raw (scalxc-scaled) spectral force and M the invertible linear
1D-preconditioner map (scale_m1 + scalfor tridiagonal solves +
faclam). At the root, dF = M df + dM f = M df up to O(|f|) =
O(ftol), so the implicit gradients of the preconditioned residual equal
those of the raw force residual to solver accuracy, and GMRES on dF/dx
inherits the preconditioning for free. The raw-force formulation
(formulation="raw") is kept for the informational with/without-
preconditioner comparison in the tests.
The m=1 constrained Z force is evaluated in its converged branch (zeroed —
residue.f90 zeroes gcz(m=1) once fsqz < 1e-6, which always holds
at the fixed point), so the corresponding constrained combinations are not
degrees of freedom: they are frozen at their converged values, exactly
mirroring the forward solver’s behavior near convergence.
Anchoring the fixed point¶
The theorem applies at a root of F, and the host solver does not stop at
one: ftol gates the sum of SQUARES of the force, so a solve it reports
converged still returns with |F| ~ sqrt(ftol) (2.7e-07 at ftol = 1e-12
on basic_non_stellsym_simsopt). Where dF/dz carries a small singular
value — the lasym m=1 families, 1.5e-04 — that residual is a 1.8e-03
displacement of the state, far enough that a solver-sensitive metric read
there is not the one whose derivative the adjoint computes. The host
callback therefore Newton-refines the state onto the root
(ImplicitConfig.refine_tol, _refined_state) before any lane reads
it, so the value, the objective’s cotangent and the linearization all sit at
the same point. Both must move together: with only the linearization
refined the two errors stop cancelling (d(sum DMerc)/d(RBS(1,1)) against
the frozen-path FD: rel 4.2e-03 at the host state, 5.7e-03 with the
linearization alone refined, 5.4e-07 with both).
Degrees of freedom / boundary handling¶
In fixed-boundary mode the R/Z edge spectral row never evolves: the full
state is assembled as x = mask*z + edge_mask*boundary(p) + frozen where
z are the evolved dofs, the edge row comes (differentiably) from the
boundary parameters, and the remaining entries (structurally zero families,
released m=1 combinations, the lambda axis row overwritten by the totzsp
closure) are frozen constants. The fixed-boundary dof mask is constructed
directly from those mode-table invariants; _dof_mask() remains the
independent structural-zero oracle and handles coupled free-boundary maps.
Gradient checking solver-sensitive metrics¶
The adjoint gradient is the derivative of the fixed point of the frozen
residual F — the preconditioner/tcon/m=1 branch/dof mask are captured
once at the base parameters, not re-derived. For solver-sensitive metrics
(iota at ncurr=1, mirror ratio, magnetic well, Boozer/QI residual) a
naive re-solve FD is not a valid reference — it can sign-flip; use
frozen_path_directional_fd(), which reproduces the adjoint to solver
accuracy (full rationale on that function; tests/test_implicit_grad.py).
Strict and optimization-safe callback lanes¶
jax.pure_callback converts any host exception into an opaque
JaxRuntimeError that embeds the whole host traceback and loses the
typed exception (__cause__ is None) — breaking the
vmex.core.errors zero-crash taxonomy. The strict diagnostic lane uses
the relay:
_host_solve_and_mask catches any VmecError,
stashes it in the single-slot _HOST_ERROR (host callbacks are serialized
per solve) and re-raises a SHORT sentinel RuntimeError;
_callback_solve pops the slot and re-raises the ORIGINAL typed exception
from None. im.run / solve_implicit() therefore fail with a
short typed error. Under jax.jit the sentinel surfaces at the jit
boundary instead (where the optimize.least_squares zero-crash penalty
lanes catch it).
Optimization uses solve_implicit_status() instead. A typed equilibrium
failure returns a fixed-shape fallback state and status code, after which the
objective selects a finite differentiable penalty. Unexpected programming
errors still propagate rather than masquerading as rejected trial points. An
invalid initial point is rejected by a strict host preflight before an
optimizer starts.
Parameter map¶
runtime_from_params rebuilds every p-dependent
RunSetup field traceably: the readin.f
boundary processing (with the theta-flip decision frozen from the reference
input — it is discrete), the profil1d.f flux/pressure/current profiles,
and the funct3d.f constraint baselines rcon0/zcon0 (which depend on
the boundary only — the edge row of any admissible state). Its output is
verified against run_setup() in
tests/test_implicit_grad.py.
- class vmex.core.implicit.ImplicitParams(rbc: Any, rbs: Any, zbc: Any, zbs: Any, phiedge: Any, pres_scale: Any, curtor: Any, am: Any, ai: Any, ac: Any, ac_aux_f: Any)¶
Differentiable run parameters (a JAX pytree).
rbc/rbs/zbc/zbsare the dense INDATA boundary arrays, shape(2*ntor + 1, mpol)indexed[n + ntor, m](physical, un-processed — exactlyVmecInputlayout, so e.g.RBC(0, 1)isrbc[ntor, 1]).am/ai/acare the dense profile coefficient arrays;ac_aux_fcontains optimizable current-spline knot values while the knot positions remain static in the input;phiedge/pres_scale/curtorare scalars.
- class vmex.core.implicit.ImplicitConfig(inp: VmecInput, resolution: Resolution, ftol: float, max_iterations: int, mode: str = 'cli', multigrid: bool = False, lconm1: bool = True, adjoint_tol: float = 1e-11, jacobian_adjoint_tol: float = 0.0001, jacobian_adjoint_maxiter: int = 10, adjoint_restart: int = 30, adjoint_maxiter: int = 300, adjoint_gcrot_m: int = 100, adjoint_gcrot_k: int = 20, refine_tol: float = 1e-10, max_fsq_ratio: float = 1000000.0, hot_restart: bool = False, device: Any = None)¶
Static (non-differentiable) context of one implicit solve.
- jacobian_adjoint_tol: float = 0.0001¶
Tolerance for certifying the columns of an implicit residual Jacobian, kept separate from
adjoint_tolbecause the two feed different consumers. A scalar gradient goes to a quasi-Newton method that accumulates curvature from it and wants every digit; a least-squares Jacobian only has to point a trust-region step, and the block factorization already backsolves every column before the certifier runs. Measured on the asymmetric quasi-axisymmetric stage: the certifier needs 542 iterations to reach 1e-6 and none to reach 1e-4, for a relative change in the Jacobian of 3.2e-5.
- jacobian_adjoint_maxiter: int = 10¶
Restart budget for the Jacobian column certifier. It corrects a direct block-tridiagonal solve, so a handful of cycles either lands it or says the factorization has stopped being a good preconditioner at this iterate; spending the full
adjoint_maxiterthere buys nothing and costs half an hour per Jacobian on an asymmetric stage.
- adjoint_gcrot_m: int = 100¶
reverse-adjoint GCROT(m, k) recycling solve (
_adjoint_solve_gcrot): inner FGMRES cycle sizemandkdeflation directions. At high mode number plain restarted GMRES stalls short ofadjoint_tolwithin its budget (the truncated cycle loses the small eigendirections); GCROT converges to the same lambda in fewer matvecs.
- refine_tol: float = 1e-10¶
Newton-refine the host state onto the root of the frozen residual whenever
|F(P(x*), p)|exceeds this (infdisables it). VMEC’sftolgates the SUM OF SQUARES of the force, so a solve the host reports converged still leaves|F| ~ sqrt(ftol)— 2.7e-07 onbasic_non_stellsym_simsoptatftol = 1e-12, which the near-null m=1 direction there (singular value 1.5e-04) puts 1.8e-03 from the root. The implicit-function-theorem derivative holds only AT the root, so the lane returns the refined state and differentiates there:|F|drops to ~1e-14 andd(sum DMerc)/d(RBS(1,1))from rel 4.2e-03 to 5e-07 against the frozen-path FD, for 14-26% of a forward solve and 9-14% of a value-and-gradient across the gradient decks.
- max_fsq_ratio: float = 1000000.0¶
Largest
(fsqr + fsqz + fsql) / ftolaccepted for implicit differentiation when a trial exhausts its iteration budget.
- hot_restart: bool = False¶
seed repeated host solves from the last converged state of this config (optimization trials; the fixed point — hence the gradient — is unchanged, only the iteration count drops). Makes the callback stateful across calls, so keep False for one-shot/diagnostic use.
- device: Any = None¶
the RESOLVED placement device (a
jax.Device) orNone. Carried in the static config so every stage of the operation — thepure_callbackhost solve (which runs on a runtime worker thread where the caller’s thread-localjax.default_devicecontext does NOT apply), the cached runtime template, the custom-VJP backward and the multi-RHS pullback — re-enters the same device context on its own, without relying on an outer user-supplied context. Without this, a gradient evaluated outside such a context creates its constants on JAX’s default device and mixes devices with the committed state.
- class vmex.core.implicit.ImplicitSolution(state: SpectralState, wb: Any, wp: Any, wmhd: Any, volume: Any, aspect: Any, iota_axis: Any, iota_edge: Any, runtime: SolverRuntime | None = None)¶
Differentiable outputs of
run()(a JAX pytree).runtimeis theSolverRuntimethatrun()built internally (runtime_from_params(params, cfg)), so objective callers can evaluate further(state, runtime)scalar targets without rebuilding it per evaluation. It is deliberately not part of the pytree (registered as a dropped field): the solution’s established pytree structure — six state leaves plus seven scalars — is unchanged, and a solution that round-trips throughflatten/unflatten(e.g. across ajax.jitboundary) comes back withruntime = None. Inside ajax.grad/jax.value_and_gradtrace ofrun()the attribute is available and fully traced, so gradients flow throughruntime-consuming objectives exactly as through an explicitruntime_from_paramsrebuild.
- class vmex.core.implicit.LinearResponseReport(residual_norm: Array, tolerance: Array, iterations: Array, converged: Array)¶
Per-right-hand-side residual certificate for an implicit response.
- vmex.core.implicit.params_from_input(inp: VmecInput, *, device: Any = None) ImplicitParams¶
Extract the differentiable parameters of an input as a pytree.
Omitted
device(like explicitNone) follows ordinary JAX placement. Passdevice="auto"to request VMEX’s measured CPU preference for implicit Jacobians, or pass"cpu"/"gpu"or ajax.Deviceexplicitly. High-level optimization entry points retaindevice="auto"as their default.
- vmex.core.implicit.input_with_params(inp: VmecInput, params: ImplicitParams) VmecInput¶
Host-side: a new
VmecInputwith the parameter values applied.
- vmex.core.implicit.runtime_from_params(params: ImplicitParams, cfg: ImplicitConfig) SolverRuntime¶
Differentiable (traceable) map
p -> SolverRuntime.Rebuilds every p-dependent
RunSetupfield with jnp operations: the processed boundary, theprofil1d.fflux/mass/current profiles (throughvmex.core.setup.flux_profiles(), which is traced inphiedge/pres_scale/curtor/am/ai/acand inr00), theprofil3d.finterior guess (whose edge row is the boundary — the initial interior is an initializer only) and the constraint baselinesrcon0/zcon0(functions of the edge row alone). All p-independent fields (grids,scalxc, axis arrays, static metadata) come from the reference runtime.Runs inside the config’s device context so freshly created constants (and the cached template) land on
cfg.deviceeven when the caller holds no outerjax.default_devicecontext.
- vmex.core.implicit.make_config(inp: VmecInput, *, ns: int | None = None, ftol: float | None = None, max_iterations: int | None = None, mode: str = 'cli', multigrid: bool = False, lconm1: bool = True, adjoint_tol: float = 1e-11, jacobian_adjoint_tol: float = 0.0001, jacobian_adjoint_maxiter: int = 10, adjoint_restart: int = 30, adjoint_maxiter: int = 300, adjoint_gcrot_m: int = 100, adjoint_gcrot_k: int = 20, refine_tol: float = 1e-10, max_fsq_ratio: float = 1000000.0, hot_restart: bool = False, device: Any = None) ImplicitConfig¶
Build the static config;
resolutionis the (final-stage) grid.deviceis the already-RESOLVED placement device (pass the result ofvmex.core.device.resolve_implicit_device(), not a policy string) — seeImplicitConfig.device.
- vmex.core.implicit.solve_implicit_with_aux(params: ImplicitParams, cfg: ImplicitConfig)¶
Return
(state, dof_mask)using the same callback as solve_implicit.
- vmex.core.implicit.implicit_state_tangent_multi_rhs(params: ImplicitParams, cfg: ImplicitConfig, x_star: SpectralState, dof_mask: SpectralState, tangent_batch: ImplicitParams, *, probe_chunk_size: int | str = 1, response_chunk_size: int | str = 1) tuple[SpectralState, LinearResponseReport]¶
State tangents for several parameter directions, factored once.
The pure-JAX raw-force Jacobian is exactly block tridiagonal in radius. One three-color assembly and SOLVAX factorization therefore initializes every right-hand side; a warm-started solve then certifies the ordinary preconditioned residual against
10 * cfg.adjoint_tol * ||rhs||. The two chunk sizes independently bound probe assembly and response solves; each is a positive int (default 1 — minimum memory, unchanged behavior) or opt-in"auto", which sizes the chunk from measured available memory on the config’s placement device divided by the per-column workspace computed exactly from the operand avals (seemeasured_chunk_size()for the memory model). The ordinary implicit reverse rule remains the default forsolve_implicit().
- vmex.core.implicit.implicit_state_pullback_multi_rhs(params: ImplicitParams, cfg: ImplicitConfig, x_star: SpectralState, dof_mask: SpectralState, gbar_batch: SpectralState, *, solver: str = 'gcrot', probe_chunk_size: int | str = 1, response_chunk_size: int | str = 1) ImplicitParams¶
Batched state-cotangent pullback with shared implicit-linearization setup.
This preserves the scalar solve_implicit VJP and only adds a helper for callers that already have several state cotangents for the same fixed point.
solver="gcrot"(default) solves every row with preconditioned Krylov.solver="block"factors the raw nearest-neighbor radial Jacobian once and solves every row exactly with the transposed factors, refined once and certified like the scalar rule. Runs inside the config’s device context (see_solve_implicit_bwd); the two chunk sizes bound probe assembly and right-hand-side solves independently — each a positive int (default 1, unchanged behavior) or opt-in"auto", sized from measured available memory and the exact per-column operand bytes (seemeasured_chunk_size()).
- vmex.core.implicit.run(source: VmecInput | str, params: ImplicitParams | None = None, *, ns: int | None = None, ftol: float | None = None, max_iterations: int | None = None, mode: str = 'cli', multigrid: bool = False, lconm1: bool = True, adjoint_tol: float = 1e-11, adjoint_restart: int = 30, adjoint_maxiter: int = 300, adjoint_gcrot_m: int = 100, adjoint_gcrot_k: int = 20, refine_tol: float = 1e-10, device: Any = None) ImplicitSolution¶
Differentiable fixed-boundary equilibrium: input -> outputs pytree.
paramsdefaults toparams_from_input(); pass a perturbed / tracedImplicitParamsto differentiate — and pass the SAMEdeviceto both calls (underjax.gradthe parameters are tracers, which carry no placement to infer):inp = VmecInput.from_file("input.solovev") gpu = jax.devices("gpu")[1] with device_scope(gpu): p0 = params_from_input(inp) grad = jax.grad(lambda p: run(inp, p).wb)(p0)
wmhdfollows the printedWMHDnormalization;gamma = 1inputs getwmhd = nan(as in VMEC). All outputs are differentiable inparams(state via the implicit adjoint; scalars additionally through their explicit parameter dependence).deviceaccepts"cpu","gpu"or ajax.Device; omitteddeviceandNoneleave placement to JAX. Usedevice_scope()around parameter creation and differentiation for a non-default accelerator."auto": whenparamsis supplied with CONCRETE arrays all committed to one device, that placement is preserved and used for the whole operation; otherwise (no params, tracers underjax.grad, or mixed placement) the default CPU preference of this launch-bound path applies — passdevice=explicitly when differentiating on an accelerator. An explicit hardware device moves a supplied parameter pytree consistently.The returned solution also carries the internally built
SolverRuntimeassol.runtime(a non-pytree convenience attribute, seeImplicitSolution), so objective code can evaluate additional(state, runtime)targets — e.g.optimize.mean_iota(sol.state, sol.runtime)— without repeatingruntime_from_params(params, make_config(...))per evaluation.
- vmex.core.implicit.measured_chunk_size(dim: int, per_column_bytes: int, *, device: Any = None, memory_fraction: float = 0.5) int¶
Largest chunk whose per-column workspace fits measured free memory.
DESC sizes its objective
jac_chunk_sizefrom measured available memory divided by a hand-fit per-column constant (desc/objectives/objective_funs.py,desc/__init__.py); vmex knows the response operands’ avals exactly, soper_column_bytesis computed from the state pytree shapes instead. Memory model: a chunk holdschunkcolumns concurrently, each pinningper_column_bytesof workspace, and may claimmemory_fractionof the measured available bytes, sochunk = memory_fraction * available // per_column_bytesclamped to[1, dim](solvax.auto_chunk_size()’s explicit-budget regime). With no measurement available the same helper’s square-root heuristic bounds the chunk instead.
- vmex.core.implicit.mhd_energy(state: SpectralState, rt: SolverRuntime) tuple[Any, Any]¶
(wb, wp)in the wout normalization (bcovar.f), differentiable.
- vmex.core.implicit.plasma_volume(state: SpectralState, rt: SolverRuntime) Any¶
Plasma volume
volume_p[m^3] (= (2 pi)^2 * hs * sum vp).Quadrature note: this differential-volume sum is pinned by
ImplicitSolution.volumeand the FD-cached gradient tables oftests/test_implicit_grad.py— keep it as is. The canonical wout-parity boundary quadrature of the same scalar isvmex.core.statephysics.volume()(re-exported asoptimize.volume); the two agree to quadrature resolution.
- vmex.core.implicit.aspect_ratio(state: SpectralState, rt: SolverRuntime, *, ntheta: int = 128, nzeta: int = 32) Any¶
VMEC-convention aspect ratio
Rmajor_p / Aminor_p(differentiable).Aminor_p = sqrt(<cross-section area>_zeta / pi)with the area from the shoelace integral-oint Z dR/dtheta dthetaon the boundary, andRmajor_p = volume_p / (2 pi^2 Aminor_p^2)(aspectratio.f).Quadrature note: this shoelace-on-a-fresh-grid variant is pinned by
ImplicitSolution.aspectand the FD-cached solovev gradient table oftests/test_implicit_grad.py— keep it as is. The canonical wout-parityaspectratio.fboundary quadrature (internal-gridwintweights, equal to the woutaspectscalar) isvmex.core.statephysics.aspect_ratio()(re-exported asoptimize.aspect_ratio); the two agree to quadrature resolution.
- vmex.core.implicit.iota_profile(state: SpectralState, rt: SolverRuntime) Any¶
Full-mesh
iotaf(add_fluxes.f90), differentiable.ncurr = 0: the prescribed profile (p-dependent throughai);ncurr = 1: reconstructed from the converged current-constrainedchipsexactly as in the solver’s result assembly.
- vmex.core.implicit.iota_axis(state: SpectralState, rt: SolverRuntime) Any¶
On-axis rotational transform
iotaf[0]ofiota_profile()(differentiable).
- vmex.core.implicit.iota_edge(state: SpectralState, rt: SolverRuntime) Any¶
Boundary rotational transform
iotaf[-1](differentiable).Naming note: the same physical scalar as
vmex.core.statephysics.edge_iota()(optimize.edge_iota) — identical forncurr = 1; atncurr = 0this evaluates the prescribed full-meshiotafendpoint while the wout-parity version extrapolates the half-meshiotas.edge_iotais provided as an alias here so either spelling works in either module.
- vmex.core.implicit.edge_iota(state: SpectralState, rt: SolverRuntime) Any¶
Boundary rotational transform
iotaf[-1](differentiable).Naming note: the same physical scalar as
vmex.core.statephysics.edge_iota()(optimize.edge_iota) — identical forncurr = 1; atncurr = 0this evaluates the prescribed full-meshiotafendpoint while the wout-parity version extrapolates the half-meshiotas.edge_iotais provided as an alias here so either spelling works in either module.
- vmex.core.implicit.residual_fn(cfg: ImplicitConfig, frozen: SpectralState, dof_mask: SpectralState, formulation: str = 'preconditioned') Callable¶
Return the implicit residual
F(z, params) -> masked force pytree.formulation="preconditioned"(default, used by the adjoint): the self-consistently preconditionedgcof a freshevaluate_forces()pass — see the module docstring for why its implicit gradients coincide with the raw formulation.formulation="raw": the un-preconditioned (scalxc-scaled, m=1 rotated/zeroed) spectral force — same root, same gradients, but the adjoint GMRES then runs without preconditioning (diagnostic only).
- vmex.core.implicit.adjoint_matvec(cfg: ImplicitConfig, params: ImplicitParams, x_star: SpectralState, dof_mask: SpectralState, formulation: str = 'preconditioned') Callable¶
v -> (dF/dz)^T vfor tests/diagnostics (both formulations).
- vmex.core.implicit.frozen_path_directional_fd(params: ImplicitParams, cfg: ImplicitConfig, metric_fn: Callable[[SpectralState, SolverRuntime], Any], tangent: ImplicitParams, *, h: float = 0.0001, newton_steps: int = 20, newton_rtol: float = 1e-11) tuple[float, dict]¶
Central FD of
metric_fnalongtangenton the frozen solve path.The correct finite-difference reference for solver-sensitive metrics –
iota(derived from the current-constrainedchipsatncurr=1), the mirror ratio, the magnetic well, the Boozer/QI residual – whose value reads the converged solver state directly rather than through a smooth bulk integral (wb,aspect, for which a naive re-solve FD is already exact andjax.grad()matches it tortol <= 1e-6).A naive full re-solve at
params +/- h*tangentlets the solver’s internal convergence logic – thebcovarpreconditioner, thetconconstraint scaling, the m=1gczzeroing branch (residue.f90), the dof mask, the multigrid schedule, and exactly where theftolcrossing lands – re-form slightly differently at each perturbed point. For a solver-sensitive metric that path variation is an O(1) contribution that can inflate or even sign-flip the finite difference (measured onli383_low_res:d(iota_edge)/d(RBC(-1,1)) = -0.773from the adjoint, but the naive central FD reads+0.045– wrong sign).The implicit adjoint deliberately does not differentiate through that logic: it linearizes the fixed point of the frozen residual
F(the preconditioner / mask / branch captured once atparams; see the module docstring), which is the stable, physical gradient. This helper reproduces exactly that path – it capturesFonce atparamsand Newton-solvesF(z, params +/- h*tangent) = 0(matrix-free, the same linearization the adjoint uses) from the convergedz*before central-differencingmetric_fn. The result therefore equalsjax.grad()of the metric contracted withtangentto solver accuracy – the gradient check a naive re-solve FD cannot provide for these metrics.The Newton steps go through the recycling GCROT solve rather than plain restarted GMRES: from a warm start already at the root the step’s RHS lies along the smallest eigendirection, where a truncated GMRES cycle stagnates. Measured on
basic_non_stellsym_simsopt, that stall froze the-hbranch at|F| = 2.2e-08— 4e-04 from the root along a singular direction ofdF/dz— and moved this FD by 35%.Returns
(fd, info)whereinfo['newton_res']are the two frozen-solve residual norms; confirm they are small (an unconverged frozen solve invalidates the comparison — that stall is exactly what they catch).
Optimization objectives and least-squares driver for the new core (§5.1, §10).
Simsopt-style vocabulary for the QA/QH/QP/QI examples on the pure new core:
QuasisymmetryRatioResidual— the two-term quasisymmetry ratio residual of Landreman & Paul (simsopt convention), evaluated from the wout-engine field tables of a converged core state (parity port of the legacyquasisymmetry_ratio_residual_from_wout).practical scalar targets —
aspect_ratio(),mean_iota(),edge_iota(),mirror_ratio(),volume(),magnetic_well(),max_elongation()— each a pure function of(SpectralState, SolverRuntime).quasi_isodynamic_residual()— a distilled Goodman-style QI residual keeping exactly the four terms the legacy minimal-seed QI examples exercised (see its docstring).least_squares()— a thinscipy.optimize.least_squares()driver over boundary Fourier dofs (pack_boundary()/unpack_boundary()), taking weighted(callable, target, weight)terms.minimize()— the same residual definition scalarized as0.5 * sum(residual**2)and minimized with L-BFGS-B, so one reverse implicit adjoint supplies the gradient without a dense residual Jacobian.
Helicity conventions (match legacy/simsopt exactly)¶
The QS residual keeps the |B| spectrum aligned with the single helicity
chi = helicity_m * theta - helicity_n * nfp * phi — helicity_n is in
units of nfp (the internal target mode number is nn = helicity_n * nfp):
QA:
(helicity_m, helicity_n) = (1, 0)QH:
(1, -1)(i.e.chi = theta + nfp*phi; legacy/simsopt sign — the plan’s “n = -nfp” written in physical toroidal mode numbers)QP:
(0, 1)
Gradient modes¶
least_squares() defaults to scipy finite differences (jac=None ->
"2-point"). jac="implicit" uses vmex.core.implicit: each
trial boundary is solved once through
solve_implicit() (a jax.custom_vjp around the
host solver) and the exact residual Jacobian comes from forward implicit
differentiation of the fixed point — one preconditioned GMRES per boundary
dof instead of one full equilibrium solve per dof (warm cost ~1.5 hot
equilibrium solves independent of the dof count, vs one hot solve per dof
for 2-point FD). In implicit mode every objective term must be a traceable
function of (SpectralState, SolverRuntime); terms exposing a
residuals_state method (QuasisymmetryRatioResidual) contribute
their full pointwise residual vector (Gauss-Newton geometry, internal-grid
sampling instead of the wout grid). Wout-engine terms (d_merc(),
l_grad_b(), the Boozer-based QI residual) run on host NumPy and are
finite-difference-only; under jac="implicit" use d_merc_state() /
mercier_stability_residual(), jdotb_residual(), and
l_grad_b_state() instead. The implicit parameter map supports lasym
via the four RBC/ZBS/RBS/ZBC boundary families and a traceable readin.f
delta rotation (FD-validated); the traceable QS-ratio term follows, using the
stored full poloidal grid for lasym states instead of mirroring VMEC’s
reduced [0, pi] grid.
- class vmex.core.optimize.VmecProblem(*args: Any, input_from_x: Callable[[Any], Any], x_from_input: Callable[[Any], Any], equilibrium_from_x: Callable[[...], Any] | None = None, boundary_from_x: Callable[[Any], Any] | None = None, **kwargs: Any)¶
A
FunctionProblembacked by a VMEX equilibrium solve.Adds the maps between the optimizer’s decision vector and VMEC objects: the input deck, the converged equilibrium, and the boundary coefficient arrays. It keeps the same optimizer contract as its base class, so the same methods go to SciPy, JAXopt, or Optax unchanged, and it enriches
evaluate()with the solve and adjoint status of the underlying equilibrium.Build one with
from_tuples(),from_loss(), orfrom_input()rather than calling this constructor: they route throughvmex.core.optimize.make_problem, which is what assembles the four callables below along with the objective, the derivative lane, the degree-of-freedom names, and the metadata.- Parameters:
*args – Forwarded positionally to
FunctionProblem; in practice the decision vectorx0.**kwargs – Forwarded to
FunctionProblem: the objective callables,names,bounds,scales, andmetadata.input_from_x – Required.
x -> VmecInput: a new input deck carrying the boundary coefficients — and the current degrees of freedom, when the problem parameterizes them — of this decision vector. Nothing is solved.x_from_input – Required. The inverse,
VmecInput -> array: the decision vector that reproduces a given deck, which is the normal starting point of a continuation stage.x_from_input()rejects a result whose shape differs fromx0.equilibrium_from_x – Optional
x -> Equilibrium, the converged equilibrium atx. Implicit problems return the accepted state the objective already computed rather than cold-solving the boundary again, which matters for strongly shaped boundaries whose cold axis guess can produce a sign-changing initial Jacobian.Nonemakesequilibrium_from_x()raiseAttributeError. A callable that accepts anewton_iterationskeyword receives it only when the caller asks for something other than the default 10, so a closure without that keyword still works.boundary_from_x – Optional
x -> tuple of arrays, the traceable boundary coefficients:(rbc, zbs)for a stellarator-symmetric input and(rbc, zbs, rbs, zbc)whenlasym. Each is a full dense INDATA-layout array of shape(2 * ntor + 1, mpol)indexed[n + ntor, m]in metres — not the trimmed decision vector — and is a JAX array, so it composes with coil or surface objectives underjax.grad().Nonemakesboundary_from_x()raiseAttributeError.
- classmethod from_tuples(inp: Any, objective_terms: Sequence[tuple[Callable[[...], Any], Any, float]], **kwargs: Any) VmecProblem¶
Build a VMEC least-squares problem from weighted objective tuples.
The README entry point. Each tuple is
(function, target, weight)and contributes one or more rows to a single residual vector; the rows of all terms are concatenated in the order given and the scalar cost is0.5 * r @ r. Named row ranges are recorded in the problem metadata, which is what letsOptimizationMonitorreport per-term costs without re-solving anything.- Parameters:
inp – The starting
VmecInput. Its boundary supplies the initial decision vector and its resolution and profiles are held fixed apart from the parameterized degrees of freedom.objective_terms –
The
(function, target, weight)triples.functionis normally a traceablefunction(state, runtime) -> scalar or vector, evaluated on the converged equilibrium — the two arguments are also spelled(equilibrium_state, solver_context). An objective object exposing aresiduals_statemethod may be passed instead (whole instance or bound method), in which case its full pointwise residual vector becomes this term’s rows. Underderivative_method="finite_difference"a one-argument host callable taking the wholeEquilibriumis accepted too; the implicit lane rejects it, since it cannot be traced.targetis the value the term is driven toward, coerced withfloat(), so it must be scalar even whenfunctionreturns a vector — the same target is then subtracted from every row.weightis a non-negative scalar, or a one-dimensional array with one entry per residual row of that term. Under the defaultweight_semantics="cost"it multiplies the squared cost, so the row issqrt(weight) * (function - target); withweight_semantics="residual"the row isweight * (function - target)and a negative entry is then allowed.**kwargs – Passed through to
vmex.core.optimize.make_problem: which boundary modes vary (max_mode,vary_major_radius,current_dofs), the derivative lane (derivative_method,implicit_jacobian_method,jacobian_batch_size), the forward solve controls, the variable scaling (use_ess,ess_alpha,bounds), andweight_semantics.
- Returns:
A
VmecProblemwhoseresidual/residual_jacpair,x0, andscalesare ready forscipy.optimize.least_squares(). A non-finite or emptyresidual at the initial point raises
FloatingPointErrorrather than starting an optimization that cannot recover.
- classmethod from_loss(inp: Any, loss: Callable[[...], Any], **kwargs: Any) VmecProblem¶
Build a VMEC scalar problem from a traceable state/runtime loss.
- Parameters:
inp – The starting
VmecInput, as forfrom_tuples().loss –
loss(state, runtime) -> scalar, evaluated on the converged equilibrium and already carrying its own weights. It must return a single value: a vector-valued objective belongs infrom_tuples(), or must be reduced here explicitly. Unlike anobjective_termsentry it is used exactly as written — an object’sresiduals_stateis never substituted for it.**kwargs – As for
from_tuples().
- Returns:
A
VmecProblemexposing only the scalar lane —their traceable counterparts — for a gradient optimizer such as
BFGS, L-BFGS-B, or Adam. It provides no residual or Jacobian.
- classmethod from_input(inp: Any, **kwargs: Any) VmecProblem¶
Parameterize an input for field VJPs without defining an objective.
Builds the same machinery as
from_loss()around an identically zero loss, so there is nothing to minimize. Use it when what you want is the parameterization itself: the decision vector and its names,input_from_x()andboundary_from_x(), and the differentiableinterior_field()andexterior_field()with exact VJPs in these degrees of freedom.inpand**kwargsare as forfrom_tuples().
- x_from_input(inp: Any) ndarray¶
Return this problem’s decision vector for
inp.This is the inverse of
input_from_x()for the boundary and any optional current degrees of freedom selected when the problem was constructed. It is the normal continuation-stage starting vector.
- equilibrium_from_x(x: Any, *, newton_iterations: int = 10) Any¶
Return the converged equilibrium evaluated at
x.Implicit problems reuse the accepted optimizer state instead of cold-solving the optimized boundary again. This matters for strongly shaped boundaries whose cold magnetic-axis guess may have a sign-changing initial Jacobian.
- jax_objective_from_state(x: Any, extra_costs: Callable[[Any, Any], Any], *, n_extra_terms: int) tuple[Any, tuple[Any, Any]]¶
Combine the VMEX least-squares cost with state-dependent costs.
extra_costs(state, runtime)returns one already-weighted scalar cost per added objective term. The auxiliary result contains the VMEX residual rows and those added costs, ready to pass as auxiliary data tojax.value_and_grad(). Failed equilibrium trials receive the same smooth finite rejection cost as the base problem, so driver scripts do not need their own accepted/rejected branches.
- jax_extra_costs_from_state(x: Any, extra_costs: Callable[[Any, Any], Any], *, n_extra_terms: int) tuple[Any, Any]¶
Evaluate additive state-dependent costs only at valid VMEC trials.
This is the split-compilation counterpart of
jax_objective_from_state(). It returns zero extra cost at a rejected trial, leaving the base problem to supply its certified rejection wall. Splitting a large virtual-casing or coil graph from the VMEC objective substantially lowers peak XLA compilation memory.
- jax_quantity_from_state(x: Any, quantity: Callable[[Any, Any], Any]) tuple[Any, Any]¶
Evaluate a differentiable floating-point quantity and solve status.
quantity(state, runtime)may return any fixed-shape JAX array. Rejected equilibrium trials return an array of NaNs with that shape, so an invalid state cannot look like a usable diagnostic. The scalar status is zero only for an accepted equilibrium.
- exterior_field(x: Any, *, external_field: Any | None = None, external_parameters: Any | None = None, external_field_from_parameters: Callable[[Any], Any] | None = None, external_dof_names: tuple[str, ...] = (), nphi: int = 32, ntheta: int = 32, digits: int = 6, levels: tuple[tuple[int, int], ...] | None = None, chunk_size: int | str = 'auto', target_chunk_size: int | str = 'auto') Any¶
Return the exterior field and exact VJPs in this problem’s DOFs.
Query points must lie outside the last closed flux surface and away from coil filaments. The returned field follows the stored-point API:
field.set_points(xyz); field.B(); field.B_vjp(cotangent). Set the source and target chunk sizes only to cap virtual-casing memory;"auto"is the tuned default.
- interior_field(x: Any, *, newton_iterations: int = 10) Any¶
Return the interior field and exact VJPs in this problem’s DOFs.
- surface_field_values(x: Any, quantity: str, *, external_field: Any | None = None, nphi: int = 32, ntheta: int = 32, digits: int = 4, precision: Any | None = None) Any¶
Return
|B|orB.n/Bon a trial boundary for plotting.B.n/Bis evaluated on the exterior side using the supplied coil or MGRID field plus the plasma-current virtual-casing field. This helper keeps optional movie coloring out of optimization driver code; it is not used by the objective or optimizer.
- evaluate(x: Any, *, derivatives: bool = True) Evaluation¶
Evaluate and attach VMEC solve/adjoint status diagnostics.
- class vmex.core.optimize.FunctionProblem(x0: Any, *, fun: Callable[[ndarray], Any] | None = None, grad: Callable[[ndarray], Any] | None = None, value_and_grad: Callable[[ndarray], Any] | None = None, residual: Callable[[ndarray], Any] | None = None, residual_jac: Callable[[ndarray], Any] | None = None, residual_and_jac: Callable[[ndarray], Any] | None = None, jax_fun: Callable[[Any], Any] | None = None, jax_value_and_grad: Callable[[Any], tuple[Any, Any]] | None = None, jax_residual: Callable[[Any], Any] | None = None, jax_residual_jac: Callable[[Any], Any] | None = None, names: Sequence[str] | None = None, bounds: Any = None, scales: Any | None = None, metadata: Mapping[str, Any] | None = None, evaluation_progress: bool = False, report_interval: float = 10.0)¶
A decision vector plus optimizer-compatible objective callables.
Parameters are explicit and immutable from the caller’s perspective. Supplying combined value/gradient or residual/Jacobian functions enables a one-entry exact-key cache, so the common SciPy call sequence does not repeat expensive work. The cache is protected by a lock; JAX-native callables do not use host state and remain suitable for tracing.
This class deliberately does not provide
solve(method=...). Pass its methods directly to the optimizer of choice.At least one of
fun,value_and_grad,residual, orresidual_and_jacis required; every other callable is optional and the matching method raisesAttributeErrorwhen its lane is absent. The host callables receive one contiguous float NumPy array and are free to be opaque; thejax_*callables receive whatever the caller traces and must stay traceable.Two independent one-entry caches, each keyed on the exact bytes of
x(shape, dtype, and contents — no tolerance), avoid repeating work across the split calls an optimizer makes at one iterate. The scalar cache is filled byvalue_and_grad()and so coversvalue_and_gradalone,funtogether withgrad, orresidual_and_jac. The least-squares cache is filled byresidual_and_jac()and so coversresidual_and_jac, orresidualtogether withresidual_jac. Whether the cache actually pays depends on which callables were supplied: withresidual_and_jac, SciPy’s separatefun(x)thenjac(x)calls both route through it and the second is free, whereas separately suppliedresidualandresidual_jacare each invoked directly and share nothing. Cached arrays are copied out, so a caller may mutate what it receives. The caches are guarded by a re-entrant lock; thejax_*lane touches no host state and stays safe to trace.- Parameters:
x0 – Initial decision vector. Copied to a float array; its size fixes the number of degrees of freedom and the expected Jacobian column count, and its shape is the shape gradients are reshaped to.
fun –
x -> float, the scalar objective. Called directly byfun()without touching the cache.grad –
x -> array, the objective gradient. Used only in combination withfun; on its own it does not enablegrad().value_and_grad –
x -> (float, array). The preferred scalar lane: it is one call for both quantities and it fills the scalar cache. The method that serves it is also reachable under SciPy’sfun_and_gradname.residual –
x -> array, the least-squares residual vectorr(x). It is flattened, and defines the scalar objective0.5 * r @ rwhen the problem has no scalar lane of its own (nofun,grad, orvalue_and_grad).residual_jac –
x -> array, the Jacobiandr_i/dx_j. It must have one column per decision variable; anything else raisesValueError.residual_and_jac –
x -> (array, array), the preferred least-squares lane: one call for both, filling the cache thatresidual()andresidual_jac()then read. The Jacobian shape is checked exactly against(r.size, x0.size).jax_fun – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_value_and_grad – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_residual – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_residual_jac – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.names – One name per decision variable, in order, surfaced as
dof_namesfor labelling output. The default isx[0], x[1], ...; a length other thanx0.sizeraisesValueError.bounds – Box constraints stored verbatim for the optimizer to consume — a SciPy
Bounds, or a(lower, upper)pair. This class neither interprets nor enforces them.scales – Positive finite per-variable scale factors with the shape of
x0, defaulting to ones. Likeboundsthey are carried, not applied: pass them to the optimizer (SciPy’sx_scale). A non-finite or non-positive entry raisesValueError.metadata – Free-form mapping copied onto the instance. VMEX-built problems use it to carry the named residual slices, the solver configuration, the mutable solve counters, and the traceable state accessors that
VmecProblemreads.evaluation_progress – Print an elapsed-time heartbeat around long evaluations. It stays silent until a call outlives the first interval, so fast calls print nothing. It wraps the standalone
residual()andresidual_jac()calls only, which is where a production deck spends minutes; the combined and scalar lanes are unaffected.report_interval – Seconds between heartbeat lines. Must be positive.
- property dof_names: tuple[str, ...]¶
Ordered names corresponding one-to-one with entries of a decision vector.
- classmethod from_functions(x0: Any, **kwargs: Any) FunctionProblem¶
Build a problem from user-supplied x-level callables.
- evaluate(x: Any, *, derivatives: bool = True) Evaluation¶
Evaluate available scalar and residual quantities at
x.
- compile_residual_and_jacobian(x: Any | None = None, *, progress: bool = True, report_interval: float = 10.0, stream: Any = None) Evaluation¶
Compile and cache the least-squares residual and Jacobian.
This call is optional: an optimizer compiles on its first evaluation if it is omitted. Calling it explicitly provides elapsed-time output during a potentially long first JAX compilation. Later calls at the same
xuse the normal one-entry problem cache.
- compile_value_and_gradient(x: Any | None = None, *, progress: bool = True, report_interval: float = 10.0, stream: Any = None) Evaluation¶
Compile and cache the scalar value and gradient.
This optional call makes the first JAX compilation visible before BFGS, L-BFGS-B, Adam, or another gradient optimizer starts.
- class vmex.core.optimize.Evaluation(x: ndarray, value: float | None = None, gradient: ndarray | None = None, residual: ndarray | None = None, jacobian: ndarray | None = None, status: str = 'success', message: str = '', diagnostics: Mapping[str, ~typing.Any]=<factory>)¶
Values and diagnostics produced at one decision vector.
Fields that were not requested or are unavailable are
None.statusis a short machine-readable value such as"success"or"failed_solve";messageis intended for a human. Optimizers use the ordinary callable methods and do not need to understand this object.Returned by
FunctionProblem.evaluate()and the twocompile_*helpers. It is a report, not a cache: nothing here is consulted by a later evaluation.- x¶
The decision vector the values were produced at, as an owned float copy with the shape of
problem.x0.- Type:
- value¶
Scalar objective at
x. For a residual-only problem this is the least-squares cost0.5 * r @ r, matching SciPy’sOptimizeResult.cost.Nonewhen the problem exposes neither a scalar objective nor residuals.- Type:
float | None
- gradient¶
Gradient of
valuewith respect tox, reshaped tox’s shape. For a residual problem it isJ.T @ r.Nonewhenderivatives=Falsewas requested or no gradient lane exists.- Type:
numpy.ndarray | None
- residual¶
The flattened residual vector
r(x), orNonefor a scalar-only problem.- Type:
numpy.ndarray | None
- jacobian¶
The residual Jacobian
dr_i/dx_jwith shape(residual.size, x.size), orNonewhen derivatives were not requested or the problem provides no Jacobian.- Type:
numpy.ndarray | None
- status¶
"success", or — from a VMEC-backed problem —"failed_solve"when the equilibrium solve atxraised, and"under_converged"when it returned but its force residual exceeds the threshold below which implicit derivatives are certified.- Type:
- message¶
Human-readable explanation, empty on success; the solver exception’s text for
"failed_solve".- Type:
- diagnostics¶
Extra per-evaluation values. Empty for a plain
FunctionProblem. AVmecProblemadds the cumulativefailed_trialsandderivative_fallbackscounters, asolve_statsmapping when the implicit lane recorded one, and — when the equilibrium atxcould be materialised — the summed force residualfsq, itsfsq_ratioto the solve tolerance, the configuredmax_fsq_ratio, and the booleanderivative_certified. A failed solve also carriesexception_type.- Type:
Mapping[str, Any]
- class vmex.core.optimize.EquilibriumReporter(*quantities: tuple[str, ~typing.Callable[[...], ~typing.Any], str], stream: ~typing.TextIO | None | object = <object object>, separator: str = ', ')¶
Print a compact set of scalar diagnostics for an equilibrium.
Each quantity is
(label, callable, format_spec). Callables may use either thefunction(equilibrium)orfunction(state, runtime)convention used by VMEX objectives. Calling the reporter prints one line and returns the values by label, so scripts can also reuse a final metric.- Parameters:
*quantities – One
(label, function, format_spec)triple per reported column; at least one is required and the labels must be unique.labelnames the column and keys the returned mapping.functionis dispatched on its signature: a callable whose second positional parameter has no default is called asfunction(state, runtime)(the VMEX objective convention), every other callable asfunction(equilibrium). It must return exactly one scalar; any other size raisesValueError.format_specis aformat()specification applied to that float, for example".6e".stream – Where the report line is written. The default is
sys.stdout; passNoneto compute and return the values without printing.separator – Text placed between the
label = valuefields of the printed line.
- class vmex.core.optimize.OptimizationMonitor(problem: FunctionProblem | None = None, *, stream: TextIO | None | object = <object object>, print_every: int = 1, trace: bool = True)¶
Record and optionally print optimizer iterations and trials.
Pass the instance as a SciPy
callback. SciPy invokes callbacks after an iteration, unlike objective functions which are also called for rejected line-search or trust-region trials. JAXopt, Optax, and custom loops can callrecord()with values they already computed.tracealso prints onetrialline per objective evaluation, so the rejected line-search trials between two accepted iterations – a full equilibrium solve each – are visible while the run is in progress.The monitor never chooses steps or changes an optimizer. If
problemis supplied, VMEX solve/failure counters are read without evaluating the objective again.- Parameters:
problem – Optional problem the optimizer is running on. It is used only to read metadata: the named residual slices that split
costinto per-term costs, the cumulative equilibrium-solve and failed-trial counters, and — as a last resort, when a callback carries neithercostnorfun— onefun()call at the accepted iterate. WithNonethose record fields stayNoneor empty and the monitor still records everything the callback provides.stream – Where the per-iteration table is written. The default is
sys.stdout; passNoneto record silently and readrecords,history, orsave()afterwards.print_every – Print one row every
print_everyrecords (the first record is always printed, together with the header). Must be at least 1; recording is unaffected.
- wrap_value_and_grad(function: Callable | Sequence[Callable], term_names: tuple[str, ...] | None = None, *, residual_slices: tuple[tuple[str, int, int], ...] = ()) Callable¶
Adapt a JAX
has_auxvalue/gradient pair for SciPy.Each
function(x)must return((cost, terms), gradient). Pass a sequence to compile large additive physics components separately; their costs and gradients are summed without changing the optimizer contract.termsmay be a mapping of labels to weighted scalar costs, or one compact vector paired withterm_names. For a large residual graph, passresidual_slicesand return(residual, extra_costs...); costs are reduced on the host to keep the compiled output small. The first evaluation is recorded as iteration zero; later evaluations are cached.
- cache_evaluation(x: Any, cost: Any, gradient: Any, terms: Mapping[str, Any] | None = None) tuple[float, ndarray]¶
Cache an already-computed objective pair for a SciPy callback.
This method does no differentiation and does not alter the objective. Driver scripts can show their explicit
jax.value_and_gradcalls, sum independently compiled physics components themselves, and use this one host conversion to avoid recomputing per-term costs when SciPy later reports an accepted iterate.
- record(x: Any, *, cost: float, optimality: float | None = None, iteration: int | None = None, equilibrium_solves: int | None = None, rejected_trials: int | None = None, terms: Mapping[str, Any] | None = None, counters: Mapping[str, Any] | None = None) OptimizationRecord¶
Append one already-computed accepted iterate and return its record.
- Parameters:
x – The accepted decision vector; a float copy is appended to
x_history.cost – Total scalar objective at
x. Nothing is recomputed.optimality – First-order optimality measure, or
Nonewhen unknown.iteration – Iteration index; defaults to the number of records already held, and is advanced past the previous record when an optimizer restarts its own counter at a continuation stage.
equilibrium_solves – Cumulative solve and failed-trial counts.
None(the default) reads them from the monitor’sproblem, leaving the fieldNonewhen no problem was supplied.rejected_trials – Cumulative solve and failed-trial counts.
None(the default) reads them from the monitor’sproblem, leaving the fieldNonewhen no problem was supplied.terms – Per-term weighted costs by label.
Nonesplits the problem’s own residual over its named term slices instead, which may evaluate the residual once atx.counters – Effort counters by name.
Nonereads them from the monitor’sproblem, empty when it carries none.
- Return type:
The appended
OptimizationRecord.
- property x_history: tuple[ndarray, ...]¶
Copies of the accepted decision vectors, including iteration zero.
- plot(path: str | Path, *, title: str = 'Optimization objective terms') Path¶
Write a compact log-scale total and per-term cost history plot.
- movie(path: str | Path, object_factory: Callable[[ndarray], Any], **kwargs: Any) Path¶
Animate accepted surface/coil iterates with one geometry callback.
- movie_surface_coils(path: str | Path, object_factory: Callable[[ndarray], Any], *, x0: Any, scales: Any, surface_color: str | Callable | None = None, plasma_problem: Any | None = None, external_field: Callable[[Any], Any] | None = None, nphi: int = 32, ntheta: int = 32, digits: int = 4, precision: Any | None = None, **kwargs: Any) Path¶
Animate normalized surface/coil iterates with optional field color.
object_factoryreceives physical variablesx0 + scales*u.surface_colormay beNone,"absB","B.n/B", or a callable(u, objects) -> values. For the two named field colors, provideplasma_problem;B.n/Badditionally needsexternal_field(objects). These plotting helpers never enter the objective or gradient graph.
- class vmex.core.optimize.OptimizationRecord(iteration: int, cost: float, reduction: float | None, optimality: float | None, equilibrium_solves: int | None, rejected_trials: int | None, terms: Mapping[str, float]=<factory>, counters: Mapping[str, float | None]=<factory>)¶
One optimizer callback, normally one accepted iteration.
Produced by
OptimizationMonitor.record()and stored inOptimizationMonitor.records. Every field is a plain host value; a field isNonewhen the optimizer callback did not supply it and the monitor could not derive it, never zero-as-unknown.- iteration¶
Iteration index of this accepted iterate. Taken from the SciPy result’s
nitwhen present, otherwise the number of records already held. Optimizers restart their counter at every continuation stage, so a repeated or decreasing value is bumped to one past the previous record; the sequence in one monitor is always strictly increasing.- Type:
- cost¶
Total scalar objective at this iterate. For a least-squares problem this is
0.5 * r @ rover the full residual vector, so it matches SciPy’sOptimizeResult.cost; for a scalar objective it is the objective value itself. Units are those of the weighted objective (dimensionless for the usual normalised VMEX terms).- Type:
- reduction¶
previous cost - this cost, positive when the step improved the objective.Nonefor the first record, which has no predecessor.- Type:
float | None
- optimality¶
First-order optimality measure of the gradient. SciPy’s own
optimalityis used when the callback provides it; otherwise it is the infinity norm of a callback-suppliedjac, or the Euclidean norm of the gradient cached byOptimizationMonitor.cache_evaluation().Nonewhen no gradient information reached the monitor.- Type:
float | None
- equilibrium_solves¶
Cumulative number of forward VMEC equilibrium solves performed for this problem’s implicit configuration, read from the solver’s own counters without re-evaluating the objective.
Nonewhen the monitor was built without aproblemor the problem carries no implicit configuration (for example a finite-difference or wout-only problem).- Type:
int | None
- rejected_trials¶
Cumulative number of optimizer trial points whose equilibrium solve failed and was replaced by the smooth rejection cost.
Noneunder the same conditions asequilibrium_solves.- Type:
int | None
- terms¶
Per-term weighted costs by label,
0.5 * r_k @ r_kover each named residual slice of the problem, so the values sum tocostfor a pure least-squares problem. Empty when the problem exposes no term slices and the caller supplied none.
- counters¶
Cumulative effort counters of the problem’s implicit configuration at this record: solves and descent iterations, refinement calls, steps and Krylov iterations, Jacobian calls, columns and certifier Krylov iterations, adjoint calls and Krylov iterations, and the host seconds of each part. An entry is
Nonewhen the work ran inside a compiled program where it cannot be observed. Empty under the same conditions asequilibrium_solves.
- vmex.core.optimize.make_problem(inp: VmecInput, *, objective_terms: Callable, float, ~typing.Any]] | None=None, loss: Callable | None = None, max_mode: int = 1, vary_major_radius: bool = False, x0: ndarray | None = None, current_dofs: int | None = None, derivative_method: str = 'implicit', fd_method: str = '3-point', fd_rel_step: float | None = None, workers: int | None = None, weight_semantics: str = 'cost', jacobian_batch_size: int | str | None = 1, implicit_jacobian_method: str = 'auto', adjoint_tol: float = 1e-06, jacobian_adjoint_tol: float = 0.0001, jacobian_adjoint_maxiter: int = 10, adjoint_maxiter: int = 300, max_fsq_ratio: float = 1000000.0, refine_tol: float = 1e-10, forward_ftol: float | None = None, forward_max_iterations: int | None = None, hot_restart: bool = True, warm_start: str | None = 'perturbation', use_ess: bool = True, ess_alpha: float = 1.2, evaluation_progress: bool = False, bounds: Any = None, device: Any = 'auto', solve_kwargs: dict | None = None, restart_from: Any = None, progress: bool = False, report_interval: float = 10.0, progress_stream: Any = None, problem_class: type[VmecProblem] = <class 'vmex.core.problem.VmecProblem'>) VmecProblem¶
Build optimizer-neutral VMEC objective and derivative callables.
Exactly one of
objective_termsandlossis required. With the defaultweight_semantics="cost", tuple weightwmultiplies the squared cost, so the residual row issqrt(w) * (f - target). Selectweight_semantics="residual"whenwshould multiply the residual itself.lossmust be a traceable(state, runtime) -> scalarcallable.derivative_method="implicit"computes exact derivatives of the converged fixed-boundary equilibrium by implicit differentiation and requires traceable objectives."finite_difference"also accepts opaque host objectives and uses independent equilibrium re-solves;workers=Noneselects the automatic host-worker count. Users with complete x-level derivatives can useFunctionProblem.from_functions().implicit_jacobian_method="auto"is the beginner-facing default: it selects one reverse adjoint for a scalar residual and an amortized block-tridiagonal factorization for vector residuals. Advanced choices are"block_tridiagonal","forward_gmres", and"reverse_adjoint"; the names describe how the exact implicit Jacobian is assembled. An uncertified automatic response is recomputed by reverse adjoint. Forced host methods raiseAdjointSolveError; transformable JAX methods use the same reverse fallback because Python exceptions cannot be raised reliably under jit.jacobian_batch_size=1is the default for QI/QS problems throughmax_mode=5: it minimizes cold compilation complexity and peak memory."auto"batches response columns and improves warm throughput, so it is preferable for long same-shape continuation campaigns that amortize the larger first compilation. This public name maps to the compatibility drivers’ establishedjac_chunk_sizeimplementation.Set
progress=Trueto report elapsed-time heartbeats while validating the seed equilibrium and building resolution-dependent solver data.VmecProblem.compile_residual_and_jacobian()orVmecProblem.compile_value_and_gradient()provides the same visibility for the first derivative evaluation after this factory returns.The returned object contains no optimization algorithm. Pass
VmecProblem.residual()/VmecProblem.residual_jac()to a nonlinear least-squares package, orVmecProblem.value_and_grad()to any scalar gradient optimizer.forward_ftolandforward_max_iterationsoverride the final VMEC solve stage for either derivative method.max_fsq_ratiocontrols how close an iteration-limited trial must be to that tolerance before VMEX differentiates it. The default acceptsFSQ / forward_ftol <= 1e6; stricter studies can reduce it without changing VMEX internals.adjoint_tolis a relative Krylov tolerance with a certified true residual check;adjoint_maxiteris the restart budget.refine_tolis the frozen fixed-point residual required before implicit differentiation. The default preserves strict gradients;numpy.infexplicitly disables refinement for legacy comparisons.restart_fromseeds the first equilibrium from a previous WOUT,Equilibrium, or solver result. This is useful when a continuation stage changesmpol,ntor, or radial resolution: trial hot restarts then continue from the remapped converged state instead of a cold axis guess.
- class vmex.core.optimize.Equilibrium(inp: VmecInput, state: SpectralState, runtime: SolverRuntime, result: SolveResult, field_factory: Callable[[], Any] | None = None, exterior_field_factory: Callable[[...], Any] | None = None)¶
A converged fixed-boundary equilibrium plus its evaluation contexts.
Objective callables in
least_squares()receive one of these. Thesolutionandsolver_contextare the clear public names for the solver-nativestateandruntimeattributes. They feed the differentiable scalar targets;wout(built lazily, host NumPy) feeds wout-table objectives (QS ratio residual, Boozer-based QI residual).- property solution: SpectralState¶
Converged spectral equilibrium coefficients and force arrays.
- property solver_context: SolverRuntime¶
Read-only grids, profiles, and constants used to evaluate the solution.
- exterior_field(**kwargs)¶
Return a field that can be queried outside the plasma surface.
- property field¶
Pointwise magnetic field inside the plasma boundary.
- set_points(points: Any) Equilibrium¶
Store Cartesian points for pointwise field evaluation.
- set_points_xyz(points: Any) Equilibrium¶
Store Cartesian
(x, y, z)points for field evaluation.
- set_points_cyl(points: Any) Equilibrium¶
Store cylindrical
(R, phi, Z)points for field evaluation.
- set_points_flux(points: Any) Equilibrium¶
Store VMEC
(s, theta, phi)points inside the plasma.
- field_in_flux_coordinates()¶
Return the interior field in the
(s, theta, phi)basis.
- gradgradB_vjp(vector: Any) Any¶
Return the VJP of
gradgradB()in the problem’s DOFs.
- gradgradgradB_vjp(vector: Any) Any¶
Return the VJP of
gradgradgradB()in the problem’s DOFs.
- vmex.core.optimize.solve_equilibrium(inp: VmecInput, *, initial_state: SpectralState | None = None, raise_on_max_iterations: bool = False, verbose: bool = False, forward_ftol: float | None = None, forward_max_iterations: int | None = None, polish_force_balance: bool | str = False, **solve_kwargs) Equilibrium¶
Converge
inpwith the core multigrid solver ->Equilibrium.verbose=Trueprints the VMEC iteration table, including the current iteration count and force residuals.raise_on_max_iterations=Falseby default: during optimization a NITER-exhausted trial state is still a usable (penalized) sample — VMEC2000 behaves the same way. Extra keywords go tovmex.core.multigrid.solve_multigrid().forward_ftolandforward_max_iterationsreplace the input ladder’s final tolerance and iteration cap, using the same names as the optimization problem API. Force-balance polishing is strictly opt-in: the defaultpolish_force_balance=Falsereturns the ordinary converged VMEC state.
- class vmex.core.optimize.QuasisymmetryRatioResidual(surfaces, helicity_m: int = 1, helicity_n: int = 0, *, weights: Iterable[float] | None = None, ntheta: int = 63, nphi: int = 64)¶
Two-term quasisymmetry ratio residual (simsopt convention).
On each requested surface the field is sampled on a uniform
(theta, phi)grid (VMEC angles) and the pointwise residualf = [(B x grad B . grad psi)(nn - iota*m) - (B . grad B)(m*G + nn*I)] / B^3(
m = helicity_m,nn = helicity_n * nfp,G/Ithe Boozer covariant field averagesbvco/buco) is weighted by the flux-surface measuresqrt(nfp*dtheta*dphi*|sqrt g| / V')so thattotal = sum(residuals**2)is simsopt’s surface-averaged QS ratio.fvanishes identically iff|B|depends on the angles only throughhelicity_m*theta - nn*phi.The evaluation consumes the parity-proven wout-engine tables (
bmnc/gmnc/bsub*/bsup*,vmex.core.nyquist) of aWoutData— fromwout_from_state()or anywout_*.nc— ported from legacyquasisymmetry_ratio_residual_from_wout(A/B bit-exact).- compute(wout) dict[str, Any]¶
Full diagnostics dict from a wout-like object or
Equilibrium.
- residuals(wout) Array¶
Flat least-squares residual vector (target 0, weight applied by the driver).
- profile(wout) Array¶
Per-surface sum of squared residuals.
- J(eq: Equilibrium) Array¶
Objective-term entry point for
least_squares()(residual vector).
- residuals_state(state: SpectralState, rt: SolverRuntime) Array¶
Traceable flat residual vector with
sum(r**2) = total_state.The internal-grid analogue of
residuals()(wout tables): the pointwise weighted residual of_pointwise_state()scaled by the square roots of the surface coefficients — this is the residual vectorjac="implicit"optimizes, giving the least-squares driver the full pointwise Gauss-Newton geometry.
- profile_state(state: SpectralState, rt: SolverRuntime) Any¶
Traceable weighted per-surface QS totals at
surfaces.weights * interp(surfaces, <f^2> profile)from_pointwise_state();sum = total_state.
- total_state(state: SpectralState, rt: SolverRuntime) Any¶
Traceable scalar QS objective:
sum(profile_state)(see there).
- vmex.core.optimize.aspect_ratio(state: SpectralState, rt: SolverRuntime) Any¶
VMEC aspect ratio
Rmajor_p / Aminor_p(aspectratio.fconvention).Aminor_p = sqrt(<cross-section area> / pi),Rmajor_p = volume_p / (2 pi <area>)from the boundary surface quadrature; equals the woutaspectscalar of the same state. This is the canonical (wout-parity) implementation, re-exported asvmex.core.optimize.aspect_ratio;vmex.core.implicit.aspect_ratio()is the implicit module’s historical shoelace-quadrature variant of the same scalar (see there).
- vmex.core.optimize.mean_iota(state: SpectralState, rt: SolverRuntime) Any¶
Mean rotational transform over the half-mesh surfaces (axis excluded).
Matches the legacy optimization
mean_iotaconvention (mean(iotas[1:]), i.e. the mean of the woutiotasprofile).
- vmex.core.optimize.min_abs_iota(state: SpectralState, rt: SolverRuntime) Any¶
Smallest
|iota|over the half-mesh surfaces (axis excluded).The transform floor that matters for a stellarator is the profile minimum, not its average: a mean target is satisfiable while a shear reversal or a current-carried profile leaves an interior surface near zero transform. The absolute value keeps the metric sign-free, since only the magnitude of the transform is physical here.
- vmex.core.optimize.soft_min_abs_iota(state: SpectralState, rt: SolverRuntime, *, tau: float = 0.02) Any¶
Smooth stand-in for
min_abs_iota().The hard minimum is differentiable except where two surfaces tie, which a least-squares step can sit on. This variant weights the profile by
softmax(-|iota| / tau), so it stays inside[min, max]and reduces to the minimum astau -> 0;taucarries the units of iota, so0.02resolves a minimum to a few percent. Thelog-sum-expsoftmin is deliberately avoided: it sitstau log(ns)below the true minimum, which turns a floor on a non-negative quantity into a negative number.
- vmex.core.optimize.edge_iota(state: SpectralState, rt: SolverRuntime) Any¶
Rotational transform at the boundary (wout
iotaf[-1]convention: linear extrapolation of the half mesh,1.5 iotas[-1] - 0.5 iotas[-2]).Naming note:
optimize.edge_iotaandvmex.core.implicit.iota_edge()are the same physical scalar — identical forncurr = 1(both reconstruct iota from the convergedchips); atncurr = 0this wout-parity version extrapolates the prescribed half-meshiotaswhile the implicit variant evaluates the prescribed full-meshiotafendpoint directly.iota_edgeis provided as an alias here (andedge_iotainimplicit) so either spelling works in either module.
- vmex.core.optimize.iota_edge(state: SpectralState, rt: SolverRuntime) Any¶
Rotational transform at the boundary (wout
iotaf[-1]convention: linear extrapolation of the half mesh,1.5 iotas[-1] - 0.5 iotas[-2]).Naming note:
optimize.edge_iotaandvmex.core.implicit.iota_edge()are the same physical scalar — identical forncurr = 1(both reconstruct iota from the convergedchips); atncurr = 0this wout-parity version extrapolates the prescribed half-meshiotaswhile the implicit variant evaluates the prescribed full-meshiotafendpoint directly.iota_edgeis provided as an alias here (andedge_iotainimplicit) so either spelling works in either module.
- vmex.core.optimize.mirror_ratio(state: SpectralState, rt: SolverRuntime, *, s_index: int = -1) Any¶
Mirror ratio
(Bmax - Bmin) / (Bmax + Bmin)on one half-mesh surface.Note the convention: this is the
|B|modulation depth on a surface, the standard QI optimization knob, notR_m = Bmax / Bmin. The two are related byR_m = (1 + m) / (1 - m). The open-mirror lane reportsR_mproper —R_m,axisper leg andR_m,LCFSseparately — throughvmex.mirror.metrics.|B|is evaluated on the solver’s internal angular grid from the half-mesh field state (|B|^2 = 2 (bsq - p),bcovar.f);s_indexselects the half-mesh surface (default: outermost). Hard max/min — smooth almost everywhere, adequate for finite-difference least squares (the legacyVMECMirrorRatiosoftmax knobs were an optimizer nicety only).
- vmex.core.optimize.volume(state: SpectralState, rt: SolverRuntime) Any¶
Plasma volume
volume_p[m^3] (wout convention, boundary quadrature).Canonical (wout-parity) implementation, re-exported as
vmex.core.optimize.volume;vmex.core.implicit.plasma_volume()is the implicit module’ssum(vp)variant of the same scalar (see there).
- vmex.core.optimize.volume_average_beta(state: SpectralState, rt: SolverRuntime) Any¶
Volume-averaged plasma beta, equal to the wout
betatotalscalar.
- vmex.core.optimize.magnetic_well(state: SpectralState, rt: SolverRuntime) Any¶
simsopt’s
Vmec.vacuum_wellproxy(V'(0) - V'(1)) / V'(0).V' = dV/dsendpoints are linear extrapolations of the half-mesh differential volumevp(bcovar.f); positive values mean a favorable well. Ported from legacyvmex.finite_beta.magnetic_well_from_vp.
- vmex.core.optimize.elongation_profile(state: SpectralState, rt: SolverRuntime, *, ntheta: int | None = None, nphi: int | None = None) Array¶
Boundary elongation at equally spaced toroidal cross-sections.
Each constant-geometric-toroidal-angle cross-section is reconstructed from the physical edge Fourier coefficients. Its area
Aand perimeterPdefine the equivalent ellipse, whose semi-axes are recovered with Ramanujan’s perimeter approximation. The returned ratio isa_major / a_minorat each toroidal sample, the same definition used by DESC’s elongation objective.The default grids (at least 32 poloidal and 24 toroidal points per field period) resolve optimization modes through
max_mode <= 5without adding user-facing sampling knobs to ordinary scripts. Explicit positiventhetaandnphivalues are available for convergence studies. The calculation is JAX-traceable and supports stellarator-asymmetric boundaries.
- vmex.core.optimize.max_elongation(state: SpectralState, rt: SolverRuntime, *, ntheta: int | None = None, nphi: int | None = None) Any¶
Maximum boundary elongation over one field period.
See
elongation_profile(). A hard maximum is deliberate: it matches the usual engineering constraint and is differentiable except when two sampled cross-sections tie exactly.
- vmex.core.optimize.d_merc(eq) Array¶
Mercier stability criterion profile
DMerc(s)(full mesh).Positive interior values indicate Mercier stability. Evaluated through the parity-proven wout engine (
vmex.core.nyquist.mercier_and_jxb()viawout_from_state()) — host NumPy, so this objective is finite-difference-only (not jit/AD transparent; the first two surfaces and the edge carry the usual near-axis noise, so practical targets should penalize e.g.min(DMerc[2:-1], 0)). Accepts anEquilibriumor any wout-like object. Use traceablemercier_stability_residual()withjac="implicit". Both symmetry modes are supported; thelasymlane is validated per-term against live VMEC2000 output (tests/test_vmec2000_live.py).
- vmex.core.optimize.d_merc_state(state: SpectralState, rt: SolverRuntime) Any¶
Traceable VMEC
DMercprofile on the full radial mesh.Positive interior values indicate Mercier stability. This is a pure-JAX port of the symmetric
jxbforce.f/mercier.fpath used byvmex.core.nyquist.mercier_and_jxb(); it accepts a live converged(state, runtime)pair and supportsjit, JVP and reverse-mode AD. The axis, first near-axis surface and edge retain VMEC’s zero/noisy output convention and should be excluded from objectives (normally[2:-1]).Supports both symmetry modes: the
lasymlane carries the sine-parity contributions through the jxbforce.f parity-split filter and the full-theta-interval surface integrals, validated per-term against live VMEC2000 lasym output (tests/test_vmec2000_live.py) and against an independent NumPy reconstruction from the wout Fourier tables (vmex.core.plotting._glasser_d_r_from_wout()).
- vmex.core.optimize.mercier_stability_residual(state: SpectralState, rt: SolverRuntime, *, margin: float = 0.0, smoothing: float = 1e-06) Any¶
Smooth Mercier-instability residual on
DMerc[2:-1].Positive
DMercis stable. For each validated interior surface this returnssmoothing * softplus((margin - DMerc) / smoothing): it tends tomax(margin - DMerc, 0)assmoothingtends to zero, while retaining a smooth gradient at the stability boundary. At finite smoothing it is strictly positive but exponentially close to zero on a sufficiently stable surface. Use target zero invmex.core.optimize.least_squares();margin > 0requests a finite stability margin. The first two surfaces and edge are excluded.
- vmex.core.optimize.jdotb_state(state: SpectralState, rt: SolverRuntime) Any¶
Traceable VMEC
jdotb = <J.B>profile in WOUT units.
- vmex.core.optimize.jdotb_residual(state: SpectralState, rt: SolverRuntime) Any¶
Interior
<J.B>profile for least-squares current objectives.
- vmex.core.optimize.mercier_shear_state(state: SpectralState, rt: SolverRuntime) Any¶
Return
S = d(iota)/d(Phi)in the VMEC Mercier normalization.
- vmex.core.optimize.glasser_d_r_state(state: SpectralState, rt: SolverRuntime, *, shear_epsilon: float = 0.0) Any¶
Traceable Glasser–Greene–Johnson
D_Rprofile.Non-positive values satisfy the necessary local resistive-interchange stability condition on nonzero-shear surfaces. With the strict default, exact zero-shear entries are set to zero because the criterion is undefined there. A positive
shear_epsilonreplaces the denominatorshear**2byshear**2 + shear_epsilon**2for smooth optimization; this regularization does not make zero-shear surfaces physically valid. Post-checkmercier_shear_state()and require every target surface to satisfyabs(S) >> shear_epsilonbefore interpreting the result. As forDMerc, use only validated interior surfaces (normally[2:-1]) as optimization targets.Supports both symmetry modes. The symmetric lane retains its DCON comparison; the
lasymlane is validated through the VMEC2000-anchoredDMercparity plus the exact in-repo identityD_R = -DMerc + (H - S²/2)²/S²(no external lasymD_Roracle exists — the DCON comparison is symmetric-only).
- vmex.core.optimize.glasser_stability_residual(state: SpectralState, rt: SolverRuntime, *, margin: float = 0.0, smoothing: float = 1e-06, shear_epsilon: float = 1e-08) Any¶
Smooth resistive-interchange residual on
D_R[2:-1].Subject to the prerequisite
DMerc > 0, stable surfaces requireD_R <= 0. Combine this residual withmercier_stability_residual(); targeting it to zero penalizesD_R > -marginwhile retaining a smooth derivative. The nonzero default shear regularization makes this optimization helper finite on zero-shear seeds; useglasser_d_r_state()with its strict default for reporting.
- vmex.core.optimize.trial_pressure_d_merc_state(state: SpectralState, rt: SolverRuntime, *, beta: float = 0.025, pressure_shape: Any = None) Any¶
Mercier profile for a trial pressure on frozen equilibrium geometry.
This inexpensive AD-transparent proxy replaces only the explicit pressure drive in VMEC’s Mercier expression. It does not include the finite-beta geometry, current, or Shafranov-shift response; certify a candidate with a self-consistent finite-pressure equilibrium.
- vmex.core.optimize.trial_pressure_glasser_d_r_state(state: SpectralState, rt: SolverRuntime, *, beta: float = 0.025, pressure_shape: Any = None, shear_epsilon: float = 0.0) Any¶
Glasser
D_Rfor the same frozen-equilibrium trial pressure proxy.
- vmex.core.optimize.trial_pressure_mercier_stability_residual(state: SpectralState, rt: SolverRuntime, *, beta: float = 0.025, pressure_shape: Any = None, margin: float = 0.0, smoothing: float = 1e-06) Any¶
Smooth instability residual for trial-pressure
DMerc[2:-1].
- vmex.core.optimize.trial_pressure_glasser_stability_residual(state: SpectralState, rt: SolverRuntime, *, beta: float = 0.025, pressure_shape: Any = None, margin: float = 0.0, smoothing: float = 1e-06, shear_epsilon: float = 1e-08) Any¶
Smooth instability residual for trial-pressure
D_R[2:-1].
- vmex.core.optimize.l_grad_b(eq, *, s_index: int = -1, ntheta: int = 24, nphi: int = 24) Any¶
Magnetic-gradient scale length
min L_grad_Bon one half-mesh surface.L_grad_B = ``|B|sqrt(2 / (grad B : grad B))`` (squared Frobenius norm of the Cartesian field-gradient tensor) — the Kappel/Landreman coil-complexity / compactness proxy. Evaluated from the wout tables of the converged state (bsupumnc/bsupvmncNyquist spectra, spectral angular derivatives ofrmnc/zmns, native half/full-mesh radial finite differences, one-sided at the edge); the pointwise math lives invmex.core.statephysics._lgradb_grid(), shared with the traceablel_grad_b_state().Returns the (hard) minimum over a uniform
(theta, phi)grid on the selected surface (s_indexindexes thens-long half-mesh arrays; default edge). Larger is better; a practical least-squares term ismax(1/L - 1/threshold, 0). Symmetric configurations only; asymmetric inputs raise instead of silently dropping their sine/cosine partners. Accepts anEquilibriumor wout-like. Host-NumPy wout tables -> finite-difference-only; usel_grad_b_state()forjac="implicit".
- vmex.core.optimize.l_grad_b_state(state: SpectralState, rt: SolverRuntime, *, s_index: int = -1, ntheta: int = 24, nphi: int = 24, softmin_k: float | None = None) Any¶
Traceable
min L_grad_Bof a core state (implicit-adjoint ready).The
(state, runtime)lane ofl_grad_b(): identical convention and grid, with the wout coefficient tables rebuilt traceably from the state (_lgradb_state_tables(), thewrout.fNyquist analysis as jnp einsums) and the same radial finite-difference stencils, so the default hard minimum matches the wout lane to float round-off. Fully jnp: usable directly as a two-positional objective term underjac="implicit".softmin_kselects the reduction:None(default) is the hardmin— exact, differentiable almost everywhere, but its gradient jumps when the minimizing gridpoint switches. A floatk[1/m] returns the smooth soft minimum-logsumexp(-k * L) / k, a lower bound on the hard minimum withinlog(ntheta * nphi) / k(about6.4 / km at the default 24x24 grid;k = 50biases a ~1 m scale length by < 0.13 m). Optimize smooth, report the hard minimum.
- vmex.core.optimize.quasi_isodynamic_residual(*, bmnc_b, bmns_b=None, xm_b, xn_b, iota_b, nfp: int, weights: Iterable[float] | None = None, nphi: int = 151, nalpha: int = 31, n_bounce: int = 51, include_bounce_endpoints: bool = False, softness: float = 0.02, width_weight: float = 1.0, branch_width_weight: float = 0.5, branch_width_softness: float = 0.01, profile_weight: float = 0.1, shuffle_profile_weight: float = 1.0, shuffle_profile_softness: float = 0.02, phimin: float = 0.0) dict[str, Any]¶
Smooth Goodman-style quasi-isodynamic residual from Boozer
|B|modes.A configuration is quasi-isodynamic when the
|B|contours are poloidally closed and the trapped-particle bounce distance between the two branches of each magnetic well is independent of the field-line labelalpha(omnigenity). This residual samples the normalized|B|along field linestheta = alpha + iota*phiover one field period and penalizes, per surface (the default weights reproduce exactly the terms used by the established minimal-seed QI formulation):level-set width variance (
width_weight): for each bounce levelB*the smooth occupancysigmoid((B* - bnorm)/softness)gives the fraction of the field line belowB*; its variance overalphameasures misalignment of the|B|contours.branch width variance (
branch_width_weight): each field line is split at its|B|minimum, both branches are made monotone with a running maximum, and the (smooth) level-crossing distances of the two branches are summed — the trapped-well bounce width, whose variance overalphais the classic omnigenity error.profile consistency (
profile_weight): small penalty on the variance ofbnormitself overalphaat fixedphi, which keeps degenerate QH-like candidates from gaming the width terms.branch-shuffle profile (
shuffle_profile_weight): the “squash and shuffle” comparison — each well’s branch crossings are shifted so every field line has the mean bounce width, the shuffled well is reinterpolated onto the original grid and compared pointwise to the originalbnorm(the closest smooth analogue of Goodman et al.’s construction of the nearest omnigenous field).
Legacy port (
quasi_isodynamic_residual_from_boozer_modes) with the unusedaligned_profile_*/weighted_shuffle_*/shuffle_profile_nphi_outmachinery removed.xn_buses physical toroidal mode numbers (booz_xform convention). Returnsresiduals1d(least-squares vector) andtotal(its squared norm).
- vmex.core.optimize.boozer_modes_from_wout(wout, *, surfaces, mboz: int = 18, nboz: int = 18, jit: bool = False) dict[str, Any]¶
Boozer
|B|spectrum of selected surfaces viabooz_xform_jax.woutis aWoutData(or any wout-like object accepted byBooz_xform.read_wout_data);surfacesare normalized-flux values matched to the nearest half-mesh surfaces. Returns{bmnc_b, bmns_b, xm_b, xn_b, iota_b, nfp, s_b}with the spectra shaped(nsurf, nmodes).bmns_bis zero for symmetric equilibria and contains the independent sine spectrum forlasym.booz_xform_jaxis an optional dependency (soft import).
- vmex.core.optimize.quasi_isodynamic_residual_from_wout(wout, *, surfaces, mboz: int = 18, nboz: int = 18, jit_booz: bool = False, **qi_kwargs) dict[str, Any]¶
QI residual of a converged equilibrium: wout -> Boozer -> residual.
Convenience composition of
boozer_modes_from_wout()andquasi_isodynamic_residual();qi_kwargsare the residual’s sampling/weight knobs. Accepts aEquilibriumtoo, so it can be used directly as aleast_squares()objective term vialambda eq: quasi_isodynamic_residual_from_wout(eq, surfaces=...)["residuals1d"].
- vmex.core.optimize.boundary_dof_names(inp: VmecInput, max_mode: int, *, vary_major_radius: bool = False) list[str]¶
Human-readable labels (“RBC(n,m)” / “ZBS(n,m)”, INDATA index order).
For
lasymboundaries the non-symmetricRBS(n,m)/ZBC(n,m)families are appended (same(m, n)order as the symmetric block).
- vmex.core.optimize.boundary_arrays_from_x(inp: VmecInput, x, max_mode: int, *, vary_major_radius: bool = False) tuple[Any, ...]¶
Traceable boundary coefficient arrays reconstructed from
x.The default returns
(rbc, zbs); an asymmetric input additionally returns(rbs, zbc).vary_major_radius=TrueappendsRBC(0,0)to the decision vector without introducing the identically-zeroZBS(0,0)direction.
- vmex.core.optimize.pack_boundary(inp: VmecInput, max_mode: int, *, vary_major_radius: bool = False) ndarray¶
Flat boundary-dof vector (see
_dof_modes()).Inverse of
unpack_boundary();RBC(0,0)is excluded by default (fixed major radius) and appended whenvary_major_radius=True. For a stellarator-symmetric boundary the layout is[rbc..., zbs...]; forlasymthe non-symmetric families are appended as[rbc..., zbs..., rbs..., zbc...](four families — the samem = 0 / RBC(0,0)fixing convention applies to every family, so the rigid vertical shiftZBC(0,0)and the identically-zeroRBS(0,0)are excluded too).
- vmex.core.optimize.unpack_boundary(inp: VmecInput, x, max_mode: int, *, vary_major_radius: bool = False) VmecInput¶
New
VmecInputwith the boundary dofsxapplied.Handles both the 2-family symmetric layout and the 4-family
lasymlayout (seepack_boundary()).
- vmex.core.optimize.residuals_from_tuples(state: SpectralState, runtime: SolverRuntime, objective_terms: Sequence[tuple[Callable, float, Any]], *, weight_semantics: str = 'cost') Array¶
Stack traceable
(function, target, weight)objective rows.With the default
weight_semantics="cost", each tuple contributessqrt(weight) * (function(state, runtime) - target)so the usual scalar objective is simply0.5 * residuals @ residuals. This small public building block is useful when a user owns the equilibrium map, such as a differentiable free-boundary solve, and wants to pass the resulting value and gradient to SciPy, JAXopt, Optax, or a custom optimizer directly.
- vmex.core.optimize.resample_current_profile(inp: VmecInput, n_spline: int, *, kind: str = 'cubic_spline_ip') VmecInput¶
Represent the current shape on
n_splineuniform spline knots.The existing enclosed-current profile is differentiated and sampled at the new knots, so continuation preserves
I(s)before the new knot values are optimized.CURTORremains the independent amplitude.kindmay becubic_spline_iporakima_spline_ip.
- vmex.core.optimize.least_squares(objective_terms: Sequence[tuple[Callable, float, Any]], inp: VmecInput, *, max_mode: int | Sequence[int] = 1, vary_major_radius: bool = False, x0: ndarray | None = None, current_dofs: int | None = None, jac: str | None = None, jac_chunk_size: int | str | None = 'auto', jac_solver: str = 'auto', adjoint_tol: float = 1e-06, jacobian_adjoint_tol: float = 0.0001, jacobian_adjoint_maxiter: int = 10, adjoint_maxiter: int = 300, max_fsq_ratio: float = 1000000.0, refine_tol: float = 1e-10, forward_ftol: float | None = None, forward_max_iterations: int | None = None, hot_restart: bool = True, warm_start: str | None = 'perturbation', use_ess: bool = False, ess_alpha: float = 1.2, device: Any = 'auto', solve_kwargs: dict | None = None, verbose: int = 0, **scipy_kwargs)¶
Boundary-shape least squares: simsopt’s
least_squares_serial_solve.objective_termsis a list of(fun, target, weight): eachfunmaps a convergedEquilibrium(or, for two-positional-argument callables, its(state, runtime)pair) to a scalar or residual vector, and contributesweight * (fun(eq) - target)rows to the stacked residual, i.e.cost = 1/2 sum_i w_i^2 (f_i - t_i)^2(scipy’s 1/2 convention). Decision variables are the boundary Fourier coefficients up tomax_mode(pack_boundary();RBC(0,0)fixed). Trial boundaries whose solve fails return a large finite residual so the trust region backs off instead of crashing. Staged campaigns with different objectives are just successive calls, each seeded with the previous call’sresult.input.current_dofs = kadditionally frees the current profile: the firstkACcoefficients, or the firstkAC_AUX_Fvalues for a spline profile, plusCURTOR. For ann-knot spline, usecurrent_dofs=n-1: the remaining fixed ordinate removes the profile’s overall-scale null direction becauseCURTORalready sets that scale. The values are scaled by their frozen seed magnitude so the trust region sees O(1) numbers. Requiresncurr = 1;resample_current_profile()changes spline resolution between continuation stages without changing the representedI(s). Both gradient modes support it (finite differences re-solve per current dof;jac="implicit"addsk + 1one-hot tangent rows throughImplicitParams.acorac_aux_fandcurtor). VMEC normalizes the AC profile by its own edge integral (only the shape ofI'matters;CURTORsets the amplitude). This is the dof set ofvmex.core.bootstrap.RedlBootstrapMismatch.max_modemay be a single int or an increasing schedule (e.g.(1, 2, 3)): each continuation stage optimizes the enlarged dof set starting from the previous stage’s boundary. Trial solves are cheap by construction: runtimes with the sameResolutionare structural pytrees, so one XLA executable is reused across all boundary trials (only the first solve of a stage compiles).deviceis forwarded to the solver ("auto"appliesvmex.core.device’s policy;Nonefollows JAX placement).use_essenables Exponential Spectral Scaling of the trust region (_ess_scale(),ess_alpha).jac=None(default) uses scipy"2-point"finite differences.jac="implicit"computes the exact residual Jacobian by forward implicit differentiation (module docstring): one hot-restarted forward solve per trial boundary and one preconditioned GMRES per boundary dof instead of one full equilibrium solve per dof. Every term must be traceable in(state, runtime)(vector terms exposeresiduals_state; wout-engine terms liked_merc()/l_grad_b()/ the Boozer QI residual needjac=None— use the traceablemercier_stability_residual()/l_grad_b_state()instead). Symmetric andlasymboundaries are both supported. The knobs below are inert forjac=None.jac_chunk_sizechunks the per-dof Jacobian columns viasolvax.chunk_map():"auto"(default) caps SOLVAX’s device-aware width by a conservative square-root policy, so an accelerator memory report cannot expand the full probe batch; anintfixes that many dofs at a time;Noneforces one wide batch. Automatic widths prefer a nearby divisor to avoid compiling a separate tail. Column blocks are mathematically independent, so the assembled Jacobian is identical across chunk sizes to float64 round-off.jac_solverselects the implicit-Jacobian direction."auto"(default) uses one matrix-free reverse solve for a scalar residual and the"block"path otherwise."reverse"requests one reverse solve per residual row."block"amortizes one block-tridiagonal factorization of the raw force Jacobian — whose radial coupling is exactly nearest-neighbor, so ns dense(3*mn, 3*mn)blocks assembled by chunked three-surface VJPs capture it completely at a cost independent of the dof count — then backsolves every dof right-hand side (solvax.block_thomas_factor()/solvax.block_thomas_solve()) and certifies each column with a warm-started GMRES pass against the preconditioned system (sameadjoint_toland configured iteration budget; columns already at tolerance cost one matvec)."gmres"is the per-dof-column fallback if the block path misbehaves on an exotic configuration; both produce the same Jacobian to solver tolerance. An uncertified automatic response is recomputed by reverse adjoint. Forced host methods raiseAdjointSolveError; transformable JAX methods fall back to reverse so no compiled public path returns an uncertified matrix.adjoint_tolandadjoint_maxitercontrol the certified linear response solves. All public optimization paths allow 300 restarts by default so QI derivatives cannot fail merely because of the former hard-coded 30-restart cap. Easier problems normally stop far earlier.hot_restartseeds each trial solve from the previous converged state (both modes; in implicit mode via the per-config host-solve cache).warm_start(jac="implicit"only) refines that seed."perturbation"(default) seeds each trial with the DESC-style first-order predictionx_ref + sum_j (dx)_j dz_j(arXiv:2203.15927eq.perturbbeforeeq.solve): the per-dof state responses are exactly the columns the implicit Jacobian already solves, so the linearization is stashed at eachjac(x_ref)call for free."state"is the plain hot restart;Nonedisables warm starting. All three converge to the same fixed points — only the inner iteration count changes — and a missing or mismatched seed falls back through the perturbation -> state -> cold ladder.hot_restart=Falseforceswarm_start=None.Remaining keywords go to
scipy.optimize.least_squares()(e.g.max_nfev,ftol,xtol,diff_step).Returns the scipy
OptimizeResultof the final stage with extra attributes:input(optimizedVmecInput),equilibrium(last successfully solvedEquilibrium),stage_results(per-max_moderesults for schedules) and, in implicit mode,solve_stats(cumulative solve, refinement, Jacobian and adjoint counters of the stage’s configuration, seeimplicit._SOLVE_STATS).
- vmex.core.optimize.minimize(objective_terms: Sequence[tuple[Callable, float, Any]], inp: VmecInput, *, max_mode: int | Sequence[int] = 1, vary_major_radius: bool = False, x0: ndarray | None = None, current_dofs: int | None = None, hot_restart: bool = True, device: Any = 'auto', solve_kwargs: dict | None = None, verbose: int = 0, method: str = 'L-BFGS-B', adjoint_tol: float = 1e-06, adjoint_maxiter: int = 300, max_fsq_ratio: float = 1000000.0, refine_tol: float = 1e-10, forward_ftol: float | None = None, forward_max_iterations: int | None = None, **scipy_kwargs)¶
Minimize the scalarized residual norm with one adjoint per gradient.
The objective is exactly
0.5 * sum(rows**2), withrowsdefined byleast_squares(). Unlike Gauss–Newton least squares, a reverse gradient of this scalar needs one matrix-free implicit adjoint and never forms the vector residual Jacobian or its dense radial block factors. This is the bounded-storage path for profile objectives such asDMerc,jdotb, and GlasserD_R. It changes the optimization algorithm, not the objective or its unconstrained minimizers, and is therefore opt-in;least_squares()retains all existing defaults.methodand remaining keywords are passed toscipy.optimize.minimize()(default"L-BFGS-B"; usebounds=andoptions={"maxiter": ...}in the usual scipy form). All objective terms must supportjac="implicit"as documented byleast_squares(). Plain state hot restarts are used because the first-order perturbation warm start requires the forward state-response columns that this lower-storage path deliberately avoids.adjoint_tol/adjoint_maxiterare exposed explicitly. Their defaults are certified on the QI, QS,L_grad_B,DMercandD_Robjective lanes; an unconverged adjoint is never returned as a gradient.
The optimizer-neutral callables behind vj.VmecProblem.from_tuples in the
README: value, residual, and derivative functions with the contracts SciPy,
JAXopt, Optax, and user code consume, and no optimization algorithm of their
own.
Optimizer-neutral objective and derivative callables.
The classes in this module contain no optimization algorithms. They expose
the small function contracts consumed by SciPy, JAXopt, Optax, and user code.
VMEC-specific construction is imported lazily so this module remains usable in
lightweight tests and does not introduce an import cycle with
vmex.core.optimize.
- class vmex.core.problem.Evaluation(x: ndarray, value: float | None = None, gradient: ndarray | None = None, residual: ndarray | None = None, jacobian: ndarray | None = None, status: str = 'success', message: str = '', diagnostics: Mapping[str, ~typing.Any]=<factory>)¶
Values and diagnostics produced at one decision vector.
Fields that were not requested or are unavailable are
None.statusis a short machine-readable value such as"success"or"failed_solve";messageis intended for a human. Optimizers use the ordinary callable methods and do not need to understand this object.Returned by
FunctionProblem.evaluate()and the twocompile_*helpers. It is a report, not a cache: nothing here is consulted by a later evaluation.- x¶
The decision vector the values were produced at, as an owned float copy with the shape of
problem.x0.- Type:
- value¶
Scalar objective at
x. For a residual-only problem this is the least-squares cost0.5 * r @ r, matching SciPy’sOptimizeResult.cost.Nonewhen the problem exposes neither a scalar objective nor residuals.- Type:
float | None
- gradient¶
Gradient of
valuewith respect tox, reshaped tox’s shape. For a residual problem it isJ.T @ r.Nonewhenderivatives=Falsewas requested or no gradient lane exists.- Type:
numpy.ndarray | None
- residual¶
The flattened residual vector
r(x), orNonefor a scalar-only problem.- Type:
numpy.ndarray | None
- jacobian¶
The residual Jacobian
dr_i/dx_jwith shape(residual.size, x.size), orNonewhen derivatives were not requested or the problem provides no Jacobian.- Type:
numpy.ndarray | None
- status¶
"success", or — from a VMEC-backed problem —"failed_solve"when the equilibrium solve atxraised, and"under_converged"when it returned but its force residual exceeds the threshold below which implicit derivatives are certified.- Type:
- message¶
Human-readable explanation, empty on success; the solver exception’s text for
"failed_solve".- Type:
- diagnostics¶
Extra per-evaluation values. Empty for a plain
FunctionProblem. AVmecProblemadds the cumulativefailed_trialsandderivative_fallbackscounters, asolve_statsmapping when the implicit lane recorded one, and — when the equilibrium atxcould be materialised — the summed force residualfsq, itsfsq_ratioto the solve tolerance, the configuredmax_fsq_ratio, and the booleanderivative_certified. A failed solve also carriesexception_type.- Type:
Mapping[str, Any]
- class vmex.core.problem.FunctionProblem(x0: Any, *, fun: Callable[[ndarray], Any] | None = None, grad: Callable[[ndarray], Any] | None = None, value_and_grad: Callable[[ndarray], Any] | None = None, residual: Callable[[ndarray], Any] | None = None, residual_jac: Callable[[ndarray], Any] | None = None, residual_and_jac: Callable[[ndarray], Any] | None = None, jax_fun: Callable[[Any], Any] | None = None, jax_value_and_grad: Callable[[Any], tuple[Any, Any]] | None = None, jax_residual: Callable[[Any], Any] | None = None, jax_residual_jac: Callable[[Any], Any] | None = None, names: Sequence[str] | None = None, bounds: Any = None, scales: Any | None = None, metadata: Mapping[str, Any] | None = None, evaluation_progress: bool = False, report_interval: float = 10.0)¶
A decision vector plus optimizer-compatible objective callables.
Parameters are explicit and immutable from the caller’s perspective. Supplying combined value/gradient or residual/Jacobian functions enables a one-entry exact-key cache, so the common SciPy call sequence does not repeat expensive work. The cache is protected by a lock; JAX-native callables do not use host state and remain suitable for tracing.
This class deliberately does not provide
solve(method=...). Pass its methods directly to the optimizer of choice.At least one of
fun,value_and_grad,residual, orresidual_and_jacis required; every other callable is optional and the matching method raisesAttributeErrorwhen its lane is absent. The host callables receive one contiguous float NumPy array and are free to be opaque; thejax_*callables receive whatever the caller traces and must stay traceable.Two independent one-entry caches, each keyed on the exact bytes of
x(shape, dtype, and contents — no tolerance), avoid repeating work across the split calls an optimizer makes at one iterate. The scalar cache is filled byvalue_and_grad()and so coversvalue_and_gradalone,funtogether withgrad, orresidual_and_jac. The least-squares cache is filled byresidual_and_jac()and so coversresidual_and_jac, orresidualtogether withresidual_jac. Whether the cache actually pays depends on which callables were supplied: withresidual_and_jac, SciPy’s separatefun(x)thenjac(x)calls both route through it and the second is free, whereas separately suppliedresidualandresidual_jacare each invoked directly and share nothing. Cached arrays are copied out, so a caller may mutate what it receives. The caches are guarded by a re-entrant lock; thejax_*lane touches no host state and stays safe to trace.- Parameters:
x0 – Initial decision vector. Copied to a float array; its size fixes the number of degrees of freedom and the expected Jacobian column count, and its shape is the shape gradients are reshaped to.
fun –
x -> float, the scalar objective. Called directly byfun()without touching the cache.grad –
x -> array, the objective gradient. Used only in combination withfun; on its own it does not enablegrad().value_and_grad –
x -> (float, array). The preferred scalar lane: it is one call for both quantities and it fills the scalar cache. The method that serves it is also reachable under SciPy’sfun_and_gradname.residual –
x -> array, the least-squares residual vectorr(x). It is flattened, and defines the scalar objective0.5 * r @ rwhen the problem has no scalar lane of its own (nofun,grad, orvalue_and_grad).residual_jac –
x -> array, the Jacobiandr_i/dx_j. It must have one column per decision variable; anything else raisesValueError.residual_and_jac –
x -> (array, array), the preferred least-squares lane: one call for both, filling the cache thatresidual()andresidual_jac()then read. The Jacobian shape is checked exactly against(r.size, x0.size).jax_fun – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_value_and_grad – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_residual – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.jax_residual_jac – Traceable counterparts of the four callables above, returned unwrapped by the matching
jax_*methods. They are never cached and never see host state, so they remain usable insidejax.jit()andjax.grad().jax_funfalls back to the first element ofjax_value_and_gradwhen it is not supplied.names – One name per decision variable, in order, surfaced as
dof_namesfor labelling output. The default isx[0], x[1], ...; a length other thanx0.sizeraisesValueError.bounds – Box constraints stored verbatim for the optimizer to consume — a SciPy
Bounds, or a(lower, upper)pair. This class neither interprets nor enforces them.scales – Positive finite per-variable scale factors with the shape of
x0, defaulting to ones. Likeboundsthey are carried, not applied: pass them to the optimizer (SciPy’sx_scale). A non-finite or non-positive entry raisesValueError.metadata – Free-form mapping copied onto the instance. VMEX-built problems use it to carry the named residual slices, the solver configuration, the mutable solve counters, and the traceable state accessors that
VmecProblemreads.evaluation_progress – Print an elapsed-time heartbeat around long evaluations. It stays silent until a call outlives the first interval, so fast calls print nothing. It wraps the standalone
residual()andresidual_jac()calls only, which is where a production deck spends minutes; the combined and scalar lanes are unaffected.report_interval – Seconds between heartbeat lines. Must be positive.
- property dof_names: tuple[str, ...]¶
Ordered names corresponding one-to-one with entries of a decision vector.
- classmethod from_functions(x0: Any, **kwargs: Any) FunctionProblem¶
Build a problem from user-supplied x-level callables.
- evaluate(x: Any, *, derivatives: bool = True) Evaluation¶
Evaluate available scalar and residual quantities at
x.
- compile_residual_and_jacobian(x: Any | None = None, *, progress: bool = True, report_interval: float = 10.0, stream: Any = None) Evaluation¶
Compile and cache the least-squares residual and Jacobian.
This call is optional: an optimizer compiles on its first evaluation if it is omitted. Calling it explicitly provides elapsed-time output during a potentially long first JAX compilation. Later calls at the same
xuse the normal one-entry problem cache.
- compile_value_and_gradient(x: Any | None = None, *, progress: bool = True, report_interval: float = 10.0, stream: Any = None) Evaluation¶
Compile and cache the scalar value and gradient.
This optional call makes the first JAX compilation visible before BFGS, L-BFGS-B, Adam, or another gradient optimizer starts.
- class vmex.core.problem.VmecProblem(*args: Any, input_from_x: Callable[[Any], Any], x_from_input: Callable[[Any], Any], equilibrium_from_x: Callable[[...], Any] | None = None, boundary_from_x: Callable[[Any], Any] | None = None, **kwargs: Any)¶
A
FunctionProblembacked by a VMEX equilibrium solve.Adds the maps between the optimizer’s decision vector and VMEC objects: the input deck, the converged equilibrium, and the boundary coefficient arrays. It keeps the same optimizer contract as its base class, so the same methods go to SciPy, JAXopt, or Optax unchanged, and it enriches
evaluate()with the solve and adjoint status of the underlying equilibrium.Build one with
from_tuples(),from_loss(), orfrom_input()rather than calling this constructor: they route throughvmex.core.optimize.make_problem, which is what assembles the four callables below along with the objective, the derivative lane, the degree-of-freedom names, and the metadata.- Parameters:
*args – Forwarded positionally to
FunctionProblem; in practice the decision vectorx0.**kwargs – Forwarded to
FunctionProblem: the objective callables,names,bounds,scales, andmetadata.input_from_x – Required.
x -> VmecInput: a new input deck carrying the boundary coefficients — and the current degrees of freedom, when the problem parameterizes them — of this decision vector. Nothing is solved.x_from_input – Required. The inverse,
VmecInput -> array: the decision vector that reproduces a given deck, which is the normal starting point of a continuation stage.x_from_input()rejects a result whose shape differs fromx0.equilibrium_from_x – Optional
x -> Equilibrium, the converged equilibrium atx. Implicit problems return the accepted state the objective already computed rather than cold-solving the boundary again, which matters for strongly shaped boundaries whose cold axis guess can produce a sign-changing initial Jacobian.Nonemakesequilibrium_from_x()raiseAttributeError. A callable that accepts anewton_iterationskeyword receives it only when the caller asks for something other than the default 10, so a closure without that keyword still works.boundary_from_x – Optional
x -> tuple of arrays, the traceable boundary coefficients:(rbc, zbs)for a stellarator-symmetric input and(rbc, zbs, rbs, zbc)whenlasym. Each is a full dense INDATA-layout array of shape(2 * ntor + 1, mpol)indexed[n + ntor, m]in metres — not the trimmed decision vector — and is a JAX array, so it composes with coil or surface objectives underjax.grad().Nonemakesboundary_from_x()raiseAttributeError.
- classmethod from_tuples(inp: Any, objective_terms: Sequence[tuple[Callable[[...], Any], Any, float]], **kwargs: Any) VmecProblem¶
Build a VMEC least-squares problem from weighted objective tuples.
The README entry point. Each tuple is
(function, target, weight)and contributes one or more rows to a single residual vector; the rows of all terms are concatenated in the order given and the scalar cost is0.5 * r @ r. Named row ranges are recorded in the problem metadata, which is what letsOptimizationMonitorreport per-term costs without re-solving anything.- Parameters:
inp – The starting
VmecInput. Its boundary supplies the initial decision vector and its resolution and profiles are held fixed apart from the parameterized degrees of freedom.objective_terms –
The
(function, target, weight)triples.functionis normally a traceablefunction(state, runtime) -> scalar or vector, evaluated on the converged equilibrium — the two arguments are also spelled(equilibrium_state, solver_context). An objective object exposing aresiduals_statemethod may be passed instead (whole instance or bound method), in which case its full pointwise residual vector becomes this term’s rows. Underderivative_method="finite_difference"a one-argument host callable taking the wholeEquilibriumis accepted too; the implicit lane rejects it, since it cannot be traced.targetis the value the term is driven toward, coerced withfloat(), so it must be scalar even whenfunctionreturns a vector — the same target is then subtracted from every row.weightis a non-negative scalar, or a one-dimensional array with one entry per residual row of that term. Under the defaultweight_semantics="cost"it multiplies the squared cost, so the row issqrt(weight) * (function - target); withweight_semantics="residual"the row isweight * (function - target)and a negative entry is then allowed.**kwargs – Passed through to
vmex.core.optimize.make_problem: which boundary modes vary (max_mode,vary_major_radius,current_dofs), the derivative lane (derivative_method,implicit_jacobian_method,jacobian_batch_size), the forward solve controls, the variable scaling (use_ess,ess_alpha,bounds), andweight_semantics.
- Returns:
A
VmecProblemwhoseresidual/residual_jacpair,x0, andscalesare ready forscipy.optimize.least_squares(). A non-finite or emptyresidual at the initial point raises
FloatingPointErrorrather than starting an optimization that cannot recover.
- classmethod from_loss(inp: Any, loss: Callable[[...], Any], **kwargs: Any) VmecProblem¶
Build a VMEC scalar problem from a traceable state/runtime loss.
- Parameters:
inp – The starting
VmecInput, as forfrom_tuples().loss –
loss(state, runtime) -> scalar, evaluated on the converged equilibrium and already carrying its own weights. It must return a single value: a vector-valued objective belongs infrom_tuples(), or must be reduced here explicitly. Unlike anobjective_termsentry it is used exactly as written — an object’sresiduals_stateis never substituted for it.**kwargs – As for
from_tuples().
- Returns:
A
VmecProblemexposing only the scalar lane —their traceable counterparts — for a gradient optimizer such as
BFGS, L-BFGS-B, or Adam. It provides no residual or Jacobian.
- classmethod from_input(inp: Any, **kwargs: Any) VmecProblem¶
Parameterize an input for field VJPs without defining an objective.
Builds the same machinery as
from_loss()around an identically zero loss, so there is nothing to minimize. Use it when what you want is the parameterization itself: the decision vector and its names,input_from_x()andboundary_from_x(), and the differentiableinterior_field()andexterior_field()with exact VJPs in these degrees of freedom.inpand**kwargsare as forfrom_tuples().
- x_from_input(inp: Any) ndarray¶
Return this problem’s decision vector for
inp.This is the inverse of
input_from_x()for the boundary and any optional current degrees of freedom selected when the problem was constructed. It is the normal continuation-stage starting vector.
- equilibrium_from_x(x: Any, *, newton_iterations: int = 10) Any¶
Return the converged equilibrium evaluated at
x.Implicit problems reuse the accepted optimizer state instead of cold-solving the optimized boundary again. This matters for strongly shaped boundaries whose cold magnetic-axis guess may have a sign-changing initial Jacobian.
- jax_objective_from_state(x: Any, extra_costs: Callable[[Any, Any], Any], *, n_extra_terms: int) tuple[Any, tuple[Any, Any]]¶
Combine the VMEX least-squares cost with state-dependent costs.
extra_costs(state, runtime)returns one already-weighted scalar cost per added objective term. The auxiliary result contains the VMEX residual rows and those added costs, ready to pass as auxiliary data tojax.value_and_grad(). Failed equilibrium trials receive the same smooth finite rejection cost as the base problem, so driver scripts do not need their own accepted/rejected branches.
- jax_extra_costs_from_state(x: Any, extra_costs: Callable[[Any, Any], Any], *, n_extra_terms: int) tuple[Any, Any]¶
Evaluate additive state-dependent costs only at valid VMEC trials.
This is the split-compilation counterpart of
jax_objective_from_state(). It returns zero extra cost at a rejected trial, leaving the base problem to supply its certified rejection wall. Splitting a large virtual-casing or coil graph from the VMEC objective substantially lowers peak XLA compilation memory.
- jax_quantity_from_state(x: Any, quantity: Callable[[Any, Any], Any]) tuple[Any, Any]¶
Evaluate a differentiable floating-point quantity and solve status.
quantity(state, runtime)may return any fixed-shape JAX array. Rejected equilibrium trials return an array of NaNs with that shape, so an invalid state cannot look like a usable diagnostic. The scalar status is zero only for an accepted equilibrium.
- exterior_field(x: Any, *, external_field: Any | None = None, external_parameters: Any | None = None, external_field_from_parameters: Callable[[Any], Any] | None = None, external_dof_names: tuple[str, ...] = (), nphi: int = 32, ntheta: int = 32, digits: int = 6, levels: tuple[tuple[int, int], ...] | None = None, chunk_size: int | str = 'auto', target_chunk_size: int | str = 'auto') Any¶
Return the exterior field and exact VJPs in this problem’s DOFs.
Query points must lie outside the last closed flux surface and away from coil filaments. The returned field follows the stored-point API:
field.set_points(xyz); field.B(); field.B_vjp(cotangent). Set the source and target chunk sizes only to cap virtual-casing memory;"auto"is the tuned default.
- interior_field(x: Any, *, newton_iterations: int = 10) Any¶
Return the interior field and exact VJPs in this problem’s DOFs.
- surface_field_values(x: Any, quantity: str, *, external_field: Any | None = None, nphi: int = 32, ntheta: int = 32, digits: int = 4, precision: Any | None = None) Any¶
Return
|B|orB.n/Bon a trial boundary for plotting.B.n/Bis evaluated on the exterior side using the supplied coil or MGRID field plus the plasma-current virtual-casing field. This helper keeps optional movie coloring out of optimization driver code; it is not used by the objective or optimizer.
- evaluate(x: Any, *, derivatives: bool = True) Evaluation¶
Evaluate and attach VMEC solve/adjoint status diagnostics.
Accepted-iteration reporting independent of optimization algorithms.
- class vmex.core.monitoring.EquilibriumReporter(*quantities: tuple[str, ~typing.Callable[[...], ~typing.Any], str], stream: ~typing.TextIO | None | object = <object object>, separator: str = ', ')¶
Print a compact set of scalar diagnostics for an equilibrium.
Each quantity is
(label, callable, format_spec). Callables may use either thefunction(equilibrium)orfunction(state, runtime)convention used by VMEX objectives. Calling the reporter prints one line and returns the values by label, so scripts can also reuse a final metric.- Parameters:
*quantities – One
(label, function, format_spec)triple per reported column; at least one is required and the labels must be unique.labelnames the column and keys the returned mapping.functionis dispatched on its signature: a callable whose second positional parameter has no default is called asfunction(state, runtime)(the VMEX objective convention), every other callable asfunction(equilibrium). It must return exactly one scalar; any other size raisesValueError.format_specis aformat()specification applied to that float, for example".6e".stream – Where the report line is written. The default is
sys.stdout; passNoneto compute and return the values without printing.separator – Text placed between the
label = valuefields of the printed line.
- class vmex.core.monitoring.OptimizationMonitor(problem: FunctionProblem | None = None, *, stream: TextIO | None | object = <object object>, print_every: int = 1, trace: bool = True)¶
Record and optionally print optimizer iterations and trials.
Pass the instance as a SciPy
callback. SciPy invokes callbacks after an iteration, unlike objective functions which are also called for rejected line-search or trust-region trials. JAXopt, Optax, and custom loops can callrecord()with values they already computed.tracealso prints onetrialline per objective evaluation, so the rejected line-search trials between two accepted iterations – a full equilibrium solve each – are visible while the run is in progress.The monitor never chooses steps or changes an optimizer. If
problemis supplied, VMEX solve/failure counters are read without evaluating the objective again.- Parameters:
problem – Optional problem the optimizer is running on. It is used only to read metadata: the named residual slices that split
costinto per-term costs, the cumulative equilibrium-solve and failed-trial counters, and — as a last resort, when a callback carries neithercostnorfun— onefun()call at the accepted iterate. WithNonethose record fields stayNoneor empty and the monitor still records everything the callback provides.stream – Where the per-iteration table is written. The default is
sys.stdout; passNoneto record silently and readrecords,history, orsave()afterwards.print_every – Print one row every
print_everyrecords (the first record is always printed, together with the header). Must be at least 1; recording is unaffected.
- wrap_value_and_grad(function: Callable | Sequence[Callable], term_names: tuple[str, ...] | None = None, *, residual_slices: tuple[tuple[str, int, int], ...] = ()) Callable¶
Adapt a JAX
has_auxvalue/gradient pair for SciPy.Each
function(x)must return((cost, terms), gradient). Pass a sequence to compile large additive physics components separately; their costs and gradients are summed without changing the optimizer contract.termsmay be a mapping of labels to weighted scalar costs, or one compact vector paired withterm_names. For a large residual graph, passresidual_slicesand return(residual, extra_costs...); costs are reduced on the host to keep the compiled output small. The first evaluation is recorded as iteration zero; later evaluations are cached.
- cache_evaluation(x: Any, cost: Any, gradient: Any, terms: Mapping[str, Any] | None = None) tuple[float, ndarray]¶
Cache an already-computed objective pair for a SciPy callback.
This method does no differentiation and does not alter the objective. Driver scripts can show their explicit
jax.value_and_gradcalls, sum independently compiled physics components themselves, and use this one host conversion to avoid recomputing per-term costs when SciPy later reports an accepted iterate.
- record(x: Any, *, cost: float, optimality: float | None = None, iteration: int | None = None, equilibrium_solves: int | None = None, rejected_trials: int | None = None, terms: Mapping[str, Any] | None = None, counters: Mapping[str, Any] | None = None) OptimizationRecord¶
Append one already-computed accepted iterate and return its record.
- Parameters:
x – The accepted decision vector; a float copy is appended to
x_history.cost – Total scalar objective at
x. Nothing is recomputed.optimality – First-order optimality measure, or
Nonewhen unknown.iteration – Iteration index; defaults to the number of records already held, and is advanced past the previous record when an optimizer restarts its own counter at a continuation stage.
equilibrium_solves – Cumulative solve and failed-trial counts.
None(the default) reads them from the monitor’sproblem, leaving the fieldNonewhen no problem was supplied.rejected_trials – Cumulative solve and failed-trial counts.
None(the default) reads them from the monitor’sproblem, leaving the fieldNonewhen no problem was supplied.terms – Per-term weighted costs by label.
Nonesplits the problem’s own residual over its named term slices instead, which may evaluate the residual once atx.counters – Effort counters by name.
Nonereads them from the monitor’sproblem, empty when it carries none.
- Return type:
The appended
OptimizationRecord.
- property x_history: tuple[ndarray, ...]¶
Copies of the accepted decision vectors, including iteration zero.
- plot(path: str | Path, *, title: str = 'Optimization objective terms') Path¶
Write a compact log-scale total and per-term cost history plot.
- movie(path: str | Path, object_factory: Callable[[ndarray], Any], **kwargs: Any) Path¶
Animate accepted surface/coil iterates with one geometry callback.
- movie_surface_coils(path: str | Path, object_factory: Callable[[ndarray], Any], *, x0: Any, scales: Any, surface_color: str | Callable | None = None, plasma_problem: Any | None = None, external_field: Callable[[Any], Any] | None = None, nphi: int = 32, ntheta: int = 32, digits: int = 4, precision: Any | None = None, **kwargs: Any) Path¶
Animate normalized surface/coil iterates with optional field color.
object_factoryreceives physical variablesx0 + scales*u.surface_colormay beNone,"absB","B.n/B", or a callable(u, objects) -> values. For the two named field colors, provideplasma_problem;B.n/Badditionally needsexternal_field(objects). These plotting helpers never enter the objective or gradient graph.
- class vmex.core.monitoring.OptimizationRecord(iteration: int, cost: float, reduction: float | None, optimality: float | None, equilibrium_solves: int | None, rejected_trials: int | None, terms: Mapping[str, float]=<factory>, counters: Mapping[str, float | None]=<factory>)¶
One optimizer callback, normally one accepted iteration.
Produced by
OptimizationMonitor.record()and stored inOptimizationMonitor.records. Every field is a plain host value; a field isNonewhen the optimizer callback did not supply it and the monitor could not derive it, never zero-as-unknown.- iteration¶
Iteration index of this accepted iterate. Taken from the SciPy result’s
nitwhen present, otherwise the number of records already held. Optimizers restart their counter at every continuation stage, so a repeated or decreasing value is bumped to one past the previous record; the sequence in one monitor is always strictly increasing.- Type:
- cost¶
Total scalar objective at this iterate. For a least-squares problem this is
0.5 * r @ rover the full residual vector, so it matches SciPy’sOptimizeResult.cost; for a scalar objective it is the objective value itself. Units are those of the weighted objective (dimensionless for the usual normalised VMEX terms).- Type:
- reduction¶
previous cost - this cost, positive when the step improved the objective.Nonefor the first record, which has no predecessor.- Type:
float | None
- optimality¶
First-order optimality measure of the gradient. SciPy’s own
optimalityis used when the callback provides it; otherwise it is the infinity norm of a callback-suppliedjac, or the Euclidean norm of the gradient cached byOptimizationMonitor.cache_evaluation().Nonewhen no gradient information reached the monitor.- Type:
float | None
- equilibrium_solves¶
Cumulative number of forward VMEC equilibrium solves performed for this problem’s implicit configuration, read from the solver’s own counters without re-evaluating the objective.
Nonewhen the monitor was built without aproblemor the problem carries no implicit configuration (for example a finite-difference or wout-only problem).- Type:
int | None
- rejected_trials¶
Cumulative number of optimizer trial points whose equilibrium solve failed and was replaced by the smooth rejection cost.
Noneunder the same conditions asequilibrium_solves.- Type:
int | None
- terms¶
Per-term weighted costs by label,
0.5 * r_k @ r_kover each named residual slice of the problem, so the values sum tocostfor a pure least-squares problem. Empty when the problem exposes no term slices and the caller supplied none.
- counters¶
Cumulative effort counters of the problem’s implicit configuration at this record: solves and descent iterations, refinement calls, steps and Krylov iterations, Jacobian calls, columns and certifier Krylov iterations, adjoint calls and Krylov iterations, and the host seconds of each part. An entry is
Nonewhen the work ran inside a compiled program where it cannot be observed. Empty under the same conditions asequilibrium_solves.
Concurrent ensembles of independent equilibrium solves on CPU (Item G).
A parameter scan or an ensemble optimization solves N independent
equilibria (different boundaries / phiedge / profiles). Each forward
solve runs on the host behind jax.pure_callback()
(vmex.core.implicit) or directly through
vmex.core.solver.solve() / vmex.core.multigrid.solve_multigrid(),
and — crucially — releases the Python GIL while XLA executes the compiled
iteration lanes. A plain concurrent.futures.ThreadPoolExecutor over
those independent solves therefore overlaps their XLA execution and gives real
wall-clock speedup, while every result stays byte-identical to solving that
input alone (the solves share no mutable state).
Measured strong scaling (10-core host, 8 balanced phiedge solves): 1.79x
with 2 workers, 3.29x with 8. The scaling is deliberately sub-linear: XLA
already multithreads within one solve, so as the worker count approaches
the core count the per-solve XLA threads contend — the ensemble speedup and
the intra-solve speedup draw from the same cores. See Why threading, not pmap
for the full mechanism study (why threading beats pmap across forced host
devices and vmap over the callback here), the honest limits (Amdahl on
imbalanced heterogeneous ensembles; the launch-bound implicit adjoint
overlaps far less than the forward solve), and the multi-GPU design sketch.
This module is a thin, additive concurrency layer: it changes nothing in the single-solve path (which stays byte-identical) and imposes no new dependency.
- vmex.core.parallel.available_cpus() int¶
Return the number of CPUs available, respecting scheduler limits.
- vmex.core.parallel.default_workers(n_items: int, workers: int | None = None) int¶
Resolve the worker count for an
n_itemsensemble.Noneuses the CPUs available to this process, including scheduler affinity limits. An explicit value is clamped to[1, n_items].
- vmex.core.parallel.evaluate_problems(problems: Sequence[Any], xs: Sequence[Any] | None = None, *, derivatives: bool = True, workers: int | None = None, return_exceptions: bool = False) list[Any]¶
Evaluate independent problem objects concurrently in input order.
Use one problem per ensemble or multistart member so each equilibrium cache remains local.
xsdefaults to each problem’sx0.
- vmex.core.parallel.finite_difference_gradient(fun: Callable[[Any], Any], x: Any, **kwargs: Any) Any¶
Parallel finite-difference gradient of a scalar host function.
- vmex.core.parallel.finite_difference_jacobian(fun: Callable[[Any], Any], x: Any, *, method: str = '3-point', rel_step: float | None = None, workers: int | None = None) Any¶
Differentiate an opaque host function with independent parallel probes.
method="3-point"(default) is central and second-order accurate;"2-point"is forward and first-order accurate.workers=Noneusesdefault_workers(), whileworkers=1is deterministic serial execution. Every probe receives its own copy ofxandfunmust not mutate shared state.
- vmex.core.parallel.map_ensemble(fn: Callable[[_T], _R], items: Iterable[_T], *, workers: int | None = None, return_exceptions: bool = False) list[_R]¶
Apply
fnto each ofitemsconcurrently on CPU; keep input order.The general primitive behind
solve_ensemble().fnmust be an independent per-item computation (e.g. a fullvj.solve/implicit.run/jax.value_and_gradover one input) that shares no mutable state with the others — which every vmex forward solve is, since each builds its own runtime and the compiled-executable cache is thread-safe. Under those conditions the results are byte-identical to a serial[fn(x) for x in items](the concurrency only overlaps the GIL-releasing XLA execution windows).workers— seedefault_workers(). Withworkers=1the pool runs sequentially (a clean serial baseline for scaling measurements).return_exceptions=False(default) re-raises the first item’s exception (preserving vmex’s typedVmecErrortaxonomy), exactly as a serial loop would.return_exceptions=Trueinstead places the caught exception object in that slot so one failed ensemble member does not abort the batch (useful for optimization ensembles / robustness scans).
- vmex.core.parallel.solve_ensemble(inputs: Sequence[Any], *, workers: int | None = None, multigrid: bool = True, return_exceptions: bool = False, **solve_kwargs: Any) list[Any]¶
Solve
NindependentVmecInputconcurrently.Threads
vmex.core.multigrid.solve_multigrid()(multigrid=True, default — runs each input’sNS_ARRAYladder) orvmex.core.solver.solve()(multigrid=False, single grid) over the ensemble on CPU, returning the list ofSolveResultin input order. Each result is byte-identical to solving that input by itself (verified intests/test_parallel.py): the helper only overlaps the solves’ XLA execution — it does not touch the numerics, the convergence path, or the default single-solve code path.Extra
**solve_kwargs(e.g.verbose,ftol,initial_state) are forwarded unchanged to every solve.workersandreturn_exceptionsbehave as inmap_ensemble().Best speedup comes from a balanced ensemble — a parameter scan at fixed resolution, where the members share a compiled executable and take a similar iteration count. A heterogeneous ensemble is limited by its slowest member (Amdahl); see Why threading, not pmap.
Outputs¶
VMEC2000-compatible wout_*.nc schema, writer and reader.
This module implements the full variable set written by VMEC2000’s
wrout.f (Appendix A). Core fixed-boundary fields use the reference
netCDF names, dimensions, dtypes and unit conventions. Free-boundary results
also populate the symmetric or LASYM NESTOR potential and surface fields when
their public VacuumOutput is supplied:
presf/pres/massare stored in Pa (wrout.fdivides the internalmu0*Pavalues bymu0on write);jcuru/jcurv/ctor/currumnc/currvmncare in A (again1/mu0applied on write);phipf/chipfcarry thetwopi*signgsfactor relative to the internalphips/chipsarrays;q_factor = 1/iotaf(HUGEat iota zeros);lmnsis on the half mesh (interpolated from VMEC’s internal full-mesh lambda),bsubsmnson the full mesh (converted injxbforce.f);lasympartner tables (rmns,zmnc, …) exist only for asymmetric runs; the free-boundary potential/surface tables (potsin,xmpot,xnpot,curlabel,*_sur) only whenlfreeb.
WoutData stores every field in file convention (exactly the
values found in the netCDF file), so read_wout(write_wout(x)) == x.
VMEX-written files also carry the versioned vmex_trapped_fraction radial
profile; older WOUT files remain readable and are rewritten without the
extension.
wout_from_state() builds the complete dataset from a converged
fixed-boundary core state (vmex.core.input.VmecInput +
vmex.core.solver.SpectralState), legacy-free: the
Nyquist/jxbforce/Mercier tables come from the wrout.f/bss.f/
jxbforce.f/mercier.f ports in vmex.core.nyquist, the
remaining VMEC2000 output quantities (eqfor.f/spectrum.f/
Compute_Currents et al.) from vmex.core.postprocess.
- class vmex.core.wout.WoutData(version_: float, input_extension: str, mgrid_file: str, pcurr_type: str, pmass_type: str, piota_type: str, wb: float, wp: float, gamma: float, rmax_surf: float, rmin_surf: float, zmax_surf: float, nfp: int, ns: int, mpol: int, ntor: int, mnmax: int, mnyq: int, nnyq: int, mnmax_nyq: int, niter: int, itfsq: int, lasym: bool, lrecon: bool, lfreeb: bool, lmove_axis: bool, lrfp: bool, ier_flag: int, aspect: float, betatotal: float, betapol: float, betator: float, betaxis: float, b0: float, rbtor0: float, rbtor: float, signgs: int, IonLarmor: float, volavgB: float, ctor: float, Aminor_p: float, Rmajor_p: float, volume_p: float, ftolv: float, fsql: float, fsqr: float, fsqz: float, nextcur: int, extcur: ndarray, mgrid_mode: str, xm: ndarray, xn: ndarray, xm_nyq: ndarray, xn_nyq: ndarray, raxis_cc: ndarray, zaxis_cs: ndarray, am: ndarray, ac: ndarray, ai: ndarray, am_aux_s: ndarray, am_aux_f: ndarray, ai_aux_s: ndarray, ai_aux_f: ndarray, ac_aux_s: ndarray, ac_aux_f: ndarray, iotaf: ndarray, q_factor: ndarray, presf: ndarray, phi: ndarray, phipf: ndarray, chi: ndarray, chipf: ndarray, jcuru: ndarray, jcurv: ndarray, iotas: ndarray, mass: ndarray, pres: ndarray, beta_vol: ndarray, buco: ndarray, bvco: ndarray, vp: ndarray, specw: ndarray, phips: ndarray, over_r: ndarray, jdotb: ndarray, bdotb: ndarray, bdotgradv: ndarray, DMerc: ndarray, DShear: ndarray, DWell: ndarray, DCurr: ndarray, DGeod: ndarray, equif: ndarray, fsqt: ndarray, wdot: ndarray, rmnc: ndarray, zmns: ndarray, lmns: ndarray, gmnc: ndarray, bmnc: ndarray, bsubumnc: ndarray, bsubvmnc: ndarray, bsubsmns: ndarray, currumnc: ndarray, currvmnc: ndarray, bsupumnc: ndarray, bsupvmnc: ndarray, raxis_cs: ndarray | None = None, zaxis_cc: ndarray | None = None, rmns: ndarray | None = None, zmnc: ndarray | None = None, lmnc: ndarray | None = None, gmns: ndarray | None = None, bmns: ndarray | None = None, bsubumns: ndarray | None = None, bsubvmns: ndarray | None = None, bsubsmnc: ndarray | None = None, currumns: ndarray | None = None, currvmns: ndarray | None = None, bsupumns: ndarray | None = None, bsupvmns: ndarray | None = None, mnmaxpot: int | None = None, nobser: int | None = None, nobd: int | None = None, nbsets: int | None = None, nbfld: ndarray | None = None, potsin: ndarray | None = None, potcos: ndarray | None = None, xmpot: ndarray | None = None, xnpot: ndarray | None = None, curlabel: tuple[str, ...] | None = None, bsubumnc_sur: ndarray | None = None, bsubvmnc_sur: ndarray | None = None, bsupumnc_sur: ndarray | None = None, bsupvmnc_sur: ndarray | None = None, bsubumns_sur: ndarray | None = None, bsubvmns_sur: ndarray | None = None, bsupumns_sur: ndarray | None = None, bsupvmns_sur: ndarray | None = None, vmex_diagnostics_schema: int = 0, vmex_trapped_fraction: ndarray | None = None)¶
Full VMEC2000
woutdataset in file conventions (wrout.f).Every field name matches its netCDF variable name (logicals drop the
__logical__suffix). Radial arrays are(ns,); Fourier tables(ns, mnmax)/(ns, mnmax_nyq). Optional groups areNonewhen not applicable: lasym partners for symmetric runs, free-boundary tables for fixed-boundary runs.
- vmex.core.wout.write_wout(path: str | Path, data: WoutData, *, overwrite: bool = True) Path¶
Write
datatopathin VMEC2000wout_*.nclayout.Uses NETCDF3 64-bit-offset format (as VMEC2000’s ezcdf) and reproduces wrout.f’s variable set, ordering, dimensions and attributes.
extcurandmgrid_modeare created but left unwritten (netCDF fill) whennextcur == 0, matching VMEC2000.
- vmex.core.wout.read_wout(path: str | Path) WoutData¶
Read a VMEC2000-compatible
wout_*.ncfile intoWoutData.All values are kept in file conventions (no unit conversions), so a
write_wout()/read_wout()round trip is the identity.
- vmex.core.wout.wout_from_state(*, inp, state, fsqr: float, fsqz: float, fsql: float, fsqt=None, wdot=None, niter: int = 0, itfsq: int = 0, converged: bool = True, input_extension: str = '', version: float = 9.0, nextcur: int = 0, extcur=None, mgrid_mode: str = '', curlabel=None, vacuum_output: VacuumOutput | None = None) WoutData¶
Build a complete
WoutDatafrom a solved equilibrium state.inpis the parsedvmex.core.input.VmecInputdeck andstatethe convergedvmex.core.solver.SpectralState(internal normalization, m = 1-constrained, odd-m withoutscalxc) — the exact representation the core solver evolves. The geometry/field state is re-evaluated on the Nyquist-extended internal grid with the core pipeline (geometry/fields), the Nyquist/jxbforce/Mercier tables come fromvmex.core.nyquist, and every remaining VMEC2000wrout.fvariable fromvmex.core.postprocessand the input deck.Unlike VMEC2000 (which zeroes the late
eqfor.fscalars when the run hits NITER), all derived quantities are always computed - vmex’s zero-crash policy keeps diagnostic output for non-converged states and records convergence inier_flag(0 = converged, 2 = more iterations needed, matching vmec_params.f).Free-boundary metadata:
nextcur/extcur/mgrid_mode/curlabelare caller-supplied (the CLI reads them from the mgrid file;extcuris the input EXTCUR array in Amperes, aswrout.fwrites it). Pass the publicresult.vacuumasvacuum_outputto populate NESTORpotsin/xmpot/xnpotand*_surtables. LASYM runs additionally populatepotcosand the four sine*_surpartners.
Optional neoclassical diagnostics¶
Effective-ripple diagnostics through the optional NEO_JAX backend.
- vmex.core.neoclassical.diagnostic_neo_config()¶
Return the bounded NEO resolution used by VMEX summary figures.
On the bundled NFP=2 QA and NFP=4 QI finite-beta decks this stays within 4% of NEO_JAX’s default
NeoConfig. A 16 x 16 spline grid aliases the summary Boozer spectrum (mboz=16) and fewer than 20 steps per field period or 200 field periods leave 7-30% errors. Pass aneo_jax.NeoConfigtoepsilon_effective_from_wout()for publication calculations.
- vmex.core.neoclassical.epsilon_effective_from_boozer(booz: Any, *, config=None)¶
Return
(s, epsilon_eff**(3/2))from Boozer-coordinate data.boozmay be a NEO_JAXBoozerDataor a booz_xform-style mapping. The returned NEO quantity is the effective-ripple transport measure \(\epsilon_\mathrm{eff}^{3/2}\), conventionally namedepstot. JAX arrays remain differentiable when the mapping and selected NEO path are JAX-native.
- vmex.core.neoclassical.epsilon_effective_from_wout(wout, *, surfaces: Sequence[float] = (0.2, 0.4, 0.6, 0.8, 0.95), mboz: int = 16, nboz: int = 12, config=None, clear_jax_caches: bool = False)¶
Compute
epsilon_eff**(3/2)directly from an in-memory VMEX wout.VMEX performs the Boozer transform in memory and passes its arrays to NEO_JAX; no
boozmnfile is required. NEO_JAX currently represents the stellarator-symmetric cosine/sine convention, soLASYMwouts are rejected rather than silently dropping asymmetric harmonics. The default is library-safe: a diagnostic call never clears the process-wide JAX executable caches behind its caller’s back. Passclear_jax_caches=Trueto release completed executables before compiling NEO when peak memory matters more than warm executables — the CLI does this itself after all requested diagnostics instead (vmex.core.cli).
Optional alpha-particle tracing¶
The vmex-to-ESSOS field handoff, and alpha tracing on top of it.
essos_vmec_field()— hand a solved equilibrium (or a wout file) to ESSOS as anessos.fields.Vmec, ready for ESSOS tracing, surfaces and field queries. The seam runs one way, ESSOS reading a VMEC equilibrium; an ESSOS coil field entering a vmex free-boundary solve goes the other way throughfrom_coils().trace_alphas()— trace fusion-born alpha particles launched from one flux surface of an equilibrium and return the exact loss-fraction diagnostics as anAlphaTracingResult.
The tracer is ESSOS (essos.dynamics.Tracing over essos.fields.Vmec),
imported inside the call so vmex imports without ESSOS installed. Only the
released ESSOS surface is used — Vmec(wout_filename), Particles,
Tracing and its loss_fractions/lost_times/trajectories
outputs. Consequences of that restriction:
essos.fields.Vmecreads a wout file, so an in-memory equilibrium (WoutData) takes a temporary-wout hop throughwrite_wout(). The file write severs any gradient; the differentiable loss-fraction objective is a separate feature gated on the ESSOS array constructor (uwplasma/ESSOS#61) and is not provided here.The particle energy along each orbit is reconstructed locally from
0.5*m*v_par^2 + mu*|B|(released ESSOS exposes it inconsistently across versions: an eager array in 0.16, a method on the development line).Released ESSOS keeps no solver-failure ledger, so
particles_failedcounts trajectories with non-finite samples that ESSOS did not attribute to a boundary loss; axis terminations are reported when the installed ESSOS tracks them and are zero otherwise (0.16 has no axis event).
Particles are sampled uniformly on the surface s: theta over
[0, 2*pi), phi over one field period [0, 2*pi/nfp), and pitch
v_par/v over [-1, 1), from jax.random.PRNGKey(seed). A particle
counts as lost when its orbit reaches s >= 0.99 (the ESSOS
loss_fraction criterion).
- class vmex.core.tracing.AlphaTracingResult(nparticles: int, loss_fraction: float, particles_lost: int, particles_unresolved: int, particles_failed: int, wall_time_s: float, particle_energy: float, total_speed: float, times: ndarray, loss_fractions: ndarray, lost_times: ndarray, trajectories: ndarray, trajectories_xyz: ndarray, energies: ndarray)¶
Exact alpha-loss diagnostics from one
trace_alphas()call.trajectoriesholds the guiding-centre coordinates(s, theta, phi, v_par)at each saved time; lost orbits keep the non-finite post-event samples ESSOS writes after the boundary crossing.lost_timesis-1for particles that were never lost.
- vmex.core.tracing.essos_vmec_field(source: Any, **kwargs: Any) Any¶
Return the
essos.fields.Vmecfield for an equilibrium or wout file.sourceis a path to awout_*.ncfile or an in-memoryWoutData. Released ESSOS reads a wout file, so an in-memory equilibrium is written to a temporary wout; ESSOS loads every table eagerly in its constructor, so the file is gone by the time the field is returned. That write severs the gradient — this seam is for diagnostics, not for differentiating through ESSOS.kwargsreachessos.fields.Vmecunchanged (ntheta,nphi,closeandrange_toruson the released constructor, which set the resolution of thefield.surfaceESSOS builds alongside the field).Released ESSOS reads the stellarator-symmetric wout tables only, so an
lasymequilibrium is rejected rather than silently half-transferred.
- vmex.core.tracing.trace_alphas(source: Any, *, tmax: float = 0.0003, nparticles: int = 200, s: float = 0.25, seed: int = 42, timestep: float = 5e-07, times_to_trace: int = 200, model: str = 'GuidingCenter') AlphaTracingResult¶
Trace fusion alphas from surface
sof a wout file or equilibrium.- Parameters:
source – Path to a
wout_*.ncfile, or an in-memoryWoutData; handed to ESSOS byessos_vmec_field().tmax – Integration horizon [s], integrator step [s], and number of saved samples (uniform in time, including
t = 0).timestep – Integration horizon [s], integrator step [s], and number of saved samples (uniform in time, including
t = 0).times_to_trace – Integration horizon [s], integrator step [s], and number of saved samples (uniform in time, including
t = 0).nparticles – Ensemble size, launch surface, and sampling seed (see module notes).
s – Ensemble size, launch surface, and sampling seed (see module notes).
seed – Ensemble size, launch surface, and sampling seed (see module notes).
model – ESSOS tracing model (
"GuidingCenter"by default).