use crate::quantum_dmrg::{C, Op, OpSum, Scalar, default_threads, lanczos};
use crate::repro::{atan2, exp, ln, sin_cos};
#[derive(Clone, Debug, PartialEq)]
pub struct SpinModel {
pub n: usize,
pub exchange: Vec<(usize, usize, f64)>,
pub zz: Vec<(usize, usize, f64)>,
pub x: Vec<(usize, f64)>,
pub z: Vec<(usize, f64)>,
}
pub fn chain_edges(n: usize, periodic: bool) -> Vec<(usize, usize)> {
let mut e: Vec<(usize, usize)> = (0..n.saturating_sub(1)).map(|i| (i, i + 1)).collect();
if periodic && n > 2 {
e.push((n - 1, 0));
}
e
}
pub fn square_edges(lx: usize, ly: usize, periodic: bool) -> Vec<(usize, usize)> {
let mut e = Vec::new();
for y in 0..ly {
for x in 0..lx {
let i = y * lx + x;
if x + 1 < lx {
e.push((i, i + 1));
} else if periodic && lx > 2 {
e.push((i, y * lx));
}
if y + 1 < ly {
e.push((i, i + lx));
} else if periodic && ly > 2 {
e.push((i, x));
}
}
}
e
}
pub fn chain_next_nearest(n: usize, periodic: bool) -> Vec<(usize, usize)> {
let mut e: Vec<(usize, usize)> = (0..n.saturating_sub(2)).map(|i| (i, i + 2)).collect();
if periodic && n > 4 {
e.push((n - 2, 0));
e.push((n - 1, 1));
}
e
}
pub fn square_diagonals(lx: usize, ly: usize, periodic: bool) -> Vec<(usize, usize)> {
let mut e = Vec::new();
let wrap = |v: isize, l: usize| -> Option<usize> {
if v >= 0 && (v as usize) < l {
Some(v as usize)
} else if periodic && l > 2 {
Some(v.rem_euclid(l as isize) as usize)
} else {
None
}
};
for y in 0..ly {
for x in 0..lx {
let i = y * lx + x;
for dx in [1isize, -1] {
if let (Some(xx), Some(yy)) = (wrap(x as isize + dx, lx), wrap(y as isize + 1, ly)) {
let j = yy * lx + xx;
if i != j && !e.contains(&(i, j)) && !e.contains(&(j, i)) {
e.push((i, j));
}
}
}
}
}
e
}
pub fn checkerboard(lx: usize, ly: usize) -> Vec<bool> {
(0..lx * ly).map(|i| (i % lx + i / lx).is_multiple_of(2)).collect()
}
impl SpinModel {
pub fn heisenberg(n: usize, edges: &[(usize, usize)], j: f64) -> SpinModel {
SpinModel { n, exchange: edges.iter().map(|&(a, b)| (a, b, j / 4.0)).collect(), zz: edges.iter().map(|&(a, b)| (a, b, j / 4.0)).collect(), x: Vec::new(), z: Vec::new() }
}
pub fn j1j2(n: usize, nn: &[(usize, usize)], nnn: &[(usize, usize)], j1: f64, j2: f64) -> SpinModel {
let mut m = SpinModel::heisenberg(n, nn, j1);
let second = SpinModel::heisenberg(n, nnn, j2);
m.exchange.extend(second.exchange);
m.zz.extend(second.zz);
m
}
pub fn ising(n: usize, edges: &[(usize, usize)], j: f64, h: f64) -> SpinModel {
SpinModel { n, exchange: Vec::new(), zz: edges.iter().map(|&(a, b)| (a, b, -j)).collect(), x: (0..n).map(|i| (i, -h)).collect(), z: Vec::new() }
}
pub fn conserves_magnetisation(&self) -> bool {
self.x.is_empty()
}
pub fn opsum(&self) -> OpSum {
let mut s = OpSum::new(self.n);
for &(i, j, c) in &self.exchange {
s.add(2.0 * c, &[(i, Op::plus()), (j, Op::minus())]);
s.add(2.0 * c, &[(i, Op::minus()), (j, Op::plus())]);
}
for &(i, j, c) in &self.zz {
s.add(c, &[(i, Op::z()), (j, Op::z())]);
}
for &(i, c) in &self.x {
s.add(c, &[(i, Op::x())]);
}
for &(i, c) in &self.z {
s.add(c, &[(i, Op::z())]);
}
s
}
fn sector_states(&self, magnetisation: Option<i32>) -> (Vec<usize>, Vec<u32>) {
let n = self.n;
let states: Vec<usize> = (0..1usize << n).filter(|&x| magnetisation.is_none_or(|m| n as i32 - 2 * x.count_ones() as i32 == m)).collect();
let mut place = vec![u32::MAX; 1usize << n];
for (k, &x) in states.iter().enumerate() {
place[x] = k as u32;
}
(states, place)
}
fn apply_sector(&self, states: &[usize], place: &[u32], inp: &[f64], out: &mut [f64]) {
let n = self.n;
let bit = |i: usize| 1usize << (n - 1 - i);
let spin = |x: usize, i: usize| if x & bit(i) == 0 { 1.0 } else { -1.0 };
out.iter_mut().for_each(|o| *o = 0.0);
for (k, (&x, &)) in states.iter().zip(inp).enumerate() {
if amp == 0.0 {
continue;
}
let mut diag = 0.0;
for &(i, j, c) in &self.zz {
diag += c * spin(x, i) * spin(x, j);
}
for &(i, c) in &self.z {
diag += c * spin(x, i);
}
out[k] += diag * amp;
for &(i, j, c) in &self.exchange {
if spin(x, i) != spin(x, j) {
out[place[x ^ bit(i) ^ bit(j)] as usize] += 2.0 * c * amp;
}
}
for &(i, c) in &self.x {
out[place[x ^ bit(i)] as usize] += c * amp;
}
}
}
pub fn exact_ground_energy(&self, magnetisation: Option<i32>) -> Option<f64> {
let n = self.n;
if n == 0 || n > 22 {
return None;
}
if magnetisation.is_some() && !self.conserves_magnetisation() {
return None;
}
let (states, place) = self.sector_states(magnetisation);
if states.is_empty() {
return None;
}
let start: Vec<f64> = states.iter().map(|&x| 1.0 + ((x as u64).wrapping_mul(2_654_435_761) % 1000) as f64 / 1000.0).collect();
let apply = |inp: &[f64], out: &mut [f64]| self.apply_sector(&states, &place, inp, out);
Some(lanczos(&apply, &start, 60, 30, 1e-9, false).0)
}
#[cfg(test)]
fn projected_ground_energy(&self, magnetisation: Option<i32>, sym: &Symmetry) -> Option<f64> {
let n = self.n;
if n == 0 || n > 22 || sym.check().is_err() || !self.commutes_with(sym) {
return None;
}
if magnetisation.is_some() && !self.conserves_magnetisation() {
return None;
}
if magnetisation.is_some_and(|m| m != 0) && sym.flips.iter().any(|&f| f) {
return None;
}
let bit = |i: usize| 1usize << (n - 1 - i);
let (states, place) = self.sector_states(magnetisation);
let order = sym.len();
let mut images = Vec::with_capacity(states.len() * order);
for &x in &states {
for (p, &flip) in sym.perms.iter().zip(&sym.flips) {
let mut y = 0usize;
for (i, &pi) in p.iter().enumerate() {
if (x & bit(pi) != 0) != flip {
y |= bit(i);
}
}
images.push(place[y]);
}
}
let project = |v: &[f64], out: &mut [f64]| {
for (k, o) in out.iter_mut().enumerate() {
let row = &images[k * order..(k + 1) * order];
let mut acc = 0.0;
for (&y, &c) in row.iter().zip(&sym.chars) {
acc += c * v[y as usize];
}
*o = acc / order as f64;
}
};
let raw: Vec<f64> = states.iter().map(|&x| 1.0 + ((x as u64).wrapping_mul(2_654_435_761) % 1000) as f64 / 1000.0).collect();
let mut start = vec![0.0; states.len()];
project(&raw, &mut start);
let (before, after) = (raw.iter().map(|v| v * v).sum::<f64>(), start.iter().map(|v| v * v).sum::<f64>());
if after <= 1e-20 * before {
return None;
}
let apply = |inp: &[f64], out: &mut [f64]| {
let mut h = vec![0.0; states.len()];
self.apply_sector(&states, &place, inp, &mut h);
project(&h, out);
};
Some(lanczos(&apply, &start, 60, 30, 1e-9, false).0)
}
pub fn commutes_with(&self, sym: &Symmetry) -> bool {
let n = self.n;
if sym.perms.iter().any(|p| p.len() != n) {
return false;
}
let bonds = |terms: &[(usize, usize, f64)], p: &[usize]| {
let mut b: Vec<(usize, usize, u64)> = terms.iter().map(|&(i, j, c)| (p[i].min(p[j]), p[i].max(p[j]), c.to_bits())).collect();
b.sort_unstable();
b
};
let fields = |terms: &[(usize, f64)], p: &[usize]| {
let mut f: Vec<(usize, u64)> = terms.iter().map(|&(i, c)| (p[i], c.to_bits())).collect();
f.sort_unstable();
f
};
let identity: Vec<usize> = (0..n).collect();
let (exchange, zz, x, z) = (bonds(&self.exchange, &identity), bonds(&self.zz, &identity), fields(&self.x, &identity), fields(&self.z, &identity));
sym.perms.iter().zip(&sym.flips).all(|(p, &flip)| bonds(&self.exchange, p) == exchange && bonds(&self.zz, p) == zz && fields(&self.x, p) == x && fields(&self.z, p) == z && !(flip && !self.z.is_empty()))
}
pub fn exact_ground_energy_in(&self, magnetisation: Option<i32>, sym: &Symmetry) -> Option<f64> {
let n = self.n;
if n == 0 || n > 24 || sym.check().is_err() || !self.commutes_with(sym) {
return None;
}
if magnetisation.is_some() && !self.conserves_magnetisation() {
return None;
}
if magnetisation.is_some_and(|m| m != 0) && sym.flips.iter().any(|&f| f) {
return None;
}
let bit = |i: usize| 1usize << (n - 1 - i);
let image = |g: usize, x: usize| -> usize {
let mut y = 0usize;
for (i, &pi) in sym.perms[g].iter().enumerate() {
if (x & bit(pi) != 0) != sym.flips[g] {
y |= bit(i);
}
}
y
};
let least = |x: usize| -> (usize, f64) {
let (mut r, mut chi) = (x, 1.0);
for g in 0..sym.len() {
let y = image(g, x);
if y < r {
r = y;
chi = sym.chars[g];
}
}
(r, chi)
};
let mut reps: Vec<usize> = Vec::new();
let mut weight: Vec<f64> = Vec::new();
for x in 0..1usize << n {
if magnetisation.is_some_and(|m| n as i32 - 2 * x.count_ones() as i32 != m) {
continue;
}
let mut stab = 0.0;
let mut least_here = true;
for g in 0..sym.len() {
let y = image(g, x);
if y < x {
least_here = false;
break;
}
if y == x {
stab += sym.chars[g];
}
}
if least_here && stab > 0.0 {
reps.push(x);
weight.push(stab);
}
}
if reps.is_empty() {
return None;
}
let mut columns: Vec<Vec<(usize, f64)>> = Vec::with_capacity(reps.len());
for (a, &r) in reps.iter().enumerate() {
let spin = |i: usize| if r & bit(i) == 0 { 1.0 } else { -1.0 };
let mut diag = 0.0;
for &(i, j, c) in &self.zz {
diag += c * spin(i) * spin(j);
}
for &(i, c) in &self.z {
diag += c * spin(i);
}
let mut col = vec![(a, diag)];
let mut reach = |x: usize, h: f64| {
let (r2, chi) = least(x);
if let Ok(b) = reps.binary_search(&r2) {
col.push((b, h * chi * (weight[b] / weight[a]).sqrt()));
}
};
for &(i, j, c) in &self.exchange {
if spin(i) != spin(j) {
reach(r ^ bit(i) ^ bit(j), 2.0 * c);
}
}
for &(i, c) in &self.x {
reach(r ^ bit(i), c);
}
col.sort_by_key(|&(b, _)| b);
let mut merged: Vec<(usize, f64)> = Vec::with_capacity(col.len());
for (b, v) in col {
match merged.last_mut() {
Some(last) if last.0 == b => last.1 += v,
_ => merged.push((b, v)),
}
}
columns.push(merged);
}
let apply = |inp: &[f64], out: &mut [f64]| {
out.iter_mut().for_each(|o| *o = 0.0);
for (col, &v) in columns.iter().zip(inp) {
for &(b, h) in col {
out[b] += h * v;
}
}
};
let start: Vec<f64> = reps.iter().map(|&x| 1.0 + ((x as u64).wrapping_mul(2_654_435_761) % 1000) as f64 / 1000.0).collect();
Some(lanczos(&apply, &start, 60, 30, 1e-9, false).0)
}
}
pub trait Amplitude: Scalar {
fn exp(self) -> Self;
fn lncosh(self) -> Self;
fn tanh(self) -> Self;
fn div_re(self, d: f64) -> Self;
fn div(self, o: Self) -> Self;
fn draw(uniform: &mut dyn FnMut() -> f64, scale: f64) -> Self;
}
impl Amplitude for f64 {
fn exp(self) -> f64 {
exp(self)
}
fn lncosh(self) -> f64 {
let a = self.abs();
a + ln(1.0 + exp(-2.0 * a)) - core::f64::consts::LN_2
}
fn tanh(self) -> f64 {
let e = exp(-2.0 * self.abs());
let t = (1.0 - e) / (1.0 + e);
if self < 0.0 { -t } else { t }
}
fn div_re(self, d: f64) -> f64 {
self / d
}
fn div(self, o: f64) -> f64 {
self / o
}
fn draw(uniform: &mut dyn FnMut() -> f64, scale: f64) -> f64 {
scale * (2.0 * uniform() - 1.0)
}
}
fn c_ln(z: C) -> C {
C { re: ln((z.re * z.re + z.im * z.im).sqrt()), im: atan2(z.im, z.re) }
}
fn c_div(a: C, b: C) -> C {
let d = b.re * b.re + b.im * b.im;
C { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }
}
impl Amplitude for C {
fn exp(self) -> C {
let m = exp(self.re);
let (s, c) = sin_cos(self.im);
C { re: m * c, im: m * s }
}
fn lncosh(self) -> C {
if self.re < 0.0 {
return Scalar::scale(self, -1.0).lncosh();
}
let e = Scalar::scale(self, -2.0).exp();
let l = c_ln(C { re: 1.0 + e.re, im: e.im });
C { re: self.re + l.re - core::f64::consts::LN_2, im: self.im + l.im }
}
fn tanh(self) -> C {
if self.re < 0.0 {
return Scalar::scale(Scalar::scale(self, -1.0).tanh(), -1.0);
}
let e = Scalar::scale(self, -2.0).exp();
c_div(C { re: 1.0 - e.re, im: -e.im }, C { re: 1.0 + e.re, im: e.im })
}
fn div_re(self, d: f64) -> C {
C { re: self.re / d, im: self.im / d }
}
fn div(self, o: C) -> C {
c_div(self, o)
}
fn draw(uniform: &mut dyn FnMut() -> f64, scale: f64) -> C {
let re = scale * (2.0 * uniform() - 1.0);
let im = scale * (2.0 * uniform() - 1.0);
C { re, im }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointIrrep {
A1,
A2,
B1,
B2,
}
impl PointIrrep {
pub fn character(self, turns: usize, reflect: bool) -> f64 {
let rotation = if turns % 2 == 1 { -1.0 } else { 1.0 };
let reflection = if reflect { -1.0 } else { 1.0 };
match self {
PointIrrep::A1 => 1.0,
PointIrrep::A2 => reflection,
PointIrrep::B1 => rotation,
PointIrrep::B2 => rotation * reflection,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RectIrrep {
A1,
A2,
B1,
B2,
}
impl RectIrrep {
pub fn character(self, flip_x: bool, flip_y: bool) -> f64 {
let sx = if flip_x { -1.0 } else { 1.0 };
let sy = if flip_y { -1.0 } else { 1.0 };
match self {
RectIrrep::A1 => 1.0,
RectIrrep::A2 => sx * sy,
RectIrrep::B1 => sy,
RectIrrep::B2 => sx,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Symmetry {
pub perms: Vec<Vec<usize>>,
pub flips: Vec<bool>,
pub chars: Vec<f64>,
inverse: Vec<Vec<usize>>,
}
impl Symmetry {
pub fn new(perms: Vec<Vec<usize>>, chars: Vec<f64>) -> Symmetry {
let flips = vec![false; perms.len()];
Symmetry::with_flips(perms, flips, chars)
}
fn with_flips(perms: Vec<Vec<usize>>, flips: Vec<bool>, chars: Vec<f64>) -> Symmetry {
assert_eq!(perms.len(), chars.len());
assert_eq!(perms.len(), flips.len());
let inverse = perms
.iter()
.map(|p| {
let mut inv = vec![usize::MAX; p.len()];
for (i, &j) in p.iter().enumerate() {
inv[j] = i;
}
assert!(inv.iter().all(|&x| x != usize::MAX), "not a permutation");
inv
})
.collect();
Symmetry { perms, flips, chars, inverse }
}
pub fn with_spin_flip(self, odd: bool) -> Symmetry {
let mut perms = self.perms.clone();
let mut flips = self.flips.clone();
let mut chars = self.chars.clone();
for g in 0..self.perms.len() {
perms.push(self.perms[g].clone());
flips.push(!self.flips[g]);
chars.push(if odd { -self.chars[g] } else { self.chars[g] });
}
Symmetry::with_flips(perms, flips, chars)
}
pub fn ring(n: usize, momentum_pi: bool) -> Symmetry {
assert!(!momentum_pi || n.is_multiple_of(2), "momentum π on a ring of odd length");
let perms = (0..n).map(|t| (0..n).map(|i| (i + t) % n).collect()).collect();
let chars = (0..n).map(|t| if momentum_pi && t % 2 == 1 { -1.0 } else { 1.0 }).collect();
Symmetry::new(perms, chars)
}
pub fn torus(lx: usize, ly: usize, kx_pi: bool, ky_pi: bool) -> Symmetry {
assert!(!kx_pi || lx.is_multiple_of(2), "momentum π along an odd length");
assert!(!ky_pi || ly.is_multiple_of(2), "momentum π along an odd length");
let mut perms = Vec::new();
let mut chars = Vec::new();
for ty in 0..ly {
for tx in 0..lx {
perms.push((0..lx * ly).map(|i| ((i / lx + ty) % ly) * lx + (i % lx + tx) % lx).collect());
let odd = (kx_pi && tx % 2 == 1) ^ (ky_pi && ty % 2 == 1);
chars.push(if odd { -1.0 } else { 1.0 });
}
}
Symmetry::new(perms, chars)
}
pub fn square(l: usize, momentum_pi: bool, irrep: PointIrrep) -> Symmetry {
assert!(l >= 3, "the square's symmetries are distinct from l = 3");
assert!(!momentum_pi || l.is_multiple_of(2), "momentum π along an odd length");
let mut perms = Vec::new();
let mut chars = Vec::new();
for ty in 0..l {
for tx in 0..l {
for turns in 0..4 {
for reflect in [false, true] {
perms.push(
(0..l * l)
.map(|i| {
let (mut x, mut y) = (i % l, i / l);
if reflect {
x = (l - x) % l;
}
for _ in 0..turns {
(x, y) = ((l - y) % l, x);
}
((y + ty) % l) * l + (x + tx) % l
})
.collect(),
);
let momentum = if momentum_pi && (tx + ty) % 2 == 1 { -1.0 } else { 1.0 };
chars.push(momentum * irrep.character(turns, reflect));
}
}
}
}
Symmetry::new(perms, chars)
}
pub fn rectangle(lx: usize, ly: usize, kx_pi: bool, ky_pi: bool, irrep: RectIrrep) -> Symmetry {
assert!(lx >= 3 && ly >= 3, "a rectangle's reflections are distinct from length 3");
assert!(!kx_pi || lx.is_multiple_of(2), "momentum π along an odd length");
assert!(!ky_pi || ly.is_multiple_of(2), "momentum π along an odd length");
let mut perms = Vec::new();
let mut chars = Vec::new();
for ty in 0..ly {
for tx in 0..lx {
for flip_x in [false, true] {
for flip_y in [false, true] {
perms.push(
(0..lx * ly)
.map(|i| {
let (mut x, mut y) = (i % lx, i / lx);
if flip_x {
x = (lx - x) % lx;
}
if flip_y {
y = (ly - y) % ly;
}
((y + ty) % ly) * lx + (x + tx) % lx
})
.collect(),
);
let odd = (kx_pi && tx % 2 == 1) ^ (ky_pi && ty % 2 == 1);
chars.push(if odd { -1.0 } else { 1.0 } * irrep.character(flip_x, flip_y));
}
}
}
}
Symmetry::new(perms, chars)
}
pub fn check(&self) -> Result<(), String> {
if self.is_empty() {
return Err("no elements".into());
}
if let Some(c) = self.chars.iter().find(|&&c| c != 1.0 && c != -1.0) {
return Err(format!("character {c} is not ±1"));
}
let mut index: std::collections::BTreeMap<(&[usize], bool), usize> = std::collections::BTreeMap::new();
for g in 0..self.len() {
match index.get(&(self.perms[g].as_slice(), self.flips[g])) {
Some(&h) if self.chars[h] != self.chars[g] => return Err(format!("elements {h} and {g} act alike with different characters")),
Some(_) => {}
None => {
index.insert((self.perms[g].as_slice(), self.flips[g]), g);
}
}
}
let n = self.perms[0].len();
let identity: Vec<usize> = (0..n).collect();
match index.get(&(identity.as_slice(), false)) {
Some(&e) if self.chars[e] == 1.0 => {}
Some(_) => return Err("the identity's character is not 1".into()),
None => return Err("no identity".into()),
}
let mut q = vec![0usize; n];
for g in 0..self.len() {
for h in 0..self.len() {
for (i, qi) in q.iter_mut().enumerate() {
*qi = self.perms[h][self.perms[g][i]];
}
let Some(&k) = index.get(&(q.as_slice(), self.flips[g] ^ self.flips[h])) else {
return Err(format!("elements {g} and {h} compose to one outside the set"));
};
if self.chars[k] != self.chars[g] * self.chars[h] {
return Err(format!("the characters of {g} and {h} do not multiply to their product's"));
}
}
}
Ok(())
}
pub fn len(&self) -> usize {
self.perms.len()
}
pub fn is_empty(&self) -> bool {
self.perms.is_empty()
}
}
#[derive(Clone, Debug)]
enum Cache<T> {
Plain(Vec<T>),
Projected { configs: Vec<Vec<i8>>, thetas: Vec<Vec<T>>, shares: Vec<T>, log_abs: f64 },
}
#[derive(Clone, Debug, PartialEq)]
pub struct Rbm<T = f64> {
pub n: usize,
pub m: usize,
pub a: Vec<T>,
pub b: Vec<T>,
pub w: Vec<T>,
pub sublattice: Option<Vec<bool>>,
pub symmetry: Option<Symmetry>,
}
#[derive(Clone, Debug, PartialEq)]
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn uniform(&mut self) -> f64 {
(self.next() >> 11) as f64 / 9_007_199_254_740_992.0
}
fn below(&mut self, k: usize) -> usize {
(self.next() % k as u64) as usize
}
}
impl Rbm<f64> {
pub fn new(n: usize, alpha: usize, scale: f64, seed: u64) -> Rbm {
Rbm::random(n, alpha, scale, seed)
}
}
impl Rbm<C> {
pub fn complex(n: usize, alpha: usize, scale: f64, seed: u64) -> Rbm<C> {
Rbm::random(n, alpha, scale, seed)
}
}
impl<T: Amplitude> Rbm<T> {
fn random(n: usize, alpha: usize, scale: f64, seed: u64) -> Rbm<T> {
let m = alpha.max(1) * n;
let mut rng = Rng(seed);
let mut u = || rng.uniform();
let w = (0..m * n).map(|_| T::draw(&mut u, scale)).collect();
let a = (0..n).map(|_| T::draw(&mut u, scale)).collect();
let b = (0..m).map(|_| T::draw(&mut u, scale)).collect();
Rbm { n, m, a, b, w, sublattice: None, symmetry: None }
}
pub fn with_marshall_sign(mut self, sublattice: Vec<bool>) -> Rbm<T> {
assert_eq!(sublattice.len(), self.n);
self.sublattice = Some(sublattice);
self
}
pub fn with_symmetry(mut self, symmetry: Symmetry) -> Rbm<T> {
assert!(symmetry.perms.iter().all(|p| p.len() == self.n));
self.symmetry = Some(symmetry);
self
}
fn cache(&self, s: &[i8]) -> Cache<T> {
let Some(sym) = &self.symmetry else {
return Cache::Plain(self.theta(s));
};
let configs: Vec<Vec<i8>> = sym.perms.iter().zip(&sym.flips).map(|(p, &f)| p.iter().map(|&j| if f { -s[j] } else { s[j] }).collect()).collect();
let thetas: Vec<Vec<T>> = configs.iter().map(|t| self.theta(t)).collect();
let logs: Vec<T> = configs.iter().map(|t| self.log_psi(t)).collect();
let top = logs.iter().map(|l| l.re()).fold(f64::NEG_INFINITY, f64::max);
let terms: Vec<T> = logs.iter().zip(&configs).zip(&sym.chars).map(|((l, t), &c)| l.sub(T::ONE.scale(top)).exp().scale(c * self.sign(t))).collect();
let total = terms.iter().fold(T::ZERO, |acc, &x| acc.add(x));
let mag2 = total.norm2();
let scale: f64 = terms.iter().map(|x| x.norm2().sqrt()).sum();
let vanishes = mag2.sqrt() <= 1e-13 * scale;
let log_abs = if vanishes { f64::NEG_INFINITY } else { top + 0.5 * ln(mag2) };
let shares = if vanishes { vec![T::ZERO; terms.len()] } else { terms.iter().map(|&x| x.div(total)).collect() };
Cache::Projected { configs, thetas, shares, log_abs }
}
fn ratio_cached(&self, s: &[i8], cache: &Cache<T>, flips: &[usize]) -> T {
match cache {
Cache::Plain(theta) => self.ratio(s, theta, flips),
Cache::Projected { configs, thetas, shares, .. } => {
let sym = self.symmetry.as_ref().expect("a projected cache has a symmetry");
let mut r = T::ZERO;
let mut moved = Vec::with_capacity(flips.len());
for g in 0..configs.len() {
if shares[g].is_zero() {
continue;
}
moved.clear();
moved.extend(flips.iter().map(|&j| sym.inverse[g][j]));
r = r.add(shares[g].mul(self.ratio(&configs[g], &thetas[g], &moved)));
}
r
}
}
}
fn derivatives_cached(&self, s: &[i8], cache: &Cache<T>, out: &mut [T]) {
match cache {
Cache::Plain(theta) => self.log_derivatives(s, theta, out),
Cache::Projected { configs, thetas, shares, .. } => {
out.iter_mut().for_each(|o| *o = T::ZERO);
let mut one = vec![T::ZERO; out.len()];
for g in 0..configs.len() {
if shares[g].is_zero() {
continue;
}
self.log_derivatives(&configs[g], &thetas[g], &mut one);
for (o, &d) in out.iter_mut().zip(&one) {
*o = o.add(shares[g].mul(d));
}
}
}
}
}
fn log_abs_cached(&self, s: &[i8], cache: &Cache<T>) -> f64 {
match cache {
Cache::Plain(_) => self.log_amplitude(s),
Cache::Projected { log_abs, .. } => *log_abs,
}
}
fn accept(&self, s: &mut [i8], cache: &mut Cache<T>, flips: &[usize], r: T) {
let n = self.n;
match cache {
Cache::Plain(theta) => {
for &i in flips {
let ds = -2.0 * f64::from(s[i]);
for (j, t) in theta.iter_mut().enumerate() {
*t = t.add(self.w[j * n + i].scale(ds));
}
s[i] = -s[i];
}
}
Cache::Projected { configs, thetas, shares, log_abs } => {
let sym = self.symmetry.as_ref().expect("a projected cache has a symmetry");
for g in 0..configs.len() {
let moved: Vec<usize> = flips.iter().map(|&j| sym.inverse[g][j]).collect();
let rg = self.ratio(&configs[g], &thetas[g], &moved);
shares[g] = shares[g].mul(rg).div(r);
for &i in &moved {
let ds = -2.0 * f64::from(configs[g][i]);
for (j, t) in thetas[g].iter_mut().enumerate() {
*t = t.add(self.w[j * n + i].scale(ds));
}
configs[g][i] = -configs[g][i];
}
}
*log_abs += 0.5 * ln(r.norm2());
for &i in flips {
s[i] = -s[i];
}
}
}
}
pub fn params(&self) -> usize {
self.n + self.m + self.n * self.m
}
fn theta(&self, s: &[i8]) -> Vec<T> {
(0..self.m)
.map(|j| {
let row = &self.w[j * self.n..(j + 1) * self.n];
let mut t = self.b[j];
for (wi, &si) in row.iter().zip(s) {
t = t.add(wi.scale(f64::from(si)));
}
t
})
.collect()
}
pub fn log_psi(&self, s: &[i8]) -> T {
let mut l = T::ZERO;
for (a, &si) in self.a.iter().zip(s) {
l = l.add(a.scale(f64::from(si)));
}
for t in self.theta(s) {
l = l.add(t.lncosh());
}
l
}
pub fn log_amplitude(&self, s: &[i8]) -> f64 {
self.log_psi(s).re()
}
pub fn sign(&self, s: &[i8]) -> f64 {
match &self.sublattice {
Some(sub) => {
let ups = s.iter().zip(sub).filter(|&(&si, &a)| a && si > 0).count();
if ups.is_multiple_of(2) { 1.0 } else { -1.0 }
}
None => 1.0,
}
}
fn ratio(&self, s: &[i8], theta: &[T], flips: &[usize]) -> T {
let mut l = T::ZERO;
for &i in flips {
l = l.sub(self.a[i].scale(2.0).scale(f64::from(s[i])));
}
for (j, &t) in theta.iter().enumerate() {
let mut d = T::ZERO;
for &i in flips {
d = d.sub(self.w[j * self.n + i].scale(2.0).scale(f64::from(s[i])));
}
l = l.add(t.add(d).lncosh().sub(t.lncosh()));
}
let mut r = l.exp();
if let Some(sub) = &self.sublattice
&& flips.iter().filter(|&&i| sub[i]).count() % 2 == 1
{
r = r.scale(-1.0);
}
r
}
fn log_derivatives(&self, s: &[i8], theta: &[T], out: &mut [T]) {
let (n, m) = (self.n, self.m);
for (o, &si) in out[..n].iter_mut().zip(s) {
*o = T::ONE.scale(f64::from(si));
}
for (j, &th) in theta.iter().enumerate() {
let t = th.tanh();
out[n + j] = t;
for (o, &si) in out[n + m + j * n..n + m + (j + 1) * n].iter_mut().zip(s) {
*o = t.scale(f64::from(si));
}
}
}
fn shift(&mut self, delta: &[T], step: f64) {
let (n, m) = (self.n, self.m);
for (x, d) in self.a.iter_mut().zip(&delta[..n]) {
*x = x.sub(d.scale(step));
}
for (x, d) in self.b.iter_mut().zip(&delta[n..n + m]) {
*x = x.sub(d.scale(step));
}
for (x, d) in self.w.iter_mut().zip(&delta[n + m..]) {
*x = x.sub(d.scale(step));
}
}
}
fn local_energy<T: Amplitude>(model: &SpinModel, rbm: &Rbm<T>, s: &[i8], cache: &Cache<T>) -> T {
let mut e = T::ZERO;
for &(i, j, c) in &model.zz {
e = e.add(T::ONE.scale(c * f64::from(s[i]) * f64::from(s[j])));
}
for &(i, c) in &model.z {
e = e.add(T::ONE.scale(c * f64::from(s[i])));
}
for &(i, j, c) in &model.exchange {
if s[i] != s[j] {
e = e.add(rbm.ratio_cached(s, cache, &[i, j]).scale(2.0 * c));
}
}
for &(i, c) in &model.x {
e = e.add(rbm.ratio_cached(s, cache, &[i]).scale(c));
}
e
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VmcConfig {
pub samples: usize,
pub chains: usize,
pub burn: usize,
pub thin: usize,
pub exact: bool,
pub magnetisation: Option<i32>,
pub lr: f64,
pub shift: f64,
pub seed: u64,
pub threads: usize,
pub guide: f64,
}
impl Default for VmcConfig {
fn default() -> VmcConfig {
VmcConfig { samples: 2000, chains: 8, burn: 50, thin: 2, exact: false, magnetisation: None, lr: 0.05, shift: 1e-3, seed: 1, threads: default_threads(), guide: 0.0 }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Estimate {
pub energy: f64,
pub variance: f64,
pub error: f64,
pub acceptance: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Vmc<T = f64> {
pub model: SpinModel,
pub rbm: Rbm<T>,
pub cfg: VmcConfig,
chains: Vec<(Vec<i8>, Rng)>,
}
struct Batch<T> {
weights: Vec<f64>,
energies: Vec<T>,
derivs: Vec<T>,
acceptance: f64,
guided: bool,
}
#[derive(Clone, Copy, Debug)]
struct Guide {
c: f64,
ln_floor: f64,
}
impl Guide {
fn ln_q(self, l: f64) -> f64 {
let x = 2.0 * (l - self.c);
let (hi, lo) = if x > self.ln_floor { (x, self.ln_floor) } else { (self.ln_floor, x) };
hi + ln(1.0 + exp(lo - hi))
}
fn weight(self, l: f64) -> f64 {
exp(2.0 * (l - self.c) - self.ln_q(l))
}
}
impl<T: Amplitude> Vmc<T> {
pub fn new(model: SpinModel, rbm: Rbm<T>, cfg: VmcConfig) -> Vmc<T> {
assert_eq!(model.n, rbm.n, "the model and the machine differ in size");
if cfg.magnetisation.is_some() {
assert!(model.conserves_magnetisation(), "the model does not conserve the magnetisation");
}
let n = model.n;
let chains = (0..cfg.chains.max(1))
.map(|c| {
let mut rng = Rng(cfg.seed ^ (c as u64).wrapping_mul(0xa076_1d64_78bd_642f));
let mut s = start_configuration(n, cfg.magnetisation, &mut rng);
let mut draws = 1;
while !rbm.log_abs_cached(&s, &rbm.cache(&s)).is_finite() {
assert!(draws < 10_000, "the projection vanishes on every start drawn: an empty sector?");
s = start_configuration(n, cfg.magnetisation, &mut rng);
draws += 1;
}
(s, rng)
})
.collect();
let mut v = Vmc { model, rbm, cfg, chains };
if !cfg.exact {
for c in 0..v.chains.len() {
let (mut s, mut rng) = v.chains[c].clone();
metropolis(&v.rbm, &mut s, &mut rng, cfg.burn * n, cfg.magnetisation.is_some(), None);
v.chains[c] = (s, rng);
}
}
v
}
fn batch(&mut self) -> Batch<T> {
if self.cfg.exact { self.enumerate() } else { self.sample() }
}
fn enumerate(&self) -> Batch<T> {
let n = self.model.n;
assert!(n <= 24, "exact enumeration takes at most 24 spins");
let configs: Vec<usize> = (0..1usize << n).filter(|&x| self.cfg.magnetisation.is_none_or(|m| n as i32 - 2 * x.count_ones() as i32 == m)).collect();
let p = self.rbm.params();
let k = configs.len();
let mut logs = vec![0.0; k];
let mut energies = vec![T::ZERO; k];
let mut derivs = vec![T::ZERO; k * p];
let threads = self.cfg.threads.max(1).min(k.div_ceil(64)).max(1);
let per = k.div_ceil(threads);
let work = |range: core::ops::Range<usize>, logs: &mut [f64], energies: &mut [T], derivs: &mut [T]| {
let start = range.start;
for idx in range {
let x = configs[idx];
let s: Vec<i8> = (0..n).map(|i| if x >> (n - 1 - i) & 1 == 0 { 1 } else { -1 }).collect();
let cache = self.rbm.cache(&s);
let r = idx - start;
logs[r] = self.rbm.log_abs_cached(&s, &cache);
if logs[r] == f64::NEG_INFINITY {
continue;
}
energies[r] = local_energy(&self.model, &self.rbm, &s, &cache);
self.rbm.derivatives_cached(&s, &cache, &mut derivs[r * p..(r + 1) * p]);
}
};
if threads <= 1 || cfg!(target_arch = "wasm32") {
work(0..k, &mut logs, &mut energies, &mut derivs);
} else {
std::thread::scope(|scope| {
let mut lrest: &mut [f64] = &mut logs;
let mut erest: &mut [T] = &mut energies;
let mut drest: &mut [T] = &mut derivs;
for t in 0..threads {
let lo = t * per;
let hi = ((t + 1) * per).min(k);
if lo >= hi {
break;
}
let (l, lr) = lrest.split_at_mut(hi - lo);
let (e, er) = erest.split_at_mut(hi - lo);
let (d, dr) = drest.split_at_mut((hi - lo) * p);
lrest = lr;
erest = er;
drest = dr;
let work = &work;
scope.spawn(move || work(lo..hi, l, e, d));
}
});
}
let top = logs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let raw: Vec<f64> = logs.iter().map(|l| exp(2.0 * (l - top))).collect();
let z: f64 = raw.iter().sum();
Batch { weights: raw.iter().map(|r| r / z).collect(), energies, derivs, acceptance: 1.0, guided: false }
}
fn sample(&mut self) -> Batch<T> {
let n = self.model.n;
let p = self.rbm.params();
let nc = self.chains.len();
let per_chain = self.cfg.samples.div_ceil(nc).max(1);
let exchange = self.cfg.magnetisation.is_some();
let thin = self.cfg.thin.max(1) * n;
let rbm = &self.rbm;
let model = &self.model;
let guide = (self.cfg.guide > 0.0).then(|| {
let logs: Vec<f64> = self.chains.iter().map(|(s, _)| rbm.log_abs_cached(s, &rbm.cache(s))).filter(|l| l.is_finite()).collect();
Guide { c: logs.iter().sum::<f64>() / logs.len().max(1) as f64, ln_floor: ln(self.cfg.guide) }
});
let run = |chain: &mut (Vec<i8>, Rng)| -> (Vec<T>, Vec<T>, Vec<f64>, f64) {
let (s, rng) = chain;
let mut energies = Vec::with_capacity(per_chain);
let mut derivs = vec![T::ZERO; per_chain * p];
let mut weights = Vec::with_capacity(if guide.is_some() { per_chain } else { 0 });
let (mut accepted, mut proposed) = (0usize, 0usize);
for k in 0..per_chain {
let (a, m) = metropolis(rbm, s, rng, thin, exchange, guide);
accepted += a;
proposed += m;
let cache = rbm.cache(s);
if let Some(g) = guide {
weights.push(g.weight(rbm.log_abs_cached(s, &cache)));
}
energies.push(local_energy(model, rbm, s, &cache));
rbm.derivatives_cached(s, &cache, &mut derivs[k * p..(k + 1) * p]);
}
(energies, derivs, weights, accepted as f64 / proposed.max(1) as f64)
};
let threads = self.cfg.threads.max(1).min(nc);
let results: Vec<(Vec<T>, Vec<T>, Vec<f64>, f64)> = if threads <= 1 || cfg!(target_arch = "wasm32") {
self.chains.iter_mut().map(run).collect()
} else {
let per = nc.div_ceil(threads);
std::thread::scope(|scope| {
let handles: Vec<_> = self
.chains
.chunks_mut(per)
.map(|group| {
let run = &run;
scope.spawn(move || group.iter_mut().map(run).collect::<Vec<_>>())
})
.collect();
handles.into_iter().flat_map(|h| h.join().expect("a sampling thread panicked")).collect()
})
};
let total = per_chain * nc;
let mut energies = Vec::with_capacity(total);
let mut derivs = Vec::with_capacity(total * p);
let mut raw = Vec::with_capacity(if guide.is_some() { total } else { 0 });
let mut acc = 0.0;
for (e, d, w, a) in results {
energies.extend(e);
derivs.extend(d);
raw.extend(w);
acc += a;
}
let weights = if guide.is_some() {
let z: f64 = raw.iter().sum();
raw.iter().map(|w| w / z).collect()
} else {
vec![1.0 / total as f64; total]
};
Batch { weights, energies, derivs, acceptance: acc / nc as f64, guided: guide.is_some() }
}
pub fn estimate(&mut self) -> Estimate {
let b = self.batch();
summarise(&b, self.cfg.exact)
}
pub fn step(&mut self) -> Estimate {
let b = self.batch();
let est = summarise(&b, self.cfg.exact);
let delta = sr_direction(&b, self.rbm.params(), self.cfg.shift);
self.rbm.shift(&delta, self.cfg.lr);
est
}
}
fn summarise<T: Amplitude>(b: &Batch<T>, exact: bool) -> Estimate {
let energy: f64 = b.weights.iter().zip(&b.energies).map(|(w, e)| w * e.re()).sum();
let et = T::ONE.scale(energy);
let variance: f64 = b
.weights
.iter()
.zip(&b.energies)
.map(|(&w, e)| {
let d = e.sub(et);
d.scale(w).mul(d.conj()).re()
})
.sum();
let error = if exact {
0.0
} else if b.guided {
b.weights.iter().zip(&b.energies).map(|(&w, e)| w * w * e.sub(et).norm2()).sum::<f64>().sqrt()
} else {
(variance / b.energies.len() as f64).sqrt()
};
Estimate { energy, variance, error, acceptance: b.acceptance }
}
fn sr_direction<T: Amplitude>(b: &Batch<T>, p: usize, shift: f64) -> Vec<T> {
let k = b.weights.len();
let energy: f64 = b.weights.iter().zip(&b.energies).map(|(w, e)| w * e.re()).sum();
let et = T::ONE.scale(energy);
let mut mean = vec![T::ZERO; p];
for (r, &w) in b.weights.iter().enumerate() {
for (m, &d) in mean.iter_mut().zip(&b.derivs[r * p..(r + 1) * p]) {
*m = m.add(d.scale(w));
}
}
let mut o = vec![T::ZERO; k * p];
let mut eps = vec![T::ZERO; k];
for r in 0..k {
let sw = b.weights[r].sqrt();
for c in 0..p {
o[r * p + c] = b.derivs[r * p + c].sub(mean[c]).scale(sw);
}
eps[r] = b.energies[r].sub(et).scale(sw);
}
if p <= k { sr_parameter_space(&o, &eps, k, p, shift) } else { sr_sample_space(&o, eps, k, p, shift) }
}
fn sr_parameter_space<T: Amplitude>(o: &[T], eps: &[T], k: usize, p: usize, shift: f64) -> Vec<T> {
let mut s = vec![T::ZERO; p * p];
for r in 0..k {
let row = &o[r * p..(r + 1) * p];
for i in 0..p {
let oi = row[i].conj();
if oi.is_zero() {
continue;
}
for j in i..p {
s[i * p + j] = s[i * p + j].add(oi.mul(row[j]));
}
}
}
for i in 0..p {
s[i * p + i] = s[i * p + i].add(T::ONE.scale(shift));
for j in 0..i {
s[i * p + j] = s[j * p + i].conj();
}
}
let mut f = vec![T::ZERO; p];
for r in 0..k {
for c in 0..p {
f[c] = f[c].add(o[r * p + c].conj().mul(eps[r]));
}
}
hermitian_solve(&mut s, &mut f, p);
f
}
fn sr_sample_space<T: Amplitude>(o: &[T], eps: Vec<T>, k: usize, p: usize, shift: f64) -> Vec<T> {
let mut t = vec![T::ZERO; k * k];
for i in 0..k {
for j in i..k {
let mut acc = T::ZERO;
for c in 0..p {
acc = acc.add(o[i * p + c].mul(o[j * p + c].conj()));
}
t[i * k + j] = acc;
t[j * k + i] = acc.conj();
}
t[i * k + i] = t[i * k + i].add(T::ONE.scale(shift));
}
let mut x = eps;
hermitian_solve(&mut t, &mut x, k);
let mut delta = vec![T::ZERO; p];
for r in 0..k {
for c in 0..p {
delta[c] = delta[c].add(o[r * p + c].conj().mul(x[r]));
}
}
delta
}
fn hermitian_solve<T: Amplitude>(a: &mut [T], b: &mut [T], n: usize) {
let (a0, b0) = (a.to_vec(), b.to_vec());
if cholesky_solve(a, b, n) {
return;
}
let (vals, vecs) = T::eigh(&a0, n);
let top = vals.iter().fold(0.0f64, |m, v| m.max(v.abs()));
for x in b.iter_mut() {
*x = T::ZERO;
}
for (i, &lambda) in vals.iter().enumerate() {
if lambda <= 1e-12 * top {
continue;
}
let mut c = T::ZERO;
for r in 0..n {
c = c.add(vecs[r * n + i].conj().mul(b0[r]));
}
let c = c.div_re(lambda);
for r in 0..n {
b[r] = b[r].add(vecs[r * n + i].mul(c));
}
}
}
#[allow(clippy::needless_range_loop)]
fn cholesky_solve<T: Amplitude>(a: &mut [T], b: &mut [T], n: usize) -> bool {
for j in 0..n {
let diag = a[j * n + j].re();
let mut d = diag;
for k in 0..j {
d -= a[j * n + k].norm2();
}
if !d.is_finite() || d <= 1e-14 * diag.abs() {
return false;
}
let d = d.max(1e-300).sqrt();
a[j * n + j] = T::ONE.scale(d);
for i in j + 1..n {
let mut s = a[i * n + j];
for k in 0..j {
s = s.sub(a[i * n + k].mul(a[j * n + k].conj()));
}
a[i * n + j] = s.div_re(d);
}
}
for i in 0..n {
let mut s = b[i];
for k in 0..i {
s = s.sub(a[i * n + k].mul(b[k]));
}
b[i] = s.div_re(a[i * n + i].re());
}
for i in (0..n).rev() {
let mut s = b[i];
for k in i + 1..n {
s = s.sub(a[k * n + i].conj().mul(b[k]));
}
b[i] = s.div_re(a[i * n + i].re());
}
b.iter().all(|x| x.re().is_finite() && x.norm2().is_finite())
}
fn start_configuration(n: usize, magnetisation: Option<i32>, rng: &mut Rng) -> Vec<i8> {
match magnetisation {
None => (0..n).map(|_| if rng.next() & 1 == 0 { 1 } else { -1 }).collect(),
Some(m) => {
let ups = ((n as i32 + m) / 2).clamp(0, n as i32) as usize;
let mut s: Vec<i8> = (0..n).map(|i| if i < ups { 1 } else { -1 }).collect();
for i in (1..n).rev() {
let j = rng.below(i + 1);
s.swap(i, j);
}
s
}
}
}
fn metropolis<T: Amplitude>(rbm: &Rbm<T>, s: &mut [i8], rng: &mut Rng, moves: usize, exchange: bool, guide: Option<Guide>) -> (usize, usize) {
let n = s.len();
let mut cache = rbm.cache(s);
let (mut accepted, mut proposed) = (0, 0);
let mut l = if guide.is_some() { rbm.log_abs_cached(s, &cache) } else { 0.0 };
for _ in 0..moves {
let flips: Vec<usize> = if exchange {
let i = rng.below(n);
let j = rng.below(n);
if s[i] == s[j] {
continue;
}
vec![i, j]
} else {
vec![rng.below(n)]
};
proposed += 1;
let r = rbm.ratio_cached(s, &cache, &flips);
let r2 = r.norm2();
let p = match guide {
None => r2,
Some(_) if r2 < 1e-20 => 0.0,
Some(g) => exp(g.ln_q(l + 0.5 * ln(r2)) - g.ln_q(l)),
};
if rng.uniform() < p {
rbm.accept(s, &mut cache, &flips, r);
accepted += 1;
if guide.is_some() {
l += 0.5 * ln(r2);
}
}
}
(accepted, proposed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum_dmrg::{Chain, exact_ground_energy};
#[test]
fn exact_diagonalisation_matches_the_chain_referees() {
let n = 10;
let e = chain_edges(n, false);
let a = SpinModel::heisenberg(n, &e, 1.0).exact_ground_energy(Some(0)).unwrap();
let b = exact_ground_energy(&Chain::heisenberg(n, 1.0, 1.0, 0.0)).unwrap();
assert!((a - b).abs() < 1e-9, "{a} vs {b}");
let a = SpinModel::ising(n, &e, 1.0, 0.7).exact_ground_energy(None).unwrap();
let b = exact_ground_energy(&Chain::ising(n, 1.0, 0.7)).unwrap();
assert!((a - b).abs() < 1e-9, "{a} vs {b}");
}
#[test]
fn the_operator_sum_is_the_model() {
let n = 9;
let mut m = SpinModel::heisenberg(n, &square_edges(3, 3, false), 0.8);
m.x.push((4, 0.3));
m.z.push((2, -0.45));
m.zz.push((0, 8, 0.2));
let dense = m.opsum().mpo().to_dense().unwrap();
let dim = 1usize << n;
let (states, place) = m.sector_states(None);
let mut col = vec![0.0; dim];
let mut out = vec![0.0; dim];
for c in 0..dim {
col.iter_mut().for_each(|x| *x = 0.0);
col[c] = 1.0;
m.apply_sector(&states, &place, &col, &mut out);
for r in 0..dim {
assert!((out[r] - dense[r * dim + c]).abs() < 1e-12, "({r}, {c})");
}
}
}
#[test]
fn ratios_and_derivatives_are_the_machines() {
let n = 7;
let mut rbm = Rbm::new(n, 2, 0.4, 3).with_marshall_sign(checkerboard(n, 1));
let mut rng = Rng(5);
for v in rbm.a.iter_mut().chain(rbm.b.iter_mut()) {
*v = 0.3 * (rng.uniform() - 0.5);
}
for _ in 0..20 {
let s = start_configuration(n, None, &mut rng);
let theta = rbm.theta(&s);
for flips in [vec![rng.below(n)], vec![0, 3], vec![2, 6]] {
let mut t = s.clone();
for &i in &flips {
t[i] = -t[i];
}
let want = rbm.sign(&t) * rbm.sign(&s) * exp(rbm.log_amplitude(&t) - rbm.log_amplitude(&s));
let got = rbm.ratio(&s, &theta, &flips);
assert!((got - want).abs() < 1e-12 * want.abs().max(1.0), "{got} vs {want}");
}
let p = rbm.params();
let mut d = vec![0.0; p];
rbm.log_derivatives(&s, &theta, &mut d);
let h = 1e-6;
for k in [0, n, n + 3, n + rbm.m + 5, p - 1] {
let mut e = vec![0.0; p];
e[k] = -h;
let mut up = rbm.clone();
up.shift(&e, 1.0);
e[k] = h;
let mut dn = rbm.clone();
dn.shift(&e, 1.0);
let fd = (up.log_amplitude(&s) - dn.log_amplitude(&s)) / (2.0 * h);
assert!((fd - d[k]).abs() < 1e-7, "param {k}: {fd} vs {}", d[k]);
}
}
}
#[test]
fn both_spaces_take_the_same_step() {
let mut rng = Rng(9);
for (k, p) in [(12usize, 5usize), (5, 12)] {
let o: Vec<f64> = (0..k * p).map(|_| rng.uniform() - 0.5).collect();
let eps: Vec<f64> = (0..k).map(|_| rng.uniform() - 0.5).collect();
let a = sr_parameter_space(&o, &eps, k, p, 1e-3);
let b = sr_sample_space(&o, eps.clone(), k, p, 1e-3);
for (x, y) in a.iter().zip(&b) {
assert!((x - y).abs() < 1e-9 * x.abs().max(1.0), "{x} vs {y}");
}
}
}
#[test]
fn exact_vmc_finds_the_ground_state() {
let n = 8;
let model = SpinModel::heisenberg(n, &chain_edges(n, false), 1.0);
let exact = model.exact_ground_energy(Some(0)).unwrap();
let rbm = Rbm::new(n, 2, 0.05, 1).with_marshall_sign(checkerboard(n, 1));
let cfg = VmcConfig { exact: true, magnetisation: Some(0), lr: 0.1, shift: 1e-4, ..VmcConfig::default() };
let mut vmc = Vmc::new(model, rbm, cfg);
let mut last = vmc.step();
for _ in 0..300 {
let e = vmc.step();
last = e;
}
let rel = (last.energy - exact) / exact.abs();
assert!(last.energy >= exact - 1e-9, "variational: {} vs {exact}", last.energy);
assert!(rel < 1e-3, "{} vs {exact} (relative {rel:e})", last.energy);
assert!(last.variance < 1e-2, "{}", last.variance);
}
#[test]
fn sampling_agrees_with_enumeration_and_repeats() {
let n = 8;
let model = SpinModel::ising(n, &chain_edges(n, false), 1.0, 1.0);
let rbm = Rbm::new(n, 1, 0.3, 4);
let exact = Vmc::new(model.clone(), rbm.clone(), VmcConfig { exact: true, ..VmcConfig::default() }).estimate();
let cfg = VmcConfig { samples: 8000, chains: 8, threads: 1, ..VmcConfig::default() };
let sampled = Vmc::new(model.clone(), rbm.clone(), cfg).estimate();
assert!((sampled.energy - exact.energy).abs() < 5.0 * sampled.error, "{sampled:?} vs {exact:?}");
let mut one = Vmc::new(model.clone(), rbm.clone(), VmcConfig { samples: 400, threads: 1, ..VmcConfig::default() });
let mut many = Vmc::new(model, rbm, VmcConfig { samples: 400, threads: 3, ..VmcConfig::default() });
for _ in 0..3 {
assert_eq!(one.step(), many.step());
}
assert_eq!(one.rbm, many.rbm);
assert_eq!(one.chains, many.chains);
}
#[test]
fn a_complex_machine_with_real_parameters_is_the_real_one() {
let n = 6;
let real = Rbm::new(n, 2, 0.4, 5);
let cplx = Rbm { n: real.n, m: real.m, a: real.a.iter().map(|&re| C { re, im: 0.0 }).collect(), b: real.b.iter().map(|&re| C { re, im: 0.0 }).collect(), w: real.w.iter().map(|&re| C { re, im: 0.0 }).collect(), sublattice: None, symmetry: None };
let mut rng = Rng(8);
for _ in 0..10 {
let s = start_configuration(n, None, &mut rng);
let (tr, tc) = (real.theta(&s), cplx.theta(&s));
assert!((real.log_amplitude(&s) - cplx.log_amplitude(&s)).abs() < 1e-14);
assert!(cplx.log_psi(&s).im.abs() < 1e-14);
for flips in [vec![1], vec![0, 4]] {
let (r, c) = (real.ratio(&s, &tr, &flips), cplx.ratio(&s, &tc, &flips));
assert!((r - c.re).abs() < 1e-13 * r.abs().max(1.0) && c.im.abs() < 1e-13);
}
}
}
#[test]
fn complex_ratios_and_derivatives_are_the_machines() {
let n = 6;
let mut rbm = Rbm::complex(n, 2, 0.4, 6);
let mut rng = Rng(12);
for v in rbm.a.iter_mut().chain(rbm.b.iter_mut()) {
*v = C { re: 0.3 * (rng.uniform() - 0.5), im: 0.3 * (rng.uniform() - 0.5) };
}
let p = rbm.params();
for _ in 0..10 {
let s = start_configuration(n, None, &mut rng);
let theta = rbm.theta(&s);
for flips in [vec![rng.below(n)], vec![1, 5]] {
let mut t = s.clone();
for &i in &flips {
t[i] = -t[i];
}
let want = rbm.log_psi(&t).sub(rbm.log_psi(&s)).exp();
let got = rbm.ratio(&s, &theta, &flips);
assert!(got.sub(want).norm2().sqrt() < 1e-12 * want.norm2().sqrt().max(1.0), "{got:?} vs {want:?}");
}
let mut d = vec![C::ZERO; p];
rbm.log_derivatives(&s, &theta, &mut d);
let h = 1e-6;
for k in [0, n + 1, n + rbm.m + 3, p - 1] {
let mut e = vec![C::ZERO; p];
e[k] = C { re: -h, im: 0.0 };
let mut up = rbm.clone();
up.shift(&e, 1.0);
e[k] = C { re: h, im: 0.0 };
let mut dn = rbm.clone();
dn.shift(&e, 1.0);
let fd = up.log_psi(&s).sub(dn.log_psi(&s)).scale(0.5 / h);
assert!(fd.sub(d[k]).norm2().sqrt() < 1e-7, "param {k}: {fd:?} vs {:?}", d[k]);
}
}
}
#[test]
fn both_spaces_take_the_same_complex_step() {
let mut rng = Rng(19);
for (k, p) in [(12usize, 5usize), (5, 12)] {
let o: Vec<C> = (0..k * p).map(|_| C { re: rng.uniform() - 0.5, im: rng.uniform() - 0.5 }).collect();
let eps: Vec<C> = (0..k).map(|_| C { re: rng.uniform() - 0.5, im: rng.uniform() - 0.5 }).collect();
let a = sr_parameter_space(&o, &eps, k, p, 1e-3);
let b = sr_sample_space(&o, eps.clone(), k, p, 1e-3);
for (x, y) in a.iter().zip(&b) {
assert!(x.sub(*y).norm2().sqrt() < 1e-9 * x.norm2().sqrt().max(1.0), "{x:?} vs {y:?}");
}
}
}
#[test]
fn exact_diagonalisation_solves_majumdar_ghosh() {
for n in [8usize, 12] {
let m = SpinModel::j1j2(n, &chain_edges(n, true), &chain_next_nearest(n, true), 1.0, 0.5);
let e = m.exact_ground_energy(Some(0)).unwrap();
assert!((e + 3.0 * n as f64 / 8.0).abs() < 1e-9, "n={n}: {e}");
}
}
#[test]
fn a_complex_machine_learns_a_sign_a_real_one_cannot() {
let n = 8;
let models = [
SpinModel::heisenberg(n, &chain_edges(n, true), 1.0),
SpinModel::j1j2(n, &chain_edges(n, true), &chain_next_nearest(n, true), 1.0, 0.5),
];
for model in models {
let exact = model.exact_ground_energy(Some(0)).unwrap();
let cfg = VmcConfig { exact: true, magnetisation: Some(0), lr: 0.1, shift: 1e-3, ..VmcConfig::default() };
let mut real = Vmc::new(model.clone(), Rbm::new(n, 2, 0.05, 1), cfg);
let mut cplx = Vmc::new(model, Rbm::complex(n, 2, 0.05, 3), cfg);
let (mut er, mut ec) = (real.step(), cplx.step());
for _ in 0..250 {
er = real.step();
ec = cplx.step();
}
assert!((er.energy - exact) / exact.abs() > 0.1, "real: {} vs {exact}", er.energy);
assert!(ec.energy >= exact - 1e-9, "variational: {} vs {exact}", ec.energy);
assert!((ec.energy - exact) / exact.abs() < 2e-3, "complex: {} vs {exact}", ec.energy);
}
}
fn projected<T: Amplitude>(rbm: &Rbm<T>, s: &[i8]) -> T {
let sym = rbm.symmetry.as_ref().unwrap();
let mut total = T::ZERO;
for g in 0..sym.len() {
let t: Vec<i8> = sym.perms[g].iter().map(|&j| if sym.flips[g] { -s[j] } else { s[j] }).collect();
total = total.add(rbm.log_psi(&t).exp().scale(sym.chars[g] * rbm.sign(&t)));
}
total
}
#[test]
fn the_projection_is_the_group_sum() {
let n = 6;
for odd in [false, true] {
let rbm = Rbm::complex(n, 1, 0.3, 4).with_symmetry(Symmetry::ring(n, true).with_spin_flip(odd));
let mut rng = Rng(3);
for _ in 0..12 {
let s = start_configuration(n, Some(0), &mut rng);
let base = projected(&rbm, &s);
if base.norm2() < 1e-20 {
continue;
}
let cache = rbm.cache(&s);
for flips in [vec![0usize, 1], vec![2, 5], vec![3]] {
let mut t = s.clone();
for &i in &flips {
t[i] = -t[i];
}
let want = projected(&rbm, &t).div(base);
let got = rbm.ratio_cached(&s, &cache, &flips);
assert!(got.sub(want).norm2().sqrt() < 1e-10 * want.norm2().sqrt().max(1.0), "{got:?} vs {want:?}");
}
let shifted: Vec<i8> = (0..n).map(|i| s[(i + 1) % n]).collect();
let flipped: Vec<i8> = s.iter().map(|&x| -x).collect();
assert!(projected(&rbm, &shifted).add(base).norm2().sqrt() < 1e-10 * base.norm2().sqrt());
let sign = if odd { -1.0 } else { 1.0 };
assert!(projected(&rbm, &flipped).sub(base.scale(sign)).norm2().sqrt() < 1e-10 * base.norm2().sqrt());
let p = rbm.params();
let mut d = vec![C::ZERO; p];
rbm.derivatives_cached(&s, &cache, &mut d);
let h = 1e-6;
for k in [0, n + 1, p - 1] {
let mut e = vec![C::ZERO; p];
e[k] = C { re: -h, im: 0.0 };
let mut up = rbm.clone();
up.shift(&e, 1.0);
e[k] = C { re: h, im: 0.0 };
let mut dn = rbm.clone();
dn.shift(&e, 1.0);
let fd = projected(&up, &s).sub(projected(&dn, &s)).div(base).scale(0.5 / h);
assert!(fd.sub(d[k]).norm2().sqrt() < 1e-6, "param {k}: {fd:?} vs {:?}", d[k]);
}
}
}
}
#[test]
fn accepted_moves_keep_the_cache_exact() {
let n = 8;
let rbm = Rbm::complex(n, 1, 0.3, 9).with_symmetry(Symmetry::ring(n, false).with_spin_flip(false));
let mut rng = Rng(21);
let mut s = start_configuration(n, Some(0), &mut rng);
let mut cache = rbm.cache(&s);
for _ in 0..200 {
let (i, j) = (rng.below(n), rng.below(n));
if s[i] == s[j] {
continue;
}
let r = rbm.ratio_cached(&s, &cache, &[i, j]);
rbm.accept(&mut s, &mut cache, &[i, j], r);
}
let fresh = rbm.cache(&s);
match (&cache, &fresh) {
(Cache::Projected { configs: c1, shares: p1, log_abs: l1, thetas: t1 }, Cache::Projected { configs: c2, shares: p2, log_abs: l2, thetas: t2 }) => {
assert_eq!(c1, c2);
assert!((l1 - l2).abs() < 1e-9, "{l1} vs {l2}");
for (a, b) in p1.iter().zip(p2) {
assert!(a.sub(*b).norm2().sqrt() < 1e-9, "{a:?} vs {b:?}");
}
for (a, b) in t1.iter().flatten().zip(t2.iter().flatten()) {
assert!(a.sub(*b).norm2().sqrt() < 1e-12);
}
}
_ => unreachable!(),
}
}
#[test]
fn projection_reaches_the_ground_state_in_its_sector() {
let n = 6;
let model = SpinModel::heisenberg(n, &chain_edges(n, true), 1.0);
let exact = model.exact_ground_energy(Some(0)).unwrap();
let cfg = VmcConfig { exact: true, magnetisation: Some(0), lr: 0.1, shift: 1e-3, ..VmcConfig::default() };
let rbm = Rbm::complex(n, 1, 0.05, 3).with_symmetry(Symmetry::ring(n, true).with_spin_flip(true));
let mut v = Vmc::new(model, rbm, cfg);
let mut e = v.step();
for _ in 0..150 {
e = v.step();
}
assert!(e.energy >= exact - 1e-9, "variational: {} vs {exact}", e.energy);
assert!((e.energy - exact) / exact.abs() < 1e-8, "{} vs {exact}", e.energy);
}
#[test]
fn square_groups_are_groups_with_characters() {
for l in [3, 4] {
for momentum_pi in [false, true] {
if momentum_pi && l % 2 == 1 {
continue;
}
for irrep in [PointIrrep::A1, PointIrrep::A2, PointIrrep::B1, PointIrrep::B2] {
let sym = Symmetry::square(l, momentum_pi, irrep);
assert_eq!(sym.len(), 8 * l * l);
assert_eq!(sym.check(), Ok(()), "{l} {momentum_pi} {irrep:?}");
}
for odd in [false, true] {
assert_eq!(Symmetry::square(l, momentum_pi, PointIrrep::B2).with_spin_flip(odd).check(), Ok(()));
}
}
}
for (lx, ly) in [(4, 3), (6, 4)] {
for (kx, ky) in [(false, false), (true, false), (false, true), (true, true)] {
if (kx && lx % 2 == 1) || (ky && ly % 2 == 1) {
continue;
}
for irrep in [RectIrrep::A1, RectIrrep::A2, RectIrrep::B1, RectIrrep::B2] {
let sym = Symmetry::rectangle(lx, ly, kx, ky, irrep);
assert_eq!(sym.len(), 4 * lx * ly);
assert_eq!(sym.check(), Ok(()), "{lx}×{ly} {kx} {ky} {irrep:?}");
}
}
}
let ring = Symmetry::ring(4, false);
assert!(Symmetry::new(ring.perms.clone(), vec![1.0, -1.0, 1.0, 1.0]).check().is_err());
assert!(Symmetry::new(ring.perms[..2].to_vec(), vec![1.0, 1.0]).check().is_err());
assert!(Symmetry::new(ring.perms.clone(), vec![1.0, 0.5, 1.0, 0.5]).check().is_err());
}
#[test]
fn a_square_projection_carries_its_representation() {
let (l, n) = (4, 16);
let quarter_turn = |x: usize, y: usize| ((l - y) % l, x);
let axis = |x: usize, y: usize| ((l - x) % l, y);
let diagonal = |x: usize, y: usize| (y, x);
let step = |x: usize, y: usize| ((x + 1) % l, y);
for irrep in [PointIrrep::A1, PointIrrep::A2, PointIrrep::B1, PointIrrep::B2] {
let rbm = Rbm::complex(n, 1, 0.3, 11).with_symmetry(Symmetry::square(l, true, irrep).with_spin_flip(false));
let mut rng = Rng(5);
let mut seen = 0;
for _ in 0..40 {
if seen == 6 {
break;
}
let s = start_configuration(n, Some(0), &mut rng);
let base = projected(&rbm, &s);
if base.norm2() < 1e-16 {
continue;
}
seen += 1;
let moved = |f: &dyn Fn(usize, usize) -> (usize, usize)| -> Vec<i8> {
(0..n)
.map(|i| {
let (x, y) = f(i % l, i / l);
s[y * l + x]
})
.collect()
};
let want = [(irrep.character(1, false), moved(&quarter_turn)), (irrep.character(0, true), moved(&axis)), (irrep.character(1, true), moved(&diagonal)), (-1.0, moved(&step))];
for (chi, t) in want {
let got = projected(&rbm, &t);
assert!(got.sub(base.scale(chi)).norm2().sqrt() < 1e-10 * base.norm2().sqrt(), "{irrep:?}: {got:?} vs {chi} × {base:?}");
}
}
assert_eq!(seen, 6, "{irrep:?}: too few configurations survive the projection");
}
}
#[test]
fn a_projected_start_near_zero_sits_on_the_ferromagnet() {
let n = 8;
let model = SpinModel::heisenberg(n, &chain_edges(n, true), 1.0);
let exact = model.exact_ground_energy(Some(0)).unwrap();
let ferromagnet = n as f64 / 4.0;
let cfg = VmcConfig { exact: true, magnetisation: Some(0), lr: 0.02, shift: 1e-3, ..VmcConfig::default() };
let run = |init: f64, steps: usize| {
let rbm = Rbm::complex(n, 1, init, 7).with_symmetry(Symmetry::ring(n, false).with_spin_flip(false));
let mut v = Vmc::new(model.clone(), rbm, cfg);
let first = v.estimate();
for _ in 0..steps {
v.step();
}
(first, v.estimate())
};
let (first, after) = run(0.01, 40);
assert!((first.energy - ferromagnet).abs() < 1e-3 && first.variance < 1e-3, "{first:?}");
assert!((after.energy - ferromagnet).abs() < 1e-2, "{after:?}");
let (_, after) = run(0.1, 200);
assert!((after.energy - exact) / exact.abs() < 1e-3, "{} vs {exact}", after.energy);
}
#[test]
fn guided_sampling_agrees_with_enumeration() {
let n = 8;
let model = SpinModel::heisenberg(n, &chain_edges(n, true), 1.0);
let rbm = Rbm::complex(n, 1, 0.3, 5).with_symmetry(Symmetry::ring(n, false).with_spin_flip(false));
let exact = Vmc::new(model.clone(), rbm.clone(), VmcConfig { exact: true, magnetisation: Some(0), ..VmcConfig::default() }).estimate();
let cfg = VmcConfig { samples: 4000, chains: 8, magnetisation: Some(0), guide: 0.5, threads: 1, ..VmcConfig::default() };
let g = Vmc::new(model.clone(), rbm.clone(), cfg).estimate();
assert!((g.energy - exact.energy).abs() < 5.0 * g.error, "{g:?} vs {exact:?}");
assert!((g.variance - exact.variance).abs() < 0.2 * exact.variance, "{g:?} vs {exact:?}");
let cfg = |threads| VmcConfig { samples: 200, magnetisation: Some(0), guide: 0.5, threads, ..VmcConfig::default() };
let mut one = Vmc::new(model.clone(), rbm.clone(), cfg(1));
let mut many = Vmc::new(model, rbm, cfg(3));
for _ in 0..2 {
assert_eq!(one.step(), many.step());
}
assert_eq!(one.rbm, many.rbm);
}
#[test]
fn no_chain_starts_where_the_projection_vanishes() {
let (l, n) = (4, 16);
let model = SpinModel::heisenberg(n, &square_edges(l, l, true), 1.0);
let rbm = Rbm::complex(n, 1, 0.1, 3).with_symmetry(Symmetry::square(l, true, PointIrrep::A1).with_spin_flip(false));
let cfg = VmcConfig { chains: 32, burn: 1, magnetisation: Some(0), threads: 1, ..VmcConfig::default() };
let v = Vmc::new(model, rbm, cfg);
for (c, (s, _)) in v.chains.iter().enumerate() {
assert!(v.rbm.log_abs_cached(s, &v.rbm.cache(s)).is_finite(), "chain {c} sits where the projection vanishes: {s:?}");
}
}
#[test]
fn sector_diagonalisation_splits_the_spectrum() {
let n = 10;
let model = SpinModel::heisenberg(n, &chain_edges(n, true), 1.0);
let all = model.exact_ground_energy(Some(0)).unwrap();
let ground = model.exact_ground_energy_in(Some(0), &Symmetry::ring(n, true).with_spin_flip(true)).unwrap();
let other = model.exact_ground_energy_in(Some(0), &Symmetry::ring(n, false).with_spin_flip(false)).unwrap();
assert!((ground - all).abs() < 1e-9, "{ground} vs {all}");
assert!(other > all + 0.1, "{other} vs {all}");
let open = SpinModel::heisenberg(n, &chain_edges(n, false), 1.0);
assert_eq!(open.exact_ground_energy_in(Some(0), &Symmetry::ring(n, false)), None);
assert_eq!(model.exact_ground_energy_in(Some(2), &Symmetry::ring(n, false).with_spin_flip(false)), None);
let mut field = model.clone();
field.z = (0..n).map(|i| (i, 0.1)).collect();
assert!(field.commutes_with(&Symmetry::ring(n, false)));
assert!(!field.commutes_with(&Symmetry::ring(n, false).with_spin_flip(false)));
}
#[test]
fn the_orbit_basis_agrees_with_the_projector() {
let close = |a: Option<f64>, b: Option<f64>, what: &str| match (a, b) {
(Some(x), Some(y)) => assert!((x - y).abs() < 1e-9 * y.abs().max(1.0), "{what}: {x} vs {y}"),
(x, y) => assert_eq!(x.is_some(), y.is_some(), "{what}: {x:?} vs {y:?}"),
};
let (lx, ly, n) = (4, 3, 12);
let model = SpinModel::j1j2(n, &square_edges(lx, ly, true), &square_diagonals(lx, ly, true), 1.0, 0.5);
for kx in [false, true] {
for irrep in [RectIrrep::A1, RectIrrep::A2, RectIrrep::B1, RectIrrep::B2] {
let base = Symmetry::rectangle(lx, ly, kx, false, irrep);
for sym in [base.clone(), base.clone().with_spin_flip(false), base.with_spin_flip(true)] {
close(model.exact_ground_energy_in(Some(0), &sym), model.projected_ground_energy(Some(0), &sym), &format!("4×3 {kx} {irrep:?} {}", sym.len()));
}
}
}
let ring = SpinModel::heisenberg(10, &chain_edges(10, true), 1.0);
let ising = SpinModel::ising(8, &chain_edges(8, true), 1.0, 0.7);
for k in [false, true] {
for odd in [false, true] {
let sym = Symmetry::ring(10, k).with_spin_flip(odd);
close(ring.exact_ground_energy_in(Some(0), &sym), ring.projected_ground_energy(Some(0), &sym), "ring");
let sym = Symmetry::ring(8, k).with_spin_flip(odd);
close(ising.exact_ground_energy_in(None, &sym), ising.projected_ground_energy(None, &sym), "ising");
}
}
let identity = Symmetry::new(vec![(0..n).collect()], vec![1.0]);
let all = model.exact_ground_energy(Some(0)).unwrap();
close(model.exact_ground_energy_in(Some(0), &identity), Some(all), "identity");
let even = model.exact_ground_energy_in(Some(0), &identity.clone().with_spin_flip(false)).unwrap();
let odd = model.exact_ground_energy_in(Some(0), &identity.with_spin_flip(true)).unwrap();
assert!((even.min(odd) - all).abs() < 1e-9 && even.max(odd) > all + 1e-6, "{even} {odd} {all}");
}
#[test]
fn the_frustrated_square_ground_state_is_fully_symmetric() {
let (l, n) = (4, 16);
let model = SpinModel::j1j2(n, &square_edges(l, l, true), &square_diagonals(l, l, true), 1.0, 0.5);
let all = model.exact_ground_energy(Some(0)).unwrap();
let sector = |momentum_pi, irrep| model.exact_ground_energy_in(Some(0), &Symmetry::square(l, momentum_pi, irrep)).unwrap();
let a1 = sector(false, PointIrrep::A1);
assert!((a1 - all).abs() < 1e-9, "{a1} vs {all}");
let b1 = sector(false, PointIrrep::B1);
assert!(b1 > all + 0.1, "{b1} vs {all}");
}
}