Source code for blackjax.adaptation.mclmc_lrd_adaptation

# Copyright 2020- The Blackjax Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pilot-free (Scheme A) MCLMC warmup with Low-Rank Diagonal preconditioning.

Overview
--------
This module implements the **Scheme A** warmup for MCLMC via
:func:`mclmc_lrd_warmup`.  Phases 1–3 are shared between the unadjusted and
adjusted inner kernels; only Phase 4 switches:

1. **Pilot phase** — a single unadjusted MCLMC chain with diagonal
   preconditioning (via
   :func:`~blackjax.adaptation.mclmc_adaptation.mclmc_find_L_and_step_size`)
   runs for ``pilot_num_warmup + pilot_num_samples`` steps to reach the
   typical set and collect geometry samples.

2. **LRD extraction** — the pilot draws are standardised and a thin SVD
   extracts the top-*k* principal directions of the correlation structure.
   The result is a
   :class:`~blackjax.mcmc.metrics.LowRankInverseMassMatrix` ``(sigma, U,
   lam)``.

   **Rank guard (non-negotiable):** before extraction, the effective sample
   size of the pilot chain is estimated via
   :func:`~blackjax.diagnostics.effective_sample_size` (the Geyer
   monotone-sequence estimator, operating on the ravelled flat-parameter
   vector; one chain, ``pilot_num_samples`` draws; basis is *not* the
   az-bulk ESS).  A safe rank bound ``k_safe = floor(n_eff / 2)`` is
   computed, and the requested ``k`` is hard-clamped to ``k_safe`` when it
   would exceed it.  Without this guard, under-mixed pilots produce
   rank-deficient SVDs and the LRD metric degrades sampling quality.  In the
   extreme under-mix regime (n_eff < 5), the Geyer estimator may exceed
   az-bulk ESS on the minimum-ESS dimension; ``k_safe`` is still floored to
   0 → ``k_used=1``, so practical rank clamping is unaffected.

3. **Multi-chain unadjusted LRD tuning** —
   :func:`~blackjax.adaptation.mclmc_adaptation.mclmc_find_L_and_step_size`
   is run in parallel over ``num_chains`` independent chains (all starting
   from the pilot's final position, with per-chain PRNG keys) with the LRD
   metric kernel (``diagonal_preconditioning=False``).  The per-chain
   ``(L, step_size)`` values are averaged to give stable estimates of the
   trajectory length and step size in LRD geometry.

4. **Inner-kernel dispatch** (controlled by ``inner_kernel``):

   * ``"mclmc"`` *(default)*: returns the Phase-3 mean
     ``(L, step_size)`` directly.

   * ``"adjusted_mclmc"`` *(experimental)*: warm-starts
     :func:`~blackjax.adaptation.adjusted_mclmc_adaptation.adjusted_mclmc_find_L_and_step_size`
     across ``num_chains`` parallel chains from the Phase-3 mean parameters.
     **Two hard constraints are enforced automatically** to avoid known
     failure modes:

     - ``params`` is set to
       ``MCLMCAdaptationState(L=L_init, step_size=..., inverse_mass_matrix=lrd_imm)``
       so that the ``sqrt(dim)`` default L initialisation (which ignores the
       baked-in LRD metric) is never used.
     - ``frac_tune2=0.0`` disables the variance-based *L* estimator, which
       computes original-space ``trace(Σ)`` and is incompatible with an
       externally-baked LRD IMM.

     ``L_init`` is floored at ``floor_factor * step_unadj`` (default
     ``floor_factor=1.15``) to prevent the Dual-Averaging ceiling
     ``(L / 1.1)`` from binding below the oracle step size.  For geometry
     where the oracle step size exceeds the oracle L (stiff
     high-condition-number targets), the default 1.15 may leave the DA
     ceiling binding; raise to approximately 1.5 and set
     ``adjusted_num_steps`` to at least 5000 for those targets.

Stan-window analogy
-------------------
The staging mirrors Stan's two-phase warmup: a cheap diagonal (fast) phase
reaches the typical set first; the LRD (slow) phase then builds the metric
from draws that are already in the right region.  The key difference is that
Scheme A uses MCLMC (unadjusted, leapfrog-free) for the pilot, eliminating
the NUTS pilot cost that dominated total gradient expenditure in our
benchmarks:

* ill_cond_50 (d=50, κ=1000): k=40, n_pilot=10k → 101.1 % oracle step-size
  recovery, 2.42× ESS/total-grad vs NUTS-pilot, 72.5 % grad savings.
* german_credit (d=26): k=8, n_pilot=5k.
* mvn_10 (d=10): k=4, n_pilot=2k.

Limitations
-----------
* **Single-chain pilot.** The pilot is single-chain; overdispersed
  multi-chain initialisation would give stronger geometry coverage and
  tighter n_eff estimates.  This is a known limitation for the rank guard's
  tightness.
* **NUTS-pilot fallback.** When the target has funnel-class geometry (e.g.
  hierarchical models near unit-root), the MCLMC pilot may under-mix even
  with n_eff/2 clamping.  A NUTS-pilot fallback (replacing Phase 1 with
  :func:`~blackjax.adaptation.window_adaptation.window_adaptation`) is the
  recommended escape hatch but is out of scope here.
* **Adjusted path experimental.** ``inner_kernel="adjusted_mclmc"`` is
  certified 3/3 on german_credit at the default recipe (4-chain phase 3,
  ``frac_tune2=0``, ``floor_factor=1.15``, ``adjusted_num_steps=3000``).
  ill_cond_50 (stiff, κ=1000) is geometrically compatible (clamp-free at
  ``floor_factor=1.5``, step size 104–114% of oracle, zero DA divergences)
  but was NOT certified at ``adjusted_num_steps=3000`` (marginal R-hat
  1.010–1.011; DA not converged).  For stiff geometry, raise
  ``floor_factor`` to ~1.5 and ``adjusted_num_steps`` to ≥5000.
  The unadjusted default remains the stable, broadly validated path.
"""

import math
import warnings
from typing import Any, NamedTuple

import jax
import jax.numpy as jnp
from jax.flatten_util import ravel_pytree

import blackjax.mcmc.adjusted_mclmc as _adj_mclmc_mod
import blackjax.mcmc.mclmc as _mclmc_mod
from blackjax.adaptation.adjusted_mclmc_adaptation import (
    adjusted_mclmc_find_L_and_step_size,
)
from blackjax.adaptation.mclmc_adaptation import (
    MCLMCAdaptationState,
    mclmc_find_L_and_step_size,
)
from blackjax.adaptation.metric_estimators import (
    select_top_eigenvalues_by_informativeness,
)
from blackjax.diagnostics import effective_sample_size
from blackjax.mcmc.metrics import LowRankInverseMassMatrix
from blackjax.types import Numeric

__all__ = [
    "MCLMCLRDAdaptationState",
    "mclmc_lrd_warmup",
]

_VALID_INNER_KERNELS = frozenset({"mclmc", "adjusted_mclmc"})
_VALID_WARMUP_STEP_INITS = frozenset({"law", "default"})

# √d scaling-law constants from the MCLMC scaling-laws study (S3).
# At good preconditioning (κ_eff ≲ 5), the optimal step and trajectory length
# scale as constant × √d with dimension-independent constants:
#   step ≈ 1.22 × √d,  L ≈ 0.85 × √d.
# Source: tuningfork/experiments/mclmc_scaling/ Q0 sweep (2026-06).
_SQRT_D_STEP_CONST = 1.22
_SQRT_D_L_CONST = 0.85

# κ_eff gate for the √d warm-start (E1).  When the pilot-derived LRD IMM
# achieves κ_eff ≤ this threshold, the geometry is sufficiently whitened for
# the scaling-law warm-start to be accurate.  κ_eff > 5 indicates the IMM is
# not yet whitening the target; falling back to the pilot's own (step, L)
# avoids overshooting the tuning region.
_KAPPA_EFF_GATE = 5.0


[docs] class MCLMCLRDAdaptationState(NamedTuple): """Result of :func:`mclmc_lrd_warmup`. L Adapted momentum decoherence length from the final tuning phase. step_size Adapted step size from the final tuning phase. inverse_mass_matrix The adapted LRD inverse mass matrix as a :class:`~blackjax.mcmc.metrics.LowRankInverseMassMatrix` NamedTuple ``(sigma, U, lam)``. diagnostics A plain dict with provenance fields: ``inner_kernel`` Which inner kernel was used (``"mclmc"`` or ``"adjusted_mclmc"``). ``n_eff`` Effective sample size of the pilot chain (Geyer monotone-sequence estimator, blackjax basis, ravelled flat vector). ``k_safe`` Floor of ``n_eff / 2`` — the maximum rank that is statistically supported by the pilot. ``k_used`` The rank actually passed to SVD after clamping (``min(k, k_safe)``). ``pilot_num_grad_evals`` Total gradient evaluations consumed by the pilot phase (warmup + samples; unadjusted MCLMC costs 2 grads/step). ``pilot_L`` Trajectory length L adapted during the diagonal pilot warmup. ``pilot_step_size`` Step size adapted during the diagonal pilot warmup. ``lrd_L`` Mean trajectory length L across ``num_chains`` chains after Phase-3 unadjusted LRD tuning. ``lrd_step_size`` Mean step size across ``num_chains`` chains after Phase-3 unadjusted LRD tuning. ``L_init`` *(adjusted path only)* The floor-guarded L initialisation value passed to the adjusted warm-start: ``max(lrd_L, floor_factor * lrd_step_size)``. This is the value whose ``/ 1.1`` sets the DA ceiling for step-size tuning. ``floor_active`` *(adjusted path only)* ``True`` when the floor guard was triggered (``floor_factor * lrd_step_size > lrd_L``), i.e. when ``L_init`` was raised above the unadjusted mean. ``N_sample`` *(adjusted path only)* Effective number of leapfrog steps per trajectory at the final adapted parameters: ``round(L_init / final_step_size)``. Provided as a bookkeeping aid for cert integration. ``e1_fired`` ``True`` when the √d warm-start (E1) was applied to Phase-3 DA initialisation; ``False`` when the fallback (pilot step/L) was used instead. Always ``False`` when ``warmup_step_init="default"``. ``kappa_eff_pilot`` Effective condition number κ(M⁻¹ Σ⁻¹) of the pilot-derived LRD IMM against the pilot sample covariance. Computed from the Phase-2 SVD eigenspectrum. Values near 1 indicate good whitening; values above 5 indicate the IMM is under-preconditioned (E1 falls back to pilot step/L). Present whenever ``warmup_step_init`` is set (both ``"law"`` and ``"default"`` paths compute it for observability). """
[docs] L: Numeric
[docs] step_size: Numeric
[docs] inverse_mass_matrix: LowRankInverseMassMatrix
[docs] diagnostics: dict
def _extract_lrd_from_samples( flat_positions: Any, k: int, ) -> tuple: """Extract LRD parameters from a ``(n_samples, d)`` array of pilot draws. Parameters ---------- flat_positions Shape ``(n_samples, d)``. Already ravelled to a 2-D float array. k Rank of the LRD approximation (after clamping by the caller). Returns ------- sigma : shape ``(d,)`` U : shape ``(d, k)`` lam : shape ``(k,)`` lam_all : shape ``(min(n, d),)`` **Full** eigenspectrum of the sample correlation matrix (all singular values, not just the top-k). Returned for :func:`_kappa_eff_pilot` so it can compute the residual contribution to κ_eff without a second SVD. The ordering here is by descending ``|λ - 1|``, matching the selection order used for ``lam`` (i.e. ``lam == lam_all[:k]``). """ mean = jnp.mean(flat_positions, axis=0) # (d,) sigma = jnp.std(flat_positions, axis=0) # (d,) sigma = jnp.where(sigma == 0.0, 1.0, sigma) # avoid div-by-zero in zero-var dims standardised = (flat_positions - mean[None, :]) / sigma[None, :] # (n, d) n = flat_positions.shape[0] # Thin SVD: columns of V are the principal directions of standardised draws. # Eigenvalues of the sample correlation matrix: lam_i = s_i^2 / n. _, S, Vt = jnp.linalg.svd(standardised, full_matrices=False) V = Vt.T # (d, min(n,d)) lam = (S**2) / n # (min(n,d),) # Top-k selection via select_top_eigenvalues_by_informativeness # (tail_handling="raw" — no masking to 1, no padding). # Using draws_singular_value_low_rank directly would require a second SVD # pass to produce lam_all_sorted (that wrapper returns only (sigma, U_k, # lam_k), not the full spectrum); keeping the single SVD here avoids the # redundancy. See metric_estimators.draws_singular_value_low_rank for # the array-based wrapper. U_k, lam_k = select_top_eigenvalues_by_informativeness( lam, V, k, tail_handling="raw" ) # Full spectrum sorted by |λ-1|, needed by _kappa_eff_pilot (G-layer concern). sort_idx = jnp.argsort(jnp.abs(lam - 1.0))[::-1] lam_all_sorted = lam[sort_idx] # shape (min(n,d),), ordered by |λ-1| desc return sigma, U_k, lam_k, lam_all_sorted def _kappa_eff_pilot( lam_all_sorted: Any, k: int, ) -> float: """Effective condition number of the pilot LRD IMM against the pilot Sigma. Computes κ_eff = κ(M⁻¹ Σ⁻¹) from the full SVD eigenspectrum produced in Phase 2, using the correct accounting of whitened vs. residual directions. **Correct formula** (the LRD preconditioning theory): The LRD IMM M⁻¹ captures the top-k principal directions of the sample correlation matrix (those with ``|λ - 1|`` largest). In the M⁻¹Σ⁻¹ product: * The **k captured** directions are whitened to eigenvalue **1** — M⁻¹ exactly inverts Σ along those directions. * The **residual** (d − k) directions, with correlation eigenvalues ``μ_i`` (the *k+1*, *k+2*, … entries of ``lam_all_sorted``), are left as *diagonal*-only (M⁻¹ = D² along those directions). In the correlation-normalised space, the diagonal correction gives eigenvalue ``1/μ_i`` for each residual direction. Hence the spectrum of M⁻¹Σ⁻¹ is: {1} × k ∪ {1/μ_i : i ∈ residual} and κ_eff = max / min of this combined set. **Why the na‌ive {lam_k ∪ {1}} formula is wrong:** it uses the captured eigenvalues (those with ``|λ - 1|`` largest) in the numerator/denominator, which are exactly the directions that *are* whitened. The actual spread comes from the *residual* directions, whose ``μ_i`` are closest to 1 and therefore ``1/μ_i`` can still be far from 1 when some residuals are very small (under-corrected). Parameters ---------- lam_all_sorted : array, shape ``(min(n, d),)`` Full eigenspectrum of the sample correlation matrix, ordered by descending ``|λ - 1|`` (as returned by :func:`_extract_lrd_from_samples`). The first ``k`` entries are the selected top-k; the remaining entries are the residuals. k : int Rank of the LRD approximation (same ``k_used`` passed to :func:`_extract_lrd_from_samples`). Returns ------- float κ_eff = max / min of {1} × k ∪ {1/μ_i : i ≥ k}. Returns the correlation κ (all residual, no whitening) when k = 0. Returns 1.0 when k ≥ len(lam_all_sorted) (all directions whitened). """ lam_np = jnp.array(lam_all_sorted) n_svd = lam_np.size # min(n_pilot, d) if n_svd == 0: return 1.0 # Residual eigenvalues: entries at positions k, k+1, … in lam_all_sorted. # These are the (d-k) directions NOT captured by the top-k selection. if k >= n_svd: # All SVD-supported directions are whitened → κ_eff = 1. return 1.0 residual_mu = lam_np[k:] # shape (n_svd - k,) # Guard against near-zero residuals (degenerate pilots with very few draws) residual_mu = jnp.where( residual_mu < 1e-12, jnp.ones_like(residual_mu), residual_mu ) residual_eigs = 1.0 / residual_mu # (n_svd - k,) # The k whitened directions each contribute eigenvalue 1. # Combine: max and min over {1} ∪ {1/μ_i}. max_eig = float(jnp.maximum(jnp.max(residual_eigs), 1.0)) min_eig = float(jnp.minimum(jnp.min(residual_eigs), 1.0)) if min_eig <= 0.0: return float("inf") return max_eig / min_eig def _check_da_ceiling_warning( final_step_size: float, L_init: float, floor_factor: float, ) -> None: """Emit a UserWarning when the adapted step_size is at or near L_init/1.1. This is the Dual-Averaging (DA) ceiling signature: when ``step_size >= L_init / 1.1 * 0.999``, the step-size tuner may have been constrained by the ceiling rather than converged to the optimal value. Parameters ---------- final_step_size Mean adapted step size across chains from the adjusted Phase-4 tuning. L_init The floor-guarded L_init value used for the adjusted warm-start. floor_factor Current ``floor_factor`` value; included in the warning message. """ da_clamp = L_init / 1.1 step_ratio = final_step_size / da_clamp if step_ratio >= 0.999: step_s = round(final_step_size, 4) clamp_s = round(da_clamp, 4) ratio_s = round(step_ratio, 3) warnings.warn( f"mclmc_lrd_warmup (adjusted_mclmc path): adapted step_size " f"({step_s}) is at or near the DA ceiling " f"L_init/1.1={clamp_s} (ratio={ratio_s}). " "The step-size tuner may have been constrained rather than " "converged. Consider raising `floor_factor` " f"(current value: {floor_factor}) — e.g. to 1.5 for " "high-condition-number targets.", UserWarning, stacklevel=3, # points to caller of mclmc_lrd_warmup )
[docs] def mclmc_lrd_warmup( logdensity_fn, position, rng_key, *, k: int = 10, pilot_num_warmup: int = 1000, pilot_num_samples: int = 5000, lrd_num_steps: int = 1000, num_chains: int = 4, inner_kernel: str = "mclmc", floor_factor: float = 1.15, adjusted_num_steps: int = 3000, adjusted_target: float = 0.9, warmup_step_init: str = "law", ): """Scheme A (pilot-free) MCLMC warmup with Low-Rank Diagonal preconditioning. Runs a cheap diagonal unadjusted MCLMC pilot to reach the typical set and collect geometry samples, extracts a low-rank diagonal (LRD) inverse mass matrix via thin SVD, then calibrates step size and trajectory length L across ``num_chains`` parallel chains with the LRD metric kernel. The inner kernel for the final tuning phase is controlled by ``inner_kernel``. Parameters ---------- logdensity_fn Log-density of the target distribution. position Initial position (any pytree). rng_key JAX PRNG key. k Requested LRD rank. Hard-clamped to ``floor(n_eff / 2)`` if the pilot chain is under-mixed; see the rank-guard note in the module docstring. pilot_num_warmup Number of steps for the diagonal pilot warmup phase (used by :func:`~blackjax.adaptation.mclmc_adaptation.mclmc_find_L_and_step_size` to adapt step size and L with a diagonal mass matrix). pilot_num_samples Number of unadjusted MCLMC draws collected *after* the pilot warmup, used for the SVD geometry estimate. lrd_num_steps Number of steps passed to the LRD tuning call(s) in Phase 3 (and Phase 4 for the adjusted path). num_chains Number of parallel chains for Phase 3 (unadjusted LRD tuning) and Phase 4 (adjusted tuning when ``inner_kernel="adjusted_mclmc"``). All chains start from the pilot's final position; per-chain PRNG keys give independent trajectories. Per-chain L and step_size are averaged for a stable multi-chain estimate. Default ``4``. inner_kernel Which inner kernel to use for the final tuning phase. One of: * ``"mclmc"`` *(default, stable)*: unadjusted MCLMC. Phase-3 mean ``(L, step_size)`` is returned directly. * ``"adjusted_mclmc"`` *(experimental)*: after Phase-3 unadjusted LRD tuning, warm-starts :func:`~blackjax.adaptation.adjusted_mclmc_adaptation.adjusted_mclmc_find_L_and_step_size` across ``num_chains`` chains with ``frac_tune2=0.0`` (variance-based *L* estimator disabled) and ``diagonal_preconditioning=False``. floor_factor For ``inner_kernel="adjusted_mclmc"`` only: the L initialisation floor is ``max(L_unadj, floor_factor * step_unadj)``. Default ``1.15``; certified on german_credit. For stiff geometry where the oracle step size exceeds the oracle L, raise to approximately 1.5 (the default leaves the DA ceiling ``L / 1.1`` binding for those targets). Ignored when ``inner_kernel="mclmc"``. adjusted_num_steps Number of DA tuning steps for the adjusted Phase-4 path. Default ``3000`` with ``frac_tune1=0.5`` → 1500 DA steps; the certified recipe for german_credit. For stiff high-κ geometry, increase to at least 5000. Ignored when ``inner_kernel="mclmc"``. adjusted_target Target acceptance rate for the adjusted MCLMC tuning phase. Default ``0.9``. Ignored when ``inner_kernel="mclmc"``. warmup_step_init : str Initialisation strategy for the Phase-3 Dual-Averaging (DA) step-size tuner. One of: * ``"law"`` *(default)*: **√d warm-start (E1)**, gated on κ_eff. When the pilot-derived LRD IMM achieves κ_eff ≤ 5 (the geometry is sufficiently whitened), Phase-3 DA is initialised at the scaling-law values ``step_size = 1.22 × √d``, ``L = 0.85 × √d``. These constants were derived from the MCLMC scaling-laws study (S3, 2026): at good preconditioning, MCLMC's optimal step and trajectory length are dimension-independent multiples of √d. When κ_eff > 5 (under-preconditioned, e.g. rank-1 LRD on a κ=1000 target), E1 is **not applied** and Phase-3 DA falls back to the pilot's own ``(step_size, L)`` — the same behaviour as ``"default"``. This gate prevents overshoot: at low rank the geometry is not yet whitened and the scaling-law values would place the DA starting point far from the actual optimum. The warm-start only affects DA *convergence speed*, not the final converged value. At sufficient ``lrd_num_steps`` budget, ``"law"`` and ``"default"`` produce statistically identical ``(step_size, L)`` outputs. The gain is in sample quality at *low-budget* warmup (measured via ESS/grad): ~20–30% improvement through n_warmup ≈ 1000 at d = 500 in the Q0 sweep (2026-06), scaling with dimension because the default DA init at 0.25 √d falls further below the optimum at larger d. * ``"default"``: Phase-3 DA is initialised by warm-starting from the pilot's own ``(step_size, L)`` (the pre-existing behaviour prior to this parameter). Use this to reproduce results from the previous code path or to suppress E1 entirely. Returns ------- MCLMCLRDAdaptationState A :class:`MCLMCLRDAdaptationState` NamedTuple with fields ``L``, ``step_size``, ``inverse_mass_matrix`` (a :class:`~blackjax.mcmc.metrics.LowRankInverseMassMatrix`), and ``diagnostics`` (see :class:`MCLMCLRDAdaptationState` for keys). Raises ------ ValueError If ``inner_kernel`` is not one of ``"mclmc"`` or ``"adjusted_mclmc"``. Examples -------- .. code-block:: python import jax import jax.numpy as jnp import blackjax logdensity_fn = lambda x: -0.5 * jnp.sum(x**2) position = jnp.zeros(10) rng_key = jax.random.key(42) # Unadjusted (stable default): result = blackjax.mclmc_lrd_warmup( logdensity_fn, position, rng_key, k=4, pilot_num_warmup=500, pilot_num_samples=2000, lrd_num_steps=1000, num_chains=4, ) # Adjusted (experimental): result_adj = blackjax.mclmc_lrd_warmup( logdensity_fn, position, rng_key, k=4, pilot_num_warmup=500, pilot_num_samples=2000, lrd_num_steps=1000, num_chains=4, inner_kernel="adjusted_mclmc", ) # Build the production kernel from the unadjusted result: import blackjax.mcmc.mclmc as mclmc_mod lrd_imm = result.inverse_mass_matrix kernel = mclmc_mod.build_kernel() init_state = mclmc_mod.init(position, logdensity_fn, jax.random.key(0)) next_state, info = kernel( jax.random.key(1), init_state, logdensity_fn, lrd_imm, result.L, result.step_size, ) """ if inner_kernel not in _VALID_INNER_KERNELS: raise ValueError( f"inner_kernel must be one of {sorted(_VALID_INNER_KERNELS)!r}, " f"got {inner_kernel!r}." ) if warmup_step_init not in _VALID_WARMUP_STEP_INITS: raise ValueError( f"warmup_step_init must be one of {sorted(_VALID_WARMUP_STEP_INITS)!r}, " f"got {warmup_step_init!r}." ) # Five independent keys — no reuse across init / pilot warmup / pilot # sampling / LRD tuning / adjusted tuning. init_key, warmup_key, sample_key, lrd_subkey, adj_subkey = jax.random.split( rng_key, 5 ) # ------------------------------------------------------------------ # Phase 1: diagonal pilot — reach typical set + collect geometry samples # ------------------------------------------------------------------ base_kernel = _mclmc_mod.build_kernel() init_state = _mclmc_mod.init(position, logdensity_fn, init_key) state_after_warmup, pilot_params, _ = mclmc_find_L_and_step_size( mclmc_kernel=base_kernel, num_steps=pilot_num_warmup, state=init_state, rng_key=warmup_key, logdensity_fn=logdensity_fn, diagonal_preconditioning=True, ) pilot_L = float(pilot_params.L) pilot_step_size_val = float(pilot_params.step_size) # Collect pilot_num_samples draws with the adapted diagonal kernel. def _pilot_step(state, key): next_state, _ = base_kernel( rng_key=key, state=state, logdensity_fn=logdensity_fn, inverse_mass_matrix=pilot_params.inverse_mass_matrix, L=pilot_params.L, step_size=pilot_params.step_size, ) return next_state, next_state.position _, pilot_positions = jax.lax.scan( _pilot_step, state_after_warmup, jax.random.split(sample_key, pilot_num_samples), ) # ------------------------------------------------------------------ # Rank guard: n_eff via blackjax Geyer ESS on ravelled flat vector. # ESS basis: Geyer monotone-sequence estimator (blackjax.diagnostics. # effective_sample_size), single chain, pilot_num_samples draws. # Shape expected: (chains, draws, d) — we add the chains axis here. # ------------------------------------------------------------------ flat_pilot = jax.vmap(lambda p: ravel_pytree(p)[0])(pilot_positions) # (n, d) # effective_sample_size expects (chains, draws, d); add singleton chain axis. # Requires ≥2 draws; guard for degenerate pilots. if pilot_num_samples >= 2: ess_per_dim = effective_sample_size(flat_pilot[None, :, :]) # (d,) n_eff = float(jnp.min(ess_per_dim)) # conservative: use min over dims else: n_eff = 0.0 # degenerate: force k_safe=0 → k_used=1 # A non-finite n_eff means the pilot diagnostics are invalid, not that the # rank bound is small: effective_sample_size reports NaN for any dimension # holding a non-finite draw. Refuse explicitly here, before the int() # conversion and before the Phase-2 SVD, rather than crashing in int() or # letting a fabricated rank through. if not math.isfinite(n_eff): raise ValueError( "mclmc_lrd_warmup: pilot diagnostics are invalid — the effective " f"sample size of the pilot draws is {n_eff}, so the rank-safety " "bound k_safe = floor(n_eff / 2) cannot be computed and low-rank " "selection cannot proceed. Inspect the pilot chain, its step size " "and the log-density over the pilot region rather than the rank " "bound." ) k_safe = int(n_eff / 2) # floor(n_eff / 2) k_used = min(k, max(k_safe, 1)) # clamp; always use at least rank 1 if k_used < k: n_eff_rounded = round(n_eff, 1) warnings.warn( f"mclmc_lrd_warmup: requested k={k} exceeds the rank-safety " f"bound k_safe=floor(n_eff/2)={k_safe} " f"(n_eff={n_eff_rounded} from {pilot_num_samples} pilot draws). " f"Clamping to k_used={k_used}. " "Increase pilot_num_samples to raise n_eff, or reduce k.", UserWarning, stacklevel=2, ) # ------------------------------------------------------------------ # Phase 2: SVD extraction → LowRankInverseMassMatrix # ------------------------------------------------------------------ sigma, U_k, lam_k, lam_all_sorted = _extract_lrd_from_samples(flat_pilot, k=k_used) lrd_imm = LowRankInverseMassMatrix(sigma=sigma, U=U_k, lam=lam_k) # ------------------------------------------------------------------ # Phase 3: multi-chain unadjusted LRD tuning # num_chains independent chains, all starting from pilot's final position # with per-chain PRNG keys for independent trajectories. L and step_size # are averaged across chains for a stable multi-chain estimate. # ------------------------------------------------------------------ def lrd_kernel(rng_key, state, logdensity_fn, inverse_mass_matrix, L, step_size): return base_kernel( rng_key=rng_key, state=state, logdensity_fn=logdensity_fn, inverse_mass_matrix=lrd_imm, # always route through LRD L=L, step_size=step_size, ) # Compute κ_eff of the pilot-derived LRD IMM. Uses the full Phase-2 SVD # eigenspectrum (lam_all_sorted) and the selected rank k_used to correctly # account for whitened vs. residual directions in the M⁻¹Σ⁻¹ product. kappa_eff_pilot_val = _kappa_eff_pilot(lam_all_sorted=lam_all_sorted, k=k_used) # Phase-3 DA initialisation (warmup_step_init controls E1). if warmup_step_init == "law": # E1: √d warm-start, gated on κ_eff ≤ 5. # Gate condition: the pilot LRD IMM must sufficiently whiten the # geometry (κ_eff ≤ 5) before the scaling-law init is accurate. e1_fired = kappa_eff_pilot_val <= _KAPPA_EFF_GATE if e1_fired: # Scaling-law warm-start: step ≈ 1.22√d, L ≈ 0.85√d. d = flat_pilot.shape[1] sqrt_d = jnp.sqrt(float(d)) lrd_init_params = MCLMCAdaptationState( L=jnp.array(_SQRT_D_L_CONST * sqrt_d), step_size=jnp.array(_SQRT_D_STEP_CONST * sqrt_d), inverse_mass_matrix=pilot_params.inverse_mass_matrix, # placeholder; overridden ) else: # Fallback: pilot's own (step, L) — same as "default". lrd_init_params = MCLMCAdaptationState( L=pilot_params.L, step_size=pilot_params.step_size, inverse_mass_matrix=pilot_params.inverse_mass_matrix, # placeholder; overridden ) else: # "default": warm-start from pilot L/step_size to skip unnecessary exploration. e1_fired = False lrd_init_params = MCLMCAdaptationState( L=pilot_params.L, step_size=pilot_params.step_size, inverse_mass_matrix=pilot_params.inverse_mass_matrix, # placeholder; overridden ) # Split into 2*num_chains keys: first half for chain init, second for tuning. lrd_all_keys = jax.random.split(lrd_subkey, 2 * num_chains) lrd_init_keys = lrd_all_keys[:num_chains] lrd_tune_keys = lrd_all_keys[num_chains:] # Replicate position for all chains (same start, different momenta via keys). chain_positions = jax.tree.map( lambda x: jnp.stack([x] * num_chains), state_after_warmup.position, ) @jax.vmap def _lrd_init_one(k, x0): return _mclmc_mod.init(x0, logdensity_fn, k) lrd_init_states = _lrd_init_one(lrd_init_keys, chain_positions) @jax.vmap def _lrd_tune_one(k, state): _, params, _ = mclmc_find_L_and_step_size( mclmc_kernel=lrd_kernel, num_steps=lrd_num_steps, state=state, rng_key=k, logdensity_fn=logdensity_fn, diagonal_preconditioning=False, params=lrd_init_params, ) return params lrd_params_all = _lrd_tune_one(lrd_tune_keys, lrd_init_states) # Multi-chain mean for stable L and step_size estimates. lrd_L = float(jnp.mean(lrd_params_all.L)) lrd_step_size = float(jnp.mean(lrd_params_all.step_size)) # ------------------------------------------------------------------ # Phase 4: inner-kernel dispatch # ------------------------------------------------------------------ if inner_kernel == "mclmc": final_L = jnp.array(lrd_L) final_step_size = jnp.array(lrd_step_size) else: # inner_kernel == "adjusted_mclmc" # Build the adjusted kernel wrapper — routes inverse_mass_matrix # through lrd_imm (same override pattern as Phase 3). adj_base_kernel = _adj_mclmc_mod.build_kernel() def adj_lrd_kernel( rng_key, state, logdensity_fn, step_size, integration_steps_params, inverse_mass_matrix, ): return adj_base_kernel( rng_key=rng_key, state=state, logdensity_fn=logdensity_fn, step_size=step_size, integration_steps_params=integration_steps_params, inverse_mass_matrix=lrd_imm, # always route through LRD ) # L_init floor guard: prevents DA ceiling from binding below oracle. # Hard constraints enforced here (see module docstring): # C1) params != None → no sqrt(dim) default L init # C2) frac_tune2=0.0 → variance-based L estimator disabled L_floor = floor_factor * lrd_step_size floor_active = bool(L_floor > lrd_L) L_init = float(max(lrd_L, L_floor)) # Note: inverse_mass_matrix is a placeholder in adj_init_params — # adj_lrd_kernel always routes through lrd_imm; using the diagonal # pilot IMM keeps the field consistent with MCLMCAdaptationState's # existing dtype contract (jnp array). adj_init_params = MCLMCAdaptationState( L=jnp.array(L_init), step_size=jnp.array(lrd_step_size), inverse_mass_matrix=pilot_params.inverse_mass_matrix, ) # Multi-chain adjusted tuning — same num_chains chains, starting # from the pilot's final position. adj_tune_keys = jax.random.split(adj_subkey, num_chains) @jax.vmap def _adj_init_one(x0): return _adj_mclmc_mod.init(x0, logdensity_fn) adj_init_states = _adj_init_one(chain_positions) @jax.vmap def _adj_tune_one(k, state): _, params, _ = adjusted_mclmc_find_L_and_step_size( mclmc_kernel=adj_lrd_kernel, logdensity_fn=logdensity_fn, num_steps=adjusted_num_steps, state=state, rng_key=k, target=adjusted_target, frac_tune1=0.5, # certified recipe: 0.5 × adjusted_num_steps DA steps frac_tune2=0.0, # REQUIRED: variance-based L estimator incompatible with LRD IMM diagonal_preconditioning=False, # don't overwrite LRD IMM params=adj_init_params, ) return params adj_params_all = _adj_tune_one(adj_tune_keys, adj_init_states) # target_num_integration_steps=2.0 (default) → avg-preserving calibration: # the tuner enforces L = 2·step per chain throughout DA. Average both # across chains for a stable multi-chain estimate; by the avg-preserving # invariant final_L ≈ 2·final_step_size. final_step_size = jnp.mean(adj_params_all.step_size) final_L = jnp.mean(adj_params_all.L) # L = 2·step by avg-preserving invariant # DA-ceiling diagnostic: non-binding at avg=2 (step ≈ L/2 ≪ L/1.1), # but kept correct: use final_L (not L_init) as the calibration anchor. _check_da_ceiling_warning(float(final_step_size), float(final_L), floor_factor) # Gradient accounting: unadjusted MCLMC costs 2 grads/step. pilot_num_grad_evals = (pilot_num_warmup + pilot_num_samples) * 2 diagnostics = { "inner_kernel": inner_kernel, "n_eff": n_eff, "k_safe": k_safe, "k_used": k_used, "pilot_num_grad_evals": pilot_num_grad_evals, "pilot_L": pilot_L, "pilot_step_size": pilot_step_size_val, "lrd_L": lrd_L, "lrd_step_size": lrd_step_size, "e1_fired": e1_fired, "kappa_eff_pilot": kappa_eff_pilot_val, } # Adjusted-path-only provenance keys (available in scope only for that branch). if inner_kernel == "adjusted_mclmc": diagnostics["L_init"] = L_init diagnostics["floor_active"] = floor_active # N_sample = effective number of leapfrog steps per trajectory at the # final adapted params — the floor-guard inputs that tuningfork cert # integration uses to verify trajectory-length bookkeeping. diagnostics["N_sample"] = round( float(final_L) / max(float(final_step_size), 1e-10) ) return MCLMCLRDAdaptationState( L=final_L, step_size=final_step_size, inverse_mass_matrix=lrd_imm, diagnostics=diagnostics, )