Skip to content

Commit 530ade4

Browse files
logan-ncclaude
andcommitted
FFS - NEW FEATURE - Add kinetic knots across unresolved near-singular F-bar structure
The cond(F-bar) scan already locates shifted/split kinetic resonances (Park & Logan Eq. 70) but only reported them. Measure each sub-threshold peak's FWHM and, where the grid puts fewer than three knots inside it, evaluate the kernel at a few targeted psi and splice them in -- existing values are reused, so the cost is one kernel call per added knot, not a re-formation. Respects MIN_KNOT_SPACING, the near-axis validity band, and a 24-knot cap; a resolved grid inserts nothing. Measured on DIII-D: no insertions on the nominal or low-collisionality cases; on the collisionless slow-rotation case two peaks (psi=0.53, 0.51, FWHM 1.5e-3 and 2.5e-3) had zero knots inside them, and one of the two coincides with a top NTV torque-density peak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
1 parent 5aa1122 commit 530ade4

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

src/ForceFreeStates/Kinetic.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,69 @@
1+
2+
"""
3+
refine_grid_at_fbar_peaks(xs, kw, kt, evaluate, ffit, equil, intr, psi_c;
4+
ngrid=1000, relaxed_frac=0.01, target=3, max_add=24) → (xs, kw, kt)
5+
6+
Insert kinetic evaluation knots across near-singular structure of F̄ that the grid does not
7+
resolve. Scans cond(F̄) (the same operator `find_kinetic_singular_surfaces!` searches — Park &
8+
Logan Eq. 70, so shifted and split resonances are included), takes peaks between
9+
`relaxed_frac`·threshold and the singular threshold, measures each peak's FWHM, and adds knots
10+
only where fewer than `target` knots lie inside it. New points respect `MIN_KNOT_SPACING`, stay
11+
above the near-axis validity band, and are capped at `max_add`; each costs one kernel evaluation
12+
and the existing values are reused. A well-resolved grid inserts nothing.
13+
"""
14+
function refine_grid_at_fbar_peaks(xs::Vector{Float64}, kw::Array{ComplexF64,3}, kt::Array{ComplexF64,3},
15+
evaluate::Function, ffit::FourFitVars, equil::Equilibrium.PlasmaEquilibrium,
16+
intr::ForceFreeStatesInternal, psi_c::Float64;
17+
ngrid::Int=1000, relaxed_frac::Float64=0.01, target::Int=3, max_add::Int=24,
18+
cond_threshold::Float64=1e8)
19+
20+
lo, hi = xs[1], xs[end]
21+
scan = collect(range(lo, hi; length=ngrid))
22+
hint = Ref(1)
23+
cond_vals = [
24+
try
25+
evaluate_fbar_condition(x, ffit, equil, intr; hint=hint)
26+
catch
27+
Inf
28+
end for x in scan
29+
]
30+
31+
add = Float64[]
32+
for i in 2:(ngrid-1)
33+
c = cond_vals[i]
34+
(c > cond_vals[i-1] && c > cond_vals[i+1] && relaxed_frac * cond_threshold < c <= cond_threshold) || continue
35+
l = i
36+
while l > 1 && cond_vals[l] > c / 2
37+
l -= 1
38+
end
39+
r = i
40+
while r < ngrid && cond_vals[r] > c / 2
41+
r += 1
42+
end
43+
inside = count(x -> scan[l] <= x <= scan[r], xs)
44+
inside >= target && continue
45+
w = (scan[r] - scan[l]) / 3
46+
for x in (scan[i], scan[i] - w, scan[i] + w)
47+
(lo < x < hi && x > 2 * psi_c) || continue
48+
minimum(abs.(xs .- x)) < Equilibrium.MIN_KNOT_SPACING && continue
49+
isempty(add) || minimum(abs.(add .- x)) >= Equilibrium.MIN_KNOT_SPACING || continue
50+
push!(add, x)
51+
end
52+
end
53+
isempty(add) && return xs, kw, kt
54+
length(add) > max_add && (add = sort(add)[1:max_add])
55+
56+
sort!(add)
57+
@info "Kinetic grid: $(length(add)) knot(s) added across unresolved near-singular F̄ structure at " *
58+
"ψ=$(round.(add; digits=4)) (cond peaks below the singular threshold)"
59+
kw_new, kt_new = evaluate(add)
60+
allxs = vcat(xs, add)
61+
perm = sortperm(allxs)
62+
kw_all = cat(kw, kw_new; dims=1)[perm, :, :]
63+
kt_all = cat(kt, kt_new; dims=1)[perm, :, :]
64+
return allxs[perm], kw_all, kt_all
65+
end
66+
167
"""
268
make_kinetic_matrix(ctrl, equil, ffit, intr, metric;
369
calculated_source=nothing)
@@ -72,6 +138,31 @@ function make_kinetic_matrix(
72138
# Pre-compute FKG derived matrices (corresponds to Fortran method=0)
73139
_compute_fkg_matrices!(ffit, equil, intr, metric, kw_flat, kt_flat; xs=xs)
74140

141+
# The FKG splines now exist, so F̄ can be scanned: add knots only where near-singular
142+
# structure (shifted/split kinetic resonances) falls in an interval that does not resolve it.
143+
if ctrl.kinetic_source == "calculated" && calculated_source !== nothing
144+
xs2, kw_flat, kt_flat = refine_grid_at_fbar_peaks(
145+
collect(xs), kw_flat, kt_flat,
146+
psis -> begin
147+
kwn, ktn = calculated_source(ctrl, equil, intr, metric, ffit; psis=psis)
148+
(kwn .* ctrl.kinetic_factor, ktn .* ctrl.kinetic_factor)
149+
end,
150+
ffit, equil, intr, axis_validity_psi_c)
151+
if length(xs2) != length(xs)
152+
xs = xs2
153+
# _compute_fkg_matrices! folds the kinetic increments into amats/bmats/cmats (saving the
154+
# ideal copies), so restore those before recomputing or the increments are added twice.
155+
ffit.amats = ffit.amats_ideal
156+
ffit.bmats = ffit.bmats_ideal
157+
ffit.cmats = ffit.cmats_ideal
158+
for ic in 1:6
159+
ffit.kwmats[ic] = cubic_interp(xs, Series(@view(kw_flat[:, :, ic])); ffit.itp_opts...)
160+
ffit.ktmats[ic] = cubic_interp(xs, Series(@view(kt_flat[:, :, ic])); ffit.itp_opts...)
161+
end
162+
_compute_fkg_matrices!(ffit, equil, intr, metric, kw_flat, kt_flat; xs=xs)
163+
end
164+
end
165+
75166
return nothing
76167
end
77168

0 commit comments

Comments
 (0)