open-standards/wai/wai-quantum/examples/nqs_ground.rs
Transaction Science e7d4380eba nqs_ground: a projected network up to 24 spins is held to its sector alone
The full reference was computed first and then replaced by the
sector's; past 20 spins that first step was a DMRG run (136 s on the
6 × 4 torus, and 3 % off the exact energy) whose answer was thrown
away.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 14:17:42 -04:00

149 lines
8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! A neural quantum state for a spin model's ground state, by variational
//! Monte Carlo with stochastic reconfiguration, against exact
//! diagonalisation (up to 20 spins) or DMRG on the same operator sum.
//!
//! ```text
//! cargo run --release --example nqs_ground --features quantum_nqs -- \
//! --model heisenberg|j1j2|ising --lx L --ly L [--j2 J] [--h H] [--periodic]
//! [--complex] [--marshall] [--alpha A] [--steps N] [--samples S] [--lr R] [--shift λ]
//! [--init W] [--exact] [--guide ε]
//! [--project [--kpi] [--point-group a1|a2|b1|b2 | --reflections a1|a2|b1|b2]
//! [--odd]] [--dmrg-bond D]
//! [--threads T] [--exact-final]
//!
//! `--project` projects onto translations of the periodic lattice at
//! momentum 0 (or π with `--kpi`), with `--point-group` on a square lattice
//! onto a representation of its rotations and reflections too, with
//! `--reflections` on any lattice onto one of its half turn and axis
//! reflections (`RectIrrep`), and onto even
//! (or `--odd`) parity under a global spin flip. Up to 20 spins, the
//! reference is then the exact ground energy in that sector. `--exact-final`
//! also evaluates the trained network with every configuration enumerated
//! (at most 24 spins), separating what training left from what sampling
//! adds. `--guide ε` samples `|ψ|²` with a floor `ε` under it, weighted
//! back (`VmcConfig::guide`); projected networks default to `0.1`, others to
//! none.
//! ```
use wai_quantum::quantum_dmrg::{dmrg_mpo, DmrgConfig, Mps, C};
use wai_quantum::quantum_nqs::*;
fn arg<T: std::str::FromStr>(a: &[String], key: &str, default: T) -> T {
a.iter().position(|x| x == key).and_then(|i| a.get(i + 1)).and_then(|v| v.parse().ok()).unwrap_or(default)
}
fn train<T: Amplitude>(model: SpinModel, rbm: Rbm<T>, cfg: VmcConfig, steps: usize, reference: f64, exact_final: bool) {
println!("RBM α={}: {} parameters, {} samples a step", rbm.m / rbm.n, rbm.params(), cfg.samples);
let mut vmc = Vmc::new(model, rbm, cfg);
let t0 = std::time::Instant::now();
for it in 1..=steps {
let e = vmc.step();
if it % (steps / 10).max(1) == 0 {
println!("step {it:>5} E {:.6} ± {:.6} variance {:.2e} acceptance {:.2} relative error {:.2e} {:.0}s", e.energy, e.error, e.variance, e.acceptance, (e.energy - reference) / reference.abs(), t0.elapsed().as_secs_f64());
}
}
vmc.cfg.samples *= 8;
let f = vmc.estimate();
println!("final E {:.6} ± {:.6} variance {:.2e} relative error {:.2e}", f.energy, f.error, f.variance, (f.energy - reference) / reference.abs());
// The trained network's own energy and variance, every configuration
// enumerated: what is left once sampling is taken out.
if exact_final {
vmc.cfg.exact = true;
let x = vmc.estimate();
println!("exact E {:.8} variance {:.2e} relative error {:.2e} (every configuration)", x.energy, x.variance, (x.energy - reference) / reference.abs());
}
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let kind: String = arg(&a, "--model", "heisenberg".to_string());
let (lx, ly): (usize, usize) = (arg(&a, "--lx", 4), arg(&a, "--ly", 4));
let periodic = a.iter().any(|x| x == "--periodic");
let threads: usize = arg(&a, "--threads", VmcConfig::default().threads);
let n = lx * ly;
let nn = if ly == 1 { chain_edges(n, periodic) } else { square_edges(lx, ly, periodic) };
let nnn = if ly == 1 { chain_next_nearest(n, periodic) } else { square_diagonals(lx, ly, periodic) };
let model = match kind.as_str() {
"heisenberg" => SpinModel::heisenberg(n, &nn, 1.0),
"j1j2" => SpinModel::j1j2(n, &nn, &nnn, 1.0, arg(&a, "--j2", 0.5)),
"ising" => SpinModel::ising(n, &nn, 1.0, arg(&a, "--h", 3.044)),
other => panic!("{other}"),
};
let sector = if model.conserves_magnetisation() { Some(0) } else { None };
let point = a.iter().position(|x| x == "--point-group").map(|i| match a.get(i + 1).map(String::as_str) {
Some("a1") => PointIrrep::A1,
Some("a2") => PointIrrep::A2,
Some("b1") => PointIrrep::B1,
Some("b2") => PointIrrep::B2,
other => panic!("--point-group: a1, a2, b1 or b2, not {other:?}"),
});
let reflections = a.iter().position(|x| x == "--reflections").map(|i| match a.get(i + 1).map(String::as_str) {
Some("a1") => RectIrrep::A1,
Some("a2") => RectIrrep::A2,
Some("b1") => RectIrrep::B1,
Some("b2") => RectIrrep::B2,
other => panic!("--reflections: a1, a2, b1 or b2, not {other:?}"),
});
let symmetry = a.iter().any(|x| x == "--project").then(|| {
assert!(periodic, "--project needs --periodic");
let kpi = a.iter().any(|x| x == "--kpi");
let base = match (point, reflections) {
(Some(_), Some(_)) => panic!("--point-group or --reflections, not both"),
(Some(irrep), None) => {
assert_eq!(lx, ly, "--point-group needs a square lattice");
Symmetry::square(lx, kpi, irrep)
}
(None, Some(irrep)) => Symmetry::rectangle(lx, ly, kpi, kpi, irrep),
(None, None) if ly == 1 => Symmetry::ring(n, kpi),
(None, None) => Symmetry::torus(lx, ly, kpi, kpi),
};
base.with_spin_flip(a.iter().any(|x| x == "--odd"))
});
let t0 = std::time::Instant::now();
// A projected network is held to its own sector's ground energy.
let in_sector = match &symmetry {
Some(sym) if n <= 24 => Some(model.exact_ground_energy_in(sector, sym).expect("the model is invariant under the group, and the sector is not empty")),
_ => None,
};
let (reference, how) = match (in_sector, if n <= 20 && in_sector.is_none() { model.exact_ground_energy(sector) } else { None }) {
(Some(e), _) => (e, "exact diagonalisation in the projection's sector".to_string()),
(None, Some(e)) => (e, "exact diagonalisation".to_string()),
(None, None) => {
let bond = arg(&a, "--dmrg-bond", 128usize);
let r = dmrg_mpo(&model.opsum().mpo(), Mps::random(n, 8, 1), &DmrgConfig { max_bond: bond, sweeps: 6, threads, ..DmrgConfig::default() });
(r.energy, format!("DMRG, bond {bond}, discarded {:.1e}", r.discarded))
}
};
println!("{kind} {lx}×{ly}{}: reference {reference:.8} ({how}, {:.1}s)", if periodic { " periodic" } else { "" }, t0.elapsed().as_secs_f64());
let alpha: usize = arg(&a, "--alpha", 2);
let mut cfg = VmcConfig { samples: arg(&a, "--samples", 1000), chains: 16, magnetisation: sector, lr: arg(&a, "--lr", 0.02), shift: arg(&a, "--shift", 1e-3), exact: a.iter().any(|x| x == "--exact"), threads, ..VmcConfig::default() };
let steps: usize = arg(&a, "--steps", 600);
let marshall = a.iter().any(|x| x == "--marshall");
let exact_final = a.iter().any(|x| x == "--exact-final");
// Parameters start uniform in ±init. A projected network that starts
// near zero is nearly the projection of a constant, the ferromagnet: an
// eigenstate, which reconfiguration barely moves.
let init: f64 = arg(&a, "--init", if symmetry.is_some() { 0.1 } else { 0.01 });
// A projected network's amplitude is a sum that cancels near its nodes,
// where the local energy is large and |ψ|² rarely samples; a guide floor
// visits there, and training then corrects what it finds.
cfg.guide = arg(&a, "--guide", if symmetry.is_some() { 0.1 } else { 0.0 });
if a.iter().any(|x| x == "--complex") {
let mut rbm: Rbm<C> = Rbm::complex(n, alpha, init, 7);
if marshall {
rbm = rbm.with_marshall_sign(checkerboard(lx, ly));
}
if let Some(sym) = symmetry {
rbm = rbm.with_symmetry(sym);
}
train(model, rbm, cfg, steps, reference, exact_final);
} else {
let mut rbm = Rbm::new(n, alpha, init, 7);
if marshall {
rbm = rbm.with_marshall_sign(checkerboard(lx, ly));
}
if let Some(sym) = symmetry {
rbm = rbm.with_symmetry(sym);
}
train(model, rbm, cfg, steps, reference, exact_final);
}
}