diff --git a/.gitignore b/.gitignore index fcb3cc96c..99ce15eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ scratch/ # Local profiling scratch (one-off study scripts, not part of the package) profiling/ +src/Tearing/CriticalResonantField/CRF Dev/ diff --git a/Project.toml b/Project.toml index fe2f324d5..658824c00 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "GeneralizedPerturbedEquilibrium" uuid = "462872dd-e066-4d2e-b993-6468b5239634" license = "MIT" -authors = ["Nikolas Logan ", "Jong-Kyu Park ", "Matthew Pharr ", "Jacob Halpern ", "Rithik Banerjee ", "Jaebeom Cho ", "Daniel Burgess ", "Min-Gu Yoo "] +authors = ["Nikolas Logan ", "Jong-Kyu Park ", "Matthew Pharr ", "Jacob Halpern ", "Rithik Banerjee ", "Jaebeom Cho ", "Daniel Burgess ", "Min-Gu Yoo ","Evan Bursch "] version = "0.1.0" [deps] @@ -69,5 +69,3 @@ Statistics = "1" TOML = "1" Test = "1" julia = "1.11" - - diff --git a/benchmarks/benchmark_against_fortran_run.jl b/benchmarks/benchmark_against_fortran_run.jl index 6116a8058..42618bb47 100644 --- a/benchmarks/benchmark_against_fortran_run.jl +++ b/benchmarks/benchmark_against_fortran_run.jl @@ -109,7 +109,7 @@ end function _find_indexed2_float(text::String, name::String, i::Int, j::Int; default=nothing) m = match(Regex( - "\\b$(name)\\s*\\(\\s*$(i)\\s*,\\s*$(j)\\s*\\)\\s*=\\s*([+-]?[\\d.]+(?:[eE][+-]?\\d+)?)", "i"), + "\\b$(name)\\s*\\(\\s*$(i)\\s*,\\s*$(j)\\s*\\)\\s*=\\s*([+-]?[\\d.]+(?:[eE][+-]?\\d+)?)", "i"), text) isnothing(m) ? default : parse(Float64, m.captures[1]) end @@ -548,8 +548,8 @@ function compare_phix( grid = ForcingTerms.sample_boundary_grid(equil, p.mtheta_coil, p.nzeta_coil; psi=psilim) nobs = p.mtheta_coil * p.nzeta_coil - obs_R = zeros(nobs); - obs_phi = zeros(nobs); + obs_R = zeros(nobs) + obs_phi = zeros(nobs) obs_Z = zeros(nobs) for j in 1:p.nzeta_coil, i in 1:p.mtheta_coil idx = i + (j - 1) * p.mtheta_coil @@ -557,8 +557,8 @@ function compare_phix( obs_phi[idx] = grid.phi_grid[j] + grid.phi_offset[i] obs_Z[idx] = grid.Z[i] end - B_R = zeros(nobs); - B_phi = zeros(nobs); + B_R = zeros(nobs) + B_phi = zeros(nobs) B_Z = zeros(nobs) ForcingTerms.compute_biot_savart_boundary!(B_R, B_phi, B_Z, obs_R, obs_phi, obs_Z, coil_sets) bn = zeros(p.mtheta_coil, p.nzeta_coil) @@ -639,9 +639,9 @@ function build_comparison_table(fort, julia, fortran_dir, bench_dir, nn) push!(lines, "") push!(lines, "--- Mercier Criterion ---") - fdi = fort["di"]; + fdi = fort["di"] fdr = fort["dr"] - jdi = julia["di"]; + jdi = julia["di"] jdr = julia["dr"] if !isempty(jdi) && !isempty(fdi) jdi_on_fg = [_interp1(jq_psi, jdi, p) for p in fq_psi] @@ -857,7 +857,7 @@ function build_comparison_table(fort, julia, fortran_dir, bench_dir, nn) end function _interp1(x::AbstractVector, y::AbstractVector, xi::Real) - n = length(x); + n = length(x) n < 2 && return y[1] xi <= x[1] && return y[1] xi >= x[end] && return y[end] @@ -1451,15 +1451,15 @@ function main(argv=ARGS) println() table_lines = build_comparison_table(fort, julia, fortran_dir, bench_dir, p.nn) for line in table_lines - ; - println(line); + + println(line) end table_path = joinpath(bench_dir, "comparison_table.txt") open(table_path, "w") do io for line in table_lines - ; - println(io, line); + + println(io, line) end end println("\nComparison table saved to: ", abspath(table_path)) diff --git a/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl b/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl index 22e2b1938..e6c9b60cf 100644 --- a/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl +++ b/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl @@ -25,7 +25,7 @@ using NCDatasets using Plots using Printf -const OUTPUT_DIR = joinpath(@__DIR__, "coil_pipeline") +const OUTPUT_DIR = joinpath(@__DIR__, "coil_pipeline") const COIL_DAT_DIR = joinpath(@__DIR__, "..", "src", "ForcingTerms", "coil_geometries") # --------------------------------------------------------------------------- @@ -89,38 +89,38 @@ end function parse_fortran_run(dir::String)::FortranRunParams equil_file = joinpath(dir, "equil.in") - dcon_file = joinpath(dir, "dcon.in") - coil_file = joinpath(dir, "coil.in") + dcon_file = joinpath(dir, "dcon.in") + coil_file = joinpath(dir, "coil.in") isfile(equil_file) || error("equil.in not found in $dir") - isfile(dcon_file) || error("dcon.in not found in $dir") - isfile(coil_file) || error("coil.in not found in $dir") + isfile(dcon_file) || error("dcon.in not found in $dir") + isfile(coil_file) || error("coil.in not found in $dir") - eq_text = _strip_fortran_comments(read(equil_file, String)) - dcon_text = _strip_fortran_comments(read(dcon_file, String)) - coil_text = _strip_fortran_comments(read(coil_file, String)) + eq_text = _strip_fortran_comments(read(equil_file, String)) + dcon_text = _strip_fortran_comments(read(dcon_file, String)) + coil_text = _strip_fortran_comments(read(coil_file, String)) # Equilibrium parameters - eq_type = _find_string(eq_text, "eq_type"; default="efit") - eq_file = _find_string(eq_text, "eq_filename"; default="") - jac_type = _find_string(eq_text, "jac_type"; default="hamada") - psilow = _find_scalar(eq_text, "psilow"; default=1e-4) - psihigh = _find_scalar(eq_text, "psihigh"; default=0.99) - mtheta_eq = _find_int(eq_text, "mtheta"; default=256) + eq_type = _find_string(eq_text, "eq_type"; default="efit") + eq_file = _find_string(eq_text, "eq_filename"; default="") + jac_type = _find_string(eq_text, "jac_type"; default="hamada") + psilow = _find_scalar(eq_text, "psilow"; default=1e-4) + psihigh = _find_scalar(eq_text, "psihigh"; default=0.99) + mtheta_eq = _find_int(eq_text, "mtheta"; default=256) grid_type = _find_string(eq_text, "grid_type"; default="ldp") # Toroidal mode number and m-range expansion - nn = _find_int(dcon_text, "nn"; default=1) - delta_mlow = _find_int(dcon_text, "delta_mlow"; default=8) + nn = _find_int(dcon_text, "nn"; default=1) + delta_mlow = _find_int(dcon_text, "delta_mlow"; default=8) delta_mhigh = _find_int(dcon_text, "delta_mhigh"; default=8) # Coil grid parameters mtheta_coil = _find_int(coil_text, "cmtheta"; default=480) - nzeta_coil = _find_int(coil_text, "cmzeta"; default=40) - machine = _find_string(coil_text, "machine"; default="d3d") - coil_num = _find_int(coil_text, "coil_num"; default=1) + nzeta_coil = _find_int(coil_text, "cmzeta"; default=40) + machine = _find_string(coil_text, "machine"; default="d3d") + coil_num = _find_int(coil_text, "coil_num"; default=1) - coil_names = String[] + coil_names = String[] coil_currents = Vector{Float64}[] for ci in 1:coil_num name = _find_indexed_string(coil_text, "coil_name", ci; default="coil$ci") @@ -135,9 +135,9 @@ function parse_fortran_run(dir::String)::FortranRunParams end return FortranRunParams(eq_type, eq_file, jac_type, psilow, psihigh, mtheta_eq, - grid_type, nn, delta_mlow, delta_mhigh, - mtheta_coil, nzeta_coil, machine, - coil_names, coil_currents) + grid_type, nn, delta_mlow, delta_mhigh, + mtheta_coil, nzeta_coil, machine, + coil_names, coil_currents) end function find_output_nc(dir::String, nn::Int) @@ -177,16 +177,16 @@ end nc_path = find_output_nc(run_dir, p.nn) # Read Fortran output (psilim and Phi_x spectrum) -fortran_m = Int[] -fortran_Phix = Float64[] +fortran_m = Int[] +fortran_Phix = Float64[] fortran_psilim = NaN NCDatasets.Dataset(nc_path, "r") do ds - global fortran_m = Int.(ds["m"][:]) + global fortran_m = Int.(ds["m"][:]) global fortran_psilim = Float64(ds.attrib["psilim"]) phi_x_raw = ds["Phi_x"][:, :] # (mpert, 2); col 1=cos, col 2=sin - global fortran_Phix = sqrt.(phi_x_raw[:, 1].^2 .+ phi_x_raw[:, 2].^2) + global fortran_Phix = sqrt.(phi_x_raw[:, 1] .^ 2 .+ phi_x_raw[:, 2] .^ 2) end -mlow = minimum(fortran_m) +mlow = minimum(fortran_m) mhigh = maximum(fortran_m) @printf " Fortran psilim=%.6f, m=%d:%d\n" fortran_psilim mlow mhigh @@ -196,15 +196,15 @@ mhigh = maximum(fortran_m) println("\n[2/4] Loading equilibrium ...") t_equil = @elapsed begin eq_dict = Dict{String,Any}( - "eq_type" => p.eq_type, - "eq_filename" => p.eq_filename, - "jac_type" => p.jac_type, - "psilow" => p.psilow, - "psihigh" => p.psihigh, - "mtheta" => p.mtheta_equil, - "grid_type" => p.grid_type, + "eq_type" => p.eq_type, + "eq_filename" => p.eq_filename, + "jac_type" => p.jac_type, + "psilow" => p.psilow, + "psihigh" => p.psihigh, + "mtheta" => p.mtheta_equil, + "grid_type" => p.grid_type, "psi_accuracy" => 0.001, - "etol" => 1e-7, + "etol" => 1e-7 ) eq_config = Equilibrium.EquilibriumConfig(eq_dict, run_dir) equil = Equilibrium.setup_equilibrium(eq_config) @@ -223,7 +223,7 @@ for (name, currents) in zip(p.coil_names, p.coil_currents) isfile(dat_path) || error("Coil geometry file not found: $dat_path") raw = read_coil_dat(dat_path) push!(coil_sets, CoilSet(raw.name, raw.ncoil, raw.s, raw.nw, raw.nsec, - raw.x, raw.y, raw.z, Float64.(currents[1:raw.ncoil]))) + raw.x, raw.y, raw.z, Float64.(currents[1:raw.ncoil]))) end for cs in coil_sets @printf " %-10s %d conductors currents: %s A\n" cs.name cs.ncoil string(round.(cs.currents; digits=1)) @@ -231,18 +231,22 @@ end julia_modes = ForcingMode[] bnd_grid = nothing -bn = nothing -t_biot = @elapsed begin +bn = nothing +t_biot = @elapsed begin bnd_grid = sample_boundary_grid(equil, p.mtheta_coil, p.nzeta_coil; psi=fortran_psilim) nobs = p.mtheta_coil * p.nzeta_coil - obs_R = zeros(nobs); obs_phi = zeros(nobs); obs_Z = zeros(nobs) + obs_R = zeros(nobs) + obs_phi = zeros(nobs) + obs_Z = zeros(nobs) for j in 1:p.nzeta_coil, i in 1:p.mtheta_coil idx = i + (j - 1) * p.mtheta_coil - obs_R[idx] = bnd_grid.R[i] + obs_R[idx] = bnd_grid.R[i] obs_phi[idx] = bnd_grid.phi_grid[j] + bnd_grid.phi_offset[i] - obs_Z[idx] = bnd_grid.Z[i] + obs_Z[idx] = bnd_grid.Z[i] end - B_R = zeros(nobs); B_phi = zeros(nobs); B_Z = zeros(nobs) + B_R = zeros(nobs) + B_phi = zeros(nobs) + B_Z = zeros(nobs) compute_biot_savart_boundary!(B_R, B_phi, B_Z, obs_R, obs_phi, obs_Z, coil_sets) bn = zeros(p.mtheta_coil, p.nzeta_coil) project_normal_flux!(bn, B_R, B_Z, bnd_grid) @@ -250,7 +254,7 @@ t_biot = @elapsed begin end @printf " Done in %.1f s, max|Phi_x| = %.3e T·m²\n" t_biot maximum(abs, bn) -julia_m = [md.m for md in sort(julia_modes; by=md -> md.m)] +julia_m = [md.m for md in sort(julia_modes; by=md -> md.m)] julia_amps = abs.([md.amplitude for md in sort(julia_modes; by=md -> md.m)]) # Print comparison table @@ -259,7 +263,7 @@ println(" " * "-"^55) for (jm, ja) in zip(julia_m, julia_amps) k = findfirst(==(jm), fortran_m) if !isnothing(k) - fp = fortran_Phix[k] + fp = fortran_Phix[k] ratio = ja > 1e-40 ? fp / ja : NaN @printf " %3d %12.4e %12.4e %.4f\n" jm ja fp ratio end @@ -272,7 +276,7 @@ println(" Expected ratio ≈ 1.000 for all modes.") println("\n[4/4] Generating figure ...") function step_series(m_vals, amps) - m_ext = [m_vals[1] - 1; m_vals; m_vals[end] + 1] + m_ext = [m_vals[1] - 1; m_vals; m_vals[end] + 1] amp_ext = [0.0; amps; 0.0] return m_ext, amp_ext end @@ -286,19 +290,20 @@ p2 = plot_bn_contour(bn, p.mtheta_coil, p.nzeta_coil; n=p.nn) title!(p2, "Normal flux Φₓ(θ, ζ) at ψ=$(round(fortran_psilim; digits=4))") # Panel 3: Julia vs Fortran Phi_x spectrum -jm_ext, ja_ext = step_series(julia_m, julia_amps) -fm_ext, fa_ext = step_series(fortran_m, fortran_Phix) +jm_ext, ja_ext = step_series(julia_m, julia_amps) +fm_ext, fa_ext = step_series(fortran_m, fortran_Phix) p3 = plot(; xlabel="Poloidal mode m", ylabel="|Phi_x| [T·m²]", - title="Spectrum: Julia vs Fortran (n=$(p.nn))", - legend=:topright) + title="Spectrum: Julia vs Fortran (n=$(p.nn))", + legend=:topright) plot!(p3, jm_ext, ja_ext; seriestype=:steppre, lw=2, color=:blue, - label="Julia ψ=$(round(fortran_psilim; digits=4))") + label="Julia ψ=$(round(fortran_psilim; digits=4))") plot!(p3, fm_ext, fa_ext; seriestype=:steppre, lw=2, color=:orange, - linestyle=:dash, label="Fortran Phi_x") -ylims!(p3, (0, Inf)); xlims!(p3, mlow - 2, mhigh + 2) + linestyle=:dash, label="Fortran Phi_x") +ylims!(p3, (0, Inf)); +xlims!(p3, mlow - 2, mhigh + 2) # Panel 4: Phi_x / Julia ratio -ratio_m = Int[] +ratio_m = Int[] ratio_vals = Float64[] for (jm, ja) in zip(julia_m, julia_amps) k = findfirst(==(jm), fortran_m) @@ -308,8 +313,8 @@ for (jm, ja) in zip(julia_m, julia_amps) end end p4 = plot(; xlabel="Poloidal mode m", ylabel="Phi_x / Julia", - title="Ratio (flat at 1.0 → correct)", - legend=:topright) + title="Ratio (flat at 1.0 → correct)", + legend=:topright) if !isempty(ratio_m) scatter!(p4, ratio_m, ratio_vals; color=:blue, label="ratio", markersize=5) hline!(p4, [1.0]; color=:red, linestyle=:dash, label="expected = 1.0") @@ -318,8 +323,8 @@ xlims!(p4, mlow - 2, mhigh + 2) run_label = basename(run_dir) fig = plot(p1, p2, p3, p4; - layout=(2, 2), size=(1400, 900), - plot_title="$run_label | $(p.machine) n=$(p.nn) psilim=$(round(fortran_psilim; digits=4))") + layout=(2, 2), size=(1400, 900), + plot_title="$run_label | $(p.machine) n=$(p.nn) psilim=$(round(fortran_psilim; digits=4))") out_png = joinpath(OUTPUT_DIR, "coil_pipeline_$(run_label)_n$(p.nn).png") savefig(fig, out_png) diff --git a/benchmarks/benchmark_delta_prime_methods.jl b/benchmarks/benchmark_delta_prime_methods.jl index 917e7c9c5..d005eea7e 100644 --- a/benchmarks/benchmark_delta_prime_methods.jl +++ b/benchmarks/benchmark_delta_prime_methods.jl @@ -32,8 +32,8 @@ function setup_and_run_solovev() intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) FFS.sing_lim!(intr, ctrl, equil) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 FFS.sing_find!(intr, equil) intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow diff --git a/benchmarks/benchmark_diiid_ideal_ntv_torque.jl b/benchmarks/benchmark_diiid_ideal_ntv_torque.jl index 7da279b91..92b788afc 100644 --- a/benchmarks/benchmark_diiid_ideal_ntv_torque.jl +++ b/benchmarks/benchmark_diiid_ideal_ntv_torque.jl @@ -35,7 +35,9 @@ const KF = GPE.KineticForces const Eq = GPE.Equilibrium const PE = GPE.PerturbedEquilibrium -"Fortran GPEC DIII-D ideal example run directory, from the environment (no on-disk default)." +""" +Fortran GPEC DIII-D ideal example run directory, from the environment (no on-disk default). +""" default_fortran_dir() = get(() -> error("Set GPEC_FORTRAN_DIIID, or pass the Fortran run directory as the first argument"), ENV, "GPEC_FORTRAN_DIIID") diff --git a/benchmarks/benchmark_diiid_kinetic_stability.jl b/benchmarks/benchmark_diiid_kinetic_stability.jl index 9ae37b6b3..91ab5b172 100644 --- a/benchmarks/benchmark_diiid_kinetic_stability.jl +++ b/benchmarks/benchmark_diiid_kinetic_stability.jl @@ -36,7 +36,9 @@ using GeneralizedPerturbedEquilibrium const GPE = GeneralizedPerturbedEquilibrium const AnalysisFFS = GPE.Analysis.ForceFreeStates -"Fortran GPEC DIII-D kinetic example run directory, from the environment (no on-disk default)." +""" +Fortran GPEC DIII-D kinetic example run directory, from the environment (no on-disk default). +""" default_fortran_dir() = get(() -> error("Set GPEC_FORTRAN_DIIID_DCON, or pass the Fortran run directory as the first argument"), ENV, "GPEC_FORTRAN_DIIID_DCON") diff --git a/benchmarks/benchmark_riccati_der.jl b/benchmarks/benchmark_riccati_der.jl index 6b23c0a81..df083891f 100644 --- a/benchmarks/benchmark_riccati_der.jl +++ b/benchmarks/benchmark_riccati_der.jl @@ -33,8 +33,8 @@ function setup_solovev() intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) FFS.sing_lim!(intr, ctrl, equil) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 FFS.sing_find!(intr, equil) intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow diff --git a/benchmarks/benchmark_solovev_kinetic_stability.jl b/benchmarks/benchmark_solovev_kinetic_stability.jl index c942a20c6..43b3ba25d 100644 --- a/benchmarks/benchmark_solovev_kinetic_stability.jl +++ b/benchmarks/benchmark_solovev_kinetic_stability.jl @@ -43,10 +43,10 @@ using HDF5 using GeneralizedPerturbedEquilibrium const GPE = GeneralizedPerturbedEquilibrium -const REPO = normpath(joinpath(@__DIR__, "..")) -const JULIA_FIXTURE = joinpath(REPO, "test", "test_data", "regression_solovev_kinetic_calculated") -const FORTRAN_DCON = get(ENV, "GPEC_FORTRAN_DCON", "") -const FORTRAN_SOLDIR = get(ENV, "GPEC_FORTRAN_SOLDIR", "") +const REPO = normpath(joinpath(@__DIR__, "..")) +const JULIA_FIXTURE = joinpath(REPO, "test", "test_data", "regression_solovev_kinetic_calculated") +const FORTRAN_DCON = get(ENV, "GPEC_FORTRAN_DCON", "") +const FORTRAN_SOLDIR = get(ENV, "GPEC_FORTRAN_SOLDIR", "") _p(args...) = (println(stderr, args...); flush(stderr)) @@ -90,35 +90,35 @@ function build_matched_fortran_deck(workdir::String) end gpec = TOML.parsefile(joinpath(JULIA_FIXTURE, "gpec.toml")) - eq = gpec["Equilibrium"] - ffs = gpec["ForceFreeStates"] - kf = get(gpec, "KineticForces", Dict{String,Any}()) + eq = gpec["Equilibrium"] + ffs = gpec["ForceFreeStates"] + kf = get(gpec, "KineticForces", Dict{String,Any}()) # equil.in — grid + coordinate settings to match the Julia [Equilibrium] block. eqin = joinpath(workdir, "equil.in") - replace_namelist_value!(eqin, "psilow", string(eq["psilow"])) + replace_namelist_value!(eqin, "psilow", string(eq["psilow"])) replace_namelist_value!(eqin, "psihigh", string(eq["psihigh"])) - replace_namelist_value!(eqin, "mpsi", string(eq["mpsi"])) - replace_namelist_value!(eqin, "mtheta", string(eq["mtheta"])) + replace_namelist_value!(eqin, "mpsi", string(eq["mpsi"])) + replace_namelist_value!(eqin, "mtheta", string(eq["mtheta"])) # dcon.in — kinetic flags already match; align mode band, edge truncation, n. dconin = joinpath(workdir, "dcon.in") - replace_namelist_value!(dconin, "nn", string(ffs["nn_low"])) - replace_namelist_value!(dconin, "delta_mlow", string(get(ffs, "delta_mlow", 0))) + replace_namelist_value!(dconin, "nn", string(ffs["nn_low"])) + replace_namelist_value!(dconin, "delta_mlow", string(get(ffs, "delta_mlow", 0))) replace_namelist_value!(dconin, "delta_mhigh", string(get(ffs, "delta_mhigh", 0))) - replace_namelist_value!(dconin, "qlow", string(ffs["qlow"])) - replace_namelist_value!(dconin, "qhigh", string(ffs["qhigh"])) + replace_namelist_value!(dconin, "qlow", string(ffs["qlow"])) + replace_namelist_value!(dconin, "qhigh", string(ffs["qhigh"])) replace_namelist_value!(dconin, "singfac_min", string(ffs["singfac_min"])) # Julia set_psilim_via_dmlim defaults true (sas_flag) with dmlim 0.2. replace_namelist_value!(dconin, "sas_flag", "t") - replace_namelist_value!(dconin, "dmlim", "0.2") + replace_namelist_value!(dconin, "dmlim", "0.2") # pentrc.in — bounce harmonics + species; defaults mirror KineticForcesControl. pentin = joinpath(workdir, "pentrc.in") - replace_namelist_value!(pentin, "nl", string(get(kf, "nl", 1))) + replace_namelist_value!(pentin, "nl", string(get(kf, "nl", 1))) replace_namelist_value!(pentin, "nutype", "\"" * string(get(kf, "nutype", "harmonic")) * "\"") replace_namelist_value!(pentin, "f0type", "\"" * string(get(kf, "f0type", "maxwellian")) * "\"") - replace_namelist_value!(pentin, "nufac", string(get(kf, "nufac", 1))) + replace_namelist_value!(pentin, "nufac", string(get(kf, "nufac", 1))) replace_namelist_value!(pentin, "kinetic_file", "\"kinetic.txt\"") # kinetic.txt — Fortran readtable wants a title line then numeric rows; the diff --git a/benchmarks/benchmark_threads.jl b/benchmarks/benchmark_threads.jl index 96e37236a..1abd8a3d4 100644 --- a/benchmarks/benchmark_threads.jl +++ b/benchmarks/benchmark_threads.jl @@ -21,8 +21,8 @@ function run_ffs(ex; integrator) intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow diff --git a/benchmarks/compare_gal_vs_el.jl b/benchmarks/compare_gal_vs_el.jl index 98c8e82ef..5b695fb0e 100644 --- a/benchmarks/compare_gal_vs_el.jl +++ b/benchmarks/compare_gal_vs_el.jl @@ -43,14 +43,14 @@ xiE = reduce(hcat, (u1[:, :, ip] * cEL for ip in 1:size(u1, 3))) # (mpert, nE) psiGk = psiG xiG = reduce(hcat, (gxi[:, :, ip] * w for ip in eachindex(psiG))) # (mpert, nGk) -ms = mlow .+ (0:mpert-1) +ms = mlow .+ (0:(mpert-1)) peak = [maximum(abs, @view xiE[i, :]) for i in 1:mpert] order = sortperm(peak; rev=true) ndom = min(5, mpert) # quantitative bulk agreement on the dominant harmonic (linear-interp EL onto gal grid, off rationals/edge) -lininterp(xq, x, y) = (j = clamp(searchsortedlast(x, xq), 1, length(x) - 1); -t = (xq - x[j]) / (x[j+1] - x[j]); y[j] * (1 - t) + y[j+1] * t) +lininterp(xq, x, y) = (j=clamp(searchsortedlast(x, xq), 1, length(x) - 1); + t=(xq - x[j]) / (x[j+1] - x[j]); y[j] * (1 - t) + y[j+1] * t) idom = order[1] inbulk(p) = 0.1 <= p <= 0.92 && all(abs(p - ps) > 8e-3 for ps in sing_psi) sel = [ip for ip in eachindex(psiGk) if inbulk(psiGk[ip])] diff --git a/benchmarks/equil_method_comparison.jl b/benchmarks/equil_method_comparison.jl index 450423d6a..151dadd72 100644 --- a/benchmarks/equil_method_comparison.jl +++ b/benchmarks/equil_method_comparison.jl @@ -21,7 +21,7 @@ using GeneralizedPerturbedEquilibrium.Equilibrium using TOML, Printf, Statistics example_path = length(ARGS) > 0 ? ARGS[1] : joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") -config_path = joinpath(example_path, "gpec.toml") +config_path = joinpath(example_path, "gpec.toml") psihigh_override = length(ARGS) > 1 ? parse(Float64, ARGS[2]) : nothing println("=" ^ 65) @@ -58,10 +58,10 @@ for method in methods success = false end results[method] = Dict( - "success" => success, - "pe" => pe, - "runtime" => (t1 + t2) / 2.0, - "config" => cfg + "success" => success, + "pe" => pe, + "runtime" => (t1 + t2) / 2.0, + "config" => cfg ) success && @printf(" Runtime (avg 2 warm): %.3f s\n", (t1 + t2) / 2.0) end @@ -105,7 +105,7 @@ for method in methods results[method]["success"] || continue Δq = abs.([q_vals[method][i] - efit_q_interp(psi_nodes[i]) for i in 1:length(psi_nodes)]) @printf(" |q_%s - q_efit| (ψ<0.95): max=%.2e rms=%.2e\n", - method, maximum(Δq[mask_mid]), sqrt(mean(Δq[mask_mid].^2))) + method, maximum(Δq[mask_mid]), sqrt(mean(Δq[mask_mid] .^ 2))) end # ─── Roundtrip error ───────────────────────────────────────────────────────── @@ -123,15 +123,15 @@ for method in methods psio = pe.psio psi_xs = pe.rzphi_xs psi_ys = pe.rzphi_ys - mpsi = length(psi_xs) - 1 + mpsi = length(psi_xs) - 1 mtheta = length(psi_ys) - 1 errors = Float64[] - for ipsi in 1:5:mpsi+1 + for ipsi in 1:5:(mpsi+1) ψ_target = psi_xs[ipsi] - for itheta in 1:8:mtheta+1 + for itheta in 1:8:(mtheta+1) θ = psi_ys[itheta] - r2 = pe.rzphi_rsquared((ψ_target, θ)) + r2 = pe.rzphi_rsquared((ψ_target, θ)) off = pe.rzphi_offset((ψ_target, θ)) rfac = sqrt(max(r2, 0.0)) η = 2π * (θ + off) diff --git a/benchmarks/equil_numerical_params.jl b/benchmarks/equil_numerical_params.jl index f80f0f64c..f37376745 100644 --- a/benchmarks/equil_numerical_params.jl +++ b/benchmarks/equil_numerical_params.jl @@ -21,26 +21,26 @@ using GeneralizedPerturbedEquilibrium.Equilibrium using TOML, Printf, Statistics example_path = length(ARGS) > 0 ? ARGS[1] : joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") -config_path = joinpath(example_path, "gpec.toml") +config_path = joinpath(example_path, "gpec.toml") function make_config(path, eq_type, mpsi, mtheta) raw = TOML.parsefile(path) raw["Equilibrium"]["eq_type"] = eq_type - raw["Equilibrium"]["mpsi"] = mpsi - raw["Equilibrium"]["mtheta"] = mtheta + raw["Equilibrium"]["mpsi"] = mpsi + raw["Equilibrium"]["mtheta"] = mtheta return Equilibrium.EquilibriumConfig(raw["Equilibrium"], dirname(path)) end function roundtrip_error(pe, raw_profile) psi_xs = pe.rzphi_xs - psio = pe.psio + psio = pe.psio mtheta = length(pe.rzphi_ys) - 1 errors = Float64[] for ipsi in 1:4:length(psi_xs) ψ = psi_xs[ipsi] - for itheta in 1:8:mtheta+1 + for itheta in 1:8:(mtheta+1) θ = pe.rzphi_ys[itheta] - r2 = pe.rzphi_rsquared((ψ, θ)) + r2 = pe.rzphi_rsquared((ψ, θ)) off = pe.rzphi_offset((ψ, θ)) rfac = sqrt(max(r2, 0.0)) η = 2π * (θ + off) diff --git a/benchmarks/equil_psihigh_scan.jl b/benchmarks/equil_psihigh_scan.jl index 56f6e9ec6..99788b281 100644 --- a/benchmarks/equil_psihigh_scan.jl +++ b/benchmarks/equil_psihigh_scan.jl @@ -29,7 +29,7 @@ using GeneralizedPerturbedEquilibrium.Equilibrium using TOML, Printf, Statistics, HDF5 example_path = length(ARGS) > 0 ? ARGS[1] : joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") -config_path = joinpath(example_path, "gpec.toml") +config_path = joinpath(example_path, "gpec.toml") psihigh_values = [0.980, 0.985, 0.990, 0.993, 0.995, 0.996, 0.997, 0.998, 0.999, 0.9995, 0.9999, 1.0] methods = ["efit", "efit_arclength", "efit_by_inversion"] @@ -121,9 +121,9 @@ for method in methods for ipsi in 1:length(psi_xs) psi_xs[ipsi] < 0.90 && continue ψ = psi_xs[ipsi] - for itheta in 1:8:mtheta+1 + for itheta in 1:8:(mtheta+1) θ = pe.rzphi_ys[itheta] - r2 = pe.rzphi_rsquared((ψ, θ)) + r2 = pe.rzphi_rsquared((ψ, θ)) off = pe.rzphi_offset((ψ, θ)) rfac = sqrt(max(r2, 0.0)) η = 2π * (θ + off) @@ -161,13 +161,16 @@ for method in methods isnan(q_at_psihigh) ? -1.0 : q_at_psihigh, et1_str) - push!(rows, ( - method=method, psihigh=psihigh, success=success, runtime=runtime, - roundtrip_max_edge=roundtrip_max_edge, - q_mono_violations_edge=q_mono_violations_edge, - q_at_psihigh=q_at_psihigh, q_edge_slope=q_edge_slope, - et1=et1, error_msg=err_msg - )) + push!( + rows, + ( + method=method, psihigh=psihigh, success=success, runtime=runtime, + roundtrip_max_edge=roundtrip_max_edge, + q_mono_violations_edge=q_mono_violations_edge, + q_at_psihigh=q_at_psihigh, q_edge_slope=q_edge_slope, + et1=et1, error_msg=err_msg + ) + ) end end @@ -198,9 +201,11 @@ println("et[1] vs psihigh (free-boundary stability eigenvalue):") println(" (positive = stable, negative = unstable)") @printf(" %-22s %s\n", "psihigh", join([@sprintf("%-22s", m) for m in methods])) for psihigh in psihigh_values - vals = [let r = findfirst(x -> x.method == m && x.psihigh == psihigh, rows) - r === nothing || isnan(rows[r].et1) ? " -" : @sprintf("%+.4f", rows[r].et1) - end for m in methods] + vals = [ + let r = findfirst(x -> x.method == m && x.psihigh == psihigh, rows) + r === nothing || isnan(rows[r].et1) ? " -" : @sprintf("%+.4f", rows[r].et1) + end for m in methods + ] @printf(" %-22.4f %s\n", psihigh, join([@sprintf("%-22s", v) for v in vals])) end println() diff --git a/benchmarks/equil_psilow_scan.jl b/benchmarks/equil_psilow_scan.jl index ff5902b7b..1129d5ee1 100644 --- a/benchmarks/equil_psilow_scan.jl +++ b/benchmarks/equil_psilow_scan.jl @@ -19,7 +19,7 @@ using GeneralizedPerturbedEquilibrium.Equilibrium using TOML, Printf, Statistics example_path = length(ARGS) > 0 ? ARGS[1] : joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") -config_path = joinpath(example_path, "gpec.toml") +config_path = joinpath(example_path, "gpec.toml") psilow_values = [1e-1, 5e-2, 1e-2, 5e-3, 1e-3, 5e-4, 1e-4] methods = ["efit", "efit_arclength", "efit_by_inversion"] @@ -82,7 +82,7 @@ for method in methods raw_profile = Equilibrium.read_efit(cfg) psi_xs = pe.rzphi_xs mtheta = length(pe.rzphi_ys) - 1 - psio = pe.psio + psio = pe.psio # q0 extrapolated to axis (linear from innermost two grid points) q1 = pe.profiles.q_spline.y[1] @@ -94,9 +94,9 @@ for method in methods for ipsi in 1:length(psi_xs) psi_xs[ipsi] > 0.10 && break ψ = psi_xs[ipsi] - for itheta in 1:8:mtheta+1 + for itheta in 1:8:(mtheta+1) θ = pe.rzphi_ys[itheta] - r2 = pe.rzphi_rsquared((ψ, θ)) + r2 = pe.rzphi_rsquared((ψ, θ)) off = pe.rzphi_offset((ψ, θ)) rfac = sqrt(max(r2, 0.0)) η = 2π * (θ + off) diff --git a/benchmarks/equil_spline_comparison.jl b/benchmarks/equil_spline_comparison.jl index 4cf5337e2..a0ea0bdc0 100644 --- a/benchmarks/equil_spline_comparison.jl +++ b/benchmarks/equil_spline_comparison.jl @@ -27,9 +27,9 @@ using TOML, Printf, Statistics, Plots # ─── CLI arguments ───────────────────────────────────────────────────────────── example_path = length(ARGS) > 0 ? ARGS[1] : - joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") + joinpath(@__DIR__, "../examples/DIIID-like_ideal_example") psihigh_arg = length(ARGS) > 1 ? parse(Float64, ARGS[2]) : 0.997 -config_path = joinpath(example_path, "gpec.toml") +config_path = joinpath(example_path, "gpec.toml") println("=" ^ 70) println("Equilibrium Spline Comparison: efit / efit_arclength / efit_by_inversion") @@ -46,38 +46,38 @@ println("Output : $outdir\n") # ─── Config factory ──────────────────────────────────────────────────────────── function make_config(path::String, eq_type::String, psihigh::Float64) raw = TOML.parsefile(path) - raw["Equilibrium"]["eq_type"] = eq_type + raw["Equilibrium"]["eq_type"] = eq_type raw["Equilibrium"]["psihigh"] = psihigh return Equilibrium.EquilibriumConfig(raw["Equilibrium"], dirname(path)) end # ─── Load all three equilibria ───────────────────────────────────────────────── # efit is the reference; efit_arclength and efit_by_inversion are compared to it. -ref_method = "efit" -compare_methods = ["efit_arclength", "efit_by_inversion"] -all_methods = vcat(ref_method, compare_methods) +ref_method = "efit" +compare_methods = ["efit_arclength", "efit_by_inversion"] +all_methods = vcat(ref_method, compare_methods) method_color = Dict( - "efit" => :blue, - "efit_arclength" => :darkorange, - "efit_by_inversion" => :red, + "efit" => :blue, + "efit_arclength" => :darkorange, + "efit_by_inversion" => :red ) method_style = Dict( - "efit" => :solid, - "efit_arclength" => :dash, - "efit_by_inversion" => :dot, + "efit" => :solid, + "efit_arclength" => :dash, + "efit_by_inversion" => :dot ) method_label = Dict( - "efit" => "efit", - "efit_arclength" => "efit_arclength", - "efit_by_inversion" => "efit_by_inv", + "efit" => "efit", + "efit_arclength" => "efit_arclength", + "efit_by_inversion" => "efit_by_inv" ) diff_color = Dict( - "efit_arclength" => :darkorange, - "efit_by_inversion" => :red, + "efit_arclength" => :darkorange, + "efit_by_inversion" => :red ) -pes = Dict{String, Any}() +pes = Dict{String,Any}() for m in all_methods println("--- Running: $m ---") cfg = make_config(config_path, m, psihigh_arg) @@ -94,22 +94,26 @@ isempty(failed) || println("Skipping failed methods in comparisons: $(join(faile haskey(pes, ref_method) || error("Reference method '$ref_method' failed — cannot continue.") pe_ref = pes[ref_method] -all_methods = filter(m -> haskey(pes, m), all_methods) +all_methods = filter(m -> haskey(pes, m), all_methods) compare_methods_active = filter(m -> haskey(pes, m), compare_methods) # ─── Helpers ─────────────────────────────────────────────────────────────────── -"Convert (ψ, θ) straight-field-line coordinates → (R, Z) physical coordinates." +""" +Convert (ψ, θ) straight-field-line coordinates → (R, Z) physical coordinates. +""" function psi_theta_to_RZ(pe, psi, theta) - r2 = pe.rzphi_rsquared((psi, theta)) - off = pe.rzphi_offset((psi, theta)) + r2 = pe.rzphi_rsquared((psi, theta)) + off = pe.rzphi_offset((psi, theta)) rfac = sqrt(max(r2, 0.0)) - η = 2π * (theta + off) - R = pe.ro + rfac * cos(η) - Z = pe.zo + rfac * sin(η) + η = 2π * (theta + off) + R = pe.ro + rfac * cos(η) + Z = pe.zo + rfac * sin(η) return R, Z end -"Compute the full (R, Z) trace of each flux surface in psi_vals." +""" +Compute the full (R, Z) trace of each flux surface in psi_vals. +""" function flux_surface_RZ(pe, psi_vals, theta_range) R_all = [Float64[] for _ in 1:length(psi_vals)] Z_all = [Float64[] for _ in 1:length(psi_vals)] @@ -130,7 +134,7 @@ end psi_lo = pe_ref.rzphi_xs[1] psi_hi = pe_ref.rzphi_xs[end] mpsi_eval = 8 * (length(pe_ref.rzphi_xs) - 1) -psi_full = psi_lo .+ (psi_hi - psi_lo) .* sin.(range(0.0, 1.0; length=mpsi_eval+1) .* (π/2)).^2 +psi_full = psi_lo .+ (psi_hi - psi_lo) .* sin.(range(0.0, 1.0; length=mpsi_eval+1) .* (π/2)) .^ 2 mask_core = psi_full .< 0.10 mask_edge = psi_full .> 0.98 @@ -148,16 +152,16 @@ println("1. 1D Profile Comparisons") println("=" ^ 70) profile_specs = [ - ("F (2π·R·Bₜ)", pe -> pe.profiles.F_spline), - ("μ₀·P", pe -> pe.profiles.P_spline), - ("dV/dψ", pe -> pe.profiles.dVdpsi_spline), - ("q", pe -> pe.profiles.q_spline), + ("F (2π·R·Bₜ)", pe -> pe.profiles.F_spline), + ("μ₀·P", pe -> pe.profiles.P_spline), + ("dV/dψ", pe -> pe.profiles.dVdpsi_spline), + ("q", pe -> pe.profiles.q_spline) ] profile_fnames = ["profile_F", "profile_P", "profile_dVdpsi", "profile_q"] for (idx, (pname, spl_getter)) in enumerate(profile_specs) spl_ref = spl_getter(pe_ref) - y_ref = [spl_ref(ψ) for ψ in psi_full] + y_ref = [spl_ref(ψ) for ψ in psi_full] p_full = plot(psi_full, y_ref; lw=2, color=method_color[ref_method], ls=method_style[ref_method], label=method_label[ref_method], @@ -187,8 +191,8 @@ for (idx, (pname, spl_getter)) in enumerate(profile_specs) for m in compare_methods_active spl_m = spl_getter(pes[m]) - y_m = [spl_m(ψ) for ψ in psi_full] - Δy = y_ref .- y_m + y_m = [spl_m(ψ) for ψ in psi_full] + Δy = y_ref .- y_m plot!(p_full, psi_full, y_m; lw=1.5, color=method_color[m], ls=method_style[m], label=method_label[m]) @@ -227,66 +231,66 @@ z_global_grid = Float64[] r_zoom_grid_vis = Float64[] z_zoom_grid_vis = Float64[] if haskey(pes, "efit_by_inversion") -let - global r_global_grid, z_global_grid, r_zoom_grid_vis, z_zoom_grid_vis - cfg_inv = make_config(config_path, "efit_by_inversion", psihigh_arg) - raw_inv = Equilibrium.read_efit(cfg_inv) - ro_g, zo_g = pes["efit_by_inversion"].ro, pes["efit_by_inversion"].zo - psio_g = raw_inv.psio - psilow_g = cfg_inv.psilow - topology_g = Equilibrium.classify_topology(raw_inv, psio_g) - - ψ_RR = abs(raw_inv.psi_in((ro_g, zo_g); deriv=DerivOp(2, 0))) - ψ_ZZ = abs(raw_inv.psi_in((ro_g, zo_g); deriv=DerivOp(0, 2))) - a_low_g = min(sqrt(2 * psilow_g * psio_g / ψ_RR), sqrt(2 * psilow_g * psio_g / ψ_ZZ)) - - nw_g = length(raw_inv.psi_in_xs) - nh_g = length(raw_inv.psi_in_ys) - β_r_g, β_z_g = 2.0, 2.0 - refine_g = 5 - - nr_g = max(4, round(Int, refine_g * nw_g)) - nz_g = max(4, round(Int, refine_g * nh_g)) - r_g = Equilibrium.make_stretched_r_grid(raw_inv.rmin, raw_inv.rmax, ro_g, nr_g, β_r_g) - z_g = Equilibrium.make_stretched_z_grid(raw_inv.zmin, raw_inv.zmax, zo_g, nz_g, topology_g, β_z_g) - - iro_g = clamp(searchsortedfirst(r_g, ro_g), 2, length(r_g)) - izo_g = clamp(searchsortedfirst(z_g, zo_g), 2, length(z_g)) - dR_ax = r_g[iro_g] - r_g[iro_g - 1] - dZ_ax = z_g[izo_g] - z_g[izo_g - 1] - sc_r = max(1, ceil(Int, dR_ax / (0.2 * a_low_g))) - sc_z = max(1, ceil(Int, dZ_ax / (0.2 * a_low_g))) - if sc_r > 1 - nr_g = (nr_g - 1) * sc_r + 1 - r_g = Equilibrium.make_stretched_r_grid(raw_inv.rmin, raw_inv.rmax, ro_g, nr_g, β_r_g) + let + global r_global_grid, z_global_grid, r_zoom_grid_vis, z_zoom_grid_vis + cfg_inv = make_config(config_path, "efit_by_inversion", psihigh_arg) + raw_inv = Equilibrium.read_efit(cfg_inv) + ro_g, zo_g = pes["efit_by_inversion"].ro, pes["efit_by_inversion"].zo + psio_g = raw_inv.psio + psilow_g = cfg_inv.psilow + topology_g = Equilibrium.classify_topology(raw_inv, psio_g) + + ψ_RR = abs(raw_inv.psi_in((ro_g, zo_g); deriv=DerivOp(2, 0))) + ψ_ZZ = abs(raw_inv.psi_in((ro_g, zo_g); deriv=DerivOp(0, 2))) + a_low_g = min(sqrt(2 * psilow_g * psio_g / ψ_RR), sqrt(2 * psilow_g * psio_g / ψ_ZZ)) + + nw_g = length(raw_inv.psi_in_xs) + nh_g = length(raw_inv.psi_in_ys) + β_r_g, β_z_g = 2.0, 2.0 + refine_g = 5 + + nr_g = max(4, round(Int, refine_g * nw_g)) + nz_g = max(4, round(Int, refine_g * nh_g)) + r_g = Equilibrium.make_stretched_r_grid(raw_inv.rmin, raw_inv.rmax, ro_g, nr_g, β_r_g) + z_g = Equilibrium.make_stretched_z_grid(raw_inv.zmin, raw_inv.zmax, zo_g, nz_g, topology_g, β_z_g) + iro_g = clamp(searchsortedfirst(r_g, ro_g), 2, length(r_g)) - dR_ax = r_g[iro_g] - r_g[iro_g - 1] - end - if sc_z > 1 - nz_g = (nz_g - 1) * sc_z + 1 - z_g = Equilibrium.make_stretched_z_grid(raw_inv.zmin, raw_inv.zmax, zo_g, nz_g, topology_g, β_z_g) izo_g = clamp(searchsortedfirst(z_g, zo_g), 2, length(z_g)) - dZ_ax = z_g[izo_g] - z_g[izo_g - 1] + dR_ax = r_g[iro_g] - r_g[iro_g-1] + dZ_ax = z_g[izo_g] - z_g[izo_g-1] + sc_r = max(1, ceil(Int, dR_ax / (0.2 * a_low_g))) + sc_z = max(1, ceil(Int, dZ_ax / (0.2 * a_low_g))) + if sc_r > 1 + nr_g = (nr_g - 1) * sc_r + 1 + r_g = Equilibrium.make_stretched_r_grid(raw_inv.rmin, raw_inv.rmax, ro_g, nr_g, β_r_g) + iro_g = clamp(searchsortedfirst(r_g, ro_g), 2, length(r_g)) + dR_ax = r_g[iro_g] - r_g[iro_g-1] + end + if sc_z > 1 + nz_g = (nz_g - 1) * sc_z + 1 + z_g = Equilibrium.make_stretched_z_grid(raw_inv.zmin, raw_inv.zmax, zo_g, nz_g, topology_g, β_z_g) + izo_g = clamp(searchsortedfirst(z_g, zo_g), 2, length(z_g)) + dZ_ax = z_g[izo_g] - z_g[izo_g-1] + end + r_global_grid = r_g + z_global_grid = z_g + + d_max_g = max(dR_ax, dZ_ax) + threshold_g = (10 * d_max_g)^2 * max(ψ_RR, ψ_ZZ) / (2 * psio_g) + psi_zoom_max = 9 * threshold_g + a_zoom_r = sqrt(2 * psi_zoom_max * psio_g / ψ_RR) * 1.2 + a_zoom_z = sqrt(2 * psi_zoom_max * psio_g / ψ_ZZ) * 1.2 + sinh_β = sinh(β_r_g) + nr_z = max(200, ceil(Int, 8 * β_r_g * a_zoom_r * 10 / (sinh_β * a_low_g))) + nz_z = max(200, ceil(Int, 8 * β_r_g * a_zoom_z * 10 / (sinh_β * a_low_g))) + r_zoom_grid_vis = Equilibrium.make_stretched_r_grid( + max(raw_inv.rmin, ro_g - a_zoom_r), min(raw_inv.rmax, ro_g + a_zoom_r), + ro_g, nr_z, β_r_g) + z_zoom_grid_vis = Equilibrium.make_stretched_r_grid( + max(raw_inv.zmin, zo_g - a_zoom_z), min(raw_inv.zmax, zo_g + a_zoom_z), + zo_g, nz_z, β_r_g) + println(" Grid vis: global $(nr_g)×$(nz_g), zoom $(nr_z)×$(nz_z)") end - r_global_grid = r_g - z_global_grid = z_g - - d_max_g = max(dR_ax, dZ_ax) - threshold_g = (10 * d_max_g)^2 * max(ψ_RR, ψ_ZZ) / (2 * psio_g) - psi_zoom_max = 9 * threshold_g - a_zoom_r = sqrt(2 * psi_zoom_max * psio_g / ψ_RR) * 1.2 - a_zoom_z = sqrt(2 * psi_zoom_max * psio_g / ψ_ZZ) * 1.2 - sinh_β = sinh(β_r_g) - nr_z = max(200, ceil(Int, 8 * β_r_g * a_zoom_r * 10 / (sinh_β * a_low_g))) - nz_z = max(200, ceil(Int, 8 * β_r_g * a_zoom_z * 10 / (sinh_β * a_low_g))) - r_zoom_grid_vis = Equilibrium.make_stretched_r_grid( - max(raw_inv.rmin, ro_g - a_zoom_r), min(raw_inv.rmax, ro_g + a_zoom_r), - ro_g, nr_z, β_r_g) - z_zoom_grid_vis = Equilibrium.make_stretched_r_grid( - max(raw_inv.zmin, zo_g - a_zoom_z), min(raw_inv.zmax, zo_g + a_zoom_z), - zo_g, nz_z, β_r_g) - println(" Grid vis: global $(nr_g)×$(nz_g), zoom $(nr_z)×$(nz_z)") -end end # haskey(pes, "efit_by_inversion") # ═══════════════════════════════════════════════════════════════════════════════ @@ -296,7 +300,7 @@ println("\n" * "=" ^ 70) println("2. Flux Surface Contour Overlays") println("=" ^ 70) -psi_contours = collect(range(psi_lo, psi_hi; length=20)) +psi_contours = collect(range(psi_lo, psi_hi; length=20)) theta_contour = range(0.0, 1.0; length=256) R_fs = Dict(m => flux_surface_RZ(pes[m], psi_contours, theta_contour) for m in all_methods) @@ -305,7 +309,7 @@ p_ctr_full = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="Flux surfaces: all methods (psihigh=$psihigh_arg)") p_ctr_core = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="Deep core zoom (innermost 4 surfaces)") -p_ctr_xpt = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", +p_ctr_xpt = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="Far edge / x-point zoom (outermost 3 surfaces)") for m in all_methods @@ -322,7 +326,7 @@ for m in all_methods ls=method_style[m], lw=1.5, label=lbl) end n_outer = length(psi_contours) - for k in (n_outer - 2):n_outer + for k in (n_outer-2):n_outer lbl = k == n_outer - 2 ? method_label[m] : "" plot!(p_ctr_xpt, Rs[k], Zs[k]; color=method_color[m], ls=method_style[m], lw=1.5, label=lbl) @@ -332,45 +336,47 @@ end # Grid overplots: scatter subsampled grid points on each panel (only if efit_by_inversion ran). # Global grid: every 20th R × 12th Z (grey). Zoom grid: every 3rd R × 3rd Z (darkgrey). if !isempty(r_global_grid) -let - step_rg, step_zg = 20, 12 - Rg = [r_global_grid[ir] for ir in 1:step_rg:length(r_global_grid) - for _ in 1:step_zg:length(z_global_grid)] - Zg = [z_global_grid[iz] for _ in 1:step_rg:length(r_global_grid) - for iz in 1:step_zg:length(z_global_grid)] - scatter!(p_ctr_full, Rg, Zg; ms=0.8, color=:grey, alpha=0.25, - markerstrokewidth=0, label="global grid") - - step_rz, step_zz = 3, 3 - Rz = [r_zoom_grid_vis[ir] for ir in 1:step_rz:length(r_zoom_grid_vis) - for _ in 1:step_zz:length(z_zoom_grid_vis)] - Zz = [z_zoom_grid_vis[iz] for _ in 1:step_rz:length(r_zoom_grid_vis) - for iz in 1:step_zz:length(z_zoom_grid_vis)] - scatter!(p_ctr_full, Rz, Zz; ms=0.8, color=:darkgrey, alpha=0.4, - markerstrokewidth=0, label="zoom grid") - - # Core panel: show both grids at full density within the core zoom region - scatter!(p_ctr_core, Rg, Zg; ms=1.2, color=:grey, alpha=0.3, - markerstrokewidth=0, label="global grid") - scatter!(p_ctr_core, Rz, Zz; ms=1.2, color=:darkgrey, alpha=0.5, - markerstrokewidth=0, label="zoom grid") - - # X-point panel: global grid only (zoom doesn't extend there) - scatter!(p_ctr_xpt, Rg, Zg; ms=1.2, color=:grey, alpha=0.3, - markerstrokewidth=0, label="global grid") -end + let + step_rg, step_zg = 20, 12 + Rg = [r_global_grid[ir] for ir in 1:step_rg:length(r_global_grid) + for _ in 1:step_zg:length(z_global_grid)] + Zg = [z_global_grid[iz] for _ in 1:step_rg:length(r_global_grid) + for iz in 1:step_zg:length(z_global_grid)] + scatter!(p_ctr_full, Rg, Zg; ms=0.8, color=:grey, alpha=0.25, + markerstrokewidth=0, label="global grid") + + step_rz, step_zz = 3, 3 + Rz = [r_zoom_grid_vis[ir] for ir in 1:step_rz:length(r_zoom_grid_vis) + for _ in 1:step_zz:length(z_zoom_grid_vis)] + Zz = [z_zoom_grid_vis[iz] for _ in 1:step_rz:length(r_zoom_grid_vis) + for iz in 1:step_zz:length(z_zoom_grid_vis)] + scatter!(p_ctr_full, Rz, Zz; ms=0.8, color=:darkgrey, alpha=0.4, + markerstrokewidth=0, label="zoom grid") + + # Core panel: show both grids at full density within the core zoom region + scatter!(p_ctr_core, Rg, Zg; ms=1.2, color=:grey, alpha=0.3, + markerstrokewidth=0, label="global grid") + scatter!(p_ctr_core, Rz, Zz; ms=1.2, color=:darkgrey, alpha=0.5, + markerstrokewidth=0, label="zoom grid") + + # X-point panel: global grid only (zoom doesn't extend there) + scatter!(p_ctr_xpt, Rg, Zg; ms=1.2, color=:grey, alpha=0.3, + markerstrokewidth=0, label="global grid") + end end # !isempty(r_global_grid) # Set zoom limits for core and x-point panels using the reference method Rc, Zc = R_fs[ref_method] n_core = 4 -all_R_c = vcat(Rc[1:n_core]...); all_Z_c = vcat(Zc[1:n_core]...) +all_R_c = vcat(Rc[1:n_core]...); +all_Z_c = vcat(Zc[1:n_core]...) pad = 0.03 xlims!(p_ctr_core, minimum(all_R_c) - pad, maximum(all_R_c) + pad) ylims!(p_ctr_core, minimum(all_Z_c) - pad, maximum(all_Z_c) + pad) n_outer = length(psi_contours) -all_R_e = vcat(Rc[(n_outer-2):n_outer]...); all_Z_e = vcat(Zc[(n_outer-2):n_outer]...) +all_R_e = vcat(Rc[(n_outer-2):n_outer]...); +all_Z_e = vcat(Zc[(n_outer-2):n_outer]...) xpt_idx = argmin(all_Z_e) xlims!(p_ctr_xpt, all_R_e[xpt_idx] - 0.20, all_R_e[xpt_idx] + 0.20) ylims!(p_ctr_xpt, all_Z_e[xpt_idx] - 0.05, all_Z_e[xpt_idx] + 0.40) @@ -388,7 +394,7 @@ p_tht_full = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="θ = const lines: all methods (psihigh=$psihigh_arg)") p_tht_core = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="Deep core zoom") -p_tht_xpt = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", +p_tht_xpt = plot(; aspect_ratio=:equal, xlabel="R [m]", ylabel="Z [m]", title="Far edge / x-point zoom") for m in all_methods @@ -402,14 +408,14 @@ for m in all_methods Zs = [psi_theta_to_RZ(pe, ψ, θ)[2] for ψ in psi_dense] plot!(p_tht_full, Rs, Zs; color=method_color[m], ls=method_style[m], lw=0.9, alpha=0.7, label=lbl) plot!(p_tht_core, Rs, Zs; color=method_color[m], ls=method_style[m], lw=1.5, label=lbl) - plot!(p_tht_xpt, Rs, Zs; color=method_color[m], ls=method_style[m], lw=1.5, label=lbl) + plot!(p_tht_xpt, Rs, Zs; color=method_color[m], ls=method_style[m], lw=1.5, label=lbl) end # Grid dots at every (ψ_i, θ_j) intersection — by construction on the lines above Rg = [psi_theta_to_RZ(pe, ψ, θ)[1] for ψ in pe.rzphi_xs for θ in θ_sub] Zg = [psi_theta_to_RZ(pe, ψ, θ)[2] for ψ in pe.rzphi_xs for θ in θ_sub] scatter!(p_tht_full, Rg, Zg; ms=1.5, color=method_color[m], alpha=0.5, markerstrokewidth=0, label="") scatter!(p_tht_core, Rg, Zg; ms=2.0, color=method_color[m], alpha=0.6, markerstrokewidth=0, label="") - scatter!(p_tht_xpt, Rg, Zg; ms=2.0, color=method_color[m], alpha=0.6, markerstrokewidth=0, label="") + scatter!(p_tht_xpt, Rg, Zg; ms=2.0, color=method_color[m], alpha=0.6, markerstrokewidth=0, label="") end xlims!(p_tht_core, minimum(all_R_c) - pad, maximum(all_R_c) + pad) @@ -430,16 +436,16 @@ println("3. 2D rzphi Geometric Spline Profiles at Select θ Values") println("=" ^ 70) rzphi_specs = [ - ("r²=(R-R₀)²+(Z-Z₀)²", pe -> ((ψ, θ) -> pe.rzphi_rsquared((ψ, θ)))), + ("r²=(R-R₀)²+(Z-Z₀)²", pe -> ((ψ, θ) -> pe.rzphi_rsquared((ψ, θ)))), ("angle offset η/2π−θ", pe -> ((ψ, θ) -> pe.rzphi_offset((ψ, θ)))), - ("ν (toroidal shift)", pe -> ((ψ, θ) -> pe.rzphi_nu((ψ, θ)))), - ("Jacobian", pe -> ((ψ, θ) -> pe.rzphi_jac((ψ, θ)))), + ("ν (toroidal shift)", pe -> ((ψ, θ) -> pe.rzphi_nu((ψ, θ)))), + ("Jacobian", pe -> ((ψ, θ) -> pe.rzphi_jac((ψ, θ)))) ] rzphi_fnames = ["rzphi_rsquared", "rzphi_angle_offset", "rzphi_nu", "rzphi_jacobian"] for (sidx, (sname, fn_getter)) in enumerate(rzphi_specs) - fn_ref = fn_getter(pe_ref) - yd_all = [[fn_ref(ψ, θ) for ψ in psi_full] for θ in theta_select] + fn_ref = fn_getter(pe_ref) + yd_all = [[fn_ref(ψ, θ) for ψ in psi_full] for θ in theta_select] p_full = plot(; xlabel="ψ", ylabel=sname, legend=:best, xlims=(0, 1), title="$sname — full domain (psihigh=$psihigh_arg)") @@ -454,20 +460,25 @@ for (sidx, (sname, fn_getter)) in enumerate(rzphi_specs) plot!(p_full, psi_full, yd; color=tc, lw=2, label="efit θ=$(θ)") plot!(p_core, psi_full[mask_core], yd[mask_core]; color=tc, lw=2, label="efit θ=$(θ)") plot!(p_edge, t_edge_full, yd[mask_edge]; color=tc, lw=2, label="efit θ=$(θ)") - xs = pe_ref.rzphi_xs; ys = [fn_ref(ψ, θ) for ψ in xs] + xs = pe_ref.rzphi_xs + ys = [fn_ref(ψ, θ) for ψ in xs] scatter!(p_full, xs, ys; ms=3, markerstrokewidth=0, color=tc, label="") - let mask = xs .< 0.10; scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end - let mask = xs .> 0.98; scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end + let mask = xs .< 0.10 + scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end + let mask = xs .> 0.98 + scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end end println(" $sname:") for m in compare_methods_active - fn_m = fn_getter(pes[m]) + fn_m = fn_getter(pes[m]) yi_all = [[fn_m(ψ, θ) for ψ in psi_full] for θ in theta_select] for (tidx, θ) in enumerate(theta_select) tc = theta_colors[tidx] yi = yi_all[tidx] - Δ = yd_all[tidx] .- yi + Δ = yd_all[tidx] .- yi plot!(p_full, psi_full, yi; color=tc, lw=1.2, ls=method_style[m], label="$(method_label[m]) θ=$(θ)") plot!(p_core, psi_full[mask_core], yi[mask_core]; color=tc, lw=1.2, @@ -476,10 +487,15 @@ for (sidx, (sname, fn_getter)) in enumerate(rzphi_specs) ls=method_style[m], label="$(method_label[m]) θ=$(θ)") plot!(p_diff, psi_full, Δ; color=tc, lw=1.2, ls=method_style[m], label="(efit−$(method_label[m])) θ=$(θ)") - xs = pes[m].rzphi_xs; ys = [fn_m(ψ, θ) for ψ in xs] + xs = pes[m].rzphi_xs + ys = [fn_m(ψ, θ) for ψ in xs] scatter!(p_full, xs, ys; ms=3, markerstrokewidth=0, color=tc, label="") - let mask = xs .< 0.10; scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end - let mask = xs .> 0.98; scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end + let mask = xs .< 0.10 + scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end + let mask = xs .> 0.98 + scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end Δc = any(mask_core) ? Δ[mask_core] : [0.0] Δe = any(mask_edge) ? Δ[mask_edge] : [0.0] @@ -501,15 +517,15 @@ println("4. Physics (eqfun) Spline Profiles at Select θ Values") println("=" ^ 70) eqfun_specs = [ - ("|B| (total field [T])", pe -> ((ψ, θ) -> pe.eqfun_B((ψ, θ)))), + ("|B| (total field [T])", pe -> ((ψ, θ) -> pe.eqfun_B((ψ, θ)))), ("metric1 (e₁·e₂+q·e₃·e₁)/JB²", pe -> ((ψ, θ) -> pe.eqfun_metric1((ψ, θ)))), - ("metric2 (e₂·e₃+q·e₃²)/JB²", pe -> ((ψ, θ) -> pe.eqfun_metric2((ψ, θ)))), + ("metric2 (e₂·e₃+q·e₃²)/JB²", pe -> ((ψ, θ) -> pe.eqfun_metric2((ψ, θ)))) ] eqfun_fnames = ["eqfun_Bmag", "eqfun_metric1", "eqfun_metric2"] for (eidx, (ename, fn_getter)) in enumerate(eqfun_specs) - fn_ref = fn_getter(pe_ref) - yd_all = [[fn_ref(ψ, θ) for ψ in psi_full] for θ in theta_select] + fn_ref = fn_getter(pe_ref) + yd_all = [[fn_ref(ψ, θ) for ψ in psi_full] for θ in theta_select] p_full = plot(; xlabel="ψ", ylabel=ename, legend=:best, xlims=(0, 1), title="$ename — full domain (psihigh=$psihigh_arg)") @@ -524,20 +540,25 @@ for (eidx, (ename, fn_getter)) in enumerate(eqfun_specs) plot!(p_full, psi_full, yd; color=tc, lw=2, label="efit θ=$(θ)") plot!(p_core, psi_full[mask_core], yd[mask_core]; color=tc, lw=2, label="efit θ=$(θ)") plot!(p_edge, t_edge_full, yd[mask_edge]; color=tc, lw=2, label="efit θ=$(θ)") - xs = pe_ref.rzphi_xs; ys = [fn_ref(ψ, θ) for ψ in xs] + xs = pe_ref.rzphi_xs + ys = [fn_ref(ψ, θ) for ψ in xs] scatter!(p_full, xs, ys; ms=3, markerstrokewidth=0, color=tc, label="") - let mask = xs .< 0.10; scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end - let mask = xs .> 0.98; scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end + let mask = xs .< 0.10 + scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end + let mask = xs .> 0.98 + scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end end println(" $ename:") for m in compare_methods_active - fn_m = fn_getter(pes[m]) + fn_m = fn_getter(pes[m]) yi_all = [[fn_m(ψ, θ) for ψ in psi_full] for θ in theta_select] for (tidx, θ) in enumerate(theta_select) tc = theta_colors[tidx] yi = yi_all[tidx] - Δ = yd_all[tidx] .- yi + Δ = yd_all[tidx] .- yi plot!(p_full, psi_full, yi; color=tc, lw=1.2, ls=method_style[m], label="$(method_label[m]) θ=$(θ)") plot!(p_core, psi_full[mask_core], yi[mask_core]; color=tc, lw=1.2, @@ -546,10 +567,15 @@ for (eidx, (ename, fn_getter)) in enumerate(eqfun_specs) ls=method_style[m], label="$(method_label[m]) θ=$(θ)") plot!(p_diff, psi_full, Δ; color=tc, lw=1.2, ls=method_style[m], label="(efit−$(method_label[m])) θ=$(θ)") - xs = pes[m].rzphi_xs; ys = [fn_m(ψ, θ) for ψ in xs] + xs = pes[m].rzphi_xs + ys = [fn_m(ψ, θ) for ψ in xs] scatter!(p_full, xs, ys; ms=3, markerstrokewidth=0, color=tc, label="") - let mask = xs .< 0.10; scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end - let mask = xs .> 0.98; scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") end + let mask = xs .< 0.10 + scatter!(p_core, xs[mask], ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end + let mask = xs .> 0.98 + scatter!(p_edge, -log.(1.0 .- xs[mask]), ys[mask]; ms=3, markerstrokewidth=0, color=tc, label="") + end Δc = any(mask_core) ? Δ[mask_core] : [0.0] Δe = any(mask_edge) ? Δ[mask_edge] : [0.0] @@ -580,13 +606,13 @@ function row_int(label, vals) @printf(" %-22s %s\n", label, join([@sprintf("%18d", v) for v in vals])) end -row("ro [m]", [pes[m].ro for m in all_methods]) -row("zo [m]", [pes[m].zo for m in all_methods]) -row("psio [Wb/rad]",[pes[m].psio for m in all_methods]) -row("psilow", [pes[m].rzphi_xs[1] for m in all_methods]) -row("psihigh", [pes[m].rzphi_xs[end] for m in all_methods]) -row("q(psilow)", [pes[m].profiles.q_spline(pes[m].rzphi_xs[1]) for m in all_methods]) -row("q(psihigh)", [pes[m].profiles.q_spline(pes[m].rzphi_xs[end]) for m in all_methods]) +row("ro [m]", [pes[m].ro for m in all_methods]) +row("zo [m]", [pes[m].zo for m in all_methods]) +row("psio [Wb/rad]", [pes[m].psio for m in all_methods]) +row("psilow", [pes[m].rzphi_xs[1] for m in all_methods]) +row("psihigh", [pes[m].rzphi_xs[end] for m in all_methods]) +row("q(psilow)", [pes[m].profiles.q_spline(pes[m].rzphi_xs[1]) for m in all_methods]) +row("q(psihigh)", [pes[m].profiles.q_spline(pes[m].rzphi_xs[end]) for m in all_methods]) q_profiles = Dict(m => [pes[m].profiles.q_spline(ψ) for ψ in psi_full] for m in all_methods) row_int("q non-mono (edge)", diff --git a/benchmarks/plot_xi_eigenmode.jl b/benchmarks/plot_xi_eigenmode.jl index 78e26ee82..1d35a834e 100644 --- a/benchmarks/plot_xi_eigenmode.jl +++ b/benchmarks/plot_xi_eigenmode.jl @@ -39,7 +39,7 @@ for ip in 1:nstep @views mul!(xi[:, ip], u1[:, :, ip], c) end -ms = mlow .+ (0:mpert-1) +ms = mlow .+ (0:(mpert-1)) peak = [maximum(abs, @view xi[i, :]) for i in 1:mpert] order = sortperm(peak; rev=true) # dominant harmonics first diff --git a/benchmarks/verify_gal_match.jl b/benchmarks/verify_gal_match.jl index 51dcdd868..28ad6f2c0 100644 --- a/benchmarks/verify_gal_match.jl +++ b/benchmarks/verify_gal_match.jl @@ -19,7 +19,12 @@ xi, dxi, cout, cin, deltar, eig, resid, sing_psi = h5open(h5path) do f end # HDF5 stores ComplexF64 as a compound (re,im); convert if needed to_c(a) = eltype(a) <: Complex ? a : map(x -> ComplexF64(x.re, x.im), a) -xi = to_c(xi); dxi = to_c(dxi); cout = to_c(cout); cin = to_c(cin); deltar = to_c(deltar); eig = to_c(eig) +xi = to_c(xi); +dxi = to_c(dxi); +cout = to_c(cout); +cin = to_c(cin); +deltar = to_c(deltar); +eig = to_c(eig) mpert, mcoil, ngrid = size(xi) msing = size(deltar, 1) diff --git a/docs/make.jl b/docs/make.jl index 92b605efc..2a971d4a4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -40,9 +40,9 @@ makedocs(; "Tearing" => "inner_layer.md", "Analysis" => "analysis.md", "Utilities" => "utilities.md" - ], + ], "Citations" => "citations.md", - "Developer Notes" => "developer_notes.md", + "Developer Notes" => "developer_notes.md" ], checkdocs=:exports ) diff --git a/docs/resources/2006-Cole-Drift-magnetohydrodynamical model of error-field penetration in tokamak plasmas.pdf b/docs/resources/2006-Cole-Drift-magnetohydrodynamical model of error-field penetration in tokamak plasmas.pdf new file mode 100644 index 000000000..17ef612ec Binary files /dev/null and b/docs/resources/2006-Cole-Drift-magnetohydrodynamical model of error-field penetration in tokamak plasmas.pdf differ diff --git a/docs/src/forcing_terms.md b/docs/src/forcing_terms.md index ba2f7d752..e88c11596 100644 --- a/docs/src/forcing_terms.md +++ b/docs/src/forcing_terms.md @@ -60,7 +60,7 @@ amplitudes directly — no conversion is applied. Required datasets: - `n`: integer array of toroidal mode numbers -- `m`: integer array of poloidal mode numbers +- `m`: integer array of poloidal mode numbers - `amplitude_real`: float array of real parts - `amplitude_imag`: float array of imaginary parts (optional) diff --git a/examples/DIIID-like_SLAYER_example/gpec.toml b/examples/DIIID-like_SLAYER_and_bcrit_example/gpec.toml similarity index 82% rename from examples/DIIID-like_SLAYER_example/gpec.toml rename to examples/DIIID-like_SLAYER_and_bcrit_example/gpec.toml index 487d41f9b..9b37ce9b4 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_and_bcrit_example/gpec.toml @@ -1,7 +1,7 @@ -# DIII-D-like SLAYER tearing-mode growth-rate example. +# DIII-D-like SLAYER tearing-mode growth-rate example with Critical Resonant Field calculations. # Reuses the equilibrium and H-mode kinetic profiles of the sibling # DIIID-like_ideal_example (geqdsk referenced by relative path; not -# duplicated). Runs equilibrium + ForceFreeStates + SLAYER and skips +# duplicated). Runs equilibrium + ForceFreeStates + SLAYER + CriticalResonantField and skips # PerturbedEquilibrium (ForceFreeStates.force_termination = true). [Equilibrium] @@ -30,7 +30,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall [ForceFreeStates] force_termination = true # Run FFS + SLAYER, skip PerturbedEquilibrium -local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile +local_stability_flag = false # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim @@ -86,3 +86,12 @@ Q_re_range = [-2.0, 2.0] # Scan box in the normalized Q plane, Re(Q) axis Q_im_range = [-0.5, 3.0] # Scan box in the normalized Q plane, Im(Q) axis nre = 41 # Grid resolution along the Re(Q) axis nim = 31 # Grid resolution along the Im(Q) axis + +[SLAYER.CriticalResonantField] # CriticalResonantField used to find the critical resonant field sufficent for tearing onset at each rational surface. +enabled = true # Run CriticalResonantField analysis +Qmin = -10 # Minimum normalized rotation scanned +Qmax = 10 # Maximum normalized rotation scanned +n = 2000 # Number of rotation points scanned +viscous_input_type = "magnetic_prandtl_number" # "angular_momentum_diffusivity" # Either "angular_momentum_diffusivity" or "magnetic_prandtl_number" +viscous_input = "test_ang_mom_diff" # Either Prandlt number or angular momentum diffusivity (based on viscous_input_type). Takes in either a string name for the .h5 profile file, a single value for all, or false, which falls back on chi_perp ~ chi_e, electron thermal diffusivity. +store_scan = true # Per surface torque and inner layer quantities diff --git a/examples/DIIID-like_SLAYER_and_bcrit_example/slayer_ex.ipynb b/examples/DIIID-like_SLAYER_and_bcrit_example/slayer_ex.ipynb new file mode 100644 index 000000000..1b3ee30f5 --- /dev/null +++ b/examples/DIIID-like_SLAYER_and_bcrit_example/slayer_ex.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "using Pkg\n", + "Pkg.activate(\"/Users/bursche/Documents/GitHub/JPEC_BCRIT\")\n", + "Base.active_project()\n", + "\n", + "using GeneralizedPerturbedEquilibrium\n", + "using GeneralizedPerturbedEquilibrium: Analysis\n", + "using Printf\n", + "\n", + "using Plots\n", + "default(\n", + " fontfamily=\"Georgia\",\n", + " margin=12Plots.mm,\n", + " size=(800, 500),\n", + " dpi=150\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "h5path = \"gpec.h5\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "println(keys(h5open(h5path, \"r\")[\"Tearing\"][\"CriticalResonantField\"][\"Scan\"][\"surface_1\"]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "function bcrit_diag_plots(Δs, Qs, bal, name)\n", + "\n", + " p1 = plot(Qs, imag.(Δs), label=\"Im(Δ)\", lw=2)\n", + " plot!(p1, Qs, real.(Δs), label=\"Re(Δ)\", lw=2)\n", + " xlabel!(p1, \"Q\")\n", + " ylabel!(p1, \"Δ\")\n", + " title!(p1, \"Inner-layer Δ(Q) - $name\")\n", + "\n", + " p2 = plot(Qs, real.(bal), label=\"Re(balance)\", lw=2)\n", + " plot!(p2, Qs, imag.(bal), label=\"Im(balance)\", lw=2)\n", + " xlabel!(p2, \"Q\")\n", + " ylabel!(p2, \"balance\")\n", + " title!(p2, \"2P(Q0-Q)/jxb - $name\")\n", + "\n", + " plot(p1, p2, layout=(2,1), size=(500, 700))\n", + "\n", + "end\n", + "\n", + "function plot_all_vs_rational_q(h5path)\n", + " h5open(h5path, \"r\") do file\n", + " tearing = file[\"Tearing\"]\n", + " crf = tearing[\"CriticalResonantField\"]\n", + " scan = crf[\"Scan\"]\n", + "\n", + " rational_q = read(tearing[\"PerSurface\"][\"rational_q\"])\n", + " bcrit = read(crf[\"br_crit\"])\n", + " bal = [read(scan[\"surface_$(i)\"][\"balance\"]) for i in eachindex(rational_q)]\n", + " P = [read(scan[\"surface_$(i)\"][\"P\"]) for i in eachindex(rational_q)]\n", + " lu = [read(scan[\"surface_$(i)\"][\"lu\"]) for i in eachindex(rational_q)]\n", + " sval = [read(scan[\"surface_$(i)\"][\"sval\"]) for i in eachindex(rational_q)]\n", + " Q0 = [read(scan[\"surface_$(i)\"][\"Q0\"]) for i in eachindex(rational_q)]\n", + " max_balance = maximum.(bal)\n", + " #read eta from persurface\n", + " eta = read(tearing[\"PerSurface\"][\"eta\"])\n", + " # Calculate angular momentum diffusivity\n", + " # P_here = (4π * 1e-7) * abs(chi_here) / eta_here\n", + " ang_mom_diff = P .* eta / (4π * 1e-7)\n", + "\n", + " fig = plot(layout=(4, 2), size=(1000, 1200))\n", + " plot!(fig[1], rational_q, bcrit; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Critical Resonant Field (T)\", legend=false)\n", + " plot!(fig[2], rational_q, max_balance; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Maximum (Torque Balance)\", legend=false)\n", + " plot!(fig[3], rational_q, P; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"P\", legend=false)\n", + " plot!(fig[4], rational_q, lu; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Lundquist Number\", legend=false)\n", + " plot!(fig[5], rational_q, sval; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Magnetic Shear\", legend=false)\n", + " plot!(fig[6], rational_q, Q0; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Q0\", legend=false)\n", + " plot!(fig[7], rational_q, ang_mom_diff; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Angular Momentum Diffusivity\", legend=false)\n", + " plot!(fig[8], rational_q, eta; marker=:circle, lw=2, xlabel=\"Rational Surface (q)\", ylabel=\"Resistivity (η)\", legend=false)\n", + " display(fig)\n", + " #return fig\n", + " end\n", + "end" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "plot_all_vs_rational_q(h5path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot All Inner-layer Δ(Q) and Torque Balance vs Q for each rational surface\n", + "\n", + "h5open(h5path, \"r\") do file\n", + " bcrit = file[\"Tearing\"][\"CriticalResonantField\"]\n", + "\n", + " for i in 1:6\n", + " ss = \"surface_$i\"\n", + "\n", + " delta = read(bcrit[\"Scan\"][ss][\"delta\"])\n", + " Q = read(bcrit[\"Scan\"][ss][\"Q\"])\n", + " balance = read(bcrit[\"Scan\"][ss][\"balance\"])\n", + "\n", + " display(bcrit_diag_plots(delta, Q, balance, \"surface_$i\"))\n", + " end\n", + "end" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "p_eq = Analysis.Equilibrium.plot_equilibrium_summary(h5path)\n", + "p_ffs = Analysis.ForceFreeStates.plot_ffs_summary(h5path)\n", + "\n", + "display(p_eq)\n", + "display(p_ffs)" + ] + } + ], + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5 b/examples/DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5 index 2f084f080..4ee95ff2d 100644 Binary files a/examples/DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5 and b/examples/DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5 differ diff --git a/examples/DIIID-like_ideal_example/analyze_example.jl b/examples/DIIID-like_ideal_example/analyze_example.jl index 715e3ec76..dae831848 100644 --- a/examples/DIIID-like_ideal_example/analyze_example.jl +++ b/examples/DIIID-like_ideal_example/analyze_example.jl @@ -41,7 +41,7 @@ function symmetric_clims(z) end geqdsk_cases = [ - "TkMkr_D3Dlike_Hmode.geqdsk", + "TkMkr_D3Dlike_Hmode.geqdsk" ] base_toml = TOML.parsefile(joinpath(@__DIR__, "gpec.toml")) diff --git a/examples/DIIID-like_ideal_example_IMAS/run_imas_example.jl b/examples/DIIID-like_ideal_example_IMAS/run_imas_example.jl index c4fe1637f..0e416e317 100644 --- a/examples/DIIID-like_ideal_example_IMAS/run_imas_example.jl +++ b/examples/DIIID-like_ideal_example_IMAS/run_imas_example.jl @@ -57,8 +57,8 @@ try config_imas = merge(SHARED_CONFIG, Dict("Equilibrium" => merge(EQ_COMMON, Dict("eq_type" => "imas", "eq_filename" => "from_dd", "imas_cocos" => 11)))) open(joinpath(tmpdir_imas, "gpec.toml"), "w") do io - ; - TOML.print(io, config_imas); + + TOML.print(io, config_imas) end result_imas = GPEC.main([tmpdir_imas]; dd=dd) global et_imas = real(result_imas.ffs.free_boundary.et[1]) @@ -76,8 +76,8 @@ try config_gfile = merge(SHARED_CONFIG, Dict("Equilibrium" => merge(EQ_COMMON, Dict("eq_type" => "efit", "eq_filename" => GEQDSK)))) open(joinpath(tmpdir_gfile, "gpec.toml"), "w") do io - ; - TOML.print(io, config_gfile); + + TOML.print(io, config_gfile) end result_gfile = GPEC.main([tmpdir_gfile]) global et_gfile = real(result_gfile.ffs.free_boundary.et[1]) diff --git a/examples/LAR_beta_scan/run_scan.jl b/examples/LAR_beta_scan/run_scan.jl index 6249ff30d..f6105a2ae 100644 --- a/examples/LAR_beta_scan/run_scan.jl +++ b/examples/LAR_beta_scan/run_scan.jl @@ -30,11 +30,11 @@ using Printf # is approximately uniform over most of the range and smoothly tightens as # the pole is approached, giving an even visual cadence without wasting # points on the flat-slope region far from the pole. -function _warped_grid(x_start::Float64, x_end::Float64, N::Int; p::Float64 = 2.0) - return [x_start + (x_end - x_start) * (1 - (1 - i / (N - 1))^p) for i in 0:N-1] +function _warped_grid(x_start::Float64, x_end::Float64, N::Int; p::Float64=2.0) + return [x_start + (x_end - x_start) * (1 - (1 - i / (N - 1))^p) for i in 0:(N-1)] end -const PC_FULL = _warped_grid(0.001, 0.1735, 40; p = 2.0) +const PC_FULL = _warped_grid(0.001, 0.1735, 40; p=2.0) const PC_TEST = [0.001, 0.10, 0.17] @@ -58,7 +58,9 @@ function run_single(pc::Float64) config = deepcopy(GPEC_BASE) config["TJ_ANALYTIC_INPUT"]["pc"] = pc config["ForceFreeStates"]["HDF5_filename"] = joinpath(run_dir, "gpec.h5") - open(joinpath(run_dir, "gpec.toml"), "w") do io; TOML.print(io, config); end + open(joinpath(run_dir, "gpec.toml"), "w") do io + TOML.print(io, config) + end GeneralizedPerturbedEquilibrium.main([run_dir]) return extract_results(joinpath(run_dir, "gpec.h5")) @@ -73,24 +75,32 @@ end function extract_results(h5_path::String) h5open(h5_path, "r") do f fbs = "ForceFreeStates/FreeBoundaryStability" - ep = read(f, "$fbs/eigenmode_plasma_energies"); ev = read(f, "$fbs/eigenmode_vacuum_energies"); et = read(f, "$fbs/eigenmode_energies") + ep = read(f, "$fbs/eigenmode_plasma_energies") + ev = read(f, "$fbs/eigenmode_vacuum_energies") + et = read(f, "$fbs/eigenmode_energies") msing = read(f, "SingularSurfaces/rational_count") m_sing = read(f, "SingularSurfaces/rational_m") dp_mat = haskey(f, "SingularSurfaces/Delta_prime_matrix") ? read(f, "SingularSurfaces/Delta_prime_matrix") : nothing qlim = haskey(f, "Info/qlim") ? read(f, "Info/qlim") : read(f, "Equilibrium/q_max") - q0 = read(f, "Equilibrium/q_axis"); qmax = read(f, "Equilibrium/q_max") + q0 = read(f, "Equilibrium/q_axis") + qmax = read(f, "Equilibrium/q_max") - dp_21 = NaN + NaN*im; dp_31 = NaN + NaN*im + dp_21 = NaN + NaN*im + dp_31 = NaN + NaN*im if dp_mat !== nothing && msing > 0 for s in 1:min(msing, size(dp_mat, 1)) m_val = size(m_sing, 1) == msing ? m_sing[s, 1] : m_sing[1, s] - if m_val == 2; dp_21 = dp_mat[s, s]; end - if m_val == 3; dp_31 = dp_mat[s, s]; end + if m_val == 2 + dp_21 = dp_mat[s, s] + end + if m_val == 3 + dp_31 = dp_mat[s, s] + end end end return (dp_21=dp_21, dp_31=dp_31, - dW_plasma=real(ep[1]), dW_vacuum=real(ev[1]), dW_total=real(et[1]), - q0=q0, qmax=qmax, qlim=qlim, msing=msing, dp_matrix=dp_mat) + dW_plasma=real(ep[1]), dW_vacuum=real(ev[1]), dW_total=real(et[1]), + q0=q0, qmax=qmax, qlim=qlim, msing=msing, dp_matrix=dp_mat) end end @@ -117,11 +127,20 @@ function main() haskey(f, gname) && delete_object(f, gname) g = create_group(f, gname) g["pressure_factor"] = pc - g["dp_21_real"] = real(result.dp_21); g["dp_21_imag"] = imag(result.dp_21) - g["dp_31_real"] = real(result.dp_31); g["dp_31_imag"] = imag(result.dp_31) - g["dW_plasma"] = result.dW_plasma; g["dW_vacuum"] = result.dW_vacuum; g["dW_total"] = result.dW_total - g["q0"] = result.q0; g["qmax"] = result.qmax; g["qlim"] = result.qlim; g["msing"] = result.msing - if result.dp_matrix !== nothing; g["dp_matrix"] = result.dp_matrix; end + g["dp_21_real"] = real(result.dp_21) + g["dp_21_imag"] = imag(result.dp_21) + g["dp_31_real"] = real(result.dp_31) + g["dp_31_imag"] = imag(result.dp_31) + g["dW_plasma"] = result.dW_plasma + g["dW_vacuum"] = result.dW_vacuum + g["dW_total"] = result.dW_total + g["q0"] = result.q0 + g["qmax"] = result.qmax + g["qlim"] = result.qlim + g["msing"] = result.msing + if result.dp_matrix !== nothing + g["dp_matrix"] = result.dp_matrix + end end @printf(" dp21=%+.4f%+.4fi dp31=%+.4f%+.4fi dW_t=%+.6f qa=%.3f\n", real(result.dp_21), imag(result.dp_21), real(result.dp_31), imag(result.dp_31), diff --git a/examples/LAR_epsilon_scan/run_scan.jl b/examples/LAR_epsilon_scan/run_scan.jl index e3327ab95..f487332f0 100644 --- a/examples/LAR_epsilon_scan/run_scan.jl +++ b/examples/LAR_epsilon_scan/run_scan.jl @@ -30,11 +30,11 @@ using Printf # spacing tightens smoothly as the pole is approached — the flat low-ε # region is covered with even cadence, and more points land in the final # few percent where Δ' rises by orders of magnitude. -function _warped_grid(x_start::Float64, x_end::Float64, N::Int; p::Float64 = 2.0) - return [x_start + (x_end - x_start) * (1 - (1 - i / (N - 1))^p) for i in 0:N-1] +function _warped_grid(x_start::Float64, x_end::Float64, N::Int; p::Float64=2.0) + return [x_start + (x_end - x_start) * (1 - (1 - i / (N - 1))^p) for i in 0:(N-1)] end -const EPSILONS_FULL = _warped_grid(0.125, 0.660, 56; p = 2.0) +const EPSILONS_FULL = _warped_grid(0.125, 0.660, 56; p=2.0) const EPSILONS_TEST = [0.2495, 0.4072, 0.5510] @@ -65,7 +65,9 @@ function run_single(epsilon::Float64) config["TJ_ANALYTIC_INPUT"]["lar_r0"] = GPEC_BASE["TJ_ANALYTIC_INPUT"]["lar_a"] / epsilon config["Equilibrium"]["eq_type"] = "tj_analytic_direct" config["ForceFreeStates"]["HDF5_filename"] = joinpath(run_dir, "gpec.h5") - open(joinpath(run_dir, "gpec.toml"), "w") do io; TOML.print(io, config); end + open(joinpath(run_dir, "gpec.toml"), "w") do io + TOML.print(io, config) + end GeneralizedPerturbedEquilibrium.main([run_dir]) return extract_results(joinpath(run_dir, "gpec.h5")) @@ -80,24 +82,32 @@ end function extract_results(h5_path::String) h5open(h5_path, "r") do f fbs = "ForceFreeStates/FreeBoundaryStability" - ep = read(f, "$fbs/eigenmode_plasma_energies"); ev = read(f, "$fbs/eigenmode_vacuum_energies"); et = read(f, "$fbs/eigenmode_energies") + ep = read(f, "$fbs/eigenmode_plasma_energies") + ev = read(f, "$fbs/eigenmode_vacuum_energies") + et = read(f, "$fbs/eigenmode_energies") msing = read(f, "SingularSurfaces/rational_count") m_sing = read(f, "SingularSurfaces/rational_m") dp_mat = haskey(f, "SingularSurfaces/Delta_prime_matrix") ? read(f, "SingularSurfaces/Delta_prime_matrix") : nothing qlim = haskey(f, "Info/qlim") ? read(f, "Info/qlim") : read(f, "Equilibrium/q_max") - q0 = read(f, "Equilibrium/q_axis"); qmax = read(f, "Equilibrium/q_max") + q0 = read(f, "Equilibrium/q_axis") + qmax = read(f, "Equilibrium/q_max") - dp_21 = NaN + NaN*im; dp_31 = NaN + NaN*im + dp_21 = NaN + NaN*im + dp_31 = NaN + NaN*im if dp_mat !== nothing && msing > 0 for s in 1:min(msing, size(dp_mat, 1)) m_val = size(m_sing, 1) == msing ? m_sing[s, 1] : m_sing[1, s] - if m_val == 2; dp_21 = dp_mat[s, s]; end - if m_val == 3; dp_31 = dp_mat[s, s]; end + if m_val == 2 + dp_21 = dp_mat[s, s] + end + if m_val == 3 + dp_31 = dp_mat[s, s] + end end end return (dp_21=dp_21, dp_31=dp_31, - dW_plasma=real(ep[1]), dW_vacuum=real(ev[1]), dW_total=real(et[1]), - q0=q0, qmax=qmax, qlim=qlim, msing=msing, dp_matrix=dp_mat) + dW_plasma=real(ep[1]), dW_vacuum=real(ev[1]), dW_total=real(et[1]), + q0=q0, qmax=qmax, qlim=qlim, msing=msing, dp_matrix=dp_mat) end end @@ -125,11 +135,20 @@ function main() haskey(f, gname) && delete_object(f, gname) g = create_group(f, gname) g["epsilon"] = eps - g["dp_21_real"] = real(result.dp_21); g["dp_21_imag"] = imag(result.dp_21) - g["dp_31_real"] = real(result.dp_31); g["dp_31_imag"] = imag(result.dp_31) - g["dW_plasma"] = result.dW_plasma; g["dW_vacuum"] = result.dW_vacuum; g["dW_total"] = result.dW_total - g["q0"] = result.q0; g["qmax"] = result.qmax; g["qlim"] = result.qlim; g["msing"] = result.msing - if result.dp_matrix !== nothing; g["dp_matrix"] = result.dp_matrix; end + g["dp_21_real"] = real(result.dp_21) + g["dp_21_imag"] = imag(result.dp_21) + g["dp_31_real"] = real(result.dp_31) + g["dp_31_imag"] = imag(result.dp_31) + g["dW_plasma"] = result.dW_plasma + g["dW_vacuum"] = result.dW_vacuum + g["dW_total"] = result.dW_total + g["q0"] = result.q0 + g["qmax"] = result.qmax + g["qlim"] = result.qlim + g["msing"] = result.msing + if result.dp_matrix !== nothing + g["dp_matrix"] = result.dp_matrix + end end @printf(" dp21=%+.4f%+.4fi dp31=%+.4f%+.4fi dW_t=%+.6f qa=%.3f\n", real(result.dp_21), imag(result.dp_21), real(result.dp_31), imag(result.dp_31), diff --git a/examples/Solovev_ideal_example/analyze_example.jl b/examples/Solovev_ideal_example/analyze_example.jl index 4c5a8eb7c..f3d6f0b99 100644 --- a/examples/Solovev_ideal_example/analyze_example.jl +++ b/examples/Solovev_ideal_example/analyze_example.jl @@ -9,8 +9,11 @@ h5path = joinpath(@__DIR__, "gpec.h5") # Summary plots p_eq = Analysis.Equilibrium.plot_equilibrium_summary(h5path) p_ffs = Analysis.ForceFreeStates.plot_ffs_summary(h5path) -p_pe = Analysis.PerturbedEquilibrium.plot_perturbed_equilibrium_summary(h5path) +p_pe = Analysis.PerturbedEquilibrium.plot_perturbed_equilibrium_summary(h5path) -display(p_eq); Plots.savefig(p_eq, joinpath(@__DIR__, "equilibrium_summary.png")) -display(p_ffs); Plots.savefig(p_ffs, joinpath(@__DIR__, "ffs_summary.png")) -display(p_pe); Plots.savefig(p_pe, joinpath(@__DIR__, "pe_summary.png")) \ No newline at end of file +display(p_eq); +Plots.savefig(p_eq, joinpath(@__DIR__, "equilibrium_summary.png")) +display(p_ffs); +Plots.savefig(p_ffs, joinpath(@__DIR__, "ffs_summary.png")) +display(p_pe); +Plots.savefig(p_pe, joinpath(@__DIR__, "pe_summary.png")) diff --git a/examples/Solovev_ideal_example_multi_n/analyze_example.jl b/examples/Solovev_ideal_example_multi_n/analyze_example.jl index 3a1e46134..068dd2a0d 100644 --- a/examples/Solovev_ideal_example_multi_n/analyze_example.jl +++ b/examples/Solovev_ideal_example_multi_n/analyze_example.jl @@ -9,11 +9,14 @@ h5path = joinpath(@__DIR__, "gpec.h5") # Summary plots p_eq = Analysis.Equilibrium.plot_equilibrium_summary(h5path) p_ffs = Analysis.ForceFreeStates.plot_ffs_summary(h5path) -p_pe = Analysis.PerturbedEquilibrium.plot_perturbed_equilibrium_summary(h5path) +p_pe = Analysis.PerturbedEquilibrium.plot_perturbed_equilibrium_summary(h5path) -display(p_eq); Plots.savefig(p_eq, joinpath(@__DIR__, "equilibrium_summary.png")) -display(p_ffs); Plots.savefig(p_ffs, joinpath(@__DIR__, "ffs_summary.png")) -display(p_pe); Plots.savefig(p_pe, joinpath(@__DIR__, "pe_summary.png")) +display(p_eq); +Plots.savefig(p_eq, joinpath(@__DIR__, "equilibrium_summary.png")) +display(p_ffs); +Plots.savefig(p_ffs, joinpath(@__DIR__, "ffs_summary.png")) +display(p_pe); +Plots.savefig(p_pe, joinpath(@__DIR__, "pe_summary.png")) # Analyze the single-n runs h5path_n1 = joinpath(@__DIR__, "single_n_1", "euler_n1.h5") @@ -21,5 +24,7 @@ h5path_n2 = joinpath(@__DIR__, "single_n_2", "euler_n2.h5") p_n1 = Analysis.ForceFreeStates.plot_ffs_summary(h5path_n1) p_n2 = Analysis.ForceFreeStates.plot_ffs_summary(h5path_n2) -display(p_n1); Plots.savefig(p_n1, joinpath(@__DIR__, "single_n_1", "ffs_summary.png")) -display(p_n2); Plots.savefig(p_n2, joinpath(@__DIR__, "single_n_2", "ffs_summary.png")) +display(p_n1); +Plots.savefig(p_n1, joinpath(@__DIR__, "single_n_1", "ffs_summary.png")) +display(p_n2); +Plots.savefig(p_n2, joinpath(@__DIR__, "single_n_2", "ffs_summary.png")) diff --git a/regression-harness/src/database.jl b/regression-harness/src/database.jl index c9a06b303..c7f043bc2 100644 --- a/regression-harness/src/database.jl +++ b/regression-harness/src/database.jl @@ -50,7 +50,9 @@ added by a later schema migration (NULL on every pre-existing row) need both cas """ _column(x, default) = (x === nothing || x === missing) ? default : x -"""Materialize SQLite query results as a Vector of NamedTuples.""" +""" +Materialize SQLite query results as a Vector of NamedTuples. +""" function query_rows(db::SQLite.DB, sql::String, params=()) result = DBInterface.execute(db, sql, params) ct = Tables.columntable(result) @@ -70,7 +72,9 @@ const ENV_COLUMNS = [ ("pinned", "INTEGER") ] -"""Add any `runs` columns missing from a database created by an earlier harness version.""" +""" +Add any `runs` columns missing from a database created by an earlier harness version. +""" function migrate_schema!(db::SQLite.DB) existing = Set(String[]) for row in query_rows(db, "PRAGMA table_info(runs)") @@ -107,10 +111,10 @@ environment. Rows predating fingerprinting hold NULL and therefore never match entries most likely to be misleading are exactly the ones that get re-run. """ function is_cached(db::SQLite.DB, commit_hash::String, case_name::String; - expected_key::Union{String,Nothing}=nothing)::Bool + expected_key::Union{String,Nothing}=nothing)::Bool if expected_key === nothing rows = query_rows(db, "SELECT id FROM runs WHERE commit_hash = ? AND case_name = ? AND success = 1", - (commit_hash, case_name)) + (commit_hash, case_name)) return !isempty(rows) end rows = query_rows(db, @@ -125,7 +129,7 @@ fingerprinting. Used to explain *why* a cached result was rejected. """ function cached_env_key(db::SQLite.DB, commit_hash::String, case_name::String)::Union{String,Nothing} rows = query_rows(db, "SELECT env_key FROM runs WHERE commit_hash = ? AND case_name = ?", - (commit_hash, case_name)) + (commit_hash, case_name)) isempty(rows) && return nothing key = _column(first(rows).env_key, nothing) key === nothing && return nothing @@ -135,15 +139,15 @@ end function delete_cached(db::SQLite.DB, commit_hash::String, case_name::String) # ON DELETE CASCADE handles quantities cleanup automatically DBInterface.execute(db, "DELETE FROM runs WHERE commit_hash = ? AND case_name = ?", - (commit_hash, case_name)) + (commit_hash, case_name)) end function store_run(db::SQLite.DB, commit_hash::AbstractString, commit_short::AbstractString, - commit_date::AbstractString, commit_msg::AbstractString, - case_name::AbstractString, runtime_s::Float64, - extracted::Vector{ExtractedQuantity}; - success::Bool=true, error_msg::AbstractString="", - fingerprint::EnvFingerprint=UNKNOWN_ENV) + commit_date::AbstractString, commit_msg::AbstractString, + case_name::AbstractString, runtime_s::Float64, + extracted::Vector{ExtractedQuantity}; + success::Bool=true, error_msg::AbstractString="", + fingerprint::EnvFingerprint=UNKNOWN_ENV) ran_at = Dates.format(Dates.now(), "yyyy-mm-ddTHH:MM:SS") SQLite.transaction(db) do @@ -155,10 +159,10 @@ function store_run(db::SQLite.DB, commit_hash::AbstractString, commit_short::Abs env_key, julia_version, os_arch, manifest_sha, nthreads, blas_threads, pinned) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (String(commit_hash), String(commit_short), String(commit_date), String(commit_msg), - String(case_name), ran_at, runtime_s, success ? 1 : 0, String(error_msg), - env_key(fingerprint), fingerprint.julia_version, fingerprint.os_arch, - fingerprint.manifest_sha, fingerprint.nthreads, fingerprint.blas_threads, - fingerprint.pinned ? 1 : 0)) + String(case_name), ran_at, runtime_s, success ? 1 : 0, String(error_msg), + env_key(fingerprint), fingerprint.julia_version, fingerprint.os_arch, + fingerprint.manifest_sha, fingerprint.nthreads, fingerprint.blas_threads, + fingerprint.pinned ? 1 : 0)) run_id = SQLite.last_insert_rowid(db) @@ -168,14 +172,14 @@ function store_run(db::SQLite.DB, commit_hash::AbstractString, commit_short::Abs (run_id, qty_name, qty_label, value_real, value_int, value_text, value_type, noise_threshold) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", (run_id, eq.name, eq.label, eq.value_real, eq.value_int, eq.value_text, - eq.value_type, eq.noise_threshold)) + eq.value_type, eq.noise_threshold)) end end end function store_failed_run(db::SQLite.DB, commit_hash::AbstractString, commit_short::AbstractString, - commit_date::AbstractString, commit_msg::AbstractString, - case_name::AbstractString, error_msg::AbstractString) + commit_date::AbstractString, commit_msg::AbstractString, + case_name::AbstractString, error_msg::AbstractString) ran_at = Dates.format(Dates.now(), "yyyy-mm-ddTHH:MM:SS") delete_cached(db, String(commit_hash), String(case_name)) DBInterface.execute(db, @@ -183,14 +187,14 @@ function store_failed_run(db::SQLite.DB, commit_hash::AbstractString, commit_sho (commit_hash, commit_short, commit_date, commit_msg, case_name, ran_at, runtime_s, success, error_msg) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", (String(commit_hash), String(commit_short), String(commit_date), String(commit_msg), - String(case_name), ran_at, 0.0, 0, String(error_msg))) + String(case_name), ran_at, 0.0, 0, String(error_msg))) end """ Get all quantities for a (commit, case) pair. Returns Dict{qty_name => NamedTuple}. """ function get_quantities(db::SQLite.DB, commit_hash::String, case_name::String) - results = Dict{String, NamedTuple}() + results = Dict{String,NamedTuple}() rows = query_rows(db, """SELECT q.qty_name, q.qty_label, q.value_real, q.value_int, q.value_text, q.value_type, q.noise_threshold @@ -202,12 +206,12 @@ function get_quantities(db::SQLite.DB, commit_hash::String, case_name::String) for row in rows name = something(row.qty_name, "") results[name] = ( - label = something(row.qty_label, name), - value_real = row.value_real, - value_int = row.value_int, - value_text = row.value_text, - value_type = something(row.value_type, "missing"), - noise_threshold = something(row.noise_threshold, 0.0), + label=something(row.qty_label, name), + value_real=row.value_real, + value_int=row.value_int, + value_text=row.value_text, + value_type=something(row.value_type, "missing"), + noise_threshold=something(row.noise_threshold, 0.0) ) end return results @@ -233,13 +237,13 @@ function get_run_info(db::SQLite.DB, commit_hash::String, case_name::String) _column(row.pinned, 0) == 1 ) return ( - commit_short = something(row.commit_short, ""), - commit_date = something(row.commit_date, ""), - commit_msg = something(row.commit_msg, ""), - runtime_s = something(row.runtime_s, 0.0), - success = coalesce(row.success, 0) == 1, - error_msg = something(row.error_msg, ""), - fingerprint = fingerprint, + commit_short=something(row.commit_short, ""), + commit_date=something(row.commit_date, ""), + commit_msg=something(row.commit_msg, ""), + runtime_s=something(row.runtime_s, 0.0), + success=coalesce(row.success, 0) == 1, + error_msg=something(row.error_msg, ""), + fingerprint=fingerprint ) end diff --git a/regression-harness/src/env.jl b/regression-harness/src/env.jl index 20718c4cb..a7db6d339 100644 --- a/regression-harness/src/env.jl +++ b/regression-harness/src/env.jl @@ -49,7 +49,9 @@ end env_key(fp::EnvFingerprint) = env_key(fp.julia_version, fp.os_arch, fp.pinned ? fp.manifest_sha : "unpinned") -"""SHA-256 of a file, or "" when it does not exist.""" +""" +SHA-256 of a file, or "" when it does not exist. +""" function file_sha256(path::AbstractString)::String isfile(path) || return "" return bytes2hex(SHA.sha256(read(path))) @@ -108,7 +110,9 @@ function read_runinfo(path::String, pinned::Bool) return (something(runtime_s, NaN), fp) end -"""One-line human-readable summary of an environment, for report headers.""" +""" +One-line human-readable summary of an environment, for report headers. +""" function describe_env(fp::EnvFingerprint)::String isempty(fp.julia_version) && return "environment unknown (cached before fingerprinting)" mani = isempty(fp.manifest_sha) ? "no Manifest" : "manifest " * fp.manifest_sha[1:min(8, end)] diff --git a/regression-harness/src/reporter.jl b/regression-harness/src/reporter.jl index 4fdecfbe3..2fef2b990 100644 --- a/regression-harness/src/reporter.jl +++ b/regression-harness/src/reporter.jl @@ -233,7 +233,7 @@ function report_two_ref_comparison(db::SQLite.DB, case_spec::CaseSpec, println("Summary: ", join(parts, ", ")) println() return (n_ok=n_ok, n_changed=n_changed, n_missing=n_missing, - n_failed=count(identity, (failed1, failed2))) + n_failed=count(identity, (failed1, failed2))) end """ @@ -366,7 +366,7 @@ function report_multi_ref(db::SQLite.DB, case_spec::CaseSpec, end end if length(refs) >= 2 - last_two = filter(!isnothing, run_infos[(end - 1):end]) + last_two = filter(!isnothing, run_infos[(end-1):end]) length(last_two) == 2 && _warn_env_difference(last_two[1].fingerprint, last_two[2].fingerprint) end println("-"^total_w) @@ -387,7 +387,7 @@ function report_multi_ref(db::SQLite.DB, case_spec::CaseSpec, end println() return (n_ok=n_ok, n_changed=n_changed, n_missing=n_missing, - n_failed=count(identity, failed_mask)) + n_failed=count(identity, failed_mask)) end """ diff --git a/regression-harness/src/types.jl b/regression-harness/src/types.jl index 05c7159ce..bfce92ed7 100644 --- a/regression-harness/src/types.jl +++ b/regression-harness/src/types.jl @@ -19,10 +19,11 @@ end Specification for a test case: what to run and what to extract. `kind` selects the runner backend: + - "gpec_run" (default) — run GPEC end-to-end on `example_dir`, extract from `gpec.h5` - "computed" — run a self-contained Julia computation that writes a small h5 - (no `example_dir` required); used for analytic/reference cases - like the GGJ inner-layer benchmark. + (no `example_dir` required); used for analytic/reference cases + like the GGJ inner-layer benchmark. """ struct CaseSpec name::String diff --git a/regression-harness/src/utils.jl b/regression-harness/src/utils.jl index 01afbd40a..d1110f8ef 100644 --- a/regression-harness/src/utils.jl +++ b/regression-harness/src/utils.jl @@ -130,7 +130,7 @@ newest. Without it, two refs are compared across two different package sets and differences surface as physics regressions. """ function create_worktree(commit_hash::String, repo_root::String; - pin_manifest_from::Union{String,Nothing}=nothing)::String + pin_manifest_from::Union{String,Nothing}=nothing)::String short = commit_hash[1:min(8, length(commit_hash))] worktree_path = tempname() * "_gpec_$(short)" try diff --git a/src/Analysis/Equilibrium.jl b/src/Analysis/Equilibrium.jl index 6dbfede40..51b10f851 100644 --- a/src/Analysis/Equilibrium.jl +++ b/src/Analysis/Equilibrium.jl @@ -219,7 +219,7 @@ function plot_flux_surfaces(h5path; n_psi=11, n_theta=18, save_path=nothing) for s in 1:msing idx = argmin(abs.(xs_rz .- psi_sing[s])) q_label = abs(q_sing[s] - round(q_sing[s])) < 0.05 ? - "q=$(round(Int, q_sing[s]))" : "q=$(round(q_sing[s], digits=2))" + "q=$(round(Int, q_sing[s]))" : "q=$(round(q_sing[s], digits=2))" plot!(p, [R_grid[idx, :]; R_grid[idx, 1]], [Z_grid[idx, :]; Z_grid[idx, 1]]; color=:red, linewidth=1.5, label=s == 1 ? "Rational surface" : "") @@ -368,23 +368,26 @@ function plot_equilibrium_summary(h5path; save_path=nothing) title_str = "q0=$(round(q0,digits=2)) q95=$(round(q95,digits=2)) βₜ=$(round(betat,digits=3)) βₙ=$(round(betan,digits=3)) κ=$(round(kappa,digits=2)) li1=$(round(li1,digits=3))" - p_rz = plot_flux_surfaces(h5path) - p_q = plot_qprofile(h5path; show_singular=true) + p_rz = plot_flux_surfaces(h5path) + p_q = plot_qprofile(h5path; show_singular=true) p_pres = plot_pressure_profile(h5path) - p_f = plot_f_profile(h5path) - p_gse = plot_gse_by_theta(h5path) # includes integrated overplot; nothing if absent + p_f = plot_f_profile(h5path) + p_gse = plot_gse_by_theta(h5path) # includes integrated overplot; nothing if absent # Suppress x-axis labels/ticks on all but the bottom profile panel — they share the # same ψ_N axis and labeling every panel wastes vertical space. hide_xaxis!(p) = plot!(p; xlabel="", xformatter=_->"", bottom_margin=1Plots.mm) if isnothing(p_gse) - hide_xaxis!(p_q); hide_xaxis!(p_pres) + hide_xaxis!(p_q) + hide_xaxis!(p_pres) l = @layout [a{0.38w} [b; c; d]] p = plot(p_rz, p_q, p_pres, p_f; layout=l, size=(1300, 750), plot_title=title_str, plot_titlefontsize=10, top_margin=8Plots.mm) else - hide_xaxis!(p_q); hide_xaxis!(p_pres); hide_xaxis!(p_f) + hide_xaxis!(p_q) + hide_xaxis!(p_pres) + hide_xaxis!(p_f) l = @layout [a{0.38w} [b; c; d; e]] p = plot(p_rz, p_q, p_pres, p_f, p_gse; layout=l, size=(1300, 1000), plot_title=title_str, plot_titlefontsize=10, top_margin=8Plots.mm) diff --git a/src/Analysis/PerturbedEquilibrium.jl b/src/Analysis/PerturbedEquilibrium.jl index 33f8bdc0f..7c1ea2d92 100644 --- a/src/Analysis/PerturbedEquilibrium.jl +++ b/src/Analysis/PerturbedEquilibrium.jl @@ -45,10 +45,10 @@ function plot_resonant_area_weighted_field_amplitude(h5path; save_path=nothing) return plot(; title="No resonant area-weighted field data — run with perturbed equilibrium enabled", legend=false) resonant_area_weighted_field, rational_psi, rational_q, rational_n = h5open(h5path, "r") do fid - read(fid[base * "resonant_area_weighted_field"]), - read(fid[base * "rational_psi"]), - read(fid[base * "rational_q"]), - read(fid[base * "rational_n"]) + read(fid[base*"resonant_area_weighted_field"]), + read(fid[base*"rational_psi"]), + read(fid[base*"rational_q"]), + read(fid[base*"rational_n"]) end p = plot(; xlabel="Norm. Poloidal Flux", ylabel="|b^r| [T]", @@ -96,9 +96,9 @@ function plot_island_widths(h5path; save_path=nothing) return plot(; title="No island width data — run with perturbed equilibrium enabled", legend=false) island_hw, rational_psi, rational_q = h5open(h5path, "r") do fid - read(fid[base * "island_half_width"]), - read(fid[base * "rational_psi"]), - read(fid[base * "rational_q"]) + read(fid[base*"island_half_width"]), + read(fid[base*"rational_psi"]), + read(fid[base*"rational_q"]) end p = scatter( @@ -150,9 +150,9 @@ function plot_chirikov_parameter(h5path; save_path=nothing) return plot(; title="No Chirikov data — run with perturbed equilibrium enabled", legend=false) chirikov, rational_psi, rational_q = h5open(h5path, "r") do fid - read(fid[base * "chirikov_parameter"]), - read(fid[base * "rational_psi"]), - read(fid[base * "rational_q"]) + read(fid[base*"chirikov_parameter"]), + read(fid[base*"rational_psi"]), + read(fid[base*"rational_q"]) end colors = [k > 1.0 ? :red : :steelblue for k in chirikov] @@ -212,10 +212,10 @@ function plot_driven_delta_prime(h5path; save_path=nothing) return plot(; title="No PE Δ' data — run with perturbed equilibrium enabled", legend=false) delta_prime, rational_psi, rational_q, rational_n = h5open(h5path, "r") do fid - read(fid[base * "Delta_prime"]), - read(fid[base * "rational_psi"]), - read(fid[base * "rational_q"]), - read(fid[base * "rational_n"]) + read(fid[base*"Delta_prime"]), + read(fid[base*"rational_psi"]), + read(fid[base*"rational_q"]), + read(fid[base*"rational_n"]) end p = plot(; xlabel="Norm. Poloidal Flux", ylabel="Re(Δ')", @@ -298,10 +298,10 @@ function _plot_resonant_current(h5path) return plot(; title="No resonant current data", legend=false) resonant_current, rational_psi, rational_q, rational_n = h5open(h5path, "r") do fid - read(fid[base * "resonant_current"]), - read(fid[base * "rational_psi"]), - read(fid[base * "rational_q"]), - read(fid[base * "rational_n"]) + read(fid[base*"resonant_current"]), + read(fid[base*"rational_psi"]), + read(fid[base*"rational_q"]), + read(fid[base*"rational_n"]) end p = plot(; xlabel="Norm. Poloidal Flux", ylabel="|I_res|", @@ -352,10 +352,10 @@ A `Plots.jl` plot object. """ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) comp_map = Dict( - :xi_psi => "xi_psi", - :b_psi => "b_psi_area_weighted", # area-normalized b^ψ + :xi_psi => "xi_psi", + :b_psi => "b_psi_area_weighted", # area-normalized b^ψ :b_theta => "b_theta", - :b_zeta => "b_zeta", + :b_zeta => "b_zeta" ) haskey(comp_map, component) || error("component must be one of :xi_psi, :b_psi, :b_theta, :b_zeta") @@ -439,9 +439,9 @@ Three-panel composite summary of perturbed equilibrium results: A `Plots.jl` plot object. """ function plot_perturbed_equilibrium_summary(h5path; save_path=nothing) - p_islands = plot_resonant_area_weighted_field_amplitude(h5path) - p_bpsi = _plot_bpsi_edge_spectrum(h5path) - p_spectro = plot_mode_spectrogram(h5path; component=:xi_psi) + p_islands = plot_resonant_area_weighted_field_amplitude(h5path) + p_bpsi = _plot_bpsi_edge_spectrum(h5path) + p_spectro = plot_mode_spectrogram(h5path; component=:xi_psi) l = @layout [grid(1, 2){0.35h}; b] p = plot(p_islands, p_bpsi, p_spectro; layout=l, size=(1100, 1100)) @@ -457,11 +457,11 @@ function _plot_bpsi_edge_spectrum(h5path) return plot(; title="No b_psi data — run with perturbed equilibrium enabled", legend=false) data, mlow, mhigh = h5open(h5path, "r") do fid - read(fid[base * "b_psi_area_weighted"]), + read(fid[base*"b_psi_area_weighted"]), read(fid["Info/mlow"]), read(fid["Info/mhigh"]) end - mpert = mhigh - mlow + 1 + mpert = mhigh - mlow + 1 m_vals = mlow:mhigh bpsi_edge = abs.(data[end, 1:mpert]) diff --git a/src/Analysis/PerturbedEquilibriumModes.jl b/src/Analysis/PerturbedEquilibriumModes.jl index 58aacdaf9..b63ac97e2 100644 --- a/src/Analysis/PerturbedEquilibriumModes.jl +++ b/src/Analysis/PerturbedEquilibriumModes.jl @@ -20,33 +20,37 @@ Reads all required metadata (mlow, nlow, mpert, npert) and spline data (ν) from the HDF5 file. # Arguments -- `h5_file::String`: Path to gpec.h5 output file -- `variable::String`: HDF5 dataset path, e.g. `"PerturbedEquilibrium/Response/xi_R"` + + - `h5_file::String`: Path to gpec.h5 output file + - `variable::String`: HDF5 dataset path, e.g. `"PerturbedEquilibrium/Response/xi_R"` # Keyword arguments -- `mtheta::Int`: theta grid resolution (default: `max(2*(|mlow|+mpert), 512)`) -- `keep_sfl_phi::Bool`: if `true` (default), output in SFL toroidal angle; - if `false`, apply `exp(i*n*ν(ψ,θ))` to convert to machine toroidal angle - and conjugate if `helicity > 0` (matches Fortran `gpout_xbrzphifun`) + + - `mtheta::Int`: theta grid resolution (default: `max(2*(|mlow|+mpert), 512)`) + - `keep_sfl_phi::Bool`: if `true` (default), output in SFL toroidal angle; + if `false`, apply `exp(i*n*ν(ψ,θ))` to convert to machine toroidal angle + and conjugate if `helicity > 0` (matches Fortran `gpout_xbrzphifun`) # Returns -- `theta_data::Array{ComplexF64,3}`: `[npsi × mtheta × npert]` -- `theta_grid::Vector{Float64}`: `[mtheta]` SFL theta ∈ [0, 1) -- `n_vals::Vector{Int}`: `[npert]` toroidal mode numbers + + - `theta_data::Array{ComplexF64,3}`: `[npsi × mtheta × npert]` + - `theta_grid::Vector{Float64}`: `[mtheta]` SFL theta ∈ [0, 1) + - `n_vals::Vector{Int}`: `[npert]` toroidal mode numbers !!! note + When used on the cylindrical components `xi_R`, `xi_Z`, `xi_phi`, `b_R`, `b_Z`, `b_phi` these are currently in beta and show up to ~20% discrepancies vs Fortran. """ function modes_to_theta(h5_file::String, variable::String; - mtheta::Union{Int,Nothing}=nothing, - keep_sfl_phi::Bool=true) + mtheta::Union{Int,Nothing}=nothing, + keep_sfl_phi::Bool=true) h5open(h5_file, "r") do f modes = read(f, variable) # (npsi, numpert_total) npsi, numpert_total = size(modes) - mlow = read(f, "Info/mlow") - nlow = read(f, "Info/nlow") + mlow = read(f, "Info/mlow") + nlow = read(f, "Info/nlow") mpert = read(f, "Info/mpert") npert = read(f, "Info/npert") @assert numpert_total == mpert * npert "Expected numpert_total=$(mpert*npert), got $numpert_total" @@ -58,7 +62,7 @@ function modes_to_theta(h5_file::String, variable::String; theta_data = zeros(ComplexF64, npsi, mth, npert) for k in 1:npert - col_range = (k-1)*mpert+1 : k*mpert + col_range = ((k-1)*mpert+1):(k*mpert) for ipsi in 1:npsi theta_data[ipsi, :, k] .= inverse(ft, view(modes, ipsi, col_range)) end @@ -68,7 +72,7 @@ function modes_to_theta(h5_file::String, variable::String; # Reconstruct ν spline from stored grid + nodal values (FastInterpolations v0.4 API) rzphi_xs = read(f, "Equilibrium/Geometry/psi") rzphi_ys = read(f, "Equilibrium/Geometry/theta") - nu_vals = read(f, "Equilibrium/Geometry/nu") + nu_vals = read(f, "Equilibrium/Geometry/nu") nu_spline = cubic_interp( (rzphi_xs, rzphi_ys), nu_vals; bc=(CubicFit(), PeriodicBC()), @@ -77,8 +81,8 @@ function modes_to_theta(h5_file::String, variable::String; psi_grid = read(f, "ForceFreeStates/Solutions/ForwardIntegration/psi") - bt_sign = haskey(f, "Equilibrium/B_T_sign") ? read(f, "Equilibrium/B_T_sign") : 1 - crnt = haskey(f, "Equilibrium/I_p") ? read(f, "Equilibrium/I_p") : 1.0 + bt_sign = haskey(f, "Equilibrium/B_T_sign") ? read(f, "Equilibrium/B_T_sign") : 1 + crnt = haskey(f, "Equilibrium/I_p") ? read(f, "Equilibrium/I_p") : 1.0 helicity = bt_sign * Int(sign(crnt)) hint = (Ref(1), Ref(1)) @@ -110,18 +114,21 @@ Extend theta-space data to a `(θ, φ)` grid via toroidal inverse DFT. f(θ, φ) = Σₙ fₙ(θ) exp(i·n·φ) # Arguments -- `theta_data::Array{ComplexF64,3}`: `[npsi × mtheta × npert]` from `modes_to_theta` -- `n_vals::Vector{Int}`: toroidal mode numbers + + - `theta_data::Array{ComplexF64,3}`: `[npsi × mtheta × npert]` from `modes_to_theta` + - `n_vals::Vector{Int}`: toroidal mode numbers # Keyword arguments -- `nphi::Int`: toroidal grid points (default: `max(4*maximum(abs.(n_vals)), 64)`) + + - `nphi::Int`: toroidal grid points (default: `max(4*maximum(abs.(n_vals)), 64)`) # Returns -- `full_data::Array{ComplexF64,3}`: `[npsi × mtheta × nphi]` -- `phi_grid::Vector{Float64}`: `[nphi]` in radians ∈ [0, 2π) + + - `full_data::Array{ComplexF64,3}`: `[npsi × mtheta × nphi]` + - `phi_grid::Vector{Float64}`: `[nphi]` in radians ∈ [0, 2π) """ function theta_to_thetaphi(theta_data::Array{ComplexF64,3}, n_vals::Vector{Int}; - nphi::Union{Int,Nothing}=nothing) + nphi::Union{Int,Nothing}=nothing) npsi, mth, npert = size(theta_data) np = isnothing(nphi) ? max(4 * maximum(abs.(n_vals)), 64) : nphi phi_grid = [(j - 1) * 2π / np for j in 1:np] diff --git a/src/Equilibrium/AnalyticEquilibrium.jl b/src/Equilibrium/AnalyticEquilibrium.jl index 72a16a05c..7ed653867 100644 --- a/src/Equilibrium/AnalyticEquilibrium.jl +++ b/src/Equilibrium/AnalyticEquilibrium.jl @@ -243,7 +243,7 @@ function tj_analytic_f1(x::Float64, nu::Float64, qc::Float64) if x < 0.1 x2 = x * x return x2 * (1 - (nu-1)*x2/2 + (nu-1)*(nu-2)*x2*x2/6 - - (nu-1)*(nu-2)*(nu-3)*x2*x2*x2/24) / qc + (nu-1)*(nu-2)*(nu-3)*x2*x2*x2/24) / qc else return (1 - (1 - x*x)^nu) / (nu * qc) end @@ -259,10 +259,10 @@ parameterization. function tj_analytic_f1p(x::Float64, nu::Float64, qc::Float64) if x < 0.1 x2 = x * x - return 2*x * (1 - (nu-1)*x2 + (nu-1)*(nu-2)*x2*x2/2 - - (nu-1)*(nu-2)*(nu-3)*x2*x2*x2/6) / qc + return 2 * x * (1 - (nu-1)*x2 + (nu-1)*(nu-2)*x2*x2/2 - + (nu-1)*(nu-2)*(nu-3)*x2*x2*x2/6) / qc else - return 2*x * (1 - x*x)^(nu-1) / qc + return 2 * x * (1 - x*x)^(nu-1) / qc end end @@ -273,10 +273,11 @@ GPEC adaptation of the analytic shape ODE used in R. Fitzpatrick's TJ code `tj_analytic_run_direct` call so both pipelines share identical numerics. Fields: + - physical: a, R0, qc, mu, pc, B0 - derived: epsa2 = (a/R0)² - near-axis BC constants: rmin, x0 = rmin, r0 = rmin·a, f1c = 1/qc, - p2ppc = d²p₂/dx²|_0 = −2·μ·pc + p2ppc = d²p₂/dx²|_0 = −2·μ·pc """ struct TJAnalyticShapeParams a::Float64 @@ -293,15 +294,15 @@ struct TJAnalyticShapeParams p2ppc::Float64 end -function TJAnalyticShapeParams(tj::TJAnalyticConfig; rmin::Float64 = 1e-4) +function TJAnalyticShapeParams(tj::TJAnalyticConfig; rmin::Float64=1e-4) a, R0 = tj.lar_a, tj.lar_r0 - mu = max(tj.mu, 1.001) + mu = max(tj.mu, 1.001) return TJAnalyticShapeParams( a, R0, tj.qc, mu, tj.pc, tj.B0, (a / R0)^2, rmin, rmin, rmin * a, 1.0 / tj.qc, - -2.0 * mu * tj.pc, + -2.0 * mu * tj.pc ) end @@ -315,9 +316,9 @@ The params argument carries TJAnalyticShapeParams fields plus the current `nu`. """ function tj_analytic_shape_rhs!(dy, y, params, r) (; a, B0, qc, mu, pc, epsa2, nu) = params - x = r / a + x = r / a xfac = max(1 - x^2, 0.0) - f1 = tj_analytic_f1(x, nu, qc) + f1 = tj_analytic_f1(x, nu, qc) f1px = tj_analytic_f1p(x, nu, qc) p2px = -2 * mu * pc * x * xfac^(mu - 1) @@ -340,16 +341,19 @@ function tj_analytic_shape_rhs!(dy, y, params, r) # f₃'(x) for Hₙ = Vₙ = 0 (n ≥ 2 harmonics rescaled to zero, as in the # TJ-analytic benchmark configuration of Fitzpatrick's TJ code). g2, f3 = y[2], y[5] - f3p_x = -f3 * f1px / f1 - - f1 * (3 * x^2 / 2 - 2 * x * H1p + H1p^2) / x + - f1px * (g2 - 3 * x^2 / 4 + H1 + 3 * H1p^2 / 2) + - x^2 * p2px * (g2 + x^2 / 2 - 3 * x * H1p - 2 * H1) / f1 + f3p_x = + -f3 * f1px / f1 - + f1 * (3 * x^2 / 2 - 2 * x * H1p + H1p^2) / x + + f1px * (g2 - 3 * x^2 / 4 + H1 + 3 * H1p^2 / 2) + + x^2 * p2px * (g2 + x^2 / 2 - 3 * x * H1p - 2 * H1) / f1 dy[5] = f3p_x / a return nothing end -"""Initial conditions at x = x0, matching the TJ-analytic model's near-axis -expansion (cf. R. Fitzpatrick's TJ code, https://github.com/rfitzp/TJ).""" +""" +Initial conditions at x = x0, matching the TJ-analytic model's near-axis +expansion (cf. R. Fitzpatrick's TJ code, https://github.com/rfitzp/TJ). +""" function tj_analytic_shape_initial(p::TJAnalyticShapeParams, nu::Float64) f1_0 = tj_analytic_f1(p.x0, nu, p.qc) y0 = zeros(5) @@ -368,14 +372,14 @@ downstream Hₙ / ψ splines sit on uniform nodes); leave it `nothing` for the default adaptive save pattern used by `tj_analytic_run`. """ function tj_analytic_shape_solve(p::TJAnalyticShapeParams, nu::Float64; - reltol::Float64 = 1e-7, abstol::Float64 = 1e-8, - saveat = nothing) - rhs_params = (; p.a, p.B0, p.qc, p.mu, p.pc, p.epsa2, nu = nu) + reltol::Float64=1e-7, abstol::Float64=1e-8, + saveat=nothing) + rhs_params = (; p.a, p.B0, p.qc, p.mu, p.pc, p.epsa2, nu=nu) prob = ODEProblem(tj_analytic_shape_rhs!, tj_analytic_shape_initial(p, nu), (p.r0, p.a), rhs_params) if saveat === nothing - return solve(prob, Vern9(); reltol, abstol, maxiters = 10000, dense = false) + return solve(prob, Vern9(); reltol, abstol, maxiters=10000, dense=false) else - return solve(prob, Vern9(); reltol, abstol, maxiters = 10000, saveat = saveat) + return solve(prob, Vern9(); reltol, abstol, maxiters=10000, saveat=saveat) end end @@ -389,9 +393,9 @@ O(εa²) correction relative to the lowest-order guess ν = qa/qc, which matters for the TJ-analytic benchmark at large ε. Falls back to the lowest-order ν if the bracket search diverges. """ -function tj_analytic_find_nu(p::TJAnalyticShapeParams, qa_target::Float64; reltol::Float64 = 1e-7) +function tj_analytic_find_nu(p::TJAnalyticShapeParams, qa_target::Float64; reltol::Float64=1e-7) function q2_edge(nu::Float64) - sol = tj_analytic_shape_solve(p, nu; reltol) + sol = tj_analytic_shape_solve(p, nu; reltol) g2end = sol.u[end][2] f3end = sol.u[end][5] f1end = tj_analytic_f1(1.0, nu, p.qc) @@ -400,7 +404,7 @@ function tj_analytic_find_nu(p::TJAnalyticShapeParams, qa_target::Float64; relto nu_guess = qa_target / p.qc return try find_zero(nu -> q2_edge(nu) - qa_target, (0.5 * nu_guess, 2 * nu_guess); - atol = 1e-8, rtol = 1e-10) + atol=1e-8, rtol=1e-10) catch err @warn "ν root-find failed for TJ-analytic equilibrium; falling back to lowest-order ν = qa/qc" error = err nu_guess @@ -443,16 +447,16 @@ included; they are zero in the TJ-analytic benchmark scans. Reference: R. Fitzpatrick, TJ code, https://github.com/rfitzp/TJ """ function tj_analytic_run(equil_input::EquilibriumConfig, tj::TJAnalyticConfig) - a, R0 = tj.lar_a, tj.lar_r0 + a, R0 = tj.lar_a, tj.lar_r0 qc, mu = tj.qc, max(tj.mu, 1.001) pc, B0 = tj.pc, tj.B0 ma, mtau = tj.ma, tj.mtau p = TJAnalyticShapeParams(tj) - epsa2 = p.epsa2 - p00_phys = B0^2 * epsa2 * pc # μ₀P = B₀²·εa²·p₂ at axis + epsa2 = p.epsa2 + p00_phys = B0^2 * epsa2 * pc # μ₀P = B₀²·εa²·p₂ at axis - nu = tj_analytic_find_nu(p, tj.qa; reltol = equil_input.etol) - sol = tj_analytic_shape_solve(p, nu; reltol = equil_input.etol) + nu = tj_analytic_find_nu(p, tj.qa; reltol=equil_input.etol) + sol = tj_analytic_shape_solve(p, nu; reltol=equil_input.etol) r_arr = sol.t y_mat = reduce(hcat, sol.u)' @@ -468,7 +472,7 @@ function tj_analytic_run(equil_input::EquilibriumConfig, tj::TJAnalyticConfig) xfac = max(1 - x^2, 0.0) f1 = tj_analytic_f1(x, nu, qc) - ψ = y_mat[i, 1] + ψ = y_mat[i, 1] g2 = y_mat[i, 2] H1 = y_mat[i, 3] f3 = y_mat[i, 5] @@ -507,7 +511,7 @@ function tj_analytic_run(equil_input::EquilibriumConfig, tj::TJAnalyticConfig) f = spl(r; hint=hint) # f[1]=F, f[2]=P, f[3]=q, f[4]=ψ, f[5]=g₂, f[6]=H₁ - sq_xs[ia] = f[4] / psio + sq_xs[ia] = f[4] / psio sq_fs[ia, 1] = f[1] # F sq_fs[ia, 2] = f[2] # P sq_fs[ia, 3] = f[3] # q @@ -525,7 +529,7 @@ function tj_analytic_run(equil_input::EquilibriumConfig, tj::TJAnalyticConfig) for itau in 1:(mtau+1) θ = 2π * (itau - 1) / mtau rzphi_fs_nodes[ia, itau, 1] = R0 + Δ + α * r * cos(θ) - rzphi_fs_nodes[ia, itau, 2] = α * r * sin(θ) + rzphi_fs_nodes[ia, itau, 2] = α * r * sin(θ) end end @@ -579,38 +583,38 @@ ODE (g₂, H₁, H₁', f₃), the `GetPSIvac` / `GetHHvac` vacuum extension, an EFIT-writer (R, Z) → (r, w) Newton inversion that this routine adapts. """ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticConfig; - nrbox::Int = 257, nzbox::Int = 257, rc::Float64 = 1.2) - a, R0 = tj.lar_a, tj.lar_r0 + nrbox::Int=257, nzbox::Int=257, rc::Float64=1.2) + a, R0 = tj.lar_a, tj.lar_r0 qc, mu = tj.qc, max(tj.mu, 1.001) pc, B0 = tj.pc, tj.B0 p = TJAnalyticShapeParams(tj) epsa, epsa2 = p.a / p.R0, p.epsa2 - p00_phys = B0^2 * epsa2 * pc + p00_phys = B0^2 * epsa2 * pc # ν root-find (cf. Fitzpatrick TJ's Setnu): q₂(1) = qa_target. - nu = tj_analytic_find_nu(p, tj.qa; reltol = equil_input.etol) + nu = tj_analytic_find_nu(p, tj.qa; reltol=equil_input.etol) # Dense saveat so the downstream splines (H₁, g₂, f₃, ψ) are evaluated on # a fine uniform r grid rather than the ~30 adaptive Vern9 steps — otherwise # the (R, Z) → (r, w) Newton iteration hits spline interpolation artifacts. - dense_r = collect(range(p.r0, p.a; length = 1024)) - sol = tj_analytic_shape_solve(p, nu; reltol = equil_input.etol, - abstol = 1e-10, saveat = dense_r) - r_arr = sol.t - y_mat = reduce(hcat, sol.u)' + dense_r = collect(range(p.r0, p.a; length=1024)) + sol = tj_analytic_shape_solve(p, nu; reltol=equil_input.etol, + abstol=1e-10, saveat=dense_r) + r_arr = sol.t + y_mat = reduce(hcat, sol.u)' # Radial splines in the TJ-analytic dimensionless x = r/a on a clean grid for H₁ etc. x_nodes = r_arr ./ a - ψ_of_r = cubic_interp(r_arr, y_mat[:, 1]; extrap=ExtendExtrap()) - H1_of_x = cubic_interp(x_nodes, y_mat[:, 3]; extrap=ExtendExtrap()) + ψ_of_r = cubic_interp(r_arr, y_mat[:, 1]; extrap=ExtendExtrap()) + H1_of_x = cubic_interp(x_nodes, y_mat[:, 3]; extrap=ExtendExtrap()) H1p_of_x = cubic_interp(x_nodes, y_mat[:, 4]; extrap=ExtendExtrap()) - g2_of_x = cubic_interp(x_nodes, y_mat[:, 2]; extrap=ExtendExtrap()) - f3_of_x = cubic_interp(x_nodes, y_mat[:, 5]; extrap=ExtendExtrap()) + g2_of_x = cubic_interp(x_nodes, y_mat[:, 2]; extrap=ExtendExtrap()) + f3_of_x = cubic_interp(x_nodes, y_mat[:, 5]; extrap=ExtendExtrap()) # Edge values needed by GetPSIvac - f1a = tj_analytic_f1(1.0, nu, qc) - f3a = f3_of_x(1.0) - H1a = H1_of_x(1.0) + f1a = tj_analytic_f1(1.0, nu, qc) + f3a = f3_of_x(1.0) + H1a = H1_of_x(1.0) H1ap = H1p_of_x(1.0) psio = ψ_of_r(a) # ψ at r = a (boundary) @@ -644,7 +648,7 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo return f_R_shift(rc - 1e-8, w) * r^2 / rc^2 end H1 = (r < 1.0) ? H1_of_x(r) : H1_vac(r) - L = r^3 / 8 - r * H1 / 2 + L = r^3 / 8 - r * H1 / 2 return epsa2 * H1 + epsa2 * epsa * L * cos(w) end function f_Z_shift(r::Float64, w::Float64) @@ -652,7 +656,7 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo return f_Z_shift(rc - 1e-8, w) * r^2 / rc^2 end H1 = (r < 1.0) ? H1_of_x(r) : H1_vac(r) - L = r^3 / 8 - r * H1 / 2 + L = r^3 / 8 - r * H1 / 2 return -epsa2 * epsa * L * sin(w) end @@ -699,9 +703,9 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo # Grid spans R₀ ± rc·a × ±rc·a (where rc is the vacuum-shell radius in # units of a), giving a comfortable margin for the separatrix finder. r_span = rc * a - psi_in_xs = collect(range(R0 - r_span, R0 + r_span; length = nrbox)) - psi_in_ys = collect(range(-r_span, r_span; length = nzbox)) - psi_rz = zeros(Float64, nrbox, nzbox) + psi_in_xs = collect(range(R0 - r_span, R0 + r_span; length=nrbox)) + psi_in_ys = collect(range(-r_span, r_span; length=nzbox)) + psi_rz = zeros(Float64, nrbox, nzbox) for i in 1:nrbox, j in 1:nzbox R_norm = psi_in_xs[i] / R0 @@ -724,8 +728,10 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo # 1D profile spline, same layout as read_efit (4 columns). Use the # TJ-analytic q₂ on the radial grid so that the prescribed q is # consistent with the ψ(R,Z) we just constructed. - psi_norm_grid = range(0.0, 1.0; length = nrbox) - F_nodes = zeros(nrbox); P_nodes = zeros(nrbox); q_nodes = zeros(nrbox) + psi_norm_grid = range(0.0, 1.0; length=nrbox) + F_nodes = zeros(nrbox) + P_nodes = zeros(nrbox) + q_nodes = zeros(nrbox) for i in 1:nrbox ψN = psi_norm_grid[i] # Invert ψN = (ψ_plasma(r) - 0) / psio ⇒ find r such that ψ_plasma(r) = ψN·psio. @@ -746,7 +752,7 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo F_nodes[i] = R0 * B0 * (1 + epsa2 * g2_val) P_nodes[i] = p00_phys * xfac^mu q_nodes[i] = (x > 1e-10) ? x^2 * (1 + epsa2 * g2_val) * - exp(-epsa2 * f3_val / f1) / f1 : qc + exp(-epsa2 * f3_val / f1) / f1 : qc end sq_fs_nodes = hcat(F_nodes, P_nodes, q_nodes, sqrt.(collect(psi_norm_grid))) sq_in = cubic_interp(collect(psi_norm_grid), Series(sq_fs_nodes); extrap=ExtendExtrap()) @@ -756,7 +762,7 @@ function tj_analytic_run_direct(equil_input::EquilibriumConfig, tj::TJAnalyticCo # Analytic: ingest=nothing — replay regenerates from the [TJ_ANALYTIC_INPUT] TOML section. return DirectRunInput(equil_input, sq_in, psi_in, psi_in_xs, psi_in_ys, - rmin_grid, rmax_grid, zmin_grid, zmax_grid, psio, 1, nothing) + rmin_grid, rmax_grid, zmin_grid, zmax_grid, psio, 1, nothing) end """ diff --git a/src/Equilibrium/DirectEquilibriumArcLength.jl b/src/Equilibrium/DirectEquilibriumArcLength.jl index 32b93a0ac..f247da7a1 100644 --- a/src/Equilibrium/DirectEquilibriumArcLength.jl +++ b/src/Equilibrium/DirectEquilibriumArcLength.jl @@ -64,11 +64,11 @@ end Arc-length-parameterized flux surface integration. Drop-in replacement for `direct_fieldline_int` with identical return format: -- `y_out[:, 1]`: geometric angle η ∈ 0 to 2π (CCW from outboard midplane) -- `y_out[:, 2]`: accumulated ∫dl/Bp -- `y_out[:, 3]`: rfac = √((R−ro)² + (Z−zo)²) -- `y_out[:, 4]`: accumulated ∫dl/(R²Bp) -- `y_out[:, 5]`: accumulated ∫jac·dl/Bp + - `y_out[:, 1]`: geometric angle η ∈ 0 to 2π (CCW from outboard midplane) + - `y_out[:, 2]`: accumulated ∫dl/Bp + - `y_out[:, 3]`: rfac = √((R−ro)² + (Z−zo)²) + - `y_out[:, 4]`: accumulated ∫dl/(R²Bp) + - `y_out[:, 5]`: accumulated ∫jac·dl/Bp The ODE is terminated by a `ContinuousCallback` that detects the return to the outboard midplane (Z = zo, R > ro) after a minimum arc-length guard. @@ -144,4 +144,3 @@ outboard midplane (Z = zo, R > ro) after a minimum arc-length guard. # bfield at the starting point carries F and P for the surface-averaged quantities return y_out, bfield end - diff --git a/src/Equilibrium/Equilibrium.jl b/src/Equilibrium/Equilibrium.jl index eed157812..6b55898ad 100644 --- a/src/Equilibrium/Equilibrium.jl +++ b/src/Equilibrium/Equilibrium.jl @@ -330,10 +330,10 @@ function equilibrium_global_parameters!(pe::PlasmaEquilibrium) P_vals = profiles.P_spline.y dVdpsi_vals = profiles.dVdpsi_spline.y - fsi_pdv = fsi(P_vals .* dVdpsi_vals) # ∫ p dV/dψ - fsi_dv = fsi(dVdpsi_vals) # ∫ dV/dψ + fsi_pdv = fsi(P_vals .* dVdpsi_vals) # ∫ p dV/dψ + fsi_dv = fsi(dVdpsi_vals) # ∫ dV/dψ fsi_p2dv = fsi(P_vals .^ 2 .* dVdpsi_vals) # ∫ p² dV/dψ - volume = fsi_dv # same integrand as hs col 2 in Fortran + volume = fsi_dv # same integrand as hs col 2 in Fortran # Poloidal-field surface integral hs_bp2(ψ) = ψ₀² ∮dθ |∇ψ|² / (R² J). # This is Fortran equil_out.f's hs%fs(:,3) and is the correct integrand for @@ -343,21 +343,21 @@ function equilibrium_global_parameters!(pe::PlasmaEquilibrium) for ipsi in 0:mpsi acc = 0.0 for itheta in 0:mtheta - r2 = pe.rzphi_rsquared.nodal_derivs.partials[1, ipsi+1, itheta+1] - offset = pe.rzphi_offset.nodal_derivs.partials[1, ipsi+1, itheta+1] - jac = pe.rzphi_jac.nodal_derivs.partials[1, ipsi+1, itheta+1] - r2_y = pe.rzphi_rsquared.nodal_derivs.partials[3, ipsi+1, itheta+1] - offset_y = pe.rzphi_offset.nodal_derivs.partials[3, ipsi+1, itheta+1] + r2 = pe.rzphi_rsquared.nodal_derivs.partials[1, ipsi+1, itheta+1] + offset = pe.rzphi_offset.nodal_derivs.partials[1, ipsi+1, itheta+1] + jac = pe.rzphi_jac.nodal_derivs.partials[1, ipsi+1, itheta+1] + r2_y = pe.rzphi_rsquared.nodal_derivs.partials[3, ipsi+1, itheta+1] + offset_y = pe.rzphi_offset.nodal_derivs.partials[3, ipsi+1, itheta+1] jacfac = π / jac - rfac = sqrt(r2) - eta = 2π * (pe.rzphi_ys[itheta+1] + offset) - r = pe.ro + rfac * cos(eta) - v21 = jacfac * r2_y / (2π * rfac) - v22 = jacfac * (1 + offset_y) * (2 * rfac) - v33 = jacfac * 2π * (r / π) - dvsq = (v21^2 + v22^2) * (v33 * jac^2)^2 - acc += dvsq / (r^2) / jac + rfac = sqrt(r2) + eta = 2π * (pe.rzphi_ys[itheta+1] + offset) + r = pe.ro + rfac * cos(eta) + v21 = jacfac * r2_y / (2π * rfac) + v22 = jacfac * (1 + offset_y) * (2 * rfac) + v33 = jacfac * 2π * (r / π) + dvsq = (v21^2 + v22^2) * (v33 * jac^2)^2 + acc += dvsq / (r^2) / jac end # Periodic trapezoidal rule on uniform θ grid reduces to a plain mean # because the first and last grid points coincide — matches the int1/int2 @@ -367,15 +367,15 @@ function equilibrium_global_parameters!(pe::PlasmaEquilibrium) fsi_bp2 = fsi(hs_bp2) p0 = P_vals[1] - profiles.P_deriv(profiles.xs[1]; hint=Ref(1)) * profiles.xs[1] # linear extrapolation - betat = 2 * (fsi_pdv / fsi_dv) / bt0^2 - betaj = 2 * sqrt(fsi_p2dv / fsi_dv) / bwall^2 - betan = 100 * amean * bt0 * betat / crnt + betat = 2 * (fsi_pdv / fsi_dv) / bt0^2 + betaj = 2 * sqrt(fsi_p2dv / fsi_dv) / bwall^2 + betan = 100 * amean * bt0 * betat / crnt betap1 = 2 * (fsi_pdv / fsi_dv) / bp0^2 betap2 = 4 * fsi_pdv / ((1e6 * mu0 * crnt)^2 * pe.ro) betap3 = 4 * fsi_pdv / ((1e6 * mu0 * crnt)^2 * rmean) - li1 = fsi_bp2 / fsi_dv / bp0^2 - li2 = 2 * fsi_bp2 / ((1e6 * mu0 * crnt)^2 * pe.ro) - li3 = 2 * fsi_bp2 / ((1e6 * mu0 * crnt)^2 * rmean) + li1 = fsi_bp2 / fsi_dv / bp0^2 + li2 = 2 * fsi_bp2 / ((1e6 * mu0 * crnt)^2 * pe.ro) + li3 = 2 * fsi_bp2 / ((1e6 * mu0 * crnt)^2 * rmean) pe.params.psi0 = psio pe.params.psi_axis = pe.psio diff --git a/src/Equilibrium/EquilibriumTypes.jl b/src/Equilibrium/EquilibriumTypes.jl index 344f5a60f..d6d8e16b8 100644 --- a/src/Equilibrium/EquilibriumTypes.jl +++ b/src/Equilibrium/EquilibriumTypes.jl @@ -245,7 +245,9 @@ A mutable struct holding parameters for the Large Aspect Ratio (LAR) plasma equi zeroth::Bool = false end -"Build a `LargeAspectRatioConfig` from a parsed `[LAR_INPUT]` TOML table." +""" +Build a `LargeAspectRatioConfig` from a parsed `[LAR_INPUT]` TOML table. +""" function LargeAspectRatioConfig(input_dict::Dict{String,Any}) return LargeAspectRatioConfig(; symbolize_keys(input_dict)...) end @@ -285,7 +287,9 @@ Reference: R. Fitzpatrick, TJ code, https://github.com/rfitzp/TJ zeroth::Bool = false # If true, suppress Shafranov shift end -"Build a `TJAnalyticConfig` from a parsed `[TJ_ANALYTIC_INPUT]` TOML table." +""" +Build a `TJAnalyticConfig` from a parsed `[TJ_ANALYTIC_INPUT]` TOML table. +""" function TJAnalyticConfig(input_dict::Dict{String,Any}) return TJAnalyticConfig(; symbolize_keys(input_dict)...) end @@ -321,7 +325,9 @@ A mutable struct holding parameters for the Solev'ev (SOL) plasma equilibrium mo f0fac::Float64 = 1 # scale toroidal field at constant pressure (s*f. beta,q changes. Phi,p,bp constant) end -"Build a `SolovevConfig` from a parsed `[SOL_INPUT]` TOML table." +""" +Build a `SolovevConfig` from a parsed `[SOL_INPUT]` TOML table. +""" function SolovevConfig(input_dict::Dict{String,Any}) return SolovevConfig(; symbolize_keys(input_dict)...) end @@ -419,6 +425,7 @@ raw equilibrium data and preparing the initial splines. 2. `μ₀ * Pressure` — plasma pressure (non-negative) [T²] 3. `q` — safety factor profile 4. `√ψ_norm` — square root of normalized flux + - `psi_in` 2D cubic interpolant on the (R, Z) grid [m]. The values correspond to the **poloidal flux** adjusted to be zero at the boundary [Wb/rad]. @@ -430,14 +437,23 @@ raw equilibrium data and preparing the initial splines. * 1D profiles are represented by `CubicInterpolant` or `CubicSeriesInterpolant` * 2D flux surfaces by `CubicInterpolantND` + - `psi_in_xs::Vector{Float64}` — R coordinate grid for psi_in [m] + - `psi_in_ys::Vector{Float64}` — Z coordinate grid for psi_in [m] + - `rmin::Float64` — Minimum R-coordinate of the computational grid [m] + - `rmax::Float64` — Maximum R-coordinate of the computational grid [m] + - `zmin::Float64` — Minimum Z-coordinate of the computational grid [m] + - `zmax::Float64` — Maximum Z-coordinate of the computational grid [m] + - `psio::Float64` — Total flux difference `|ψ_axis - ψ_boundary|` [Wb/rad] + - `bt_sign::Int` — Sign of the toroidal field (+1 or -1); read from fpol sign in EFIT g-files + - `ingest::EquilibriumIngest` — captured raw arrays for the `gpec.h5` rerun snapshot (a [`DirectIngest`](@ref) for file-based reads, or `nothing` for analytic equilibria) - `psihigh_resolved::Float64` — outer flux limit the equilibrium is formed on: `config.psihigh` @@ -749,7 +765,7 @@ function GeometryProfileSplines(xs::Vector{Float64}, GeometryProfileSplines{typeof(area_spline)}( xs, npts, npts - 1, - area_spline, avg_r_spline, avg_R_spline, + area_spline, avg_r_spline, avg_R_spline ) end @@ -828,7 +844,7 @@ function KineticProfileSplines(xs::Vector{Float64}, xs, npts, npts - 1, ni_spline, ne_spline, Ti_spline, Te_spline, omegaE_spline, loglam_spline, nui_spline, nue_spline, zeff_spline, - ni_deriv, ne_deriv, Ti_deriv, Te_deriv, + ni_deriv, ne_deriv, Ti_deriv, Te_deriv ) end @@ -845,17 +861,21 @@ This object provides a complete representation of the processed plasma equilibri - `params::EquilibriumParameters`: Computed equilibrium parameters and diagnostics. + - `profiles::ProfileSplines`: Named 1D profile splines (F, P, dV/dψ, q) on normalized psi grid. Access values at grid points via `profiles.F_spline.y[i]`, etc. Access derivatives via `profiles.F_deriv.y[i]` or `profiles.F_deriv(psi)`. + - `geometry::GeometryProfileSplines`: Named 1D splines for flux-surface-averaged geometry (area, ⟨r⟩, ⟨R⟩), populated automatically by `compute_geometry_profiles` during construction. + - **Grid coordinates (shared by all rzphi/eqfun interpolants):** + `rzphi_xs::Vector{Float64}`: ψ coordinates (length mpsi+1) + `rzphi_ys::Vector{Float64}`: θ coordinates (length mtheta+1) + - **Geometric quantities (rzphi, 4 interpolants):** 2D cubic interpolants for flux-coordinate mapping with periodic BC in theta. @@ -865,6 +885,7 @@ This object provides a complete representation of the processed plasma equilibri + `rzphi_offset::CubicInterpolantND`: η/(2π) - θₙₑw (angle offset) + `rzphi_nu::CubicInterpolantND`: ν in ϕ = 2πζ + ν(ψ, θ) + `rzphi_jac::CubicInterpolantND`: Jacobian + - **Physics quantities (eqfun, 3 interpolants):** 2D cubic interpolants storing local physics and geometric quantities. @@ -873,9 +894,13 @@ This object provides a complete representation of the processed plasma equilibri + `eqfun_B::CubicInterpolantND`: Total magnetic field strength [T] + `eqfun_metric1::CubicInterpolantND`: (e₁⋅e₂ + q⋅e₃⋅e₁)/(J⋅B²) + `eqfun_metric2::CubicInterpolantND`: (e₂⋅e₃ + q⋅e₃⋅e₃)/(J⋅B²) + - `ro::Float64`: R-coordinate of the magnetic axis [m] + - `zo::Float64`: Z-coordinate of the magnetic axis [m] + - `psio::Float64`: Total flux difference |Ψ_axis - Ψ_boundary| [Weber/radian] + - `ingest::EquilibriumIngest`: raw arrays forwarded from the equilibrium input for the `gpec.h5` rerun snapshot — a [`DirectIngest`](@ref)/[`InverseIngest`](@ref) for file-based equilibria, or `nothing` for analytic ones (regenerated from their TOML section on replay) diff --git a/src/Equilibrium/GeometryProfiles.jl b/src/Equilibrium/GeometryProfiles.jl index a9596c308..9b414cf83 100644 --- a/src/Equilibrium/GeometryProfiles.jl +++ b/src/Equilibrium/GeometryProfiles.jl @@ -27,7 +27,7 @@ function compute_geometry_profiles( rzphi_rsquared::FastInterpolations.CubicInterpolantND, rzphi_offset::FastInterpolations.CubicInterpolantND, rzphi_jac::FastInterpolations.CubicInterpolantND, - ro::Float64, + ro::Float64 ) npsi = length(rzphi_xs) mthsurf = length(rzphi_ys) - 1 # skip the periodic duplicate at θ = 1 @@ -78,6 +78,6 @@ function compute_geometry_profiles(pe::PlasmaEquilibrium) return compute_geometry_profiles( pe.rzphi_xs, pe.rzphi_ys, pe.rzphi_rsquared, pe.rzphi_offset, pe.rzphi_jac, - pe.ro, + pe.ro ) end diff --git a/src/Equilibrium/ReadEquilibrium.jl b/src/Equilibrium/ReadEquilibrium.jl index 8cc221b7a..068814505 100644 --- a/src/Equilibrium/ReadEquilibrium.jl +++ b/src/Equilibrium/ReadEquilibrium.jl @@ -432,15 +432,17 @@ Load an equilibrium from an IMAS data dictionary and return a `DirectRunInput`. The `dd.equilibrium.time_slice[]` is used (active time slice). Poloidal flux is converted from the IMAS COCOS convention (set by `config.imas_cocos`) to the internal COCOS 2 convention: + - `imas_cocos = 11` (default, IMAS standard): divide ψ by 2π - `imas_cocos = 2` (GPEC internal): no conversion ## Arguments -- `config`: `EquilibriumConfig` with `eq_type = "imas"` and `imas_cocos` set. -- `dd`: populated `IMASdd.dd` with `dd.equilibrium.time_slice[]` containing: - - `global_quantities.psi_axis`, `global_quantities.psi_boundary` - - `profiles_1d.psi`, `profiles_1d.f`, `profiles_1d.pressure`, `profiles_1d.q` - - `profiles_2d[1].grid.dim1` (R), `profiles_2d[1].grid.dim2` (Z), `profiles_2d[1].psi` + + - `config`: `EquilibriumConfig` with `eq_type = "imas"` and `imas_cocos` set. + - `dd`: populated `IMASdd.dd` with `dd.equilibrium.time_slice[]` containing: + + `global_quantities.psi_axis`, `global_quantities.psi_boundary` + + `profiles_1d.psi`, `profiles_1d.f`, `profiles_1d.pressure`, `profiles_1d.q` + + `profiles_2d[1].grid.dim1` (R), `profiles_2d[1].grid.dim2` (Z), `profiles_2d[1].psi` """ function read_imas(config::EquilibriumConfig, dd) @info "Processing IMAS equilibrium at global_time = $(dd.global_time) s" diff --git a/src/ForceFreeStates/EulerLagrange.jl b/src/ForceFreeStates/EulerLagrange.jl index 88c1d9e4a..3ed691261 100644 --- a/src/ForceFreeStates/EulerLagrange.jl +++ b/src/ForceFreeStates/EulerLagrange.jl @@ -37,9 +37,9 @@ end # at the interval endpoints. Coefficients are ported from STRIDE's ode_itime cost model # (Fortran reference) and unchanged here. Tune only after re-fitting against a per-chunk # step-count sweep; touching these affects parallel-chunk load balancing. -const ODE_COST_AXIS = (a = 39695.0, b = 212830.0) -const ODE_COST_RAT = (a = 17147.0, b = 470710.0) -const ODE_COST_EDGE = (a = 1646.0, b = 4683.0) +const ODE_COST_AXIS = (a=39695.0, b=212830.0) +const ODE_COST_RAT = (a=17147.0, b=470710.0) +const ODE_COST_EDGE = (a=1646.0, b=4683.0) """ ode_itime_cost(psi1, psi2, intr) -> Float64 @@ -72,7 +72,7 @@ never from `Threads.nthreads()` — so the chunk list, and hence every Riccati o identical whatever thread count `julia -t` provides. Each split finds the equal-cost midpoint ψ_mid via bisection: - ode_itime_cost(psi_start, psi_mid) ≈ ode_itime_cost(psi_start, psi_end) / 2 +ode_itime_cost(psi_start, psi_mid) ≈ ode_itime_cost(psi_start, psi_end) / 2 Sub-chunks inherit `needs_crossing=false` and `ising=0`. Only the LAST sub-chunk of each original chunk retains `needs_crossing=true` and the original `ising`, so the @@ -131,10 +131,10 @@ function balance_integration_chunks(chunks::Vector{IntegrationChunk}, ctrl::Forc psi_mid = (lo + hi) / 2.0 left = IntegrationChunk(; psi_start=chunk.psi_start, psi_end=psi_mid, - needs_crossing=false, ising=0, direction=1) + needs_crossing=false, ising=0, direction=1) right = IntegrationChunk(; psi_start=psi_mid, psi_end=chunk.psi_end, - needs_crossing=chunk.needs_crossing, ising=chunk.ising, - direction=chunk.direction) + needs_crossing=chunk.needs_crossing, ising=chunk.ising, + direction=chunk.direction) splice!(result, best_idx, [left, right]) end @@ -284,20 +284,20 @@ is identified by dominant |U₁| component, giving the physically correct consta Frobenius solution and avoiding the spurious logarithmic irregularity. """ function compute_axis_init(ffit::FourFitVars, profiles::Equilibrium.ProfileSplines, - intr::ForceFreeStatesInternal, psi_low::Float64) - N = intr.numpert_total + intr::ForceFreeStatesInternal, psi_low::Float64) + N = intr.numpert_total hint = Ref(1) # Evaluate stability matrices at psi_low F_lower = zeros(ComplexF64, N, N) - kmat = zeros(ComplexF64, N, N) - gmat = zeros(ComplexF64, N, N) + kmat = zeros(ComplexF64, N, N) + gmat = zeros(ComplexF64, N, N) ffit.fmats_lower(vec(F_lower), psi_low; hint=hint) - ffit.kmats(vec(kmat), psi_low; hint=hint) - ffit.gmats(vec(gmat), psi_low; hint=hint) + ffit.kmats(vec(kmat), psi_low; hint=hint) + ffit.gmats(vec(gmat), psi_low; hint=hint) # singfac[j] = 1 / (m_j − n_j · q) for each mode j - q0 = profiles.q_spline(psi_low; hint=hint) + q0 = profiles.q_spline(psi_low; hint=hint) singfac = vec(1.0 ./ ((intr.mlow:intr.mhigh) .- q0 .* (intr.nlow:intr.nhigh)')) # F̄⁻¹ = (F_lower · F_lower')⁻¹ via the Cholesky factor @@ -311,15 +311,15 @@ function compute_axis_init(ffit::FourFitVars, profiles::Equilibrium.ProfileSplin for j in 1:N sf = singfac[j] fi = Finv[j, j] - k = kmat[j, j] + k = kmat[j, j] kd = conj(k) # K̄†[j,j] - g = gmat[j, j] + g = gmat[j, j] # 2×2 ODE matrix block for mode j [Glasser 2016 Eq. 22-24, diagonal approximation] m11 = -sf * fi * k - m12 = sf^2 * fi - m21 = g - kd * fi * k - m22 = sf * kd * fi + m12 = sf^2 * fi + m21 = g - kd * fi * k + m22 = sf * kd * fi # Frobenius matrix A₀_j = ψ_low · M_j [Glasser 2016 Eq. 51] #! format: off @@ -346,7 +346,7 @@ function compute_axis_init(ffit::FourFitVars, profiles::Equilibrium.ProfileSplin if abs(v2) > Base.sqrt(Base.eps(Float64)) * abs(v1) U1_init[j, j] = v1 / v2 else - U1_init[j, j] = one(ComplexF64) + U1_init[j, j] = one(ComplexF64) U2_init[j, j] = zero(ComplexF64) end end @@ -365,7 +365,7 @@ Formerly `ode_axis_init!`. This now only initializes `psifac`, `ising_start`, an Move ising_start logic to chunk_el_integration_bounds? """ function initialize_el_at_axis!(odet::OdeState, ctrl::ForceFreeStatesControl, ffit::FourFitVars, - profiles::Equilibrium.ProfileSplines, intr::ForceFreeStatesInternal) + profiles::Equilibrium.ProfileSplines, intr::ForceFreeStatesInternal) # Default psifac to minimum equilibrium psi value odet.psifac = profiles.xs[1] @@ -542,7 +542,7 @@ function chunk_el_integration_bounds(odet::OdeState, ctrl::ForceFreeStatesContro psi_end=psi_end, needs_crossing=true, ising=ising_current, - direction = bidirectional ? -1 : 1 + direction=bidirectional ? -1 : 1 )) # After crossing, we jump to the other side of the singular surface @@ -975,7 +975,7 @@ function transform_u!(odet::OdeState, intr::ForceFreeStatesInternal) temp[ksol, jsol] = odet.fixfac[ksol, jsol, ifix] end end - mul!(gauss_buffer, view(gauss,:,:,ifix), temp) + mul!(gauss_buffer, view(gauss, :, :, ifix), temp) gauss[:, :, ifix] .= gauss_buffer end # Account for zeroed indices at singular surfaces in `ode_ideal_cross` @@ -992,7 +992,7 @@ function transform_u!(odet::OdeState, intr::ForceFreeStatesInternal) # and mfix + 1 is the for the region after the last fixup and before the edge transforms[:, :, end] .= identity for ifix in odet.ifix:-1:1 - mul!(view(transforms,:,:,ifix), view(gauss,:,:,ifix), view(transforms,:,:,(ifix+1))) + mul!(view(transforms, :, :, ifix), view(gauss, :, :, ifix), view(transforms, :, :, (ifix+1))) end # Now that we have the transform matrices, we can apply them to the solution vectors diff --git a/src/ForceFreeStates/Galerkin/GalerkinAssembly.jl b/src/ForceFreeStates/Galerkin/GalerkinAssembly.jl index 3032e6ef7..787305d96 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinAssembly.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinAssembly.jl @@ -119,11 +119,12 @@ function gal_gauss_quad!(cell::GalCell, ffit::FourFitVars, profiles, intr::Force pb = swap_edge ? (pbt[1], pbt[2], pbt[4], pbt[3]) : pbt qb = swap_edge ? (qbt[1], qbt[2], qbt[4], qbt[3]) : qbt for ip in 0:np, ipert in 1:N, jp in 0:np, jpert in 1:N - cell.mat[ipert, jpert, ip+1, jp+1] += w * ( - F[ipert, jpert] * qb[ip+1] * qb[jp+1] + - K[ipert, jpert] * qb[ip+1] * pb[jp+1] + - conj(K[jpert, ipert]) * pb[ip+1] * qb[jp+1] + - G[ipert, jpert] * pb[ip+1] * pb[jp+1]) + cell.mat[ipert, jpert, ip+1, jp+1] += + w * ( + F[ipert, jpert] * qb[ip+1] * qb[jp+1] + + K[ipert, jpert] * qb[ip+1] * pb[jp+1] + + conj(K[jpert, ipert]) * pb[ip+1] * qb[jp+1] + + G[ipert, jpert] * pb[ip+1] * pb[jp+1]) end end return cell @@ -345,8 +346,8 @@ function gal_resonant!(cell::GalCell, ising::Int, ffit::FourFitVars, profiles, if verbose @info " resonant jsing=$jsing side=$(cell.extra) qerr=$(qerr) res1=$(raw[1]) res2=$(raw[2])" end - hbig = reshape(@view(raw[3:2+N*(np+1)]), N, np + 1) - hsmall = reshape(@view(raw[3+N*(np+1):end]), N, np + 1) + hbig = reshape(@view(raw[3:(2+N*(np+1))]), N, np + 1) + hsmall = reshape(@view(raw[(3+N*(np+1)):end]), N, np + 1) # Apply the right-side negation and the gal_make_arrays signs (gal.f gal_make_arrays) cell.erhs = -sgn_u * raw[1] @@ -449,7 +450,7 @@ including its full three-way edge branch (rpec count `ncoil = ws.nsol - 2*msing` - free boundary (`wv_edge !== nothing`): add the vacuum block `wvac·psio²` at the edge value DOF (gal.f). - fixed boundary (else): identity edge (gal.f). -`wv_edge` is `nothing` for the rpec and fixed cases. + `wv_edge` is `nothing` for the rpec and fixed cases. """ function gal_set_boundary!(ws::GalWorkspace, mpert::Int, wv_edge::Union{Nothing,Matrix{ComplexF64}}) chol = ws.solver == "cholesky" @@ -474,7 +475,7 @@ function gal_set_boundary!(ws::GalWorkspace, mpert::Int, wv_edge::Union{Nothing, cell.mat[idx, idx, 4, 4] = 1 end for ipert in 1:mpert - ws.rhs[cell.map[ipert, 4], 2 * msing + ipert] = 1 + ws.rhs[cell.map[ipert, 4], 2*msing+ipert] = 1 end elseif wv_edge !== nothing # free boundary: vacuum block wvac·psio² (gal.f) diff --git a/src/ForceFreeStates/Galerkin/GalerkinGrid.jl b/src/ForceFreeStates/Galerkin/GalerkinGrid.jl index 497e26e70..49b369fba 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinGrid.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinGrid.jl @@ -20,7 +20,7 @@ function gal_pack(nx::Int, pfac::Float64, side::String) elseif side == "right" xi = collect(0:2nx) ./ (2nx) elseif side == "both" - xi = collect(-nx:nx) ./ nx + xi = collect((-nx):nx) ./ nx else error("gal_pack: unrecognized side = $side") end diff --git a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl index 66dddd893..3b659e2da 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl @@ -39,7 +39,9 @@ function gal_make_arrays!(ws::GalWorkspace, ctrl::ForceFreeStatesControl, equil, return ws end -"""Empty `GalerkinResult` for a domain with no resonant surfaces.""" +""" +Empty `GalerkinResult` for a domain with no resonant surfaces. +""" function empty_galerkin_result() return GalerkinResult(0, Float64[], Float64[], Int[], Int[], Float64[], ComplexF64[], nothing, nothing) end diff --git a/src/ForceFreeStates/Resist.jl b/src/ForceFreeStates/Resist.jl index 3c436daa2..066d10179 100644 --- a/src/ForceFreeStates/Resist.jl +++ b/src/ForceFreeStates/Resist.jl @@ -33,11 +33,13 @@ coefficients E, F, G, H, K, M, the local time scales τ_A (`taua`), τ_R (`taur` by total volume), and the surface index `ising`. # Required keyword arguments + - `eta::Real` — plasma resistivity η at this surface [Ω·m] - `rho::Real` — mass density ρ at this surface [kg/m³] - `gamma::Real` — ratio of specific heats (enters G; physical thermodynamic value, e.g. 5/3) # Physics + The six flux-surface averages (Fortran `avg(1:6)`) are `⟨B²/|∇ψ|²⟩, ⟨1/|∇ψ|²⟩, ⟨1/B²⟩, ⟨1/(B²|∇ψ|²)⟩, ⟨B²⟩, ⟨|∇ψ|²/B²⟩`, each weighted by `J/V′` and integrated over θ ∈ [0,1) — exactly the resist.f integrands. The timescales are @@ -150,10 +152,10 @@ function resonant_match_rpec(delta_out_raw::AbstractMatrix, delta_coil_raw::Abst size(delta_coil_raw, 1) == 2msing || error("delta_coil_raw rows $(size(delta_coil_raw,1)) != 2msing") if ctrl.gal_ideal_flag # ideal limit: no inner layer, no reconnection - return ResonantMatchResult(zeros(ComplexF64,2msing,ncoil), zeros(ComplexF64,2msing,ncoil), - zeros(ComplexF64,msing,2), zeros(ComplexF64,msing), Matrix{ComplexF64}(delta_coil_raw), empty_bpen, 0.0) + return ResonantMatchResult(zeros(ComplexF64, 2msing, ncoil), zeros(ComplexF64, 2msing, ncoil), + zeros(ComplexF64, msing, 2), zeros(ComplexF64, msing), Matrix{ComplexF64}(delta_coil_raw), empty_bpen, 0.0) end - for (nm,v) in (("gal_eta",ctrl.gal_eta),("gal_rho",ctrl.gal_rho),("gal_rotation",ctrl.gal_rotation)) + for (nm, v) in (("gal_eta", ctrl.gal_eta), ("gal_rho", ctrl.gal_rho), ("gal_rotation", ctrl.gal_rotation)) length(v) == msing || error("$nm length $(length(v)) != msing $msing") end @@ -169,37 +171,47 @@ function resonant_match_rpec(delta_out_raw::AbstractMatrix, delta_coil_raw::Abst rpec_eig[i] = γ inner = InnerLayer.solve_inner_profile(InnerLayer.GGJModel(; solver=:galerkin), params, γ; xfac=ctrl.gal_inner_xfac, nx=ctrl.gal_inner_nx, nq=ctrl.gal_inner_nq, cutoff=ctrl.gal_inner_cutoff, kmax=ctrl.gal_inner_kmax) - deltar[i,1] = inner.Δ[1]; deltar[i,2] = inner.Δ[2] + deltar[i, 1] = inner.Δ[1] + deltar[i, 2] = inner.Δ[2] scale = -2π * chi1 * im * nn * sings[i].q1 * inner.dψdx # b_m = −2πi·χ₁·n·q′·dψdx·rescale·Ψ (GalerkinMatch.jl) - pen[i,1] = scale * inner.Ψ[1,1] * inner.rescale # parity 1 (Ψ(0)≠0) - pen[i,2] = scale * inner.Ψ[1,2] * inner.rescale # parity 2 (Ψ(0)=0 ⇒ ~0) + pen[i, 1] = scale * inner.Ψ[1, 1] * inner.rescale # parity 1 (Ψ(0)≠0) + pen[i, 2] = scale * inner.Ψ[1, 2] * inner.rescale # parity 2 (Ψ(0)=0 ⇒ ~0) end - mat = zeros(ComplexF64, 4msing, 4msing) + mat = zeros(ComplexF64, 4msing, 4msing) rmat = zeros(ComplexF64, 4msing, ncoil) - @views mat[2msing+1:4msing, 1:2msing] .= transpose(delta_out_raw) - @views rmat[2msing+1:4msing, :] .= .-delta_coil_raw + @views mat[(2msing+1):4msing, 1:2msing] .= transpose(delta_out_raw) + @views rmat[(2msing+1):4msing, :] .= .-delta_coil_raw for i in 1:msing - a=2i-1; b=2i; c=a+2msing; d=b+2msing - d1=deltar[i,1]; d2=deltar[i,2] - mat[a,a]=1; mat[b,b]=1 - mat[a,c]=-1; mat[a,d]=1 - mat[b,c]=-1; mat[b,d]=-1 - mat[c,c]=-d1; mat[c,d]=d2 - mat[d,c]=-d1; mat[d,d]=-d2 + a=2i-1 + b=2i + c=a+2msing + d=b+2msing + d1=deltar[i, 1] + d2=deltar[i, 2] + mat[a, a]=1 + mat[b, b]=1 + mat[a, c]=-1 + mat[a, d]=1 + mat[b, c]=-1 + mat[b, d]=-1 + mat[c, c]=-d1 + mat[c, d]=d2 + mat[d, c]=-d1 + mat[d, d]=-d2 end cof = mat \ rmat residual = norm(mat*cof - rmat) / max(norm(rmat), 1e-300) cout = cof[1:2msing, :] - cin = cof[2msing+1:4msing, :] - + cin = cof[(2msing+1):4msing, :] + reconnected_flux = delta_coil_raw .+ transpose(delta_out_raw)*cout # Inner-layer penetrated (reconnected) resonant field per surface — ONE quantity per surface, read off # the inner solution at the layer center (match.f intotsol_b; GalerkinMatch.jl): bpen[i,j] = pen₁(i)·cin[2i,j] + pen₂(i)·cin[2i-1,j]. bpen = zeros(ComplexF64, msing, ncoil) for i in 1:msing, j in 1:ncoil - bpen[i,j] = pen[i,1]*cin[2i,j] + pen[i,2]*cin[2i-1,j] + bpen[i, j] = pen[i, 1]*cin[2i, j] + pen[i, 2]*cin[2i-1, j] end return ResonantMatchResult(cout, cin, deltar, rpec_eig, reconnected_flux, bpen, residual) -end \ No newline at end of file +end diff --git a/src/ForceFreeStates/ResistEval.jl b/src/ForceFreeStates/ResistEval.jl index 04953b6c0..0f472bb38 100644 --- a/src/ForceFreeStates/ResistEval.jl +++ b/src/ForceFreeStates/ResistEval.jl @@ -33,23 +33,23 @@ Per-singular-surface Glasser-Greene-Johnson geometric coefficients and supporting flux-surface averages. -| field | meaning | -|-------------|------------------------------------------------------| -| `E`, `F` | Glasser interchange parameters (enter `D_I = E+F+H-¼`) | -| `G` | Coupling coefficient (curvature × pressure gradient) | -| `H` | Pfirsch-Schlüter coefficient | -| `K` | Glasser parameter | -| `M` | Mass factor | -| `avg_bsq_over_dpsisq` | ⟨B²/|∇ψ|²⟩ — needed for τ_R | -| `avg_bsq` | ⟨B²⟩ — needed for τ_R | -| `avg_B` | ⟨B⟩ — needed for Lin-Liu-Miller f_t | -| `B_max`, `B_min` | θ-extrema of B on the surface [T] | -| `f_trap` | Lin-Liu & Miller 1995 trapped-particle fraction | -| `R_major` | flux-surface-averaged major radius ⟨R⟩ [m] | -| `eps_local` | (R_max − R_min)/2 / R_major — local inverse aspect ratio | -| `p_local` | Plasma pressure at this surface [Pa] | -| `p1_local` | dp/dψ at this surface | -| `v1_local` | dV/dψ at this surface | +| field | meaning | +|:--------------------- |:-------------------------------------------------------- | +| `E`, `F` | Glasser interchange parameters (enter `D_I = E+F+H-¼`) | +| `G` | Coupling coefficient (curvature × pressure gradient) | +| `H` | Pfirsch-Schlüter coefficient | +| `K` | Glasser parameter | +| `M` | Mass factor | +| `avg_bsq_over_dpsisq` | ⟨B²/ | +| `avg_bsq` | ⟨B²⟩ — needed for τ_R | +| `avg_B` | ⟨B⟩ — needed for Lin-Liu-Miller f_t | +| `B_max`, `B_min` | θ-extrema of B on the surface [T] | +| `f_trap` | Lin-Liu & Miller 1995 trapped-particle fraction | +| `R_major` | flux-surface-averaged major radius ⟨R⟩ [m] | +| `eps_local` | (R_max − R_min)/2 / R_major — local inverse aspect ratio | +| `p_local` | Plasma pressure at this surface [Pa] | +| `p1_local` | dp/dψ at this surface | +| `v1_local` | dV/dψ at this surface | `H` here is identical to the `H` reported by `mercier_scan!` and stored in `LocalStability/h` — the GGJ routine recomputes it for convenience. @@ -99,6 +99,7 @@ standard GGJ formulas. - `gamma` — adiabatic index (default 5/3) !!! note "Contract" + `psifac` must be a genuine interior rational surface (`0 < ψ < 1`) with nonzero `q1`, `p1 = dp/dψ`, and `p`. The GGJ combination divides by these and by `|∇ψ|²` (which → 0 at the axis), so calling on the magnetic axis, @@ -107,47 +108,47 @@ standard GGJ formulas. rationals. """ function resist_geometry(equil::Equilibrium.PlasmaEquilibrium, - psifac::Real, q1::Real; gamma::Real=5/3) + psifac::Real, q1::Real; gamma::Real=5/3) profiles = equil.profiles - twopi = 2π - chi1 = twopi * equil.psio - psi_f = Float64(psifac) + twopi = 2π + chi1 = twopi * equil.psio + psi_f = Float64(psifac) # Surface-profile quantities (evaluate via the existing splines) twopif = profiles.F_spline(psi_f) - p = profiles.P_spline(psi_f) - p1 = profiles.P_deriv(psi_f) - v1 = profiles.dVdpsi_spline(psi_f) - v2 = profiles.dVdpsi_deriv(psi_f) - q = profiles.q_spline(psi_f) + p = profiles.P_spline(psi_f) + p1 = profiles.P_deriv(psi_f) + v1 = profiles.dVdpsi_spline(psi_f) + v2 = profiles.dVdpsi_deriv(psi_f) + q = profiles.q_spline(psi_f) # Build the 6 GGJ θ-integrands plus a 7th (B) for the neoclassical # resistivity f_t calculation, and accumulate running extrema of # (B, R) for Lin-Liu-Miller f_t and the local ε. ntheta = length(equil.rzphi_ys) - ff = zeros(Float64, ntheta, 7) - B_max = -Inf - B_min = Inf - R_max = -Inf - R_min = Inf + ff = zeros(Float64, ntheta, 7) + B_max = -Inf + B_min = Inf + R_max = -Inf + R_min = Inf for itheta in 1:ntheta theta = equil.rzphi_ys[itheta] - f1 = equil.rzphi_rsquared((psi_f, theta)) - f2 = equil.rzphi_offset((psi_f, theta)) + f1 = equil.rzphi_rsquared((psi_f, theta)) + f2 = equil.rzphi_offset((psi_f, theta)) jac = equil.rzphi_jac((psi_f, theta)) fy1 = FastInterpolations.deriv_view(equil.rzphi_rsquared, (0, 1))((psi_f, theta)) - fy2 = FastInterpolations.deriv_view(equil.rzphi_offset, (0, 1))((psi_f, theta)) - fy3 = FastInterpolations.deriv_view(equil.rzphi_nu, (0, 1))((psi_f, theta)) + fy2 = FastInterpolations.deriv_view(equil.rzphi_offset, (0, 1))((psi_f, theta)) + fy3 = FastInterpolations.deriv_view(equil.rzphi_nu, (0, 1))((psi_f, theta)) rfac = sqrt(f1) - eta = twopi * (theta + f2) - r = equil.ro + rfac * cos(eta) + eta = twopi * (theta + f2) + r = equil.ro + rfac * cos(eta) v21 = fy1 / (2 * rfac * jac) v22 = (1 + fy2) * twopi * rfac / jac v23 = fy3 * r / jac v33 = twopi * r / jac - bsq = chi1^2 * (v21^2 + v22^2 + (v23 + q*v33)^2) + bsq = chi1^2 * (v21^2 + v22^2 + (v23 + q*v33)^2) dpsisq = (twopi * r)^2 * (v21^2 + v22^2) B_here = sqrt(bsq) @@ -180,7 +181,7 @@ function resist_geometry(equil::Equilibrium.PlasmaEquilibrium, (twopif * q1 * chi1 / avg[5] - v2) F_coef = (p1 * v1 / (q1 * chi1^2))^2 * (avg[1] * avg[3] + (twopif / chi1)^2 * - (avg[1] * avg[4] - avg[2]^2)) + (avg[1] * avg[4] - avg[2]^2)) H_coef = twopif * p1 * v1 / (q1 * chi1^3) * (avg[2] - avg[1] / avg[5]) M_coef = avg[1] * (avg[6] + (twopif / chi1)^2 * (avg[3] - 1.0 / avg[5])) @@ -192,7 +193,7 @@ function resist_geometry(equil::Equilibrium.PlasmaEquilibrium, E_coef, F_coef, G_coef, H_coef, K_coef, M_coef, avg[1], avg[5], avg_B, B_max, B_min, f_trap, R_major, eps_local, - p, p1, v1, + p, p1, v1 ) end @@ -204,8 +205,8 @@ Populate `sing.restype` for every `SingType` in `intr.sing` using filled. """ function resist_eval_all!(intr::ForceFreeStatesInternal, - equil::Equilibrium.PlasmaEquilibrium; - gamma::Real=5/3) + equil::Equilibrium.PlasmaEquilibrium; + gamma::Real=5/3) for sing in intr.sing sing.restype === nothing || continue sing.restype = resist_geometry(equil, sing.psifac, sing.q1; gamma=gamma) diff --git a/src/ForceFreeStates/Riccati.jl b/src/ForceFreeStates/Riccati.jl index c1710cc12..7f9c8ee3a 100644 --- a/src/ForceFreeStates/Riccati.jl +++ b/src/ForceFreeStates/Riccati.jl @@ -97,7 +97,7 @@ This is compatible with downstream code (which uses U₁/U₂ ratio): # length chunks; the absolute floor catches short chunks where 5% of the span would be # smaller than the typical ODE step. const SAVE_NEAR_END_FRAC = 0.05 -const SAVE_NEAR_END_PSI = 1e-4 +const SAVE_NEAR_END_PSI = 1e-4 """ assemble_fm_matrix(propagators, idx_range; condition=false) -> Matrix{ComplexF64} @@ -107,6 +107,7 @@ in order for indices `idx_range`. Returns Φ_end * ... * Φ_start, so that the r maps the IC at the start of `idx_range[1]` to the state at the end of `idx_range[end]`. Each `ChunkPropagator` stores the 2N columns of Φ split into two N×N×2 blocks: + ``` block_upper_ic[:,:,1:2] ↔ Φ[:,1:N] (result from IC=(I,0)) block_lower_ic[:,:,1:2] ↔ Φ[:,N+1:2N] (result from IC=(0,I)) @@ -124,8 +125,8 @@ means only U₂ ICs are needed. Do NOT use for inter-surface segments where both and U₂ components carry physical information. """ function assemble_fm_matrix(propagators::Vector{ChunkPropagator}, idx_range; - condition::Bool=false, - T_init::Union{Nothing,Matrix{ComplexF64}}=nothing) + condition::Bool=false, + T_init::Union{Nothing,Matrix{ComplexF64}}=nothing) # Determine matrix size from T_init if provided (lets us handle empty idx_range and even # an empty propagators list, provided T_init carries the dimension). Otherwise fall back # to the first propagator that actually exists in idx_range, with a final fallback to @@ -167,7 +168,7 @@ After conditioning, only the U₂ columns carry meaningful information; the U₁ """ function condition_propagator!(Phi::Matrix{ComplexF64}, N::Int) # Work on the right half: columns N+1:2N (U₂ initial conditions) - cols = view(Phi, :, N+1:2N) + cols = view(Phi, :, (N+1):2N) # Sort columns by norm of the U₁ (upper N) block — largest first norms = [norm(view(cols, 1:N, k)) for k in 1:N] @@ -217,6 +218,7 @@ Compute the inter-surface tearing stability matrix (msing × msing) using the STRIDE global BVP formulation [Glasser 2018 Phys. Plasmas 25, 032501, Sec. III.B]. The BVP encodes the full plasma response with unknowns at each surface boundary: + ``` x_axis (N): free IC parameters at the axis (U₁ = 0 regular solutions) x_left[j] (2N): state at left inner-layer boundary of surface j @@ -229,9 +231,11 @@ The BVP encodes the full plasma response with unknowns at each surface boundary: When `wv` is provided (the vacuum response matrix, singfac-scaled), the edge BC follows the Fortran STRIDE convention: + ``` U₁ = c, U₂ = -wv·ψ₀²·c ``` + which is the free-boundary condition `wp + wv = 0` at the edge. When `wv` is `nothing`, a conducting wall BC (`U₁ = 0`) is used. @@ -248,9 +252,11 @@ keeps the BVP matrix full-rank and well-conditioned. The raw BVP solution is a 2·msing × 2·msing matrix `dp` with left/right sub-indices at each surface. The PEST3-convention Δ' matrix is the linear combination [Chance, PPPL-2527]: + ``` deltap(i,j) = dp(2i,2j) - dp(2i,2j-1) - dp(2i-1,2j) + dp(2i-1,2j-1) ``` + stored in `intr.delta_prime_matrix` (msing × msing). ## Limitations @@ -275,13 +281,13 @@ function compute_delta_prime_matrix!( intr::ForceFreeStatesInternal, propagators::Vector{ChunkPropagator}, chunks::Vector{IntegrationChunk}; - wv::Union{Nothing,Matrix{ComplexF64}} = nothing, - psio::Float64 = 0.0, - debug::Bool = false, - S_at_surface_left::Union{Nothing,Vector{Matrix{ComplexF64}}} = nothing, - ctrl::Union{Nothing,ForceFreeStatesControl} = nothing, - equil::Union{Nothing,Equilibrium.PlasmaEquilibrium} = nothing, - ffit::Union{Nothing,FourFitVars} = nothing + wv::Union{Nothing,Matrix{ComplexF64}}=nothing, + psio::Float64=0.0, + debug::Bool=false, + S_at_surface_left::Union{Nothing,Vector{Matrix{ComplexF64}}}=nothing, + ctrl::Union{Nothing,ForceFreeStatesControl}=nothing, + equil::Union{Nothing,Equilibrium.PlasmaEquilibrium}=nothing, + ffit::Union{Nothing,FourFitVars}=nothing ) intr.msing == 0 && return _has_unsupported_multi_resonance(intr) && return @@ -301,8 +307,10 @@ function compute_delta_prime_matrix!( if !use_S_axis for ic in i_crossings chunks[ic].direction == 1 || - error("compute_delta_prime_matrix!: FM-axis fallback (use_S_axis=false) requires forward crossing chunks; " * - "chunk $ic has direction=$(chunks[ic].direction). Either provide S_at_surface_left or use bidirectional=false.") + error( + "compute_delta_prime_matrix!: FM-axis fallback (use_S_axis=false) requires forward crossing chunks; " * + "chunk $ic has direction=$(chunks[ic].direction). Either provide S_at_surface_left or use bidirectional=false." + ) end end @@ -315,15 +323,15 @@ function compute_delta_prime_matrix!( _build_asymptotic_basis_matrices(sing, has_ua, N, msing) debug && _log_bvp_setup(chunks, sing, S_at_surface_left, use_S_axis, has_ua, - Phi_L_mats, Phi_R_mats, Phi_R_halves, ipert_all, wv, psio, N, msing) + Phi_L_mats, Phi_R_mats, Phi_R_halves, ipert_all, wv, psio, N, msing) if use_S_axis uShootR, uShootL, uAxis = _build_S_axis_shooting_propagators( propagators, chunks, i_crossings, sing, msing, N, T_left_mats, T_right_mats, has_ua, ctrl, equil, ffit, intr, debug) debug && _log_S_axis_shooting_propagators(uShootR, uShootL, uAxis, - S_at_surface_left, T_left_mats, - ipert_all, has_ua, msing, N) + S_at_surface_left, T_left_mats, + ipert_all, has_ua, msing, N) M, nMat, col_edge = _assemble_bvp_S_axis( uShootR, uShootL, uAxis, ipert_all, msing, N, wv, psio) else @@ -350,13 +358,13 @@ function compute_delta_prime_matrix!( # The raw matrix is consumed by `pest3_decompose` to recover (A', B', Γ', Δ') for the full # det(D' − D(γ)) = 0 eigenvalue problem; see ForceFreeStatesStructs.jl docstring. intr.delta_prime_matrix = deltap - intr.delta_prime_raw = dp_raw_persisted + intr.delta_prime_raw = dp_raw_persisted end # Column index helpers for the BVP matrix. j is the 1-based singular-surface index, # N is numpert_total. Layout: c_axis(N), c_left[1](2N), c_right[1](2N), ..., c_edge(N). -_col_left(j::Int, N::Int) = (N + 4N*(j-1) + 1):(N + 4N*(j-1) + 2N) -_col_right(j::Int, N::Int) = (N + 4N*(j-1) + 2N + 1):(N + 4N*j) +_col_left(j::Int, N::Int) = (N+4N*(j-1)+1):(N+4N*(j-1)+2N) +_col_right(j::Int, N::Int) = (N+4N*(j-1)+2N+1):(N+4N*j) # Multi-resonance surfaces (one q value satisfying multiple (m,n) tuples in a multi-n run) # are not yet handled by the inter-surface BVP. Returns true if any surface has >1 modes; @@ -395,28 +403,28 @@ end # Midpoint splitting halves each inter-surface span's condition number — STRIDE's trick: # cond(full) = 10¹⁵ → cond(half) ≈ 10⁷·⁵, an 8-digit accuracy gain. function _assemble_segment_propagators(propagators::Vector{ChunkPropagator}, - chunks::Vector{IntegrationChunk}, - i_crossings::Vector{Int}, msing::Int, N::Int, - use_S_axis::Bool) + chunks::Vector{IntegrationChunk}, + i_crossings::Vector{Int}, msing::Int, N::Int, + use_S_axis::Bool) Phi_L_mats = [assemble_fm_matrix(propagators, i_crossings[j]:i_crossings[j]) for j in 1:msing] Phi_R_mats = Vector{Matrix{ComplexF64}}(undef, msing + 1) if !use_S_axis - Phi_R_mats[1] = assemble_fm_matrix(propagators, 1:i_crossings[1]-1; condition=true) + Phi_R_mats[1] = assemble_fm_matrix(propagators, 1:(i_crossings[1]-1); condition=true) end for j in 2:msing - Phi_R_mats[j] = assemble_fm_matrix(propagators, i_crossings[j-1]+1:i_crossings[j]-1) + Phi_R_mats[j] = assemble_fm_matrix(propagators, (i_crossings[j-1]+1):(i_crossings[j]-1)) end - Phi_R_mats[msing+1] = assemble_fm_matrix(propagators, i_crossings[msing]+1:length(chunks)) + Phi_R_mats[msing+1] = assemble_fm_matrix(propagators, (i_crossings[msing]+1):length(chunks)) Phi_R_halves = Vector{Tuple{Matrix{ComplexF64},Matrix{ComplexF64}}}(undef, msing - 1) - for j in 1:msing-1 + for j in 1:(msing-1) chunk_start = i_crossings[j] + 1 - chunk_end = i_crossings[j+1] - 1 - n_chunks = chunk_end - chunk_start + 1 + chunk_end = i_crossings[j+1] - 1 + n_chunks = chunk_end - chunk_start + 1 if n_chunks >= 2 i_mid = chunk_start + div(n_chunks, 2) - 1 - Phi_left_half = assemble_fm_matrix(propagators, chunk_start:i_mid) - Phi_right_half = assemble_fm_matrix(propagators, i_mid+1:chunk_end) + Phi_left_half = assemble_fm_matrix(propagators, chunk_start:i_mid) + Phi_right_half = assemble_fm_matrix(propagators, (i_mid+1):chunk_end) Phi_R_halves[j] = (Phi_left_half, Phi_right_half) else Phi_R_halves[j] = (Matrix{ComplexF64}(I, 2N, 2N), Phi_R_mats[j+1]) @@ -430,17 +438,17 @@ end # N+1:2N = small solutions (z^{+α}, bounded). Fortran STRIDE bakes T into the shooting # propagators (uFM_sing_init); we multiply T into the BVP propagator blocks at each surface. function _build_asymptotic_basis_matrices(sing::Vector{SingType}, has_ua::Bool, N::Int, msing::Int) - T_left_mats = Vector{Matrix{ComplexF64}}(undef, msing) + T_left_mats = Vector{Matrix{ComplexF64}}(undef, msing) T_right_mats = Vector{Matrix{ComplexF64}}(undef, msing) - T_left_inv = Vector{Matrix{ComplexF64}}(undef, msing) - T_right_inv = Vector{Matrix{ComplexF64}}(undef, msing) + T_left_inv = Vector{Matrix{ComplexF64}}(undef, msing) + T_right_inv = Vector{Matrix{ComplexF64}}(undef, msing) if has_ua for j in 1:msing sp = sing[j] - T_left_mats[j] = [sp.ua_left[:,:,1]; sp.ua_left[:,:,2]] - T_right_mats[j] = [sp.ua_right[:,:,1]; sp.ua_right[:,:,2]] - T_left_inv[j] = inv(T_left_mats[j]) - T_right_inv[j] = inv(T_right_mats[j]) + T_left_mats[j] = [sp.ua_left[:, :, 1]; sp.ua_left[:, :, 2]] + T_right_mats[j] = [sp.ua_right[:, :, 1]; sp.ua_right[:, :, 2]] + T_left_inv[j] = inv(T_left_mats[j]) + T_right_inv[j] = inv(T_right_mats[j]) end end return T_left_mats, T_right_mats, T_left_inv, T_right_inv @@ -471,7 +479,7 @@ function _build_S_axis_shooting_propagators( end if can_reintegrate && !isempty(shoot_range_R) uShootR[j] = integrate_fm_with_ua_ic(chunks, shoot_range_R, sing[j].ua_right, - ctrl, equil, ffit, intr; backward=false, psi_ua=sing[j].psi_ua_right) + ctrl, equil, ffit, intr; backward=false, psi_ua=sing[j].psi_ua_right) else T_init = has_ua ? T_right_mats[j] : nothing uShootR[j] = assemble_fm_matrix(propagators, shoot_range_R; T_init=T_init) @@ -488,7 +496,7 @@ function _build_S_axis_shooting_propagators( end if can_reintegrate && !isempty(shoot_range_L) uShootL[j] = integrate_fm_with_ua_ic(chunks, shoot_range_L, sing[j].ua_left, - ctrl, equil, ffit, intr; backward=true, psi_ua=sing[j].psi_ua_left) + ctrl, equil, ffit, intr; backward=true, psi_ua=sing[j].psi_ua_left) else T_init = has_ua ? T_left_mats[j] : nothing uShootL[j] = assemble_fm_matrix(propagators, shoot_range_L; T_init=T_init) @@ -497,10 +505,10 @@ function _build_S_axis_shooting_propagators( uAxis, i_axis_mid = _build_conditioned_axis_propagator(propagators, i_crossings, N) uShootL[1] = _build_uShootL_first(propagators, chunks, i_crossings, sing, - T_left_mats, has_ua, can_reintegrate, i_axis_mid, - ctrl, equil, ffit, intr, N) + T_left_mats, has_ua, can_reintegrate, i_axis_mid, + ctrl, equil, ffit, intr, N) if debug - shoot_range_L1 = (i_axis_mid + 1):(i_crossings[1] - 1) + shoot_range_L1 = (i_axis_mid+1):(i_crossings[1]-1) @info " Axis propagator: $(i_axis_mid) chunks, cond=$(@sprintf("%.2e", cond(uAxis)))" @info " uShootL[1]: range=$(shoot_range_L1), cond=$(@sprintf("%.2e", cond(uShootL[1])))" end @@ -513,25 +521,25 @@ end # chunk+1 to chunk(i_crossings[j]-1). The ψ midpoint is used (not the chunk-index midpoint) # because chunks near singularities are packed tighter in ψ — Fortran convention. function _midpoint_shoot_range(chunks::Vector{IntegrationChunk}, i_crossings::Vector{Int}, - j::Int, msing::Int; side::Symbol) + j::Int, msing::Int; side::Symbol) if side === :right - j == msing && return (i_crossings[msing] + 1):length(chunks) + j == msing && return (i_crossings[msing]+1):length(chunks) chunk_start = i_crossings[j] + 1 - chunk_end = i_crossings[j+1] - 1 + chunk_end = i_crossings[j+1] - 1 else # :left, j >= 2 chunk_start = i_crossings[j-1] + 1 - chunk_end = i_crossings[j] - 1 + chunk_end = i_crossings[j] - 1 end psi_mid_target = (chunks[chunk_start].psi_start + chunks[chunk_end].psi_end) / 2 i_mid_inter = chunk_start - for ic in chunk_start:chunk_end-1 + for ic in chunk_start:(chunk_end-1) if chunks[ic].psi_end >= psi_mid_target i_mid_inter = ic break end i_mid_inter = ic end - return side === :right ? (chunk_start:i_mid_inter) : ((i_mid_inter + 1):chunk_end) + return side === :right ? (chunk_start:i_mid_inter) : ((i_mid_inter+1):chunk_end) end # Build a well-conditioned axis propagator by forward-propagating [0; I] through the @@ -539,7 +547,7 @@ end # midpoint is placed one chunk before the first surface so that uShootL[1] covers only the # last chunk, keeping it well-conditioned. function _build_conditioned_axis_propagator(propagators::Vector{ChunkPropagator}, - i_crossings::Vector{Int}, N::Int) + i_crossings::Vector{Int}, N::Int) n_pre_cross = i_crossings[1] - 1 i_axis_mid = max(1, n_pre_cross - 1) uAxis = zeros(ComplexF64, 2N, N) @@ -549,9 +557,9 @@ function _build_conditioned_axis_propagator(propagators::Vector{ChunkPropagator} for ic in 1:i_axis_mid prop = propagators[ic] upper_old = uAxis[1:N, :] - lower_old = uAxis[N+1:2N, :] - uAxis[1:N, :] .= prop.block_upper_ic[:,:,1] * upper_old .+ prop.block_lower_ic[:,:,1] * lower_old - uAxis[N+1:2N, :] .= prop.block_upper_ic[:,:,2] * upper_old .+ prop.block_lower_ic[:,:,2] * lower_old + lower_old = uAxis[(N+1):2N, :] + uAxis[1:N, :] .= prop.block_upper_ic[:, :, 1] * upper_old .+ prop.block_lower_ic[:, :, 1] * lower_old + uAxis[(N+1):2N, :] .= prop.block_upper_ic[:, :, 2] * upper_old .+ prop.block_lower_ic[:, :, 2] * lower_old Q, _ = qr(uAxis) uAxis .= Matrix(Q)[:, 1:N] end @@ -565,18 +573,18 @@ end # Falls back to T_left_mats[1] (or identity if no ua) when there's only 1 chunk before the # first crossing. function _build_uShootL_first(propagators::Vector{ChunkPropagator}, - chunks::Vector{IntegrationChunk}, i_crossings::Vector{Int}, - sing::Vector{SingType}, T_left_mats::Vector{Matrix{ComplexF64}}, - has_ua::Bool, can_reintegrate::Bool, i_axis_mid::Int, - ctrl, equil, ffit, intr::ForceFreeStatesInternal, N::Int) - shoot_range_L1 = (i_axis_mid + 1):(i_crossings[1] - 1) + chunks::Vector{IntegrationChunk}, i_crossings::Vector{Int}, + sing::Vector{SingType}, T_left_mats::Vector{Matrix{ComplexF64}}, + has_ua::Bool, can_reintegrate::Bool, i_axis_mid::Int, + ctrl, equil, ffit, intr::ForceFreeStatesInternal, N::Int) + shoot_range_L1 = (i_axis_mid+1):(i_crossings[1]-1) if can_reintegrate && !isempty(shoot_range_L1) return integrate_fm_with_ua_ic(chunks, shoot_range_L1, sing[1].ua_left, - ctrl, equil, ffit, intr; - backward=true, psi_ua=sing[1].psi_ua_left) + ctrl, equil, ffit, intr; + backward=true, psi_ua=sing[1].psi_ua_left) elseif !isempty(shoot_range_L1) return assemble_fm_matrix(propagators, shoot_range_L1; - T_init=has_ua ? T_left_mats[1] : nothing) + T_init=has_ua ? T_left_mats[1] : nothing) else return has_ua ? T_left_mats[1] : Matrix{ComplexF64}(I, 2N, 2N) end @@ -587,19 +595,19 @@ end # the catastrophically ill-conditioned axis FM. Fortran-matched structure with # nMat = (2 + 4·msing)·N. Returns (M, nMat, col_edge). function _assemble_bvp_S_axis(uShootR::Vector{Matrix{ComplexF64}}, - uShootL::Vector{Matrix{ComplexF64}}, - uAxis::Matrix{ComplexF64}, ipert_all::Vector{Int}, - msing::Int, N::Int, - wv::Union{Nothing,Matrix{ComplexF64}}, psio::Float64) + uShootL::Vector{Matrix{ComplexF64}}, + uAxis::Matrix{ComplexF64}, ipert_all::Vector{Int}, + msing::Int, N::Int, + wv::Union{Nothing,Matrix{ComplexF64}}, psio::Float64) # STRIDE global BVP block structure [Glasser-Kolemen 2018 PoP 25, 032501 Eq. 37]. nMat = (2 + 4 * msing) * N col_axis = 1:N - col_edge = (nMat - N + 1):nMat + col_edge = (nMat-N+1):nMat M = zeros(ComplexF64, nMat, nMat) # Axis matching: uShootL[1] · c_left[1] = uAxis · c_axis (2N equations) M[1:2N, _col_left(1, N)] .= uShootL[1] - M[1:2N, col_axis] .= -uAxis + M[1:2N, col_axis] .= -uAxis row_offset = 2N for j in 1:msing @@ -608,24 +616,24 @@ function _assemble_bvp_S_axis(uShootR::Vector{Matrix{ComplexF64}}, for i in 1:2N if i != ipert_j && i != ipert_j + N row_offset += 1 - M[row_offset, _col_left(j, N)[i]] = 1 + M[row_offset, _col_left(j, N)[i]] = 1 M[row_offset, _col_right(j, N)[i]] = -1 end end - junc_rows = (row_offset + 1):(row_offset + 2N) + junc_rows = (row_offset+1):(row_offset+2N) if j < msing # Midpoint matching between consecutive surfaces - M[junc_rows, _col_right(j, N)] .= -uShootR[j] - M[junc_rows, _col_left(j+1, N)] .= uShootL[j+1] + M[junc_rows, _col_right(j, N)] .= -uShootR[j] + M[junc_rows, _col_left(j+1, N)] .= uShootL[j+1] else # Edge junction M[junc_rows, _col_right(msing, N)] .= uShootR[msing] if wv !== nothing - M[junc_rows[1:N], col_edge] .= -I(N) - M[junc_rows[N+1:end], col_edge] .= wv .* psio^2 + M[junc_rows[1:N], col_edge] .= -I(N) + M[junc_rows[(N+1):end], col_edge] .= wv .* psio^2 else - M[junc_rows[N+1:end], col_edge] .= -I(N) + M[junc_rows[(N+1):end], col_edge] .= -I(N) end end row_offset = last(junc_rows) @@ -635,7 +643,7 @@ function _assemble_bvp_S_axis(uShootR::Vector{Matrix{ComplexF64}}, for j in 1:msing ipert_j = ipert_all[j] row_offset += 1 - M[row_offset, _col_left(j, N)[ipert_j]] = 1 + M[row_offset, _col_left(j, N)[ipert_j]] = 1 row_offset += 1 M[row_offset, _col_right(j, N)[ipert_j]] = 1 end @@ -670,18 +678,18 @@ end # Fallback BVP assembly with FM-based axis BC (used when no Riccati S matrices are available). # Uses the conditioned axis propagator Phi_R[1][:,N+1:2N] in place of S-axis matching. function _assemble_bvp_FM_axis(Phi_L_mats::Vector{Matrix{ComplexF64}}, - Phi_R_mats::Vector{Matrix{ComplexF64}}, ipert_all::Vector{Int}, - msing::Int, N::Int, - T_left_inv::Vector{Matrix{ComplexF64}}, - T_right_inv::Vector{Matrix{ComplexF64}}, has_ua::Bool, - wv::Union{Nothing,Matrix{ComplexF64}}, psio::Float64) + Phi_R_mats::Vector{Matrix{ComplexF64}}, ipert_all::Vector{Int}, + msing::Int, N::Int, + T_left_inv::Vector{Matrix{ComplexF64}}, + T_right_inv::Vector{Matrix{ComplexF64}}, has_ua::Bool, + wv::Union{Nothing,Matrix{ComplexF64}}, psio::Float64) nMat = (2 + 4 * msing) * N col_axis = 1:N - col_edge = (N + 4N*msing + 1):nMat + col_edge = (N+4N*msing+1):nMat M = zeros(ComplexF64, nMat, nMat) M[1:2N, (N+1):(N+2N)] .= Phi_L_mats[1] - M[1:2N, col_axis] .= -view(Phi_R_mats[1], :, N+1:2N) + M[1:2N, col_axis] .= -view(Phi_R_mats[1], :, (N+1):2N) row_drive_base = 2N + (4N-2)*msing for j in 1:msing @@ -692,29 +700,29 @@ function _assemble_bvp_FM_axis(Phi_L_mats::Vector{Matrix{ComplexF64}}, for i in 1:2N if i != ipert_j && i != ipert_j + N row_cont += 1 - M[row_cont, cl[i]] = 1 + M[row_cont, cl[i]] = 1 M[row_cont, cr[i]] = -1 end end - junc_rows = (row_cont + 1):(2N + (4N-2)*j) + junc_rows = (row_cont+1):(2N+(4N-2)*j) if j < msing - M[junc_rows, cr] .= Phi_R_mats[j+1] + M[junc_rows, cr] .= Phi_R_mats[j+1] M[junc_rows, _col_left(j+1, N)] .= -Phi_L_mats[j+1] else M[junc_rows, cr] .= Phi_R_mats[msing+1] if wv !== nothing - M[junc_rows[1:N], col_edge] .= -I(N) - M[junc_rows[N+1:end], col_edge] .= wv .* psio^2 + M[junc_rows[1:N], col_edge] .= -I(N) + M[junc_rows[(N+1):end], col_edge] .= wv .* psio^2 else - M[junc_rows[N+1:end], col_edge] .= -I(N) + M[junc_rows[(N+1):end], col_edge] .= -I(N) end end if has_ua - M[row_drive_base + 2j-1, cl] .= T_left_inv[j][ipert_j, :] - M[row_drive_base + 2j, cr] .= T_right_inv[j][ipert_j, :] + M[row_drive_base+2j-1, cl] .= T_left_inv[j][ipert_j, :] + M[row_drive_base+2j, cr] .= T_right_inv[j][ipert_j, :] else - M[row_drive_base + 2j-1, cl[ipert_j]] = 1 - M[row_drive_base + 2j, cr[ipert_j]] = 1 + M[row_drive_base+2j-1, cl[ipert_j]] = 1 + M[row_drive_base+2j, cr[ipert_j]] = 1 end end return M, nMat, col_edge @@ -725,8 +733,8 @@ end # combination subtracts dp_raw entries up to ~3×10⁴ larger than the result, and Float64 # precision lets the imaginary part drift 2–5× on DIIID-class equilibria. function _solve_bvp_and_combine_pest3(M::Matrix{ComplexF64}, msing::Int, N::Int, nMat::Int, - use_S_axis::Bool, ipert_all::Vector{Int}, col_edge, - ctrl, debug::Bool) + use_S_axis::Bool, ipert_all::Vector{Int}, col_edge, + ctrl, debug::Bool) s2 = 2 * msing Tc = (ctrl === nothing || ctrl.extended_precision_bvp) ? Complex{Double64} : ComplexF64 M_solve = Tc.(M) @@ -748,12 +756,12 @@ function _solve_bvp_and_combine_pest3(M::Matrix{ComplexF64}, msing::Int, N::Int, x = use_lu ? (M_lu \ b) : (M_pinv * b) debug && _log_bvp_solve(x, b, M_solve, jsing, side, dRow, msing, N, - ipert_all, col_edge, use_S_axis) + ipert_all, col_edge, use_S_axis) for ksing in 1:msing ipert_k = ipert_all[ksing] dp_raw[dRow, 2ksing-1] = x[_col_left(ksing, N)[ipert_k+N]] - dp_raw[dRow, 2ksing] = x[_col_right(ksing, N)[ipert_k+N]] + dp_raw[dRow, 2ksing] = x[_col_right(ksing, N)[ipert_k+N]] end end @@ -773,7 +781,7 @@ end # Logging helpers for `compute_delta_prime_matrix!`. Called only when debug=true. function _log_bvp_setup(chunks, sing, S_at_surface_left, use_S_axis, has_ua, - Phi_L_mats, Phi_R_mats, Phi_R_halves, ipert_all, wv, psio, N, msing) + Phi_L_mats, Phi_R_mats, Phi_R_halves, ipert_all, wv, psio, N, msing) @info "Δ' BVP: $(length(chunks)) chunks, $msing surfaces, N=$N" @info "Δ' BVP: Axis BC: $(use_S_axis ? "S-based (Riccati)" : "FM-based (conditioned)")" @info "Δ' BVP: Asymptotic basis: $(has_ua ? "available" : "NOT available (raw basis driving)")" @@ -785,8 +793,8 @@ function _log_bvp_setup(chunks, sing, S_at_surface_left, use_S_axis, has_ua, if has_ua for j in 1:msing sp = sing[j] - T_l = [sp.ua_left[:,:,1]; sp.ua_left[:,:,2]] - T_r = [sp.ua_right[:,:,1]; sp.ua_right[:,:,2]] + T_l = [sp.ua_left[:, :, 1]; sp.ua_left[:, :, 2]] + T_r = [sp.ua_right[:, :, 1]; sp.ua_right[:, :, 2]] @info " Surface $j: cond(T_left)=$(@sprintf("%.2e", cond(T_l))), cond(T_right)=$(@sprintf("%.2e", cond(T_r)))" ipert_j = ipert_all[j] @info " Surface $j ua_left (ipert=$ipert_j, psi_ua_left=$(@sprintf("%.8f", sp.psi_ua_left))):" @@ -796,7 +804,7 @@ function _log_bvp_setup(chunks, sing, S_at_surface_left, use_S_axis, has_ua, @info " small: ua(1,$(ipert_j+N),1)=$(@sprintf("%16.8e %16.8e", real(sp.ua_left[1,ipert_j+N,1]), imag(sp.ua_left[1,ipert_j+N,1])))" end end - for j in 1:msing-1 + for j in 1:(msing-1) Phi_L_h, Phi_R_h = Phi_R_halves[j] @info " Inter-surface $j→$(j+1): half_L cond=$(@sprintf("%.2e",cond(Phi_L_h))), half_R cond=$(@sprintf("%.2e",cond(Phi_R_h))), full cond=$(@sprintf("%.2e",cond(Phi_R_mats[j+1])))" end @@ -813,7 +821,7 @@ function _log_bvp_setup(chunks, sing, S_at_surface_left, use_S_axis, has_ua, end function _log_S_axis_shooting_propagators(uShootR, uShootL, uAxis, S_at_surface_left, - T_left_mats, ipert_all, has_ua, msing, N) + T_left_mats, ipert_all, has_ua, msing, N) @info " Shooting propagators (S-based axis BC, no axis unknowns):" for j in 1:msing shoot_R_str = @sprintf("%.2e", cond(uShootR[j])) @@ -823,7 +831,7 @@ function _log_S_axis_shooting_propagators(uShootR, uShootL, uAxis, S_at_surface_ S1 = S_at_surface_left[1] if has_ua T1 = T_left_mats[1] - axis_BC = T1[1:N, :] - S1 * T1[N+1:2N, :] + axis_BC = T1[1:N, :] - S1 * T1[(N+1):2N, :] @info " S-axis BC matrix: cond=$(@sprintf("%.2e", cond(axis_BC)))" end for j in 1:msing @@ -837,7 +845,7 @@ function _log_S_axis_shooting_propagators(uShootR, uShootL, uAxis, S_at_surface_ @info " uShootL[$j] col ipert=$ipert_j norm=$(@sprintf("%.2e", col_norms_L[ipert_j])), col ipert+N=$(ipert_j+N) norm=$(@sprintf("%.2e", col_norms_L[ipert_j+N]))" end end - for j in 1:msing-1 + for j in 1:(msing-1) mid_block = hcat(uShootR[j], -uShootL[j+1]) @info " Midpoint $j→$(j+1): cond([uShootR[$j] | -uShootL[$(j+1)]]) = $(@sprintf("%.2e", cond(mid_block)))" col_norms_Ljp1 = [norm(view(uShootL[j+1], :, k)) for k in 1:2N] @@ -846,7 +854,7 @@ function _log_S_axis_shooting_propagators(uShootR, uShootL, uAxis, S_at_surface_ end function _log_bvp_solve(x, b, M_solve, jsing, side, dRow, msing, N, - ipert_all, col_edge, use_S_axis) + ipert_all, col_edge, use_S_axis) residual = norm(ComplexF64.(M_solve * x - b)) side_str = side == 1 ? "left" : "right" @info " BVP solve: jsing=$jsing side=$side_str (dRow=$dRow): ||Mx-b||=$(@sprintf("%.2e", residual)), ||x||=$(@sprintf("%.2e", Float64(norm(x))))" @@ -854,9 +862,9 @@ function _log_bvp_solve(x, b, M_solve, jsing, side, dRow, msing, N, ipert_ks = ipert_all[ks] cl = _col_left(ks, N) cr = _col_right(ks, N) - xl_big = ComplexF64(x[cl[ipert_ks]]) + xl_big = ComplexF64(x[cl[ipert_ks]]) xl_small = ComplexF64(x[cl[ipert_ks+N]]) - xr_big = ComplexF64(x[cr[ipert_ks]]) + xr_big = ComplexF64(x[cr[ipert_ks]]) xr_small = ComplexF64(x[cr[ipert_ks+N]]) @info " surf $ks: x_left[big]=$(@sprintf("%+.4e%+.4ei", real(xl_big), imag(xl_big))), x_left[small]=$(@sprintf("%+.4e%+.4ei", real(xl_small), imag(xl_small)))" @info " surf $ks: x_right[big]=$(@sprintf("%+.4e%+.4ei", real(xr_big), imag(xr_big))), x_right[small]=$(@sprintf("%+.4e%+.4ei", real(xr_small), imag(xr_small)))" @@ -870,7 +878,7 @@ end function _log_bvp_pest3(dp_raw, deltap, s2, msing, Tc) @info "Δ' BVP: Full dp_raw matrix ($(s2)×$(s2)) [$(Tc)]:" for i in 1:s2 - row_str = join([@sprintf("%+.6e", Float64(real(dp_raw[i,j]))) for j in 1:s2], " ") + row_str = join([@sprintf("%+.6e", Float64(real(dp_raw[i, j]))) for j in 1:s2], " ") @info " dp_raw[$i,:] = $row_str" end @info "Δ' BVP: Raw dp diagonal = $([@sprintf("%.4f%+.4fi", Float64(real(dp_raw[i,i])), Float64(imag(dp_raw[i,i]))) for i in 1:s2])" @@ -930,8 +938,8 @@ function pest3_decompose(dp_raw::AbstractMatrix) for i in 1:m, j in 1:m LL = dp_raw[2i-1, 2j-1] LR = dp_raw[2i-1, 2j] - RL = dp_raw[2i, 2j-1] - RR = dp_raw[2i, 2j] + RL = dp_raw[2i, 2j-1] + RR = dp_raw[2i, 2j] Ap[i, j] = RR + RL + LR + LL Bp[i, j] = RR - RL + LR - LL Gp[i, j] = RR + RL - LR - LL @@ -944,7 +952,7 @@ end riccati_der!(du, u, params, psieval) Evaluate the explicit dual Riccati ODE right-hand side: - dS/dψ = w†·F̄⁻¹·w - S·Ḡ·S, w = Q - K̄·S +dS/dψ = w†·F̄⁻¹·w - S·Ḡ·S, w = Q - K̄·S where Q = diag(1/(m - n·q)) is the diagonal singular factor matrix. The identity slice u[:,:,2] = I does not evolve (du[:,:,2] = 0). @@ -968,7 +976,7 @@ See: Glasser (2018) Phys. Plasmas 25, 032507 — Eq. 19 (dual Riccati form) _, equil, ffit, intr, odet, _ = params Npert = intr.numpert_total - S = @view u[:, :, 1] + S = @view u[:, :, 1] dS = @view du[:, :, 1] @view(du[:, :, 2]) .= 0 # identity does not evolve @@ -983,9 +991,9 @@ See: Glasser (2018) Phys. Plasmas 25, 032507 — Eq. 19 (dual Riccati form) fmat_lower = acquire!(pool, ComplexF64, Npert, Npert) kmat = similar!(pool, fmat_lower) gmat = similar!(pool, fmat_lower) - w = similar!(pool, fmat_lower) # w = Q - K̄·S - v = similar!(pool, fmat_lower) # v = F̄⁻¹·w (then reused for S·Ḡ·S) - tmp = similar!(pool, fmat_lower) # scratch + w = similar!(pool, fmat_lower) # w = Q - K̄·S + v = similar!(pool, fmat_lower) # v = F̄⁻¹·w (then reused for S·Ḡ·S) + tmp = similar!(pool, fmat_lower) # scratch # Evaluate F̄ (Cholesky factor), K̄, Ḡ splines at current ψ ffit.fmats_lower(vec(fmat_lower), psieval; hint=ffit._hint) @@ -1076,7 +1084,7 @@ function riccati_integrate_chunk!( cb = DiscreteCallback((u, t, integrator) -> true, riccati_integrator_callback!) rtol = ctrl.eulerlagrange_tolerance prob = ODEProblem(sing_der!, odet.u, (chunk.psi_start, chunk.psi_end), - (ctrl, equil, ffit, intr, odet, chunk)) + (ctrl, equil, ffit, intr, odet, chunk)) sol = solve(prob, Vern9(); reltol=rtol, callback=cb, save_everystep=false, save_end=true) odet.u .= sol.u[end] odet.psifac = sol.t[end] @@ -1094,8 +1102,8 @@ end renormalize_riccati!(odet, intr) After a singular surface crossing, restore the canonical Riccati storage convention: - u[:,:,1] = S_new = U₁_new · U₂_new⁻¹ - u[:,:,2] = I +u[:,:,1] = S_new = U₁_new · U₂_new⁻¹ +u[:,:,2] = I `riccati_cross_ideal_singular_surf!` leaves u[:,:,1] = U₁_new and u[:,:,2] = U₂_new (not I), so this step is required before continuing the Riccati integration. @@ -1119,8 +1127,8 @@ end renormalize_riccati_inplace!(u, N) In-place Riccati renormalization on an arbitrary N×N×2 array: - u[:,:,1] = U₁ · U₂⁻¹ (new S) - u[:,:,2] = I +u[:,:,1] = U₁ · U₂⁻¹ (new S) +u[:,:,2] = I Used in `riccati_integrator_callback!` to renormalize the integrator's live state when column norms grow beyond `ctrl.ucrit`, analogous to Gaussian reduction in the @@ -1142,14 +1150,14 @@ end Cross a singular surface for the Riccati formulation. Replaces `cross_ideal_singular_surf!` for the Riccati integration path with two key differences: -1. **No Gaussian reduction**: `cross_ideal_singular_surf!` calls `compute_solution_norms!` - which applies Gaussian reduction to (S, I). This divides by pivot elements of S, which - can be near-zero (S = 0 at axis and grows slowly), producing NaN/Inf in U₂. For Riccati, - S is bounded so Gaussian reduction is unnecessary. + 1. **No Gaussian reduction**: `cross_ideal_singular_surf!` calls `compute_solution_norms!` + which applies Gaussian reduction to (S, I). This divides by pivot elements of S, which + can be near-zero (S = 0 at axis and grows slowly), producing NaN/Inf in U₂. For Riccati, + S is bounded so Gaussian reduction is unnecessary. -2. **Direct column zeroing**: Instead of using the GR-sorted `odet.index` to identify the - column to zero, we use `ipert_res` directly (the resonant mode index). This is valid since - without GR there is no permutation applied to the columns of S. + 2. **Direct column zeroing**: Instead of using the GR-sorted `odet.index` to identify the + column to zero, we use `ipert_res` directly (the resonant mode index). This is valid since + without GR there is no permutation applied to the columns of S. **Δ' normalization**: This function expects `odet.u` in the bounded (U₁, U₂) form produced by `riccati_integrate_chunk!` with `needs_crossing=true` (final renorm skipped). ca_l is computed @@ -1195,11 +1203,11 @@ Fortran STRIDE's separate vmatl/vmatr (sing_vmat). Alpha is taken from the right side and shared with the left. """ function _two_sided_singular_asymptotics(singp::SingType, ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, - intr::ForceFreeStatesInternal) + equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, + intr::ForceFreeStatesInternal) sing_asymp_right = compute_sing_asymptotics(singp, ctrl, equil, ffit, intr; sig=1.0) - sing_asymp_left = compute_sing_asymptotics(singp, ctrl, equil, ffit, intr; sig=-1.0, - alpha_override=sing_asymp_right.alpha) + sing_asymp_left = compute_sing_asymptotics(singp, ctrl, equil, ffit, intr; sig=-1.0, + alpha_override=sing_asymp_right.alpha) return sing_asymp_left, sing_asymp_right end @@ -1219,7 +1227,7 @@ end # Capture left-side asymptotic data into odet.ca_l and singp.ua_left/psi_ua_left. function _capture_left_crossing_data!(odet::OdeState, singp::SingType, sing_asymp_left, - dpsi::Float64, intr::ForceFreeStatesInternal, ising::Int) + dpsi::Float64, intr::ForceFreeStatesInternal, ising::Int) ua = sing_get_ua(sing_asymp_left, dpsi) singp.ua_left = copy(ua) singp.psi_ua_left = odet.psifac @@ -1231,9 +1239,9 @@ end # odet.psifac to the right side. The zeroed columns stay zero through the predictor # since du[:, ipert_res, :] = 0 when u[:, ipert_res, :] = 0. function _predict_across_singular_surface!(odet::OdeState, ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, - intr::ForceFreeStatesInternal, ising::Int, - ipert_res, dpsi::Float64, sing_asymp_right) + equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, + intr::ForceFreeStatesInternal, ising::Int, + ipert_res, dpsi::Float64, sing_asymp_right) if ctrl.kinetic_factor == 0 for i in eachindex(sing_asymp_right.r1) odet.u[:, ipert_res[i], :] .= 0 @@ -1253,8 +1261,8 @@ end # Column ipert_res of [U₁_new; U₂_new] = ua[:, ipert_res+N, :] (the introduced small asymptotic), # so ca_r[ipert_res, ipert_res, 2] = 1 regardless of other columns' normalization. function _capture_right_crossing_data!(odet::OdeState, singp::SingType, sing_asymp_right, - dpsi::Float64, intr::ForceFreeStatesInternal, ising::Int, - ipert_res, ctrl::ForceFreeStatesControl) + dpsi::Float64, intr::ForceFreeStatesInternal, ising::Int, + ipert_res, ctrl::ForceFreeStatesControl) ua = sing_get_ua(sing_asymp_right, dpsi) singp.ua_right = copy(ua) singp.psi_ua_right = odet.psifac @@ -1270,9 +1278,9 @@ end # STUB: per-surface ca-based Δ' (not physically valid; see SingType.delta_prime docstring). # The canonical Δ' is intr.delta_prime_matrix from compute_delta_prime_matrix!. function _stash_per_surface_delta_prime_stub!(odet::OdeState, intr::ForceFreeStatesInternal, - ising::Int, ipert_res, sing_asymp_right, - equil::Equilibrium.PlasmaEquilibrium, - ctrl::ForceFreeStatesControl) + ising::Int, ipert_res, sing_asymp_right, + equil::Equilibrium.PlasmaEquilibrium, + ctrl::ForceFreeStatesControl) ctrl.kinetic_factor == 0 || return denom = (2π)^2 * equil.psio n_res = length(sing_asymp_right.r1) @@ -1323,8 +1331,8 @@ function integrate_propagator_chunk!( # naturally. The resulting propagator maps state at psi_end → psi_start, which is # well-conditioned because exponentially growing solutions (forward) decay backward. tspan = chunk.direction == 1 ? - (chunk.psi_start, chunk.psi_end) : - (chunk.psi_end, chunk.psi_start) + (chunk.psi_start, chunk.psi_end) : + (chunk.psi_end, chunk.psi_start) rtol = ctrl.eulerlagrange_tolerance params = (ctrl, equil, ffit, intr, odet_proxy, chunk) @@ -1378,12 +1386,12 @@ function integrate_fm_with_ua_ic( equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal; - backward::Bool = false, - psi_ua::Float64 = NaN + backward::Bool=false, + psi_ua::Float64=NaN ) N = intr.numpert_total psi_start = chunks[first(chunk_range)].psi_start - psi_end = chunks[last(chunk_range)].psi_end + psi_end = chunks[last(chunk_range)].psi_end # Use stored ua ψ location if provided; otherwise fall back to chunk boundary. # The ua is evaluated at the inner-layer boundary (exact ψ from singular crossing), # which may differ slightly from the nearest chunk boundary. @@ -1409,18 +1417,18 @@ function integrate_fm_with_ua_ic( odet_proxy.ffit_hint[] = 1 prob = ODEProblem(sing_der!, u0, tspan, params) sol = solve(prob, Vern9(); reltol=rtol, save_everystep=false, save_end=true) - result[1:N, 1:N] .= sol.u[end][:, :, 1] - result[N+1:2N, 1:N] .= sol.u[end][:, :, 2] + result[1:N, 1:N] .= sol.u[end][:, :, 1] + result[(N+1):2N, 1:N] .= sol.u[end][:, :, 2] # Batch 2: columns N+1:2N of T (small solutions) - u0[:, :, 1] .= ua[:, N+1:2N, 1] - u0[:, :, 2] .= ua[:, N+1:2N, 2] + u0[:, :, 1] .= ua[:, (N+1):2N, 1] + u0[:, :, 2] .= ua[:, (N+1):2N, 2] odet_proxy.spline_hint[] = 1 odet_proxy.ffit_hint[] = 1 prob = ODEProblem(sing_der!, u0, tspan, params) sol = solve(prob, Vern9(); reltol=rtol, save_everystep=false, save_end=true) - result[1:N, N+1:2N] .= sol.u[end][:, :, 1] - result[N+1:2N, N+1:2N] .= sol.u[end][:, :, 2] + result[1:N, (N+1):2N] .= sol.u[end][:, :, 1] + result[(N+1):2N, (N+1):2N] .= sol.u[end][:, :, 2] return result end @@ -1432,8 +1440,8 @@ Apply the chunk propagator `prop` to the current state `odet.u` in-place. The propagator acts as a linear map on the (U₁, U₂) pair: - U₁_new = block_upper_ic[:,:,1] · U₁_prev + block_lower_ic[:,:,1] · U₂_prev - U₂_new = block_upper_ic[:,:,2] · U₁_prev + block_lower_ic[:,:,2] · U₂_prev +U₁_new = block_upper_ic[:,:,1] · U₁_prev + block_lower_ic[:,:,1] · U₂_prev +U₂_new = block_upper_ic[:,:,2] · U₁_prev + block_lower_ic[:,:,2] · U₂_prev This correctly propagates any state (not just the identity), including the (S, I) form produced by Riccati-style crossings. @@ -1488,10 +1496,10 @@ function apply_propagator_inverse!(odet::OdeState, prop::ChunkPropagator) # Φ_bwd maps state at psi_end → psi_start (well-conditioned). # We want Φ_fwd = Φ_bwd⁻¹ to advance state from psi_start → psi_end. # Solving Φ_bwd · x = [U₁_old; U₂_old] gives x = Φ_bwd⁻¹ · [U₁_old; U₂_old]. - u_old = [odet.u[:,:,1]; odet.u[:,:,2]] # 2N × N + u_old = [odet.u[:, :, 1]; odet.u[:, :, 2]] # 2N × N u_new = Φ \ u_old # LU solve, 2N × N - odet.u[:,:,1] .= u_new[1:N, :] - odet.u[:,:,2] .= u_new[N+1:2N, :] + odet.u[:, :, 1] .= u_new[1:N, :] + odet.u[:, :, 2] .= u_new[(N+1):2N, :] end """ @@ -1504,33 +1512,34 @@ this is the only branch that produces them. Solves the same system as [`forward_eulerlagrange_integration`](@ref), but integrates all bulk chunks concurrently using `Threads.@threads`, then re-integrates the outer plasma serially: -1. **Chunk generation**: calls `chunk_el_integration_bounds`, then `balance_integration_chunks` - to sub-divide chunks for load balancing. The chunk count depends only on `intr.msing` and - `ctrl.nchunks`, never on the thread count, so results are thread-independent. -2. **Propagator phase**: `integrate_propagator_chunk!` integrates each chunk independently - from identity initial conditions (no accumulated state, no normalization/callback). - Each thread uses a private `OdeState` proxy for `sing_der!` side effects. -3. **Serial assembly**: propagators are applied sequentially with `apply_propagator!`. - Rational surface crossings use `riccati_cross_ideal_singular_surf!` (no Gaussian - reduction). -4. **Outer plasma re-integration**: after the last rational surface crossing, the outer - plasma (from last ψ_s to psilim) is re-integrated using `riccati_integrate_chunk!`. - FM propagation in this region is prone to precision loss for high N (exponential growth - without renormalization); Riccati integration keeps matrices bounded and provides dense - checkpoints for `findmax_dW_edge!`. + 1. **Chunk generation**: calls `chunk_el_integration_bounds`, then `balance_integration_chunks` + to sub-divide chunks for load balancing. The chunk count depends only on `intr.msing` and + `ctrl.nchunks`, never on the thread count, so results are thread-independent. + 2. **Propagator phase**: `integrate_propagator_chunk!` integrates each chunk independently + from identity initial conditions (no accumulated state, no normalization/callback). + Each thread uses a private `OdeState` proxy for `sing_der!` side effects. + 3. **Serial assembly**: propagators are applied sequentially with `apply_propagator!`. + Rational surface crossings use `riccati_cross_ideal_singular_surf!` (no Gaussian + reduction). + 4. **Outer plasma re-integration**: after the last rational surface crossing, the outer + plasma (from last ψ_s to psilim) is re-integrated using `riccati_integrate_chunk!`. + FM propagation in this region is prone to precision loss for high N (exponential growth + without renormalization); Riccati integration keeps matrices bounded and provides dense + checkpoints for `findmax_dW_edge!`. Select via `integrator = "riccati"` in `[ForceFreeStates]` of gpec.toml. Requires `singfac_min != 0`. Uses whatever threads `julia -t` provides; `ctrl.nchunks` is the only tunable. **Key differences from the forward integrator:** -- No Gaussian reduction in the propagator BVP phase (crossings use the - Riccati-style algorithm, `odet.ifix` stays 0) -- `transform_u!` is called on the odet but is a no-op (ifix=0) -- Outer plasma uses serial Riccati integration for numerical stability -- `odet.u_store` holds chunk-endpoint Riccati states, not dense Euler-Lagrange ξ, and - `odet.u_store_el_basis` stays `false`: this integrator never claims the EL basis, so - PerturbedEquilibrium and the HDF5 forward-integration ξ datasets require the forward path. + + - No Gaussian reduction in the propagator BVP phase (crossings use the + Riccati-style algorithm, `odet.ifix` stays 0) + - `transform_u!` is called on the odet but is a no-op (ifix=0) + - Outer plasma uses serial Riccati integration for numerical stability + - `odet.u_store` holds chunk-endpoint Riccati states, not dense Euler-Lagrange ξ, and + `odet.u_store_el_basis` stays `false`: this integrator never claims the EL basis, so + PerturbedEquilibrium and the HDF5 forward-integration ξ datasets require the forward path. **Bidirectional integration for large-N accuracy:** The crossing chunk (nearest to each rational surface singL[j]) is integrated *backward* @@ -1577,9 +1586,9 @@ end # Build odet and initialize at the magnetic axis. Same path as serial eulerlagrange_integration. function _initialize_parallel_odet(ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, - ffit::FourFitVars, - intr::ForceFreeStatesInternal) + equil::Equilibrium.PlasmaEquilibrium, + ffit::FourFitVars, + intr::ForceFreeStatesInternal) odet = OdeState(intr.numpert_total, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) if ctrl.sing_start <= 0 initialize_el_at_axis!(odet, ctrl, ffit, equil.profiles, intr) @@ -1598,7 +1607,7 @@ end # per-thread proxy OdeStates sized by maxthreadid() (Julia 1.9+ may report threadid # values above nthreads() due to the interactive thread pool). function _setup_parallel_chunks_and_proxies(odet::OdeState, ctrl::ForceFreeStatesControl, - intr::ForceFreeStatesInternal) + intr::ForceFreeStatesInternal) # Bidirectional chunks: crossing chunks are assigned direction=-1 so they are # integrated backward. The resulting Φ_bwd is well-conditioned because growing EL # solutions decay backward; forward propagation is recovered via LU solve in @@ -1612,8 +1621,8 @@ function _setup_parallel_chunks_and_proxies(odet::OdeState, ctrl::ForceFreeState end function _log_parallel_start(ctrl::ForceFreeStatesControl, odet::OdeState, - equil::Equilibrium.PlasmaEquilibrium, - chunks::Vector{IntegrationChunk}) + equil::Equilibrium.PlasmaEquilibrium, + chunks::Vector{IntegrationChunk}) ctrl.verbose || return @info " ψ = $((@sprintf "%.3f" odet.psifac)), q = $((@sprintf "%.3f" equil.profiles.q_spline(odet.psifac)))" @info " Riccati FM: $(length(chunks)) chunks over $(Threads.nthreads()) thread$(Threads.nthreads() == 1 ? "" : "s")" @@ -1624,14 +1633,14 @@ end # Each chunk is independent (identity IC, no accumulated state), so the result does not # depend on how chunks are distributed across threads. function _run_parallel_bvp_phase!(propagators::Vector{ChunkPropagator}, - chunks::Vector{IntegrationChunk}, - ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, - intr::ForceFreeStatesInternal, - odet_proxies::Vector{OdeState}) + chunks::Vector{IntegrationChunk}, + ctrl::ForceFreeStatesControl, + equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, + intr::ForceFreeStatesInternal, + odet_proxies::Vector{OdeState}) Threads.@threads :static for i in eachindex(chunks) integrate_propagator_chunk!(propagators[i], chunks[i], ctrl, equil, ffit, intr, - odet_proxies[Threads.threadid()]) + odet_proxies[Threads.threadid()]) end end @@ -1643,10 +1652,10 @@ end # the well-conditioned Riccati S at each surface's left boundary for use as the Δ' BVP # axis BC. Returns (S_at_surface_left, last_crossing_step). function _assemble_propagators_serially!(odet::OdeState, propagators::Vector{ChunkPropagator}, - chunks::Vector{IntegrationChunk}, - ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, - ffit::FourFitVars, intr::ForceFreeStatesInternal) + chunks::Vector{IntegrationChunk}, + ctrl::ForceFreeStatesControl, + equil::Equilibrium.PlasmaEquilibrium, + ffit::FourFitVars, intr::ForceFreeStatesInternal) N = intr.numpert_total S_at_surface_left = Matrix{ComplexF64}[] last_crossing_step = 1 @@ -1693,9 +1702,9 @@ end # entry at last_crossing_step holds (U₁_new, U₂_new) from riccati_cross_ideal_singular_surf! # before renormalization; we renorm here to (S_new, I) as the Riccati starting state. function _reintegrate_outer_plasma!(odet::OdeState, last_crossing_step::Int, - ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, - intr::ForceFreeStatesInternal) + ctrl::ForceFreeStatesControl, + equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, + intr::ForceFreeStatesInternal) N = intr.numpert_total odet.u .= odet.u_store[:, :, :, last_crossing_step] odet.psifac = odet.psi_store[last_crossing_step] @@ -1703,7 +1712,7 @@ function _reintegrate_outer_plasma!(odet::OdeState, last_crossing_step::Int, odet.step = last_crossing_step + 1 renormalize_riccati_inplace!(odet.u, N) outer_chunk = IntegrationChunk(; psi_start=odet.psifac, psi_end=intr.psilim * (1 - eps), - needs_crossing=false, ising=0) + needs_crossing=false, ising=0) riccati_integrate_chunk!(odet, ctrl, equil, ffit, intr, outer_chunk) # Post: odet.u is in (S, I) form; odet.step points to next empty slot. end @@ -1717,10 +1726,10 @@ end # original psilim — silently shifting the outermost rational's Δ' by tens of percent. # Returns the (possibly truncated) chunks and propagators arrays. function _handle_edge_dW_scan!(odet::OdeState, chunks::Vector{IntegrationChunk}, - propagators::Vector{ChunkPropagator}, - ctrl::ForceFreeStatesControl, - equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, - intr::ForceFreeStatesInternal) + propagators::Vector{ChunkPropagator}, + ctrl::ForceFreeStatesControl, + equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, + intr::ForceFreeStatesInternal) N = intr.numpert_total odet.step -= 1 trim_storage!(odet) @@ -1754,22 +1763,22 @@ function _handle_edge_dW_scan!(odet::OdeState, chunks::Vector{IntegrationChunk}, end straddling = chunks[last_chunk_idx] if straddling.psi_end > peak_psi - new_chunk = IntegrationChunk( - psi_start = straddling.psi_start, - psi_end = peak_psi, - needs_crossing = straddling.needs_crossing, - ising = straddling.ising, - direction = straddling.direction, + new_chunk = IntegrationChunk(; + psi_start=straddling.psi_start, + psi_end=peak_psi, + needs_crossing=straddling.needs_crossing, + ising=straddling.ising, + direction=straddling.direction ) chunks[last_chunk_idx] = new_chunk odet_proxy = OdeState(N, 1, 1, 0) integrate_propagator_chunk!(propagators[last_chunk_idx], new_chunk, - ctrl, equil, ffit, intr, odet_proxy) + ctrl, equil, ffit, intr, odet_proxy) end n_dropped = 0 if last_chunk_idx < length(chunks) n_dropped = length(chunks) - last_chunk_idx - chunks = chunks[1:last_chunk_idx] + chunks = chunks[1:last_chunk_idx] propagators = propagators[1:last_chunk_idx] end if ctrl.verbose diff --git a/src/ForceFreeStates/Sing.jl b/src/ForceFreeStates/Sing.jl index 960530f6a..33694477a 100644 --- a/src/ForceFreeStates/Sing.jl +++ b/src/ForceFreeStates/Sing.jl @@ -123,9 +123,11 @@ function sing_lim!(intr::ForceFreeStatesInternal, ctrl::ForceFreeStatesControl, # strategy. Multi-n runs are not supported — the "outermost rational + dmlim/n" cutoff depends # on which n is used — and fall back to qhigh / psihigh truncation with a warning. if ctrl.set_psilim_via_dmlim && intr.nlow <= 0 - error("sing_lim!: set_psilim_via_dmlim = true requires a resolved toroidal range, but got intr.nlow=$(intr.nlow). " * - "Assign intr.nlow / intr.nhigh (from ctrl.nn_low / ctrl.nn_high) before calling sing_lim!, " * - "or set set_psilim_via_dmlim = false to truncate via qhigh / psihigh instead.") + error( + "sing_lim!: set_psilim_via_dmlim = true requires a resolved toroidal range, but got intr.nlow=$(intr.nlow). " * + "Assign intr.nlow / intr.nhigh (from ctrl.nn_low / ctrl.nn_high) before calling sing_lim!, " * + "or set set_psilim_via_dmlim = false to truncate via qhigh / psihigh instead." + ) elseif ctrl.set_psilim_via_dmlim && intr.nlow != intr.nhigh @warn "set_psilim_via_dmlim = true is ignored for multi-n runs (nn_low=$(intr.nlow), nn_high=$(intr.nhigh)); falling back to qhigh / psihigh truncation." elseif ctrl.set_psilim_via_dmlim diff --git a/src/ForcingTerms/BiotSavart.jl b/src/ForcingTerms/BiotSavart.jl index c03ab07c6..cd0bbb79e 100644 --- a/src/ForcingTerms/BiotSavart.jl +++ b/src/ForcingTerms/BiotSavart.jl @@ -45,27 +45,30 @@ Build contiguous segment-vector and midpoint arrays for strand `(j,k)` of coil s function precompute_segments(cs::CoilSet, j::Int, k::Int, current_nw::Float64) nsec = cs.nsec nseg = nsec - 1 - dlx = Vector{Float64}(undef, nseg) - dly = Vector{Float64}(undef, nseg) - dlz = Vector{Float64}(undef, nseg) + dlx = Vector{Float64}(undef, nseg) + dly = Vector{Float64}(undef, nseg) + dlz = Vector{Float64}(undef, nseg) midx = Vector{Float64}(undef, nseg) midy = Vector{Float64}(undef, nseg) midz = Vector{Float64}(undef, nseg) @inbounds for l in 1:nseg - x1 = cs.x[j, k, l]; x2 = cs.x[j, k, l+1] - y1 = cs.y[j, k, l]; y2 = cs.y[j, k, l+1] - z1 = cs.z[j, k, l]; z2 = cs.z[j, k, l+1] - dlx[l] = x2 - x1 - dly[l] = y2 - y1 - dlz[l] = z2 - z1 + x1 = cs.x[j, k, l] + x2 = cs.x[j, k, l+1] + y1 = cs.y[j, k, l] + y2 = cs.y[j, k, l+1] + z1 = cs.z[j, k, l] + z2 = cs.z[j, k, l+1] + dlx[l] = x2 - x1 + dly[l] = y2 - y1 + dlz[l] = z2 - z1 midx[l] = (x1 + x2) * 0.5 midy[l] = (y1 + y2) * 0.5 midz[l] = (z1 + z2) * 0.5 end return PrecomputedSegments(dlx, dly, dlz, midx, midy, midz, - MU0_OVER_4PI * current_nw, nseg) + MU0_OVER_4PI * current_nw, nseg) end """ @@ -74,15 +77,16 @@ end Accumulate the Biot-Savart contribution from one conductor strand onto observation point `i`. Uses midpoint-rule integration: each segment from point `l` to `l+1` contributes: - dB = μ₀/(4π) × current_nw × (dl⃗ × r⃗) / |r|³ +dB = μ₀/(4π) × current_nw × (dl⃗ × r⃗) / |r|³ where `r⃗` is the displacement from the segment midpoint to the observer. ## Arguments -- `Bx`, `By`, `Bz`: pre-allocated output arrays (Cartesian field, Tesla); updated in-place -- `i`: observation point index -- `obs_x/y/z`: observer Cartesian coordinates [m] -- `xs`, `ys`, `zs`: strand point coordinates [nsec], Cartesian [m] -- `current_nw`: effective current = I × nw [A⋅turns] + + - `Bx`, `By`, `Bz`: pre-allocated output arrays (Cartesian field, Tesla); updated in-place + - `i`: observation point index + - `obs_x/y/z`: observer Cartesian coordinates [m] + - `xs`, `ys`, `zs`: strand point coordinates [nsec], Cartesian [m] + - `current_nw`: effective current = I × nw [A⋅turns] """ function accumulate_strand_field!( Bx::AbstractVector{Float64}, @@ -98,7 +102,7 @@ function accumulate_strand_field!( nsec = length(xs) prefactor = MU0_OVER_4PI * current_nw - @inbounds for l in 1:(nsec - 1) + @inbounds for l in 1:(nsec-1) # Segment vector dl⃗ dlx = xs[l+1] - xs[l] dly = ys[l+1] - ys[l] @@ -109,7 +113,7 @@ function accumulate_strand_field!( ry = obs_y - (ys[l] + ys[l+1]) * 0.5 rz = obs_z - (zs[l] + zs[l+1]) * 0.5 - r2 = rx*rx + ry*ry + rz*rz + r2 = rx*rx + ry*ry + rz*rz r2 < BIOT_SAVART_MIN_DIST_SQ && continue # avoid singularity (observer on conductor) inv_r3 = prefactor / (r2 * sqrt(r2)) @@ -146,8 +150,8 @@ cache efficiency. r2 = rx*rx + ry*ry + rz*rz # Guard: zero contribution if observer is on conductor (avoids Inf/NaN) safe_r2 = ifelse(r2 < BIOT_SAVART_MIN_DIST_SQ, one(r2), r2) - scale = ifelse(r2 < BIOT_SAVART_MIN_DIST_SQ, zero(pf), pf) - inv_r3 = scale / (safe_r2 * sqrt(safe_r2)) + scale = ifelse(r2 < BIOT_SAVART_MIN_DIST_SQ, zero(pf), pf) + inv_r3 = scale / (safe_r2 * sqrt(safe_r2)) bx_acc += (seg.dly[l]*rz - seg.dlz[l]*ry) * inv_r3 by_acc += (seg.dlz[l]*rx - seg.dlx[l]*rz) * inv_r3 @@ -172,9 +176,10 @@ Performance: segment vectors and midpoints are pre-computed once outside the observation-point loop, then accessed from contiguous arrays in the inner kernel. ## Arguments -- `B_R`, `B_phi`, `B_Z`: output field arrays [Tesla], length `nobs`; overwritten -- `obs_R`, `obs_phi`, `obs_Z`: observation point cylindrical coordinates [m, rad, m] -- `coil_sets`: conductor geometry and current data + + - `B_R`, `B_phi`, `B_Z`: output field arrays [Tesla], length `nobs`; overwritten + - `obs_R`, `obs_phi`, `obs_Z`: observation point cylindrical coordinates [m, rad, m] + - `coil_sets`: conductor geometry and current data """ function compute_biot_savart_boundary!( B_R::AbstractVector{Float64}, @@ -204,8 +209,8 @@ function compute_biot_savart_boundary!( # Pre-compute sincos for cylindrical ↔ Cartesian conversion (avoids redundant trig) cos_phi = Vector{Float64}(undef, nobs) sin_phi = Vector{Float64}(undef, nobs) - obs_x = Vector{Float64}(undef, nobs) - obs_y = Vector{Float64}(undef, nobs) + obs_x = Vector{Float64}(undef, nobs) + obs_y = Vector{Float64}(undef, nobs) @inbounds for i in 1:nobs sp, cp = sincos(obs_phi[i]) @@ -218,7 +223,9 @@ function compute_biot_savart_boundary!( # Each thread writes exclusively to its own index i — no synchronization needed Threads.@threads for i in 1:nobs @inbounds begin - ox = obs_x[i]; oy = obs_y[i]; oz = obs_Z[i] + ox = obs_x[i] + oy = obs_y[i] + oz = obs_Z[i] bx_total = 0.0 by_total = 0.0 bz_total = 0.0 @@ -233,9 +240,9 @@ function compute_biot_savart_boundary!( # Convert Cartesian → cylindrical in-place cp = cos_phi[i] sp = sin_phi[i] - B_R[i] = bx_total * cp + by_total * sp + B_R[i] = bx_total * cp + by_total * sp B_phi[i] = -bx_total * sp + by_total * cp - B_Z[i] = bz_total + B_Z[i] = bz_total end end end diff --git a/src/ForcingTerms/CoilGeometry.jl b/src/ForcingTerms/CoilGeometry.jl index b2d19ab21..477502927 100644 --- a/src/ForcingTerms/CoilGeometry.jl +++ b/src/ForcingTerms/CoilGeometry.jl @@ -265,7 +265,7 @@ function read_coil_dat(filepath::String) for l in 1:nsec # Skip blank/comment lines between data rows while row <= length(lines) && - (isempty(strip(lines[row])) || startswith(strip(lines[row]), "#")) + (isempty(strip(lines[row])) || startswith(strip(lines[row]), "#")) row += 1 end row > length(lines) && error( @@ -402,10 +402,12 @@ function make_window_pane_standoff(equil; standoff::Real, poloidal_angle::Real, # boundary along the normal is standoff - half*|sin(tilt)| — the tilt tips the legs toward the # surface, while tangential displacement leaves the normal clearance unchanged. clearance = standoff - half * abs(sin(deg2rad(poloidal_tilt))) - clearance > 0 || error("make_window_pane_standoff: coil intersects the plasma boundary " * - "(standoff=$standoff, poloidal_length=$poloidal_length, poloidal_tilt=$poloidal_tilt); " * - "an external coil must lie outside the control surface — increase standoff or reduce " * - "poloidal_length/poloidal_tilt") + clearance > 0 || error( + "make_window_pane_standoff: coil intersects the plasma boundary " * + "(standoff=$standoff, poloidal_length=$poloidal_length, poloidal_tilt=$poloidal_tilt); " * + "an external coil must lie outside the control surface — increase standoff or reduce " * + "poloidal_length/poloidal_tilt" + ) c1 = [Rc - half * uR, Zc - half * uZ] c2 = [Rc + half * uR, Zc + half * uZ] diff --git a/src/ForcingTerms/coil_geometries/jet_ex_mid.dat b/src/ForcingTerms/coil_geometries/jet_ex_mid.dat index ea2549ad3..639e8ca41 100644 --- a/src/ForcingTerms/coil_geometries/jet_ex_mid.dat +++ b/src/ForcingTerms/coil_geometries/jet_ex_mid.dat @@ -1,169 +1,169 @@ - 4 1 42 1.00 - 5.5000e+00 0.0000e+00 3.0900e+00 - 5.4795e+00 4.7404e-01 3.0900e+00 - 5.4183e+00 9.4456e-01 3.0900e+00 - 5.3167e+00 1.4080e+00 3.0900e+00 - 5.1756e+00 1.8611e+00 3.0900e+00 - 4.9959e+00 2.3002e+00 3.0900e+00 - 4.7791e+00 2.7222e+00 3.0900e+00 - 4.5266e+00 3.1240e+00 3.0900e+00 - 4.2405e+00 3.5025e+00 3.0900e+00 - 3.9229e+00 3.8550e+00 3.0900e+00 - 3.5760e+00 4.1788e+00 3.0900e+00 - 3.2025e+00 4.4714e+00 3.0900e+00 - 2.8052e+00 4.7308e+00 3.0900e+00 - 2.3870e+00 4.9550e+00 3.0900e+00 - 1.9511e+00 5.1423e+00 3.0900e+00 - 1.5006e+00 5.2913e+00 3.0900e+00 - 1.0390e+00 5.4010e+00 3.0900e+00 - 5.6960e-01 5.4704e+00 3.0900e+00 - 9.5988e-02 5.4992e+00 3.0900e+00 - 9.5988e-02 5.4992e+00 3.0900e+00 - 9.5988e-02 5.4992e+00 1.5700e+00 - 9.5988e-02 5.4992e+00 5.0000e-02 - 9.5988e-02 5.4992e+00 -1.4700e+00 - 9.5988e-02 5.4992e+00 -2.9900e+00 - 7.0430e-01 5.4547e+00 -2.9900e+00 - 1.3039e+00 5.3432e+00 -2.9900e+00 - 1.8876e+00 5.1660e+00 -2.9900e+00 - 2.4479e+00 4.9252e+00 -2.9900e+00 - 2.9782e+00 4.6239e+00 -2.9900e+00 - 3.4719e+00 4.2657e+00 -2.9900e+00 - 3.9229e+00 3.8550e+00 -2.9900e+00 - 4.3256e+00 3.3969e+00 -2.9900e+00 - 4.6751e+00 2.8971e+00 -2.9900e+00 - 4.9672e+00 2.3616e+00 -2.9900e+00 - 5.1981e+00 1.7971e+00 -2.9900e+00 - 5.3651e+00 1.2105e+00 -2.9900e+00 - 5.4662e+00 6.0899e-01 -2.9900e+00 - 5.5000e+00 0.0000e+00 -2.9900e+00 - 5.5000e+00 0.0000e+00 -1.4700e+00 - 5.5000e+00 0.0000e+00 5.0000e-02 - 5.5000e+00 0.0000e+00 1.5700e+00 - 5.5000e+00 0.0000e+00 3.0900e+00 - 3.3678e-16 5.5000e+00 3.0900e+00 - -4.7404e-01 5.4795e+00 3.0900e+00 - -9.4456e-01 5.4183e+00 3.0900e+00 - -1.4080e+00 5.3167e+00 3.0900e+00 - -1.8611e+00 5.1756e+00 3.0900e+00 - -2.3002e+00 4.9959e+00 3.0900e+00 - -2.7222e+00 4.7791e+00 3.0900e+00 - -3.1240e+00 4.5266e+00 3.0900e+00 - -3.5025e+00 4.2405e+00 3.0900e+00 - -3.8550e+00 3.9229e+00 3.0900e+00 - -4.1788e+00 3.5760e+00 3.0900e+00 - -4.4714e+00 3.2025e+00 3.0900e+00 - -4.7308e+00 2.8052e+00 3.0900e+00 - -4.9550e+00 2.3870e+00 3.0900e+00 - -5.1423e+00 1.9511e+00 3.0900e+00 - -5.2913e+00 1.5006e+00 3.0900e+00 - -5.4010e+00 1.0390e+00 3.0900e+00 - -5.4704e+00 5.6960e-01 3.0900e+00 - -5.4992e+00 9.5988e-02 3.0900e+00 - -5.4992e+00 9.5988e-02 3.0900e+00 - -5.4992e+00 9.5988e-02 1.5700e+00 - -5.4992e+00 9.5988e-02 5.0000e-02 - -5.4992e+00 9.5988e-02 -1.4700e+00 - -5.4992e+00 9.5988e-02 -2.9900e+00 - -5.4547e+00 7.0430e-01 -2.9900e+00 - -5.3432e+00 1.3039e+00 -2.9900e+00 - -5.1660e+00 1.8876e+00 -2.9900e+00 - -4.9252e+00 2.4479e+00 -2.9900e+00 - -4.6239e+00 2.9782e+00 -2.9900e+00 - -4.2657e+00 3.4719e+00 -2.9900e+00 - -3.8550e+00 3.9229e+00 -2.9900e+00 - -3.3969e+00 4.3256e+00 -2.9900e+00 - -2.8971e+00 4.6751e+00 -2.9900e+00 - -2.3616e+00 4.9672e+00 -2.9900e+00 - -1.7971e+00 5.1981e+00 -2.9900e+00 - -1.2105e+00 5.3651e+00 -2.9900e+00 - -6.0899e-01 5.4662e+00 -2.9900e+00 - 3.3678e-16 5.5000e+00 -2.9900e+00 - 3.3678e-16 5.5000e+00 -1.4700e+00 - 3.3678e-16 5.5000e+00 5.0000e-02 - 3.3678e-16 5.5000e+00 1.5700e+00 - 3.3678e-16 5.5000e+00 3.0900e+00 - -5.5000e+00 6.7356e-16 3.0900e+00 - -5.4795e+00 -4.7404e-01 3.0900e+00 - -5.4183e+00 -9.4456e-01 3.0900e+00 - -5.3167e+00 -1.4080e+00 3.0900e+00 - -5.1756e+00 -1.8611e+00 3.0900e+00 - -4.9959e+00 -2.3002e+00 3.0900e+00 - -4.7791e+00 -2.7222e+00 3.0900e+00 - -4.5266e+00 -3.1240e+00 3.0900e+00 - -4.2405e+00 -3.5025e+00 3.0900e+00 - -3.9229e+00 -3.8550e+00 3.0900e+00 - -3.5760e+00 -4.1788e+00 3.0900e+00 - -3.2025e+00 -4.4714e+00 3.0900e+00 - -2.8052e+00 -4.7308e+00 3.0900e+00 - -2.3870e+00 -4.9550e+00 3.0900e+00 - -1.9511e+00 -5.1423e+00 3.0900e+00 - -1.5006e+00 -5.2913e+00 3.0900e+00 - -1.0390e+00 -5.4010e+00 3.0900e+00 - -5.6960e-01 -5.4704e+00 3.0900e+00 - -9.5988e-02 -5.4992e+00 3.0900e+00 - -9.5988e-02 -5.4992e+00 3.0900e+00 - -9.5988e-02 -5.4992e+00 1.5700e+00 - -9.5988e-02 -5.4992e+00 5.0000e-02 - -9.5988e-02 -5.4992e+00 -1.4700e+00 - -9.5988e-02 -5.4992e+00 -2.9900e+00 - -7.0430e-01 -5.4547e+00 -2.9900e+00 - -1.3039e+00 -5.3432e+00 -2.9900e+00 - -1.8876e+00 -5.1660e+00 -2.9900e+00 - -2.4479e+00 -4.9252e+00 -2.9900e+00 - -2.9782e+00 -4.6239e+00 -2.9900e+00 - -3.4719e+00 -4.2657e+00 -2.9900e+00 - -3.9229e+00 -3.8550e+00 -2.9900e+00 - -4.3256e+00 -3.3969e+00 -2.9900e+00 - -4.6751e+00 -2.8971e+00 -2.9900e+00 - -4.9672e+00 -2.3616e+00 -2.9900e+00 - -5.1981e+00 -1.7971e+00 -2.9900e+00 - -5.3651e+00 -1.2105e+00 -2.9900e+00 - -5.4662e+00 -6.0899e-01 -2.9900e+00 - -5.5000e+00 6.7356e-16 -2.9900e+00 - -5.5000e+00 6.7356e-16 -1.4700e+00 - -5.5000e+00 6.7356e-16 5.0000e-02 - -5.5000e+00 6.7356e-16 1.5700e+00 - -5.5000e+00 6.7356e-16 3.0900e+00 - -1.0103e-15 -5.5000e+00 3.0900e+00 - 4.7404e-01 -5.4795e+00 3.0900e+00 - 9.4456e-01 -5.4183e+00 3.0900e+00 - 1.4080e+00 -5.3167e+00 3.0900e+00 - 1.8611e+00 -5.1756e+00 3.0900e+00 - 2.3002e+00 -4.9959e+00 3.0900e+00 - 2.7222e+00 -4.7791e+00 3.0900e+00 - 3.1240e+00 -4.5266e+00 3.0900e+00 - 3.5025e+00 -4.2405e+00 3.0900e+00 - 3.8550e+00 -3.9229e+00 3.0900e+00 - 4.1788e+00 -3.5760e+00 3.0900e+00 - 4.4714e+00 -3.2025e+00 3.0900e+00 - 4.7308e+00 -2.8052e+00 3.0900e+00 - 4.9550e+00 -2.3870e+00 3.0900e+00 - 5.1423e+00 -1.9511e+00 3.0900e+00 - 5.2913e+00 -1.5006e+00 3.0900e+00 - 5.4010e+00 -1.0390e+00 3.0900e+00 - 5.4704e+00 -5.6960e-01 3.0900e+00 - 5.4992e+00 -9.5988e-02 3.0900e+00 - 5.4992e+00 -9.5988e-02 3.0900e+00 - 5.4992e+00 -9.5988e-02 1.5700e+00 - 5.4992e+00 -9.5988e-02 5.0000e-02 - 5.4992e+00 -9.5988e-02 -1.4700e+00 - 5.4992e+00 -9.5988e-02 -2.9900e+00 - 5.4547e+00 -7.0430e-01 -2.9900e+00 - 5.3432e+00 -1.3039e+00 -2.9900e+00 - 5.1660e+00 -1.8876e+00 -2.9900e+00 - 4.9252e+00 -2.4479e+00 -2.9900e+00 - 4.6239e+00 -2.9782e+00 -2.9900e+00 - 4.2657e+00 -3.4719e+00 -2.9900e+00 - 3.8550e+00 -3.9229e+00 -2.9900e+00 - 3.3969e+00 -4.3256e+00 -2.9900e+00 - 2.8971e+00 -4.6751e+00 -2.9900e+00 - 2.3616e+00 -4.9672e+00 -2.9900e+00 - 1.7971e+00 -5.1981e+00 -2.9900e+00 - 1.2105e+00 -5.3651e+00 -2.9900e+00 - 6.0899e-01 -5.4662e+00 -2.9900e+00 - -1.0103e-15 -5.5000e+00 -2.9900e+00 - -1.0103e-15 -5.5000e+00 -1.4700e+00 - -1.0103e-15 -5.5000e+00 5.0000e-02 - -1.0103e-15 -5.5000e+00 1.5700e+00 - -1.0103e-15 -5.5000e+00 3.0900e+00 + 4 1 42 1.00 + 5.5000e+00 0.0000e+00 3.0900e+00 + 5.4795e+00 4.7404e-01 3.0900e+00 + 5.4183e+00 9.4456e-01 3.0900e+00 + 5.3167e+00 1.4080e+00 3.0900e+00 + 5.1756e+00 1.8611e+00 3.0900e+00 + 4.9959e+00 2.3002e+00 3.0900e+00 + 4.7791e+00 2.7222e+00 3.0900e+00 + 4.5266e+00 3.1240e+00 3.0900e+00 + 4.2405e+00 3.5025e+00 3.0900e+00 + 3.9229e+00 3.8550e+00 3.0900e+00 + 3.5760e+00 4.1788e+00 3.0900e+00 + 3.2025e+00 4.4714e+00 3.0900e+00 + 2.8052e+00 4.7308e+00 3.0900e+00 + 2.3870e+00 4.9550e+00 3.0900e+00 + 1.9511e+00 5.1423e+00 3.0900e+00 + 1.5006e+00 5.2913e+00 3.0900e+00 + 1.0390e+00 5.4010e+00 3.0900e+00 + 5.6960e-01 5.4704e+00 3.0900e+00 + 9.5988e-02 5.4992e+00 3.0900e+00 + 9.5988e-02 5.4992e+00 3.0900e+00 + 9.5988e-02 5.4992e+00 1.5700e+00 + 9.5988e-02 5.4992e+00 5.0000e-02 + 9.5988e-02 5.4992e+00 -1.4700e+00 + 9.5988e-02 5.4992e+00 -2.9900e+00 + 7.0430e-01 5.4547e+00 -2.9900e+00 + 1.3039e+00 5.3432e+00 -2.9900e+00 + 1.8876e+00 5.1660e+00 -2.9900e+00 + 2.4479e+00 4.9252e+00 -2.9900e+00 + 2.9782e+00 4.6239e+00 -2.9900e+00 + 3.4719e+00 4.2657e+00 -2.9900e+00 + 3.9229e+00 3.8550e+00 -2.9900e+00 + 4.3256e+00 3.3969e+00 -2.9900e+00 + 4.6751e+00 2.8971e+00 -2.9900e+00 + 4.9672e+00 2.3616e+00 -2.9900e+00 + 5.1981e+00 1.7971e+00 -2.9900e+00 + 5.3651e+00 1.2105e+00 -2.9900e+00 + 5.4662e+00 6.0899e-01 -2.9900e+00 + 5.5000e+00 0.0000e+00 -2.9900e+00 + 5.5000e+00 0.0000e+00 -1.4700e+00 + 5.5000e+00 0.0000e+00 5.0000e-02 + 5.5000e+00 0.0000e+00 1.5700e+00 + 5.5000e+00 0.0000e+00 3.0900e+00 + 3.3678e-16 5.5000e+00 3.0900e+00 + -4.7404e-01 5.4795e+00 3.0900e+00 + -9.4456e-01 5.4183e+00 3.0900e+00 + -1.4080e+00 5.3167e+00 3.0900e+00 + -1.8611e+00 5.1756e+00 3.0900e+00 + -2.3002e+00 4.9959e+00 3.0900e+00 + -2.7222e+00 4.7791e+00 3.0900e+00 + -3.1240e+00 4.5266e+00 3.0900e+00 + -3.5025e+00 4.2405e+00 3.0900e+00 + -3.8550e+00 3.9229e+00 3.0900e+00 + -4.1788e+00 3.5760e+00 3.0900e+00 + -4.4714e+00 3.2025e+00 3.0900e+00 + -4.7308e+00 2.8052e+00 3.0900e+00 + -4.9550e+00 2.3870e+00 3.0900e+00 + -5.1423e+00 1.9511e+00 3.0900e+00 + -5.2913e+00 1.5006e+00 3.0900e+00 + -5.4010e+00 1.0390e+00 3.0900e+00 + -5.4704e+00 5.6960e-01 3.0900e+00 + -5.4992e+00 9.5988e-02 3.0900e+00 + -5.4992e+00 9.5988e-02 3.0900e+00 + -5.4992e+00 9.5988e-02 1.5700e+00 + -5.4992e+00 9.5988e-02 5.0000e-02 + -5.4992e+00 9.5988e-02 -1.4700e+00 + -5.4992e+00 9.5988e-02 -2.9900e+00 + -5.4547e+00 7.0430e-01 -2.9900e+00 + -5.3432e+00 1.3039e+00 -2.9900e+00 + -5.1660e+00 1.8876e+00 -2.9900e+00 + -4.9252e+00 2.4479e+00 -2.9900e+00 + -4.6239e+00 2.9782e+00 -2.9900e+00 + -4.2657e+00 3.4719e+00 -2.9900e+00 + -3.8550e+00 3.9229e+00 -2.9900e+00 + -3.3969e+00 4.3256e+00 -2.9900e+00 + -2.8971e+00 4.6751e+00 -2.9900e+00 + -2.3616e+00 4.9672e+00 -2.9900e+00 + -1.7971e+00 5.1981e+00 -2.9900e+00 + -1.2105e+00 5.3651e+00 -2.9900e+00 + -6.0899e-01 5.4662e+00 -2.9900e+00 + 3.3678e-16 5.5000e+00 -2.9900e+00 + 3.3678e-16 5.5000e+00 -1.4700e+00 + 3.3678e-16 5.5000e+00 5.0000e-02 + 3.3678e-16 5.5000e+00 1.5700e+00 + 3.3678e-16 5.5000e+00 3.0900e+00 + -5.5000e+00 6.7356e-16 3.0900e+00 + -5.4795e+00 -4.7404e-01 3.0900e+00 + -5.4183e+00 -9.4456e-01 3.0900e+00 + -5.3167e+00 -1.4080e+00 3.0900e+00 + -5.1756e+00 -1.8611e+00 3.0900e+00 + -4.9959e+00 -2.3002e+00 3.0900e+00 + -4.7791e+00 -2.7222e+00 3.0900e+00 + -4.5266e+00 -3.1240e+00 3.0900e+00 + -4.2405e+00 -3.5025e+00 3.0900e+00 + -3.9229e+00 -3.8550e+00 3.0900e+00 + -3.5760e+00 -4.1788e+00 3.0900e+00 + -3.2025e+00 -4.4714e+00 3.0900e+00 + -2.8052e+00 -4.7308e+00 3.0900e+00 + -2.3870e+00 -4.9550e+00 3.0900e+00 + -1.9511e+00 -5.1423e+00 3.0900e+00 + -1.5006e+00 -5.2913e+00 3.0900e+00 + -1.0390e+00 -5.4010e+00 3.0900e+00 + -5.6960e-01 -5.4704e+00 3.0900e+00 + -9.5988e-02 -5.4992e+00 3.0900e+00 + -9.5988e-02 -5.4992e+00 3.0900e+00 + -9.5988e-02 -5.4992e+00 1.5700e+00 + -9.5988e-02 -5.4992e+00 5.0000e-02 + -9.5988e-02 -5.4992e+00 -1.4700e+00 + -9.5988e-02 -5.4992e+00 -2.9900e+00 + -7.0430e-01 -5.4547e+00 -2.9900e+00 + -1.3039e+00 -5.3432e+00 -2.9900e+00 + -1.8876e+00 -5.1660e+00 -2.9900e+00 + -2.4479e+00 -4.9252e+00 -2.9900e+00 + -2.9782e+00 -4.6239e+00 -2.9900e+00 + -3.4719e+00 -4.2657e+00 -2.9900e+00 + -3.9229e+00 -3.8550e+00 -2.9900e+00 + -4.3256e+00 -3.3969e+00 -2.9900e+00 + -4.6751e+00 -2.8971e+00 -2.9900e+00 + -4.9672e+00 -2.3616e+00 -2.9900e+00 + -5.1981e+00 -1.7971e+00 -2.9900e+00 + -5.3651e+00 -1.2105e+00 -2.9900e+00 + -5.4662e+00 -6.0899e-01 -2.9900e+00 + -5.5000e+00 6.7356e-16 -2.9900e+00 + -5.5000e+00 6.7356e-16 -1.4700e+00 + -5.5000e+00 6.7356e-16 5.0000e-02 + -5.5000e+00 6.7356e-16 1.5700e+00 + -5.5000e+00 6.7356e-16 3.0900e+00 + -1.0103e-15 -5.5000e+00 3.0900e+00 + 4.7404e-01 -5.4795e+00 3.0900e+00 + 9.4456e-01 -5.4183e+00 3.0900e+00 + 1.4080e+00 -5.3167e+00 3.0900e+00 + 1.8611e+00 -5.1756e+00 3.0900e+00 + 2.3002e+00 -4.9959e+00 3.0900e+00 + 2.7222e+00 -4.7791e+00 3.0900e+00 + 3.1240e+00 -4.5266e+00 3.0900e+00 + 3.5025e+00 -4.2405e+00 3.0900e+00 + 3.8550e+00 -3.9229e+00 3.0900e+00 + 4.1788e+00 -3.5760e+00 3.0900e+00 + 4.4714e+00 -3.2025e+00 3.0900e+00 + 4.7308e+00 -2.8052e+00 3.0900e+00 + 4.9550e+00 -2.3870e+00 3.0900e+00 + 5.1423e+00 -1.9511e+00 3.0900e+00 + 5.2913e+00 -1.5006e+00 3.0900e+00 + 5.4010e+00 -1.0390e+00 3.0900e+00 + 5.4704e+00 -5.6960e-01 3.0900e+00 + 5.4992e+00 -9.5988e-02 3.0900e+00 + 5.4992e+00 -9.5988e-02 3.0900e+00 + 5.4992e+00 -9.5988e-02 1.5700e+00 + 5.4992e+00 -9.5988e-02 5.0000e-02 + 5.4992e+00 -9.5988e-02 -1.4700e+00 + 5.4992e+00 -9.5988e-02 -2.9900e+00 + 5.4547e+00 -7.0430e-01 -2.9900e+00 + 5.3432e+00 -1.3039e+00 -2.9900e+00 + 5.1660e+00 -1.8876e+00 -2.9900e+00 + 4.9252e+00 -2.4479e+00 -2.9900e+00 + 4.6239e+00 -2.9782e+00 -2.9900e+00 + 4.2657e+00 -3.4719e+00 -2.9900e+00 + 3.8550e+00 -3.9229e+00 -2.9900e+00 + 3.3969e+00 -4.3256e+00 -2.9900e+00 + 2.8971e+00 -4.6751e+00 -2.9900e+00 + 2.3616e+00 -4.9672e+00 -2.9900e+00 + 1.7971e+00 -5.1981e+00 -2.9900e+00 + 1.2105e+00 -5.3651e+00 -2.9900e+00 + 6.0899e-01 -5.4662e+00 -2.9900e+00 + -1.0103e-15 -5.5000e+00 -2.9900e+00 + -1.0103e-15 -5.5000e+00 -1.4700e+00 + -1.0103e-15 -5.5000e+00 5.0000e-02 + -1.0103e-15 -5.5000e+00 1.5700e+00 + -1.0103e-15 -5.5000e+00 3.0900e+00 diff --git a/src/ForcingTerms/coil_geometries/jtext_l.dat b/src/ForcingTerms/coil_geometries/jtext_l.dat index b744bedd8..476b55451 100644 --- a/src/ForcingTerms/coil_geometries/jtext_l.dat +++ b/src/ForcingTerms/coil_geometries/jtext_l.dat @@ -1,81 +1,81 @@ - 4 1 20 1.00 - 1.2000e+00 1.6000e-02 -3.9800e-01 - 1.2000e+00 1.6000e-02 -3.3300e-01 - 1.2000e+00 2.5370e-02 -3.1000e-01 - 1.2000e+00 4.8000e-02 -3.0100e-01 - 1.2000e+00 1.3200e-01 -3.0100e-01 - 1.1880e+00 1.5800e-01 -3.0100e-01 - 1.1620e+00 1.6300e-01 -3.0100e-01 - 7.9400e-01 9.0000e-02 -3.0100e-01 - 7.7500e-01 7.9000e-02 -3.0100e-01 - 7.6800e-01 5.8000e-02 -3.0100e-01 - 7.6800e-01 -6.2000e-02 -3.0100e-01 - 7.7500e-01 -8.3000e-02 -3.0100e-01 - 7.9500e-01 -9.4000e-02 -3.0100e-01 - 1.1620e+00 -1.6700e-01 -3.0100e-01 - 1.1880e+00 -1.6000e-01 -3.0100e-01 - 1.2000e+00 -1.3500e-01 -3.0100e-01 - 1.2000e+00 -5.0000e-02 -3.0100e-01 - 1.2000e+00 -2.5370e-02 -3.1000e-01 - 1.2000e+00 -1.6000e-02 -3.3300e-01 - 1.2000e+00 -1.6000e-02 -3.9800e-01 - 1.6000e-02 1.2000e+00 -3.9800e-01 - 1.6000e-02 1.2000e+00 -3.3300e-01 - 2.5370e-02 1.2000e+00 -3.1000e-01 - 4.8000e-02 1.2000e+00 -3.0100e-01 - 1.3200e-01 1.2000e+00 -3.0100e-01 - 1.5800e-01 1.1880e+00 -3.0100e-01 - 1.6300e-01 1.1620e+00 -3.0100e-01 - 9.0000e-02 7.9400e-01 -3.0100e-01 - 7.9000e-02 7.7500e-01 -3.0100e-01 - 5.8000e-02 7.6800e-01 -3.0100e-01 - -6.2000e-02 7.6800e-01 -3.0100e-01 - -8.3000e-02 7.7500e-01 -3.0100e-01 - -9.4000e-02 7.9500e-01 -3.0100e-01 - -1.6700e-01 1.1620e+00 -3.0100e-01 - -1.6000e-01 1.1880e+00 -3.0100e-01 - -1.3500e-01 1.2000e+00 -3.0100e-01 - -5.0000e-02 1.2000e+00 -3.0100e-01 - -2.5370e-02 1.2000e+00 -3.1000e-01 - -1.6000e-02 1.2000e+00 -3.3300e-01 - -1.6000e-02 1.2000e+00 -3.9800e-01 - -7.7650e-01 1.6000e-02 -3.9800e-01 - -7.7650e-01 1.6000e-02 -3.3300e-01 - -7.7650e-01 2.5370e-02 -3.1000e-01 - -7.7650e-01 4.8000e-02 -3.0100e-01 - -7.7650e-01 6.1000e-02 -3.0100e-01 - -7.8350e-01 8.2000e-02 -3.0100e-01 - -8.0250e-01 9.3000e-02 -3.0100e-01 - -1.2015e+00 1.7100e-01 -3.0100e-01 - -1.2275e+00 1.6500e-01 -3.0100e-01 - -1.2395e+00 1.4100e-01 -3.0100e-01 - -1.2395e+00 -1.4100e-01 -3.0100e-01 - -1.2275e+00 -1.6600e-01 -3.0100e-01 - -1.2015e+00 -1.7200e-01 -3.0100e-01 - -8.0250e-01 -9.3000e-02 -3.0100e-01 - -7.8350e-01 -8.2000e-02 -3.0100e-01 - -7.7650e-01 -6.2000e-02 -3.0100e-01 - -7.7650e-01 -4.9000e-02 -3.0100e-01 - -7.7650e-01 -2.5370e-02 -3.1000e-01 - -7.7650e-01 -1.6000e-02 -3.3300e-01 - -7.7650e-01 -1.6000e-02 -3.9800e-01 - 1.6000e-02 -7.7650e-01 -3.9800e-01 - 1.6000e-02 -7.7650e-01 -3.3300e-01 - 2.5370e-02 -7.7650e-01 -3.1000e-01 - 4.8000e-02 -7.7650e-01 -3.0100e-01 - 6.1000e-02 -7.7650e-01 -3.0100e-01 - 8.2000e-02 -7.8350e-01 -3.0100e-01 - 9.3000e-02 -8.0250e-01 -3.0100e-01 - 1.7100e-01 -1.2015e+00 -3.0100e-01 - 1.6500e-01 -1.2275e+00 -3.0100e-01 - 1.4100e-01 -1.2395e+00 -3.0100e-01 - -1.4100e-01 -1.2395e+00 -3.0100e-01 - -1.6600e-01 -1.2275e+00 -3.0100e-01 - -1.7200e-01 -1.2015e+00 -3.0100e-01 - -9.3000e-02 -8.0250e-01 -3.0100e-01 - -8.2000e-02 -7.8350e-01 -3.0100e-01 - -6.2000e-02 -7.7650e-01 -3.0100e-01 - -4.9000e-02 -7.7650e-01 -3.0100e-01 - -2.5370e-02 -7.7650e-01 -3.1000e-01 - -1.6000e-02 -7.7650e-01 -3.3300e-01 - -1.6000e-02 -7.7650e-01 -3.9800e-01 + 4 1 20 1.00 + 1.2000e+00 1.6000e-02 -3.9800e-01 + 1.2000e+00 1.6000e-02 -3.3300e-01 + 1.2000e+00 2.5370e-02 -3.1000e-01 + 1.2000e+00 4.8000e-02 -3.0100e-01 + 1.2000e+00 1.3200e-01 -3.0100e-01 + 1.1880e+00 1.5800e-01 -3.0100e-01 + 1.1620e+00 1.6300e-01 -3.0100e-01 + 7.9400e-01 9.0000e-02 -3.0100e-01 + 7.7500e-01 7.9000e-02 -3.0100e-01 + 7.6800e-01 5.8000e-02 -3.0100e-01 + 7.6800e-01 -6.2000e-02 -3.0100e-01 + 7.7500e-01 -8.3000e-02 -3.0100e-01 + 7.9500e-01 -9.4000e-02 -3.0100e-01 + 1.1620e+00 -1.6700e-01 -3.0100e-01 + 1.1880e+00 -1.6000e-01 -3.0100e-01 + 1.2000e+00 -1.3500e-01 -3.0100e-01 + 1.2000e+00 -5.0000e-02 -3.0100e-01 + 1.2000e+00 -2.5370e-02 -3.1000e-01 + 1.2000e+00 -1.6000e-02 -3.3300e-01 + 1.2000e+00 -1.6000e-02 -3.9800e-01 + 1.6000e-02 1.2000e+00 -3.9800e-01 + 1.6000e-02 1.2000e+00 -3.3300e-01 + 2.5370e-02 1.2000e+00 -3.1000e-01 + 4.8000e-02 1.2000e+00 -3.0100e-01 + 1.3200e-01 1.2000e+00 -3.0100e-01 + 1.5800e-01 1.1880e+00 -3.0100e-01 + 1.6300e-01 1.1620e+00 -3.0100e-01 + 9.0000e-02 7.9400e-01 -3.0100e-01 + 7.9000e-02 7.7500e-01 -3.0100e-01 + 5.8000e-02 7.6800e-01 -3.0100e-01 + -6.2000e-02 7.6800e-01 -3.0100e-01 + -8.3000e-02 7.7500e-01 -3.0100e-01 + -9.4000e-02 7.9500e-01 -3.0100e-01 + -1.6700e-01 1.1620e+00 -3.0100e-01 + -1.6000e-01 1.1880e+00 -3.0100e-01 + -1.3500e-01 1.2000e+00 -3.0100e-01 + -5.0000e-02 1.2000e+00 -3.0100e-01 + -2.5370e-02 1.2000e+00 -3.1000e-01 + -1.6000e-02 1.2000e+00 -3.3300e-01 + -1.6000e-02 1.2000e+00 -3.9800e-01 + -7.7650e-01 1.6000e-02 -3.9800e-01 + -7.7650e-01 1.6000e-02 -3.3300e-01 + -7.7650e-01 2.5370e-02 -3.1000e-01 + -7.7650e-01 4.8000e-02 -3.0100e-01 + -7.7650e-01 6.1000e-02 -3.0100e-01 + -7.8350e-01 8.2000e-02 -3.0100e-01 + -8.0250e-01 9.3000e-02 -3.0100e-01 + -1.2015e+00 1.7100e-01 -3.0100e-01 + -1.2275e+00 1.6500e-01 -3.0100e-01 + -1.2395e+00 1.4100e-01 -3.0100e-01 + -1.2395e+00 -1.4100e-01 -3.0100e-01 + -1.2275e+00 -1.6600e-01 -3.0100e-01 + -1.2015e+00 -1.7200e-01 -3.0100e-01 + -8.0250e-01 -9.3000e-02 -3.0100e-01 + -7.8350e-01 -8.2000e-02 -3.0100e-01 + -7.7650e-01 -6.2000e-02 -3.0100e-01 + -7.7650e-01 -4.9000e-02 -3.0100e-01 + -7.7650e-01 -2.5370e-02 -3.1000e-01 + -7.7650e-01 -1.6000e-02 -3.3300e-01 + -7.7650e-01 -1.6000e-02 -3.9800e-01 + 1.6000e-02 -7.7650e-01 -3.9800e-01 + 1.6000e-02 -7.7650e-01 -3.3300e-01 + 2.5370e-02 -7.7650e-01 -3.1000e-01 + 4.8000e-02 -7.7650e-01 -3.0100e-01 + 6.1000e-02 -7.7650e-01 -3.0100e-01 + 8.2000e-02 -7.8350e-01 -3.0100e-01 + 9.3000e-02 -8.0250e-01 -3.0100e-01 + 1.7100e-01 -1.2015e+00 -3.0100e-01 + 1.6500e-01 -1.2275e+00 -3.0100e-01 + 1.4100e-01 -1.2395e+00 -3.0100e-01 + -1.4100e-01 -1.2395e+00 -3.0100e-01 + -1.6600e-01 -1.2275e+00 -3.0100e-01 + -1.7200e-01 -1.2015e+00 -3.0100e-01 + -9.3000e-02 -8.0250e-01 -3.0100e-01 + -8.2000e-02 -7.8350e-01 -3.0100e-01 + -6.2000e-02 -7.7650e-01 -3.0100e-01 + -4.9000e-02 -7.7650e-01 -3.0100e-01 + -2.5370e-02 -7.7650e-01 -3.1000e-01 + -1.6000e-02 -7.7650e-01 -3.3300e-01 + -1.6000e-02 -7.7650e-01 -3.9800e-01 diff --git a/src/ForcingTerms/coil_geometries/jtext_m.dat b/src/ForcingTerms/coil_geometries/jtext_m.dat index f211a2d1e..9defc821f 100644 --- a/src/ForcingTerms/coil_geometries/jtext_m.dat +++ b/src/ForcingTerms/coil_geometries/jtext_m.dat @@ -1,81 +1,81 @@ - 4 1 20 1.00 - 1.3300e+00 1.6000e-02 3.5600e-01 - 1.2830e+00 1.6000e-02 2.6500e-01 - 1.3065e+00 2.5370e-02 2.4300e-01 - 1.3300e+00 4.8000e-02 2.1100e-01 - 1.3300e+00 1.3200e-01 2.1100e-01 - 1.3300e+00 1.5800e-01 2.0200e-01 - 1.3300e+00 1.6300e-01 1.7900e-01 - 1.3300e+00 9.0000e-02 -2.0600e-01 - 1.3300e+00 7.9000e-02 -2.2900e-01 - 1.3300e+00 5.8000e-02 -2.3800e-01 - 1.3300e+00 -6.2000e-02 -2.3800e-01 - 1.3300e+00 -8.3000e-02 -2.2900e-01 - 1.3300e+00 -9.4000e-02 -2.0600e-01 - 1.3300e+00 -1.6700e-01 1.7900e-01 - 1.3300e+00 -1.6000e-01 2.0200e-01 - 1.3300e+00 -1.3500e-01 2.1100e-01 - 1.3300e+00 -5.0000e-02 2.1100e-01 - 1.3065e+00 -2.5370e-02 2.4300e-01 - 1.2830e+00 -1.6000e-02 2.6500e-01 - 1.3300e+00 -1.6000e-02 3.5600e-01 - -8.7000e-02 1.2000e+00 -3.5600e-01 - -8.7000e-02 1.2000e+00 -2.6500e-01 - -7.1000e-02 1.2000e+00 -2.4300e-01 - -5.5000e-02 1.2000e+00 -2.1100e-01 - 1.6900e-01 1.2000e+00 -2.1100e-01 - 1.9100e-01 1.1880e+00 -2.0200e-01 - 2.0100e-01 1.1620e+00 -1.7900e-01 - 2.0100e-01 7.9400e-01 2.0600e-01 - 1.9100e-01 7.7500e-01 2.2900e-01 - 1.6900e-01 7.6800e-01 2.3800e-01 - -1.6800e-01 7.6800e-01 2.3800e-01 - -1.9100e-01 7.7500e-01 2.2900e-01 - -2.0000e-01 7.9500e-01 2.0600e-01 - -2.0000e-01 1.1620e+00 -1.7900e-01 - -1.9100e-01 1.1880e+00 -2.0200e-01 - -1.6800e-01 1.2000e+00 -2.1100e-01 - -1.5100e-01 1.2000e+00 -2.1100e-01 - -1.3500e-01 1.2000e+00 -2.4300e-01 - -1.1900e-01 1.2000e+00 -2.6500e-01 - -1.1900e-01 1.2000e+00 -3.5600e-01 - -1.3300e+00 1.6000e-02 3.5600e-01 - -1.2830e+00 1.6000e-02 2.6500e-01 - -1.3065e+00 2.5370e-02 2.4300e-01 - -1.3300e+00 4.8000e-02 2.1100e-01 - -1.3300e+00 6.1000e-02 2.1100e-01 - -1.3300e+00 8.2000e-02 2.0200e-01 - -1.3300e+00 9.3000e-02 1.7900e-01 - -1.3300e+00 1.7100e-01 -2.0600e-01 - -1.3300e+00 1.6500e-01 -2.2900e-01 - -1.3300e+00 1.4100e-01 -2.3800e-01 - -1.3300e+00 -1.4100e-01 -2.3800e-01 - -1.3300e+00 -1.6600e-01 -2.2900e-01 - -1.3300e+00 -1.7200e-01 -2.0600e-01 - -1.3300e+00 -9.3000e-02 1.7900e-01 - -1.3300e+00 -8.2000e-02 2.0200e-01 - -1.3300e+00 -6.2000e-02 2.1100e-01 - -1.3300e+00 -4.9000e-02 2.1100e-01 - -1.3065e+00 -2.5370e-02 2.4300e-01 - -1.2830e+00 -1.6000e-02 2.6500e-01 - -1.3300e+00 -1.6000e-02 3.5600e-01 - 8.7000e-02 -7.7650e-01 -3.5600e-01 - 8.7000e-02 -7.7650e-01 -2.6500e-01 - 7.1000e-02 -7.7650e-01 -2.4300e-01 - 5.5000e-02 -7.7650e-01 -2.1100e-01 - 5.5000e-02 -7.7650e-01 -2.1100e-01 - 3.2600e-02 -7.8350e-01 -2.0200e-01 - 2.2600e-02 -8.0250e-01 -1.7900e-01 - 2.2600e-02 -1.2015e+00 2.0600e-01 - 3.2600e-02 -1.2275e+00 2.2900e-01 - 5.4600e-02 -1.2395e+00 2.3800e-01 - 1.6800e-01 -1.2395e+00 2.3800e-01 - 1.9100e-01 -1.2275e+00 2.2900e-01 - 2.0000e-01 -1.2015e+00 2.0600e-01 - 2.0000e-01 -8.0250e-01 -1.7900e-01 - 1.9100e-01 -7.8350e-01 -2.0200e-01 - 1.6800e-01 -7.7650e-01 -2.1100e-01 - 1.5100e-01 -7.7650e-01 -2.1100e-01 - 1.3500e-01 -7.7650e-01 -2.4300e-01 - 1.1900e-01 -7.7650e-01 -2.6500e-01 - 1.1900e-01 -7.7650e-01 -3.5600e-01 + 4 1 20 1.00 + 1.3300e+00 1.6000e-02 3.5600e-01 + 1.2830e+00 1.6000e-02 2.6500e-01 + 1.3065e+00 2.5370e-02 2.4300e-01 + 1.3300e+00 4.8000e-02 2.1100e-01 + 1.3300e+00 1.3200e-01 2.1100e-01 + 1.3300e+00 1.5800e-01 2.0200e-01 + 1.3300e+00 1.6300e-01 1.7900e-01 + 1.3300e+00 9.0000e-02 -2.0600e-01 + 1.3300e+00 7.9000e-02 -2.2900e-01 + 1.3300e+00 5.8000e-02 -2.3800e-01 + 1.3300e+00 -6.2000e-02 -2.3800e-01 + 1.3300e+00 -8.3000e-02 -2.2900e-01 + 1.3300e+00 -9.4000e-02 -2.0600e-01 + 1.3300e+00 -1.6700e-01 1.7900e-01 + 1.3300e+00 -1.6000e-01 2.0200e-01 + 1.3300e+00 -1.3500e-01 2.1100e-01 + 1.3300e+00 -5.0000e-02 2.1100e-01 + 1.3065e+00 -2.5370e-02 2.4300e-01 + 1.2830e+00 -1.6000e-02 2.6500e-01 + 1.3300e+00 -1.6000e-02 3.5600e-01 + -8.7000e-02 1.2000e+00 -3.5600e-01 + -8.7000e-02 1.2000e+00 -2.6500e-01 + -7.1000e-02 1.2000e+00 -2.4300e-01 + -5.5000e-02 1.2000e+00 -2.1100e-01 + 1.6900e-01 1.2000e+00 -2.1100e-01 + 1.9100e-01 1.1880e+00 -2.0200e-01 + 2.0100e-01 1.1620e+00 -1.7900e-01 + 2.0100e-01 7.9400e-01 2.0600e-01 + 1.9100e-01 7.7500e-01 2.2900e-01 + 1.6900e-01 7.6800e-01 2.3800e-01 + -1.6800e-01 7.6800e-01 2.3800e-01 + -1.9100e-01 7.7500e-01 2.2900e-01 + -2.0000e-01 7.9500e-01 2.0600e-01 + -2.0000e-01 1.1620e+00 -1.7900e-01 + -1.9100e-01 1.1880e+00 -2.0200e-01 + -1.6800e-01 1.2000e+00 -2.1100e-01 + -1.5100e-01 1.2000e+00 -2.1100e-01 + -1.3500e-01 1.2000e+00 -2.4300e-01 + -1.1900e-01 1.2000e+00 -2.6500e-01 + -1.1900e-01 1.2000e+00 -3.5600e-01 + -1.3300e+00 1.6000e-02 3.5600e-01 + -1.2830e+00 1.6000e-02 2.6500e-01 + -1.3065e+00 2.5370e-02 2.4300e-01 + -1.3300e+00 4.8000e-02 2.1100e-01 + -1.3300e+00 6.1000e-02 2.1100e-01 + -1.3300e+00 8.2000e-02 2.0200e-01 + -1.3300e+00 9.3000e-02 1.7900e-01 + -1.3300e+00 1.7100e-01 -2.0600e-01 + -1.3300e+00 1.6500e-01 -2.2900e-01 + -1.3300e+00 1.4100e-01 -2.3800e-01 + -1.3300e+00 -1.4100e-01 -2.3800e-01 + -1.3300e+00 -1.6600e-01 -2.2900e-01 + -1.3300e+00 -1.7200e-01 -2.0600e-01 + -1.3300e+00 -9.3000e-02 1.7900e-01 + -1.3300e+00 -8.2000e-02 2.0200e-01 + -1.3300e+00 -6.2000e-02 2.1100e-01 + -1.3300e+00 -4.9000e-02 2.1100e-01 + -1.3065e+00 -2.5370e-02 2.4300e-01 + -1.2830e+00 -1.6000e-02 2.6500e-01 + -1.3300e+00 -1.6000e-02 3.5600e-01 + 8.7000e-02 -7.7650e-01 -3.5600e-01 + 8.7000e-02 -7.7650e-01 -2.6500e-01 + 7.1000e-02 -7.7650e-01 -2.4300e-01 + 5.5000e-02 -7.7650e-01 -2.1100e-01 + 5.5000e-02 -7.7650e-01 -2.1100e-01 + 3.2600e-02 -7.8350e-01 -2.0200e-01 + 2.2600e-02 -8.0250e-01 -1.7900e-01 + 2.2600e-02 -1.2015e+00 2.0600e-01 + 3.2600e-02 -1.2275e+00 2.2900e-01 + 5.4600e-02 -1.2395e+00 2.3800e-01 + 1.6800e-01 -1.2395e+00 2.3800e-01 + 1.9100e-01 -1.2275e+00 2.2900e-01 + 2.0000e-01 -1.2015e+00 2.0600e-01 + 2.0000e-01 -8.0250e-01 -1.7900e-01 + 1.9100e-01 -7.8350e-01 -2.0200e-01 + 1.6800e-01 -7.7650e-01 -2.1100e-01 + 1.5100e-01 -7.7650e-01 -2.1100e-01 + 1.3500e-01 -7.7650e-01 -2.4300e-01 + 1.1900e-01 -7.7650e-01 -2.6500e-01 + 1.1900e-01 -7.7650e-01 -3.5600e-01 diff --git a/src/ForcingTerms/coil_geometries/jtext_u.dat b/src/ForcingTerms/coil_geometries/jtext_u.dat index 45404b00f..b70ef435f 100644 --- a/src/ForcingTerms/coil_geometries/jtext_u.dat +++ b/src/ForcingTerms/coil_geometries/jtext_u.dat @@ -1,81 +1,81 @@ - 4 1 20 1.00 - 1.2000e+00 1.6000e-02 3.9800e-01 - 1.2000e+00 1.6000e-02 3.3300e-01 - 1.2000e+00 2.5370e-02 3.1000e-01 - 1.2000e+00 4.8000e-02 3.0100e-01 - 1.2000e+00 1.3200e-01 3.0100e-01 - 1.1880e+00 1.5800e-01 3.0100e-01 - 1.1620e+00 1.6300e-01 3.0100e-01 - 7.9400e-01 9.0000e-02 3.0100e-01 - 7.7500e-01 7.9000e-02 3.0100e-01 - 7.6800e-01 5.8000e-02 3.0100e-01 - 7.6800e-01 -6.2000e-02 3.0100e-01 - 7.7500e-01 -8.3000e-02 3.0100e-01 - 7.9500e-01 -9.4000e-02 3.0100e-01 - 1.1620e+00 -1.6700e-01 3.0100e-01 - 1.1880e+00 -1.6000e-01 3.0100e-01 - 1.2000e+00 -1.3500e-01 3.0100e-01 - 1.2000e+00 -5.0000e-02 3.0100e-01 - 1.2000e+00 -2.5370e-02 3.1000e-01 - 1.2000e+00 -1.6000e-02 3.3300e-01 - 1.2000e+00 -1.6000e-02 3.9800e-01 - 1.6000e-02 1.2000e+00 3.9800e-01 - 1.6000e-02 1.2000e+00 3.3300e-01 - 2.5370e-02 1.2000e+00 3.1000e-01 - 4.8000e-02 1.2000e+00 3.0100e-01 - 1.3200e-01 1.2000e+00 3.0100e-01 - 1.5800e-01 1.1880e+00 3.0100e-01 - 1.6300e-01 1.1620e+00 3.0100e-01 - 9.0000e-02 7.9400e-01 3.0100e-01 - 7.9000e-02 7.7500e-01 3.0100e-01 - 5.8000e-02 7.6800e-01 3.0100e-01 - -6.2000e-02 7.6800e-01 3.0100e-01 - -8.3000e-02 7.7500e-01 3.0100e-01 - -9.4000e-02 7.9500e-01 3.0100e-01 - -1.6700e-01 1.1620e+00 3.0100e-01 - -1.6000e-01 1.1880e+00 3.0100e-01 - -1.3500e-01 1.2000e+00 3.0100e-01 - -5.0000e-02 1.2000e+00 3.0100e-01 - -2.5370e-02 1.2000e+00 3.1000e-01 - -1.6000e-02 1.2000e+00 3.3300e-01 - -1.6000e-02 1.2000e+00 3.9800e-01 - -7.7650e-01 1.6000e-02 3.9800e-01 - -7.7650e-01 1.6000e-02 3.3300e-01 - -7.7650e-01 2.5370e-02 3.1000e-01 - -7.7650e-01 4.8000e-02 3.0100e-01 - -7.7650e-01 6.1000e-02 3.0100e-01 - -7.8350e-01 8.2000e-02 3.0100e-01 - -8.0250e-01 9.3000e-02 3.0100e-01 - -1.2015e+00 1.7100e-01 3.0100e-01 - -1.2275e+00 1.6500e-01 3.0100e-01 - -1.2395e+00 1.4100e-01 3.0100e-01 - -1.2395e+00 -1.4100e-01 3.0100e-01 - -1.2275e+00 -1.6600e-01 3.0100e-01 - -1.2015e+00 -1.7200e-01 3.0100e-01 - -8.0250e-01 -9.3000e-02 3.0100e-01 - -7.8350e-01 -8.2000e-02 3.0100e-01 - -7.7650e-01 -6.2000e-02 3.0100e-01 - -7.7650e-01 -4.9000e-02 3.0100e-01 - -7.7650e-01 -2.5370e-02 3.1000e-01 - -7.7650e-01 -1.6000e-02 3.3300e-01 - -7.7650e-01 -1.6000e-02 3.9800e-01 - 1.6000e-02 -7.7650e-01 3.9800e-01 - 1.6000e-02 -7.7650e-01 3.3300e-01 - 2.5370e-02 -7.7650e-01 3.1000e-01 - 4.8000e-02 -7.7650e-01 3.0100e-01 - 6.1000e-02 -7.7650e-01 3.0100e-01 - 8.2000e-02 -7.8350e-01 3.0100e-01 - 9.3000e-02 -8.0250e-01 3.0100e-01 - 1.7100e-01 -1.2015e+00 3.0100e-01 - 1.6500e-01 -1.2275e+00 3.0100e-01 - 1.4100e-01 -1.2395e+00 3.0100e-01 - -1.4100e-01 -1.2395e+00 3.0100e-01 - -1.6600e-01 -1.2275e+00 3.0100e-01 - -1.7200e-01 -1.2015e+00 3.0100e-01 - -9.3000e-02 -8.0250e-01 3.0100e-01 - -8.2000e-02 -7.8350e-01 3.0100e-01 - -6.2000e-02 -7.7650e-01 3.0100e-01 - -4.9000e-02 -7.7650e-01 3.0100e-01 - -2.5370e-02 -7.7650e-01 3.1000e-01 - -1.6000e-02 -7.7650e-01 3.3300e-01 - -1.6000e-02 -7.7650e-01 3.9800e-01 + 4 1 20 1.00 + 1.2000e+00 1.6000e-02 3.9800e-01 + 1.2000e+00 1.6000e-02 3.3300e-01 + 1.2000e+00 2.5370e-02 3.1000e-01 + 1.2000e+00 4.8000e-02 3.0100e-01 + 1.2000e+00 1.3200e-01 3.0100e-01 + 1.1880e+00 1.5800e-01 3.0100e-01 + 1.1620e+00 1.6300e-01 3.0100e-01 + 7.9400e-01 9.0000e-02 3.0100e-01 + 7.7500e-01 7.9000e-02 3.0100e-01 + 7.6800e-01 5.8000e-02 3.0100e-01 + 7.6800e-01 -6.2000e-02 3.0100e-01 + 7.7500e-01 -8.3000e-02 3.0100e-01 + 7.9500e-01 -9.4000e-02 3.0100e-01 + 1.1620e+00 -1.6700e-01 3.0100e-01 + 1.1880e+00 -1.6000e-01 3.0100e-01 + 1.2000e+00 -1.3500e-01 3.0100e-01 + 1.2000e+00 -5.0000e-02 3.0100e-01 + 1.2000e+00 -2.5370e-02 3.1000e-01 + 1.2000e+00 -1.6000e-02 3.3300e-01 + 1.2000e+00 -1.6000e-02 3.9800e-01 + 1.6000e-02 1.2000e+00 3.9800e-01 + 1.6000e-02 1.2000e+00 3.3300e-01 + 2.5370e-02 1.2000e+00 3.1000e-01 + 4.8000e-02 1.2000e+00 3.0100e-01 + 1.3200e-01 1.2000e+00 3.0100e-01 + 1.5800e-01 1.1880e+00 3.0100e-01 + 1.6300e-01 1.1620e+00 3.0100e-01 + 9.0000e-02 7.9400e-01 3.0100e-01 + 7.9000e-02 7.7500e-01 3.0100e-01 + 5.8000e-02 7.6800e-01 3.0100e-01 + -6.2000e-02 7.6800e-01 3.0100e-01 + -8.3000e-02 7.7500e-01 3.0100e-01 + -9.4000e-02 7.9500e-01 3.0100e-01 + -1.6700e-01 1.1620e+00 3.0100e-01 + -1.6000e-01 1.1880e+00 3.0100e-01 + -1.3500e-01 1.2000e+00 3.0100e-01 + -5.0000e-02 1.2000e+00 3.0100e-01 + -2.5370e-02 1.2000e+00 3.1000e-01 + -1.6000e-02 1.2000e+00 3.3300e-01 + -1.6000e-02 1.2000e+00 3.9800e-01 + -7.7650e-01 1.6000e-02 3.9800e-01 + -7.7650e-01 1.6000e-02 3.3300e-01 + -7.7650e-01 2.5370e-02 3.1000e-01 + -7.7650e-01 4.8000e-02 3.0100e-01 + -7.7650e-01 6.1000e-02 3.0100e-01 + -7.8350e-01 8.2000e-02 3.0100e-01 + -8.0250e-01 9.3000e-02 3.0100e-01 + -1.2015e+00 1.7100e-01 3.0100e-01 + -1.2275e+00 1.6500e-01 3.0100e-01 + -1.2395e+00 1.4100e-01 3.0100e-01 + -1.2395e+00 -1.4100e-01 3.0100e-01 + -1.2275e+00 -1.6600e-01 3.0100e-01 + -1.2015e+00 -1.7200e-01 3.0100e-01 + -8.0250e-01 -9.3000e-02 3.0100e-01 + -7.8350e-01 -8.2000e-02 3.0100e-01 + -7.7650e-01 -6.2000e-02 3.0100e-01 + -7.7650e-01 -4.9000e-02 3.0100e-01 + -7.7650e-01 -2.5370e-02 3.1000e-01 + -7.7650e-01 -1.6000e-02 3.3300e-01 + -7.7650e-01 -1.6000e-02 3.9800e-01 + 1.6000e-02 -7.7650e-01 3.9800e-01 + 1.6000e-02 -7.7650e-01 3.3300e-01 + 2.5370e-02 -7.7650e-01 3.1000e-01 + 4.8000e-02 -7.7650e-01 3.0100e-01 + 6.1000e-02 -7.7650e-01 3.0100e-01 + 8.2000e-02 -7.8350e-01 3.0100e-01 + 9.3000e-02 -8.0250e-01 3.0100e-01 + 1.7100e-01 -1.2015e+00 3.0100e-01 + 1.6500e-01 -1.2275e+00 3.0100e-01 + 1.4100e-01 -1.2395e+00 3.0100e-01 + -1.4100e-01 -1.2395e+00 3.0100e-01 + -1.6600e-01 -1.2275e+00 3.0100e-01 + -1.7200e-01 -1.2015e+00 3.0100e-01 + -9.3000e-02 -8.0250e-01 3.0100e-01 + -8.2000e-02 -7.8350e-01 3.0100e-01 + -6.2000e-02 -7.7650e-01 3.0100e-01 + -4.9000e-02 -7.7650e-01 3.0100e-01 + -2.5370e-02 -7.7650e-01 3.1000e-01 + -1.6000e-02 -7.7650e-01 3.3300e-01 + -1.6000e-02 -7.7650e-01 3.9800e-01 diff --git a/src/ForcingTerms/coil_geometries/mastu_efcc.dat b/src/ForcingTerms/coil_geometries/mastu_efcc.dat index e94da889f..5259e11cc 100644 --- a/src/ForcingTerms/coil_geometries/mastu_efcc.dat +++ b/src/ForcingTerms/coil_geometries/mastu_efcc.dat @@ -1,169 +1,169 @@ - 4 1 42 1.00 - 2.9319e+00 0.0000e+00 1.6550e+00 - 2.9213e+00 2.4953e-01 1.6550e+00 - 2.8894e+00 4.9726e-01 1.6550e+00 - 2.8366e+00 7.4137e-01 1.6550e+00 - 2.7632e+00 9.8011e-01 1.6550e+00 - 2.6698e+00 1.2117e+00 1.6550e+00 - 2.5570e+00 1.4346e+00 1.6550e+00 - 2.4256e+00 1.6470e+00 1.6550e+00 - 2.2766e+00 1.8474e+00 1.6550e+00 - 2.1111e+00 2.0345e+00 1.6550e+00 - 1.9303e+00 2.2068e+00 1.6550e+00 - 1.7355e+00 2.3631e+00 1.6550e+00 - 1.5281e+00 2.5022e+00 1.6550e+00 - 1.3096e+00 2.6232e+00 1.6550e+00 - 1.0815e+00 2.7251e+00 1.6550e+00 - 8.4568e-01 2.8073e+00 1.6550e+00 - 6.0369e-01 2.8691e+00 1.6550e+00 - 3.5731e-01 2.9100e+00 1.6550e+00 - 3.5731e-01 2.9100e+00 1.6550e+00 - 3.5729e-01 2.9099e+00 9.1800e-01 - 3.5727e-01 2.9097e+00 1.8100e-01 - 3.5725e-01 2.9096e+00 -5.5600e-01 - 3.5723e-01 2.9094e+00 -1.2930e+00 - 3.5721e-01 2.9093e+00 -2.0300e+00 - 6.7851e-01 2.8515e+00 -2.0300e+00 - 9.9139e-01 2.7583e+00 -2.0300e+00 - 1.2920e+00 2.6310e+00 -2.0300e+00 - 1.5765e+00 2.4710e+00 -2.0300e+00 - 1.8415e+00 2.2804e+00 -2.0300e+00 - 2.0837e+00 2.0614e+00 -2.0300e+00 - 2.3000e+00 1.8169e+00 -2.0300e+00 - 2.4878e+00 1.5499e+00 -2.0300e+00 - 2.6447e+00 1.2636e+00 -2.0300e+00 - 2.7688e+00 9.6171e-01 -2.0300e+00 - 2.8586e+00 6.4785e-01 -2.0300e+00 - 2.9129e+00 3.2594e-01 -2.0300e+00 - 2.9311e+00 0.0000e+00 -2.0300e+00 - 2.9313e+00 0.0000e+00 -1.2930e+00 - 2.9314e+00 0.0000e+00 -5.5600e-01 - 2.9316e+00 0.0000e+00 1.8100e-01 - 2.9317e+00 0.0000e+00 9.1800e-01 - 2.9319e+00 0.0000e+00 1.6550e+00 - 1.7953e-16 2.9319e+00 1.6550e+00 - -2.4953e-01 2.9213e+00 1.6550e+00 - -4.9726e-01 2.8894e+00 1.6550e+00 - -7.4137e-01 2.8366e+00 1.6550e+00 - -9.8011e-01 2.7632e+00 1.6550e+00 - -1.2117e+00 2.6698e+00 1.6550e+00 - -1.4346e+00 2.5570e+00 1.6550e+00 - -1.6470e+00 2.4256e+00 1.6550e+00 - -1.8474e+00 2.2766e+00 1.6550e+00 - -2.0345e+00 2.1111e+00 1.6550e+00 - -2.2068e+00 1.9303e+00 1.6550e+00 - -2.3631e+00 1.7355e+00 1.6550e+00 - -2.5022e+00 1.5281e+00 1.6550e+00 - -2.6232e+00 1.3096e+00 1.6550e+00 - -2.7251e+00 1.0815e+00 1.6550e+00 - -2.8073e+00 8.4568e-01 1.6550e+00 - -2.8691e+00 6.0369e-01 1.6550e+00 - -2.9100e+00 3.5731e-01 1.6550e+00 - -2.9100e+00 3.5731e-01 1.6550e+00 - -2.9099e+00 3.5729e-01 9.1800e-01 - -2.9097e+00 3.5727e-01 1.8100e-01 - -2.9096e+00 3.5725e-01 -5.5600e-01 - -2.9094e+00 3.5723e-01 -1.2930e+00 - -2.9093e+00 3.5721e-01 -2.0300e+00 - -2.8515e+00 6.7851e-01 -2.0300e+00 - -2.7583e+00 9.9139e-01 -2.0300e+00 - -2.6310e+00 1.2920e+00 -2.0300e+00 - -2.4710e+00 1.5765e+00 -2.0300e+00 - -2.2804e+00 1.8415e+00 -2.0300e+00 - -2.0614e+00 2.0837e+00 -2.0300e+00 - -1.8169e+00 2.3000e+00 -2.0300e+00 - -1.5499e+00 2.4878e+00 -2.0300e+00 - -1.2636e+00 2.6447e+00 -2.0300e+00 - -9.6171e-01 2.7688e+00 -2.0300e+00 - -6.4785e-01 2.8586e+00 -2.0300e+00 - -3.2594e-01 2.9129e+00 -2.0300e+00 - 1.7948e-16 2.9311e+00 -2.0300e+00 - 1.7949e-16 2.9313e+00 -1.2930e+00 - 1.7950e-16 2.9314e+00 -5.5600e-01 - 1.7951e-16 2.9316e+00 1.8100e-01 - 1.7952e-16 2.9317e+00 9.1800e-01 - 1.7953e-16 2.9319e+00 1.6550e+00 - -2.9319e+00 3.5905e-16 1.6550e+00 - -2.9213e+00 -2.4953e-01 1.6550e+00 - -2.8894e+00 -4.9726e-01 1.6550e+00 - -2.8366e+00 -7.4137e-01 1.6550e+00 - -2.7632e+00 -9.8011e-01 1.6550e+00 - -2.6698e+00 -1.2117e+00 1.6550e+00 - -2.5570e+00 -1.4346e+00 1.6550e+00 - -2.4256e+00 -1.6470e+00 1.6550e+00 - -2.2766e+00 -1.8474e+00 1.6550e+00 - -2.1111e+00 -2.0345e+00 1.6550e+00 - -1.9303e+00 -2.2068e+00 1.6550e+00 - -1.7355e+00 -2.3631e+00 1.6550e+00 - -1.5281e+00 -2.5022e+00 1.6550e+00 - -1.3096e+00 -2.6232e+00 1.6550e+00 - -1.0815e+00 -2.7251e+00 1.6550e+00 - -8.4568e-01 -2.8073e+00 1.6550e+00 - -6.0369e-01 -2.8691e+00 1.6550e+00 - -3.5731e-01 -2.9100e+00 1.6550e+00 - -3.5731e-01 -2.9100e+00 1.6550e+00 - -3.5729e-01 -2.9099e+00 9.1800e-01 - -3.5727e-01 -2.9097e+00 1.8100e-01 - -3.5725e-01 -2.9096e+00 -5.5600e-01 - -3.5723e-01 -2.9094e+00 -1.2930e+00 - -3.5721e-01 -2.9093e+00 -2.0300e+00 - -6.7851e-01 -2.8515e+00 -2.0300e+00 - -9.9139e-01 -2.7583e+00 -2.0300e+00 - -1.2920e+00 -2.6310e+00 -2.0300e+00 - -1.5765e+00 -2.4710e+00 -2.0300e+00 - -1.8415e+00 -2.2804e+00 -2.0300e+00 - -2.0837e+00 -2.0614e+00 -2.0300e+00 - -2.3000e+00 -1.8169e+00 -2.0300e+00 - -2.4878e+00 -1.5499e+00 -2.0300e+00 - -2.6447e+00 -1.2636e+00 -2.0300e+00 - -2.7688e+00 -9.6171e-01 -2.0300e+00 - -2.8586e+00 -6.4785e-01 -2.0300e+00 - -2.9129e+00 -3.2594e-01 -2.0300e+00 - -2.9311e+00 3.5896e-16 -2.0300e+00 - -2.9313e+00 3.5898e-16 -1.2930e+00 - -2.9314e+00 3.5900e-16 -5.5600e-01 - -2.9316e+00 3.5902e-16 1.8100e-01 - -2.9317e+00 3.5903e-16 9.1800e-01 - -2.9319e+00 3.5905e-16 1.6550e+00 - -5.3858e-16 -2.9319e+00 1.6550e+00 - 2.4953e-01 -2.9213e+00 1.6550e+00 - 4.9726e-01 -2.8894e+00 1.6550e+00 - 7.4137e-01 -2.8366e+00 1.6550e+00 - 9.8011e-01 -2.7632e+00 1.6550e+00 - 1.2117e+00 -2.6698e+00 1.6550e+00 - 1.4346e+00 -2.5570e+00 1.6550e+00 - 1.6470e+00 -2.4256e+00 1.6550e+00 - 1.8474e+00 -2.2766e+00 1.6550e+00 - 2.0345e+00 -2.1111e+00 1.6550e+00 - 2.2068e+00 -1.9303e+00 1.6550e+00 - 2.3631e+00 -1.7355e+00 1.6550e+00 - 2.5022e+00 -1.5281e+00 1.6550e+00 - 2.6232e+00 -1.3096e+00 1.6550e+00 - 2.7251e+00 -1.0815e+00 1.6550e+00 - 2.8073e+00 -8.4568e-01 1.6550e+00 - 2.8691e+00 -6.0369e-01 1.6550e+00 - 2.9100e+00 -3.5731e-01 1.6550e+00 - 2.9100e+00 -3.5731e-01 1.6550e+00 - 2.9099e+00 -3.5729e-01 9.1800e-01 - 2.9097e+00 -3.5727e-01 1.8100e-01 - 2.9096e+00 -3.5725e-01 -5.5600e-01 - 2.9094e+00 -3.5723e-01 -1.2930e+00 - 2.9093e+00 -3.5721e-01 -2.0300e+00 - 2.8515e+00 -6.7851e-01 -2.0300e+00 - 2.7583e+00 -9.9139e-01 -2.0300e+00 - 2.6310e+00 -1.2920e+00 -2.0300e+00 - 2.4710e+00 -1.5765e+00 -2.0300e+00 - 2.2804e+00 -1.8415e+00 -2.0300e+00 - 2.0614e+00 -2.0837e+00 -2.0300e+00 - 1.8169e+00 -2.3000e+00 -2.0300e+00 - 1.5499e+00 -2.4878e+00 -2.0300e+00 - 1.2636e+00 -2.6447e+00 -2.0300e+00 - 9.6171e-01 -2.7688e+00 -2.0300e+00 - 6.4785e-01 -2.8586e+00 -2.0300e+00 - 3.2594e-01 -2.9129e+00 -2.0300e+00 - -5.3843e-16 -2.9311e+00 -2.0300e+00 - -5.3846e-16 -2.9313e+00 -1.2930e+00 - -5.3849e-16 -2.9314e+00 -5.5600e-01 - -5.3852e-16 -2.9316e+00 1.8100e-01 - -5.3855e-16 -2.9317e+00 9.1800e-01 - -5.3858e-16 -2.9319e+00 1.6550e+00 + 4 1 42 1.00 + 2.9319e+00 0.0000e+00 1.6550e+00 + 2.9213e+00 2.4953e-01 1.6550e+00 + 2.8894e+00 4.9726e-01 1.6550e+00 + 2.8366e+00 7.4137e-01 1.6550e+00 + 2.7632e+00 9.8011e-01 1.6550e+00 + 2.6698e+00 1.2117e+00 1.6550e+00 + 2.5570e+00 1.4346e+00 1.6550e+00 + 2.4256e+00 1.6470e+00 1.6550e+00 + 2.2766e+00 1.8474e+00 1.6550e+00 + 2.1111e+00 2.0345e+00 1.6550e+00 + 1.9303e+00 2.2068e+00 1.6550e+00 + 1.7355e+00 2.3631e+00 1.6550e+00 + 1.5281e+00 2.5022e+00 1.6550e+00 + 1.3096e+00 2.6232e+00 1.6550e+00 + 1.0815e+00 2.7251e+00 1.6550e+00 + 8.4568e-01 2.8073e+00 1.6550e+00 + 6.0369e-01 2.8691e+00 1.6550e+00 + 3.5731e-01 2.9100e+00 1.6550e+00 + 3.5731e-01 2.9100e+00 1.6550e+00 + 3.5729e-01 2.9099e+00 9.1800e-01 + 3.5727e-01 2.9097e+00 1.8100e-01 + 3.5725e-01 2.9096e+00 -5.5600e-01 + 3.5723e-01 2.9094e+00 -1.2930e+00 + 3.5721e-01 2.9093e+00 -2.0300e+00 + 6.7851e-01 2.8515e+00 -2.0300e+00 + 9.9139e-01 2.7583e+00 -2.0300e+00 + 1.2920e+00 2.6310e+00 -2.0300e+00 + 1.5765e+00 2.4710e+00 -2.0300e+00 + 1.8415e+00 2.2804e+00 -2.0300e+00 + 2.0837e+00 2.0614e+00 -2.0300e+00 + 2.3000e+00 1.8169e+00 -2.0300e+00 + 2.4878e+00 1.5499e+00 -2.0300e+00 + 2.6447e+00 1.2636e+00 -2.0300e+00 + 2.7688e+00 9.6171e-01 -2.0300e+00 + 2.8586e+00 6.4785e-01 -2.0300e+00 + 2.9129e+00 3.2594e-01 -2.0300e+00 + 2.9311e+00 0.0000e+00 -2.0300e+00 + 2.9313e+00 0.0000e+00 -1.2930e+00 + 2.9314e+00 0.0000e+00 -5.5600e-01 + 2.9316e+00 0.0000e+00 1.8100e-01 + 2.9317e+00 0.0000e+00 9.1800e-01 + 2.9319e+00 0.0000e+00 1.6550e+00 + 1.7953e-16 2.9319e+00 1.6550e+00 + -2.4953e-01 2.9213e+00 1.6550e+00 + -4.9726e-01 2.8894e+00 1.6550e+00 + -7.4137e-01 2.8366e+00 1.6550e+00 + -9.8011e-01 2.7632e+00 1.6550e+00 + -1.2117e+00 2.6698e+00 1.6550e+00 + -1.4346e+00 2.5570e+00 1.6550e+00 + -1.6470e+00 2.4256e+00 1.6550e+00 + -1.8474e+00 2.2766e+00 1.6550e+00 + -2.0345e+00 2.1111e+00 1.6550e+00 + -2.2068e+00 1.9303e+00 1.6550e+00 + -2.3631e+00 1.7355e+00 1.6550e+00 + -2.5022e+00 1.5281e+00 1.6550e+00 + -2.6232e+00 1.3096e+00 1.6550e+00 + -2.7251e+00 1.0815e+00 1.6550e+00 + -2.8073e+00 8.4568e-01 1.6550e+00 + -2.8691e+00 6.0369e-01 1.6550e+00 + -2.9100e+00 3.5731e-01 1.6550e+00 + -2.9100e+00 3.5731e-01 1.6550e+00 + -2.9099e+00 3.5729e-01 9.1800e-01 + -2.9097e+00 3.5727e-01 1.8100e-01 + -2.9096e+00 3.5725e-01 -5.5600e-01 + -2.9094e+00 3.5723e-01 -1.2930e+00 + -2.9093e+00 3.5721e-01 -2.0300e+00 + -2.8515e+00 6.7851e-01 -2.0300e+00 + -2.7583e+00 9.9139e-01 -2.0300e+00 + -2.6310e+00 1.2920e+00 -2.0300e+00 + -2.4710e+00 1.5765e+00 -2.0300e+00 + -2.2804e+00 1.8415e+00 -2.0300e+00 + -2.0614e+00 2.0837e+00 -2.0300e+00 + -1.8169e+00 2.3000e+00 -2.0300e+00 + -1.5499e+00 2.4878e+00 -2.0300e+00 + -1.2636e+00 2.6447e+00 -2.0300e+00 + -9.6171e-01 2.7688e+00 -2.0300e+00 + -6.4785e-01 2.8586e+00 -2.0300e+00 + -3.2594e-01 2.9129e+00 -2.0300e+00 + 1.7948e-16 2.9311e+00 -2.0300e+00 + 1.7949e-16 2.9313e+00 -1.2930e+00 + 1.7950e-16 2.9314e+00 -5.5600e-01 + 1.7951e-16 2.9316e+00 1.8100e-01 + 1.7952e-16 2.9317e+00 9.1800e-01 + 1.7953e-16 2.9319e+00 1.6550e+00 + -2.9319e+00 3.5905e-16 1.6550e+00 + -2.9213e+00 -2.4953e-01 1.6550e+00 + -2.8894e+00 -4.9726e-01 1.6550e+00 + -2.8366e+00 -7.4137e-01 1.6550e+00 + -2.7632e+00 -9.8011e-01 1.6550e+00 + -2.6698e+00 -1.2117e+00 1.6550e+00 + -2.5570e+00 -1.4346e+00 1.6550e+00 + -2.4256e+00 -1.6470e+00 1.6550e+00 + -2.2766e+00 -1.8474e+00 1.6550e+00 + -2.1111e+00 -2.0345e+00 1.6550e+00 + -1.9303e+00 -2.2068e+00 1.6550e+00 + -1.7355e+00 -2.3631e+00 1.6550e+00 + -1.5281e+00 -2.5022e+00 1.6550e+00 + -1.3096e+00 -2.6232e+00 1.6550e+00 + -1.0815e+00 -2.7251e+00 1.6550e+00 + -8.4568e-01 -2.8073e+00 1.6550e+00 + -6.0369e-01 -2.8691e+00 1.6550e+00 + -3.5731e-01 -2.9100e+00 1.6550e+00 + -3.5731e-01 -2.9100e+00 1.6550e+00 + -3.5729e-01 -2.9099e+00 9.1800e-01 + -3.5727e-01 -2.9097e+00 1.8100e-01 + -3.5725e-01 -2.9096e+00 -5.5600e-01 + -3.5723e-01 -2.9094e+00 -1.2930e+00 + -3.5721e-01 -2.9093e+00 -2.0300e+00 + -6.7851e-01 -2.8515e+00 -2.0300e+00 + -9.9139e-01 -2.7583e+00 -2.0300e+00 + -1.2920e+00 -2.6310e+00 -2.0300e+00 + -1.5765e+00 -2.4710e+00 -2.0300e+00 + -1.8415e+00 -2.2804e+00 -2.0300e+00 + -2.0837e+00 -2.0614e+00 -2.0300e+00 + -2.3000e+00 -1.8169e+00 -2.0300e+00 + -2.4878e+00 -1.5499e+00 -2.0300e+00 + -2.6447e+00 -1.2636e+00 -2.0300e+00 + -2.7688e+00 -9.6171e-01 -2.0300e+00 + -2.8586e+00 -6.4785e-01 -2.0300e+00 + -2.9129e+00 -3.2594e-01 -2.0300e+00 + -2.9311e+00 3.5896e-16 -2.0300e+00 + -2.9313e+00 3.5898e-16 -1.2930e+00 + -2.9314e+00 3.5900e-16 -5.5600e-01 + -2.9316e+00 3.5902e-16 1.8100e-01 + -2.9317e+00 3.5903e-16 9.1800e-01 + -2.9319e+00 3.5905e-16 1.6550e+00 + -5.3858e-16 -2.9319e+00 1.6550e+00 + 2.4953e-01 -2.9213e+00 1.6550e+00 + 4.9726e-01 -2.8894e+00 1.6550e+00 + 7.4137e-01 -2.8366e+00 1.6550e+00 + 9.8011e-01 -2.7632e+00 1.6550e+00 + 1.2117e+00 -2.6698e+00 1.6550e+00 + 1.4346e+00 -2.5570e+00 1.6550e+00 + 1.6470e+00 -2.4256e+00 1.6550e+00 + 1.8474e+00 -2.2766e+00 1.6550e+00 + 2.0345e+00 -2.1111e+00 1.6550e+00 + 2.2068e+00 -1.9303e+00 1.6550e+00 + 2.3631e+00 -1.7355e+00 1.6550e+00 + 2.5022e+00 -1.5281e+00 1.6550e+00 + 2.6232e+00 -1.3096e+00 1.6550e+00 + 2.7251e+00 -1.0815e+00 1.6550e+00 + 2.8073e+00 -8.4568e-01 1.6550e+00 + 2.8691e+00 -6.0369e-01 1.6550e+00 + 2.9100e+00 -3.5731e-01 1.6550e+00 + 2.9100e+00 -3.5731e-01 1.6550e+00 + 2.9099e+00 -3.5729e-01 9.1800e-01 + 2.9097e+00 -3.5727e-01 1.8100e-01 + 2.9096e+00 -3.5725e-01 -5.5600e-01 + 2.9094e+00 -3.5723e-01 -1.2930e+00 + 2.9093e+00 -3.5721e-01 -2.0300e+00 + 2.8515e+00 -6.7851e-01 -2.0300e+00 + 2.7583e+00 -9.9139e-01 -2.0300e+00 + 2.6310e+00 -1.2920e+00 -2.0300e+00 + 2.4710e+00 -1.5765e+00 -2.0300e+00 + 2.2804e+00 -1.8415e+00 -2.0300e+00 + 2.0614e+00 -2.0837e+00 -2.0300e+00 + 1.8169e+00 -2.3000e+00 -2.0300e+00 + 1.5499e+00 -2.4878e+00 -2.0300e+00 + 1.2636e+00 -2.6447e+00 -2.0300e+00 + 9.6171e-01 -2.7688e+00 -2.0300e+00 + 6.4785e-01 -2.8586e+00 -2.0300e+00 + 3.2594e-01 -2.9129e+00 -2.0300e+00 + -5.3843e-16 -2.9311e+00 -2.0300e+00 + -5.3846e-16 -2.9313e+00 -1.2930e+00 + -5.3849e-16 -2.9314e+00 -5.5600e-01 + -5.3852e-16 -2.9316e+00 1.8100e-01 + -5.3855e-16 -2.9317e+00 9.1800e-01 + -5.3858e-16 -2.9319e+00 1.6550e+00 diff --git a/src/ForcingTerms/coil_geometries/mastu_rmpl.dat b/src/ForcingTerms/coil_geometries/mastu_rmpl.dat index 7d3a798d3..1bb9ca785 100644 --- a/src/ForcingTerms/coil_geometries/mastu_rmpl.dat +++ b/src/ForcingTerms/coil_geometries/mastu_rmpl.dat @@ -1,337 +1,337 @@ - 8 1 42 1.00 - 1.3110e+00 0.0000e+00 -7.9100e-01 - 1.3107e+00 2.6300e-02 -7.9100e-01 - 1.3099e+00 5.2589e-02 -7.9100e-01 - 1.3086e+00 7.8857e-02 -7.9100e-01 - 1.3068e+00 1.0509e-01 -7.9100e-01 - 1.3044e+00 1.3129e-01 -7.9100e-01 - 1.3015e+00 1.5743e-01 -7.9100e-01 - 1.2981e+00 1.8351e-01 -7.9100e-01 - 1.2942e+00 2.0951e-01 -7.9100e-01 - 1.2897e+00 2.3543e-01 -7.9100e-01 - 1.2847e+00 2.6125e-01 -7.9100e-01 - 1.2792e+00 2.8697e-01 -7.9100e-01 - 1.2732e+00 3.1258e-01 -7.9100e-01 - 1.2667e+00 3.3806e-01 -7.9100e-01 - 1.2596e+00 3.6340e-01 -7.9100e-01 - 1.2521e+00 3.8859e-01 -7.9100e-01 - 1.2440e+00 4.1363e-01 -7.9100e-01 - 1.2355e+00 4.3851e-01 -7.9100e-01 - 1.2264e+00 4.6320e-01 -7.9100e-01 - 1.2169e+00 4.8771e-01 -7.9100e-01 - 1.2169e+00 4.8771e-01 -7.9100e-01 - 1.2703e+00 5.0910e-01 -6.9100e-01 - 1.3237e+00 5.3049e-01 -5.9100e-01 - 1.3352e+00 5.0068e-01 -5.9100e-01 - 1.3461e+00 4.7062e-01 -5.9100e-01 - 1.3563e+00 4.4032e-01 -5.9100e-01 - 1.3658e+00 4.0980e-01 -5.9100e-01 - 1.3747e+00 3.7908e-01 -5.9100e-01 - 1.3828e+00 3.4816e-01 -5.9100e-01 - 1.3903e+00 3.1707e-01 -5.9100e-01 - 1.3971e+00 2.8582e-01 -5.9100e-01 - 1.4031e+00 2.5442e-01 -5.9100e-01 - 1.4085e+00 2.2290e-01 -5.9100e-01 - 1.4131e+00 1.9127e-01 -5.9100e-01 - 1.4170e+00 1.5954e-01 -5.9100e-01 - 1.4203e+00 1.2773e-01 -5.9100e-01 - 1.4228e+00 9.5850e-02 -5.9100e-01 - 1.4246e+00 6.3927e-02 -5.9100e-01 - 1.4256e+00 3.1972e-02 -5.9100e-01 - 1.4260e+00 0.0000e+00 -5.9100e-01 - 1.3685e+00 0.0000e+00 -6.9100e-01 - 1.3110e+00 0.0000e+00 -7.9100e-01 - 9.2702e-01 9.2702e-01 -7.9100e-01 - 9.0823e-01 9.4543e-01 -7.9100e-01 - 8.8909e-01 9.6346e-01 -7.9100e-01 - 8.6958e-01 9.8110e-01 -7.9100e-01 - 8.4972e-01 9.9835e-01 -7.9100e-01 - 8.2952e-01 1.0152e+00 -7.9100e-01 - 8.0899e-01 1.0316e+00 -7.9100e-01 - 7.8813e-01 1.0476e+00 -7.9100e-01 - 7.6696e-01 1.0632e+00 -7.9100e-01 - 7.4547e-01 1.0784e+00 -7.9100e-01 - 7.2369e-01 1.0932e+00 -7.9100e-01 - 7.0161e-01 1.1075e+00 -7.9100e-01 - 6.7926e-01 1.1213e+00 -7.9100e-01 - 6.5663e-01 1.1347e+00 -7.9100e-01 - 6.3373e-01 1.1477e+00 -7.9100e-01 - 6.1058e-01 1.1601e+00 -7.9100e-01 - 5.8718e-01 1.1721e+00 -7.9100e-01 - 5.6355e-01 1.1837e+00 -7.9100e-01 - 5.3969e-01 1.1948e+00 -7.9100e-01 - 5.1562e-01 1.2053e+00 -7.9100e-01 - 5.1562e-01 1.2053e+00 -7.9100e-01 - 5.3823e-01 1.2582e+00 -6.9100e-01 - 5.6085e-01 1.3111e+00 -5.9100e-01 - 5.9010e-01 1.2982e+00 -5.9100e-01 - 6.1906e-01 1.2846e+00 -5.9100e-01 - 6.4770e-01 1.2704e+00 -5.9100e-01 - 6.7602e-01 1.2556e+00 -5.9100e-01 - 7.0400e-01 1.2401e+00 -5.9100e-01 - 7.3163e-01 1.2240e+00 -5.9100e-01 - 7.5889e-01 1.2073e+00 -5.9100e-01 - 7.8577e-01 1.1900e+00 -5.9100e-01 - 8.1225e-01 1.1721e+00 -5.9100e-01 - 8.3832e-01 1.1536e+00 -5.9100e-01 - 8.6398e-01 1.1345e+00 -5.9100e-01 - 8.8919e-01 1.1148e+00 -5.9100e-01 - 9.1397e-01 1.0946e+00 -5.9100e-01 - 9.3828e-01 1.0738e+00 -5.9100e-01 - 9.6212e-01 1.0525e+00 -5.9100e-01 - 9.8547e-01 1.0307e+00 -5.9100e-01 - 1.0083e+00 1.0083e+00 -5.9100e-01 - 9.6768e-01 9.6768e-01 -6.9100e-01 - 9.2702e-01 9.2702e-01 -7.9100e-01 - 8.0276e-17 1.3110e+00 -7.9100e-01 - -2.6300e-02 1.3107e+00 -7.9100e-01 - -5.2589e-02 1.3099e+00 -7.9100e-01 - -7.8857e-02 1.3086e+00 -7.9100e-01 - -1.0509e-01 1.3068e+00 -7.9100e-01 - -1.3129e-01 1.3044e+00 -7.9100e-01 - -1.5743e-01 1.3015e+00 -7.9100e-01 - -1.8351e-01 1.2981e+00 -7.9100e-01 - -2.0951e-01 1.2942e+00 -7.9100e-01 - -2.3543e-01 1.2897e+00 -7.9100e-01 - -2.6125e-01 1.2847e+00 -7.9100e-01 - -2.8697e-01 1.2792e+00 -7.9100e-01 - -3.1258e-01 1.2732e+00 -7.9100e-01 - -3.3806e-01 1.2667e+00 -7.9100e-01 - -3.6340e-01 1.2596e+00 -7.9100e-01 - -3.8859e-01 1.2521e+00 -7.9100e-01 - -4.1363e-01 1.2440e+00 -7.9100e-01 - -4.3851e-01 1.2355e+00 -7.9100e-01 - -4.6320e-01 1.2264e+00 -7.9100e-01 - -4.8771e-01 1.2169e+00 -7.9100e-01 - -4.8771e-01 1.2169e+00 -7.9100e-01 - -5.0910e-01 1.2703e+00 -6.9100e-01 - -5.3049e-01 1.3237e+00 -5.9100e-01 - -5.0068e-01 1.3352e+00 -5.9100e-01 - -4.7062e-01 1.3461e+00 -5.9100e-01 - -4.4032e-01 1.3563e+00 -5.9100e-01 - -4.0980e-01 1.3658e+00 -5.9100e-01 - -3.7908e-01 1.3747e+00 -5.9100e-01 - -3.4816e-01 1.3828e+00 -5.9100e-01 - -3.1707e-01 1.3903e+00 -5.9100e-01 - -2.8582e-01 1.3971e+00 -5.9100e-01 - -2.5442e-01 1.4031e+00 -5.9100e-01 - -2.2290e-01 1.4085e+00 -5.9100e-01 - -1.9127e-01 1.4131e+00 -5.9100e-01 - -1.5954e-01 1.4170e+00 -5.9100e-01 - -1.2773e-01 1.4203e+00 -5.9100e-01 - -9.5850e-02 1.4228e+00 -5.9100e-01 - -6.3927e-02 1.4246e+00 -5.9100e-01 - -3.1972e-02 1.4256e+00 -5.9100e-01 - 8.7317e-17 1.4260e+00 -5.9100e-01 - 8.3796e-17 1.3685e+00 -6.9100e-01 - 8.0276e-17 1.3110e+00 -7.9100e-01 - -9.2702e-01 9.2702e-01 -7.9100e-01 - -9.4543e-01 9.0823e-01 -7.9100e-01 - -9.6346e-01 8.8909e-01 -7.9100e-01 - -9.8110e-01 8.6958e-01 -7.9100e-01 - -9.9835e-01 8.4972e-01 -7.9100e-01 - -1.0152e+00 8.2952e-01 -7.9100e-01 - -1.0316e+00 8.0899e-01 -7.9100e-01 - -1.0476e+00 7.8813e-01 -7.9100e-01 - -1.0632e+00 7.6696e-01 -7.9100e-01 - -1.0784e+00 7.4547e-01 -7.9100e-01 - -1.0932e+00 7.2369e-01 -7.9100e-01 - -1.1075e+00 7.0161e-01 -7.9100e-01 - -1.1213e+00 6.7926e-01 -7.9100e-01 - -1.1347e+00 6.5663e-01 -7.9100e-01 - -1.1477e+00 6.3373e-01 -7.9100e-01 - -1.1601e+00 6.1058e-01 -7.9100e-01 - -1.1721e+00 5.8718e-01 -7.9100e-01 - -1.1837e+00 5.6355e-01 -7.9100e-01 - -1.1948e+00 5.3969e-01 -7.9100e-01 - -1.2053e+00 5.1562e-01 -7.9100e-01 - -1.2053e+00 5.1562e-01 -7.9100e-01 - -1.2582e+00 5.3823e-01 -6.9100e-01 - -1.3111e+00 5.6085e-01 -5.9100e-01 - -1.2982e+00 5.9010e-01 -5.9100e-01 - -1.2846e+00 6.1906e-01 -5.9100e-01 - -1.2704e+00 6.4770e-01 -5.9100e-01 - -1.2556e+00 6.7602e-01 -5.9100e-01 - -1.2401e+00 7.0400e-01 -5.9100e-01 - -1.2240e+00 7.3163e-01 -5.9100e-01 - -1.2073e+00 7.5889e-01 -5.9100e-01 - -1.1900e+00 7.8577e-01 -5.9100e-01 - -1.1721e+00 8.1225e-01 -5.9100e-01 - -1.1536e+00 8.3832e-01 -5.9100e-01 - -1.1345e+00 8.6398e-01 -5.9100e-01 - -1.1148e+00 8.8919e-01 -5.9100e-01 - -1.0946e+00 9.1397e-01 -5.9100e-01 - -1.0738e+00 9.3828e-01 -5.9100e-01 - -1.0525e+00 9.6212e-01 -5.9100e-01 - -1.0307e+00 9.8547e-01 -5.9100e-01 - -1.0083e+00 1.0083e+00 -5.9100e-01 - -9.6768e-01 9.6768e-01 -6.9100e-01 - -9.2702e-01 9.2702e-01 -7.9100e-01 - -1.3110e+00 1.6055e-16 -7.9100e-01 - -1.3107e+00 -2.6300e-02 -7.9100e-01 - -1.3099e+00 -5.2589e-02 -7.9100e-01 - -1.3086e+00 -7.8857e-02 -7.9100e-01 - -1.3068e+00 -1.0509e-01 -7.9100e-01 - -1.3044e+00 -1.3129e-01 -7.9100e-01 - -1.3015e+00 -1.5743e-01 -7.9100e-01 - -1.2981e+00 -1.8351e-01 -7.9100e-01 - -1.2942e+00 -2.0951e-01 -7.9100e-01 - -1.2897e+00 -2.3543e-01 -7.9100e-01 - -1.2847e+00 -2.6125e-01 -7.9100e-01 - -1.2792e+00 -2.8697e-01 -7.9100e-01 - -1.2732e+00 -3.1258e-01 -7.9100e-01 - -1.2667e+00 -3.3806e-01 -7.9100e-01 - -1.2596e+00 -3.6340e-01 -7.9100e-01 - -1.2521e+00 -3.8859e-01 -7.9100e-01 - -1.2440e+00 -4.1363e-01 -7.9100e-01 - -1.2355e+00 -4.3851e-01 -7.9100e-01 - -1.2264e+00 -4.6320e-01 -7.9100e-01 - -1.2169e+00 -4.8771e-01 -7.9100e-01 - -1.2169e+00 -4.8771e-01 -7.9100e-01 - -1.2703e+00 -5.0910e-01 -6.9100e-01 - -1.3237e+00 -5.3049e-01 -5.9100e-01 - -1.3352e+00 -5.0068e-01 -5.9100e-01 - -1.3461e+00 -4.7062e-01 -5.9100e-01 - -1.3563e+00 -4.4032e-01 -5.9100e-01 - -1.3658e+00 -4.0980e-01 -5.9100e-01 - -1.3747e+00 -3.7908e-01 -5.9100e-01 - -1.3828e+00 -3.4816e-01 -5.9100e-01 - -1.3903e+00 -3.1707e-01 -5.9100e-01 - -1.3971e+00 -2.8582e-01 -5.9100e-01 - -1.4031e+00 -2.5442e-01 -5.9100e-01 - -1.4085e+00 -2.2290e-01 -5.9100e-01 - -1.4131e+00 -1.9127e-01 -5.9100e-01 - -1.4170e+00 -1.5954e-01 -5.9100e-01 - -1.4203e+00 -1.2773e-01 -5.9100e-01 - -1.4228e+00 -9.5850e-02 -5.9100e-01 - -1.4246e+00 -6.3927e-02 -5.9100e-01 - -1.4256e+00 -3.1972e-02 -5.9100e-01 - -1.4260e+00 1.7463e-16 -5.9100e-01 - -1.3685e+00 1.6759e-16 -6.9100e-01 - -1.3110e+00 1.6055e-16 -7.9100e-01 - -9.2702e-01 -9.2702e-01 -7.9100e-01 - -9.0823e-01 -9.4543e-01 -7.9100e-01 - -8.8909e-01 -9.6346e-01 -7.9100e-01 - -8.6958e-01 -9.8110e-01 -7.9100e-01 - -8.4972e-01 -9.9835e-01 -7.9100e-01 - -8.2952e-01 -1.0152e+00 -7.9100e-01 - -8.0899e-01 -1.0316e+00 -7.9100e-01 - -7.8813e-01 -1.0476e+00 -7.9100e-01 - -7.6696e-01 -1.0632e+00 -7.9100e-01 - -7.4547e-01 -1.0784e+00 -7.9100e-01 - -7.2369e-01 -1.0932e+00 -7.9100e-01 - -7.0161e-01 -1.1075e+00 -7.9100e-01 - -6.7926e-01 -1.1213e+00 -7.9100e-01 - -6.5663e-01 -1.1347e+00 -7.9100e-01 - -6.3373e-01 -1.1477e+00 -7.9100e-01 - -6.1058e-01 -1.1601e+00 -7.9100e-01 - -5.8718e-01 -1.1721e+00 -7.9100e-01 - -5.6355e-01 -1.1837e+00 -7.9100e-01 - -5.3969e-01 -1.1948e+00 -7.9100e-01 - -5.1562e-01 -1.2053e+00 -7.9100e-01 - -5.1562e-01 -1.2053e+00 -7.9100e-01 - -5.3823e-01 -1.2582e+00 -6.9100e-01 - -5.6085e-01 -1.3111e+00 -5.9100e-01 - -5.9010e-01 -1.2982e+00 -5.9100e-01 - -6.1906e-01 -1.2846e+00 -5.9100e-01 - -6.4770e-01 -1.2704e+00 -5.9100e-01 - -6.7602e-01 -1.2556e+00 -5.9100e-01 - -7.0400e-01 -1.2401e+00 -5.9100e-01 - -7.3163e-01 -1.2240e+00 -5.9100e-01 - -7.5889e-01 -1.2073e+00 -5.9100e-01 - -7.8577e-01 -1.1900e+00 -5.9100e-01 - -8.1225e-01 -1.1721e+00 -5.9100e-01 - -8.3832e-01 -1.1536e+00 -5.9100e-01 - -8.6398e-01 -1.1345e+00 -5.9100e-01 - -8.8919e-01 -1.1148e+00 -5.9100e-01 - -9.1397e-01 -1.0946e+00 -5.9100e-01 - -9.3828e-01 -1.0738e+00 -5.9100e-01 - -9.6212e-01 -1.0525e+00 -5.9100e-01 - -9.8547e-01 -1.0307e+00 -5.9100e-01 - -1.0083e+00 -1.0083e+00 -5.9100e-01 - -9.6768e-01 -9.6768e-01 -6.9100e-01 - -9.2702e-01 -9.2702e-01 -7.9100e-01 - -2.4083e-16 -1.3110e+00 -7.9100e-01 - 2.6300e-02 -1.3107e+00 -7.9100e-01 - 5.2589e-02 -1.3099e+00 -7.9100e-01 - 7.8857e-02 -1.3086e+00 -7.9100e-01 - 1.0509e-01 -1.3068e+00 -7.9100e-01 - 1.3129e-01 -1.3044e+00 -7.9100e-01 - 1.5743e-01 -1.3015e+00 -7.9100e-01 - 1.8351e-01 -1.2981e+00 -7.9100e-01 - 2.0951e-01 -1.2942e+00 -7.9100e-01 - 2.3543e-01 -1.2897e+00 -7.9100e-01 - 2.6125e-01 -1.2847e+00 -7.9100e-01 - 2.8697e-01 -1.2792e+00 -7.9100e-01 - 3.1258e-01 -1.2732e+00 -7.9100e-01 - 3.3806e-01 -1.2667e+00 -7.9100e-01 - 3.6340e-01 -1.2596e+00 -7.9100e-01 - 3.8859e-01 -1.2521e+00 -7.9100e-01 - 4.1363e-01 -1.2440e+00 -7.9100e-01 - 4.3851e-01 -1.2355e+00 -7.9100e-01 - 4.6320e-01 -1.2264e+00 -7.9100e-01 - 4.8771e-01 -1.2169e+00 -7.9100e-01 - 4.8771e-01 -1.2169e+00 -7.9100e-01 - 5.0910e-01 -1.2703e+00 -6.9100e-01 - 5.3049e-01 -1.3237e+00 -5.9100e-01 - 5.0068e-01 -1.3352e+00 -5.9100e-01 - 4.7062e-01 -1.3461e+00 -5.9100e-01 - 4.4032e-01 -1.3563e+00 -5.9100e-01 - 4.0980e-01 -1.3658e+00 -5.9100e-01 - 3.7908e-01 -1.3747e+00 -5.9100e-01 - 3.4816e-01 -1.3828e+00 -5.9100e-01 - 3.1707e-01 -1.3903e+00 -5.9100e-01 - 2.8582e-01 -1.3971e+00 -5.9100e-01 - 2.5442e-01 -1.4031e+00 -5.9100e-01 - 2.2290e-01 -1.4085e+00 -5.9100e-01 - 1.9127e-01 -1.4131e+00 -5.9100e-01 - 1.5954e-01 -1.4170e+00 -5.9100e-01 - 1.2773e-01 -1.4203e+00 -5.9100e-01 - 9.5850e-02 -1.4228e+00 -5.9100e-01 - 6.3927e-02 -1.4246e+00 -5.9100e-01 - 3.1972e-02 -1.4256e+00 -5.9100e-01 - -2.6195e-16 -1.4260e+00 -5.9100e-01 - -2.5139e-16 -1.3685e+00 -6.9100e-01 - -2.4083e-16 -1.3110e+00 -7.9100e-01 - 9.2702e-01 -9.2702e-01 -7.9100e-01 - 9.4543e-01 -9.0823e-01 -7.9100e-01 - 9.6346e-01 -8.8909e-01 -7.9100e-01 - 9.8110e-01 -8.6958e-01 -7.9100e-01 - 9.9835e-01 -8.4972e-01 -7.9100e-01 - 1.0152e+00 -8.2952e-01 -7.9100e-01 - 1.0316e+00 -8.0899e-01 -7.9100e-01 - 1.0476e+00 -7.8813e-01 -7.9100e-01 - 1.0632e+00 -7.6696e-01 -7.9100e-01 - 1.0784e+00 -7.4547e-01 -7.9100e-01 - 1.0932e+00 -7.2369e-01 -7.9100e-01 - 1.1075e+00 -7.0161e-01 -7.9100e-01 - 1.1213e+00 -6.7926e-01 -7.9100e-01 - 1.1347e+00 -6.5663e-01 -7.9100e-01 - 1.1477e+00 -6.3373e-01 -7.9100e-01 - 1.1601e+00 -6.1058e-01 -7.9100e-01 - 1.1721e+00 -5.8718e-01 -7.9100e-01 - 1.1837e+00 -5.6355e-01 -7.9100e-01 - 1.1948e+00 -5.3969e-01 -7.9100e-01 - 1.2053e+00 -5.1562e-01 -7.9100e-01 - 1.2053e+00 -5.1562e-01 -7.9100e-01 - 1.2582e+00 -5.3823e-01 -6.9100e-01 - 1.3111e+00 -5.6085e-01 -5.9100e-01 - 1.2982e+00 -5.9010e-01 -5.9100e-01 - 1.2846e+00 -6.1906e-01 -5.9100e-01 - 1.2704e+00 -6.4770e-01 -5.9100e-01 - 1.2556e+00 -6.7602e-01 -5.9100e-01 - 1.2401e+00 -7.0400e-01 -5.9100e-01 - 1.2240e+00 -7.3163e-01 -5.9100e-01 - 1.2073e+00 -7.5889e-01 -5.9100e-01 - 1.1900e+00 -7.8577e-01 -5.9100e-01 - 1.1721e+00 -8.1225e-01 -5.9100e-01 - 1.1536e+00 -8.3832e-01 -5.9100e-01 - 1.1345e+00 -8.6398e-01 -5.9100e-01 - 1.1148e+00 -8.8919e-01 -5.9100e-01 - 1.0946e+00 -9.1397e-01 -5.9100e-01 - 1.0738e+00 -9.3828e-01 -5.9100e-01 - 1.0525e+00 -9.6212e-01 -5.9100e-01 - 1.0307e+00 -9.8547e-01 -5.9100e-01 - 1.0083e+00 -1.0083e+00 -5.9100e-01 - 9.6768e-01 -9.6768e-01 -6.9100e-01 - 9.2702e-01 -9.2702e-01 -7.9100e-01 + 8 1 42 1.00 + 1.3110e+00 0.0000e+00 -7.9100e-01 + 1.3107e+00 2.6300e-02 -7.9100e-01 + 1.3099e+00 5.2589e-02 -7.9100e-01 + 1.3086e+00 7.8857e-02 -7.9100e-01 + 1.3068e+00 1.0509e-01 -7.9100e-01 + 1.3044e+00 1.3129e-01 -7.9100e-01 + 1.3015e+00 1.5743e-01 -7.9100e-01 + 1.2981e+00 1.8351e-01 -7.9100e-01 + 1.2942e+00 2.0951e-01 -7.9100e-01 + 1.2897e+00 2.3543e-01 -7.9100e-01 + 1.2847e+00 2.6125e-01 -7.9100e-01 + 1.2792e+00 2.8697e-01 -7.9100e-01 + 1.2732e+00 3.1258e-01 -7.9100e-01 + 1.2667e+00 3.3806e-01 -7.9100e-01 + 1.2596e+00 3.6340e-01 -7.9100e-01 + 1.2521e+00 3.8859e-01 -7.9100e-01 + 1.2440e+00 4.1363e-01 -7.9100e-01 + 1.2355e+00 4.3851e-01 -7.9100e-01 + 1.2264e+00 4.6320e-01 -7.9100e-01 + 1.2169e+00 4.8771e-01 -7.9100e-01 + 1.2169e+00 4.8771e-01 -7.9100e-01 + 1.2703e+00 5.0910e-01 -6.9100e-01 + 1.3237e+00 5.3049e-01 -5.9100e-01 + 1.3352e+00 5.0068e-01 -5.9100e-01 + 1.3461e+00 4.7062e-01 -5.9100e-01 + 1.3563e+00 4.4032e-01 -5.9100e-01 + 1.3658e+00 4.0980e-01 -5.9100e-01 + 1.3747e+00 3.7908e-01 -5.9100e-01 + 1.3828e+00 3.4816e-01 -5.9100e-01 + 1.3903e+00 3.1707e-01 -5.9100e-01 + 1.3971e+00 2.8582e-01 -5.9100e-01 + 1.4031e+00 2.5442e-01 -5.9100e-01 + 1.4085e+00 2.2290e-01 -5.9100e-01 + 1.4131e+00 1.9127e-01 -5.9100e-01 + 1.4170e+00 1.5954e-01 -5.9100e-01 + 1.4203e+00 1.2773e-01 -5.9100e-01 + 1.4228e+00 9.5850e-02 -5.9100e-01 + 1.4246e+00 6.3927e-02 -5.9100e-01 + 1.4256e+00 3.1972e-02 -5.9100e-01 + 1.4260e+00 0.0000e+00 -5.9100e-01 + 1.3685e+00 0.0000e+00 -6.9100e-01 + 1.3110e+00 0.0000e+00 -7.9100e-01 + 9.2702e-01 9.2702e-01 -7.9100e-01 + 9.0823e-01 9.4543e-01 -7.9100e-01 + 8.8909e-01 9.6346e-01 -7.9100e-01 + 8.6958e-01 9.8110e-01 -7.9100e-01 + 8.4972e-01 9.9835e-01 -7.9100e-01 + 8.2952e-01 1.0152e+00 -7.9100e-01 + 8.0899e-01 1.0316e+00 -7.9100e-01 + 7.8813e-01 1.0476e+00 -7.9100e-01 + 7.6696e-01 1.0632e+00 -7.9100e-01 + 7.4547e-01 1.0784e+00 -7.9100e-01 + 7.2369e-01 1.0932e+00 -7.9100e-01 + 7.0161e-01 1.1075e+00 -7.9100e-01 + 6.7926e-01 1.1213e+00 -7.9100e-01 + 6.5663e-01 1.1347e+00 -7.9100e-01 + 6.3373e-01 1.1477e+00 -7.9100e-01 + 6.1058e-01 1.1601e+00 -7.9100e-01 + 5.8718e-01 1.1721e+00 -7.9100e-01 + 5.6355e-01 1.1837e+00 -7.9100e-01 + 5.3969e-01 1.1948e+00 -7.9100e-01 + 5.1562e-01 1.2053e+00 -7.9100e-01 + 5.1562e-01 1.2053e+00 -7.9100e-01 + 5.3823e-01 1.2582e+00 -6.9100e-01 + 5.6085e-01 1.3111e+00 -5.9100e-01 + 5.9010e-01 1.2982e+00 -5.9100e-01 + 6.1906e-01 1.2846e+00 -5.9100e-01 + 6.4770e-01 1.2704e+00 -5.9100e-01 + 6.7602e-01 1.2556e+00 -5.9100e-01 + 7.0400e-01 1.2401e+00 -5.9100e-01 + 7.3163e-01 1.2240e+00 -5.9100e-01 + 7.5889e-01 1.2073e+00 -5.9100e-01 + 7.8577e-01 1.1900e+00 -5.9100e-01 + 8.1225e-01 1.1721e+00 -5.9100e-01 + 8.3832e-01 1.1536e+00 -5.9100e-01 + 8.6398e-01 1.1345e+00 -5.9100e-01 + 8.8919e-01 1.1148e+00 -5.9100e-01 + 9.1397e-01 1.0946e+00 -5.9100e-01 + 9.3828e-01 1.0738e+00 -5.9100e-01 + 9.6212e-01 1.0525e+00 -5.9100e-01 + 9.8547e-01 1.0307e+00 -5.9100e-01 + 1.0083e+00 1.0083e+00 -5.9100e-01 + 9.6768e-01 9.6768e-01 -6.9100e-01 + 9.2702e-01 9.2702e-01 -7.9100e-01 + 8.0276e-17 1.3110e+00 -7.9100e-01 + -2.6300e-02 1.3107e+00 -7.9100e-01 + -5.2589e-02 1.3099e+00 -7.9100e-01 + -7.8857e-02 1.3086e+00 -7.9100e-01 + -1.0509e-01 1.3068e+00 -7.9100e-01 + -1.3129e-01 1.3044e+00 -7.9100e-01 + -1.5743e-01 1.3015e+00 -7.9100e-01 + -1.8351e-01 1.2981e+00 -7.9100e-01 + -2.0951e-01 1.2942e+00 -7.9100e-01 + -2.3543e-01 1.2897e+00 -7.9100e-01 + -2.6125e-01 1.2847e+00 -7.9100e-01 + -2.8697e-01 1.2792e+00 -7.9100e-01 + -3.1258e-01 1.2732e+00 -7.9100e-01 + -3.3806e-01 1.2667e+00 -7.9100e-01 + -3.6340e-01 1.2596e+00 -7.9100e-01 + -3.8859e-01 1.2521e+00 -7.9100e-01 + -4.1363e-01 1.2440e+00 -7.9100e-01 + -4.3851e-01 1.2355e+00 -7.9100e-01 + -4.6320e-01 1.2264e+00 -7.9100e-01 + -4.8771e-01 1.2169e+00 -7.9100e-01 + -4.8771e-01 1.2169e+00 -7.9100e-01 + -5.0910e-01 1.2703e+00 -6.9100e-01 + -5.3049e-01 1.3237e+00 -5.9100e-01 + -5.0068e-01 1.3352e+00 -5.9100e-01 + -4.7062e-01 1.3461e+00 -5.9100e-01 + -4.4032e-01 1.3563e+00 -5.9100e-01 + -4.0980e-01 1.3658e+00 -5.9100e-01 + -3.7908e-01 1.3747e+00 -5.9100e-01 + -3.4816e-01 1.3828e+00 -5.9100e-01 + -3.1707e-01 1.3903e+00 -5.9100e-01 + -2.8582e-01 1.3971e+00 -5.9100e-01 + -2.5442e-01 1.4031e+00 -5.9100e-01 + -2.2290e-01 1.4085e+00 -5.9100e-01 + -1.9127e-01 1.4131e+00 -5.9100e-01 + -1.5954e-01 1.4170e+00 -5.9100e-01 + -1.2773e-01 1.4203e+00 -5.9100e-01 + -9.5850e-02 1.4228e+00 -5.9100e-01 + -6.3927e-02 1.4246e+00 -5.9100e-01 + -3.1972e-02 1.4256e+00 -5.9100e-01 + 8.7317e-17 1.4260e+00 -5.9100e-01 + 8.3796e-17 1.3685e+00 -6.9100e-01 + 8.0276e-17 1.3110e+00 -7.9100e-01 + -9.2702e-01 9.2702e-01 -7.9100e-01 + -9.4543e-01 9.0823e-01 -7.9100e-01 + -9.6346e-01 8.8909e-01 -7.9100e-01 + -9.8110e-01 8.6958e-01 -7.9100e-01 + -9.9835e-01 8.4972e-01 -7.9100e-01 + -1.0152e+00 8.2952e-01 -7.9100e-01 + -1.0316e+00 8.0899e-01 -7.9100e-01 + -1.0476e+00 7.8813e-01 -7.9100e-01 + -1.0632e+00 7.6696e-01 -7.9100e-01 + -1.0784e+00 7.4547e-01 -7.9100e-01 + -1.0932e+00 7.2369e-01 -7.9100e-01 + -1.1075e+00 7.0161e-01 -7.9100e-01 + -1.1213e+00 6.7926e-01 -7.9100e-01 + -1.1347e+00 6.5663e-01 -7.9100e-01 + -1.1477e+00 6.3373e-01 -7.9100e-01 + -1.1601e+00 6.1058e-01 -7.9100e-01 + -1.1721e+00 5.8718e-01 -7.9100e-01 + -1.1837e+00 5.6355e-01 -7.9100e-01 + -1.1948e+00 5.3969e-01 -7.9100e-01 + -1.2053e+00 5.1562e-01 -7.9100e-01 + -1.2053e+00 5.1562e-01 -7.9100e-01 + -1.2582e+00 5.3823e-01 -6.9100e-01 + -1.3111e+00 5.6085e-01 -5.9100e-01 + -1.2982e+00 5.9010e-01 -5.9100e-01 + -1.2846e+00 6.1906e-01 -5.9100e-01 + -1.2704e+00 6.4770e-01 -5.9100e-01 + -1.2556e+00 6.7602e-01 -5.9100e-01 + -1.2401e+00 7.0400e-01 -5.9100e-01 + -1.2240e+00 7.3163e-01 -5.9100e-01 + -1.2073e+00 7.5889e-01 -5.9100e-01 + -1.1900e+00 7.8577e-01 -5.9100e-01 + -1.1721e+00 8.1225e-01 -5.9100e-01 + -1.1536e+00 8.3832e-01 -5.9100e-01 + -1.1345e+00 8.6398e-01 -5.9100e-01 + -1.1148e+00 8.8919e-01 -5.9100e-01 + -1.0946e+00 9.1397e-01 -5.9100e-01 + -1.0738e+00 9.3828e-01 -5.9100e-01 + -1.0525e+00 9.6212e-01 -5.9100e-01 + -1.0307e+00 9.8547e-01 -5.9100e-01 + -1.0083e+00 1.0083e+00 -5.9100e-01 + -9.6768e-01 9.6768e-01 -6.9100e-01 + -9.2702e-01 9.2702e-01 -7.9100e-01 + -1.3110e+00 1.6055e-16 -7.9100e-01 + -1.3107e+00 -2.6300e-02 -7.9100e-01 + -1.3099e+00 -5.2589e-02 -7.9100e-01 + -1.3086e+00 -7.8857e-02 -7.9100e-01 + -1.3068e+00 -1.0509e-01 -7.9100e-01 + -1.3044e+00 -1.3129e-01 -7.9100e-01 + -1.3015e+00 -1.5743e-01 -7.9100e-01 + -1.2981e+00 -1.8351e-01 -7.9100e-01 + -1.2942e+00 -2.0951e-01 -7.9100e-01 + -1.2897e+00 -2.3543e-01 -7.9100e-01 + -1.2847e+00 -2.6125e-01 -7.9100e-01 + -1.2792e+00 -2.8697e-01 -7.9100e-01 + -1.2732e+00 -3.1258e-01 -7.9100e-01 + -1.2667e+00 -3.3806e-01 -7.9100e-01 + -1.2596e+00 -3.6340e-01 -7.9100e-01 + -1.2521e+00 -3.8859e-01 -7.9100e-01 + -1.2440e+00 -4.1363e-01 -7.9100e-01 + -1.2355e+00 -4.3851e-01 -7.9100e-01 + -1.2264e+00 -4.6320e-01 -7.9100e-01 + -1.2169e+00 -4.8771e-01 -7.9100e-01 + -1.2169e+00 -4.8771e-01 -7.9100e-01 + -1.2703e+00 -5.0910e-01 -6.9100e-01 + -1.3237e+00 -5.3049e-01 -5.9100e-01 + -1.3352e+00 -5.0068e-01 -5.9100e-01 + -1.3461e+00 -4.7062e-01 -5.9100e-01 + -1.3563e+00 -4.4032e-01 -5.9100e-01 + -1.3658e+00 -4.0980e-01 -5.9100e-01 + -1.3747e+00 -3.7908e-01 -5.9100e-01 + -1.3828e+00 -3.4816e-01 -5.9100e-01 + -1.3903e+00 -3.1707e-01 -5.9100e-01 + -1.3971e+00 -2.8582e-01 -5.9100e-01 + -1.4031e+00 -2.5442e-01 -5.9100e-01 + -1.4085e+00 -2.2290e-01 -5.9100e-01 + -1.4131e+00 -1.9127e-01 -5.9100e-01 + -1.4170e+00 -1.5954e-01 -5.9100e-01 + -1.4203e+00 -1.2773e-01 -5.9100e-01 + -1.4228e+00 -9.5850e-02 -5.9100e-01 + -1.4246e+00 -6.3927e-02 -5.9100e-01 + -1.4256e+00 -3.1972e-02 -5.9100e-01 + -1.4260e+00 1.7463e-16 -5.9100e-01 + -1.3685e+00 1.6759e-16 -6.9100e-01 + -1.3110e+00 1.6055e-16 -7.9100e-01 + -9.2702e-01 -9.2702e-01 -7.9100e-01 + -9.0823e-01 -9.4543e-01 -7.9100e-01 + -8.8909e-01 -9.6346e-01 -7.9100e-01 + -8.6958e-01 -9.8110e-01 -7.9100e-01 + -8.4972e-01 -9.9835e-01 -7.9100e-01 + -8.2952e-01 -1.0152e+00 -7.9100e-01 + -8.0899e-01 -1.0316e+00 -7.9100e-01 + -7.8813e-01 -1.0476e+00 -7.9100e-01 + -7.6696e-01 -1.0632e+00 -7.9100e-01 + -7.4547e-01 -1.0784e+00 -7.9100e-01 + -7.2369e-01 -1.0932e+00 -7.9100e-01 + -7.0161e-01 -1.1075e+00 -7.9100e-01 + -6.7926e-01 -1.1213e+00 -7.9100e-01 + -6.5663e-01 -1.1347e+00 -7.9100e-01 + -6.3373e-01 -1.1477e+00 -7.9100e-01 + -6.1058e-01 -1.1601e+00 -7.9100e-01 + -5.8718e-01 -1.1721e+00 -7.9100e-01 + -5.6355e-01 -1.1837e+00 -7.9100e-01 + -5.3969e-01 -1.1948e+00 -7.9100e-01 + -5.1562e-01 -1.2053e+00 -7.9100e-01 + -5.1562e-01 -1.2053e+00 -7.9100e-01 + -5.3823e-01 -1.2582e+00 -6.9100e-01 + -5.6085e-01 -1.3111e+00 -5.9100e-01 + -5.9010e-01 -1.2982e+00 -5.9100e-01 + -6.1906e-01 -1.2846e+00 -5.9100e-01 + -6.4770e-01 -1.2704e+00 -5.9100e-01 + -6.7602e-01 -1.2556e+00 -5.9100e-01 + -7.0400e-01 -1.2401e+00 -5.9100e-01 + -7.3163e-01 -1.2240e+00 -5.9100e-01 + -7.5889e-01 -1.2073e+00 -5.9100e-01 + -7.8577e-01 -1.1900e+00 -5.9100e-01 + -8.1225e-01 -1.1721e+00 -5.9100e-01 + -8.3832e-01 -1.1536e+00 -5.9100e-01 + -8.6398e-01 -1.1345e+00 -5.9100e-01 + -8.8919e-01 -1.1148e+00 -5.9100e-01 + -9.1397e-01 -1.0946e+00 -5.9100e-01 + -9.3828e-01 -1.0738e+00 -5.9100e-01 + -9.6212e-01 -1.0525e+00 -5.9100e-01 + -9.8547e-01 -1.0307e+00 -5.9100e-01 + -1.0083e+00 -1.0083e+00 -5.9100e-01 + -9.6768e-01 -9.6768e-01 -6.9100e-01 + -9.2702e-01 -9.2702e-01 -7.9100e-01 + -2.4083e-16 -1.3110e+00 -7.9100e-01 + 2.6300e-02 -1.3107e+00 -7.9100e-01 + 5.2589e-02 -1.3099e+00 -7.9100e-01 + 7.8857e-02 -1.3086e+00 -7.9100e-01 + 1.0509e-01 -1.3068e+00 -7.9100e-01 + 1.3129e-01 -1.3044e+00 -7.9100e-01 + 1.5743e-01 -1.3015e+00 -7.9100e-01 + 1.8351e-01 -1.2981e+00 -7.9100e-01 + 2.0951e-01 -1.2942e+00 -7.9100e-01 + 2.3543e-01 -1.2897e+00 -7.9100e-01 + 2.6125e-01 -1.2847e+00 -7.9100e-01 + 2.8697e-01 -1.2792e+00 -7.9100e-01 + 3.1258e-01 -1.2732e+00 -7.9100e-01 + 3.3806e-01 -1.2667e+00 -7.9100e-01 + 3.6340e-01 -1.2596e+00 -7.9100e-01 + 3.8859e-01 -1.2521e+00 -7.9100e-01 + 4.1363e-01 -1.2440e+00 -7.9100e-01 + 4.3851e-01 -1.2355e+00 -7.9100e-01 + 4.6320e-01 -1.2264e+00 -7.9100e-01 + 4.8771e-01 -1.2169e+00 -7.9100e-01 + 4.8771e-01 -1.2169e+00 -7.9100e-01 + 5.0910e-01 -1.2703e+00 -6.9100e-01 + 5.3049e-01 -1.3237e+00 -5.9100e-01 + 5.0068e-01 -1.3352e+00 -5.9100e-01 + 4.7062e-01 -1.3461e+00 -5.9100e-01 + 4.4032e-01 -1.3563e+00 -5.9100e-01 + 4.0980e-01 -1.3658e+00 -5.9100e-01 + 3.7908e-01 -1.3747e+00 -5.9100e-01 + 3.4816e-01 -1.3828e+00 -5.9100e-01 + 3.1707e-01 -1.3903e+00 -5.9100e-01 + 2.8582e-01 -1.3971e+00 -5.9100e-01 + 2.5442e-01 -1.4031e+00 -5.9100e-01 + 2.2290e-01 -1.4085e+00 -5.9100e-01 + 1.9127e-01 -1.4131e+00 -5.9100e-01 + 1.5954e-01 -1.4170e+00 -5.9100e-01 + 1.2773e-01 -1.4203e+00 -5.9100e-01 + 9.5850e-02 -1.4228e+00 -5.9100e-01 + 6.3927e-02 -1.4246e+00 -5.9100e-01 + 3.1972e-02 -1.4256e+00 -5.9100e-01 + -2.6195e-16 -1.4260e+00 -5.9100e-01 + -2.5139e-16 -1.3685e+00 -6.9100e-01 + -2.4083e-16 -1.3110e+00 -7.9100e-01 + 9.2702e-01 -9.2702e-01 -7.9100e-01 + 9.4543e-01 -9.0823e-01 -7.9100e-01 + 9.6346e-01 -8.8909e-01 -7.9100e-01 + 9.8110e-01 -8.6958e-01 -7.9100e-01 + 9.9835e-01 -8.4972e-01 -7.9100e-01 + 1.0152e+00 -8.2952e-01 -7.9100e-01 + 1.0316e+00 -8.0899e-01 -7.9100e-01 + 1.0476e+00 -7.8813e-01 -7.9100e-01 + 1.0632e+00 -7.6696e-01 -7.9100e-01 + 1.0784e+00 -7.4547e-01 -7.9100e-01 + 1.0932e+00 -7.2369e-01 -7.9100e-01 + 1.1075e+00 -7.0161e-01 -7.9100e-01 + 1.1213e+00 -6.7926e-01 -7.9100e-01 + 1.1347e+00 -6.5663e-01 -7.9100e-01 + 1.1477e+00 -6.3373e-01 -7.9100e-01 + 1.1601e+00 -6.1058e-01 -7.9100e-01 + 1.1721e+00 -5.8718e-01 -7.9100e-01 + 1.1837e+00 -5.6355e-01 -7.9100e-01 + 1.1948e+00 -5.3969e-01 -7.9100e-01 + 1.2053e+00 -5.1562e-01 -7.9100e-01 + 1.2053e+00 -5.1562e-01 -7.9100e-01 + 1.2582e+00 -5.3823e-01 -6.9100e-01 + 1.3111e+00 -5.6085e-01 -5.9100e-01 + 1.2982e+00 -5.9010e-01 -5.9100e-01 + 1.2846e+00 -6.1906e-01 -5.9100e-01 + 1.2704e+00 -6.4770e-01 -5.9100e-01 + 1.2556e+00 -6.7602e-01 -5.9100e-01 + 1.2401e+00 -7.0400e-01 -5.9100e-01 + 1.2240e+00 -7.3163e-01 -5.9100e-01 + 1.2073e+00 -7.5889e-01 -5.9100e-01 + 1.1900e+00 -7.8577e-01 -5.9100e-01 + 1.1721e+00 -8.1225e-01 -5.9100e-01 + 1.1536e+00 -8.3832e-01 -5.9100e-01 + 1.1345e+00 -8.6398e-01 -5.9100e-01 + 1.1148e+00 -8.8919e-01 -5.9100e-01 + 1.0946e+00 -9.1397e-01 -5.9100e-01 + 1.0738e+00 -9.3828e-01 -5.9100e-01 + 1.0525e+00 -9.6212e-01 -5.9100e-01 + 1.0307e+00 -9.8547e-01 -5.9100e-01 + 1.0083e+00 -1.0083e+00 -5.9100e-01 + 9.6768e-01 -9.6768e-01 -6.9100e-01 + 9.2702e-01 -9.2702e-01 -7.9100e-01 diff --git a/src/ForcingTerms/coil_geometries/mastu_rmpu.dat b/src/ForcingTerms/coil_geometries/mastu_rmpu.dat index 59d20c96d..ba2a4e14f 100644 --- a/src/ForcingTerms/coil_geometries/mastu_rmpu.dat +++ b/src/ForcingTerms/coil_geometries/mastu_rmpu.dat @@ -1,169 +1,169 @@ - 4 1 42 1.00 - 1.4180e+00 0.0000e+00 7.6000e-01 - 1.4177e+00 3.0026e-02 7.6000e-01 - 1.4167e+00 6.0039e-02 7.6000e-01 - 1.4151e+00 9.0025e-02 7.6000e-01 - 1.4129e+00 1.1997e-01 7.6000e-01 - 1.4101e+00 1.4986e-01 7.6000e-01 - 1.4066e+00 1.7969e-01 7.6000e-01 - 1.4024e+00 2.0943e-01 7.6000e-01 - 1.3977e+00 2.3908e-01 7.6000e-01 - 1.3923e+00 2.6862e-01 7.6000e-01 - 1.3863e+00 2.9805e-01 7.6000e-01 - 1.3797e+00 3.2733e-01 7.6000e-01 - 1.3725e+00 3.5648e-01 7.6000e-01 - 1.3646e+00 3.8546e-01 7.6000e-01 - 1.3561e+00 4.1427e-01 7.6000e-01 - 1.3471e+00 4.4289e-01 7.6000e-01 - 1.3374e+00 4.7132e-01 7.6000e-01 - 1.3271e+00 4.9953e-01 7.6000e-01 - 1.3162e+00 5.2752e-01 7.6000e-01 - 1.3162e+00 5.2752e-01 7.6000e-01 - 1.3487e+00 5.4054e-01 6.2950e-01 - 1.3812e+00 5.5356e-01 4.9900e-01 - 1.3926e+00 5.2419e-01 4.9900e-01 - 1.4034e+00 4.9458e-01 4.9900e-01 - 1.4136e+00 4.6475e-01 4.9900e-01 - 1.4231e+00 4.3472e-01 4.9900e-01 - 1.4320e+00 4.0449e-01 4.9900e-01 - 1.4402e+00 3.7407e-01 4.9900e-01 - 1.4478e+00 3.4349e-01 4.9900e-01 - 1.4548e+00 3.1276e-01 4.9900e-01 - 1.4611e+00 2.8188e-01 4.9900e-01 - 1.4667e+00 2.5088e-01 4.9900e-01 - 1.4717e+00 2.1977e-01 4.9900e-01 - 1.4760e+00 1.8856e-01 4.9900e-01 - 1.4797e+00 1.5726e-01 4.9900e-01 - 1.4827e+00 1.2589e-01 4.9900e-01 - 1.4850e+00 9.4469e-02 4.9900e-01 - 1.4867e+00 6.3003e-02 4.9900e-01 - 1.4877e+00 3.1509e-02 4.9900e-01 - 1.4880e+00 0.0000e+00 4.9900e-01 - 1.4530e+00 0.0000e+00 6.2950e-01 - 1.4180e+00 0.0000e+00 7.6000e-01 - 8.6827e-17 1.4180e+00 7.6000e-01 - -3.0026e-02 1.4177e+00 7.6000e-01 - -6.0039e-02 1.4167e+00 7.6000e-01 - -9.0025e-02 1.4151e+00 7.6000e-01 - -1.1997e-01 1.4129e+00 7.6000e-01 - -1.4986e-01 1.4101e+00 7.6000e-01 - -1.7969e-01 1.4066e+00 7.6000e-01 - -2.0943e-01 1.4024e+00 7.6000e-01 - -2.3908e-01 1.3977e+00 7.6000e-01 - -2.6862e-01 1.3923e+00 7.6000e-01 - -2.9805e-01 1.3863e+00 7.6000e-01 - -3.2733e-01 1.3797e+00 7.6000e-01 - -3.5648e-01 1.3725e+00 7.6000e-01 - -3.8546e-01 1.3646e+00 7.6000e-01 - -4.1427e-01 1.3561e+00 7.6000e-01 - -4.4289e-01 1.3471e+00 7.6000e-01 - -4.7132e-01 1.3374e+00 7.6000e-01 - -4.9953e-01 1.3271e+00 7.6000e-01 - -5.2752e-01 1.3162e+00 7.6000e-01 - -5.2752e-01 1.3162e+00 7.6000e-01 - -5.4054e-01 1.3487e+00 6.2950e-01 - -5.5356e-01 1.3812e+00 4.9900e-01 - -5.2419e-01 1.3926e+00 4.9900e-01 - -4.9458e-01 1.4034e+00 4.9900e-01 - -4.6475e-01 1.4136e+00 4.9900e-01 - -4.3472e-01 1.4231e+00 4.9900e-01 - -4.0449e-01 1.4320e+00 4.9900e-01 - -3.7407e-01 1.4402e+00 4.9900e-01 - -3.4349e-01 1.4478e+00 4.9900e-01 - -3.1276e-01 1.4548e+00 4.9900e-01 - -2.8188e-01 1.4611e+00 4.9900e-01 - -2.5088e-01 1.4667e+00 4.9900e-01 - -2.1977e-01 1.4717e+00 4.9900e-01 - -1.8856e-01 1.4760e+00 4.9900e-01 - -1.5726e-01 1.4797e+00 4.9900e-01 - -1.2589e-01 1.4827e+00 4.9900e-01 - -9.4469e-02 1.4850e+00 4.9900e-01 - -6.3003e-02 1.4867e+00 4.9900e-01 - -3.1509e-02 1.4877e+00 4.9900e-01 - 9.1114e-17 1.4880e+00 4.9900e-01 - 8.8971e-17 1.4530e+00 6.2950e-01 - 8.6827e-17 1.4180e+00 7.6000e-01 - -1.4180e+00 1.7365e-16 7.6000e-01 - -1.4177e+00 -3.0026e-02 7.6000e-01 - -1.4167e+00 -6.0039e-02 7.6000e-01 - -1.4151e+00 -9.0025e-02 7.6000e-01 - -1.4129e+00 -1.1997e-01 7.6000e-01 - -1.4101e+00 -1.4986e-01 7.6000e-01 - -1.4066e+00 -1.7969e-01 7.6000e-01 - -1.4024e+00 -2.0943e-01 7.6000e-01 - -1.3977e+00 -2.3908e-01 7.6000e-01 - -1.3923e+00 -2.6862e-01 7.6000e-01 - -1.3863e+00 -2.9805e-01 7.6000e-01 - -1.3797e+00 -3.2733e-01 7.6000e-01 - -1.3725e+00 -3.5648e-01 7.6000e-01 - -1.3646e+00 -3.8546e-01 7.6000e-01 - -1.3561e+00 -4.1427e-01 7.6000e-01 - -1.3471e+00 -4.4289e-01 7.6000e-01 - -1.3374e+00 -4.7132e-01 7.6000e-01 - -1.3271e+00 -4.9953e-01 7.6000e-01 - -1.3162e+00 -5.2752e-01 7.6000e-01 - -1.3162e+00 -5.2752e-01 7.6000e-01 - -1.3487e+00 -5.4054e-01 6.2950e-01 - -1.3812e+00 -5.5356e-01 4.9900e-01 - -1.3926e+00 -5.2419e-01 4.9900e-01 - -1.4034e+00 -4.9458e-01 4.9900e-01 - -1.4136e+00 -4.6475e-01 4.9900e-01 - -1.4231e+00 -4.3472e-01 4.9900e-01 - -1.4320e+00 -4.0449e-01 4.9900e-01 - -1.4402e+00 -3.7407e-01 4.9900e-01 - -1.4478e+00 -3.4349e-01 4.9900e-01 - -1.4548e+00 -3.1276e-01 4.9900e-01 - -1.4611e+00 -2.8188e-01 4.9900e-01 - -1.4667e+00 -2.5088e-01 4.9900e-01 - -1.4717e+00 -2.1977e-01 4.9900e-01 - -1.4760e+00 -1.8856e-01 4.9900e-01 - -1.4797e+00 -1.5726e-01 4.9900e-01 - -1.4827e+00 -1.2589e-01 4.9900e-01 - -1.4850e+00 -9.4469e-02 4.9900e-01 - -1.4867e+00 -6.3003e-02 4.9900e-01 - -1.4877e+00 -3.1509e-02 4.9900e-01 - -1.4880e+00 1.8223e-16 4.9900e-01 - -1.4530e+00 1.7794e-16 6.2950e-01 - -1.4180e+00 1.7365e-16 7.6000e-01 - -2.6048e-16 -1.4180e+00 7.6000e-01 - 3.0026e-02 -1.4177e+00 7.6000e-01 - 6.0039e-02 -1.4167e+00 7.6000e-01 - 9.0025e-02 -1.4151e+00 7.6000e-01 - 1.1997e-01 -1.4129e+00 7.6000e-01 - 1.4986e-01 -1.4101e+00 7.6000e-01 - 1.7969e-01 -1.4066e+00 7.6000e-01 - 2.0943e-01 -1.4024e+00 7.6000e-01 - 2.3908e-01 -1.3977e+00 7.6000e-01 - 2.6862e-01 -1.3923e+00 7.6000e-01 - 2.9805e-01 -1.3863e+00 7.6000e-01 - 3.2733e-01 -1.3797e+00 7.6000e-01 - 3.5648e-01 -1.3725e+00 7.6000e-01 - 3.8546e-01 -1.3646e+00 7.6000e-01 - 4.1427e-01 -1.3561e+00 7.6000e-01 - 4.4289e-01 -1.3471e+00 7.6000e-01 - 4.7132e-01 -1.3374e+00 7.6000e-01 - 4.9953e-01 -1.3271e+00 7.6000e-01 - 5.2752e-01 -1.3162e+00 7.6000e-01 - 5.2752e-01 -1.3162e+00 7.6000e-01 - 5.4054e-01 -1.3487e+00 6.2950e-01 - 5.5356e-01 -1.3812e+00 4.9900e-01 - 5.2419e-01 -1.3926e+00 4.9900e-01 - 4.9458e-01 -1.4034e+00 4.9900e-01 - 4.6475e-01 -1.4136e+00 4.9900e-01 - 4.3472e-01 -1.4231e+00 4.9900e-01 - 4.0449e-01 -1.4320e+00 4.9900e-01 - 3.7407e-01 -1.4402e+00 4.9900e-01 - 3.4349e-01 -1.4478e+00 4.9900e-01 - 3.1276e-01 -1.4548e+00 4.9900e-01 - 2.8188e-01 -1.4611e+00 4.9900e-01 - 2.5088e-01 -1.4667e+00 4.9900e-01 - 2.1977e-01 -1.4717e+00 4.9900e-01 - 1.8856e-01 -1.4760e+00 4.9900e-01 - 1.5726e-01 -1.4797e+00 4.9900e-01 - 1.2589e-01 -1.4827e+00 4.9900e-01 - 9.4469e-02 -1.4850e+00 4.9900e-01 - 6.3003e-02 -1.4867e+00 4.9900e-01 - 3.1509e-02 -1.4877e+00 4.9900e-01 - -2.7334e-16 -1.4880e+00 4.9900e-01 - -2.6691e-16 -1.4530e+00 6.2950e-01 - -2.6048e-16 -1.4180e+00 7.6000e-01 + 4 1 42 1.00 + 1.4180e+00 0.0000e+00 7.6000e-01 + 1.4177e+00 3.0026e-02 7.6000e-01 + 1.4167e+00 6.0039e-02 7.6000e-01 + 1.4151e+00 9.0025e-02 7.6000e-01 + 1.4129e+00 1.1997e-01 7.6000e-01 + 1.4101e+00 1.4986e-01 7.6000e-01 + 1.4066e+00 1.7969e-01 7.6000e-01 + 1.4024e+00 2.0943e-01 7.6000e-01 + 1.3977e+00 2.3908e-01 7.6000e-01 + 1.3923e+00 2.6862e-01 7.6000e-01 + 1.3863e+00 2.9805e-01 7.6000e-01 + 1.3797e+00 3.2733e-01 7.6000e-01 + 1.3725e+00 3.5648e-01 7.6000e-01 + 1.3646e+00 3.8546e-01 7.6000e-01 + 1.3561e+00 4.1427e-01 7.6000e-01 + 1.3471e+00 4.4289e-01 7.6000e-01 + 1.3374e+00 4.7132e-01 7.6000e-01 + 1.3271e+00 4.9953e-01 7.6000e-01 + 1.3162e+00 5.2752e-01 7.6000e-01 + 1.3162e+00 5.2752e-01 7.6000e-01 + 1.3487e+00 5.4054e-01 6.2950e-01 + 1.3812e+00 5.5356e-01 4.9900e-01 + 1.3926e+00 5.2419e-01 4.9900e-01 + 1.4034e+00 4.9458e-01 4.9900e-01 + 1.4136e+00 4.6475e-01 4.9900e-01 + 1.4231e+00 4.3472e-01 4.9900e-01 + 1.4320e+00 4.0449e-01 4.9900e-01 + 1.4402e+00 3.7407e-01 4.9900e-01 + 1.4478e+00 3.4349e-01 4.9900e-01 + 1.4548e+00 3.1276e-01 4.9900e-01 + 1.4611e+00 2.8188e-01 4.9900e-01 + 1.4667e+00 2.5088e-01 4.9900e-01 + 1.4717e+00 2.1977e-01 4.9900e-01 + 1.4760e+00 1.8856e-01 4.9900e-01 + 1.4797e+00 1.5726e-01 4.9900e-01 + 1.4827e+00 1.2589e-01 4.9900e-01 + 1.4850e+00 9.4469e-02 4.9900e-01 + 1.4867e+00 6.3003e-02 4.9900e-01 + 1.4877e+00 3.1509e-02 4.9900e-01 + 1.4880e+00 0.0000e+00 4.9900e-01 + 1.4530e+00 0.0000e+00 6.2950e-01 + 1.4180e+00 0.0000e+00 7.6000e-01 + 8.6827e-17 1.4180e+00 7.6000e-01 + -3.0026e-02 1.4177e+00 7.6000e-01 + -6.0039e-02 1.4167e+00 7.6000e-01 + -9.0025e-02 1.4151e+00 7.6000e-01 + -1.1997e-01 1.4129e+00 7.6000e-01 + -1.4986e-01 1.4101e+00 7.6000e-01 + -1.7969e-01 1.4066e+00 7.6000e-01 + -2.0943e-01 1.4024e+00 7.6000e-01 + -2.3908e-01 1.3977e+00 7.6000e-01 + -2.6862e-01 1.3923e+00 7.6000e-01 + -2.9805e-01 1.3863e+00 7.6000e-01 + -3.2733e-01 1.3797e+00 7.6000e-01 + -3.5648e-01 1.3725e+00 7.6000e-01 + -3.8546e-01 1.3646e+00 7.6000e-01 + -4.1427e-01 1.3561e+00 7.6000e-01 + -4.4289e-01 1.3471e+00 7.6000e-01 + -4.7132e-01 1.3374e+00 7.6000e-01 + -4.9953e-01 1.3271e+00 7.6000e-01 + -5.2752e-01 1.3162e+00 7.6000e-01 + -5.2752e-01 1.3162e+00 7.6000e-01 + -5.4054e-01 1.3487e+00 6.2950e-01 + -5.5356e-01 1.3812e+00 4.9900e-01 + -5.2419e-01 1.3926e+00 4.9900e-01 + -4.9458e-01 1.4034e+00 4.9900e-01 + -4.6475e-01 1.4136e+00 4.9900e-01 + -4.3472e-01 1.4231e+00 4.9900e-01 + -4.0449e-01 1.4320e+00 4.9900e-01 + -3.7407e-01 1.4402e+00 4.9900e-01 + -3.4349e-01 1.4478e+00 4.9900e-01 + -3.1276e-01 1.4548e+00 4.9900e-01 + -2.8188e-01 1.4611e+00 4.9900e-01 + -2.5088e-01 1.4667e+00 4.9900e-01 + -2.1977e-01 1.4717e+00 4.9900e-01 + -1.8856e-01 1.4760e+00 4.9900e-01 + -1.5726e-01 1.4797e+00 4.9900e-01 + -1.2589e-01 1.4827e+00 4.9900e-01 + -9.4469e-02 1.4850e+00 4.9900e-01 + -6.3003e-02 1.4867e+00 4.9900e-01 + -3.1509e-02 1.4877e+00 4.9900e-01 + 9.1114e-17 1.4880e+00 4.9900e-01 + 8.8971e-17 1.4530e+00 6.2950e-01 + 8.6827e-17 1.4180e+00 7.6000e-01 + -1.4180e+00 1.7365e-16 7.6000e-01 + -1.4177e+00 -3.0026e-02 7.6000e-01 + -1.4167e+00 -6.0039e-02 7.6000e-01 + -1.4151e+00 -9.0025e-02 7.6000e-01 + -1.4129e+00 -1.1997e-01 7.6000e-01 + -1.4101e+00 -1.4986e-01 7.6000e-01 + -1.4066e+00 -1.7969e-01 7.6000e-01 + -1.4024e+00 -2.0943e-01 7.6000e-01 + -1.3977e+00 -2.3908e-01 7.6000e-01 + -1.3923e+00 -2.6862e-01 7.6000e-01 + -1.3863e+00 -2.9805e-01 7.6000e-01 + -1.3797e+00 -3.2733e-01 7.6000e-01 + -1.3725e+00 -3.5648e-01 7.6000e-01 + -1.3646e+00 -3.8546e-01 7.6000e-01 + -1.3561e+00 -4.1427e-01 7.6000e-01 + -1.3471e+00 -4.4289e-01 7.6000e-01 + -1.3374e+00 -4.7132e-01 7.6000e-01 + -1.3271e+00 -4.9953e-01 7.6000e-01 + -1.3162e+00 -5.2752e-01 7.6000e-01 + -1.3162e+00 -5.2752e-01 7.6000e-01 + -1.3487e+00 -5.4054e-01 6.2950e-01 + -1.3812e+00 -5.5356e-01 4.9900e-01 + -1.3926e+00 -5.2419e-01 4.9900e-01 + -1.4034e+00 -4.9458e-01 4.9900e-01 + -1.4136e+00 -4.6475e-01 4.9900e-01 + -1.4231e+00 -4.3472e-01 4.9900e-01 + -1.4320e+00 -4.0449e-01 4.9900e-01 + -1.4402e+00 -3.7407e-01 4.9900e-01 + -1.4478e+00 -3.4349e-01 4.9900e-01 + -1.4548e+00 -3.1276e-01 4.9900e-01 + -1.4611e+00 -2.8188e-01 4.9900e-01 + -1.4667e+00 -2.5088e-01 4.9900e-01 + -1.4717e+00 -2.1977e-01 4.9900e-01 + -1.4760e+00 -1.8856e-01 4.9900e-01 + -1.4797e+00 -1.5726e-01 4.9900e-01 + -1.4827e+00 -1.2589e-01 4.9900e-01 + -1.4850e+00 -9.4469e-02 4.9900e-01 + -1.4867e+00 -6.3003e-02 4.9900e-01 + -1.4877e+00 -3.1509e-02 4.9900e-01 + -1.4880e+00 1.8223e-16 4.9900e-01 + -1.4530e+00 1.7794e-16 6.2950e-01 + -1.4180e+00 1.7365e-16 7.6000e-01 + -2.6048e-16 -1.4180e+00 7.6000e-01 + 3.0026e-02 -1.4177e+00 7.6000e-01 + 6.0039e-02 -1.4167e+00 7.6000e-01 + 9.0025e-02 -1.4151e+00 7.6000e-01 + 1.1997e-01 -1.4129e+00 7.6000e-01 + 1.4986e-01 -1.4101e+00 7.6000e-01 + 1.7969e-01 -1.4066e+00 7.6000e-01 + 2.0943e-01 -1.4024e+00 7.6000e-01 + 2.3908e-01 -1.3977e+00 7.6000e-01 + 2.6862e-01 -1.3923e+00 7.6000e-01 + 2.9805e-01 -1.3863e+00 7.6000e-01 + 3.2733e-01 -1.3797e+00 7.6000e-01 + 3.5648e-01 -1.3725e+00 7.6000e-01 + 3.8546e-01 -1.3646e+00 7.6000e-01 + 4.1427e-01 -1.3561e+00 7.6000e-01 + 4.4289e-01 -1.3471e+00 7.6000e-01 + 4.7132e-01 -1.3374e+00 7.6000e-01 + 4.9953e-01 -1.3271e+00 7.6000e-01 + 5.2752e-01 -1.3162e+00 7.6000e-01 + 5.2752e-01 -1.3162e+00 7.6000e-01 + 5.4054e-01 -1.3487e+00 6.2950e-01 + 5.5356e-01 -1.3812e+00 4.9900e-01 + 5.2419e-01 -1.3926e+00 4.9900e-01 + 4.9458e-01 -1.4034e+00 4.9900e-01 + 4.6475e-01 -1.4136e+00 4.9900e-01 + 4.3472e-01 -1.4231e+00 4.9900e-01 + 4.0449e-01 -1.4320e+00 4.9900e-01 + 3.7407e-01 -1.4402e+00 4.9900e-01 + 3.4349e-01 -1.4478e+00 4.9900e-01 + 3.1276e-01 -1.4548e+00 4.9900e-01 + 2.8188e-01 -1.4611e+00 4.9900e-01 + 2.5088e-01 -1.4667e+00 4.9900e-01 + 2.1977e-01 -1.4717e+00 4.9900e-01 + 1.8856e-01 -1.4760e+00 4.9900e-01 + 1.5726e-01 -1.4797e+00 4.9900e-01 + 1.2589e-01 -1.4827e+00 4.9900e-01 + 9.4469e-02 -1.4850e+00 4.9900e-01 + 6.3003e-02 -1.4867e+00 4.9900e-01 + 3.1509e-02 -1.4877e+00 4.9900e-01 + -2.7334e-16 -1.4880e+00 4.9900e-01 + -2.6691e-16 -1.4530e+00 6.2950e-01 + -2.6048e-16 -1.4180e+00 7.6000e-01 diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index 96f9f4b1a..5d388d8bf 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -47,6 +47,7 @@ export Tearing # Backward-compat top-level aliases so callers can still reach these # directly; the canonical nested path is `Tearing.{Dispersion,Runner}`. import .Tearing.Dispersion as Dispersion +import .Tearing.CriticalResonantField as CriticalResonantField import .Tearing.Runner as Runner export Dispersion, Runner diff --git a/src/HDF5Schema.jl b/src/HDF5Schema.jl index b8b0333c7..0ad15083e 100644 --- a/src/HDF5Schema.jl +++ b/src/HDF5Schema.jl @@ -243,7 +243,7 @@ const MAIN_H5_ANNOTATIONS = [ "SurfaceGeometries/Plasma/z" => (; long_name="Cartesian z of plasma-surface point cloud", units="m"), "SurfaceGeometries/Wall/x" => (; long_name="Cartesian x of wall point cloud", units="m"), "SurfaceGeometries/Wall/y" => (; long_name="Cartesian y of wall point cloud", units="m"), - "SurfaceGeometries/Wall/z" => (; long_name="Cartesian z of wall point cloud", units="m"), + "SurfaceGeometries/Wall/z" => (; long_name="Cartesian z of wall point cloud", units="m") ] # Euler-Lagrange operator matrices: same wording per letter, Ideal/ and Kinetic/ variants. @@ -256,7 +256,7 @@ const _ELM_IDEAL_LETTERS = [ ("H", "Euler-Lagrange primitive coefficient matrix H"), ("F", "Euler-Lagrange derived coefficient matrix F"), ("K", "Euler-Lagrange derived coefficient matrix K"), - ("G", "Euler-Lagrange derived coefficient matrix G"), + ("G", "Euler-Lagrange derived coefficient matrix G") ] # The kinetic branch overwrites only A, B, C, K, G and adds f0; D, E, H, F are shared # unchanged from the ideal set and are not re-emitted. @@ -266,14 +266,19 @@ const _ELM_KINETIC_LETTERS = [ ("C", "Euler-Lagrange primitive coefficient matrix C"), ("K", "Euler-Lagrange derived coefficient matrix K"), ("G", "Euler-Lagrange derived coefficient matrix G"), - ("f0", "raw kinetic component matrix f0"), + ("f0", "raw kinetic component matrix f0") ] const ELM_H5_ANNOTATIONS = vcat( ["ForceFreeStates/EulerLagrangeMatrices/psi" => (; long_name="normalized poloidal flux ψ_N grid of the operator matrices", scale="psi")], - ["ForceFreeStates/EulerLagrangeMatrices/Ideal/$l" => - (; long_name="ideal " * d, dims=("psi", "mode_row", "mode_col"), attach=(1 => "ForceFreeStates/EulerLagrangeMatrices/psi",)) for (l, d) in _ELM_IDEAL_LETTERS], - ["ForceFreeStates/EulerLagrangeMatrices/Kinetic/$l" => - (; long_name="kinetic-modified " * d, dims=("psi", "mode_row", "mode_col"), attach=(1 => "ForceFreeStates/EulerLagrangeMatrices/psi",)) for (l, d) in _ELM_KINETIC_LETTERS] + [ + "ForceFreeStates/EulerLagrangeMatrices/Ideal/$l" => + (; long_name="ideal " * d, dims=("psi", "mode_row", "mode_col"), attach=(1 => "ForceFreeStates/EulerLagrangeMatrices/psi",)) for (l, d) in _ELM_IDEAL_LETTERS + ], + [ + "ForceFreeStates/EulerLagrangeMatrices/Kinetic/$l" => + (; long_name="kinetic-modified " * d, dims=("psi", "mode_row", "mode_col"), attach=(1 => "ForceFreeStates/EulerLagrangeMatrices/psi",)) for + (l, d) in _ELM_KINETIC_LETTERS + ] ) """ diff --git a/src/InnerLayer/GGJ/Ray.jl b/src/InnerLayer/GGJ/Ray.jl index cb188f4aa..3abbc4f46 100644 --- a/src/InnerLayer/GGJ/Ray.jl +++ b/src/InnerLayer/GGJ/Ray.jl @@ -943,8 +943,6 @@ end npc=8, certify_rtol=1e-3, kwargs...) -> (; Δ, x, Ψ, Ξ, dψdx, rescale, certΔ) -Rotated-ray implementation of the [`solve_inner_profile`](@ref) interface. The -certified `Δ` comes from the optimal-contour solve at θ = arg(Q)/4 (robust for |Q| ≳ 1, where real-axis methods drift); the profiles come from a θ = 0 re-solve on the real axis, valid at physical (RPEC) |Q| since the on-axis pseudo-resonance is a regular point resolved by the BVP refinement. The diff --git a/src/InnerLayer/InnerLayerInterface.jl b/src/InnerLayer/InnerLayerInterface.jl index 52e1b36ea..5ad7dedaf 100644 --- a/src/InnerLayer/InnerLayerInterface.jl +++ b/src/InnerLayer/InnerLayerInterface.jl @@ -87,13 +87,13 @@ tuple with at least: - `Δ` — the same `(Δ_odd, Δ_even)` matching data as [`solve_inner`](@ref) - `x` — real ascending grid in the model's stretched inner coordinate, - `x ≥ 0` with the rational surface at `x = 0` + `x ≥ 0` with the rational surface at `x = 0` - `Ψ`, `Ξ` — `length(x) × 2` profiles, columns (odd, even) parity, in the - model's inner normalization: `Ψ` the normal-field - (reconnected-flux) variable, `Ξ` the displacement + model's inner normalization: `Ψ` the normal-field + (reconnected-flux) variable, `Ξ` the displacement - `dψdx` — conversion to poloidal-flux distance, `δψ = dψdx · x` - `rescale` — amplitude factor converting the inner-normalized profiles to - the outer δψ-normalized convention (companion of the Δ rescale) + the outer δψ-normalized convention (companion of the Δ rescale) Concrete models may return additional diagnostic fields (e.g. a solve-quality certificate). Solver-knob keywords are model-specific. diff --git a/src/InnerLayer/SLAYER/LayerInputs.jl b/src/InnerLayer/SLAYER/LayerInputs.jl index 96177903d..b25faa154 100644 --- a/src/InnerLayer/SLAYER/LayerInputs.jl +++ b/src/InnerLayer/SLAYER/LayerInputs.jl @@ -87,11 +87,15 @@ profiles, without an intermediate file round-trip. physical field, so passing it as a scalar is almost always wrong. - `mu_i` -- ion mass in proton-mass units (default `2.0` for D). + - `zeff` -- effective charge (default `1.0`). + - `chi_perp` -- perpendicular heat diffusivity [m²/s]. Scalar or a callable of `psi` (default `1.0`). + - `chi_tor` -- toroidal heat diffusivity [m²/s]. Scalar or a callable of `psi` (default `1.0`). + - `dr_val` -- resistive interchange index `D_R = E + F + H²` (Glasser-Greene-Johnson 1975) feeding the critical-Δ formulas (`:lar`, `:rfitzp`, `:toroidal`). When `nothing` (default), Julia @@ -106,6 +110,7 @@ profiles, without an intermediate file round-trip. NOT the Mercier index `D_I = E + F + H − 1/4`. The two differ by `(H − 1/2)²`, which is non-trivial on shaped equilibria (~factor 3 on DIII-D); this code uses the physically correct `D_R`. + - `dgeo_val` -- Connor 2015 (PPCF 57 065001) Eq. 59 geometric factor used by `dc_type=:toroidal`. When `nothing` (default), an error is raised if `dc_type=:toroidal` is also requested — the auto-derived @@ -113,15 +118,19 @@ profiles, without an intermediate file round-trip. doesn't currently expose. Pass a scalar / vector / callable to use a prescribed value. (For `dc_type=:rfitzp` and `:lar`, dgeo_val is not consulted.) + - `dc_type` -- `:none` (default), `:lar`, `:rfitzp`, or `:toroidal`. + - `theta` -- poloidal angle at which to measure minor radius (default `0.0`, outboard midplane). + - `resistivity_model` -- `SauterNeoModel()` (default), `RedlNeoModel()`, `SpitzerModel()`, or `SpitzerHarmModel()` (legacy Fitzpatrick σ_∥). Sets the η entering τ_R = μ₀r_s²/η. With a neoclassical model, `f_trap` and ν*_e are taken from the surface's `ResistGeometry` if populated (via `ForceFreeStates.resist_eval_all!`), otherwise fall back to the ε-only Lin-Liu-Miller form and `rs/R_0` aspect ratio. + - `lnLambda_form` -- Coulomb-log form passed through to `slayer_parameters` (default `:nrl`; `:wesson` + `SpitzerHarmModel()` reproduces legacy SLAYER exactly). diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index 5dbf9163f..8e3c17fb1 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -20,32 +20,32 @@ Fitzpatrick two-fluid drift-MHD SLAYER inner-layer model (Fitzpatrick de-normalization. The parametrization uses `P_perp`, `P_tor`, and `D_norm` (not the older `pr`/`pe`/`ds` set). -| field | meaning | -|:---------- |:----------------------------------------------------------------- | -| `ising` | Singular-surface index (traceability only) | -| `m`, `n` | Poloidal / toroidal mode numbers at this surface | -| `tau` | T_i / T_e | -| `lu` | Lundquist number S = τ_R / τ_H | -| `c_beta` | Compressibility √(β_local / (1 + β_local)) | -| `D_norm` | (d_β/r_s) · S^(1/3) · √(τ/(1+τ)) (Fitzpatrick normalized scale) | -| `P_perp` | Perpendicular Prandtl number τ_R / τ_⊥ | -| `P_tor` | Toroidal-direction Prandtl number τ_R / τ_‖tor | -| `Q_e` | Normalized electron diamagnetic: −tauk · ω_*e | -| `Q_i` | Normalized ion diamagnetic: −tauk · ω_*i | -| `iota_e` | Q_e / (Q_e − Q_i) | -| `tauk` | Q-conversion factor S^(1/3) · τ_H [s] — multiplies ω to get Q | -| `tau_r` | Resistive diffusion time [s] | -| `delta_n` | Δ-normalization factor S^(1/3) / r_s [m⁻¹] | -| `rs` | Minor radius at this surface [m] | -| `R0` | Major radius [m] | -| `bt` | Toroidal field [T] | -| `sval_r` | r-based magnetic shear r_s · (dq/dr) / q (Fitzpatrick convention) | +| field | meaning | +|:---------- |:--------------------------------------------------------------------------------------------------------------- | +| `ising` | Singular-surface index (traceability only) | +| `m`, `n` | Poloidal / toroidal mode numbers at this surface | +| `tau` | T_i / T_e | +| `lu` | Lundquist number S = τ_R / τ_H | +| `c_beta` | Compressibility √(β_local / (1 + β_local)) | +| `D_norm` | (d_β/r_s) · S^(1/3) · √(τ/(1+τ)) (Fitzpatrick normalized scale) | +| `P_perp` | Perpendicular Prandtl number τ_R / τ_⊥ | +| `P_tor` | Toroidal-direction Prandtl number τ_R / τ_‖tor | +| `Q_e` | Normalized electron diamagnetic: −tauk · ω_*e | +| `Q_i` | Normalized ion diamagnetic: −tauk · ω_*i | +| `iota_e` | Q_e / (Q_e − Q_i) | +| `tauk` | Q-conversion factor S^(1/3) · τ_H [s] — multiplies ω to get Q | +| `tau_r` | Resistive diffusion time [s] | +| `delta_n` | Δ-normalization factor S^(1/3) / r_s [m⁻¹] | +| `rs` | Minor radius at this surface [m] | +| `R0` | Major radius [m] | +| `bt` | Toroidal field [T] | +| `sval_r` | r-based magnetic shear r_s · (dq/dr) / q (Fitzpatrick convention) | | `dr_val` | Resistive interchange D_R = E + F + H² (critical-Δ input; auto-derived from GGJ coefficients unless overridden) | -| `dgeo_val` | Connor-Hastie-Helander 2015 Eq. 59 geometric factor (0 unless supplied) | -| `eta` | Parallel resistivity entering τ_R = μ₀r_s²/η [Ω·m] | -| `d_beta` | Beta-weighted ion length scale c_β · d_i [m] | -| `dc_tmp` | Critical-Δ offset from chi_parallel matching | -| `dc_type` | Selector for `dc_tmp` formula | +| `dgeo_val` | Connor-Hastie-Helander 2015 Eq. 59 geometric factor (0 unless supplied) | +| `eta` | Parallel resistivity entering τ_R = μ₀r_s²/η [Ω·m] | +| `d_beta` | Beta-weighted ion length scale c_β · d_i [m] | +| `dc_tmp` | Critical-Δ offset from chi_parallel matching | +| `dc_type` | Selector for `dc_tmp` formula | The complex normalized growth rate `Q = ω + iγ` is **not** stored here; it is passed as a separate argument to `solve_inner`. diff --git a/src/KineticForces/BounceAveraging.jl b/src/KineticForces/BounceAveraging.jl index 63a59aedc..5dddae01d 100644 --- a/src/KineticForces/BounceAveraging.jl +++ b/src/KineticForces/BounceAveraging.jl @@ -31,7 +31,7 @@ struct BounceData # For matrix path (nothing if scalar-only): packed layout (nlmda, nqty_matrix(mpert)). # Consumer fills into fbnce_data[:, 3:end] by direct copy. See `nqty_matrix` for # the 3-Hermitian-triangle + 3-full-block packing (Logan 2015 Eqs 7.30–7.35). - wmats_vs_lambda::Union{Nothing, Matrix{ComplexF64}} + wmats_vs_lambda::Union{Nothing,Matrix{ComplexF64}} end @@ -44,13 +44,19 @@ end # the full mpert² for the three non-Hermitian blocks. Block packing order: # A-tri, D-tri, H-tri, B-full, C-full, E-full. -"""Number of packed complex entries per λ for the 6 kinetic matrices.""" +""" +Number of packed complex entries per λ for the 6 kinetic matrices. +""" @inline nqty_matrix(mpert::Int) = 3 * (mpert * (mpert + 1)) ÷ 2 + 3 * mpert^2 -"""Upper-triangle index (1 ≤ i ≤ j ≤ mpert) within a triangular block.""" +""" +Upper-triangle index (1 ≤ i ≤ j ≤ mpert) within a triangular block. +""" @inline _tri_idx(i::Int, j::Int) = (j * (j - 1)) ÷ 2 + i -"""Full-block index (column-major) within a non-Hermitian block.""" +""" +Full-block index (column-major) within a non-Hermitian block. +""" @inline _full_idx(i::Int, j::Int, mpert::Int) = (j - 1) * mpert + i @@ -65,14 +71,16 @@ Generate a grid with power-law concentration near endpoints. Port of Fortran `powspace_sub` from equil/grid.f90. # Arguments -- `xmin, xmax`: Grid bounds -- `pow::Int`: Power of grid concentration (higher = more refined near edges) -- `num::Int`: Number of grid points -- `endpoints::String`: Where to concentrate: "lower", "upper", or "both" + + - `xmin, xmax`: Grid bounds + - `pow::Int`: Power of grid concentration (higher = more refined near edges) + - `num::Int`: Number of grid points + - `endpoints::String`: Where to concentrate: "lower", "upper", or "both" # Returns -- `points::Vector{Float64}`: Grid point locations -- `weights::Vector{Float64}`: Derivatives dx/dnorm (integration weights) + + - `points::Vector{Float64}`: Grid point locations + - `weights::Vector{Float64}`: Derivatives dx/dnorm (integration weights) """ function powspace(xmin::Float64, xmax::Float64, pow::Int, num::Int, endpoints::String) if xmax <= xmin @@ -81,11 +89,11 @@ function powspace(xmin::Float64, xmax::Float64, pow::Int, num::Int, endpoints::S # Linear base grid in [-1,1], [0,1], or [-1,0] x = if endpoints == "lower" - collect(range(-1.0, 0.0, length=num)) + collect(range(-1.0, 0.0; length=num)) elseif endpoints == "upper" - collect(range(0.0, 1.0, length=num)) + collect(range(0.0, 1.0; length=num)) elseif endpoints == "both" - collect(range(-1.0, 1.0, length=num)) + collect(range(-1.0, 1.0; length=num)) else error("powspace: invalid endpoints '$endpoints' — use lower, upper, or both") end @@ -160,31 +168,33 @@ needed by the pitch-angle quadrature. Ports Fortran torque.F90 lines 530-816 (GAR branch). # Arguments -- `psi`: Normalized poloidal flux -- `n`: Toroidal mode number -- `l`: Bounce harmonic number -- `q`: Safety factor at this ψ -- `bo`: On-axis toroidal field [T] -- `bmax, bmin`: Max/min of B(θ) at this ψ -- `theta_bmax`: θ location of Bmax (nodal knot; the passing-transit start) -- `tspl`: Periodic poloidal interpolant: tspl(θ) → [B, dB/dψ, dB/dθ, J, dJ/dψ] -- `B_extrap`: Endpoint-fit (non-periodic) cubic of B(θ) used for v_par and the - bounce-point roots (the Fortran `vspl` equivalent) -- `mfac`: Poloidal mode numbers [mlow:mhigh] -- `chi1`: 2π·ψ₀ flux normalization -- `ro`: Major radius [m] -- `dbob_m_f`: δB/B Fourier modes at this ψ (ComplexF64 vector, length mpert) -- `divx_m_f`: ∇·ξ⊥ Fourier modes at this ψ (ComplexF64 vector, length mpert) -- `divxfac, wdfac`: Scaling factors -- `mass`: Particle mass [kg] -- `chrg`: Particle charge [C] -- `T_s`: Species temperature at this ψ [J] -- `method`: Method string (first char: f/t/p determines λ range) + + - `psi`: Normalized poloidal flux + - `n`: Toroidal mode number + - `l`: Bounce harmonic number + - `q`: Safety factor at this ψ + - `bo`: On-axis toroidal field [T] + - `bmax, bmin`: Max/min of B(θ) at this ψ + - `theta_bmax`: θ location of Bmax (nodal knot; the passing-transit start) + - `tspl`: Periodic poloidal interpolant: tspl(θ) → [B, dB/dψ, dB/dθ, J, dJ/dψ] + - `B_extrap`: Endpoint-fit (non-periodic) cubic of B(θ) used for v_par and the + bounce-point roots (the Fortran `vspl` equivalent) + - `mfac`: Poloidal mode numbers [mlow:mhigh] + - `chi1`: 2π·ψ₀ flux normalization + - `ro`: Major radius [m] + - `dbob_m_f`: δB/B Fourier modes at this ψ (ComplexF64 vector, length mpert) + - `divx_m_f`: ∇·ξ⊥ Fourier modes at this ψ (ComplexF64 vector, length mpert) + - `divxfac, wdfac`: Scaling factors + - `mass`: Particle mass [kg] + - `chrg`: Particle charge [C] + - `T_s`: Species temperature at this ψ [J] + - `method`: Method string (first char: f/t/p determines λ range) # Keyword Arguments -- `nlmda`: Number of pitch angle grid points (default 128, matching Fortran pentrc nlmda) -- `ntheta`: Number of poloidal grid points per bounce (default 128) -- `smat, tmat, xmat, ymat, zmat`: Geometric matrices (mpert×mpert) for kinetic matrix path + + - `nlmda`: Number of pitch angle grid points (default 128, matching Fortran pentrc nlmda) + - `ntheta`: Number of poloidal grid points per bounce (default 128) + - `smat, tmat, xmat, ymat, zmat`: Geometric matrices (mpert×mpert) for kinetic matrix path """ function compute_bounce_data( psi::Float64, n::Int, l::Int, q::Float64, @@ -262,7 +272,7 @@ function compute_bounce_data( end return BounceData(nlmda, lambda, dlambda, sigma_arr, - wb_arr, wd_arr, dJdJ_arr, wmats_arr) + wb_arr, wd_arr, dJdJ_arr, wmats_arr) end @@ -275,23 +285,24 @@ Per-surface scratch for the bounce-averaging inner loops, allocated once in partially are `fill!`-reset per λ. ## Fields -- `g_wb::Vector{Float64}`: length `ntheta` — bounce-action integrand samples -- `g_wd::Vector{Float64}`: length `ntheta` — drift integrand samples -- `cum_wb_arr::Vector{Float64}`: length `ntheta` — cumulative bounce-action integral -- `jvtheta::Vector{ComplexF64}`: length `ntheta` — action integrand -- `bj_samples::Vector{ComplexF64}`: length `ntheta` — action bounce-integral samples -- `wsamp::Vector{ComplexF64}`: length `ntheta` — per-mode W bounce-integral samples -- `wmu_mt::Matrix{ComplexF64}`: `mpert × ntheta` — W_μ per θ -- `wen_mt::Matrix{ComplexF64}`: `mpert × ntheta` — W_E per θ -- `expm::Vector{ComplexF64}`: length `mpert` — Fourier basis at a θ -- `pl::Vector{ComplexF64}`: length `ntheta` — bounce phase factor -- `wmu_ba::Vector{ComplexF64}`: length `mpert` — bounce-averaged W_μ -- `wen_ba::Vector{ComplexF64}`: length `mpert` — bounce-averaged W_E -- `wmats_lmda::Vector{ComplexF64}`: length `nqty_matrix(mpert)` — packed W outer products -- `tspl_f::Vector{Float64}`: length 5 — in-place tspl(θ) evaluation -- `int_w::Vector{Float64}`, `cumint_W::Matrix{Float64}`: precomputed exact-cubic - quadrature weights on the fixed unit θ-grid (`∫ = int_w·y`, `cumulative = cumint_W·y`); - shared read-only across surfaces, see `_quadrature_weights` + + - `g_wb::Vector{Float64}`: length `ntheta` — bounce-action integrand samples + - `g_wd::Vector{Float64}`: length `ntheta` — drift integrand samples + - `cum_wb_arr::Vector{Float64}`: length `ntheta` — cumulative bounce-action integral + - `jvtheta::Vector{ComplexF64}`: length `ntheta` — action integrand + - `bj_samples::Vector{ComplexF64}`: length `ntheta` — action bounce-integral samples + - `wsamp::Vector{ComplexF64}`: length `ntheta` — per-mode W bounce-integral samples + - `wmu_mt::Matrix{ComplexF64}`: `mpert × ntheta` — W_μ per θ + - `wen_mt::Matrix{ComplexF64}`: `mpert × ntheta` — W_E per θ + - `expm::Vector{ComplexF64}`: length `mpert` — Fourier basis at a θ + - `pl::Vector{ComplexF64}`: length `ntheta` — bounce phase factor + - `wmu_ba::Vector{ComplexF64}`: length `mpert` — bounce-averaged W_μ + - `wen_ba::Vector{ComplexF64}`: length `mpert` — bounce-averaged W_E + - `wmats_lmda::Vector{ComplexF64}`: length `nqty_matrix(mpert)` — packed W outer products + - `tspl_f::Vector{Float64}`: length 5 — in-place tspl(θ) evaluation + - `int_w::Vector{Float64}`, `cumint_W::Matrix{Float64}`: precomputed exact-cubic + quadrature weights on the fixed unit θ-grid (`∫ = int_w·y`, `cumulative = cumint_W·y`); + shared read-only across surfaces, see `_quadrature_weights` """ struct BounceScratch g_wb::Vector{Float64} @@ -330,7 +341,7 @@ function BounceScratch(ntheta::Int, mpert::Int) Vector{ComplexF64}(undef, nqty_matrix(mpert)), Vector{Float64}(undef, 5), int_w, - cumint_W, + cumint_W ) end @@ -351,7 +362,7 @@ fitting and integrating each sample vector directly, but reducing the per-λ hot function _quadrature_weights(ntheta::Int) lock(_QUAD_WEIGHTS_LOCK) do get!(_QUAD_WEIGHTS, ntheta) do - xs = collect(range(0.0, 1.0, length=ntheta)) + xs = collect(range(0.0, 1.0; length=ntheta)) int_w = zeros(Float64, ntheta) cumint_W = zeros(Float64, ntheta, ntheta) ej = zeros(Float64, ntheta) @@ -384,12 +395,12 @@ function _build_lambda_grid(method_char::Char, lmdatpb::Float64, lmdamax::Float6 if method_char == 't' # Trapped only: λ ∈ (lmdatpb, lmdamax), exclude endpoints pts_inc, wts_inc = powspace(lmdatpb, lmdamax, 1, 2 + nlmda, "both") - return pts_inc[2:end-1], wts_inc[2:end-1] + return pts_inc[2:(end-1)], wts_inc[2:(end-1)] elseif method_char == 'p' # Passing only: λ ∈ (lmdamin, lmdatpb), exclude endpoints pts_inc, wts_inc = powspace(lmdamin, lmdatpb, 1, 2 + nlmda, "both") - return pts_inc[2:end-1], wts_inc[2:end-1] + return pts_inc[2:(end-1)], wts_inc[2:(end-1)] else # 'f' = full if lmdatpb ≈ lmdamax @@ -401,8 +412,8 @@ function _build_lambda_grid(method_char::Char, lmdatpb::Float64, lmdamax::Float6 pts_p, wts_p = powspace(lmdamin, lmdatpb, 2, 2 + nhalf_p, "upper") pts_t, wts_t = powspace(lmdatpb, lmdamax, 2, 2 + nhalf_t, "lower") # Exclude boundary points - lambda = vcat(pts_p[2:end-1], pts_t[2:end-1]) - dlambda = vcat(wts_p[2:end-1], wts_t[2:end-1]) + lambda = vcat(pts_p[2:(end-1)], pts_t[2:(end-1)]) + dlambda = vcat(wts_p[2:(end-1)], wts_t[2:(end-1)]) return lambda, dlambda end end @@ -538,7 +549,7 @@ function _bounce_integrate( tspl_f = scr.tspl_f expm = scr.expm - for i in 2:ntheta-1 # Edge weights are 0 from powspace + for i in 2:(ntheta-1) # Edge weights are 0 from powspace θ = tdt_pts[i] dt = tdt_wts[i] θmod = mod(θ, 1.0) @@ -595,8 +606,8 @@ function _bounce_integrate( # Action integrand phase = cis(-twopi * n * q * (θ - theta0)) jvtheta[i] = dt * jac * B_val * - (divx * sqrt_vpar + dbob * (1.0 - 1.5 * lmda * B_val / bo) / sqrt_vpar) * - phase + (divx * sqrt_vpar + dbob * (1.0 - 1.5 * lmda * B_val / bo) / sqrt_vpar) * + phase # W vectors for matrix path if do_matrices @@ -611,9 +622,9 @@ function _bounce_integrate( # Smooth backfill for points zeroed before a restart. Exact equality with # 0.0 is safe: the restart branch set these entries with fill!(…, 0.0). if i >= 3 && g_wb[i-1] == 0.0 - fill!(view(g_wb, 3:i-1), g_wb[i]) - fill!(view(g_wd, 3:i-1), g_wd[i]) - fill!(view(jvtheta, 2:i-1), jvtheta[i]) + fill!(view(g_wb, 3:(i-1)), g_wb[i]) + fill!(view(g_wd, 3:(i-1)), g_wd[i]) + fill!(view(jvtheta, 2:(i-1)), jvtheta[i]) end end @@ -698,32 +709,32 @@ function _bounce_integrate( # A (Hermitian): upper triangle of W_Z†W_Z, rank-1 → conj(wz[i])·wz[j]. off = 0 @inbounds for j in 1:mpert, i in 1:j - wmats_lmda[off + _tri_idx(i, j)] = conj(wz[i]) * wz[j] * scale + wmats_lmda[off+_tri_idx(i, j)] = conj(wz[i]) * wz[j] * scale end off += Mu # D (Hermitian): upper triangle of W_X†W_X. @inbounds for j in 1:mpert, i in 1:j - wmats_lmda[off + _tri_idx(i, j)] = conj(wx[i]) * wx[j] * scale + wmats_lmda[off+_tri_idx(i, j)] = conj(wx[i]) * wx[j] * scale end off += Mu # H (Hermitian): upper triangle of W_Y†W_Y. @inbounds for j in 1:mpert, i in 1:j - wmats_lmda[off + _tri_idx(i, j)] = conj(wy[i]) * wy[j] * scale + wmats_lmda[off+_tri_idx(i, j)] = conj(wy[i]) * wy[j] * scale end off += Mu # B (full): W_Z†W_X. @inbounds for j in 1:mpert, i in 1:mpert - wmats_lmda[off + _full_idx(i, j, mpert)] = conj(wz[i]) * wx[j] * scale + wmats_lmda[off+_full_idx(i, j, mpert)] = conj(wz[i]) * wx[j] * scale end off += mpert^2 # C (full): W_Z†W_Y. @inbounds for j in 1:mpert, i in 1:mpert - wmats_lmda[off + _full_idx(i, j, mpert)] = conj(wz[i]) * wy[j] * scale + wmats_lmda[off+_full_idx(i, j, mpert)] = conj(wz[i]) * wy[j] * scale end off += mpert^2 # E (full): W_X†W_Y. @inbounds for j in 1:mpert, i in 1:mpert - wmats_lmda[off + _full_idx(i, j, mpert)] = conj(wx[i]) * wy[j] * scale + wmats_lmda[off+_full_idx(i, j, mpert)] = conj(wx[i]) * wy[j] * scale end end diff --git a/src/KineticForces/EnergyIntegration.jl b/src/KineticForces/EnergyIntegration.jl index 36cc1ecef..261c403ce 100644 --- a/src/KineticForces/EnergyIntegration.jl +++ b/src/KineticForces/EnergyIntegration.jl @@ -40,10 +40,10 @@ end Collision frequency ν(x) at normalized energy x = E/T. -- `"zero"`: collisionless (ν = 0) -- `"small"`: 1e-5 * we -- `"krook"`: unmodified Krook operator -- `"harmonic"`: (1 + (l/2)²) * krook * x^(-3/2) + - `"zero"`: collisionless (ν = 0) + - `"small"`: 1e-5 * we + - `"krook"`: unmodified Krook operator + - `"harmonic"`: (1 + (l/2)²) * krook * x^(-3/2) """ @inline function _energy_collision_frequency(x::Float64, p::EnergyParams)::Float64 nux = if p.nutype == "zero" @@ -188,7 +188,7 @@ Writing `h(x) = N(x)·exp(-x)`, the limit is `Ω′ = leff·wb/(2√x) + n·wd`, `Ω″ = −leff·wb/(4·x^{3/2})`. """ @inline function _real_pole_regular_part(xr::Float64, p::EnergyParams, leff::Float64, - wb::Float64, n::Int, wd::Float64)::ComplexF64 + wb::Float64, n::Int, wd::Float64)::ComplexF64 sx = sqrt(xr) op = leff * wb / (2.0 * sx) + n * wd # Ω′(x_res) opp = -leff * wb / (4.0 * xr * sx) # Ω″(x_res) @@ -211,8 +211,8 @@ add-back below), and its on-axis `0/0` window by the analytic regular-part limit # Real x-space resonant integrand with pole subtractions # Explicit function keeps memory allocation out of the QuadGK inner loop. @inline function _resonant_integrand(x::Float64, p::EnergyParams, - residues::Vector{ComplexF64}, x_poles::Vector{ComplexF64}, npole::Int, - leff::Float64, wb::Float64, n::Int, wd::Float64)::ComplexF64 + residues::Vector{ComplexF64}, x_poles::Vector{ComplexF64}, npole::Int, + leff::Float64, wb::Float64, n::Int, wd::Float64)::ComplexF64 val = _energy_integrand_real(x, p) @inbounds for k in 1:npole val -= residues[k] / (x - x_poles[k]) @@ -234,8 +234,8 @@ add-back below), and its on-axis `0/0` window by the analytic regular-part limit end function _integrate_energy_resonant(p::EnergyParams, leff::Float64, wb::Float64, - n::Int, we::Float64, wd::Float64, - atol::Float64, rtol::Float64, segbuf=nothing)::ComplexF64 + n::Int, we::Float64, wd::Float64, + atol::Float64, rtol::Float64, segbuf=nothing)::ComplexF64 x_res_list = find_resonance_energies(leff, wb, n, we, wd) # ≤ 2 roots of a quadratic in √x x_poles = ComplexF64[] residues = ComplexF64[] @@ -308,17 +308,18 @@ Distribution function types (`f0type`): `"maxwellian"`, `"jkp"`, `"cgl"`. poles are now handled analytically rather than by contour deformation. # Returns -- `ComplexF64`: energy integral value + + - `ComplexF64`: energy integral value """ function integrate_energy(wn::Float64, wt::Float64, we::Float64, wd::Float64, - wb::Float64, nuk::Float64, ell::Int, leff::Float64, - n::Int, psi::Float64, lambda::Float64, method::String; - nutype::String="harmonic", f0type::String="maxwellian", - nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false, - atol::Float64=1e-7, rtol::Float64=1e-5, segbuf=nothing)::ComplexF64 + wb::Float64, nuk::Float64, ell::Int, leff::Float64, + n::Int, psi::Float64, lambda::Float64, method::String; + nutype::String="harmonic", f0type::String="maxwellian", + nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false, + atol::Float64=1e-7, rtol::Float64=1e-5, segbuf=nothing)::ComplexF64 p = EnergyParams(wn, wt, we, wd, wb, nuk, leff, n, - nutype, f0type, nufac, ximag, qt) + nutype, f0type, nufac, ximag, qt) # CGL has no resonance denominator and no pole — integrate the physical # x-space integrand directly over the half line (QuadGK maps [0,∞) itself). @@ -343,19 +344,20 @@ N(x)·exp(-x)/denom(x) at specified x = E/T values. Returns the integrand value integrand shape and verifying kinetic resonance resolution. # Example + ```julia -x = 10 .^ range(-2, stop=2, length=500) +x = 10 .^ range(-2; stop=2, length=500) f = KineticForces.evaluate_energy_integrand(x; wn=1e3, wt=2e3, we=5e4, - wd=1e2, wb=3e4, nuk=1e3, leff=1.0, n=1) -plot(x, real.(f); xscale=:log10, xlabel="x = E/T", ylabel="Re(integrand)") + wd=1e2, wb=3e4, nuk=1e3, leff=1.0, n=1) +plot(x, real.(f); xscale=:log10, xlabel=\"x = E/T\", ylabel=\"Re(integrand)\") ``` """ function evaluate_energy_integrand(x_grid::AbstractVector{Float64}; - wn::Float64, wt::Float64, we::Float64, - wd::Float64, wb::Float64, nuk::Float64, - leff::Float64, n::Int, - nutype::String="harmonic", f0type::String="maxwellian", - nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false) + wn::Float64, wt::Float64, we::Float64, + wd::Float64, wb::Float64, nuk::Float64, + leff::Float64, n::Int, + nutype::String="harmonic", f0type::String="maxwellian", + nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false) p = EnergyParams(wn, wt, we, wd, wb, nuk, leff, n, nutype, f0type, nufac, ximag, qt) return [energy_integrand_scalar(x, p) for x in x_grid] end diff --git a/src/KineticForces/KineticForces.jl b/src/KineticForces/KineticForces.jl index 1ac79ad22..748d19e64 100644 --- a/src/KineticForces/KineticForces.jl +++ b/src/KineticForces/KineticForces.jl @@ -59,11 +59,11 @@ include("CalculatedKineticMatrices.jl") # ============================================================================ const mp = 1.672_614e-27 # proton mass (kg) const me = 9.109_1e-31 # electron mass (kg) -const e = 1.602_191_7e-19 # elementary charge (C) +const e = 1.602_191_7e-19 # elementary charge (C) const eV = e # joules per electron-volt const twopi = 2π -const μ₀ = 4e-7 * π +const μ₀ = 4e-7 * π const rad2deg = 180 / π const deg2rad = π / 180 const iunit = 1im # equivalent to Fortran's (0,1) diff --git a/src/KineticForces/KineticForcesStructs.jl b/src/KineticForces/KineticForcesStructs.jl index e4eca2581..5d2505b69 100644 --- a/src/KineticForces/KineticForcesStructs.jl +++ b/src/KineticForces/KineticForcesStructs.jl @@ -3,12 +3,13 @@ Single source of truth for the NTV calculation methods. Each entry is a NamedTuple `(name, flag, kind, doc)`: -- `name` — short method identifier used as the HDF5 group key and in `intr.method` -- `flag` — the `KineticForcesControl` field symbol that enables the method -- `kind` — dispatch routing tag consumed by `method_kind` / `Torque.jl` - (`:gar` for the GAR/matrix family, `:fcgl`/`:rlar`/`:clar` for the - three special-cased methods) -- `doc` — one-line description printed in verbose output + + - `name` — short method identifier used as the HDF5 group key and in `intr.method` + - `flag` — the `KineticForcesControl` field symbol that enables the method + - `kind` — dispatch routing tag consumed by `method_kind` / `Torque.jl` + (`:gar` for the GAR/matrix family, `:fcgl`/`:rlar`/`:clar` for the + three special-cased methods) + - `doc` — one-line description printed in verbose output The method names/docs and the `Compute.jl` enable list are all derived from this tuple, and `Torque.jl` routes on `kind`, so the methods are enumerated in one place. @@ -77,8 +78,9 @@ User-facing control parameters from the TOML `[KineticForces]` section. Configures which NTV methods to run, species parameters, tolerances, and output options. Constructed via keyword arguments or from a TOML dict: + ```julia -ctrl = KineticForcesControl(; (Symbol(k) => v for (k, v) in inputs["KineticForces"])...) +ctrl = KineticForcesControl(; (Symbol(k) => v for (k, v) in inputs[\"KineticForces\"])...) ``` Immutable: vary a field by building a new control rather than assigning to one (the @@ -202,12 +204,13 @@ Internal working state for KineticForces calculations. Holds equilibrium-derived quantities, profile interpolants, and integration results. Fields replacing former module-level globals: -- `ro`, `bo`, `chi1`: Equilibrium geometry parameters -- `mthsurf`, `mfac`: Poloidal grid info -- `dbob_m`, `divx_m`: Perturbation mode interpolants -- `sing_psis`: Rational-surface ψ locations (sorted, from the stability analysis), used as - panel boundaries for the outer ψ torque quadrature so the resonant peaks fall on - Gauss-Kronrod interval endpoints instead of driving deep adaptive bisection + + - `ro`, `bo`, `chi1`: Equilibrium geometry parameters + - `mthsurf`, `mfac`: Poloidal grid info + - `dbob_m`, `divx_m`: Perturbation mode interpolants + - `sing_psis`: Rational-surface ψ locations (sorted, from the stability analysis), used as + panel boundaries for the outer ψ torque quadrature so the resonant peaks fall on + Gauss-Kronrod interval endpoints instead of driving deep adaptive bisection Equilibrium and kinetic profile data are read directly from the `PlasmaEquilibrium` (`equil.profiles`, `equil.geometry`) and the @@ -297,17 +300,17 @@ function KineticForcesInternal(equil; verbose::Bool=false) # Axis toroidal field F(0)/ro that normalizes λ = μ·bo/E; F_spline stores 2πF. bo_axis = abs(equil.profiles.F_spline(0.0)) / (2π * equil.ro) KineticForcesInternal(; - ro = equil.ro, - bo = bo_axis, - chi1 = 2π * equil.psio, + ro=equil.ro, + bo=bo_axis, + chi1=2π * equil.psio, mthsurf, - tpsi_xs = collect(range(0.0, 1.0, length=nth)), - tpsi_B = Vector{Float64}(undef, nth), - tpsi_dBdpsi = Vector{Float64}(undef, nth), - tpsi_dBdtheta = Vector{Float64}(undef, nth), - tpsi_jac = Vector{Float64}(undef, nth), - tpsi_djdpsi = Vector{Float64}(undef, nth), - verbose, + tpsi_xs=collect(range(0.0, 1.0; length=nth)), + tpsi_B=Vector{Float64}(undef, nth), + tpsi_dBdpsi=Vector{Float64}(undef, nth), + tpsi_dBdtheta=Vector{Float64}(undef, nth), + tpsi_jac=Vector{Float64}(undef, nth), + tpsi_djdpsi=Vector{Float64}(undef, nth), + verbose ) end @@ -317,15 +320,17 @@ end Populate perturbation data from PerturbedEquilibriumState into KineticForcesInternal. Builds three interpolant sets from PE Clebsch displacements: -1. `xs_m` — [ξ^ψ, ∂ξ^ψ/∂ψ, ξ^α] CubicSeriesInterpolants over ψ -2. `dbob_m` — δB/B Fourier modes via JBB deweighting (Fortran set_peq) -3. `divx_m` — ∇·ξ⊥ Fourier modes via JBB deweighting + + 1. `xs_m` — [ξ^ψ, ∂ξ^ψ/∂ψ, ξ^α] CubicSeriesInterpolants over ψ + 2. `dbob_m` — δB/B Fourier modes via JBB deweighting (Fortran set_peq) + 3. `divx_m` — ∇·ξ⊥ Fourier modes via JBB deweighting The JBB deweighting algorithm (Fortran pentrc/inputs.f90:828-868): -1. Apply geometric matrices S,T,X,Y,Z in m-space -2. Inverse DFT to θ-space -3. Divide by J·B² at each θ -4. Forward DFT back to m-space + + 1. Apply geometric matrices S,T,X,Y,Z in m-space + 2. Inverse DFT to θ-space + 3. Divide by J·B² at each θ + 4. Forward DFT back to m-space """ function set_perturbation_data!(kf_intr::KineticForcesInternal, pe_state::PerturbedEquilibrium.PerturbedEquilibriumState, ffs::ForceFreeStates.ForceFreeStatesResult, @@ -399,9 +404,9 @@ function set_perturbation_data!(kf_intr::KineticForcesInternal, pe_state::Pertur psi = psi_grid[ipsi] # Get Clebsch displacement vectors at this ψ - xsp = view(xi_modes.clebsch_psi, ipsi, :) # ξ^ψ [mpert] + xsp = view(xi_modes.clebsch_psi, ipsi, :) # ξ^ψ [mpert] xmp1 = view(xi_modes.clebsch_psi1, ipsi, :) # ∂ξ^ψ/∂ψ [mpert] - xms = view(clebsch_alpha_mat, ipsi, :) # ξ^α [mpert] + xms = view(clebsch_alpha_mat, ipsi, :) # ξ^α [mpert] # Evaluate geometric matrices at ψ → mpert² flat vectors, reshape to mpert×mpert geom_mats.smats(smat_flat, psi; hint=hint_s) @@ -421,8 +426,8 @@ function set_perturbation_data!(kf_intr::KineticForcesInternal, pe_state::Pertur mul!(jbb_kapx, smat, xsp) mul!(jbb_kapx, tmat, xms, 1.0 + 0.0im, 1.0 + 0.0im) # += tmat * xms mul!(jbb_divx, xmat, xmp1) - mul!(jbb_divx, ymat, xsp, 1.0 + 0.0im, 1.0 + 0.0im) # += ymat * xsp - mul!(jbb_divx, zmat, xms, 1.0 + 0.0im, 1.0 + 0.0im) # += zmat * xms + mul!(jbb_divx, ymat, xsp, 1.0 + 0.0im, 1.0 + 0.0im) # += ymat * xsp + mul!(jbb_divx, zmat, xms, 1.0 + 0.0im, 1.0 + 0.0im) # += zmat * xms @. jbb_dbob = -(jbb_divx + jbb_kapx) # Inverse DFT to θ-space, divide by J·B², forward DFT back @@ -449,9 +454,9 @@ Matches Fortran set_peq lines 859-868: transforms JBB-weighted m-space data to θ-space, removes the J·B² weighting at each poloidal angle, and transforms back. """ function _jbb_deweight!(out::AbstractVector{ComplexF64}, jbb_modes::Vector{ComplexF64}, - ft::Utilities.FourierTransforms.FourierTransform, - psi::Float64, equil::Equilibrium.PlasmaEquilibrium, - mthsurf::Int, theta_buf::Vector{ComplexF64}) + ft::Utilities.FourierTransforms.FourierTransform, + psi::Float64, equil::Equilibrium.PlasmaEquilibrium, + mthsurf::Int, theta_buf::Vector{ComplexF64}) # Inverse DFT: m-space → θ-space theta_buf .= Utilities.FourierTransforms.inverse(ft, jbb_modes) @@ -521,8 +526,8 @@ Accumulated results from all KineticForces computations. Written to gpec.h5 under the "KineticForces" group. """ @kwdef mutable struct KineticForcesState - method_results::Dict{String, MethodResult} = Dict{String, MethodResult}() + method_results::Dict{String,MethodResult} = Dict{String,MethodResult}() # Block-diagonal kinetic matrices: key=method, value=(numpert_total, numpert_total, 6) - kinetic_matrices::Dict{String, Array{ComplexF64,3}} = Dict{String, Array{ComplexF64,3}}() + kinetic_matrices::Dict{String,Array{ComplexF64,3}} = Dict{String,Array{ComplexF64,3}}() completed::Bool = false end diff --git a/src/KineticForces/Output.jl b/src/KineticForces/Output.jl index 75f527ae2..96594f06d 100644 --- a/src/KineticForces/Output.jl +++ b/src/KineticForces/Output.jl @@ -140,8 +140,9 @@ Write variable-length integration trajectory records using offset-indexed concat This is the standard HDF5 ragged array pattern for storing variable-length data. # Arguments -- `mg::HDF5.Group`: HDF5 group for this method -- `records::Vector{EnergyIntegrationResult}`: Integration records to write + + - `mg::HDF5.Group`: HDF5 group for this method + - `records::Vector{EnergyIntegrationResult}`: Integration records to write """ function write_integration_records!(mg::HDF5.Group, records::Vector{EnergyIntegrationResult}) rg = create_group(mg, "EnergyIntegrals") @@ -173,13 +174,14 @@ end Print a summary of KineticForces results to stdout. # Arguments -- `state::KineticForcesState`: Accumulated computation results -- `verbose::Bool`: Print detailed per-surface results + + - `state::KineticForcesState`: Accumulated computation results + - `verbose::Bool`: Print detailed per-surface results """ function print_summary(state::KineticForcesState; verbose::Bool=false) for (method_name, result) in state.method_results @printf("%-8s T_phi = %11.3e 2n*dW_k = %11.3e\n", - method_name, real(result.total_torque), imag(result.total_torque)) + method_name, real(result.total_torque), imag(result.total_torque)) end if verbose for (method_name, _) in state.kinetic_matrices diff --git a/src/KineticForces/PitchIntegration.jl b/src/KineticForces/PitchIntegration.jl index 845cfabc9..52fd40ec6 100644 --- a/src/KineticForces/PitchIntegration.jl +++ b/src/KineticForces/PitchIntegration.jl @@ -61,15 +61,17 @@ Gauss-Kronrod quadrature. Uses `QuadGK.quadgk!` with an in-place ComplexF64 kernel buffer. The fbnce interpolant returns [ωb, ωd, f₁, f₂, ...] at each λ, where: -- f₁ = ωb|δJ|²/ro² (scalar torque) -- f₂:end = ωb·W_outer_products/ro² (kinetic matrix elements, if present) + + - f₁ = ωb|δJ|²/ro² (scalar torque) + - f₂:end = ωb·W_outer_products/ro² (kinetic matrix elements, if present) Splits the domain at the trapped/passing boundary so Gauss-Kronrod resolves the kink in leff = ell + n*q (circulating) → ell (trapped). One `quadgk!` call writes all `nqty` complex quantities per λ-evaluation. # Returns -- `Vector{ComplexF64}` of length nqty: integrated pitch-angle results + + - `Vector{ComplexF64}` of length nqty: integrated pitch-angle results """ function integrate_pitch_gar_quadgk( wn::Float64, wt::Float64, we::Float64, nuk::Float64, @@ -129,28 +131,28 @@ function _pitch_gar_kernel_quadgk!(out::Vector{ComplexF64}, lambda, p::PitchGARP if is_circulating xint_co = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint_counter = integrate_energy(p.wn, p.wt, p.we, wd, -wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint = xint_co + xint_counter else xint = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol) end xint_decomposed = complex(p.rex * real(xint), p.imx * imag(xint)) @inbounds for i in 1:p.nqty - out[i] = fvals[i + 2] * xint_decomposed + out[i] = fvals[i+2] * xint_decomposed end return nothing end @@ -212,8 +214,8 @@ end _pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchGARParams) Dual-output pitch kernel. Fills a length-`2*nqty` buffer: - out[1:nqty] — fwmm half: `fvals * complex(0, imag(xint))` - out[nqty+1:2*nqty] — ftmm half: `fvals * complex(real(xint), 0)` +out[1:nqty] — fwmm half: `fvals * complex(0, imag(xint))` +out[nqty+1:2*nqty] — ftmm half: `fvals * complex(real(xint), 0)` One energy integration per λ; both halves share it. """ @@ -229,22 +231,22 @@ function _pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchG if is_circulating xint_co = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint_counter = integrate_energy(p.wn, p.wt, p.we, wd, -wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint = xint_co + xint_counter else xint = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, - p.ell, leff, p.n, p.psi, lambda, p.method; - nutype=p.nutype, f0type=p.f0type, - nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + p.ell, leff, p.n, p.psi, lambda, p.method; + nutype=p.nutype, f0type=p.f0type, + nufac=p.nufac, ximag=p.ximag, qt=p.qt, + atol=p.energy_atol, rtol=p.energy_rtol) end xint_w = complex(0.0, imag(xint)) # fwmm: rex=0, imx=1 @@ -252,9 +254,9 @@ function _pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchG nq = p.nqty @inbounds for i in 1:nq - f = fvals[i + 2] - out[i] = f * xint_w - out[i + nq] = f * xint_t + f = fvals[i+2] + out[i] = f * xint_w + out[i+nq] = f * xint_t end return nothing end diff --git a/src/KineticForces/Torque.jl b/src/KineticForces/Torque.jl index 1cd40285e..99949647c 100644 --- a/src/KineticForces/Torque.jl +++ b/src/KineticForces/Torque.jl @@ -6,36 +6,39 @@ Toroidal torque resulting from nonambipolar transport in perturbed equilibrium. Imaginary component is proportional to the kinetic energy Im(T) = 2*n*dW_k. # Arguments -- `tpsi_var`: Output complex torque value -- `psi::Float64`: Normalized poloidal flux -- `n::Int`: Toroidal mode number -- `l::Int`: Bounce harmonic number -- `zi::Int`: Ion charge in fundamental units (e) -- `mi::Int`: Ion mass (units of proton mass) -- `wdfac::Float64`: Drift factor -- `divxfac::Float64`: Divergence factor -- `electron::Bool`: Calculate quantities for electrons (zi,mi ignored) -- `method::String`: Integration method (RLAR, CLAR, *GAR, *TMM, *WMM, *KMM) + + - `tpsi_var`: Output complex torque value + - `psi::Float64`: Normalized poloidal flux + - `n::Int`: Toroidal mode number + - `l::Int`: Bounce harmonic number + - `zi::Int`: Ion charge in fundamental units (e) + - `mi::Int`: Ion mass (units of proton mass) + - `wdfac::Float64`: Drift factor + - `divxfac::Float64`: Divergence factor + - `electron::Bool`: Calculate quantities for electrons (zi,mi ignored) + - `method::String`: Integration method (RLAR, CLAR, *GAR, *TMM, *WMM, *KMM) where * = F,T,P for full,trapped,passing -- `equil`: PlasmaEquilibrium with 2D interpolants and named profile/geometry splines -- `intr::KineticForcesInternal`: Internal state with mode indexing and perturbation splines -- `kinetic_profiles::Equilibrium.KineticProfileSplines`: Named kinetic-profile splines + - `equil`: PlasmaEquilibrium with 2D interpolants and named profile/geometry splines + - `intr::KineticForcesInternal`: Internal state with mode indexing and perturbation splines + - `kinetic_profiles::Equilibrium.KineticProfileSplines`: Named kinetic-profile splines (n_i, n_e, T_i, T_e, ω_E, ν_i, ν_e) loaded from `kinetic.dat` # Optional Arguments -- `op_wmats::Array{ComplexF64,3}`: Store ForceFreeStates matrix elements + + - `op_wmats::Array{ComplexF64,3}`: Store ForceFreeStates matrix elements # Returns -- `ComplexF64`: Toroidal torque due to nonambipolar transport + + - `ComplexF64`: Toroidal torque due to nonambipolar transport """ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, - zi::Int, mi::Int, wdfac::Float64, divxfac::Float64, - electron::Bool, method::String, equil, intr::KineticForcesInternal, - kinetic_profiles::Equilibrium.KineticProfileSplines; - op_wmats::Union{Nothing,Array{ComplexF64,3}}=nothing, - rex_override::Union{Nothing,Float64}=nothing, - imx_override::Union{Nothing,Float64}=nothing, - atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6) + zi::Int, mi::Int, wdfac::Float64, divxfac::Float64, + electron::Bool, method::String, equil, intr::KineticForcesInternal, + kinetic_profiles::Equilibrium.KineticProfileSplines; + op_wmats::Union{Nothing,Array{ComplexF64,3}}=nothing, + rex_override::Union{Nothing,Float64}=nothing, + imx_override::Union{Nothing,Float64}=nothing, + atol_xlmda::Float64=1e-9, rtol_xlmda::Float64=1e-6) # Enforce bounds if psi > 1 @@ -72,12 +75,12 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, # Sample poloidal quantities on theta grid (buffers pre-allocated on intr). mthsurf_local = intr.mthsurf - xs = intr.tpsi_xs - B_vals = intr.tpsi_B - dBdpsi_vals = intr.tpsi_dBdpsi + xs = intr.tpsi_xs + B_vals = intr.tpsi_B + dBdpsi_vals = intr.tpsi_dBdpsi dBdtheta_vals = intr.tpsi_dBdtheta - jac_vals = intr.tpsi_jac - djdpsi_vals = intr.tpsi_djdpsi + jac_vals = intr.tpsi_jac + djdpsi_vals = intr.tpsi_djdpsi hB = intr.hint2d_eqfun_B hJ = intr.hint2d_rzphi_jac @@ -144,14 +147,14 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, # Flux-function quantities — read directly from named splines on the # PlasmaEquilibrium and the externally-loaded KineticProfileSplines. - q = equil.profiles.q_spline(psi) - dVdpsi = equil.profiles.dVdpsi_spline(psi) + q = equil.profiles.q_spline(psi) + dVdpsi = equil.profiles.dVdpsi_spline(psi) if electron - n_s = kinetic_profiles.ne_spline(psi) - T_s = kinetic_profiles.Te_spline(psi) + n_s = kinetic_profiles.ne_spline(psi) + T_s = kinetic_profiles.Te_spline(psi) dn_s_dpsi = kinetic_profiles.ne_deriv(psi) dT_s_dpsi = kinetic_profiles.Te_deriv(psi) - nu_s = kinetic_profiles.nue_spline(psi) + nu_s = kinetic_profiles.nue_spline(psi) else # ni_spline / nui_spline carry THIS species' resonant density and its # full-composition collisionality (per-species views built by the loader). @@ -159,7 +162,7 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, T_s = kinetic_profiles.Ti_spline(psi) dn_s_dpsi = kinetic_profiles.ni_deriv(psi) dT_s_dpsi = kinetic_profiles.Ti_deriv(psi) - nu_s = kinetic_profiles.nui_spline(psi) + nu_s = kinetic_profiles.nui_spline(psi) end welec = kinetic_profiles.omegaE_spline(psi) @@ -167,10 +170,10 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, # time from the kinetic-profile derivatives — no longer baked into the loader. wdian = -twopi * T_s * dn_s_dpsi / (chrg * intr.chi1 * n_s) wdiat = -twopi * dT_s_dpsi / (chrg * intr.chi1) - wphi = welec + wdian + wdiat + wphi = welec + wdian + wdiat wtran = sqrt(2 * T_s / mass) / (q * intr.ro) wgyro = chrg * intr.bo / mass - nuk = nu_s + nuk = nu_s rsquared_bmin = equil.rzphi_rsquared((psi, theta_bmin)) if rsquared_bmin <= 0 @@ -187,7 +190,7 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, avg_r = equil.geometry.avg_r_spline(psi) avg_R = equil.geometry.avg_R_spline(psi) - epsr = avg_r / avg_R + epsr = avg_r / avg_R wbhat = (π / 4) * sqrt(epsr / 2) * wtran wdhat = q^3 * wtran^2 / (4 * epsr * wgyro) * wdfac nueff = nu_s / (2 * epsr) @@ -196,17 +199,17 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, kind = method_kind(method) if kind == :fcgl tpsi_var[] = calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac, - n_s, T_s, equil, intr) + n_s, T_s, equil, intr) elseif kind == :rlar tpsi_var[] = calculate_rlar(psi, n, l, q, epsr, wdian, wdiat, welec, - wdhat, wbhat, nueff, dVdpsi, n_s, T_s, - dbob_m_f, intr.bo, bmin) + wdhat, wbhat, nueff, dVdpsi, n_s, T_s, + dbob_m_f, intr.bo, bmin) elseif kind == :clar tpsi_var[] = calculate_clar(psi, n, l, q, epsr, wdian, wdiat, welec, - nuk, intr.bo, bmax, bmin, n_s, T_s, mass, chrg, - tspl, dbob_m_f, divx_m_f, divxfac, wdfac) + nuk, intr.bo, bmax, bmin, n_s, T_s, mass, chrg, + tspl, dbob_m_f, divx_m_f, divxfac, wdfac) else # :gar — fgar/tgar/pgar + all *mm matrix methods # Evaluate geometric matrices at current ψ if matrix path @@ -223,17 +226,17 @@ function tpsi!(tpsi_var::Ref{ComplexF64}, psi::Float64, n::Int, l::Int, zmat_f = reshape(intr.zmats(psi), intr.mpert, intr.mpert) end tpsi_var[] = calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, - nuk, intr.bo, bmax, bmin, n_s, T_s, mass, chrg, - tspl, dbob_m_f, divx_m_f, divxfac, wdfac, - method, op_wmats; - chi1=intr.chi1, ro=intr.ro, mfac=intr.mfac, - mpert=intr.mpert, theta_bmax=theta_bmax, - B_extrap=B_extrap, - smat=smat_f, tmat=tmat_f, xmat=xmat_f, - ymat=ymat_f, zmat=zmat_f, - energy_atol=atol_xlmda, energy_rtol=rtol_xlmda, - pitch_atol=atol_xlmda, pitch_rtol=rtol_xlmda, - rex_override=rex_override, imx_override=imx_override) + nuk, intr.bo, bmax, bmin, n_s, T_s, mass, chrg, + tspl, dbob_m_f, divx_m_f, divxfac, wdfac, + method, op_wmats; + chi1=intr.chi1, ro=intr.ro, mfac=intr.mfac, + mpert=intr.mpert, theta_bmax=theta_bmax, + B_extrap=B_extrap, + smat=smat_f, tmat=tmat_f, xmat=xmat_f, + ymat=ymat_f, zmat=zmat_f, + energy_atol=atol_xlmda, energy_rtol=rtol_xlmda, + pitch_atol=atol_xlmda, pitch_rtol=rtol_xlmda, + rex_override=rex_override, imx_override=imx_override) end return nothing @@ -255,8 +258,8 @@ Only valid for bounce harmonic l=0. Based on: [Logan et al., Phys. Plasmas 2013] """ function calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac::Float64, - n_s::Float64, T_s::Float64, equil, - intr::KineticForcesInternal)::ComplexF64 + n_s::Float64, T_s::Float64, equil, + intr::KineticForcesInternal)::ComplexF64 # Only implemented for l=0 if l != 0 @@ -267,7 +270,7 @@ function calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac::Float64, # Create spline for poloidal integrands cglspl = zeros(2, mthsurf_local + 1) - theta_vals = range(0, 1, length=mthsurf_local + 1) + theta_vals = range(0, 1; length=mthsurf_local + 1) # Evaluate poloidal functions and field perturbations hB = intr.hint2d_eqfun_B @@ -301,8 +304,8 @@ function calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac::Float64, # Calculate torque: T = 2*n*i*n_s*T_s * [weighted integral] result = 2.0 * n * im * n_s * T_s * - (0.5 * (5.0/3.0) * integral_1 + - 0.5 * (1.0/3.0) * integral_2) + (0.5 * (5.0/3.0) * integral_1 + + 0.5 * (1.0/3.0) * integral_2) return result end @@ -319,9 +322,9 @@ Valid for low aspect ratio tokamaks (ε << 1). Reference: [Logan et al., Phys. Plasmas, 2013] """ function calculate_rlar(psi, n, l, q, epsr, wdian, wdiat, welec, - wdhat, wbhat, nueff, dVdpsi::Float64, - n_s::Float64, T_s::Float64, - dbob_m_f, bo, bmin=0.5)::ComplexF64 + wdhat, wbhat, nueff, dVdpsi::Float64, + n_s::Float64, T_s::Float64, + dbob_m_f, bo, bmin=0.5)::ComplexF64 # Setup parameters for energy integration lnq = Float64(l) # Resonant mode for trapped particles @@ -332,11 +335,11 @@ function calculate_rlar(psi, n, l, q, epsr, wdian, wdiat, welec, # Energy-space quadrature # This computes ∫ K(x) dx where K is the resonance operator xint = integrate_energy(wdian, wdiat, welec, wdhat, wbhat, nueff, - l, lnq, n, psi, lmdamax, "rlar") + l, lnq, n, psi, lmdamax, "rlar") # Kappa/bounce averaging (effect of field perturbations) # Placeholder: simplified estimate - kappaint_val = sqrt(mean(abs.(dbob_m_f).^2)) + kappaint_val = sqrt(mean(abs.(dbob_m_f) .^ 2)) # dV/dpsi gradient term from Clebsch coordinate Jacobian psi_factor = dVdpsi @@ -363,8 +366,8 @@ Includes bounce-averaged integrals over lambda (pitch angle). Status: Partially implemented (stub for full calculation) """ function calculate_clar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo, - bmax, bmin, n_s::Float64, T_s::Float64, mass, chrg, tspl, - dbob_m_f, divx_m_f, divxfac, wdfac)::ComplexF64 + bmax, bmin, n_s::Float64, T_s::Float64, mass, chrg, tspl, + dbob_m_f, divx_m_f, divxfac, wdfac)::ComplexF64 @warn "CLAR method not yet fully implemented, returning zero" maxlog=1 return ComplexF64(0.0, 0.0) @@ -384,35 +387,37 @@ Can compute torque (*TMM), energy (*WMM), or matrix elements (*KMM/*RMM). Ports Fortran torque.F90 GAR branch (lines 529-932). # Steps -1. Compute bounce-averaged quantities via `compute_bounce_data()` -2. Build fbnce interpolant over λ, normalize for numerical stability -3. Integrate over pitch angle via `integrate_pitch_gar_quadgk()` -4. Apply torque normalization (Eq. 19, Logan et al. 2013) -5. If matrix path: assemble and normalize kinetic matrices + + 1. Compute bounce-averaged quantities via `compute_bounce_data()` + 2. Build fbnce interpolant over λ, normalize for numerical stability + 3. Integrate over pitch angle via `integrate_pitch_gar_quadgk()` + 4. Apply torque normalization (Eq. 19, Logan et al. 2013) + 5. If matrix path: assemble and normalize kinetic matrices # Keyword Arguments (rex/imx override) -- `rex_override::Union{Nothing,Float64}`: Override real-part multiplier for resonance - operator. When both overrides are provided, bypasses method-string derivation. -- `imx_override::Union{Nothing,Float64}`: Override imaginary-part multiplier. - Use `rex_override=1.0, imx_override=1.0` to get full complex result for - simultaneous kwmat/ktmat extraction via `compute_kinetic_matrices_at_psi!`. + + - `rex_override::Union{Nothing,Float64}`: Override real-part multiplier for resonance + operator. When both overrides are provided, bypasses method-string derivation. + - `imx_override::Union{Nothing,Float64}`: Override imaginary-part multiplier. + Use `rex_override=1.0, imx_override=1.0` to get full complex result for + simultaneous kwmat/ktmat extraction via `compute_kinetic_matrices_at_psi!`. Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)] """ function calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo, - bmax, bmin, n_s::Float64, T_s::Float64, mass, chrg, tspl, - dbob_m_f, divx_m_f, divxfac, wdfac, method, op_wmats; - chi1::Float64, ro::Float64, mfac::Vector{Int}, mpert::Int, - theta_bmax::Float64, B_extrap, - smat=nothing, tmat=nothing, xmat=nothing, - ymat=nothing, zmat=nothing, - nlmda::Int=128, ntheta::Int=128, - nutype::String="harmonic", f0type::String="maxwellian", - nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false, - energy_atol::Float64=1e-7, energy_rtol::Float64=1e-5, - pitch_atol::Float64=1e-9, pitch_rtol::Float64=1e-6, - rex_override::Union{Nothing,Float64}=nothing, - imx_override::Union{Nothing,Float64}=nothing)::ComplexF64 + bmax, bmin, n_s::Float64, T_s::Float64, mass, chrg, tspl, + dbob_m_f, divx_m_f, divxfac, wdfac, method, op_wmats; + chi1::Float64, ro::Float64, mfac::Vector{Int}, mpert::Int, + theta_bmax::Float64, B_extrap, + smat=nothing, tmat=nothing, xmat=nothing, + ymat=nothing, zmat=nothing, + nlmda::Int=128, ntheta::Int=128, + nutype::String="harmonic", f0type::String="maxwellian", + nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false, + energy_atol::Float64=1e-7, energy_rtol::Float64=1e-5, + pitch_atol::Float64=1e-9, pitch_rtol::Float64=1e-6, + rex_override::Union{Nothing,Float64}=nothing, + imx_override::Union{Nothing,Float64}=nothing)::ComplexF64 # Bounce-averaged quantities per pitch angle bounce = compute_bounce_data( @@ -443,7 +448,7 @@ function calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo, # keeps the historical OLD-path behavior (torque pipeline uses only the # real part of the outer products). @inbounds for q in 1:nqty_mat, ilmda in 1:bounce.nlmda - fbnce_data[ilmda, 3 + q] = real(bounce.wmats_vs_lambda[ilmda, q]) + fbnce_data[ilmda, 3+q] = real(bounce.wmats_vs_lambda[ilmda, q]) end end @@ -497,7 +502,7 @@ function calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo, base = 1 # lxint offset after scalar-torque slot (index 1) # Helper: fetch normalized pitch integral at lxint[base+q] for q ∈ [1..nqty_mat]. - @inline elem(q) = complex(lxint[base + q] / fbnce_norm[base + q]) * energy_factor + @inline elem(q) = complex(lxint[base+q] / fbnce_norm[base+q]) * energy_factor # A (Hermitian, k=1): upper triangle stored at q ∈ [1..Mu]; mirror to lower. off = 0 @@ -586,7 +591,7 @@ without perturbing the perturbative torque pipeline. function _setup_surface_state( psi::Float64, zi::Int, mi::Int, electron::Bool, equil, intr::KineticForcesInternal, - kinetic_profiles::Equilibrium.KineticProfileSplines, + kinetic_profiles::Equilibrium.KineticProfileSplines ) if electron chrg = -1 * e @@ -597,12 +602,12 @@ function _setup_surface_state( end mthsurf_local = intr.mthsurf - xs = intr.tpsi_xs - B_vals = intr.tpsi_B - dBdpsi_vals = intr.tpsi_dBdpsi + xs = intr.tpsi_xs + B_vals = intr.tpsi_B + dBdpsi_vals = intr.tpsi_dBdpsi dBdtheta_vals = intr.tpsi_dBdtheta - jac_vals = intr.tpsi_jac - djdpsi_vals = intr.tpsi_djdpsi + jac_vals = intr.tpsi_jac + djdpsi_vals = intr.tpsi_djdpsi hB = intr.hint2d_eqfun_B hJ = intr.hint2d_rzphi_jac @@ -657,17 +662,17 @@ function _setup_surface_state( q = equil.profiles.q_spline(psi) if electron - n_s = kinetic_profiles.ne_spline(psi) - T_s = kinetic_profiles.Te_spline(psi) + n_s = kinetic_profiles.ne_spline(psi) + T_s = kinetic_profiles.Te_spline(psi) dn_s_dpsi = kinetic_profiles.ne_deriv(psi) dT_s_dpsi = kinetic_profiles.Te_deriv(psi) - nu_s = kinetic_profiles.nue_spline(psi) + nu_s = kinetic_profiles.nue_spline(psi) else - n_s = kinetic_profiles.ni_spline(psi) - T_s = kinetic_profiles.Ti_spline(psi) + n_s = kinetic_profiles.ni_spline(psi) + T_s = kinetic_profiles.Ti_spline(psi) dn_s_dpsi = kinetic_profiles.ni_deriv(psi) dT_s_dpsi = kinetic_profiles.Ti_deriv(psi) - nu_s = kinetic_profiles.nui_spline(psi) + nu_s = kinetic_profiles.nui_spline(psi) end welec = kinetic_profiles.omegaE_spline(psi) @@ -675,7 +680,7 @@ function _setup_surface_state( wdiat = -twopi * dT_s_dpsi / (chrg * intr.chi1) wtran = sqrt(2 * T_s / mass) / (q * intr.ro) wgyro = chrg * intr.bo / mass - nuk = nu_s + nuk = nu_s rsquared_bmin = equil.rzphi_rsquared((psi, theta_bmin)) rsquared_bmin > 0 || @@ -683,14 +688,14 @@ function _setup_surface_state( avg_r = equil.geometry.avg_r_spline(psi) avg_R = equil.geometry.avg_R_spline(psi) - epsr = avg_r / avg_R + epsr = avg_r / avg_R return (; chrg, mass, tspl, B_extrap, bmax, bmin, theta_bmax, q, n_s, T_s, welec, wdian, wdiat, wtran, wgyro, nuk, - epsr, + epsr ) end @@ -714,8 +719,8 @@ match against Fortran `fourfit.F:1080-1082` (`kwmat_l`, `ktmat_l`). For the Hermitian-outer-product blocks A/D/H stored as upper-triangles, the mirror rule differs between halves: - kwmat[j,i] = conj(kwmat[i,j]) — Hermitian (S_w pure imaginary) - ktmat[j,i] = -conj(ktmat[i,j]) — anti-Hermitian (S_t pure real) +kwmat[j,i] = conj(kwmat[i,j]) — Hermitian (S_w pure imaginary) +ktmat[j,i] = -conj(ktmat[i,j]) — anti-Hermitian (S_t pure real) Derivation: `conj(S_w) = -S_w` vs `conj(S_t) = S_t`, combined with `conj(factor) = -factor` (factor = -i/(2n)). These mirrors recover Fortran's independent-slot computation at the mirrored (j,i) positions. @@ -735,8 +740,8 @@ function kinetic_energy_matrices_for_euler_lagrange!( mpert = intr.mpert mfac = intr.mfac chi1 = intr.chi1 - ro = intr.ro - bo = intr.bo + ro = intr.ro + bo = intr.bo # Geometric matrices at this ψ smat_f = reshape(intr.smats(psi), mpert, mpert) @@ -805,7 +810,7 @@ function kinetic_energy_matrices_for_euler_lagrange!( Mu = (mpert * (mpert + 1)) ÷ 2 # Fetch normalized element at pitch-integral slot q, from either half. # half_offset = 0 for wmm (→kwmat), = nqty for tmm (→ktmat). - @inline elem(q, half_offset) = complex(lxint[half_offset + q] / fbnce_norm[q]) * energy_factor + @inline elem(q, half_offset) = complex(lxint[half_offset+q] / fbnce_norm[q]) * energy_factor # For A/D/H Hermitian-outer-product blocks stored as upper-triangles, the # lower-triangle reconstruction uses different mirrors per half: @@ -829,11 +834,16 @@ function kinetic_energy_matrices_for_euler_lagrange!( for (dest, half, mirror_sign) in ((kwmat, 0, 1.0), (ktmat, nqty, -1.0)) off = 0 - _assemble_hermitian!(dest, 1, off, half, mirror_sign); off += Mu # A (k=1) - _assemble_hermitian!(dest, 4, off, half, mirror_sign); off += Mu # D (k=4) - _assemble_hermitian!(dest, 6, off, half, mirror_sign); off += Mu # H (k=6) - _assemble_full!(dest, 2, off, half); off += mpert^2 # B (k=2) - _assemble_full!(dest, 3, off, half); off += mpert^2 # C (k=3) + _assemble_hermitian!(dest, 1, off, half, mirror_sign) + off += Mu # A (k=1) + _assemble_hermitian!(dest, 4, off, half, mirror_sign) + off += Mu # D (k=4) + _assemble_hermitian!(dest, 6, off, half, mirror_sign) + off += Mu # H (k=6) + _assemble_full!(dest, 2, off, half) + off += mpert^2 # B (k=2) + _assemble_full!(dest, 3, off, half) + off += mpert^2 # C (k=3) _assemble_full!(dest, 5, off, half) # E (k=5) end @@ -875,14 +885,15 @@ adjoint combinations `kwmat ± ktmat` in `ForceFreeStates/Kinetic.jl` / Fortran `dcon/sing.f:967-1075` for non-Hermitian B_k, C_k, E_k. # Arguments -- `kwmat::Array{ComplexF64,3}`: Output (mpert×mpert×6), fwmm half, zeroed on entry -- `ktmat::Array{ComplexF64,3}`: Output (mpert×mpert×6), ftmm half, zeroed on entry -- `psi, n, l, zi, mi, wdfac, divxfac, electron`: Same as `tpsi!` (divxfac unused - on the matrix path — retained for call-site compatibility) -- `equil`: PlasmaEquilibrium -- `intr::KineticForcesInternal`: Internal state with mode indexing, geometric - matrices (smats/tmats/xmats/ymats/zmats), and per-surface θ-grid buffers -- `kinetic_profiles::Equilibrium.KineticProfileSplines`: Named kinetic-profile splines + + - `kwmat::Array{ComplexF64,3}`: Output (mpert×mpert×6), fwmm half, zeroed on entry + - `ktmat::Array{ComplexF64,3}`: Output (mpert×mpert×6), ftmm half, zeroed on entry + - `psi, n, l, zi, mi, wdfac, divxfac, electron`: Same as `tpsi!` (divxfac unused + on the matrix path — retained for call-site compatibility) + - `equil`: PlasmaEquilibrium + - `intr::KineticForcesInternal`: Internal state with mode indexing, geometric + matrices (smats/tmats/xmats/ymats/zmats), and per-surface θ-grid buffers + - `kinetic_profiles::Equilibrium.KineticProfileSplines`: Named kinetic-profile splines Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)] """ @@ -904,7 +915,7 @@ function compute_kinetic_matrices_at_psi!( end state = _setup_surface_state(psi, zi, mi, electron, - equil, intr, kinetic_profiles) + equil, intr, kinetic_profiles) kinetic_energy_matrices_for_euler_lagrange!( kwmat, ktmat, state, psi, n, l, wdfac, intr; @@ -914,5 +925,3 @@ function compute_kinetic_matrices_at_psi!( return nothing end - - diff --git a/src/KineticForces/Utils.jl b/src/KineticForces/Utils.jl index 51c0cd3a8..0be9b3406 100644 --- a/src/KineticForces/Utils.jl +++ b/src/KineticForces/Utils.jl @@ -44,7 +44,7 @@ in `psi_panel_points`). function _resonance_nodes_from_frequencies(wbhat_f, welec_f, wdhat_f, grid; n::Int, nl::Int, xeval::Float64=2.5) nodes = Float64[] sx = sqrt(xeval) - for l in -nl:nl + for l in (-nl):nl append!(nodes, find_sign_change_roots(psi -> l * wbhat_f(psi) * sx + n * (welec_f(psi) + wdhat_f(psi) * xeval), grid)) end return nodes @@ -71,7 +71,7 @@ the estimate degenerates. Panel placement only needs ~peak-width accuracy, so th estimates (single spline evaluations) are sufficient and no bounce averaging is performed. """ function kinetic_resonance_psi_nodes(kinetic_profiles::Equilibrium.KineticProfileSplines, equil; - n::Int, nl::Int, zi::Int=1, mi::Int=2, electron::Bool=false, wdfac::Float64=1.0, xeval::Float64=2.5) + n::Int, nl::Int, zi::Int=1, mi::Int=2, electron::Bool=false, wdfac::Float64=1.0, xeval::Float64=2.5) chrg = electron ? -e : zi * e mass = electron ? me : mi * mp T_spline = electron ? kinetic_profiles.Te_spline : kinetic_profiles.Ti_spline diff --git a/src/LocalStability/Ballooning.jl b/src/LocalStability/Ballooning.jl index 2add47a62..5f11e9992 100644 --- a/src/LocalStability/Ballooning.jl +++ b/src/LocalStability/Ballooning.jl @@ -755,17 +755,18 @@ function ballooning_alpha_crossings( # Δ' as a function of the α scaling at fixed magnetic shear. Failed evaluations # (extreme corrections can break the coefficient assembly) count as non-stable # samples and are rejected by the crossing classification, like in scan_delta_prime_map. - delta_at(scale) = try - ballooning_delta_prime( - psi_idx, - plasma_eq; - corr_qprime=0.0, - corr_pprime=ref.pprime_norm_ref * (scale - 1.0), - theta_k=theta_k - ).delta_prime - catch - NaN - end + delta_at(scale) = + try + ballooning_delta_prime( + psi_idx, + plasma_eq; + corr_qprime=0.0, + corr_pprime=ref.pprime_norm_ref * (scale - 1.0), + theta_k=theta_k + ).delta_prime + catch + NaN + end samples = collect(range(0.0, max_alpha_scale; length=n_scan + 1)) scales = _ballooning_marginal_crossings(delta_at, samples, tol; max_crossings=max_crossings) @@ -796,7 +797,7 @@ function _ballooning_marginal_crossings(delta_at, samples, tol; pole_cap=3.0, ma k0 == 0 && return Float64[] d_anchor = abs(d_prev) locs = Float64[] - for k in k0+1:n + for k in (k0+1):n d = delta_at(samples[k]) if isfinite(d_prev) && isfinite(d) && sign(d) != sign(d_prev) && max(abs(d_prev), abs(d)) <= pole_cap * d_anchor lo, hi = samples[k-1], samples[k] @@ -981,17 +982,18 @@ function ballooning_qprime_crossings( # Δ' as a function of the q' scaling at the experimental pressure gradient. Failed # evaluations (reversed-shear corrections can break the coefficient assembly) count # as non-stable samples and are rejected by the crossing classification. - delta_at(scale) = try - ballooning_delta_prime( - psi_idx, - plasma_eq; - corr_qprime=ref.qprime_norm_ref * (scale - 1.0), - corr_pprime=0.0, - theta_k=theta_k - ).delta_prime - catch - NaN - end + delta_at(scale) = + try + ballooning_delta_prime( + psi_idx, + plasma_eq; + corr_qprime=ref.qprime_norm_ref * (scale - 1.0), + corr_pprime=0.0, + theta_k=theta_k + ).delta_prime + catch + NaN + end samples = collect(range(max_qprime_scale, min_qprime_scale; length=n_scan + 1)) scales = _ballooning_marginal_crossings(delta_at, samples, tol) diff --git a/src/PerturbedEquilibrium/FieldReconstruction.jl b/src/PerturbedEquilibrium/FieldReconstruction.jl index 65f36ee51..f4532c6ef 100644 --- a/src/PerturbedEquilibrium/FieldReconstruction.jl +++ b/src/PerturbedEquilibrium/FieldReconstruction.jl @@ -980,10 +980,12 @@ function _apply_rzphi_transform( # Per-thread scratch (the immutable `ft` functor and `geom` are shared read-only): θ-space # transform inputs/outputs (length mtheta) and mode-space forward-DFT outputs (length mpert), # so the DFTs run in place with no per-surface allocation. - bufs = [(R=zeros(ComplexF64, mtheta), Z=zeros(ComplexF64, mtheta), P=zeros(ComplexF64, mtheta), - psi=zeros(ComplexF64, mtheta), th=zeros(ComplexF64, mtheta), ze=zeros(ComplexF64, mtheta), - Ro=zeros(ComplexF64, mpert), Zo=zeros(ComplexF64, mpert), Po=zeros(ComplexF64, mpert)) - for _ in 1:Threads.maxthreadid()] + bufs = [ + (R=zeros(ComplexF64, mtheta), Z=zeros(ComplexF64, mtheta), P=zeros(ComplexF64, mtheta), + psi=zeros(ComplexF64, mtheta), th=zeros(ComplexF64, mtheta), ze=zeros(ComplexF64, mtheta), + Ro=zeros(ComplexF64, mpert), Zo=zeros(ComplexF64, mpert), Po=zeros(ComplexF64, mpert)) + for _ in 1:Threads.maxthreadid() + ] Threads.@threads :static for ipsi in 1:npsi buf = bufs[Threads.threadid()] diff --git a/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl index d8251de38..aa1250030 100644 --- a/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl +++ b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl @@ -26,7 +26,9 @@ Medium Priority (defer for MWE): - `singular_point_method::String` - Method for singular point treatment (default: "standard") Regularization: - # High Priority (MWE) + +# High Priority (MWE) + - `reg_spot::Float64` - Regularization width for singular surface smoothing (default: 0.05). Set to 0 to disable. Must be ≥ 0. """ @kwdef struct PerturbedEquilibriumControl @@ -121,6 +123,7 @@ Metadata [n_rational] — identifies each (surface, n) row: Control-surface forcing/response spectra [numpert_total], in the three Pharr (2026) field representations (all tesla; no flux/weber is stored): + - `forcing_b`/`response_b` - bare normal field b (Σ⁻¹·b̃) - `forcing_b_rootarea`/`response_b_rootarea` - root-area-weighted field b̃ (coordinate-invariant) - `forcing_b_area`/`response_b_area` - area-weighted field b̄ (= S·b̃; flux is Φ = A·b̄) @@ -185,18 +188,18 @@ well-conditioned flux-space inductances L, Λ: rational_surface_idx::Vector{Int} = Int[] # Control-surface forcing/response spectra in the three weightings of field representations [numpert_total], tesla - forcing_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (forcing Φ_x) - forcing_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃ (coordinate-invariant) - forcing_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄ - response_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (response Φ_tot = P·Φ_x) + forcing_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (forcing Φ_x) + forcing_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃ (coordinate-invariant) + forcing_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄ + response_b::Vector{ComplexF64} = ComplexF64[] # bare normal field b (response Φ_tot = P·Φ_x) response_b_rootarea::Vector{ComplexF64} = ComplexF64[] # root-area-weighted field b̃ - response_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄ + response_b_area::Vector{ComplexF64} = ComplexF64[] # area-weighted field b̄ # Control surface matrices [numpert_total × numpert_total], root-area-weighted field (b̃) space - plasma_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # Λ̃ (field space) + plasma_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # Λ̃ (field space) surface_inductance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # L̃ (field space) - permeability::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # P̃ = R⁻¹·Λ·L⁻¹·R - reluctance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # ϱ̃ = R†·L⁻¹·(Λ−L)·L⁻¹·R + permeability::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # P̃ = R⁻¹·Λ·L⁻¹·R + reluctance::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # ϱ̃ = R†·L⁻¹·(Λ−L)·L⁻¹·R rootarea_to_area_weight::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # S = Σ/√A at psilim: b̃→b̄ recovery operator surface_area::Float64 = 0.0 # scalar control-surface area A = ∫J|∇ψ|dθ (flux: Φ = A·b̄; conform R = S·A) diff --git a/src/PerturbedEquilibrium/Utils.jl b/src/PerturbedEquilibrium/Utils.jl index fc1eb9c0a..38be26f87 100644 --- a/src/PerturbedEquilibrium/Utils.jl +++ b/src/PerturbedEquilibrium/Utils.jl @@ -12,8 +12,9 @@ avoiding repeated index arithmetic throughout the code. ## Mode Indexing Convention For linear index i ∈ [1, numpert_total]: -- m_modes[i] = (i-1) % mpert + mlow -- n_modes[i] = (i-1) ÷ mpert + nlow + + - m_modes[i] = (i-1) % mpert + mlow + - n_modes[i] = (i-1) ÷ mpert + nlow This matches the convention used in ForceFreeStates where modes are ordered as: (m1,n1), (m2,n1), ..., (mpert,n1), (m1,n2), (m2,n2), ..., (mpert,npert) @@ -110,47 +111,47 @@ function write_outputs_to_HDF5( # Forcing modes forcing_group = haskey(pe_group, "ForcingModes") ? pe_group["ForcingModes"] : create_group(pe_group, "ForcingModes") - forcing_group["n"] = [mode.n for mode in intr.forcing_modes] - forcing_group["m"] = [mode.m for mode in intr.forcing_modes] + forcing_group["n"] = [mode.n for mode in intr.forcing_modes] + forcing_group["m"] = [mode.m for mode in intr.forcing_modes] forcing_group["amplitude"] = [mode.amplitude for mode in intr.forcing_modes] # Control-surface forcing/response spectra in the three Pharr field representations # (all tesla; flux/weber is never stored). b̃ = root-area-weighted (coordinate-invariant). - !isempty(state.forcing_b) && (pe_group["forcing_b"] = state.forcing_b) - !isempty(state.forcing_b_rootarea) && (pe_group["forcing_b_root_area"] = state.forcing_b_rootarea) - !isempty(state.forcing_b_area) && (pe_group["forcing_b_area"] = state.forcing_b_area) - !isempty(state.response_b) && (pe_group["response_b"] = state.response_b) + !isempty(state.forcing_b) && (pe_group["forcing_b"] = state.forcing_b) + !isempty(state.forcing_b_rootarea) && (pe_group["forcing_b_root_area"] = state.forcing_b_rootarea) + !isempty(state.forcing_b_area) && (pe_group["forcing_b_area"] = state.forcing_b_area) + !isempty(state.response_b) && (pe_group["response_b"] = state.response_b) !isempty(state.response_b_rootarea) && (pe_group["response_b_root_area"] = state.response_b_rootarea) - !isempty(state.response_b_area) && (pe_group["response_b_area"] = state.response_b_area) + !isempty(state.response_b_area) && (pe_group["response_b_area"] = state.response_b_area) # Control surface matrices [numpert_total × numpert_total], in coordinate-invariant # root-area-weighted field (b̃) space. Recover the area-weighted field b̄ with the stored # operator S ≡ rootarea_to_area_weight (b̄ = S·b̃): e.g. L_b̄ = S·L̃·S†; recover flux with the # scalar surface_area A: Φ = A·b̄ (internally R = S·A, Φ = R·b̃). [Pharr 2026] mat_group = haskey(pe_group, "ResponseMatrices") ? pe_group["ResponseMatrices"] : create_group(pe_group, "ResponseMatrices") - !isempty(state.plasma_inductance) && (mat_group["plasma_inductance"] = state.plasma_inductance) + !isempty(state.plasma_inductance) && (mat_group["plasma_inductance"] = state.plasma_inductance) !isempty(state.surface_inductance) && (mat_group["surface_inductance"] = state.surface_inductance) - !isempty(state.permeability) && (mat_group["permeability"] = state.permeability) - !isempty(state.reluctance) && (mat_group["reluctance"] = state.reluctance) + !isempty(state.permeability) && (mat_group["permeability"] = state.permeability) + !isempty(state.reluctance) && (mat_group["reluctance"] = state.reluctance) !isempty(state.rootarea_to_area_weight) && (mat_group["rootarea_to_area_weight_operator"] = state.rootarea_to_area_weight) - (state.surface_area != 0.0) && (mat_group["surface_area"] = state.surface_area) + (state.surface_area != 0.0) && (mat_group["surface_area"] = state.surface_area) # Response fields (ComplexF64 directly) response_group = haskey(pe_group, "Response") ? pe_group["Response"] : create_group(pe_group, "Response") !isempty(state.psi_grid) && (response_group["psi"] = state.psi_grid) have_xi = !isnothing(state.xi_modes) - have_b = have_xi && !isnothing(state.b_modes) - response_group["xi_psi"] = have_xi ? state.xi_modes.psi : ComplexF64[] - response_group["b_psi_area_weighted"] = have_b ? state.b_modes.b_psi_area_weighted : ComplexF64[] - response_group["Jb_theta"] = have_b ? state.b_modes.theta : ComplexF64[] - response_group["Jb_zeta"] = have_b ? state.b_modes.zeta : ComplexF64[] - response_group["b_n"] = !isnothing(state.b_n_modes) ? state.b_n_modes : ComplexF64[] - response_group["xi_n"] = !isnothing(state.xi_n_modes) ? state.xi_n_modes : ComplexF64[] + have_b = have_xi && !isnothing(state.b_modes) + response_group["xi_psi"] = have_xi ? state.xi_modes.psi : ComplexF64[] + response_group["b_psi_area_weighted"] = have_b ? state.b_modes.b_psi_area_weighted : ComplexF64[] + response_group["Jb_theta"] = have_b ? state.b_modes.theta : ComplexF64[] + response_group["Jb_zeta"] = have_b ? state.b_modes.zeta : ComplexF64[] + response_group["b_n"] = !isnothing(state.b_n_modes) ? state.b_n_modes : ComplexF64[] + response_group["xi_n"] = !isnothing(state.xi_n_modes) ? state.xi_n_modes : ComplexF64[] # Clebsch displacements for PENTRC (matches Fortran gpout_xclebsch) if have_xi - response_group["xi_clebsch_psi"] = state.xi_modes.clebsch_psi - response_group["dxi_clebsch_psidpsi"] = state.xi_modes.clebsch_psi1 + response_group["xi_clebsch_psi"] = state.xi_modes.clebsch_psi + response_group["dxi_clebsch_psidpsi"] = state.xi_modes.clebsch_psi1 response_group["xi_clebsch_alpha"] = state.xi_modes.clebsch_alpha end @@ -158,36 +159,36 @@ function write_outputs_to_HDF5( if have_xi response_group["Jxi_psi"] = state.xi_modes.psi_J response_group["Jxi_theta"] = state.xi_modes.theta - response_group["Jxi_zeta"] = state.xi_modes.zeta + response_group["Jxi_zeta"] = state.xi_modes.zeta end # Covariant components (from gpeq_cova) if have_xi - response_group["xi_cov_psi"] = state.xi_modes.cova_psi + response_group["xi_cov_psi"] = state.xi_modes.cova_psi response_group["xi_cov_theta"] = state.xi_modes.cova_theta - response_group["xi_cov_zeta"] = state.xi_modes.cova_zeta + response_group["xi_cov_zeta"] = state.xi_modes.cova_zeta end if have_xi response_group["Jxi_theta_reg"] = state.xi_modes.theta_reg - response_group["Jxi_zeta_reg"] = state.xi_modes.zeta_reg + response_group["Jxi_zeta_reg"] = state.xi_modes.zeta_reg end if have_b response_group["Jb_theta_reg"] = state.b_modes.theta_reg - response_group["Jb_zeta_reg"] = state.b_modes.zeta_reg - response_group["b_cov_psi"] = state.b_modes.cova_psi + response_group["Jb_zeta_reg"] = state.b_modes.zeta_reg + response_group["b_cov_psi"] = state.b_modes.cova_psi response_group["b_cov_theta"] = state.b_modes.cova_theta - response_group["b_cov_zeta"] = state.b_modes.cova_zeta + response_group["b_cov_zeta"] = state.b_modes.cova_zeta end # R,Z,φ cylindrical components in mode-space (from gpeq_rzphi) if have_xi - response_group["xi_R"] = state.xi_modes.R - response_group["xi_Z"] = state.xi_modes.Z + response_group["xi_R"] = state.xi_modes.R + response_group["xi_Z"] = state.xi_modes.Z response_group["xi_phi"] = state.xi_modes.phi end if have_b - response_group["b_R"] = state.b_modes.R - response_group["b_Z"] = state.b_modes.Z + response_group["b_R"] = state.b_modes.R + response_group["b_Z"] = state.b_modes.Z response_group["b_phi"] = state.b_modes.phi end @@ -195,34 +196,34 @@ function write_outputs_to_HDF5( coupling_group = haskey(pe_group, "SingularCoupling") ? pe_group["SingularCoupling"] : create_group(pe_group, "SingularCoupling") # Coupling matrices [n_rational × numpert_total] - !isempty(state.C_resonant_area_weighted_field) && (coupling_group["C_resonant_area_weighted_field"] = state.C_resonant_area_weighted_field) + !isempty(state.C_resonant_area_weighted_field) && (coupling_group["C_resonant_area_weighted_field"] = state.C_resonant_area_weighted_field) !isempty(state.C_resonant_current) && (coupling_group["C_resonant_current"] = state.C_resonant_current) - !isempty(state.C_island_width_sq) && (coupling_group["C_island_width_sq"] = state.C_island_width_sq) + !isempty(state.C_island_width_sq) && (coupling_group["C_island_width_sq"] = state.C_island_width_sq) !isempty(state.C_penetrated_area_weighted_field) && (coupling_group["C_penetrated_area_weighted_field"] = state.C_penetrated_area_weighted_field) - !isempty(state.C_delta_prime) && (coupling_group["C_Delta_prime"] = state.C_delta_prime) + !isempty(state.C_delta_prime) && (coupling_group["C_Delta_prime"] = state.C_delta_prime) # Applied resonant vectors [n_rational] - !isempty(state.resonant_area_weighted_field) && (coupling_group["resonant_area_weighted_field"] = state.resonant_area_weighted_field) - !isempty(state.resonant_current) && (coupling_group["resonant_current"] = state.resonant_current) - !isempty(state.island_width_sq) && (coupling_group["island_width_sq"] = state.island_width_sq) - !isempty(state.penetrated_area_weighted_field) && (coupling_group["penetrated_area_weighted_field"] = state.penetrated_area_weighted_field) - !isempty(state.delta_prime) && (coupling_group["Delta_prime"] = state.delta_prime) - !isempty(state.forcing_solution_weights) && (coupling_group["forcing_solution_weights"] = state.forcing_solution_weights) + !isempty(state.resonant_area_weighted_field) && (coupling_group["resonant_area_weighted_field"] = state.resonant_area_weighted_field) + !isempty(state.resonant_current) && (coupling_group["resonant_current"] = state.resonant_current) + !isempty(state.island_width_sq) && (coupling_group["island_width_sq"] = state.island_width_sq) + !isempty(state.penetrated_area_weighted_field) && (coupling_group["penetrated_area_weighted_field"] = state.penetrated_area_weighted_field) + !isempty(state.delta_prime) && (coupling_group["Delta_prime"] = state.delta_prime) + !isempty(state.forcing_solution_weights) && (coupling_group["forcing_solution_weights"] = state.forcing_solution_weights) !isempty(state.rational_area) && (coupling_group["rational_area"] = state.rational_area) - !isempty(state.island_half_width) && (coupling_group["island_half_width"] = state.island_half_width) + !isempty(state.island_half_width) && (coupling_group["island_half_width"] = state.island_half_width) !isempty(state.chirikov_parameter) && (coupling_group["chirikov_parameter"] = state.chirikov_parameter) # Metadata [n_rational] - !isempty(state.rational_psi) && (coupling_group["rational_psi"] = state.rational_psi) - !isempty(state.rational_q) && (coupling_group["rational_q"] = state.rational_q) - !isempty(state.rational_m_res) && (coupling_group["rational_m"] = state.rational_m_res) - !isempty(state.rational_n) && (coupling_group["rational_n"] = state.rational_n) + !isempty(state.rational_psi) && (coupling_group["rational_psi"] = state.rational_psi) + !isempty(state.rational_q) && (coupling_group["rational_q"] = state.rational_q) + !isempty(state.rational_m_res) && (coupling_group["rational_m"] = state.rational_m_res) + !isempty(state.rational_n) && (coupling_group["rational_n"] = state.rational_n) # Energies energy_group = haskey(pe_group, "Energies") ? pe_group["Energies"] : create_group(pe_group, "Energies") - energy_group["vacuum_energy"] = state.vacuum_energy - energy_group["surface_energy"] = state.surface_energy - energy_group["plasma_energy"] = state.plasma_energy + energy_group["vacuum_energy"] = state.vacuum_energy + energy_group["surface_energy"] = state.surface_energy + energy_group["plasma_energy"] = state.plasma_energy energy_group["toroidal_torque"] = state.toroidal_torque annotate_pe!(pe_group) @@ -261,7 +262,8 @@ const PE_H5_ANNOTATIONS = [ "Response/xi_cov_psi" => (; long_name="covariant radial displacement ξ_ψ = ξ·e_ψ", units="m^2", dims=("psi", "mode"), attach=(1 => "Response/psi",)), "Response/xi_cov_theta" => (; long_name="covariant poloidal displacement ξ_θ = ξ·e_θ", units="m^2", dims=("psi", "mode"), attach=(1 => "Response/psi",)), "Response/xi_cov_zeta" => (; long_name="covariant toroidal displacement ξ_ζ = ξ·e_ζ", units="m^2", dims=("psi", "mode"), attach=(1 => "Response/psi",)), - "Response/xi_clebsch_psi" => (; long_name="Clebsch displacement component ξ^ψ (PENTRC input, gpout_xclebsch convention)", dims=("psi", "mode"), attach=(1 => "Response/psi",)), + "Response/xi_clebsch_psi" => + (; long_name="Clebsch displacement component ξ^ψ (PENTRC input, gpout_xclebsch convention)", dims=("psi", "mode"), attach=(1 => "Response/psi",)), "Response/dxi_clebsch_psidpsi" => (; long_name="regularized ψ_N derivative of ξ^ψ (× singfac²/(singfac²+reg_spot²))", dims=("psi", "mode"), attach=(1 => "Response/psi",)), "Response/xi_clebsch_alpha" => diff --git a/src/Rerun.jl b/src/Rerun.jl index 03c19bea6..a71401637 100644 --- a/src/Rerun.jl +++ b/src/Rerun.jl @@ -37,7 +37,8 @@ function read_equilibrium_ingest(in_h5) haskey(in_h5, group_path) || return nothing group = in_h5[group_path] kind = read(group, "ingest_kind") - T = kind == "direct" ? Equilibrium.DirectIngest : + T = + kind == "direct" ? Equilibrium.DirectIngest : kind == "inverse" ? Equilibrium.InverseIngest : error("Unknown equilibrium ingest_kind in gpec.h5: $kind (expected \"direct\" or \"inverse\")") # Positional reconstruction: relies on the default constructor, so `fieldnames(T)` order @@ -75,8 +76,8 @@ numbers, booleans, and quoted strings without worrying about shell quoting. function parse_override_flag(expr::AbstractString) eqidx = findfirst(==('='), expr) eqidx === nothing && error("--override expects key=value, got: $expr") - lhs = String(strip(expr[1:eqidx-1])) - rhs = String(strip(expr[eqidx+1:end])) + lhs = String(strip(expr[1:(eqidx-1)])) + rhs = String(strip(expr[(eqidx+1):end])) isempty(lhs) && error("--override key cannot be empty: $expr") parts = split(lhs, '.') @@ -97,7 +98,7 @@ function parse_override_flag(expr::AbstractString) # Build nested dict from dotted path. root = Dict{String,Any}() cursor = root - for p in parts[1:end-1] + for p in parts[1:(end-1)] cursor[String(p)] = Dict{String,Any}() cursor = cursor[String(p)] end @@ -176,7 +177,7 @@ function resolve_rerun_output_path(source_h5::String, output_dir::String, output source_abs = abspath(source_h5) if output_name === nothing base = basename(source_abs) - stem = endswith(lowercase(base), ".h5") ? base[1:end-3] : base + stem = endswith(lowercase(base), ".h5") ? base[1:(end-3)] : base output_name = string(stem, "_rerun.h5") end if abspath(joinpath(output_dir, output_name)) == source_abs @@ -281,9 +282,11 @@ function build_inputs_from_h5(args::Vector{String}) elseif ingest isa Equilibrium.InverseIngest Equilibrium.build_inverse_from_ingest(eq_config, ingest) else - error("gpec.h5 has no equilibrium ingest and eq_type=$(eq_config.eq_type) is not analytic — cannot replay. " * - "A file-based eq_type needs a stored ingest (pre-ingest snapshots lack one); a new analytic kind must be " * - "registered in Equilibrium.ANALYTIC_EQ.") + error( + "gpec.h5 has no equilibrium ingest and eq_type=$(eq_config.eq_type) is not analytic — cannot replay. " * + "A file-based eq_type needs a stored ingest (pre-ingest snapshots lack one); a new analytic kind must be " * + "registered in Equilibrium.ANALYTIC_EQ." + ) end return inputs, eq_config, additional_input, output_dir, current_git, preloaded_forcing, preloaded_coils diff --git a/src/Tearing/CriticalResonantField/CriticalResonantField.jl b/src/Tearing/CriticalResonantField/CriticalResonantField.jl new file mode 100644 index 000000000..b51360cd0 --- /dev/null +++ b/src/Tearing/CriticalResonantField/CriticalResonantField.jl @@ -0,0 +1,22 @@ +# CriticalResonantField.jl +# +# Uses the inner-layer delta from InnerLayer.jl to find the critical resonant field +# required for mode penetration at each rational surface. This can then be compared +# to the actual resonant field from the perturbed equilibrium to determine which modes +# are predicted to penetrate. + +module CriticalResonantField + +using LinearAlgebra +using StaticArrays +using Printf + +using ..InnerLayer +using ..InnerLayer: InnerLayerModel, solve_inner, GGJModel, GGJParameters, + SLAYERModel, SLAYERParameters + +include("TorqueBalance.jl") + +export TorqueBalance, torque_balance_value, torque_balance_scan + +end # module CriticalResonantField diff --git a/src/Tearing/CriticalResonantField/TorqueBalance.jl b/src/Tearing/CriticalResonantField/TorqueBalance.jl new file mode 100644 index 000000000..76042479d --- /dev/null +++ b/src/Tearing/CriticalResonantField/TorqueBalance.jl @@ -0,0 +1,149 @@ +# TorqueBalance.jl +# +# `TorqueBalance` solves the torque balance eqution for each rational surface. +# It follows the derivation found in Cole PopP 2006. For simplicity, the script +# uses equation 62 of Cole. This can be improved by incorporating the true +# outer-layer Δ' from the perturbed equilibrium and then solving equation 61. +# +# The viscous torque is: +# +# T_v = 2 · P (Q_0 - Q) / ((S kappa^hat) · (b_r(r_s)/B_phi))^2 +# +# The electromagnetic torque is: +# +# T_em = Im[Δ_inner(Q)] / |alpha + Δ_inner(Q)|^2 +# +# Now solving for critical resonant field, we have: +# +# (b_r(r_s)/B_phi))^2_crit = max(2 · P (Q_0 - Q) / ((S kappa^hat) · Im[-Δ_inner(Q)^-1]) +# +# Normalizations, definitions, and other conventions are taken from the Cole paper. +# The critical resonant field is found at each rational surface. For each, the script +# scans over a range of Q values and finds the maximum value of the right-hand side of +# the equation above. + +""" + TorqueBalance{M<:InnerLayerModel, P} + +Per-surface torque balance data: `(model, params, Q0, P, lu, sval)`. + +""" + +struct TorqueBalance{M<:InnerLayerModel,P} + model::M + params::P + Q0::Float64 + P::Float64 + lu::Float64 + sval::Float64 +end + +function torque_balance_value(tb::TorqueBalance, Q::Number) + Δ = solve_inner(tb.model, tb.params, ComplexF64(Q)).tearing + alpha = 1e-2 # Equation 62 of Cole PopP 2006 takes limit of alpha << 1. Set at 1e-2 but more accurate method is alpha = S^(-1/3) * (-r_s Δ'_s) + jxb = -imag(1.0 / (Δ + alpha)) + return 2.0 * tb.P * (tb.Q0 - Q) / jxb, Δ +end + +""" + torque_balance_scan(model::InnerLayerModel, params::InnerLayerParameters, Q0::Real, P::Real, + lu::Real, sval::Real;Qmin=-10.0, Qmax=10.0, n=200) -> (Qs, bal, Qs_positive, bal_positive, + Qpeak, br_crit, Qpeak_ind, Δs) + +Scan over a range of Q values to find the maximum of the torque balance equation. +Returns the Q values, torque balance values, positive Q values, positive torque balance values, +the Q value at the peak, the critical resonant field, the index of the peak Q value, and the Δ values for each Q. +""" + +function torque_balance_scan(tb; Qmin=-10.0, Qmax=10.0, n=2000) + Qs = range(Qmin, Qmax; length=n) + torque_out = [torque_balance_value(tb, q) for q in Qs] + bal = [x[1] for x in torque_out] + Δs = [x[2] for x in torque_out] + positive = isfinite.(bal) .& (bal .> 0.0) + + if !any(positive) + @warn "No positive torque balance found in the specified range. Try increasing the number of Q samples scanned or adjusting the range." + return Qs, bal, NaN, NaN, NaN, Δs + end + + # Find the highest point, then remove candidates within ΔQ of it. + idx_maxima = findall(isfinite.(bal) .& (bal .> 0)) + sort!(idx_maxima; by=i -> bal[i], rev=true) + selected = Int[] + min_ΔQ = 0.001 * abs(Qmax - Qmin) + + for i in idx_maxima + if all(abs(Qs[i] - Qs[j]) > min_ΔQ for j in selected) + push!(selected, i) + end + length(selected) == 2 && break + end + + if isempty(selected) + @warn "No usable positive torque-balance maximum found. Try increasing the number of Q samples scanned or adjusting the range." + return Qs, bal, NaN, NaN, NaN, Δs + end + + idx_maxima = selected + maxima = bal[idx_maxima] + Qs_maxima = Qs[idx_maxima] + + # find index, q val and bal val of the q closest to Q_e + idx_closest_Q_e = argmin(abs.(Qs .+ tb.params.Q_e)) # + Q_e since Q_e = -omega_e * Qconv + q_closest_Q_e = Qs[idx_closest_Q_e] + bal_closest_Q_e = bal[idx_closest_Q_e] + + # find index, q val and bal val of the q closest to Q_i + idx_closest_Q_i = argmin(abs.(Qs .+ tb.params.Q_i)) # + Q_i since Q_i = -omega_i * Qconv + q_closest_Q_i = Qs[idx_closest_Q_i] + bal_closest_Q_i = bal[idx_closest_Q_i] + + println( + "Highest Qs: ", + [@sprintf("%.2f", q) for q in Qs_maxima], + ", Closest Q to Q_e: ", + @sprintf("%.2f", q_closest_Q_e), + " closest Q to Q_i: ", + @sprintf("%.2f", q_closest_Q_i) + ) + + # check if 1st q_maxima is same as Q_e or Q_i from p + if idx_maxima[1] == idx_closest_Q_e || idx_maxima[1] == idx_closest_Q_i + if length(idx_maxima) < 2 + @warn "The first local maximum corresponds to a pole from Q_e or Q_i, but there is no second local maximum to select. Try increasing the number of Q samples scanned or adjusting the range." + return Qs, bal, NaN, NaN, NaN, Δs + end + @warn "The first local maximum may correspond to an electron or ion diamagnetic resonance. Selecting the second local maximum instead." + if idx_maxima[2] == idx_closest_Q_e || idx_maxima[2] == idx_closest_Q_i + @warn "Both the first and second local maxima correspond to poles from Q_e or Q_i. Unable to select a valid local maximum. Try increasing the number of Q samples scanned or adjusting the range." + return Qs, bal, NaN, NaN, NaN, Δs + end + idx_maximum = idx_maxima[2] + maximum = bal[idx_maximum] + Qmaximum = Qs[idx_maximum] + else + idx_maximum = idx_maxima[1] + maximum = bal[idx_maximum] + Qmaximum = Qs[idx_maximum] + end + + br_crit = sqrt(maximum / tb.lu * (tb.sval^2 / 2.0)) + + println( + "Critical Resonant Field at ", + tb.params.m, + "/", + tb.params.n, + " surface: ", + @sprintf("%.2e", br_crit), + " T at index: ", + idx_maximum, + " with value: ", + @sprintf("%.2f", maximum), + " and corresponding Q: ", + @sprintf("%.2f", Qmaximum) + ) + + return Qs, bal, Qmaximum, br_crit, idx_maximum, Δs +end diff --git a/src/Tearing/Dispersion/BruteForceScan.jl b/src/Tearing/Dispersion/BruteForceScan.jl index 6e7b8aead..c1f2b55fb 100644 --- a/src/Tearing/Dispersion/BruteForceScan.jl +++ b/src/Tearing/Dispersion/BruteForceScan.jl @@ -15,12 +15,12 @@ Output of a brute-force or AMR Q-plane scan. -| field | meaning | -|------------|---------------------------------------------------| -| `Q` | Complex Q values (`Matrix` for grid, `Vector` for AMR) | -| `Δ` | Residual values, same shape as `Q` | -| `re_axis` | Real-axis grid (only for regular-grid `ScanResult`) | -| `im_axis` | Imaginary-axis grid (only for regular-grid `ScanResult`) | +| field | meaning | +|:--------- |:-------------------------------------------------------- | +| `Q` | Complex Q values (`Matrix` for grid, `Vector` for AMR) | +| `Δ` | Residual values, same shape as `Q` | +| `re_axis` | Real-axis grid (only for regular-grid `ScanResult`) | +| `im_axis` | Imaginary-axis grid (only for regular-grid `ScanResult`) | """ struct ScanResult Q::Matrix{ComplexF64} @@ -53,15 +53,15 @@ from the result. - `threaded` -- distribute Q evaluations across `Threads.@threads` """ function brute_force_scan(f, Q_re_range::NTuple{2,<:Real}, - Q_im_range::NTuple{2,<:Real}; - nre::Integer, nim::Integer, - threaded::Bool=true) + Q_im_range::NTuple{2,<:Real}; + nre::Integer, nim::Integer, + threaded::Bool=true) nre >= 2 || throw(ArgumentError("brute_force_scan: nre must be ≥ 2")) nim >= 2 || throw(ArgumentError("brute_force_scan: nim must be ≥ 2")) re_axis = collect(range(Float64(Q_re_range[1]); stop=Float64(Q_re_range[2]), - length=nre)) + length=nre)) im_axis = collect(range(Float64(Q_im_range[1]); stop=Float64(Q_im_range[2]), - length=nim)) + length=nim)) Q = ComplexF64[(qr + qi*im) for qr in re_axis, qi in im_axis] Δ = Matrix{ComplexF64}(undef, nre, nim) if threaded diff --git a/src/Tearing/Dispersion/ContourSearchAMR.jl b/src/Tearing/Dispersion/ContourSearchAMR.jl index 3533a1773..5cf369eda 100644 --- a/src/Tearing/Dispersion/ContourSearchAMR.jl +++ b/src/Tearing/Dispersion/ContourSearchAMR.jl @@ -243,7 +243,7 @@ function amr_scan(f, Q_re_range::NTuple{2,<:Real}, _bulk_eval_into_cache!(cache, f, corners; parallel=parallel) cells = Vector{AMRCell}(undef, nre0 * nim0) - @inbounds for j in 0:nim0-1, i in 0:nre0-1 + @inbounds for j in 0:(nim0-1), i in 0:(nre0-1) # Read corner Q values from the same `corners` array used to populate # the cache. Recomputing them with `x + re_step` here would differ in # the last floating-point bit from the cache keys, causing spurious @@ -573,7 +573,7 @@ function multi_box_amr_scan(f, # Build pre-screen cells ps_cells = Vector{AMRCell}(undef, prescreen_nre * prescreen_nim) - @inbounds for j in 0:prescreen_nim-1, i in 0:prescreen_nre-1 + @inbounds for j in 0:(prescreen_nim-1), i in 0:(prescreen_nre-1) q_bl = corners[j*ncorners_x+i+1] q_br = corners[j*ncorners_x+(i+1)+1] q_tl = corners[(j+1)*ncorners_x+i+1] diff --git a/src/Tearing/Dispersion/CoupledFullMatch.jl b/src/Tearing/Dispersion/CoupledFullMatch.jl index 9b0f136c9..ed68c4445 100644 --- a/src/Tearing/Dispersion/CoupledFullMatch.jl +++ b/src/Tearing/Dispersion/CoupledFullMatch.jl @@ -155,7 +155,7 @@ function (mc::MultiSurfaceCouplingFull)(Q::Number) # Allocate the matching matrix and fill the lower-left 2m × 2m block # with transpose(dp_raw[1:s2, 1:s2]). mat = zeros(ComplexF64, s4, s4) - @views mat[s2+1:s4, 1:s2] .= transpose(mc.dp_raw[1:s2, 1:s2]) + @views mat[(s2+1):s4, 1:s2] .= transpose(mc.dp_raw[1:s2, 1:s2]) # Per-surface inner-layer assembly @inbounds for k in 1:m diff --git a/src/Tearing/Dispersion/GrowthRateExtraction.jl b/src/Tearing/Dispersion/GrowthRateExtraction.jl index 42c2c9681..796e67633 100644 --- a/src/Tearing/Dispersion/GrowthRateExtraction.jl +++ b/src/Tearing/Dispersion/GrowthRateExtraction.jl @@ -253,10 +253,10 @@ function _all_intersections(re_paths::Vector{Vector{ComplexF64}}, im_paths::Vector{Vector{ComplexF64}}) out = ComplexF64[] for re_path in re_paths - for i in 1:length(re_path)-1 + for i in 1:(length(re_path)-1) a, b = re_path[i], re_path[i+1] for im_path in im_paths - for j in 1:length(im_path)-1 + for j in 1:(length(im_path)-1) c, d = im_path[j], im_path[j+1] pt = _segment_intersection(a, b, c, d) pt !== nothing && push!(out, pt) @@ -468,7 +468,7 @@ function _median_segment_length(re_paths::Vector{Vector{ComplexF64}}, im_paths::Vector{Vector{ComplexF64}}) lens = Float64[] for paths in (re_paths, im_paths), p in paths - @inbounds for i in 1:length(p)-1 + @inbounds for i in 1:(length(p)-1) push!(lens, abs(p[i+1] - p[i])) end end @@ -811,7 +811,7 @@ function _march_triangle(p1::ComplexF64, p2::ComplexF64, p3::ComplexF64, v1::ComplexF64, v2::ComplexF64, v3::ComplexF64, re_target::Float64, im_target::Float64) return (_march_single(p1, p2, p3, real(v1), real(v2), real(v3), - imag(v1), imag(v2), imag(v3), re_target), + imag(v1), imag(v2), imag(v3), re_target), _march_single(p1, p2, p3, imag(v1), imag(v2), imag(v3), real(v1), real(v2), real(v3), im_target)) end diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index 1a12ff7e6..d465b8cde 100644 --- a/src/Tearing/Runner/Control.jl +++ b/src/Tearing/Runner/Control.jl @@ -4,6 +4,45 @@ # growth-rate analysis. Populated either directly via the `@kwdef` # constructor or by parsing the `[SLAYER]` (and nested `[SLAYER.*]`) # section(s) of a `gpec.toml`. +# +# `CriticalResonantFieldControl` holds the user-facing knobs that drive the critical resonant field +# analysis. Populated either directly via the `@kwdef` constructor or by parsing the `[CriticalResonantField]` section of a `gpec.toml`. +""" + CriticalResonantFieldControl + +Configuration for the critical resonant field analysis. All fields are +user-facing: read from the `[CriticalResonantField]` TOML section of a `gpec.toml` via +`critical_resonant_field_control_from_toml`, or built directly via the `@kwdef` keyword +constructor. +""" +@kwdef struct CriticalResonantFieldControl + enabled::Bool = false + Qmin::Float64 = -10.0 + Qmax::Float64 = 10.0 + n::Int = 200 + viscous_input_type::String = "angular_momentum_diffusivity" # Either "angular_momentum_diffusivity" or "magnetic_prandtl_number" + viscous_input::Any = 1.0 + store_scan::Bool = false +end + +function critical_resonant_field_control_from_toml(section::AbstractDict) + flat = Dict{String,Any}() + for (k, v) in section + flat[k] = v + end + + kwargs = Dict{Symbol,Any}() + for (k, v) in flat + sym = Symbol(k) + if sym in (:inner_model,) + kwargs[sym] = v isa Symbol ? v : Symbol(String(v)) + else + kwargs[sym] = v + end + end + + return CriticalResonantFieldControl(; kwargs...) +end """ SLAYERControl @@ -155,6 +194,9 @@ there is one consistent interface for resistive and kinetic profiles. profile_group::String = "/" store_scan::Bool = false + + # Critical resonant field analysis. Enabled via `[SLAYER.CriticalResonantField]` TOML section. + critical_resonant_field::CriticalResonantFieldControl = CriticalResonantFieldControl() end const _VALID_INNER_MODELS = (:slayer_fitzpatrick, :ggj_shooting, :ggj_galerkin) @@ -224,6 +266,10 @@ function slayer_control_from_toml(section::AbstractDict) haskey(v, "pole_threshold") && (flat["pole_threshold"] = v["pole_threshold"]) haskey(v, "filter_above_poles") && (flat["filter_above_poles"] = v["filter_above_poles"]) haskey(v, "filter_outside_re") && (flat["filter_outside_re"] = v["filter_outside_re"]) + elseif k == "CriticalResonantField" && v isa AbstractDict + flat["critical_resonant_field"] = + critical_resonant_field_control_from_toml(v) + else flat[k] = v end diff --git a/src/Tearing/Runner/HDF5Output.jl b/src/Tearing/Runner/HDF5Output.jl index ca4287147..2ae545211 100644 --- a/src/Tearing/Runner/HDF5Output.jl +++ b/src/Tearing/Runner/HDF5Output.jl @@ -1,6 +1,6 @@ # HDF5Output.jl # -# Write a `SLAYERResult` into an HDF5 group. Designed to be called by the +# Write a `SLAYERResult` or `CriticalResonantFieldResult` into an HDF5 group. Designed to be called by the # existing `PerturbedEquilibrium.write_outputs_to_HDF5` path — the # top-level GPEC runner wires that up; this file only defines the pure # writer. @@ -38,6 +38,10 @@ function write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, # disjoint field sets, so readers must not have to infer it from the schema. attrs(g)["layer_model"] = result.enabled ? _layer_model_token(eltype(result.params)) : "none" + if result.critical_resonant_field.enabled + write_critical_resonant_field_hdf5!(g, result.critical_resonant_field) + end + if !result.enabled # nothing else to write _annotate_tearing!(g) return g @@ -57,6 +61,40 @@ function write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, return g end +function write_critical_resonant_field_hdf5!(parent::Union{HDF5.File,HDF5.Group}, + result::CriticalResonantFieldResult) + + g = create_group(parent, "CriticalResonantField") + + g["surface_index"] = result.surface_index + g["Qpeak"] = result.Qpeak + g["br_crit"] = result.br_crit + g["Q0"] = result.Q0 + g["P"] = result.P + + if !isempty(result.scan_data) + scan_group = create_group(g, "Scan") + for d in result.scan_data + sg = create_group(scan_group, "surface_$(d.surface)") + sg["Q"] = d.Q + sg["balance"] = d.balance + sg["delta"] = d.delta + sg["Qpeak"] = d.Qpeak + sg["br_crit"] = d.br_crit + + # Save inputs for analysis in post + for field in (:Q0, :P, :lu, :sval, :m, :n) + if field in keys(d) + sg[String(field)] = getfield(d, field) + end + end + end + end + + attrs(g)["kind"] = "critical_resonant_field" + return g +end + # Token recorded in the Tearing group's layer_model attribute; keyed by the # per-surface parameter type since the SLAYER and GGJ branches write disjoint fields. _layer_model_token(::Type{SLAYERParameters}) = "slayer" diff --git a/src/Tearing/Runner/Result.jl b/src/Tearing/Runner/Result.jl index 0cffc21c9..df68bb868 100644 --- a/src/Tearing/Runner/Result.jl +++ b/src/Tearing/Runner/Result.jl @@ -3,6 +3,41 @@ # `SLAYERResult` packages the output of a full SLAYER analysis run: # per-surface layer parameters, the extracted tearing eigenvalues, and (if # `control.store_scan`) the full Q-plane scan data for plotting. +# +# `CriticalResonantFieldResult` packages the output of the critical resonant field analysis run: +# per-surface critical resoant field. If `control.store_scan` is true, the full Q scan data, +# viscous torque, and electromagnetic torque are also stored for plotting. + +""" + CriticalResonantFieldResult + +Output of `run_critical_resonant_field`. Carries both summary critical resonant field values and if `control.store_scan` is true, the full Q scan data, viscous torque, and electromagnetic torque for plotting. +""" + +struct CriticalResonantFieldResult + enabled::Bool + params::AbstractVector{<:InnerLayerParameters} + surface_index::Vector{Int} + Qpeak::Vector{Float64} + br_crit::Vector{Float64} + Q0::Vector{Float64} + P::Vector{Float64} + scan_data::Vector{NamedTuple} +end + +function empty_critical_resonant_field_result() + return CriticalResonantFieldResult( + false, + InnerLayerParameters[], + Int[], + Float64[], + Float64[], + Float64[], + Float64[], + NamedTuple[] + ) +end + """ SLAYERResult @@ -22,9 +57,9 @@ downstream inspection and HDF5 output. the surface list), in which case the HDF5 writer skips them. - `dp_matrix` -- outer-region Δ' matrix used in the analysis - `Q_root` -- tearing eigenvalue(s) in normalized Q - * length `nsurfaces` in `:uncoupled` mode - * length `1` in `:coupled` mode (global eigenvalue normalized by - `params[1].tauk`) + + length `nsurfaces` in `:uncoupled` mode + + length `1` in `:coupled` mode (global eigenvalue normalized by + `params[1].tauk`) - `omega_Hz`, `gamma_Hz` -- physical rotation frequency / growth rate - `per_surface_extraction` -- `Vector{GrowthRateResult}` of length `nsurfaces` in uncoupled mode (each includes polelines, pole list, @@ -51,16 +86,18 @@ struct SLAYERResult coupled_extraction::Union{Nothing,GrowthRateResult} layer_widths::Vector{LayerWidths} scan_data::Vector{Union{ScanResult,AMRResult}} + critical_resonant_field::CriticalResonantFieldResult end # Empty result (enabled=false path) function empty_slayer_result(control::SLAYERControl) return SLAYERResult(false, control, - SLAYERParameters[], - Float64[], Float64[], - zeros(ComplexF64, 0, 0), - ComplexF64[], Float64[], Float64[], - GrowthRateResult[], nothing, - LayerWidths[], - Union{ScanResult,AMRResult}[]) + SLAYERParameters[], + Float64[], Float64[], + zeros(ComplexF64, 0, 0), + ComplexF64[], Float64[], Float64[], + GrowthRateResult[], nothing, + LayerWidths[], + Union{ScanResult,AMRResult}[], + empty_critical_resonant_field_result()) end diff --git a/src/Tearing/Runner/Runner.jl b/src/Tearing/Runner/Runner.jl index 919088065..2d45b3d54 100644 --- a/src/Tearing/Runner/Runner.jl +++ b/src/Tearing/Runner/Runner.jl @@ -27,16 +27,19 @@ using LinearAlgebra using Statistics: mean, median using HDF5 + using FastInterpolations: cubic_interp using ..Utilities using ..Utilities: KineticProfiles -using ...Equilibrium: read_kinetic_file, KineticProfileData using ..InnerLayer -using ..InnerLayer: InnerLayerParameters, InnerLayerResponse, solve_inner, +using ..InnerLayer: + InnerLayerParameters, InnerLayerResponse, solve_inner, SLAYERModel, SLAYERParameters, build_slayer_inputs, GGJModel, GGJParameters, LayerWidths, slayer_layer_thickness import ..build_ggj_inputs # defined at the Tearing level (needs ForceFreeStates) +import ...Equilibrium as Equilibrium +using ...Equilibrium: read_kinetic_file, KineticProfileData, load_kinetic_profiles using ..Dispersion using ..Dispersion: SurfaceCoupling, surface_coupling, MultiSurfaceCoupling, multi_surface_coupling, @@ -44,6 +47,7 @@ using ..Dispersion: SurfaceCoupling, surface_coupling, AMRResult, amr_scan, MultiBoxAMRResult, multi_box_amr_scan, as_amr_result, GrowthRateResult, find_growth_rates +using ..CriticalResonantField: TorqueBalance, torque_balance_scan include("Control.jl") include("Result.jl") @@ -54,5 +58,9 @@ export SLAYERControl, slayer_control_from_toml, validate export SLAYERResult, empty_slayer_result export run_slayer, run_slayer_from_inputs, ggj_inner_deltas export write_slayer_hdf5! +export CriticalResonantFieldControl, critical_resonant_field_control_from_toml +export CriticalResonantFieldResult, empty_critical_resonant_field_result +export run_critical_resonant_field +export write_critical_resonant_field_hdf5! end # module Runner diff --git a/src/Tearing/Runner/run_slayer.jl b/src/Tearing/Runner/run_slayer.jl index 2a29c63a8..b05b6b403 100644 --- a/src/Tearing/Runner/run_slayer.jl +++ b/src/Tearing/Runner/run_slayer.jl @@ -53,7 +53,40 @@ function _load_profiles(control::SLAYERControl, dir_path::AbstractString) cubic_interp(psi_xs, collect(Float64, v)) chi_perp = _chi_spline(data.chi_e) chi_tor = _chi_spline(data.chi_phi) - return (profiles=profiles, chi_perp=chi_perp, chi_tor=chi_tor) + + # If present, load viscosity data for the critical resonant field. + viscous_input = control.critical_resonant_field.viscous_input + if viscous_input isa AbstractString + profile_name = String(viscous_input) + isempty(profile_name) && + error("run_slayer: CriticalResonantField.viscous_input is an empty profile name.") + + viscous_data = HDF5.h5open(path, "r") do f + grp = control.profile_group == "/" ? f : f[control.profile_group] + + haskey(grp, profile_name) || + error("run_slayer: kinetic file '$path' is missing requested " * + "CriticalResonantField profile '$profile_name'.") + + collect(Float64, read(grp[profile_name])) + end + + length(viscous_data) == npsi || + error("run_slayer: CriticalResonantField profile '$profile_name' " * + "has length $(length(viscous_data)), expected $npsi.") + + viscous_input = cubic_interp( + collect(Float64, data.psi), + viscous_data + ) + end + + return ( + profiles=profiles, + chi_perp=chi_perp, + chi_tor=chi_tor, + viscous_input=viscous_input + ) end # --------------------------------------------------------------------- @@ -314,7 +347,7 @@ function run_slayer_from_inputs(params::AbstractVector{<:InnerLayerParameters}, return SLAYERResult(true, control, params, rational_psi, rational_q, dp, Q_root, omega_Hz, gamma_Hz, per_surface_extraction, coupled_extraction, - layer_widths, scan_data_list) + layer_widths, scan_data_list, empty_critical_resonant_field_result()) end # --------------------------------------------------------------------- @@ -346,6 +379,222 @@ function ggj_inner_deltas(params::AbstractVector{GGJParameters}, Q::Number; end return out end +# --------------------------------------------------------------------- +# Critical Resonant Field (Torque-Balance) Workflow +# --------------------------------------------------------------------- +""" + Critical Resonant Field (Torque-Balance) Workflow + + The critical resonant field is the minimum resonant magnetic perturbation + amplitude that can drive a tearing mode unstable. + + Returns a `CriticalResonantFieldResult` containing the critical resonant field + and the corresponding critical resonant field values for each rational surface. +""" + +function run_critical_resonant_field( + equil, intr, ctrl; + dir_path="./", + slayer_result=nothing, + profiles=nothing, + chi_prof=nothing, + viscous_profile=nothing +) + slayer_ctrl = ctrl + ctrl = slayer_ctrl.critical_resonant_field + ctrl.enabled || return empty_critical_resonant_field_result() + + _eval(x, ψ) = x isa Real ? Float64(x) : Float64(x(ψ)) + + profiles === nothing && + throw(ArgumentError("CriticalResonantField requires kinetic profiles.")) + + params = if slayer_result !== nothing && slayer_result.enabled && !isempty(slayer_result.params) + slayer_result.params + else + throw(ArgumentError( + "CriticalResonantField requires SLAYERParameters; " * + "run SLAYER first or provide compatible params." + )) + end + + all(p -> p isa SLAYERParameters, params) || + throw(ArgumentError( + "CriticalResonantField requires SLAYERParameters; " * + "run SLAYER first or provide compatible params." + )) + + surface_index = Int[] + Qpeak_vec = Float64[] + br_vec = Float64[] + Q0_vec = Float64[] + P_vec = Float64[] + scan_data = NamedTuple[] + + chi_vec = nothing + P_input = nothing + use_P = false + + if ctrl.viscous_input_type === "angular_momentum_diffusivity" + + if ctrl.viscous_input === false + chi_vec = nothing + + elseif ctrl.viscous_input isa AbstractArray + length(ctrl.viscous_input) == length(params) || + throw(ArgumentError( + "CriticalResonantField viscous_input has length " * + "$(length(ctrl.viscous_input)), expected $(length(params))." + )) + chi_vec = ctrl.viscous_input + + elseif ctrl.viscous_input isa Number + chi_vec = fill(Float64(ctrl.viscous_input), length(params)) + + elseif ctrl.viscous_input isa String + viscous_profile === nothing && + throw(ArgumentError( + "CriticalResonantField profile " * + "'$(ctrl.viscous_input)' was not loaded." + )) + chi_vec = viscous_profile + + else + throw(ArgumentError( + "Invalid viscous_input for angular_momentum_diffusivity." + )) + end + + elseif ctrl.viscous_input_type === "magnetic_prandtl_number" + + use_P = true + + if ctrl.viscous_input isa AbstractArray + length(ctrl.viscous_input) == length(params) || + throw(ArgumentError( + "CriticalResonantField viscous_input has length " * + "$(length(ctrl.viscous_input)), expected $(length(params))." + )) + P_input = ctrl.viscous_input + + elseif ctrl.viscous_input isa Number + P_input = fill(Float64(ctrl.viscous_input), length(params)) + + elseif ctrl.viscous_input isa String + viscous_profile === nothing && + throw(ArgumentError( + "CriticalResonantField profile " * + "'$(ctrl.viscous_input)' was not loaded." + )) + P_input = viscous_profile + + elseif ctrl.viscous_input === false + use_P = false + + else + throw(ArgumentError( + "Invalid viscous_input for magnetic_prandtl_number." + )) + end + + else + throw(ArgumentError( + "Invalid viscous_input_type: $(ctrl.viscous_input_type). " * + "Must be 'angular_momentum_diffusivity' or " * + "'magnetic_prandtl_number'." + )) + end + + for (isurf, p) in enumerate(params) + + psi_here = intr[isurf].psifac + omega_here = profiles(psi_here).omega + Q0_here = p.tauk * omega_here + eta_here = p.eta + + if use_P + P_here = P_input isa AbstractArray ? + Float64(P_input[isurf]) : + _eval(P_input, psi_here) + + elseif chi_vec === nothing + if chi_prof === nothing + @warn "CriticalResonantField: chi_prof is not provided, using control chi_perp as fallback." + chi_here = slayer_ctrl.chi_perp + else + chi_here = _eval(chi_prof, psi_here) + end + + P_here = (4π * 1e-7) * abs(chi_here) / eta_here + + else + chi_here = chi_vec isa AbstractArray ? + Float64(chi_vec[isurf]) : + _eval(chi_vec, psi_here) + + P_here = (4π * 1e-7) * abs(chi_here) / eta_here + end + + + if P_here < 1 + @warn "CriticalResonantField: P < 1 at surface $isurf (P = $P_here)." + end + + tb = TorqueBalance( + SLAYERModel{:fitzpatrick}(), + p, + Q0_here, + P_here, + p.lu, + p.sval_r + ) + + Qs, bal, Qpeak, br_crit, _, Δs = + torque_balance_scan( + tb; + Qmin=ctrl.Qmin, + Qmax=ctrl.Qmax, + n=ctrl.n + ) + + push!(surface_index, isurf) + push!(Qpeak_vec, Qpeak) + push!(br_vec, br_crit) + push!(Q0_vec, Q0_here) + push!(P_vec, P_here) + + push!( + scan_data, + ( + surface=isurf, + Q=collect(Qs), + balance=collect(bal), + delta=collect(Δs), + Qpeak=Qpeak, + br_crit=br_crit, + Q0=Q0_here, + P=P_here, + lu=p.lu, + sval=p.sval_r, + m=p.m, + n=p.n, + params=p + ) + ) + end + + return CriticalResonantFieldResult( + true, + params, + surface_index, + Qpeak_vec, + br_vec, + Q0_vec, + P_vec, + scan_data + ) +end + # --------------------------------------------------------------------- # Full pipeline: equilibrium + ForceFreeStates → parameters → analysis @@ -453,5 +702,34 @@ function run_slayer(equil, surfaces::AbstractVector, delta_prime_matrix::Abstrac rational_psi = Float64[surfaces[p.ising].psifac for p in params] rational_q = Float64[surfaces[p.ising].q for p in params] + # include critical resonant field workflow here + if control.critical_resonant_field.enabled + slayer_result = run_slayer_from_inputs(params, dp, control; rational_psi=rational_psi, rational_q=rational_q) + crf_result = run_critical_resonant_field(equil, surfaces, control; + dir_path=dir_path, + slayer_result=slayer_result, + profiles=profiles, + chi_prof=chi_perp, + viscous_profile=loaded.viscous_input) + combined_result = SLAYERResult( + slayer_result.enabled, + slayer_result.control, + slayer_result.params, + slayer_result.rational_psi, + slayer_result.rational_q, + slayer_result.dp_matrix, + slayer_result.Q_root, + slayer_result.omega_Hz, + slayer_result.gamma_Hz, + slayer_result.per_surface_extraction, + slayer_result.coupled_extraction, + slayer_result.layer_widths, + slayer_result.scan_data, + crf_result + ) + @info("SLAYER: critical resonant field workflow completed; " * + "critical br values for each rational surface are available in the result.") + return combined_result + end return run_slayer_from_inputs(params, dp, control; rational_psi=rational_psi, rational_q=rational_q) end diff --git a/src/Tearing/Tearing.jl b/src/Tearing/Tearing.jl index 745a30857..3ebd35996 100644 --- a/src/Tearing/Tearing.jl +++ b/src/Tearing/Tearing.jl @@ -6,6 +6,8 @@ # InnerLayer -- pure physics: Δ_inner(Q) for GGJ or SLAYER models # Dispersion -- physics-agnostic scan + contour-intersection root # extraction (consumes any InnerLayerModel) +# CriticalResonantField -- physics-agnostic scan + torque-balance root +# extraction (consumes any InnerLayerModel) # Runner -- user-facing orchestration: TOML config, profile # loading, HDF5 output, workflow hooks # @@ -23,12 +25,14 @@ import ..InnerLayer as InnerLayer include("LayerInputs.jl") include("Dispersion/Dispersion.jl") +include("CriticalResonantField/CriticalResonantField.jl") include("Runner/Runner.jl") import .Dispersion as Dispersion +import .CriticalResonantField as CriticalResonantField import .Runner as Runner -export InnerLayer, Dispersion, Runner +export InnerLayer, Dispersion, CriticalResonantField, Runner export build_ggj_inputs end # module Tearing diff --git a/src/Utilities/GridUtilities.jl b/src/Utilities/GridUtilities.jl index a02bf0d08..71e255699 100644 --- a/src/Utilities/GridUtilities.jl +++ b/src/Utilities/GridUtilities.jl @@ -11,25 +11,27 @@ Power-law spaced grid with analytic derivative calculation. Creates a grid with more points concentrated near boundaries. # Arguments -- `x_min, x_max`: Grid boundaries -- `npower::Int`: Power law exponent (1=linear, higher=more concentration at edges) -- `nx::Int`: Number of grid points -- `spacing::String`: "lower" (concentrate at x_min), "upper" (at x_max), or "both" + + - `x_min, x_max`: Grid boundaries + - `npower::Int`: Power law exponent (1=linear, higher=more concentration at edges) + - `nx::Int`: Number of grid points + - `spacing::String`: "lower" (concentrate at x_min), "upper" (at x_max), or "both" # Returns -- `Matrix{Float64}`: (2, nx) where row 1 = positions, row 2 = dr/d(norm) derivatives + + - `Matrix{Float64}`: (2, nx) where row 1 = positions, row 2 = dr/d(norm) derivatives """ function powspace(x_min::Float64, x_max::Float64, npower::Int, nx::Int, - spacing::String)::Matrix{Float64} + spacing::String)::Matrix{Float64} result = zeros(Float64, 2, nx) y = if spacing == "lower" - collect(range(-1.0, 0.0, length=nx)) + collect(range(-1.0, 0.0; length=nx)) elseif spacing == "upper" - collect(range(0.0, 1.0, length=nx)) + collect(range(0.0, 1.0; length=nx)) elseif spacing == "both" - collect(range(-1.0, 1.0, length=nx)) + collect(range(-1.0, 1.0; length=nx)) else error("Unknown spacing type: $spacing. Use \"lower\", \"upper\", or \"both\".") end @@ -78,10 +80,10 @@ end function _powspace_numeric_integral(y::Float64, npower::Int)::Float64 y == 0.0 && return 0.0 npts = 100 - y_int = range(0.0, y, length=npts) + y_int = range(0.0, y; length=npts) f_int = abs.((y_int .- 1) .* (y_int .+ 1)) .^ npower integral = (f_int[1] + f_int[npts]) / 2 - for i in 2:(npts - 1) + for i in 2:(npts-1) integral += f_int[i] end return integral * (y / (npts - 1)) diff --git a/src/Utilities/HDF5Annotations.jl b/src/Utilities/HDF5Annotations.jl index 1fff44e62..33e055b4f 100644 --- a/src/Utilities/HDF5Annotations.jl +++ b/src/Utilities/HDF5Annotations.jl @@ -7,8 +7,7 @@ Self-describing metadata for `gpec.h5` (the contract in datasets are marked as HDF5 Dimension Scales (netCDF-4 coordinate variables) attached to the arrays that share their axis, so h5py/xarray/HDFView read the file unaided. -Writers stay table-driven: each writer keeps a table of `path => (; long_name, units, -dims)` entries next to it and calls [`annotate!`](@ref) once after its datasets are +Writers stay table-driven: each writer keeps a table of `path => (; long_name, units, dims)` entries next to it and calls [`annotate!`](@ref) once after its datasets are written. Paths absent from the file are skipped silently (many writes are conditional). """ module HDF5Annotations diff --git a/src/Utilities/PhysicalConstants.jl b/src/Utilities/PhysicalConstants.jl index a9668b611..6511e6df2 100644 --- a/src/Utilities/PhysicalConstants.jl +++ b/src/Utilities/PhysicalConstants.jl @@ -10,11 +10,11 @@ All quantities in SI units. module PhysicalConstants # Match the Fortran GPEC/SLAYER sglobal_mod values exactly so cross-code numerical comparison is meaningful. -const MU_0 = 4.0e-7 * π # vacuum permeability [H/m] -const M_E = 9.1094e-31 # electron mass [kg] -const M_P = 1.6726e-27 # proton mass [kg] +const MU_0 = 4.0e-7 * π # vacuum permeability [H/m] +const M_E = 9.1094e-31 # electron mass [kg] +const M_P = 1.6726e-27 # proton mass [kg] const E_CHG = 1.6021917e-19 # elementary charge [C] -const K_B = 1.3807e-23 # Boltzmann constant [J/K] +const K_B = 1.3807e-23 # Boltzmann constant [J/K] const EPS_0 = 8.8542e-12 # vacuum permittivity [F/m] export MU_0, M_E, M_P, E_CHG, K_B, EPS_0 diff --git a/src/Vacuum/PnQuadCache.jl b/src/Vacuum/PnQuadCache.jl index 6e0b8b719..4d6de9850 100644 --- a/src/Vacuum/PnQuadCache.jl +++ b/src/Vacuum/PnQuadCache.jl @@ -26,7 +26,7 @@ const _PN_BGAUS = 2.5 const (_PN_TG02, _PN_WANUMR) = let x32, w32 = gausslegendre(32) tg0 = [_PN_AGAUS + x32[i] * _PN_BGAUS for i in 1:32] - tg02 = NTuple{32,Float64}(t * t for t in tg0) + tg02 = NTuple{32,Float64}(t * t for t in tg0) wanumr = NTuple{32,Float64}(w32[i] * tg0[i] * exp(-tg0[i]^2) for i in 1:32) (tg02, wanumr) end @@ -94,17 +94,19 @@ end function _make_pn_quad_entry(n::Int) @assert n >= 1 "PnQuadEntry is only defined for n ≥ 1 (Γ(1/2 - n) diverges at n = 0)" - inv_2n = 1.0 / (2.0 * n) + inv_2n = 1.0 / (2.0 * n) inv_2np2 = 1.0 / (2.0 * n + 2.0) - sh = Vector{Float64}(undef, 32) - ch = Vector{Float64}(undef, 32) + sh = Vector{Float64}(undef, 32) + ch = Vector{Float64}(undef, 32) shp = Vector{Float64}(undef, 32) chp = Vector{Float64}(undef, 32) @inbounds for ig in 1:32 - x = _PN_TG02[ig] * inv_2n + x = _PN_TG02[ig] * inv_2n xp = _PN_TG02[ig] * inv_2np2 - sh[ig] = sinh(x); ch[ig] = cosh(x) - shp[ig] = sinh(xp); chp[ig] = cosh(xp) + sh[ig] = sinh(x) + ch[ig] = cosh(x) + shp[ig] = sinh(xp) + chp[ig] = cosh(xp) end # Compute the Gamma function Γ(1/2 - n) via the product formula. @@ -117,7 +119,7 @@ function _make_pn_quad_entry(n::Int) # Combine into scale factors used in the final Legendre function assembly. # Pre-computing these once per n avoids repeating the Gamma product on every s-call. sqtwo = sqrt(2.0) - gauss_norm_n = sqtwo / (n * sqpi * gamn) + gauss_norm_n = sqtwo / (n * sqpi * gamn) gauss_norm_np1 = sqtwo / ((n + 1.0) * sqpi * gamp) return PnQuadEntry(sh, ch, shp, chp, gauss_norm_n, gauss_norm_np1) diff --git a/test/runtests_coordinate_invariant.jl b/test/runtests_coordinate_invariant.jl index 39b20330a..63fe773e3 100644 --- a/test/runtests_coordinate_invariant.jl +++ b/test/runtests_coordinate_invariant.jl @@ -43,31 +43,31 @@ end fm = PE.field_space_response_matrices(Λ, L, P, ϱ, S, jarea) @testset "Flux recovery contract (round-trip via R = S·A)" begin - @test R * fm.permeability / R ≈ P rtol = 1e-10 - @test R * fm.surface_inductance * R' ≈ L rtol = 1e-10 - @test R * fm.plasma_inductance * R' ≈ Λ rtol = 1e-10 - @test (R') \ fm.reluctance / R ≈ ϱ rtol = 1e-10 + @test R * fm.permeability / R ≈ P rtol = 1e-10 + @test R * fm.surface_inductance * R' ≈ L rtol = 1e-10 + @test R * fm.plasma_inductance * R' ≈ Λ rtol = 1e-10 + @test (R') \ fm.reluctance / R ≈ ϱ rtol = 1e-10 end @testset "Area-weighted (b̄) recovery via S (= flux/A²)" begin # b̄-space inductance L_b̄ = S·L̃·S† = L/A² since S·R⁻¹ = A⁻¹·I. - @test S * fm.surface_inductance * S' ≈ L ./ jarea^2 rtol = 1e-10 - @test S * fm.plasma_inductance * S' ≈ Λ ./ jarea^2 rtol = 1e-10 - @test S * fm.permeability / S ≈ P rtol = 1e-10 # similarity: A cancels + @test S * fm.surface_inductance * S' ≈ L ./ jarea^2 rtol = 1e-10 + @test S * fm.plasma_inductance * S' ≈ Λ ./ jarea^2 rtol = 1e-10 + @test S * fm.permeability / S ≈ P rtol = 1e-10 # similarity: A cancels end @testset "Internal consistency of the b̃ transform rules" begin - @test fm.permeability ≈ fm.plasma_inductance / fm.surface_inductance rtol = 1e-10 + @test fm.permeability ≈ fm.plasma_inductance / fm.surface_inductance rtol = 1e-10 L̃inv = inv(fm.surface_inductance) - @test fm.reluctance ≈ L̃inv * (fm.plasma_inductance - fm.surface_inductance) * L̃inv rtol = 1e-10 + @test fm.reluctance ≈ L̃inv * (fm.plasma_inductance - fm.surface_inductance) * L̃inv rtol = 1e-10 end @testset "Energy-scalar invariance (flux ↔ b̃)" begin Φ = ComplexF64[cis(0.3k) / k for k in 1:n] b̃ = R \ Φ # root-area-weighted field - @test dot(Φ, L \ Φ) ≈ dot(b̃, fm.surface_inductance \ b̃) rtol = 1e-10 - @test dot(Φ, Λ \ Φ) ≈ dot(b̃, fm.plasma_inductance \ b̃) rtol = 1e-10 - @test dot(Φ, ϱ * Φ) ≈ dot(b̃, fm.reluctance * b̃) rtol = 1e-10 + @test dot(Φ, L \ Φ) ≈ dot(b̃, fm.surface_inductance \ b̃) rtol = 1e-10 + @test dot(Φ, Λ \ Φ) ≈ dot(b̃, fm.plasma_inductance \ b̃) rtol = 1e-10 + @test dot(Φ, ϱ * Φ) ≈ dot(b̃, fm.reluctance * b̃) rtol = 1e-10 end @testset "Three-field vector relations (b, b̃, b̄; flux = A·b̄)" begin @@ -75,9 +75,9 @@ end b̄ = S * b̃ # area-weighted field b = (S .* sqrt(jarea)) \ b̃ # bare field b = Σ⁻¹·b̃ Φ = R * b̃ # poloidal flux - @test Φ ≈ jarea .* b̄ rtol = 1e-12 # Φ = A·b̄ - @test b̄ ≈ Φ ./ jarea rtol = 1e-12 - @test (S .* sqrt(jarea)) * b ≈ b̃ rtol = 1e-12 # Σ·b = b̃ + @test Φ ≈ jarea .* b̄ rtol = 1e-12 # Φ = A·b̄ + @test b̄ ≈ Φ ./ jarea rtol = 1e-12 + @test (S .* sqrt(jarea)) * b ≈ b̃ rtol = 1e-12 # Σ·b = b̃ end end diff --git a/test/runtests_dispersion_amr.jl b/test/runtests_dispersion_amr.jl index 014f3d019..86ed61653 100644 --- a/test/runtests_dispersion_amr.jl +++ b/test/runtests_dispersion_amr.jl @@ -13,7 +13,7 @@ # Small 2×2 initial grid → 9 unique corners amr = amr_scan(counting_f, (-1.0, 1.0), (-1.0, 1.0); - nre0=2, nim0=2, passes=0) + nre0=2, nim0=2, passes=0) @test amr isa AMRResult @test length(amr.cells) == 4 # 2×2 cells # Dedup: 9 unique corners (3×3) @@ -27,7 +27,7 @@ amr0 = amr_scan(f, (-1.0, 1.0), (-1.0, 1.0); nre0=4, nim0=4, passes=0) amr3 = amr_scan(f, (-1.0, 1.0), (-1.0, 1.0); nre0=4, nim0=4, passes=3) @test length(amr3.cells) > length(amr0.cells) - @test length(amr3.Q) > length(amr0.Q) + @test length(amr3.Q) > length(amr0.Q) # A 4×4 coarse grid is 16 cells; adding 3 refinement passes must # leave the total bounded by exponential growth of only the cells # bracketing the root (roughly linear in the path length). @@ -36,26 +36,26 @@ @testset "amr_scan: argument validation" begin @test_throws ArgumentError amr_scan(identity, (0.0, 1.0), (0.0, 1.0); - nre0=0, nim0=2, passes=1) + nre0=0, nim0=2, passes=1) @test_throws ArgumentError amr_scan(identity, (0.0, 1.0), (0.0, 1.0); - nre0=2, nim0=0, passes=1) + nre0=2, nim0=0, passes=1) @test_throws ArgumentError amr_scan(identity, (0.0, 1.0), (0.0, 1.0); - nre0=2, nim0=2, passes=-1) + nre0=2, nim0=2, passes=-1) end @testset "amr_scan: max_cells safety cap fires" begin # A pathological f that forces every cell to subdivide every pass f(Q) = 0.0 + 0.0im # identically zero → every cell crosses @test_throws ErrorException amr_scan(f, (-1.0, 1.0), (-1.0, 1.0); - nre0=4, nim0=4, passes=10, - max_cells=100) + nre0=4, nim0=4, passes=10, + max_cells=100) end @testset "find_growth_rates(AMR): single isolated root" begin Q_root = 0.42 + 0.27im f(Q) = ComplexF64(Q) - Q_root amr = amr_scan(f, (-1.0, 1.5), (-0.5, 1.0); - nre0=8, nim0=6, passes=4) + nre0=8, nim0=6, passes=4) result = find_growth_rates(amr, 1.0) @test result isa GrowthRateResult @test abs(result.Q_root - Q_root) < 1e-3 # AMR-resolution limited @@ -68,7 +68,7 @@ Q2 = -0.4 + 0.1im f(Q) = (ComplexF64(Q) - Q1) * (ComplexF64(Q) - Q2) amr = amr_scan(f, (-1.0, 1.0), (-0.3, 0.8); - nre0=10, nim0=8, passes=4) + nre0=10, nim0=8, passes=4) result = find_growth_rates(amr, 1.0) @test length(result.valid_roots) == 2 @test abs(result.Q_root - Q1) < 1e-2 @@ -79,7 +79,7 @@ Q_p = -0.5 + 0.6im f(Q) = (ComplexF64(Q) - Q_r) / (ComplexF64(Q) - Q_p) amr = amr_scan(f, (-1.5, 1.5), (-0.5, 1.5); - nre0=10, nim0=8, passes=5) + nre0=10, nim0=8, passes=5) result = find_growth_rates(amr, 1.0; pole_threshold=10.0) @test length(result.poles) >= 1 @test any(p -> abs(p - Q_p) < 0.05, result.poles) @@ -90,7 +90,7 @@ Q_root = 1.0 + 2.0im f(Q) = ComplexF64(Q) - Q_root amr = amr_scan(f, (-2.0, 3.0), (-1.0, 4.0); - nre0=8, nim0=8, passes=4) + nre0=8, nim0=8, passes=4) tauk = 5e-5 result = find_growth_rates(amr, tauk) @test result.omega_Hz ≈ real(result.Q_root) / tauk @@ -119,13 +119,13 @@ Q_root = 0.5 + 0.3im f(Q) = ComplexF64(Q) - Q_root scan = brute_force_scan(f, (-1.0, 1.0), (-0.5, 1.0); - nre=80, nim=60, threaded=false) - amr = amr_scan(f, (-1.0, 1.0), (-0.5, 1.0); - nre0=8, nim0=6, passes=4) + nre=80, nim=60, threaded=false) + amr = amr_scan(f, (-1.0, 1.0), (-0.5, 1.0); + nre0=8, nim0=6, passes=4) r_grid = find_growth_rates(scan, 1.0) - r_amr = find_growth_rates(amr, 1.0) + r_amr = find_growth_rates(amr, 1.0) @test abs(r_grid.Q_root - Q_root) < 1e-3 - @test abs(r_amr.Q_root - Q_root) < 1e-3 + @test abs(r_amr.Q_root - Q_root) < 1e-3 @test abs(r_grid.Q_root - r_amr.Q_root) < 5e-3 end @@ -140,22 +140,22 @@ Q_pin = 0.7 - 0.3im sc = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - Q_pin; scale=1.0, tauk=1.0) + Q_pin; scale=1.0, tauk=1.0) amr = amr_scan(sc, (-0.5, 1.5), (-1.0, 0.5); - nre0=8, nim0=6, passes=4) + nre0=8, nim0=6, passes=4) r = find_growth_rates(amr, sc.tauk) @test abs(r.Q_root - Q_pin) < 1e-2 # Multi-surface coupled scan through AMR Q_a, Q_b = 0.7 - 0.3im, -0.4 + 0.5im sc1 = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - ComplexF64(0); scale=1.0, tauk=1.0) + ComplexF64(0); scale=1.0, tauk=1.0) sc2 = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - ComplexF64(0); scale=1.0, tauk=1.0) + ComplexF64(0); scale=1.0, tauk=1.0) dp = ComplexF64[Q_a 0.0; 0.0 Q_b] mc = multi_surface_coupling([sc1, sc2], dp) amr_c = amr_scan(mc, (-1.0, 1.5), (-1.0, 1.0); - nre0=10, nim0=8, passes=4) + nre0=10, nim0=8, passes=4) r_c = find_growth_rates(amr_c, mc.surfaces[mc.ref_idx].tauk) @test abs(r_c.Q_root - Q_b) < 1e-2 # higher-γ root end @@ -175,15 +175,15 @@ # otherwise crosses the entire real axis). f(Q) = (ComplexF64(Q) - 0.0) / (ComplexF64(Q) - (-50.0)) + (1.0 + 1.0im) boxes = [((-75.0, -25.0), (-25.0, 25.0)), - ((-25.0, 25.0), (-25.0, 25.0)), - (( 25.0, 75.0), (-25.0, 25.0))] + ((-25.0, 25.0), (-25.0, 25.0)), + ((25.0, 75.0), (-25.0, 25.0))] result = multi_box_amr_scan(f, boxes; - pole_magnitude_threshold=10.0, - prescreen_nre=25, prescreen_nim=25, - nre0=25, nim0=25, passes=2, - max_cells=100_000, - max_cells_action=:warn_truncate, - parallel=false) + pole_magnitude_threshold=10.0, + prescreen_nre=25, prescreen_nim=25, + nre0=25, nim0=25, passes=2, + max_cells=100_000, + max_cells_action=:warn_truncate, + parallel=false) @test result isa MultiBoxAMRResult @test length(result.box_results) == 3 @test length(result.box_activity) == 3 @@ -212,15 +212,15 @@ # independent of sign-change tests. g(Q) = 1000.0 / (ComplexF64(Q) - (-50.0))^2 + (5.0 + 5.0im) boxes = [((-75.0, -25.0), (-25.0, 25.0)), - ((-25.0, 25.0), (-25.0, 25.0)), - (( 25.0, 75.0), (-25.0, 25.0))] + ((-25.0, 25.0), (-25.0, 25.0)), + ((25.0, 75.0), (-25.0, 25.0))] result = multi_box_amr_scan(g, boxes; - pole_magnitude_threshold=50.0, - prescreen_nre=25, prescreen_nim=25, - nre0=25, nim0=25, passes=1, - max_cells=100_000, - max_cells_action=:warn_truncate, - parallel=false) + pole_magnitude_threshold=50.0, + prescreen_nre=25, prescreen_nim=25, + nre0=25, nim0=25, passes=1, + max_cells=100_000, + max_cells_action=:warn_truncate, + parallel=false) @test result.box_activity[1] != NoActivity @test result.box_activity[2] == NoActivity @test result.box_activity[3] == NoActivity diff --git a/test/runtests_dispersion_coupled.jl b/test/runtests_dispersion_coupled.jl index 5a65539ff..d29745479 100644 --- a/test/runtests_dispersion_coupled.jl +++ b/test/runtests_dispersion_coupled.jl @@ -29,9 +29,9 @@ @testset "Constructor validation" begin sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 1.0+0im; scale=1.0, tauk=1.0) + 1.0+0im; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 2.0+0im; scale=1.0, tauk=1.0) + 2.0+0im; scale=1.0, tauk=1.0) good_dp = ComplexF64[1.0 0.1; 0.1 2.0] mc = multi_surface_coupling([sc1, sc2], good_dp) @@ -41,7 +41,7 @@ # 3-surface default also caps at 3 (min(3, 3) = 3) sc3 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 3.0+0im; scale=1.0, tauk=1.0) + 3.0+0im; scale=1.0, tauk=1.0) good_dp3 = ComplexF64[1.0 0.1 0.0; 0.1 2.0 0.0; 0.0 0.0 3.0] mc3 = multi_surface_coupling([sc1, sc2, sc3], good_dp3) @test mc3.msing_max == 3 @@ -49,16 +49,16 @@ # 4-surface case caps at 3 (the design default — Δ' beyond 3 surfaces # tends to be erratic in practice) sc4 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 4.0+0im; scale=1.0, tauk=1.0) + 4.0+0im; scale=1.0, tauk=1.0) good_dp4 = ComplexF64[1.0 0.0 0.0 0.0; - 0.0 2.0 0.0 0.0; - 0.0 0.0 3.0 0.0; - 0.0 0.0 0.0 4.0] + 0.0 2.0 0.0 0.0; + 0.0 0.0 3.0 0.0; + 0.0 0.0 0.0 4.0] mc4 = multi_surface_coupling([sc1, sc2, sc3, sc4], good_dp4) @test mc4.msing_max == 3 # default capped at 3 # Caller can opt in to all 4 mc4_full = multi_surface_coupling([sc1, sc2, sc3, sc4], good_dp4; - msing_max=4) + msing_max=4) @test mc4_full.msing_max == 4 # Mismatched dp size @@ -69,15 +69,15 @@ # Out-of-range ref_idx @test_throws ArgumentError multi_surface_coupling([sc1, sc2], good_dp; - ref_idx=3) + ref_idx=3) @test_throws ArgumentError multi_surface_coupling([sc1, sc2], good_dp; - ref_idx=0) + ref_idx=0) # Out-of-range msing_max @test_throws ArgumentError multi_surface_coupling([sc1, sc2], good_dp; - msing_max=3) + msing_max=3) @test_throws ArgumentError multi_surface_coupling([sc1, sc2], good_dp; - msing_max=0) + msing_max=0) end @testset "Diagonal Δ' factorizes (det = ∏ per-surface residuals)" begin @@ -85,14 +85,14 @@ # the coupled determinant should reduce exactly to the product of # per-surface residuals. sc1 = surface_coupling(LinTestModel(1.0+0im, 1.0+0im), nothing, - 5.0+0im; scale=1.0, tauk=1.0) + 5.0+0im; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(2.0+0im, 1.0+0im), nothing, - 7.0+0im; scale=1.0, tauk=1.0) + 7.0+0im; scale=1.0, tauk=1.0) sc3 = surface_coupling(LinTestModel(0.5+0im, 0.5+0im), nothing, - 3.0+0im; scale=1.0, tauk=1.0) + 3.0+0im; scale=1.0, tauk=1.0) dp = ComplexF64[5.0 0.0 0.0; - 0.0 7.0 0.0; - 0.0 0.0 3.0] + 0.0 7.0 0.0; + 0.0 0.0 3.0] mc = multi_surface_coupling([sc1, sc2, sc3], dp) for Q in (0.5+0im, 2.0+0.3im, -1.0-0.5im, 4.5+1.0im) @test mc(Q) ≈ sc1(Q) * sc2(Q) * sc3(Q) rtol = 1e-12 @@ -105,9 +105,9 @@ # single-surface roots. Q1, Q2 = 0.5+0.0im, 2.0+0.0im sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - Q1; scale=1.0, tauk=1.0) + Q1; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - Q2; scale=1.0, tauk=1.0) + Q2; scale=1.0, tauk=1.0) dp = ComplexF64[real(Q1) 0.0; 0.0 real(Q2)] mc = multi_surface_coupling([sc1, sc2], dp) @test abs(mc(Q1)) < 1e-12 @@ -117,9 +117,9 @@ @testset "Off-diagonal coupling shifts the roots away from the diagonal" begin sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 0.5+0im; scale=1.0, tauk=1.0) + 0.5+0im; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 2.0+0im; scale=1.0, tauk=1.0) + 2.0+0im; scale=1.0, tauk=1.0) # Coupling-free baseline dp_diag = ComplexF64[0.5 0.0; 0.0 2.0] mc_diag = multi_surface_coupling([sc1, sc2], dp_diag) @@ -138,14 +138,14 @@ @testset "msing_max truncation uses upper-left submatrix" begin sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 1.0+0im; scale=1.0, tauk=1.0) + 1.0+0im; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 2.0+0im; scale=1.0, tauk=1.0) + 2.0+0im; scale=1.0, tauk=1.0) sc3 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 3.0+0im; scale=1.0, tauk=1.0) + 3.0+0im; scale=1.0, tauk=1.0) dp = ComplexF64[1.0 0.0 0.0; - 0.0 2.0 0.0; - 0.0 0.0 3.0] + 0.0 2.0 0.0; + 0.0 0.0 3.0] # msing_max = 1 reduces to sc1(Q) alone mc1 = multi_surface_coupling([sc1, sc2, sc3], dp; msing_max=1) @@ -173,9 +173,9 @@ # M[k,k] = dp_diag_k - scale·Q·(tauk_ref/tauk_k) # Verify against an explicit closed form with mismatched tauks. sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 0.0+0im; scale=1.0, tauk=2.0) # ref tauk + 0.0+0im; scale=1.0, tauk=2.0) # ref tauk sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 0.0+0im; scale=1.0, tauk=4.0) # half rate + 0.0+0im; scale=1.0, tauk=4.0) # half rate dp = ComplexF64[0.0 0.0; 0.0 0.0] mc = multi_surface_coupling([sc1, sc2], dp; ref_idx=1) for Q in (1.0+0im, 0.5+0.3im) @@ -219,8 +219,8 @@ # Pick M[1,1] arbitrarily, solve for M[2,2]: M11 = 0.7 + 0.0im M22 = (c12 * c21) / M11 - dp = ComplexF64[M11+Δ1 c12; - c21 M22+Δ2] + dp = ComplexF64[M11+Δ1 c12; + c21 M22+Δ2] mc = multi_surface_coupling([sc1, sc2], dp) # The constructed M(Q_pin) is exactly singular by construction @@ -244,9 +244,9 @@ @testset "Broadcast over a 2D Q grid" begin # Coupled residual must be broadcast-compatible for PR 5/6 scans. sc1 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 0.0+0im; scale=1.0, tauk=1.0) + 0.0+0im; scale=1.0, tauk=1.0) sc2 = surface_coupling(LinTestModel(0.0im, 1.0+0im), nothing, - 0.0+0im; scale=1.0, tauk=1.0) + 0.0+0im; scale=1.0, tauk=1.0) dp = ComplexF64[0.0 0.0; 0.0 0.0] mc = multi_surface_coupling([sc1, sc2], dp) diff --git a/test/runtests_dispersion_coupled_full.jl b/test/runtests_dispersion_coupled_full.jl index d471650f8..65eec71c8 100644 --- a/test/runtests_dispersion_coupled_full.jl +++ b/test/runtests_dispersion_coupled_full.jl @@ -255,7 +255,7 @@ 0.2 0.1 0.05 0.1 0.3 -6.5] # Non-trivial Q dependence: Δ_t(Q) = a + 0.5·Q, Δ_i(Q) = b + 0.2·Q scs = [surface_coupling(_LinearInnerF(0.3 + 0.01k * im, 0.5 + 0im, - 0.7 + 0.02k * im, 0.2 + 0im), + 0.7 + 0.02k * im, 0.2 + 0im), nothing, 0 + 0im; scale=1.0, tauk=1.0) for k in 1:m] mc = multi_surface_coupling_full(scs, Random_dp) diff --git a/test/runtests_dispersion_polish.jl b/test/runtests_dispersion_polish.jl index 8bc4c9702..8e7ae2e7b 100644 --- a/test/runtests_dispersion_polish.jl +++ b/test/runtests_dispersion_polish.jl @@ -106,7 +106,7 @@ r1 = 0.30 + 0.20im r2 = r1 + 0.002 # second root 0.002 away f(Q) = (ComplexF64(Q) - r1) * (ComplexF64(Q) - r2) - pts = ComplexF64[r1 + 0.0003, r2 - 0.0003] # coarse contour estimates + pts = ComplexF64[r1+0.0003, r2-0.0003] # coarse contour estimates R1 = D._polish_trust_radius(pts, 1, 0.001) R2 = D._polish_trust_radius(pts, 2, 0.001) p1 = D._polish_root(f, pts[1], R1)[1] @@ -124,7 +124,7 @@ im_paths = [ComplexF64[0+0im, 0+0.1im]] # segment length 0.1 @test D._median_segment_length(re_paths, im_paths) ≈ 0.1 # median of [0.1,0.2,0.1] @test D._median_segment_length(Vector{Vector{ComplexF64}}(), - Vector{Vector{ComplexF64}}()) == 0.0 + Vector{Vector{ComplexF64}}()) == 0.0 end # ---------------------------------------------------------------- diff --git a/test/runtests_dispersion_residual.jl b/test/runtests_dispersion_residual.jl index d0235a0da..40cdbfd5d 100644 --- a/test/runtests_dispersion_residual.jl +++ b/test/runtests_dispersion_residual.jl @@ -30,35 +30,35 @@ @testset "Constructor scale defaults" begin # SLAYER: scale = lu^(1/3) so the dimensionless Δ from riccati_f # is mapped to outer ψ-units (Fortran SLAYER growthrates routine) - p_sl = _slayer_ref() + p_sl = _slayer_ref() sc_sl = surface_coupling(SLAYERModel(), p_sl, -1.0 + 0.0im) @test sc_sl.scale ≈ p_sl.lu^(1/3) @test sc_sl.dc == 0.0 @test sc_sl.dp_diag == ComplexF64(-1.0) # GGJ: scale = 1 because rescale_delta is applied inside solve_inner - p_ggj = glasser_wang_2020_eq55() + p_ggj = glasser_wang_2020_eq55() sc_ggj = surface_coupling(GGJModel(solver=:shooting), p_ggj, - -1.0 + 0.0im) + -1.0 + 0.0im) @test sc_ggj.scale == 1.0 # Generic fallback honors explicit scale + dc kwargs sc_lin = surface_coupling(LinearTestModel(0.0im, 1.0+0im), nothing, - 3.0 + 0.0im; dc=0.5, scale=2.0) + 3.0 + 0.0im; dc=0.5, scale=2.0) @test sc_lin.scale == 2.0 @test sc_lin.dc == 0.5 end @testset "Residual arithmetic on synthetic linear model" begin # r(Q) = dp_diag - scale·(a + b·Q) - dc - a, b = 1.0 + 2.0im, -0.5 + 1.0im - scale = 3.0 - dc = 0.25 + a, b = 1.0 + 2.0im, -0.5 + 1.0im + scale = 3.0 + dc = 0.25 Q_root = -0.7 + 0.3im dp_diag = (a + b * Q_root) * scale + dc # construct a known root sc = surface_coupling(LinearTestModel(a, b), nothing, dp_diag; - dc=dc, scale=scale) + dc=dc, scale=scale) @test sc(Q_root) ≈ 0 atol = 1e-12 # Off-root residual matches the closed form @@ -89,14 +89,14 @@ # Both inner-layer models flow through the same SurfaceCoupling # API. Numerical agreement is *not* asserted (different physics) — # only that both pipelines construct and evaluate. - p_sl = _slayer_ref() + p_sl = _slayer_ref() sc_sl = surface_coupling(SLAYERModel(), p_sl, -100.0 + 0.0im) @test sc_sl isa SurfaceCoupling{SLAYERModel{:fitzpatrick},SLAYERParameters} @test sc_sl(0.0 + 0.5im) isa ComplexF64 - p_ggj = glasser_wang_2020_eq55() + p_ggj = glasser_wang_2020_eq55() sc_ggj = surface_coupling(GGJModel(solver=:shooting), p_ggj, - -1.0 + 0.0im) + -1.0 + 0.0im) @test sc_ggj isa SurfaceCoupling{GGJModel{:shooting},GGJParameters} @test sc_ggj(1e-3 + 0.0im) isa ComplexF64 end @@ -106,7 +106,7 @@ # complex-Q grid; verify that broadcasting works element-wise. a, b = 0.0+0im, 1.0+0im sc = surface_coupling(LinearTestModel(a, b), nothing, 2.0+0im; - dc=0.0, scale=1.0) + dc=0.0, scale=1.0) Q_grid = [(qr + qi*im) for qr in -1.0:0.5:1.0, qi in -1.0:0.5:1.0] Δ_grid = sc.(Q_grid) @test size(Δ_grid) == size(Q_grid) diff --git a/test/runtests_dispersion_scan.jl b/test/runtests_dispersion_scan.jl index f50b449fc..00145ad63 100644 --- a/test/runtests_dispersion_scan.jl +++ b/test/runtests_dispersion_scan.jl @@ -7,16 +7,16 @@ @testset "brute_force_scan: regular grid evaluation" begin f(Q) = ComplexF64(Q)^2 - 1 scan = brute_force_scan(f, (-2.0, 2.0), (-1.0, 1.0); - nre=21, nim=11, threaded=false) + nre=21, nim=11, threaded=false) @test scan isa ScanResult @test size(scan.Q) == (21, 11) @test size(scan.Δ) == (21, 11) @test length(scan.re_axis) == 21 @test length(scan.im_axis) == 11 - @test scan.re_axis[1] == -2.0 - @test scan.re_axis[end] == 2.0 - @test scan.im_axis[1] == -1.0 - @test scan.im_axis[end] == 1.0 + @test scan.re_axis[1] == -2.0 + @test scan.re_axis[end] == 2.0 + @test scan.im_axis[1] == -1.0 + @test scan.im_axis[end] == 1.0 # Spot-check a grid value i, j = 11, 6 @test scan.Q[i, j] ≈ scan.re_axis[i] + scan.im_axis[j]*im @@ -26,17 +26,17 @@ @testset "brute_force_scan: threaded vs non-threaded agree" begin f(Q) = sin(ComplexF64(Q)) s_t = brute_force_scan(f, (-1.0, 1.0), (-0.5, 0.5); - nre=15, nim=10, threaded=true) + nre=15, nim=10, threaded=true) s_n = brute_force_scan(f, (-1.0, 1.0), (-0.5, 0.5); - nre=15, nim=10, threaded=false) + nre=15, nim=10, threaded=false) @test s_t.Δ == s_n.Δ end @testset "brute_force_scan: argument validation" begin @test_throws ArgumentError brute_force_scan(identity, (0.0, 1.0), - (0.0, 1.0); nre=1, nim=10) + (0.0, 1.0); nre=1, nim=10) @test_throws ArgumentError brute_force_scan(identity, (0.0, 1.0), - (0.0, 1.0); nre=10, nim=1) + (0.0, 1.0); nre=10, nim=1) end @testset "find_growth_rates: single isolated root" begin @@ -44,7 +44,7 @@ Q_root = 0.42 + 0.27im f(Q) = ComplexF64(Q) - Q_root scan = brute_force_scan(f, (-1.0, 1.5), (-0.5, 1.0); - nre=80, nim=60, threaded=false) + nre=80, nim=60, threaded=false) result = find_growth_rates(scan, 1.0) @test result isa GrowthRateResult @test isempty(result.poles) @@ -60,7 +60,7 @@ Q2 = -0.4 + 0.1im # lower γ f(Q) = (ComplexF64(Q) - Q1) * (ComplexF64(Q) - Q2) scan = brute_force_scan(f, (-1.0, 1.0), (-0.3, 0.8); - nre=100, nim=80, threaded=false) + nre=100, nim=80, threaded=false) result = find_growth_rates(scan, 1.0) @test length(result.valid_roots) == 2 @test abs(result.Q_root - Q1) < 1e-3 # higher-γ root chosen @@ -73,7 +73,7 @@ Q_p = -0.5 + 0.6im # pole at higher γ f(Q) = (ComplexF64(Q) - Q_r) / (ComplexF64(Q) - Q_p) scan = brute_force_scan(f, (-1.5, 1.5), (-0.5, 1.5); - nre=120, nim=100, threaded=false) + nre=120, nim=100, threaded=false) result = find_growth_rates(scan, 1.0; pole_threshold=10.0) # Pole correctly classified — but the root is at lower γ than the # pole, so even with filter_above_poles=true the root must survive. @@ -86,14 +86,14 @@ Q_root = 1.0 + 2.0im f(Q) = ComplexF64(Q) - Q_root scan = brute_force_scan(f, (-2.0, 3.0), (-1.0, 4.0); - nre=80, nim=80, threaded=false) + nre=80, nim=80, threaded=false) tauk = 5.0e-5 result = find_growth_rates(scan, tauk) @test result.omega_Hz ≈ real(result.Q_root) / tauk @test result.gamma_Hz ≈ imag(result.Q_root) / tauk # Check sensible orders of magnitude (Q_root ≈ 1+2im, tauk ≈ 5e-5) - @test result.omega_Hz ≈ 1 / tauk atol = 1 / tauk * 5e-3 - @test result.gamma_Hz ≈ 2 / tauk atol = 2 / tauk * 5e-3 + @test result.omega_Hz ≈ 1 / tauk atol = 1 / tauk * 5e-3 + @test result.gamma_Hz ≈ 2 / tauk atol = 2 / tauk * 5e-3 end @testset "find_growth_rates: empty result when no contour intersections" begin @@ -102,7 +102,7 @@ f(Q) = 1.0 + ComplexF64(Q) # Choose a box where Δ has no zeros — far above the real axis scan = brute_force_scan(f, (1.0, 2.0), (1.0, 2.0); - nre=30, nim=30, threaded=false) + nre=30, nim=30, threaded=false) result = find_growth_rates(scan, 1.0) # Either no valid roots, or a NaN Q_root @test isempty(result.valid_roots) || isnan(real(result.Q_root)) @@ -122,9 +122,9 @@ # Single-surface scan via SurfaceCoupling (Q_root by construction = 0.7-0.3im) Q_pin = 0.7 - 0.3im sc = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - Q_pin; scale=1.0, tauk=1.0) + Q_pin; scale=1.0, tauk=1.0) scan = brute_force_scan(sc, (-0.5, 1.5), (-1.0, 0.5); - nre=80, nim=80, threaded=false) + nre=80, nim=80, threaded=false) res = find_growth_rates(scan, sc.tauk) @test abs(res.Q_root - Q_pin) < 1e-3 @@ -136,13 +136,13 @@ # full complex value, not just its real part). Q_a, Q_b = 0.7 - 0.3im, -0.4 + 0.5im sc1 = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - ComplexF64(0); scale=1.0, tauk=1.0) + ComplexF64(0); scale=1.0, tauk=1.0) sc2 = surface_coupling(LinModel(0.0im, 1.0+0im), nothing, - ComplexF64(0); scale=1.0, tauk=1.0) + ComplexF64(0); scale=1.0, tauk=1.0) dp = ComplexF64[Q_a 0.0; 0.0 Q_b] # diagonal Δ' mc = multi_surface_coupling([sc1, sc2], dp) scan_c = brute_force_scan(mc, (-1.0, 1.5), (-1.0, 1.0); - nre=120, nim=100, threaded=false) + nre=120, nim=100, threaded=false) res_c = find_growth_rates(scan_c, mc.surfaces[mc.ref_idx].tauk) # With diagonal Δ', det = (Q_a - Q)·(Q_b - Q) → roots at Q_a, Q_b. # The higher-γ root is Q_b (γ = 0.5). diff --git a/test/runtests_equil.jl b/test/runtests_equil.jl index af3cd5743..782ae6120 100644 --- a/test/runtests_equil.jl +++ b/test/runtests_equil.jl @@ -143,7 +143,7 @@ @testset "EFIT Method Consistency" begin # All three methods solve the same equilibrium — q-profiles should broadly agree. # Tolerance is 10% to allow for method-specific discretisation differences. - q_efit = plasma_eq_efit.profiles.q_spline.y + q_efit = plasma_eq_efit.profiles.q_spline.y q_arclength = plasma_eq_arclength.profiles.q_spline.y q_inversion = plasma_eq_inversion.profiles.q_spline.y @@ -242,7 +242,7 @@ b0exp = 7.4 # CHEASE normalization field [T] B_nodes_binary = plasma_eq_binary.eqfun_B.nodal_derivs.partials[1, :, :] - B_nodes_ascii = plasma_eq_ascii.eqfun_B.nodal_derivs.partials[1, :, :] + B_nodes_ascii = plasma_eq_ascii.eqfun_B.nodal_derivs.partials[1, :, :] # B field must be finite and positive everywhere @test all(isfinite, B_nodes_binary) @@ -258,7 +258,7 @@ # q must be finite, positive, and in a physically reasonable range q_binary = plasma_eq_binary.profiles.q_spline.y - q_ascii = plasma_eq_ascii.profiles.q_spline.y + q_ascii = plasma_eq_ascii.profiles.q_spline.y @test all(isfinite, q_binary) @test all(isfinite, q_ascii) @test all(>(0), q_binary) @@ -436,7 +436,7 @@ Eq = GeneralizedPerturbedEquilibrium.Equilibrium function build_solovev_equilibrium(; e=1.6, a=0.33, r0=1.0, q0=1.9, - mpsi=64, mtheta=128) + mpsi=64, mtheta=128) eq_config = Eq.EquilibriumConfig(; eq_type="sol", eq_filename="unused", jac_type="pest", grid_type="ldp", @@ -515,24 +515,24 @@ @test pe.params.zsep[1] > pe.params.zsep[2] # Shape parameters — all physically positive quantities. - @test pe.params.amean > 0 - @test pe.params.rmean > 0 + @test pe.params.amean > 0 + @test pe.params.rmean > 0 @test pe.params.aratio > 0 - @test pe.params.kappa > 0 - @test pe.params.kappa ≈ 1.6 rtol=0.02 + @test pe.params.kappa > 0 + @test pe.params.kappa ≈ 1.6 rtol=0.02 # For Solovev (e=1.6, a=0.33, r0=1.0) the shape is approximately # recovered (Shafranov shift loosens the match). @test pe.params.amean ≈ 0.33 rtol=0.15 - @test pe.params.rmean ≈ 1.0 rtol=0.15 + @test pe.params.rmean ≈ 1.0 rtol=0.15 # Consistency with separatrix formulae. @test pe.params.rmean ≈ (pe.params.rsep[1] + pe.params.rsep[2]) / 2 @test pe.params.amean ≈ (pe.params.rsep[1] - pe.params.rsep[2]) / 2 # Beta and field quantities — all physically positive. - @test pe.params.bt0 > 0 - @test pe.params.crnt > 0 + @test pe.params.bt0 > 0 + @test pe.params.crnt > 0 @test pe.params.bwall > 0 @test pe.params.betat > 0 @test pe.params.betan > 0 diff --git a/test/runtests_eulerlagrange.jl b/test/runtests_eulerlagrange.jl index 6547352e6..6794ec296 100644 --- a/test/runtests_eulerlagrange.jl +++ b/test/runtests_eulerlagrange.jl @@ -494,7 +494,7 @@ end # materialize after the Gaussian fixups and free-boundary normalization rather than # transforming stored derivatives alongside u_store. npert = intr.numpert_total - T = Matrix{ComplexF64}(I, npert, npert) .+ 0.25 .* ComplexF64.(reshape(sin.(1:npert^2), npert, npert)) + T = Matrix{ComplexF64}(I, npert, npert) .+ 0.25 .* ComplexF64.(reshape(sin.(1:(npert^2)), npert, npert)) odet_t = deepcopy(odet_pristine) for istep in 1:odet_t.step odet_t.u_store[:, :, 1, istep] = odet_t.u_store[:, :, 1, istep] * T diff --git a/test/runtests_imas.jl b/test/runtests_imas.jl index 2d82d5b0e..f261cc45a 100644 --- a/test/runtests_imas.jl +++ b/test/runtests_imas.jl @@ -20,31 +20,31 @@ using GeneralizedPerturbedEquilibrium.Equilibrium # COCOS 11: ψ_IMAS = 2π × ψ_internal psi_axis_int = 0.0 - psi_bnd_int = 1.5 # Wb/rad + psi_bnd_int = 1.5 # Wb/rad cf = cocos == 11 ? 2π : 1.0 - eqt.global_quantities.psi_axis = psi_axis_int * cf - eqt.global_quantities.psi_boundary = psi_bnd_int * cf + eqt.global_quantities.psi_axis = psi_axis_int * cf + eqt.global_quantities.psi_boundary = psi_bnd_int * cf nw = 64 psi_1d = collect(LinRange(psi_axis_int, psi_bnd_int, nw)) - eqt.profiles_1d.psi = psi_1d .* cf - eqt.profiles_1d.f = fill(5.0, nw) + eqt.profiles_1d.psi = psi_1d .* cf + eqt.profiles_1d.f = fill(5.0, nw) eqt.profiles_1d.pressure = collect(LinRange(1e4, 0.0, nw)) - eqt.profiles_1d.q = collect(LinRange(1.0, 3.0, nw)) + eqt.profiles_1d.q = collect(LinRange(1.0, 3.0, nw)) # Minimal 2D ψ(R,Z) — rough circular flux surfaces nR, nZ = 32, 40 R_grid = collect(LinRange(1.0, 2.5, nR)) Z_grid = collect(LinRange(-0.8, 0.8, nZ)) - Rmag = 1.75 + Rmag = 1.75 psi_rz = [min((R - Rmag)^2 / 0.6 + Z^2 / 0.7, psi_bnd_int * 1.4) for R in R_grid, Z in Z_grid] resize!(eqt.profiles_2d, 1; wipe=true) - prof2d = eqt.profiles_2d[1] - prof2d.grid.dim1 = R_grid - prof2d.grid.dim2 = Z_grid - prof2d.psi = psi_rz .* cf + prof2d = eqt.profiles_2d[1] + prof2d.grid.dim1 = R_grid + prof2d.grid.dim2 = Z_grid + prof2d.psi = psi_rz .* cf return dd, psi_bnd_int end @@ -58,8 +58,8 @@ using GeneralizedPerturbedEquilibrium.Equilibrium # psio should equal |psi_boundary - psi_axis| in internal (COCOS 2) units @test isapprox(result.psio, psi_bnd_int; rtol=1e-6) - @test result.rmin ≈ 1.0 atol=1e-9 - @test result.rmax ≈ 2.5 atol=1e-9 + @test result.rmin ≈ 1.0 atol=1e-9 + @test result.rmax ≈ 2.5 atol=1e-9 end # Test 2: read_imas — COCOS 2 input requires no conversion @@ -121,7 +121,7 @@ using GeneralizedPerturbedEquilibrium.Equilibrium GeneralizedPerturbedEquilibrium.write_imas(dd, mock_result) - @test dd.mhd_linear.code.name == "GPEC" + @test dd.mhd_linear.code.name == "GPEC" @test dd.mhd_linear.ideal_flag == 1 @test length(dd.mhd_linear.time_slice) == 1 @@ -142,7 +142,7 @@ using GeneralizedPerturbedEquilibrium.Equilibrium # et sorted ascending (least stable globally first). # n=1 (j=0) has eigenvalues: real=0.3 (idx 1) and real=0.6 (idx 3) # n=2 (j=1) has eigenvalues: real=0.5 (idx 2) and real=0.7 (idx 4) - mock_et = [0.3+0im, 0.5+0im, 0.6+0im, 0.7+0im] + mock_et = [0.3+0im, 0.5+0im, 0.6+0im, 0.7+0im] mock_n_idx = [0, 1, 0, 1] mock_result = ( ffs = (integrator=:forward, free_boundary=(et=mock_et, n_tor_idx=mock_n_idx), @@ -190,9 +190,9 @@ using GeneralizedPerturbedEquilibrium.Equilibrium ) GeneralizedPerturbedEquilibrium.write_imas(dd_multi, result_multi) - ts_single = dd_single.mhd_linear.time_slice[1] + ts_single = dd_single.mhd_linear.time_slice[1] ts_single2 = dd_single2.mhd_linear.time_slice[1] - ts_multi = dd_multi.mhd_linear.time_slice[1] + ts_multi = dd_multi.mhd_linear.time_slice[1] # n=1 energy from combined run must match the standalone n=1 run @test ts_multi.toroidal_mode[1].energy_perturbed == ts_single.toroidal_mode[1].energy_perturbed diff --git a/test/runtests_kinetic.jl b/test/runtests_kinetic.jl index 29955c2c7..7f1ac8fcf 100644 --- a/test/runtests_kinetic.jl +++ b/test/runtests_kinetic.jl @@ -26,7 +26,7 @@ @testset "endpoint modes" begin pts_lower, _ = KF.powspace(0.0, 1.0, 2, 50, "lower") pts_upper, _ = KF.powspace(0.0, 1.0, 2, 50, "upper") - pts_both, _ = KF.powspace(0.0, 1.0, 2, 50, "both") + pts_both, _ = KF.powspace(0.0, 1.0, 2, 50, "both") # All modes should span the full range for pts in [pts_lower, pts_upper, pts_both] @@ -194,11 +194,11 @@ # independent reference for the u-substitution + pole-extraction result. wn, wt, we, wd, wb, nuk, leff, n = 0.5, 0.8, -2.0, 0.5, 1.0, 0.3, 1.0, 1 p = KF.EnergyParams(wn, wt, we, wd, wb, nuk, leff, n, - "harmonic", "maxwellian", 1.0, 0.0, false) + "harmonic", "maxwellian", 1.0, 0.0, false) x_res = KF.find_resonance_energies(leff, wb, n, we, wd) @test length(x_res) == 1 # this case has exactly one resonance reference, _ = KF.quadgk(x -> KF.energy_integrand_scalar(x, p), - 0.0, x_res[1], Inf; rtol=1e-12, atol=1e-14) + 0.0, x_res[1], Inf; rtol=1e-12, atol=1e-14) result = KF.integrate_energy( wn, wt, we, wd, wb, nuk, 0, leff, n, 0.5, 0.5, "fgar"; nutype="harmonic", f0type="maxwellian", atol=1e-12, rtol=1e-10 @@ -317,7 +317,7 @@ @test all(isfinite, vals) # Element-wise match against the scalar form documented in the docstring. p = KF.EnergyParams(0.5, 0.8, -2.0, 0.5, 1.0, 0.3, 1.0, 1, - "harmonic", "maxwellian", 1.0, 0.0, false) + "harmonic", "maxwellian", 1.0, 0.0, false) @test vals ≈ [KF.energy_integrand_scalar(x, p) for x in x_grid] end diff --git a/test/runtests_parallel_integration.jl b/test/runtests_parallel_integration.jl index 7e7737e06..616cc28d6 100644 --- a/test/runtests_parallel_integration.jl +++ b/test/runtests_parallel_integration.jl @@ -114,9 +114,12 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -201,9 +204,12 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -262,11 +268,14 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -311,7 +320,10 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) # Apply the two-pass auto grid exactly as the main driver does (the example ships # grid_type="auto", mpsi=0): measured-curvature refinement with rational surfaces # pinned as mandatory knots, re-formed from the captured ingest. The pinned values @@ -324,8 +336,8 @@ using TOML end intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -372,9 +384,12 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -423,11 +438,14 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], example_dir) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) @@ -476,7 +494,10 @@ using TOML ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex) - equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config, haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing) + equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium( + eq_config, + haskey(inputs, "SOL_INPUT") ? GeneralizedPerturbedEquilibrium.Equilibrium.SolovevConfig(inputs["SOL_INPUT"]) : nothing + ) # Apply the two-pass auto grid (measured-curvature refinement, rational surfaces pinned # as mandatory knots) exactly as the main driver does (see the FM testset above); the # pinned values below are for this grid, the production default. @@ -488,8 +509,8 @@ using TOML end intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil) GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil) diff --git a/test/runtests_rerun_from_h5.jl b/test/runtests_rerun_from_h5.jl index dc242426f..db10e3c9d 100644 --- a/test/runtests_rerun_from_h5.jl +++ b/test/runtests_rerun_from_h5.jl @@ -102,7 +102,11 @@ end inputs, = GeneralizedPerturbedEquilibrium.build_inputs_from_h5([source_h5, "--output-dir", out_dir, "--override", "ForceFreeStates.use_parallel=true"]) @test inputs["ForceFreeStates"]["use_parallel"] == true - @test_logs (:warn,) GeneralizedPerturbedEquilibrium._drop_deprecated_keys!(inputs["ForceFreeStates"], GeneralizedPerturbedEquilibrium._DEPRECATED_FFS_KEYS, "ForceFreeStates") + @test_logs (:warn,) GeneralizedPerturbedEquilibrium._drop_deprecated_keys!( + inputs["ForceFreeStates"], + GeneralizedPerturbedEquilibrium._DEPRECATED_FFS_KEYS, + "ForceFreeStates" + ) @test !haskey(inputs["ForceFreeStates"], "use_parallel") # With the key gone the control struct builds again, on the default integrator. diff --git a/test/runtests_riccati.jl b/test/runtests_riccati.jl index afd084056..69edfb972 100644 --- a/test/runtests_riccati.jl +++ b/test/runtests_riccati.jl @@ -9,8 +9,8 @@ function make_solovev_intr(inputs, ctrl, equil, ex) intr = FFS.ForceFreeStatesInternal(; dir_path=ex) intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) - intr.nlow = ctrl.nn_low; - intr.nhigh = ctrl.nn_high; + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high intr.npert = 1 FFS.sing_lim!(intr, ctrl, equil) FFS.sing_find!(intr, equil) diff --git a/test/runtests_sing.jl b/test/runtests_sing.jl index 83b85a765..18ce06c59 100644 --- a/test/runtests_sing.jl +++ b/test/runtests_sing.jl @@ -41,14 +41,14 @@ using FastInterpolations: cubic_interp, CubicFit, LinearBinarySearch, Series, Ex function read_solutions_3d(fname::String) lines = readlines(fname) - blocks = Vector{Vector{Vector{Float64}}}(); + blocks = Vector{Vector{Vector{Float64}}}() current = Vector{Vector{Float64}}() for s in lines - t = strip(s); + t = strip(s) isempty(t) && continue if occursin("Solution index", t) if !isempty(current) - push!(blocks, current); + push!(blocks, current) current = Vector{Vector{Float64}}() end continue @@ -62,11 +62,11 @@ using FastInterpolations: cubic_interp, CubicFit, LinearBinarySearch, Series, Ex if !isempty(current) push!(blocks, current) end - mpert = length(blocks[1]); + mpert = length(blocks[1]) nsol = length(blocks) result = Array{ComplexF64}(undef, mpert, nsol, 2) for (j, block) in enumerate(blocks), (i, row) in enumerate(block) - result[i, j, 1] = complex(row[2], row[3]); + result[i, j, 1] = complex(row[2], row[3]) result[i, j, 2] = complex(row[4], row[5]) end return result @@ -105,7 +105,7 @@ using FastInterpolations: cubic_interp, CubicFit, LinearBinarySearch, Series, Ex odet = GeneralizedPerturbedEquilibrium.ForceFreeStates.OdeState(; numpert_total=intr.numpert_total, numsteps_init=ctrl.numsteps_init, numunorms_init=ctrl.numunorms_init, msing=intr.msing) - psifac_dummy = collect(range(0, 1, 10)); + psifac_dummy = collect(range(0, 1, 10)) points = length(psifac_dummy) amat = read_complex_fortran(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/amat.dat")) amats = copyForSplines(amat, psifac_dummy) @@ -115,7 +115,7 @@ using FastInterpolations: cubic_interp, CubicFit, LinearBinarySearch, Series, Ex cmats = copyForSplines(cmat, psifac_dummy) fmat = read_complex_fortran(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/fmat.dat")) - fmat .= cholesky(Hermitian(fmat)).L; + fmat .= cholesky(Hermitian(fmat)).L fmats = copyForSplines(fmat, psifac_dummy) kmat = read_complex_fortran(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/kmat.dat")) kmats = copyForSplines(kmat, psifac_dummy) @@ -125,7 +125,7 @@ using FastInterpolations: cubic_interp, CubicFit, LinearBinarySearch, Series, Ex umat_p1 = read_complex_fortran(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/umat_p1.dat")) umat_p2 = read_complex_fortran(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/umat_p2.dat")) odet.psifac = extract_value(joinpath(@__DIR__, "test_data/sing_der_testing/mat_dat/sing_der_output_normal.dat"), "psifac") - odet.u[:, :, 1] .= umat_p1; + odet.u[:, :, 1] .= umat_p1 odet.u[:, :, 2] .= umat_p2 ffit = GeneralizedPerturbedEquilibrium.ForceFreeStates.FourFitVars(; mpert=intr.numpert_total, numpert_total=intr.numpert_total) diff --git a/test/runtests_tj_analytic.jl b/test/runtests_tj_analytic.jl index 5bbcb25d2..b36702001 100644 --- a/test/runtests_tj_analytic.jl +++ b/test/runtests_tj_analytic.jl @@ -19,12 +19,12 @@ using GeneralizedPerturbedEquilibrium.Equilibrium: TJAnalyticConfig, Equilibrium @testset "TJ-analytic model" begin @testset "tj_analytic_run (inverse) — basic invariants at ε = 0.25" begin # Keep ε, mpsi, mtheta modest so the whole block runs in ~1 s. - tj = TJAnalyticConfig(lar_r0 = 1.0 / 0.25, lar_a = 1.0, - qc = 1.5, qa = 3.6, pc = 0.001, mu = 2.0, B0 = 12.0, - ma = 64, mtau = 64) - eq = EquilibriumConfig(eq_type = "tj_analytic", - psilow = 0.01, psihigh = 0.995, - mpsi = 64, mtheta = 128, etol = 1e-7) + tj = TJAnalyticConfig(lar_r0=1.0 / 0.25, lar_a=1.0, + qc=1.5, qa=3.6, pc=0.001, mu=2.0, B0=12.0, + ma=64, mtau=64) + eq = EquilibriumConfig(eq_type="tj_analytic", + psilow=0.01, psihigh=0.995, + mpsi=64, mtheta=128, etol=1e-7) pe = setup_equilibrium(eq, tj) # psio is a physical-scale ψ; regressions in the a→a² normalization @@ -33,12 +33,12 @@ using GeneralizedPerturbedEquilibrium.Equilibrium: TJAnalyticConfig, Equilibrium @test isfinite(pe.psio) # ν root-find pins q₂(x=1) = qa; qmax at psihigh=0.995 lands ~0.04 below. - @test pe.params.q0 ≈ 1.5 rtol = 1e-3 + @test pe.params.q0 ≈ 1.5 rtol = 1e-3 @test pe.params.qmax > 3.5 @test pe.params.qmax < 3.7 # Magnetic axis at R = R0, Z = 0 for the shifted-circle benchmark. - @test pe.ro ≈ 4.0 rtol = 1e-3 + @test pe.ro ≈ 4.0 rtol = 1e-3 @test abs(pe.zo) < 1e-8 end @@ -46,12 +46,12 @@ using GeneralizedPerturbedEquilibrium.Equilibrium: TJAnalyticConfig, Equilibrium # ε = 0.60 sits on the stable side of the ideal-external-kink pole at # ε ≈ 0.665 for this (qc, qa, pc, μ) combination. Pole-approach shape # (δW_t small, Δ' > 0 and growing) is the Option B success criterion. - tj = TJAnalyticConfig(lar_r0 = 1.0 / 0.60, lar_a = 1.0, - qc = 1.5, qa = 3.6, pc = 0.001, mu = 2.0, B0 = 12.0, - ma = 64, mtau = 64) - eq = EquilibriumConfig(eq_type = "tj_analytic_direct", - psilow = 0.01, psihigh = 0.995, - mpsi = 64, mtheta = 128, etol = 1e-7) + tj = TJAnalyticConfig(lar_r0=1.0 / 0.60, lar_a=1.0, + qc=1.5, qa=3.6, pc=0.001, mu=2.0, B0=12.0, + ma=64, mtau=64) + eq = EquilibriumConfig(eq_type="tj_analytic_direct", + psilow=0.01, psihigh=0.995, + mpsi=64, mtheta=128, etol=1e-7) pe = setup_equilibrium(eq, tj) @test pe.psio > 0 @@ -59,13 +59,13 @@ using GeneralizedPerturbedEquilibrium.Equilibrium: TJAnalyticConfig, Equilibrium # Direct-GS line integration at ε=0.60 gives qmax between 3.8 and 4.0. # If the εa³·L shape terms in f_R / f_Z regress, qmax jumps above 5. - @test pe.params.q0 ≈ 1.5 rtol = 1e-2 + @test pe.params.q0 ≈ 1.5 rtol = 1e-2 @test pe.params.qmax > 3.75 @test pe.params.qmax < 4.1 # Magnetic axis at R = R0. Shafranov shift of the O-point itself is # zero by construction (H₁(0) = 0). - @test pe.ro ≈ (1.0 / 0.60) rtol = 1e-3 + @test pe.ro ≈ (1.0 / 0.60) rtol = 1e-3 @test abs(pe.zo) < 1e-4 end @@ -73,18 +73,18 @@ using GeneralizedPerturbedEquilibrium.Equilibrium: TJAnalyticConfig, Equilibrium # At the magnetic axis ψ_in should equal psio (axis convention: ψ # positive at axis, zero at LCFS); sampling well outside the LCFS should # give a negative value (the vacuum branch of psi_rz). - tj = TJAnalyticConfig(lar_r0 = 1.0 / 0.25, lar_a = 1.0, - qc = 1.5, qa = 3.6, pc = 0.001, mu = 2.0, B0 = 12.0, - ma = 64, mtau = 64) - eq = EquilibriumConfig(eq_type = "tj_analytic_direct", - psilow = 0.01, psihigh = 0.995, - mpsi = 64, mtheta = 128, etol = 1e-7) + tj = TJAnalyticConfig(lar_r0=1.0 / 0.25, lar_a=1.0, + qc=1.5, qa=3.6, pc=0.001, mu=2.0, B0=12.0, + ma=64, mtau=64) + eq = EquilibriumConfig(eq_type="tj_analytic_direct", + psilow=0.01, psihigh=0.995, + mpsi=64, mtheta=128, etol=1e-7) inp = tj_analytic_run_direct(eq, tj) # ψ at the geometric axis matches psio (see DirectRunInput docstring for # the sign convention: psi_in is positive at axis, zero at LCFS). R0 = 1.0 / 0.25 - @test inp.psi_in((R0, 0.0)) ≈ inp.psio rtol = 1e-3 + @test inp.psi_in((R0, 0.0)) ≈ inp.psio rtol = 1e-3 # Well outside the LCFS → negative ψ_in (vacuum branch of the grid). R_out = R0 + 1.05 # plasma LCFS is at R ≈ R0 + 0.94