#![allow(clippy::needless_range_loop)]
use crate::linalg::{C, jacobi_svd_strict, qr};
use crate::quantum_spd::{Op, Pauli, RotCircuit};
use crate::repro::sin_cos;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
pub enum TnsError {
QubitOutOfRange(u32),
NotAnEdge(u32, u32),
TooWide(usize),
Invalid(String),
}
impl fmt::Display for TnsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TnsError::QubitOutOfRange(q) => write!(f, "qubit {q} is not on the graph"),
TnsError::NotAnEdge(a, b) => write!(f, "no edge between {a} and {b}"),
TnsError::TooWide(w) => write!(f, "a rotation on {w} qubits; at most 2 here"),
TnsError::Invalid(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for TnsError {}
pub struct Tns {
n: usize,
edges: Vec<(u32, u32)>,
nbrs: Vec<Vec<usize>>,
t: Vec<Vec<C>>,
lam: Vec<Vec<f64>>,
chi: usize,
discarded: f64,
}
const CUTOFF: f64 = 1e-10;
impl Tns {
pub fn new(n: u32, edges: &[(u32, u32)], chi: usize) -> Result<Tns, TnsError> {
let n = n as usize;
let mut nbrs = vec![Vec::new(); n];
for (e, &(a, b)) in edges.iter().enumerate() {
for q in [a, b] {
if q as usize >= n {
return Err(TnsError::QubitOutOfRange(q));
}
}
if a == b {
return Err(TnsError::NotAnEdge(a, b));
}
nbrs[a as usize].push(e);
nbrs[b as usize].push(e);
}
let t = (0..n).map(|_| vec![C::ONE, C::ZERO]).collect();
Ok(Tns { n, edges: edges.to_vec(), nbrs, t, lam: vec![vec![1.0]; edges.len()], chi: chi.max(1), discarded: 0.0 })
}
pub fn discarded(&self) -> f64 {
self.discarded
}
pub fn max_bond(&self) -> usize {
self.lam.iter().map(Vec::len).max().unwrap_or(1)
}
fn bond_dims(&self, v: usize) -> Vec<usize> {
self.nbrs[v].iter().map(|&e| self.lam[e].len()).collect()
}
fn edge_of(&self, a: u32, b: u32) -> Option<usize> {
self.nbrs.get(a as usize)?.iter().copied().find(|&e| {
let (x, y) = self.edges[e];
(x, y) == (a, b) || (x, y) == (b, a)
})
}
pub fn run(&mut self, circuit: &RotCircuit) -> Result<(), TnsError> {
self.run_inner(circuit, None)
}
pub fn run_gauged(&mut self, circuit: &RotCircuit, sweeps: usize, tol: f64) -> Result<(), TnsError> {
self.run_inner(circuit, Some((sweeps, tol)))
}
fn run_inner(&mut self, circuit: &RotCircuit, gauge: Option<(usize, f64)>) -> Result<(), TnsError> {
circuit.validate().map_err(|e| TnsError::Invalid(e.to_string()))?;
if circuit.n as usize != self.n {
return Err(TnsError::Invalid(format!("a {}-qubit circuit on a {}-qubit graph", circuit.n, self.n)));
}
let mut batch: Vec<(u32, u32, [[C; 4]; 4])> = Vec::new();
let mut busy = vec![false; self.n];
for op in &circuit.ops {
match gate_of(op)? {
Gate::One(q, g) => {
if busy[q as usize] {
self.flush(&mut batch, &mut busy, gauge)?;
}
self.gate1(q, g)?;
}
Gate::Two(a, b, g) => {
for q in [a, b] {
if q as usize >= self.n {
return Err(TnsError::QubitOutOfRange(q));
}
}
if busy[a as usize] || busy[b as usize] {
self.flush(&mut batch, &mut busy, gauge)?;
}
busy[a as usize] = true;
busy[b as usize] = true;
batch.push((a, b, g));
}
}
}
self.flush(&mut batch, &mut busy, gauge)
}
fn flush(
&mut self,
batch: &mut Vec<(u32, u32, [[C; 4]; 4])>,
busy: &mut [bool],
gauge: Option<(usize, f64)>,
) -> Result<(), TnsError> {
if batch.is_empty() {
return Ok(());
}
let threads = if cfg!(target_arch = "wasm32") {
1
} else {
std::thread::available_parallelism().map_or(1, |n| n.get()).min(batch.len())
};
let updates: Vec<Result<Update, TnsError>> = if threads <= 1 {
batch.iter().map(|&(a, b, g)| self.compute2(a, b, g)).collect()
} else {
let this = &*self;
let chunk = batch.len().div_ceil(threads);
std::thread::scope(|scope| {
let handles: Vec<_> = batch
.chunks(chunk)
.map(|part| scope.spawn(move || part.iter().map(|&(a, b, g)| this.compute2(a, b, g)).collect::<Vec<_>>()))
.collect();
handles.into_iter().flat_map(|h| h.join().expect("a gate's computation panicked")).collect()
})
};
for up in updates {
self.commit(up?);
}
for &(a, b, _) in batch.iter() {
busy[a as usize] = false;
busy[b as usize] = false;
}
batch.clear();
if let Some((sweeps, tol)) = gauge {
let env = self.belief_propagation(sweeps, tol);
self.regauge(&env);
}
Ok(())
}
pub fn apply(&mut self, op: &Op) -> Result<(), TnsError> {
match gate_of(op)? {
Gate::One(q, g) => self.gate1(q, g),
Gate::Two(a, b, g) => {
let up = self.compute2(a, b, g)?;
self.commit(up);
Ok(())
}
}
}
fn gate1(&mut self, q: u32, g: [[C; 2]; 2]) -> Result<(), TnsError> {
let v = q as usize;
if v >= self.n {
return Err(TnsError::QubitOutOfRange(q));
}
let half = self.t[v].len() / 2;
let t = &mut self.t[v];
for i in 0..half {
let (a, b) = (t[i], t[half + i]);
t[i] = g[0][0].mul(a).add(g[0][1].mul(b));
t[half + i] = g[1][0].mul(a).add(g[1][1].mul(b));
}
Ok(())
}
fn compute2(&self, a: u32, b: u32, g: [[C; 4]; 4]) -> Result<Update, TnsError> {
for q in [a, b] {
if q as usize >= self.n {
return Err(TnsError::QubitOutOfRange(q));
}
}
let e = self.edge_of(a, b).ok_or(TnsError::NotAnEdge(a, b))?;
let (u, v) = (a as usize, b as usize);
let d = self.lam[e].len();
let (mu, ru, rest_u) = self.side_matrix(u, e);
let (mv, rv, rest_v) = self.side_matrix(v, e);
let (qu, rru, ku) = qr(&mu, ru, 2 * d);
let (qv, rrv, kv) = qr(&mv, rv, 2 * d);
let lam = &self.lam[e];
let rows = 2 * ku;
let cols = 2 * kv;
let mut theta = vec![C::ZERO; rows * cols];
for x in 0..ku {
for s in 0..2 {
for y in 0..kv {
for t in 0..2 {
let mut acc = C::ZERO;
for i in 0..d {
acc = acc.add(rru[x * 2 * d + s * d + i].scale(lam[i]).mul(rrv[y * 2 * d + t * d + i]));
}
theta[(x * 2 + s) * cols + t * kv + y] = acc;
}
}
}
}
let mut gated = vec![C::ZERO; rows * cols];
for x in 0..ku {
for y in 0..kv {
for s2 in 0..2 {
for t2 in 0..2 {
let mut acc = C::ZERO;
for s in 0..2 {
for t in 0..2 {
acc = acc.add(g[2 * s2 + t2][2 * s + t].mul(theta[(x * 2 + s) * cols + t * kv + y]));
}
}
gated[(x * 2 + s2) * cols + t2 * kv + y] = acc;
}
}
}
}
let (left, sv, right) = svd_rows(&gated, rows, cols);
let total: f64 = sv.iter().map(|x| x * x).sum();
let top = sv.first().copied().unwrap_or(0.0);
let keep = sv.iter().take(self.chi).take_while(|&&x| x > CUTOFF * top).count().max(1);
let kept: f64 = sv[..keep].iter().map(|x| x * x).sum();
let discarded = if total > 0.0 { (total - kept) / total } else { 0.0 };
let norm = kept.sqrt();
let new_lam: Vec<f64> = sv[..keep].iter().map(|x| x / norm).collect();
let mut new_u = vec![C::ZERO; rest_u * 2 * keep];
for o in 0..rest_u {
for s in 0..2 {
for k in 0..keep {
let mut acc = C::ZERO;
for x in 0..ku {
acc = acc.add(qu[o * ku + x].mul(left[k][x * 2 + s]));
}
new_u[(o * 2 + s) * keep + k] = acc;
}
}
}
let mut new_v = vec![C::ZERO; rest_v * 2 * keep];
for o in 0..rest_v {
for t in 0..2 {
for k in 0..keep {
let mut acc = C::ZERO;
for y in 0..kv {
acc = acc.add(qv[o * kv + y].mul(right[k][t * kv + y].conj()));
}
new_v[(o * 2 + t) * keep + k] = acc;
}
}
}
Ok(Update {
e,
u,
v,
tu: self.rebuilt(u, e, &new_u, keep),
tv: self.rebuilt(v, e, &new_v, keep),
lam: new_lam,
discarded,
})
}
fn commit(&mut self, up: Update) {
self.discarded += up.discarded;
self.lam[up.e] = up.lam;
self.t[up.u] = up.tu;
self.t[up.v] = up.tv;
}
fn side_matrix(&self, v: usize, e: usize) -> (Vec<C>, usize, usize) {
let dims = self.bond_dims(v);
let pos = self.nbrs[v].iter().position(|&x| x == e).expect("the edge is incident");
let d = dims[pos];
let bonds: usize = dims.iter().product();
let rest = bonds / d;
let mut m = vec![C::ZERO; rest * 2 * d];
let mut idx = vec![0usize; dims.len()];
for flat in 0..bonds {
let mut r = flat;
for j in (0..dims.len()).rev() {
idx[j] = r % dims[j];
r /= dims[j];
}
let mut row = 0usize;
let mut w = 1.0;
for j in 0..dims.len() {
if j != pos {
row = row * dims[j] + idx[j];
w *= self.lam[self.nbrs[v][j]][idx[j]];
}
}
for s in 0..2 {
m[row * 2 * d + s * d + idx[pos]] = self.t[v][s * bonds + flat].scale(w);
}
}
(m, rest, rest)
}
fn rebuilt(&self, v: usize, e: usize, m: &[C], keep: usize) -> Vec<C> {
let pos = self.nbrs[v].iter().position(|&x| x == e).expect("the edge is incident");
let mut dims = self.bond_dims(v);
dims[pos] = keep;
let bonds: usize = dims.iter().product();
let mut t = vec![C::ZERO; 2 * bonds];
let mut idx = vec![0usize; dims.len()];
for flat in 0..bonds {
let mut r = flat;
for j in (0..dims.len()).rev() {
idx[j] = r % dims[j];
r /= dims[j];
}
let mut row = 0usize;
let mut w = 1.0;
for j in 0..dims.len() {
if j != pos {
row = row * dims[j] + idx[j];
w *= self.lam[self.nbrs[v][j]][idx[j]];
}
}
let inv = 1.0 / w;
for s in 0..2 {
t[s * bonds + flat] = m[(row * 2 + s) * keep + idx[pos]].scale(inv);
}
}
t
}
pub fn expect1(&self, q: u32, p: Pauli) -> Result<f64, TnsError> {
let v = q as usize;
if v >= self.n {
return Err(TnsError::QubitOutOfRange(q));
}
let dims = self.bond_dims(v);
let bonds: usize = dims.iter().product();
let mut rho = [[C::ZERO; 2]; 2];
let mut idx = vec![0usize; dims.len()];
for flat in 0..bonds {
let mut r = flat;
for j in (0..dims.len()).rev() {
idx[j] = r % dims[j];
r /= dims[j];
}
let w: f64 = (0..dims.len()).map(|j| self.lam[self.nbrs[v][j]][idx[j]]).map(|l| l * l).product();
for s in 0..2 {
for s2 in 0..2 {
let x = self.t[v][s * bonds + flat].mul(self.t[v][s2 * bonds + flat].conj()).scale(w);
rho[s][s2] = rho[s][s2].add(x);
}
}
}
let pm = pauli(p);
let mut num = C::ZERO;
for s in 0..2 {
for s2 in 0..2 {
num = num.add(rho[s][s2].mul(pm[s2][s]));
}
}
let den = rho[0][0].re + rho[1][1].re;
Ok(num.re / den)
}
}
pub struct BpEnv {
msgs: Vec<[Vec<C>; 2]>,
pub sweeps: usize,
pub residual: f64,
}
impl Tns {
fn absorbed(&self, v: usize) -> Vec<C> {
let dims = self.bond_dims(v);
let bonds: usize = dims.iter().product();
let mut out = self.t[v].clone();
let mut idx = vec![0usize; dims.len()];
for flat in 0..bonds {
let mut r = flat;
for j in (0..dims.len()).rev() {
idx[j] = r % dims[j];
r /= dims[j];
}
let w: f64 = (0..dims.len()).map(|j| self.lam[self.nbrs[v][j]][idx[j]].sqrt()).product();
for s in 0..2 {
out[s * bonds + flat] = out[s * bonds + flat].scale(w);
}
}
out
}
fn incoming<'a>(&self, msgs: &'a [[Vec<C>; 2]], v: usize, j: usize) -> &'a [C] {
let e = self.nbrs[v][j];
if self.edges[e].0 as usize == v { &msgs[e][1] } else { &msgs[e][0] }
}
fn with_messages(&self, a: &[C], v: usize, msgs: &[[Vec<C>; 2]], skip: Option<usize>) -> Vec<C> {
let dims = self.bond_dims(v);
let bonds: usize = dims.iter().product();
let mut cur = a.to_vec();
for j in 0..dims.len() {
if skip == Some(j) {
continue;
}
let m = self.incoming(msgs, v, j);
let d = dims[j];
let stride: usize = dims[j + 1..].iter().product();
let mut next = vec![C::ZERO; cur.len()];
for s in 0..2 {
for flat in 0..bonds {
let x = cur[s * bonds + flat];
if x.re == 0.0 && x.im == 0.0 {
continue;
}
let i = (flat / stride) % d;
let base = flat - i * stride;
for ip in 0..d {
let k = s * bonds + base + ip * stride;
next[k] = next[k].add(x.mul(m[i * d + ip]));
}
}
}
cur = next;
}
cur
}
fn message(&self, abs: &[Vec<C>], msgs: &[[Vec<C>; 2]], u: usize, p: usize) -> Vec<C> {
let a = &abs[u];
let c = self.with_messages(a, u, msgs, Some(p));
let dims = self.bond_dims(u);
let bonds: usize = dims.iter().product();
let d = dims[p];
let stride: usize = dims[p + 1..].iter().product();
let mut m = vec![C::ZERO; d * d];
for s in 0..2 {
for base in 0..bonds {
if (base / stride) % d != 0 {
continue;
}
for i in 0..d {
let ci = c[s * bonds + base + i * stride];
for ip in 0..d {
m[i * d + ip] = m[i * d + ip].add(ci.mul(a[s * bonds + base + ip * stride].conj()));
}
}
}
}
let tr: f64 = (0..d).map(|i| m[i * d + i].re).sum();
if tr > 0.0 {
for x in m.iter_mut() {
*x = x.scale(1.0 / tr);
}
}
m
}
pub fn belief_propagation(&self, max_sweeps: usize, tol: f64) -> BpEnv {
let abs: Vec<Vec<C>> = (0..self.n).map(|v| self.absorbed(v)).collect();
let mut msgs: Vec<[Vec<C>; 2]> = self
.lam
.iter()
.map(|l| {
let d = l.len();
let sum: f64 = l.iter().sum();
let diag: Vec<C> = (0..d * d).map(|k| if k % (d + 1) == 0 { C::new(l[k / (d + 1)] / sum, 0.0) } else { C::ZERO }).collect();
[diag.clone(), diag]
})
.collect();
let directed: Vec<(usize, usize, usize, usize)> = (0..self.edges.len())
.flat_map(|e| {
let (a, b) = (self.edges[e].0 as usize, self.edges[e].1 as usize);
let pa = self.nbrs[a].iter().position(|&x| x == e).expect("incident");
let pb = self.nbrs[b].iter().position(|&x| x == e).expect("incident");
[(e, 0, a, pa), (e, 1, b, pb)]
})
.collect();
let mut sweeps = 0;
let mut residual = f64::INFINITY;
while sweeps < max_sweeps && residual > tol {
let threads = if cfg!(target_arch = "wasm32") {
1
} else {
std::thread::available_parallelism().map_or(1, |n| n.get()).min(directed.len())
};
let fresh: Vec<Vec<C>> = if threads <= 1 {
directed.iter().map(|&(_, _, u, p)| self.message(&abs, &msgs, u, p)).collect()
} else {
let (this, abs_ref, msgs_ref) = (&*self, &abs, &msgs);
let chunk = directed.len().div_ceil(threads);
std::thread::scope(|scope| {
let handles: Vec<_> = directed
.chunks(chunk)
.map(|part| scope.spawn(move || part.iter().map(|&(_, _, u, p)| this.message(abs_ref, msgs_ref, u, p)).collect::<Vec<_>>()))
.collect();
handles.into_iter().flat_map(|h| h.join().expect("a message's computation panicked")).collect()
})
};
residual = 0.0;
for (&(e, dir, _, _), m) in directed.iter().zip(fresh) {
for (old, new) in msgs[e][dir].iter().zip(&m) {
residual = residual.max(old.sub(*new).norm2().sqrt());
}
msgs[e][dir] = m;
}
sweeps += 1;
}
BpEnv { msgs, sweeps, residual }
}
pub fn regauge(&mut self, env: &BpEnv) {
let abs: Vec<Vec<C>> = (0..self.n).map(|v| self.absorbed(v)).collect();
let mut transforms: Vec<[Vec<C>; 2]> = Vec::with_capacity(self.edges.len());
let mut new_lam: Vec<Vec<f64>> = Vec::with_capacity(self.edges.len());
for e in 0..self.edges.len() {
let d = self.lam[e].len();
let (xa, xap, ra) = psd_sqrt(&env.msgs[e][0], d);
let (xb, xbp, rb) = psd_sqrt(&env.msgs[e][1], d);
let mut bm = vec![C::ZERO; ra * rb];
for p in 0..ra {
for q in 0..rb {
let mut acc = C::ZERO;
for i in 0..d {
acc = acc.add(xa[i * ra + p].mul(xb[i * rb + q]));
}
bm[p * rb + q] = acc;
}
}
let (u, sv, v) = svd_rows(&bm, ra, rb);
let top = sv.first().copied().unwrap_or(0.0);
let k = sv.iter().take_while(|&&x| x > CUTOFF * top).count().max(1);
let norm = sv[..k].iter().map(|x| x * x).sum::<f64>().sqrt();
new_lam.push(sv[..k].iter().map(|x| x / norm).collect());
let mut ta = vec![C::ZERO; d * k];
let mut tb = vec![C::ZERO; d * k];
for i in 0..d {
for n in 0..k {
let mut a_acc = C::ZERO;
for p in 0..ra {
a_acc = a_acc.add(xap[p * d + i].mul(u[n][p]));
}
ta[i * k + n] = a_acc;
let mut b_acc = C::ZERO;
for q in 0..rb {
b_acc = b_acc.add(v[n][q].conj().mul(xbp[q * d + i]));
}
tb[i * k + n] = b_acc;
}
}
transforms.push([ta, tb]);
}
for v in 0..self.n {
let mut dims = self.bond_dims(v);
let mut cur = abs[v].clone();
for j in 0..dims.len() {
let e = self.nbrs[v][j];
let side = if self.edges[e].0 as usize == v { 0 } else { 1 };
let k = new_lam[e].len();
cur = transform_bond(&cur, &dims, j, &transforms[e][side], k);
dims[j] = k;
}
self.t[v] = cur;
}
self.lam = new_lam;
}
pub fn expect1_bp(&self, env: &BpEnv, q: u32, p: Pauli) -> Result<f64, TnsError> {
let v = q as usize;
if v >= self.n {
return Err(TnsError::QubitOutOfRange(q));
}
let a = self.absorbed(v);
let c = self.with_messages(&a, v, &env.msgs, None);
let bonds = a.len() / 2;
let mut rho = [[C::ZERO; 2]; 2];
for s in 0..2 {
for s2 in 0..2 {
for k in 0..bonds {
rho[s][s2] = rho[s][s2].add(c[s * bonds + k].mul(a[s2 * bonds + k].conj()));
}
}
}
let pm = pauli(p);
let mut num = C::ZERO;
for s in 0..2 {
for s2 in 0..2 {
num = num.add(rho[s][s2].mul(pm[s2][s]));
}
}
Ok(num.re / (rho[0][0].re + rho[1][1].re))
}
}
enum Gate {
One(u32, [[C; 2]; 2]),
Two(u32, u32, [[C; 4]; 4]),
}
struct Update {
e: usize,
u: usize,
v: usize,
tu: Vec<C>,
tv: Vec<C>,
lam: Vec<f64>,
discarded: f64,
}
fn gate_of(op: &Op) -> Result<Gate, TnsError> {
Ok(match op {
Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => Gate::One(*q, fixed1(op)),
Op::CX(a, b) => Gate::Two(*a, *b, cx()),
Op::CZ(a, b) => Gate::Two(*a, *b, cz()),
Op::Swap(a, b) => Gate::Two(*a, *b, swap()),
Op::Rot { axis, theta } => {
let (s, c) = sin_cos(0.5 * theta);
rotation(axis, c, s)?
}
Op::QuarterRot { axis, k } => {
let h = std::f64::consts::FRAC_1_SQRT_2;
let (c, s) = match k {
1 => (h, h),
2 => (0.0, 1.0),
_ => (-h, h),
};
rotation(axis, c, s)?
}
})
}
fn rotation(axis: &[(u32, Pauli)], c: f64, s: f64) -> Result<Gate, TnsError> {
let minus_is = C::new(0.0, -s);
match axis {
[(q, p)] => {
let pm = pauli(*p);
let mut g = [[C::ZERO; 2]; 2];
for r in 0..2 {
for col in 0..2 {
let id = if r == col { C::new(c, 0.0) } else { C::ZERO };
g[r][col] = id.add(minus_is.mul(pm[r][col]));
}
}
Ok(Gate::One(*q, g))
}
[(a, pa), (b, pb)] => {
let (ma, mb) = (pauli(*pa), pauli(*pb));
let mut g = [[C::ZERO; 4]; 4];
for r in 0..4 {
for col in 0..4 {
let kron = ma[r >> 1][col >> 1].mul(mb[r & 1][col & 1]);
let id = if r == col { C::new(c, 0.0) } else { C::ZERO };
g[r][col] = id.add(minus_is.mul(kron));
}
}
Ok(Gate::Two(*a, *b, g))
}
_ => Err(TnsError::TooWide(axis.len())),
}
}
fn psd_sqrt(m: &[C], d: usize) -> (Vec<C>, Vec<C>, usize) {
let (u, s, _) = svd_rows(m, d, d);
let top = s.first().copied().unwrap_or(0.0);
let r = s.iter().take_while(|&&x| x > CUTOFF * top).count().max(1);
let mut x = vec![C::ZERO; d * r];
let mut xp = vec![C::ZERO; r * d];
for k in 0..r {
let root = s[k].sqrt();
for i in 0..d {
x[i * r + k] = u[k][i].scale(root);
xp[k * d + i] = u[k][i].conj().scale(1.0 / root);
}
}
(x, xp, r)
}
fn transform_bond(t: &[C], dims: &[usize], j: usize, m: &[C], k: usize) -> Vec<C> {
let d = dims[j];
let bonds: usize = dims.iter().product();
let stride: usize = dims[j + 1..].iter().product();
let new_bonds = bonds / d * k;
let mut out = vec![C::ZERO; 2 * new_bonds];
for s in 0..2 {
for flat in 0..bonds {
let x = t[s * bonds + flat];
if x.re == 0.0 && x.im == 0.0 {
continue;
}
let pre = flat / (d * stride);
let i = (flat / stride) % d;
let post = flat % stride;
for n in 0..k {
let at = s * new_bonds + pre * k * stride + n * stride + post;
out[at] = out[at].add(x.mul(m[i * k + n]));
}
}
}
out
}
fn svd_rows(a: &[C], rows: usize, cols: usize) -> (Vec<Vec<C>>, Vec<f64>, Vec<Vec<C>>) {
if cols <= rows {
let columns: Vec<Vec<C>> = (0..cols).map(|c| (0..rows).map(|r| a[r * cols + c]).collect()).collect();
jacobi_svd_strict(columns, rows, cols)
} else {
let columns: Vec<Vec<C>> = (0..rows).map(|r| (0..cols).map(|c| a[r * cols + c].conj()).collect()).collect();
let (v, s, u) = jacobi_svd_strict(columns, cols, rows);
(u, s, v)
}
}
fn pauli(p: Pauli) -> [[C; 2]; 2] {
let (z, o) = (C::ZERO, C::ONE);
match p {
Pauli::X => [[z, o], [o, z]],
Pauli::Y => [[z, C::new(0.0, -1.0)], [C::new(0.0, 1.0), z]],
Pauli::Z => [[o, z], [z, C::new(-1.0, 0.0)]],
}
}
fn fixed1(op: &Op) -> [[C; 2]; 2] {
let (z, o) = (C::ZERO, C::ONE);
let (i, ni) = (C::new(0.0, 1.0), C::new(0.0, -1.0));
let h = std::f64::consts::FRAC_1_SQRT_2;
let (hp, hm) = (C::new(h, 0.0), C::new(-h, 0.0));
let (pp, pm) = (C::new(0.5, 0.5), C::new(0.5, -0.5));
match op {
Op::H(_) => [[hp, hp], [hp, hm]],
Op::S(_) => [[o, z], [z, i]],
Op::Sdg(_) => [[o, z], [z, ni]],
Op::SX(_) => [[pp, pm], [pm, pp]],
Op::SXdg(_) => [[pm, pp], [pp, pm]],
Op::X(_) => pauli(Pauli::X),
Op::Y(_) => pauli(Pauli::Y),
_ => pauli(Pauli::Z),
}
}
fn cx() -> [[C; 4]; 4] {
let (z, o) = (C::ZERO, C::ONE);
[[o, z, z, z], [z, o, z, z], [z, z, z, o], [z, z, o, z]]
}
fn cz() -> [[C; 4]; 4] {
let (z, o) = (C::ZERO, C::ONE);
[[o, z, z, z], [z, o, z, z], [z, z, o, z], [z, z, z, C::new(-1.0, 0.0)]]
}
fn swap() -> [[C; 4]; 4] {
let (z, o) = (C::ZERO, C::ONE);
[[o, z, z, z], [z, z, o, z], [z, o, z, z], [z, z, z, o]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum_spd::{SpdConfig, expect_z, expectation, heavy_hex_127, kicked_ising};
fn exact(c: &RotCircuit, q: u32, p: Pauli) -> f64 {
expectation(c, &[(1.0, vec![(q, p)])], &[], &SpdConfig { threshold: 0.0, max_weight: None }).unwrap().value
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn angle(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1u64 << 53) as f64 * 6.0 - 3.0
}
}
fn random_on(rng: &mut Rng, n: u32, edges: &[(u32, u32)], len: usize) -> RotCircuit {
let p = |r: &mut Rng| [Pauli::X, Pauli::Y, Pauli::Z][r.below(3) as usize];
let mut c = RotCircuit::new(n);
for _ in 0..len {
let q = rng.below(u64::from(n)) as u32;
let (a, b) = edges[rng.below(edges.len() as u64) as usize];
let (a, b) = if rng.below(2) == 0 { (a, b) } else { (b, a) };
let op = match rng.below(12) {
0 => Op::H(q),
1 => Op::S(q),
2 => Op::SX(q),
3 => Op::SXdg(q),
4 => Op::Y(q),
5 => Op::CX(a, b),
6 => Op::CZ(a, b),
7 => Op::Swap(a, b),
8 => Op::QuarterRot { axis: vec![(a, p(rng)), (b, p(rng))], k: 1 + rng.below(3) as u8 },
9 => Op::Rot { axis: vec![(a, p(rng)), (b, p(rng))], theta: rng.angle() },
_ => Op::Rot { axis: vec![(q, p(rng))], theta: rng.angle() },
};
c.push(op);
}
c
}
#[test]
fn on_a_tree_with_no_truncation_it_is_exact() {
let chain: Vec<(u32, u32)> = (0..7).map(|q| (q, q + 1)).collect();
let tree = vec![(0, 1), (1, 2), (1, 3), (3, 4), (3, 5), (0, 6), (6, 7)];
let mut rng = Rng(0x5eed);
for edges in [chain, tree] {
for trial in 0..25 {
let c = random_on(&mut rng, 8, &edges, 40);
let mut tns = Tns::new(8, &edges, 16).unwrap();
tns.run(&c).unwrap();
assert!(tns.discarded() < 1e-20, "nothing truncated: {}", tns.discarded());
for q in 0..8 {
for p in [Pauli::X, Pauli::Y, Pauli::Z] {
let (got, want) = (tns.expect1(q, p).unwrap(), exact(&c, q, p));
assert!((got - want).abs() < 1e-12, "trial {trial} q {q} {p:?}: {got} vs {want}");
}
}
}
}
}
#[test]
fn belief_propagation_is_exact_on_a_tree() {
let tree = vec![(0, 1), (1, 2), (1, 3), (3, 4), (3, 5), (0, 6), (6, 7)];
let mut rng = Rng(0xbe1);
for trial in 0..15 {
let c = random_on(&mut rng, 8, &tree, 40);
let mut tns = Tns::new(8, &tree, 16).unwrap();
tns.run(&c).unwrap();
let env = tns.belief_propagation(200, 1e-14);
assert!(env.residual <= 1e-14, "trial {trial}: BP converged ({} sweeps, {:e})", env.sweeps, env.residual);
for q in 0..8 {
for p in [Pauli::X, Pauli::Y, Pauli::Z] {
let (got, want) = (tns.expect1_bp(&env, q, p).unwrap(), exact(&c, q, p));
assert!((got - want).abs() < 1e-11, "trial {trial} q {q} {p:?}: {got} vs {want}");
}
}
}
}
#[test]
fn regauging_changes_no_expectation_and_makes_the_bond_weights_the_environment() {
let edges = heavy_hex_127();
let mut tns = Tns::new(127, &edges, 8).unwrap();
tns.run(&kicked_ising(127, &edges, 0.6, 4)).unwrap();
let env = tns.belief_propagation(200, 1e-13);
let before: Vec<f64> = (0..127).map(|q| tns.expect1_bp(&env, q, Pauli::Z).unwrap()).collect();
tns.regauge(&env);
let env2 = tns.belief_propagation(200, 1e-13);
for q in 0..127 {
let (after_bp, after_simple) = (tns.expect1_bp(&env2, q, Pauli::Z).unwrap(), tns.expect1(q, Pauli::Z).unwrap());
assert!((after_bp - before[q as usize]).abs() < 1e-9, "q {q}: the state changed: {after_bp} vs {}", before[q as usize]);
assert!((after_simple - after_bp).abs() < 1e-9, "q {q}: bond weights {after_simple} vs BP {after_bp}");
}
}
#[test]
fn gauged_evolution_is_exact_on_a_tree() {
let tree = vec![(0, 1), (1, 2), (1, 3), (3, 4), (3, 5), (0, 6), (6, 7)];
let mut rng = Rng(0x9a);
for trial in 0..10 {
let c = random_on(&mut rng, 8, &tree, 40);
let mut tns = Tns::new(8, &tree, 16).unwrap();
tns.run_gauged(&c, 100, 1e-14).unwrap();
for q in 0..8 {
let (got, want) = (tns.expect1(q, Pauli::Z).unwrap(), exact(&c, q, Pauli::Z));
assert!((got - want).abs() < 1e-10, "trial {trial} q {q}: {got} vs {want}");
}
}
}
#[test]
fn belief_propagation_is_bit_identical_however_it_is_parallelised() {
let edges = heavy_hex_127();
let mut tns = Tns::new(127, &edges, 4).unwrap();
tns.run(&kicked_ising(127, &edges, 0.5, 3)).unwrap();
let a = tns.belief_propagation(30, 1e-12);
let b = tns.belief_propagation(30, 1e-12);
assert_eq!((a.sweeps, a.residual.to_bits()), (b.sweeps, b.residual.to_bits()));
assert_eq!(tns.expect1_bp(&a, 62, Pauli::Z).unwrap().to_bits(), tns.expect1_bp(&b, 62, Pauli::Z).unwrap().to_bits());
}
#[test]
fn truncation_is_reported_and_bounded_by_chi() {
let chain: Vec<(u32, u32)> = (0..9).map(|q| (q, q + 1)).collect();
let mut rng = Rng(77);
let c = random_on(&mut rng, 10, &chain, 200);
let mut tns = Tns::new(10, &chain, 4).unwrap();
tns.run(&c).unwrap();
assert!(tns.max_bond() <= 4);
assert!(tns.discarded() > 0.0, "a deep random circuit at χ = 4 must truncate");
}
#[test]
fn a_gate_off_the_graph_is_refused() {
let mut tns = Tns::new(3, &[(0, 1), (1, 2)], 4).unwrap();
assert_eq!(tns.apply(&Op::CX(0, 2)), Err(TnsError::NotAnEdge(0, 2)));
assert_eq!(
tns.apply(&Op::Rot { axis: vec![(0, Pauli::Z), (1, Pauli::Z), (2, Pauli::Z)], theta: 0.1 }),
Err(TnsError::TooWide(3))
);
}
#[test]
fn results_are_bit_identical_from_run_to_run() {
let c = kicked_ising(127, &heavy_hex_127(), 0.6, 3);
let run = || {
let mut t = Tns::new(127, &heavy_hex_127(), 8).unwrap();
t.run(&c).unwrap();
(t.expect1(62, Pauli::Z).unwrap().to_bits(), t.discarded().to_bits())
};
assert_eq!(run(), run());
}
#[test]
fn a_layer_computed_in_parallel_gives_the_bits_of_one_gate_at_a_time() {
let edges = heavy_hex_127();
let c = kicked_ising(127, &edges, 0.7, 3);
let mut batched = Tns::new(127, &edges, 8).unwrap();
batched.run(&c).unwrap();
let mut serial = Tns::new(127, &edges, 8).unwrap();
for op in &c.ops {
serial.apply(op).unwrap();
}
assert_eq!(batched.discarded().to_bits(), serial.discarded().to_bits());
for q in 0..127 {
assert_eq!(batched.expect1(q, Pauli::Z).unwrap().to_bits(), serial.expect1(q, Pauli::Z).unwrap().to_bits());
}
}
#[test]
fn the_heavy_hex_magnetization_tracks_sparse_pauli_dynamics() {
let edges = heavy_hex_127();
let c = kicked_ising(127, &edges, std::f64::consts::PI / 16.0, 5);
let mut tns = Tns::new(127, &edges, 8).unwrap();
tns.run(&c).unwrap();
let spd = expect_z(&c, 62, &SpdConfig { threshold: 1e-7, max_weight: None }).unwrap().value;
let bp = tns.expect1(62, Pauli::Z).unwrap();
assert!((bp - spd).abs() < 2e-3, "BP {bp} vs SPD {spd}");
}
}