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}