vmex 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. vmex-0.2.0/LICENSE +21 -0
  2. vmex-0.2.0/PKG-INFO +615 -0
  3. vmex-0.2.0/README.md +565 -0
  4. vmex-0.2.0/pyproject.toml +122 -0
  5. vmex-0.2.0/setup.cfg +4 -0
  6. vmex-0.2.0/tests/test_bootstrap.py +614 -0
  7. vmex-0.2.0/tests/test_boozer_tables.py +142 -0
  8. vmex-0.2.0/tests/test_cli.py +238 -0
  9. vmex-0.2.0/tests/test_cli_freeboundary.py +168 -0
  10. vmex-0.2.0/tests/test_compat.py +152 -0
  11. vmex-0.2.0/tests/test_coverage_margin.py +92 -0
  12. vmex-0.2.0/tests/test_device.py +114 -0
  13. vmex-0.2.0/tests/test_doctor.py +109 -0
  14. vmex-0.2.0/tests/test_examples.py +385 -0
  15. vmex-0.2.0/tests/test_fetch_assets.py +65 -0
  16. vmex-0.2.0/tests/test_forces_residuals.py +172 -0
  17. vmex-0.2.0/tests/test_fourier_transforms.py +226 -0
  18. vmex-0.2.0/tests/test_freeboundary.py +415 -0
  19. vmex-0.2.0/tests/test_freeboundary_diff.py +195 -0
  20. vmex-0.2.0/tests/test_geometry_fields.py +211 -0
  21. vmex-0.2.0/tests/test_golden_digests.py +89 -0
  22. vmex-0.2.0/tests/test_implicit_grad.py +467 -0
  23. vmex-0.2.0/tests/test_implicit_multi_rhs.py +63 -0
  24. vmex-0.2.0/tests/test_input_profiles.py +75 -0
  25. vmex-0.2.0/tests/test_lgradb.py +204 -0
  26. vmex-0.2.0/tests/test_mgrid.py +169 -0
  27. vmex-0.2.0/tests/test_multigrid_interp.py +135 -0
  28. vmex-0.2.0/tests/test_multigrid_ladder.py +368 -0
  29. vmex-0.2.0/tests/test_omnigenity.py +165 -0
  30. vmex-0.2.0/tests/test_optimization_convergence.py +132 -0
  31. vmex-0.2.0/tests/test_optimize.py +485 -0
  32. vmex-0.2.0/tests/test_optimize_penalty.py +154 -0
  33. vmex-0.2.0/tests/test_optimize_traceable_qs.py +97 -0
  34. vmex-0.2.0/tests/test_package_api.py +241 -0
  35. vmex-0.2.0/tests/test_packaging_metadata.py +77 -0
  36. vmex-0.2.0/tests/test_parallel.py +138 -0
  37. vmex-0.2.0/tests/test_parity_breadth.py +340 -0
  38. vmex-0.2.0/tests/test_plotting_boozer.py +136 -0
  39. vmex-0.2.0/tests/test_preconditioner.py +316 -0
  40. vmex-0.2.0/tests/test_preconditioner_2d.py +167 -0
  41. vmex-0.2.0/tests/test_printing.py +106 -0
  42. vmex-0.2.0/tests/test_profiles.py +293 -0
  43. vmex-0.2.0/tests/test_setup.py +243 -0
  44. vmex-0.2.0/tests/test_setup_extras.py +88 -0
  45. vmex-0.2.0/tests/test_single_stage_simultaneous.py +131 -0
  46. vmex-0.2.0/tests/test_solver_end_to_end.py +188 -0
  47. vmex-0.2.0/tests/test_stability.py +143 -0
  48. vmex-0.2.0/tests/test_step_control.py +164 -0
  49. vmex-0.2.0/tests/test_turbulence.py +310 -0
  50. vmex-0.2.0/tests/test_validation_guards.py +153 -0
  51. vmex-0.2.0/tests/test_wout_golden.py +399 -0
  52. vmex-0.2.0/vmec_jax/__init__.py +45 -0
  53. vmex-0.2.0/vmex/__init__.py +131 -0
  54. vmex-0.2.0/vmex/__main__.py +14 -0
  55. vmex-0.2.0/vmex/_compat.py +310 -0
  56. vmex-0.2.0/vmex/core/__init__.py +29 -0
  57. vmex-0.2.0/vmex/core/bootstrap.py +985 -0
  58. vmex-0.2.0/vmex/core/boozer.py +155 -0
  59. vmex-0.2.0/vmex/core/boozer_tables.py +183 -0
  60. vmex-0.2.0/vmex/core/cli.py +835 -0
  61. vmex-0.2.0/vmex/core/device.py +171 -0
  62. vmex-0.2.0/vmex/core/errors.py +119 -0
  63. vmex-0.2.0/vmex/core/fields.py +703 -0
  64. vmex-0.2.0/vmex/core/forces.py +913 -0
  65. vmex-0.2.0/vmex/core/fourier.py +371 -0
  66. vmex-0.2.0/vmex/core/freeboundary.py +1194 -0
  67. vmex-0.2.0/vmex/core/freeboundary_diff.py +727 -0
  68. vmex-0.2.0/vmex/core/geometry.py +386 -0
  69. vmex-0.2.0/vmex/core/implicit.py +1291 -0
  70. vmex-0.2.0/vmex/core/input.py +649 -0
  71. vmex-0.2.0/vmex/core/mgrid.py +456 -0
  72. vmex-0.2.0/vmex/core/multigrid.py +299 -0
  73. vmex-0.2.0/vmex/core/nyquist.py +1013 -0
  74. vmex-0.2.0/vmex/core/omnigenity.py +589 -0
  75. vmex-0.2.0/vmex/core/optimize.py +1906 -0
  76. vmex-0.2.0/vmex/core/parallel.py +154 -0
  77. vmex-0.2.0/vmex/core/plotting.py +693 -0
  78. vmex-0.2.0/vmex/core/postprocess.py +617 -0
  79. vmex-0.2.0/vmex/core/preconditioner.py +697 -0
  80. vmex-0.2.0/vmex/core/preconditioner_2d.py +179 -0
  81. vmex-0.2.0/vmex/core/printing.py +145 -0
  82. vmex-0.2.0/vmex/core/profiles.py +634 -0
  83. vmex-0.2.0/vmex/core/residuals.py +598 -0
  84. vmex-0.2.0/vmex/core/setup.py +999 -0
  85. vmex-0.2.0/vmex/core/solver.py +1520 -0
  86. vmex-0.2.0/vmex/core/stability.py +432 -0
  87. vmex-0.2.0/vmex/core/statephysics.py +458 -0
  88. vmex-0.2.0/vmex/core/step.py +168 -0
  89. vmex-0.2.0/vmex/core/transforms.py +837 -0
  90. vmex-0.2.0/vmex/core/turbulence.py +571 -0
  91. vmex-0.2.0/vmex/core/vacuum.py +869 -0
  92. vmex-0.2.0/vmex/core/wout.py +865 -0
  93. vmex-0.2.0/vmex/doctor.py +204 -0
  94. vmex-0.2.0/vmex/mirror/__init__.py +84 -0
  95. vmex-0.2.0/vmex/mirror/analytic.py +368 -0
  96. vmex-0.2.0/vmex/mirror/basis.py +600 -0
  97. vmex-0.2.0/vmex/mirror/exterior.py +954 -0
  98. vmex-0.2.0/vmex/mirror/forces.py +885 -0
  99. vmex-0.2.0/vmex/mirror/free_boundary.py +934 -0
  100. vmex-0.2.0/vmex/mirror/geometry.py +596 -0
  101. vmex-0.2.0/vmex/mirror/implicit.py +536 -0
  102. vmex-0.2.0/vmex/mirror/model.py +242 -0
  103. vmex-0.2.0/vmex/mirror/output.py +1015 -0
  104. vmex-0.2.0/vmex/mirror/solver.py +639 -0
  105. vmex-0.2.0/vmex/mirror/splines.py +1177 -0
  106. vmex-0.2.0/vmex/resources/input.nfp4_QH_warm_start +47 -0
  107. vmex-0.2.0/vmex.egg-info/PKG-INFO +615 -0
  108. vmex-0.2.0/vmex.egg-info/SOURCES.txt +110 -0
  109. vmex-0.2.0/vmex.egg-info/dependency_links.txt +1 -0
  110. vmex-0.2.0/vmex.egg-info/entry_points.txt +3 -0
  111. vmex-0.2.0/vmex.egg-info/requires.txt +31 -0
  112. vmex-0.2.0/vmex.egg-info/top_level.txt +2 -0
vmex-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UW Plasma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
vmex-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,615 @@
1
+ Metadata-Version: 2.4
2
+ Name: vmex
3
+ Version: 0.2.0
4
+ Summary: JAX implementation of VMEC2000 with differentiable fixed-boundary and branch-local free-boundary research paths.
5
+ Author: vmex contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/uwplasma/VMEX
8
+ Project-URL: Documentation, https://vmex.readthedocs.io/en/latest/
9
+ Project-URL: Repository, https://github.com/uwplasma/VMEX
10
+ Project-URL: Issues, https://github.com/uwplasma/VMEX/issues
11
+ Project-URL: Changelog, https://github.com/uwplasma/VMEX/releases
12
+ Keywords: vmec,stellarator,mhd,jax,optimization
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy
24
+ Requires-Dist: jax
25
+ Requires-Dist: jaxlib
26
+ Requires-Dist: scipy
27
+ Requires-Dist: netCDF4
28
+ Requires-Dist: matplotlib
29
+ Requires-Dist: packaging
30
+ Requires-Dist: booz_xform_jax
31
+ Requires-Dist: solvax
32
+ Requires-Dist: tomli; python_version < "3.11"
33
+ Provides-Extra: freeb
34
+ Requires-Dist: virtual_casing_jax; extra == "freeb"
35
+ Provides-Extra: docs
36
+ Requires-Dist: sphinx; extra == "docs"
37
+ Requires-Dist: sphinx-copybutton; extra == "docs"
38
+ Requires-Dist: furo; extra == "docs"
39
+ Provides-Extra: dev
40
+ Requires-Dist: build; extra == "dev"
41
+ Requires-Dist: furo; extra == "dev"
42
+ Requires-Dist: mypy; extra == "dev"
43
+ Requires-Dist: pytest; extra == "dev"
44
+ Requires-Dist: pytest-cov; extra == "dev"
45
+ Requires-Dist: pytest-xdist; extra == "dev"
46
+ Requires-Dist: ruff; extra == "dev"
47
+ Requires-Dist: sphinx; extra == "dev"
48
+ Requires-Dist: types-setuptools; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ # VMEX
52
+
53
+ [![PyPI version](https://img.shields.io/pypi/v/vmex.svg)](https://pypi.org/project/vmex/)
54
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://github.com/uwplasma/VMEX/blob/main/pyproject.toml)
55
+ [![License](https://img.shields.io/github/license/uwplasma/vmex)](https://github.com/uwplasma/VMEX/blob/main/LICENSE)
56
+ [![CI](https://img.shields.io/github/actions/workflow/status/uwplasma/vmex/ci.yml?branch=main&label=ci)](https://github.com/uwplasma/VMEX/actions/workflows/ci.yml)
57
+ [![Docs](https://img.shields.io/readthedocs/vmex/latest?label=docs)](https://vmex.readthedocs.io/en/latest/)
58
+
59
+ > **`vmec_jax` is now `vmex`.** The package was renamed: install with
60
+ > `pip install vmex` and `import vmex`. The `vmec` CLI command still works as an
61
+ > alias, and `import vmec_jax` keeps working (with a deprecation warning) for
62
+ > one release. Full documentation: **[vmex.readthedocs.io](https://vmex.readthedocs.io/en/latest/)**.
63
+
64
+ **VMEX** is a clean-room, JAX-native reimplementation of the
65
+ [VMEC2000](https://princetonuniversity.github.io/STELLOPT/VMEC) ideal-MHD
66
+ equilibrium code for stellarators and tokamaks. It reproduces VMEC2000
67
+ iteration-for-iteration on benchmark decks — and, unlike the Fortran
68
+ original, it is differentiable and runs on GPUs.
69
+
70
+ - **VMEC2000 parity.** The solver ports VMEC2000's algorithms
71
+ constant-for-constant (steepest-descent moment method, radial
72
+ preconditioner, spectral condensation, NESTOR vacuum solve). Benchmark
73
+ decks converge in the *same* number of iterations and reproduce the
74
+ plasma energy at machine precision. An optional **2D block
75
+ preconditioner** cuts iterations 2.5–11x on stiff cases while leaving the
76
+ default path byte-identical.
77
+ - **Differentiable.** Gradients of *fixed-boundary* equilibrium outputs with
78
+ respect to boundary shape and profile parameters by implicit
79
+ differentiation of the converged fixed point — no finite differences, no
80
+ unrolling — validated against central finite differences to ~1e-6 relative
81
+ (see the gradient table in the docs), with an O(1)-memory adjoint. **Free
82
+ boundary** is differentiable end-to-end through the virtual-casing vacuum
83
+ field (coil / `extcur` derivatives), finite-difference-validated.
84
+ - **Drop-in.** Reads VMEC2000 `input.*` namelists and VMEC++-style JSON,
85
+ prints VMEC2000-format iteration output, and writes `wout_*.nc` files
86
+ that load unchanged in simsopt and booz_xform.
87
+ - **Batteries included.** Plotting (`vmex --plot`), Boozer transform
88
+ (`vmex --booz`), spline profiles, multigrid, hot restart, free boundary
89
+ from mgrid files *or* directly from coils,
90
+ typed zero-crash errors — with the shared linear/adjoint solver layer
91
+ factored out into [SOLVAX](https://pypi.org/project/solvax/).
92
+
93
+ ![Flux surfaces, 3-D geometry, and Boozer |B| of the bundled quick-start QH case](docs/_static/figures/readme_equilibrium_showcase.png)
94
+
95
+ *The bundled quick-start case (`vmex --test`): flux-surface cross sections,
96
+ the 3-D plasma boundary coloured by `|B|`, and `|B|` in **Boozer coordinates**
97
+ on the last closed flux surface (the near-straight diagonal contours are the
98
+ signature of quasi-helical symmetry) for a four-field-period stellarator —
99
+ all from the built-in `vmex.core.plotting` / `core.boozer` helpers.*
100
+
101
+ ## Install
102
+
103
+ Install from PyPI:
104
+
105
+ ```bash
106
+ pip install vmex
107
+ ```
108
+
109
+ Development install from source:
110
+
111
+ ```bash
112
+ git clone https://github.com/uwplasma/VMEX
113
+ cd vmex && pip install -e .
114
+ ```
115
+
116
+ ## Quickstart
117
+
118
+ ```bash
119
+ vmex --doctor # check the installation and JAX backend
120
+ vmex --test # solve the bundled QH case, write wout + plots
121
+ vmex input.X # run any VMEC2000 input deck (or VMEC++-style JSON)
122
+ ```
123
+
124
+ `vmex input.X` writes `wout_X.nc` next to the input (`--outdir` to
125
+ redirect). To try it on a real deck:
126
+
127
+ ```bash
128
+ curl -L -O https://raw.githubusercontent.com/uwplasma/vmex/main/examples/data/input.nfp4_QH_warm_start
129
+ vmex input.nfp4_QH_warm_start
130
+ ```
131
+
132
+ Post-process any wout file, including ones written by VMEC2000:
133
+
134
+ ```bash
135
+ vmex --plot wout_nfp4_QH_warm_start.nc # surfaces, |B|, profiles, 3D
136
+ vmex --booz wout_nfp4_QH_warm_start.nc # Boozer transform -> boozmn_*.nc
137
+ vmex --plot boozmn_nfp4_QH_warm_start.nc # Boozer |B| contours + spectrum
138
+ ```
139
+
140
+ ## Parity with VMEC2000
141
+
142
+ VMEX is validated end-to-end against golden VMEC2000 (PARVMEC 9.0) runs:
143
+ benchmark decks converge in **exactly** the golden iteration count — including
144
+ DSHAPE's mid-run jacobian reset — and reproduce the plasma energy `wb` to
145
+ 1 part in 10¹⁵. Across the full benchmark suite (14 rows, all at `ns ≥ 201`),
146
+ the iteration count matches VMEC2000 exactly on 12 rows; on the free-boundary
147
+ CTH-like row it converges in a ~9% iteration tail, and on Nuhrenberg–Zille QHS
148
+ it converges in *fewer* iterations (1681 vs 2829). Per-variable wout agreement
149
+ and the full test gates live in the
150
+ [documentation](https://vmex.readthedocs.io/en/latest/).
151
+
152
+ ![Force residual vs iteration for vmex, VMEC2000, and VMEC++](docs/_static/figures/readme_convergence.png)
153
+
154
+ *Parity is per-iteration, not just end-to-end: the total force residual
155
+ (`fsqr + fsqz + fsql`) of the quick-start QH case at ns=51, per iteration.
156
+ The vmex trajectory lies exactly on top of VMEC2000's (both converge in
157
+ 502 iterations); VMEC++ follows a near-identical path (501 iterations).
158
+ Traces: vmex `SolveResult.fsq_history`, VMEC2000 `NSTEP=1` stdout,
159
+ VMEC++ wout `fsqt`.*
160
+
161
+ ### Optional 2D preconditioner: fewer iterations on stiff cases
162
+
163
+ The default radial (1D) preconditioner reproduces VMEC2000 iteration-for-iteration.
164
+ An opt-in **2D block preconditioner** (matrix-free Newton: a Jacobian-vector-product
165
+ Hessian on SOLVAX's GMRES) cuts the iteration count **2.5–11×** at *identical*
166
+ accuracy — the converged `wb` matches the 1D result to ~1e-10 (it changes the path,
167
+ not the fixed point).
168
+
169
+ ![2D vs 1D preconditioner iteration counts on stiff cases](docs/_static/figures/readme_precond.png)
170
+
171
+ **Why it is opt-in, not the default.** Fewer iterations is not the same as less
172
+ wall-clock: each 2D Newton step (a GMRES solve of Hessian-vector products) costs far
173
+ more than a 1D radial sweep. Measured across easy and stiff decks the wall-clock
174
+ ranges 0.55–1.16× — a wash to *slower* (e.g. ~2× slower on a plain circular tokamak,
175
+ a tie even on an aspect-ratio-100 stiff case) — and peak memory is ~30% higher (the
176
+ extra GMRES/HVP compile graph). So the 1D path stays the byte-identical default, and
177
+ the 2D preconditioner is there for cases where the 1D iteration count is the
178
+ bottleneck or stalls.
179
+
180
+ ## Performance
181
+
182
+ ![Wall-clock comparison against VMEC2000 and VMEC++](docs/_static/figures/readme_runtime_compare.png)
183
+
184
+ Full-solve wall-clock times on the bundled benchmark suite (Apple Silicon
185
+ CPU, single thread; `benchmarks/baseline.json`; reproduce with
186
+ `python benchmarks/run_baseline.py`):
187
+
188
+ - **Warm** — kernels already compiled; the number that matters inside an
189
+ optimization loop or scan. Faster than VMEC2000 on **every** benchmark row
190
+ (1.3–2.6× on typical decks, up to ~7× on small ones) — including the
191
+ free-boundary rows (1.3–1.5×) since the NESTOR iteration loop was fused
192
+ into jitted multi-iteration lanes. Ratios measured on a shared CPU are
193
+ conservative lower bounds.
194
+ - **Cold** — a fresh CLI process pays a one-time 5–25 s JAX/XLA compile, so a
195
+ single run is slower than Fortran. Executables cache per solver structure, so
196
+ scans, ladders, and optimizations recompile nothing — which is why *warm* is
197
+ the workflow number.
198
+ - **GPU** — at these sizes a fixed per-solve dispatch cost dominates and the CPU
199
+ wins outright; per-iteration throughput favours the GPU ~3× on the largest
200
+ decks. The device policy picks CPU or GPU per stage.
201
+ - **Memory** — peak (0.6–3.3 GB) is the transient XLA *compile* working set, not
202
+ the data: the equilibrium state is a few MB. The optimization Jacobian is
203
+ bounded by column chunking (`jac_chunk_size="auto"`), so it does not grow with
204
+ the number of design variables.
205
+
206
+ ## Features
207
+
208
+ | | VMEX | VMEC2000 | VMEC++ |
209
+ |---|:---:|:---:|:---:|
210
+ | Fixed-boundary equilibria | ✅ | ✅ | ✅ |
211
+ | Free boundary from an mgrid file | ✅ | ✅ | ✅ |
212
+ | Free boundary directly from coils (no mgrid) | ✅ | ❌ | ❌ |
213
+ | Free-boundary tokamaks (`ntor = 0`) | ✅ | ✅ | ❌ |
214
+ | Non-stellarator-symmetric (`LASYM = T`) | ✅ | ✅ | ❌ |
215
+ | Fixed-boundary fallback on missing mgrid | ✅ | ✅ | ❌ |
216
+ | Spline profiles (cubic / Akima) | ✅ | ✅ | ❌ |
217
+ | VMEC++-schema JSON input | ✅ | ❌ | ✅ |
218
+ | Hot restart from a previous state | ✅ | ❌ | ✅ |
219
+ | Typed zero-crash errors | ✅ | ❌ | ✅ |
220
+ | Boozer transform built in (`--booz`) | ✅ | ❌ | ❌ |
221
+ | Plotting built in (`--plot`) | ✅ | ❌ | ❌ |
222
+ | GPU execution | ✅ | ❌ | ❌ |
223
+ | Differentiable fixed boundary (implicit diff, O(1) memory) | ✅ | ❌ | ❌ |
224
+ | Differentiable free boundary (virtual casing) | ✅ | ❌ | ❌ |
225
+ | 2D block preconditioner (stiff-case speedup) | ✅ | ❌ | ❌ |
226
+
227
+ ### Free boundary straight from coils
228
+
229
+ Free-boundary solves can run directly from a coil set: tabulate an
230
+ [ESSOS](https://github.com/uwplasma/ESSOS) coil set onto the solver grid in
231
+ memory (`essos.coils.Coils.to_mgrid`) and pass it as `external_field=`,
232
+ with no MAKEGRID file involved. For gradients, the differentiable free
233
+ boundary evaluates a JAX Biot-Savart (a plain `xyz→B` callable) at the
234
+ boundary points of each iteration, keeping the coil degrees of freedom
235
+ differentiable end-to-end. All coil geometry lives in ESSOS; vmex has no
236
+ coil code of its own.
237
+
238
+ ![Free-boundary Landreman-Paul QA pressure scan directly from ESSOS coils](docs/_static/figures/readme_essos_beta_scan.png)
239
+
240
+ *Free-boundary equilibria of the Landreman–Paul precise-QA configuration held
241
+ by its 16 modular coils as optimized in
242
+ [ESSOS](https://github.com/uwplasma/ESSOS) (3 KB coil JSON bundled in
243
+ `examples/data/`). Pressure is ramped at fixed coil currents with each point
244
+ warm-started from the previous boundary, and `PRES_SCALE` is calibrated per
245
+ point so the **actual** volume-average beta of the converged wout
246
+ (`betatotal`) — not a nominal input value — lands on 0, 1, 2, 3 % (all within
247
+ 0.08 %, force residual ~2e-10 at ns = 51). The plasma dilates and the magnetic
248
+ axis Shafranov-shifts 14 cm outboard at the φ = 0 section (right panel) while
249
+ the coils never move. Reproduce with
250
+ `python examples/free_boundary_essos_coils.py`.*
251
+
252
+ ### Single-stage plasma + coil optimization
253
+
254
+ VMEX can optimize the plasma boundary and the coils together, with one
255
+ exact gradient. A single `jax.value_and_grad` differentiates through the
256
+ fixed-boundary equilibrium (implicit adjoint), the virtual-casing surface
257
+ field, and the Biot–Savart law of the ESSOS coil filaments, covering boundary
258
+ Fourier modes, coil shapes, and coil currents at once. The benchmark below
259
+ compares this against the classical two-stage approach — stage 1 shapes the
260
+ boundary for quasi-axisymmetry, stage 2 fits coils to that frozen boundary —
261
+ from the same seeds (a circular torus and four circular coils), with identical
262
+ coil budgets, scored on the equilibrium each final coil set actually produces.
263
+ The finite-β case runs the same joint optimization with a pressure profile;
264
+ no published code demonstrates this in general form.
265
+
266
+ The most effective use is to polish the two-stage result, the "stage 3" of
267
+ [arXiv:2302.10622](https://arxiv.org/abs/2302.10622): warm-start the joint
268
+ objective from the stage-1 boundary and stage-2 coils and let both adapt.
269
+ In 10–30 minutes this lowers the normal-field error by 33% (vacuum) and 17%
270
+ (finite β) below the two-stage result, with quasisymmetry and iota unchanged —
271
+ stage 2 cannot make this correction because it holds the boundary frozen.
272
+ A pure cold start (third column) shows the same joint descent from the crude
273
+ seeds: after 50 iterations it reaches low B·n with compact coils, but its
274
+ quasisymmetry is far from what a dedicated stage 1 delivers, which is why the
275
+ polish pattern is recommended.
276
+
277
+ ![Cold-start single-stage vs two-stage plasma+coil optimization, vacuum and finite beta](docs/_static/figures/readme_single_stage.png)
278
+
279
+ *Top: seed (grey, dashed) vs two-stage (orange) vs cold-start single-stage
280
+ (blue) boundaries at φ = 0 and a half field period — the polish boundary is
281
+ visually indistinguishable from two-stage (same aspect and iota), so it is not
282
+ drawn. Middle/bottom: each approach's final LCFS coloured by |B| inside its
283
+ own final coils.*
284
+
285
+ Vacuum (measured; identical seeds and coil budgets across columns):
286
+
287
+ | metric (vacuum) | two-stage | + single-stage polish | single-stage (cold) |
288
+ |---|---|---|---|
289
+ | QS ratio residual | 9.3e-05 | 1.6e-04 | 2.4e-02 |
290
+ | mean iota (target 0.42) | 0.420 | 0.420 | 0.396 |
291
+ | ⟨\|B·n\|⟩/⟨B⟩ | 2.38e-03 | **1.60e-03** | 3.05e-03 |
292
+ | max\|B·n\|/⟨B⟩ | 1.30e-02 | **7.84e-03** | 1.18e-02 |
293
+ | coil lengths [m] (≤ 4.40) | 4.12–4.39 | 4.11–4.40 | 3.60–3.87 |
294
+
295
+ Finite β (⟨β⟩ ≈ 1.5 %, same pressure profile in all columns):
296
+
297
+ | metric (finite β) | two-stage | + single-stage polish | single-stage (cold) |
298
+ |---|---|---|---|
299
+ | QS ratio residual | 4.4e-05 | 2.4e-04 | 2.3e-02 |
300
+ | mean iota (target 0.42) | 0.420 | 0.422 | 0.100 |
301
+ | ⟨\|B·n\|⟩/⟨B⟩ | 2.80e-03 | **2.34e-03** | 6.20e-03 |
302
+ | max\|B·n\|/⟨B⟩ | 1.37e-02 | **1.27e-02** | 1.75e-02 |
303
+ | coil lengths [m] (≤ 4.40) | 3.91–4.18 | 3.91–4.19 | 3.25–3.28 |
304
+
305
+ Reproduce with `python examples/single_stage_vs_two_stage.py --case vacuum
306
+ --phase all` (and `--case beta`). Measured on a 36-core CPU: stage 1 ≈ 7–9 min,
307
+ stage 2 ≈ 6 min, polish ≈ 10–30 min; the optional cold-start single column is
308
+ the long pole (≈ 1.5 h vacuum, several hours at finite β). The phases are
309
+ resumable, so long runs can be split across sessions.
310
+
311
+ ## Code size
312
+
313
+ VMEX delivers that superset of capabilities in little more than **half the
314
+ code**, and is the most densely documented of the three. Solver source only (tests,
315
+ language bindings, and vendored third-party excluded), counted with
316
+ [`pygount`](https://pypi.org/project/pygount/) 3.2:
317
+
318
+ | code base | language | files | code (SLOC) | comments / docstrings | doc-to-code |
319
+ |---|---|---:|---:|---:|---:|
320
+ | **VMEX** | Python | 41 | **13,326** | 6,744 | **0.51** |
321
+ | VMEC2000 (PARVMEC) | Fortran | 115 | 24,190 | 8,425 | 0.35 |
322
+ | VMEC++ | C++ / Python | 117 | 22,824 | 7,646 | 0.34 |
323
+
324
+ VMEX is little more than half the SLOC of VMEC2000 and VMEC++, while
325
+ *adding* differentiability, GPU execution, direct-coil free boundary, and a
326
+ built-in Boozer transform — and it carries the highest comment/docstring
327
+ density of the three (reproduce with
328
+ `pygount --format=summary vmex`).
329
+
330
+ ## Python API
331
+
332
+ ```python
333
+ from vmex.core.input import VmecInput
334
+ from vmex.core import optimize as opt
335
+ from vmex.core.wout import write_wout
336
+ from vmex.core.plotting import plot_wout
337
+
338
+ inp = VmecInput.from_file("input.nfp4_QH_warm_start")
339
+ eq = opt.solve_equilibrium(inp) # full NS_ARRAY ladder, VMEC2000 numerics
340
+ print(eq.result.converged, eq.result.iterations, float(eq.wout.aspect))
341
+
342
+ write_wout("wout_nfp4_QH_warm_start.nc", eq.wout) # wout built lazily on eq
343
+ plot_wout(eq.wout, "figures/")
344
+ ```
345
+
346
+ Choosing an entry point: `optimize.solve_equilibrium` for Python analysis and
347
+ objectives (state + runtime + lazy `.wout`); `multigrid.solve_multigrid` when
348
+ you only need the converged state (the CLI's engine); `implicit.run` for
349
+ gradients (`jax.grad`-able `ImplicitSolution`); `solver.solve` as the
350
+ low-level single-grid building block.
351
+
352
+ Optimization building blocks live in `vmex.core.optimize`
353
+ (quasisymmetry and omnigenity residuals; aspect ratio, iota, mirror ratio,
354
+ magnetic well, ballooning-stability targets; a least-squares driver over
355
+ boundary Fourier coefficients) with implicit-differentiation gradients from
356
+ `vmex.core.implicit` (`jac="implicit"`). The recommended pattern is **one
357
+ `least_squares` call** — no `max_mode` continuation loop — with **Exponential
358
+ Spectral Scaling** ordering the harmonics through the trust region:
359
+
360
+ ```python
361
+ from vmex import optimize as opt
362
+
363
+ qs = opt.QuasisymmetryRatioResidual(surfaces, helicity_m=1, helicity_n=0)
364
+ result = opt.least_squares(
365
+ [(qs, 0.0, 1.0), (opt.aspect_ratio, 6.0, 1.0), (opt.mean_iota, 0.42, 1.0)],
366
+ inp, max_mode=5, jac="implicit",
367
+ use_ess=True, # exp(-alpha*max(|m|,|n|)) trust radius per dof:
368
+ ) # high harmonics on short leashes — no ladder needed
369
+ ```
370
+
371
+ Measured on a 36-core CPU from a near-circular torus (single call, all
372
+ harmonics released at once; `examples/optimization/*_ess.py`; the staged
373
+ `max_mode`-ladder variants live alongside for comparison):
374
+
375
+ | class | nfp | residual | seed | achieved | max_mode | wall | status |
376
+ |-------|-----|----------|------|----------|----------|------|--------|
377
+ | QA | 2 | QS (1, 0) | 2.04e-01 | **7.2e-06** | 5 | **14.5 min** | precise; aspect 6.00, iota 0.42 (ladder: 3.7e-07 in 25.5 min) |
378
+ | QH | 4 | QS (1, −1) | 6.91e-01 | **5.83e-05** | 5 | 25.5 min (ladder) | precise; aspect 8.00, iota −1.22 |
379
+ | QP | 2 | QS (0, 1) | 4.46e-01 | 3.3e-02 | 5 | ~3.4 h (ladder + refinement) | hardest QS class — see caption |
380
+ | QI | 1 | omnigenity | 4.52e-01 | **1.81e-02** | 6 | **17.3 min** | 25× via the traceable Goodman constructed-QI residual |
381
+
382
+ ![QA/QH/QP optimization: seed vs optimized boundary, 3-D |B| geometry, and Boozer |B| on the LCFS](docs/_static/figures/readme_optimization.png)
383
+
384
+ *Each quasisymmetry class starts from a near-circular torus (grey, dashed) and
385
+ is shaped into a quasi-symmetric stellarator (blue) by the least-squares driver
386
+ (top row); the middle row is the optimized last-closed flux surface in 3-D
387
+ coloured by `|B|`, and the bottom row is `|B|` in Boozer coordinates on the LCFS
388
+ (jet line contours), whose contour geometry reads off the symmetry family —
389
+ horizontal for QA, diagonal for QH, vertical for QP. `QS` is the quasisymmetry
390
+ residual measured on the plotted equilibrium: QA **1.1e-6**, QH **5.8e-5**
391
+ (note QH's near-straight diagonal contours), QP **3.3e-2**. Quasi-poloidal QP
392
+ is the hardest class: the ladder plateaus near 5e-2, and an extended
393
+ warm-start refinement of the shipped deck reaches 3.3e-2. Reproduce with
394
+ `python benchmarks/make_readme_figures.py --only optimization` from the decks
395
+ in `benchmarks/opt_decks/`.*
396
+
397
+ Quasi-isodynamic (QI) shaping is intrinsically harder than quasisymmetry, so it
398
+ gets its own row across field periods:
399
+
400
+ ![QI equilibria at nfp 1-4: boundary, 3-D |B| geometry, and Boozer |B| on the LCFS](docs/_static/figures/readme_qi.png)
401
+
402
+ *Quasi-isodynamic (QI) equilibria at nfp 1, 2, 3, 4 (bundled decks in
403
+ `examples/data/`): boundary cross-sections (top), 3-D `|B|` geometry (middle),
404
+ and `|B|` in Boozer coordinates on the LCFS (jet, bottom). The label is the QI
405
+ (omnigenity) residual — **not** QS; QI is hard, so ~1e-3–1e-2 is expected here,
406
+ not the ~1e-5 reachable for quasisymmetry. Reproduce with
407
+ `python benchmarks/make_readme_figures.py --only qi`.*
408
+
409
+ These campaigns need implicit gradients. Finite differences stall at the
410
+ axisymmetric seed of the QH target (a saddle point) and land in a worse basin
411
+ for QP. Three measured optimizations keep each campaign in the minutes range:
412
+
413
+ - the residual Jacobian uses a block-tridiagonal factorization of the force
414
+ linearization (33× faster than per-dof GMRES);
415
+ - each trial equilibrium starts from a first-order perturbation prediction
416
+ (3.7× fewer solver iterations);
417
+ - a converged-state memo avoids re-solving the point the residual just
418
+ converged.
419
+
420
+ The implicit path runs on CPU by default, where it is fastest at production
421
+ sizes; high-resolution forward solves can use the GPU. The device policy
422
+ chooses per stage.
423
+
424
+ ### Beyond quasisymmetry: any objective, same gradients
425
+
426
+ Any physics objective can drive the same machinery. Starting from the
427
+ precise-QA deck above (QS ~1e-6, aspect 6.00, mean iota 0.42), five short
428
+ campaigns each optimize one new objective while keeping the QA residual in the
429
+ objective at a stiff weight:
430
+
431
+ - raise the coil-simplicity proxy min L∇B (`l_grad_b_state`);
432
+ - deepen the vacuum magnetic well;
433
+ - raise mean iota to 0.55 at fixed aspect;
434
+ - lower the aspect ratio to 4.8 at fixed iota;
435
+ - push the Mercier criterion `DMerc` toward stability at ⟨β⟩ ≈ 1.25%.
436
+
437
+ The first four use the implicit adjoint (`jac="implicit"`). `DMerc` has no
438
+ traceable lane yet (it is computed from host-side Mercier tables), so that
439
+ campaign uses finite differences at `max_mode` 2. The self-consistent Redl
440
+ bootstrap objective has its own section below.
441
+
442
+ ![Objectives showcase: five one-objective campaigns off the precise-QA seed](docs/_static/figures/readme_objectives.png)
443
+
444
+ | campaign | objective | seed → final | QS held? |
445
+ |----------|-----------|--------------|----------|
446
+ | `lgradb` | raise min L∇B to 1.3× seed (implicit adjoint) | 0.520 → 0.522 m (stiff — see note) | 9.8e-07 → 1.3e-06 |
447
+ | `well` | deepen the vacuum magnetic well (implicit adjoint) | **−0.037 → +0.0002** (hill → well) | 9.8e-07 → 1.5e-05 |
448
+ | `iota_up` | mean iota 0.42 → 0.55 at aspect 6 (implicit adjoint) | **0.420 → 0.535** | 9.8e-07 → 1.8e-05 |
449
+ | `aspect_down` | aspect 6.00 → 4.8 at iota 0.42 (implicit adjoint) | **6.00 → 4.84** | 9.8e-07 → 4.2e-06 |
450
+ | `dmerc` | interior DMerc → positive at ⟨β⟩ ≈ 1.25% (finite differences) | −16.6 → −16.5 (stiff — see note) | 6.6e-05 → 6.6e-05 |
451
+
452
+ The `well`, `iota_up`, and `aspect_down` campaigns each take 2–3 minutes on a
453
+ workstation CPU. The other two barely move, for physical reasons: with QS,
454
+ aspect, and iota all held, the precise-QA shape is already close to its best
455
+ attainable L∇B, and improving interior Mercier stability at fixed pressure
456
+ requires profile or current degrees of freedom that boundary shaping alone
457
+ does not provide.
458
+
459
+ *Reproduce with `python examples/optimization/objectives_showcase.py` (an
460
+ `--only lgradb,dmerc` flag runs subsets), then
461
+ `python benchmarks/make_readme_figures.py --only objectives`.*
462
+
463
+ ### Self-consistent bootstrap current
464
+
465
+ VMEX implements the **Redl** analytic bootstrap-current formula
466
+ ([Redl et al. 2021](https://doi.org/10.1063/5.0012664)) as a differentiable
467
+ objective, and a fixed-boundary self-consistency loop that regenerates the
468
+ toroidal current from the plasma geometry and kinetic profiles. Below,
469
+ reproducing [Landreman, Buller & Drevlak 2022](https://arxiv.org/abs/2205.02914):
470
+ the published precise QA and QH optima are loaded, their current profile is
471
+ **erased**, and `self_consistent_bootstrap` recovers it from the Redl formula
472
+ plus the paper's density/temperature profiles.
473
+
474
+ ![Self-consistent bootstrap current vs the published equilibria and SFINCS](docs/_static/figures/readme_bootstrap.png)
475
+
476
+ *Recovered current density &#10216;J·B&#10217; (VMEC, blue) matches the analytic
477
+ Redl profile (green), the published self-consistent equilibrium (grey), and —
478
+ for QA — the paper's SFINCS drift-kinetic benchmark (circles). Converged in 7
479
+ (QA) / 4 (QH) Picard iterations to bootstrap mismatch `f_boot` = 2.0e-6 / 7.5e-6;
480
+ the recovered plasma current lands within **1.9 % (QA)** and **0.3 % (QH)** of
481
+ the published `CURTOR`. Reproduce with
482
+ `python examples/optimization/{QA,QH}_bootstrap_selfconsistent.py` (needs the
483
+ paper's Zenodo dataset).*
484
+
485
+ ## VMEX vs DESC
486
+
487
+ [DESC](https://desc-docs.readthedocs.io/) is the other JAX-native,
488
+ differentiable, GPU-capable stellarator-equilibrium code. The key difference:
489
+ DESC minimises the MHD force in a global Zernike–Fourier basis — its own
490
+ equilibrium — while VMEX reproduces VMEC exactly. The two are
491
+ complementary:
492
+
493
+ | Where **VMEX** wins | Where **DESC** wins |
494
+ |---|---|
495
+ | **Is VMEC**: iteration-for-iteration VMEC2000 parity, standard `wout_*.nc`, VMEC-format prints | **Low-resolution accuracy**: global Zernike basis converges in fewer radial points |
496
+ | **Drop-in**: reads VMEC2000 `input.*` and VMEC++ JSON unchanged | **Objective library**: large, mature set of built-in optimization targets |
497
+ | **Full namelist**: non-symmetric surfaces (`LASYM = T`), NESTOR *and* virtual-casing free boundary | **Optimizers**: more built-in stochastic / constrained optimizers |
498
+ | **O(1)-memory adjoint**: peak memory flat in the number of design variables | Adjoint gradients (both codes are differentiable) |
499
+
500
+ Reach for **VMEX** to drop a differentiable code that *is* VMEC into an
501
+ existing VMEC workflow (simsopt, `booz_xform`, near-axis tooling). Reach for
502
+ **DESC** for its spectral accuracy at low radial resolution or its mature
503
+ objective library.
504
+
505
+ ## CLI reference
506
+
507
+ ```text
508
+ vmex input.X solve (INDATA or VMEC++ JSON), write wout_X.nc
509
+ vmex --plot wout_*.nc diagnostic plots from a WOUT file
510
+ vmex --booz wout_*.nc run booz_xform_jax, write boozmn_*.nc
511
+ vmex --plot boozmn_*.nc Boozer contour/spectrum plots
512
+ vmex --test run and plot the bundled quick-start case
513
+ vmex --doctor installation and JAX backend diagnostics
514
+
515
+ options:
516
+ --outdir PATH directory for wout/boozmn/figure output
517
+ --mode {cli,jit} jitted blocks with live printing (cli, default)
518
+ or a single lax.while_loop (jit)
519
+ --ftol F override the final-stage FTOL_ARRAY tolerance
520
+ --max-iter N override the final-stage NITER_ARRAY cap
521
+ --coils PATH ESSOS-style coils file: drive an LFREEB = T deck
522
+ by direct Biot-Savart instead of an mgrid file
523
+ --mbooz/--nbooz N Boozer spectral resolution (default 32/32)
524
+ --booz-surfaces S Boozer surfaces ('all' or a list of s values)
525
+ --quiet silence the VMEC-style stdout
526
+ ```
527
+
528
+ `vmec` follows the selected JAX backend: with CPU-only JAX it runs on the
529
+ CPU; with CUDA-enabled JAX it uses the GPU for the solver stages where that
530
+ is faster (`JAX_PLATFORMS=cpu|cuda` pins it explicitly).
531
+
532
+ ## Documentation
533
+
534
+ Full documentation — installation, quickstart, theory and numerics with
535
+ equation-to-source cross-references, API reference, and
536
+ performance/validation notes — at
537
+ [vmex.readthedocs.io](https://vmex.readthedocs.io/en/latest/).
538
+
539
+ ## Mirror equilibria
540
+
541
+ Alongside the toroidal VMEC core, `vmex.mirror` solves scalar-pressure
542
+ equilibria for **open magnetic mirrors** and **closed stellarator–mirror
543
+ hybrids** — the same differentiable, spline-native machinery applied to a
544
+ straight (open) axis. Open mirrors use nonperiodic axial coordinates
545
+ `(s, θ, ξ)` with fixed-flux end cuts, not thin-torus approximations. Coils and
546
+ Biot–Savart fields stay in [ESSOS](https://github.com/uwplasma/ESSOS); VMEX
547
+ consumes a supplied `xyz → B` field. The divergence-free field and
548
+ scalar-pressure energy are
549
+
550
+ ```text
551
+ √g B^θ = I'(s) − ∂_ξ λ, √g B^ξ = Ψ'(s) + ∂_θ λ, B^s = 0
552
+ W = ∫ [ B²/(2μ₀) + p/(γ − 1) ] dV
553
+ ```
554
+
555
+ ### Fixed-boundary open mirrors
556
+
557
+ A fixed-boundary solve is one call:
558
+
559
+ ```python
560
+ from vmex.mirror import MirrorConfig, MirrorResolution, solve_fixed_boundary_from_radius
561
+
562
+ config = MirrorConfig(resolution=MirrorResolution(ns=7, mpol=4, nxi=17))
563
+ result = solve_fixed_boundary_from_radius(0.3, config) # radius: scalar, (nxi,), or (ntheta, nxi)
564
+ ```
565
+
566
+ ![Fixed-boundary rotating-ellipse mirror: solved geometry, field lines, cross-sections, and convergence](docs/_static/figures/mirror_fixed_boundary_3d.png)
567
+
568
+ The rotating-ellipse mirror converges at `ftol = 1e-12` to a normalized
569
+ divergence of `6.6e-15`, in **6 s cold / 0.2 s warm** (peak ≈1.2 GB, CPU). Its
570
+ implicit boundary gradient agrees with two fully reconverged finite-difference
571
+ solves to `5.9e-10` relative — the derivative an external optimizer needs.
572
+
573
+ ### Free-boundary β scan
574
+
575
+ `solve_beta_scan` jointly updates the spline last-closed surface, the plasma
576
+ state, and the unbounded exterior vacuum, driven by an ESSOS two-coil field.
577
+ The supported sequence runs from 0 % to 10 % β, and the implicit free-boundary
578
+ derivative matches a reconverged finite difference to `1.1e-10` relative
579
+ (adjoint residual `1.4e-9`). Pushing to a requested 50 % β grows the central
580
+ radius 7.7 % and drops the on-axis field 23.7 % from vacuum, exercising the
581
+ finite-β coupling end to end.
582
+
583
+ ![Free-boundary beta scan with ESSOS coils: field lines, LCFS, |B|, pressure, and residual histories](docs/_static/figures/mirror_free_boundary_beta50_summary.png)
584
+
585
+ ### Stellarator–mirror hybrid (research)
586
+
587
+ A closed periodic hybrid — two exactly straight mirror legs joined by two
588
+ curved stellarator returns on a rotation-minimizing B-spline axis — has a
589
+ complete fixed-boundary solve and example. A finite axial current gives
590
+ `ι = 0.085`; the case reaches a `2.4e-14` variational residual and `3.1e-14`
591
+ normalized divergence. Its independent strong-force gate does not yet converge
592
+ under same-geometry refinement, so it ships as a validated **research
593
+ candidate**, not a supported benchmark — the same implicit API already
594
+ differentiates its periodic boundary and axis controls.
595
+
596
+ ![Periodic B-spline stellarator–mirror hybrid: straight legs, rotating returns, field lines, and |B|](docs/_static/figures/stellarator_mirror_hybrid.png)
597
+
598
+ ### Run the mirror examples
599
+
600
+ ```bash
601
+ python examples/mirror_fixed_boundary_nonaxisymmetric.py
602
+ python examples/mirror_free_boundary_beta_scan.py
603
+ python examples/stellarator_mirror_hybrid.py
604
+ ```
605
+
606
+ Open-mirror `mout_*.nc` files plot with `vmex --plot mout_*.nc`. The
607
+ [mirror-geometry documentation](https://vmex.readthedocs.io/en/latest/mirror_geometry.html)
608
+ derives the coordinate and field models, defines the boundary conditions and
609
+ residuals, and records the validation and derivative limits.
610
+
611
+ ## License
612
+
613
+ MIT. If you use VMEX in published work, please cite this repository and
614
+ the original VMEC papers (Hirshman & Whitson, *Phys. Fluids* 1983;
615
+ Hirshman, van Rij & Merkel, *Comput. Phys. Commun.* 1986).