ALL - IMPROVEMENT - Self-describing HDF5 metadata (issue #226, PR B) - #364
Conversation
…dims, scales) Every gpec.h5 dataset outside Input/ and the debug-only GalerkinIntegration/Match/ now answers "what is this, in what units, plotted against what" without opening the source, readable natively by h5py/xarray/HDFView: - New Utilities.HDF5Annotations: annotate!(parent, table) applies per-writer path => (; long_name, units, dims) tables post-write (skipping absent paths); make_scale!/attach_scale! wrap the H5DS dimension-scale API (netCDF-4 coordinate mechanism) with Julia-axis -> C-dim index translation; write_root_attrs! stamps schema_version=2.0, Conventions=GPEC-HDF5-2.0, references, title, date_created. - Table-driven, not per-write-call: writers are untouched except one annotate call at the end of each (src/HDF5Schema.jl for the main writer; tables live next to write_galerkin!, the PE writer, KineticForces/Output.jl, and Tearing/Runner/HDF5Output.jl). ~340 dataset annotations total. - Coordinate datasets (psi grids, rational-surface psi, geometry xs/ys) are marked as HDF5 Dimension Scales and attached to the profiles sharing the axis; a greppable "dims" attribute mirrors the scales in Julia axis order. - Attribute wording/units audited by the fortran-physics-reviewer against the layer/field-reconstruction sources; 27 corrections applied (J-weighted field units T*m^2, Q-normalization time vs resistive-kink time, island half-width vs full width, omega_Hz actually rad/s, etc.). - runtests_h5_schema.jl now enforces the contract: long_name+units on every non-exempt dataset, dims on rank >= 2 arrays, root attrs present, scales attached; docs/development/hdf5-conventions.md gains the metadata contract. Attributes are invisible to the rerun leaf-walk and the regression extractor: no tracked value moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
@d-burg please review your outputs carefully here. In particular, the PR overview points out:
It looks like you also have a |
|
@d-burg is voluntold to do the full diff review on this one since he has some specific things called out. |
There was a problem hiding this comment.
Reviewed with a focus on the Tearing/ metadata table, since this PR's annotations become the machine-readable statement of record for the tearing outputs. Scope: I audited the Tearing/ table exhaustively against the writer bodies, the Fortran GPEC slayer/ sources, and Park 2022; I spot-checked the PerturbedEquilibrium, KineticForces and Galerkin tables; I did not audit the main HDF5Schema.jl table. A green review here should not be read as coverage of that.
The mechanism is sound — table-driven post-hoc annotation is the right call for low-conflict stacking, and the table is genuinely complete: I diffed it field-by-field against both _write_per_surface! methods, _write_roots!, _write_layer_widths!, the ragged Diagnostics encoding and both _write_single_scan! branches, and nothing is missing. Requesting changes on one physics error and one enforcement gap.
Blocking
1. PerSurface/tauk states the wrong sign convention (inline suggestion below)
The (Q = −τ_k·ω) parenthetical generalizes a diamagnetic-only minus sign onto the mode frequency. Fortran GPEC slayer/params.f:47-52 is unambiguous:
Qconv=lu**(1.0/3.0)*tau_h ! conversion to Qs based on Cole
Q=Qconv*omega
Q_e=-Qconv*omega_e
Q_i=-Qconv*omega_iQ = +Qconv·ω for the mode; the minus on Q_e/Q_i exists only to turn the negative-by-convention omega_e (params.f:42) into Q_e > 0, exactly as LayerParameters.jl:228-241 already documents and scopes correctly. Park 2022 agrees independently: Q = S^(1/3)·ω_E·τ_H, positive, with the perturbation taken as ψ ∝ e^(iky − iωt).
As written, this annotation tells every downstream reader that Roots/omega_Hz carries the opposite sign from what GrowthRateExtraction.jl:749 actually produces. I traced the full extraction path — _run_analysis, the AMR contour tracer, the contour intersection code — looking for a compensating flip, and there is none; Im(Q) > 0 = unstable falls out of Q = +τ_k·ω directly. The code is right and the annotation is wrong.
2. The metadata contract never walks Tearing/
runtests_h5_schema.jl runs test_data/regression_solovev_ideal_example, which is ideal-only — and the Solovev equilibrium cannot host tearing at all (examples/Solovev_ideal_example/gpec.toml:47-50: "SLAYER tearing analysis is not run on the Solovev analytic equilibrium"). No committed test deck has a [tearing] section. So _collect_metadata_violations never sees a Tearing/ group, and the PR's "add the table entry in the same commit or the test fails" guarantee does not hold for this module. The DIIID-SLAYER walk cited in the description was a one-time manual run and won't catch drift.
No new fixture is needed. The existing write_slayer_hdf5!: round-trip structure testset (test/runtests_slayer_runner.jl:149) already builds a synthetic two-surface SLAYERResult with store_scan=true and writes it to a temp file — covering PerSurface/, Roots/, LayerWidths/, Diagnostics/ and Scan/ with no equilibrium solve. Three lines close the gap at ~zero runtime cost:
mktemp() do path, io
close(io)
h5open(path, "w") do f
write_slayer_hdf5!(f, r)
end
+
+ # Metadata contract must hold for the Tearing group too — the full-run
+ # schema test only exercises an ideal deck, which writes no Tearing/.
+ h5open(path, "r") do f
+ viol = _collect_metadata_violations(f)
+ isempty(viol) || @error "metadata contract violations in Tearing/" viol
+ @test isempty(viol)
+ endwith include("h5_metadata_check.jl") alongside the file's other includes (see the inline suggestion on runtests_h5_schema.jl for that shared helper). Note this covers the SLAYER branch only — PerSurface/taua/taur/v1 are reachable only through the GGJ writer, so if there's no separate GGJ writer testset those three stay unenforced.
The same pattern generalizes to write_galerkin! and KineticForces.write_to_hdf5!, which are pure writers too, but that's beyond what this PR needs to be safe.
Non-blocking, outside this diff
SLAYERParameters' docstring has two rows that this PR (correctly) contradicts. Worth fixing here so the file and the docstring don't ship disagreeing:
--- a/src/InnerLayer/SLAYER/LayerParameters.jl
+++ b/src/InnerLayer/SLAYER/LayerParameters.jl
@@ -33,7 +33,7 @@
-| `tauk` | Q-conversion factor S^(1/3) · τ_H [s] — multiplies ω to get Q |
+| `tauk` | Q-conversion factor S^(1/3) · τ_H [s]: Q = tauk · ω (ω in rad/s) |
@@ -40,8 +40,8 @@
-| `dr_val` | Radial width parameter at surface (input to dc_tmp) |
-| `dgeo_val` | Geometric Δ (Shafranov shift factor) |
+| `dr_val` | Resistive interchange index D_R = E + F + H² (input to dc_tmp) |
+| `dgeo_val` | Connor-Hastie-Helander 2015 Eq. 59 geometric factor |dr_val/dgeo_val are settled by LayerInputs.jl:95-99, Control.jl:39-40 and GGJParameters.jl:56 — the PR's wording is the correct one, the docstring is what's stale.
Confirming the calls that are right
Several judgement calls in this PR are correct and I'd rather they not get relitigated alongside the one that isn't:
Roots/omega_Hzasrad/sis right. Park's ω enters ase^(−iωt), so it is angular by construction, andQconvis in seconds. The_Hzin the dataset name is the misnomer, not the units attribute.- The
rad/svs1/sasymmetry between ω and γ is right and principled — the cycles-vs-radians distinction only bites for oscillation; an e-folding rate is the same number either way. - Not renaming
omega_Hz/gamma_Hzhere is right — that's a breaking output-schema change and belongs behind aschema_versionbump. tauk = S^(1/3)·τ_H,delta_n = S^(1/3)/r_sin1/m,tau_r = μ₀r_s²/η,c_beta = √(β/(1+β)),D_norm,sval_r,d_beta = c_β·d_i— all verified againstparams.fandLayerParameters.jl. Correct.- The brute-force scan
dims="(re_axis, im_axis)"matchesScanResult.Q::Matrixbeingnre × nimin Julia order. Correct.
Filed separately
The units audit surfaced a pre-existing 2π bug on develop: GrowthRateExtraction.jl:441 converts a rad/s quantity to "kHz" with only a 1e-3 factor and compares it against gap_kHz_threshold. Filed as #372 rather than loaded onto this PR — it only gates the :gap warning flag, so no value and no regression-harness quantity moves.
| "PerSurface/Q_e" => (; long_name="normalized electron diamagnetic frequency Q_e per surface", dims=("surface",)), | ||
| "PerSurface/Q_i" => (; long_name="normalized ion diamagnetic frequency Q_i per surface", dims=("surface",)), | ||
| "PerSurface/iota_e" => (; long_name="electron fraction ι_e = Q_e/(Q_e − Q_i) per surface", dims=("surface",)), | ||
| "PerSurface/tauk" => (; long_name="Q-normalization time S^(1/3)·τ_H per surface (Q = −τ_k·ω)", units="s", dims=("surface",)), |
There was a problem hiding this comment.
A1 — blocking. See the review summary: Fortran slayer/params.f:50 is Q=Qconv*omega (plus sign); the minus belongs only to Q_e/Q_i on lines 51-52.
| "PerSurface/tauk" => (; long_name="Q-normalization time S^(1/3)·τ_H per surface (Q = −τ_k·ω)", units="s", dims=("surface",)), | |
| "PerSurface/tauk" => (; long_name="Q-normalization time S^(1/3)·τ_H per surface (Q = τ_k·ω; the diamagnetic inputs Q_e, Q_i carry the opposite sign by convention)", units="s", dims=("surface",)), |
| "PerSurface/Q_i" => (; long_name="normalized ion diamagnetic frequency Q_i per surface", dims=("surface",)), | ||
| "PerSurface/iota_e" => (; long_name="electron fraction ι_e = Q_e/(Q_e − Q_i) per surface", dims=("surface",)), | ||
| "PerSurface/tauk" => (; long_name="Q-normalization time S^(1/3)·τ_H per surface (Q = −τ_k·ω)", units="s", dims=("surface",)), | ||
| "PerSurface/tau_r" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), |
There was a problem hiding this comment.
A2. tau_r is written by the SLAYER _write_per_surface! (line 164) and taur by the GGJ one (line 185). They never coexist, but they currently carry byte-identical long_names, so the annotation can't tell a reader which branch produced their file.
| "PerSurface/tau_r" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), | |
| "PerSurface/tau_r" => (; long_name="resistive diffusion time τ_R = μ₀r_s²/η per surface (SLAYER layer parameters)", units="s", dims=("surface",)), |
| "PerSurface/taua" => (; long_name="Alfvén time τ_A per surface", units="s", dims=("surface",)), | ||
| "PerSurface/taur" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), |
There was a problem hiding this comment.
A3. Same disambiguation on the GGJ side.
| "PerSurface/taua" => (; long_name="Alfvén time τ_A per surface", units="s", dims=("surface",)), | |
| "PerSurface/taur" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), | |
| "PerSurface/taua" => (; long_name="Alfvén time τ_A per surface (GGJ layer parameters)", units="s", dims=("surface",)), | |
| "PerSurface/taur" => (; long_name="resistive diffusion time τ_R per surface (GGJ layer parameters)", units="s", dims=("surface",)), |
| "Roots/Q_root_real" => (; long_name="Re of the dispersion-root normalized frequency Q (NaN = no root)"), | ||
| "Roots/Q_root_imag" => (; long_name="Im of the dispersion-root normalized frequency Q (NaN = no root)"), |
There was a problem hiding this comment.
A5. Roots/* are the only rank-1 tearing datasets without dims, which breaks the greppability the attribute exists for. (Length-1 in coupled mode, length-msing in uncoupled — surface is right for both.)
| "Roots/Q_root_real" => (; long_name="Re of the dispersion-root normalized frequency Q (NaN = no root)"), | |
| "Roots/Q_root_imag" => (; long_name="Im of the dispersion-root normalized frequency Q (NaN = no root)"), | |
| "Roots/Q_root_real" => (; long_name="Re of the dispersion-root normalized frequency Q (NaN = no root)", dims=("surface",)), | |
| "Roots/Q_root_imag" => (; long_name="Im of the dispersion-root normalized frequency Q (NaN = no root)", dims=("surface",)), |
| "Roots/omega_Hz" => (; long_name="mode rotation angular frequency of each root", units="rad/s"), | ||
| "Roots/gamma_Hz" => (; long_name="growth rate of each root", units="1/s"), |
There was a problem hiding this comment.
A4. Both units calls here are correct — this just records why, so nobody "fixes" them later, and adds the one invariant a downstream reader most needs (positive = unstable), which the file currently states nowhere.
| "Roots/omega_Hz" => (; long_name="mode rotation angular frequency of each root", units="rad/s"), | |
| "Roots/gamma_Hz" => (; long_name="growth rate of each root", units="1/s"), | |
| "Roots/omega_Hz" => (; long_name="mode rotation angular frequency ω = Re(Q)/τ_k of each root (the dataset name is a misnomer: this is angular, not cycles/s)", units="rad/s", dims=("surface",)), | |
| "Roots/gamma_Hz" => (; long_name="growth rate γ = Im(Q)/τ_k of each root, positive = unstable (an e-folding rate, so no 2π distinction applies)", units="1/s", dims=("surface",)), |
| "Roots/Q_root_imag" => (; long_name="Im of the dispersion-root normalized frequency Q (NaN = no root)"), | ||
| "Roots/omega_Hz" => (; long_name="mode rotation angular frequency of each root", units="rad/s"), | ||
| "Roots/gamma_Hz" => (; long_name="growth rate of each root", units="1/s"), | ||
| "Roots/no_root" => (; long_name="flag: no usable dispersion root found (Q_root is NaN, ω/γ are placeholders)"), |
There was a problem hiding this comment.
A5 (cont.) — dims on the remaining Roots/ entry.
| "Roots/no_root" => (; long_name="flag: no usable dispersion root found (Q_root is NaN, ω/γ are placeholders)"), | |
| "Roots/no_root" => (; long_name="flag: no usable dispersion root found (Q_root is NaN, ω/γ are placeholders)", dims=("surface",)), |
| "PerSurface/eta" => (; long_name="parallel resistivity at each surface", units="Ohm*m", dims=("surface",)), | ||
| "PerSurface/d_beta" => (; long_name="β-weighted ion drift scale d_β", units="m", dims=("surface",)), | ||
| "PerSurface/dc_tmp" => (; long_name="critical-Δ offset from χ_∥/χ_⊥ matching (Connor-Hastie-Helander 2015 Eq. 59)", dims=("surface",)), | ||
| "PerSurface/dc_type" => (; long_name="per-surface D_c prescription label"), |
There was a problem hiding this comment.
A6 — dims for consistency with the rest of PerSurface/.
| "PerSurface/dc_type" => (; long_name="per-surface D_c prescription label"), | |
| "PerSurface/dc_type" => (; long_name="per-surface D_c prescription label", dims=("surface",)), |
| g = create_group(parent, "Tearing") | ||
| g["enabled"] = Int(result.enabled) | ||
|
|
||
| result.enabled || return g # nothing else to write | ||
| if !result.enabled # nothing else to write |
There was a problem hiding this comment.
A7. The SLAYER and GGJ _write_per_surface! methods write disjoint field sets, so today the only way to tell which model produced a file is to notice which PerSurface datasets exist. Inferring the model from the shape of the schema is exactly what self-describing metadata should eliminate — and it makes A2/A3 a convenience rather than the sole signal. A group attribute keeps this out of the dataset walk, so the schema test is unaffected.
| g = create_group(parent, "Tearing") | |
| g["enabled"] = Int(result.enabled) | |
| result.enabled || return g # nothing else to write | |
| if !result.enabled # nothing else to write | |
| g = create_group(parent, "Tearing") | |
| g["enabled"] = Int(result.enabled) | |
| # Which inner-layer model produced PerSurface/: the SLAYER and GGJ branches write | |
| # 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.enabled # nothing else to write |
with the helper added next to the annotation tables (e.g. just above TEARING_H5_ANNOTATIONS):
# Token recorded in the Tearing group's `layer_model` attribute.
_layer_model_token(::Type{<:SLAYERParameters}) = "slayer"
_layer_model_token(::Type{<:GGJParameters}) = "ggj"
_layer_model_token(::Type) = "unknown"GGJParameters is already in scope here via the _write_per_surface! dispatch, and empty_slayer_result builds SLAYERParameters[] even when disabled, hence the result.enabled guard.
| # Metadata contract (docs/development/hdf5-conventions.md): every dataset carries | ||
| # long_name + units, and rank ≥ 2 datasets carry a dims axis-name attribute. Exempt: | ||
| # the Input/ raw snapshot and the debug-only GalerkinIntegration Match/ group. | ||
| _metadata_exempt(path) = startswith(path, "Input/") || occursin("/Match/", path) | ||
|
|
||
| function _collect_metadata_violations(h5) | ||
| bad = String[] | ||
| function walk(node, prefix) | ||
| for k in keys(node) | ||
| child = node[k] | ||
| full = isempty(prefix) ? k : prefix * "/" * k | ||
| if child isa HDF5.Group | ||
| walk(child, full) | ||
| elseif !_metadata_exempt(full) | ||
| a = attrs(child) | ||
| haskey(a, "long_name") || push!(bad, "$full: missing long_name") | ||
| haskey(a, "units") || push!(bad, "$full: missing units") | ||
| ndims(child) >= 2 && !haskey(a, "dims") && push!(bad, "$full: missing dims") | ||
| end | ||
| end | ||
| end | ||
| walk(h5, "") | ||
| return bad | ||
| end |
There was a problem hiding this comment.
B2 — needed for the blocking enforcement fix. Lift this walk verbatim into a shared test/h5_metadata_check.jl so runtests_slayer_runner.jl can call it too (see the review summary). No behaviour change here.
| # Metadata contract (docs/development/hdf5-conventions.md): every dataset carries | |
| # long_name + units, and rank ≥ 2 datasets carry a dims axis-name attribute. Exempt: | |
| # the Input/ raw snapshot and the debug-only GalerkinIntegration Match/ group. | |
| _metadata_exempt(path) = startswith(path, "Input/") || occursin("/Match/", path) | |
| function _collect_metadata_violations(h5) | |
| bad = String[] | |
| function walk(node, prefix) | |
| for k in keys(node) | |
| child = node[k] | |
| full = isempty(prefix) ? k : prefix * "/" * k | |
| if child isa HDF5.Group | |
| walk(child, full) | |
| elseif !_metadata_exempt(full) | |
| a = attrs(child) | |
| haskey(a, "long_name") || push!(bad, "$full: missing long_name") | |
| haskey(a, "units") || push!(bad, "$full: missing units") | |
| ndims(child) >= 2 && !haskey(a, "dims") && push!(bad, "$full: missing dims") | |
| end | |
| end | |
| end | |
| walk(h5, "") | |
| return bad | |
| end | |
| include("h5_metadata_check.jl") |
New file test/h5_metadata_check.jl:
# Metadata contract (docs/development/hdf5-conventions.md): every dataset carries
# long_name + units, and rank ≥ 2 datasets carry a dims axis-name attribute. Exempt:
# the Input/ raw snapshot and the debug-only GalerkinIntegration Match/ group.
_metadata_exempt(path) = startswith(path, "Input/") || occursin("/Match/", path)
function _collect_metadata_violations(h5)
bad = String[]
function walk(node, prefix)
for k in keys(node)
child = node[k]
full = isempty(prefix) ? k : prefix * "/" * k
if child isa HDF5.Group
walk(child, full)
elseif !_metadata_exempt(full)
a = attrs(child)
haskey(a, "long_name") || push!(bad, "$full: missing long_name")
haskey(a, "units") || push!(bad, "$full: missing units")
ndims(child) >= 2 && !haskey(a, "dims") && push!(bad, "$full: missing dims")
end
end
end
walk(h5, "")
return bad
end|
Follow-up on the blocking enforcement gap — it now has a concrete instance rather than a hypothetical one. #298 ( δ_FKR = p.rs * p.lu^(-1.0/3.0)
δ_visco = δ_FKR * p.P_perp^(1.0/6.0)It doesn't touch That's the failure mode the three-line fix in Two clarifications while I'm here, so nothing above reads as a criticism of work that's actually correct:
Also worth flagging for whoever rebases: #368's fix to the legacy |
Brings the remediated PR-A base (second develop sync, independent-review
fixes, reviewer dispositions) up the stack. Two content conflicts:
- src/GeneralizedPerturbedEquilibrium.jl module header: base dropped the H5_*
writer/rerun consts; this branch adds include("HDF5Schema.jl") in the same
region. Kept the include, dropped the consts.
- test/runtests_h5_schema.jl: base added the _group_name_ok unit testset where
this branch adds the metadata-contract helpers. Kept both.
Post-merge: no conflict markers, no stale H5_* references, package loads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
Review remediation for the Tearing/ annotation table and its enforcement:
- Fix the tauk sign convention: Q = +tau_k*omega per Fortran slayer/params.f
(the minus applies only to the diamagnetic inputs Q_e/Q_i); align the two
contradicted SLAYERParameters docstring rows (dr_val, dgeo_val) with the
corrected annotations.
- Record why omega_Hz/gamma_Hz carry rad/s and 1/s (dataset names are
historical misnomers) and the positive = unstable invariant in long_names.
- Disambiguate tau_r (SLAYER) from taua/taur (GGJ) long_names; add dims to
Roots/* and PerSurface/dc_type.
- New Tearing group attribute layer_model ("slayer"/"ggj"/"none") so readers
need not infer the inner-layer model from which PerSurface fields exist.
- Lift the metadata-contract walker into shared test/h5_metadata_check.jl and
enforce it from the SLAYER round-trip testset (the full-run schema test only
exercises an ideal deck, which writes no Tearing/) plus a synthetic
GGJParameters write covering the GGJ-only table rows.
- Drop a stale doc sentence referencing the removed H5_* consts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
Branch updated Blocking 1 (tauk sign) — applied your suggested wording verbatim (modulo a 3-word trim for the 180-char margin): Blocking 2 (Tearing/ never walked) — the walker now lives in shared A1–A7 — all applied: Note the stack base has moved (#363 got a second develop sync + the legacy-map drop per review there) — the metadata mechanism here is untouched by that, but the diff base shifted. Full targeted suites green on this tip (schema 14/14 + 6/6, SLAYER 65/65). Requesting re-review. 🤖 Generated with Claude Code |
Compute.jl stores total_energy = Im(T)/(2n), i.e. plain deltaW_k in joules; the long_name wrongly claimed 2n*deltaW_k. The psi-profiles (T_imag, dTdpsi_imag) ARE raw Im(T) and keep the 2n label — both long_names now state the asymmetry explicitly so neither gets 'corrected' to match the other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
One more annotation correction pushed ( |
logan-nc
left a comment
There was a problem hiding this comment.
Finding lots of human intuition issues with the AI overhaul (as expected)... posting review of SLAYER and KF now so claude can knock 'em out while I review PE. Goal is to merge tonight
| const KF_METHOD_H5_ANNOTATIONS = [ | ||
| "nn" => (; long_name="toroidal mode number n of this torque calculation"), | ||
| "total_torque" => (; long_name="total NTV toroidal torque T_φ", units="N*m"), | ||
| "total_energy" => (; long_name="total perturbed kinetic energy 2n·δW_k", units="J"), |
There was a problem hiding this comment.
Is the 2n really here? Something reported as energy should not have the 2n in it. Something reported as Imaginary component of the torque should
| "panel_psi" => (; long_name="ψ_N panel boundaries of the radial quadrature"), | ||
| "resonance_psi" => (; long_name="ψ_N of located kinetic-resonance surfaces"), | ||
| "psi" => (; long_name="normalized poloidal flux ψ_N at quadrature evaluation points"), | ||
| "dTdpsi_real" => (; long_name="Re dT_φ/dψ_N torque density at quadrature points", units="N*m", dims=("psi",)), |
There was a problem hiding this comment.
I believe *_real and *_imag subscripts are not necessary and torque can be stored natively as complex. Is there a reason it is broken up here? This should be standardized throughout all h5 writes... add something to the h5 instruction md?
| "resonance_psi" => (; long_name="ψ_N of located kinetic-resonance surfaces"), | ||
| "psi" => (; long_name="normalized poloidal flux ψ_N at quadrature evaluation points"), | ||
| "dTdpsi_real" => (; long_name="Re dT_φ/dψ_N torque density at quadrature points", units="N*m", dims=("psi",)), | ||
| "dTdpsi_imag" => (; long_name="Im dT_φ/dψ_N (2n·dδW_k/dψ_N energy density) at quadrature points", units="J", dims=("psi",)), |
There was a problem hiding this comment.
Most other codes report dT/dV the torque density [N/m^2]. Lets make sure we have dVdpsi in the outputs so this is easily available
| | `sval_r` | r-based magnetic shear r_s · (dq/dr) / q (Fitzpatrick convention) | | ||
| | `dr_val` | Radial width parameter at surface (input to dc_tmp) | | ||
| | `dgeo_val` | Geometric Δ (Shafranov shift factor) | | ||
| | `dr_val` | Resistive interchange D_R = E + F + H² (critical-Δ input; auto-derived from GGJ coefficients unless overridden) | |
There was a problem hiding this comment.
@d-burg should confirm this meaningful change and suggest new names for this an other *_val SLAYER outputs. IMO the _val subscripts add nothing but are taking room. This could be Interchange_D_R or D_R_interchange for example.
| "dTdpsi_real" => (; long_name="Re dT_φ/dψ_N torque density at quadrature points", units="N*m", dims=("psi",)), | ||
| "dTdpsi_imag" => (; long_name="Im dT_φ/dψ_N (2n·dδW_k/dψ_N energy density) at quadrature points", units="J", dims=("psi",)), | ||
| "T_real" => (; long_name="cumulative toroidal torque T_φ(ψ_N) (trapezoidal)", units="N*m", dims=("psi",)), | ||
| "T_imag" => (; long_name="cumulative 2n·δW_k(ψ_N) (trapezoidal; raw Im(T) — not divided by 2n like total_energy)", units="J", dims=("psi",)), |
There was a problem hiding this comment.
Parenthetical is overkill - these are still meant to be names, not general annotations
| "EnergyIntegrals/psi" => (; long_name="ψ_N of each energy-integration record"), | ||
| "EnergyIntegrals/lambda" => (; long_name="pitch λ = μB0/E of each record"), | ||
| "EnergyIntegrals/ell" => (; long_name="bounce harmonic ℓ of each record"), | ||
| "EnergyIntegrals/leff" => (; long_name="effective bounce harmonic ℓ_eff of each record"), |
There was a problem hiding this comment.
could include the actual equation for l_eff here since it is short, which would be consistent with lambda
| "EnergyIntegrals/leff" => (; long_name="effective bounce harmonic ℓ_eff of each record"), | ||
| "EnergyIntegrals/torque_real" => (; long_name="Re of the record's torque contribution", units="N*m"), | ||
| "EnergyIntegrals/torque_imag" => (; long_name="Im of the record's torque contribution", units="N*m"), | ||
| "EnergyIntegrals/kinetic_energy_real" => (; long_name="Re of the record's kinetic energy contribution", units="J"), |
There was a problem hiding this comment.
The docs should explain why there are complex torque and energy here but only a complex torque (with energy corresponding to the imaginary part) at the top level of KineticForces outputs.
| "EnergyIntegrals/torque_imag" => (; long_name="Im of the record's torque contribution", units="N*m"), | ||
| "EnergyIntegrals/kinetic_energy_real" => (; long_name="Re of the record's kinetic energy contribution", units="J"), | ||
| "EnergyIntegrals/kinetic_energy_imag" => (; long_name="Im of the record's kinetic energy contribution", units="J"), | ||
| "EnergyIntegrals/trajectory_offsets" => (; long_name="ragged-array offsets: record k spans offsets[k]+1:offsets[k+1] of the *_all arrays"), |
There was a problem hiding this comment.
seems confusing for new users? A re-review should think hard about if there is a cleaner way to organize these outputs in the h5
| "EnergyIntegrals/integrand_imag_all" => (; long_name="Im of the energy-space torque integrand along all trajectories (concatenated)"), | ||
| "EnergyIntegrals/integral_real_all" => (; long_name="Re of the cumulative energy-space integral along all trajectories (concatenated)"), | ||
| "EnergyIntegrals/integral_imag_all" => (; long_name="Im of the cumulative energy-space integral along all trajectories (concatenated)"), | ||
| "KineticMatrices/matrix_1" => (; long_name="drift-kinetic coefficient matrix 1 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), |
There was a problem hiding this comment.
Users familiar with the papers will know these by names like A,B, etc. while the index numbers are just an artifact of the code's consolidated storage. Rename more intuitively.
There was a problem hiding this comment.
Will repeated dims like (mode, mode) break interfaces we claim to be facilitating like xarray? Do we need to name them distinctly? That would introduce it's own possible confusion points. Is there an industry standard for this kind of thing?
| ] | ||
|
|
||
| # Attach long_name/units/dims + the ψ_N quadrature scale to one method group. | ||
| function annotate_kinetic_forces!(method_g) |
There was a problem hiding this comment.
Why are separate helper functions defined each time? It seems like its just to do manual make_scale calls or looped calls. Couldn't that be handed by the general annotate function is we just added a (optional?) scale entry to the *_H5_ANNOTATIONS tables?
logan-nc
left a comment
There was a problem hiding this comment.
Finished human review of Galerkin - plenty of issues for a new AI plan to address.
Now on to PE for real
| const GALERKIN_H5_ANNOTATIONS = [ | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/msing" => (; long_name="number of rational (singular) surfaces in the Galerkin solve"), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi" => (; long_name="normalized poloidal flux ψ_N grid of the Galerkin solution"), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/q" => (; long_name="safety factor on the Galerkin solution grid", dims=("psi",)), |
There was a problem hiding this comment.
why are we outputting such a fine grid q profile? That seems excessive as we already output the q spline information it comes directly from in the Equilibrium section of the h5
| "ForceFreeStates/Solutions/GalerkinIntegration/msing" => (; long_name="number of rational (singular) surfaces in the Galerkin solve"), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi" => (; long_name="normalized poloidal flux ψ_N grid of the Galerkin solution"), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/q" => (; long_name="safety factor on the Galerkin solution grid", dims=("psi",)), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing" => (; long_name="flag: grid node lies on a rational surface", dims=("psi",)), |
There was a problem hiding this comment.
Do we need a documented standard for variable name syntax in cases like this where the variables are words? To me, the is_singular seems most natural despite using CamelCase for the section labels... but I guess I could be convinced of IsSingular if that conforms better to standards (i.e. if the CamelCase choice means we should be CamelCase through and through). We should standardize throughout either way
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/q" => (; long_name="safety factor on the Galerkin solution grid", dims=("psi",)), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing" => (; long_name="flag: grid node lies on a rational surface", dims=("psi",)), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi" => (; long_name="Galerkin solution functions ξ (arbitrary amplitude)", dims=("mode", "psi", "solution")), | ||
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi_deriv" => |
There was a problem hiding this comment.
We used d*dpsi for derivatives in KineticForces. Fortran used *1 and we might have a few uses of that laying around too. We should again standardize throughout for a consistent h5 experience. I think d*dpsi is the least ambiguous (note simple *_psi is not available since we use that for the psi component of things). Again, standardization means changing everything now and adding a note to the h5 conventions md
| "ForceFreeStates/Solutions/GalerkinIntegration/Solution/cut_range" => | ||
| (; long_name="ψ_N bounds of the excised resonant + extension cells per surface", dims=("surface", "bound")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/delta" => | ||
| (; long_name="outer-region Δ' matrix (2msing×2msing, side-major [L_s1, R_s1, ...]; RDCON Galerkin)", dims=("surface_side", "surface_side")), |
There was a problem hiding this comment.
I don't understand the ; RDCON Galerkin part of this. Remove?
| "SingularSurfaces/GalerkinDeltaPrime/pest3_B" => (; long_name="PEST-3 matching block B' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/pest3_Gamma" => (; long_name="PEST-3 matching block Γ' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta" => (; long_name="PEST-3 matching block Δ' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), |
There was a problem hiding this comment.
This physical quantity is written multiple places. That is ok (it isn't large) but the name should be consistent throughout. Have claude check for other repeated variables while at it and make sure the names are consistent (again, add a note in the conventions so anyone adding to the h5 later is sure to follow suite).
| "SingularSurfaces/GalerkinDeltaPrime/pest3_Gamma" => (; long_name="PEST-3 matching block Γ' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta" => (; long_name="PEST-3 matching block Δ' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), |
There was a problem hiding this comment.
another quantity written multiple places. Note that I prefer "rational" to "singular" since we have options to do kinetic and resistive runs where the solutions are not singular on the rationals.
| "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta" => (; long_name="PEST-3 matching block Δ' (Galerkin outer region)", dims=("surface", "surface")), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_m" => (; long_name="resonant poloidal mode number m at each rational surface", dims=("surface",)), |
There was a problem hiding this comment.
Perfect example of mixed use of singular (sing_*) and rational. While I am at it, I will explicitly say the ordering of specifiers should be standardized as well... no good having sing_bloop and blip_sing in the same h5 output file! This goes for all specifiers (no using pest3_this here and that_pest3 somewhere else, for example)
| "SingularSurfaces/GalerkinDeltaPrime/sing_q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_m" => (; long_name="resonant poloidal mode number m at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/sing_n" => (; long_name="resonant toroidal mode number n at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/di" => (; long_name="Mercier D_I at each rational surface", dims=("surface",)), |
There was a problem hiding this comment.
lowercase and all one word is inconsistent with use of D_R elsewhere. This was inherited from bad formatting in lazy old fortran. We should modernize and make names intuitively match the standard literature → D_I here. Claude should spin a subagent to look for any similar opportunities to improve and standardize names.
| "SingularSurfaces/GalerkinDeltaPrime/sing_n" => (; long_name="resonant toroidal mode number n at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/di" => (; long_name="Mercier D_I at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/alpha" => (; long_name="Frobenius small-solution exponent α at each rational surface", dims=("surface",)), | ||
| "SingularSurfaces/GalerkinDeltaPrime/delta_coil" => (; long_name="edge coil-response matrix (edge mode × surface-side; RPEC columns)", dims=("mode", "surface_side")) |
There was a problem hiding this comment.
Is the capitalization of this delta consistent with the literature?
…ames, table-driven scales Addresses the SLAYER/KF review comments: - Complex quantities are now stored natively everywhere (never *_real/*_imag dataset pairs): KineticForces dTdpsi/T and the EnergyIntegrals record and trajectory arrays; Tearing Q_root, dels_db, delta_s, ragged Diagnostics flat, Scan Q/Delta; the DpMatrix real/imag subgroup collapses to a single complex PerSurface/dp_matrix. Sole sanctioned exception (documented): Input/RawInputs/ForcingTerms amplitude pair, which mirrors the external ingest format and keeps old snapshots replayable. Conventions doc updated; the harness gains a complex-preserving first_N_complex extract and diiid_slayer_n1 tracks one complex slayer_Q quantity. - KineticMatrices matrix_1..6 renamed to the Logan 2015 letters A,B,C,D,E,H with identities (A = Wz'Wz etc.) and the energy normalization stated. - Tearing PerSurface dr_val/dgeo_val renamed to the literature names D_R and D_geo (struct fields unchanged). - dVdpsi is now written per KineticForces method at the quadrature points (spline passed from the main pipeline) so dT/dV = dTdpsi/dVdpsi is directly available; dTdpsi long_name says so. - leff long_name carries its formula; overkill parentheticals trimmed. - Square-matrix dims use distinct row/col axis names ((mode_row, mode_col) etc.) since xarray mangles repeated dimension names; documented. - Dimension scales and attachments are now declared in the annotation tables (scale=/attach= entry fields handled by annotate! itself); the per-writer imperative make_scale!/attach_scale! helpers collapse to bare annotate! calls. - Galerkin: drop the redundant fine-grid Solution/q (derivable from Equilibrium/Profiles); trim a cryptic long_name fragment. - docs/src/kinetic_forces.md explains the complex-torque convention split (T profiles carry 2n, total_energy does not; records keep both halves) and gives worked flat+offsets read examples in Julia and h5py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
…ls, rational, d*dpsi) Review-driven naming standardization across all writers (conventions codified in docs/development/hdf5-conventions.md): - Literature capitalization: di/dr/di0 -> D_I, D_R; the full Delta-prime family (delta_prime* -> Delta_prime*, delta -> Delta_prime_raw since it is the same side-major object, delta_coil -> Delta_coil, deltar -> Delta_r, Tearing dp_matrix -> Delta_prime_matrix, delta_n -> Delta_prime_norm); tau_r/taur collision unified as tau_R with tau_A/tau_k following; dc_tmp -> D_c_offset, dc_type -> D_c_type. - One rational-surface convention, specifier-first: sing_*/bare psi,q,m,n -> rational_psi/rational_q/rational_m/rational_n everywhere (SingularSurfaces, GalerkinDeltaPrime, SingularCoupling incl. rational_m_res -> rational_m); msing/kmsing -> rational_count; issing -> is_rational; ising -> rational_index. - Derivatives are d<x>dpsi: q1 -> dqdpsi, q1lim -> dqdpsi_lim, dxi_psi -> dxi_psidpsi, xi_deriv -> dxidpsi, clebsch_psi1 -> dclebsch_psidpsi; p_local/p1_local/v1_local -> mu0p/dmu0pdpsi/dVdpsi (same-quantity leaf names as Profiles); GGJ v1 -> dVdpsi. - Misnomer cleanup: Roots/omega_Hz -> omega, gamma_Hz -> gamma (values are rad/s and 1/s); dels_db -> delta_s_over_d_beta; delta_s_m -> delta_s_abs. - Struct fields keep their legacy spellings; only HDF5 leaves change. All readers, tests, benchmarks, and harness case TOMLs updated in lockstep (quantity names preserved). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
Both review rounds addressed across two commits ( KF/SLAYER round:
Galerkin round (naming standardization,
On superscripts for contravariant components: there is no HDF5/netCDF standard for marking super vs subscripts in names — the Tests green at each step (schema 14/14 + 6/6 incl. metadata walk with the renamed tables, SLAYER 64/64, fullruns 17/17). Ready for your re-review — and per policy, your review is the merge gate tonight. 🤖 Generated with Claude Code |
logan-nc
left a comment
There was a problem hiding this comment.
Final human comments done!
| "ResponseMatrices/reluctance" => (; long_name="reluctance ϱ̃ in root-area-weighted field space", dims=("mode", "mode")), | ||
| "ResponseMatrices/rootarea_to_area_weight_operator" => (; long_name="operator S = Σ/√A at ψ_lim; b̄ = S·b̃", dims=("mode", "mode")), | ||
| "ResponseMatrices/surface_area" => (; long_name="control-surface scalar area A = ∮J|∇ψ|dθ; Φ = A·b̄", units="m^2"), | ||
| "Response/psi_n" => (; long_name="normalized poloidal flux ψ_N grid shared by the response profiles"), |
There was a problem hiding this comment.
lets make sure the radial variable of normalized poloidal flux is consistently named throughout. It is psi_n here, but I think it might be just psi in ForceFreeStates → standardize throughout the h5
| "ResponseMatrices/rootarea_to_area_weight_operator" => (; long_name="operator S = Σ/√A at ψ_lim; b̄ = S·b̃", dims=("mode", "mode")), | ||
| "ResponseMatrices/surface_area" => (; long_name="control-surface scalar area A = ∮J|∇ψ|dθ; Φ = A·b̄", units="m^2"), | ||
| "Response/psi_n" => (; long_name="normalized poloidal flux ψ_N grid shared by the response profiles"), | ||
| "Response/xi_psi" => (; long_name="contravariant radial displacement ξ^ψ = ξ·∇ψ_N", dims=("psi", "mode")), |
There was a problem hiding this comment.
Consider if there is a h5 industry standard for subscripts - the use of _ here and elsewhere for contravariant components is potentially confusing.
| "ResponseMatrices/surface_area" => (; long_name="control-surface scalar area A = ∮J|∇ψ|dθ; Φ = A·b̄", units="m^2"), | ||
| "Response/psi_n" => (; long_name="normalized poloidal flux ψ_N grid shared by the response profiles"), | ||
| "Response/xi_psi" => (; long_name="contravariant radial displacement ξ^ψ = ξ·∇ψ_N", dims=("psi", "mode")), | ||
| "Response/xi_psi_J" => (; long_name="Jacobian-weighted contravariant radial displacement J·ξ^ψ", units="m^3", dims=("psi", "mode")), |
There was a problem hiding this comment.
Variable name reads as though J is a subscript. Other variables I believe put the J in front (like Jbgradpsi). Standardize throughout.
| "Response/psi_n" => (; long_name="normalized poloidal flux ψ_N grid shared by the response profiles"), | ||
| "Response/xi_psi" => (; long_name="contravariant radial displacement ξ^ψ = ξ·∇ψ_N", dims=("psi", "mode")), | ||
| "Response/xi_psi_J" => (; long_name="Jacobian-weighted contravariant radial displacement J·ξ^ψ", units="m^3", dims=("psi", "mode")), | ||
| "Response/xi_theta" => (; long_name="Jacobian-weighted contravariant poloidal displacement J·ξ^θ", units="m^3", dims=("psi", "mode")), |
There was a problem hiding this comment.
Is it confusing to have the J explicitly in the psi component but implicit here and in zeta?
| "Response/xi_zeta" => (; long_name="Jacobian-weighted contravariant toroidal displacement J·ξ^ζ", units="m^3", dims=("psi", "mode")), | ||
| "Response/xi_theta_reg" => (; long_name="regularized Jacobian-weighted contravariant poloidal displacement J·ξ^θ", units="m^3", dims=("psi", "mode")), | ||
| "Response/xi_zeta_reg" => (; long_name="regularized Jacobian-weighted contravariant toroidal displacement J·ξ^ζ", units="m^3", dims=("psi", "mode")), | ||
| "Response/xi_cova_psi" => (; long_name="covariant radial displacement ξ_ψ", units="m^2", dims=("psi", "mode")), |
There was a problem hiding this comment.
If we find a better standard for distinguishing superscripts and subscripts, we can get rid of this clumsy _cova_ notation
| "Response/xi_phi" => (; long_name="cylindrical displacement component ξ_φ (mode space)", units="m", dims=("psi", "mode")), | ||
| "Response/b_psi_area_weighted" => (; long_name="area-normalized radial field b^ψ/⟨J|∇ψ|⟩_θ", units="T", dims=("psi", "mode")), | ||
| "Response/b_n" => (; long_name="physical normal field b_n", units="T", dims=("psi", "mode")), | ||
| "Response/b_theta" => (; long_name="Jacobian-weighted contravariant poloidal field J·b^θ", units="T*m^2", dims=("psi", "mode")), |
There was a problem hiding this comment.
I thought there was a big push to make all the weights of the fields be Tesla... was it just the psi components then? This deserves a second hard look from a devoted physics agent review.
| "Response/b_zeta" => (; long_name="Jacobian-weighted contravariant toroidal field J·b^ζ", units="T*m^2", dims=("psi", "mode")), | ||
| "Response/b_theta_reg" => (; long_name="regularized Jacobian-weighted contravariant poloidal field J·b^θ", units="T*m^2", dims=("psi", "mode")), | ||
| "Response/b_zeta_reg" => (; long_name="regularized Jacobian-weighted contravariant toroidal field J·b^ζ", units="T*m^2", dims=("psi", "mode")), | ||
| "Response/b_cova_psi" => (; long_name="covariant radial field b_ψ", units="T*m", dims=("psi", "mode")), |
There was a problem hiding this comment.
Also spin a devoted agent review of these cova units
| "SingularCoupling/C_resonant_current" => (; long_name="coupling matrix: applied b̃ → pitch-resonant current", units="A/T", dims=("surface", "mode")), | ||
| "SingularCoupling/C_island_width_sq" => (; long_name="coupling matrix: applied b̃ → squared island half-width", units="1/T", dims=("surface", "mode")), | ||
| "SingularCoupling/C_penetrated_area_weighted_field" => (; long_name="coupling matrix: applied b̃ → penetrated area-weighted field", dims=("surface", "mode")), | ||
| "SingularCoupling/C_delta_prime" => (; long_name="coupling matrix: applied b̃ → forcing-driven Δ'", units="1/T", dims=("surface", "mode")), |
There was a problem hiding this comment.
I believe we are conforming all Δ → Delta and reserving any delta for δ type variables. This should conform as well.
| "Response/b_R" => (; long_name="cylindrical field component b_R (mode space)", units="T", dims=("psi", "mode")), | ||
| "Response/b_Z" => (; long_name="cylindrical field component b_Z (mode space)", units="T", dims=("psi", "mode")), | ||
| "Response/b_phi" => (; long_name="cylindrical field component b_φ (mode space)", units="T", dims=("psi", "mode")), | ||
| "SingularCoupling/C_resonant_area_weighted_field" => (; long_name="coupling matrix: applied b̃ → resonant area-weighted field b̄^r = Φ^r/A^r", dims=("surface", "mode")), |
There was a problem hiding this comment.
how does "surface" work as a dimension? If it's the rational surface index, this deserves a second look. Any physicist would want things to natively plot vs rational q or rational psi. That being said, perhaps the index is necessary to properly prepare for the future expansion to full 3D with multiple n such that there can be multiple resonances per rational q... if that is the case, then ok. But perhaps add documentation somewhere of how to quickly plot things like the effective resonant field vs q
| "SingularCoupling/resonant_current" => (; long_name="pitch-resonant current per rational surface", units="A", dims=("surface",)), | ||
| "SingularCoupling/island_width_sq" => (; long_name="squared island half-width per rational surface (in ψ_N²)", dims=("surface",)), | ||
| "SingularCoupling/penetrated_area_weighted_field" => (; long_name="penetrated area-weighted field per rational surface", units="T", dims=("surface",)), | ||
| "SingularCoupling/delta_prime" => (; long_name="forcing-driven tearing Δ' per rational surface (Riccati; response to applied forcing)", dims=("surface",)), |
There was a problem hiding this comment.
Should be Delta_prime
…dit fixes Final review round (PE comments) plus an independent whole-schema standardization audit and a devoted physics review of the PE units. Naming: - One coordinate vocabulary: the radial abscissa is `psi` and the poloidal one `theta` in every group (Response/psi_n, Profiles/xs, Geometry/xs,ys are gone). - Vector components follow one scheme: bare suffix = contravariant, `_cov_` = covariant, leading `J` = Jacobian-weighted (Jxi_psi, Jxi_theta, Jb_zeta, xi_cov_psi, ...). There is no HDF5/netCDF super/subscript standard, so the typeset form stays in long_name; both rules are documented. - One leaf per quantity across integrator backends: Galerkin Solution xi -> xi_psi, dxidpsi -> dxi_psidpsi, xi_cut -> xi_psi_cut; Match InnerParams taua/taur/v1 -> tau_A/tau_R/dVdpsi (matching the Tearing mapping); KineticForces nn -> n; dims axis mode_index -> mode. Bug fixes found by the audit: - LocalStability had a name collision hiding a missing coordinate: D_I, D_R and ballooning_Delta_prime live on the locstab grid, which was never written, while LocalStability/psi was actually the unrelated ballooning alpha-scan grid. The profile grid is now written as psi (a scale, attached to all three) and the alpha scan as ballooning_psi with its own scale. - Tearing PerSurface/tau_R had two table entries with conflicting long_names; annotate! is last-wins, so every SLAYER run was labelled "(GGJ layer parameters)". Merged into one neutral entry (the layer_model group attribute already disambiguates the branch). Metadata completeness: rational_q is now a dimension scale in all three rational-surface groups (plot vs psi or q natively); SingularSurfaces/Kinetic datasets gained dims/scale/attach; resonant_area_weighted_field and dclebsch_psidpsi gained their missing attaches; dead b_norm and unreachable Kinetic ELM entries trimmed. Physics review verdict (no unit changes): the tesla convention covers the normal-field representations only - Jb_theta/Jb_zeta are flux-like densities (T*m^2) and converting them would falsely imply invariance; the covariant units are confirmed by the metric construction rather than dimensional analysis. long_names now state both so they are not "fixed" later. Remaining non-blocking recommendations tracked in #388. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
Final round pushed ( PE round (Q1–Q11)
Independent standardization audit (your closing ask)Two blocking finds, both real and fixed here:
Also fixed: Audit also confirmed clean: units-string formatting is uniform across all five tables, scale names agree across all seven ψ-grid producers, no VerificationSchema+metadata test 14/14 (+6/6 rule tests), SLAYER runner 64/64, fullruns, coils, rerun-from-h5 all green; package loads; margins/whitespace clean. The schema test earned its keep twice tonight — it caught an interpolated 🤖 Generated with Claude Code |
Closes the one review item left half-addressed: is_rational was renamed but the standard behind it was never written down. States that multi-word dataset names are snake_case English (CamelCase is groups-only) and that flags carry an is_ prefix only where the bare word would read as a noun or collide with a data family - which is why is_rational has one and enabled/truncated/no_root do not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
|
Audit correction ( Now documented in the conventions md: multi-word dataset names are snake_case English (CamelCase is groups-only), and a boolean flag takes an Also verified in the same pass, for the record: |
…sch_*) The three Clebsch datasets were the only Response/ entries that omitted the variable they represent: clebsch_psi / clebsch_alpha / dclebsch_psidpsi say the representation and the coordinate but never that it is a xi displacement. Renamed to xi_clebsch_psi / xi_clebsch_alpha / dxi_clebsch_psidpsi, matching xi_cov_psi and the rest of the Response group. The component rule in the conventions doc now states the full pattern — [d]<variable>[_<representation>]_<coordinate>[dpsi], variable always first, coordinate always the trailing subscript — so this class of omission is a documented error rather than a judgement call. A sweep of every component- shaped leaf name confirms these three were the only instances. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
Rename the Equilibrium/ scalar datasets from the Fortran-era contractions to spelled-out physics names with trailing qualifier subscripts: q0/qa/q95 -> q_axis/q_edge/q_95, bt0/bwall -> B_T_axis/B_T_wall, ro/zo/b0 -> R_axis/Z_axis/B_axis, amean/rmean/aratio -> a_mean/R_mean/aspect_ratio, crnt -> I_p, delta1/delta2 -> delta_upper/delta_lower, betat/betan/betap*/betaj -> beta_t/beta_N/beta_p_*/beta_j, li1-3 -> l_i_1-3, psio -> psi_total, mextrema -> q_extrema_count, and the boundary shape arrays to R_midplane (inboard/outboard midplane crossings) and R_extremum/Z_extremum (Z extrema). EquilibriumParameters keeps its field spellings; EQUIL_H5_NAMES maps fields to dataset names and EQUIL_H5_SKIP drops the four exact duplicates of psi_total / psi_norm (psi0, psi_axis, psi_axis_norm, zsep) and the three control-flag echoes (verbose, diagnose_src, diagnose_maxima), which belong under Input/ only. Readers, benchmark and LAR scan scripts, the harness runner and case h5paths follow; harness quantity names are unchanged so cached history still matches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGmFuAw5JdYrAyBSXCVssR
Equilibrium/ scalar naming pass (final standardization item)The last group still carrying Fortran-era contractions was
Descriptive On Seven datasets leave the file: four exact duplicates (
Follow-up filed as #389: while auditing the assignment sites I found that most of the flux bookkeeping ( Docs updated ( Important This PR still requires a third-party human review approval at its current head before merge. The branch has moved several commits past the earlier approval, and with |
Equilibrium/psi_boundary is assigned a hardcoded 1.0, so labelling it as the boundary poloidal flux in Wb/rad was actively wrong. Say what it is - the boundary value in the internal normalized convention, 1 by construction - and drop the Wb/rad unit so it annotates as dimensionless. Same treatment for psi_boundary_norm, which is tautological in psi_N. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGmFuAw5JdYrAyBSXCVssR
|
Correction to my previous comment: the separatrix bracket solves θ + η(θ) − η₀ = 0 (the poloidal-angle offset Also pushed Stack propagated: #368 ( Local |
Summary
PR B of the two stacked PRs closing #226 (stacked on #363 — review/merge that first; this diff is additive on top of it). Every
gpec.h5dataset now answers "what is this, in what units, plotted against what" without opening the source — netCDF-grade, natively readable by h5py/xarray/HDFView.What every dataset gets
long_name— plain-text physics description.units— SI strings;"1"for dimensionless (CF convention); normalized quantities state the normalization in the long_name.dims(rank ≥ 2) — greppable axis-name string in Julia (column-major) order, e.g."(psi, mode)".h5ds_set_scaleand attached per axis with labels: h5py.dims, xarray, and HDFView resolve axes natively (verified with h5py:DIMENSION_LIST+ labels present).schema_version="2.0"(readers get a dispatch point for future breaking changes),Conventions="GPEC-HDF5-2.0",references,title,date_created(ISO 8601 UTC).Exempt (whitelisted in the enforcement test):
Input/**(raw rerun snapshot) andGalerkinIntegration/Match/**(debug-only, drop pending Galerkin-owner decision — no point hand-curating ~15 entries for it).Mechanism — one deviation from the approved plan wording
The plan said "route all writers through a
write_annotated!helper". Implemented instead as table-driven post-hoc annotation: each writer keeps apath => (; long_name, units, dims)table next to it and applies it with oneannotate!call after its datasets are written (Utilities.HDF5Annotations+src/HDF5Schema.jlfor the main writer). Same contract, enforced identically by the schema test — but the writer bodies are untouched, keeping this PR genuinely low-conflict with #354/#345/#339/#318 and handling dynamic writes (per-field equilibrium loop, per-method KF groups) naturally. ~340 annotation entries total.Physics review
The tables were audited by the fortran-physics-reviewer agent against the layer/field-reconstruction sources and the Fortran gpout.f attribute crib; 27 corrections applied, including: J-weighted contravariant fields are
T*m^2(χ₁-weighted), notT;Tearing/PerSurface/taukis the Q-normalization time S^⅓·τ_H, not a resistive-kink time;island_width_sqis the squared half-width;Roots/omega_Hzis actually an angular frequency (rad/s) — the dataset name is a misnomer, flagged here rather than renamed (renaming is a breaking change; candidate for a future schema bump);delta_nis the Δ′-normalization S^⅓/r_s in 1/m. Reviewers: the covariant-component units (T*m,m^2) were settled by dimensional analysis — worth a second look.Enforcement
test/runtests_h5_schema.jlnow asserts on a full-run output:long_name+unitson every non-exempt dataset,dimson every rank ≥ 2 array, root attributes present, and the ψ_N scale attached (28/28 passing). Additional metadata walks were run on the DIIID SLAYER, Solovev kinetic-NTV, and DIIID gal-resistive decks — zero violations. When adding a dataset, add its table entry in the same commit or the test fails — this is the anti-drift teeth #226 asked for.Notes
h5ds_set_labelwrapper mis-types the Cconst char*asRef{UInt8}; we pass a NUL-terminated byte buffer.Dates(stdlib) added to Project.toml fordate_created.LocalStability/di/drhave no stored coordinate grid to attach (their ψ grid isn't written); theirdimsattribute documents the axis name only.Regression harness (mandatory gate) — zero movement
regress --cases <all 12> --refs develop,local: identical table to PR A — every surviving quantity OK at 0.0e+00 (48/21/15/16/10/4+4 pre-existing N/A/14/14/6/5/4/4). Attributes are invisible to the extractor and the rerun leaf-walk, as designed; the bit-for-bit rerun round-trip test passes with annotations present.Other verification
Full schema test 28/28; SLAYER runner, coils, fullruns, rerun-from-h5 all pass; docs build clean (new
HDF5Annotationsautodocs covered); h5py external check confirms scales/attrs.This PR must not be merged until a third-party human reviewer has approved it — non-negotiable project policy. Merge order: #363 first, then this.
🤖 Generated with Claude Code
https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz