From 10e8b4c9e2a00e8aca91f9648b0dd816c8372b26 Mon Sep 17 00:00:00 2001 From: logan-nc Date: Fri, 13 Nov 2026 23:10:29 -0500 Subject: [PATCH 1/3] Repo - PERF - Cover the perturbed equilibrium, kinetic torque and Galerkin paths in the precompile workload The workload ran a Solovev case through the Riccati and forward stability solves only, so the first full run in a fresh process compiled the coil forcing, the perturbed equilibrium, the kinetic torque and the Galerkin solve on first use. Add a Galerkin pass on the Solovev case and a coarse TJ-analytic case with kinetic profiles and C-coil forcing that runs the response and the torque. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01BgNxCG7pGc8HRXGZ3r3psj --- docs/src/developer_notes.md | 4 ++- src/Precompile.jl | 69 ++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/docs/src/developer_notes.md b/docs/src/developer_notes.md index e84e98d68d..ad1804d6be 100644 --- a/docs/src/developer_notes.md +++ b/docs/src/developer_notes.md @@ -94,7 +94,9 @@ a comparison, and the exit-status contract are documented in ### Precompile workload -Building the package runs a small Solovev case through the solver so that a fresh Julia process +Building the package runs two small cases through every stage of a full run (a Solovev case through +the Riccati, forward and Galerkin stability solves, and a TJ-analytic case with kinetic profiles and +coil forcing through the perturbed equilibrium and the kinetic torque) so that a fresh Julia process skips most first-call compilation, at the cost of a longer one-time build. Code compiled into the package image can round differently in the last bit from code compiled on first use, and near-marginal quantities such as Δ′ amplify that, so results are reproducible only within one diff --git a/src/Precompile.jl b/src/Precompile.jl index 79091d88d5..1e1e5dfd79 100644 --- a/src/Precompile.jl +++ b/src/Precompile.jl @@ -1,12 +1,26 @@ -# Precompile workload: run a small Solovev case through the Riccati and forward formalisms at -# build time so a fresh process skips most first-call compilation. Disable with the -# PrecompileTools preference `precompile_workload = false` (see docs/src/set_up.md). +# Precompile workload: run small cases through each stage at build time so a fresh process skips most +# first-call compilation. A Solovev case covers the Riccati, forward and Galerkin formalisms; a TJ-analytic +# case with kinetic profiles and coil forcing covers the perturbed equilibrium and the kinetic torque. +# Disable with the PrecompileTools preference `precompile_workload = false` (see docs/src/set_up.md). using PrecompileTools: @setup_workload, @compile_workload import Logging @setup_workload begin + # Run one deck silently in a scratch directory; `files` are written beside it first. + function run_workload(inputs; files=()) + mktempdir() do workdir + foreach(((name, text),) -> write(joinpath(workdir, name), text), files) + open(io -> TOML.print(io, inputs), joinpath(workdir, "gpec.toml"), "w") + Logging.with_logger(Logging.NullLogger()) do + redirect_stdio(; stdout=devnull) do + return main([workdir]) + end + end + end + end + # examples/Solovev_ideal_example, coarsened in mpsi and mtheta_vacuum to keep the build short. - inputs = Dict{String,Any}( + stability = Dict{String,Any}( "Equilibrium" => Dict{String,Any}( "jac_type" => "pest", "grid_type" => "core_edge_packed", "psilow" => 1e-4, "psihigh" => 0.9995, "mpsi" => 16, "mtheta" => 256, "newq0" => 0, "flux_surface_rtol" => 1e-7 @@ -18,20 +32,43 @@ import Logging ), "Solovev" => Dict{String,Any}("mr" => 128, "mz" => 128, "ma" => 128, "elongation" => 1.6, "a" => 0.33, "r0" => 1.0, "q0" => 1.9) ) + + # examples/TJ_kinetic_NTV_example, coarsened, at a DIII-D-like size so the DIII-D C-coil forcing applies. + kinetic_table = join(("$(x) 1.2e19 1.2e19 $(20 + 180 * (1 - x)) $(20 + 180 * (1 - x)) $(2e4 * (1 - x))" for x in range(0, 1; length=12)), "\n") + response = Dict{String,Any}( + "Equilibrium" => Dict{String,Any}( + "kinetic_file" => "kinetic.dat", "jac_type" => "hamada", "grid_type" => "core_edge_packed", + "psilow" => 0.01, "psihigh" => 0.99, "mpsi" => 16, "mtheta" => 64 + ), + "TJAnalytic" => Dict{String,Any}( + "r0" => 1.7, "a" => 0.17, "q0" => 1.6, "qa" => 3.2, "p0" => 0.1, "pressure_peaking" => 2.0, "B0" => 1.0, + "ma" => 32, "mtheta" => 64 + ), + "ForceFreeStates" => Dict{String,Any}( + "vac_flag" => true, "qlow" => 1.02, "qhigh" => 1e3, "nn" => 1, "delta_mlow" => 4, "delta_mhigh" => 4, + "mtheta_vacuum" => 64, "singfac_min" => 1e-4, "integrator" => "Forward", "truncate_at_rational_offset" => false + ), + "Forward" => Dict{String,Any}("eulerlagrange_tolerance" => 1e-6), + "ForcingTerms" => Dict{String,Any}( + "forcing_data_format" => "coil", "machine" => "d3d", "mtheta_coil" => 32, "nzeta_coil" => 8, + "coil_set" => [Dict{String,Any}("name" => "c", "currents" => [1000.0, 500.0, -500.0, -1000.0, -500.0, 500.0])] + ), + "PerturbedEquilibrium" => Dict{String,Any}("compute_singular_coupling" => true, "regularization_width" => 0.05), + "KineticForces" => Dict{String,Any}( + "bounce_harmonic_max" => 1, "electron" => false, "nutype" => "harmonic", "f0type" => "maxwellian", + "atol_pitch" => 1e-4, "axis_validity_suppression" => true + ) + ) + @compile_workload begin - for formalism in ("Riccati", "Forward") - inputs["ForceFreeStates"]["integrator"] = formalism - foreach(name -> delete!(inputs, name), ("Riccati", "Forward")) - inputs[formalism] = Dict{String,Any}("renorm_threshold" => 1e3, "eulerlagrange_tolerance" => 1e-7) - mktempdir() do workdir - open(io -> TOML.print(io, inputs), joinpath(workdir, "gpec.toml"), "w") - Logging.with_logger(Logging.NullLogger()) do - redirect_stdio(; stdout=devnull) do - return main([workdir]) - end - end - end + for formalism in ("Riccati", "Forward", "Galerkin") + stability["ForceFreeStates"]["integrator"] = formalism + foreach(name -> delete!(stability, name), ("Riccati", "Forward", "Galerkin")) + stability[formalism] = formalism == "Galerkin" ? Dict{String,Any}("nx" => 32) : + Dict{String,Any}("renorm_threshold" => 1e3, "eulerlagrange_tolerance" => 1e-7) + run_workload(stability) end + run_workload(response; files=("kinetic.dat" => kinetic_table,)) end # Start each session with empty caches, as a JIT build does, instead of the workload's run state. Vacuum.reset_caches!() From 3311015be5364a548aa647dcd5991da80a348804 Mon Sep 17 00:00:00 2001 From: logan-nc Date: Fri, 13 Nov 2026 23:41:36 -0500 Subject: [PATCH 2/3] Repo - PERF - Include the Galerkin coil-response columns in the precompile workload Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01BgNxCG7pGc8HRXGZ3r3psj --- src/Precompile.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Precompile.jl b/src/Precompile.jl index 1e1e5dfd79..9c467a5f7a 100644 --- a/src/Precompile.jl +++ b/src/Precompile.jl @@ -64,7 +64,7 @@ import Logging for formalism in ("Riccati", "Forward", "Galerkin") stability["ForceFreeStates"]["integrator"] = formalism foreach(name -> delete!(stability, name), ("Riccati", "Forward", "Galerkin")) - stability[formalism] = formalism == "Galerkin" ? Dict{String,Any}("nx" => 32) : + stability[formalism] = formalism == "Galerkin" ? Dict{String,Any}("nx" => 32, "rpec_flag" => true) : Dict{String,Any}("renorm_threshold" => 1e3, "eulerlagrange_tolerance" => 1e-7) run_workload(stability) end From b066ad812297c8f0095d4c968a2e0fa555efac3d Mon Sep 17 00:00:00 2001 From: logan-nc Date: Sat, 14 Nov 2026 03:39:58 -0500 Subject: [PATCH 3/3] Repo - PERF - Run the precompile workload's response case on the direct equilibrium solver The TJ-analytic case compiled the inverse-equilibrium path into the package image, which changed the last bit of a re-formed inverse equilibrium against a freshly read one. The direct solver leaves that path compiled on first use. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01BgNxCG7pGc8HRXGZ3r3psj --- src/Precompile.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Precompile.jl b/src/Precompile.jl index 9c467a5f7a..04bddb8f60 100644 --- a/src/Precompile.jl +++ b/src/Precompile.jl @@ -33,7 +33,8 @@ import Logging "Solovev" => Dict{String,Any}("mr" => 128, "mz" => 128, "ma" => 128, "elongation" => 1.6, "a" => 0.33, "r0" => 1.0, "q0" => 1.9) ) - # examples/TJ_kinetic_NTV_example, coarsened, at a DIII-D-like size so the DIII-D C-coil forcing applies. + # examples/TJ_kinetic_NTV_example, coarsened, at a DIII-D-like size so the DIII-D C-coil forcing applies, and on the + # direct solver, so the inverse-solver path stays compiled on first use as before. kinetic_table = join(("$(x) 1.2e19 1.2e19 $(20 + 180 * (1 - x)) $(20 + 180 * (1 - x)) $(2e4 * (1 - x))" for x in range(0, 1; length=12)), "\n") response = Dict{String,Any}( "Equilibrium" => Dict{String,Any}( @@ -42,7 +43,7 @@ import Logging ), "TJAnalytic" => Dict{String,Any}( "r0" => 1.7, "a" => 0.17, "q0" => 1.6, "qa" => 3.2, "p0" => 0.1, "pressure_peaking" => 2.0, "B0" => 1.0, - "ma" => 32, "mtheta" => 64 + "ma" => 32, "mtheta" => 64, "solver" => "direct" ), "ForceFreeStates" => Dict{String,Any}( "vac_flag" => true, "qlow" => 1.02, "qhigh" => 1e3, "nn" => 1, "delta_mlow" => 4, "delta_mhigh" => 4, @@ -64,8 +65,9 @@ import Logging for formalism in ("Riccati", "Forward", "Galerkin") stability["ForceFreeStates"]["integrator"] = formalism foreach(name -> delete!(stability, name), ("Riccati", "Forward", "Galerkin")) - stability[formalism] = formalism == "Galerkin" ? Dict{String,Any}("nx" => 32, "rpec_flag" => true) : - Dict{String,Any}("renorm_threshold" => 1e3, "eulerlagrange_tolerance" => 1e-7) + stability[formalism] = + formalism == "Galerkin" ? Dict{String,Any}("nx" => 32, "rpec_flag" => true) : + Dict{String,Any}("renorm_threshold" => 1e3, "eulerlagrange_tolerance" => 1e-7) run_workload(stability) end run_workload(response; files=("kinetic.dat" => kinetic_table,))