Skip to content

ForceFreeStates - BUGFIX - ⚠️ Remove the resonant solution at ideal crossings regardless of reduction timing - #489

Draft
d-burg wants to merge 7 commits into
developfrom
bugfix/ffs-crossing-resonant-elimination
Draft

d-burg wants to merge 7 commits into
developfrom
bugfix/ffs-crossing-resonant-elimination

Conversation

@d-burg

@d-burg d-burg commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: runs in which a Gaussian reduction fires a step or two before an ideal rational-surface crossing are now correct; on the Solovev n = 1 deck at ucrit = 2, et[1] goes from −30.75 to 0.67734. The shipped decks move only at integration-error level: energies by ≤ 3.5e-9 relative, ODE step counts by ≤ 1.3 %, NTV torques by ≤ 0.09 %. (harness @ 7de3a61)
  • Migration: none

In the standard ForceFreeStates (FFS) integration, an ideal rational-surface crossing could remove the wrong solution when a Gaussian reduction had just fired. It did so silently, and the free-boundary energies came out arbitrarily wrong. The crossing now always removes the resonant solution.

Regression report

regress --refs 97bd878d3,7de3a61df --force (all cases) on feynman (SLURM, 8 threads; develop vs head, both fresh, manifest pinned).

Regression Report: solovev_n1_frequent_reduction
=========================================================================================
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
-----------------------------------------------------------------------------------------
Quantity                 97bd878d3      7de3a61df      Diff                 Status       
-----------------------------------------------------------------------------------------
total energy Re(et[1])   -3.075070e+01  6.773400e-01   3.143e+01 (102.20%)  ** CHANGED **
total energy Im(et[1])   -1.189339e+02  -1.099802e-03  1.189e+02 (100.00%)  ** CHANGED **
plasma energy Re(ep[1])  -5.750084e+01  -9.736132e+00  4.776e+01 (83.07%)   ** CHANGED **
vacuum energy Re(ev[1])  2.675013e+01   1.041347e+01   1.634e+01 (61.07%)   ** CHANGED **
=========================================================================================
Summary: 4 changed
  • solovev_n1_frequent_reduction: new in this PR. It is the Solovev deck at ucrit = 2, eulerlagrange_tolerance = 1e-10, where develop removes a non-resonant solution at q = 2.
  • Unchanged: diiid_n1_riccati, diiid_slayer_n1, ggj_reference, ggj_ray_q500i, gal_resistive_diiid, solovev_multi_n, solovev_kinetic_calculated, solovev_kinetic_nuzero and efit_fixedbdy_separatrix.
  • gal_resistive_pe: extracts nothing on either ref. That is pre-existing on develop and unrelated to this PR.
  • The five cases that move do so at integration-error level:
solovev_n1
Regression Report: solovev_n1
================================================================================================
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
------------------------------------------------------------------------------------------------
Quantity                      97bd878d3        7de3a61df        Diff               Status       
------------------------------------------------------------------------------------------------
total energy Re(et[1])        6.773398e-01     6.773398e-01     3.508e-09 (0.00%)  ** CHANGED **
total energy Im(et[1])        -1.099802e-03    -1.099802e-03    1.3e-11            OK           
plasma energy Re(ep[1])       -9.736133e+00    -9.736133e+00    3.916e-09 (0.00%)  ** CHANGED **
vacuum energy Re(ev[1])       1.041347e+01     1.041347e+01     4.085e-10 (0.00%)  ** CHANGED **
vacuum matrix min eigenvalue  2.171581e+00     2.171581e+00     0.0e+00            OK           
plasma energy (all)           [32 elem]        [32 elem]        4.342e-04 (0.00%)  ** CHANGED **
vacuum energy (all)           [32 elem]        [32 elem]        3.682e-04 (0.00%)  ** CHANGED **
total energy (all)            [32 elem]        [32 elem]        1.113e-04 (0.00%)  ** CHANGED **
ODE steps (saved)             388              387              1.000e+00 (0.26%)  ** CHANGED **
ODE steps (total)             618              615              3.000e+00 (0.49%)  ** CHANGED **
q0                            1.900006e+00     1.900006e+00     0.0e+00            OK           
q95                           3.147422e+00     3.147422e+00     0.0e+00            OK           
beta_t                        4.620277e-02     4.620277e-02     0.0e+00            OK           
beta_n                        3.215290e+00     3.215290e+00     0.0e+00            OK           
# singular surfaces           2                2                0.0e+00            OK           
singular psi locations        [2 elem]         [2 elem]         0.0e+00            OK           
singular q values             [2 elem]         [2 elem]         0.0e+00            OK           
mpert                         32               32               0.0e+00            OK           
npert                         1                1                0.0e+00            OK           
q profile (checksum)          049799b65749...  049799b65749...  identical          OK           
pressure profile (checksum)   46c4ecb65021...  46c4ecb65021...  identical          OK           
ca_left (checksum)            320262e6cac8...  9ff9b7fba714...  0.000e+00 (0.00%)  ** CHANGED **
Runtime (s)                   224.2s           266.2s                              --           
================================================================================================
Summary: 9 changed, 13 unchanged
diiid_n1
Regression Report: diiid_n1
================================================================================================================
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
----------------------------------------------------------------------------------------------------------------
Quantity                                      97bd878d3        7de3a61df        Diff               Status       
----------------------------------------------------------------------------------------------------------------
total energy Re(et[1])                        8.012318e-01     8.012318e-01     6.2e-12            OK           
total energy Im(et[1])                        4.142529e-05     4.142529e-05     1.8e-12            OK           
plasma energy Re(ep[1])                       -1.348486e+00    -1.348486e+00    2.5e-11            OK           
vacuum energy Re(ev[1])                       2.149718e+00     2.149718e+00     1.9e-11            OK           
vacuum matrix min eigenvalue                  1.873976e-01     1.873976e-01     0.0e+00            OK           
plasma energy (all)                           [35 elem]        [35 elem]        8.350e-10 (0.00%)  ** CHANGED **
vacuum energy (all)                           [35 elem]        [35 elem]        3.051e-10 (0.00%)  ** CHANGED **
total energy (all)                            [35 elem]        [35 elem]        7.076e-10 (0.00%)  ** CHANGED **
ODE steps (saved)                             2576             2595             1.900e+01 (0.74%)  ** CHANGED **
ODE steps (total)                             4572             4630             5.800e+01 (1.27%)  ** CHANGED **
q0                                            1.204212e+00     1.204212e+00     0.0e+00            OK           
q95                                           4.781723e+00     4.781723e+00     0.0e+00            OK           
beta_t                                        1.327024e-02     1.327024e-02     0.0e+00            OK           
beta_n                                        1.372511e+00     1.372511e+00     0.0e+00            OK           
internal inductance li1                       8.842392e-01     8.842392e-01     0.0e+00            OK           
internal inductance li2                       7.080847e-01     7.080847e-01     0.0e+00            OK           
internal inductance li3                       7.304433e-01     7.304433e-01     0.0e+00            OK           
poloidal beta betap1                          6.680744e-01     6.680744e-01     0.0e+00            OK           
poloidal beta betap2                          5.349834e-01     5.349834e-01     0.0e+00            OK           
poloidal beta betap3                          5.518761e-01     5.518761e-01     0.0e+00            OK           
# singular surfaces                           5                5                0.0e+00            OK           
singular psi locations                        [5 elem]         [5 elem]         0.0e+00            OK           
singular q values                             [5 elem]         [5 elem]         0.0e+00            OK           
current beta betaj                            4.236478e-01     4.236478e-01     0.0e+00            OK           
plasma volume                                 1.829472e+01     1.829472e+01     0.0e+00            OK           
plasma current                                1.152130e+00     1.152130e+00     0.0e+00            OK           
mpert                                         35               35               0.0e+00            OK           
npert                                         1                1                0.0e+00            OK           
toroidal field bt0                            2.006573e+00     2.006573e+00     0.0e+00            OK           
wall field bwall                              3.880145e-01     3.880145e-01     0.0e+00            OK           
aspect ratio                                  2.845746e+00     2.845746e+00     0.0e+00            OK           
elongation kappa                              1.708350e+00     1.708350e+00     0.0e+00            OK           
q profile (checksum)                          0cd285cea88d...  0cd285cea88d...  identical          OK           
pressure profile (checksum)                   a1c48b266622...  a1c48b266622...  identical          OK           
Mercier D_I profile (checksum)                eeb06744e795...  eeb06744e795...  identical          OK           
resistive interchange D_R profile (checksum)  fa37296851f6...  fa37296851f6...  identical          OK           
ballooning Delta' profile (checksum)          6eb0ea075ccf...  6eb0ea075ccf...  identical          OK           
island half-widths                            [5 elem]         [5 elem]         9.948e-07 (0.00%)  ** CHANGED **
Chirikov parameter                            [5 elem]         [5 elem]         7.950e-05 (0.01%)  ** CHANGED **
||resonant area-weighted field||              5.189179e-04     5.189183e-04     4.4e-10            OK           
dominant-coupling singular values             [3 elem]         [3 elem]         1.216e-05 (0.00%)  ** CHANGED **
|forcing overlap with dominant mode|          1.415683e-04     1.415685e-04     2.5e-10            OK           
|delta_nominal| of coil set 1                 7.055228e-05     7.055240e-05     1.234e-10 (0.00%)  ** CHANGED **
||ddelta/d(shift)|| over coil sets            1.246233e-04     1.246234e-04     7.3e-11            OK           
||ddelta/d(tilt)|| over coil sets             4.180643e-07     4.180645e-07     2.3e-13            OK           
PE plasma energy                              3.422677e+00     3.422677e+00     3.7e-12            OK           
PE vacuum energy                              3.174510e+00     3.174510e+00     0.0e+00            OK           
PE surface energy                             5.826684e+00     5.826684e+00     5.3e-11            OK           
PE toroidal torque                            5.087465e-02     5.087465e-02     1.298e-12 (0.00%)  ** CHANGED **
NTV torque FGAR [N·m]                         5.296762e-01     5.296763e-01     1.5e-07            OK           
NTV kinetic energy dW FGAR [J]                7.924971e-02     7.924888e-02     8.3e-07            OK           
Runtime (s)                                   318.3s           381.0s                              --           
||forcing b~|| (root-area-weighted)           4.683328e-04     4.683328e-04     0.0e+00            OK           
resonant area-weighted field b^r              [5 elem]         [5 elem]         1.545e-08 (0.00%)  ** CHANGED **
================================================================================================================
Summary: 11 changed, 42 unchanged
solovev_kinetic_ntv
Regression Report: solovev_kinetic_ntv
=======================================================================================================
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
-------------------------------------------------------------------------------------------------------
Quantity                                   97bd878d3     7de3a61df     Diff               Status       
-------------------------------------------------------------------------------------------------------
NTV torque fgar [Re, Im]                   [1 elem]      [1 elem]      1.457e-07 (0.09%)  ** CHANGED **
NTV ψ quadrature evaluations               840           810           3.000e+01 (3.57%)  ** CHANGED **
root-area-weighted total energy Re(et[1])  6.773398e-01  6.773398e-01  3.508e-09 (0.00%)  ** CHANGED **
# singular surfaces                        2             2             0.0e+00            OK           
singular psi locations                     [2 elem]      [2 elem]      0.0e+00            OK           
q0                                         1.900006e+00  1.900006e+00  0.0e+00            OK           
Runtime (s)                                290.7s        362.0s                           --           
=======================================================================================================
Summary: 3 changed, 3 unchanged
solovev_kinetic_multiion
Regression Report: solovev_kinetic_multiion
===========================================================================================
[ Info: Running: solovev_n1_frequent_reduction @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 232.7s — 4 quantities extracted
[ Info: Running: ggj_ray_q500i @ 97bd878d3 (2026-09-28T10:43:40-04:00) [computed]
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 23.023s — 5 quantities extracted
[ Info: Running: solovev_kinetic_nuzero @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 314.2s — 15 quantities extracted
[ Info: Running: diiid_error_field @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 909.6s — 9 quantities extracted
[ Info: Running: diiid_slayer_n1 @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 313.7s — 18 quantities extracted
[ Info: Running: gal_resistive_pe @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 226.0s — 9 quantities extracted
[ Info: Running: solovev_kinetic_calculated @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 231.0s — 15 quantities extracted
[ Info: Running: gal_resistive_diiid @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 218.3s — 11 quantities extracted
[ Info: Running: solovev_n1 @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 224.2s — 23 quantities extracted
[ Info: Running: ggj_reference @ 97bd878d3 (2026-09-28T10:43:40-04:00) [computed]
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 10.208s — 5 quantities extracted
[ Info: Running: solovev_multi_n @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 225.3s — 16 quantities extracted
[ Info: Running: diiid_n1 @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 318.3s — 54 quantities extracted
[ Info: Running: solovev_kinetic_ntv @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 290.7s — 7 quantities extracted
[ Info: Running: diiid_n1_riccati @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 231.7s — 18 quantities extracted
[ Info: Running: solovev_kinetic_multiion @ 97bd878d3 (2026-09-28T10:43:40-04:00)
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 364.7s — 7 quantities extracted
[ Info: Running: efit_fixedbdy_separatrix @ 97bd878d3 (2026-09-28T10:43:40-04:00) [computed]
[ Info:   ErrorFields - FEATURE - Add the overlap phasor, tolerance...
[ Info:   Completed in 44.228s — 6 quantities extracted
[ Info: Running: solovev_n1_frequent_reduction @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 222.9s — 4 quantities extracted
[ Info: Running: ggj_ray_q500i @ 7de3a61df (2026-09-28T11:30:13-04:00) [computed]
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 22.416s — 5 quantities extracted
[ Info: Running: solovev_kinetic_nuzero @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 329.8s — 15 quantities extracted
[ Info: Running: diiid_error_field @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 944.9s — 9 quantities extracted
[ Info: Running: diiid_slayer_n1 @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 279.5s — 18 quantities extracted
[ Info: Running: gal_resistive_pe @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 251.0s — 9 quantities extracted
[ Info: Running: solovev_kinetic_calculated @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 234.1s — 15 quantities extracted
[ Info: Running: gal_resistive_diiid @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 204.4s — 11 quantities extracted
[ Info: Running: solovev_n1 @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 266.2s — 23 quantities extracted
[ Info: Running: ggj_reference @ 7de3a61df (2026-09-28T11:30:13-04:00) [computed]
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 11.681s — 5 quantities extracted
[ Info: Running: solovev_multi_n @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 258.0s — 16 quantities extracted
[ Info: Running: diiid_n1 @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 381.0s — 54 quantities extracted
[ Info: Running: solovev_kinetic_ntv @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 362.0s — 7 quantities extracted
[ Info: Running: diiid_n1_riccati @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 277.6s — 18 quantities extracted
[ Info: Running: solovev_kinetic_multiion @ 7de3a61df (2026-09-28T11:30:13-04:00)
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 389.0s — 7 quantities extracted
[ Info: Running: efit_fixedbdy_separatrix @ 7de3a61df (2026-09-28T11:30:13-04:00) [computed]
[ Info:   Regression - TEST - Track Solovev n=1 with reductions jus...
[ Info:   Completed in 47.262s — 6 quantities extracted
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
-------------------------------------------------------------------------------------------
Quantity                     97bd878d3      7de3a61df      Diff               Status       
-------------------------------------------------------------------------------------------
NTV total (D+T+imp+e) [N·m]  1.315589e-04   1.314604e-04   9.848e-08 (0.07%)  ** CHANGED **
NTV Deuterium [N·m]          7.785958e-05   7.778671e-05   7.287e-08 (0.09%)  ** CHANGED **
NTV Tritium [N·m]            8.195889e-05   8.190842e-05   5.047e-08 (0.06%)  ** CHANGED **
NTV electron [N·m]           -2.825956e-05  -2.823470e-05  2.486e-08 (0.09%)  ** CHANGED **
total energy Re(et[1])       6.773398e-01   6.773398e-01   3.508e-09 (0.00%)  ** CHANGED **
# singular surfaces          2              2              0.0e+00            OK           
Runtime (s)                  364.7s         389.0s                            --           
===========================================================================================
Summary: 5 changed, 1 unchanged
diiid_error_field
Regression Report: diiid_error_field
=============================================================================================================
Ref 1: 97bd878d3  @ 97bd878d3 (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
Ref 2: 7de3a61df  @ 7de3a61df (2026-09-28)
       env: julia 1.11.9, x86_64-linux-gnu, manifest f826ee26 (pinned), 8 threads/8 BLAS
-------------------------------------------------------------------------------------------------------------
Quantity                                         97bd878d3     7de3a61df     Diff               Status       
-------------------------------------------------------------------------------------------------------------
EFC overlap per kAt                              [1 elem]      [1 elem]      3.1e-11            OK           
EFC whole-field NTV torque per kAt² [N·m]        [1 elem]      [1 elem]      9.2e-09            OK           
EFC residual-field NTV torque per kAt² [N·m]     [1 elem]      [1 elem]      2.372e-08 (0.00%)  ** CHANGED **
EFC reference rotation ω_ref [rad/s]             [1 elem]      [1 elem]      0.0e+00            OK           
EFC rotation-scan shifts [rad/s]                 [9 elem]      [9 elem]      0.0e+00            OK           
EFC residual torque against rotation [N·m/kAt²]  [9 elem]      [9 elem]      6.254e-08 (0.00%)  ** CHANGED **
dominant-coupling singular values                [3 elem]      [3 elem]      1.216e-05 (0.00%)  ** CHANGED **
|delta_nominal| of coil set 1                    1.411046e-06  1.411048e-06  2.5e-12            OK           
Runtime (s)                                      909.6s        944.9s                           --           
=============================================================================================================
Summary: 3 changed, 5 unchanged

What was wrong

At a crossing, cross_ideal_singular_surf! reduces the solution columns and then zeroes the one it takes to be the large resonant solution. The reduction orders columns by their growth since the previous reduction (unorm ./ unorm0) and zeroes the first.

That order carries no information when a ucrit reduction fired only a few steps earlier: every column has grown by about 1. The lead column's pivot then lands on an arbitrary harmonic, and the crossing removes a non-resonant solution. A crossing on the very step after a reduction was worse: compute_solution_norms! skipped the crossing's reduction altogether, and the crossing zeroed a column chosen by the previous reduction's ordering.

On develop, Solovev n = 1 with ucrit = 2–5 hits this for most tolerances. Examples (reference et[1] = 0.6773399618, from Vern9 at reltol 1e-12, abstol 1e-14):

ucrit tolerance develop et[1] column zeroed at q = 2 / q = 3 this PR
2 1e-10 −30.75 m = −8 / m = 0 0.6773399618
2 1.2e-10 3.363 m = −10 / m = 2 0.6773399622
3 1e-9 1.013 m = 2 / m = −6 0.6773399623
5 1e-8 −0.657 m = 2 / m = −12 0.6773399701
5 1.4e-10 −1.310 m = −12 / m = 1 0.6773399619

Every correct run zeroes m = 2 at q = 2 and m = 3 at q = 3. Across all 18 develop-failing configurations (ucrit ∈ {2, 3, 5} at six tolerances from 1e-7 to 1.4e-10), this PR gives et[1] within 1.8e-7 of the reference at tolerance 1e-7, and within 7.5e-10 at 1e-9 and tighter. At the deck values of ucrit (1e3 and 1e4) reductions are rare enough that this was seldom hit, but nothing prevented it.

The fix

  • cross_ideal_singular_surf!: computes the resonant rows before its reduction, and passes them in as resonant_rows.
  • compute_solution_norms!: with resonant_rows, always reduces, including right after a ucrit reduction.
  • apply_gaussian_reduction!:
    • puts first, for each resonant row, the column with the largest component on that row;
    • pivots that column on the resonant row, so it alone keeps the resonant harmonic;
    • the remaining columns keep the growth order.
  • Zeroing: the crossing zeroes exactly those lead columns, and zeroed_idx records their positions for transform_u! as before.
  • Multi-n: each lead column lies in the n-block of its resonance, so the block structure the old findfirst preserved is kept.
  • Unchanged: kinetic crossings and ucrit reductions are unchanged. The Riccati path has its own crossing, which zeroes by ipert_res directly; its harness case is bit-identical.

Tests

  • test/runtests_eulerlagrange.jl, new testset "ideal-crossing reduction isolates the resonant harmonic": a 3 × 3 case in which the resonant component is not in the largest column. It checks three things:

    • that the reduction leads with and pivots on the resonant column, leaving the resonant row zero in every other column;
    • that it still does so right after a reduction;
    • that the growth-ordered reduction is unchanged without resonant rows.

    The file passes 107/107.

  • regression-harness/cases/solovev_n1_frequent_reduction.toml: the end-to-end reproducer above.

Notes for reviewers


NO MERGE WITHOUT HUMAN REVIEW. This PR is a draft and requires a named human reviewer and an assignee before it can be considered for merge. This is non-negotiable.

🤖 Generated with Claude Code

d-burg and others added 2 commits September 28, 2026 11:21
…sings regardless of reduction timing

At an ideal rational-surface crossing the Gaussian reduction ordered columns by growth since
the previous reduction and the crossing zeroed the first one. When a ucrit reduction had fired
a few steps earlier every column had grown alike, the order was arbitrary, the lead column's
pivot fell on a non-resonant harmonic, and the crossing removed the wrong solution. A crossing
that landed on the step right after a reduction skipped its own reduction and zeroed a column
chosen from the previous one.

The ideal crossing now always reduces, leads with the column carrying the most of each
resonant harmonic, and pivots it on the resonant row, so that column alone holds the resonant
component and is the one zeroed. Kinetic crossings and ucrit reductions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rational-surface crossings

ucrit = 2 at eulerlagrange_tolerance 1e-10 makes a ucrit reduction fire two steps before the
q = 2 crossing, which on develop leaves the crossing removing a non-resonant solution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jhalpern30

Copy link
Copy Markdown
Collaborator

@d-burg I actually remember what I think is this exact same bug come up in the process of identifying #480. During that, it seemed that this issue had disappeared when using the column wise absolute tolerance. Can you confirm if these results are with that change included, and if not, can you quote the combination of the two fixes and if they are both necessary?

@d-burg

d-burg commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

@jhalpern30 my claude answers: the main table in the body is on develop, without the column tolerance. I also ran your #480 head (3d6acd813) and #480 with this branch merged in. Same deck (Solovev n = 1), same reference; a run counts as wrong when et[1] is off by more than 1e-5:

runs with a wrong et[1] develop #480 this PR #480 + this PR
ucrit = 2, 3, 5 (18 runs) 12 8 0 0
ucrit = 10 (9 runs) 0 6 not run 0
ucrit = 1e2 (9 runs) 0 0 not run 0

So the column tolerance does not remove it. Every wrong run on #480 still zeroes a non-resonant column at a crossing (m = −10, 12, −6, 17, …). At ucrit = 10, #480 fails where develop does not, most likely because it takes about half the steps, so a reduction lands a step or two before a crossing more often. That may be why it looked fixed: it depends on where the steps land, so it comes and goes with tolerance and step count.

The two fixes do different jobs. #480 controls the error of the small columns; this PR makes the crossing remove the resonant solution whatever the reduction timing. Both are needed, and this branch merges onto #480 without conflicts.

To reproduce on your branch: the new solovev_n1_frequent_reduction harness case (ucrit = 2, tolerance 1e-10) gives et[1] = 1.013 on #480, against 0.67734.

@jhalpern30

Copy link
Copy Markdown
Collaborator

Cool thanks, I'll give this a review at some point in the near future

…tity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@d-burg d-burg changed the title ForceFreeStates - BUGFIX - Remove the resonant solution at ideal crossings regardless of reduction timing ForceFreeStates - BUGFIX - ⚠️ Remove the resonant solution at ideal crossings regardless of reduction timing Oct 5, 2026

@jhalpern30 jhalpern30 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Take what you like, most of my recommendations are for clarity in a section that I think is already confusing and this adds even more logic too.

I'll note that I've found even more bugs in our singular crossing logic and will be documenting them in #481 for further input.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am not a fan of our overpopulation of regression harnesses. Is this not a bugfix that can get a targeted unit test for expected behavior? Its a rather niche thing to get an entire regression case, and no one is realistically running with urit = 2 anyway. Is there something covered here that isn't touched by the unit tests below?

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated

# Fixup solution at singular surface; the ideal jump reduces on the resonant rows so the
# solution it removes below is the resonant one
compute_solution_norms!(odet.u, odet, ctrl, intr, true; resonant_rows=(ctrl.kinetic_factor == 0 ? ipert_res : Int[]))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is an ideal crossing - I don't think kinetic factor can ever be nonzero here

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated
# The reduction above placed the column pivoted on ipert_res[i] at index position i, which also
# keeps each zeroed column in the same n-block as the resonant mode introduced below; zeroed_idx
# records the positions for transforming u back to the full solution after integration.
if ctrl.kinetic_factor == 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This wasn't changed in this function (and maybe is why the above expression has it too), but I think the same logic applies - this crossing is only called when kinetic factor is > 0.

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here- I think remove unless you can see a reason to keep

compute_solution_norms!(odet.u, odet, ctrl, intr, true)
singp = intr.sing[ising]
ipert_res = 1 .+ singp.m .- intr.mlow .+ (singp.n .- intr.nlow) .* intr.mpert

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For clarity/concistency, maybe define

nres = length(ipert_res)

here, and then use for loop bounds below?

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Claude says with your changes, this could be reduced to

odet.u[:, odet.index[i, odet.ifix], :] .= ua[:, ipert_res[i]+intr.numpert_total, :]

with the loop for i in 1:nres

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated

- u: Current solution vector array, updated in-place if fixfac is called
- sing_flag: Indicates if normalization is occuring at a singular surface or not
- resonant_rows: Rows of the resonant harmonics at an ideal crossing. When given, the reduction

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"resonant_rows: Resonant rows at an ideal crossing; when given, the reduction always runs and pivots on them." is shorter and gets the same point across

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated

# Normalize unorm and perform Gaussian reduction if required
if odet.new
if odet.new && isempty(resonant_rows)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Completely up to you on this, but I had to read it a few times to understand the control flow here. An option I think is a bit more clear:

# The first call after a reduction records the reference norms; an ideal crossing reduces regardless
at_ideal_crossing = !isempty(resonant_rows)
if odet.new && !at_ideal_crossing
    odet.new = false
    odet.unorm0 .= odet.unorm
    return
end

# Growth since the last reduction (raw norms if a crossing comes right after one), then reduce if required
if !odet.new
    odet.unorm ./= odet.unorm0
end
uratio = maximum(odet.unorm) / minimum(odet.unorm)

# Perform Gaussian reduction if ucrit ratio is reached or at singular surface
if uratio > ctrl.ucrit || sing_flag
    # …
    apply_gaussian_reduction!(u, odet, intr, sing_flag; resonant_rows)
    odet.new = true
end

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated
description of `compute_solution_norms!` for more details on the benefits of in-place `u`
updates.

Columns are triangularized in order of their growth since the last reduction. At an ideal

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shorter option: "At an ideal crossing, the first pivots are the resonant_rows, each on the remaining column largest there. Growth order cannot pick the resonant solution if a ucrit reduction fired just before the crossing, since every column has then grown alike."

Comment thread src/ForceFreeStates/EulerLagrange.jl Outdated
end
# Sort unorm in descending order (since we triangularize from largest to smallest)
odet.index[:, ifix] = sortperm(odet.unorm; rev=true)
# Resonant columns first, then the rest in descending unorm (we triangularize from largest to smallest)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Claude's suggested cleanup for this section that I actually kinda like:

# Triangularize primary solutions, resonant rows first, then columns from largest to smallest growth
growth_order = sortperm(odet.unorm; rev=true)
row_free = trues(intr.numpert_total)
col_free = trues(intr.numpert_total)
for isol in 1:intr.numpert_total
    if isol <= length(resonant_rows)
        kpert = resonant_rows[isol]
        ksol = argmax(j -> col_free[j] ? abs(u[kpert, j, 1]) : -Inf, 1:intr.numpert_total)
    else
        ksol = growth_order[findfirst(j -> col_free[j], growth_order)]
        @views kpert = argmax(abs.(u[:, ksol, 1]) .* row_free)
    end
    odet.index[isol, ifix] = ksol
    col_free[ksol] = false
    row_free[kpert] = false
    # Eliminate other solution vectors below the pivot
    for jsol in 1:intr.numpert_total
        if col_free[jsol]
            # … elimination body unchanged …

Note that some of it (i.e. renaming mask to row_free/col_free is outside the scope of your PR but can be done here if you agree with it). This fix put the resonant logic in the same loops and removes some of this complicated lead/filter logic

d-burg and others added 4 commits October 9, 2026 12:58
…deal crossings and flatten the reduction control flow

The ideal crossing is only reached with kinetic_factor = 0, so its guards never
took the other branch. Review suggestions; no change in behavior intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…duction loop

One loop picks each resonant row's column from the columns still free at that
step, then continues in growth order. Identical for a single resonant row; with
several, later resonant columns are chosen after the earlier eliminations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion frequency in the full-run tests

Runs the coarse Solovev deck at ucrit = 2, 3, 5 and 10 against its default and
replaces the solovev_n1_frequent_reduction harness case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Something was wrong and now is not

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants