HMC Utilities - lattice/quda GitHub Wiki

QUDA includes an internal Hybrid Monte Carlo (HMC) layer that exercises the GPU-resident gauge / fermion / momentum infrastructure end-to-end. It began life as a development and validation harness for solver-acceleration work (notably MG with Eigenspace Tracking) and has since grown into a functioning HMC in its own right: two-flavour pseudofermion forces for Wilson, Wilson-clover, twisted-mass (degenerate singlet), and twisted-clover (degenerate singlet), four symplectic integrators, MG-preconditioned solves with automatic re-setup, and Metropolis accept/reject — all validated statically (per-link and directional force oracles at the 1e-7 level, exact heatbath identities) and dynamically (dH ∝ dt^p at the theoretical integrator order for every action × integrator combination; see Validation).

Status: working, under review

The HMC layer works well within its supported scope and is validated to the level documented on this page. It should nevertheless be regarded as work in progress: it is new code awaiting deep scrutiny from the QUDA review team, and interfaces and conventions may still move in response. For production ensemble generation the mature path remains driving QUDA from an established lattice code (Chroma, tmLQCD, MILC, ...) via the inverter / force C-API.

Supported now:

  • Wilson, Wilson-clover, twisted-mass and twisted-clover (degenerate singlet) two-flavour HMC — complete forces (hopping + clover-sigma + per-action TrLog determinant terms), heatbath, action, all four integrators, MG integration.

Not yet available (out of scope for the current contribution):

  • Non-degenerate twisted doublets (ε ≠ 0) — the underlying kernels exist; the force/heatbath layer is singlet-only.
  • Staggered, HISQ, asqtad HMC.
  • Domain-wall and Möbius HMC.
  • Multi-flavour / RHMC / rational forces (n_f ≠ 2).
  • Hasenbusch mass preconditioning, multiple pseudofermions per flavour (planned next).
  • Stout / HEX / link smearing inside the action.

Contents

  1. What is provided
  2. Test-suite coverage
  3. CLI parameters
  4. Example invocations
  5. Saved gauges and checkpoints
  6. What this is not

What is provided

Integrators

Polymorphic Integrator hierarchy (include/hmc_integrator.h, lib/hmc_integrator.cpp) with a common factory + getOrCreateIntegrator cache that survives across trajectories so per-integrator persistent state (e.g. nested-FGI's coarse deflation manager) is not torn down and rebuilt every trajectory.

Integrator Description Order CLI
LeapfrogIntegrator Standard 2nd-order PQP leapfrog. 2 --hmc-integrator 0
OmelyanIntegrator 2nd-order minimum-norm Omelyan, λ = 0.1931833... 2 --hmc-integrator 1
FGIntegrator 4th-order Hessian-free force-gradient (Yin–Mawhinney / Schäfers; PQPQP_FG, λ=1/6, ξ=1/72). 4 --hmc-integrator 2
NestedFGIIntegrator 4th-order force-gradient outer + leapfrog/Omelyan inner. Uses a LowModeForce inner approximation built from a coarse-grid deflation manager, allowing the inner timescale to skip the expensive fine-grid solve. Required when MG is enabled. 4 --hmc-integrator 3

The Hessian-free force-gradient term follows Schäfers et al. (arXiv:2501.17632 Eq. 8) with the appropriate convention mapping for QUDA's force sign convention. The FG sub-step performs the full resident-state refresh (ghost exchange, gauge hierarchy, extended-gauge halo, clover recompute) at both the displaced and the restored gauge, so the central kick's force is evaluated on a fully consistent state.

Forces and actions

Two-flavour even-odd-preconditioned pseudofermion forces are provided for Wilson, Wilson-clover, twisted-mass (degenerate singlet), and twisted-clover (degenerate singlet), all through one action-generic entry point.

Function File Purpose
computeEOFermionForce lib/hmc.cpp Action-generic EO fermion force: hopping term for all actions, plus the clover-sigma and (twisted) TrLog terms for clover-type actions. Fields are built dagger-explicitly (Dagger(DAG_YES) + PC Dslash under asymmetric matpc), which handles the twisted μ → −μ flip that a γ₅-sandwich cannot.
computeFermionAction / computeEOFermionAction lib/hmc.cpp Pseudofermion action S_f = φ†(M̂†M̂)⁻¹φ plus the per-action determinant term (−2·TrLog[C_oo] clover, −TrLog[(C²+μ̃²)_oo] twisted clover). MG-enabled path uses the γ₅ two-pass with the μ-flip twisted actions require (pass 1 solves M(−μ)).
generateEOPseudofermion lib/hmc.cpp Heatbath φ = M̂†η from Gaussian η, so S_f = ‖η‖² exactly (checked per-configuration by HMC.HeatbathActionIdentity).
Standard QUDA gauge force lib/gauge_force_quda.cu (existing) Pure-gauge Wilson plaquette force, called from Integrator::kick.

Force convention. With QUDA's momentum machinery (momAction T = ¼Σ_a p_a² in Gell-Mann components, MD flow U' = exp(dt·P)U), conservation of H = T + S requires the stored force to satisfy F_a = −2·∂S/∂u_a — the same convention the production gauge force obeys. All fermion-force coefficients are derived under this convention and pinned by three static oracles (per-link, directional, gauge-consistency) plus the dynamical dH ∝ dt^p arbiter (see Validation below).

State-refresh discipline. gaugePrecise is a ghost-padded field whose pads only loadGaugeQuda fills; the Dirac operators read the pads. Every device-side gauge write in the HMC path (drift, nested inner step, force-gradient excursion and restore, Metropolis reject-restore) must be followed by the full refresh — ghost exchange, sloppy-gauge sync, extended-gauge rebuild, in-place clover recompute (computed at double, demoted) and in-place sloppy-clover family refresh. This is centralised in hmcRefreshResidentGaugeState (lib/hmc_integrator.cpp); skipping any element produces a dt-independent dH plateau that pointwise force tests at the initial gauge cannot see. In-place is mandatory throughout: reallocating any of these fields dangles pointers an MG hierarchy captured at setup.

The C-API is hmcTrajectoryQuda (one trajectory) and hmcRunQuda (multi-trajectory loop with Metropolis accept/reject, optional thermalisation, optional checkpointing).

MG re-setup integration

The HMC loop is the natural test bed for MG-preconditioner staleness. Three update modes are wired into hmcRunQuda:

Mode Trigger Cost Description
Thin update every accepted trajectory cheapest Coarse Dirac rebuilt from new gauge; null vectors and prolongator preserved.
Standard refresh iter-ratio trip OR --hmc-mg-setup-interval N medium Existing null vectors polished with --mg-setup-maxiter-refresh N CG iters; prolongator and coarse Dirac rebuilt.
Pool refresh (with Eigenspace Tracking) as standard refresh, plus --eigentracking-mg-refresh-iters N>=0 medium-cheap Existing null vectors replaced from the eigentracker pool, polished N CG iters per vector; prolongator and coarse Dirac rebuilt.
Full re-setup --hmc-mg-setup-interval N with thin_update_only=FALSE and ET disabled most expensive Null vectors regenerated from random with --mg-setup-maxiter CG iters.

The 2.0× iter-ratio adaptive trigger is QUDA's default safety net: if per-solve iters exceed 2.0× the early-trajectory baseline, the configured refresh path fires.

Eigenspace tracking

See the dedicated Eigenspace Tracking page. The HMC layer is the primary integration site — it is where the tracker's "operator changes between solves" assumption is non-trivially exercised.

Test-suite coverage

tests/hmc_test.cpp registers the following gtest cases. They all run on small lattices (default 4^4) and complete in seconds; they form the regression net for HMC-side changes.

Test What it verifies
HMC.ParameterDefaults QudaHMCParam defaults round-trip through newQudaHMCParam / checkHMCParam.
HMC.LeapfrogTrajectory / HMC.OmelyanTrajectory / HMC.ForceGradientTrajectory / HMC.NestedFGIParameterSetup Each integrator runs a single trajectory without crashing and produces a finite dH.
HMC.MultiTrajectoryRun hmcRunQuda with n_traj > 1 correctly resets per-trajectory state.
HMC.MGPreconditionedRun MG-preconditioned trajectory works end-to-end (single trajectory, smoke-test of all three MG-update modes).
HMC.GaugeForceActionConsistency S(U + dt·F) - S(U) = dt × dS/dU + O(dt²); cross-checks the gauge force against finite-difference of the gauge action.
HMC.DirectionalForceTest / HMC.PerLinkForceTest Per-link numerical derivative of the action vs analytical force. The single most important regression test for any new force kernel. Registered per action (hmc_force_oracles_{clover,twisted_mass,twisted_clover} ctest entries) together with degenerate-limit cross-checks (μ → 0 reproduces Wilson/clover, csw → 0 reproduces twisted mass).
HMC.HeatbathActionIdentity Exact per-configuration identity S_f = ‖η‖² for the pseudofermion heatbath (bilinear part). Catches mis-sampled heatbaths (φ = M̂η instead of M̂†η) that dH and reversibility tests cannot see, and sharply tests the twisted μ-dagger handling.
HMC.dHStatistics / HMC.dHScaling / HMC.dHScalingNestedFGI Multi-trajectory <dH> distributions and dH ∝ τ·dt^p scaling for the integrator order p. Catches integrator scaling regressions.
HMC.ReversibilityTest / HMC.ReversibilityAllIntegrators Bit-for-bit reversibility for each integrator (U(τ) then U(-τ) returns to the starting gauge).
HMC.Production Long-running production-style trajectory; configurable via CLI for in-context testing on real lattices. The test harness used for the Eigenspace Tracking results.

Validation: dH scaling studies

Two 8⁴ studies validate every action × integrator combination dynamically (the per-link and directional oracles pin the force normalisation pointwise; only dH ∝ dt^p scaling ties force, action, momAction and integrator together).

Deterministic hot-start study. Single trajectory per point from an identical hot start (fixed momentum seed, no thermalisation), four geometric step sizes, least-squares fit of log|dH| vs log dt:

action \ integrator leapfrog (p≈2) Omelyan (p≈2) FGI (p≈4) nested-FGI (p≈4)†
Wilson 1.998 1.999 4.006 4.012
clover 1.998 1.999 4.005 3.900
twisted-mass 1.998 1.999 4.006 4.012
twisted-clover 1.998 1.999 4.005 3.901

Equilibrated study. Per point: load a 100-sweep β = 6 quenched heatbath lattice, run 5 forced-accept trajectories (dynamical-fermion thermalisation), 5 with Metropolis on (equilibration, discarded), then 5 measured; ⟨|dH|⟩ of the measured five enters the fit. This removes hot-start bias and exercises accept/reject:

action \ integrator leapfrog Omelyan FGI nested-FGI†
Wilson 2.189 2.000 4.040 4.074
clover 2.094 1.999 4.054 4.071
twisted-mass 2.184 2.000 4.040 4.076
twisted-clover 2.334 1.999 4.055 4.036

† Nested-FGI is measured on the same step grid as FGI (n_outer ∈ {4, 6, 9, 14}), the h⁴-dominated side of its two-term error crossover (see next section), with n_inner = 3 and d/s/s mixed precision for all four actions; every equilibrated point accepted 5/5.

Notes:

  • FGI holds clean 4th order for the clover-type actions because the force-gradient gauge excursion recomputes the clover at the displaced gauge (part of the state-refresh discipline above); the same applies to the nested variant.
  • The equilibrated protocol exercises the full accept/reject machinery, including complete resident-state restoration after a rejection — coarse-step points reject routinely while fine-step points do not, so both paths enter the fit.
  • Mixed precision (double outer, single MG) reproduces double-quality conservation for clover-type nested-FGI via the in-place sloppy-clover family refresh each step; pure single precision floors at |dH| ≈ 26 on 8⁴ and is not recommended for clover-type energy bookkeeping.

Nested-FGI integrator order: (h, m) theory verification

The nested-FGI scheme is structurally the adapted nested force-gradient integrator of Shcherbakov, Ehrhardt, Finkenrath, Günther, Knechtli & Peardon (arXiv:1512.03812), Eq. (2.11): a 5-stage force-gradient outer scheme (λ = 1/6, middle weight 2/3, ξ = 1/72) whose drift is replaced by m fixed leapfrog substeps of the cheap (low-mode) action. The theory prediction is a two-term error surface

dH(h, m) = a·(h/m)² + b·h⁴ + higher order

— the outer h² term is cancelled by the force-gradient correction, the inner leapfrog leaves an h² term suppressed by 1/m², and the b·h⁴ term is the surviving outer remainder. Asymptotically (h → 0 at fixed m) the scheme is therefore order 2, but with the inner coefficient suppressed it exhibits an effective order 4 over the practical step-size window.

This is verified directly with a 30-point (h, m) grid — n_outer ∈ {6…40} × m ∈ {1, 2, 3, 6, 12}, Wilson 8⁴, one deterministic trajectory per point from an identical thermalized start (fixed seed, tol = 1e-10), signed dH fitted against the model (run_nested_fgi_theory.sh / fit_nested_theory.py):

  • Global two-parameter fit: a = −0.68, b = +32.7 with 2.7 % median relative residual over all 30 signed dH values spanning four orders of magnitude.
  • Blind extrapolation in m: coefficients fitted on the m ≤ 3 half of the grid predict the unseen m ∈ {6, 12} rows at 10 % median error, including the dH sign changes at the predicted crossover h*(m) = √|a/b|/m.
  • Slice behaviour: the fixed-h slope in m walks from 0 (b·h⁴ plateau) to −2 (inner term) exactly as the model dictates, and the fixed-m effective exponents — which range from 0.9 to 5.7 through the interference region — are all reproduced by the same two numbers. Because the inner term scales as 1/m², an m-independent h² component in this scan would signal a defect in the outer force-gradient stage (e.g. incomplete state refresh at the displaced gauge), making the grid a sharp regression test as well as an order verification.

Practical corollaries: n_inner is a genuine conservation knob (doubling it cuts the inner error term ×4); and near the crossover a single-exponent dt^p fit is ill-posed — apparent p anywhere between 2 and 6 is consistent with correct behaviour, so scaling checks of the nested integrator should either fit the two-term model or sit clearly on one side of h*.

The harnesses (run_dh_scaling.sh, run_dh_scaling_thermalized.sh, run_nested_fgi_theory.sh) drive HMC.Production and are reproduced in full in the appendix below, so the studies can be verified or adapted directly; the in-tree HMC.dHScaling gtest pins the Wilson leapfrog case as a fast regression.

CLI parameters

The HMC layer adds the following groups to tests/hmc_test. --inv-multigrid true (and the standard MG parameter group) is required to exercise the MG path.

Trajectory configuration

Flag Description Default
--hmc-integrator <0/1/2/3> Leapfrog / Omelyan / FG / Nested-FGI. 2 (FG)
--hmc-tau <τ> Trajectory length. 1.0
--hmc-n-steps <N> Outer MD steps per trajectory. 20
--hmc-beta <β> Pure-gauge coupling. 5.6
--hmc-n-trajectories <N> Total trajectories in hmcRunQuda. 1
--hmc-thermalization <N> Auto-accept (no Metropolis) for the first N trajectories. 0
--hmc-momentum-seed <seed> RNG seed for momentum sampling and Metropolis draws. Same seed → reproducible accept/reject sequence (required for paired comparisons of solver flags). 0
--fermion-t-boundary <periodic/anti-periodic> Fermion temporal BC. anti-periodic

Gauge I/O

Flag Description
--hmc-gauge-infile <path> Load starting gauge from a LIME-format file (e.g., a Chroma cfg).
--hmc-gauge-outfile <path> Write final gauge to LIME at the end of the run.
--hmc-checkpoint <N> Write a gauge checkpoint every N accepted trajectories.
--hmc-checkpoint-prefix <str> Prefix for checkpoint filenames.

Nested FGI inner timescale

Flag Description Default
--hmc-n-inner-steps <N> Inner sub-trajectory steps per outer FG step (cheap-force scale). 5
--hmc-inner-integrator <0/1> Inner integrator: leapfrog (0) or Omelyan (1). 1
--hmc-n-defl <N> Coarse deflation eigenpair count for the LowModeForce inner approximation. 8
--hmc-defl-refresh-interval <N> Inner steps between coarse-deflation Rayleigh-Ritz refreshes (0 = frozen). 0

MG re-setup interaction

Flag Description Default
--hmc-mg-setup-interval <N> Force MG refresh every N accepted trajectories (0 = no periodic refresh). 0
--hmc-mg-setup-iter-ratio <r> Adaptive trigger: refresh when iters/solve exceeds baseline. 2.0
--hmc-mg-setup-iter-baseline-traj <N> Trajectories used to establish baseline. 5

See Eigenspace Tracking for the full set of --eigentracking-* flags.

Example invocations

Fast regression test (4^4 Wilson, all integrators)

./tests/hmc_test --gtest_filter="HMC.ReversibilityAllIntegrators" \
    --dim 4 4 4 4 \
    --dslash-type wilson --mass -0.5 \
    --prec double --prec-sloppy double --prec-precondition double \
    --verbosity verbose

Runs reversibility checks on all four integrators in a few seconds. Use this as the smoke test after any HMC code change.

Per-link force test

The most stringent regression for force-kernel work. Compares numerical (S(U + ε·δU) - S(U)) / ε against the analytical force, link by link.

./tests/hmc_test --gtest_filter="HMC.PerLinkForceTest" \
    --dim 4 4 4 4 \
    --dslash-type wilson \
    --prec double --prec-sloppy double --prec-precondition double \
    --verbosity verbose

Production-style trajectory with MG + Eigenspace Tracking

24³×48 Wilson at m = -0.835 (κ = 0.158), nested FGI with 80 outer steps at τ = 1, MG with eigentracker pool refresh every accepted trajectory:

./tests/hmc_test --gtest_filter="HMC.Production" \
    --dim 24 24 24 48 \
    --prec double --prec-sloppy single --prec-precondition single \
    --prec-null single --prec-eigensolver single --prec-refine single \
    --dslash-type wilson --mass -0.835 \
    --inv-type gcr --solution-type mat-pc --solve-type direct-pc --matpc even-even \
    --niter 5000 --tol 1e-9 \
    --hmc-integrator 3 --hmc-tau 1.0 --hmc-n-steps 80 --hmc-beta 5.6 \
    --inv-multigrid true --mg-levels 2 --mg-block-size 0 4 4 4 4 --mg-nvec 0 24 \
    --mg-setup-inv 0 cgnr --mg-setup-iters 0 2 \
    --mg-setup-tol 0 1e-6 --mg-setup-maxiter 0 2000 --mg-setup-maxiter-refresh 0 100 \
    --eigentracking true --eigentracking-mg-refresh-iters 2 \
    --hmc-mg-setup-interval 1 \
    --hmc-mg-setup-iter-baseline-traj 5 \
    --fermion-t-boundary anti-periodic \
    --hmc-gauge-infile path/to/start.lime \
    --hmc-gauge-outfile path/to/end.lime \
    --hmc-checkpoint 5 --hmc-checkpoint-prefix path/to/ckpt_ \
    --hmc-n-trajectories 20 --hmc-momentum-seed 67890

This is the configuration used to produce the Eigenspace Tracking results. The matching baseline (no ET) drops the four --eigentracking-* lines and --hmc-mg-setup-interval 1.

Saved gauges and checkpoints

LIME-format I/O is supported via the existing QUDA helpers. Gauge files written by this layer can be read by Chroma, tmLQCD, etc., and vice versa. Checkpointing writes the gauge after every N-th accepted trajectory, named <prefix>traj_<n>.lime.

What this is not

For users surveying QUDA's gauge utilities and deciding whether to use this layer for production running:

  • Not yet review-hardened — the layer is validated as documented above but is new code under active review; treat conventions and interfaces as subject to change until it has been through the QUDA review process.
  • Not a staggered / HISQ / domain-wall HMC — no force kernels for these actions are wired into this layer. Production: use MILC + QUDA, CPS + QUDA.
  • Not a multi-flavour or mass-preconditioned HMC — single-pseudofermion n_f = 2 only, degenerate-singlet twists only. No rational approximation; no Hasenbusch splitting yet (planned); no multi-time-scale separation beyond the nested-FGI inner / outer split.
  • Not a HMC with smeared links in the action — pure-gauge force is Wilson plaquette only.

If you need any of the above today, drive QUDA from a production HMC code (Chroma, tmLQCD, MILC, ...) via the standard inverter / force C-API. For the supported action set, the layer documented here is a complete, validated HMC and a convenient self-contained harness for solver-side development.

Appendix: validation harnesses

The scripts behind the studies above, verbatim. Each runs from build/tests next to the hmc_test binary and writes per-point logs plus a fitted summary; step grids, actions, precisions and lattice parameters are plain shell variables, so adapting a study (different masses, volumes, step windows, n_inner, …) is an edit away. run_nested_fgi_theory.sh uses the companion fitter fit_nested_theory.py, also listed.

run_dh_scaling.sh

#!/usr/bin/env bash
# run_dh_scaling.sh — dH-vs-dt scaling study on 8^4: 4 actions x 4 integrators.
# For each cell, runs HMC.Production once per step count (1 trajectory, no
# thermalization, fixed momentum seed => identical (U0, p0, phi) across the
# dt scan), extracts dH, and fits p in |dH| ~ dt^p by least squares on logs.
# Run from build/tests:  bash run_dh_scaling.sh
set -u

BIN=./hmc_test
RES_DIR="$PWD/tunecache_dh_scaling"
LOG_DIR="$PWD/logs_scaling"
DIM="--dim 8 8 8 8"
TAU=0.5
SEED=12345
TOL=1e-9
CELL_TIMEOUT=3600

[ -x "$BIN" ] || { echo "ERROR: run from build/tests"; exit 1; }
mkdir -p "$RES_DIR" "$LOG_DIR"
if [ -f "$RES_DIR/tunecache.tsv" ] && [ "$BIN" -nt "$RES_DIR/tunecache.tsv" ]; then
  echo "Wiping stale tunecache."
  rm -f "$RES_DIR"/*.tsv
fi

PREC_DBL="--prec double --prec-sloppy double --prec-precondition double"
PREC_MIX="--prec double --prec-sloppy single --prec-precondition single"   # nested (all actions)

action_flags() { # $1 action
  case "$1" in
    wilson)         echo "--dslash-type wilson --mass 0.5" ;;
    clover)         echo "--dslash-type clover --compute-clover true --clover-csw 1.0 --mass 0.5" ;;
    twisted-mass)   echo "--dslash-type twisted-mass --mu 0.1 --flavor singlet --mass 0.5" ;;
    twisted-clover) echo "--dslash-type twisted-clover --mu 0.1 --flavor singlet --compute-clover true --clover-csw 1.0 --mass 0.5" ;;
  esac
}
integ_id()    { case "$1" in leapfrog) echo 0;; omelyan) echo 1;; fgi) echo 2;; nested-fgi) echo 3;; esac; }
integ_steps() { case "$1" in leapfrog|omelyan) echo "16 24 36 54";; fgi|nested-fgi) echo "4 6 9 14";; esac; }
integ_p()     { case "$1" in leapfrog|omelyan) echo 2.0;; fgi|nested-fgi) echo 4.0;; esac; }

RESULTS=""

for action in wilson clover twisted-mass twisted-clover; do
  for integ in leapfrog omelyan fgi nested-fgi; do
    cell="${action}_${integ}"
    id=$(integ_id "$integ")
    expected=$(integ_p "$integ")
    note=""

    if [ "$integ" = nested-fgi ]; then
      mg="--mg-levels 2 --mg-block-size 0 4 4 4 4"
      prec=$PREC_MIX
    else
      mg="--mg-levels 1"
      prec=$PREC_DBL
    fi

    echo "=== [$(date '+%H:%M:%S')] $cell (expect p ~ $expected) $note"
    datafile="$LOG_DIR/$cell.dat"
    : > "$datafile"
    fail=0
    for n in $(integ_steps "$integ"); do
      log="$LOG_DIR/${cell}_n${n}.log"
      # shellcheck disable=SC2086
      QUDA_RESOURCE_PATH="$RES_DIR" timeout "$CELL_TIMEOUT" mpiexec -n 1 "$BIN" $DIM \
        $(action_flags "$action") $prec --tol $TOL \
        --hmc-integrator "$id" --hmc-n-steps "$n" --hmc-tau $TAU \
        --hmc-n-trajectories 1 --hmc-thermalization 0 --hmc-momentum-seed $SEED \
        $mg --gtest_filter=HMC.Production > "$log" 2>&1
      rc=$?
      dh=$(grep -oE 'dH = [+-][0-9.e+-]+' "$log" | head -1 | awk '{print $3}')
      if [ $rc -ne 0 ] || [ -z "$dh" ]; then
        echo "    n=$n : FAILED (rc=$rc)"
        fail=1
        continue
      fi
      dt=$(awk -v t=$TAU -v n=$n 'BEGIN{printf "%.8f", t/n}')
      echo "$dt $dh" >> "$datafile"
      printf '    n=%-3d dt=%-9s dH=%s\n' "$n" "$dt" "$dh"
    done

    npts=$(wc -l < "$datafile")
    if [ "$npts" -lt 3 ]; then
      RESULTS="$RESULTS$cell|FAIL|$expected|n/a|$note
"
      continue
    fi
    # least-squares fit of log|dH| vs log dt
    pfit=$(awk '{x=log($1); v=$2; if (v<0) v=-v; if (v<=0) next; y=log(v);
                 n++; sx+=x; sy+=y; sxx+=x*x; sxy+=x*y}
                END {if (n>=3) printf "%.3f", (n*sxy-sx*sy)/(n*sxx-sx*sx); else printf "n/a"}' "$datafile")
    verdict="PASS"
    awk -v p="$pfit" -v e="$expected" 'BEGIN{d=p-e; if (d<0) d=-d; exit !(d>0.5)}' && verdict="CHECK"
    [ $fail -eq 1 ] && verdict="PARTIAL"
    printf '    p_fit = %s (expected %s) -> %s\n' "$pfit" "$expected" "$verdict"
    RESULTS="$RESULTS$cell|$verdict|$expected|$pfit|$note
"
  done
done

{
  echo
  echo "==================== dH SCALING SUMMARY (8^4, tau=$TAU, tol=$TOL) ===================="
  printf '%-30s %-9s %-10s %-8s %s\n' "cell" "verdict" "expected p" "p_fit" "note"
  printf '%s\n' "--------------------------------------------------------------------------------"
  printf '%s' "$RESULTS" | while IFS='|' read -r c v e p nt; do
    [ -n "$c" ] && printf '%-30s %-9s %-10s %-8s %s\n' "$c" "$v" "$e" "$p" "$nt"
  done
  echo
  echo "CHECK = |p_fit - expected| > 0.5; inspect logs_scaling/<cell>.dat and per-n logs."
  echo "nested-fgi: effective 4th order on this step grid (h^4-dominated side of its"
  echo "two-term error crossover dH = a*(h/m)^2 + b*h^4; see run_nested_fgi_theory.sh)."
} | tee "$LOG_DIR/summary.txt"

run_dh_scaling_thermalized.sh

#!/usr/bin/env bash
# run_dh_scaling_thermalized.sh — equilibrated dH-vs-dt scaling on 8^4.
#
# Protocol (per action x integrator x step size):
#   1. Load the 100-sweep quenched heatbath lattice (beta = 6.0).
#   2. 5 trajectories with forced accept   (dynamical-fermion thermalization)
#   3. 5 trajectories with Metropolis on   (equilibration, discarded)
#   4. 5 trajectories measured             (<|dH|> enters the scaling fit)
# The gauge is re-loaded fresh for every step size, so each dt point runs the
# identical protocol from the identical partially-thermalized, unbiased start.
# Fixed momentum seed => same heatbath momenta per trajectory index across the
# dt scan (variance reduction for the fit).
#
# Run from build/tests:  bash run_dh_scaling_thermalized.sh
set -u

BIN=./hmc_test
GAUGE="$PWD/therm_8x8_b6.0_hb100.lime"
RES_DIR="$PWD/tunecache_dh_scaling"
LOG_DIR="$PWD/logs_scaling_therm"
DIM="--dim 8 8 8 8"
TAU=0.5
SEED=12345
TOL=1e-9
N_TRAJ=15
N_THERM=5      # forced-accept trajectories
N_EQUIL=5      # Metropolis-on, discarded
N_SKIP=$((N_THERM + N_EQUIL))
CELL_TIMEOUT=3600

[ -x "$BIN" ] || { echo "ERROR: run from build/tests"; exit 1; }
[ -f "$GAUGE" ] || { echo "ERROR: thermalized gauge $GAUGE not found"; exit 1; }
mkdir -p "$RES_DIR" "$LOG_DIR"
if [ -f "$RES_DIR/tunecache.tsv" ] && [ "$BIN" -nt "$RES_DIR/tunecache.tsv" ]; then
  echo "Wiping stale tunecache."
  rm -f "$RES_DIR"/*.tsv
fi

PREC_DBL="--prec double --prec-sloppy double --prec-precondition double"
PREC_MIX="--prec double --prec-sloppy single --prec-precondition single"
PREC_SGL="--prec single --prec-sloppy single --prec-precondition single"

action_flags() {
  case "$1" in
    wilson)         echo "--dslash-type wilson --mass 0.5" ;;
    clover)         echo "--dslash-type clover --compute-clover true --clover-csw 1.0 --mass 0.5" ;;
    twisted-mass)   echo "--dslash-type twisted-mass --mu 0.1 --flavor singlet --mass 0.5" ;;
    twisted-clover) echo "--dslash-type twisted-clover --mu 0.1 --flavor singlet --compute-clover true --clover-csw 1.0 --mass 0.5" ;;
  esac
}
integ_id()    { case "$1" in leapfrog) echo 0;; omelyan) echo 1;; fgi) echo 2;; nested-fgi) echo 3;; esac; }
integ_steps() { case "$1" in leapfrog|omelyan) echo "16 24 36 54";; fgi|nested-fgi) echo "4 6 9 14";; esac; }
integ_p()     { case "$1" in leapfrog|omelyan) echo 2.0;; fgi|nested-fgi) echo 4.0;; esac; }

RESULTS=""

for action in wilson clover twisted-mass twisted-clover; do
  for integ in leapfrog omelyan fgi nested-fgi; do
    cell="${action}_${integ}"
    id=$(integ_id "$integ")
    expected=$(integ_p "$integ")
    note=""

    if [ "$integ" = nested-fgi ]; then
      mg="--mg-levels 2 --mg-block-size 0 4 4 4 4"
      # d/s/s mixed precision (in-place sloppy-clover family refresh keeps
      # clover-type actions at double-quality conservation)
      prec=$PREC_MIX
    else
      mg="--mg-levels 1"
      prec=$PREC_DBL
    fi

    echo "=== [$(date '+%H:%M:%S')] $cell (expect p ~ $expected) $note"
    datafile="$LOG_DIR/$cell.dat"
    : > "$datafile"
    fail=0
    for n in $(integ_steps "$integ"); do
      log="$LOG_DIR/${cell}_n${n}.log"
      # shellcheck disable=SC2086
      QUDA_RESOURCE_PATH="$RES_DIR" timeout "$CELL_TIMEOUT" mpiexec -n 1 "$BIN" $DIM \
        --load-gauge "$GAUGE" \
        $(action_flags "$action") $prec --tol $TOL \
        --hmc-integrator "$id" --hmc-n-steps "$n" --hmc-tau $TAU \
        --hmc-n-trajectories $N_TRAJ --hmc-thermalization $N_THERM --hmc-momentum-seed $SEED \
        $mg --gtest_filter=HMC.Production > "$log" 2>&1
      rc=$?
      # mean |dH| over the final measurement trajectories (skip therm + equil)
      stats=$(grep -E 'hmcRunQuda: (ACCEPTED|REJECTED)' "$log" \
        | awk -v skip=$N_SKIP 'NR>skip {v=$5+0; if (v<0) v=-v; s+=v; n++; if ($2=="ACCEPTED") a++}
                               END {if (n) printf "%.6e %d/%d", s/n, a, n; else printf "n/a n/a"}')
      dh=$(echo "$stats" | awk '{print $1}')
      acc=$(echo "$stats" | awk '{print $2}')
      if [ $rc -ne 0 ] || [ "$dh" = "n/a" ]; then
        echo "    n=$n : FAILED (rc=$rc)"
        fail=1
        continue
      fi
      dt=$(awk -v t=$TAU -v n=$n 'BEGIN{printf "%.8f", t/n}')
      echo "$dt $dh" >> "$datafile"
      printf '    n=%-3d dt=%-9s <|dH|>=%-13s acc(meas)=%s\n' "$n" "$dt" "$dh" "$acc"
    done

    npts=$(wc -l < "$datafile")
    if [ "$npts" -lt 3 ]; then
      RESULTS="$RESULTS$cell|FAIL|$expected|n/a|$note
"
      continue
    fi
    pfit=$(awk '{x=log($1); v=$2+0; if (v<=0) next; y=log(v);
                 n++; sx+=x; sy+=y; sxx+=x*x; sxy+=x*y}
                END {if (n>=3) printf "%.3f", (n*sxy-sx*sy)/(n*sxx-sx*sx); else printf "n/a"}' "$datafile")
    verdict="PASS"
    awk -v p="$pfit" -v e="$expected" 'BEGIN{d=p-e; if (d<0) d=-d; exit !(d>0.5)}' && verdict="CHECK"
    [ $fail -eq 1 ] && verdict="PARTIAL"
    printf '    p_fit = %s (expected %s) -> %s\n' "$pfit" "$expected" "$verdict"
    RESULTS="$RESULTS$cell|$verdict|$expected|$pfit|$note
"
  done
done

{
  echo
  echo "============ EQUILIBRATED dH SCALING (8^4 heatbath start, 5+5+5 protocol) ============"
  printf '%-30s %-9s %-10s %-8s %s\n' "cell" "verdict" "expected p" "p_fit" "note"
  printf '%s\n' "--------------------------------------------------------------------------------"
  printf '%s' "$RESULTS" | while IFS='|' read -r c v e p nt; do
    [ -n "$c" ] && printf '%-30s %-9s %-10s %-8s %s\n' "$c" "$v" "$e" "$p" "$nt"
  done
  echo
  echo "Protocol per dt point: load 100-sweep beta=6 heatbath lattice, 5 forced-accept +"
  echo "5 Metropolis (discarded) + 5 measured. <|dH|> of the 5 measured enters the fit."
  echo "CHECK = |p_fit - expected| > 0.5. Data: logs_scaling_therm/<cell>.dat"
} | tee "$LOG_DIR/summary.txt"

run_nested_fgi_theory.sh

#!/usr/bin/env bash
# run_nested_fgi_theory.sh — nested-FGI two-timescale theory verification on 8^4.
#
# Theory (Shcherbakov et al., arXiv:1512.03812 §2.2): with force-gradient
# information at the outer level only and m fixed inner leapfrog substeps,
#
#     dH(h, m) = a*(h/m)^2 + b*h^4 + higher order.
#
# Protocol: single deterministic trajectory per (n_outer, m) grid point —
# identical 100-sweep heatbath start, identical momentum seed, forced accept —
# so every point shares the same start state and hence the same (a, b).
# The signed dH of trajectory 1 is recorded; fit_nested_theory.py then
#   T1: fits (a, b) globally over the (h, m) grid,
#   T2: fits on m <= 3 only and PREDICTS the m in {6, 12} rows,
#   T3: checks h-slice effective exponents (2 -> 4 crossover with m),
#   T4: checks m-slice 1/m^2 decay onto the m-independent b*h^4 plateau.
#
# Run from build/tests:  bash run_nested_fgi_theory.sh
set -u

BIN=./hmc_test
GAUGE="$PWD/therm_8x8_b6.0_hb100.lime"
RES_DIR="$PWD/tunecache_nested_theory"
LOG_DIR="$PWD/logs_nested_theory"
DIM="--dim 8 8 8 8"
TAU=0.5
SEED=12345
TOL=1e-10                    # keep solver noise far below the smallest dH
N_OUTER_GRID="6 8 12 18 27 40"
M_INNER_GRID="1 2 3 6 12"
CELL_TIMEOUT=1800

PREC_MIX="--prec double --prec-sloppy single --prec-precondition single"
ACTION="--dslash-type wilson --mass 0.5"
MG="--mg-levels 2 --mg-block-size 0 4 4 4 4"

[ -x "$BIN" ] || { echo "ERROR: run from build/tests"; exit 1; }
[ -f "$GAUGE" ] || { echo "ERROR: thermalized gauge $GAUGE not found"; exit 1; }
mkdir -p "$RES_DIR" "$LOG_DIR"
if [ -f "$RES_DIR/tunecache.tsv" ] && [ "$BIN" -nt "$RES_DIR/tunecache.tsv" ]; then
  echo "Wiping stale tunecache."
  rm -f "$RES_DIR"/*.tsv
fi

GRID="$LOG_DIR/grid.dat"
{
  echo "# nested-FGI theory grid: single deterministic trajectory per point"
  echo "# wilson 8^4, tau=$TAU, seed=$SEED, tol=$TOL, start=$(basename "$GAUGE")"
  echo "# n_outer  m_inner  h  dH(signed)"
} > "$GRID"

total=0 ok=0
for m in $M_INNER_GRID; do
  for n in $N_OUTER_GRID; do
    total=$((total + 1))
    log="$LOG_DIR/n${n}_m${m}.log"
    # shellcheck disable=SC2086
    QUDA_RESOURCE_PATH="$RES_DIR" timeout "$CELL_TIMEOUT" mpiexec -n 1 "$BIN" $DIM \
      --load-gauge "$GAUGE" \
      $ACTION $PREC_MIX --tol $TOL \
      --hmc-integrator 3 --hmc-n-steps "$n" --hmc-n-inner-steps "$m" --hmc-tau $TAU \
      --hmc-n-trajectories 1 --hmc-thermalization 1 --hmc-momentum-seed $SEED \
      $MG --gtest_filter=HMC.Production > "$log" 2>&1
    rc=$?
    # signed dH of the single (forced-accept) trajectory
    dh=$(grep -E 'hmcRunQuda: (ACCEPTED|REJECTED)' "$log" | awk 'NR==1 {printf "%+.8e", $5+0}')
    if [ $rc -ne 0 ] || [ -z "$dh" ]; then
      echo "  n=$n m=$m : FAILED (rc=$rc), see $log"
      continue
    fi
    ok=$((ok + 1))
    h=$(awk -v t=$TAU -v n=$n 'BEGIN{printf "%.8f", t/n}')
    echo "$n $m $h $dh" >> "$GRID"
    printf '  [%s] n=%-3d m=%-3d h=%-11s dH=%s\n' "$(date '+%H:%M:%S')" "$n" "$m" "$h" "$dh"
  done
done

echo
echo "Grid complete: $ok/$total points -> $GRID"
python3 "$(dirname "$0")/fit_nested_theory.py" "$GRID" | tee "$LOG_DIR/summary.txt"

fit_nested_theory.py

#!/usr/bin/env python3
"""Fit nested-FGI dH(h, m) data against the two-timescale theory model.

Theory (Shcherbakov et al., arXiv:1512.03812 §2.2): a nested scheme with
force-gradient information at the outer level only and a fixed number m of
inner leapfrog substeps has shadow-Hamiltonian error

    dH(h, m) = a * (h/m)^2  +  b * h^4  + higher order,

where `a` is set by commutators of the inner (fast) action and `b` by the
outer force-gradient remainder. Both coefficients are functionals of the
(gauge, momentum, pseudofermion) start point only — so a grid of single
deterministic trajectories from an identical start shares one (a, b) pair.

Tests performed:
  T1  Global 2-parameter fit of signed dH over the full (h, m) grid.
  T2  Extrapolation: fit (a, b) on the m <= M_TRAIN subset, predict the rest.
  T3  h-slices at fixed m: effective exponent p_eff vs the model's prediction
      (p -> 2 where the (h/m)^2 term dominates, -> 4 where h^4 dominates).
  T4  m-slices at fixed h: 1/m^2 decay crossing over to an m-independent
      b*h^4 plateau.

Input: whitespace-separated rows "n_outer m h dH" (comments with #).
"""

import sys

import numpy as np

M_TRAIN_MAX = 3    # T2: train on m <= 3, predict m > 3
RESID_PASS = 0.10  # T1 verdict threshold on median |relative residual|
PRED_PASS = 0.25   # T2 verdict threshold on median |relative prediction error|


def wls_fit(h, m, dh):
    """Weighted least squares of dh on [(h/m)^2, h^4]; ~relative residuals."""
    X = np.column_stack([(h / m) ** 2, h**4])
    w = 1.0 / np.maximum(np.abs(dh), 1e-300)  # relative-error weighting
    Xw, yw = X * w[:, None], dh * w
    coef, *_ = np.linalg.lstsq(Xw, yw, rcond=None)
    resid = dh - X @ coef
    return coef, resid


def slope(x, y):
    return np.polyfit(np.log(x), np.log(np.abs(y)), 1)[0]


def main(path):
    rows = np.loadtxt(path, ndmin=2)
    n_out, m, h, dh = rows[:, 0], rows[:, 1], rows[:, 2], rows[:, 3]
    model = lambda a, b: a * (h / m) ** 2 + b * h**4

    print(f"Loaded {len(dh)} grid points from {path}")
    print(f"  h  in [{h.min():.5f}, {h.max():.5f}]   m in {sorted(set(m.astype(int)))}")
    print(f"  dH in [{dh.min():+.3e}, {dh.max():+.3e}]\n")

    # T1: global fit
    (a, b), resid = wls_fit(h, m, dh)
    rel = np.abs(resid) / np.abs(dh)
    print("== T1: global fit  dH = a*(h/m)^2 + b*h^4 ==")
    print(f"  a = {a:+.6e}   b = {b:+.6e}")
    print(f"  relative residuals: median {np.median(rel):.3f}, max {rel.max():.3f}")
    hstar = np.sqrt(np.abs(a / b)) if b != 0 else np.inf
    print(f"  predicted crossover h*(m) = sqrt(|a/b|)/m = {hstar:.4f}/m")
    t1 = np.median(rel) < RESID_PASS

    # T2: train on small m, predict large m
    tr = m <= M_TRAIN_MAX
    (a2, b2), _ = wls_fit(h[tr], m[tr], dh[tr])
    te = ~tr
    pred = a2 * (h[te] / m[te]) ** 2 + b2 * h[te] ** 4
    perr = np.abs(pred - dh[te]) / np.abs(dh[te])
    print(f"\n== T2: fit on m <= {M_TRAIN_MAX} ({tr.sum()} pts), predict m > {M_TRAIN_MAX} ({te.sum()} pts) ==")
    print(f"  a = {a2:+.6e}   b = {b2:+.6e}")
    for i in np.where(te)[0]:
        p = a2 * (h[i] / m[i]) ** 2 + b2 * h[i] ** 4
        print(f"  n={int(n_out[i]):3d} m={int(m[i]):3d}  dH={dh[i]:+.4e}  pred={p:+.4e}  "
              f"relerr={abs(p - dh[i]) / abs(dh[i]):.3f}")
    print(f"  median |rel err| = {np.median(perr):.3f}, max = {perr.max():.3f}")
    t2 = np.median(perr) < PRED_PASS

    # T3: h-slice exponents at fixed m (finest 4 h points), vs model prediction
    print("\n== T3: effective h-exponent per m (measured vs model) ==")
    print("  m    p_eff(data)  p_eff(model)   [2 = inner-leapfrog term, 4 = FG term]")
    for mv in sorted(set(m)):
        s = m == mv
        idx = np.argsort(h[s])[:4]
        hh, dd = h[s][idx], dh[s][idx]
        mm = a * (hh / mv) ** 2 + b * hh**4
        print(f"  {int(mv):2d}   {slope(hh, dd):11.3f}  {slope(hh, mm):12.3f}")

    # T4: m-slice decay at fixed h, vs model prediction
    print("\n== T4: m-dependence per h (measured vs model)  [-2 = 1/m^2, 0 = b*h^4 plateau] ==")
    print("  n_out    h        q(data)   q(model)   dH(m_max)/[b*h^4]")
    for nv in sorted(set(n_out)):
        s = n_out == nv
        if s.sum() < 3:
            continue
        idx = np.argsort(m[s])
        mm_, dd = m[s][idx], dh[s][idx]
        hv = h[s][idx][0]
        mod = a * (hv / mm_) ** 2 + b * hv**4
        floor = dd[-1] / (b * hv**4) if b != 0 else np.nan
        print(f"  {int(nv):4d}  {hv:.5f}  {slope(mm_, dd):8.3f}  {slope(mm_, mod):9.3f}   {floor:8.3f}")

    print("\n== VERDICT ==")
    print(f"  T1 global two-term fit:  {'PASS' if t1 else 'CHECK'} (median rel resid = {np.median(rel):.3f})")
    print(f"  T2 extrapolation in m:   {'PASS' if t2 else 'CHECK'} (median rel err = {np.median(perr):.3f})")
    print("  T3/T4: compare columns above — data should track the model, not a pure power law.")
    return 0 if (t1 and t2) else 1


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "logs_nested_theory/grid.dat"))
⚠️ **GitHub.com Fallback** ⚠️