Skip to main content

bloq_graph/zx/
readout.rs

1//! Freeze causal physical readouts from a composed signed correlation space.
2
3use std::collections::{BTreeMap, HashSet};
4
5use glam::IVec3;
6
7use super::stabilizer::SearchBudget;
8use super::{
9    DerivedSurface, NodeKind, RuntimeBasisError, RuntimeStabilizerBasis, Stabilizer,
10    StabilizerError, StabilizerGenerators, ZXGraph,
11};
12use crate::{Action, ModuleCertificationLimits, Pauli};
13
14/// Coordinates carried by a selected readout's coefficient ordinals.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ReadoutCoordinates {
17    /// The stable public generator presentation, including this readout itself.
18    PublicRows,
19    /// Earlier corrected source outcomes, XORed with the physical surface.
20    SourceOutcomes,
21}
22
23/// One frozen named readout, indexed by its already available resolve choices.
24#[derive(Debug, Clone, PartialEq)]
25pub struct ReadoutPlan {
26    /// Interpretation of the surface's coefficient ordinals.
27    pub coordinates: ReadoutCoordinates,
28    /// Resolve sites whose values select this recipe's surface.
29    pub sites: Vec<IVec3>,
30    /// Reachable assignments and their signed physical parity. Interpret the
31    /// surface's readout ordinals according to `coordinates`.
32    // ponytail: O(branches × support) materialized cache; pack signed rows if memory dominates.
33    pub branches: Vec<(Vec<bool>, DerivedSurface)>,
34    /// Certified cut by which every selected physical parity is available.
35    pub deadline: i64,
36}
37
38impl ReadoutPlan {
39    /// Selects the certified surface for one reachable resolve assignment.
40    ///
41    /// # Panics
42    /// Panics if the supplied values are not a jointly reachable assignment.
43    pub fn select(&self, mut value: impl FnMut(IVec3) -> bool) -> &DerivedSurface {
44        let values = self
45            .sites
46            .iter()
47            .map(|&site| value(site))
48            .collect::<Vec<_>>();
49        &self
50            .branches
51            .iter()
52            .find(|(branch, _)| *branch == values)
53            .expect("reachable assignment has a certified readout")
54            .1
55    }
56}
57
58/// Executable named-readout recipe. Fixed rows borrow their existing storage.
59#[derive(Debug, Clone, Copy)]
60pub enum NamedReadout<'a> {
61    /// The authored physical records need no selective choices.
62    Fixed(&'a Stabilizer),
63    /// The selected family owns the physical records and their coordinate map.
64    Guarded(&'a ReadoutPlan),
65}
66
67impl<'a> NamedReadout<'a> {
68    /// Physical surface selected by already available resolve values.
69    pub fn select(self, value: impl FnMut(IVec3) -> bool) -> &'a Stabilizer {
70        match self {
71            Self::Fixed(surface) => surface,
72            Self::Guarded(recipe) => &recipe.select(value).stabilizer,
73        }
74    }
75
76    /// All certified physical surfaces, without expanding predecessor readouts.
77    pub fn surfaces(self) -> impl Iterator<Item = &'a Stabilizer> {
78        let (fixed, branches) = match self {
79            Self::Fixed(surface) => (Some(surface), &[][..]),
80            Self::Guarded(recipe) => (None, recipe.branches.as_slice()),
81        };
82        fixed
83            .into_iter()
84            .chain(branches.iter().map(|(_, surface)| &surface.stabilizer))
85    }
86}
87
88/// Graph analysis may retain output-supported rows; compilation requires C0.
89#[derive(Clone, Copy)]
90pub(crate) enum ReadoutClosure {
91    Required,
92    #[cfg(feature = "verify")]
93    Analysis,
94}
95
96/// Prepare ordinary readouts from their certified causal action cuts.
97pub(crate) fn plan_causal_readouts(
98    generators: &StabilizerGenerators,
99    dag: &crate::ActionDag,
100    limits: ModuleCertificationLimits,
101    closure: ReadoutClosure,
102) -> Result<BTreeMap<String, ReadoutPlan>, RuntimeBasisError> {
103    let limit = limits.max_guarded_domain_size;
104    let mut budget = SearchBudget::with_limits(limits);
105    let pending = generators
106        .generators
107        .iter()
108        .filter_map(|generator| {
109            let name = generator.measurement_name()?;
110            if generator.readout_plan().is_some() {
111                return None;
112            }
113            let sites = dag
114                .resolve_sites_for_measurement(name)
115                .expect("named source action");
116            (!sites.is_empty()).then_some((name, generator, sites))
117        })
118        .collect::<Vec<_>>();
119    if pending.is_empty() {
120        return Ok(BTreeMap::new());
121    }
122    let rank = generators.basis_rank();
123    budget.check_matrix(
124        rank.saturating_mul(3),
125        generators.zx_graph.total_ids(),
126        0,
127        0,
128    )?;
129    let basis = RuntimeStabilizerBasis::from_generators(generators).with_t_nodes_as_ports();
130    pending
131        .into_iter()
132        .map(|(name, generator, sites)| {
133            budget.visit("causal readout search states")?;
134            let deadline = generator
135                .stabilizer
136                .measurement_deadline(&generators.zx_graph)
137                .expect("measurement has support");
138            let domain = dag
139                .resolve_value_domain_bounded_with_limits(&sites, limit, limits.boolean_limits())
140                .map_err(|error| error.into_stabilizer("readout-domain branches", limit))?;
141            let mut branches = Vec::new();
142            for values in domain.values().chunks(64) {
143                budget.check_matrix(
144                    rank.saturating_mul(values.len().saturating_add(3)),
145                    generators.zx_graph.total_ids(),
146                    0,
147                    0,
148                )?;
149                let fills = values
150                    .iter()
151                    .map(|values| selective_fills(&generators.zx_graph, &sites, values))
152                    .collect::<Vec<_>>();
153                for (values, branch) in values
154                    .iter()
155                    .zip(basis.apply_selective_fill_assignments(&fills)?)
156                {
157                    budget.visit("causal readout search states")?;
158                    let surface = match closure {
159                        ReadoutClosure::Required => {
160                            branch.derive_causal_measurement_surface(name, deadline)
161                        }
162                        #[cfg(feature = "verify")]
163                        ReadoutClosure::Analysis => branch
164                            .derive_causal_measurement_surface(name, deadline)
165                            .or_else(|error| match error {
166                                RuntimeBasisError::Stabilizer(
167                                    StabilizerError::UnavailableControlParity { .. },
168                                ) => branch.derive_measurement_surface(name, deadline),
169                                error => Err(error),
170                            }),
171                    }?;
172                    branches.push((values.clone(), surface));
173                }
174            }
175            Ok((
176                name.to_owned(),
177                ReadoutPlan {
178                    coordinates: ReadoutCoordinates::PublicRows,
179                    sites,
180                    branches,
181                    deadline,
182                },
183            ))
184        })
185        .collect()
186}
187
188pub(crate) fn plan_readouts(
189    generators: &StabilizerGenerators,
190    limits: ModuleCertificationLimits,
191) -> Result<BTreeMap<String, ReadoutPlan>, RuntimeBasisError> {
192    let zx = &generators.zx_graph;
193    let mut budget = SearchBudget::with_limits(limits);
194    let rank = generators.basis_rank();
195    budget.check_matrix(
196        rank.saturating_mul(3),
197        zx.total_ids(),
198        rank,
199        generators
200            .generators
201            .len()
202            .saturating_add(zx.nodes().len().saturating_mul(2))
203            .saturating_add(zx.action_graph().ordered_nodes().count()),
204    )?;
205    let dag = zx.action_graph();
206    let final_deadline = zx
207        .nodes()
208        .iter()
209        .map(|node| i64::from(node.pos.z) + 1)
210        .max()
211        .unwrap_or(0);
212    let actions = dag.ordered_nodes().collect::<Vec<_>>();
213    let mut predecessors = vec![Vec::new(); actions.len()];
214    for (from, to, _) in dag
215        .dependencies()
216        .filter(|(_, _, kind)| !kind.is_implicit())
217    {
218        predecessors[to].push(from);
219    }
220    let basis = RuntimeStabilizerBasis::for_source_readouts(generators)?;
221    let mut known = HashSet::new();
222    let mut readouts = BTreeMap::new();
223    loop {
224        budget.visit("composed readout search states")?;
225        loop {
226            let prior = known.len();
227            for node in &actions {
228                if !matches!(node.action, Action::Measure { .. })
229                    && predecessors[node.ordinal]
230                        .iter()
231                        .all(|ordinal| known.contains(ordinal))
232                {
233                    known.insert(node.ordinal);
234                }
235            }
236            if prior == known.len() {
237                break;
238            }
239        }
240        let mut sites = actions
241            .iter()
242            .filter_map(|node| match node.action {
243                Action::Resolve { target, .. } if known.contains(&node.ordinal) => Some(target),
244                _ => None,
245            })
246            .collect::<Vec<_>>();
247        sites.sort_unstable_by_key(IVec3::to_array);
248        let absent = unavailable_feedback(zx, &known);
249        let available = generators
250            .generators
251            .iter()
252            .enumerate()
253            .filter_map(|(index, generator)| {
254                generator
255                    .measurement_name()
256                    .filter(|name| readouts.contains_key(*name))
257                    .map(|_| index)
258            })
259            .collect::<Vec<_>>();
260        let pending = actions
261            .iter()
262            .filter_map(|node| match &node.action {
263                Action::Measure { name, .. } if !known.contains(&node.ordinal) => {
264                    Some((node.ordinal, name))
265                }
266                _ => None,
267            })
268            .collect::<Vec<_>>();
269        if pending.is_empty() {
270            return Ok(readouts);
271        }
272        let baseline = dag
273            .first_resolve_values_with_limits(&sites, limits.boolean_limits())?
274            .expect("valid resolve domain");
275        let baseline = basis.apply_selective_fills(&selective_fills(zx, &sites, &baseline))?;
276        let mut candidates = Vec::new();
277        for (ordinal, name) in &pending {
278            budget.visit("composed readout search states")?;
279            let derive = |basis: &RuntimeStabilizerBasis| {
280                let mut surface =
281                    basis.derive_readout_from_known(name, final_deadline, &absent, &available)?;
282                surface
283                    .readout_ordinals
284                    .retain(|ordinal| available.contains(ordinal));
285                Ok(surface)
286            };
287            let recipe = derive(&baseline).and_then(|surface| {
288                plan_guarded_surface(&basis, &sites, surface, &derive, limits, &mut budget)
289            });
290            match recipe {
291                Ok(recipe) => candidates.push((*ordinal, (*name).clone(), recipe)),
292                Err(RuntimeBasisError::Stabilizer(StabilizerError::UnavailableControlParity {
293                    ..
294                })) => {}
295                Err(error) => return Err(error),
296            }
297        }
298        if candidates.is_empty() {
299            return Err(StabilizerError::UnavailableControlParity {
300                mvar: pending[0].1.clone(),
301                deadline: final_deadline,
302            }
303            .into());
304        }
305        for (ordinal, name, mut recipe) in candidates {
306            for &predecessor in recipe
307                .branches
308                .iter()
309                .flat_map(|(_, branch)| &branch.readout_ordinals)
310            {
311                let name = generators.generators[predecessor]
312                    .measurement_name()
313                    .expect("known named readout");
314                recipe.deadline = recipe.deadline.max(readouts[name].deadline);
315            }
316            readouts.insert(name, recipe);
317            known.insert(ordinal);
318        }
319    }
320}
321
322fn plan_guarded_surface(
323    basis: &RuntimeStabilizerBasis,
324    sites: &[IVec3],
325    initial: DerivedSurface,
326    derive: &impl Fn(&RuntimeStabilizerBasis) -> Result<DerivedSurface, RuntimeBasisError>,
327    limits: ModuleCertificationLimits,
328    budget: &mut SearchBudget,
329) -> Result<ReadoutPlan, RuntimeBasisError> {
330    let limit = limits.max_guarded_domain_size;
331    let zx = basis.zx_graph();
332    if sites.is_empty() && limit != 0 {
333        // The initial solve already certifies the only assignment.
334        return Ok(ReadoutPlan {
335            coordinates: ReadoutCoordinates::SourceOutcomes,
336            sites: Vec::new(),
337            deadline: initial.stabilizer.measurement_deadline(zx).unwrap_or(0),
338            branches: vec![(Vec::new(), initial)],
339        });
340    }
341    let support = |surface: &DerivedSurface| {
342        sites
343            .iter()
344            .copied()
345            .filter(|&pos| {
346                surface
347                    .stabilizer
348                    .paulis
349                    .get(zx.node_at(pos).expect("resolve node").id)
350                    != Pauli::I
351            })
352            .collect::<Vec<_>>()
353    };
354    let mut local = support(&initial);
355    loop {
356        budget.visit("guarded readout search states")?;
357        let domain = zx
358            .action_graph()
359            .resolve_value_domain_bounded_with_limits(&local, limit, limits.boolean_limits())
360            .map_err(|error| error.into_stabilizer("guarded-domain branches", limit))?;
361        let mut branches = Vec::new();
362        let mut missing = None;
363        'assignments: for values in domain.values().chunks(64) {
364            budget.check_matrix(
365                basis
366                    .generators()
367                    .len()
368                    .saturating_mul(values.len().saturating_add(3)),
369                zx.total_ids(),
370                0,
371                0,
372            )?;
373            let fills = values
374                .iter()
375                .map(|values| selective_fills(zx, &local, values))
376                .collect::<Vec<_>>();
377            for (values, branch) in values
378                .iter()
379                .zip(basis.apply_selective_fill_assignments(&fills)?)
380            {
381                budget.visit("guarded readout search states")?;
382                match derive(&branch) {
383                    Ok(surface) => branches.push((values.clone(), surface)),
384                    Err(RuntimeBasisError::Stabilizer(
385                        StabilizerError::UnavailableControlParity { .. },
386                    )) => {
387                        missing = Some(values.clone());
388                        break 'assignments;
389                    }
390                    Err(error) => return Err(error),
391                }
392            }
393        }
394        if let Some(missing) = missing {
395            let fixed = local.iter().copied().zip(missing).collect::<Vec<_>>();
396            let values = zx
397                .action_graph()
398                .complete_resolve_values_with_limits(sites, &fixed, limits.boolean_limits())?
399                .expect("projected assignment has an extension");
400            let branch = basis.apply_selective_fills(&selective_fills(zx, sites, &values))?;
401            let surface = derive(&branch)?;
402            let prior = local.len();
403            local.extend(support(&surface));
404            local.sort_unstable_by_key(IVec3::to_array);
405            local.dedup();
406            if local.len() == prior {
407                local = sites.to_vec();
408            }
409            continue;
410        }
411        // Every omitted selector remained unresolved during each successful
412        // solve, which required identity at that site. This proves that the
413        // same recipe holds for all of its reachable extensions.
414        let (latest, deadline) = branches
415            .iter()
416            .enumerate()
417            .map(|(index, (_, surface))| {
418                (
419                    index,
420                    surface.stabilizer.measurement_deadline(zx).unwrap_or(0),
421                )
422            })
423            .max_by_key(|&(_, deadline)| deadline)
424            .expect("nonempty resolve domain");
425        branches.swap(0, latest);
426        if branches
427            .iter()
428            .all(|(_, surface)| surface == &branches[0].1)
429            && support(&branches[0].1).is_empty()
430        {
431            branches.truncate(1);
432            branches[0].0.clear();
433            local.clear();
434        }
435        return Ok(ReadoutPlan {
436            coordinates: ReadoutCoordinates::SourceOutcomes,
437            sites: local,
438            branches,
439            deadline,
440        });
441    }
442}
443
444fn selective_fills(
445    zx: &ZXGraph,
446    sites: &[IVec3],
447    values: &[bool],
448) -> Vec<(IVec3, crate::PauliBasis)> {
449    sites
450        .iter()
451        .zip(values)
452        .map(|(&pos, &value)| {
453            let NodeKind::Selective(kind) = zx.node_at(pos).expect("resolve node").kind else {
454                unreachable!("resolve sites name selective nodes")
455            };
456            (
457                pos,
458                if value {
459                    kind.pauli_if_true()
460                } else {
461                    kind.pauli_if_false()
462                },
463            )
464        })
465        .collect()
466}
467
468fn unavailable_feedback(zx: &ZXGraph, known: &HashSet<usize>) -> Vec<Vec<(usize, Pauli)>> {
469    zx.action_graph()
470        .ordered_nodes()
471        .filter(|node| !known.contains(&node.ordinal))
472        .filter_map(|action| {
473            let Action::Feedback { targets, .. } = &action.action else {
474                return None;
475            };
476            Some(
477                targets
478                    .iter()
479                    .map(|target| zx.feedback_column(target).expect("validated feedback wire"))
480                    .collect(),
481            )
482        })
483        .collect()
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn readout_matrix_limit_counts_private_completion_rows() {
492        let mut generators = crate::GalleryItem::CNOT
493            .build()
494            .materialize_root_graph()
495            .expect("gallery flat projection")
496            .stabilizers()
497            .unwrap();
498        let private = generators.generators.pop().unwrap().stabilizer.paulis;
499        let generators = StabilizerGenerators::from_parts(
500            generators.zx_graph,
501            generators.generators,
502            vec![private],
503        );
504        plan_readouts(&generators, ModuleCertificationLimits::UNLIMITED).unwrap();
505        // Three public rows fit (147 words), but the complete four-row
506        // relation and its readable coordinates need 196 words.
507        assert!(matches!(
508            plan_readouts(
509                &generators,
510                ModuleCertificationLimits {
511                    max_matrix_words: 160,
512                    ..ModuleCertificationLimits::UNLIMITED
513                }
514            ),
515            Err(RuntimeBasisError::Stabilizer(
516                StabilizerError::ResourceLimited {
517                    phase: "dense matrix words",
518                    observed: 196,
519                    limit: 160,
520                }
521            ))
522        ));
523    }
524
525    #[test]
526    fn unavailable_feedback_uses_the_joint_pauli_action() {
527        let source = crate::BlockGraph::from_blog_text(
528            "BLOG 1.0
529            0: ZXZ [0,0,0]
530            1: ZXZ [0,0,1]
531            2: ZXZ [0,0,2]
532            0 -> +Z
533            1 -> +Z
534            m = measure 1
535            ",
536        )
537        .unwrap();
538        for last in [IVec3::ZERO, 2 * IVec3::Z] {
539            let targets = [IVec3::ZERO, last]
540                .map(|target| crate::FeedbackTarget {
541                    target,
542                    pauli: crate::PauliBasis::X,
543                    direction: None,
544                })
545                .to_vec();
546            let mut graph = source.clone();
547            let mut actions = vec![Action::Feedback {
548                targets: targets.clone(),
549                condition: Some(crate::Expr::Var("m".into())),
550            }];
551            actions.extend(source.actions());
552            graph.set_actions(actions).unwrap();
553            let generators = graph.stabilizers().unwrap();
554            let plans = plan_readouts(&generators, ModuleCertificationLimits::DEFAULT).unwrap();
555            let surface = &plans["m"].branches[0].1.stabilizer;
556            assert!(surface.odd_anticommutes_feedback(&targets[..1], Some(&generators.zx_graph)));
557            assert!(!surface.odd_anticommutes_feedback(&targets, Some(&generators.zx_graph)));
558        }
559    }
560}