Skip to content

Commit 1708ef0

Browse files
committed
Merge remote-tracking branch 'origin/develop' into performance/consistent-surface-theta-parametrization
2 parents a765dec + 349a0c2 commit 1708ef0

5 files changed

Lines changed: 76 additions & 43 deletions

File tree

src/KineticForces/BounceAveraging.jl

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ end
147147

148148
"""
149149
compute_bounce_data(psi, n, l, q, bo, bmax, bmin, theta_bmax,
150-
tspl, B_extrap, mfac, chi1, ro, dbob_m_f, divx_m_f,
150+
tspl, B_vpar, mfac, chi1, ro, dbob_m_f, divx_m_f,
151151
divxfac, wdfac, mass, chrg, T_s, method;
152152
nlmda=128, ntheta=128,
153153
smat=nothing, tmat=nothing, xmat=nothing,
@@ -168,8 +168,8 @@ Ports Fortran torque.F90 lines 530-816 (GAR branch).
168168
- `bmax, bmin`: Max/min of B(θ) at this ψ
169169
- `theta_bmax`: θ location of Bmax (nodal knot; the passing-transit start)
170170
- `tspl`: Periodic poloidal interpolant: tspl(θ) → [B, dB/dψ, dB/dθ, J, dJ/dψ]
171-
- `B_extrap`: Endpoint-fit (non-periodic) cubic of B(θ) used for v_par and the
172-
bounce-point roots (the Fortran `vspl` equivalent)
171+
- `B_vpar`: Periodic cubic of B(θ) used for v_par and the bounce-point roots
172+
(the Fortran `vspl` equivalent)
173173
- `mfac`: Poloidal mode numbers [mlow:mhigh]
174174
- `chi1`: 2π·ψ₀ flux normalization
175175
- `ro`: Major radius [m]
@@ -190,7 +190,7 @@ function compute_bounce_data(
190190
psi::Float64, n::Int, l::Int, q::Float64,
191191
bo::Float64, bmax::Float64, bmin::Float64,
192192
theta_bmax::Float64,
193-
tspl, B_extrap, mfac::Vector{Int}, chi1::Float64, ro::Float64,
193+
tspl, B_vpar, mfac::Vector{Int}, chi1::Float64, ro::Float64,
194194
dbob_m_f::Vector{ComplexF64}, divx_m_f::Vector{ComplexF64},
195195
divxfac::Float64, wdfac::Float64,
196196
mass::Float64, chrg::Float64,
@@ -241,12 +241,12 @@ function compute_bounce_data(
241241

242242
# Find bounce points and build θ sub-grid
243243
_, _, tdt_pts, tdt_wts = _find_bounce_points_and_grid(
244-
lmda, bo, sigma, B_extrap, theta_bmax, psi, ntheta)
244+
lmda, bo, sigma, B_vpar, theta_bmax, psi, ntheta)
245245

246246
# Bounce integrals over θ (Fortran lines 674-735)
247247
wbbar, wdbar, dJdJ_val, wmats_lmda = _bounce_integrate(
248248
tdt_pts, tdt_wts, lmda, lnq, sigma, n, q, bo,
249-
tspl, B_extrap, chi1, ro, mfac, dbob_m_f, divx_m_f, divxfac, wdfac,
249+
tspl, B_vpar, chi1, ro, mfac, dbob_m_f, divx_m_f, divxfac, wdfac,
250250
do_matrices, mpert, smat, tmat, xmat, ymat, zmat, scr)
251251

252252
# Physical frequencies (Fortran lines 744-745)
@@ -409,12 +409,12 @@ end
409409

410410

411411
"""
412-
Parallel-velocity factor `v_par = 1 − (λ/bo)·B(θ)` from the endpoint-fit cubic of B
413-
(`B_extrap`, built where the surface interpolants are constructed), keeping v_par
412+
Parallel-velocity factor `v_par = 1 − (λ/bo)·B(θ)` from the periodic cubic of B
413+
(`B_vpar`, built where the surface interpolants are constructed), keeping v_par
414414
consistent with the bounce-point roots as in Fortran's `vspl`.
415415
"""
416-
@inline _vpar_from_extrap(B_extrap, lmda::Float64, bo::Float64, θ::Float64) =
417-
1.0 - (lmda / bo) * B_extrap(mod(θ, 1.0))
416+
@inline _vpar_from_spline(B_vpar, lmda::Float64, bo::Float64, θ::Float64) =
417+
1.0 - (lmda / bo) * B_vpar(mod(θ, 1.0))
418418

419419

420420
"""
@@ -423,14 +423,14 @@ Returns (t1, t2, theta_points, theta_weights).
423423
"""
424424
function _find_bounce_points_and_grid(
425425
lmda::Float64, bo::Float64, sigma::Int,
426-
B_extrap, theta_bmax::Float64, psi::Float64,
426+
B_vpar, theta_bmax::Float64, psi::Float64,
427427
ntheta::Int
428428
)
429429
if sigma == 0 # trapped
430-
# Bounce points: all roots of v_par(θ) = 1 − (λ/bo)·B_extrap(θ) in (0,1),
430+
# Bounce points: all roots of v_par(θ) = 1 − (λ/bo)·B_vpar(θ) in (0,1),
431431
# sorted descending — the same order as Fortran spline_roots, which the
432432
# marginally-trapped and deepest-well wrap logic below assume.
433-
vpar_fn = θ -> _vpar_from_extrap(B_extrap, lmda, bo, θ)
433+
vpar_fn = θ -> _vpar_from_spline(B_vpar, lmda, bo, θ)
434434
bpts = sort!(Roots.find_zeros(vpar_fn, 0.0, 1.0); rev=true)
435435

436436
nbpts = length(bpts)
@@ -443,7 +443,7 @@ function _find_bounce_points_and_grid(
443443
t1 = bpts[1]
444444
t2 = bpts[1] + 1.0
445445
else
446-
t1, t2 = _find_deepest_well(bpts, B_extrap, lmda, bo)
446+
t1, t2 = _find_deepest_well(bpts, B_vpar, lmda, bo)
447447
end
448448

449449
# Power-law grid refined near bounce points
@@ -463,7 +463,7 @@ end
463463
Find the deepest potential well (largest midpoint v_par) among bounce-point pairs,
464464
handling pairs that wrap through θ = 0/1.
465465
"""
466-
function _find_deepest_well(bpts::Vector{Float64}, B_extrap, lmda::Float64, bo::Float64)
466+
function _find_deepest_well(bpts::Vector{Float64}, B_vpar, lmda::Float64, bo::Float64)
467467
nbpts = length(bpts)
468468
best_vpar = 0.0
469469
best_t1 = 0.0
@@ -477,7 +477,7 @@ function _find_deepest_well(bpts::Vector{Float64}, B_extrap, lmda::Float64, bo::
477477
else
478478
θmid = 0.5 * (bpts[i] + bpts[j])
479479
end
480-
vpar_mid = _vpar_from_extrap(B_extrap, lmda, bo, θmid)
480+
vpar_mid = _vpar_from_spline(B_vpar, lmda, bo, θmid)
481481
if vpar_mid > best_vpar
482482
best_t1 = bpts[i]
483483
best_t2 = bpts[j]
@@ -506,7 +506,7 @@ Ports Fortran torque.F90 lines 674-793.
506506
function _bounce_integrate(
507507
tdt_pts::Vector{Float64}, tdt_wts::Vector{Float64},
508508
lmda::Float64, lnq::Float64, sigma::Int, n::Int, q::Float64, bo::Float64,
509-
tspl, B_extrap, chi1::Float64, ro::Float64,
509+
tspl, B_vpar, chi1::Float64, ro::Float64,
510510
mfac::Vector{Int}, dbob_m_f::Vector{ComplexF64}, divx_m_f::Vector{ComplexF64},
511511
divxfac::Float64, wdfac::Float64,
512512
do_matrices::Bool, mpert::Int,
@@ -550,9 +550,9 @@ function _bounce_integrate(
550550
jac = tspl_f[4]
551551
djdpsi = tspl_f[5]
552552

553-
# v_par from the endpoint-fit cubic (consistent with the bounce points);
553+
# v_par from the periodic cubic (consistent with the bounce points);
554554
# the periodic tspl B_val remains the numerator field in the integrands.
555-
vpar = 1.0 - (lmda / bo) * B_extrap(θmod)
555+
vpar = 1.0 - (lmda / bo) * B_vpar(θmod)
556556

557557
if vpar <= 0
558558
# Negative v_par near a bounce point: same fill rules as the Fortran bounce loop.
@@ -632,9 +632,13 @@ function _bounce_integrate(
632632
return 0.0, 0.0, 0.0, nothing
633633
end
634634

635-
# Bounce-averaged frequencies
635+
# Bounce-averaged frequencies. wbbar already carries one factor of ro that its own
636+
# normalization bhat = sqrt(2T/m)/ro cancels; reusing it inside wdbar imports that ro
637+
# a third time while dhat = (T/q)/(bo·ro²) removes only the two written explicitly, so
638+
# the drift prefactor takes ro, not ro². (Otherwise ω_D = wdbar·dhat carries a surplus
639+
# length: 4π·(I₂/I₁)·(T/q) is already V/Wb = 1/s, so the extra ro leaves m/s.)
636640
wbbar = ro * twopi / ((2 - sigma) * total_wb)
637-
wdbar = ro^2 * bo * wdfac * wbbar * 2 * (2 - sigma) * total_wd
641+
wdbar = ro * bo * wdfac * wbbar * 2 * (2 - sigma) * total_wd
638642

639643
# Phase factor pl_i = exp(-2πi·lnq·fsi_wb(θ_i)/((2-σ)·total_wb)), using the
640644
# cumulative spline integral of the bounce action.

src/KineticForces/CalculatedKineticMatrices.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ function compute_calculated_kinetic_matrices(
136136
z_s, m_s, kf_ctrl.wdfac, kf_ctrl.divxfac,
137137
el_s, equil, intr_t, prof_s;
138138
nutype=kf_ctrl.nutype, f0type=kf_ctrl.f0type, nufac=kf_ctrl.nufac,
139-
atol_xlmda=kf_ctrl.atol_xlmda, rtol_xlmda=kf_ctrl.rtol_xlmda
139+
atol_xlmda=kf_ctrl.atol_xlmda, rtol_xlmda=kf_ctrl.rtol_xlmda,
140+
atol_x=kf_ctrl.atol_x, rtol_x=kf_ctrl.rtol_x,
141+
nested_tolerance_margin=kf_ctrl.nested_tolerance_margin
140142
)
141143
full_w .+= block_w
142144
full_t .+= block_t

src/KineticForces/Compute.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ function integrate_psi_quadgk(
137137
tpsi!(thread_tpsi[tid], psi, n, l, zi, mi, wdfac, divxfac,
138138
electron, method, equil, thread_intrs[tid], kinetic_profiles;
139139
op_wmats=w,
140-
atol_xlmda=ctrl.atol_xlmda, rtol_xlmda=ctrl.rtol_xlmda)
140+
atol_xlmda=ctrl.atol_xlmda, rtol_xlmda=ctrl.rtol_xlmda,
141+
atol_x=ctrl.atol_x, rtol_x=ctrl.rtol_x,
142+
nested_tolerance_margin=ctrl.nested_tolerance_margin)
141143
harm_vals[ell_idx] = thread_tpsi[tid][]
142144
is_matrix_method && (harm_elems[ell_idx] .= w)
143145
end

src/KineticForces/KineticForcesStructs.jl

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,20 @@ builds a second control for its differing tolerance).
121121
nn::Int = 1 # Toroidal mode number
122122
nl::Int = 1 # Bounce harmonic number
123123

124-
# Tolerances.
125-
# *_xlmda: shared tolerances for inner λ (pitch) and x (energy) integrations
126-
# *_psi: tolerances for outer ψ quadrature
127-
atol_xlmda::Float64 = 1e-8 # Absolute tolerance for inner pitch + energy integrations
128-
rtol_xlmda::Float64 = 1e-5 # Relative tolerance for inner pitch + energy integrations
124+
# Tolerances, outermost to innermost: ψ quadrature ⊃ λ (pitch) ⊃ x (energy).
125+
# Each level must be resolved more tightly than the one enclosing it, or the outer
126+
# integrator chases its integrand's own quadrature noise instead of converging.
127+
# *_xlmda: tolerances for the λ (pitch) integration
128+
# *_x: tolerances for the x (energy) integration nested inside it; NaN ⇒ derive as
129+
# nested_tolerance_margin × the pitch tolerances
130+
# *_psi: tolerances for the outer ψ quadrature
131+
atol_xlmda::Float64 = 1e-8 # Absolute tolerance for the inner pitch integration
132+
rtol_xlmda::Float64 = 1e-5 # Relative tolerance for the inner pitch integration
133+
atol_x::Float64 = NaN # Absolute tolerance for the energy integration (NaN ⇒ derived)
134+
rtol_x::Float64 = NaN # Relative tolerance for the energy integration (NaN ⇒ derived)
135+
# The pitch integrand IS the energy integral, so the energy level is resolved this much
136+
# tighter than the pitch level by default.
137+
nested_tolerance_margin::Float64 = 1e-2 # Factor relating derived energy tolerances to the pitch ones
129138
# rtol_psi is the primary convergence knob: ~2 significant figures matches the validity
130139
# of the NTV model approximations. Do not set it tighter than the noise floor of the
131140
# inner integrals (keep rtol_psi ≳ 10 × rtol_xlmda).

src/KineticForces/Torque.jl

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int,
3535
op_wmats::Union{Nothing,Array{ComplexF64,3}}=nothing,
3636
rex_override::Union{Nothing,Float64}=nothing,
3737
imx_override::Union{Nothing,Float64}=nothing,
38-
atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6)
38+
atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6,
39+
atol_x::Float64=NaN, rtol_x::Float64=NaN,
40+
nested_tolerance_margin::Float64=1e-2)
41+
42+
# The pitch integrand is itself the energy integral, so the energy level is resolved
43+
# tighter than the pitch level (explicit atol_x/rtol_x override the derived values).
44+
atol_energy = isnan(atol_x) ? nested_tolerance_margin * atol_xlmda : atol_x
45+
rtol_energy = isnan(rtol_x) ? nested_tolerance_margin * rtol_xlmda : rtol_x
3946

4047
# Enforce bounds
4148
if psi > 1
@@ -94,10 +101,12 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int,
94101

95102
# Create periodic interpolant for poloidal quantities
96103
tspl = cubic_interp(xs, Series(hcat(B_vals, dBdpsi_vals, dBdtheta_vals, jac_vals, djdpsi_vals)); bc=PeriodicBC())
97-
# v_par and the bounce points use a separate endpoint-fit (non-periodic) cubic
98-
# of B, like Fortran's vspl. The endpoint fit of 1−(λ/bo)B equals 1−(λ/bo)
99-
# times the fit of B, so one B_extrap per surface serves every λ.
100-
B_extrap = cubic_interp(xs, B_vals; bc=CubicFit())
104+
# v_par and the bounce points use a separate scalar cubic of B (Fortran's vspl).
105+
# 1−(λ/bo)B is periodic on the closed θ interval, so the fit must be periodic too:
106+
# a non-periodic endpoint fit is only C⁰ at the θ=0/1 seam and manufactures false
107+
# near-seam extrema and root pairs there. The fit of 1−(λ/bo)B equals 1−(λ/bo)
108+
# times the fit of B, so one B_vpar per surface serves every λ.
109+
B_vpar = cubic_interp(xs, B_vals; bc=PeriodicBC())
101110

102111
bmax = maximum(B_vals)
103112
ibmax = argmax(B_vals)
@@ -228,10 +237,10 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int,
228237
method, op_wmats;
229238
chi1=intr.chi1, ro=intr.ro, mfac=intr.mfac,
230239
mpert=intr.mpert, theta_bmax=theta_bmax,
231-
B_extrap=B_extrap,
240+
B_vpar=B_vpar,
232241
smat=smat_f, tmat=tmat_f, xmat=xmat_f,
233242
ymat=ymat_f, zmat=zmat_f,
234-
energy_atol=atol_xlmda, energy_rtol=rtol_xlmda,
243+
energy_atol=atol_energy, energy_rtol=rtol_energy,
235244
pitch_atol=atol_xlmda, pitch_rtol=rtol_xlmda,
236245
rex_override=rex_override, imx_override=imx_override)
237246
end
@@ -403,7 +412,7 @@ function calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo,
403412
bmax, bmin, n_s::Float64, T_s::Float64, mass, chrg, tspl,
404413
dbob_m_f, divx_m_f, divxfac, wdfac, method, op_wmats;
405414
chi1::Float64, ro::Float64, mfac::Vector{Int}, mpert::Int,
406-
theta_bmax::Float64, B_extrap,
415+
theta_bmax::Float64, B_vpar,
407416
smat=nothing, tmat=nothing, xmat=nothing,
408417
ymat=nothing, zmat=nothing,
409418
nlmda::Int=128, ntheta::Int=128,
@@ -417,7 +426,7 @@ function calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo,
417426
# Bounce-averaged quantities per pitch angle
418427
bounce = compute_bounce_data(
419428
psi, n, l, q, bo, bmax, bmin, theta_bmax,
420-
tspl, B_extrap, mfac, chi1, ro, dbob_m_f, divx_m_f, divxfac, wdfac,
429+
tspl, B_vpar, mfac, chi1, ro, dbob_m_f, divx_m_f, divxfac, wdfac,
421430
mass, chrg, T_s, method;
422431
nlmda, ntheta, smat, tmat, xmat, ymat, zmat)
423432

@@ -617,8 +626,9 @@ function _setup_surface_state(
617626
end
618627

619628
tspl = cubic_interp(xs, Series(hcat(B_vals, dBdpsi_vals, dBdtheta_vals, jac_vals, djdpsi_vals)); bc=PeriodicBC())
620-
# Endpoint-fit (non-periodic) cubic of B for v_par and bounce points (Fortran vspl equivalent).
621-
B_extrap = cubic_interp(xs, B_vals; bc=CubicFit())
629+
# Periodic cubic of B for v_par and bounce points (Fortran vspl equivalent); 1−(λ/bo)B
630+
# is periodic on the closed θ interval, so a non-periodic fit would break at the seam.
631+
B_vpar = cubic_interp(xs, B_vals; bc=PeriodicBC())
622632

623633
bmax = maximum(B_vals)
624634
ibmax = argmax(B_vals)
@@ -687,7 +697,7 @@ function _setup_surface_state(
687697

688698
return (;
689699
chrg, mass,
690-
tspl, B_extrap, bmax, bmin, theta_bmax,
700+
tspl, B_vpar, bmax, bmin, theta_bmax,
691701
q, n_s, T_s, welec,
692702
wdian, wdiat, wtran, wgyro, nuk,
693703
epsr,
@@ -752,7 +762,7 @@ function kinetic_energy_matrices_for_euler_lagrange!(
752762

753763
bounce = compute_bounce_data(
754764
psi, n, l, state.q, bo, state.bmax, state.bmin, state.theta_bmax,
755-
state.tspl, state.B_extrap, mfac, chi1, ro, dbob_m_f, divx_m_f, 1.0, wdfac,
765+
state.tspl, state.B_vpar, mfac, chi1, ro, dbob_m_f, divx_m_f, 1.0, wdfac,
756766
state.mass, state.chrg, state.T_s, "fwmm";
757767
nlmda, ntheta, smat=smat_f, tmat=tmat_f, xmat=xmat_f, ymat=ymat_f, zmat=zmat_f)
758768

@@ -894,7 +904,9 @@ function compute_kinetic_matrices_at_psi!(
894904
electron::Bool, equil, intr::KineticForcesInternal,
895905
kinetic_profiles::Equilibrium.KineticProfileSplines;
896906
nutype::String="harmonic", f0type::String="maxwellian", nufac::Float64=1.0,
897-
atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6)
907+
atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6,
908+
atol_x::Float64=NaN, rtol_x::Float64=NaN,
909+
nested_tolerance_margin::Float64=1e-2)
898910

899911
# Bypass ψ > 1 (no kinetic contribution outside plasma)
900912
if psi > 1
@@ -903,13 +915,17 @@ function compute_kinetic_matrices_at_psi!(
903915
return nothing
904916
end
905917

918+
# See tpsi!: the energy integral is the pitch integrand, so it is resolved tighter.
919+
atol_energy = isnan(atol_x) ? nested_tolerance_margin * atol_xlmda : atol_x
920+
rtol_energy = isnan(rtol_x) ? nested_tolerance_margin * rtol_xlmda : rtol_x
921+
906922
state = _setup_surface_state(psi, zi, mi, electron,
907923
equil, intr, kinetic_profiles)
908924

909925
kinetic_energy_matrices_for_euler_lagrange!(
910926
kwmat, ktmat, state, psi, n, l, wdfac, intr;
911927
nutype, f0type, nufac,
912-
energy_atol=atol_xlmda, energy_rtol=rtol_xlmda,
928+
energy_atol=atol_energy, energy_rtol=rtol_energy,
913929
pitch_atol=atol_xlmda, pitch_rtol=rtol_xlmda)
914930

915931
return nothing

0 commit comments

Comments
 (0)