use crate::gates::Gate;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SignedPauli {
pub x: Vec<u64>,
pub z: Vec<u64>,
pub phase4: u8,
}
impl SignedPauli {
fn zero(num_words: usize) -> Self {
Self {
x: vec![0u64; num_words],
z: vec![0u64; num_words],
phase4: 0,
}
}
#[inline(always)]
pub fn get_x(&self, q: usize) -> bool {
(self.x[q >> 6] >> (q & 63)) & 1 == 1
}
#[inline(always)]
pub fn get_z(&self, q: usize) -> bool {
(self.z[q >> 6] >> (q & 63)) & 1 == 1
}
#[inline(always)]
fn set_x(&mut self, q: usize, b: bool) {
let m = 1u64 << (q & 63);
if b {
self.x[q >> 6] |= m;
} else {
self.x[q >> 6] &= !m;
}
}
#[inline(always)]
fn set_z(&mut self, q: usize, b: bool) {
let m = 1u64 << (q & 63);
if b {
self.z[q >> 6] |= m;
} else {
self.z[q >> 6] &= !m;
}
}
pub fn pauli_at(&self, q: usize) -> PauliKind {
match (self.get_x(q), self.get_z(q)) {
(false, false) => PauliKind::I,
(true, false) => PauliKind::X,
(true, true) => PauliKind::Y,
(false, true) => PauliKind::Z,
}
}
pub(crate) fn mps_factors(&self, n: usize) -> Vec<(usize, crate::backend::mps::MpsPauliAxis)> {
(0..n)
.filter_map(|q| self.pauli_at(q).to_mps_axis().map(|axis| (q, axis)))
.collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PauliKind {
I,
X,
Y,
Z,
}
impl PauliKind {
pub(crate) fn to_mps_axis(self) -> Option<crate::backend::mps::MpsPauliAxis> {
use crate::backend::mps::MpsPauliAxis;
match self {
PauliKind::I => None,
PauliKind::X => Some(MpsPauliAxis::X),
PauliKind::Y => Some(MpsPauliAxis::Y),
PauliKind::Z => Some(MpsPauliAxis::Z),
}
}
}
#[inline(always)]
fn letter_product_phase(ax: bool, az: bool, bx: bool, bz: bool) -> u8 {
match ((ax, az), (bx, bz)) {
((false, false), _) | (_, (false, false)) => 0,
((true, false), (true, false)) => 0,
((false, true), (false, true)) => 0,
((true, true), (true, true)) => 0,
((true, false), (true, true)) => 1,
((true, false), (false, true)) => 3,
((true, true), (true, false)) => 3,
((true, true), (false, true)) => 1,
((false, true), (true, false)) => 1,
((false, true), (true, true)) => 3,
}
}
fn rowmul_into(dst: &mut SignedPauli, src: &SignedPauli, n: usize, extra_phase4: u8) {
let mut total: u32 = u32::from(dst.phase4) + u32::from(src.phase4) + u32::from(extra_phase4);
for q in 0..n {
let ax = dst.get_x(q);
let az = dst.get_z(q);
let bx = src.get_x(q);
let bz = src.get_z(q);
total += u32::from(letter_product_phase(ax, az, bx, bz));
}
for w in 0..dst.x.len() {
dst.x[w] ^= src.x[w];
dst.z[w] ^= src.z[w];
}
dst.phase4 = (total & 3) as u8;
}
fn rowmul_within(rows: &mut [SignedPauli], dst: usize, src: usize, n: usize, extra_phase4: u8) {
debug_assert_ne!(dst, src, "rowmul_within requires distinct rows");
let hi = dst.max(src);
let (left, right) = rows.split_at_mut(hi);
if dst < src {
rowmul_into(&mut left[dst], &right[0], n, extra_phase4);
} else {
rowmul_into(&mut right[0], &left[src], n, extra_phase4);
}
}
#[derive(Clone, Debug)]
pub(crate) struct SignedCliffordPrefix {
num_qubits: usize,
pub(crate) inv_x: Vec<SignedPauli>,
pub(crate) inv_z: Vec<SignedPauli>,
}
impl SignedCliffordPrefix {
pub fn identity(num_qubits: usize) -> Self {
let num_words = num_qubits.div_ceil(64).max(1);
let mut inv_x = Vec::with_capacity(num_qubits);
let mut inv_z = Vec::with_capacity(num_qubits);
for q in 0..num_qubits {
let mut x = SignedPauli::zero(num_words);
x.set_x(q, true);
inv_x.push(x);
let mut z = SignedPauli::zero(num_words);
z.set_z(q, true);
inv_z.push(z);
}
Self {
num_qubits,
inv_x,
inv_z,
}
}
pub fn num_qubits(&self) -> usize {
self.num_qubits
}
pub fn conjugate_z(&self, q: usize) -> SignedPauli {
self.inv_z[q].clone()
}
pub fn conjugate_x(&self, q: usize) -> SignedPauli {
self.inv_x[q].clone()
}
pub fn apply_state_gate(&mut self, gate: &Gate, targets: &[usize]) -> Result<(), &'static str> {
match gate {
Gate::Id => Ok(()),
Gate::H => {
self.apply_h(targets[0]);
Ok(())
}
Gate::S => {
self.apply_s(targets[0]);
Ok(())
}
Gate::Sdg => {
self.apply_sdg(targets[0]);
Ok(())
}
Gate::SX => {
self.apply_sx(targets[0]);
Ok(())
}
Gate::SXdg => {
self.apply_sxdg(targets[0]);
Ok(())
}
Gate::X => {
self.apply_x(targets[0]);
Ok(())
}
Gate::Y => {
self.apply_y(targets[0]);
Ok(())
}
Gate::Z => {
self.apply_z(targets[0]);
Ok(())
}
Gate::Cx => {
self.apply_cx(targets[0], targets[1]);
Ok(())
}
Gate::Cz => {
self.apply_cz(targets[0], targets[1]);
Ok(())
}
Gate::Swap => {
self.apply_swap(targets[0], targets[1]);
Ok(())
}
_ => Err("non-Clifford gate cannot be absorbed into the SignedCliffordPrefix"),
}
}
fn apply_h(&mut self, p: usize) {
std::mem::swap(&mut self.inv_x[p], &mut self.inv_z[p]);
}
fn apply_s(&mut self, p: usize) {
let n = self.num_qubits;
rowmul_into(&mut self.inv_x[p], &self.inv_z[p], n, 3);
}
fn apply_sdg(&mut self, p: usize) {
let n = self.num_qubits;
rowmul_into(&mut self.inv_x[p], &self.inv_z[p], n, 1);
}
fn apply_sx(&mut self, p: usize) {
let n = self.num_qubits;
rowmul_into(&mut self.inv_z[p], &self.inv_x[p], n, 3);
}
fn apply_sxdg(&mut self, p: usize) {
let n = self.num_qubits;
rowmul_into(&mut self.inv_z[p], &self.inv_x[p], n, 1);
}
fn apply_x(&mut self, p: usize) {
self.inv_z[p].phase4 = (self.inv_z[p].phase4 + 2) & 3;
}
fn apply_y(&mut self, p: usize) {
self.inv_x[p].phase4 = (self.inv_x[p].phase4 + 2) & 3;
self.inv_z[p].phase4 = (self.inv_z[p].phase4 + 2) & 3;
}
fn apply_z(&mut self, p: usize) {
self.inv_x[p].phase4 = (self.inv_x[p].phase4 + 2) & 3;
}
fn apply_cx(&mut self, ctrl: usize, tgt: usize) {
let n = self.num_qubits;
rowmul_within(&mut self.inv_x, ctrl, tgt, n, 0);
rowmul_within(&mut self.inv_z, tgt, ctrl, n, 0);
}
fn apply_cz(&mut self, a: usize, b: usize) {
let n = self.num_qubits;
rowmul_into(&mut self.inv_x[a], &self.inv_z[b], n, 0);
rowmul_into(&mut self.inv_x[b], &self.inv_z[a], n, 0);
}
fn apply_swap(&mut self, a: usize, b: usize) {
self.inv_x.swap(a, b);
self.inv_z.swap(a, b);
}
pub(crate) fn fold_right_state_gate(
&mut self,
gate: &Gate,
targets: &[usize],
) -> Result<(), &'static str> {
match gate {
Gate::Id => Ok(()),
Gate::H => {
self.fold_right_h(targets[0]);
Ok(())
}
Gate::S => {
self.fold_right_s(targets[0]);
Ok(())
}
Gate::Sdg => {
self.fold_right_sdg(targets[0]);
Ok(())
}
Gate::X => {
self.fold_right_x(targets[0]);
Ok(())
}
Gate::Y => {
self.fold_right_y(targets[0]);
Ok(())
}
Gate::Z => {
self.fold_right_z(targets[0]);
Ok(())
}
Gate::Cx => {
self.fold_right_cx(targets[0], targets[1]);
Ok(())
}
Gate::Cz => {
self.fold_right_cz(targets[0], targets[1]);
Ok(())
}
_ => Err("gate not supported in fold_right_state_gate"),
}
}
fn fold_right_h(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
let xp = row.get_x(p);
let zp = row.get_z(p);
row.set_x(p, zp);
row.set_z(p, xp);
if xp && zp {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
fn fold_right_s(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
if row.get_x(p) {
let had_z = row.get_z(p);
row.set_z(p, !had_z);
if !had_z {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
}
fn fold_right_sdg(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
if row.get_x(p) {
let had_z = row.get_z(p);
row.set_z(p, !had_z);
if had_z {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
}
fn fold_right_x(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
if row.get_z(p) {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
fn fold_right_y(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
let xp = row.get_x(p);
let zp = row.get_z(p);
if xp ^ zp {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
fn fold_right_z(&mut self, p: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
if row.get_x(p) {
row.phase4 = (row.phase4 + 2) & 3;
}
}
}
fn fold_right_cx(&mut self, c: usize, t: usize) {
for row in self.inv_x.iter_mut().chain(self.inv_z.iter_mut()) {
let xc = row.get_x(c);
let zc = row.get_z(c);
let xt = row.get_x(t);
let zt = row.get_z(t);
if xc && zt && (xt ^ zc ^ true) {
row.phase4 = (row.phase4 + 2) & 3;
}
if xc {
row.set_x(t, !xt);
}
if zt {
row.set_z(c, !zc);
}
}
}
fn fold_right_cz(&mut self, a: usize, b: usize) {
self.fold_right_h(b);
self.fold_right_cx(a, b);
self.fold_right_h(b);
}
}
pub(crate) type OfdGate = (Gate, Vec<usize>);
fn build_xy_anchor_cascade(p: &SignedPauli, n: usize, num_qubits: usize) -> Vec<OfdGate> {
let mut cascade: Vec<OfdGate> = Vec::new();
for m in 0..num_qubits {
if m == n {
continue;
}
match p.pauli_at(m) {
PauliKind::I => continue,
PauliKind::X => cascade.push((Gate::Cx, vec![n, m])),
PauliKind::Y => {
cascade.push((Gate::Sdg, vec![m]));
cascade.push((Gate::Cx, vec![n, m]));
cascade.push((Gate::S, vec![m]));
}
PauliKind::Z => cascade.push((Gate::Cz, vec![n, m])),
}
}
cascade
}
fn support_qubits(p: &SignedPauli, num_qubits: usize) -> Vec<usize> {
(0..num_qubits)
.filter(|&q| !matches!(p.pauli_at(q), PauliKind::I))
.collect()
}
pub(crate) fn cascade_routing_cost(
mps: &crate::backend::mps::MpsBackend,
cascade: &[OfdGate],
) -> usize {
cascade
.iter()
.filter(|(_, targets)| targets.len() == 2)
.map(|(_, targets)| {
mps.site_for_qubit(targets[0])
.abs_diff(mps.site_for_qubit(targets[1]))
})
.sum()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DisentanglerKind {
Ofd,
Ofds,
}
pub(crate) fn choose_disentangler(
mps: &crate::backend::mps::MpsBackend,
p: &SignedPauli,
num_qubits: usize,
tol: f64,
) -> crate::error::Result<Option<(usize, Vec<OfdGate>, DisentanglerKind)>> {
let ofd = build_ofd_disentangler(mps, p, num_qubits, tol)?;
let ofds = build_ofds_disentangler(mps, p, num_qubits);
Ok(match (ofd, ofds) {
(Some((n, c_ofd)), Some((m, c_ofds))) => {
if cascade_routing_cost(mps, &c_ofds) < cascade_routing_cost(mps, &c_ofd) {
Some((m, c_ofds, DisentanglerKind::Ofds))
} else {
Some((n, c_ofd, DisentanglerKind::Ofd))
}
}
(Some((n, c)), None) => Some((n, c, DisentanglerKind::Ofd)),
(None, Some((m, c))) => Some((m, c, DisentanglerKind::Ofds)),
(None, None) => None,
})
}
fn anchor_routing_cost(
mps: &crate::backend::mps::MpsBackend,
anchor: usize,
support: &[usize],
) -> usize {
let anchor_site = mps.site_for_qubit(anchor);
support
.iter()
.filter(|&&q| q != anchor)
.map(|&q| mps.site_for_qubit(q).abs_diff(anchor_site))
.sum()
}
pub(crate) fn build_ofd_disentangler(
mps: &crate::backend::mps::MpsBackend,
p: &SignedPauli,
num_qubits: usize,
tol: f64,
) -> crate::error::Result<Option<(usize, Vec<OfdGate>)>> {
let support = support_qubits(p, num_qubits);
let mut best: Option<(usize, usize)> = None;
for &n in &support {
if !matches!(p.pauli_at(n), PauliKind::X | PauliKind::Y) {
continue;
}
if !mps.is_qubit_in_zero_state(n, tol)? {
continue;
}
let cost = anchor_routing_cost(mps, n, &support);
if best.is_none_or(|(_, c)| cost < c) {
best = Some((n, cost));
}
}
Ok(best.map(|(n, _)| (n, build_xy_anchor_cascade(p, n, num_qubits))))
}
pub(crate) fn build_ofds_disentangler(
mps: &crate::backend::mps::MpsBackend,
p: &SignedPauli,
num_qubits: usize,
) -> Option<(usize, Vec<OfdGate>)> {
let support = support_qubits(p, num_qubits);
let xy_candidates: Vec<usize> = support
.iter()
.copied()
.filter(|&n| matches!(p.pauli_at(n), PauliKind::X | PauliKind::Y))
.collect();
if let Some(&anchor) = xy_candidates
.iter()
.min_by_key(|&&n| anchor_routing_cost(mps, n, &support))
{
return Some((anchor, build_xy_anchor_cascade(p, anchor, num_qubits)));
}
let z_support: Vec<usize> = support
.iter()
.copied()
.filter(|&q| matches!(p.pauli_at(q), PauliKind::Z))
.collect();
if z_support.len() < 2 {
return None;
}
let anchor = *z_support
.iter()
.min_by_key(|&&q| anchor_routing_cost(mps, q, &z_support))
.unwrap();
let mut z_sites: Vec<(usize, usize)> = z_support
.iter()
.map(|&q| (mps.site_for_qubit(q), q))
.collect();
z_sites.sort_by_key(|&(s, _)| s);
let ordered: Vec<usize> = z_sites.into_iter().map(|(_, q)| q).collect();
let anchor_pos = ordered.iter().position(|&q| q == anchor).unwrap();
let mut cascade: Vec<OfdGate> = Vec::with_capacity(ordered.len() - 1);
for i in 0..anchor_pos {
cascade.push((Gate::Cx, vec![ordered[i], ordered[i + 1]]));
}
for i in (anchor_pos + 1..ordered.len()).rev() {
cascade.push((Gate::Cx, vec![ordered[i], ordered[i - 1]]));
}
Some((anchor, cascade))
}
pub(crate) fn evaluate_pauli_observable_camps(
prefix: &SignedCliffordPrefix,
mps: &crate::backend::mps::MpsBackend,
terms: &[crate::sim::unified_pauli::PauliTerm],
) -> crate::error::Result<f64> {
use crate::sim::unified_pauli::PauliAxis;
let n = prefix.num_qubits();
let num_words = n.div_ceil(64).max(1);
let mut combined = SignedPauli::zero(num_words);
for term in terms {
let row = match term.axis {
PauliAxis::Z => prefix.conjugate_z(term.qubit),
PauliAxis::X => prefix.conjugate_x(term.qubit),
PauliAxis::Y => {
let mut row = prefix.conjugate_x(term.qubit);
rowmul_into(&mut row, &prefix.inv_z[term.qubit], n, 1);
row
}
};
rowmul_into(&mut combined, &row, n, 0);
}
let factors = combined.mps_factors(n);
let p = u32::from(combined.phase4);
let coef_re = match p {
0 => 1.0,
2 => -1.0,
_ => {
return Err(crate::error::PrismError::InvalidParameter {
message: format!(
"CAMPS observable: expected Hermitian (real ±1) twisted coefficient, \
got i^{p}"
),
});
}
};
let val = mps.pauli_expectation(&factors)?;
Ok(coef_re * val.re)
}
const CAMPS_TRUNCATION_TOL: f64 = 1e-12;
fn check_camps_truncation(
mps: &crate::backend::mps::MpsBackend,
target: usize,
) -> crate::error::Result<()> {
let discarded = mps.truncation_discarded();
if discarded > CAMPS_TRUNCATION_TOL {
return Err(crate::error::PrismError::InvalidParameter {
message: format!(
"CAMPS T-gate on qubit {target}: disentangler cascade exceeded the MPS bond-dim \
cap and SVD truncation discarded {discarded:.3e} of the state weight. Raise \
`max_bond_dim` or use a less-entangling disentangler."
),
});
}
Ok(())
}
pub(crate) fn apply_t_via_camps(
prefix: &mut SignedCliffordPrefix,
mps: &mut crate::backend::mps::MpsBackend,
target: usize,
is_dagger: bool,
tol: f64,
) -> crate::error::Result<()> {
let z_bar = prefix.conjugate_z(target);
let n_qubits = prefix.num_qubits();
let support: Vec<usize> = (0..n_qubits)
.filter(|&q| !matches!(z_bar.pauli_at(q), PauliKind::I))
.collect();
if support.is_empty() {
return Ok(());
}
if support.len() == 1 {
mps.reset_truncation_tracking();
match build_ofd_disentangler(mps, &z_bar, n_qubits, tol)? {
Some((n, cascade)) => {
apply_cascade_and_rotate(prefix, mps, &cascade, n, target, is_dagger)?;
}
_ => {
apply_single_qubit_rotation_to_mps(mps, &z_bar, support[0], is_dagger)?;
}
}
return check_camps_truncation(mps, target);
}
match choose_disentangler(mps, &z_bar, n_qubits, tol)? {
Some((n, cascade, _kind)) => {
mps.reset_truncation_tracking();
apply_cascade_and_rotate(prefix, mps, &cascade, n, target, is_dagger)?;
check_camps_truncation(mps, target)
}
None => {
let letters: String = (0..n_qubits)
.map(|q| match z_bar.pauli_at(q) {
PauliKind::I => '.',
PauliKind::X => 'X',
PauliKind::Y => 'Y',
PauliKind::Z => 'Z',
})
.collect();
Err(crate::error::PrismError::InvalidParameter {
message: format!(
"CAMPS T-gate on qubit {target}: invariant violation in disentangler dispatch. \
Twisted Pauli has support size {sz} (>=2 expected) at qubits {support:?} with letters \
`{letters}`, phase4={phase}. Both OFD and OFDS declined a multi-qubit support. \
Add an explicit fallback for this support pattern in `apply_t_via_camps`.",
sz = support.len(),
phase = z_bar.phase4,
),
})
}
}
}
fn apply_cascade_and_rotate(
prefix: &mut SignedCliffordPrefix,
mps: &mut crate::backend::mps::MpsBackend,
cascade: &[OfdGate],
anchor_n: usize,
target: usize,
is_dagger: bool,
) -> crate::error::Result<()> {
use crate::backend::Backend;
use crate::circuit::{Instruction, SmallVec};
for (gate, targets) in cascade {
mps.apply(&Instruction::Gate {
gate: gate.clone(),
targets: SmallVec::from_slice(targets),
})?;
}
for (gate, targets) in cascade.iter() {
let inv = match gate {
Gate::S => Gate::Sdg,
Gate::Sdg => Gate::S,
Gate::Cx | Gate::Cz => gate.clone(),
other => {
return Err(crate::error::PrismError::InvalidParameter {
message: format!(
"CAMPS T-gate: cascade emitted unexpected gate {other:?} (no inverse rule)"
),
});
}
};
prefix.fold_right_state_gate(&inv, targets).map_err(|e| {
crate::error::PrismError::InvalidParameter {
message: format!("CAMPS T-gate: prefix update failed: {e}"),
}
})?;
}
let new_z_bar = prefix.conjugate_z(target);
let n_qubits = prefix.num_qubits();
let stray: Vec<usize> = (0..n_qubits)
.filter(|&q| q != anchor_n && !matches!(new_z_bar.pauli_at(q), PauliKind::I))
.collect();
if !stray.is_empty() {
return Err(crate::error::PrismError::InvalidParameter {
message: format!(
"CAMPS T-gate on qubit {target}: disentangler did not concentrate the twisted \
Pauli onto anchor qubit {anchor_n}; residual support remains on qubits {stray:?}. \
The single-qubit rotation would silently drop those factors and corrupt the state."
),
});
}
apply_single_qubit_rotation_to_mps(mps, &new_z_bar, anchor_n, is_dagger)
}
fn apply_single_qubit_rotation_to_mps(
mps: &mut crate::backend::mps::MpsBackend,
pauli: &SignedPauli,
q: usize,
is_dagger: bool,
) -> crate::error::Result<()> {
use crate::backend::Backend;
use crate::circuit::{Instruction, SmallVec};
use num_complex::Complex64;
let phase = match pauli.phase4 & 3 {
0 => Complex64::new(1.0, 0.0),
1 => Complex64::new(0.0, 1.0),
2 => Complex64::new(-1.0, 0.0),
3 => Complex64::new(0.0, -1.0),
_ => unreachable!(),
};
let bx = pauli.get_x(q);
let bz = pauli.get_z(q);
let zc = Complex64::new(0.0, 0.0);
let i = Complex64::new(0.0, 1.0);
let op_at_q: [[Complex64; 2]; 2] = match (bx, bz) {
(true, false) => [[zc, phase], [phase, zc]],
(true, true) => [[zc, -i * phase], [i * phase, zc]],
(false, true) => [[phase, zc], [zc, -phase]],
_ => {
return Err(crate::error::PrismError::InvalidParameter {
message: format!("CAMPS rotation: Pauli at qubit {q} is identity; expected X/Y/Z"),
});
}
};
let alpha = (std::f64::consts::FRAC_PI_8).cos();
let sin_pi8 = (std::f64::consts::FRAC_PI_8).sin();
let beta = if is_dagger {
Complex64::new(0.0, sin_pi8)
} else {
Complex64::new(0.0, -sin_pi8)
};
let alpha_c = Complex64::new(alpha, 0.0);
let mat: [[Complex64; 2]; 2] = [
[alpha_c + beta * op_at_q[0][0], beta * op_at_q[0][1]],
[beta * op_at_q[1][0], alpha_c + beta * op_at_q[1][1]],
];
mps.apply(&Instruction::Gate {
gate: Gate::Fused(Box::new(mat)),
targets: SmallVec::from_slice(&[q]),
})?;
Ok(())
}
#[cfg(test)]
#[path = "camps_prefix_tests.rs"]
mod tests;