Skip to content

pybind: expose VMEC preconditioner operator + preconditioned JFNK#9

Draft
krystophny wants to merge 8 commits into
external-optimizersfrom
expose-preconditioner
Draft

pybind: expose VMEC preconditioner operator + preconditioned JFNK#9
krystophny wants to merge 8 commits into
external-optimizersfrom
expose-preconditioner

Conversation

@krystophny

@krystophny krystophny commented Jun 14, 2026

Copy link
Copy Markdown
Member

What

Expose VMEC's hand-built preconditioner as a reusable linear operator and use it
to precondition the external Newton-Krylov solver.

  • VmecModel.apply_preconditioner(v): applies VMEC's block preconditioner
    M^-1 (the m=1, radial, and lambda blocks) -- its approximate inverse Hessian.
    Exact application: M^-1 . F_raw == F_preconditioned. It requires a prior
    evaluate(precondition=true) (which assembles the radial block); the operator
    is then state-invariant and can be frozen across a Krylov solve.
  • solve_newton_krylov(..., preconditioned=True): uses M^-1 as the inner
    Krylov preconditioner.

This preconditioner is the metric for the preconditioned Krylov / quasi-Newton
solvers and the inner preconditioner for the Hessian solve in the adjoint
sensitivities (#11), and it is why first-order JFNK is so eval-efficient.

Verification (force evals counted in VMEC++, ns=11)

optimizer                  F-evals  time[s]    ||F||      dW
Newton-Krylov (JFNK)          2243     0.37   2.0e-09  3.3e-13
JFNK + M^-1 (this PR)          507     0.08   2.5e-10  2.7e-15   (solovev)
JFNK + M^-1 (this PR)         1633     1.99   2.9e-09  2.1e-09   (cth_like)

Preconditioning cuts JFNK from 2243 to 507 force evaluations on solovev (4.4x)
and converges cth_like where unpreconditioned JFNK does not. This is the
best-of-breed solver in the stack; the exact autodiff HVP (#23) is the operator
that needs no force evaluation per matvec.

Stacked on #8 (external optimizers).

Add VmecModel.apply_preconditioner(v): applies VMEC's preconditioner
M^-1 (m=1, radial, lambda steps) to a vector in the decomposed basis.
M^-1 is VMEC's hand-built approximate inverse Hessian; this exposes it
as a reusable linear operator for preconditioned Krylov / quasi-Newton
and for the Hessian solve in adjoint sensitivities. It requires a prior
evaluate(precondition=true), which assembles the radial preconditioner.

Validated exactly: apply_preconditioner(raw force) equals the native
preconditioned search direction; the operator is linear and, once
assembled, state-invariant.

Use it as the inner Krylov preconditioner in Newton-Krylov: on solovev
(ns=11) this cuts force evaluations from 2242 to 505 (4.4x) versus
unpreconditioned JFNK, converging to the same equilibrium.
The 'Compare benchmark result' step uses github-action-benchmark with
comment-on-alert and the GITHUB_TOKEN, which is read-only for pull requests from
forks -> 'Resource not accessible by integration'. Gate that step on the PR
coming from the same repo so fork PRs still run the benchmarks but skip the
write-back instead of failing.
The pinned vmec-0.0.6 cp310 wheel was f90wrapped against numpy 1.x. Under
the numpy 2.x that the test env now resolves, importing it dies in the
f90wrap array interface (f90wrap_vmec_input__array__rbc: 0-th dimension
must be fixed to 2 but got 4), so test_ensure_vmec2000_input_from_vmecpp_input
could never actually run on CI (and is currently red on main too, where the
wheel's runtime libs are not even installed).

Build VMEC2000 from upstream source with current f90wrap, which produces
numpy-2-compatible bindings. The recipe mirrors SIMSOPT's own CI
(hiddenSymmetries/VMEC2000, cmake/machines/ubuntu.json). An explicit
'import vmec' check in the install step surfaces any remaining problem here
rather than as a confusing test failure.
With VMEC2000 built from current upstream source, the compatibility test
runs for the first time and hits vmecpp indata fields that have no
counterpart in the legacy VMEC2000 INDATA namelist (e.g.
free_boundary_method), which raised AttributeError. The test explicitly
checks only the common subset, so guard the lookup with hasattr and skip
fields VMEC2000 does not have, instead of enumerating them one by one.
…mit pin

Bring this stack branch up to the corrected CI baseline (from proximafusion#583/proximafusion#564):
- tests.yaml: build VMEC2000 from the pinned source commit and cache the
  wheel; drop the unused FFTW/HDF5 dev packages.
- benchmarks.yaml: skip the result upload on fork PRs (read-only token).
- test_simsopt_compat.py: skip vmecpp-only INDATA fields.
- CMakeLists: pin abseil to the 20260107.1 commit hash for Clang >= 21.
…hmark fork guard (proximafusion#564)

* build: bump CMake abseil pin to 20260107.1 for Clang >= 21

The CMake FetchContent abseil pin (2024-08) fails to compile under
Clang >= 21: absl::Nonnull SFINAE in absl/strings/ascii.cc and the
numbers.cc nullability annotations are rejected by the newer frontend.
Bump to the 20260107.1 LTS, which compiles cleanly under Clang 21.1.8
and GCC. Clang is the compiler required for the Enzyme autodiff build.

The Bazel build keeps its own (BCR) abseil pin and is unaffected.

* ci: skip benchmark result upload on fork PRs (token is read-only)

The 'Compare benchmark result' step uses github-action-benchmark with
comment-on-alert and the GITHUB_TOKEN, which is read-only for pull requests from
forks -> 'Resource not accessible by integration'. Gate that step on the PR
coming from the same repo so fork PRs still run the benchmarks but skip the
write-back instead of failing.

* ci: build VMEC2000 from source so the compat test runs on numpy 2

The pinned vmec-0.0.6 cp310 wheel was f90wrapped against numpy 1.x. Under
the numpy 2.x that the test env now resolves, importing it dies in the
f90wrap array interface (f90wrap_vmec_input__array__rbc: 0-th dimension
must be fixed to 2 but got 4), so test_ensure_vmec2000_input_from_vmecpp_input
could never actually run on CI (and is currently red on main too, where the
wheel's runtime libs are not even installed).

Build VMEC2000 from upstream source with current f90wrap, which produces
numpy-2-compatible bindings. The recipe mirrors SIMSOPT's own CI
(hiddenSymmetries/VMEC2000, cmake/machines/ubuntu.json). An explicit
'import vmec' check in the install step surfaces any remaining problem here
rather than as a confusing test failure.

* test: skip vmecpp-only indata fields in the VMEC2000 compat subset

With VMEC2000 built from current upstream source, the compatibility test
runs for the first time and hits vmecpp indata fields that have no
counterpart in the legacy VMEC2000 INDATA namelist (e.g.
free_boundary_method), which raised AttributeError. The test explicitly
checks only the common subset, so guard the lookup with hasattr and skip
fields VMEC2000 does not have, instead of enumerating them one by one.

* build: pin abseil to the 20260107.1 commit hash

Pin the FetchContent abseil dependency to commit 255c84d (the exact
commit behind the 20260107.1 LTS tag) instead of the tag itself, so a
moved tag cannot change the dependency under us.

* ci: cache and pin the VMEC2000-from-source build

Use the canonical recipe (cache the built wheel keyed on the pinned
source commit 728af8b, drop the unused FFTW/HDF5 dev packages) instead
of rebuilding VMEC2000 unpinned on every run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant