Skip to content

iteration: fix post-reguess lambda scaling divergence, add Python multigrid driver#560

Open
CharlesCNorton wants to merge 2 commits into
proximafusion:mainfrom
CharlesCNorton:fix-iteration-divergence
Open

iteration: fix post-reguess lambda scaling divergence, add Python multigrid driver#560
CharlesCNorton wants to merge 2 commits into
proximafusion:mainfrom
CharlesCNorton:fix-iteration-divergence

Conversation

@CharlesCNorton

Copy link
Copy Markdown
Contributor

Fixes #555.

Root cause of the Python/C++ divergence

VmecConstants::rmsPhiP is accumulated by RadialProfiles::evalRadialProfiles, and Vmec::run calls constants_.reset() before every InitializeRadial (vmec.cc:302). The VmecModel bindings Reinitialize() (the axis-reguess path) and RefineTo() (the multigrid step) re-ran InitializeRadial without that reset, so each re-initialization doubled rmsPhiP and rescaled lamscale by sqrt(2).

That rescales the entire lambda sector. The preconditioned residual fsql1 carries the factor directly (it has no normalization that cancels a lamscale change), while the invariant fsql stays unchanged, so the damping average sees a different fsq1 and the post-reguess trajectory splits from the C++ inner solve from the second step onward. The converged physics is unaffected (lamscale is an internal scaling), which is why this never showed in convergence tests, only in step-for-step traces. On li383_low_res at ns=31 the signature is clean: bit-identical fsqr/fsqr1/fsqz1 with fsql1 off by exactly 2.0 at the first post-reguess evaluation.

A secondary, much smaller divergence channel was the damping average itself: numpy's pairwise summation associates differently from Eigen's sequential VectorXd::sum(), and the last-bit difference grows through long chaotic transients. The Python loop now sums the ring buffer left to right, matching Eigen bit for bit.

Changes

  • pybind_vmec.cc: constants_.reset() before InitializeRadial in Reinitialize() and RefineTo(), matching Vmec::run.
  • _iteration.py: sequential left-to-right damping-average sum.
  • _iteration.py / __init__.py: solve_multigrid() — drives the full coarse-to-fine ns_array ramp from Python, mirroring the Vmec::run multi-grid loop (skip-coarser ns_min rule, per-stage ftol/niter, refine_to interpolation between stages, C++ failure semantics). This covers the "multigrid logic isn't implemented yet" part of the issue.
  • tests/test_iteration.py:
    • regression test for the lambda scaling under repeated reinitialize(),
    • multigrid parity test: solve_multigrid on cma's own two-stage input (ns_array [25, 51]) against the full vmecpp.run, comparing the concatenated force-residual and restart-reason traces step for step,
    • li383_low_res added to the restart-path matrix (cold-start reguess with a clean descent — a sharp probe of the post-reguess state),
    • whole-trace residual tolerance tightened from rtol=0.5 to rtol=1e-3, with the first 50 iterations at rtol=1e-9.

Validation

Full-length (to convergence) Python-vs-C++ parity sweep, comparing iteration counts, restart-reason traces, and residual traces:

case ns before after
cma 72 1729 vs 1728 iters, residuals split at it 5 1729 vs 1729, traces match
cma 15 1539 vs 1540 1539 vs 1539, traces match
cma 31 1655 vs 1649 1655 vs 1655, traces match
li383_low_res 31 476 vs 511, residuals split at it 2 476 vs 476, traces match
solovev 15, cth_like 25, circular_tokamak 25, w7x 15/25 already matching still matching

Restart-reason traces are identical in every case; residual traces are tight for the whole run (the only remaining non-bit-exact effect is last-bit noise through the longest chaotic transients, staying below 1e-3 relative).

tests/test_iteration.py: 12 passed. Full Python suite: 144 passed, 4 skipped (the cth_like_free_bdy wout-reference failure pre-exists at main in this environment and is unrelated).

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.

Python _iteration and C++ iteration logic diverge for some equilibria

1 participant