1use std::collections::{BTreeMap, BTreeSet};
4
5use glam::IVec3;
6use quizx::fscalar::{FScalar, Zero};
7use quizx::graph::{GraphLike, VType};
8use quizx::tensor::ToTensor;
9
10use crate::zx::{CoeffVec, solve_coeff_combination};
11use crate::{Action, BlockGraph, Expr, FeedbackTarget, Pauli, PauliBasis};
12
13use super::diagram::eval_expr;
14use super::qasm::QasmCircuit;
15use super::{
16 BoundaryOrder, BranchAssignment, BranchStatus, LogicalVerifier, QasmError, QuizxGraph,
17};
18
19#[derive(Debug, Clone)]
26pub struct FeedbackOptions {
27 boundaries: Option<BoundaryOrder>,
28 measurements: BTreeMap<String, Expr>,
29 fixed_measurements: BTreeMap<String, bool>,
30 discarded_measurements: BTreeSet<String>,
31 preparation: Option<String>,
32 discard_if: Option<Expr>,
33 max_branches: usize,
34 max_classes: usize,
35 max_tensor_qubits: usize,
36 max_trials: usize,
37 internal_sites: Option<Vec<IVec3>>,
38}
39
40impl Default for FeedbackOptions {
41 fn default() -> Self {
42 Self {
43 boundaries: None,
44 measurements: BTreeMap::new(),
45 fixed_measurements: BTreeMap::new(),
46 discarded_measurements: BTreeSet::new(),
47 preparation: None,
48 discard_if: None,
49 max_branches: 256,
50 max_classes: 256,
51 max_tensor_qubits: 20,
52 max_trials: 4096,
53 internal_sites: None,
54 }
55 }
56}
57
58impl FeedbackOptions {
59 #[must_use]
62 pub fn with_search_limit(mut self, trials: usize) -> Self {
63 self.max_trials = trials;
64 self
65 }
66
67 #[must_use]
70 pub fn with_internal_sites(mut self, sites: Vec<IVec3>) -> Self {
71 self.internal_sites = Some(sites);
72 self
73 }
74
75 #[must_use]
77 pub fn with_boundaries(mut self, boundaries: BoundaryOrder) -> Self {
78 self.boundaries = Some(boundaries);
79 self
80 }
81
82 #[must_use]
85 pub fn with_measurement(mut self, bit: impl Into<String>, value: Expr) -> Self {
86 self.measurements.insert(bit.into(), value);
87 self
88 }
89
90 #[must_use]
93 pub fn with_fixed_measurement(mut self, bit: impl Into<String>, value: bool) -> Self {
94 self.fixed_measurements.insert(bit.into(), value);
95 self
96 }
97
98 #[must_use]
101 pub fn with_discarded_measurement(mut self, bit: impl Into<String>) -> Self {
102 self.discarded_measurements.insert(bit.into());
103 self
104 }
105
106 #[must_use]
110 pub fn with_input_preparation(mut self, qasm: impl Into<String>) -> Self {
111 self.preparation = Some(qasm.into());
112 self
113 }
114
115 #[must_use]
118 pub fn with_discard_if(mut self, condition: Expr) -> Self {
119 self.discard_if = Some(condition);
120 self
121 }
122
123 #[must_use]
126 pub fn with_limits(mut self, branches: usize, classes: usize, tensor_qubits: usize) -> Self {
127 self.max_branches = branches;
128 self.max_classes = classes;
129 self.max_tensor_qubits = tensor_qubits;
130 self
131 }
132}
133
134#[derive(Debug, thiserror::Error)]
136#[non_exhaustive]
137pub enum FeedbackInferenceError {
138 #[error("{0}")]
140 Qasm(#[from] QasmError),
141 #[error("{0}")]
143 Graph(#[from] crate::BlockGraphError),
144 #[error("{0}")]
146 Verify(#[from] super::VerifyLogicalError),
147 #[error("{0}")]
149 RuntimeBasis(#[from] crate::RuntimeBasisError),
150 #[error("{0}")]
152 OutputCorrection(#[from] crate::OutputCorrectionError),
153 #[error("invalid feedback correspondence: {0}")]
155 Binding(String),
156 #[error("feedback inference exceeded the {phase} bound (size {observed}, limit {limit})")]
158 Limit {
159 phase: &'static str,
161 observed: usize,
163 limit: usize,
165 },
166 #[error(
168 "anonymous outcome certificate is incomplete: boundary rank {rank}, expected {expected}"
169 )]
170 IncompleteCertificate {
171 rank: usize,
173 expected: usize,
175 },
176 #[error("source and graph reject different branches: {assignment:?}")]
178 RejectionMismatch {
179 assignment: BranchAssignment,
181 },
182 #[error("source and graph have different nonzero branch support: {assignment:?}")]
184 SupportMismatch {
185 assignment: BranchAssignment,
187 },
188 #[error("no output Pauli makes this branch equal to the source: {assignment:?}")]
190 NoOutputPauli {
191 assignment: BranchAssignment,
193 },
194 #[error("no valid Pauli feedback found in {trials} internal trials")]
196 NoPauliFeedback {
197 trials: usize,
199 },
200 #[error("emitted feedback does not implement the inferred output correction")]
202 EmissionMismatch,
203 #[error("the comparison contains no admitted nonzero branch")]
205 EmptyDomain,
206 #[error("QuiZX tensor arithmetic produced a non-finite scalar")]
208 NonFiniteTensor,
209 #[error("dyadic comparison exceeded its checked arithmetic range or lost precision")]
211 ArithmeticLimit,
212 #[error("cannot establish the existing signed C0 readout convention for {0:?}")]
214 ReadoutConventionUnavailable(String),
215}
216
217pub fn infer_feedback(
249 qasm: &str,
250 graph: &BlockGraph,
251) -> Result<Vec<Action>, FeedbackInferenceError> {
252 infer_feedback_with(qasm, graph, &FeedbackOptions::default())
253}
254
255pub fn infer_feedback_with(
268 qasm: &str,
269 graph: &BlockGraph,
270 options: &FeedbackOptions,
271) -> Result<Vec<Action>, FeedbackInferenceError> {
272 let source = QasmCircuit::parse(qasm)?;
273 let names = graph
274 .action_graph()
275 .inputs()
276 .map(str::to_owned)
277 .chain(
278 graph
279 .actions()
280 .into_iter()
281 .filter_map(|action| match action {
282 Action::Measure { name, .. } => Some(name),
283 _ => None,
284 }),
285 )
286 .collect::<BTreeSet<_>>()
287 .into_iter()
288 .collect::<Vec<_>>();
289 let count = checked_count(names.len(), options.max_branches, "named branches")?;
290 validate_records(graph, options.max_branches)?;
291 let verifier = make_verifier(graph, options)?;
292 let preparation = options
293 .preparation
294 .as_deref()
295 .map(QasmCircuit::parse)
296 .transpose()?;
297 if let Some(preparation) = &preparation {
298 if preparation.measurements.iter().any(Option::is_some) {
299 return Err(FeedbackInferenceError::Binding(
300 "input preparation must have no measurements".into(),
301 ));
302 }
303 if preparation.output_count() != verifier.boundaries().inputs().len()
304 || preparation.input_count() != source.input_count()
305 {
306 return Err(FeedbackInferenceError::Binding(
307 "input preparation does not match source/graph input widths".into(),
308 ));
309 }
310 } else if source.input_count() != verifier.boundaries().inputs().len() {
311 return Err(FeedbackInferenceError::Binding("source and graph input widths differ; supply ordered boundaries and any resource preparation".into()));
312 }
313 if source.output_count() != verifier.boundaries().outputs().len() {
314 return Err(FeedbackInferenceError::Binding(
315 "source and graph output widths differ".into(),
316 ));
317 }
318 let channel_axes = 2 * (source.input_count() + source.output_count());
321 if channel_axes > options.max_tensor_qubits || channel_axes >= usize::BITS as usize {
322 return Err(FeedbackInferenceError::Limit {
323 phase: "channel tensor qubits",
324 observed: channel_axes,
325 limit: options.max_tensor_qubits,
326 });
327 }
328 let bindings = source_bindings(&source, &names, options)?;
329 if let Some(condition) = &options.discard_if {
330 validate_expr(condition, &names)?;
331 }
332 let preparation = preparation.map(|source| source.instantiate(&[]));
333 let discarded = bindings
334 .iter()
335 .enumerate()
336 .filter_map(|(bit, binding)| matches!(binding, BitBinding::Discarded).then_some(bit))
337 .collect::<Vec<_>>();
338 let source_count = checked_count(
339 discarded.len(),
340 options.max_classes,
341 "discarded source outcomes",
342 )?;
343 let expected = (0..count)
344 .map(|mask| {
345 let assignment = assignment(&names, mask);
346 let mut values = bindings
347 .iter()
348 .map(|binding| match binding {
349 BitBinding::Fixed(value) => *value,
350 BitBinding::Record(expr) => eval_expr(expr, &assignment),
351 BitBinding::Discarded => false,
352 })
353 .collect::<Vec<_>>();
354 let maps = (0..source_count)
355 .map(|mask| {
356 for (index, &bit) in discarded.iter().enumerate() {
357 values[bit] = mask & (1 << index) != 0;
358 }
359 contract(source.instantiate(&values), options.max_tensor_qubits)
360 })
361 .collect::<Result<Vec<_>, _>>()?;
362 channel(&maps)
363 })
364 .collect::<Result<Vec<_>, _>>()?;
365 let problem = FeedbackProblem {
366 names: &names,
367 expected: &expected,
368 preparation: preparation.as_ref(),
369 options,
370 };
371 match problem.complete(graph, &verifier, Vec::new()) {
372 Err(
373 FeedbackInferenceError::NoOutputPauli { .. }
374 | FeedbackInferenceError::SupportMismatch { .. }
375 | FeedbackInferenceError::EmissionMismatch,
376 ) if options.max_trials != 0 => problem.search(graph),
377 result => result,
378 }
379}
380
381struct FeedbackProblem<'a> {
382 names: &'a [String],
383 expected: &'a [Vec<FScalar>],
384 preparation: Option<&'a QuizxGraph>,
385 options: &'a FeedbackOptions,
386}
387
388impl FeedbackProblem<'_> {
389 fn frames(
390 &self,
391 verifier: &LogicalVerifier,
392 ) -> Result<Vec<Vec<Pauli>>, FeedbackInferenceError> {
393 compare_branches(
394 verifier,
395 self.names,
396 self.expected,
397 self.preparation,
398 self.options,
399 )
400 }
401
402 fn complete(
403 &self,
404 graph: &BlockGraph,
405 verifier: &LogicalVerifier,
406 mut actions: Vec<Action>,
407 ) -> Result<Vec<Action>, FeedbackInferenceError> {
408 let frames = self.frames(verifier)?;
409 let output = frame_actions(self.names, verifier.boundaries().outputs(), &frames);
410 if output.is_empty() {
411 return Ok(actions);
412 }
413 let mut candidate = graph.clone();
414 let mut combined = candidate.actions();
415 combined.extend(output.iter().cloned());
416 candidate.set_actions(combined)?;
417 validate_records(&candidate, self.options.max_branches)?;
418 let remaining = self.frames(&make_verifier(&candidate, self.options)?)?;
419 if remaining.iter().flatten().any(|pauli| *pauli != Pauli::I) {
420 return Err(FeedbackInferenceError::EmissionMismatch);
421 }
422 actions.extend(output);
423 Ok(actions)
424 }
425
426 fn search(&self, graph: &BlockGraph) -> Result<Vec<Action>, FeedbackInferenceError> {
427 let graphs = if graph.branch_regions()?.is_empty() {
428 vec![graph.to_zx_graph()?]
429 } else {
430 graph
431 .branch_projections_up_to(self.options.max_branches)?
432 .iter()
433 .map(|projection| projection.graph().to_zx_graph())
434 .collect::<Result<Vec<_>, _>>()?
435 };
436 let zx = &graphs[0];
437 let is_wire = |position| {
438 graphs.iter().all(|zx| {
439 zx.feedback_edge(&FeedbackTarget {
440 target: position,
441 pauli: PauliBasis::X,
442 direction: None,
443 })
444 .is_some()
445 })
446 };
447 let mut sites = match &self.options.internal_sites {
448 Some(sites) => {
449 for &site in sites {
450 if !is_wire(site) {
451 return Err(FeedbackInferenceError::Binding(format!(
452 "{site} is not an unambiguous feedback wire in every structural arm"
453 )));
454 }
455 }
456 sites.clone()
457 }
458 None => zx
459 .nodes()
460 .iter()
461 .filter(|node| graph.get_block(node.pos).is_some() && is_wire(node.pos))
462 .map(|node| node.pos)
463 .collect(),
464 };
465 sites.sort_by_key(|position| {
468 (
469 zx.node_at(*position)
470 .expect("feedback sites are checked against this ZX graph")
471 .kind
472 != crate::NodeKind::Y,
473 std::cmp::Reverse(position.z),
474 position.to_array(),
475 )
476 });
477 sites.dedup();
478 let mut monomials = (0..self.expected.len()).collect::<Vec<_>>();
479 monomials.sort_by_key(|mask| (mask.count_ones(), *mask));
480 let atoms = monomials
481 .into_iter()
482 .flat_map(|mask| {
483 let condition = join(
484 crate::BinaryOp::And,
485 self.names
486 .iter()
487 .enumerate()
488 .filter(|(bit, _)| mask & (1 << bit) != 0)
489 .map(|(_, name)| Expr::Var(name.clone()))
490 .collect(),
491 );
492 sites.iter().flat_map(move |&target| {
493 [PauliBasis::Z, PauliBasis::X, PauliBasis::Y].map(|pauli| Action::Feedback {
494 targets: vec![FeedbackTarget {
495 pauli,
496 target,
497 direction: None,
498 }],
499 condition: condition.clone(),
500 })
501 })
502 })
503 .take(self.options.max_trials.saturating_add(1))
504 .collect::<Vec<_>>();
505 let mut trials = 0;
506 for weight in 1..=atoms.len() {
509 let mut indices = (0..weight).collect::<Vec<_>>();
510 loop {
511 trials += 1;
512 if trials > self.options.max_trials {
513 return Err(FeedbackInferenceError::Limit {
514 phase: "internal feedback trials",
515 observed: trials,
516 limit: self.options.max_trials,
517 });
518 }
519 let additions = indices
520 .iter()
521 .map(|&index| atoms[index].clone())
522 .collect::<Vec<_>>();
523 let mut candidate = graph.clone();
524 let mut actions = candidate.actions();
525 actions.extend(additions.iter().cloned());
526 let result = candidate
527 .set_actions(actions)
528 .map_err(FeedbackInferenceError::from)
529 .and_then(|()| validate_records(&candidate, self.options.max_branches))
530 .and_then(|()| make_verifier(&candidate, self.options))
531 .and_then(|verifier| self.complete(&candidate, &verifier, additions));
532 match result {
533 Ok(actions) => return Ok(actions),
534 Err(
535 error @ (FeedbackInferenceError::Limit { .. }
536 | FeedbackInferenceError::NonFiniteTensor
537 | FeedbackInferenceError::ArithmeticLimit),
538 ) => return Err(error),
539 Err(_) => {}
540 }
541 let Some(pivot) = (0..weight)
542 .rev()
543 .find(|&index| indices[index] < atoms.len() - weight + index)
544 else {
545 break;
546 };
547 indices[pivot] += 1;
548 for index in pivot + 1..weight {
549 indices[index] = indices[index - 1] + 1;
550 }
551 }
552 }
553 Err(FeedbackInferenceError::NoPauliFeedback { trials })
554 }
555}
556
557fn make_verifier(
558 graph: &BlockGraph,
559 options: &FeedbackOptions,
560) -> Result<LogicalVerifier, FeedbackInferenceError> {
561 Ok(match &options.boundaries {
562 Some(boundaries) => {
563 LogicalVerifier::with_boundaries_and_internal_measurements(graph, boundaries.clone())?
564 }
565 None => LogicalVerifier::with_internal_measurements(graph)?,
566 })
567}
568
569fn validate_records(graph: &BlockGraph, limit: usize) -> Result<(), FeedbackInferenceError> {
570 graph.validate()?;
571 if graph.branch_regions()?.is_empty() {
572 graph
573 .stabilizers()?
574 .validate_measurements_close_before_outputs_with_limit(limit)?;
575 } else {
576 let projections = graph.branch_projections_up_to(limit.saturating_add(1))?;
577 if projections.len() > limit {
578 return Err(FeedbackInferenceError::Limit {
579 phase: "structural branches",
580 observed: projections.len(),
581 limit,
582 });
583 }
584 for projection in projections {
585 projection
586 .graph()
587 .stabilizers()?
588 .validate_measurements_close_before_outputs_with_limit(limit)?;
589 }
590 }
591 Ok(())
592}
593
594enum BitBinding {
595 Fixed(bool),
596 Record(Expr),
597 Discarded,
598}
599
600fn source_bindings(
601 source: &QasmCircuit,
602 names: &[String],
603 options: &FeedbackOptions,
604) -> Result<Vec<BitBinding>, FeedbackInferenceError> {
605 let bits = source.program.bits();
606 for name in options
607 .measurements
608 .keys()
609 .chain(options.fixed_measurements.keys())
610 .chain(options.discarded_measurements.iter())
611 {
612 if !bits.contains(name) {
613 return Err(FeedbackInferenceError::Binding(format!(
614 "unknown QASM bit {name:?}"
615 )));
616 }
617 if usize::from(options.measurements.contains_key(name))
618 + usize::from(options.fixed_measurements.contains_key(name))
619 + usize::from(options.discarded_measurements.contains(name))
620 > 1
621 {
622 return Err(FeedbackInferenceError::Binding(format!(
623 "QASM bit {name:?} has conflicting bindings"
624 )));
625 }
626 }
627 bits.iter()
628 .enumerate()
629 .map(|(bit, name)| {
630 if options.discarded_measurements.contains(name) {
631 return Ok(BitBinding::Discarded);
632 }
633 if let Some(value) = options.fixed_measurements.get(name) {
634 return Ok(BitBinding::Fixed(*value));
635 }
636 if let Some(expr) = options.measurements.get(name) {
637 validate_expr(expr, names)?;
638 return Ok(BitBinding::Record(expr.clone()));
639 }
640 if !source.measurements.contains(&Some(bit)) {
641 return Ok(BitBinding::Fixed(false));
642 }
643 let matched = names
644 .iter()
645 .find(|candidate| *candidate == name)
646 .or_else(|| {
647 let bare = name.strip_suffix("[0]")?;
648 if bits.contains(&format!("{bare}[1]")) {
649 return None;
650 }
651 names.iter().find(|candidate| candidate.as_str() == bare)
652 });
653 matched
654 .map(|name| BitBinding::Record(Expr::Var(name.clone())))
655 .ok_or_else(|| FeedbackInferenceError::Binding(format!(
656 "QASM result {name:?} needs a graph binding, fixed outcome, or explicit discard"
657 )))
658 })
659 .collect()
660}
661
662fn validate_expr(expr: &Expr, names: &[String]) -> Result<(), FeedbackInferenceError> {
663 match expr {
664 Expr::Var(name) if !names.contains(name) => Err(FeedbackInferenceError::Binding(format!(
665 "unknown graph record {name:?}"
666 ))),
667 Expr::Var(_) => Ok(()),
668 Expr::Not(inner) => validate_expr(inner, names),
669 Expr::Binary(_, left, right) => {
670 validate_expr(left, names)?;
671 validate_expr(right, names)
672 }
673 }
674}
675
676fn assignment(names: &[String], mask: usize) -> BranchAssignment {
677 names
678 .iter()
679 .enumerate()
680 .map(|(bit, name)| (name.clone(), mask & (1 << bit) != 0))
681 .collect()
682}
683
684fn compare_branches(
685 verifier: &LogicalVerifier,
686 names: &[String],
687 expected: &[Vec<FScalar>],
688 preparation: Option<&QuizxGraph>,
689 options: &FeedbackOptions,
690) -> Result<Vec<Vec<Pauli>>, FeedbackInferenceError> {
691 let mut frames = Vec::with_capacity(expected.len());
692 let mut nonzero = false;
693 for (mask, expected) in expected.iter().enumerate() {
694 let assignment = assignment(names, mask);
695 let (branch, diagrams) = verifier.branch_family(&assignment, options.max_classes)?;
696 let rejected = options
697 .discard_if
698 .as_ref()
699 .is_some_and(|condition| eval_expr(condition, &assignment));
700 if rejected != (branch.status() == BranchStatus::Rejected) {
701 return Err(FeedbackInferenceError::RejectionMismatch { assignment });
702 }
703 let mut frame = vec![Pauli::I; verifier.boundaries().outputs().len()];
704 if rejected {
705 frames.push(frame);
706 continue;
707 }
708 let expected_nonzero = expected.iter().any(|value| !value.is_zero());
709 let mut maps = Vec::new();
710 for diagram in diagrams {
711 let prepared = match preparation {
712 Some(preparation) => {
713 let mut prepared = preparation.clone();
714 prepared.plug(&diagram);
715 prepared
716 }
717 None => diagram,
718 };
719 let actual = contract(prepared, options.max_tensor_qubits)?;
720 if actual.iter().all(Zero::is_zero) {
721 continue;
722 }
723 if !expected_nonzero {
724 return Err(FeedbackInferenceError::SupportMismatch { assignment });
725 }
726 maps.push(actual);
727 }
728 if expected_nonzero == maps.is_empty() {
729 return Err(FeedbackInferenceError::SupportMismatch { assignment });
730 }
731 if !maps.is_empty() {
732 let actual = channel(&maps)?;
733 frame = output_pauli(&actual, expected, frame.len())?.ok_or_else(|| {
734 FeedbackInferenceError::NoOutputPauli {
735 assignment: assignment.clone(),
736 }
737 })?;
738 nonzero = true;
739 }
740 frames.push(frame);
741 }
742 if !nonzero {
743 return Err(FeedbackInferenceError::EmptyDomain);
744 }
745 Ok(frames)
746}
747
748pub(super) fn checked_count(
749 bits: usize,
750 limit: usize,
751 phase: &'static str,
752) -> Result<usize, FeedbackInferenceError> {
753 let count = u32::try_from(bits)
754 .ok()
755 .and_then(|bits| 1usize.checked_shl(bits));
756 let observed = count.unwrap_or(usize::MAX);
757 if count.is_none() || observed > limit {
758 Err(FeedbackInferenceError::Limit {
759 phase,
760 observed,
761 limit,
762 })
763 } else {
764 Ok(observed)
765 }
766}
767
768fn contract(mut graph: QuizxGraph, limit: usize) -> Result<Vec<FScalar>, FeedbackInferenceError> {
769 let boundary = graph.inputs().len() + graph.outputs().len();
770 if boundary > limit || boundary >= usize::BITS as usize {
771 return Err(FeedbackInferenceError::Limit {
772 phase: "tensor qubits",
773 observed: boundary,
774 limit,
775 });
776 }
777 quizx::simplify::full_simp(&mut graph);
778 if graph.scalar().is_zero() {
779 return Ok(vec![FScalar::zero(); 1 << boundary]);
780 }
781 graph.x_to_z();
782 let order = graph
785 .inputs()
786 .iter()
787 .copied()
788 .chain(
789 graph
790 .vertices()
791 .filter(|&vertex| graph.vertex_type(vertex) != VType::B),
792 )
793 .chain(graph.outputs().iter().copied())
794 .collect::<Vec<_>>();
795 let mut seen = BTreeMap::new();
796 let mut live = BTreeSet::new();
797 for vertex in order.into_iter().rev() {
798 let width = live.len() + 1;
799 if width > limit {
800 return Err(FeedbackInferenceError::Limit {
801 phase: "intermediate tensor qubits",
802 observed: width,
803 limit,
804 });
805 }
806 live.insert(vertex);
807 let mut degree = 0;
808 for neighbor in graph.neighbors(vertex) {
809 if let Some(previous) = seen.get_mut(&neighbor) {
810 degree += 1;
811 *previous += 1;
812 if graph.vertex_type(neighbor) != VType::B && *previous == graph.degree(neighbor) {
813 live.remove(&neighbor);
814 }
815 }
816 }
817 if graph.vertex_type(vertex) != VType::B && degree == graph.degree(vertex) {
818 live.remove(&vertex);
819 }
820 seen.insert(vertex, degree);
821 }
822 let values = graph.to_tensorf().iter().copied().collect::<Vec<_>>();
823 if values.iter().any(|value| {
824 let value = value.complex_value();
825 !value.re.is_finite() || !value.im.is_finite()
826 }) {
827 return Err(FeedbackInferenceError::NonFiniteTensor);
828 }
829 Ok(values)
830}
831
832fn channel(maps: &[Vec<FScalar>]) -> Result<Vec<FScalar>, FeedbackInferenceError> {
833 let width = maps[0].len();
834 let mut choi = vec![FScalar::zero(); width * width];
835 for map in maps {
838 for (i, a) in map.iter().enumerate().filter(|(_, value)| !value.is_zero()) {
839 for (j, b) in map.iter().enumerate().filter(|(_, value)| !value.is_zero()) {
840 let product = *a * b.conj();
841 if exact_scalar(product)? != exact_product(*a, b.conj())? {
842 return Err(FeedbackInferenceError::ArithmeticLimit);
843 }
844 let entry = &mut choi[i * width + j];
845 let sum = *entry + product;
846 if exact_scalar(sum)?
847 != exact_terms(scalar_terms(*entry).chain(scalar_terms(product)))?
848 {
849 return Err(FeedbackInferenceError::ArithmeticLimit);
850 }
851 *entry = sum;
852 }
853 }
854 }
855 Ok(choi)
856}
857
858fn output_pauli(
860 actual: &[FScalar],
861 expected: &[FScalar],
862 outputs: usize,
863) -> Result<Option<Vec<Pauli>>, FeedbackInferenceError> {
864 if actual.len() != expected.len() {
865 return Ok(None);
866 }
867 let Some(pivot) = expected.iter().position(|value| !value.is_zero()) else {
868 return Ok(None);
869 };
870 let dimension = 1usize << outputs;
871 let half = actual.len().ilog2() as usize / 2;
872 let feature = |index: usize| ((index >> half) ^ index) & (dimension - 1);
873 for x in 0..dimension {
874 let permutation = (x << half) | x;
875 let reference = actual[pivot ^ permutation];
876 if reference.is_zero() {
877 continue;
878 }
879 let mut signs = vec![None; dimension];
880 let mut valid = true;
881 for (index, target) in expected.iter().enumerate() {
882 let value = actual[index ^ permutation];
883 if value.is_zero() || target.is_zero() {
884 if value.is_zero() != target.is_zero() {
885 valid = false;
886 break;
887 }
888 continue;
889 }
890 let lhs = exact_product(*target, reference)?;
891 let rhs = exact_product(value, expected[pivot])?;
892 let flip = if lhs == rhs {
893 false
894 } else if lhs.0 == rhs.0
895 && lhs
896 .1
897 .iter()
898 .zip(rhs.1)
899 .all(|(a, b)| a.checked_add(b) == Some(0))
900 {
901 true
902 } else {
903 valid = false;
904 break;
905 };
906 let delta = feature(index) ^ feature(pivot);
907 if signs[delta]
908 .replace(flip)
909 .is_some_and(|previous| previous != flip)
910 {
911 valid = false;
912 break;
913 }
914 }
915 if !valid {
916 continue;
917 }
918 let equations = signs
919 .into_iter()
920 .enumerate()
921 .filter_map(|(delta, sign)| sign.map(|sign| (delta, sign)))
922 .collect::<Vec<_>>();
923 let mut rhs = CoeffVec::zeros(equations.len());
924 let mut columns = vec![CoeffVec::zeros(equations.len()); outputs];
925 for (row, &(delta, flip)) in equations.iter().enumerate() {
926 rhs.set_bit(row, flip);
927 for (output, column) in columns.iter_mut().enumerate() {
928 column.set_bit(row, delta & (1 << (outputs - 1 - output)) != 0);
929 }
930 }
931 if let Some(z) = solve_coeff_combination(&columns, &rhs) {
932 return Ok(Some(
933 (0..outputs)
934 .map(
935 |output| match (x & (1 << (outputs - 1 - output)) != 0, z.bit(output)) {
936 (false, false) => Pauli::I,
937 (true, false) => Pauli::X,
938 (false, true) => Pauli::Z,
939 (true, true) => Pauli::Y,
940 },
941 )
942 .collect(),
943 ));
944 }
945 }
946 Ok(None)
947}
948
949fn scalar_terms(value: FScalar) -> impl Iterator<Item = (usize, i128, i32)> {
950 value
951 .exact_dyadic_form()
952 .into_iter()
953 .enumerate()
954 .filter(|(_, (coefficient, _))| *coefficient != 0)
955 .map(|(index, (coefficient, power))| (index, i128::from(coefficient), i32::from(power)))
956}
957
958fn exact_scalar(value: FScalar) -> Result<(i32, [i128; 4]), FeedbackInferenceError> {
959 let complex = value.complex_value();
960 if !complex.re.is_finite() || !complex.im.is_finite() {
961 return Err(FeedbackInferenceError::ArithmeticLimit);
962 }
963 exact_terms(scalar_terms(value))
964}
965
966fn exact_product(
967 left: FScalar,
968 right: FScalar,
969) -> Result<(i32, [i128; 4]), FeedbackInferenceError> {
970 exact_scalar(left)?;
971 exact_scalar(right)?;
972 exact_terms(scalar_terms(left).flat_map(|(i, a, power_a)| {
973 scalar_terms(right).map(move |(j, b, power_b)| {
974 (
976 (i + j) % 4,
977 a * b * if i + j >= 4 { -1 } else { 1 },
978 power_a + power_b,
979 )
980 })
981 }))
982}
983
984fn exact_terms(
988 terms: impl Iterator<Item = (usize, i128, i32)>,
989) -> Result<(i32, [i128; 4]), FeedbackInferenceError> {
990 let terms = terms.collect::<Vec<_>>();
991 let Some(mut power) = terms.iter().map(|&(_, _, power)| power).min() else {
992 return Ok((0, [0; 4]));
993 };
994 let mut coefficients = [0_i128; 4];
995 for (index, coefficient, exponent) in terms {
996 let shift =
997 u32::try_from(exponent - power).map_err(|_| FeedbackInferenceError::ArithmeticLimit)?;
998 let factor = 1_i128
999 .checked_shl(shift)
1000 .filter(|factor| *factor > 0)
1001 .ok_or(FeedbackInferenceError::ArithmeticLimit)?;
1002 coefficients[index] = coefficient
1003 .checked_mul(factor)
1004 .and_then(|value| coefficients[index].checked_add(value))
1005 .ok_or(FeedbackInferenceError::ArithmeticLimit)?;
1006 }
1007 let Some(shift) = coefficients
1008 .iter()
1009 .filter(|coefficient| **coefficient != 0)
1010 .map(|coefficient| coefficient.trailing_zeros())
1011 .min()
1012 else {
1013 return Ok((0, [0; 4]));
1014 };
1015 power += shift as i32;
1016 for coefficient in &mut coefficients {
1017 *coefficient >>= shift;
1018 }
1019 Ok((power, coefficients))
1020}
1021
1022fn frame_actions(names: &[String], outputs: &[IVec3], frames: &[Vec<Pauli>]) -> Vec<Action> {
1023 let mut actions = Vec::new();
1024 for (output, &target) in outputs.iter().enumerate() {
1025 for (axis, pauli) in [(Pauli::X, PauliBasis::X), (Pauli::Z, PauliBasis::Z)] {
1026 let mut coefficients = frames
1027 .iter()
1028 .map(|frame| frame[output] & axis)
1029 .collect::<Vec<_>>();
1030 for bit in 0..names.len() {
1031 for mask in 0..coefficients.len() {
1032 if mask & (1 << bit) != 0 {
1033 coefficients[mask] ^= coefficients[mask ^ (1 << bit)];
1034 }
1035 }
1036 }
1037 let constant = coefficients[0];
1038 let terms = coefficients
1039 .iter()
1040 .enumerate()
1041 .skip(1)
1042 .filter(|(_, bit)| **bit)
1043 .map(|(mask, _)| {
1044 join(
1045 crate::BinaryOp::And,
1046 names
1047 .iter()
1048 .enumerate()
1049 .filter(|(bit, _)| mask & (1 << bit) != 0)
1050 .map(|(_, name)| Expr::Var(name.clone()))
1051 .collect(),
1052 )
1053 .expect("nonconstant monomial has variables")
1054 })
1055 .collect();
1056 let expression = join(crate::BinaryOp::Xor, terms);
1057 let condition = match (constant, expression) {
1058 (false, None) => continue,
1059 (true, None) => None,
1060 (false, Some(expr)) => Some(expr),
1061 (true, Some(expr)) => Some(Expr::Not(Box::new(expr))),
1062 };
1063 actions.push(Action::Feedback {
1064 targets: vec![FeedbackTarget {
1065 pauli,
1066 target,
1067 direction: None,
1068 }],
1069 condition,
1070 });
1071 }
1072 }
1073 actions
1074}
1075
1076fn join(op: crate::BinaryOp, mut expressions: Vec<Expr>) -> Option<Expr> {
1077 while expressions.len() > 1 {
1079 let mut iter = expressions.into_iter();
1080 expressions = std::iter::from_fn(|| {
1081 let left = iter.next()?;
1082 Some(match iter.next() {
1083 Some(right) => Expr::Binary(op, Box::new(left), Box::new(right)),
1084 None => left,
1085 })
1086 })
1087 .collect();
1088 }
1089 expressions.pop()
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use quizx::circuit::Circuit;
1096 use quizx::fscalar::One;
1097
1098 #[test]
1099 fn dyadic_scaling_never_turns_distinct_channels_into_a_match() {
1100 let zero = FScalar::zero();
1101 for exponent in [-600, 0, 600] {
1102 let epsilon = FScalar::dyadic(exponent, [1, 0, 0, 0]);
1103 let actual = [epsilon, zero, zero, epsilon * FScalar::real(3.0)];
1104 let different = [epsilon, zero, zero, epsilon * FScalar::real(2.0)];
1105 assert_eq!(output_pauli(&actual, &different, 1).unwrap(), None);
1106 assert_eq!(
1107 output_pauli(&actual, &actual, 1).unwrap(),
1108 Some(vec![Pauli::I])
1109 );
1110 }
1111 let tiny = FScalar::dyadic(-600, [1, 0, 0, 0]);
1112 assert!(matches!(
1113 channel(&[vec![tiny, tiny]]),
1114 Err(FeedbackInferenceError::ArithmeticLimit)
1115 ));
1116 let wide = FScalar::dyadic(500, [1, 0, 0, 0]) + FScalar::dyadic(-500, [0, 1, 0, 0]);
1117 assert!(matches!(
1118 exact_product(wide, wide),
1119 Err(FeedbackInferenceError::ArithmeticLimit)
1120 ));
1121 }
1122
1123 #[test]
1124 fn output_matching_checks_inputs_and_solves_entangled_paulis() {
1125 for outputs in 1..=3 {
1126 let mut circuit = Circuit::new(outputs);
1127 circuit.add_gate("h", vec![0]);
1128 if outputs > 1 {
1129 circuit.add_gate("cx", vec![0, 1]);
1130 }
1131 circuit.add_gate("t", vec![outputs - 1]);
1132 let actual = circuit.to_tensorf().iter().copied().collect::<Vec<_>>();
1133 for mask in 0..1usize << (2 * outputs) {
1134 let mut corrected = circuit.clone();
1135 let mut expected_paulis = Vec::new();
1136 for output in 0..outputs {
1137 let x = mask & (1 << (2 * output)) != 0;
1138 let z = mask & (1 << (2 * output + 1)) != 0;
1139 if x {
1140 corrected.add_gate("x", vec![output]);
1141 }
1142 if z {
1143 corrected.add_gate("z", vec![output]);
1144 }
1145 expected_paulis.push(match (x, z) {
1146 (false, false) => Pauli::I,
1147 (true, false) => Pauli::X,
1148 (false, true) => Pauli::Z,
1149 (true, true) => Pauli::Y,
1150 });
1151 }
1152 let expected = corrected.to_tensorf().iter().copied().collect::<Vec<_>>();
1153 assert_eq!(
1154 output_pauli(
1155 &channel(std::slice::from_ref(&actual)).unwrap(),
1156 &channel(&[expected]).unwrap(),
1157 outputs,
1158 )
1159 .unwrap(),
1160 Some(expected_paulis)
1161 );
1162 }
1163 }
1164 let one = FScalar::one();
1165 let zero = FScalar::zero();
1166 for (actual, expected, outputs, paulis) in [
1167 (
1168 vec![one, zero, zero, zero],
1169 vec![zero, zero, zero, one],
1170 1,
1171 None,
1172 ),
1173 (vec![one, zero], vec![zero, one], 1, Some(vec![Pauli::X])),
1174 (vec![one, one], vec![one, zero - one], 0, None),
1175 ] {
1176 assert_eq!(
1177 output_pauli(
1178 &channel(&[actual]).unwrap(),
1179 &channel(&[expected]).unwrap(),
1180 outputs,
1181 )
1182 .unwrap(),
1183 paulis
1184 );
1185 }
1186 }
1187}