diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bce5d8f93..54e179385 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,3 +45,29 @@ repos: hooks: - id: julia-formatter name: Format Julia code + + # Config-style TOML annotation conventions (docs/development/toml-conventions.md). + # pygrep hooks fail when the pattern matches, so each entry matches a violation. + - repo: local + hooks: + - id: toml-header-block + name: 'TOML conventions: file starts with a # header block' + language: pygrep + entry: '\A(?!#[^\n]*\n#)' + args: [--multiline] + files: ^(examples/.*\.toml|test/test_data/.*\.toml|regression-harness/cases/.*\.toml)$ + - id: toml-no-decorative-dividers + name: 'TOML conventions: no decorative comment dividers' + language: pygrep + entry: '^#\s*[-=*_~#]{6,}\s*$' + files: ^(examples/.*\.toml|test/test_data/.*\.toml|regression-harness/cases/.*\.toml)$ + - id: toml-inline-annotations + name: 'TOML conventions: every variable line has a # description' + language: pygrep + entry: '^[A-Za-z0-9_]+\s*=\s*[^#]*$' + files: ^(examples/.*\.toml|test/test_data/.*\.toml)$ + - id: toml-no-deprecated-keys + name: 'TOML conventions: no deprecated config keys' + language: pygrep + entry: '^(mer_flag|force_wv_symmetry|ode_flag|cyl_flag|mat_flag|power_bp|power_b|power_r|power_rc)\s*=' + files: ^(examples/.*\.toml|test/test_data/.*\.toml)$ diff --git a/docs/development/toml-conventions.md b/docs/development/toml-conventions.md index e5d3b3316..5ee2c3d5d 100644 --- a/docs/development/toml-conventions.md +++ b/docs/development/toml-conventions.md @@ -14,7 +14,10 @@ modify them. **Do not invent a new convention** — match what the existing exam `PerturbedEquilibriumControl`, `SolovevConfig`, `TJAnalyticConfig`, `KineticForcesControl`) and keep it to one terse line. **Use the same description for the same variable across all files** — `examples/DIIID-like_ideal_example/gpec.toml` is the canonical reference for the - common sections. + common sections. Exception: in `regression-harness/cases/*.toml` the repeated + `[quantities.*]` schema keys (`h5path`, `type`, `extract`, `label`, `noise_threshold`, + `order`) are developer metadata and do **not** need inline comments — a case file needs + the header plus an informative block comment per quantity group instead. 3. **Section comments only when informative.** No comment is required above a section. Keep a block comment only when it carries real information (e.g. the Solovev `[Wall]` note on why a conformal wall is needed). Never add decorative section dividers. @@ -34,3 +37,22 @@ modify them. **Do not invent a new convention** — match what the existing exam 6. **`Project.toml`-family files are exempt.** `Project.toml`, `docs/Project.toml`, `regression-harness/Project.toml`, and `.JuliaFormatter.toml` are machine-managed; do not inline-annotate dependency UUIDs or `[compat]` entries. + +## Enforcement + +Four `pygrep` hooks in `.pre-commit-config.yaml` (no external scripts — a pygrep hook +fails when its pattern matches a violation) lint-check the covered TOMLs on commit: + +- `toml-header-block` — the file must open with a `#` header block (all covered files). +- `toml-no-decorative-dividers` — no `# ----`-style divider lines (all covered files). +- `toml-inline-annotations` — every `key = value` line carries a trailing `# description` + (`examples/*` and `test/test_data/*` only, per the rule-2 exception above). +- `toml-no-deprecated-keys` — no deprecated config keys; its key list mirrors + `_DEPRECATED_FFS_KEYS`/`_DEPRECATED_EQUIL_KEYS` in `src/GeneralizedPerturbedEquilibrium.jl`, + so extend the hook's pattern whenever a key is deprecated there. + +Run them manually with: + +```bash +pre-commit run --all-files +``` diff --git a/docs/src/set_up.md b/docs/src/set_up.md index 38c9dcebf..a7499ea78 100644 --- a/docs/src/set_up.md +++ b/docs/src/set_up.md @@ -398,6 +398,8 @@ The repository uses pre-commit hooks to maintain code quality. They run automati - Remove trailing whitespace and fix line endings - Validate YAML/TOML syntax - Prevent accidentally committing large files (>5MB) +- Check config-style TOML annotation conventions (header block, inline `# description` + comments, no deprecated keys — see `docs/development/toml-conventions.md`) **Installation:** diff --git a/examples/DIIID-like_SLAYER_example/gpec.toml b/examples/DIIID-like_SLAYER_example/gpec.toml index 2374ea514..46a52e049 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_example/gpec.toml @@ -8,9 +8,6 @@ eq_filename = "../DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # Poloidal field power exponent for Jacobian -power_b = 0 # Toroidal field power exponent for Jacobian -power_r = 0 # Major radius power exponent for Jacobian grid_type = "log_asymptotic" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized flux coordinate psihigh = 0.9995 # Upper limit of normalized flux coordinate @@ -34,11 +31,9 @@ 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 -mat_flag = true # Construct coefficient matrices for diagnostic purposes -ode_flag = true # Integrate ODEs for stability of the internal long-wavelength mode (must be true for GPEC) vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak +psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -60,7 +55,6 @@ ucrit = 1e4 # Maximum fraction of solutions allowed before re use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) parallel_threads = 1 # serial/bit-deterministic BVP — keeps the regression Δ' (and hence γ) reproducible populate_dense_xi = false # No PerturbedEquilibrium here; the dense EL pass has no consumer (auto-disabled under force_termination anyway). SLAYER needs only delta_prime_matrix from the parallel BVP. -truncate_at_dW_peak = false # Edge-dW scan stays diagnostic; integration domain set by qhigh / psihigh / dmlim set_psilim_via_dmlim = true # TRUE for diverted geqdsks — q → ∞ at separatrix, so dmlim truncation avoids the δW kink instability at negligible domain cost dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n @@ -71,26 +65,26 @@ dmlim = 0.2 # Truncate integration at (last_rational_q + dmli # kinetic file shared with the kinetic/NTV physics. Per-surface (uncoupled) # analysis: the headline is the unstable 2/1; the validity gate drops surfaces # with no real root (e.g. the 5/1, whose Δ' BVP yields a huge uncancellable Δ'). -enabled = true -inner_model = "slayer_fitzpatrick" -scan_mode = "amr" -coupling_mode = "uncoupled" -dc_type = "none" -mu_i = 2.0 -zeff = 1.0 +enabled = true # Run the SLAYER tearing-mode analysis +inner_model = "slayer_fitzpatrick" # Inner-layer Δ(Q) model: "slayer_fitzpatrick", "ggj_shooting", or "ggj_galerkin" +scan_mode = "amr" # Q-plane scan strategy: "amr" (adaptive refinement) or "brute_force" +coupling_mode = "uncoupled" # "uncoupled" (per-surface) or "coupled" (multi-surface determinant) +dc_type = "none" # Critical-Δ offset selector: "none", "lar", "rfitzp", or "toroidal" +mu_i = 2.0 # Ion mass in proton-mass units (2.0 = deuterium) +zeff = 1.0 # Effective charge chi_perp = 1.0 # fallback only; the kinetic file supplies χ⊥(ψ) chi_tor = 1.0 # fallback only; the kinetic file supplies χ_φ(ψ) pole_threshold_adaptive = true # SLAYER |Δ| spans many orders; adapt to 10·median(|Δ|) -filter_above_poles = true -filter_outside_re = true +filter_above_poles = true # Discard roots above the highest pole γ +filter_outside_re = true # Condition the above-pole filter on the +γ step exiting the Re(Δ)=0 contour loop polish_roots = true # refine each root to the true zero; enables the validity gate store_scan = false # per-surface scans omitted from HDF5 (uncoupled has one per surface) # Standardized kinetic-profile file (GPEC HDF5 kinetic schema), shared with the # sibling ideal example; carries n_e, T_e, T_i, omega_E, chi_e (χ⊥), chi_phi (χ_φ). -profile_file = "../DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5" +profile_file = "../DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5" # Kinetic-profile file (relative to the run dir), required when SLAYER is enabled [SLAYER.scan_grid] -Q_re_range = [-2.0, 2.0] -Q_im_range = [-0.5, 3.0] -nre = 41 -nim = 31 +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 diff --git a/examples/DIIID-like_gal_resistive_example/gpec.toml b/examples/DIIID-like_gal_resistive_example/gpec.toml index 62b7930f5..e296d0585 100644 --- a/examples/DIIID-like_gal_resistive_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_example/gpec.toml @@ -1,3 +1,9 @@ +# DIII-D-like H-mode resistive-MHD example: outer-region singular Galerkin Δ′ solve. +# n=1 tearing stability on the bundled geqdsk: the gal solver computes the Δ′ +# matching matrix (PEST-3 convention) plus rpec coil-response columns (delta_coil). +# The ldp radial grid packs points toward the rational surfaces; psihigh = 0.993 +# stays clear of the separatrix. No forcing and no PerturbedEquilibrium stage — +# see the sibling DIIID-like_gal_resistive_pe_example for the coil-driven continuation. [Equilibrium] eq_filename = "TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file @@ -26,7 +32,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall local_stability_flag = true # 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: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak +psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml index a6c6612a9..2d2cbe86b 100644 --- a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml @@ -1,3 +1,8 @@ +# DIII-D-like H-mode DRIVEN resistive example: gal Δ′ + inner-layer matching → PerturbedEquilibrium. +# Extends the sibling DIIID-like_gal_resistive_example with DIII-D C-coil forcing +# (n=1 cosine phasing): the gal-matched resistive ξ is fed into PerturbedEquilibrium +# for coil-driven singular coupling — island half-widths, Chirikov overlap, resonant flux. +# Same equilibrium and Galerkin settings as the sibling; differences start at [ForcingTerms]. [Equilibrium] eq_filename = "TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file @@ -26,7 +31,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall local_stability_flag = true # 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: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak +psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/examples/DIIID-like_ideal_example/gpec.toml b/examples/DIIID-like_ideal_example/gpec.toml index a36d21271..451a7a81f 100644 --- a/examples/DIIID-like_ideal_example/gpec.toml +++ b/examples/DIIID-like_ideal_example/gpec.toml @@ -87,7 +87,7 @@ electron = false # Ion-only NTV nutype = "harmonic" # Collision operator f0type = "maxwellian" # Equilibrium distribution moment = "pressure" # Pressure-moment NTV torque -atol_xlmda = 1e-9 # Inner pitch/energy integration tolerances -rtol_xlmda = 1e-5 -write_outputs_to_HDF5 = true -verbose = true +atol_xlmda = 1e-9 # Absolute tolerance for inner pitch + energy integrations +rtol_xlmda = 1e-5 # Relative tolerance for inner pitch + energy integrations +write_outputs_to_HDF5 = true # Write outputs to the HDF5 file +verbose = true # Enable verbose logging diff --git a/examples/Solovev_kinetic_NTV_example/gpec.toml b/examples/Solovev_kinetic_NTV_example/gpec.toml index f1d3ee96c..7812d4f9b 100644 --- a/examples/Solovev_kinetic_NTV_example/gpec.toml +++ b/examples/Solovev_kinetic_NTV_example/gpec.toml @@ -66,7 +66,6 @@ kinetic_factor = 0.0 # Scaling of kinetic matrices (0 = ideal path; >0 eulerlagrange_tolerance = 1e-7 # Relative tolerance for ODE integration of Euler-Lagrange equations singfac_min = 1e-4 # Fractional distance from rational q at which ideal jump enforced ucrit = 1e3 # Column-norm threshold that triggers solution renormalization -force_wv_symmetry = true # Enforce symmetry of the vacuum energy matrix save_interval = 3 # Save every Nth ODE step (1=all). Always saves near rational surfaces. # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) diff --git a/examples/Solovev_kinetic_calculated_example/gpec.toml b/examples/Solovev_kinetic_calculated_example/gpec.toml index ac27543cb..149f40af7 100644 --- a/examples/Solovev_kinetic_calculated_example/gpec.toml +++ b/examples/Solovev_kinetic_calculated_example/gpec.toml @@ -45,8 +45,8 @@ kinetic_factor = 1.0 # Full-strength kinetic matrices (the "calculated" eulerlagrange_tolerance = 1e-7 # Relative tolerance for ODE integration of Euler-Lagrange equations singfac_min = 1e-4 # Fractional distance from rational q at which ideal jump enforced ucrit = 1e3 # Maximum fraction of solutions allowed before re-normalized -write_outputs_to_HDF5 = true -verbose = false +write_outputs_to_HDF5 = true # Write outputs to the HDF5 file +verbose = false # Enable verbose logging # Solovev analytic equilibrium parameters (eq_type = "sol"); see SolovevConfig in src/Equilibrium. [SOL_INPUT] diff --git a/regression-harness/cases/diiid_slayer_n1.toml b/regression-harness/cases/diiid_slayer_n1.toml index 13167efe5..c1009fcbf 100644 --- a/regression-harness/cases/diiid_slayer_n1.toml +++ b/regression-harness/cases/diiid_slayer_n1.toml @@ -1,11 +1,14 @@ +# Regression case: DIII-D-like H-mode n=1 SLAYER tearing-mode growth rates. +# Uncoupled per-surface analysis with the AMR Q-plane scan; tracks the per-surface +# layer parameters and the extracted roots (Q, ω, γ) for the inner three rationals. +# 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] name = "diiid_slayer_n1" description = "DIII-D-like H-mode equilibrium, n=1, SLAYER tearing-mode analysis (uncoupled per-surface, AMR, validity-gated)" example_dir = "examples/DIIID-like_SLAYER_example" -# --------------------------------------------------------------------- # Per-surface SLAYER layer parameters (geometry + dimensionless) -# --------------------------------------------------------------------- [quantities.slayer_ising] h5path = "slayer/per_surface/ising" type = "real_vector" @@ -86,7 +89,6 @@ label = "SLAYER iota_e" noise_threshold = 1e-12 order = 19 -# --------------------------------------------------------------------- # Tearing eigenvalue (coupled mode → length 1). The headline deliverable: # a real, nonzero growth rate on a realistic equilibrium. Root-extraction # is sensitive to the AMR cell topology and ODE solver, so the Q/ω/γ @@ -96,7 +98,6 @@ order = 19 # surfaces only (2/1, 3/1, 4/1) via `first_3`: the Δ'/γ contour search is # numerically unreliable on the outermost surfaces (e.g. 5/1, 6/1, 7/1 near the # edge), so those are deliberately not golden-tracked. -# --------------------------------------------------------------------- [quantities.slayer_Q_re] h5path = "slayer/roots/Q_root_real" type = "real_vector" @@ -139,9 +140,7 @@ label = "SLAYER no_root flags [2/1,3/1,4/1]" noise_threshold = 0 order = 34 -# --------------------------------------------------------------------- # Settings (catches accidental config drift) -# --------------------------------------------------------------------- [quantities.slayer_enabled] h5path = "slayer/enabled" type = "int_scalar" @@ -150,9 +149,6 @@ label = "SLAYER enabled flag" noise_threshold = 0 order = 90 -# --------------------------------------------------------------------- -# Runtime -# --------------------------------------------------------------------- [quantities.runtime] h5path = "" type = "runtime" diff --git a/regression-harness/cases/gal_resistive_diiid.toml b/regression-harness/cases/gal_resistive_diiid.toml index 9ec61109e..6811d113a 100644 --- a/regression-harness/cases/gal_resistive_diiid.toml +++ b/regression-harness/cases/gal_resistive_diiid.toml @@ -1,3 +1,9 @@ +# Regression case: DIII-D-like n=1 outer-region singular Galerkin Δ′ solve. +# Tracks the resonant-surface inventory, the PEST-3 Δ matching-matrix diagonal +# (the physics-meaningful tearing Δ′), norms of the full outer Δ′ matrix and the +# rpec coil-response (Δ_coil) block, and the inner-layer matching outputs. +# 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] name = "gal_resistive_diiid" description = "DIII-D-like, n=1, RDCON outer-region Galerkin Δ′ with rpec coil columns (delta_coil)" diff --git a/regression-harness/cases/gal_resistive_pe.toml b/regression-harness/cases/gal_resistive_pe.toml index 7a26d7563..c2b059d7e 100644 --- a/regression-harness/cases/gal_resistive_pe.toml +++ b/regression-harness/cases/gal_resistive_pe.toml @@ -1,3 +1,9 @@ +# Regression case: DIII-D-like n=1 DRIVEN/RPEC pipeline — gal-matched resistive ξ +# 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, Δ′). +# 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] name = "gal_resistive_pe" description = "DIII-D-like, n=1, DRIVEN/RPEC: gal-matched resistive ξ → PerturbedEquilibrium (coil-driven singular coupling)" diff --git a/regression-harness/cases/solovev_kinetic_nuzero.toml b/regression-harness/cases/solovev_kinetic_nuzero.toml index e51efe873..078b9c363 100644 --- a/regression-harness/cases/solovev_kinetic_nuzero.toml +++ b/regression-harness/cases/solovev_kinetic_nuzero.toml @@ -1,3 +1,9 @@ +# Regression case: Solovev n=1 collisionless kinetic run (nutype = "zero"). +# Reuses the Solovev_kinetic_calculated_example deck via [overrides] at a +# mode-converged band; locks the collisionless real-x-space energy-integral +# eigenvalue plus equilibrium and singular-surface anchors. +# 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] name = "solovev_kinetic_nuzero" description = "Solovev, n=1, kinetic_source=calculated, nutype=zero (collisionless energy integral, issue #281), mode-converged band (delta_m=8, mpert=32). Reuses the calculated example deck via override; et[1]≈2.333-2.174i with the pressure-consistent kinetic.dat." diff --git a/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml b/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml index 3aab75163..ca6eeb003 100644 --- a/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml @@ -1,3 +1,8 @@ +# Solovev n=1 collisionless-kinetic smoke fixture (nutype = "zero"). +# Intentionally coarse (mpsi = 16, delta_m = 0) so the full-run test suite can +# exercise the real-x-space collisionless energy-integral path end-to-end; the +# test asserts only finiteness/sign of et[1]. The mode-converged eigenvalue is +# tracked separately by the solovev_kinetic_nuzero regression case. [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) @@ -40,11 +45,11 @@ kinetic_factor = 1.0 # Full-strength kinetic matrices (the "calculated" eulerlagrange_tolerance = 1e-7 # Relative tolerance for ODE integration of Euler-Lagrange equations singfac_min = 1e-4 # Fractional distance from rational q at which ideal jump enforced ucrit = 1e3 # Maximum fraction of solutions allowed before re-normalized -write_outputs_to_HDF5 = true -verbose = false +write_outputs_to_HDF5 = true # Write outputs to the HDF5 file +verbose = false # Enable verbose logging [KineticForces] -nutype = "zero" # Collisionless operator — isolates the real-x-space collisionless energy integral (issue #281) +nutype = "zero" # Collisionless operator — isolates the real-x-space collisionless energy integral # Solovev analytic equilibrium parameters (eq_type = "sol"); see SolovevConfig in src/Equilibrium. [SOL_INPUT]