|
| 1 | +diff --git a/src/Equilibrium/DirectEquilibrium.jl b/src/Equilibrium/DirectEquilibrium.jl |
| 2 | +index ed6582131..c3c9ccfdd 100644 |
| 3 | +--- a/src/Equilibrium/DirectEquilibrium.jl |
| 4 | ++++ b/src/Equilibrium/DirectEquilibrium.jl |
| 5 | +@@ -227,6 +227,32 @@ function direct_position!(raw_profile::DirectRunInput) |
| 6 | + return ro, zo, rs1, rs2 |
| 7 | + end |
| 8 | + |
| 9 | ++""" |
| 10 | ++ eta_at_sfl_angle(sol, y_out, x, total_x) -> Float64 |
| 11 | ++ |
| 12 | ++Integration angle η at which the normalised straight-fieldline angle ∫jac·dl/Bp reaches `x`. |
| 13 | ++ |
| 14 | ++`y_out[:, 5]` is monotone in η, so it brackets the root to one solver step and Brent converges in |
| 15 | ++a handful of dense-output evaluations. Used to sample every flux surface at the *same* SFL angles |
| 16 | ++instead of resampling each surface's own solver steps (issue #376). |
| 17 | ++""" |
| 18 | ++function eta_at_sfl_angle(sol, y_out::Matrix{Float64}, x::Float64, total_x::Float64) |
| 19 | ++ x <= 0 && return y_out[1, 1] |
| 20 | ++ x >= 1 && return y_out[end, 1] |
| 21 | ++ target = x * total_x |
| 22 | ++ hi = searchsortedfirst(view(y_out, :, 5), target) |
| 23 | ++ hi = clamp(hi, 2, size(y_out, 1)) |
| 24 | ++ lo = hi - 1 |
| 25 | ++ eta_lo, eta_hi = y_out[lo, 1], y_out[hi, 1] |
| 26 | ++ f(eta) = sol(eta)[4] - target |
| 27 | ++ flo, fhi = f(eta_lo), f(eta_hi) |
| 28 | ++ # Degenerate bracket (repeated η, or the root sitting exactly on a step) needs no solve. |
| 29 | ++ flo == 0 && return eta_lo |
| 30 | ++ fhi == 0 && return eta_hi |
| 31 | ++ (flo * fhi > 0 || eta_hi <= eta_lo) && return eta_lo + (eta_hi - eta_lo) * (target - y_out[lo, 5]) / max(y_out[hi, 5] - y_out[lo, 5], eps()) |
| 32 | ++ return find_zero(f, (eta_lo, eta_hi), Roots.Brent()) |
| 33 | ++end |
| 34 | ++ |
| 35 | + """ |
| 36 | + direct_fieldline_int(psifac, raw_profile, ro, zo, rs2) |
| 37 | + |
| 38 | +@@ -250,9 +276,12 @@ from 1:5 rather than 0:4 as in Fortran. |
| 39 | + - `y_out[:, 4]`: ∫(dl/(R²Bp)) |
| 40 | + - `y_out[:, 5]`: ∫(jac*dl/Bp) |
| 41 | + |
| 42 | ++ - `sol`: the dense ODE solution, so callers can evaluate the trace at prescribed SFL angles |
| 43 | ++ rather than resampling this surface's own solver steps (`nothing` for tracers without it). |
| 44 | ++ |
| 45 | + - `bfield`: A `DirectBField` object with values at the integration start point. |
| 46 | + """ |
| 47 | +-function direct_fieldline_int(psifac::Float64, raw_profile::DirectRunInput, ro::Float64, zo::Float64, rs2::Float64)::Tuple{Matrix{Float64},DirectBField} |
| 48 | ++function direct_fieldline_int(psifac::Float64, raw_profile::DirectRunInput, ro::Float64, zo::Float64, rs2::Float64) |
| 49 | + |
| 50 | + # Find the starting point on the flux surface (outboard midplane) |
| 51 | + psi0_guess = raw_profile.psio * (1.0 - psifac) |
| 52 | +@@ -291,10 +320,12 @@ function direct_fieldline_int(psifac::Float64, raw_profile::DirectRunInput, ro:: |
| 53 | + callback = DiscreteCallback((u, t, i) -> true, refine_affect!; save_positions=(true, false)) |
| 54 | + |
| 55 | + prob = ODEProblem{true}(direct_fieldline_der!, u0, (0.0, 2π), params) |
| 56 | +- sol = solve(prob, Vern9(); callback=callback, reltol=equil_config.etol, abstol=1e-8, dt=2π / 200, adaptive=true, dense=false) |
| 57 | ++ # Dense output lets the caller evaluate the trace at the SFL angles it actually wants, instead |
| 58 | ++ # of splining this surface's solver-chosen steps and resampling (issue #376). |
| 59 | ++ sol = solve(prob, Vern9(); callback=callback, reltol=equil_config.etol, abstol=1e-8, dt=2π / 200, adaptive=true, dense=true) |
| 60 | + |
| 61 | + sol_matrix = reduce(hcat, sol.u::Vector{Vector{Float64}})' |
| 62 | +- return hcat(sol.t::Vector{Float64}, sol_matrix), bfield |
| 63 | ++ return hcat(sol.t::Vector{Float64}, sol_matrix), bfield, sol |
| 64 | + end |
| 65 | + |
| 66 | + """ |
| 67 | +@@ -495,18 +526,39 @@ robustness. |
| 68 | + ff_deriv_val = zeros!(pool, Float64, 4) |
| 69 | + |
| 70 | + for ipsi in (mpsi+1):-1:1 # outermost to innermost |
| 71 | +- y_out, bfield = fieldline_int(psi_nodes[ipsi], raw_profile, ro, zo, rs2) |
| 72 | ++ y_out, bfield, sol = fieldline_int(psi_nodes[ipsi], raw_profile, ro, zo, rs2) |
| 73 | + checkpoint!(pool, Float64) |
| 74 | + |
| 75 | +- # Fit data into temporary straight fieldline poloidal angle splines |
| 76 | +- ff_x_nodes = acquire!(pool, Float64, size(y_out, 1)) |
| 77 | +- @. ff_x_nodes = @view(y_out[:, 5]) / y_out[end, 5] |
| 78 | +- |
| 79 | +- ff_fs_nodes = acquire!(pool, Float64, size(y_out, 1), 4) |
| 80 | +- @. ff_fs_nodes[:, 1] = @view(y_out[:, 3])^2 |
| 81 | +- @. ff_fs_nodes[:, 2] = @view(y_out[:, 1]) / (2π) - ff_x_nodes |
| 82 | +- @. ff_fs_nodes[:, 3] = bfield.f * (@view(y_out[:, 4]) - ff_x_nodes * y_out[end, 4]) |
| 83 | +- @. ff_fs_nodes[:, 4] = @view(y_out[:, 2]) / y_out[end, 2] - ff_x_nodes |
| 84 | ++ # Straight-fieldline angle x = normalised ∫jac·dl/Bp, monotone in the integration angle η. |
| 85 | ++ # |
| 86 | ++ # Sampling x at this surface's own solver steps and resampling onto theta_nodes leaves a |
| 87 | ++ # resample error that is uncorrelated between neighbouring surfaces, i.e. white noise in ψ |
| 88 | ++ # that grid refinement then amplifies (issue #376). With dense output we instead solve for |
| 89 | ++ # the η where x hits each target node and evaluate there, so every surface is sampled at |
| 90 | ++ # the same abscissae and the resample error at the output nodes is zero. |
| 91 | ++ nff = sol === nothing ? size(y_out, 1) : mtheta + 1 |
| 92 | ++ ff_x_nodes = acquire!(pool, Float64, nff) |
| 93 | ++ ff_fs_nodes = acquire!(pool, Float64, nff, 4) |
| 94 | ++ |
| 95 | ++ if sol === nothing |
| 96 | ++ @. ff_x_nodes = @view(y_out[:, 5]) / y_out[end, 5] |
| 97 | ++ @. ff_fs_nodes[:, 1] = @view(y_out[:, 3])^2 |
| 98 | ++ @. ff_fs_nodes[:, 2] = @view(y_out[:, 1]) / (2π) - ff_x_nodes |
| 99 | ++ @. ff_fs_nodes[:, 3] = bfield.f * (@view(y_out[:, 4]) - ff_x_nodes * y_out[end, 4]) |
| 100 | ++ @. ff_fs_nodes[:, 4] = @view(y_out[:, 2]) / y_out[end, 2] - ff_x_nodes |
| 101 | ++ else |
| 102 | ++ total_x = y_out[end, 5] |
| 103 | ++ for itheta in 1:(mtheta+1) |
| 104 | ++ x = theta_nodes[itheta] |
| 105 | ++ eta = eta_at_sfl_angle(sol, y_out, x, total_x) |
| 106 | ++ u = sol(eta) |
| 107 | ++ ff_x_nodes[itheta] = x |
| 108 | ++ ff_fs_nodes[itheta, 1] = u[2]^2 |
| 109 | ++ ff_fs_nodes[itheta, 2] = eta / (2π) - x |
| 110 | ++ ff_fs_nodes[itheta, 3] = bfield.f * (u[3] - x * y_out[end, 4]) |
| 111 | ++ ff_fs_nodes[itheta, 4] = u[1] / y_out[end, 2] - x |
| 112 | ++ end |
| 113 | ++ end |
| 114 | + |
| 115 | + ff_fs_nodes[end, :] .= ff_fs_nodes[1, :] # enforce periodic endpoint |
| 116 | + |
| 117 | +diff --git a/src/Equilibrium/DirectEquilibriumArcLength.jl b/src/Equilibrium/DirectEquilibriumArcLength.jl |
| 118 | +index 32b93a0ac..6774e5601 100644 |
| 119 | +--- a/src/Equilibrium/DirectEquilibriumArcLength.jl |
| 120 | ++++ b/src/Equilibrium/DirectEquilibriumArcLength.jl |
| 121 | +@@ -142,6 +142,6 @@ outboard midplane (Z = zo, R > ro) after a minimum arc-length guard. |
| 122 | + end |
| 123 | + |
| 124 | + # bfield at the starting point carries F and P for the surface-averaged quantities |
| 125 | +- return y_out, bfield |
| 126 | ++ return y_out, bfield, nothing |
| 127 | + end |
| 128 | + |
0 commit comments