Skip to content

Separate boundary modes from VMEC solver resolution - #181

Open
rogeriojorge wants to merge 3 commits into
mainfrom
fix/boundary-vmec-io
Open

rogeriojorge wants to merge 3 commits into
mainfrom
fix/boundary-vmec-io

Conversation

@rogeriojorge

@rogeriojorge rogeriojorge commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Read the surface resolution from nonzero boundary coefficients instead of VMEC's solver MPOL/NTOR. Explicit mpol, ntor, or max_mode now truncate or pad the optimization boundary independently.

Export a zero-current vacuum input with FTOL_ARRAY=1e-12, NS_ARRAY=51, NITER_ARRAY=3500, and MPOL=boundary max(m)+2 and NTOR=boundary max(|n|)+2. MPOL is a count: the retained maxima are boundary max(m)+1 and boundary max(|n|)+2. Preserve signed modes and asymmetric coefficients.

Validation: 41 field/surface tests passed; VMEX parsed the exported coefficients and controls correctly.

@gftd13 please review and test this with your single-stage input and a higher-resolution restart. Check that the default surface includes every nonzero boundary mode, and that explicit mode limits give the intended optimization variables.

@gftd13 gftd13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question inline about the default MPOL headroom.

Comment thread essos/surfaces.py
"""
families = {'RBC': self.rc, 'ZBS': self.zs, 'RBS': self.rs, 'ZBC': self.zc}
asym = self.rs is not None or self.zc is not None
solver_mpol = self.mpol + 2 if mpol is None else mpol

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on the default solver resolution:

Since MPOL is a count (VMEC keeps m < MPOL), the default self.mpol + 2 retains m only up to mpol+1, while NTOR = self.ntor + 2 retains |n| up to ntor+2. The PR description says "two above" for both, so the poloidal headroom is one mode less than described.

Why it may matter: VMEC uses the same truncated series for the interior surfaces, which need harmonics above the boundary's (e.g. m=1 × m=2 shaping couples into m=3, m=2 × m=2 into m=4). With only +1 in m, the interior R_mn(s) can be under-resolved, which affects iota(s) and the B_mn spectrum, i.e. exactly the VMEX iota and QS-breaking values we use to validate the optimization.

It doesn't affect the single-stage example (it passes VMEC_MPOL=VMEC_NTOR=9), but anyone calling to_vmec() with defaults would get it. Should the default be self.mpol + 3 (max m = mpol+2), or should the description/docstring say +1 in m?

@rogeriojorge

Copy link
Copy Markdown
Member Author

@gftd13 correct: MPOL is a count, so the default adds one retained poloidal mode and two toroidal modes. I clarified the docstring and PR description. I kept the requested boundary-max + 2 defaults; these are starting resolutions, not convergence guarantees. The single-stage validation checks higher resolutions independently. Please review the wording and test the export with your boundary modes.

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.

2 participants