wai-quantum 0.3.40: rectangle space groups, a sector referee on its own basis, and a sector larger than the network #194
9 changed files with 370 additions and 81 deletions
2
wai/wai-pack/Cargo.lock
generated
2
wai/wai-pack/Cargo.lock
generated
|
|
@ -682,7 +682,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -589,7 +589,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@
|
|||
//! --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] [--odd]] [--dmrg-bond D]
|
||||
//! [--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, and onto even
|
||||
//! 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
|
||||
|
|
@ -66,10 +69,45 @@ fn main() {
|
|||
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();
|
||||
let (reference, how) = match if n <= 20 { model.exact_ground_energy(sector) } else { None } {
|
||||
Some(e) => (e, "exact diagonalisation".to_string()),
|
||||
None => {
|
||||
// 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))
|
||||
|
|
@ -81,35 +119,6 @@ fn main() {
|
|||
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");
|
||||
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 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 {
|
||||
Some(irrep) => {
|
||||
assert_eq!(lx, ly, "--point-group needs a square lattice");
|
||||
Symmetry::square(lx, kpi, irrep)
|
||||
}
|
||||
None if ly == 1 => Symmetry::ring(n, kpi),
|
||||
None => Symmetry::torus(lx, ly, kpi, kpi),
|
||||
};
|
||||
base.with_spin_flip(a.iter().any(|x| x == "--odd"))
|
||||
});
|
||||
// A projected network is held to its own sector's ground energy.
|
||||
let reference = match &symmetry {
|
||||
Some(sym) if n <= 20 => {
|
||||
let e = model.exact_ground_energy_in(sector, sym).expect("the model is invariant under the group, and the sector is not empty");
|
||||
println!("in its sector: {e:.8}");
|
||||
e
|
||||
}
|
||||
_ => reference,
|
||||
};
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
//! π along each direction), on a square lattice its whole space group
|
||||
//! ([`Symmetry::square`]: the translations with the eight rotations and
|
||||
//! reflections, in one of their four one-dimensional representations,
|
||||
//! [`PointIrrep`]), and, optionally, the flip of every spin (even or odd).
|
||||
//! [`PointIrrep`]), on any lattice the translations with its half turn and
|
||||
//! axis reflections ([`Symmetry::rectangle`], [`RectIrrep`]), and,
|
||||
//! optionally, the flip of every spin (even or odd).
|
||||
//! [`Symmetry::check`] proves the elements a group and the characters
|
||||
//! multiplicative, without which the sum projects onto nothing in
|
||||
//! particular. Ratios and log-derivatives become weighted averages over the
|
||||
|
|
@ -46,10 +48,10 @@
|
|||
//! - **Referees.** [`SpinModel::exact_ground_energy`] is Lanczos on the
|
||||
//! amplitudes of a magnetisation sector, or all `2ⁿ` (at most 22 spins).
|
||||
//! [`SpinModel::exact_ground_energy_in`] is the same within one symmetry
|
||||
//! sector: Lanczos on `P H P`, re-projected after every product, so a
|
||||
//! projected network is held to its own sector's answer. It refuses a
|
||||
//! group that is not one and a model the group does not preserve
|
||||
//! ([`SpinModel::commutes_with`]). [`SpinModel::opsum`] gives the model to
|
||||
//! sector, on the sector's own basis (one state per orbit of the group,
|
||||
//! up to 24 spins), so a projected network is held to its own sector's
|
||||
//! answer. It refuses a group that is not one and a model the group does
|
||||
//! not preserve ([`SpinModel::commutes_with`]). [`SpinModel::opsum`] gives the model to
|
||||
//! DMRG on its exact MPO, for lattices past exact diagonalisation.
|
||||
//!
|
||||
//! Chains draw from their own seeded generators, and every sum runs in a
|
||||
|
|
@ -94,6 +96,15 @@
|
|||
//! starts on the ferromagnet (`E = n/4`, variance below `10⁻³`) and is
|
||||
//! within `10⁻²` of it after 40 steps; from `±0.1` it reaches the ground
|
||||
//! state to `10⁻³` in 200.
|
||||
//! - **Two sector referees agree.** The orbit basis gives the same energy,
|
||||
//! to `10⁻⁹`, as Lanczos on `P H P` re-projected after every product
|
||||
//! (kept in the tests): in all 24 sectors of the 4 × 3 J₁–J₂ torus (four
|
||||
//! representations, both momenta along x, no flip, an even one and an odd
|
||||
//! one), on a 10-site ring, and on an Ising ring in a transverse field,
|
||||
//! where the magnetisation is not conserved. With the identity alone it is
|
||||
//! the plain diagonalisation; with the flip alone one half holds the
|
||||
//! ground state and the other lies above. Every rectangle group on 4 × 3
|
||||
//! and 6 × 4 closes and multiplies its characters.
|
||||
//! - **The sector referee.** On a 10-site ring the sector of momentum π and
|
||||
//! odd parity holds the ground state to `10⁻⁹`, and the sector of momentum
|
||||
//! 0 and even parity lies above it. On the 4 × 4 lattice at `J₂ = J₁/2`,
|
||||
|
|
@ -209,8 +220,9 @@
|
|||
//! configurations whose probabilities total `2.4·10⁻³`. Guided at `0.1`,
|
||||
//! the same network, start and steps reach the ground energy to
|
||||
//! `1.7·10⁻¹²`. Projected networks in the CLI and `nqs_ground` now sample
|
||||
//! with a guide of `0.1`. Unprojected ones gain nothing from it and sample
|
||||
//! `|ψ|²`.
|
||||
//! with a guide of `0.1`. Unprojected ones gain nothing from it (on the
|
||||
//! 6 × 6 open Heisenberg lattice, 800 steps at `α = 2`, it doubled the
|
||||
//! error) and sample `|ψ|²`.
|
||||
//!
|
||||
//! The same trap caught the referee once. Lanczos stopped a restart once its
|
||||
//! lowest Ritz pair had converged, which is safe from DMRG's warm starts.
|
||||
|
|
@ -250,7 +262,22 @@
|
|||
//! - The sector is small: 107 configurations up to its symmetries, against
|
||||
//! the network's 288 parameters. Reaching its ground state to rounding
|
||||
//! says the training finds what the network can hold. It says nothing of
|
||||
//! a sector larger than the network.
|
||||
//! a sector larger than the network, which the next point is.
|
||||
//! - **A sector larger than the network.** On the periodic 6 × 4 lattice at
|
||||
//! `J₂ = J₁/2`, exact diagonalisation gives `−12.54059845`, in the sector
|
||||
//! even under its half turn and both reflections at momentum zero, with
|
||||
//! even parity: 15 578 configurations up to its symmetries, against a
|
||||
//! complex `α = 1` network's 624 parameters. After 600 steps of 1000
|
||||
//! samples, energies over every configuration:
|
||||
//!
|
||||
//! | projection, even parity | guide | relative error |
|
||||
//! |-------------------------|-------|----------------|
|
||||
//! | translations | `0.1` | `1.06·10⁻³` |
|
||||
//! | rectangle group, A₁ | none | `8.32·10⁻³` |
|
||||
//! | rectangle group, A₁ | `0.1` | `5.57·10⁻⁴` |
|
||||
//!
|
||||
//! The guide is worth a factor of fifteen here and the reflections a
|
||||
//! factor of two. What remains is the network's.
|
||||
//! - **One-dimensional representations only.** Characters are `±1`: momenta
|
||||
//! 0 and π, and the four one-dimensional representations of the square's
|
||||
//! rotations and reflections. The two-dimensional one and every other
|
||||
|
|
@ -475,37 +502,12 @@ impl SpinModel {
|
|||
Some(lanczos(&apply, &start, 60, 30, 1e-9, false).0)
|
||||
}
|
||||
|
||||
/// Whether every element of `sym` maps the model onto itself: each bond
|
||||
/// and field to one of the same strength, and under a flip of every spin
|
||||
/// no `σᶻ` field, which the flip reverses.
|
||||
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()))
|
||||
}
|
||||
|
||||
/// The exact ground energy within the sector `sym` projects onto (at most
|
||||
/// 22 sites), and in the magnetisation sector when given: Lanczos on
|
||||
/// `P H P` with `P = (1/|G|) Σ_g χ(g) g`, from a start in the sector and
|
||||
/// projected again after every product, so rounding cannot carry it into
|
||||
/// a lower sector. `None` when `sym` is not a group with characters, when
|
||||
/// the model is not invariant under it, when a flip would leave the
|
||||
/// magnetisation sector, or when the sector is empty.
|
||||
pub fn exact_ground_energy_in(&self, magnetisation: Option<i32>, sym: &Symmetry) -> Option<f64> {
|
||||
/// The projector referee the orbit basis replaced, kept to check it:
|
||||
/// Lanczos on `P H P`, `P = (1/|G|) Σ_g χ(g) g`, re-projected after
|
||||
/// every product, on the magnetisation sector's amplitudes (at most 22
|
||||
/// sites).
|
||||
#[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;
|
||||
|
|
@ -557,6 +559,150 @@ impl SpinModel {
|
|||
};
|
||||
Some(lanczos(&apply, &start, 60, 30, 1e-9, false).0)
|
||||
}
|
||||
|
||||
/// Whether every element of `sym` maps the model onto itself: each bond
|
||||
/// and field to one of the same strength, and under a flip of every spin
|
||||
/// no `σᶻ` field, which the flip reverses.
|
||||
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()))
|
||||
}
|
||||
|
||||
/// The exact ground energy within the sector `sym` projects onto (at most
|
||||
/// 24 sites), and in the magnetisation sector when given, by Lanczos on
|
||||
/// the sector's own basis: for each orbit of the group, the state
|
||||
/// `Σ_g χ(g) |g·r⟩` of its least configuration `r`, normalised, unless an
|
||||
/// element of character −1 fixes `r` and the state vanishes. `None` when
|
||||
/// `sym` is not a group with characters, when the model is not invariant
|
||||
/// under it, when a flip would leave the magnetisation sector, or when
|
||||
/// the sector 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);
|
||||
// g·x: its spin at i is x's at perms[g][i], flipped if g flips.
|
||||
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
|
||||
};
|
||||
// The least configuration of x's orbit, and the character of an
|
||||
// element taking x to it.
|
||||
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)
|
||||
};
|
||||
// Representatives, ascending, each with Σ χ over its stabiliser:
|
||||
// |G| times that is its state's squared norm, and zero when an
|
||||
// element of character −1 fixes it.
|
||||
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;
|
||||
}
|
||||
// H on the basis, column by column: H|r⟩ = Σ_x h_xr |x⟩, and the
|
||||
// state of x's orbit carries it with χ(k) √(S_r′/S_r), x = k·r′.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -677,6 +823,38 @@ impl PointIrrep {
|
|||
}
|
||||
}
|
||||
|
||||
/// One of the four one-dimensional representations of a rectangle's
|
||||
/// symmetries (the half turn, and the reflections in its two axes), by its
|
||||
/// character on each.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RectIrrep {
|
||||
/// Even under the half turn and both reflections.
|
||||
A1,
|
||||
/// Even under the half turn, odd under both reflections.
|
||||
A2,
|
||||
/// Odd under the half turn and the reflection `y → −y`, even under
|
||||
/// `x → −x`.
|
||||
B1,
|
||||
/// Odd under the half turn and the reflection `x → −x`, even under
|
||||
/// `y → −y`.
|
||||
B2,
|
||||
}
|
||||
|
||||
impl RectIrrep {
|
||||
/// The character of reflecting `x → −x` if `flip_x` and `y → −y` if
|
||||
/// `flip_y` (both: the half turn).
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A symmetry group of the lattice: site permutations, each possibly
|
||||
/// followed by flipping every spin, each with a real character. The
|
||||
/// projection `Ψ(s) = Σ_g χ(g) ψ(g·s)` picks the sector: a momentum of 0 or
|
||||
|
|
@ -796,6 +974,45 @@ impl Symmetry {
|
|||
Symmetry::new(perms, chars)
|
||||
}
|
||||
|
||||
/// The space group of an `lx × ly` torus with its rectangle's symmetries:
|
||||
/// every translation, each composed with the identity, the reflections
|
||||
/// `x → −x` and `y → −y` about a site, or both (the half turn), in one of
|
||||
/// their four one-dimensional representations. Momentum 0 or π along
|
||||
/// each direction, all of which the reflections leave alone.
|
||||
pub fn rectangle(lx: usize, ly: usize, kx_pi: bool, ky_pi: bool, irrep: RectIrrep) -> Symmetry {
|
||||
// At length 2 a reflection moves every site as the identity does.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Whether the elements form a group under composition and every
|
||||
/// character is `±1` and multiplies as the elements do: what makes
|
||||
/// `Σ_g χ(g) ψ(g·s)` a projection onto one sector, not onto a mixture or
|
||||
|
|
@ -2059,6 +2276,18 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Characters that do not multiply, and elements that do not close.
|
||||
let ring = Symmetry::ring(4, false);
|
||||
assert!(Symmetry::new(ring.perms.clone(), vec![1.0, -1.0, 1.0, 1.0]).check().is_err());
|
||||
|
|
@ -2197,6 +2426,46 @@ mod tests {
|
|||
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:?}"),
|
||||
};
|
||||
// Every sector of a 4 × 3 torus at J₂ = J₁/2: four representations,
|
||||
// both momenta along x, and no flip, an even one or an odd one.
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// A 10-site ring, and an Ising ring in a transverse field, where the
|
||||
// magnetisation is not conserved.
|
||||
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");
|
||||
}
|
||||
}
|
||||
// The identity alone is no reduction, and the flip alone splits the
|
||||
// spectrum in two, one half holding the ground state.
|
||||
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() {
|
||||
// 4 × 4 at J₂ = J₁/2: the ground state is even under every rotation
|
||||
|
|
|
|||
2
wai/wai-rs/Cargo.lock
generated
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -3900,7 +3900,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -174,7 +174,8 @@ fn usage() -> ! {
|
|||
wai-quantum chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
|
||||
NEURAL QUANTUM STATES (RBM, variational Monte Carlo, stochastic reconfiguration):\n \
|
||||
wai-quantum nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
|
||||
[--complex] [--project [--kpi] [--point-group a1|a2|b1|b2] [--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \
|
||||
[--complex] [--project [--kpi] [--point-group a1|a2|b1|b2 | --reflections a1|a2|b1|b2]\n \
|
||||
[--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \
|
||||
[--init W] [--guide ε] [--exact] [--seed S] [--json]\n\n\
|
||||
OPTIMISATION (simulated bifurcation, annealing, exact):\n \
|
||||
wai-quantum qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \
|
||||
|
|
@ -1205,6 +1206,13 @@ fn main() {
|
|||
"b2" => PointIrrep::B2,
|
||||
_ => die("--point-group: a1, a2, b1 or b2"),
|
||||
});
|
||||
let reflections = opt(args, "--reflections", "").map(|g| match g.as_str() {
|
||||
"a1" => RectIrrep::A1,
|
||||
"a2" => RectIrrep::A2,
|
||||
"b1" => RectIrrep::B1,
|
||||
"b2" => RectIrrep::B2,
|
||||
_ => die("--reflections: a1, a2, b1 or b2"),
|
||||
});
|
||||
let symmetry = flag(args, "--project").then(|| {
|
||||
if !periodic {
|
||||
die("--project needs --periodic");
|
||||
|
|
@ -1213,16 +1221,19 @@ fn main() {
|
|||
if kpi && (lx % 2 == 1 || (ly > 1 && ly % 2 == 1)) {
|
||||
die("--kpi needs even lengths");
|
||||
}
|
||||
let base = match point {
|
||||
Some(_) if lx != ly || lx < 3 => die("--point-group needs a square lattice, --lx = --ly ≥ 3"),
|
||||
Some(irrep) => Symmetry::square(lx, kpi, irrep),
|
||||
None if ly == 1 => Symmetry::ring(n, kpi),
|
||||
None => Symmetry::torus(lx, ly, kpi, kpi),
|
||||
let base = match (point, reflections) {
|
||||
(Some(_), Some(_)) => die("--point-group or --reflections, not both"),
|
||||
(Some(_), None) if lx != ly || lx < 3 => die("--point-group needs a square lattice, --lx = --ly ≥ 3"),
|
||||
(Some(irrep), None) => Symmetry::square(lx, kpi, irrep),
|
||||
(None, Some(_)) if lx < 3 || ly < 3 => die("--reflections needs both lengths ≥ 3"),
|
||||
(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(flag(args, "--odd"))
|
||||
});
|
||||
if point.is_some() && symmetry.is_none() {
|
||||
die("--point-group needs --project");
|
||||
if (point.is_some() || reflections.is_some()) && symmetry.is_none() {
|
||||
die("--point-group and --reflections need --project");
|
||||
}
|
||||
// Parameters start uniform in ±init. A projected network that
|
||||
// starts near zero is nearly the projection of a constant, the
|
||||
|
|
@ -1234,7 +1245,7 @@ fn main() {
|
|||
cfg.guide = num(args, "--guide", "", if symmetry.is_some() { 0.1 } else { 0.0 });
|
||||
// The exact ground energy in the projection's own sector.
|
||||
let in_sector = match &symmetry {
|
||||
Some(sym) if n <= 20 => model.exact_ground_energy_in(sector, sym),
|
||||
Some(sym) if n <= 24 => model.exact_ground_energy_in(sector, sym),
|
||||
_ => None,
|
||||
};
|
||||
fn run<T: Amplitude>(model: SpinModel, rbm: Rbm<T>, cfg: VmcConfig, steps: usize) -> (usize, Estimate, Estimate) {
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -734,7 +734,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue