use super::{FourStarTorus, MatchingDecode};
use crate::photonic::stabilizer::{PauliNoise, ReferenceSampler, StabilizerSimulator};
use crate::photonic::{CompiledFusionNetwork, runtime::shot_seed};
use crate::{Error, Result};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use std::time::Instant;
#[derive(Clone, Copy, Debug)]
pub struct NetworkNoise {
pub(super) loss: f64,
pub(super) pauli: PauliNoise,
pub(super) unboosted: bool,
}
impl NetworkNoise {
pub fn new(photon_loss: f64, pauli: PauliNoise, unboosted: bool) -> Result<Self> {
if !photon_loss.is_finite() || !(0.0..=1.0).contains(&photon_loss) {
return Err(Error::InvalidValue("invalid photon loss".into()));
}
Ok(Self {
loss: photon_loss,
pauli,
unboosted,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WilsonInterval {
pub lower: f64,
pub upper: f64,
}
impl WilsonInterval {
pub fn new(events: usize, trials: usize, z: f64) -> Result<Self> {
if trials == 0 || events > trials || !z.is_finite() || z <= 0. {
return Err(Error::InvalidValue("invalid interval inputs".into()));
}
let n = trials as f64;
let p = events as f64 / n;
let d = 1. + z * z / n;
let c = (p + z * z / (2. * n)) / d;
let h = z * ((p * (1. - p) + z * z / (4. * n)) / n).sqrt() / d;
Ok(Self {
lower: (c - h).max(0.),
upper: (c + h).min(1.),
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MemoryStudy {
pub attempts: usize,
pub corrected: usize,
pub logical_errors: usize,
pub erased_logicals: usize,
pub decoder_budget: usize,
}
impl MemoryStudy {
pub fn unsuccessful_interval(&self, z: f64) -> Result<WilsonInterval> {
let unsuccessful = self
.attempts
.checked_sub(self.corrected)
.ok_or_else(|| Error::InvalidValue("corrected attempts exceed total".into()))?;
WilsonInterval::new(unsuccessful, self.attempts, z)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MemoryStudyTimings {
pub setup_seconds: f64,
pub sampling_seconds: f64,
pub decoding_seconds: f64,
}
#[allow(clippy::too_many_arguments)]
pub fn run_four_star_memory(
network: &FourStarTorus,
plan: &CompiledFusionNetwork,
noise: NetworkNoise,
start: u64,
shots: usize,
seed: u64,
max_defects: usize,
max_bits: usize,
) -> Result<MemoryStudy> {
Ok(run_four_star_memory_profiled(
network,
plan,
noise,
start,
shots,
seed,
max_defects,
max_bits,
)?
.0)
}
#[allow(clippy::too_many_arguments)]
pub fn run_four_star_memory_profiled(
network: &FourStarTorus,
plan: &CompiledFusionNetwork,
noise: NetworkNoise,
start: u64,
shots: usize,
seed: u64,
max_defects: usize,
max_bits: usize,
) -> Result<(MemoryStudy, MemoryStudyTimings)> {
if plan.program() != network.program() || !plan.outputs().is_empty() {
return Err(Error::InvalidValue(
"memory plan does not match closed network".into(),
));
}
if plan.transmissions().iter().any(|t| t.values() != [1., 1.]) {
return Err(Error::Unsupported(
"study loss must be specified once via NetworkNoise".into(),
));
}
let setup_start = Instant::now();
let initial = StabilizerSimulator::default().prepare(plan)?;
let observables = network.observables()?;
let sampler = ReferenceSampler::compile(&initial, &observables, 1 << 24)?;
let mut timings = MemoryStudyTimings {
setup_seconds: setup_start.elapsed().as_secs_f64(),
..Default::default()
};
let sampling_start = Instant::now();
let clean = sampler.sample(start, shots, seed, PauliNoise::NONE, max_bits)?;
let noisy = sampler.sample(start, shots, seed, noise.pauli, max_bits)?;
timings.sampling_seconds = sampling_start.elapsed().as_secs_f64();
let fusions = plan.fusions().len();
let mut report = MemoryStudy {
attempts: shots,
..Default::default()
};
let seed = shot_seed(seed, 0, 0x4642_5143_4c4f_0001);
for shot in 0..shots {
let sampling_start = Instant::now();
let mut lost = vec![false; plan.program().num_photons()];
for (q, lost) in lost.iter_mut().enumerate() {
let mut rng = ChaCha8Rng::seed_from_u64(shot_seed(seed, start + shot as u64, q as u64));
*lost = rng.random::<f64>() < noise.loss;
}
let mut xx = vec![false; fusions];
let mut zz = xx.clone();
let mut erased_x = xx.clone();
let mut erased_z = xx.clone();
let mut reference_x = xx.clone();
let mut reference_z = xx.clone();
for (i, fusion) in plan.fusions().iter().enumerate() {
reference_x[i] = clean.bit(2 * i, shot)?;
reference_z[i] = clean.bit(2 * i + 1, shot)?;
let x = noisy.bit(2 * i, shot)?;
let z = noisy.bit(2 * i + 1, shot)?;
let erased = lost[fusion.photons[0].0] || lost[fusion.photons[1].0];
erased_x[i] = erased || (noise.unboosted && !z);
erased_z[i] = erased;
xx[i] = !erased_x[i] && x;
zz[i] = !erased_z[i] && z;
}
timings.sampling_seconds += sampling_start.elapsed().as_secs_f64();
let decoding_start = Instant::now();
let mut ambiguous = false;
let mut budget = false;
let mut failed = false;
for (graph, observed, erased, truth) in [
(network.dual(), &xx, &erased_x, &reference_x),
(network.primal(), &zz, &erased_z, &reference_z),
] {
if graph.syndrome(truth)?.iter().any(|&v| v) {
return Err(Error::InvalidValue(
"resource sample violates geometric fusion checks".into(),
));
}
if graph.erasure_logicals(erased)?.iter().any(|&v| v) {
ambiguous = true;
continue;
}
match graph.decode(&graph.syndrome(observed)?, erased, max_defects)? {
MatchingDecode::Correction(correction) => {
let residual: Vec<_> = observed
.iter()
.zip(truth.iter())
.zip(correction)
.map(|((&a, &b), c)| a ^ b ^ c)
.collect();
if graph.syndrome(&residual)?.iter().any(|&v| v) {
return Err(Error::InvalidValue(
"decoder did not close residual chain".into(),
));
}
failed |= graph.logical_parities(&residual)?.iter().any(|&v| v);
}
MatchingDecode::BudgetExceeded => budget = true,
MatchingDecode::Inconsistent => {
return Err(Error::InvalidValue(
"closed network gave inconsistent syndrome".into(),
));
}
}
}
if ambiguous {
report.erased_logicals += 1;
} else if budget {
report.decoder_budget += 1;
} else if failed {
report.logical_errors += 1;
} else {
report.corrected += 1;
}
timings.decoding_seconds += decoding_start.elapsed().as_secs_f64();
}
Ok((report, timings))
}