Skip to main content

qec_code/
distance.rs

1use crate::Pauli;
2use crate::binary::try_in_row_span;
3use crate::code::StabilizerCode;
4#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
5use crate::distance_exact::ExactCssDistanceSolverStatus;
6use crate::distance_exact::{
7    ExactCssDistanceBackend, ExactCssDistanceSolverOptions, ExactCssDistanceSolverReport,
8};
9use crate::error::{QecError, Result};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum LogicalClass {
15    XLike,
16    ZLike,
17    Mixed,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct DistanceResult {
22    pub distance: usize,
23    pub witness: Pauli,
24    pub logical_class: LogicalClass,
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub struct ExactCssDistanceComputation {
29    pub distance: DistanceResult,
30    pub solver_report: Option<ExactCssDistanceSolverReport>,
31}
32
33pub fn compute_distance(code: &StabilizerCode) -> Result<DistanceResult> {
34    Ok(
35        compute_distance_with_solver_options(code, ExactCssDistanceSolverOptions::default())?
36            .distance,
37    )
38}
39
40pub fn compute_distance_with_solver_options(
41    code: &StabilizerCode,
42    solver: ExactCssDistanceSolverOptions,
43) -> Result<ExactCssDistanceComputation> {
44    if code.num_logical_qubits() == 0 {
45        return Err(QecError::DistanceWitnessNotFound);
46    }
47
48    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
49    {
50        compute_distance_via_ilp(code, solver)
51    }
52
53    #[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
54    {
55        compute_distance_without_ilp(code, solver)
56    }
57}
58
59#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
60fn compute_distance_via_ilp(
61    code: &StabilizerCode,
62    solver: ExactCssDistanceSolverOptions,
63) -> Result<ExactCssDistanceComputation> {
64    let lowered = crate::distance_ilp::lower_distance_problem(code)?;
65    let config = qec_ilp_core::BinaryIlpConfig {
66        backend: qec_ilp_core::BackendConfig {
67            kind: backend_kind_to_ilp(solver.backend),
68            time_limit_seconds: solver.time_limit_seconds,
69            mip_gap: solver.mip_gap,
70            threads: solver.threads,
71            verbose: solver.verbose_solver,
72        },
73    };
74    let mut backend = qec_ilp_core::backend::build_binary_backend(&lowered.model, &config)?;
75    let backend_kind = backend.kind();
76    let solution = backend.solve()?;
77    let solver_status = solver_status_from_ilp(solution.status)?;
78    let start = lowered.symplectic_var_offset;
79    let end = start + code.n() * 2;
80    let row = solution.binary_values[start..end]
81        .iter()
82        .map(|&bit| u8::from(bit))
83        .collect::<Vec<_>>();
84    let witness = Pauli::from_symplectic_row(row)?;
85    post_validate_distance_witness(code, &witness)?;
86
87    Ok(ExactCssDistanceComputation {
88        distance: DistanceResult {
89            distance: witness.weight(),
90            logical_class: classify_logical(&witness),
91            witness,
92        },
93        solver_report: Some(ExactCssDistanceSolverReport {
94            backend: backend_kind_from_ilp(backend_kind),
95            status: solver_status,
96        }),
97    })
98}
99
100#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
101fn compute_distance_without_ilp(
102    code: &StabilizerCode,
103    solver: ExactCssDistanceSolverOptions,
104) -> Result<ExactCssDistanceComputation> {
105    if solver != ExactCssDistanceSolverOptions::default() {
106        if solver.backend == ExactCssDistanceBackend::Gurobi {
107            return Err(QecError::IlpBackendUnavailable("Gurobi".into()));
108        }
109        return Err(QecError::DistanceComputationUnsupported {
110            n: code.n(),
111            reason: "solver options require an ILP-enabled build".into(),
112        });
113    }
114    Ok(ExactCssDistanceComputation {
115        distance: compute_distance_via_exhaustive_search(code)?,
116        solver_report: None,
117    })
118}
119
120#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
121fn compute_distance_via_exhaustive_search(code: &StabilizerCode) -> Result<DistanceResult> {
122    validate_exhaustive_search_width(code.n())?;
123    for weight in 1..=code.n() {
124        if let Some(witness) = find_normalizer_witness_of_weight(code, weight)? {
125            return Ok(DistanceResult {
126                distance: weight,
127                logical_class: classify_logical(&witness),
128                witness,
129            });
130        }
131    }
132    Err(QecError::DistanceWitnessNotFound)
133}
134
135#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
136fn validate_exhaustive_search_width(n: usize) -> Result<()> {
137    let symplectic_bits = n
138        .checked_mul(2)
139        .ok_or(QecError::DistanceComputationUnsupported {
140            n,
141            reason: "enable a distance ILP feature or use a smaller code".into(),
142        })?;
143    let _ = 1usize.checked_shl(symplectic_bits as u32).ok_or(
144        QecError::DistanceComputationUnsupported {
145            n,
146            reason: "enable a distance ILP feature or use a smaller code".into(),
147        },
148    )?;
149    Ok(())
150}
151
152#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
153fn find_normalizer_witness_of_weight(
154    code: &StabilizerCode,
155    weight: usize,
156) -> Result<Option<Pauli>> {
157    let stabilizer_rows = code.stabilizer_rows();
158    let mut support = Vec::with_capacity(weight);
159    search_supports(code, &stabilizer_rows, weight, 0, &mut support)
160}
161
162#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
163fn search_supports(
164    code: &StabilizerCode,
165    stabilizer_rows: &[Vec<u8>],
166    target_weight: usize,
167    next_qubit: usize,
168    support: &mut Vec<usize>,
169) -> Result<Option<Pauli>> {
170    if support.len() == target_weight {
171        let mut x = vec![0; code.n()];
172        let mut z = vec![0; code.n()];
173        return search_pauli_assignments(code, stabilizer_rows, support, 0, &mut x, &mut z);
174    }
175
176    let remaining = target_weight - support.len();
177    let max_qubit = code.n() - remaining;
178    for qubit in next_qubit..=max_qubit {
179        support.push(qubit);
180        if let Some(witness) =
181            search_supports(code, stabilizer_rows, target_weight, qubit + 1, support)?
182        {
183            return Ok(Some(witness));
184        }
185        support.pop();
186    }
187    Ok(None)
188}
189
190#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
191fn search_pauli_assignments(
192    code: &StabilizerCode,
193    stabilizer_rows: &[Vec<u8>],
194    support: &[usize],
195    support_index: usize,
196    x: &mut [u8],
197    z: &mut [u8],
198) -> Result<Option<Pauli>> {
199    if support_index == support.len() {
200        let candidate = Pauli::from_xz_bits(x.to_vec(), z.to_vec())?;
201        return if is_nontrivial_normalizer_witness(code, stabilizer_rows, &candidate)? {
202            Ok(Some(candidate))
203        } else {
204            Ok(None)
205        };
206    }
207
208    let qubit = support[support_index];
209    for (x_bit, z_bit) in [(1, 0), (0, 1), (1, 1)] {
210        x[qubit] = x_bit;
211        z[qubit] = z_bit;
212        if let Some(witness) =
213            search_pauli_assignments(code, stabilizer_rows, support, support_index + 1, x, z)?
214        {
215            return Ok(Some(witness));
216        }
217    }
218    x[qubit] = 0;
219    z[qubit] = 0;
220    Ok(None)
221}
222
223#[cfg(not(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi")))]
224fn is_nontrivial_normalizer_witness(
225    code: &StabilizerCode,
226    stabilizer_rows: &[Vec<u8>],
227    candidate: &Pauli,
228) -> Result<bool> {
229    Ok(code
230        .stabilizers()
231        .iter()
232        .all(|stabilizer| candidate.commutes_with(stabilizer))
233        && !try_in_row_span(stabilizer_rows, &candidate.to_symplectic_row())?)
234}
235
236fn classify_logical(pauli: &Pauli) -> LogicalClass {
237    let has_x = pauli.x_bits().contains(&1);
238    let has_z = pauli.z_bits().contains(&1);
239
240    match (has_x, has_z) {
241        (true, false) => LogicalClass::XLike,
242        (false, true) => LogicalClass::ZLike,
243        (true, true) => LogicalClass::Mixed,
244        (false, false) => unreachable!("logical witnesses are non-identity"),
245    }
246}
247
248#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
249fn backend_kind_to_ilp(kind: ExactCssDistanceBackend) -> qec_ilp_core::BackendKind {
250    match kind {
251        ExactCssDistanceBackend::Auto => qec_ilp_core::BackendKind::Auto,
252        ExactCssDistanceBackend::Highs => qec_ilp_core::BackendKind::Highs,
253        ExactCssDistanceBackend::Gurobi => qec_ilp_core::BackendKind::Gurobi,
254    }
255}
256
257#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
258fn backend_kind_from_ilp(kind: qec_ilp_core::BackendKind) -> ExactCssDistanceBackend {
259    match kind {
260        qec_ilp_core::BackendKind::Auto => ExactCssDistanceBackend::Auto,
261        qec_ilp_core::BackendKind::Highs => ExactCssDistanceBackend::Highs,
262        qec_ilp_core::BackendKind::Gurobi => ExactCssDistanceBackend::Gurobi,
263    }
264}
265
266#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
267fn solver_status_from_ilp(
268    status: qec_ilp_core::model::ModelSolutionStatus,
269) -> Result<ExactCssDistanceSolverStatus> {
270    Ok(match status {
271        qec_ilp_core::model::ModelSolutionStatus::Optimal => ExactCssDistanceSolverStatus::Optimal,
272        qec_ilp_core::model::ModelSolutionStatus::Infeasible => {
273            return Err(QecError::IlpInfeasible);
274        }
275        qec_ilp_core::model::ModelSolutionStatus::TimeLimit => {
276            ExactCssDistanceSolverStatus::TimeLimit
277        }
278        qec_ilp_core::model::ModelSolutionStatus::SolutionLimit => {
279            ExactCssDistanceSolverStatus::SolutionLimit
280        }
281        qec_ilp_core::model::ModelSolutionStatus::SubOptimal => {
282            ExactCssDistanceSolverStatus::SubOptimal
283        }
284    })
285}
286
287#[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
288fn post_validate_distance_witness(code: &StabilizerCode, witness: &Pauli) -> Result<()> {
289    if !code
290        .stabilizers()
291        .iter()
292        .all(|stabilizer| witness.commutes_with(stabilizer))
293    {
294        return Err(QecError::IlpSolveFailed(
295            "returned witness does not commute with stabilizers".into(),
296        ));
297    }
298
299    if witness.weight() == 0 {
300        return Err(QecError::IlpInfeasible);
301    }
302
303    if try_in_row_span(&code.stabilizer_rows(), &witness.to_symplectic_row())? {
304        return Err(QecError::IlpSolveFailed(
305            "returned witness lies in stabilizer span".into(),
306        ));
307    }
308
309    Ok(())
310}
311
312#[cfg(test)]
313mod tests {
314    use super::{LogicalClass, classify_logical};
315    use crate::Pauli;
316    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
317    use crate::{QecError, StabilizerCode};
318
319    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
320    fn single_qubit_z_stabilizer_code() -> StabilizerCode {
321        StabilizerCode::from_stabilizers(1, vec![Pauli::from_xz_bits(vec![0], vec![1]).unwrap()])
322            .unwrap()
323    }
324
325    #[test]
326    fn classify_logical_distinguishes_x_z_and_mixed_supports() {
327        let x_like = Pauli::from_xz_bits(vec![1, 0], vec![0, 0]).unwrap();
328        let z_like = Pauli::from_xz_bits(vec![0, 0], vec![0, 1]).unwrap();
329        let mixed = Pauli::from_xz_bits(vec![0, 1], vec![0, 1]).unwrap();
330
331        assert_eq!(classify_logical(&x_like), LogicalClass::XLike);
332        assert_eq!(classify_logical(&z_like), LogicalClass::ZLike);
333        assert_eq!(classify_logical(&mixed), LogicalClass::Mixed);
334    }
335
336    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
337    #[test]
338    fn post_validate_distance_witness_rejects_non_commuting_witnesses() {
339        let code = single_qubit_z_stabilizer_code();
340        let witness = Pauli::from_xz_bits(vec![1], vec![0]).unwrap();
341
342        assert_eq!(
343            super::post_validate_distance_witness(&code, &witness),
344            Err(QecError::IlpSolveFailed(
345                "returned witness does not commute with stabilizers".into(),
346            ))
347        );
348    }
349
350    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
351    #[test]
352    fn post_validate_distance_witness_rejects_stabilizer_span_elements() {
353        let code = single_qubit_z_stabilizer_code();
354        let witness = Pauli::from_xz_bits(vec![0], vec![1]).unwrap();
355
356        assert_eq!(
357            super::post_validate_distance_witness(&code, &witness),
358            Err(QecError::IlpSolveFailed(
359                "returned witness lies in stabilizer span".into(),
360            ))
361        );
362    }
363
364    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
365    #[test]
366    fn post_validate_distance_witness_rejects_zero_weight_witnesses() {
367        let code = StabilizerCode::from_stabilizers(1, vec![]).unwrap();
368        let witness = Pauli::from_xz_bits(vec![0], vec![0]).unwrap();
369
370        assert_eq!(
371            super::post_validate_distance_witness(&code, &witness),
372            Err(QecError::IlpInfeasible)
373        );
374    }
375
376    #[cfg(any(feature = "distance-ilp-highs", feature = "distance-ilp-gurobi"))]
377    #[test]
378    fn infeasible_solver_status_is_rejected_before_reading_solution_values() {
379        assert_eq!(
380            super::solver_status_from_ilp(qec_ilp_core::ModelSolutionStatus::Infeasible),
381            Err(QecError::IlpInfeasible),
382        );
383    }
384}