Skip to content

Commit 168857b

Browse files
logan-ncclaude
andcommitted
PE/KF - BUGFIX! - Disable ideal-singularity regularization in self-consistent kinetic runs
reg_spot smooths the ideal 1/(m-nq) divergence of the displacements before they drive the NTV integrand. The self-consistent kinetic Euler-Lagrange operator has no such divergence -- det(F-bar) is complex and nonzero at the rationals (Park & Logan, Phys. Plasmas 24, 032505 (2017) Eq. 70) -- so regularizing there suppresses a finite physical response, and inconsistently: xi^psi is never regularized, so damping the other two breaks their near-resonance cancellation in dB/B. Measured (DIII-D-like, n=1) against the EL solution's own dissipation: NTV torque 0.1655 vs 0.1322 N*m with reg_spot=0.05, and 0.1324 vs 0.1322 (0.15%) with it off. Kinetic runs now force reg_spot=0 and log the override; ideal runs are unchanged, where without it the displacement and torque diverge by four orders of magnitude. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
1 parent 0def353 commit 168857b

4 files changed

Lines changed: 68 additions & 16 deletions

File tree

docs/src/kinetic_forces.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,42 @@ terms respectively, and do not modify the stored kinetic profile splines.
8989
when computing the `toroidal_rotation_factor` back-solve. Julia uses a
9090
clean reimplementation with consistent pre-scaling derivatives throughout.
9191

92+
## Regularization: why kinetic runs set `reg_spot = 0`
93+
94+
`[PerturbedEquilibrium] reg_spot` smooths the displacements before they drive the NTV
95+
integrand, multiplying ``\xi^{\psi\prime}`` and ``\xi^\alpha`` by
96+
``Q^2/(Q^2 + \mathrm{reg\_spot}^2)`` with ``Q = m - nq``. It exists because **ideal** MHD is
97+
singular at the rationals: ``\bar F_\mathrm{ideal} = Q F Q`` has ``\det \bar F = 0`` there, so
98+
those two components diverge as ``1/Q`` and the torque integral does not converge.
99+
100+
The **self-consistent kinetic** workflow has no such singularity. Park & Logan
101+
([Phys. Plasmas 24, 032505 (2017)](https://doi.org/10.1063/1.4978562), §III D) decompose the
102+
kinetic composite matrix as ``F_k = Q \bar F_k Q - P_l^\dagger Q - Q P_u + R_1`` where
103+
``R_1 \neq 0`` at ``Q = 0``; with finite torque ``\det \bar F`` is complex, its zeros leave the
104+
real ``\psi`` axis, and the singularity is removed from both the solution and the torque
105+
integral. (Torque-free kinetic energy principles instead *shift and split* the zeros to
106+
``\psi_r \mp r_{L,R}``, where the singularity is logarithmic and integrable — still not a case
107+
for smoothing.)
108+
109+
GPEC therefore **forces `reg_spot = 0` whenever `kinetic_factor > 0`**, logging the override.
110+
Leaving it on suppresses a finite physical response and does so inconsistently — ``\xi^\psi``
111+
is never regularized, so damping the other two breaks their near-resonance cancellation in
112+
``\delta B/B`` and leaves a spurious residue driving the NTV integrand.
113+
114+
Measured on the DIII-D-like H-mode case (n = 1, C-coil drive), comparing the NTV torque against
115+
the Euler–Lagrange solution's own dissipation ``-2n\,\mathrm{Im}\langle \xi, u_2\rangle/4\mu_0``
116+
— two independent calculations of the same quantity:
117+
118+
| configuration | max ``|\xi^\alpha|`` | NTV torque [N·m] | EL dissipation [N·m] |
119+
|---|---|---|---|
120+
| ideal, `reg_spot = 0` | 643.8 | 6074.3 ||
121+
| ideal, `reg_spot = 0.05` | 0.058 | 0.554 ||
122+
| kinetic, `reg_spot = 0.05` | 0.055 | 0.1655 | 0.1322 |
123+
| kinetic, `reg_spot = 0` | 0.059 | **0.1324** | **0.1322** |
124+
125+
The ideal rows show why the knob exists; the kinetic rows show why it must be off there — the
126+
two independent torques agree to 0.15 % with no regularization, and to 20 % with it.
127+
92128
## HDF5 outputs: complex torque convention and the EnergyIntegrals layout
93129

94130
The method level of `KineticForces/<method>/` reports the two physical scalars a user

src/GeneralizedPerturbedEquilibrium.jl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,19 @@ function main_from_inputs(
603603
ft_ctrl = ForcingTerms.ForcingTermsControl() # Use defaults
604604
end
605605

606+
pe_raw_in = inputs["PerturbedEquilibrium"]
607+
# reg_spot smooths the ideal 1/(m−nq) divergence of ξ^ψ′ and ξ^α before they reach the NTV
608+
# integrand. The self-consistent kinetic Euler-Lagrange operator has no such divergence —
609+
# det(F̄) is complex and nonzero at the rationals — so regularizing there suppresses a finite
610+
# physical response, and does so inconsistently (ξ^ψ is left unregularized, breaking the
611+
# near-resonance cancellation in δB/B). Park & Logan, Phys. Plasmas 24, 032505 (2017) §III D.
612+
if ctrl.kinetic_factor > 0 && get(pe_raw_in, "reg_spot", 0.0) != 0
613+
@info "Self-consistent kinetic run: overriding reg_spot=$(pe_raw_in["reg_spot"]) with 0 " *
614+
"(the kinetic terms remove the ideal resonant singularity; see docs/src/kinetic_forces.md)"
615+
pe_raw_in = merge(pe_raw_in, Dict("reg_spot" => 0.0))
616+
end
606617
pe_ctrl = PerturbedEquilibrium.PerturbedEquilibriumControl(;
607-
(Symbol(k) => v for (k, v) in inputs["PerturbedEquilibrium"])...
618+
(Symbol(k) => v for (k, v) in pe_raw_in)...
608619
)
609620
pe_intr = PerturbedEquilibrium.PerturbedEquilibriumInternal(; dir_path=intr.dir_path)
610621

src/PerturbedEquilibrium/FieldReconstruction.jl

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ where χ₁ = 2π·Ψ₀ [Park Phys. Plasmas 14, 052110 (2007) eq. 8-10].
1919
Clebsch displacement components for PENTRC (matches Fortran gpout_xclebsch):
2020
ξ^ψ = xsp_mn (unregularized)
2121
∂ξ^ψ/∂ψ = xmp1_mn (regularized: xsp1 * singfac²/(singfac² + reg_spot²))
22+
reg_spot = 0 in kinetic runs — no ideal singularity to smooth
2223
ξ^α = xms_mn (regularized: -A⁻¹(B·xmp1 + C·xsp), divided by χ₁ in output)
2324
2425
Contravariant displacement from Jacobian convolution (matches Fortran gpeq_contra):
@@ -404,7 +405,7 @@ function compute_clebsch_displacements(
404405
# xms = -(A\B)*xmp1 - (A\C)*xsp
405406
xsp_vec = view(xi_psi_modes, ipsi, :)
406407
mul!(xms_vec, bmat, xmp1_vec) # xms = B*xmp1
407-
mul!(xms_vec, cmat_buf, xsp_vec, 1.0+0.0im, 1.0+0.0im) # xms += C*xsp
408+
mul!(xms_vec, cmat_buf, xsp_vec, 1.0 + 0.0im, 1.0 + 0.0im) # xms += C*xsp
408409
# amat is positive-definite by construction (Newcomb kinetic-energy form), so cholesky is
409410
# safe. cholesky! factorizes in place (amat is a per-thread scratch buffer, refilled by
410411
# ffit.amats each surface), avoiding a fresh factorization allocation per surface.
@@ -980,10 +981,12 @@ function _apply_rzphi_transform(
980981
# Per-thread scratch (the immutable `ft` functor and `geom` are shared read-only): θ-space
981982
# transform inputs/outputs (length mtheta) and mode-space forward-DFT outputs (length mpert),
982983
# so the DFTs run in place with no per-surface allocation.
983-
bufs = [(R=zeros(ComplexF64, mtheta), Z=zeros(ComplexF64, mtheta), P=zeros(ComplexF64, mtheta),
984-
psi=zeros(ComplexF64, mtheta), th=zeros(ComplexF64, mtheta), ze=zeros(ComplexF64, mtheta),
985-
Ro=zeros(ComplexF64, mpert), Zo=zeros(ComplexF64, mpert), Po=zeros(ComplexF64, mpert))
986-
for _ in 1:Threads.maxthreadid()]
984+
bufs = [
985+
(R=zeros(ComplexF64, mtheta), Z=zeros(ComplexF64, mtheta), P=zeros(ComplexF64, mtheta),
986+
psi=zeros(ComplexF64, mtheta), th=zeros(ComplexF64, mtheta), ze=zeros(ComplexF64, mtheta),
987+
Ro=zeros(ComplexF64, mpert), Zo=zeros(ComplexF64, mpert), Po=zeros(ComplexF64, mpert))
988+
for _ in 1:Threads.maxthreadid()
989+
]
987990

988991
Threads.@threads :static for ipsi in 1:npsi
989992
buf = bufs[Threads.threadid()]

src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ Medium Priority (defer for MWE):
2626
- `singular_point_method::String` - Method for singular point treatment (default: "standard")
2727
2828
Regularization:
29-
# High Priority (MWE)
30-
- `reg_spot::Float64` - Regularization width for singular surface smoothing (default: 0.05). Set to 0 to disable. Must be ≥ 0.
29+
# High Priority (MWE)
30+
31+
- `reg_spot::Float64` - Regularization width for singular surface smoothing (default: 0.05). Set to 0 to disable. Must be ≥ 0. Forced to 0 in self-consistent kinetic runs, whose Euler-Lagrange operator has no resonant singularity to smooth.
3132
"""
3233
@kwdef struct PerturbedEquilibriumControl
3334
# High Priority (MWE)
@@ -121,6 +122,7 @@ Metadata [n_rational] — identifies each (surface, n) row:
121122
122123
Control-surface forcing/response spectra [numpert_total], in the three Pharr (2026) field
123124
representations (all tesla; no flux/weber is stored):
125+
124126
- `forcing_b`/`response_b` - bare normal field b (Σ⁻¹·b̃)
125127
- `forcing_b_rootarea`/`response_b_rootarea` - root-area-weighted field b̃ (coordinate-invariant)
126128
- `forcing_b_area`/`response_b_area` - area-weighted field b̄ (= S·b̃; flux is Φ = A·b̄)
@@ -185,18 +187,18 @@ well-conditioned flux-space inductances L, Λ:
185187
rational_surface_idx::Vector{Int} = Int[]
186188

187189
# Control-surface forcing/response spectra in the three weightings of field representations [numpert_total], tesla
188-
forcing_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (forcing Φ_x)
189-
forcing_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃ (coordinate-invariant)
190-
forcing_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄
191-
response_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (response Φ_tot = P·Φ_x)
190+
forcing_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (forcing Φ_x)
191+
forcing_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃ (coordinate-invariant)
192+
forcing_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄
193+
response_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (response Φ_tot = P·Φ_x)
192194
response_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃
193-
response_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄
195+
response_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄
194196

195197
# Control surface matrices [numpert_total × numpert_total], root-area-weighted field (b̃) space
196-
plasma_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # Λ̃ (field space)
198+
plasma_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # Λ̃ (field space)
197199
surface_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # L̃ (field space)
198-
permeability::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # P̃ = R⁻¹·Λ·L⁻¹·R
199-
reluctance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # ϱ̃ = R†·L⁻¹·(Λ−L)·L⁻¹·R
200+
permeability::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # P̃ = R⁻¹·Λ·L⁻¹·R
201+
reluctance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # ϱ̃ = R†·L⁻¹·(Λ−L)·L⁻¹·R
200202
rootarea_to_area_weight::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # S = Σ/√A at psilim: b̃→b̄ recovery operator
201203
surface_area::Float64 = 0.0 # scalar control-surface area A = ∫J|∇ψ|dθ (flux: Φ = A·b̄; conform R = S·A)
202204

0 commit comments

Comments
 (0)