diff --git a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml index a93549f3b..5398fd0d6 100644 --- a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml @@ -1,8 +1,9 @@ # DIII-D-like H-mode DRIVEN resistive example: gal Δ′ + inner-layer matching, with DIII-D -# C-coil forcing (n=1 cosine phasing) staged for PerturbedEquilibrium. The PE stage currently -# warns and skips: it requires the free-boundary δW, which the Galerkin formalism does not -# yet produce — the gal-matched resistive ξ and penetrated field are carried on the result -# for when gal-side δW lands. Same equilibrium and Galerkin settings as the sibling +# C-coil forcing (n=1 cosine phasing) staged for PerturbedEquilibrium. The response and the +# singular coupling are switched off: they require the free-boundary δW, which the Galerkin +# formalism does not yet produce, and PE refuses them on a Galerkin or matched result. The +# gal-matched resistive ξ and penetrated field are carried on the result for when gal-side +# δW lands. Same equilibrium and Galerkin settings as the sibling # DIIID-like_gal_resistive_example; differences start at [ForcingTerms]. [Equilibrium] eq_filename = "TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file @@ -94,8 +95,8 @@ currents = [1000.0, 500.0, -500.0, -1000.0, -500.0, 500.0] # n=1 cosine phasing [PerturbedEquilibrium] fixed_boundary = false # Use fixed boundary conditions output_eigenmodes = true # Output eigenmode fields as b-fields -compute_response = true # Compute plasma response to forcing -compute_singular_coupling = true # Compute singular layer coupling metrics +compute_response = false # Compute plasma response to forcing +compute_singular_coupling = false # Compute singular layer coupling metrics verbose = true # Enable verbose logging write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 regularization_width = 0.05 # Regularization width for singular surfaces (0 = disabled) diff --git a/regression-harness/cases/gal_resistive_pe.toml b/regression-harness/cases/gal_resistive_pe.toml index 9a04b30ac..3bc8b1ed1 100644 --- a/regression-harness/cases/gal_resistive_pe.toml +++ b/regression-harness/cases/gal_resistive_pe.toml @@ -2,6 +2,8 @@ # fed into PerturbedEquilibrium with DIII-D C-coil forcing. Tracks the finite # per-rational-surface singular-coupling scalars (island half-widths, Chirikov # overlap, resonant flux, penetrated field, Δ′). +# Expected N/A: the example switches the PE response and coupling off until Galerkin and +# matched results publish the free-boundary δW, so none of these datasets are written yet. # Each [quantities.*] block names an HDF5 path in the run output, how to extract it, # and the noise floor below which a difference is treated as zero. [case] diff --git a/src/ForceFreeStates/Result.jl b/src/ForceFreeStates/Result.jl index 5da8c6549..4c03700f9 100644 --- a/src/ForceFreeStates/Result.jl +++ b/src/ForceFreeStates/Result.jl @@ -39,7 +39,8 @@ once this has been built. Optional fields are `Union{Nothing,T}` and follow a presence-equals-capability rule: a consumer that needs one gates on [`require`](@ref) (or [`require_solution`](@ref) for the ξ profiles) and warns-and-skips rather than erroring, so a result from an integrator that -cannot supply a given product still flows through the pipeline. +cannot supply a given product still flows through the pipeline. PerturbedEquilibrium refuses +Galerkin and `:matched` results instead, since they publish no `wp` or `free_boundary` yet. A jump condition at the rational surfaces is always applied before a final result is built, so bpen and closure are always present. @@ -126,10 +127,7 @@ that produced `result`. Warns naming the calculation being skipped otherwise. """ function require(result::ForceFreeStatesResult, field::Symbol, calc::AbstractString) getfield(result, field) === nothing || return true - why = result.closure === :matched && field in (:wp, :free_boundary) ? - "a :matched result carries no δW (the resistive δW is not implemented)" : - "`$field` was not produced by the $(result.integrator) integrator" - @warn "Skipping $calc: $why" + @warn "Skipping $calc: `$field` was not produced by the $(result.integrator) integrator" return false end diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index db5e75cfc..6af667a0a 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -1008,8 +1008,9 @@ Compute the plasma response to the external field `rmp` on top of a force-free-s arguments are `PerturbedEquilibrium.PerturbedEquilibriumControl` fields. Products the producing integrator could not supply gate the corresponding calculation: the -step warns and is skipped rather than erroring, so a Riccati- or Galerkin-fed result still -flows through. +step warns and is skipped rather than erroring, so a Riccati-fed result still flows through. +Galerkin and `:matched` results publish no free-boundary δW yet and are refused when the +response or the singular coupling is requested. `coil_sets` injects already-built coil geometry (the gpec.h5 replay path), bypassing the geometry read. A `runtimes` collector, when given, receives the forcing-mode materialization's wall-clock seconds as `"forcing_terms" => dt`. diff --git a/src/PerturbedEquilibrium/PerturbedEquilibrium.jl b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl index 2e0fb8ee6..3d9ceadf6 100644 --- a/src/PerturbedEquilibrium/PerturbedEquilibrium.jl +++ b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl @@ -50,7 +50,8 @@ Computes plasma response to external forcing and calculates singular layer coupling metrics. Every ForceFreeStates input — the equilibrium, the mode space, the metric and matrix fits, the free-boundary energies and the ξ solution — is read off `ffs`. Products the producing integrator could not supply gate the corresponding calculation: -the step warns and is skipped instead of erroring. +the step warns and is skipped instead of erroring. Galerkin and `:matched` results publish +no free-boundary δW yet, so asking for the response or the coupling on one is an error. ## Arguments @@ -78,6 +79,13 @@ function compute_perturbed_equilibrium( mats = ffs.mats mtheta_vacuum = ffs.control.mtheta_vacuum + # Galerkin and matched results publish no plasma edge response wp, so neither step could run. + if (ctrl.compute_response || ctrl.compute_singular_coupling) && ffs.free_boundary === nothing && + (ffs.integrator === :galerkin || ffs.closure === :matched) + error("PerturbedEquilibrium needs the free-boundary δW, which a $(ffs.integrator) result with $(ffs.closure) closure " * + "does not publish yet; set compute_response = false and compute_singular_coupling = false, or use a forward solve") + end + # Step 0: Initialize mode arrays for convenient indexing initialize_mode_arrays!(intr, ffs) diff --git a/test/runtests_solve_api.jl b/test/runtests_solve_api.jl index b48decae5..bc77acf04 100644 --- a/test/runtests_solve_api.jl +++ b/test/runtests_solve_api.jl @@ -90,6 +90,10 @@ using TOML @test size(dp.matrix) == (msing, msing) @test dp.matrix ≈ FFS.pest3_decompose(dp.raw).Δ @test dp.A !== nothing # the parity blocks the galerkin solve persists + + # With no free-boundary δW, PE refuses the response rather than returning an empty state. + forcing = joinpath(@__DIR__, "..", "examples", "Solovev_ideal_example", "forcing.dat") + @test_throws ErrorException perturbed_equilibrium(api, RMPField(forcing); compute_response=true, write_outputs_to_HDF5=false) end end @@ -270,6 +274,11 @@ using TOML @test matched.match !== nothing && matched.bpen == matched.match.bpen @test matched.wp === nothing && matched.free_boundary === nothing @test matched.solution === nothing # no Galerkin basis to build a matched ξ from + # PE refuses the response and coupling on it, but still materializes the forcing alone. + forcing = joinpath(@__DIR__, "..", "examples", "Solovev_ideal_example", "forcing.dat") + @test_throws ErrorException perturbed_equilibrium(matched, RMPField(forcing); compute_singular_coupling=true, write_outputs_to_HDF5=false) + idle = perturbed_equilibrium(matched, RMPField(forcing); compute_response=false, compute_singular_coupling=false, write_outputs_to_HDF5=false) + @test idle isa GPEC.PerturbedEquilibrium.PerturbedEquilibriumState # A matched result is a new solution, not an outer solve to match again. @test_throws ErrorException MatchProblem(matched; ideal=true) end