Skip to content

Commit 1f4e23f

Browse files
d-burgclaude
andcommitted
REGRESSION - NEW FEATURE - Pin the declared integrator as Delta-prime provenance
The Galerkin and STRIDE-like BVP Delta-prime are converging on one canonical SingularSurfaces/ path, so the numbers alone no longer say which formalism produced them: a deck switched from riccati to galerkin would be compared against the other formalism's gold with no signal. The schema forbids echoing control values into output groups, and the subgroup name that used to carry provenance is going away, so the pin reads the deck's own declaration out of the Input/gpec_toml_raw rerun snapshot -- the one group where control values belong. Adds a 'toml_key:<dotted.path>' extraction yielding a 'token' value type, compared by exact equality and classed topological, since no tolerance means anything between "riccati" and "galerkin". A key the deck leaves unset pins as "<unset>", so starting or stopping declaring one is itself a change. Pinned on the four Delta-prime-bearing cases. The two Galerkin cases pin integrator AND gal_flag, because the Galerkin Delta-prime is currently the additive gal_flag path over a riccati sweep rather than a standalone integrator; when the standalone integrator lands and gal_flag retires, these fail loudly, which is the intent -- a deliberate formalism change must regenerate the gold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e07cf27 commit 1f4e23f

7 files changed

Lines changed: 101 additions & 3 deletions

File tree

regression-harness/cases/diiid_n1_riccati.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ name = "diiid_n1_riccati"
1010
description = "DIII-D-like equilibrium, n=1, Riccati integrator Δ' matrix"
1111
example_dir = "examples/DIIID-like_riccati_deltaprime_example"
1212

13+
# Which formalism produced the Δ′ below. The Δ′ matrix is written to one canonical
14+
# SingularSurfaces/ path whichever integrator computed it, so the numbers alone cannot say
15+
# whether a gold pinned from the BVP is being compared against a Galerkin run. Pinning the
16+
# deck's declared choice makes a formalism swap a loud failure instead of a silent re-gold.
17+
[quantities.integrator]
18+
h5path = "Input/gpec_toml_raw"
19+
type = "token"
20+
extract = "toml_key:ForceFreeStates.integrator"
21+
label = "integrator (Δ′ provenance)"
22+
noise_threshold = 0
23+
order = 1
24+
1325
# Tearing stability Δ' — the msing×msing BVP matrix diagonal, and the raw side-major
1426
# outer-region matrix it is projected from.
1527
[quantities.delta_prime]

regression-harness/cases/diiid_slayer_n1.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ description = "DIII-D-like H-mode equilibrium, n=1, SLAYER tearing-mode analysis
99
example_dir = "examples/DIIID-like_SLAYER_example"
1010

1111
# Per-surface SLAYER layer parameters (geometry + dimensionless)
12+
# Which formalism produced the Δ′ this case's layer physics consumes. The Δ′ matrix lands on
13+
# one canonical SingularSurfaces/ path whichever integrator computed it, so pinning the deck's
14+
# declared choice is what keeps a BVP-derived gold from being compared against a Galerkin run.
15+
[quantities.integrator]
16+
h5path = "Input/gpec_toml_raw"
17+
type = "token"
18+
extract = "toml_key:ForceFreeStates.integrator"
19+
label = "integrator (Δ′ provenance)"
20+
noise_threshold = 0
21+
order = 1
22+
1223
[quantities.slayer_ising]
1324
h5path = "Tearing/PerSurface/rational_index"
1425
type = "real_vector"

regression-harness/cases/gal_resistive_diiid.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ description = "DIII-D-like, n=1, RDCON outer-region Galerkin Δ′ with rpec coi
1010
example_dir = "examples/DIIID-like_gal_resistive_example"
1111

1212
# Number of resonant surfaces in the gal domain
13+
# Which formalism produced the Galerkin Δ′ below. Both this deck's `integrator` and its
14+
# `gal_flag` are pinned because the Galerkin Δ′ is currently the additive path (gal_flag
15+
# layered on a riccati sweep) rather than a standalone integrator, so the pair is what names
16+
# the formalism. Once the standalone `integrator = "galerkin"` lands and gal_flag is retired,
17+
# these pins fail loudly — which is the intent: a deliberate formalism change must regenerate
18+
# the gold rather than silently re-pointing it at different physics.
19+
[quantities.integrator]
20+
h5path = "Input/gpec_toml_raw"
21+
type = "token"
22+
extract = "toml_key:ForceFreeStates.integrator"
23+
label = "integrator (Δ′ provenance)"
24+
noise_threshold = 0
25+
order = 1
26+
27+
[quantities.gal_flag]
28+
h5path = "Input/gpec_toml_raw"
29+
type = "token"
30+
extract = "toml_key:ForceFreeStates.gal_flag"
31+
label = "gal_flag (Δ′ provenance)"
32+
noise_threshold = 0
33+
order = 2
34+
1335
[quantities.gal_msing]
1436
h5path = "ForceFreeStates/Solutions/GalerkinIntegration/rational_count"
1537
type = "int_scalar"

regression-harness/cases/gal_resistive_pe.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ example_dir = "examples/DIIID-like_gal_resistive_pe_example"
1414
# any whole-profile ξ/ξ′ norm (those diverge at the rationals and are ill-posed as benchmarks).
1515

1616
# Surface locations / count (sanity)
17+
# Which formalism produced the Galerkin Δ′ below. Both this deck's `integrator` and its
18+
# `gal_flag` are pinned because the Galerkin Δ′ is currently the additive path (gal_flag
19+
# layered on a riccati sweep) rather than a standalone integrator, so the pair is what names
20+
# the formalism. Once the standalone `integrator = "galerkin"` lands and gal_flag is retired,
21+
# these pins fail loudly — which is the intent: a deliberate formalism change must regenerate
22+
# the gold rather than silently re-pointing it at different physics.
23+
[quantities.integrator]
24+
h5path = "Input/gpec_toml_raw"
25+
type = "token"
26+
extract = "toml_key:ForceFreeStates.integrator"
27+
label = "integrator (Δ′ provenance)"
28+
noise_threshold = 0
29+
order = 1
30+
31+
[quantities.gal_flag]
32+
h5path = "Input/gpec_toml_raw"
33+
type = "token"
34+
extract = "toml_key:ForceFreeStates.gal_flag"
35+
label = "gal_flag (Δ′ provenance)"
36+
noise_threshold = 0
37+
order = 2
38+
1739
[quantities.pe_rational_q]
1840
h5path = "PerturbedEquilibrium/SingularCoupling/rational_q"
1941
type = "real_vector"

regression-harness/src/extractor.jl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ function apply_extraction(spec::QuantitySpec, raw)::ExtractedQuantity
112112
json_str = JSON.json(pairs; allownan=true)
113113
return ExtractedQuantity(name, label, nothing, nothing, json_str, "json_array", threshold)
114114

115+
elseif startswith(spec.extract, "toml_key:")
116+
# "toml_key:<dotted.path>": pin a deck's declared control value, read out of the
117+
# `Input/gpec_toml_raw` rerun snapshot. Used to record which formalism a case asked
118+
# for, because the Δ′ matrix is written to one canonical `SingularSurfaces/` path
119+
# whichever integrator produced it, so the numbers alone no longer say. Reading it
120+
# from `Input/` respects the schema rule that control values live nowhere else.
121+
# A key the deck leaves unset pins as "<unset>", so a deck that starts or stops
122+
# declaring one is itself a change rather than being silently absorbed.
123+
keypath = spec.extract[(length("toml_key:")+1):end]
124+
table = TOML.parse(raw isa AbstractString ? raw : String(raw))
125+
node = table
126+
for key in split(keypath, ".")
127+
node = (node isa AbstractDict && haskey(node, key)) ? node[key] : nothing
128+
node === nothing && break
129+
end
130+
token = node === nothing ? "<unset>" : string(node)
131+
return ExtractedQuantity(name, label, nothing, nothing, token, "token", threshold)
132+
115133
elseif spec.extract == "checksum"
116134
bytes = reinterpret(UInt8, vec(collect(raw)))
117135
hash = bytes2hex(sha256(bytes))
@@ -218,7 +236,7 @@ function compare_values(q1::NamedTuple, q2::NamedTuple)
218236
status = max_diff <= threshold ? "OK" : "CHANGED"
219237
return (max_diff, rel_diff, status)
220238

221-
elseif vtype == "checksum"
239+
elseif vtype == "checksum" || vtype == "token"
222240
t1 = q1.value_text
223241
t2 = q2.value_text
224242
if t1 === nothing || t2 === nothing

regression-harness/src/golden.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ function infer_class(spec::QuantitySpec)::String
223223
spec.type == "runtime" && return "diagnostic"
224224
spec.name in ("nstep", "nstep_total") && return "diagnostic"
225225
spec.type == "int_scalar" && return "topological"
226+
# A declared control token is an exact-match gate, like a count: there is no tolerance
227+
# that means anything between "riccati" and "galerkin".
228+
startswith(spec.extract, "toml_key:") && return "topological"
226229
name = spec.name
227230
# sing_psi / sing_q are deliberately absent: singular-surface locations come from a root
228231
# search, not pure spline/quadrature, so they take the measured physics_converged path.
@@ -269,6 +272,15 @@ function compare_to_golden(q::NamedTuple, g::GoldenValue)
269272
gold = JSON.parse(g.value_text; allownan=true)
270273
length(got) == length(gold) && return _compare_arrays(got, gold, g, within)
271274
return (false, NaN, "length $(length(gold))$(length(got))")
275+
276+
elseif g.value_type == "token"
277+
# A token names a discrete choice (which integrator produced the Δ′), so there is no
278+
# tolerance to apply: it matches or the run is answering a different question than the
279+
# gold does. Deviation is reported as 1.0 on mismatch rather than NaN so it sorts as a
280+
# real failure in reports.
281+
(q.value_text === nothing || g.value_text === nothing) && return (false, NaN, "missing token")
282+
matched = q.value_text == g.value_text
283+
return (matched, matched ? 0.0 : 1.0, matched ? "" : "$(g.value_text)$(q.value_text)")
272284
end
273285

274286
return (false, NaN, "unsupported value type $(g.value_type)")

regression-harness/src/types.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ Specification for a single quantity to extract from gpec.h5.
88
struct QuantitySpec
99
name::String
1010
h5path::String # HDF5 dataset path (e.g. "ForceFreeStates/FreeBoundaryStability/eigenmode_energies"), empty for runtime
11-
type::String # "complex_vector", "real_vector", "real_scalar", "int_scalar", "real_matrix", "runtime"
12-
extract::String # "value", "real_first", "imag_first", "abs_first", "norm", "all_real", "all_complex", "checksum"
11+
type::String # "complex_vector", "real_vector", "real_scalar", "int_scalar", "real_matrix", "token", "runtime"
12+
extract::String # "value", "real_first", "imag_first", "abs_first", "norm", "all_real", "all_complex",
13+
# "diagonal_complex", "first_<N>", "first_<N>_complex", "checksum", "toml_key:<dotted.path>"
1314
label::String # Human-readable label for reports
1415
noise_threshold::Float64 # Absolute changes below this are noise
1516
order::Int # Display order in reports (lower = earlier)

0 commit comments

Comments
 (0)