Skip to content

Validate the direct-EQDSK local field-line pitch - #91

Open
krystophny wants to merge 2 commits into
diag/tc24-resonance-root-ledgerfrom
agent/eqdsk-direct-fieldline-label
Open

Validate the direct-EQDSK local field-line pitch#91
krystophny wants to merge 2 commits into
diag/tc24-resonance-root-ledgerfrom
agent/eqdsk-direct-fieldline-label

Conversation

@krystophny

@krystophny krystophny commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

The direct core already projects drift with the local field-line pitch. This PR now contains the independent validation and fail-closed guard only:

  • integrate the field line directly in cylindrical coordinates and compare its rotational transform with the theta-average of B^phi/B^theta
  • verify the local pitch genuinely varies with geometric theta
  • reject a vanishing B^theta before forming the local pitch

The superseded duplicate implementation commit was dropped during the stack rebuild.

Validation

  • rebuilt cumulative stack passes full fo
  • independent field-line integration agrees at s=0.15,0.35,0.60

Stack: #90 -> this PR.

krystophny added a commit that referenced this pull request Jul 25, 2026
…d line

PR #91 substitutes the local pitch hctrvr(2)/hctrvr(3) for the global q in the
field-line label, but nothing tested that the substituted quantity is the pitch.

The oracle is deliberately not the tabulated q: it is a field line integrated in
cylindrical coordinates from field_eq alone,

    dR/dphi = R B_R/B_phi ,   dZ/dphi = R B_Z/B_phi ,

advanced until the geometric angle about the magnetic axis completes one
circuit, giving the rotational transform as Delta_phi/(2 pi). That shares no
code with do_magfie_eqdsk's coordinate transformation, so it is an independent
behavioral oracle rather than a restatement of the patch. The two agree to
7e-4 at s = 0.15, 0.35, 0.60.

The test also asserts the local pitch genuinely varies with theta. Without that
an implementation returning the global q would satisfy the averaged comparison,
and the test would not discriminate the change it exists to protect.

Scope: this validates the substituted quantity, not the full canonical
precession. The straight-field-line angle map also depends on s, so its gradient
carries a radial term that the inp_swi=11 branch still omits; see #85.

Writing this test surfaced a separate defect, now fixed in libneo: geoflux built
both q and the s label from the GEQDSK's stored qpsi record, which on circ.eqdsk
disagrees with the rotational transform of the fixture's own field by a factor
of about 1.55. That is why test_eqdsk_axis could only afford a factor-of-two
bound on the same comparison. This test needs the libneo fix to pass.
@krystophny

Copy link
Copy Markdown
Member Author

Update: added a test, and found the reason one could not be written before.

The substituted quantity is now validated

The change substitutes hctrvr(2)/hctrvr(3) for the global q, and nothing here
tested that the substituted quantity is actually the field-line pitch. Commit
71c39fa adds test_eqdsk_local_pitch, whose oracle is a field line integrated
in cylindrical coordinates from field_eq alone:

dR/dphi = R B_R/B_phi ,   dZ/dphi = R B_Z/B_phi

advanced until the geometric angle about the magnetic axis completes one circuit,
giving the rotational transform directly as Delta_phi/(2 pi). It shares no code
with do_magfie_eqdsk's coordinate transformation, so it is an independent
behavioural oracle rather than a restatement of the patch. The two agree to
7e-4 at s = 0.15, 0.35, 0.60.

The test also asserts the local pitch genuinely varies with theta — without that,
an implementation that simply returned the global q would satisfy the averaged
comparison, and the test would not discriminate the change it exists to protect.

Why the obvious test could not be written against q

Writing it surfaced a separate and larger defect. geoflux built both the q
profile and the s label from the GEQDSK's stored qpsi record, and on
circ.eqdsk that record disagrees with the rotational transform of the fixture's
own field by a factor of about 1.55. A test written against the tabulated q
therefore fails for a reason having nothing to do with the field-line label — and
that is exactly why test_eqdsk_axis could only afford a permissive
factor-of-two bound on the same comparison.

Fixed upstream in itpplasma/libneo#404,
which computes q(psi) = (F/(2 pi)) oint dl_p/(R |grad psi|) from psirz and
fpol. This PR's new test needs that libneo change to pass.

I briefly suspected this PR's own substituted pitch was mis-normalised, since its
theta-average was low against q by a factor close to R0 = 1.6. That was
wrong: the metric is right and the tabulated q was wrong. The field-line
integration settles it, and this PR's quantity is correct.

Still not sufficient, unchanged from the original description

The radial term is still missing. alpha = phi - q(s) f(s,theta) has

grad(alpha) = grad(phi) - q_local grad(theta) - (q' f + q f_s) grad(s)

and only the first two terms are implemented here. inp_swi=9 carries an
analogue of the third (shearterm = Bphcov*dqds); inp_swi=11 has none. So this
remains necessary and not sufficient, and #85 stays open after it merges.

krystophny added a commit to itpplasma/libneo that referenced this pull request Jul 25, 2026
…ed qpsi (#404)

A GEQDSK's stored `qpsi` record is not required to be consistent with
its own
`psirz` and `fpol`, and frequently is not — it may be a fit, sit on a
different
radial grid, or simply be stale. It should not be trusted for anything
quantitative; q has to be computed from the field.

`geoflux` trusted it for **two** things, so an inconsistent record
corrupted more
than the q profile:

1. the q returned by `geoflux_get_flux_profiles`, and
2. the toroidal flux, and hence **the `s` label itself** —
`integrate_toroidal_flux`
   builds `s` by integrating `q dpsi_pol/(2 pi)`.

So a bad `qpsi` means every direct-EQDSK quantity is evaluated at the
wrong
radius *and* labelled with the wrong q.

## How wrong, measured

On `POTATO/test/golden_record_resonance/circ.eqdsk` the stored q
disagrees with
the rotational transform of the fixture's own field by a factor of about
**1.55**:

| s | q (stored, before) | rotational transform of the field | ratio |
|---|---:|---:|---:|
| 0.15 | 1.875 | 1.191 | 1.574 |
| 0.35 | 2.292 | 1.537 | 1.491 |
| 0.60 | 2.708 | 1.976 | 1.371 |

This is also why `test_eqdsk_axis` could only afford a permissive
factor-of-two
bound when comparing `<B^phi/B^theta>` with q: a tighter bound would
have failed
on the fixture.

## The fix

In axisymmetry with `B = F grad(phi) + grad(phi) x grad(psi)` and `psi`
the
poloidal flux **per radian**, `B_phi = F/R` and `B_pol = |grad(psi)|/R`,
so a
field line advances `dphi/dl_p = F/(R |grad(psi)|)` per unit poloidal
arclength
and one poloidal circuit gives

```
q(psi) = (F(psi)/(2 pi)) oint dl_p / (R |grad(psi)|).
```

Only `psirz` (through `psi_from_position`) and `fpol` are used, so the
result is
consistent with the field the rest of the chart evaluates.

`locate_flux_surface`'s bisection is split out as
`locate_by_normalized_flux` so
contours can be traced from `psi` alone — necessary because `s` does not
exist yet
at the point q is needed.

## The two ends, which both need care

- **Axis.** Surfaces too close to it collapse to a point and integrate
to zero.
The first *resolved* index is found rather than assumed to be 2: a zero
left in
place propagates into the toroidal flux and makes `s_grid`
non-monotonic, which
then corrupts every later interpolation. (This bit me during development
— it
  produced `q ~ 1e12`.)
- **Edge.** The outermost grid point sits on `sibry`, where the tracer
has nothing
to bracket against, and where a diverted equilibrium's q genuinely
diverges.
Left in, it produced a single `1.5e13` outlier that blew up
`psi_tor_edge`.

Both are extrapolated linearly in `psi` from the nearest resolved
interior
surfaces.

## Verification, against an independent oracle

A field line integrated in cylindrical coordinates from `field_eq`
alone,
advanced until the geometric angle about the magnetic axis completes one
circuit.
It shares no code with the chart's coordinate transformation or with the
q path.

| s | q (computed, after) | field-line transform | ratio |
|---|---:|---:|---:|
| 0.15 | 1.19391 | 1.19077 | 1.0026 |
| 0.35 | 1.53716 | 1.53669 | 1.0003 |
| 0.60 | 1.97419 | 1.97610 | 0.9990 |

From a factor 1.58 discrepancy to 0.03–0.3%.

Note the ratio lands at `1.000` and not at `2 pi` or `1/(2 pi)`, which
is the
direct check that the flux-per-radian convention in the formula above is
the
right one — the usual place this calculation goes wrong.

The matching regression test lives in NEO-RT
([itpplasma/NEO-RT#91](itpplasma/NEO-RT#91),
commit
`71c39fa`) because that is where the consuming chart is; it fails
without this
change. All 14 NEO-RT tests pass with it.

## Risk

This changes q, and therefore the `s` label, for **every** direct-GEQDSK
consumer. That is the point — the previous values were wrong wherever a
file's
`qpsi` was inconsistent — but it will move existing results, and
anything that
was tuned against the old labelling will shift. Worth a look from
whoever owns
the GEQDSK consumers.

@slopqueue slopqueue Bot 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.

Review verdict: Approve

Summary: PR #91 fixes the canonical toroidal precession (Om_tB) in the direct-EQDSK (inp_swi=11) path of src/orbit.f90 to use the local field-line pitch B^phi/B^theta (from hctrvr(2)/hctrvr(2) (global, tabulated) — because in geoflux coordinates theta is the geometric poloidal angle, not a straight-field-line angle, so alpha = phi - q*theta is not a field-line label. A new test (test_eqdsk_local_pitch.f90`) validates the theta-averaged local pitch against an independent cylindrical field-line integration. The change is small, well-scoped, and well-documented.

Findings:

  1. minor src/orbit.f90:447q_local = hctrvr(3) has no guard against a vanishing denominator, whereas the test (test_eqdsk_local_pitch.f90:90) explicitly guards abs(hcon(3)) <= tiny. For the tested fixture and any real tokamak at s > 0, B^theta is non-zero so this never triggers, but if a particle orbit ever reaches a near-axis region it would emit NaNs into the orbit integrator. Optional defensive guard (or at least a documented precondition) would harden it; not a blocker.

Verdict: Approve — the physics fix is correct (the local pitch is the actual dphi/dtheta = B^phi/B^theta), the test is a genuine independent oracle (cylindrical field_eq shares no code with do_magfie_eqdsk's metric path) and is non-vacuous (it asserts the local pitch varies along theta, so an implementation returning the global q would fail), and the honestly-scoped limitation (the radial gradient term of #85 is still omitted) is clearly documented in both code comments.

@krystophny
krystophny marked this pull request as draft July 26, 2026 15:16
@krystophny
krystophny marked this pull request as ready for review July 26, 2026 16:46

@krystophny krystophny left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed latest head 85d6ef8 against updated #90 after merging current main. The change uses the local direct-EQDSK field-line pitch B^phi/B^theta where geometric theta is not a straight-field-line angle. The new regression is a genuine independent oracle: it integrates the cylindrical field directly, requires non-constant local pitch, and checks the theta average. The full integrated fo pipeline passes.

The prior minor hardening note still applies: production divides by hctrvr(2) without the near-zero guard used by the test. That is not a blocker for the validated off-axis tokamak domain, but the precondition should eventually be explicit if near-axis orbits are admitted. No blocking finding.

@krystophny krystophny left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed latest cee794ec relative to updated #90. The local field-line pitch remains validated against independent cylindrical field-line integration, and the prior denominator hardening note is fixed: production now fails closed before dividing when direct-EQDSK B^theta vanishes. Full integrated fo is green. No blocking finding.

…d line

PR #91 substitutes the local pitch hctrvr(2)/hctrvr(3) for the global q in the
field-line label, but nothing tested that the substituted quantity is the pitch.

The oracle is deliberately not the tabulated q: it is a field line integrated in
cylindrical coordinates from field_eq alone,

    dR/dphi = R B_R/B_phi ,   dZ/dphi = R B_Z/B_phi ,

advanced until the geometric angle about the magnetic axis completes one
circuit, giving the rotational transform as Delta_phi/(2 pi). That shares no
code with do_magfie_eqdsk's coordinate transformation, so it is an independent
behavioral oracle rather than a restatement of the patch. The two agree to
7e-4 at s = 0.15, 0.35, 0.60.

The test also asserts the local pitch genuinely varies with theta. Without that
an implementation returning the global q would satisfy the averaged comparison,
and the test would not discriminate the change it exists to protect.

Scope: this validates the substituted quantity, not the full canonical
precession. The straight-field-line angle map also depends on s, so its gradient
carries a radial term that the inp_swi=11 branch still omits; see #85.

Writing this test surfaced a separate defect, now fixed in libneo: geoflux built
both q and the s label from the GEQDSK's stored qpsi record, which on circ.eqdsk
disagrees with the rotational transform of the fixture's own field by a factor
of about 1.55. That is why test_eqdsk_axis could only afford a factor-of-two
bound on the same comparison. This test needs the libneo fix to pass.
@krystophny
krystophny force-pushed the diag/tc24-resonance-root-ledger branch from d97892a to 77f73f3 Compare July 27, 2026 06:09
@krystophny
krystophny force-pushed the agent/eqdsk-direct-fieldline-label branch from cee794e to 4454d14 Compare July 27, 2026 06:09
@krystophny krystophny changed the title Use the local field-line pitch in the direct-EQDSK toroidal precession Validate the direct-EQDSK local field-line pitch Jul 27, 2026
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.

1 participant