Skip to content

Fix swapped inner-quadrant labels in luce_squareness - #318

Merged
smithsp merged 2 commits into
masterfrom
fix/luce-squareness-inner-labels
Oct 27, 2026
Merged

smithsp merged 2 commits into
masterfrom
fix/luce-squareness-inner-labels

Conversation

@tomneiser

@tomneiser tomneiser commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Two fixes to the Luce shape functions.

1. luce_squareness inner-quadrant labels swapped

luce_squareness computes the four quadrant squareness values in the order (ou, ol, iu, il) (see the names/POs/PEs tuples), but the unpack after the loop read zetaou, zetaol, zetail, zetaiu = z, assigning the upper-inner value to zetail and the lower-inner value to zetaiu. Since the function returns (zetaou, zetaol, zetail, zetaiu) and miller_R_a_κ_δ_ζ / flux_surfaces consume it in that documented order, squareness_upper_inner and squareness_lower_inner were exchanged on output. Outer quadrants were unaffected.

Verified numerically against an independent implementation on five DIII-D boundaries: outer quadrants agree to 1e-16, inner quadrants agree only after exchanging the labels. A lower-single-null shape (δ_lower = 0.67) had its strongly negative X-point squareness reported as squareness_upper_inner.

2. boundary_shape lower half reflected about the wrong axis

The lower quadrants of boundary_shape were built by negating the upper-half Z after zoffset had already been added, so for any nonzero zoffset the lower half was mirrored about Z = 0 instead of Z = zoffset and displaced vertically by twice the offset. Found while regenerating the sample DIII-D boundary from its extracted parameters (outboard extremum at Z = -0.05 m, lower half 0.10 m too high). No callers in IMAS or FUSE pass a nonzero zoffset today.

Changes

  • One-line fix to the unpack in luce_squareness (src/physics/fluxsurfaces.jl). Return order, docstring, and quadrant geometry are unchanged.
  • Mirror the lower half of boundary_shape about zoffset (src/physics/boundary.jl).
  • New testset luce_squareness quadrant labels in test/runtests_fluxsurfaces.jl: builds a boundary from Luce's superellipse parameterisation (PPCF 55 (2013) 095009) with four distinct per-quadrant squareness values and checks each is recovered in the right quadrant. A mirror-symmetric test cannot catch this bug; this one fails before the fix with the two inner values exchanged and passes after.
  • New testset boundary_shape zoffset: a nonzero zoffset must be a pure vertical translation of the zoffset = 0 shape, with and without X-points. Fails 9 of 18 before the fix.

Downstream impact

Consumers of squareness_lower_inner / squareness_upper_inner (e.g. FUSE pulse_schedule initialisation, shape-control targets, any stored references derived from earlier IMAS output) will see the two inner values exchange after this fix.

Test plan

  • luce_squareness quadrant labels fails before the fix (ζil gets -0.20, ζiu gets 0.25) and passes after
  • boundary_shape zoffset fails before the fix (9/18) and passes after
  • Full Pkg.test("IMAS") passes

🤖 Generated with Claude Code

The quadrant loop computes (ou, ol, iu, il) but the unpack assigned the
third value to zetail and the fourth to zetaiu, so squareness_upper_inner
and squareness_lower_inner were exchanged on output. Outer quadrants were
unaffected. Add a per-quadrant test on a Luce superellipse boundary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@smithsp

smithsp commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

I would appreciate a diagram here.

The lower quadrants were built by negating the upper-half Z after zoffset
had already been added, so for any nonzero zoffset the lower half was
reflected about Z = 0 instead of Z = zoffset and ended up displaced by
twice the offset. Mirror about zoffset and add a test that a nonzero
zoffset is a pure vertical translation, with and without X-points.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tomneiser

Copy link
Copy Markdown
Member Author

Two figures to illustrate the bugs and the fixes.

Figure 1: luce_squareness geometry. Left is the Luce superellipse boundary used in the new test, with four distinct prescribed squareness values. Right is the sample DIII-D LCFS from IMASdd. Dashed lines are the PO→PE diagonals, green dots the ζ=0 reference point PC, orange dots the boundary crossing PD. Outer quadrants are unchanged by this PR. On master the two inner quadrants report each other's value; after the fix each quadrant reports its own. For the sample DIII-D shape, the strongly negative upper-inner value (-0.62) was being reported as squareness_lower_inner.

luce_squareness quadrant geometry

Figure 2: regenerating a boundary from the extracted parameters. Shape parameters are extracted with miller_R_a_κ_δ_ζ and fed back to boundary_shape, with no X-point treatment. Orange is master: swapped inner labels and the old lower-half mirror. Green has the squareness fix only. Blue has both fixes.

  • The label swap puts the pinched inboard corner on the wrong side of the midplane (orange vs green/blue).
  • The old boundary_shape mirror reflected the lower half about Z = 0 instead of Z = zoffset, so with zoffset = -0.05 m on the DIII-D case the lower half sits 0.10 m too high (green vs blue). On the test boundary zoffset = 0, so green and blue coincide.

boundary_shape reconstruction, master vs fixes

With both fixes the test boundary is reproduced exactly. The residual mismatch on the DIII-D case, at the inboard midplane and at the X-point corner on top, is a limitation of the Luce form with a single zoffset and no null, not of this PR.

(Figures are hosted on the pr-318-figures branch.)

@smithsp
smithsp merged commit ba09eda into master Oct 27, 2026
2 checks passed
@smithsp
smithsp deleted the fix/luce-squareness-inner-labels branch October 27, 2026 19:39
tomneiser added a commit that referenced this pull request Oct 27, 2026
…d decreasing-psi r_mid

Structural tests for the defect class behind #318 (right computation, wrong
label / axis / order). Every shape and flux-surface routine is run on an
asymmetric input and on the same input translated in Z, reflected up/down,
with reversed point order, or with the sign of psi flipped; the outputs
must transform the way their labels say. Smoke tests cover the physics
files that had no test at all (equilibrium, currents, neoclassical,
radiation, transport, pedestal, rf, particles, tf, pf_active,
thermal_loads), including Jtor<->Jpar and JtoR<->JparB round trips.

Found by the new tests:

- geometry.jl intersection: several crossings on one segment of the first
  path were returned in second-path order instead of along the first path.
  find_psi_last_diverted relies on along-contour order to decide whether a
  surface is diverted; on the D3D sample the result differed between the
  plasma and its mirror image (psi_last_lfs -1.3146 vs -1.3128, converged
  to 1e-6) because a wall-corner clip was filtered out in one orientation.
  Sort within-segment crossings along the segment.
- fluxsurfaces.jl interp_rmid_at_psi: a decreasing psi(R) was handed to the
  interpolant as decreasing knots, which FastInterpolations does not
  support (3 mm error at the far SOL on the sample). Reverse to increasing
  knots. sol.jl worked around this with its own untruncated interpolant in
  the psi<0 branch; both signs now go through interp_rmid_at_psi
  (consistency; no numerical change on the sample for either sign).
- fluxsurface_extrema and luce_squareness now return NamedTuples in the same
  order (positional unpacking unchanged), so consumers can use names.

Shared test helpers moved to test/helpers.jl. Noted for the author:
flux_surfaces is not idempotent at the 2e-4 level (a second call starts
from the refined boundary psi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jmcclena pushed a commit that referenced this pull request Nov 2, 2026
…#319)

* Fix label, axis and argument-order defects found in a targeted review

Review of IMAS.jl for the defect class behind #318 (right computation,
wrong label / mirror axis / argument order). Each fix is covered by a test
that fails on the previous code and uses an asymmetric case.

- thermal_loads.jl mesher_heat_flux: unpacked 3 of the 4 fields returned by
  find_psi_last_diverted (a 4th field was inserted in Sept 2024), so
  psi_first_lfs_far received psi_first_open and null_within_wall received a
  Float. This disabled the wall-mask term that adds points in the private
  region around the second null (the psi window collapsed to a point), and
  would have raised a TypeError had that term ever selected a point. For
  the D3D sample and the FUSE D3D H-mode case the term selects no points,
  so their meshes are unchanged. Use the named fields.
- fluxsurfaces.jl find_psi_last_diverted: limited-plasma branch referenced
  an undefined variable (UndefVarError). Use find_psi_separatrix.
- fluxsurfaces.jl find_psi_separatrix: psi_sep_closed / psi_sep_open were
  returned swapped (closed label gave an open surface). Swap the return and
  make the sol.jl limited-case check use psi_sep_closed, which is what the
  old mislabelled value was.
- equilibrium.jl symmetrize_equilibrium!: evaluation on the grid dispatched
  to an in-place interpolant method and threw; and the clipped Z grid was
  centred on (grid centre + ZA)/2, so the up-down mirror was not about the
  magnetic axis. Evaluate pointwise and clip symmetrically about ZA.
- fluxsurfaces.jl find_x_point!: private regions classified with
  sum(pz) < Z0 instead of a mean, and the opposite-side and strike-point
  ordering tests were relative to Z = 0 instead of Z0, so a vertically
  shifted plasma lost its second X-point. Reference Z0 throughout.
- sol.jl find_levels_from_P: the wall and dd wrappers forwarded (q, r) to
  the (r, q) core method; the double swap cancelled on the dd path but the
  wall method failed its r[1] == 0 assertion when called as documented.
- geometry.jl curvature: on closed polygons the first point used a
  reversed entering segment (sign flipped) and the last point a zero-length
  leaving segment (curvature 0). Use the segments across the closing point.
- profiles.jl: retrhow -> rethrow.

Downstream: find_psi_separatrix consumers, symmetrize_equilibrium! (FUSE
ActorEquilibrium with symmetrize=true) and curvature endpoints (FUSE
limit_curvature, init_pf_active) change numerically. FUSE
ActorParticleHeatFlux on the D3D H-mode case gives identical output before
and after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Fix remaining Z=0 references, interp_rmid_at_psi truncation and boundary_shape end points

Second pass of the review (same defect classes), with asymmetric tests.

- fluxsurfaces.jl find_psi_tangent_omp: separatrix half-height, outboard
  wall window and the two reference lines were taken about Z = 0 instead of
  the magnetic axis; the function threw on a vertically shifted plasma.
- fluxsurfaces.jl find_psi_max: "opposite sides of the midplane" tests
  used products of raw Z instead of Z - ZA.
- thermal_loads.jl mesher_heat_flux: the "beyond the second null" wall
  test was about Z = 0 instead of Z0.
- profiles.jl L_H_threshold: primary x-point above/below is now judged
  relative to the magnetic axis, as the grad-B drift argument requires.
- fluxsurfaces.jl interp_rmid_at_psi: findlast on the sign of dpsi kept
  every sample up to the last one with the initial sign, so a
  non-monotonic stretch in the middle was included and the last monotonic
  sample was always dropped. Keep the first monotonic stretch, including
  its end.
- boundary.jl boundary_shape: the quadrant angle grids excluded theta =
  pi/2 and pi, so the top, bottom and inner-midplane points were never
  generated and the [2:end] trims dropped distinct points instead of
  duplicates (minor radius short by ~1e-4 m, uneven spacing). The closing
  point also differed from the first by rounding (upper vs lower
  triangularity), so the polygon was not exactly closed. Generate the end
  points, drop only shared points, snap the closing point.
- sol.jl: rename levels_is_not_monotonic_in_Ip_direction to what it means.
- Docstrings: cumlul_volume_integrate / cumlul_surface_integrate named the
  wrong function, boundary_shape documented `lkap` for `kapl`, getdata
  omitted the `units` field.

Reviewed and left unchanged: bootstrap sign convention, orientation
conventions in sol / fluxsurfaces, line_wall_2_wall index heuristics,
particle window centring, x-point dedupe, relative convergence tolerances
(psi-offset invariance verified), boundary_shape h-search loops, the
tollZ band in mesher_heat_flux, dynamic.jl geometric_axis.z / elongation
limitations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add invariance and smoke tests; fix crossing order in intersection and decreasing-psi r_mid

Structural tests for the defect class behind #318 (right computation, wrong
label / axis / order). Every shape and flux-surface routine is run on an
asymmetric input and on the same input translated in Z, reflected up/down,
with reversed point order, or with the sign of psi flipped; the outputs
must transform the way their labels say. Smoke tests cover the physics
files that had no test at all (equilibrium, currents, neoclassical,
radiation, transport, pedestal, rf, particles, tf, pf_active,
thermal_loads), including Jtor<->Jpar and JtoR<->JparB round trips.

Found by the new tests:

- geometry.jl intersection: several crossings on one segment of the first
  path were returned in second-path order instead of along the first path.
  find_psi_last_diverted relies on along-contour order to decide whether a
  surface is diverted; on the D3D sample the result differed between the
  plasma and its mirror image (psi_last_lfs -1.3146 vs -1.3128, converged
  to 1e-6) because a wall-corner clip was filtered out in one orientation.
  Sort within-segment crossings along the segment.
- fluxsurfaces.jl interp_rmid_at_psi: a decreasing psi(R) was handed to the
  interpolant as decreasing knots, which FastInterpolations does not
  support (3 mm error at the far SOL on the sample). Reverse to increasing
  knots. sol.jl worked around this with its own untruncated interpolant in
  the psi<0 branch; both signs now go through interp_rmid_at_psi
  (consistency; no numerical change on the sample for either sign).
- fluxsurface_extrema and luce_squareness now return NamedTuples in the same
  order (positional unpacking unchanged), so consumers can use names.

Shared test helpers moved to test/helpers.jl. Noted for the author:
flux_surfaces is not idempotent at the 2e-4 level (a second call starts
from the refined boundary psi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Remove silent error swallowing, stray debug output and misnamed doc registrations

- luce_squareness caught every exception and pushed 0.0, so any failure
  (including a genuine bug) looked like zero squareness. The only expected
  failure is the quadrant diagonal missing the boundary, which never happens
  on the sample equilibria (0 of 588 diagonals). That case is now explicit:
  NaN for that quadrant plus a one-time warning. Any other error propagates.
- find_psi_last_diverted plotted and @show'd to the display before asserting
  on the intersection count; replaced with an error message carrying the
  intersections. find_flux formats the particle into its error message
  instead of @show.
- Removed commented-out debug plotting/printing in intersection,
  inscribed_polygon, flux_surfaces, luce_squareness, find_flux, and the dead
  final_area / total_removed bookkeeping that only fed those prints.
- Doc registrations copied from the previous block: Zcoord was registered
  as Particle, zohm_divertor_figure_of_merit as q_par_omp_eich,
  total_sources as retain_source, exponential_profile under the stale name
  edge_profile (undefined); beta_tor was registered twice. Every registered
  symbol now resolves and no section lists a name twice.

Not changed: the empty catch blocks in plot.jl overlay optional diagnostic
data and are intentional; the goto catch in total_fluxes skips models that
lack an ion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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