Γ-CCM / aiccm2026dev-a: step by step¶
This tutorial walks through the Γ-CCM (direct-torus Γ-supercell CCM)
line, code selector aiccm2026dev-a. Every step is a runnable Python
fragment — paste them in order. The companion user guide is
Γ-CCM reference; the independent χ-CCM line has
its own tutorial.
See also
The H-chain ancestor tutorial derives the CCM–Bloch equivalence on a 1-D chain — read that first if the cluster-size ↔ k-mesh duality is new.
1. Build a cyclic cluster¶
import numpy as np
from vibeqc import Atom, PeriodicSystem
from vibeqc.periodic.ccm import CCMSystem
BOHR = 1.0 / 0.529177210903
# H4 alternating chain: 4 atoms/cell, a=4.0 bohr
pos = [[x * BOHR, 0, 0] for x in (0.0, 0.8, 2.0, 2.8)]
unit = PeriodicSystem(
3, # 3-D periodicity
np.diag([4.0 * BOHR, 40.0, 40.0]), # lattice: a=4, b=c=40 (vacuum)
[Atom(1, p) for p in pos],
charge=0, multiplicity=1,
)
# 4-cell chain along x (= 16 H atoms in the supercell)
ccm = CCMSystem(unit, (4, 1, 1), "sto-3g")
print(f"n_atoms = {ccm.n_atoms}, nbf = {ccm.nbf}")
CCMSystem builds the Wigner-Seitz supercell environment automatically.
The (4, 1, 1) means 4 replicas along the first lattice vector, 1
along the second and third — a 1-D chain.
2. The 8-fold symmetry gate¶
The historical union12 weight breaks ERI permutation symmetry in
non-orthorhombic lattices. The symmetric aiccm2026dev-a weight
restores it. Check it:
from vibeqc.periodic.ccm.padded import build_padded_cluster, ccm_eri, ccm_eri_symmetric, eri_cells
pad = build_padded_cluster(ccm, eri_cells(ccm))
def max_asym(V):
nrm = np.linalg.norm(V)
return max(np.linalg.norm(V - V.transpose(1, 0, 2, 3)) / nrm,
np.linalg.norm(V - V.transpose(0, 1, 3, 2)) / nrm,
np.linalg.norm(V - V.transpose(2, 3, 0, 1)) / nrm)
V_union = ccm_eri(ccm, pad) # historical product weight
V_sym = ccm_eri_symmetric(ccm, pad) # symmetric four-center
print(f"union12 asymmetry: {max_asym(V_union):.2e}")
print(f"aiccm2026dev-a asymmetry: {max_asym(V_sym):.2e}")
On the H4 chain the two weights give the same energy (the 1-D WS symmetry hides the asymmetry). In 2-D hexagonal and oblique lattices they diverge by ~1.4 mHa/atom — the reference page has the numbers.
3. Hartree–Fock¶
from vibeqc.periodic.ccm.scf import run_ccm_rhf, run_ccm_rhf_scalable
rhf = run_ccm_rhf(ccm, method="aiccm2026dev-a")
scalable = run_ccm_rhf_scalable(ccm, method="aiccm2026dev-a") # C++ kernel, reaches 3-D
print(f"RHF (Python) E/atom = {rhf.energy_per_atom:.8f} Ha "
f"{rhf.n_iter} iters, converged={rhf.converged}")
print(f"RHF (scalable) E/atom = {scalable.energy_per_atom:.8f} Ha "
f"{scalable.n_iter} iters, converged={scalable.converged}")
The 1-D H4 chain gold reference is −0.542875 Ha/atom. Your number will
approach it as the cluster grows: at nrep=(4,1,1) it is −0.542676; at
(8,1,1) it is −0.542870.
4. Kohn–Sham DFT¶
Any libxc functional works. Pure and hybrid:
from vibeqc.periodic.ccm.dft import run_ccm_rks
for func in ("pbe", "pbe0", "b3lyp"):
rks = run_ccm_rks(ccm, func, method="aiccm2026dev-a")
print(f"RKS/{func:6s} E/atom = {rks.energy_per_atom:.8f} Ha")
5. MP2 and CCSD(T) correlation¶
from vibeqc.periodic.ccm.mp2 import run_ccm_mp2
from vibeqc.periodic.ccm.ccsd import run_ccm_ccsd
mp2 = run_ccm_mp2(ccm, method="aiccm2026dev-a")
cc = run_ccm_ccsd(ccm, method="aiccm2026dev-a", compute_triples=True)
print(f"MP2 Ecorr = {mp2.e_correlation:.8f} Ha")
print(f"CCSD Ecorr = {cc.e_ccsd_correlation:.8f} Ha")
print(f"CCSD(T) Ecorr = {cc.e_correlation:.8f} Ha, (T) = {cc.e_t:.2e}")
The post-HF stack uses the CCM MO integrals directly — formally
molecular algebra over the cyclic-cluster orbitals. The molecular limit
is exact: CCSD(T) matches run_ccsd to the DF gap and (T) to ~1e-8.
6. The RI hierarchy¶
The 4-center → RI → RIJCOSX approximation ladder:
from vibeqc.periodic.ccm.ri import run_ccm_rhf_gdf, run_ccm_rhf_rijcosx
hf_4c = run_ccm_rhf(ccm, method="aiccm2026dev-a")
hf_ri = run_ccm_rhf_gdf(ccm) # RI Coulomb via GDF
hf_cox = run_ccm_rhf_rijcosx(ccm) # RI-J + chain-of-spheres K
print(f"4-center E/atom = {hf_4c.energy_per_atom:.8f} Ha")
print(f"RI (GDF) E/atom = {hf_ri.energy_per_atom:.8f} Ha")
print(f"RIJCOSX E/atom = {hf_cox.energy_per_atom:.8f} Ha")
The RI route (GDF) reproduces the four-center to the RI fitting error (≲0.1 mHa/atom). For 3-D ionic crystals the GDF route is the production energy path — the bare four-center carries a Madelung gap there (see step 8).
7. Cluster sizing by interaction radius¶
Instead of an explicit (N1, N2, N3) mesh, size the cluster by a
real-space cutoff — the dual of choosing a k-mesh density:
from vibeqc.periodic.ccm import nrep_for_interaction_range
# Derive the minimal cluster that encloses a sphere of R_c = 12 bohr
ccm_r = CCMSystem.from_interaction_range(unit, 12.0, "sto-3g")
print(f"R_c = 12 bohr → nrep = {ccm_r.nrep}, n_atoms = {ccm_r.n_atoms}")
# Or in Angstrom
ccm_a = CCMSystem(unit, basis="sto-3g", interaction_range_ang=6.5)
print(f"R_c = 6.5 Å → nrep = {ccm_a.nrep}")
# Convergence scan — the CCM analogue of a k-point convergence study
from vibeqc.periodic.ccm import ccm_interaction_range_scan
scan = ccm_interaction_range_scan(unit, "sto-3g",
radii_bohr=[6, 9, 12, 15, 18, 24, 30],
method="aiccm2026dev-a")
print(f"Converged to 0.1 mHa/atom at R_c = {scan.converged_radius(1e-4)} bohr")
The relationship is Δk = π/R_c: a larger real-space radius ⇔ a denser
k-mesh. On the 1-D H2 chain the energy converges to 0.1 mHa/atom by
R_c ≈ 9 bohr and to 10 µHa/atom by R_c ≈ 18 bohr.
8. Ionic systems and the neutral reference¶
The bare-1/r four-center over-binds ionic crystals by a Madelung-scale
shift. The neutral (Ewald) four-center g_eff = Σ_P L⊗L fixes this:
from vibeqc.periodic.ccm import ccm_eri_neutral, ccm_neutral_background_constant
from vibeqc.periodic.ccm.scf import run_ccm_rhf
# Build a LiH chain (ionic)
a = 3.2 * BOHR
lih = PeriodicSystem(
1, np.diag([a, 40.0, 40.0]),
[Atom(3, [0, 0, 0]), Atom(1, [a/2, 0, 0])],
)
ccm_lih = CCMSystem(lih, (6, 1, 1), "sto-3g")
# The Madelung background constant ξ
xi = ccm_neutral_background_constant(ccm_lih)
print(f"Cell Madelung constant ξ = {xi:.4f}")
# Neutral four-center g_eff
g_neutral = ccm_eri_neutral(ccm_lih, ke_cutoff=120.0)
# Bare vs neutral SCF
scf_bare = run_ccm_rhf(ccm_lih, method="aiccm2026dev-a")
scf_neutral = run_ccm_rhf(ccm_lih, eri=g_neutral)
print(f"Bare 4c E/atom = {scf_bare.energy_per_atom:.8f} Ha")
print(f"Neutral 4c E/atom = {scf_neutral.energy_per_atom:.8f} Ha")
print(f"Δ = {(scf_bare.energy_per_atom - scf_neutral.energy_per_atom) * 1000:.2f} mHa/atom")
For absolute ionic HF/KS energies use the GDF route
(run_ccm_rhf_gdf). For ionic correlation, run the post-HF stack on
the neutral four-center (bare-vs-neutral references disagree because the
Madelung background shifts the occupied–virtual denominators). See
the Madelung gap
for the full explanation.
9. Properties¶
from vibeqc.periodic.ccm import (
ccm_homo_lumo_gap, ccm_mulliken_charges,
ccm_lowdin_charges, ccm_dipole,
)
# Gap (folded-Γ spectrum on the supercell)
homo, lumo, gap = ccm_homo_lumo_gap(rhf, ccm)
print(f"Gap: HOMO = {homo:.4f} LUMO = {lumo:.4f} Δ = {gap:.4f} Ha")
# Per-cell charges
mq = ccm_mulliken_charges(rhf, ccm)
lq = ccm_lowdin_charges(rhf, ccm)
for i, (m, l) in enumerate(zip(mq.per_cell_charges, lq.per_cell_charges)):
print(f" Atom {i}: Mulliken = {m:+.4f} Löwdin = {l:+.4f}")
print(f" Translational spread = {mq.translational_spread:.4e}")
# Finite-cluster dipole (≈ 0 for centrosymmetric)
mu = ccm_dipole(rhf, ccm)
print(f"Dipole = ({mu.x:.2e}, {mu.y:.2e}, {mu.z:.2e}) e·bohr")
10. Wannier localization and symmetry¶
from vibeqc.periodic.ccm import localise_ccm, localization_density_residual
from vibeqc.periodic.ccm import analyze_ccm_symmetry, ccm_symmetry_invariance_residuals
# Localize the occupied orbitals (Pipek–Mezey)
loc = localise_ccm(rhf, ccm, method="pm")
print(f"Localization objective: {loc.objective_initial:.4f} → {loc.objective_final:.4f}")
print(f"Density residual (must be ≈ 0): {localization_density_residual(rhf, loc):.2e}")
for i, (center, spread) in enumerate(zip(loc.centers, loc.spreads)):
print(f" Wannier {i}: center = ({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f})"
f" spread = {spread:.4f} bohr²")
# Symmetry analysis (spglib)
sym = analyze_ccm_symmetry(ccm)
print(f"Space group: {sym.space_group_number} ({sym.space_group_symbol})")
s_res, t_res = ccm_symmetry_invariance_residuals(ccm, sym)
print(f"S-invariance residual: {s_res:.2e}")
print(f"T-invariance residual: {t_res:.2e}")
Next steps¶
The Γ-CCM reference has the complete method stack, the DLPNO local-correlation entry point, open-shell UCCSD(T), and the full feature matrix.
The test-set runner runs any of 28 curated systems through any route with one command.
For the independent χ-CCM line, see the χ-CCM tutorial and user guide.