use rust_physics_engine::fractals::Complex;
use rust_physics_engine::monte_carlo::Rng;
use rust_physics_engine::quantum::algorithms::{
bernstein_vazirani, deutsch_jozsa, grover, grover_optimal_iterations, hhl_lite_2x2, iqft,
pauli_sum_expectation, pauli_sum_ground_energy, phase_estimation, qft_check_vs_fft,
qft_circuit, quantum_walk_line, simon_lite, three_bit_code_logical_error,
trotter_evolution,
};
use rust_physics_engine::quantum::circuit::{
amplitude_damping, bell_state, bit_flip, depolarizing_channel, ghz, pauli_decompose,
phase_damping, phase_flip, random_state, w_state, Circuit, DensityMatrix, Gate, QState,
};
fn pick(rng: &mut Rng, n: usize) -> usize {
((u128::from(rng.next_u64()) * n as u128) >> 64) as usize
}
fn spread(rng: &mut Rng, half_width: f64) -> f64 {
(rng.next_f64() * 2.0 - 1.0) * half_width
}
fn random_circuit(rng: &mut Rng, n: usize, gates: usize) -> Circuit {
let mut circuit = Circuit::new(n).unwrap();
for _ in 0..gates {
match pick(rng, 8) {
0 => {
circuit.h(pick(rng, n));
}
1 => {
circuit.gate(pick(rng, n), Gate::t());
}
2 => {
circuit.rx(pick(rng, n), spread(rng, 3.0));
}
3 => {
circuit.ry(pick(rng, n), spread(rng, 3.0));
}
4 => {
circuit.rz(pick(rng, n), spread(rng, 3.0));
}
5 if n >= 2 => {
let a = pick(rng, n);
let b = (a + 1 + pick(rng, n - 1)) % n;
circuit.cx(a, b);
}
6 if n >= 2 => {
let a = pick(rng, n);
let b = (a + 1 + pick(rng, n - 1)) % n;
circuit.cphase(a, b, spread(rng, 3.0));
}
7 if n >= 3 => {
let a = pick(rng, n);
let b = (a + 1 + pick(rng, n - 1)) % n;
let c = (0..n).find(|&q| q != a && q != b).unwrap();
circuit.ccx(a, b, c);
}
_ => {
circuit.x(pick(rng, n));
}
}
}
circuit
}
#[test]
fn prop_every_circuit_preserves_the_norm_and_is_exactly_invertible() {
let mut rng = Rng::new(0x_C111_0001);
for _ in 0..200 {
let n = 1 + pick(&mut rng, 4);
let gates = 5 + pick(&mut rng, 25);
let circuit = random_circuit(&mut rng, n, gates);
let start = random_state(n, &mut rng).unwrap();
let out = circuit.run(&start).unwrap();
assert!(
(out.norm() - 1.0).abs() < 1e-12,
"the norm became {} after {} gates",
out.norm(),
circuit.gate_count()
);
let back = circuit.inverse().run(&out).unwrap();
for (a, b) in back.amps.iter().zip(&start.amps) {
assert!(
(a.re - b.re).abs() < 1e-11 && (a.im - b.im).abs() < 1e-11,
"the inverse did not restore the state"
);
}
let total: f64 = out.probabilities().iter().sum();
assert!((total - 1.0).abs() < 1e-12, "the probabilities sum to {total}");
assert!(out.probabilities().iter().all(|p| *p >= 0.0));
}
}
#[test]
fn prop_the_matrix_and_the_simulation_agree_on_every_random_circuit() {
let mut rng = Rng::new(0x_C111_0002);
for _ in 0..80 {
let n = 1 + pick(&mut rng, 3);
let size = 1usize << n;
let gates = 4 + pick(&mut rng, 12);
let circuit = random_circuit(&mut rng, n, gates);
let unitary = circuit.unitary_small().unwrap();
for i in 0..size {
for j in 0..size {
let entry = (0..size).fold(Complex::new(0.0, 0.0), |acc, k| {
acc + unitary[k][i].conjugate() * unitary[k][j]
});
let expected = f64::from(i == j);
assert!(
(entry.re - expected).abs() < 1e-11 && entry.im.abs() < 1e-11,
"the columns are not orthonormal at ({i}, {j})"
);
}
}
let state = random_state(n, &mut rng).unwrap();
let simulated = circuit.run(&state).unwrap();
for row in 0..size {
let expected = (0..size)
.fold(Complex::new(0.0, 0.0), |acc, k| acc + unitary[row][k] * state.amps[k]);
assert!(
(simulated.amps[row].re - expected.re).abs() < 1e-11
&& (simulated.amps[row].im - expected.im).abs() < 1e-11,
"the matrix and the run disagree at row {row}"
);
}
}
}
#[test]
fn prop_entanglement_entropy_is_symmetric_across_every_cut() {
let mut rng = Rng::new(0x_C111_0003);
for _ in 0..120 {
let n = 2 + pick(&mut rng, 3);
let state = random_state(n, &mut rng).unwrap();
let mut left: Vec<usize> = Vec::new();
for q in 0..n {
if rng.next_f64() < 0.5 {
left.push(q);
}
}
if left.is_empty() || left.len() == n {
continue;
}
let right: Vec<usize> = (0..n).filter(|q| !left.contains(q)).collect();
let a = state.entanglement_entropy(&left).unwrap();
let b = state.entanglement_entropy(&right).unwrap();
assert!(
(a - b).abs() < 1e-7,
"the cut {left:?} gives {a} and its complement {b}"
);
let bound = left.len().min(right.len()) as f64;
assert!(a <= bound + 1e-7, "the entropy {a} exceeds {bound} bits");
assert!(a >= -1e-9, "the entropy is negative: {a}");
let rho = DensityMatrix {
n: left.len(),
rho: state.reduced_density_matrix(&left).unwrap(),
};
assert!(rho.is_valid(1e-8), "the reduced state is not a state");
if a < 1e-8 {
assert!((rho.purity() - 1.0).abs() < 1e-6, "zero entropy but purity {}", rho.purity());
} else {
assert!(rho.purity() < 1.0 - 1e-9, "positive entropy but purity {}", rho.purity());
}
}
}
#[test]
fn prop_a_product_state_has_no_entanglement_however_it_is_built() {
let mut rng = Rng::new(0x_C111_0004);
for _ in 0..150 {
let n = 2 + pick(&mut rng, 3);
let mut circuit = Circuit::new(n).unwrap();
for q in 0..n {
circuit.ry(q, spread(&mut rng, 3.0));
circuit.rz(q, spread(&mut rng, 3.0));
circuit.gate(q, Gate::t());
}
let state = circuit.run(&QState::zero(n).unwrap()).unwrap();
for q in 0..n {
assert!(
state.entanglement_entropy(&[q]).unwrap() < 1e-9,
"a product state has entropy {}",
state.entanglement_entropy(&[q]).unwrap()
);
let (x, y, z) = state.bloch_vector(q).unwrap();
assert!(
(x.hypot(y).hypot(z) - 1.0).abs() < 1e-9,
"an unentangled qubit has Bloch length {}",
x.hypot(y).hypot(z)
);
}
}
}
#[test]
fn prop_pauli_expectations_are_bounded_and_reconstruct_the_state() {
let mut rng = Rng::new(0x_C111_0005);
for _ in 0..200 {
let single = random_state(1, &mut rng).unwrap();
let x = single.expectation_pauli_string("X").unwrap();
let y = single.expectation_pauli_string("Y").unwrap();
let z = single.expectation_pauli_string("Z").unwrap();
for value in [x, y, z] {
assert!((-1.0..=1.0).contains(&value), "an expectation is {value}");
}
assert!(
(x * x + y * y + z * z - 1.0).abs() < 1e-9,
"a pure qubit's Bloch vector has length squared {}",
x * x + y * y + z * z
);
let (bx, by, bz) = single.bloch_vector(0).unwrap();
assert!((bx - x).abs() < 1e-12 && (by - y).abs() < 1e-12 && (bz - z).abs() < 1e-12);
let n = 2 + pick(&mut rng, 2);
let state = random_state(n, &mut rng).unwrap();
let symbols = ['I', 'X', 'Y', 'Z'];
for _ in 0..20 {
let name: String = (0..n).map(|_| symbols[pick(&mut rng, 4)]).collect();
let value = state.expectation_pauli_string(&name).unwrap();
assert!((-1.0 - 1e-12..=1.0 + 1e-12).contains(&value), "{name} gives {value}");
}
}
}
#[test]
fn prop_every_channel_is_trace_preserving_and_never_increases_purity() {
let mut rng = Rng::new(0x_C111_0006);
for _ in 0..200 {
let state = random_state(1, &mut rng).unwrap();
let start = DensityMatrix::from_state(&state);
let p = rng.next_f64();
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()),
] {
let mut rho = start.clone();
rho.apply_channel(&kraus).unwrap();
let trace = rho.trace();
assert!(
(trace.re - 1.0).abs() < 1e-12 && trace.im.abs() < 1e-12,
"{name} at p = {p} left trace {trace:?}"
);
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()
);
assert!(
rho.von_neumann_entropy().unwrap() >= -1e-9,
"{name} gave a negative entropy"
);
let mut again = rho.clone();
again.apply_channel(&kraus).unwrap();
assert!(again.is_valid(1e-9), "{name} does not compose");
}
}
}
#[test]
fn prop_pauli_decomposition_is_exact_and_its_coefficients_are_real() {
let mut rng = Rng::new(0x_C111_0007);
for _ in 0..150 {
let n = 1 + pick(&mut rng, 2);
let size = 1usize << n;
let raw: Vec<Vec<Complex>> = (0..size)
.map(|_| {
(0..size)
.map(|_| Complex::new(spread(&mut rng, 2.0), spread(&mut rng, 2.0)))
.collect()
})
.collect();
let h: Vec<Vec<Complex>> = (0..size)
.map(|i| {
(0..size)
.map(|j| {
let s = raw[i][j] + raw[j][i].conjugate();
Complex::new(s.re * 0.5, s.im * 0.5)
})
.collect()
})
.collect();
let terms = pauli_decompose(&h).unwrap();
let mut rebuilt = vec![vec![Complex::new(0.0, 0.0); size]; size];
for (name, coefficient) in &terms {
for i in 0..size {
for j in 0..size {
let mut entry = Complex::new(1.0, 0.0);
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 >> (n - 1 - k)) & 1;
let column = (j >> (n - 1 - k)) & 1;
entry = entry * gate.matrix[row][column];
}
rebuilt[i][j] = rebuilt[i][j]
+ Complex::new(entry.re * coefficient, entry.im * coefficient);
}
}
}
for i in 0..size {
for j in 0..size {
assert!(
(rebuilt[i][j].re - h[i][j].re).abs() < 1e-9
&& (rebuilt[i][j].im - h[i][j].im).abs() < 1e-9,
"the decomposition does not rebuild entry ({i}, {j})"
);
}
}
let state = random_state(n, &mut rng).unwrap();
let from_terms = pauli_sum_expectation(&terms, &state).unwrap();
let mut direct = 0.0;
for i in 0..size {
for j in 0..size {
let contribution = state.amps[i].conjugate() * h[i][j] * state.amps[j];
direct += contribution.re;
}
}
assert!(
(from_terms - direct).abs() < 1e-9,
"the two expectations are {from_terms} and {direct}"
);
}
}
#[test]
fn prop_the_qft_matches_the_dft_at_every_width_and_inverts_itself() {
for n in 1..=6usize {
assert!(
qft_check_vs_fft(n).unwrap() < 1e-11,
"at {n} qubits the QFT is off by {}",
qft_check_vs_fft(n).unwrap()
);
let mut round_trip = qft_circuit(n).unwrap();
round_trip.append(&iqft(n).unwrap()).unwrap();
let mut rng = Rng::new(0x_C111_0008 + n as u64);
for _ in 0..20 {
let state = random_state(n, &mut rng).unwrap();
let back = round_trip.run(&state).unwrap();
for (a, b) in back.amps.iter().zip(&state.amps) {
assert!(
(a.re - b.re).abs() < 1e-11 && (a.im - b.im).abs() < 1e-11,
"the QFT round trip moved a state at {n} qubits"
);
}
}
}
}
#[test]
fn prop_deutsch_jozsa_and_bernstein_vazirani_answer_correctly_on_random_promises() {
let mut rng = Rng::new(0x_C111_0009);
for n in 1..=6usize {
let size = 1u64 << n;
for _ in 0..30 {
let mut values: Vec<bool> = (0..size).map(|i| i < size / 2).collect();
for i in (1..values.len()).rev() {
values.swap(i, pick(&mut rng, i + 1));
}
let balanced = |x: u64| values[x as usize];
assert!(
!deutsch_jozsa(&balanced, n).unwrap(),
"a balanced function at {n} qubits read as constant"
);
}
assert!(deutsch_jozsa(&|_| true, n).unwrap());
assert!(deutsch_jozsa(&|_| false, n).unwrap());
for _ in 0..40 {
let secret = rng.next_u64() % size;
assert_eq!(bernstein_vazirani(secret, n).unwrap(), secret);
}
}
}
#[test]
fn prop_simon_recovers_every_period_it_is_given() {
let mut rng = Rng::new(0x_C111_000A);
for n in 2..=5usize {
for _ in 0..12 {
let secret = 1 + rng.next_u64() % ((1u64 << n) - 1);
let f = |x: u64| -> u64 { x.min(x ^ secret) };
let found = simon_lite(&f, n, &mut rng).unwrap();
assert_eq!(found, secret, "at {n} qubits the period {secret} came back {found}");
}
}
}
#[test]
fn prop_grover_succeeds_with_high_probability_for_every_marked_set() {
let mut rng = Rng::new(0x_C111_000B);
for n in 3..=8usize {
let size = 1usize << n;
for _ in 0..20 {
let count = 1 + pick(&mut rng, 4.min(size / 4));
let mut marked: Vec<u64> = Vec::new();
while marked.len() < count {
let candidate = rng.next_u64() % size as u64;
if !marked.contains(&candidate) {
marked.push(candidate);
}
}
let iterations = grover_optimal_iterations(size, marked.len()).unwrap();
let (_, success) = grover(&marked, n, None, &mut rng).unwrap();
let trials = 400usize;
let hits = (0..trials)
.filter(|_| {
let (drawn, _) = grover(&marked, n, None, &mut rng).unwrap();
marked.contains(&drawn)
})
.count();
let observed = hits as f64 / trials as f64;
assert!(
(observed - success).abs() < 5.0 / (trials as f64).sqrt(),
"at {n} qubits the marked states came up {observed} against the stated {success}"
);
let theta = (marked.len() as f64 / size as f64).sqrt().asin();
let predicted = ((2 * iterations + 1) as f64 * theta).sin().powi(2);
assert!(
(success - predicted).abs() < 1e-9,
"the success probability is {success}, the formula gives {predicted}"
);
assert!(success > 0.8, "at {n} qubits with {count} marked, success is {success}");
}
}
}
#[test]
fn prop_phase_estimation_is_exact_on_representable_phases_and_close_otherwise() {
let one = QState::basis(1, 1).unwrap();
let mut rng = Rng::new(0x_C111_000C);
for ancilla in 3..=8usize {
let resolution = 1u64 << ancilla;
for _ in 0..20 {
let k = rng.next_u64() % resolution;
let phase = k as f64 / resolution as f64;
let gate = Gate::phase(2.0 * std::f64::consts::PI * phase);
let estimate = phase_estimation(&gate, &one, ancilla).unwrap();
assert!(
(estimate - phase).abs() < 1e-11,
"the representable phase {phase} came back {estimate}"
);
}
for _ in 0..20 {
let phase = rng.next_f64();
let gate = Gate::phase(2.0 * std::f64::consts::PI * phase);
let estimate = phase_estimation(&gate, &one, ancilla).unwrap();
let error = (estimate - phase).abs().min(1.0 - (estimate - phase).abs());
assert!(
error <= 1.0 / resolution as f64 + 1e-9,
"with {ancilla} ancillas the phase {phase} came back {estimate}"
);
}
}
}
#[test]
fn prop_trotterisation_is_unitary_and_converges_with_the_step_count() {
let mut rng = Rng::new(0x_C111_000D);
let symbols = ['I', 'X', 'Y', 'Z'];
for _ in 0..60 {
let n = 1 + pick(&mut rng, 2);
let count = 1 + pick(&mut rng, 3);
let terms: Vec<(String, f64)> = (0..count)
.map(|_| {
let name: String = (0..n).map(|_| symbols[pick(&mut rng, 4)]).collect();
(name, spread(&mut rng, 1.5))
})
.collect();
let t = spread(&mut rng, 2.0);
let reference = trotter_evolution(&terms, t, 2000, n)
.unwrap()
.unitary_small()
.unwrap();
let size = 1usize << n;
let mut previous = f64::INFINITY;
for steps in [1usize, 8, 64] {
let circuit = trotter_evolution(&terms, t, steps, n).unwrap();
let unitary = circuit.unitary_small().unwrap();
for i in 0..size {
for j in 0..size {
let entry = (0..size).fold(Complex::new(0.0, 0.0), |acc, k| {
acc + unitary[k][i].conjugate() * unitary[k][j]
});
let expected = f64::from(i == j);
assert!(
(entry.re - expected).abs() < 1e-10 && entry.im.abs() < 1e-10,
"the Trotter circuit is not unitary at {steps} steps"
);
}
}
let mut worst: f64 = 0.0;
for i in 0..size {
for j in 0..size {
worst = worst
.max((unitary[i][j].re - reference[i][j].re).abs())
.max((unitary[i][j].im - reference[i][j].im).abs());
}
}
assert!(
worst <= previous + 1e-9,
"the error rose from {previous} to {worst} at {steps} steps"
);
previous = worst;
}
}
}
#[test]
fn prop_the_two_by_two_solver_satisfies_the_system_it_solves() {
let mut rng = Rng::new(0x_C111_000E);
let mut solved = 0usize;
for _ in 0..500 {
let d0 = spread(&mut rng, 4.0);
let d1 = spread(&mut rng, 4.0);
let off = spread(&mut rng, 3.0);
let a = [[d0, off], [off, d1]];
let b = [spread(&mut rng, 3.0), spread(&mut rng, 3.0)];
let Ok(x) = hhl_lite_2x2(&a, &b) else {
continue;
};
solved += 1;
let determinant = d0 * d1 - off * off;
let magnitude = x[0].abs().max(x[1].abs()).max(1.0);
for row in 0..2 {
let lhs = a[row][0] * x[0] + a[row][1] * x[1];
assert!(
(lhs - b[row]).abs() < 1e-6 * magnitude / determinant.abs().clamp(1e-3, 1.0),
"row {row} of {a:?} x = {b:?} gives {lhs}, solved as {x:?}"
);
}
}
assert!(solved > 400, "only {solved} systems were solvable");
assert!(hhl_lite_2x2(&[[1.0, 2.0], [3.0, 1.0]], &[1.0, 1.0]).is_err());
}
#[test]
fn prop_the_quantum_walk_conserves_probability_for_every_coin() {
let mut rng = Rng::new(0x_C111_000F);
for _ in 0..80 {
let coin = Gate::u3(spread(&mut rng, 3.0), spread(&mut rng, 3.0), spread(&mut rng, 3.0));
let steps = 5 + pick(&mut rng, 40);
let distribution = quantum_walk_line(steps, &coin).unwrap();
assert_eq!(distribution.len(), 2 * steps + 1);
let total: f64 = distribution.iter().sum();
assert!((total - 1.0).abs() < 1e-9, "the walk lost probability: {total}");
assert!(distribution.iter().all(|p| *p >= 0.0));
assert!(distribution[0] >= 0.0 && distribution[2 * steps] >= 0.0);
for (site, p) in distribution.iter().enumerate() {
if site % 2 == 1 {
assert!(*p < 1e-15, "site {site} is occupied against parity: {p}");
}
}
assert!(distribution.iter().step_by(2).sum::<f64>() > 0.999);
}
}
#[test]
fn prop_the_three_bit_code_helps_below_a_half_and_hurts_above_it() {
for k in 1..500 {
let p = k as f64 / 1000.0;
assert!(
three_bit_code_logical_error(p) < p,
"at p = {p} the code should help"
);
}
for k in 501..1000 {
let p = k as f64 / 1000.0;
assert!(
three_bit_code_logical_error(p) > p,
"at p = {p} the code should hurt"
);
}
assert!((three_bit_code_logical_error(0.5) - 0.5).abs() < 1e-12);
let mut previous = -1.0;
for k in 0..=1000 {
let value = three_bit_code_logical_error(k as f64 / 1000.0);
assert!(value >= previous - 1e-12, "the logical rate fell at p = {}", k as f64 / 1000.0);
previous = value;
}
}
#[test]
fn prop_ground_energies_bound_every_expectation_of_the_same_hamiltonian() {
let mut rng = Rng::new(0x_C111_0010);
let symbols = ['I', 'X', 'Y', 'Z'];
for _ in 0..80 {
let n = 1 + pick(&mut rng, 2);
let count = 1 + pick(&mut rng, 4);
let terms: Vec<(String, f64)> = (0..count)
.map(|_| {
let name: String = (0..n).map(|_| symbols[pick(&mut rng, 4)]).collect();
(name, spread(&mut rng, 2.0))
})
.collect();
let ground = pauli_sum_ground_energy(&terms, n).unwrap();
for _ in 0..30 {
let state = random_state(n, &mut rng).unwrap();
let energy = pauli_sum_expectation(&terms, &state).unwrap();
assert!(
energy >= ground - 1e-8,
"a state reached {energy}, below the ground energy {ground}"
);
}
if n == 2 {
for candidate in [bell_state(0).unwrap(), ghz(2).unwrap(), w_state(2).unwrap()] {
let energy = pauli_sum_expectation(&terms, &candidate).unwrap();
assert!(energy >= ground - 1e-8, "a named state reached {energy}");
}
}
}
}