"""
Comparison of spin-spin correlation functions G(r) for the q-state Clock model.
Analyzes correlation behavior in ordered, quasi-ordered, and disordered phases.
The q=6 clock model has two Kosterlitz-Thouless transitions at T1 ≈ 0.68
and T2 ≈ 0.92 (José et al. 1977), yielding three distinct correlation regimes:
long-range order below T1, algebraic (quasi-long-range) order between T1 and T2,
and exponential decay above T2.
"""
from __future__ import annotations
import argparse
import logging
import matplotlib.pyplot as plt
import numpy as np
from models.clock_model import ClockSimulation
from utils.equilibration import convergence_equilibrate_with_status
from utils.observables import get_averaged_correlation
from utils.plotting import ensure_results_dir, save_plot
from utils.system import parse_args_compat, setup_logging
# Approximate KT transition temperatures for q=6 (José et al. 1977).
T1_CLOCK6: float = 0.68
T2_CLOCK6: float = 0.92
[docs]
def simulate_correlation(
*,
T: float,
L: int,
q: int,
steps: int,
eq_probe: int,
eq_max: int,
sample_interval: int,
seed: int,
logger: logging.Logger,
) -> tuple[np.ndarray, np.ndarray]:
"""Equilibrate and measure the averaged correlation function at temperature T.
Uses two-start convergence equilibration to avoid initialization bias.
Parameters
----------
T : float
Temperature for the measurement.
L : int
Linear lattice size.
q : int
Number of clock states.
steps : int
Measurement steps after equilibration.
eq_probe : int
Chunk size for convergence equilibration probes.
eq_max : int
Maximum equilibration steps.
sample_interval : int
Spacing between correlation samples during measurement.
seed : int
Random seed for reproducibility.
logger : logging.Logger
Logger instance.
Returns
-------
tuple[np.ndarray, np.ndarray]
Radial distances r and averaged correlations G(r).
"""
logger.debug(f'Equilibrating at T={T:.3f} (q={q}, L={L}, seed={seed})...')
sim_r = ClockSimulation(
size=L, temp=T, q=q, update='checkerboard', init_state='random', seed=seed,
)
sim_o = ClockSimulation(
size=L, temp=T, q=q, update='checkerboard', init_state='ordered', seed=seed,
)
_, converged = convergence_equilibrate_with_status(
sim_r, sim_o, chunk_size=eq_probe, max_steps=eq_max,
)
# Fall back to ordered-start simulation when random-start is stuck.
sim_meas = sim_r if converged else sim_o
if not converged:
logger.info(f'T={T:.3f}: convergence not reached, falling back to ordered start')
logger.debug(f'Measuring correlations at T={T:.3f}...')
return get_averaged_correlation(
sim=sim_meas, total_steps=steps, sample_interval=sample_interval,
)
[docs]
def main() -> None:
"""Run the clock model correlation comparison analysis."""
parser = argparse.ArgumentParser(
description='q-state Clock Model Correlation Comparison',
)
parser.add_argument('--size', type=int, default=128, help='Linear lattice size L')
parser.add_argument('--q', type=int, default=6, help='Number of clock states')
parser.add_argument('--steps', type=int, default=4000, help='Measurement steps')
parser.add_argument('--eq-probe', type=int, default=200, help='Convergence probe chunk size')
parser.add_argument('--eq-max', type=int, default=50000, help='Max equilibration steps')
parser.add_argument('--interval', type=int, default=20, help='Sample interval')
parser.add_argument('--seed', type=int, default=520, help='Base random seed')
parser.add_argument('--output-dir', type=str, default='results/clock', help='Output directory')
parser.add_argument('--log-file', type=str, default=None, help='Optional log file path')
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
args = parse_args_compat(parser)
log_level = logging.DEBUG if args.verbose else logging.INFO
logger = setup_logging(level=log_level, log_file=args.log_file)
# Three representative temperatures spanning the three phases.
T_ORDERED: float = 0.5 # T < T1 (long-range order)
T_QUASI: float = 0.8 # T1 < T < T2 (algebraic quasi-order)
T_DISORDERED: float = 1.2 # T > T2 (exponential decay)
logger.info(
f'Starting clock correlation comparison '
f'(q={args.q}, L={args.size}, steps={args.steps})'
)
logger.info(
f'Temperatures: ordered={T_ORDERED}, quasi={T_QUASI}, disordered={T_DISORDERED}'
)
common = dict(
L=args.size, q=args.q, steps=args.steps,
eq_probe=args.eq_probe, eq_max=args.eq_max,
sample_interval=args.interval, seed=args.seed, logger=logger,
)
r_ordered, G_ordered = simulate_correlation(T=T_ORDERED, **common)
r_quasi, G_quasi = simulate_correlation(T=T_QUASI, **common)
r_disordered, G_disordered = simulate_correlation(T=T_DISORDERED, **common)
# Verify r arrays are identical (all from same lattice size).
assert np.array_equal(r_ordered, r_quasi) and np.array_equal(r_quasi, r_disordered)
r = r_ordered
# --- Plotting ---
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
ax1.loglog(r[1:], G_ordered[1:], 'o-', label=f'T={T_ORDERED} (ordered, T < T₁)', alpha=0.7)
ax1.loglog(r[1:], G_quasi[1:], 's-', label=f'T={T_QUASI} (quasi-ordered)', alpha=0.7)
ax1.loglog(
r[1:], G_disordered[1:], 'x-',
label=f'T={T_DISORDERED} (disordered, T > T\u2082)', alpha=0.7,
)
ax1.set_title('Log-Log Plot')
ax1.set_xlabel('Distance r')
ax1.set_ylabel('Correlation G(r)')
ax1.legend()
ax1.grid(True, which='both', ls='-', alpha=0.5)
ax2.plot(r, G_ordered, 'o-', label=f'T={T_ORDERED} (ordered)', alpha=0.7)
ax2.plot(r, G_quasi, 's-', label=f'T={T_QUASI} (quasi-ordered)', alpha=0.7)
ax2.plot(r, G_disordered, 'x-', label=f'T={T_DISORDERED} (disordered)', alpha=0.7)
ax2.set_yscale('log')
ax2.set_title('Semi-Log Plot')
ax2.set_xlabel('Distance r')
ax2.set_ylabel('Correlation G(r)')
ax2.legend()
ax2.grid(True, which='both', ls='-', alpha=0.5)
fig.suptitle(f'{args.q}-state Clock Model: Correlation Comparison (L={args.size})')
output_dir: str = ensure_results_dir(directory=args.output_dir)
save_plot(filename='correlation_comparison.png', directory=output_dir)
# Save data for notebook consumption.
npz_path = f'{output_dir}/correlation_comparison.npz'
np.savez_compressed(
npz_path,
r=r,
G_ordered=G_ordered,
G_quasi=G_quasi,
G_disordered=G_disordered,
T_ordered=T_ORDERED,
T_quasi=T_QUASI,
T_disordered=T_DISORDERED,
T1=T1_CLOCK6,
T2=T2_CLOCK6,
L=args.size,
q=args.q,
steps=args.steps,
eq_probe=args.eq_probe,
eq_max=args.eq_max,
sample_interval=args.interval,
seed=args.seed,
)
logger.info(f'Data saved to {npz_path}')
if __name__ == '__main__':
main()