use std::{
hash::{BuildHasher, Hasher, RandomState},
thread,
time::Instant,
};
use crate::batch::{
BatchDetectorPostselectionOptions, BatchDetectorPostselectionScratch,
BatchFactoredExecutorState, default_batch_count, execute_batch_in_place_expressions,
execute_batch_postselected_in_place, prepare_batch_detector_postselection_scratch_for_program,
reset_batch_executor,
};
use crate::circuit::ir::CircuitObservableInclude;
use crate::circuit::{Circuit, has_postselection, plan_circuit};
use crate::errors::{Result, TicitError};
use crate::exogenous::{
PackedPresampledExogenous, prepare_presampled_exogenous_packed,
resample_prepared_exogenous_packed_in_place,
};
use crate::factored::FactoredInstructionProgram;
use crate::pinning::{ForcedBranch, MeasurementParity, plan_pinned_measurements};
use crate::presampled_expression::{
PresampledExpressionBlock, PresampledExpressionPlan, evaluate_presampled_expression_block,
prepare_presampled_expression_plan,
};
use crate::random::block_seed;
const EXOGENOUS_SEED_BASE: u64 = 0x7eed_0000;
const BRANCH_SEED_BASE: u64 = 0x5eed_1234;
const DEFAULT_SAMPLE_CHUNK_SHOTS: usize = 2048;
const POSTSELECTION_COMPACTION_DENOMINATOR: usize = 2;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SampleCounts {
pub shots: u64,
pub discarded: u64,
pub accepted: u64,
pub logical_errors: u64,
}
impl SampleCounts {
#[must_use]
pub fn discard_rate(&self) -> f64 {
if self.shots == 0 {
f64::NAN
} else {
self.discarded as f64 / self.shots as f64
}
}
#[must_use]
pub fn logical_error_rate(&self) -> f64 {
if self.accepted == 0 {
f64::NAN
} else {
self.logical_errors as f64 / self.accepted as f64
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct SamplingTiming {
pub compile_s: f64,
pub presample_s: f64,
pub execute_s: f64,
pub sample_s: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SamplerOptions {
pub observable: usize,
pub postselection_mask: Vec<u8>,
pub normalize_syndromes: bool,
pub expected_detectors: Vec<u8>,
pub expected_observables: Vec<u8>,
pub pin_measurements: Vec<MeasurementParity>,
pub sample_chunk_shots: usize,
pub batch_size: usize,
pub threads: usize,
}
impl Default for SamplerOptions {
fn default() -> Self {
Self {
observable: 0,
postselection_mask: Vec::new(),
normalize_syndromes: false,
expected_detectors: Vec::new(),
expected_observables: Vec::new(),
pin_measurements: Vec::new(),
sample_chunk_shots: 0,
batch_size: 0,
threads: 1,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SamplerInfo {
pub qubits: usize,
pub measurement_records: usize,
pub detectors: usize,
pub observables: usize,
pub expectation_values: usize,
pub observable: usize,
pub max_active_qubits: usize,
pub batch_size: usize,
pub sample_chunk_shots: usize,
pub threads: usize,
pub active_components: bool,
pub detector_postselection: bool,
pub cpu_backend: &'static str,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SampleResult {
pub counts: SampleCounts,
pub timing: SamplingTiming,
pub active_threads: usize,
pub record_rows: usize,
pub bit_packed: bool,
pub measurements: Vec<u8>,
pub detectors: Vec<u8>,
pub observables: Vec<u8>,
pub observable_ones: Vec<u64>,
pub exp_vals: Vec<f64>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReferenceSample {
pub detectors: Vec<u8>,
pub observables: Vec<u8>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct SamplingInput {
pub program: FactoredInstructionProgram,
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub logical_records: Vec<Vec<i32>>,
pub observable_records: Vec<Vec<Vec<i32>>>,
pub observable: usize,
pub observable_includes: usize,
pub preprocessing_timing: SamplingTiming,
}
pub(crate) fn logical_records_for_observable(
observables: &[CircuitObservableInclude],
observable: usize,
) -> Vec<Vec<i32>> {
observables
.iter()
.filter(|include| include.index == observable)
.map(|include| {
include
.records
.iter()
.map(|&record| record as i32)
.collect()
})
.collect()
}
pub(crate) fn logical_records_by_observable(
observables: &[CircuitObservableInclude],
observable_count: usize,
) -> Vec<Vec<Vec<i32>>> {
(0..observable_count)
.map(|observable| logical_records_for_observable(observables, observable))
.collect()
}
#[cfg(any(test, feature = "gpu"))]
pub(crate) fn make_circuit_sampling_input(
program: FactoredInstructionProgram,
logical_records: Vec<Vec<i32>>,
observable: usize,
observable_includes: usize,
preprocessing_timing: SamplingTiming,
) -> SamplingInput {
let mut observable_records = vec![Vec::new(); observable_includes];
if let Some(records) = observable_records.get_mut(observable) {
*records = logical_records.clone();
}
SamplingInput {
program,
logical_records,
observable_records,
observable,
observable_includes,
preprocessing_timing,
}
}
fn seconds_since(start: Instant) -> f64 {
start.elapsed().as_secs_f64()
}
fn random_seed() -> u64 {
RandomState::new().build_hasher().finish()
}
fn checked_output_len(shots: u64, columns: usize) -> Result<usize> {
usize::try_from(shots)
.ok()
.and_then(|shots| shots.checked_mul(columns))
.ok_or_else(|| TicitError::new("sampling output size exceeds usize"))
}
fn output_columns(columns: usize, bit_packed: bool) -> usize {
if bit_packed {
columns.div_ceil(8)
} else {
columns
}
}
fn sample_chunk_or_default(requested: usize, min_auto_shots: usize) -> usize {
if requested > 0 {
requested
} else {
DEFAULT_SAMPLE_CHUNK_SHOTS.max(min_auto_shots)
}
}
fn make_info(
program: &FactoredInstructionProgram,
observable: usize,
observable_includes: usize,
options: &SamplerOptions,
batch_size: usize,
postselection: bool,
) -> SamplerInfo {
SamplerInfo {
qubits: program.n,
measurement_records: program.nrecords,
detectors: program.ndetectors,
observables: observable_includes,
expectation_values: program.nexpvals,
observable,
max_active_qubits: program.max_k,
batch_size,
sample_chunk_shots: options.sample_chunk_shots,
threads: options.threads,
active_components: program.use_active_components,
detector_postselection: postselection,
cpu_backend: crate::contiguous::backend_name(),
}
}
fn prepare_expression_plan(
expression_plan: &mut PresampledExpressionPlan,
program: &FactoredInstructionProgram,
) -> Result<()> {
let mut samples = PackedPresampledExogenous::default();
prepare_presampled_exogenous_packed(&mut samples, program)?;
prepare_presampled_expression_plan(expression_plan, program, &samples)
}
pub(crate) fn reference_sample_for_program(
program: &FactoredInstructionProgram,
observable_records: &[Vec<Vec<i32>>],
forced_branches: &[ForcedBranch],
) -> Result<ReferenceSample> {
if program.ndetectors == 0 && observable_records.is_empty() {
return Ok(ReferenceSample::default());
}
let mut samples = PackedPresampledExogenous::default();
prepare_presampled_exogenous_packed(&mut samples, program)?;
samples.nshots = 1;
samples.shot_words = 1;
samples.value_words.resize(program.nsymbols, 0);
samples.value_words.fill(0);
samples.sparse_condition_words.fill(0);
samples.sparse_hit_offsets.resize(program.nsymbols + 1, 0);
samples.sparse_hit_offsets.fill(0);
let mut expression_plan = PresampledExpressionPlan::default();
prepare_presampled_expression_plan(&mut expression_plan, program, &samples)?;
let mut expression_block = PresampledExpressionBlock::default();
evaluate_presampled_expression_block(&mut expression_block, &expression_plan, &samples)?;
let mut runtime = BatchFactoredExecutorState::new(program, 1, 1)?;
runtime.dense_shot_major_active = true;
runtime.store_detector_records = true;
runtime.forced_branches = forced_branches.to_vec();
reset_batch_executor(&mut runtime, program, 1)?;
execute_batch_in_place_expressions(
&mut runtime,
program,
&expression_plan,
&expression_block,
0,
)?;
let detectors = (0..program.ndetectors)
.map(|detector| u8::from(runtime.detector_words[detector * runtime.batch_words] & 1 != 0))
.collect();
let mut observable_words = Vec::new();
fill_observable_words(&mut observable_words, &runtime, observable_records)?;
let observables = (0..observable_records.len())
.map(|observable| u8::from(observable_words[observable * runtime.batch_words] & 1 != 0))
.collect();
Ok(ReferenceSample {
detectors,
observables,
})
}
pub(crate) fn circuit_reference_sample(circuit: &Circuit) -> Result<ReferenceSample> {
let program = plan_circuit(circuit, &[])?;
let observable_records =
logical_records_by_observable(&circuit.observables, circuit.observable_count());
reference_sample_for_program(&program, &observable_records, &[])
}
fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64> {
if denominator == 0 {
return Err(TicitError::new("division by zero in chunk sizing"));
}
Ok(numerator.div_ceil(denominator))
}
fn active_worker_count(requested: usize, nchunks: u64) -> usize {
nchunks.max(1).min(requested as u64) as usize
}
struct BatchWorker {
counts: SampleCounts,
timing: SamplingTiming,
samples: PackedPresampledExogenous,
expression_block: PresampledExpressionBlock,
runtime: BatchFactoredExecutorState,
observable_words: Vec<u64>,
measurements: Vec<u8>,
detectors: Vec<u8>,
observables: Vec<u8>,
observable_ones: Vec<u64>,
exp_vals: Vec<f64>,
postselection_scratch: BatchDetectorPostselectionScratch,
}
pub struct Sampler {
options: SamplerOptions,
postselection: bool,
program: FactoredInstructionProgram,
observable_records: Vec<Vec<Vec<i32>>>,
retained_observable_records: Vec<Vec<i32>>,
retained_output_records: Vec<Vec<i32>>,
info: SamplerInfo,
preprocessing_timing: SamplingTiming,
expression_plan: PresampledExpressionPlan,
workers: Vec<BatchWorker>,
}
impl Sampler {
pub(crate) fn new(circuit: &Circuit, options: SamplerOptions) -> Result<Self> {
let compile_start = Instant::now();
let program = plan_circuit(circuit, &options.postselection_mask)?;
let observable_includes = circuit.observable_count();
let observable_records =
logical_records_by_observable(&circuit.observables, observable_includes);
let input = SamplingInput {
program,
logical_records: observable_records
.get(options.observable)
.cloned()
.unwrap_or_default(),
observable_records,
observable: options.observable,
observable_includes,
preprocessing_timing: SamplingTiming {
compile_s: seconds_since(compile_start),
..Default::default()
},
};
Self::from_input(input, options)
}
pub(crate) fn from_input(input: SamplingInput, mut options: SamplerOptions) -> Result<Self> {
if options.normalize_syndromes
&& (!options.expected_detectors.is_empty() || !options.expected_observables.is_empty())
{
return Err(TicitError::new(
"normalize_syndromes cannot be combined with expected_detectors or expected_observables",
));
}
if !options.expected_detectors.is_empty()
&& options.expected_detectors.len() != input.program.ndetectors
{
return Err(TicitError::new(format!(
"expected_detectors has length {}, expected {}",
options.expected_detectors.len(),
input.program.ndetectors,
)));
}
if !options.expected_observables.is_empty()
&& options.expected_observables.len() != input.observable_includes
{
return Err(TicitError::new(format!(
"expected_observables has length {}, expected {}",
options.expected_observables.len(),
input.observable_includes,
)));
}
let forced_branches = plan_pinned_measurements(&input.program, &options.pin_measurements)?;
if options.normalize_syndromes {
let reference = reference_sample_for_program(
&input.program,
&input.observable_records,
&forced_branches,
)?;
options.expected_detectors = reference.detectors;
options.expected_observables = reference.observables;
}
options.batch_size = if options.batch_size > 0 {
options.batch_size
} else {
default_batch_count(input.program.max_k)?
};
options.sample_chunk_shots =
sample_chunk_or_default(options.sample_chunk_shots, options.batch_size);
options.threads = options.threads.max(1);
let postselection = has_postselection(&input.program);
let info = make_info(
&input.program,
input.observable,
input.observable_includes,
&options,
options.batch_size,
postselection,
);
let mut expression_plan = PresampledExpressionPlan::default();
prepare_expression_plan(&mut expression_plan, &input.program)?;
let retained_observable_records = input
.observable_records
.iter()
.flatten()
.cloned()
.collect::<Vec<_>>();
let retained_output_records = vec![(1..=input.program.nrecords as i32).collect()];
let mut workers = Vec::with_capacity(options.threads);
for _ in 0..options.threads {
let mut runtime =
BatchFactoredExecutorState::new(&input.program, options.batch_size, 1)?;
runtime.dense_shot_major_active = true;
runtime.forced_branches = forced_branches.clone();
let mut samples = PackedPresampledExogenous::default();
prepare_presampled_exogenous_packed(&mut samples, &input.program)?;
let mut worker = BatchWorker {
counts: SampleCounts::default(),
timing: SamplingTiming::default(),
samples,
expression_block: PresampledExpressionBlock::default(),
observable_words: vec![0; input.observable_includes * runtime.batch_words],
measurements: Vec::new(),
detectors: Vec::new(),
observables: Vec::new(),
observable_ones: vec![0; input.observable_includes],
exp_vals: Vec::new(),
postselection_scratch: BatchDetectorPostselectionScratch::default(),
runtime,
};
if postselection {
let postselection_options = BatchDetectorPostselectionOptions {
mask_dead_shots_min_fraction_denominator: POSTSELECTION_COMPACTION_DENOMINATOR,
retained_record_uses: Some(&retained_output_records),
expected_detectors: &options.expected_detectors,
};
prepare_batch_detector_postselection_scratch_for_program(
&mut worker.postselection_scratch,
&worker.runtime,
&input.program,
&postselection_options,
)?;
}
workers.push(worker);
}
Ok(Self {
options,
postselection,
observable_records: input.observable_records,
retained_observable_records,
retained_output_records,
info,
preprocessing_timing: input.preprocessing_timing,
expression_plan,
workers,
program: input.program,
})
}
#[must_use]
pub fn info(&self) -> &SamplerInfo {
&self.info
}
#[must_use]
pub fn preprocessing_timing(&self) -> &SamplingTiming {
&self.preprocessing_timing
}
pub fn sample(&mut self, shots: u64, bit_packed: bool) -> Result<SampleResult> {
self.sample_impl(shots, random_seed(), true, bit_packed)
}
pub fn sample_with_seed(
&mut self,
shots: u64,
seed: u64,
bit_packed: bool,
) -> Result<SampleResult> {
self.sample_impl(shots, seed, true, bit_packed)
}
pub fn sample_counts(&mut self, shots: u64) -> Result<SampleResult> {
self.sample_impl(shots, random_seed(), false, false)
}
pub fn sample_counts_with_seed(&mut self, shots: u64, seed: u64) -> Result<SampleResult> {
self.sample_impl(shots, seed, false, false)
}
fn sample_impl(
&mut self,
shots: u64,
seed: u64,
keep_records: bool,
bit_packed: bool,
) -> Result<SampleResult> {
if keep_records {
checked_output_len(
shots,
output_columns(self.info.measurement_records, bit_packed),
)?;
checked_output_len(shots, output_columns(self.info.detectors, bit_packed))?;
checked_output_len(shots, output_columns(self.info.observables, bit_packed))?;
checked_output_len(shots, self.info.expectation_values)?;
}
let chunk_shots = self.options.sample_chunk_shots as u64;
let batch_size = self.options.batch_size;
let nchunks = ceil_div_u64(shots, chunk_shots)?;
let blocks_per_chunk = ceil_div_u64(chunk_shots, batch_size as u64)?;
let active_threads = active_worker_count(self.options.threads, nchunks);
let mut result = SampleResult {
active_threads,
bit_packed,
observable_ones: vec![0; self.info.observables],
..Default::default()
};
let postselection_options = BatchDetectorPostselectionOptions {
mask_dead_shots_min_fraction_denominator: POSTSELECTION_COMPACTION_DENOMINATOR,
retained_record_uses: Some(if keep_records {
&self.retained_output_records
} else {
&self.retained_observable_records
}),
expected_detectors: &self.options.expected_detectors,
};
let sample_start = Instant::now();
let postselection = self.postselection;
let program = &self.program;
let expression_plan = &self.expression_plan;
let observable_records = &self.observable_records;
let expected_detectors = &self.options.expected_detectors;
let expected_observables = &self.options.expected_observables;
let selected_observable = self.options.observable;
let workers = &mut self.workers[..active_threads];
let run_worker = |worker_id: usize, worker: &mut BatchWorker| -> Result<()> {
worker.counts = SampleCounts::default();
worker.timing = SamplingTiming::default();
worker.measurements.clear();
worker.detectors.clear();
worker.observables.clear();
worker.observable_ones.fill(0);
worker.exp_vals.clear();
worker.runtime.store_detector_records = keep_records;
let mut chunk_index = worker_id as u64;
while chunk_index < nchunks {
let chunk_offset = chunk_index * chunk_shots;
let chunk_shots_here = chunk_shots.min(shots - chunk_offset) as usize;
let presample_start = Instant::now();
resample_prepared_exogenous_packed_in_place(
&mut worker.samples,
program,
chunk_shots_here,
block_seed(EXOGENOUS_SEED_BASE, seed, chunk_index),
)?;
evaluate_presampled_expression_block(
&mut worker.expression_block,
expression_plan,
&worker.samples,
)?;
worker.timing.presample_s += seconds_since(presample_start);
let execute_start = Instant::now();
let mut chunk_local_offset = 0usize;
let mut local_block_index = 0u64;
while chunk_local_offset < chunk_shots_here {
let block = batch_size.min(chunk_shots_here - chunk_local_offset);
let block_index = chunk_index * blocks_per_chunk + local_block_index;
reset_batch_executor(&mut worker.runtime, program, block)?;
worker.runtime.rng_state = block_seed(BRANCH_SEED_BASE, seed, block_index);
if postselection {
let postselection_result = execute_batch_postselected_in_place(
&mut worker.runtime,
program,
expression_plan,
&worker.expression_block,
chunk_local_offset,
&mut worker.postselection_scratch,
&postselection_options,
)?;
worker.counts.discarded += postselection_result.discarded as u64;
} else {
execute_batch_in_place_expressions(
&mut worker.runtime,
program,
expression_plan,
&worker.expression_block,
chunk_local_offset,
)?;
}
append_block_outputs(
worker,
observable_records,
expected_detectors,
expected_observables,
selected_observable,
keep_records,
bit_packed,
)?;
worker.counts.shots += block as u64;
chunk_local_offset += batch_size;
local_block_index += 1;
}
worker.timing.execute_s += seconds_since(execute_start);
chunk_index += active_threads as u64;
}
Ok(())
};
if active_threads == 1 {
run_worker(0, &mut workers[0])?;
} else {
thread::scope(|scope| -> Result<()> {
let mut handles = Vec::with_capacity(active_threads);
for (worker_id, worker) in workers.iter_mut().enumerate() {
let run_worker = &run_worker;
handles.push(scope.spawn(move || run_worker(worker_id, worker)));
}
for handle in handles {
handle.join().map_err(|_| TicitError::WorkerPanic)??;
}
Ok(())
})?;
}
for worker in workers {
result.counts.shots += worker.counts.shots;
result.counts.discarded += worker.counts.discarded;
result.counts.accepted += worker.counts.accepted;
result.counts.logical_errors += worker.counts.logical_errors;
result.timing.presample_s += worker.timing.presample_s;
result.timing.execute_s += worker.timing.execute_s;
for (total, count) in result
.observable_ones
.iter_mut()
.zip(&worker.observable_ones)
{
*total += count;
}
result.measurements.append(&mut worker.measurements);
result.detectors.append(&mut worker.detectors);
result.observables.append(&mut worker.observables);
result.exp_vals.append(&mut worker.exp_vals);
}
if keep_records {
result.record_rows = usize::try_from(result.counts.accepted)
.map_err(|_| TicitError::new("sample row count exceeds usize"))?;
}
result.timing.sample_s = seconds_since(sample_start);
Ok(result)
}
}
fn batch_word_count(shots: usize) -> usize {
shots.div_ceil(64)
}
fn live_word_mask(shots: usize, word: usize) -> u64 {
let remaining = shots as i64 - ((word as i64) << 6);
if remaining <= 0 {
0
} else if remaining >= 64 {
u64::MAX
} else {
(1u64 << remaining) - 1
}
}
fn fill_observable_words(
out: &mut Vec<u64>,
runtime: &BatchFactoredExecutorState,
observable_records: &[Vec<Vec<i32>>],
) -> Result<()> {
let stride_words = runtime.batch_words;
let nwords = batch_word_count(runtime.active_shots);
out.resize(observable_records.len() * stride_words, 0);
out.fill(0);
for (observable, includes) in observable_records.iter().enumerate() {
let out_base = observable * stride_words;
for records in includes {
for &record in records {
if record <= 0 || record as usize > runtime.nrecords {
return Err(TicitError::new(
"observable references an out-of-range measurement record",
));
}
let record_base = (record - 1) as usize * stride_words;
for word in 0..nwords {
out[out_base + word] ^= runtime.measurement_words[record_base + word];
}
}
}
}
Ok(())
}
fn append_bit_rows(
out: &mut Vec<u8>,
columns: &[u64],
expected: &[u8],
column_count: usize,
stride_words: usize,
rows: usize,
bit_packed: bool,
) {
let output_columns = output_columns(column_count, bit_packed);
out.reserve(rows * output_columns);
for shot in 0..rows {
let word = shot >> 6;
let mask = 1u64 << (shot & 63);
if bit_packed {
let row_start = out.len();
out.resize(row_start + output_columns, 0);
for column in 0..column_count {
let bit = (columns[column * stride_words + word] & mask != 0)
^ expected.get(column).is_some_and(|&value| value != 0);
if bit {
out[row_start + column / 8] |= 1 << (column % 8);
}
}
} else {
for column in 0..column_count {
let bit = (columns[column * stride_words + word] & mask != 0)
^ expected.get(column).is_some_and(|&value| value != 0);
out.push(u8::from(bit));
}
}
}
}
fn append_block_outputs(
worker: &mut BatchWorker,
observable_records: &[Vec<Vec<i32>>],
expected_detectors: &[u8],
expected_observables: &[u8],
selected_observable: usize,
keep_records: bool,
bit_packed: bool,
) -> Result<()> {
let BatchWorker {
counts,
runtime,
observable_words,
measurements,
detectors,
observables,
observable_ones,
exp_vals,
..
} = worker;
let rows = runtime.active_shots;
fill_observable_words(observable_words, runtime, observable_records)?;
for (observable, &expected) in expected_observables.iter().enumerate() {
if expected == 0 {
continue;
}
let base = observable * runtime.batch_words;
for word in 0..batch_word_count(rows) {
observable_words[base + word] ^= live_word_mask(rows, word);
}
}
counts.accepted += rows as u64;
let nwords = batch_word_count(rows);
for (observable, total) in observable_ones
.iter_mut()
.enumerate()
.take(observable_records.len())
{
let base = observable * runtime.batch_words;
let ones = (0..nwords)
.map(|word| {
(observable_words[base + word] & live_word_mask(rows, word)).count_ones() as u64
})
.sum::<u64>();
*total += ones;
if observable == selected_observable {
counts.logical_errors += ones;
}
}
if keep_records {
append_bit_rows(
measurements,
&runtime.measurement_words,
&[],
runtime.nrecords,
runtime.batch_words,
rows,
bit_packed,
);
append_bit_rows(
detectors,
&runtime.detector_words,
expected_detectors,
runtime.ndetectors,
runtime.batch_words,
rows,
bit_packed,
);
append_bit_rows(
observables,
observable_words,
&[],
observable_records.len(),
runtime.batch_words,
rows,
bit_packed,
);
exp_vals.reserve(rows * runtime.nexpvals);
for shot in 0..rows {
for exp_val in 0..runtime.nexpvals {
exp_vals.push(runtime.exp_values[exp_val * runtime.batches + shot]);
}
}
}
Ok(())
}
#[cfg(test)]
pub(crate) fn estimate_logical_error_rate(
circuit: &Circuit,
shots: u64,
seed: u64,
) -> Result<SampleCounts> {
let options = SamplerOptions {
postselection_mask: vec![1; circuit.detector_count()],
..Default::default()
};
Ok(Sampler::new(circuit, options)?
.sample_with_seed(shots, seed, false)?
.counts)
}
#[cfg(test)]
pub(crate) fn discard_rate(counts: &SampleCounts) -> f64 {
counts.discard_rate()
}
#[cfg(test)]
pub(crate) fn logical_error_rate(counts: &SampleCounts) -> f64 {
counts.logical_error_rate()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::circuit::parse_ticit_text;
use crate::test_support::ccz_nontels_circuits;
fn sampler_input(text: &str, options: &SamplerOptions) -> SamplingInput {
let parsed = parse_ticit_text(text).expect("test circuit parses");
let program = crate::circuit::plan_circuit(&parsed, &options.postselection_mask)
.expect("test circuit plans");
let logical_records =
logical_records_for_observable(&parsed.observables, options.observable);
make_circuit_sampling_input(
program,
logical_records,
options.observable,
parsed.observables.len(),
Default::default(),
)
}
#[test]
#[ignore = "slow end-to-end CCZ fixture check"]
fn direct_t_fixture_has_deterministic_detectors_without_noise() {
let path = ccz_nontels_circuits().join("d05_p1e-3.clifft");
let mut text = std::fs::read_to_string(path).expect("reads CCZ fixture");
assert_eq!(text.matches("E(0.125)").count(), 8);
for (noise, zero) in [
("E(0.125)", "E(0)"),
("DEPOLARIZE1(0.001)", "DEPOLARIZE1(0)"),
("DEPOLARIZE2(0.001)", "DEPOLARIZE2(0)"),
("X_ERROR(0.001)", "X_ERROR(0)"),
("Z_ERROR(0.001)", "Z_ERROR(0)"),
("M(0.001)", "M(0)"),
("MX(0.001)", "MX(0)"),
("MY(0.001)", "MY(0)"),
] {
text = text.replace(noise, zero);
}
let circuit = parse_ticit_text(&text).expect("CCZ fixture parses");
let options = SamplerOptions {
postselection_mask: vec![1; circuit.detector_count()],
normalize_syndromes: true,
..Default::default()
};
let counts = Sampler::new(&circuit, options)
.expect("CCZ fixture compiles")
.sample_counts_with_seed(64, 7)
.expect("CCZ fixture samples")
.counts;
assert_eq!(counts.discarded, 0);
}
#[test]
fn estimate_matches_the_cli_conventions() {
let parsed = parse_ticit_text("M !0\nOBSERVABLE_INCLUDE(0) rec[-1]\n").expect("parses");
let summary = estimate_logical_error_rate(&parsed, 5, 1).expect("estimates");
assert_eq!(summary.shots, 5);
assert_eq!(summary.discarded, 0);
assert_eq!(summary.accepted, 5);
assert_eq!(summary.logical_errors, 5);
assert!((logical_error_rate(&summary) - 1.0).abs() < 1e-15);
assert!((discard_rate(&summary) - 0.0).abs() < 1e-15);
let parsed = parse_ticit_text("M !0\nDETECTOR rec[-1]\nOBSERVABLE_INCLUDE(0) rec[-1]\n")
.expect("parses");
let summary = estimate_logical_error_rate(&parsed, 5, 1).expect("estimates");
assert_eq!(summary.discarded, 5);
assert_eq!(summary.accepted, 0);
assert!(logical_error_rate(&summary).is_nan());
let parsed = parse_ticit_text(
"M !0\nOBSERVABLE_INCLUDE(0) rec[-1]\nOBSERVABLE_INCLUDE(1) rec[-1]\n",
)
.expect("parses");
let summary = estimate_logical_error_rate(&parsed, 5, 1).expect("estimates");
assert_eq!(summary.logical_errors, 5, "classifies by observable 0 only");
}
#[test]
fn batch_sampler_defaults_follow_the_cpp_sizing() {
let options = SamplerOptions::default();
let input = sampler_input("H 0\nT 0\nM 0\nOBSERVABLE_INCLUDE(0) rec[-1]\n", &options);
let sampler = Sampler::from_input(input, options).expect("sampler builds");
let info = sampler.info();
assert_eq!(info.batch_size, 2048);
assert_eq!(info.sample_chunk_shots, 2048);
assert_eq!(info.threads, 1);
assert!(!info.detector_postselection);
}
#[test]
fn logical_counts_flag_a_deterministic_flip() {
let options = SamplerOptions::default();
let input = sampler_input("M !0\nOBSERVABLE_INCLUDE(0) rec[-1]\n", &options);
let mut sampler = Sampler::from_input(input, options).expect("sampler builds");
let result = sampler.sample(100, false).expect("sampling succeeds");
assert_eq!(result.counts.shots, 100);
assert_eq!(result.counts.discarded, 0);
assert_eq!(result.counts.accepted, 100);
assert_eq!(result.counts.logical_errors, 100);
}
#[test]
fn noiseless_reference_normalizes_outputs_before_counting() {
let circuit = Circuit::from_text(
"X 0\nM 0\nDETECTOR rec[-1]\n\
OBSERVABLE_INCLUDE(0)\nOBSERVABLE_INCLUDE(1) rec[-1]\n",
)
.expect("circuit parses");
let reference = circuit.reference_sample().expect("reference samples");
assert_eq!(reference.detectors, [1]);
assert_eq!(reference.observables, [0, 1]);
let mut sampler = circuit
.compile(SamplerOptions {
observable: 1,
postselection_mask: vec![1],
normalize_syndromes: true,
..Default::default()
})
.expect("circuit compiles");
let result = sampler
.sample_with_seed(4, 7, false)
.expect("sampling succeeds");
assert_eq!(
result.counts,
SampleCounts {
shots: 4,
discarded: 0,
accepted: 4,
logical_errors: 0,
}
);
assert_eq!(result.detectors, [0; 4]);
assert_eq!(result.observables, [0; 8]);
assert_eq!(result.observable_ones, [0, 0]);
}
#[test]
fn packed_rows_match_numpy_little_bit_order() {
let columns = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1];
let mut packed = Vec::new();
append_bit_rows(&mut packed, &columns, &[], columns.len(), 1, 1, true);
assert_eq!(packed, [0xff, 0x04]);
}
#[test]
fn sample_returns_row_major_clifft_shaped_records() {
let circuit = Circuit::from_text(
"M !0\nM 1\nDETECTOR rec[-2] rec[-1]\n\
OBSERVABLE_INCLUDE(0) rec[-2]\nOBSERVABLE_INCLUDE(2) rec[-1]\n\
EXP_VAL Z0\n",
)
.expect("circuit parses");
let mut sampler = circuit
.compile(SamplerOptions::default())
.expect("circuit compiles");
let result = sampler
.sample_with_seed(3, 7, false)
.expect("sampling succeeds");
assert_eq!(result.measurements, vec![1, 0, 1, 0, 1, 0]);
assert_eq!(result.detectors, vec![1, 1, 1]);
assert_eq!(result.observables, vec![1, 0, 0, 1, 0, 0, 1, 0, 0]);
assert_eq!(result.observable_ones, vec![3, 0, 0]);
assert_eq!(result.exp_vals, vec![1.0; 3]);
let packed = sampler
.sample_with_seed(3, 7, true)
.expect("packed sampling succeeds");
assert!(packed.bit_packed);
assert_eq!(packed.measurements, vec![1; 3]);
assert_eq!(packed.detectors, vec![1; 3]);
assert_eq!(packed.observables, vec![1; 3]);
assert_eq!(packed.observable_ones, result.observable_ones);
assert_eq!(packed.exp_vals, result.exp_vals);
let counts_only = sampler
.sample_counts_with_seed(3, 7)
.expect("count sampling succeeds");
assert_eq!(counts_only.counts, result.counts);
assert_eq!(counts_only.observable_ones, result.observable_ones);
assert_eq!(counts_only.record_rows, 0);
assert!(counts_only.measurements.is_empty());
}
#[test]
fn postselection_returns_only_survivor_rows() {
let circuit = Circuit::from_text(
"EXP_VAL Z1\nM !1\nDETECTOR rec[-1]\n\
X_ERROR(0.5) 0\nM 0\nDETECTOR rec[-1]\n\
OBSERVABLE_INCLUDE(0) rec[-2]\n",
)
.expect("circuit parses");
let mut sampler = circuit
.compile(SamplerOptions {
postselection_mask: vec![0, 1],
batch_size: 16,
..Default::default()
})
.expect("circuit compiles");
let result = sampler
.sample_with_seed(128, 9, false)
.expect("sampling succeeds");
let rows = result.counts.accepted as usize;
assert!(rows > 0);
assert!(rows < 128);
assert!(result.measurements.chunks_exact(2).all(|row| row == [1, 0]));
assert!(result.detectors.chunks_exact(2).all(|row| row == [1, 0]));
assert_eq!(result.observables, vec![1; rows]);
assert_eq!(result.observable_ones, vec![rows as u64]);
assert_eq!(result.exp_vals, vec![1.0; rows]);
}
#[test]
fn source_discards_and_the_passed_mask_are_unioned() {
let circuit = crate::Circuit::from_text("M !0\nDETECTOR rec[-1]\nM 0\nDISCARD rec[-1]\n")
.expect("circuit parses");
assert_eq!(circuit.detector_count(), 2);
let mut source_only = Sampler::new(&circuit, SamplerOptions::default()).expect("prepares");
let counts = source_only
.sample_with_seed(8, 1, false)
.expect("samples")
.counts;
assert_eq!((counts.accepted, counts.discarded), (8, 0));
let mut union = Sampler::new(
&circuit,
SamplerOptions {
postselection_mask: vec![1],
..Default::default()
},
)
.expect("prepares with a short mask");
let counts = union.sample_with_seed(8, 1, false).expect("samples").counts;
assert_eq!((counts.accepted, counts.discarded), (0, 8));
let source_fires = crate::Circuit::from_text("M !0\nDISCARD rec[-1]\n").expect("parses");
let mut sampler = Sampler::new(&source_fires, SamplerOptions::default()).expect("prepares");
let counts = sampler
.sample_with_seed(8, 1, false)
.expect("samples")
.counts;
assert_eq!((counts.accepted, counts.discarded), (0, 8));
}
#[test]
fn identical_seeds_reproduce_counts() {
let text = "X_ERROR(0.125) 0\nM 0\nDETECTOR rec[-1]\nH 1\nT 1\nM 1\nOBSERVABLE_INCLUDE(0) rec[-1]\n";
let options = SamplerOptions {
postselection_mask: vec![1],
batch_size: 32,
..Default::default()
};
let input = sampler_input(text, &options);
let mut sampler = Sampler::from_input(input, options).expect("sampler builds");
let first = sampler
.sample_with_seed(4096, 12, false)
.expect("sampling succeeds");
let second = sampler
.sample_with_seed(4096, 12, false)
.expect("sampling succeeds");
assert_eq!(first.counts, second.counts);
assert_eq!(first.measurements, second.measurements);
assert_eq!(first.detectors, second.detectors);
assert_eq!(first.observables, second.observables);
assert!(first.counts.discarded > 0, "the X error should fire");
assert_eq!(
first.counts.accepted + first.counts.discarded,
first.counts.shots
);
let other = sampler
.sample_with_seed(4096, 13, false)
.expect("sampling succeeds");
assert_ne!(first.counts, other.counts);
}
#[test]
fn batch_threads_preserve_chunk_seeded_counts() {
let text = "X_ERROR(0.125) 0\nM 0\nDETECTOR rec[-1]\nH 1\nT 1\nM 1\nOBSERVABLE_INCLUDE(0) rec[-1]\n";
let single_options = SamplerOptions {
postselection_mask: vec![1],
sample_chunk_shots: 96,
batch_size: 32,
threads: 1,
..Default::default()
};
let threaded_options = SamplerOptions {
threads: 3,
..single_options.clone()
};
let mut single = Sampler::from_input(sampler_input(text, &single_options), single_options)
.expect("single-worker sampler builds");
let mut threaded =
Sampler::from_input(sampler_input(text, &threaded_options), threaded_options)
.expect("threaded sampler builds");
let single_result = single
.sample_with_seed(385, 12, false)
.expect("single worker samples");
let threaded_result = threaded
.sample_with_seed(385, 12, false)
.expect("three workers sample");
assert_eq!(single_result.active_threads, 1);
assert_eq!(threaded_result.active_threads, 3);
assert_eq!(threaded_result.counts, single_result.counts);
assert_eq!(threaded_result.counts.shots, 385);
let capped = threaded
.sample_with_seed(95, 13, false)
.expect("one chunk uses one worker");
assert_eq!(capped.active_threads, 1);
}
fn row_parity(measurements: &[u8], columns: usize, row: usize, records: &[usize]) -> u8 {
records.iter().fold(0u8, |acc, &record| {
acc ^ measurements[row * columns + record]
})
}
#[test]
fn pinning_holds_the_parity_in_every_shot() {
let circuit = Circuit::from_text("H 0\nH 1\nM 0\nM 1\n").expect("circuit parses");
for value in [false, true] {
let mut sampler = circuit
.compile(SamplerOptions {
pin_measurements: vec![MeasurementParity::new([0, 1], value)],
..Default::default()
})
.expect("circuit compiles");
let result = sampler.sample_with_seed(512, 9, false).expect("samples");
assert_eq!(result.record_rows, 512);
let mut individually_random = false;
for row in 0..result.record_rows {
assert_eq!(
row_parity(&result.measurements, 2, row, &[0, 1]),
u8::from(value)
);
individually_random |= result.measurements[row * 2] == 1;
}
assert!(
individually_random,
"only the parity is pinned; each record stays random"
);
}
}
#[test]
fn pinning_leaves_noise_on_the_records() {
let text = "H 0\nM 0\nX_ERROR(0.25) 0\nM 0\n";
let circuit = Circuit::from_text(text).expect("circuit parses");
let mut sampler = circuit
.compile(SamplerOptions {
pin_measurements: vec![MeasurementParity::new([0], true)],
..Default::default()
})
.expect("circuit compiles");
let result = sampler.sample_with_seed(4096, 11, false).expect("samples");
let mut flipped = 0usize;
for row in 0..result.record_rows {
assert_eq!(
result.measurements[row * 2],
1,
"the pinned record is fixed"
);
flipped += usize::from(result.measurements[row * 2 + 1] != 1);
}
let rate = flipped as f64 / result.record_rows as f64;
assert!(
(rate - 0.25).abs() < 0.03,
"noise flipped the repeat at {rate}, expected about 0.25"
);
}
#[test]
fn a_deterministic_parity_only_accepts_its_own_value() {
let circuit = Circuit::from_text("H 0\nM 0\nX_ERROR(0.25) 0\nM 0\n").expect("parses");
let pinned = |value| SamplerOptions {
pin_measurements: vec![MeasurementParity::new([0, 1], value)],
..Default::default()
};
circuit
.compile(pinned(false))
.expect("the noiseless repeat agrees");
let error = circuit
.compile(pinned(true))
.err()
.expect("the noiseless repeat cannot disagree");
assert!(error.to_string().contains("deterministic"));
}
#[test]
fn pinning_a_biased_branch_is_refused() {
let circuit = Circuit::from_text("H 0\nT 0\nH 0\nM 0\n").expect("circuit parses");
let mut sampler = circuit
.compile(SamplerOptions {
pin_measurements: vec![MeasurementParity::new([0], true)],
..Default::default()
})
.expect("circuit compiles");
let error = sampler
.sample_with_seed(8, 3, false)
.expect_err("the branch is biased");
assert!(error.to_string().contains("not one half"));
}
#[test]
fn pinning_both_values_reproduces_the_unpinned_distribution() {
let text = "H 0\nCX 0 1\nM 0\nX_ERROR(0.25) 1\nM 1\nOBSERVABLE_INCLUDE(0) rec[-1]\n";
let circuit = Circuit::from_text(text).expect("circuit parses");
let shots = 200_000;
let mut unpinned = circuit
.compile(SamplerOptions::default())
.expect("circuit compiles");
let free = unpinned
.sample_counts_with_seed(2 * shots, 5)
.expect("samples");
let free_rate = free.counts.logical_error_rate();
let mut pinned_ones = 0u64;
for value in [false, true] {
let mut sampler = circuit
.compile(SamplerOptions {
pin_measurements: vec![MeasurementParity::new([0], value)],
..Default::default()
})
.expect("circuit compiles");
pinned_ones += sampler
.sample_counts_with_seed(shots, 5)
.expect("samples")
.counts
.logical_errors;
}
let pinned_rate = pinned_ones as f64 / (2 * shots) as f64;
assert!(
(pinned_rate - free_rate).abs() < 0.005,
"pinned {pinned_rate} vs free {free_rate}"
);
}
}