Fix the sign of the cross terms in NESTOR's analytic add-back - #825
Fix the sign of the cross terms in NESTOR's analytic add-back#825CharlesCNorton wants to merge 10 commits into
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Wow, well done on spotting this issue - you mention that you did a convergence check against the analytical solution, could you share some plots for it, since you already generated the data? In particular I'm curious how the B.n decays qualitatively (is/was there clear structures visible in the error? How did this go unnoticed?), and rms decay in log-log |
|
Plots from the exact test (a line of dipoles on the axis of a helically deformed torus; the exact vacuum field on the surface is zero, so the total field NESTOR reconstructs is the error). NESTOR imposes B·n itself, so the residual is the tangential field. The old-sign residual is 6% of |B_ext| at every resolution and lives entirely in the low modes of the deformation, as (m, +1)/(m, −1) pairs of equal weight, which is the signature of the (m, n) ↔ (m, −n) swap. With the fix it drops to 0.1–0.4% and decays close to h⁻² up to nzeta 48, then plateaus around 1e-3 in |B| (1e-6 in |B|²) with an n = 0 structure; that floor is not the mgrid interpolation (a 601² grid gives the same numbers) and is three orders below the defect, so I have not chased it. rms |B_total| / rms |B_ext| for nzeta 18, 24, 36, 48, 72 (mpol = nzeta/3, ntor = nzeta/6): old 6.2e-2 throughout; fixed 3.9e-3, 2.4e-3, 1.2e-3, 7.6e-4, 1.1e-3. It went unnoticed because the error vanishes on axisymmetric surfaces, is smooth and shares the symmetry of the geometry, moves ordinary equilibria by fractions of a millimetre, and Fortran VMEC and VMEC++ agree with each other since both carry it; only a solver-independent reference exposes it. |
|
Haha, so fast! :) |
|
Really great work catching that. I guess the remaining m=0 structure is primarily due to the expansion error of the singularity subtraction, what do you think? |
|
@jurasic-pf Yes. The plateau is the auto grid tying ntheta to 2·mpol+6: at mpol 8, ntor 4, nzeta 24 the residual is 2.4e-3 / 7.2e-4 / 4.2e-4 for ntheta 22 / 44 / 88 (nzeta 24 → 96 only gives 4.2e-4 → 3.3e-4), and at ntheta 88 it is 4.2e-4 / 1.7e-4 / 1.5e-4 for mpol 8 / 12 / 16, about first order in ntheta as expected for a remainder that is only C⁰ at the singular point. |
|
@jurasic-pf The rise is mpol, not nzeta (the ladder ties mpol to nzeta/3): at fixed ntheta the residual is 1.5e-4 for mpol 16 and 5.7e-4 for mpol 24, mpol 32 diverges for both signs and also on the axisymmetric 1/R test, while nzeta 24 → 96 at fixed mpol improves it. That points at the analytic side at high l — the T_l recurrences and the alternating cmn sums with kL = mpol+1+ntor — rather than the geometry, whose derivatives are exact here (three Fourier modes). |
|
The PR convincingly demonstrates that there's a bug that needs fixing, but I wonder where it actually is - NESTOR and VMEC use different handedness in the coordinates, so there's a few terms that are lacking a or here have the sign convention backwards and we are just patching the consequence here |
|
@jons-pf Yes, it is the same issue. Negating guv and auv on the way into the analytic part exchanges ap ↔ am and Ap ↔ Am, i.e. T^± and S^±, which is exactly what the call-site swap does in the m ≥ 1, n ≥ 1 branch (the m = 0 / n = 0 branch uses T^+ + T^- and is unaffected); the explicit un-swap, implemented as a check, is bit-identical to this change. Intentional or not is settled by the exact solution: with the swap the assembled coefficients of mode (m, n) are those of the mirror kernel (m, −n), and NESTOR's residual on an exact exterior solution is 4.9e-3 at every resolution; without it 7e-6 and converging. If you want the form that mirrors the Fortran call, it can be written as the explicit swap instead. |
|
@jurasic-pf Checked: the stored guv equals 2 X_θ·X_ζ/nfp evaluated analytically on the helical torus to 3e-17, so greenf subtracts the physical kernel; the mismatch is on the analytic side, and the analysum2 argument swap jons found is its origin. |
# Conflicts: # src/vmecpp/cpp/vmecpp/test_data/wout_cth_like_free_bdy.nc # src/vmecpp/cpp/vmecpp/vmec/vmec/vmec_test.cc # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/add_fluxes/add_fluxes_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/bcontrav/bcontrav_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/bcov/bcov_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/bcov_full/bcov_full_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/bcovar_fileout/bcovar_fileout_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/bss/bss_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/calc_fbal/calc_fbal_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/constraint_force/constraint_force_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/evolve/evolve_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/evolve/evolve_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/forces/forces_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/forces/forces_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/freeb_data/freeb_data_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/fsq/fsq_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/fsq/fsq_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/fsq1/fsq1_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/fsq1/fsq1_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/funct3d_geometry/funct3d_geometry_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/jacobian/jacobian_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/jxbforce_bsub_lowpass/jxbforce_bsub_lowpass_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/jxbout/jxbout_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/lambda_forces/lambda_forces_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/lulv_comb/lulv_comb_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/mercier/mercier_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/metric/metric_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/phys_gc/phys_gc_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/phys_gc/phys_gc_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/rbsq/rbsq_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/rbsq/rbsq_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/scale_m1/scale_m1_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/scale_m1/scale_m1_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/scalfor_out/scalfor_out_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/scalfor_out/scalfor_out_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_axis/threed1_axis_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_beta/threed1_beta_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_firstTable/threed1_firstTable_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_geomag/threed1_geomag_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_shafrint/threed1_shafrint_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/threed1_volquant/threed1_volquant_00015_000000_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/tomnsps/tomnsps_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/tomnsps/tomnsps_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/totzsp_input/totzsp_input_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_analyt/vac1n_analyt_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_analyt/vac1n_analyt_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_bextern/vac1n_bextern_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_bsqvac/vac1n_bsqvac_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_bsqvac/vac1n_bsqvac_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_fouri/vac1n_fouri_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_fourp/vac1n_fourp_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_solver/vac1n_solver_00015_000053_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_solver/vac1n_solver_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_surface/vac1n_surface_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/vac1n_vacuum/vac1n_vacuum_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy/volume/volume_00015_000054_01.cth_like_free_bdy.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy_asym/vac1n_analyt/vac1n_analyt_00015_000053_01.cth_like_free_bdy_asym.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy_asym/vac1n_analyt/vac1n_analyt_00015_000054_01.cth_like_free_bdy_asym.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/cth_like_free_bdy_asym/vac1n_fouri/vac1n_fouri_00015_000053_01.cth_like_free_bdy_asym.json # src/vmecpp/cpp/vmecpp_large_cpp_tests/test_data/wout_cth_like_free_bdy.nc # tests/test_free_boundary.py
|
fixing conflicts |


NESTOR subtracts a tangent-plane kernel numerically (
RegularizedIntegrals) and adds its Fourier coefficients back analytically (SingularIntegrals). The analytic side used the opposite sign of the metric and curvature cross termsguv,auv, so mode (m, n) received the coefficient of (m, −n). The error vanishes on axisymmetric surfaces, is first order in the helical shaping, and does not decrease with resolution; VMEC 8.52 has the same sign inanalyt.f90.On an exact exterior Neumann problem (interior dipole line, helical torus) the residual drops from 4.9e-3 to 7e-6 and now converges with resolution; on the cth_like β=0.2% equilibrium the NESTOR boundary moves from 0.43 mm rms to 0.08 mm rms from a 10-digit BIEST solve of the same problem, and a W7-X standard-configuration equilibrium at β=2.2% moves by 1.7 mm on the axis and 1.3 mm rms on the boundary. educational_VMEC with the same two-line change reproduces the corrected results to 1e-11.
New tests: the assembled analytic coefficients against Gauss–Legendre quadrature of the subtracted kernels (
singular_integrals_test, fails with the old sign) and the interior-dipole exact solution (nestor_test). The golden scalars inLasymFreeBoundaryMatchesEducationalVmeccome from the patched educational_VMEC, the multigrid iteration guard is 328, andwout_cth_like_free_bdy.ncis regenerated (it matches the patched educational_VMEC at the reference test's tolerances).Fixes the NESTOR part of #628.