use crate::{PauliBasis, PauliString};
use super::{MeasureResult, SimError, TableauSimulator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Gate1Q {
X,
Y,
Z,
H,
S,
SDag,
SqrtX,
SqrtXDag,
SqrtY,
SqrtYDag,
Hxy,
Hyz,
Hnxy,
Hnxz,
Hnyz,
Cxyz,
Czyx,
Cnxyz,
Cxnyz,
Cxynz,
Cnzyx,
Cznyx,
Czynx,
}
impl Gate1Q {
#[must_use]
pub const fn images(self) -> ((PauliBasis, bool), (PauliBasis, bool)) {
use PauliBasis::{X, Y, Z};
match self {
Gate1Q::X => ((X, false), (Z, true)),
Gate1Q::Y => ((X, true), (Z, true)),
Gate1Q::Z => ((X, true), (Z, false)),
Gate1Q::H => ((Z, false), (X, false)),
Gate1Q::S => ((Y, false), (Z, false)),
Gate1Q::SDag => ((Y, true), (Z, false)),
Gate1Q::SqrtX => ((X, false), (Y, true)),
Gate1Q::SqrtXDag => ((X, false), (Y, false)),
Gate1Q::SqrtY => ((Z, true), (X, false)),
Gate1Q::SqrtYDag => ((Z, false), (X, true)),
Gate1Q::Hxy => ((Y, false), (Z, true)),
Gate1Q::Hyz => ((X, true), (Y, false)),
Gate1Q::Hnxy => ((Y, true), (Z, true)),
Gate1Q::Hnxz => ((Z, true), (X, true)),
Gate1Q::Hnyz => ((X, true), (Y, true)),
Gate1Q::Cxyz => ((Y, false), (X, false)),
Gate1Q::Czyx => ((Z, false), (Y, false)),
Gate1Q::Cnxyz => ((Y, true), (X, true)),
Gate1Q::Cxnyz => ((Y, true), (X, false)),
Gate1Q::Cxynz => ((Y, false), (X, true)),
Gate1Q::Cnzyx => ((Z, true), (Y, true)),
Gate1Q::Cznyx => ((Z, false), (Y, true)),
Gate1Q::Czynx => ((Z, true), (Y, false)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Instruction {
Gate1 {
gate: Gate1Q,
qubit: usize,
},
Gate2 {
control: PauliBasis,
target: PauliBasis,
control_qubit: usize,
target_qubit: usize,
},
Pauli {
basis: PauliBasis,
qubit: usize,
},
T {
basis: PauliBasis,
qubit: usize,
adjoint: bool,
},
TPauli {
axis: PauliString,
adjoint: bool,
},
Measure(PauliString),
Reset {
basis: PauliBasis,
qubit: usize,
},
ConditionalPauli {
basis: PauliBasis,
qubit: usize,
control: usize,
},
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BatchOutcome {
pub records: Vec<MeasureResult>,
pub max_rank: usize,
}
impl TableauSimulator {
pub fn gate1(&mut self, gate: Gate1Q, qubit: usize) {
match gate {
Gate1Q::X => self.x(qubit),
Gate1Q::Y => self.y(qubit),
Gate1Q::Z => self.z(qubit),
Gate1Q::H => self.h(qubit),
Gate1Q::S => self.s(qubit),
Gate1Q::SDag => self.s_dag(qubit),
Gate1Q::SqrtX => self.sqrt_x(qubit),
Gate1Q::SqrtXDag => self.sqrt_x_dag(qubit),
Gate1Q::SqrtY => self.sqrt_y(qubit),
Gate1Q::SqrtYDag => self.sqrt_y_dag(qubit),
Gate1Q::Hxy => {
self.s_dag(qubit);
self.x(qubit);
}
Gate1Q::Hyz => {
self.sqrt_x(qubit);
self.z(qubit);
}
Gate1Q::Hnxy => {
self.s(qubit);
self.x(qubit);
}
Gate1Q::Hnxz => {
self.h(qubit);
self.y(qubit);
}
Gate1Q::Hnyz => {
self.sqrt_x(qubit);
self.y(qubit);
}
Gate1Q::Cxyz => {
self.sqrt_x(qubit);
self.s(qubit);
}
Gate1Q::Cnxyz => {
self.sqrt_x(qubit);
self.s_dag(qubit);
}
Gate1Q::Cxnyz => {
self.sqrt_x_dag(qubit);
self.s_dag(qubit);
}
Gate1Q::Cxynz => {
self.sqrt_x_dag(qubit);
self.s(qubit);
}
Gate1Q::Czyx => {
self.s_dag(qubit);
self.sqrt_x_dag(qubit);
}
Gate1Q::Cnzyx => {
self.s_dag(qubit);
self.sqrt_x(qubit);
}
Gate1Q::Cznyx => {
self.s(qubit);
self.sqrt_x(qubit);
}
Gate1Q::Czynx => {
self.s(qubit);
self.sqrt_x_dag(qubit);
}
}
}
pub fn gate2(
&mut self,
control: PauliBasis,
target: PauliBasis,
control_qubit: usize,
target_qubit: usize,
) -> Result<(), SimError> {
if control_qubit == target_qubit {
return Err(SimError::RepeatedQubit(control_qubit));
}
use PauliBasis::{X, Z};
match (control, target) {
(Z, Z) => self.cz(control_qubit, target_qubit),
(Z, X) => self.cx(control_qubit, target_qubit),
(X, Z) => self.cx(target_qubit, control_qubit),
_ => {
self.basis_to_z(control, control_qubit);
self.basis_to_z(target, target_qubit);
let applied = self.cz(control_qubit, target_qubit);
self.z_to_basis(target, target_qubit);
self.z_to_basis(control, control_qubit);
applied
}
}
}
pub fn t_basis(
&mut self,
basis: PauliBasis,
qubit: usize,
adjoint: bool,
) -> Result<(), SimError> {
self.basis_to_z(basis, qubit);
let rotated = if adjoint {
self.t_dag(qubit)
} else {
self.t(qubit)
};
self.z_to_basis(basis, qubit);
rotated
}
pub fn apply_batch(&mut self, instructions: &[Instruction]) -> Result<BatchOutcome, SimError> {
let mut outcome = BatchOutcome::default();
for instruction in instructions {
match instruction {
Instruction::Gate1 { gate, qubit } => self.gate1(*gate, *qubit),
Instruction::Gate2 {
control,
target,
control_qubit,
target_qubit,
} => self.gate2(*control, *target, *control_qubit, *target_qubit)?,
Instruction::Pauli { basis, qubit } => self.basis_pauli(*basis, *qubit),
Instruction::T {
basis,
qubit,
adjoint,
} => self.t_basis(*basis, *qubit, *adjoint)?,
Instruction::TPauli { axis, adjoint } => self.t_pauli(axis, *adjoint)?,
Instruction::Measure(observable) => {
outcome.records.push(self.measure_observable(observable)?);
}
Instruction::Reset { basis, qubit } => self.reset_basis(*basis, *qubit)?,
Instruction::ConditionalPauli {
basis,
qubit,
control,
} => {
let gate = outcome
.records
.get(*control)
.ok_or(SimError::MissingBatchRecord { index: *control })?;
if gate.outcome {
self.basis_pauli(*basis, *qubit);
}
}
}
outcome.max_rank = outcome.max_rank.max(self.rank());
}
Ok(outcome)
}
fn basis_to_z(&mut self, basis: PauliBasis, qubit: usize) {
match basis {
PauliBasis::X => self.h(qubit),
PauliBasis::Y => self.sqrt_x(qubit),
PauliBasis::Z => {}
}
}
fn z_to_basis(&mut self, basis: PauliBasis, qubit: usize) {
match basis {
PauliBasis::X => self.h(qubit),
PauliBasis::Y => self.sqrt_x_dag(qubit),
PauliBasis::Z => {}
}
}
fn basis_pauli(&mut self, basis: PauliBasis, qubit: usize) {
match basis {
PauliBasis::X => self.x(qubit),
PauliBasis::Y => self.y(qubit),
PauliBasis::Z => self.z(qubit),
}
}
fn reset_basis(&mut self, basis: PauliBasis, qubit: usize) -> Result<(), SimError> {
match basis {
PauliBasis::X => self.reset_x(qubit),
PauliBasis::Y => self.reset_y(qubit),
PauliBasis::Z => self.reset_z(qubit),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Pauli;
use num_complex::Complex64;
use paulimer::{Clifford, CliffordUnitary, DensePauli, Pauli as PaulimerPauli, PauliMutable};
const ALL_GATE1: [Gate1Q; 23] = [
Gate1Q::X,
Gate1Q::Y,
Gate1Q::Z,
Gate1Q::H,
Gate1Q::S,
Gate1Q::SDag,
Gate1Q::SqrtX,
Gate1Q::SqrtXDag,
Gate1Q::SqrtY,
Gate1Q::SqrtYDag,
Gate1Q::Hxy,
Gate1Q::Hyz,
Gate1Q::Hnxy,
Gate1Q::Hnxz,
Gate1Q::Hnyz,
Gate1Q::Cxyz,
Gate1Q::Czyx,
Gate1Q::Cnxyz,
Gate1Q::Cxnyz,
Gate1Q::Cxynz,
Gate1Q::Cnzyx,
Gate1Q::Cznyx,
Gate1Q::Czynx,
];
const BASES: [PauliBasis; 3] = [PauliBasis::X, PauliBasis::Y, PauliBasis::Z];
fn scrambled() -> TableauSimulator {
let mut sim = TableauSimulator::with_seed(3, 0xC0FF_EE01);
sim.h(0);
sim.t(0).expect("magic injection stays under the rank cap");
sim.h(1);
sim.s(1);
sim.cx(0, 1).expect("distinct operands");
sim.sqrt_x(2);
sim.t(2).expect("magic injection stays under the rank cap");
sim.cz(1, 2).expect("distinct operands");
sim
}
fn overlap(a: &TableauSimulator, b: &TableauSimulator) -> f64 {
let (left, right) = (a.state_vector(), b.state_vector());
assert_eq!(
left.len(),
right.len(),
"compared registers differ in width"
);
let inner: Complex64 = left
.iter()
.zip(&right)
.map(|(x, y)| x.conj() * y)
.sum::<Complex64>();
inner.norm()
}
fn assert_same_state(actual: &TableauSimulator, expected: &TableauSimulator, what: &str) {
let fidelity = overlap(actual, expected);
assert!(
(fidelity - 1.0).abs() < 1e-9,
"{what}: states differ (overlap {fidelity})"
);
}
fn signed_image(axis: PauliBasis, negated: bool) -> DensePauli {
let mut image = match axis {
PauliBasis::X => <DensePauli as PaulimerPauli>::x(0, 1),
PauliBasis::Y => <DensePauli as PaulimerPauli>::y(0, 1),
PauliBasis::Z => <DensePauli as PaulimerPauli>::z(0, 1),
};
if negated {
image.add_assign_phase_exp(2); }
image
}
fn reference_clifford(gate: Gate1Q) -> CliffordUnitary {
let ((x_axis, x_neg), (z_axis, z_neg)) = gate.images();
CliffordUnitary::from_preimages(&[signed_image(x_axis, x_neg), signed_image(z_axis, z_neg)])
.inverse()
}
fn single_qubit_pauli(basis: PauliBasis, qubit: usize, n: usize) -> PauliString {
PauliString::single(n, qubit, basis.into())
}
#[test]
fn gate1_compositions_match_their_tableaux() {
for gate in ALL_GATE1 {
for qubit in 0..3 {
let mut fast = scrambled();
fast.gate1(gate, qubit);
let mut reference = scrambled();
reference.apply_clifford(&reference_clifford(gate), &[qubit]);
assert_same_state(&fast, &reference, &format!("{gate:?} on qubit {qubit}"));
}
}
}
#[test]
fn gate2_matches_controlled_pauli() {
for control in BASES {
for target in BASES {
for (c, t) in [(0usize, 1usize), (1, 0), (0, 2)] {
let mut fast = scrambled();
fast.gate2(control, target, c, t)
.expect("distinct operands");
let mut reference = scrambled();
let n = reference.num_qubits();
reference
.controlled_pauli(
&single_qubit_pauli(control, c, n),
&single_qubit_pauli(target, t, n),
)
.expect("single-qubit axes on distinct qubits commute");
assert_same_state(
&fast,
&reference,
&format!("{control:?}C{target:?} on ({c},{t})"),
);
}
}
}
}
#[test]
fn gate2_rejects_repeated_operands_without_mutating() {
let mut sim = scrambled();
let before = sim.state_vector();
for control in BASES {
for target in BASES {
assert_eq!(
sim.gate2(control, target, 1, 1),
Err(SimError::RepeatedQubit(1))
);
}
}
assert_eq!(sim.state_vector(), before);
}
#[test]
fn t_basis_matches_t_pauli() {
for basis in BASES {
for adjoint in [false, true] {
let mut fast = scrambled();
fast.t_basis(basis, 1, adjoint)
.expect("a single-qubit axis rotation stays under the cap");
let mut reference = scrambled();
let n = reference.num_qubits();
reference
.t_pauli(&single_qubit_pauli(basis, 1, n), adjoint)
.expect("a single-qubit axis rotation stays under the cap");
assert_same_state(&fast, &reference, &format!("T_{basis:?} adjoint={adjoint}"));
}
}
}
#[test]
fn t_pauli_instruction_matches_the_engine() {
let axis = PauliString::from_terms(3, [(0, Pauli::Z), (2, Pauli::Z)]);
for adjoint in [false, true] {
let mut batched = scrambled();
batched
.apply_batch(&[Instruction::TPauli {
axis: axis.clone(),
adjoint,
}])
.expect("a two-qubit axis rotation stays under the cap");
let mut manual = scrambled();
manual
.t_pauli(&axis, adjoint)
.expect("a two-qubit axis rotation stays under the cap");
assert_same_state(&batched, &manual, &format!("T_Z0Z2 adjoint={adjoint}"));
}
}
#[test]
fn batch_matches_the_equivalent_procedural_run() {
let program = [
Instruction::Gate1 {
gate: Gate1Q::H,
qubit: 0,
},
Instruction::T {
basis: PauliBasis::Z,
qubit: 0,
adjoint: false,
},
Instruction::Gate2 {
control: PauliBasis::Z,
target: PauliBasis::X,
control_qubit: 0,
target_qubit: 1,
},
Instruction::Gate1 {
gate: Gate1Q::Cxyz,
qubit: 2,
},
Instruction::Measure(PauliString::single(3, 1, Pauli::Z)),
Instruction::Pauli {
basis: PauliBasis::X,
qubit: 2,
},
Instruction::Reset {
basis: PauliBasis::X,
qubit: 2,
},
Instruction::Measure(PauliString::single(3, 2, Pauli::Z)),
];
let mut batched = TableauSimulator::with_seed(3, 99);
let outcome = batched.apply_batch(&program).expect("valid program");
let mut manual = TableauSimulator::with_seed(3, 99);
manual.h(0);
manual.t(0).expect("under the cap");
manual.cx(0, 1).expect("distinct operands");
manual.gate1(Gate1Q::Cxyz, 2);
let first = manual.measure(1).expect("within the rank cap");
manual.x(2);
manual
.reset_x(2)
.expect("reset is a measure plus a frame Z");
let second = manual.measure(2).expect("within the rank cap");
assert_eq!(outcome.records, vec![first, second]);
assert_eq!(
outcome.max_rank, 2,
"the T doubles the rank and nothing collapses it"
);
assert_same_state(&batched, &manual, "batch vs procedural");
}
#[test]
fn conditional_pauli_reads_its_own_batch() {
let program = [
Instruction::Gate1 {
gate: Gate1Q::H,
qubit: 0,
},
Instruction::Measure(PauliString::single(1, 0, Pauli::Z)),
Instruction::ConditionalPauli {
basis: PauliBasis::X,
qubit: 0,
control: 0,
},
];
for seed in 0..8 {
let mut sim = TableauSimulator::with_seed(1, seed);
sim.apply_batch(&program).expect("valid program");
let z = sim.peek_z(0).expect("qubit 0 is live");
assert!((z - 1.0).abs() < 1e-9, "seed {seed}: correction left |1⟩");
}
}
#[test]
fn conditional_pauli_rejects_an_unreached_record() {
let program = [Instruction::ConditionalPauli {
basis: PauliBasis::X,
qubit: 0,
control: 0,
}];
let mut sim = TableauSimulator::with_seed(1, 0);
assert_eq!(
sim.apply_batch(&program),
Err(SimError::MissingBatchRecord { index: 0 })
);
}
#[test]
fn empty_batch_reports_no_peak() {
let mut sim = TableauSimulator::with_seed(2, 0);
let outcome = sim.apply_batch(&[]).expect("empty program");
assert_eq!(outcome.max_rank, 0);
assert!(outcome.records.is_empty());
}
#[test]
fn max_rank_reports_the_peak_not_the_final_rank() {
let program = [
Instruction::Gate1 {
gate: Gate1Q::H,
qubit: 0,
},
Instruction::T {
basis: PauliBasis::Z,
qubit: 0,
adjoint: false,
},
Instruction::Reset {
basis: PauliBasis::Z,
qubit: 0,
},
];
let mut sim = TableauSimulator::with_seed(1, 5);
let outcome = sim.apply_batch(&program).expect("valid program");
assert_eq!(outcome.max_rank, 2);
assert_eq!(sim.rank(), 1);
}
#[test]
fn a_failed_instruction_aborts_the_batch_in_place() {
let program = [
Instruction::Gate1 {
gate: Gate1Q::H,
qubit: 0,
},
Instruction::Gate2 {
control: PauliBasis::Z,
target: PauliBasis::X,
control_qubit: 0,
target_qubit: 0,
},
Instruction::Gate1 {
gate: Gate1Q::X,
qubit: 0,
},
];
let mut sim = TableauSimulator::with_seed(1, 0);
assert_eq!(
sim.apply_batch(&program),
Err(SimError::RepeatedQubit(0)),
"the batch must surface the operand error"
);
let mut prefix = TableauSimulator::with_seed(1, 0);
prefix.h(0);
assert_same_state(&sim, &prefix, "aborted batch keeps its prefix");
}
}