Source code for pytc.vmc.blocking
import numpy as np
import logging
import jax.tree_util as jtu
from typing import Dict, Any, Union, Tuple
from .mcmc_utils import load_optimization_history
logger = logging.getLogger(__name__)
[docs]
def block_analysis(data: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Perform block analysis on a 1D array of correlated data to estimate standard error.
Args:
data: 1D numpy array of correlated samples (e.g. energies from MCMC).
Returns:
A tuple of (block_sizes, block_errors, block_overall_means):
- block_sizes: Array of block sizes used.
- block_errors: Array of estimated standard errors for each block size.
- block_overall_means: Array of the mean value of the data for the given block size.
"""
data = np.asarray(data)
if data.ndim != 1:
raise ValueError("Data must be a 1D array for standard block analysis.")
n_data = len(data)
max_blocks = int(np.log2(n_data))
if max_blocks < 1:
return np.array([1]), np.array([0.0]), np.array([np.mean(data) if n_data > 0 else 0.0])
block_sizes = []
block_errors = []
block_overall_means = []
for i in range(max_blocks):
block_size = 2**i
n_blocks = n_data // block_size
# Truncate data to neatly fit into blocks
truncated_data = data[:n_blocks * block_size]
blocks = truncated_data.reshape((n_blocks, block_size))
# Mean of each block
block_means = np.mean(blocks, axis=1)
if n_blocks > 1:
# Error of the means
block_var = np.var(block_means, ddof=1)
block_error = np.sqrt(block_var / n_blocks)
block_sizes.append(block_size)
block_errors.append(block_error)
block_overall_means.append(np.mean(block_means))
return np.array(block_sizes), np.array(block_errors), np.array(block_overall_means)
[docs]
def detect_equilibration_cma(data: np.ndarray, threshold_sigma: float = 0.5, print_results: bool = True) -> int:
"""
Detects the equilibration point (burn-in length) using the reverse
Cumulative Moving Average (CMA) deviation method.
It scans backward from the end of the simulation. For each step i, it calculates
the mean of the data from i to the end. The burn-in period is considered over when
this 'tail mean' is within a tight threshold of the final steady-state mean.
"""
data = np.asarray(data)
n = len(data)
if n < 100:
return 0
# Assume the second half of the simulation is firmly equilibrated to define the steady state
steady_state_tail = data[n // 2:]
steady_state_mean = np.mean(steady_state_tail)
steady_state_std = np.std(steady_state_tail)
# Calculate reverse CMA: cma_from_end[i] is the mean of data[i:n]
rev_data = data[::-1]
rev_cma = np.cumsum(rev_data) / np.arange(1, n + 1)
cma_from_end = rev_cma[::-1]
deviations = np.abs(cma_from_end - steady_state_mean)
# We use a strict threshold based on the intrinsic noise of the data
threshold = threshold_sigma * steady_state_std
if print_results:
logger.info(f"Reverse CMA: Assumed steady state is the last {len(steady_state_tail)} iterations.")
logger.info(f"Reverse CMA: Steady state mean = {steady_state_mean:.6g}, deviation std = {steady_state_std:.6g}")
logger.info(f"Reverse CMA: Tolerance threshold for burn-in = {threshold:.6g} ({threshold_sigma} sigma)")
exceeds_threshold = deviations > threshold
if not np.any(exceeds_threshold):
if print_results:
logger.info("Reverse CMA: Entire trace appears within tolerance. No burn-in detected.")
return 0
# Find the last index where the tail mean was still biased by initial configurations
burn_in_idx = np.where(exceeds_threshold)[0][-1] + 1
if print_results:
logger.info(f"Reverse CMA: Trace deviates significantly from steady state mean until step {burn_in_idx}.")
# Cap burn-in at 80% to ensure we always have at least some data to analyze
return min(burn_in_idx, int(0.8 * n))
[docs]
def analyze_optimization_history(
history: Union[str, Dict[str, Any]],
burn_in_frac: Union[float, str] = 'auto',
print_results: bool = True
) -> Dict[str, Any]:
"""
Analyze an optimization history, discarding burn-in and computing block analysis.
Args:
history: Filepath to an HDF5 history file, or a dictionary containing history data.
burn_in_frac: Fraction of the initial steps to discard as burn-in.
print_results: Whether to print textual output.
Returns:
A dictionary containing the block analysis results and optimal parameters (Polyak averaged).
"""
if isinstance(history, str):
if print_results:
logger.info(f"Loading history from {history}...")
history = load_optimization_history(history)
energies = np.array(history['energies'])
n_total = len(energies)
if isinstance(burn_in_frac, str) and burn_in_frac.lower() == 'auto':
burn_in_idx = detect_equilibration_cma(energies, print_results=print_results)
if print_results:
logger.info(f"Auto-detected burn-in point at step {burn_in_idx} using Reverse CMA method.")
else:
burn_in_idx = int(burn_in_frac * n_total)
if n_total < 100:
burn_in_idx = 0
equil_energies = energies[burn_in_idx:]
if print_results:
logger.info(f"Loaded {n_total} energy values.")
logger.info(f"Using {len(equil_energies)} energies after discarding {burn_in_idx} for burn-in.")
mean_energy = np.mean(equil_energies)
sizes, errors, block_means_arr = block_analysis(equil_energies)
max_err = np.max(errors) if len(errors) > 0 else 0.0
if print_results:
logger.info(f"Mean Energy: {mean_energy:.6f}")
logger.info("\\nBlock Analysis:")
logger.info(f"{'Block Size':>12} {'Mean Energy':>16} {'Std Error':>15}")
for s, m, e in zip(sizes, block_means_arr, errors):
logger.info(f"{s:12d} {m:16.6f} {e:15.6f}")
logger.info(f"\\nEstimated standard error (from plateau): {max_err:.6f}")
logger.info(f"Final Result: {mean_energy:.6f} +/- {max_err:.6f}")
# Also perform Polyak-Ruppert averaging on the parameters if they exist
polyak_params = None
if 'params' in history:
params_history = history['params']
# Extract the equilibrated part of the parameters history (slices each leaf's axis=0)
equil_params_history = jtu.tree_map(lambda x: x[burn_in_idx:], params_history)
# Average each parameter over the equilibrated trace
polyak_params = jtu.tree_map(lambda x: np.mean(x, axis=0), equil_params_history)
return {
"mean_energy": mean_energy,
"standard_error": max_err,
"block_sizes": sizes,
"block_errors": errors,
"block_means": block_means_arr,
"polyak_params": polyak_params
}