From 6c070c84ecd627e64d77fc9c20f346ded93de752 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Wed, 5 Aug 2026 01:38:04 -0600 Subject: [PATCH] Consolidate duplicate distance searches and parallelize the search engine --- crates/benchmarks/benches/benchmarks.rs | 6 +- .../benches/modules/code_distance.rs | 89 ++++ crates/pecos-qec/src/distance.rs | 491 ++++++++++++++++-- .../stabilizer_flip_checker.rs | 37 +- crates/pecos-qec/src/lib.rs | 2 +- 5 files changed, 576 insertions(+), 49 deletions(-) create mode 100644 crates/benchmarks/benches/modules/code_distance.rs diff --git a/crates/benchmarks/benches/benchmarks.rs b/crates/benchmarks/benches/benchmarks.rs index d496fa89b..d20390bd2 100644 --- a/crates/benchmarks/benches/benchmarks.rs +++ b/crates/benchmarks/benches/benchmarks.rs @@ -17,6 +17,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; mod modules { pub mod allocation_overhead; + pub mod code_distance; pub mod cpu_stabilizer_comparison; pub mod dem_builder; pub mod dem_sampler; @@ -58,14 +59,15 @@ use modules::sparse_stab_vs_cpp; #[cfg(feature = "stab-tn")] use modules::stab_mps_vs_stab_vec; use modules::{ - allocation_overhead, cpu_stabilizer_comparison, dem_builder, dem_sampler, dod_statevec, - fault_catalog, measurement_sampling, native_statevec_comparison, noise_models, + allocation_overhead, code_distance, cpu_stabilizer_comparison, dem_builder, dem_sampler, + dod_statevec, fault_catalog, measurement_sampling, native_statevec_comparison, noise_models, pecos_neo_comparison, quizx_eval, rng, set_ops, sparse_stab_w_vs_y, sparse_state_vec, stab_vec, stabilizer_sims, state_vec_sims, surface_code, tick_circuit_layout, trig, }; fn all_benchmarks(c: &mut Criterion) { allocation_overhead::benchmarks(c); + code_distance::benchmarks(c); stab_vec::benchmarks(c); cpu_stabilizer_comparison::benchmarks(c); quizx_eval::benchmarks(c); diff --git a/crates/benchmarks/benches/modules/code_distance.rs b/crates/benchmarks/benches/modules/code_distance.rs new file mode 100644 index 000000000..87a9a1117 --- /dev/null +++ b/crates/benchmarks/benches/modules/code_distance.rs @@ -0,0 +1,89 @@ +// Copyright 2026 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +//! Exhaustive code-distance search benchmarks. + +use criterion::{Criterion, measurement::Measurement}; +use pecos_core::{Xs, Zs}; +use pecos_qec::{ + DistanceSearchConfig, StabilizerCode, StabilizerCodeSpec, calculate_distance, + find_shortest_logicals, +}; +use std::hint::black_box; + +pub fn benchmarks(c: &mut Criterion) { + eprintln!( + "code-distance benchmark available parallelism: {}", + std::thread::available_parallelism().map_or(1, std::num::NonZero::get) + ); + + let five_qubit = standard_code_spec(&StabilizerCode::five_qubit()); + let steane = standard_code_spec(&StabilizerCode::steane()); + let toric_3 = standard_code_spec(&StabilizerCode::toric(3)); + let color_17 = color_code_17(); + let config = DistanceSearchConfig::default(); + + let mut group = c.benchmark_group("code_distance/calculate_distance"); + group.sample_size(10); + group.bench_function("five_qubit_5_1_3", |b| { + b.iter(|| calculate_distance(black_box(&five_qubit), black_box(&config))); + }); + group.bench_function("steane_7_1_3", |b| { + b.iter(|| calculate_distance(black_box(&steane), black_box(&config))); + }); + group.bench_function("toric_3_18_2_3", |b| { + b.iter(|| calculate_distance(black_box(&toric_3), black_box(&config))); + }); + group.bench_function("color_17_1_5", |b| { + b.iter(|| calculate_distance(black_box(&color_17), black_box(&config))); + }); + group.finish(); + + let mut group = c.benchmark_group("code_distance/find_shortest_logicals"); + group.sample_size(10); + group.bench_function("color_17_1_5_delta_1", |b| { + b.iter(|| find_shortest_logicals(black_box(&color_17), black_box(&config), 1)); + }); + group.finish(); +} + +fn standard_code_spec(code: &StabilizerCode) -> StabilizerCodeSpec { + StabilizerCodeSpec::from_stabilizer_code(code) + .expect("standard stabilizer code should have discoverable logicals") +} + +fn color_code_17() -> StabilizerCodeSpec { + const SUPPORTS: [&[usize]; 8] = [ + &[0, 9, 12, 15], + &[1, 9, 12, 16], + &[2, 11, 13, 14], + &[3, 8, 9, 12], + &[4, 8, 10, 12, 13, 14, 15, 16], + &[5, 10, 11, 13], + &[6, 8, 9, 10, 13, 14, 15, 16], + &[7, 10, 11, 14], + ]; + + let mut builder = StabilizerCodeSpec::builder(17); + for support in SUPPORTS { + builder = builder.check(Xs(support)); + } + for support in SUPPORTS { + builder = builder.check(Zs(support)); + } + + builder + .logical_x(Xs(0..17)) + .logical_z(Zs(0..17)) + .build() + .expect("[[17,1,5]] color code should be valid") +} diff --git a/crates/pecos-qec/src/distance.rs b/crates/pecos-qec/src/distance.rs index a954d69a2..1ca33124c 100644 --- a/crates/pecos-qec/src/distance.rs +++ b/crates/pecos-qec/src/distance.rs @@ -16,7 +16,25 @@ //! by exhaustively searching for minimum weight logical operators. use crate::StabilizerCodeSpec; +use crate::stabilizer_code_spec::CodeIndices; use pecos_core::{Pauli, PauliString, QubitId}; +use rayon::prelude::*; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Minimum candidate count at a single weight before that weight is searched in parallel. +/// +/// Rayon's fixed per-weight overhead is worth paying only once a weight carries enough +/// candidates to amortize it, so the decision is made per weight rather than per code: a +/// large code's low weights are still cheap and stay serial. +/// +/// The value sits inside a measured window (see `benches/modules/code_distance.rs`). +/// Below roughly 22k candidates parallelism loses: forcing the toric [[18, 2, 3]] weight-3 +/// tier (22,032 candidates) parallel made that search 4.6x slower than serial. Above +/// roughly 193k it stops engaging where it matters: the color [[17, 1, 5]] search spends +/// most of its time in the weight-4 tier (192,780 candidates), and a threshold past that +/// erased its speedup entirely. 65,536 is near the geometric centre of that window, so +/// both bounds keep margin. +const PARALLEL_CANDIDATE_THRESHOLD: usize = 65_536; /// Result of a distance calculation, including the minimum weight logical operator found. #[derive(Clone, Debug)] @@ -111,6 +129,16 @@ pub struct WeightedPauliIterator { css_pauli_type: usize, } +/// Position in the serial candidate enumeration. +/// +/// General searches order by support and then Pauli assignment. CSS searches order +/// all X supports before all Z supports, so the fields hold Pauli type and support. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct CandidateIndex { + outer: usize, + inner: usize, +} + impl WeightedPauliIterator { /// Create a new iterator for Pauli operators of the given weight. /// @@ -269,27 +297,41 @@ pub fn calculate_distance( let max_weight = config.max_weight.unwrap_or(code.num_qubits()); // Build indices once for O(weight) lookups instead of O(num_stabilizers * weight) - let stab_index = code.build_stabilizer_index(); - let log_index = code.build_logical_index(); + let indices = code.build_indices(); for weight in 1..=max_weight { if config.verbose { eprintln!("Checking weight {weight}..."); } - for pauli in WeightedPauliIterator::new(code.num_qubits(), weight, config.css_only) { - if code.is_logical_error_indexed(&pauli, &stab_index, &log_index) { - return Some(DistanceResult { - distance: weight, - min_weight_operator: pauli, - }); - } + if let Some(pauli) = first_logical_error_at_weight(code, weight, config, &indices) { + return Some(DistanceResult { + distance: weight, + min_weight_operator: pauli, + }); } } None } +/// Check whether a code has a logical error at exactly one physical weight. +/// +/// Stabilizer and logical column indices are built once for the complete scan. +#[must_use] +pub fn has_logical_error_at_weight( + code: &StabilizerCodeSpec, + weight: usize, + config: &DistanceSearchConfig, +) -> bool { + if config.verbose { + eprintln!("Checking weight {weight}..."); + } + + let indices = code.build_indices(); + first_logical_error_at_weight(code, weight, config, &indices).is_some() +} + /// Find all minimum weight logical operators. /// /// Unlike `calculate_distance`, this returns all logical operators of the minimum weight, @@ -364,8 +406,7 @@ pub fn find_shortest_logicals( let mut found_distance: Option = None; // Build indices once for O(weight) lookups instead of O(num_stabilizers * weight) - let stab_index = code.build_stabilizer_index(); - let log_index = code.build_logical_index(); + let indices = code.build_indices(); for weight in 1..=max_weight { // If we've searched through the requested range above the minimum, stop. @@ -379,31 +420,331 @@ pub fn find_shortest_logicals( eprintln!("Checking weight {weight}..."); } - for pauli in WeightedPauliIterator::new(code.num_qubits(), weight, config.css_only) { - if code.is_logical_error_indexed(&pauli, &stab_index, &log_index) { - if found_distance.is_none() { - found_distance = Some(weight); - } + let weight_matches = logical_errors_at_weight(code, weight, config, &indices); + if !weight_matches.is_empty() && found_distance.is_none() { + found_distance = Some(weight); + } - // Determine which logical operators this is equivalent to - let equivalent_logicals = classify_logical_equivalence_indexed( - &log_index, - code.num_logical_qubits(), - &pauli, - ); - - results.push(LogicalOperatorInfo { - operator: pauli, - weight, - equivalent_logicals, - }); - } + for pauli in weight_matches { + // Determine which logical operators this is equivalent to + let equivalent_logicals = classify_logical_equivalence_indexed( + &indices.logical, + code.num_logical_qubits(), + &pauli, + ); + + results.push(LogicalOperatorInfo { + operator: pauli, + weight, + equivalent_logicals, + }); } } results } +fn first_logical_error_at_weight( + code: &StabilizerCodeSpec, + weight: usize, + config: &DistanceSearchConfig, + indices: &CodeIndices, +) -> Option { + if should_parallelize(code.num_qubits(), weight, config.css_only) { + first_logical_error_at_weight_parallel(code, weight, config, indices) + } else { + first_logical_error_at_weight_serial(code, weight, config, indices) + } +} + +fn first_logical_error_at_weight_serial( + code: &StabilizerCodeSpec, + weight: usize, + config: &DistanceSearchConfig, + indices: &CodeIndices, +) -> Option { + WeightedPauliIterator::new(code.num_qubits(), weight, config.css_only) + .find(|pauli| code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical)) +} + +fn first_logical_error_at_weight_parallel( + code: &StabilizerCodeSpec, + weight: usize, + config: &DistanceSearchConfig, + indices: &CodeIndices, +) -> Option { + let best_support = AtomicUsize::new(usize::MAX); + + support_combinations_at_weight(code.num_qubits(), weight) + .filter_map(|(support_index, support)| { + if support_index > best_support.load(Ordering::Relaxed) { + return None; + } + + let candidate = first_matching_pauli_for_support( + code, + indices, + support_index, + &support.qubits(), + config.css_only, + &best_support, + ); + if candidate + .as_ref() + .is_some_and(|(index, _)| !config.css_only || index.outer == 0) + { + best_support.fetch_min(support_index, Ordering::Relaxed); + } + candidate + }) + .min_by_key(|(index, _)| *index) + .map(|(_, pauli)| pauli) +} + +fn logical_errors_at_weight( + code: &StabilizerCodeSpec, + weight: usize, + config: &DistanceSearchConfig, + indices: &CodeIndices, +) -> Vec { + if should_parallelize(code.num_qubits(), weight, config.css_only) { + logical_errors_at_weight_parallel(code, weight, config.css_only, indices) + } else { + logical_errors_at_weight_serial(code, weight, config.css_only, indices) + } +} + +fn logical_errors_at_weight_serial( + code: &StabilizerCodeSpec, + weight: usize, + css_only: bool, + indices: &CodeIndices, +) -> Vec { + WeightedPauliIterator::new(code.num_qubits(), weight, css_only) + .filter(|pauli| code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical)) + .collect() +} + +fn logical_errors_at_weight_parallel( + code: &StabilizerCodeSpec, + weight: usize, + css_only: bool, + indices: &CodeIndices, +) -> Vec { + let mut matches: Vec<_> = matching_paulis_at_weight(code, weight, css_only, indices).collect(); + matches.sort_unstable_by_key(|(index, _)| *index); + matches.into_iter().map(|(_, pauli)| pauli).collect() +} + +fn should_parallelize(num_qubits: usize, weight: usize, css_only: bool) -> bool { + candidate_count_at_weight(num_qubits, weight, css_only) > PARALLEL_CANDIDATE_THRESHOLD +} + +fn candidate_count_at_weight(num_qubits: usize, weight: usize, css_only: bool) -> usize { + let support_count = saturating_binomial(num_qubits, weight); + let assignments_per_support = if css_only { + 2 + } else { + 3usize.saturating_pow(u32::try_from(weight).unwrap_or(u32::MAX)) + }; + + support_count.saturating_mul(assignments_per_support) +} + +fn saturating_binomial(n: usize, k: usize) -> usize { + if k > n { + return 0; + } + + let k = k.min(n - k); + let mut result = 1_u128; + for i in 1..=k { + result = result * (n - k + i) as u128 / i as u128; + if result > usize::MAX as u128 { + return usize::MAX; + } + } + + usize::try_from(result).unwrap_or(usize::MAX) +} + +fn matching_paulis_at_weight<'a>( + code: &'a StabilizerCodeSpec, + weight: usize, + css_only: bool, + indices: &'a CodeIndices, +) -> impl ParallelIterator + 'a { + support_combinations_at_weight(code.num_qubits(), weight).flat_map_iter( + move |(support_index, support)| { + matching_paulis_for_support(code, indices, support_index, &support.qubits(), css_only) + .into_iter() + }, + ) +} + +fn support_combinations_at_weight( + num_qubits: usize, + weight: usize, +) -> impl ParallelIterator { + // Bridge only support combinations into Rayon. Each task enumerates all Pauli + // assignments for its support serially, amortizing synchronization overhead. + WeightedPauliIterator::new(num_qubits, weight, true) + .take_while(|pauli| { + pauli + .paulis() + .first() + .is_some_and(|(pauli, _)| *pauli == Pauli::X) + }) + .enumerate() + .par_bridge() +} + +fn first_matching_pauli_for_support( + code: &StabilizerCodeSpec, + indices: &CodeIndices, + support_index: usize, + positions: &[usize], + css_only: bool, + best_support: &AtomicUsize, +) -> Option<(CandidateIndex, PauliString)> { + if css_only { + let x = PauliString::xs(positions); + if code.is_logical_error_indexed(&x, &indices.stabilizer, &indices.logical) { + return Some(( + CandidateIndex { + outer: 0, + inner: support_index, + }, + x, + )); + } + + // Any X match sorts before every Z match. Once another task has found an + // X candidate, a Z candidate cannot improve the reduction. + if best_support.load(Ordering::Relaxed) != usize::MAX { + return None; + } + + let z = PauliString::zs(positions); + return code + .is_logical_error_indexed(&z, &indices.stabilizer, &indices.logical) + .then_some(( + CandidateIndex { + outer: 1, + inner: support_index, + }, + z, + )); + } + + let mut paulis = vec![0; positions.len()]; + let mut assignment_index = 0; + + loop { + let pauli = pauli_string_for_assignment(positions, &paulis); + if code.is_logical_error_indexed(&pauli, &indices.stabilizer, &indices.logical) { + return Some(( + CandidateIndex { + outer: support_index, + inner: assignment_index, + }, + pauli, + )); + } + + if !increment_pauli_assignment(&mut paulis) { + return None; + } + assignment_index += 1; + } +} + +fn matching_paulis_for_support( + code: &StabilizerCodeSpec, + indices: &CodeIndices, + support_index: usize, + positions: &[usize], + css_only: bool, +) -> Vec<(CandidateIndex, PauliString)> { + if css_only { + let candidates = [ + ( + CandidateIndex { + outer: 0, + inner: support_index, + }, + PauliString::xs(positions), + ), + ( + CandidateIndex { + outer: 1, + inner: support_index, + }, + PauliString::zs(positions), + ), + ]; + + return candidates + .into_iter() + .filter(|(_, pauli)| { + code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical) + }) + .collect(); + } + + let mut matches = Vec::new(); + let mut paulis = vec![0; positions.len()]; + let mut assignment_index = 0; + + loop { + let pauli = pauli_string_for_assignment(positions, &paulis); + if code.is_logical_error_indexed(&pauli, &indices.stabilizer, &indices.logical) { + matches.push(( + CandidateIndex { + outer: support_index, + inner: assignment_index, + }, + pauli, + )); + } + + if !increment_pauli_assignment(&mut paulis) { + break; + } + assignment_index += 1; + } + + matches +} + +fn pauli_string_for_assignment(positions: &[usize], paulis: &[usize]) -> PauliString { + let paulis = positions + .iter() + .zip(paulis) + .map(|(&position, &pauli)| { + let pauli = match pauli { + 0 => Pauli::X, + 1 => Pauli::Y, + _ => Pauli::Z, + }; + (pauli, QubitId::new(position)) + }) + .collect(); + + PauliString::with_phase_and_paulis(pecos_core::QuarterPhase::PlusOne, paulis) +} + +fn increment_pauli_assignment(paulis: &mut [usize]) -> bool { + for index in (0..paulis.len()).rev() { + if paulis[index] < 2 { + paulis[index] += 1; + paulis[index + 1..].fill(0); + return true; + } + } + false +} + /// Classify which logical operators a given Pauli operator is equivalent to. /// /// Uses precomputed column indices for O(weight) performance instead of @@ -442,7 +783,7 @@ fn classify_logical_equivalence_indexed( #[cfg(test)] mod tests { use super::*; - use pecos_core::{Pauli, PauliOperator}; + use pecos_core::{Pauli, PauliOperator, Xs, Zs}; fn pauli_string(paulis: &[(Pauli, usize)]) -> PauliString { PauliString::with_phase_and_paulis( @@ -484,6 +825,33 @@ mod tests { .unwrap() } + fn color_code_17() -> StabilizerCodeSpec { + const SUPPORTS: [&[usize]; 8] = [ + &[0, 9, 12, 15], + &[1, 9, 12, 16], + &[2, 11, 13, 14], + &[3, 8, 9, 12], + &[4, 8, 10, 12, 13, 14, 15, 16], + &[5, 10, 11, 13], + &[6, 8, 9, 10, 13, 14, 15, 16], + &[7, 10, 11, 14], + ]; + + let mut builder = StabilizerCodeSpec::builder(17); + for support in SUPPORTS { + builder = builder.check(Xs(support)); + } + for support in SUPPORTS { + builder = builder.check(Zs(support)); + } + + builder + .logical_x(Xs(0..17)) + .logical_z(Zs(0..17)) + .build() + .expect("[[17,1,5]] color code should be valid") + } + #[test] fn test_weighted_pauli_iterator_weight_1() { let iter = WeightedPauliIterator::new(3, 1, false); @@ -624,6 +992,69 @@ mod tests { ); } + #[test] + fn test_serial_branch_preserves_candidate_order() { + let code = five_qubit_code(); + let config = DistanceSearchConfig::default(); + let indices = code.build_indices(); + assert!(!(1..=5).any(|weight| should_parallelize(5, weight, false))); + let expected: Vec<_> = (1..=5) + .flat_map(|weight| { + WeightedPauliIterator::new(code.num_qubits(), weight, config.css_only) + .filter(|pauli| { + code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical) + }) + .map(move |pauli| (weight, pauli)) + }) + .collect(); + + let distance = calculate_distance(&code, &config).unwrap(); + assert_eq!(distance.min_weight_operator, expected[0].1); + + let actual = find_shortest_logicals(&code, &config, 2); + assert!( + actual + .iter() + .map(|info| (info.weight, &info.operator)) + .eq(expected.iter().map(|(weight, pauli)| (*weight, pauli))) + ); + + let css_config = DistanceSearchConfig::css(); + let expected_css = (1..=code.num_qubits()) + .flat_map(|weight| { + WeightedPauliIterator::new(code.num_qubits(), weight, true) + .filter(|pauli| { + code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical) + }) + .map(move |pauli| (weight, pauli)) + }) + .next() + .unwrap(); + let actual_css = calculate_distance(&code, &css_config).unwrap(); + assert_eq!(actual_css.distance, expected_css.0); + assert_eq!(actual_css.min_weight_operator, expected_css.1); + } + + #[test] + fn test_parallel_branch_preserves_serial_candidate_order() { + let code = color_code_17(); + let config = DistanceSearchConfig::with_max_weight(5); + let indices = code.build_indices(); + assert!(should_parallelize(17, 5, false)); + + let expected: Vec<_> = WeightedPauliIterator::new(17, 5, false) + .filter(|pauli| { + code.is_logical_error_indexed(pauli, &indices.stabilizer, &indices.logical) + }) + .collect(); + let actual = logical_errors_at_weight(&code, 5, &config, &indices); + assert_eq!(actual, expected); + + let distance = calculate_distance(&code, &config).unwrap(); + assert_eq!(distance.distance, 5); + assert_eq!(distance.min_weight_operator, expected[0]); + } + #[test] fn test_logical_equivalence_tracking() { // 3-qubit bit flip code diff --git a/crates/pecos-qec/src/fault_tolerance/stabilizer_flip_checker.rs b/crates/pecos-qec/src/fault_tolerance/stabilizer_flip_checker.rs index 85c7ea21e..d4043b97b 100644 --- a/crates/pecos-qec/src/fault_tolerance/stabilizer_flip_checker.rs +++ b/crates/pecos-qec/src/fault_tolerance/stabilizer_flip_checker.rs @@ -473,21 +473,11 @@ impl<'a> StabilizerFlipChecker<'a> { /// Returns early on first failure, more efficient than full analysis. #[must_use] pub fn has_undetectable_logical(&self, weight: usize) -> bool { - let n = self.code.num_qubits(); - let pauli_types = [1u8, 2, 3]; // X, Y, Z - - for positions in combinations(n, weight) { - for paulis in pauli_product(&pauli_types, weight) { - let error = build_pauli_string(&positions, &paulis); - let flips = self.compute_flips(&error); - - if flips.is_undetectable() && flips.has_logical_error() { - return true; - } - } - } - - false + crate::distance::has_logical_error_at_weight( + self.code, + weight, + &crate::DistanceSearchConfig::default(), + ) } /// Compute the distance of the code. @@ -496,7 +486,11 @@ impl<'a> StabilizerFlipChecker<'a> { /// Returns None if no undetectable logical error is found up to `max_weight`. #[must_use] pub fn compute_distance(&self, max_weight: usize) -> Option { - (1..=max_weight).find(|&w| self.has_undetectable_logical(w)) + crate::calculate_distance( + self.code, + &crate::DistanceSearchConfig::with_max_weight(max_weight), + ) + .map(|result| result.distance) } } @@ -957,6 +951,17 @@ mod tests { assert_eq!(distance, Some(3), "Steane code distance should be 3"); } + #[test] + fn test_compute_distance_matches_calculate_distance() { + let code = steane_code(); + let checker_distance = StabilizerFlipChecker::new(&code).compute_distance(5); + let engine_distance = + crate::calculate_distance(&code, &crate::DistanceSearchConfig::with_max_weight(5)) + .map(|result| result.distance); + + assert_eq!(checker_distance, engine_distance); + } + #[test] fn test_steane_code_parameters() { let code = steane_code(); diff --git a/crates/pecos-qec/src/lib.rs b/crates/pecos-qec/src/lib.rs index 77823cfcf..0dadd95f8 100644 --- a/crates/pecos-qec/src/lib.rs +++ b/crates/pecos-qec/src/lib.rs @@ -81,7 +81,7 @@ pub use parity_check_matrix::{ParityCheckMatrix, ParityCheckMatrixError}; pub use distance::{ DistanceResult, DistanceSearchConfig, LogicalOperatorInfo, WeightedPauliIterator, calculate_distance, find_min_weight_logicals, find_min_weight_logicals_with_info, - find_shortest_logicals, + find_shortest_logicals, has_logical_error_at_weight, }; pub use fault_tolerance::dem_builder::{ DecomposedFault, DemBuilder, DemBuilderError, DemOutput, DetectorDef, DetectorErrorModel,