use std::collections::HashMap;
use std::fmt::Write as _;
use crate::circuit::{fmt_args, Circuit};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Piece {
pub detectors: Vec<u32>,
pub observables: u64,
}
#[derive(Clone, Debug)]
pub struct Mechanism {
pub p: f64,
pub detectors: Vec<u32>,
pub observables: u64,
pub pieces: Vec<Piece>,
pub tag: String,
}
#[derive(Clone, Debug, Default)]
pub struct Dem {
pub num_detectors: usize,
pub num_observables: usize,
pub detector_coords: Vec<Vec<f64>>,
pub mechanisms: Vec<Mechanism>,
pub detector_tags: Vec<String>,
pub observable_tags: Vec<String>,
}
pub fn depolarize1_component(p: f64) -> f64 {
0.5 - 0.5 * (1.0 - 4.0 * p / 3.0).sqrt()
}
pub fn depolarize2_component(p: f64) -> f64 {
0.5 - 0.5 * (1.0 - 16.0 * p / 15.0).powf(0.125)
}
pub fn pauli_channel_1_independent(px: f64, py: f64, pz: f64) -> Result<(f64, f64, f64), String> {
let lx = 1.0 - 2.0 * (py + pz);
let ly = 1.0 - 2.0 * (px + pz);
let lz = 1.0 - 2.0 * (px + py);
if lx <= 0.0 || ly <= 0.0 || lz <= 0.0 {
return Err(format!("PAULI_CHANNEL_1({px}, {py}, {pz}) has no equivalent set of independent errors"));
}
let q = |a: f64| 0.5 * (1.0 - a);
Ok((q((ly * lz / lx).sqrt()), q((lx * lz / ly).sqrt()), q((lx * ly / lz).sqrt())))
}
pub fn xor_prob(a: f64, b: f64) -> f64 {
a * (1.0 - b) + b * (1.0 - a)
}
impl Dem {
pub fn from_circuit(circuit: &Circuit) -> Result<Dem, String> {
Dem::build(circuit, true, None)
}
pub fn from_circuit_with(circuit: &Circuit, decompose: bool, approximate: Option<f64>) -> Result<Dem, String> {
Dem::build(circuit, decompose, approximate)
}
pub fn from_circuit_undecomposed(circuit: &Circuit) -> Result<Dem, String> {
Dem::build(circuit, false, None)
}
fn build(circuit: &Circuit, decompose: bool, approximate: Option<f64>) -> Result<Dem, String> {
crate::dem_build::build(circuit, decompose, approximate, true)?.to_dem_merged()
}
}
fn push_targets(s: &mut String, dets: &[u32], obs: u64) {
for d in dets {
let _ = write!(s, " D{d}");
}
for k in 0..64 {
if (obs >> k) & 1 == 1 {
let _ = write!(s, " L{k}");
}
}
}
impl Dem {
pub fn to_stim(&self, with_pieces: bool) -> String {
let mut s = String::new();
let tagged = |t: &str| if t.is_empty() { String::new() } else { format!("[{t}]") };
for (i, c) in self.detector_coords.iter().enumerate() {
let tag = tagged(self.detector_tags.get(i).map_or("", |t| t.as_str()));
if c.is_empty() {
let _ = writeln!(s, "detector{tag} D{i}");
} else {
let _ = writeln!(s, "detector{tag}({}) D{i}", fmt_args(c));
}
}
let flipped = self.mechanisms.iter().fold(0u64, |acc, m| acc | m.observables);
for k in 0..self.num_observables.min(64) {
let tag = self.observable_tags.get(k).map_or("", |t| t.as_str());
if !tag.is_empty() || (flipped >> k) & 1 == 0 {
let _ = writeln!(s, "logical_observable{} L{k}", tagged(tag));
}
}
for m in &self.mechanisms {
let _ = write!(s, "error{}({})", tagged(&m.tag), m.p);
if with_pieces && !m.pieces.is_empty() {
for (k, piece) in m.pieces.iter().enumerate() {
if k > 0 {
s.push_str(" ^");
}
push_targets(&mut s, &piece.detectors, piece.observables);
}
} else {
push_targets(&mut s, &m.detectors, m.observables);
}
s.push('\n');
}
s
}
pub fn parse(text: &str) -> Result<Dem, String> {
crate::dem_program::DemProgram::parse(text)?.to_dem()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Comparison {
pub ours: usize,
pub theirs: usize,
pub missing: usize,
pub extra: usize,
pub differing: usize,
pub max_rel: f64,
}
fn merged(d: &Dem) -> HashMap<(Vec<u32>, u64), f64> {
let mut out: HashMap<(Vec<u32>, u64), f64> = HashMap::new();
for m in &d.mechanisms {
let e = out.entry((m.detectors.clone(), m.observables)).or_insert(0.0);
*e = xor_prob(*e, m.p);
}
out
}
pub fn compare(ours: &Dem, theirs: &Dem, tol: f64) -> Comparison {
let a = merged(ours);
let b = merged(theirs);
let mut c = Comparison { ours: a.len(), theirs: b.len(), missing: 0, extra: 0, differing: 0, max_rel: 0.0 };
for (k, &pa) in &a {
match b.get(k) {
Some(&pb) => {
let rel = (pa - pb).abs() / pa.abs().max(pb.abs());
c.max_rel = c.max_rel.max(rel);
if rel > tol {
c.differing += 1;
}
}
None => c.extra += 1,
}
}
c.missing = b.keys().filter(|k| !a.contains_key(*k)).count();
c
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::{REP3, SAMPLE};
fn dem(text: &str) -> Dem {
Dem::from_circuit(&Circuit::parse(text).unwrap()).unwrap()
}
fn p_of(d: &Dem, dets: &[u32], obs: u64) -> f64 {
d.mechanisms.iter().find(|m| m.detectors == dets && m.observables == obs).map(|m| m.p).unwrap()
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-12 * a.abs().max(b.abs())
}
#[test]
fn depolarizing_channels_match_stim() {
let d = dem("R 0\nDEPOLARIZE1(0.01) 0\nM 0\nDETECTOR rec[-1]");
assert_eq!(d.mechanisms.len(), 1);
assert!(close(d.mechanisms[0].p, 0.006666666666666613), "{}", d.mechanisms[0].p);
let d = dem("R 0 1\nDEPOLARIZE2(0.01) 0 1\nM 0 1\nDETECTOR rec[-1]\nDETECTOR rec[-2]");
assert_eq!(d.mechanisms.len(), 3);
for m in &d.mechanisms {
assert!(close(m.p, 0.002673815958446298), "{}", m.p);
}
}
#[test]
fn pauli_channel_and_flips_match_stim() {
let d = dem("R 0\nPAULI_CHANNEL_1(0.01, 0.02, 0.03) 0\nM 0\nDETECTOR rec[-1]\n\
RX 1\nPAULI_CHANNEL_1(0.01, 0.02, 0.03) 1\nMX 1\nDETECTOR rec[-1]");
assert!(close(p_of(&d, &[0], 0), 0.03) && close(p_of(&d, &[1], 0), 0.05));
let d = dem("R 0\nX_ERROR(0.01) 0\nM(0.02) 0\nDETECTOR rec[-1]\nRX 1\nY_ERROR(0.1) 1\nMRX 1\nDETECTOR rec[-1]");
assert!(close(p_of(&d, &[0], 0), 0.0296) && close(p_of(&d, &[1], 0), 0.1));
}
#[test]
fn gates_propagate_errors_backwards_correctly() {
let d = dem("R 0 1\nX_ERROR(0.1) 0\nCX 0 1\nM 0 1\nDETECTOR rec[-2]\nDETECTOR rec[-1]");
assert!(close(p_of(&d, &[0, 1], 0), 0.1));
let d = dem("RX 0 1\nZ_ERROR(0.2) 1\nCX 0 1\nMX 0 1\nDETECTOR rec[-2]\nDETECTOR rec[-1]");
assert!(close(p_of(&d, &[0, 1], 0), 0.2));
let d = dem("R 0\nRX 1\nX_ERROR(0.1) 0\nCZ 0 1\nM 0\nMX 1\nDETECTOR rec[-2]\nDETECTOR rec[-1]");
assert!(close(p_of(&d, &[0, 1], 0), 0.1));
let d = dem("R 0\nX_ERROR(0.1) 0\nZ_ERROR(0.2) 0\nH 0\nMX 0\nDETECTOR rec[-1]");
assert_eq!(d.mechanisms.len(), 1);
assert!(close(p_of(&d, &[0], 0), 0.1));
}
#[test]
fn nondeterministic_detectors_are_errors() {
let e = Dem::from_circuit(&Circuit::parse("RX 0\nM 0\nDETECTOR rec[-1]").unwrap()).unwrap_err();
assert!(e.contains("not deterministic"), "{e}");
let e = Dem::from_circuit(&Circuit::parse("M 0\nMX 0\nDETECTOR rec[-1]").unwrap()).unwrap_err();
assert!(e.contains("not deterministic"), "{e}");
}
#[test]
fn undetectable_logical_errors_are_errors() {
let e = Dem::from_circuit(&Circuit::parse("R 0\nX_ERROR(0.1) 0\nM 0\nOBSERVABLE_INCLUDE(0) rec[-1]").unwrap())
.unwrap_err();
assert!(e.contains("undetectable"), "{e}");
}
#[test]
fn sample_and_rep3_build() {
let d = dem(SAMPLE);
assert_eq!(d.num_detectors, 4);
let d = dem(REP3);
assert_eq!((d.num_detectors, d.num_observables), (6, 1));
assert!(d.mechanisms.iter().any(|m| m.observables == 1));
}
#[test]
fn stim_text_round_trips() {
let d = dem(REP3);
let back = Dem::parse(&d.to_stim(false)).unwrap();
let c = compare(&d, &back, 1e-12);
assert_eq!((c.missing, c.extra, c.differing), (0, 0, 0));
assert_eq!(back.num_detectors, d.num_detectors);
assert_eq!(back.detector_coords, d.detector_coords);
}
#[test]
fn parse_reads_pieces_repeats_and_shifts() {
let d = Dem::parse(
"error(0.1) D0 D1 ^ D2\nrepeat 2 {\n error(0.2) D0\n shift_detectors 1\n}\ndetector(1, 2) D0\n",
)
.unwrap();
assert_eq!(d.mechanisms[0].detectors, vec![0, 1, 2]);
assert_eq!(d.mechanisms[0].pieces.len(), 2);
assert_eq!(d.mechanisms[1].detectors, vec![0]);
assert_eq!(d.mechanisms[2].detectors, vec![1]);
assert_eq!(d.detector_coords[2], vec![1.0, 2.0]);
assert_eq!(d.num_detectors, 3);
}
#[test]
fn comparison_counts_differences() {
let a = Dem::parse("error(0.1) D0\nerror(0.2) D1").unwrap();
let b = Dem::parse("error(0.1) D0\nerror(0.25) D1\nerror(0.1) D0 D1").unwrap();
let c = compare(&a, &b, 1e-9);
assert_eq!((c.ours, c.theirs, c.missing, c.extra, c.differing), (2, 3, 1, 0, 1));
assert!((c.max_rel - 0.2).abs() < 1e-12);
}
#[test]
fn pauli_gates_and_sweep_bits_leave_the_model_alone() {
let base = Circuit::parse(crate::fixtures::REP3).unwrap();
let text = crate::fixtures::REP3.replacen("TICK\n", "TICK\nX 0 1\nY 2\nCX sweep[0] 1\nI 0\n", 1);
let with = Circuit::parse(&text).unwrap();
assert_ne!(with, base);
assert_eq!(Dem::from_circuit(&with).unwrap().to_stim(true), Dem::from_circuit(&base).unwrap().to_stim(true));
}
fn normalised(dem: &Dem) -> Vec<String> {
let mut out: Vec<String> = dem
.mechanisms
.iter()
.map(|m| {
let mut pieces: Vec<String> = m
.pieces
.iter()
.map(|pc| {
let mut t: Vec<String> = pc.detectors.iter().map(|d| format!("D{d}")).collect();
t.extend((0..64).filter(|i| (pc.observables >> i) & 1 == 1).map(|i| format!("L{i}")));
t.sort();
t.join(" ")
})
.collect();
pieces.sort();
let p = format!("{:.9}", m.p);
format!("{} {}", p.trim_end_matches('0').trim_end_matches('.'), pieces.join(" ^ "))
})
.collect();
out.sort();
out
}
#[test]
fn conflicting_pieces_split_as_stim_splits_them() {
let cases: &[(&str, &str, &[&str])] = &[
("obs-on-q1-reordered", "R 0 1 2\nX_ERROR(0.05) 0\nCX 0 2\nX_ERROR(0.2) 1\nX_ERROR(0.1) 0\nX_ERROR(0.1) 2\nM 0 1 2\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-2]\n", &["0.05 D0 D1 L0 ^ D2 ^ L0", "0.1 D0 D1", "0.1 D2", "0.2 D0 D1 L0"]),
("obs-on-q1", "R 0 1 2\nX_ERROR(0.05) 0\nCX 0 2\nX_ERROR(0.1) 0\nX_ERROR(0.2) 1\nX_ERROR(0.1) 2\nM 0 1 2\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-2]\n", &["0.05 D0 D1 L0 ^ D2 ^ L0", "0.1 D0 D1", "0.1 D2", "0.2 D0 D1 L0"]),
("obs-on-q0", "R 0 1 2\nX_ERROR(0.05) 0\nCX 0 2\nX_ERROR(0.1) 0\nX_ERROR(0.2) 1\nX_ERROR(0.1) 2\nM 0 1 2\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-3]\n", &["0.05 D0 D1 L0 ^ D2", "0.1 D0 D1 L0", "0.1 D2", "0.2 D0 D1"]),
("split-through-q1", "R 0 1 2\nX_ERROR(0.05) 1\nCX 1 2\nX_ERROR(0.1) 0\nX_ERROR(0.2) 1\nX_ERROR(0.1) 2\nM 0 1 2\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-3] rec[-2]\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-2]\n", &["0.05 D0 D1 L0 ^ D2", "0.1 D0 D1", "0.1 D2", "0.2 D0 D1 L0"]),
("three-way", "R 0 1 2 3\nX_ERROR(0.05) 0\nCX 0 3\nX_ERROR(0.04) 2\nCX 2 3\nX_ERROR(0.1) 0\nX_ERROR(0.2) 1\nX_ERROR(0.15) 2\nX_ERROR(0.1) 3\nM 0 1 2 3\nDETECTOR rec[-4] rec[-3] rec[-2]\nDETECTOR rec[-4] rec[-3] rec[-2]\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-3]\nOBSERVABLE_INCLUDE(1) rec[-2]\n", &["0.04 D0 D1 L1 ^ D2", "0.05 D0 D1 L1 ^ D2 ^ L1", "0.1 D0 D1", "0.1 D2", "0.15 D0 D1 L1", "0.2 D0 D1 L0"]),
];
for &(name, text, stim) in cases {
let dem = Dem::from_circuit(&Circuit::parse(text).unwrap()).unwrap();
assert_eq!(normalised(&dem), stim.iter().map(|s| s.to_string()).collect::<Vec<_>>(), "{name}");
}
}
}