Round-Trip Diagnostics#
Functions for measuring tempering performance: round-trip rates, the communication barrier Λ, chain count recommendations, and the log normalizing constant via thermodynamic integration.
Before trusting a Λ̂ or a round-trip rate, check the two-part trust gate.
barrier_is_identified asks the structural question — does the ladder
saturate? — so that "identified" means Λ̂ is within ~10% regardless of the round
budget. conveyor_is_alive separately answers the dynamical question, and
reports None (unmeasured, not stalled) when the window affords too few
expected trips to tell.
hamon.round_trip_summary(index_state: dict, rejection_rates: jax.Array, betas: jax.Array, n_rounds: int) -> dict
#
Compute full diagnostic summary for NRPT run.
Jitted so the handful of reductions below (Λ, τ̄, the local-barrier profile,
the round-trip rate) fuse into a single compiled kernel instead of ~8 eager
op-by-op dispatches, each of which otherwise pays a first-shape XLA compile
when called once per probe at a new chain count. n_rounds is traced (not
static), so the compile is shared across round counts.
Returns dict with
Lambda: global communication barrier estimate tau_predicted: theoretical optimal round trip rate tau_observed: empirical round trip rate efficiency: tau_observed / tau_predicted (closer to 1 = better) lambda_profile: local barrier at each pair midpoint round_trips_per_chain: per-machine round trip counts restarts_per_chain: per-machine restart counts
hamon.recommend_n_chains(Lambda: float | jax.Array, target_acceptance: float = 0.5) -> int
#
Suggest chain count for a given barrier and target acceptance rate.
For NRPT with equalized rejection rates: Nr ≈ Λ where r = 1 - target_acceptance. Solving: N = Λ / r* = Λ / (1 - target_acceptance).
The default target_acceptance=0.5 (r = 1/2 ⇒ N ≈ 2Λ) is the round-trip- optimal rejection rate from Syed et al., not the 0.77 of reversible PT.
Note: Λ from too few chains is biased low. Use tune_chains
for iterative bootstrapping if recommendations keep increasing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Lambda
|
float | Array
|
estimated global communication barrier |
required |
target_acceptance
|
float
|
desired per-pair acceptance rate (default: 0.5 = 50%) |
0.5
|
Returns:
| Type | Description |
|---|---|
int
|
Recommended number of chains (minimum 2). |
hamon.thermodynamic_integration(betas: jax.Array, mean_energies: jax.Array, *, method: str = 'trapezoid') -> jax.Array
#
Log normalizing-constant ratio via thermodynamic integration.
Estimates log Z(β_max) / Z(β_min) = -∫ μ(β) dβ (Syed et al. 2021,
Sec. 5.5), where μ(β) = E_{π^(β)}[V] is the mean base energy and the
integral runs over the supplied ladder [β_min, β_max]. mean_energies
are the per-chain means μ(β_i) — accumulate them with
hamon.NRPTEnergyObserver and divide its (sum_E, count) carry, or
use :func:nrpt_log_normalizing_constant.
The reference chain (β_min, typically 0) has a known normalizer: for a
discrete model with a uniform β=0 reference over M configurations,
log Z(β_min) = log M (e.g. n·log 2 for n spins), so the absolute
log Z(β_max) is this result plus that constant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
betas
|
Array
|
ascending β ladder, shape |
required |
mean_energies
|
Array
|
per-chain mean base energy μ(β_i), shape |
required |
method
|
str
|
|
'trapezoid'
|
Returns:
| Type | Description |
|---|---|
Array
|
Scalar |
hamon.nrpt_log_normalizing_constant(stats: dict, *, log_z0: float = 0.0, method: str = 'trapezoid') -> jax.Array
#
Log normalizing constant from an NRPT run with an energy observer.
Convenience over :func:thermodynamic_integration: reads the
(sum_E, count) carry left by hamon.NRPTEnergyObserver in
stats["observer_carry"], forms the per-chain mean energies, and
integrates them against stats["betas"].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stats
|
dict
|
the stats dict from |
required |
log_z0
|
float
|
|
0.0
|
method
|
str
|
quadrature rule, see :func: |
'trapezoid'
|
Returns:
| Type | Description |
|---|---|
Array
|
Scalar |
Trust gates#
hamon.round_trips.barrier_is_identified(rejection_rates: jax.Array | np.ndarray, *, max_rej_resolved: float = 0.75) -> bool
#
Whether Λ̂ = Σ rejection_rates is a trustworthy barrier estimate.
Λ̂ is cap-limited, not merely noisy, when the ladder saturates: each
rejection rate is at most 1, so Λ̂ <= N-1 by construction and a ladder
whose pairs pin at r = 1 reports that cap rather than the barrier (an
unbridged pair also blocks the DEO conveyor outright, freezing every chain —
and every replica — in one basin, which is why replicas agree on the
artifact and low cross-replica spread is a false consistency signal). So the
question "is Λ̂ resolved?" is structural: it is answered by the ladder's
own rejection rates, and needs no round-trip observation.
This deliberately does not gate on the round-trip rate. That signal is
budget-dependent and answers a different question (:func:conveyor_is_alive):
measured at fixed N = 47 on a planted 32x32, the same well-resolved ladder
(Λ̂ ≈ 20.7, max rej ≈ 0.6) reports 0 round trips at 500 rounds and 96 at
12000 — the zero-trip reading is indistinguishable from a genuine stall, so
using it here reports "add chains" for a ladder that is already correct.
Returns True when Λ̂ is resolved. False means the ladder is
saturating and Λ̂ is a cap-limited underestimate — add chains / equalize
rather than believing it.
hamon.round_trips.conveyor_is_alive(tau_observed: float, tau_predicted: float, n_rounds: int, *, min_efficiency: float = 0.15, min_expected_trips: float = 40.0) -> bool | None
#
Whether the DEO index process is observed traversing the ladder.
A dynamical question, distinct from :func:barrier_is_identified: a ladder
can be perfectly resolved yet not (yet) observed round-tripping, and the two
dissociate cleanly under budget — Λ̂ and max(rej) are flat across a 24x round
sweep while efficiency swings 0.000 → 0.348.
Gates on efficiency τ_obs / τ_pred, not on τ_obs itself: with
τ_pred = 1/(2+2Λ) an absolute floor demands an efficiency that grows with
Λ (0.01 asks 19% of optimal at Λ = 8.5, 43% at Λ = 21, and is unsatisfiable
at Λ >= 49 — strictest exactly where a trustworthy answer matters most).
Returns None when the window is too short to tell (fewer than
min_expected_trips round trips expected even at the optimal rate), so
callers can say "not measured" instead of "stalled".