Skip to content

Expose per-point qflux current-channel contributions#151

Open
krystophny wants to merge 26 commits into
mainfrom
feat/flux-pumping/pointwise-current
Open

Expose per-point qflux current-channel contributions#151
krystophny wants to merge 26 commits into
mainfrom
feat/flux-pumping/pointwise-current

Conversation

@krystophny

@krystophny krystophny commented Jul 11, 2026

Copy link
Copy Markdown
Member

Exposes a per-field-line-point partition of the raw mode-1 local-solver
qflux(2,k) current channel.

Stack: top of the open flux-pumping response stack. The parent is
feat/flux-pumping/source-conjugacy at 86f9503. No merge is requested during
the integration freeze.

The diagnostic partitions flux_vector into its existing per-step column
blocks. Their dot products sum to qflux(2,k) exactly. The output is
qflux_current_contributions.dat; it contains raw normalized quadrature
contributions, not pointwise physical parallel-current density.

The correction at fd22f71 removes the former division by centered toroidal
angle spacing. That spacing was not the solver's direction-dependent
Runge-Kutta/Simpson measure and could not produce a local current. Physical
current remains a later normalization and force-application step.

Invariant: the kinetic operator, source, discretization, convergence criteria,
current-row sign, normalization, units, coordinate conventions, boundary
conditions, ABI, and memory layout are unchanged. Runs without the helical
drive remain bitwise unchanged. A hard guard still rejects any partition that
does not sum to qflux(2,k).

Verification

Test fails on main

main has no qflux-profile diagnostic. The rejected behavior is present on
the previous PR head:

$ git show 1affd4a:NEO-2-QL/pointwise_current_mod.f90 | rg 'current_density\(i\) = acc/phi_weight\(i\)'
current_density(i) = acc/phi_weight(i)

This divided a weighted qflux contribution by an unrelated centered angle
spacing and labelled the result as physical current density.

Test passes after fix

$ fo check
Build OK; 343 modules; tests passed
$ fo build
Build OK
$ fo test qflux_profile
qflux_profile PASS 0.07s
1 passed, 0 failed, 0 skipped
$ fo
Static OK; Build OK; Tests OK; Lint OK
all stages passed
$ git diff --check HEAD^
[no output]

isw_qflux_NA=1 with in_file_pert='none' assembles an identically zero
non-axisymmetric source. That zero vector is the Arnoldi seed, so its
normalization divides by zero (0/0 -> NaN) and zhseqr then fails to
converge at every step, spinning without qflux output. Reject the
configuration in check() with a clear message instead.
Add an optional right-hand side to the non-axisymmetric solve: the
drive of a single-helicity perturbation given by a radial corrugation
field (delta B^s/B_0^phi)_mn and an electrostatic potential harmonic
Phi_mn. The vpar corrugation piece enters the A1/A2 force channels
with velocity weights a3m and x^2 (sigma-odd), the ExB piece with
weights x^-1 and a1m (sigma-even); the two new velocity moments are
computed in compute_sources. Input via new &ntv_input variables
(isw_hel_drive, m_theta_hel, hel_brad_re/im, hel_phim_re/im), default
off. Band profiles, moments and dimensional prefactors follow a
CAS-checked derivation (see issue #118).

Off by default: golden-record QL summary.h5 is reproduced to 1e-15
against a main build.
Review findings on the drive commit: fold the x^(3k-4) weight into a
single power so the x=0 quadrature node stays finite; skip the moment
quadrature on the precomputed-matrix-element path and reject
isw_hel_drive at run time when the moments are unavailable; reject the
unvalidated relativistic branch, reconstruction mode and unset species
data; deallocate asource_hel in collop_unload.
The zero-source guard added upstream rejects isw_qflux_NA=1 with
in_file_pert='none'. With the misalignment drive, that configuration is
valid: the drive supplies the non-axisymmetric source. Relax the guard
so it fires only when the drive is off or all its amplitudes are zero.
The opposite-helicity drive (m,n) -> (m,-n) enters add_helical_source as the
complex conjugate of the band profile (phase convention exp(+i(m theta +
n phi))). The assembly stencil is a real-coefficient linear map, so it must
conjugate the assembled source pointwise; a sign or phase error in the stencil
breaks this. The existing checks use a profile whose real and imaginary parts
vary independently, so they do not exercise conjugate symmetry.

Add a check that conjg(profile) yields conjg(source) to 1e-14, plus a
vacuity guard that the source carries an imaginary part.

Verification:
- ctest helical_source_test: Passed (0.07 s)
- ctest -LE external-data: 18/18 passed
- red-before: injecting a complex coefficient (1,0.1) into one stencil term
  makes the new check error stop with
  'conjugate profile did not conjugate the source pointwise' (exit 1)
Decompose the phi-integrated current channel
qflux(2,k) = sum_col flux_vector(2,col)*source_vector(col,k) into its
per-field-line-point contributions in the mode-1 local (axi) solver, which
HELCORE uses. The new pointwise_current_mod reuses the already-verified
flux_vector current row: its counter-passing block enters with the explicit
minus sign (parallel current is odd under sigma -> -sigma) and the step_factor
phi-quadrature weight is baked in, so the decomposition introduces no new sign,
weight, or normalization. flux_vector's columns partition into disjoint
per-istep blocks, so the block dot products sum back to qflux(2,k) exactly.

The solver writes current_profile.dat (phi_mfl, per-drive contributions, and
current densities) guarded by isw_hel_drive, so runs without the helical drive
are bitwise unchanged. A hard self-check stops the run if the field-line sum
does not reproduce qflux(2,k).

Verification:
- 17/17 TEST ctests pass; new pointwise_current_test added.
- Red-before: breaking the block partition (nblk without the npassing+1)
  gives "ERROR STOP FAIL: per-point contributions do not sum to the qflux
  current channel"; restored -> Passed.
- Local HELCORE simple_l1 run, all three drive cases (nminus_phi,
  nminus_phi_im, nplus_phi): gamma_current/D31/D32 reproduce the accepted
  aCluster response (-7.338706/-20.944508, -2.711060/-3.138948,
  -2.369674/-1.825414) to ~1e-11; each current_profile.dat reconstructs
  qflux(2,k) to ~1e-15 relative over 961 finite points.
@krystophny krystophny changed the title Add pointwise helical parallel-current profile to the local solver Expose per-point qflux current-channel contributions Jul 11, 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