1use std::collections::BTreeSet;
13
14use bloq_utils::PauliString;
15use glam::IVec3;
16use thiserror::Error;
17
18use super::modular::toggle;
19use super::stabilizer::{CoeffVec, axis_pivot_constraints, gaussian_elimination_with_tracking};
20use crate::{FxHashMap, Pauli};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OutputCorrectionRow {
33 pub output_support: Vec<(IVec3, Pauli)>,
35 pub readout_ordinals: Vec<usize>,
37}
38
39#[derive(Debug, Clone, Error, PartialEq, Eq)]
43pub enum OutputCorrectionError {
44 #[error("output-correction RHS length mismatch: expected {expected}, got {actual}")]
46 RhsLengthMismatch {
47 expected: usize,
49 actual: usize,
51 },
52 #[error("output {output} named by a correction surface is not a requested output")]
54 UnknownOutput {
55 output: IVec3,
57 },
58 #[error("output corrections are inconsistent")]
60 Inconsistent,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct SymbolicFramePair {
67 pub output: IVec3,
69 pub x: Vec<usize>,
72 pub z: Vec<usize>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SymbolicOutputCorrection {
80 frames: Vec<SymbolicFramePair>,
81 inconsistencies: Vec<Vec<usize>>,
82 num_constraints: usize,
83}
84
85impl SymbolicOutputCorrection {
86 pub fn frames(&self) -> &[SymbolicFramePair] {
88 &self.frames
89 }
90
91 pub fn evaluate(
98 &self,
99 needs_flip: &[bool],
100 ) -> Result<Vec<(IVec3, Pauli)>, OutputCorrectionError> {
101 if needs_flip.len() != self.num_constraints {
102 return Err(OutputCorrectionError::RhsLengthMismatch {
103 expected: self.num_constraints,
104 actual: needs_flip.len(),
105 });
106 }
107 let xor = |set: &[usize]| {
108 set.iter()
109 .fold(false, |acc, &index| acc ^ needs_flip[index])
110 };
111 if self.inconsistencies.iter().any(|set| xor(set)) {
112 return Err(OutputCorrectionError::Inconsistent);
113 }
114 Ok(self
115 .frames
116 .iter()
117 .map(|pair| (pair.output, pauli_from_xz(xor(&pair.x), xor(&pair.z))))
118 .collect())
119 }
120}
121
122pub(super) fn pivot_output_correction_rows(
126 mut rows: Vec<PauliString>,
127 mut coeffs: Vec<CoeffVec>,
128 outputs: &[IVec3],
129 output_cols: &[usize],
130 input_cols: &[usize],
131 protected_cols: &[usize],
132) -> Vec<(OutputCorrectionRow, PauliString)> {
133 debug_assert_eq!(outputs.len(), output_cols.len());
134 let num_pivoted = gaussian_elimination_with_tracking(
135 &mut rows,
136 &mut coeffs,
137 axis_pivot_constraints(output_cols.iter().copied()),
138 |row, &(col, basis)| row.get(col) & basis,
139 None,
140 );
141
142 let mut output_start = 0;
148 if !input_cols.is_empty() {
149 let protected_rank = gaussian_elimination_with_tracking(
150 &mut rows[num_pivoted..],
151 &mut coeffs[num_pivoted..],
152 axis_pivot_constraints(protected_cols.iter().copied()),
153 |row, &(col, basis)| row.get(col) & basis,
154 None,
155 );
156 let kernel_start = num_pivoted + protected_rank;
157 let kernel_len = rows.len() - kernel_start;
158 if kernel_len != 0 {
159 rows.rotate_left(kernel_start);
160 coeffs.rotate_left(kernel_start);
161 gaussian_elimination_with_tracking(
162 &mut rows,
163 &mut coeffs,
164 axis_pivot_constraints(input_cols.iter().copied()),
165 |row, &(col, basis)| row.get(col) & basis,
166 Some(kernel_len),
167 );
168 output_start = kernel_len;
169 }
170 }
171
172 rows.into_iter()
173 .zip(coeffs)
174 .skip(output_start)
175 .take(num_pivoted)
176 .filter_map(|(row, coeff)| {
177 let output_support = outputs
178 .iter()
179 .zip(output_cols)
180 .filter_map(|(&output, &col)| {
181 let pauli = row.get(col);
182 (pauli != Pauli::I).then_some((output, pauli))
183 })
184 .collect::<Vec<_>>();
185 (!output_support.is_empty()).then(|| {
186 (
187 OutputCorrectionRow {
188 output_support,
189 readout_ordinals: coeff.to_indices(),
190 },
191 row,
192 )
193 })
194 })
195 .collect()
196}
197
198pub fn solve_output_correction_symbolic(
210 outputs: &[IVec3],
211 constraints: &[OutputCorrectionRow],
212) -> Result<SymbolicOutputCorrection, OutputCorrectionError> {
213 solve_output_correction_symbolic_prefix(outputs, constraints, outputs.len())
214}
215
216pub fn solve_output_correction_symbolic_prefix(
231 outputs: &[IVec3],
232 constraints: &[OutputCorrectionRow],
233 output_count: usize,
234) -> Result<SymbolicOutputCorrection, OutputCorrectionError> {
235 let num_vars = outputs.len() * 2;
236 let num_constraints = constraints.len();
237 let mut output_indices = FxHashMap::default();
238 for (index, &output) in outputs.iter().enumerate() {
239 output_indices.entry(output).or_insert(index);
241 }
242 let mut parities = ConstraintParities {
243 sources: num_constraints,
244 xors: Vec::new(),
245 };
246 let mut rows = constraints
247 .iter()
248 .enumerate()
249 .map(|(constraint_index, constraint)| {
250 let mut lhs = BTreeSet::new();
251 for &(output, surface_pauli) in &constraint.output_support {
252 let &output_index = output_indices
253 .get(&output)
254 .ok_or(OutputCorrectionError::UnknownOutput { output })?;
255 if surface_pauli & Pauli::Z {
256 toggle(&mut lhs, 2 * output_index);
257 }
258 if surface_pauli & Pauli::X {
259 toggle(&mut lhs, 2 * output_index + 1);
260 }
261 }
262 Ok((lhs, Some(constraint_index)))
263 })
264 .collect::<Result<Vec<_>, _>>()?;
265
266 let mut by_first = vec![BTreeSet::new(); num_vars];
269 for (index, (lhs, _)) in rows.iter().enumerate() {
270 if let Some(&col) = lhs.first() {
271 by_first[col].insert(index);
272 }
273 }
274 let mut pivot_row = 0;
275 for col in 0..num_vars {
276 let Some(found) = by_first[col].pop_first() else {
277 continue;
278 };
279 if found != pivot_row {
282 if let Some(&displaced_col) = rows[pivot_row].0.first() {
283 by_first[displaced_col].remove(&pivot_row);
284 by_first[displaced_col].insert(found);
285 }
286 rows.swap(pivot_row, found);
287 }
288 let pivot = rows[pivot_row].clone();
289 for index in std::mem::take(&mut by_first[col]) {
290 let (lhs, rhs) = &mut rows[index];
291 for &entry in &pivot.0 {
292 toggle(lhs, entry);
293 }
294 *rhs = parities.xor(*rhs, pivot.1);
295 if let Some(&next_col) = lhs.first() {
296 by_first[next_col].insert(index);
297 }
298 }
299 pivot_row += 1;
300 }
301
302 let inconsistencies = rows[pivot_row..]
303 .iter()
304 .map(|(lhs, rhs)| {
305 debug_assert!(lhs.is_empty());
306 parities.indices(*rhs)
307 })
308 .filter(|set| !set.is_empty())
309 .collect();
310
311 let mut solution = vec![None; num_vars];
314 for (lhs, rhs) in rows[..pivot_row].iter().rev() {
315 let mut columns = lhs.iter();
316 let &col = columns.next().expect("pivot row is nonzero");
317 let mut value = *rhs;
318 for &other in columns {
319 value = parities.xor(value, solution[other]);
320 }
321 solution[col] = value;
322 }
323 let frames = outputs
324 .iter()
325 .take(output_count)
326 .enumerate()
327 .map(|(index, &output)| SymbolicFramePair {
328 output,
329 x: parities.indices(solution[2 * index]),
330 z: parities.indices(solution[2 * index + 1]),
331 })
332 .collect();
333
334 Ok(SymbolicOutputCorrection {
335 frames,
336 inconsistencies,
337 num_constraints,
338 })
339}
340
341struct ConstraintParities {
345 sources: usize,
346 xors: Vec<(usize, usize)>,
347}
348
349impl ConstraintParities {
350 fn xor(&mut self, left: Option<usize>, right: Option<usize>) -> Option<usize> {
351 match (left, right) {
352 (None, other) | (other, None) => other,
353 (Some(left), Some(right)) if left == right => None,
354 (Some(left), Some(right)) => {
355 self.xors.push((left, right));
356 Some(self.sources + self.xors.len() - 1)
357 }
358 }
359 }
360
361 fn indices(&self, expression: Option<usize>) -> Vec<usize> {
362 let mut pending = expression.into_iter().collect::<BTreeSet<_>>();
363 while let Some(&node) = pending.last() {
364 if node < self.sources {
365 break;
366 }
367 pending.pop_last();
368 let (left, right) = self.xors[node - self.sources];
369 toggle(&mut pending, left);
370 toggle(&mut pending, right);
371 }
372 pending.into_iter().collect()
373 }
374}
375
376fn pauli_from_xz(x: bool, z: bool) -> Pauli {
377 match (x, z) {
378 (false, false) => Pauli::I,
379 (true, false) => Pauli::X,
380 (false, true) => Pauli::Z,
381 (true, true) => Pauli::Y,
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use glam::ivec3;
388 use rand::rngs::StdRng;
389 use rand::{RngExt, SeedableRng};
390
391 use super::*;
392 fn constraint(support: Vec<(IVec3, Pauli)>) -> OutputCorrectionRow {
393 OutputCorrectionRow {
394 output_support: support,
395 readout_ordinals: Vec::new(),
396 }
397 }
398
399 #[test]
400 fn input_kernel_normalization_preserves_resources_and_readout_provenance() {
401 let r = PauliString::try_from("X__X").unwrap();
404 let k = PauliString::try_from("XX__").unwrap();
405 let j = PauliString::try_from("X_X_").unwrap();
406 let coeffs = (0..3)
407 .map(|index| CoeffVec::singleton(index, 3))
408 .collect::<Vec<_>>();
409 let solve = |rows, coeffs| {
410 pivot_output_correction_rows(rows, coeffs, &[ivec3(0, 0, 1)], &[3], &[0, 1], &[2])
411 };
412 let expected = solve(vec![r.clone(), k.clone(), j.clone()], coeffs.clone());
413 assert_eq!(expected.len(), 1);
414 assert_eq!(expected[0].1, PauliString::try_from("_X_X").unwrap());
415 assert_eq!(expected[0].0.readout_ordinals, vec![0, 1]);
416
417 let mut combined = coeffs[0].clone();
418 combined.xor_assign(&coeffs[1]);
419 assert_eq!(
420 solve(
421 vec![&r ^ &k, j.clone(), k],
422 vec![combined, coeffs[2].clone(), coeffs[1].clone()]
423 ),
424 expected,
425 "replacing a witness by the same input-kernel coset preserves its map and provenance"
426 );
427 let protected = solve(
428 vec![r.clone(), j],
429 vec![coeffs[0].clone(), coeffs[2].clone()],
430 );
431 assert_eq!(
432 protected[0].1, r,
433 "a resource-bearing relation cannot change the witness"
434 );
435 assert_eq!(protected[0].0.readout_ordinals, vec![0]);
436 }
437
438 fn reference_solve(
440 outputs: &[IVec3],
441 constraints: &[(Vec<(usize, Pauli)>, bool)],
442 ) -> Result<Vec<(IVec3, Pauli)>, ()> {
443 let num_vars = outputs.len() * 2;
444 let mut rows = constraints
445 .iter()
446 .map(|(support, needs_flip)| {
447 let mut row = vec![false; num_vars + 1];
448 for &(output_index, surface_pauli) in support {
449 if surface_pauli & Pauli::Z {
450 row[2 * output_index] ^= true;
451 }
452 if surface_pauli & Pauli::X {
453 row[2 * output_index + 1] ^= true;
454 }
455 }
456 row[num_vars] = *needs_flip;
457 row
458 })
459 .collect::<Vec<_>>();
460
461 let mut pivot_row = 0;
462 for col in 0..num_vars {
463 let Some(found) = (pivot_row..rows.len()).find(|&row| rows[row][col]) else {
464 continue;
465 };
466 rows.swap(pivot_row, found);
467 let pivot = rows[pivot_row].clone();
468 for (row_index, row) in rows.iter_mut().enumerate() {
469 if row_index == pivot_row || !row[col] {
470 continue;
471 }
472 for (entry, pivot_entry) in row.iter_mut().zip(&pivot).skip(col) {
473 *entry ^= *pivot_entry;
474 }
475 }
476 pivot_row += 1;
477 }
478
479 if rows
480 .iter()
481 .any(|row| row[..num_vars].iter().all(|bit| !*bit) && row[num_vars])
482 {
483 return Err(());
484 }
485
486 let mut solution = vec![false; num_vars];
487 for row in rows.iter().take(pivot_row) {
488 if let Some(col) = row[..num_vars].iter().position(|bit| *bit) {
489 solution[col] = row[num_vars];
490 }
491 }
492
493 Ok(outputs
494 .iter()
495 .copied()
496 .enumerate()
497 .map(|(index, output)| {
498 (
499 output,
500 pauli_from_xz(solution[2 * index], solution[2 * index + 1]),
501 )
502 })
503 .collect())
504 }
505
506 const PAULIS: [Pauli; 4] = [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z];
507
508 #[test]
509 fn symbolic_solve_matches_reference_on_random_systems() {
510 let mut rng = StdRng::seed_from_u64(0x0510);
511 for case in 0..600 {
512 let num_outputs = rng.random_range(1..=if case < 500 { 3 } else { 70 });
513 let outputs: Vec<IVec3> = (0..num_outputs).map(|i| ivec3(i, 0, i)).collect();
514 let num_constraints =
515 rng.random_range(0..=if case < 500 { 6 } else { 2 * outputs.len() + 3 });
516 let mut constraints: Vec<OutputCorrectionRow> = (0..num_constraints)
517 .map(|_| {
518 constraint(
519 outputs
520 .iter()
521 .filter_map(|&output| {
522 let pauli = if case >= 500 && case % 2 == 0 {
523 if rng.random_range(0..8) == 0 {
524 PAULIS[rng.random_range(1..4)]
525 } else {
526 Pauli::I
527 }
528 } else {
529 PAULIS[rng.random_range(0..4)]
530 };
531 (pauli != Pauli::I).then_some((output, pauli))
532 })
533 .collect(),
534 )
535 })
536 .collect();
537 if case % 3 == 0 && constraints.len() > 1 {
538 constraints[1] = constraints[0].clone();
539 }
540 let symbolic = solve_output_correction_symbolic(&outputs, &constraints)
541 .expect("all supports name requested outputs");
542 let projected = [0, outputs.len() / 2, outputs.len() + 1].map(|count| {
543 let solve =
544 solve_output_correction_symbolic_prefix(&outputs, &constraints, count).unwrap();
545 assert_eq!(
546 solve.frames(),
547 &symbolic.frames()[..count.min(outputs.len())]
548 );
549 solve
550 });
551
552 for trial in 0..8 {
555 let assigned = (0..outputs.len())
556 .map(|_| (rng.random::<bool>(), rng.random::<bool>()))
557 .collect::<Vec<_>>();
558 let reference_constraints = constraints
559 .iter()
560 .map(|constraint| {
561 let indexed = constraint
562 .output_support
563 .iter()
564 .map(|&(output, pauli)| {
565 let index = outputs
566 .iter()
567 .position(|candidate| *candidate == output)
568 .expect("support built from outputs");
569 (index, pauli)
570 })
571 .collect::<Vec<_>>();
572 let flip = if trial < 4 {
573 indexed.iter().fold(false, |value, &(index, pauli)| {
574 value
575 ^ (pauli & Pauli::Z && assigned[index].0)
576 ^ (pauli & Pauli::X && assigned[index].1)
577 })
578 } else {
579 rng.random()
580 };
581 (indexed, flip)
582 })
583 .collect::<Vec<_>>();
584 let needs_flip = reference_constraints
585 .iter()
586 .map(|(_, flip)| *flip)
587 .collect::<Vec<_>>();
588 let expected = reference_solve(&outputs, &reference_constraints);
589 for solve in std::iter::once(&symbolic).chain(&projected) {
590 match (solve.evaluate(&needs_flip), &expected) {
591 (Ok(actual), Ok(expected)) => {
592 assert_eq!(actual, expected[..solve.frames().len()])
593 }
594 (Err(OutputCorrectionError::Inconsistent), Err(())) => {}
595 (actual, expected) => {
596 panic!("solver drift: symbolic={actual:?} reference={expected:?}")
597 }
598 }
599 }
600 }
601 }
602 }
603
604 #[test]
605 fn public_frame_through_long_seam_chain_keeps_all_constraint_parities() {
606 let outputs = (0..4096).map(|i| ivec3(i, 0, 0)).collect::<Vec<_>>();
607 let mut constraints = outputs
608 .windows(2)
609 .map(|pair| constraint(vec![(pair[0], Pauli::Z), (pair[1], Pauli::Z)]))
610 .collect::<Vec<_>>();
611 constraints[0]
612 .output_support
613 .extend([(outputs[0], Pauli::X), (outputs[0], Pauli::X)]);
614 constraints.push(constraint(vec![(*outputs.last().unwrap(), Pauli::Z)]));
615 let solve = solve_output_correction_symbolic_prefix(&outputs, &constraints, 1).unwrap();
616 assert_eq!(
617 solve.frames(),
618 &[SymbolicFramePair {
619 output: outputs[0],
620 x: (0..constraints.len()).collect(),
621 z: Vec::new(),
622 }]
623 );
624 }
625
626 #[test]
627 fn unknown_output_in_support_is_rejected() {
628 let outputs = [ivec3(0, 0, 1)];
629 let constraints = [constraint(vec![(ivec3(9, 9, 9), Pauli::Z)])];
630 assert_eq!(
631 solve_output_correction_symbolic(&outputs, &constraints),
632 Err(OutputCorrectionError::UnknownOutput {
633 output: ivec3(9, 9, 9)
634 })
635 );
636 assert!(matches!(
637 solve_output_correction_symbolic_prefix(&outputs, &constraints, 0),
638 Err(OutputCorrectionError::UnknownOutput { .. })
639 ));
640 }
641
642 #[test]
643 fn dependent_rows_become_runtime_consistency_checks() {
644 let outputs = [ivec3(0, 0, 1)];
647 let constraints = [
648 constraint(vec![(ivec3(0, 0, 1), Pauli::Z)]),
649 constraint(vec![(ivec3(0, 0, 1), Pauli::Z)]),
650 ];
651 let symbolic = solve_output_correction_symbolic(&outputs, &constraints).unwrap();
652
653 assert_eq!(
654 symbolic.evaluate(&[true, true]).unwrap(),
655 vec![(ivec3(0, 0, 1), Pauli::X)]
656 );
657 assert_eq!(symbolic.frames()[0].x, vec![0]);
658 let error = symbolic
659 .evaluate(&[true, false])
660 .expect_err("unequal dependent parities are inconsistent");
661 assert_eq!(error, OutputCorrectionError::Inconsistent);
662 assert_eq!(error.to_string(), "output corrections are inconsistent");
663 }
664
665 #[test]
666 fn evaluate_rejects_rhs_length_mismatch() {
667 let outputs = [ivec3(0, 0, 1)];
668 let constraints = [constraint(vec![(ivec3(0, 0, 1), Pauli::Z)])];
669 let symbolic = solve_output_correction_symbolic(&outputs, &constraints).unwrap();
670
671 assert_eq!(
672 symbolic.evaluate(&[]),
673 Err(OutputCorrectionError::RhsLengthMismatch {
674 expected: 1,
675 actual: 0,
676 })
677 );
678 assert_eq!(
679 symbolic.evaluate(&[false, true]),
680 Err(OutputCorrectionError::RhsLengthMismatch {
681 expected: 1,
682 actual: 2,
683 })
684 );
685 }
686}