use crate::error::GeomError;
use crate::fractals::Complex;
use crate::monte_carlo::Rng;
const QUANTUM_TOL: f64 = 1e-10;
const MAX_QUBITS: usize = 26;
const ZERO: Complex = Complex { re: 0.0, im: 0.0 };
const ONE: Complex = Complex { re: 1.0, im: 0.0 };
fn scale(z: Complex, k: f64) -> Complex {
Complex::new(z.re * k, z.im * k)
}
fn cis(theta: f64) -> Complex {
Complex::new(theta.cos(), theta.sin())
}
#[derive(Debug, Clone)]
pub struct QState {
pub n: usize,
pub amps: Vec<Complex>,
}
impl QState {
pub fn zero(n: usize) -> Result<Self, GeomError> {
Self::basis(n, 0)
}
pub fn basis(n: usize, index: u64) -> Result<Self, GeomError> {
if n == 0 || n > MAX_QUBITS {
return Err(GeomError::InvalidArgument("the qubit count is out of range"));
}
let size = 1usize << n;
if index as usize >= size {
return Err(GeomError::InvalidArgument("the basis index is out of range"));
}
let mut amps = vec![ZERO; size];
amps[index as usize] = ONE;
Ok(Self { n, amps })
}
pub fn from_amps(amps: Vec<Complex>) -> Result<Self, GeomError> {
if !amps.len().is_power_of_two() {
return Err(GeomError::InvalidArgument("the amplitude count must be a power of two"));
}
let n = amps.len().trailing_zeros() as usize;
if n == 0 || n > MAX_QUBITS {
return Err(GeomError::InvalidArgument("the qubit count is out of range"));
}
let mut state = Self { n, amps };
if state.norm() <= 0.0 {
return Err(GeomError::InvalidArgument("the state is identically zero"));
}
state.normalize();
Ok(state)
}
pub fn plus_all(n: usize) -> Result<Self, GeomError> {
if n == 0 || n > MAX_QUBITS {
return Err(GeomError::InvalidArgument("the qubit count is out of range"));
}
let size = 1usize << n;
let amplitude = 1.0 / (size as f64).sqrt();
Ok(Self { n, amps: vec![Complex::new(amplitude, 0.0); size] })
}
#[must_use]
pub fn len(&self) -> usize {
self.amps.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
false
}
#[must_use]
pub fn norm(&self) -> f64 {
self.amps.iter().map(|z| z.norm_sq()).sum::<f64>().sqrt()
}
pub fn normalize(&mut self) {
let n = self.norm();
if n > 0.0 {
let inverse = 1.0 / n;
for z in &mut self.amps {
*z = scale(*z, inverse);
}
}
}
#[must_use]
pub fn probability(&self, index: u64) -> f64 {
self.amps.get(index as usize).map_or(0.0, |z| z.norm_sq())
}
#[must_use]
pub fn probabilities(&self) -> Vec<f64> {
self.amps.iter().map(|z| z.norm_sq()).collect()
}
pub fn measure_all(&self, rng: &mut Rng) -> u64 {
let target = rng.next_f64() * self.amps.iter().map(|z| z.norm_sq()).sum::<f64>();
let mut running = 0.0;
for (index, z) in self.amps.iter().enumerate() {
running += z.norm_sq();
if running >= target {
return index as u64;
}
}
(self.len() - 1) as u64
}
pub fn measure_qubit(&self, q: usize, rng: &mut Rng) -> Result<(bool, Self), GeomError> {
if q >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
let mask = 1usize << q;
let one_weight: f64 = self
.amps
.iter()
.enumerate()
.filter(|(i, _)| i & mask != 0)
.map(|(_, z)| z.norm_sq())
.sum();
let outcome = rng.next_f64() < one_weight;
let weight = if outcome { one_weight } else { 1.0 - one_weight };
if weight <= 0.0 {
return Err(GeomError::Degenerate("the measured outcome has zero probability"));
}
let inverse = 1.0 / weight.sqrt();
let amps = self
.amps
.iter()
.enumerate()
.map(|(i, z)| if (i & mask != 0) == outcome { scale(*z, inverse) } else { ZERO })
.collect();
Ok((outcome, Self { n: self.n, amps }))
}
pub fn sample_counts(&self, shots: usize, rng: &mut Rng) -> Vec<(u64, u64)> {
let mut counts = std::collections::BTreeMap::new();
for _ in 0..shots {
*counts.entry(self.measure_all(rng)).or_insert(0u64) += 1;
}
counts.into_iter().collect()
}
pub fn expectation_z(&self, q: usize) -> Result<f64, GeomError> {
if q >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
let mask = 1usize << q;
Ok(self
.amps
.iter()
.enumerate()
.map(|(i, z)| if i & mask == 0 { z.norm_sq() } else { -z.norm_sq() })
.sum())
}
pub fn expectation_pauli_string(&self, pauli: &str) -> Result<f64, GeomError> {
if pauli.len() != self.n {
return Err(GeomError::InvalidArgument("the Pauli string has the wrong length"));
}
let mut rotated = self.clone();
for (position, symbol) in pauli.chars().enumerate() {
let q = self.n - 1 - position;
match symbol {
'I' | 'Z' => {}
'X' => rotated.apply_single(q, &Gate::h())?,
'Y' => {
rotated.apply_single(q, &Gate::sdg())?;
rotated.apply_single(q, &Gate::h())?;
}
_ => return Err(GeomError::InvalidArgument("unknown Pauli symbol")),
}
}
let acting: Vec<usize> = pauli
.chars()
.enumerate()
.filter(|(_, c)| *c != 'I')
.map(|(position, _)| self.n - 1 - position)
.collect();
Ok(rotated
.amps
.iter()
.enumerate()
.map(|(i, z)| {
let parity = acting.iter().filter(|&&q| i >> q & 1 == 1).count();
if parity % 2 == 0 {
z.norm_sq()
} else {
-z.norm_sq()
}
})
.sum())
}
pub fn inner(&self, other: &Self) -> Result<Complex, GeomError> {
if self.n != other.n {
return Err(GeomError::InvalidArgument("the states have different sizes"));
}
Ok(self
.amps
.iter()
.zip(&other.amps)
.fold(ZERO, |acc, (a, b)| acc + a.conjugate() * *b))
}
pub fn fidelity(&self, other: &Self) -> Result<f64, GeomError> {
Ok(self.inner(other)?.norm_sq())
}
pub fn apply_single(&mut self, q: usize, gate: &Gate) -> Result<(), GeomError> {
if q >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
let mask = 1usize << q;
for i in 0..self.len() {
if i & mask != 0 {
continue;
}
let (a, b) = (self.amps[i], self.amps[i | mask]);
self.amps[i] = gate.matrix[0][0] * a + gate.matrix[0][1] * b;
self.amps[i | mask] = gate.matrix[1][0] * a + gate.matrix[1][1] * b;
}
Ok(())
}
pub fn apply_controlled(
&mut self,
control: usize,
target: usize,
gate: &Gate,
) -> Result<(), GeomError> {
if control >= self.n || target >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
if control == target {
return Err(GeomError::InvalidArgument("a gate cannot control itself"));
}
let control_mask = 1usize << control;
let target_mask = 1usize << target;
for i in 0..self.len() {
if i & target_mask != 0 || i & control_mask == 0 {
continue;
}
let (a, b) = (self.amps[i], self.amps[i | target_mask]);
self.amps[i] = gate.matrix[0][0] * a + gate.matrix[0][1] * b;
self.amps[i | target_mask] = gate.matrix[1][0] * a + gate.matrix[1][1] * b;
}
Ok(())
}
pub fn apply_ccx(&mut self, a: usize, b: usize, target: usize) -> Result<(), GeomError> {
if a >= self.n || b >= self.n || target >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
if a == b || a == target || b == target {
return Err(GeomError::InvalidArgument("the Toffoli qubits must be distinct"));
}
let controls = (1usize << a) | (1usize << b);
let mask = 1usize << target;
for i in 0..self.len() {
if i & controls == controls && i & mask == 0 {
self.amps.swap(i, i | mask);
}
}
Ok(())
}
pub fn apply_swap(&mut self, a: usize, b: usize) -> Result<(), GeomError> {
if a >= self.n || b >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
if a == b {
return Ok(());
}
let (ma, mb) = (1usize << a, 1usize << b);
for i in 0..self.len() {
if i & ma != 0 && i & mb == 0 {
self.amps.swap(i, (i & !ma) | mb);
}
}
Ok(())
}
pub fn reduced_density_matrix(&self, keep: &[usize]) -> Result<Vec<Vec<Complex>>, GeomError> {
if keep.is_empty() || keep.len() > self.n {
return Err(GeomError::InvalidArgument("the kept set is the wrong size"));
}
let mut seen = vec![false; self.n];
for &q in keep {
if q >= self.n || seen[q] {
return Err(GeomError::InvalidArgument("the kept qubits must be distinct"));
}
seen[q] = true;
}
let traced: Vec<usize> = (0..self.n).filter(|q| !seen[*q]).collect();
let kept_size = 1usize << keep.len();
let traced_size = 1usize << traced.len();
let assemble = |kept_index: usize, traced_index: usize| -> usize {
let mut full = 0usize;
for (bit, &q) in keep.iter().enumerate() {
if kept_index >> bit & 1 == 1 {
full |= 1 << q;
}
}
for (bit, &q) in traced.iter().enumerate() {
if traced_index >> bit & 1 == 1 {
full |= 1 << q;
}
}
full
};
let mut rho = vec![vec![ZERO; kept_size]; kept_size];
for t in 0..traced_size {
for r in 0..kept_size {
for c in 0..kept_size {
let a = self.amps[assemble(r, t)];
let b = self.amps[assemble(c, t)];
rho[r][c] = rho[r][c] + a * b.conjugate();
}
}
}
Ok(rho)
}
pub fn schmidt_coefficients(&self, partition: &[usize]) -> Result<Vec<f64>, GeomError> {
let rho = self.reduced_density_matrix(partition)?;
let mut values = hermitian_eigenvalues(&rho)?;
values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
Ok(values.iter().map(|v| v.max(0.0).sqrt()).collect())
}
pub fn entanglement_entropy(&self, partition: &[usize]) -> Result<f64, GeomError> {
let rho = self.reduced_density_matrix(partition)?;
let values = hermitian_eigenvalues(&rho)?;
Ok(values
.iter()
.filter(|v| **v > 1e-12)
.map(|v| -v * v.log2())
.sum())
}
pub fn bloch_vector(&self, q: usize) -> Result<(f64, f64, f64), GeomError> {
let rho = self.reduced_density_matrix(&[q])?;
Ok((
2.0 * rho[0][1].re,
-2.0 * rho[0][1].im,
rho[0][0].re - rho[1][1].re,
))
}
}
fn hermitian_eigenvalues(m: &[Vec<Complex>]) -> Result<Vec<f64>, GeomError> {
let n = m.len();
if n == 0 || m.iter().any(|row| row.len() != n) {
return Err(GeomError::InvalidArgument("the matrix is not square"));
}
let mut embedded = crate::linalg::matrix::Matrix::zeros(2 * n, 2 * n);
for i in 0..n {
for j in 0..n {
embedded.set(i, j, m[i][j].re);
embedded.set(i + n, j + n, m[i][j].re);
embedded.set(i, j + n, -m[i][j].im);
embedded.set(i + n, j, m[i][j].im);
}
}
let decomposition = crate::linalg::eigen::eigen_symmetric(&embedded, 1e-13, 200)
.map_err(|_| GeomError::Degenerate("the density matrix eigenproblem failed"))?;
Ok(decomposition.values.iter().step_by(2).copied().collect())
}
#[derive(Debug, Clone, Copy)]
pub struct Gate {
pub matrix: [[Complex; 2]; 2],
}
impl Gate {
pub fn from_matrix(matrix: [[Complex; 2]; 2]) -> Result<Self, GeomError> {
let gate = Self { matrix };
if !gate.is_unitary(QUANTUM_TOL) {
return Err(GeomError::InvalidArgument("the gate matrix is not unitary"));
}
Ok(gate)
}
#[must_use]
pub fn is_unitary(&self, tol: f64) -> bool {
for i in 0..2 {
for j in 0..2 {
let entry = (0..2)
.fold(ZERO, |acc, k| acc + self.matrix[k][i].conjugate() * self.matrix[k][j]);
let expected = if i == j { 1.0 } else { 0.0 };
if (entry.re - expected).abs() > tol || entry.im.abs() > tol {
return false;
}
}
}
true
}
#[must_use]
pub fn dagger(&self) -> Self {
Self {
matrix: [
[self.matrix[0][0].conjugate(), self.matrix[1][0].conjugate()],
[self.matrix[0][1].conjugate(), self.matrix[1][1].conjugate()],
],
}
}
#[must_use]
pub fn identity() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, ONE]] }
}
#[must_use]
pub fn x() -> Self {
Self { matrix: [[ZERO, ONE], [ONE, ZERO]] }
}
#[must_use]
pub fn y() -> Self {
Self {
matrix: [
[ZERO, Complex::new(0.0, -1.0)],
[Complex::new(0.0, 1.0), ZERO],
],
}
}
#[must_use]
pub fn z() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, Complex::new(-1.0, 0.0)]] }
}
#[must_use]
pub fn h() -> Self {
let a = Complex::new(std::f64::consts::FRAC_1_SQRT_2, 0.0);
Self { matrix: [[a, a], [a, scale(a, -1.0)]] }
}
#[must_use]
pub fn s() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, Complex::new(0.0, 1.0)]] }
}
#[must_use]
pub fn sdg() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, Complex::new(0.0, -1.0)]] }
}
#[must_use]
pub fn t() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, cis(std::f64::consts::FRAC_PI_4)]] }
}
#[must_use]
pub fn tdg() -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, cis(-std::f64::consts::FRAC_PI_4)]] }
}
#[must_use]
pub fn rx(theta: f64) -> Self {
let c = Complex::new((theta / 2.0).cos(), 0.0);
let s = Complex::new(0.0, -(theta / 2.0).sin());
Self { matrix: [[c, s], [s, c]] }
}
#[must_use]
pub fn ry(theta: f64) -> Self {
let c = Complex::new((theta / 2.0).cos(), 0.0);
let s = Complex::new((theta / 2.0).sin(), 0.0);
Self { matrix: [[c, scale(s, -1.0)], [s, c]] }
}
#[must_use]
pub fn rz(theta: f64) -> Self {
Self { matrix: [[cis(-theta / 2.0), ZERO], [ZERO, cis(theta / 2.0)]] }
}
#[must_use]
pub fn phase(phi: f64) -> Self {
Self { matrix: [[ONE, ZERO], [ZERO, cis(phi)]] }
}
#[must_use]
pub fn u3(theta: f64, phi: f64, lambda: f64) -> Self {
let c = (theta / 2.0).cos();
let s = (theta / 2.0).sin();
Self {
matrix: [
[Complex::new(c, 0.0), scale(cis(lambda), -s)],
[scale(cis(phi), s), scale(cis(phi + lambda), c)],
],
}
}
#[must_use]
pub fn sqrt_x() -> Self {
let half = Complex::new(0.5, 0.5);
let other = Complex::new(0.5, -0.5);
Self { matrix: [[half, other], [other, half]] }
}
}
#[derive(Debug, Clone)]
pub enum Op {
Single(usize, Gate),
Controlled(usize, usize, Gate),
CCX(usize, usize, usize),
Swap(usize, usize),
Barrier,
}
#[derive(Debug, Clone)]
pub struct Circuit {
pub n: usize,
pub ops: Vec<Op>,
}
impl Circuit {
pub fn new(n: usize) -> Result<Self, GeomError> {
if n == 0 || n > MAX_QUBITS {
return Err(GeomError::InvalidArgument("the qubit count is out of range"));
}
Ok(Self { n, ops: Vec::new() })
}
pub fn gate(&mut self, q: usize, gate: Gate) -> &mut Self {
self.ops.push(Op::Single(q, gate));
self
}
pub fn x(&mut self, q: usize) -> &mut Self {
self.gate(q, Gate::x())
}
pub fn y(&mut self, q: usize) -> &mut Self {
self.gate(q, Gate::y())
}
pub fn z(&mut self, q: usize) -> &mut Self {
self.gate(q, Gate::z())
}
pub fn h(&mut self, q: usize) -> &mut Self {
self.gate(q, Gate::h())
}
pub fn rx(&mut self, q: usize, theta: f64) -> &mut Self {
self.gate(q, Gate::rx(theta))
}
pub fn ry(&mut self, q: usize, theta: f64) -> &mut Self {
self.gate(q, Gate::ry(theta))
}
pub fn rz(&mut self, q: usize, theta: f64) -> &mut Self {
self.gate(q, Gate::rz(theta))
}
pub fn phase(&mut self, q: usize, phi: f64) -> &mut Self {
self.gate(q, Gate::phase(phi))
}
pub fn cx(&mut self, control: usize, target: usize) -> &mut Self {
self.ops.push(Op::Controlled(control, target, Gate::x()));
self
}
pub fn cz(&mut self, control: usize, target: usize) -> &mut Self {
self.ops.push(Op::Controlled(control, target, Gate::z()));
self
}
pub fn cphase(&mut self, control: usize, target: usize, phi: f64) -> &mut Self {
self.ops.push(Op::Controlled(control, target, Gate::phase(phi)));
self
}
pub fn ccx(&mut self, a: usize, b: usize, target: usize) -> &mut Self {
self.ops.push(Op::CCX(a, b, target));
self
}
pub fn swap(&mut self, a: usize, b: usize) -> &mut Self {
self.ops.push(Op::Swap(a, b));
self
}
pub fn barrier(&mut self) -> &mut Self {
self.ops.push(Op::Barrier);
self
}
pub fn append(&mut self, other: &Self) -> Result<&mut Self, GeomError> {
if other.n != self.n {
return Err(GeomError::InvalidArgument("the circuits have different widths"));
}
self.ops.extend(other.ops.iter().cloned());
Ok(self)
}
#[must_use]
pub fn inverse(&self) -> Self {
let ops = self
.ops
.iter()
.rev()
.map(|op| match op {
Op::Single(q, g) => Op::Single(*q, g.dagger()),
Op::Controlled(c, t, g) => Op::Controlled(*c, *t, g.dagger()),
Op::CCX(a, b, t) => Op::CCX(*a, *b, *t),
Op::Swap(a, b) => Op::Swap(*a, *b),
Op::Barrier => Op::Barrier,
})
.collect();
Self { n: self.n, ops }
}
#[must_use]
pub fn gate_count(&self) -> usize {
self.ops.iter().filter(|op| !matches!(op, Op::Barrier)).count()
}
#[must_use]
pub fn depth(&self) -> usize {
let mut layer = vec![0usize; self.n];
for op in &self.ops {
let touched: Vec<usize> = match op {
Op::Single(q, _) => vec![*q],
Op::Controlled(c, t, _) | Op::Swap(c, t) => vec![*c, *t],
Op::CCX(a, b, t) => vec![*a, *b, *t],
Op::Barrier => continue,
};
let next = touched.iter().map(|&q| layer[q]).max().unwrap_or(0) + 1;
for &q in &touched {
layer[q] = next;
}
}
layer.into_iter().max().unwrap_or(0)
}
pub fn run(&self, initial: &QState) -> Result<QState, GeomError> {
if initial.n != self.n {
return Err(GeomError::InvalidArgument("the state has the wrong width"));
}
let mut state = initial.clone();
for op in &self.ops {
match op {
Op::Single(q, g) => state.apply_single(*q, g)?,
Op::Controlled(c, t, g) => state.apply_controlled(*c, *t, g)?,
Op::CCX(a, b, t) => state.apply_ccx(*a, *b, *t)?,
Op::Swap(a, b) => state.apply_swap(*a, *b)?,
Op::Barrier => {}
}
}
Ok(state)
}
pub fn run_shots(&self, shots: usize, rng: &mut Rng) -> Result<Vec<(u64, u64)>, GeomError> {
let state = self.run(&QState::zero(self.n)?)?;
Ok(state.sample_counts(shots, rng))
}
pub fn unitary_small(&self) -> Result<Vec<Vec<Complex>>, GeomError> {
if self.n > 10 {
return Err(GeomError::InvalidArgument("unitary_small is capped at ten qubits"));
}
let size = 1usize << self.n;
let mut columns = vec![vec![ZERO; size]; size];
for column in 0..size {
let out = self.run(&QState::basis(self.n, column as u64)?)?;
for (row, z) in out.amps.iter().enumerate() {
columns[row][column] = *z;
}
}
Ok(columns)
}
#[must_use]
pub fn to_qasm_lite(&self) -> String {
let mut out = format!("qubits {}\n", self.n);
for op in &self.ops {
match op {
Op::Single(q, g) => out.push_str(&format!("u {} {}\n", q, gate_name(g))),
Op::Controlled(c, t, g) => {
out.push_str(&format!("c{} {} {}\n", gate_name(g), c, t));
}
Op::CCX(a, b, t) => out.push_str(&format!("ccx {a} {b} {t}\n")),
Op::Swap(a, b) => out.push_str(&format!("swap {a} {b}\n")),
Op::Barrier => out.push_str("barrier\n"),
}
}
out
}
#[must_use]
pub fn draw_ascii(&self) -> String {
let mut rows: Vec<String> = (0..self.n).map(|q| format!("q{q}: ")).collect();
for op in &self.ops {
let labels: Vec<(usize, String)> = match op {
Op::Single(q, g) => vec![(*q, format!("-{}-", gate_name(g)))],
Op::Controlled(c, t, g) => {
vec![(*c, "-*-".into()), (*t, format!("-{}-", gate_name(g)))]
}
Op::CCX(a, b, t) => {
vec![(*a, "-*-".into()), (*b, "-*-".into()), (*t, "-X-".into())]
}
Op::Swap(a, b) => vec![(*a, "-x-".into()), (*b, "-x-".into())],
Op::Barrier => (0..self.n).map(|q| (q, "-|-".into())).collect(),
};
let width = labels.iter().map(|(_, s)| s.len()).max().unwrap_or(3);
for q in 0..self.n {
let piece = labels
.iter()
.find(|(target, _)| *target == q)
.map_or_else(|| "-".repeat(width), |(_, s)| s.clone());
rows[q].push_str(&format!("{piece:-<width$}"));
}
}
rows.join("\n")
}
}
fn gate_name(g: &Gate) -> String {
for (name, candidate) in [
("I", Gate::identity()),
("X", Gate::x()),
("Y", Gate::y()),
("Z", Gate::z()),
("H", Gate::h()),
("S", Gate::s()),
("SD", Gate::sdg()),
("T", Gate::t()),
("TD", Gate::tdg()),
("SX", Gate::sqrt_x()),
] {
let same = (0..2).all(|i| {
(0..2).all(|j| {
(g.matrix[i][j].re - candidate.matrix[i][j].re).abs() < 1e-12
&& (g.matrix[i][j].im - candidate.matrix[i][j].im).abs() < 1e-12
})
});
if same {
return name.into();
}
}
"U".into()
}
#[derive(Debug, Clone)]
pub struct DensityMatrix {
pub n: usize,
pub rho: Vec<Vec<Complex>>,
}
impl DensityMatrix {
#[must_use]
pub fn from_state(state: &QState) -> Self {
let size = state.len();
let mut rho = vec![vec![ZERO; size]; size];
for i in 0..size {
for j in 0..size {
rho[i][j] = state.amps[i] * state.amps[j].conjugate();
}
}
Self { n: state.n, rho }
}
pub fn from_mixture(states: &[QState], weights: &[f64]) -> Result<Self, GeomError> {
if states.is_empty() || states.len() != weights.len() {
return Err(GeomError::InvalidArgument("from_mixture: mismatched input"));
}
if states.iter().any(|s| s.n != states[0].n) {
return Err(GeomError::InvalidArgument("the states have different widths"));
}
if weights.iter().any(|w| *w < 0.0)
|| (weights.iter().sum::<f64>() - 1.0).abs() > QUANTUM_TOL
{
return Err(GeomError::InvalidArgument("the weights must be a distribution"));
}
let size = states[0].len();
let mut rho = vec![vec![ZERO; size]; size];
for (state, &w) in states.iter().zip(weights) {
for i in 0..size {
for j in 0..size {
rho[i][j] = rho[i][j] + scale(state.amps[i] * state.amps[j].conjugate(), w);
}
}
}
Ok(Self { n: states[0].n, rho })
}
#[must_use]
pub fn trace(&self) -> Complex {
(0..self.rho.len()).fold(ZERO, |acc, i| acc + self.rho[i][i])
}
#[must_use]
pub fn purity(&self) -> f64 {
let size = self.rho.len();
let mut total = 0.0;
for i in 0..size {
for j in 0..size {
total += (self.rho[i][j] * self.rho[j][i]).re;
}
}
total
}
pub fn von_neumann_entropy(&self) -> Result<f64, GeomError> {
let values = hermitian_eigenvalues(&self.rho)?;
Ok(values.iter().filter(|v| **v > 1e-12).map(|v| -v * v.log2()).sum())
}
#[must_use]
pub fn is_valid(&self, tol: f64) -> bool {
let size = self.rho.len();
let trace = self.trace();
if (trace.re - 1.0).abs() > tol || trace.im.abs() > tol {
return false;
}
for i in 0..size {
for j in 0..size {
let a = self.rho[i][j];
let b = self.rho[j][i].conjugate();
if (a.re - b.re).abs() > tol || (a.im - b.im).abs() > tol {
return false;
}
}
}
hermitian_eigenvalues(&self.rho)
.map(|values| values.iter().all(|v| *v > -tol))
.unwrap_or(false)
}
pub fn apply_gate(&mut self, q: usize, gate: &Gate) -> Result<(), GeomError> {
if q >= self.n {
return Err(GeomError::InvalidArgument("the qubit index is out of range"));
}
let full = lift_single(self.n, q, gate);
self.rho = conjugate(&full, &self.rho);
Ok(())
}
pub fn apply_channel(&mut self, kraus: &[Vec<Vec<Complex>>]) -> Result<(), GeomError> {
let size = self.rho.len();
if kraus.is_empty() || kraus.iter().any(|k| k.len() != size || k.iter().any(|r| r.len() != size)) {
return Err(GeomError::InvalidArgument("the Kraus operators are the wrong size"));
}
if !is_trace_preserving(kraus, QUANTUM_TOL) {
return Err(GeomError::InvalidArgument("the channel is not trace preserving"));
}
let mut out = vec![vec![ZERO; size]; size];
for k in kraus {
let piece = conjugate(k, &self.rho);
for i in 0..size {
for j in 0..size {
out[i][j] = out[i][j] + piece[i][j];
}
}
}
self.rho = out;
Ok(())
}
pub fn partial_trace(&self, keep: &[usize]) -> Result<Self, GeomError> {
if keep.is_empty() || keep.len() > self.n {
return Err(GeomError::InvalidArgument("the kept set is the wrong size"));
}
let mut seen = vec![false; self.n];
for &q in keep {
if q >= self.n || seen[q] {
return Err(GeomError::InvalidArgument("the kept qubits must be distinct"));
}
seen[q] = true;
}
let traced: Vec<usize> = (0..self.n).filter(|q| !seen[*q]).collect();
let kept_size = 1usize << keep.len();
let traced_size = 1usize << traced.len();
let assemble = |kept_index: usize, traced_index: usize| -> usize {
let mut full = 0usize;
for (bit, &q) in keep.iter().enumerate() {
if kept_index >> bit & 1 == 1 {
full |= 1 << q;
}
}
for (bit, &q) in traced.iter().enumerate() {
if traced_index >> bit & 1 == 1 {
full |= 1 << q;
}
}
full
};
let mut out = vec![vec![ZERO; kept_size]; kept_size];
for t in 0..traced_size {
for r in 0..kept_size {
for c in 0..kept_size {
out[r][c] = out[r][c] + self.rho[assemble(r, t)][assemble(c, t)];
}
}
}
Ok(Self { n: keep.len(), rho: out })
}
}
fn is_trace_preserving(kraus: &[Vec<Vec<Complex>>], tol: f64) -> bool {
let size = kraus[0].len();
let mut total = vec![vec![ZERO; size]; size];
for k in kraus {
for i in 0..size {
for j in 0..size {
let entry = (0..size).fold(ZERO, |acc, r| acc + k[r][i].conjugate() * k[r][j]);
total[i][j] = total[i][j] + entry;
}
}
}
for i in 0..size {
for j in 0..size {
let expected = if i == j { 1.0 } else { 0.0 };
if (total[i][j].re - expected).abs() > tol || total[i][j].im.abs() > tol {
return false;
}
}
}
true
}
fn conjugate(m: &[Vec<Complex>], rho: &[Vec<Complex>]) -> Vec<Vec<Complex>> {
let size = rho.len();
let mut left = vec![vec![ZERO; size]; size];
for i in 0..size {
for j in 0..size {
left[i][j] = (0..size).fold(ZERO, |acc, k| acc + m[i][k] * rho[k][j]);
}
}
let mut out = vec![vec![ZERO; size]; size];
for i in 0..size {
for j in 0..size {
out[i][j] = (0..size).fold(ZERO, |acc, k| acc + left[i][k] * m[j][k].conjugate());
}
}
out
}
fn lift_single(n: usize, q: usize, gate: &Gate) -> Vec<Vec<Complex>> {
let size = 1usize << n;
let mask = 1usize << q;
let mut out = vec![vec![ZERO; size]; size];
for i in 0..size {
for j in 0..size {
if i & !mask != j & !mask {
continue;
}
let row = usize::from(i & mask != 0);
let column = usize::from(j & mask != 0);
out[i][j] = gate.matrix[row][column];
}
}
out
}
fn from_rows(rows: [[Complex; 2]; 2]) -> Vec<Vec<Complex>> {
vec![rows[0].to_vec(), rows[1].to_vec()]
}
pub fn depolarizing_channel(p: f64) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
if !(0.0..=1.0).contains(&p) {
return Err(GeomError::InvalidArgument("the error rate must be a probability"));
}
let keep = (1.0 - 3.0 * p / 4.0).max(0.0).sqrt();
let each = (p / 4.0).sqrt();
Ok(vec![
from_rows([[scale(ONE, keep), ZERO], [ZERO, scale(ONE, keep)]]),
from_rows([[ZERO, scale(ONE, each)], [scale(ONE, each), ZERO]]),
from_rows([
[ZERO, Complex::new(0.0, -each)],
[Complex::new(0.0, each), ZERO],
]),
from_rows([[scale(ONE, each), ZERO], [ZERO, scale(ONE, -each)]]),
])
}
pub fn amplitude_damping(gamma: f64) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
if !(0.0..=1.0).contains(&gamma) {
return Err(GeomError::InvalidArgument("gamma must be a probability"));
}
Ok(vec![
from_rows([[ONE, ZERO], [ZERO, scale(ONE, (1.0 - gamma).sqrt())]]),
from_rows([[ZERO, scale(ONE, gamma.sqrt())], [ZERO, ZERO]]),
])
}
pub fn phase_damping(gamma: f64) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
if !(0.0..=1.0).contains(&gamma) {
return Err(GeomError::InvalidArgument("gamma must be a probability"));
}
Ok(vec![
from_rows([[ONE, ZERO], [ZERO, scale(ONE, (1.0 - gamma).sqrt())]]),
from_rows([[ZERO, ZERO], [ZERO, scale(ONE, gamma.sqrt())]]),
])
}
pub fn bit_flip(p: f64) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
pauli_channel(p, Gate::x())
}
pub fn phase_flip(p: f64) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
pauli_channel(p, Gate::z())
}
fn pauli_channel(p: f64, gate: Gate) -> Result<Vec<Vec<Vec<Complex>>>, GeomError> {
if !(0.0..=1.0).contains(&p) {
return Err(GeomError::InvalidArgument("the error rate must be a probability"));
}
let keep = (1.0 - p).sqrt();
let flip = p.sqrt();
Ok(vec![
from_rows([[scale(ONE, keep), ZERO], [ZERO, scale(ONE, keep)]]),
from_rows([
[scale(gate.matrix[0][0], flip), scale(gate.matrix[0][1], flip)],
[scale(gate.matrix[1][0], flip), scale(gate.matrix[1][1], flip)],
]),
])
}
pub fn bell_state(which: u8) -> Result<QState, GeomError> {
if which > 3 {
return Err(GeomError::InvalidArgument("there are four Bell states"));
}
let mut circuit = Circuit::new(2)?;
if which & 2 != 0 {
circuit.x(1);
}
if which & 1 != 0 {
circuit.x(0);
}
circuit.h(1).cx(1, 0);
circuit.run(&QState::zero(2)?)
}
pub fn ghz(n: usize) -> Result<QState, GeomError> {
if n < 2 {
return Err(GeomError::InvalidArgument("a GHZ state needs at least two qubits"));
}
let mut circuit = Circuit::new(n)?;
circuit.h(0);
for q in 1..n {
circuit.cx(0, q);
}
circuit.run(&QState::zero(n)?)
}
pub fn w_state(n: usize) -> Result<QState, GeomError> {
if !(2..=MAX_QUBITS).contains(&n) {
return Err(GeomError::InvalidArgument("a W state needs two qubits or more"));
}
let amplitude = 1.0 / (n as f64).sqrt();
let mut amps = vec![ZERO; 1usize << n];
for q in 0..n {
amps[1usize << q] = Complex::new(amplitude, 0.0);
}
Ok(QState { n, amps })
}
pub fn random_state(n: usize, rng: &mut Rng) -> Result<QState, GeomError> {
if n == 0 || n > MAX_QUBITS {
return Err(GeomError::InvalidArgument("the qubit count is out of range"));
}
let size = 1usize << n;
let mut amps = Vec::with_capacity(size);
for _ in 0..size {
let u1 = rng.next_f64().max(1e-300);
let u2 = rng.next_f64();
let radius = (-2.0 * u1.ln()).sqrt();
let angle = 2.0 * std::f64::consts::PI * u2;
amps.push(Complex::new(radius * angle.cos(), radius * angle.sin()));
}
QState::from_amps(amps)
}
pub fn chsh_value(state: &QState, angles: (f64, f64, f64, f64)) -> Result<f64, GeomError> {
if state.n != 2 {
return Err(GeomError::InvalidArgument("CHSH is a two-qubit quantity"));
}
let (a, a_prime, b, b_prime) = angles;
let correlate = |theta_a: f64, theta_b: f64| -> Result<f64, GeomError> {
let mut rotated = state.clone();
rotated.apply_single(1, &Gate::ry(-theta_a))?;
rotated.apply_single(0, &Gate::ry(-theta_b))?;
rotated.expectation_pauli_string("ZZ")
};
Ok(correlate(a, b)? - correlate(a, b_prime)? + correlate(a_prime, b)?
+ correlate(a_prime, b_prime)?)
}
#[must_use]
pub fn chsh_optimal_angles() -> (f64, f64, f64, f64) {
let q = std::f64::consts::FRAC_PI_4;
(0.0, 2.0 * q, q, 3.0 * q)
}
pub fn quantum_teleportation_demo(
theta: f64,
phi: f64,
rng: &mut Rng,
) -> Result<((f64, f64, f64), (f64, f64, f64)), GeomError> {
let mut state = QState::zero(3)?;
state.apply_single(2, &Gate::u3(theta, phi, 0.0))?;
let input = state.bloch_vector(2)?;
state.apply_single(1, &Gate::h())?;
state.apply_controlled(1, 0, &Gate::x())?;
state.apply_controlled(2, 1, &Gate::x())?;
state.apply_single(2, &Gate::h())?;
let (bit1, state) = state.measure_qubit(1, rng)?;
let (bit2, mut state) = state.measure_qubit(2, rng)?;
if bit1 {
state.apply_single(0, &Gate::x())?;
}
if bit2 {
state.apply_single(0, &Gate::z())?;
}
let output = state.bloch_vector(0)?;
Ok((input, output))
}
pub fn superdense_coding_demo(bits: (bool, bool)) -> Result<(bool, bool), GeomError> {
let mut state = bell_state(0)?;
if bits.1 {
state.apply_single(1, &Gate::x())?;
}
if bits.0 {
state.apply_single(1, &Gate::z())?;
}
state.apply_controlled(1, 0, &Gate::x())?;
state.apply_single(1, &Gate::h())?;
let outcome = state
.probabilities()
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(index, _)| index)
.unwrap_or(0);
Ok((outcome & 2 != 0, outcome & 1 != 0))
}
#[must_use]
pub fn no_cloning_fidelity_bound() -> f64 {
5.0 / 6.0
}
pub fn pauli_decompose(h: &[Vec<Complex>]) -> Result<Vec<(String, f64)>, GeomError> {
let size = h.len();
if (size != 2 && size != 4) || h.iter().any(|row| row.len() != size) {
return Err(GeomError::InvalidArgument("pauli_decompose handles one or two qubits"));
}
let qubits = size.trailing_zeros() as usize;
let symbols = ['I', 'X', 'Y', 'Z'];
let single = |c: char| -> Gate {
match c {
'X' => Gate::x(),
'Y' => Gate::y(),
'Z' => Gate::z(),
_ => Gate::identity(),
}
};
let mut out = Vec::new();
let combinations = 4usize.pow(qubits as u32);
for code in 0..combinations {
let name: String = (0..qubits)
.rev()
.map(|k| symbols[(code >> (2 * k)) & 3])
.collect();
let mut trace = ZERO;
for i in 0..size {
for j in 0..size {
let mut entry = ONE;
for k in 0..qubits {
let gate = single(symbols[(code >> (2 * (qubits - 1 - k))) & 3]);
let row = (i >> (qubits - 1 - k)) & 1;
let column = (j >> (qubits - 1 - k)) & 1;
entry = entry * gate.matrix[row][column];
}
trace = trace + entry * h[j][i];
}
}
let coefficient = trace.re / size as f64;
if coefficient.abs() > 1e-12 {
out.push((name, coefficient));
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn matrix_close(a: &[Vec<Complex>], b: &[Vec<Complex>], tol: f64) -> bool {
a.len() == b.len()
&& a.iter().zip(b).all(|(ra, rb)| {
ra.len() == rb.len()
&& ra.iter().zip(rb).all(|(x, y)| {
(x.re - y.re).abs() < tol && (x.im - y.im).abs() < tol
})
})
}
fn identity_matrix(size: usize) -> Vec<Vec<Complex>> {
(0..size)
.map(|i| (0..size).map(|j| if i == j { ONE } else { ZERO }).collect())
.collect()
}
#[test]
fn every_named_gate_is_unitary_and_its_own_stated_inverse() {
let named: Vec<(&str, Gate)> = vec![
("I", Gate::identity()),
("X", Gate::x()),
("Y", Gate::y()),
("Z", Gate::z()),
("H", Gate::h()),
("S", Gate::s()),
("Sdg", Gate::sdg()),
("T", Gate::t()),
("Tdg", Gate::tdg()),
("sqrtX", Gate::sqrt_x()),
("rx", Gate::rx(0.7)),
("ry", Gate::ry(-1.3)),
("rz", Gate::rz(2.2)),
("phase", Gate::phase(0.4)),
("u3", Gate::u3(0.6, 1.1, -0.3)),
];
for (name, gate) in &named {
assert!(gate.is_unitary(1e-12), "{name} is not unitary");
let mut state = QState::from_amps(vec![
Complex::new(0.6, 0.2),
Complex::new(-0.3, 0.7),
])
.unwrap();
let original = state.clone();
state.apply_single(0, gate).unwrap();
state.apply_single(0, &gate.dagger()).unwrap();
for (a, b) in state.amps.iter().zip(&original.amps) {
assert!(
(a.re - b.re).abs() < 1e-12 && (a.im - b.im).abs() < 1e-12,
"{name} followed by its adjoint is not the identity"
);
}
}
for (a, b) in [(Gate::s(), Gate::sdg()), (Gate::t(), Gate::tdg())] {
for i in 0..2 {
for j in 0..2 {
let entry =
(0..2).fold(ZERO, |acc, k| acc + a.matrix[i][k] * b.matrix[k][j]);
let expected = f64::from(i == j);
assert!(close(entry.re, expected, 1e-12) && close(entry.im, 0.0, 1e-12));
}
}
}
assert!(Gate::from_matrix([[ONE, ONE], [ZERO, ONE]]).is_err());
assert!(Gate::from_matrix([[scale(ONE, 2.0), ZERO], [ZERO, ONE]]).is_err());
assert!(Gate::from_matrix(Gate::h().matrix).is_ok());
}
#[test]
fn the_pauli_algebra_holds_as_the_gates_are_defined() {
let multiply = |a: &Gate, b: &Gate| -> [[Complex; 2]; 2] {
let mut out = [[ZERO; 2]; 2];
for i in 0..2 {
for j in 0..2 {
out[i][j] = (0..2).fold(ZERO, |acc, k| acc + a.matrix[i][k] * b.matrix[k][j]);
}
}
out
};
let i_times = |g: &Gate| -> [[Complex; 2]; 2] {
let mut out = [[ZERO; 2]; 2];
for r in 0..2 {
for c in 0..2 {
out[r][c] = Complex::new(0.0, 1.0) * g.matrix[r][c];
}
}
out
};
let same = |a: &[[Complex; 2]; 2], b: &[[Complex; 2]; 2]| -> bool {
(0..2).all(|i| {
(0..2).all(|j| {
(a[i][j].re - b[i][j].re).abs() < 1e-12
&& (a[i][j].im - b[i][j].im).abs() < 1e-12
})
})
};
assert!(same(&multiply(&Gate::x(), &Gate::y()), &i_times(&Gate::z())), "X Y != i Z");
assert!(same(&multiply(&Gate::y(), &Gate::z()), &i_times(&Gate::x())), "Y Z != i X");
assert!(same(&multiply(&Gate::z(), &Gate::x()), &i_times(&Gate::y())), "Z X != i Y");
for g in [Gate::x(), Gate::y(), Gate::z(), Gate::h()] {
assert!(same(&multiply(&g, &g), &Gate::identity().matrix), "a Pauli did not square to I");
}
assert!(same(&multiply(&Gate::s(), &Gate::s()), &Gate::z().matrix));
assert!(same(&multiply(&Gate::t(), &Gate::t()), &Gate::s().matrix));
assert!(same(&multiply(&Gate::sqrt_x(), &Gate::sqrt_x()), &Gate::x().matrix));
let full = Gate::rx(2.0 * std::f64::consts::PI);
assert!(close(full.matrix[0][0].re, -1.0, 1e-12), "rx(2 pi) is {:?}", full.matrix[0][0]);
let double = Gate::rx(4.0 * std::f64::consts::PI);
assert!(close(double.matrix[0][0].re, 1.0, 1e-12));
}
#[test]
fn a_hadamard_on_each_qubit_makes_the_uniform_superposition() {
for n in 1..=5usize {
let mut circuit = Circuit::new(n).unwrap();
for q in 0..n {
circuit.h(q);
}
let state = circuit.run(&QState::zero(n).unwrap()).unwrap();
let expected = 1.0 / (1usize << n) as f64;
for p in state.probabilities() {
assert!(close(p, expected, 1e-12), "an outcome has probability {p}");
}
let direct = QState::plus_all(n).unwrap();
assert!(close(state.fidelity(&direct).unwrap(), 1.0, 1e-12));
let back = circuit.run(&state).unwrap();
assert!(close(back.probability(0), 1.0, 1e-12), "H twice is not the identity");
}
}
#[test]
fn measurement_probabilities_match_the_amplitudes_and_the_collapse_is_consistent() {
let mut rng = Rng::new(0x_9E11_0001);
let mut state = QState::zero(2).unwrap();
state.apply_single(0, &Gate::ry(1.1)).unwrap();
state.apply_single(1, &Gate::ry(0.4)).unwrap();
let expected = state.probabilities();
let shots = 200_000usize;
let counts = state.sample_counts(shots, &mut rng);
for (outcome, count) in &counts {
let observed = *count as f64 / shots as f64;
let target = expected[*outcome as usize];
assert!(
(observed - target).abs() < 4.0 / (shots as f64).sqrt(),
"outcome {outcome} came up {observed} against {target}"
);
}
assert_eq!(counts.iter().map(|(_, c)| c).sum::<u64>(), shots as u64);
let (bit, collapsed) = state.measure_qubit(0, &mut rng).unwrap();
assert!(close(collapsed.norm(), 1.0, 1e-12));
for _ in 0..20 {
let (again, _) = collapsed.measure_qubit(0, &mut rng).unwrap();
assert_eq!(again, bit, "a collapsed qubit changed its mind");
}
assert!(close(collapsed.expectation_z(0).unwrap().abs(), 1.0, 1e-12));
}
#[test]
fn measuring_one_half_of_a_bell_pair_determines_the_other() {
let mut rng = Rng::new(0x_9E11_0002);
for _ in 0..200 {
let state = bell_state(0).unwrap();
let (first, collapsed) = state.measure_qubit(0, &mut rng).unwrap();
let (second, _) = collapsed.measure_qubit(1, &mut rng).unwrap();
assert_eq!(first, second, "the Bell pair disagreed with itself");
}
let state = bell_state(0).unwrap();
for q in 0..2 {
let (x, y, z) = state.bloch_vector(q).unwrap();
assert!(
x.hypot(y).hypot(z) < 1e-12,
"qubit {q} has a Bloch vector of length {}",
x.hypot(y).hypot(z)
);
}
}
#[test]
fn entanglement_entropy_separates_the_states_it_is_meant_to() {
let mut product = QState::zero(2).unwrap();
product.apply_single(0, &Gate::ry(0.9)).unwrap();
product.apply_single(1, &Gate::rx(1.4)).unwrap();
assert!(
close(product.entanglement_entropy(&[0]).unwrap(), 0.0, 1e-9),
"a product state has entropy {}",
product.entanglement_entropy(&[0]).unwrap()
);
for which in 0..4u8 {
let bell = bell_state(which).unwrap();
assert!(
close(bell.entanglement_entropy(&[0]).unwrap(), 1.0, 1e-9),
"Bell state {which} has entropy {}",
bell.entanglement_entropy(&[0]).unwrap()
);
assert!(close(
bell.entanglement_entropy(&[0]).unwrap(),
bell.entanglement_entropy(&[1]).unwrap(),
1e-9
));
}
for n in 2..=4usize {
let state = ghz(n).unwrap();
assert!(
close(state.entanglement_entropy(&[0]).unwrap(), 1.0, 1e-9),
"GHZ({n}) has entropy {}",
state.entanglement_entropy(&[0]).unwrap()
);
}
let ghz3 = ghz(3).unwrap();
let rest = ghz3.reduced_density_matrix(&[0, 1]).unwrap();
let mixed = DensityMatrix { n: 2, rho: rest };
assert!(close(mixed.purity(), 0.5, 1e-9), "GHZ's pair has purity {}", mixed.purity());
let w = w_state(3).unwrap();
assert!(
w.entanglement_entropy(&[0]).unwrap() > 0.9,
"W(3) should be entangled across a single cut"
);
let w_pair = DensityMatrix { n: 2, rho: w.reduced_density_matrix(&[0, 1]).unwrap() };
assert!(
w_pair.von_neumann_entropy().unwrap() > 0.9,
"the remaining W pair should still be mixed"
);
}
#[test]
fn the_schmidt_coefficients_reproduce_the_entropy_they_encode() {
let mut rng = Rng::new(0x_9E11_0003);
for _ in 0..40 {
let state = random_state(4, &mut rng).unwrap();
let coefficients = state.schmidt_coefficients(&[0, 1]).unwrap();
let total: f64 = coefficients.iter().map(|c| c * c).sum();
assert!(close(total, 1.0, 1e-8), "the coefficients square to {total}");
assert!(
coefficients.windows(2).all(|w| w[0] >= w[1] - 1e-12),
"the coefficients are not descending"
);
let from_schmidt: f64 = coefficients
.iter()
.filter(|c| **c > 1e-8)
.map(|c| -(c * c) * (c * c).log2())
.sum();
let direct = state.entanglement_entropy(&[0, 1]).unwrap();
assert!(
close(from_schmidt, direct, 1e-7),
"the two entropies are {from_schmidt} and {direct}"
);
}
let bell = bell_state(0).unwrap();
let coefficients = bell.schmidt_coefficients(&[0]).unwrap();
assert_eq!(coefficients.len(), 2);
for c in &coefficients {
assert!(close(*c, std::f64::consts::FRAC_1_SQRT_2, 1e-9), "a coefficient is {c}");
}
}
#[test]
fn the_bloch_vector_has_unit_length_exactly_when_the_qubit_is_unentangled() {
let mut rng = Rng::new(0x_9E11_0004);
for _ in 0..200 {
let single = random_state(1, &mut rng).unwrap();
let (x, y, z) = single.bloch_vector(0).unwrap();
assert!(
close(x.hypot(y).hypot(z), 1.0, 1e-9),
"a pure qubit has Bloch length {}",
x.hypot(y).hypot(z)
);
assert!(close(z, single.expectation_z(0).unwrap(), 1e-12));
let bigger = random_state(3, &mut rng).unwrap();
let (x, y, z) = bigger.bloch_vector(1).unwrap();
let length = x.hypot(y).hypot(z);
assert!(length <= 1.0 + 1e-9, "the Bloch length is {length}");
}
let mut plus = QState::zero(1).unwrap();
plus.apply_single(0, &Gate::h()).unwrap();
let (x, y, z) = plus.bloch_vector(0).unwrap();
assert!(close(x, 1.0, 1e-12) && close(y, 0.0, 1e-12) && close(z, 0.0, 1e-12));
let mut plus_i = QState::zero(1).unwrap();
plus_i.apply_single(0, &Gate::h()).unwrap();
plus_i.apply_single(0, &Gate::s()).unwrap();
let (x, y, z) = plus_i.bloch_vector(0).unwrap();
assert!(close(x, 0.0, 1e-12) && close(y, 1.0, 1e-12) && close(z, 0.0, 1e-12));
}
#[test]
fn pauli_string_expectations_agree_with_the_matrices_they_name() {
let mut rng = Rng::new(0x_9E11_0005);
let symbols = ['I', 'X', 'Y', 'Z'];
for _ in 0..60 {
let state = random_state(3, &mut rng).unwrap();
for code in 0..64usize {
let name: String = (0..3).rev().map(|k| symbols[(code >> (2 * k)) & 3]).collect();
let reported = state.expectation_pauli_string(&name).unwrap();
let mut applied = state.clone();
for (position, symbol) in name.chars().enumerate() {
let q = 3 - 1 - position;
match symbol {
'X' => applied.apply_single(q, &Gate::x()).unwrap(),
'Y' => applied.apply_single(q, &Gate::y()).unwrap(),
'Z' => applied.apply_single(q, &Gate::z()).unwrap(),
_ => {}
}
}
let direct = state.inner(&applied).unwrap();
assert!(close(direct.im, 0.0, 1e-9), "{name} has an imaginary expectation");
assert!(
close(reported, direct.re, 1e-9),
"{name}: {reported} against {}",
direct.re
);
}
}
let state = random_state(2, &mut rng).unwrap();
assert!(close(state.expectation_pauli_string("II").unwrap(), 1.0, 1e-12));
assert!(state.expectation_pauli_string("XYZ").is_err());
assert!(state.expectation_pauli_string("XQ").is_err());
}
#[test]
fn a_circuit_followed_by_its_inverse_is_the_identity() {
let mut circuit = Circuit::new(3).unwrap();
circuit
.h(0)
.t(1)
.cx(0, 1)
.ry(2, 0.7)
.ccx(0, 1, 2)
.rz(0, -1.1)
.swap(1, 2)
.phase(1, 0.35)
.cx(2, 0);
let mut round_trip = circuit.clone();
round_trip.append(&circuit.inverse()).unwrap();
let unitary = round_trip.unitary_small().unwrap();
assert!(
matrix_close(&unitary, &identity_matrix(8), 1e-12),
"the round trip is not the identity"
);
let mut naive = circuit.clone();
let reversed = Circuit { n: 3, ops: circuit.ops.iter().rev().cloned().collect() };
naive.append(&reversed).unwrap();
assert!(
!matrix_close(&naive.unitary_small().unwrap(), &identity_matrix(8), 1e-9),
"reversing alone happened to work, so the test proves nothing"
);
assert!(circuit.append(&Circuit::new(2).unwrap()).is_err());
}
trait TGate {
fn t(&mut self, q: usize) -> &mut Self;
}
impl TGate for Circuit {
fn t(&mut self, q: usize) -> &mut Self {
self.gate(q, Gate::t())
}
}
#[test]
fn the_unitary_is_unitary_and_matches_running_the_circuit() {
let mut rng = Rng::new(0x_9E11_0006);
let mut circuit = Circuit::new(3).unwrap();
circuit.h(0).cx(0, 1).ry(2, 1.2).ccx(1, 2, 0).cz(0, 2).swap(0, 1);
let unitary = circuit.unitary_small().unwrap();
let size = 8usize;
for i in 0..size {
for j in 0..size {
let entry =
(0..size).fold(ZERO, |acc, k| acc + unitary[k][i].conjugate() * unitary[k][j]);
let expected = f64::from(i == j);
assert!(
close(entry.re, expected, 1e-12) && close(entry.im, 0.0, 1e-12),
"the columns are not orthonormal at ({i}, {j})"
);
}
}
for _ in 0..30 {
let state = random_state(3, &mut rng).unwrap();
let run = circuit.run(&state).unwrap();
for row in 0..size {
let expected =
(0..size).fold(ZERO, |acc, k| acc + unitary[row][k] * state.amps[k]);
assert!(
(run.amps[row].re - expected.re).abs() < 1e-12
&& (run.amps[row].im - expected.im).abs() < 1e-12,
"the matrix and the run disagree at row {row}"
);
}
}
assert!(Circuit::new(11).unwrap().unitary_small().is_err());
}
#[test]
fn depth_counts_layers_and_gate_count_counts_gates() {
let mut wide = Circuit::new(3).unwrap();
wide.h(0).h(1).h(2);
assert_eq!(wide.depth(), 1, "disjoint gates should share a layer");
assert_eq!(wide.gate_count(), 3);
let mut deep = Circuit::new(3).unwrap();
deep.h(0).x(0).z(0);
assert_eq!(deep.depth(), 3, "gates on one qubit cannot share a layer");
let mut mixed = Circuit::new(3).unwrap();
mixed.h(0).h(2).cx(0, 1).h(2);
assert_eq!(mixed.depth(), 2);
assert_eq!(mixed.gate_count(), 4);
mixed.barrier();
assert_eq!(mixed.depth(), 2);
assert_eq!(mixed.gate_count(), 4);
assert_eq!(Circuit::new(2).unwrap().depth(), 0);
}
#[test]
fn the_text_and_diagram_forms_describe_the_circuit_they_came_from() {
let mut circuit = Circuit::new(3).unwrap();
circuit.h(0).cx(0, 1).ccx(0, 1, 2).swap(1, 2).barrier().rx(2, 0.3);
let text = circuit.to_qasm_lite();
assert!(text.starts_with("qubits 3\n"));
assert!(text.contains("u 0 H"), "{text}");
assert!(text.contains("cX 0 1"), "{text}");
assert!(text.contains("ccx 0 1 2"), "{text}");
assert!(text.contains("swap 1 2"), "{text}");
assert!(text.contains("barrier"), "{text}");
assert!(text.contains("u 2 U"), "{text}");
let drawing = circuit.draw_ascii();
assert_eq!(drawing.lines().count(), 3);
assert!(drawing.lines().next().unwrap().starts_with("q0: "));
let widths: Vec<usize> = drawing.lines().map(str::len).collect();
assert!(widths.windows(2).all(|w| w[0] == w[1]), "the rows are ragged: {widths:?}");
}
#[test]
fn the_bell_state_violates_the_chsh_bound_and_a_product_state_does_not() {
let angles = chsh_optimal_angles();
let bell = bell_state(0).unwrap();
let value = chsh_value(&bell, angles).unwrap();
assert!(
close(value.abs(), 2.0 * 2.0f64.sqrt(), 1e-9),
"the Bell state gives {value}, not 2 sqrt 2"
);
let mut rng = Rng::new(0x_9E11_0007);
for _ in 0..300 {
let a = random_state(1, &mut rng).unwrap();
let b = random_state(1, &mut rng).unwrap();
let mut amps = vec![ZERO; 4];
for i in 0..2 {
for j in 0..2 {
amps[2 * i + j] = a.amps[i] * b.amps[j];
}
}
let product = QState::from_amps(amps).unwrap();
let random_angles = (
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
);
for angles in [angles, random_angles] {
let value = chsh_value(&product, angles).unwrap();
assert!(
value.abs() <= 2.0 + 1e-9,
"a product state reached {value}, above the classical bound"
);
}
}
for _ in 0..200 {
let state = random_state(2, &mut rng).unwrap();
let random_angles = (
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
rng.next_f64() * std::f64::consts::TAU,
);
let value = chsh_value(&state, random_angles).unwrap();
assert!(
value.abs() <= 2.0 * 2.0f64.sqrt() + 1e-9,
"a state reached {value}, above Tsirelson's bound"
);
}
assert!(chsh_value(&ghz(3).unwrap(), angles).is_err());
}
#[test]
fn teleportation_moves_the_state_exactly_whatever_it_was() {
let mut rng = Rng::new(0x_9E11_0008);
for _ in 0..200 {
let theta = rng.next_f64() * std::f64::consts::PI;
let phi = rng.next_f64() * std::f64::consts::TAU;
let (input, output) = quantum_teleportation_demo(theta, phi, &mut rng).unwrap();
assert!(
(input.0 - output.0).abs() < 1e-10
&& (input.1 - output.1).abs() < 1e-10
&& (input.2 - output.2).abs() < 1e-10,
"sent {input:?} and received {output:?}"
);
let length = input.0.hypot(input.1).hypot(input.2);
assert!(close(length, 1.0, 1e-9), "the input is not pure: length {length}");
}
}
#[test]
fn superdense_coding_carries_two_bits_on_one_qubit() {
for bits in [(false, false), (false, true), (true, false), (true, true)] {
let decoded = superdense_coding_demo(bits).unwrap();
assert_eq!(decoded, bits, "sent {bits:?} and received {decoded:?}");
}
assert!(close(no_cloning_fidelity_bound(), 5.0 / 6.0, 1e-15));
}
#[test]
fn a_pure_state_has_purity_one_and_a_mixture_has_less() {
let mut rng = Rng::new(0x_9E11_0009);
for _ in 0..60 {
let state = random_state(2, &mut rng).unwrap();
let rho = DensityMatrix::from_state(&state);
assert!(rho.is_valid(1e-9), "a pure state's density matrix is invalid");
assert!(close(rho.purity(), 1.0, 1e-9), "purity is {}", rho.purity());
assert!(
close(rho.von_neumann_entropy().unwrap(), 0.0, 1e-8),
"a pure state has entropy {}",
rho.von_neumann_entropy().unwrap()
);
}
for n in 1..=3usize {
let size = 1usize << n;
let states: Vec<QState> =
(0..size).map(|i| QState::basis(n, i as u64).unwrap()).collect();
let weights = vec![1.0 / size as f64; size];
let rho = DensityMatrix::from_mixture(&states, &weights).unwrap();
assert!(rho.is_valid(1e-9));
assert!(
close(rho.purity(), 1.0 / size as f64, 1e-9),
"purity is {}",
rho.purity()
);
assert!(
close(rho.von_neumann_entropy().unwrap(), n as f64, 1e-8),
"entropy is {}",
rho.von_neumann_entropy().unwrap()
);
}
let mut a = QState::zero(1).unwrap();
a.apply_single(0, &Gate::h()).unwrap();
let b = QState::zero(1).unwrap();
let rho = DensityMatrix::from_mixture(&[a, b], &[0.3, 0.7]).unwrap();
assert!(rho.is_valid(1e-9));
assert!(rho.purity() < 1.0 && rho.purity() > 0.5, "purity is {}", rho.purity());
assert!(DensityMatrix::from_mixture(&[], &[]).is_err());
assert!(DensityMatrix::from_mixture(
&[QState::zero(1).unwrap()],
&[0.5]
)
.is_err());
}
#[test]
fn every_channel_preserves_the_trace_and_moves_the_bloch_vector_as_advertised() {
let mut source = QState::zero(1).unwrap();
source.apply_single(0, &Gate::ry(0.9)).unwrap();
source.apply_single(0, &Gate::rz(0.5)).unwrap();
let start = DensityMatrix::from_state(&source);
let bloch = |rho: &DensityMatrix| -> (f64, f64, f64) {
(
2.0 * rho.rho[0][1].re,
-2.0 * rho.rho[0][1].im,
rho.rho[0][0].re - rho.rho[1][1].re,
)
};
let (x0, y0, z0) = bloch(&start);
for p in [0.0f64, 0.1, 0.35, 1.0] {
for (name, kraus) in [
("depolarizing", depolarizing_channel(p).unwrap()),
("amplitude", amplitude_damping(p).unwrap()),
("phase", phase_damping(p).unwrap()),
("bitflip", bit_flip(p).unwrap()),
("phaseflip", phase_flip(p).unwrap()),
] {
assert!(
is_trace_preserving(&kraus, 1e-12),
"{name} at p = {p} is not trace preserving"
);
let mut rho = start.clone();
rho.apply_channel(&kraus).unwrap();
assert!(rho.is_valid(1e-9), "{name} at p = {p} produced an invalid state");
assert!(
rho.purity() <= start.purity() + 1e-9,
"{name} at p = {p} raised the purity to {}",
rho.purity()
);
let (x, y, z) = bloch(&rho);
match name {
"depolarizing" if p > 0.0 && x0.abs() > 1e-9 => {
let factor = x / x0;
assert!(
close(y / y0, factor, 1e-9) && close(z / z0, factor, 1e-9),
"depolarising was not isotropic: {}, {}, {}",
x / x0,
y / y0,
z / z0
);
}
"phase" => {
assert!(close(z, z0, 1e-12), "phase damping moved z to {z}");
assert!(x.abs() <= x0.abs() + 1e-12 && y.abs() <= y0.abs() + 1e-12);
}
"bitflip" => assert!(close(x, x0, 1e-12), "the bit flip moved x to {x}"),
"phaseflip" => assert!(close(z, z0, 1e-12), "the phase flip moved z to {z}"),
_ => {}
}
}
}
let mut decayed = start.clone();
decayed.apply_channel(&litude_damping(1.0).unwrap()).unwrap();
assert!(close(decayed.rho[0][0].re, 1.0, 1e-12), "the decayed state is {:?}", decayed.rho);
let mut wrecked = start.clone();
wrecked.apply_channel(&depolarizing_channel(1.0).unwrap()).unwrap();
assert!(close(wrecked.purity(), 0.5, 1e-9), "purity is {}", wrecked.purity());
assert!(depolarizing_channel(-0.1).is_err());
assert!(amplitude_damping(1.5).is_err());
assert!(phase_damping(-1.0).is_err());
assert!(bit_flip(2.0).is_err());
assert!(phase_flip(-0.5).is_err());
}
#[test]
fn the_partial_trace_agrees_with_the_state_vector_route() {
let mut rng = Rng::new(0x_9E11_000A);
for _ in 0..40 {
let state = random_state(3, &mut rng).unwrap();
let full = DensityMatrix::from_state(&state);
for keep in [vec![0usize], vec![1], vec![0, 2], vec![1, 2]] {
let from_state = state.reduced_density_matrix(&keep).unwrap();
let from_rho = full.partial_trace(&keep).unwrap();
assert!(
matrix_close(&from_state, &from_rho.rho, 1e-12),
"the two partial traces disagree on {keep:?}"
);
assert!(from_rho.is_valid(1e-9), "the reduced state is invalid");
}
}
let state = random_state(2, &mut rng).unwrap();
assert!(state.reduced_density_matrix(&[]).is_err());
assert!(state.reduced_density_matrix(&[0, 0]).is_err());
assert!(state.reduced_density_matrix(&[5]).is_err());
}
#[test]
fn pauli_decomposition_reconstructs_the_matrix_it_came_from() {
let cases: Vec<Vec<Vec<Complex>>> = vec![
vec![
vec![Complex::new(1.5, 0.0), Complex::new(0.3, -0.7)],
vec![Complex::new(0.3, 0.7), Complex::new(-0.4, 0.0)],
],
lift_single(2, 0, &Gate::z()),
(0..4)
.map(|i| {
(0..4)
.map(|j| Complex::new(((i * 4 + j) % 5) as f64 - 2.0, 0.0))
.collect()
})
.collect(),
];
for h in &cases {
let size = h.len();
let hermitian: Vec<Vec<Complex>> = (0..size)
.map(|i| {
(0..size)
.map(|j| scale(h[i][j] + h[j][i].conjugate(), 0.5))
.collect()
})
.collect();
let terms = pauli_decompose(&hermitian).unwrap();
let qubits = size.trailing_zeros() as usize;
let mut rebuilt = vec![vec![ZERO; size]; size];
for (name, coefficient) in &terms {
for i in 0..size {
for j in 0..size {
let mut entry = ONE;
for (k, symbol) in name.chars().enumerate() {
let gate = match symbol {
'X' => Gate::x(),
'Y' => Gate::y(),
'Z' => Gate::z(),
_ => Gate::identity(),
};
let row = (i >> (qubits - 1 - k)) & 1;
let column = (j >> (qubits - 1 - k)) & 1;
entry = entry * gate.matrix[row][column];
}
rebuilt[i][j] = rebuilt[i][j] + scale(entry, *coefficient);
}
}
}
assert!(
matrix_close(&rebuilt, &hermitian, 1e-9),
"the decomposition does not rebuild the matrix"
);
}
let terms = pauli_decompose(&lift_single(2, 0, &Gate::z())).unwrap();
assert_eq!(terms.len(), 1);
assert_eq!(terms[0].0, "IZ");
assert!(close(terms[0].1, 1.0, 1e-12));
assert!(pauli_decompose(&vec![vec![ONE; 3]; 3]).is_err());
}
#[test]
fn the_constructors_refuse_degenerate_input() {
assert!(QState::zero(0).is_err());
assert!(QState::zero(MAX_QUBITS + 1).is_err());
assert!(QState::basis(2, 4).is_err());
assert!(QState::from_amps(vec![ONE; 3]).is_err());
assert!(QState::from_amps(vec![ZERO; 4]).is_err());
assert!(QState::plus_all(0).is_err());
assert!(Circuit::new(0).is_err());
assert!(bell_state(4).is_err());
assert!(ghz(1).is_err());
assert!(w_state(1).is_err());
assert!(random_state(0, &mut Rng::new(1)).is_err());
let mut state = QState::zero(2).unwrap();
assert!(state.apply_single(2, &Gate::x()).is_err());
assert!(state.apply_controlled(0, 0, &Gate::x()).is_err());
assert!(state.apply_controlled(0, 5, &Gate::x()).is_err());
assert!(state.apply_ccx(0, 1, 1).is_err());
assert!(state.apply_swap(0, 9).is_err());
assert!(state.measure_qubit(7, &mut Rng::new(2)).is_err());
assert!(state.expectation_z(9).is_err());
assert!(state.inner(&QState::zero(3).unwrap()).is_err());
assert!(state.apply_swap(1, 1).is_ok());
let mut rho = DensityMatrix::from_state(&state);
assert!(rho.apply_gate(4, &Gate::x()).is_err());
assert!(rho.apply_channel(&[]).is_err());
assert!(rho
.apply_channel(&[from_rows([[ONE, ZERO], [ZERO, ZERO]])])
.is_err());
assert!(rho.partial_trace(&[0, 0]).is_err());
}
}