Skip to main content

bloq_graph/
gallery.rs

1//! Built-in block-graph and module-program examples.
2
3use strum::{Display, EnumCount, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
4
5use crate::{BlockGraph, ModuleCertificationLimits, ModuleError, parse_inline_graph_with_limits};
6
7/// A tag used to filter [`GalleryItem`] entries.
8#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Display, EnumString)]
9#[strum(ascii_case_insensitive)]
10pub enum GalleryCategory {
11    /// Entries using only Clifford operations.
12    #[strum(to_string = "clifford")]
13    Clifford,
14    /// Entries using non-Clifford (T-based) operations.
15    #[strum(to_string = "non_clifford")]
16    NonClifford,
17    /// Magic-state factory constructions.
18    #[strum(to_string = "factory")]
19    Factory,
20    /// Computations that require a prepared non-Clifford state on open ports.
21    #[strum(to_string = "external_resource")]
22    ExternalResource,
23    /// Computations with runtime-selected measurement branches.
24    #[strum(to_string = "adaptive")]
25    Adaptive,
26    /// Arithmetic constructions and their reusable recipes.
27    #[strum(to_string = "arithmetic")]
28    Arithmetic,
29    /// Addition layouts and their AND, MAJ, and UMA recipes.
30    #[strum(to_string = "addition")]
31    Addition,
32    /// Graph examples outside the compiler's accepted input model.
33    #[strum(to_string = "analysis_only")]
34    AnalysisOnly,
35}
36
37/// A gallery example: its description, categories, and embedded `.blog` source.
38#[derive(Debug, Copy, Clone)]
39pub struct GalleryEntry {
40    pub(crate) description: &'static str,
41    pub(crate) categories: &'static [GalleryCategory],
42    pub(crate) blog: &'static str,
43}
44
45impl GalleryEntry {
46    /// Returns the human-readable description.
47    pub fn description(&self) -> &'static str {
48        self.description
49    }
50
51    /// Returns the categories this entry belongs to.
52    pub fn categories(&self) -> &'static [GalleryCategory] {
53        self.categories
54    }
55
56    /// Returns the embedded `.blog` source text.
57    pub fn blog(&self) -> &'static str {
58        self.blog
59    }
60}
61
62macro_rules! gallery {
63    ($( $(#[$metadata:meta])* $variant:ident => ($description:literal, $categories:expr, $source:literal $(,)?), )*) => {
64        /// A built-in example block graph, identified by a stable string id.
65        ///
66        /// Each entry retains its description, categories, and embedded BLOG source.
67        #[derive(
68            Debug, Copy, Clone, PartialEq, Eq, Hash, Display, EnumCount, EnumIter, EnumString,
69            IntoStaticStr,
70        )]
71        #[repr(usize)]
72        #[strum(ascii_case_insensitive)]
73        pub enum GalleryItem {
74            $(
75                #[doc = $description]
76                $(#[$metadata])*
77                $variant,
78            )*
79        }
80
81        impl GalleryItem {
82            /// Returns the metadata entry for this example.
83            pub fn entry(self) -> &'static GalleryEntry {
84                match self {
85                    $(Self::$variant => &GalleryEntry {
86                        description: $description,
87                        categories: $categories,
88                        blog: include_str!($source),
89                    },)*
90                }
91            }
92        }
93    };
94}
95
96gallery! {
97    #[strum(to_string = "cnot")]
98    CNOT => ("CNOT gate", CLIFFORD, "../assets/cnot.blog"),
99    #[strum(to_string = "cz_spatial_h")]
100    CZSpatialH => ("CZ gate with a spatial Hadamard.", CLIFFORD, "../assets/cz_spatial_h.blog"),
101    #[strum(to_string = "cz_temporal_h")]
102    CZTemporalH => ("CZ gate with a temporal Hadamard.", CLIFFORD, "../assets/cz_temporal_h.blog"),
103    #[strum(to_string = "s_gate")]
104    S => ("S gate teleportation.", CLIFFORD, "../assets/s.blog"),
105    #[strum(to_string = "t_gate")]
106    T => ("T gate teleportation.", NON_CLIFFORD, "../assets/t.blog"),
107    #[strum(to_string = "t_with_prepared_y")]
108    TWithPreparedY => (
109        "T gate teleportation with a prepared Y for Clifford fix.",
110        NON_CLIFFORD,
111        "../assets/t_with_prepared_y.blog",
112    ),
113    #[strum(to_string = "t_comparison")]
114    TComparison => (
115        "Naive T-state comparison with a feed-forward X/Y parity check.",
116        NON_CLIFFORD,
117        "../assets/t_comparison.blog",
118    ),
119    #[strum(to_string = "phase_gradient", serialize = "phase_gradient_k4")]
120    PhaseGradientK4 => (
121        "Fixed single-qubit sequence of 22 signed pi/4 X/Z rotations and a final H, composed from T-injection modules.",
122        NON_CLIFFORD,
123        "../assets/phase_gradient_k4.blog",
124    ),
125    #[strum(to_string = "and_4t")]
126    And4T => (
127        "AND gate (x, y -> x, y, x AND y) using four T injections, multiplex controls, and a spatial output.",
128        NON_CLIFFORD,
129        "../assets/and_4t.blog",
130    ),
131    #[strum(to_string = "ccz_injected_and")]
132    CCZInjectedAnd => (
133        "Compact CCZ-injected temporary AND with multiplex controls and one temporal Hadamard; based on Low et al., Fig. 43(b), arXiv:2605.30455v1. Three open ports accept |CCZ>.",
134        ADDITION_RESOURCE,
135        "../assets/ccz_injected_and.blog",
136    ),
137    #[strum(to_string = "ccz_injected_maj")]
138    CCZInjectedMaj => (
139        "SAT-synthesized CCZ-injected MAJ carry slice from Low et al., Figs. 42(b), 43(c), arXiv:2605.30455v1; three open ports accept |CCZ>.",
140        ADDITION_RESOURCE,
141        "../assets/ccz_injected_maj.blog",
142    ),
143    #[strum(to_string = "uma")]
144    UMA => (
145        "SAT-synthesized UMA carry-uncompute and sum-extraction slice from Low et al., Figs. 42(c), 43(c), and 62, arXiv:2605.30455v1.",
146        &[
147            GalleryCategory::Clifford,
148            GalleryCategory::Adaptive,
149            GalleryCategory::Arithmetic,
150            GalleryCategory::Addition,
151        ],
152        "../assets/uma.blog",
153    ),
154    #[strum(to_string = "three_bit_adder", serialize = "controlled_adder_3")]
155    ThreeBitAdder => (
156        "Three-bit controlled-adder layout with compact routing, five external CCZ states, and adaptive final CZ corrections.",
157        ADDITION_RESOURCE,
158        "../assets/three_bit_adder.blog",
159    ),
160    #[strum(to_string = "ten_bit_adder", serialize = "controlled_adder_10")]
161    TenBitAdder => (
162        "Ten-bit controlled-adder layout with compact routing, nineteen external CCZ states, and adaptive final CZ corrections.",
163        ADDITION_RESOURCE,
164        "../assets/ten_bit_adder.blog",
165    ),
166    #[strum(to_string = "toffoli_from_and_delayed_cz", serialize = "toffoli")]
167    ToffoliFromAndDelayedCZ => (
168        "Toffoli gate (x, y, z -> x, y, z XOR xy) from the four-T AND gadget with a delayed CZ.",
169        NON_CLIFFORD,
170        "../assets/toffoli_from_and_delayed_cz.blog",
171    ),
172    #[strum(to_string = "ccz_gate_teleport")]
173    CCZGateTeleport => (
174        "CCZ gate teleportation with three explicit correction branches.",
175        EXTERNAL_RESOURCE_NON_CLIFFORD,
176        "../assets/ccz_gate_teleport.blog",
177    ),
178    #[strum(to_string = "ccz_4x3x7_tels")]
179    CCZFactoryWithTels => (
180        "CCZ state factory constructed in arXiv:2409.17595.",
181        FACTORY_NON_CLIFFORD,
182        "../assets/ccz_4x3x7_tels.blog",
183    ),
184    #[strum(to_string = "ccz_4x3x6")]
185    CCZ4x3_6 => (
186        "CCZ state factory, 4x3 layout, without the TELS check.",
187        FACTORY_NON_CLIFFORD,
188        "../assets/ccz_4x3x6.blog",
189    ),
190    #[strum(to_string = "bell_state")]
191    BellState => ("Bell state preparation.", CLIFFORD, "../assets/bell_state.blog"),
192    #[strum(to_string = "ghz")]
193    GHZ => ("GHZ state preparation.", CLIFFORD, "../assets/ghz.blog"),
194    #[strum(to_string = "ghz_slide_then_glide")]
195    GHZSlideThenGlide => (
196        "GHZ state preparation with sliding then gliding blocks.",
197        CLIFFORD,
198        "../assets/ghz_slide_then_glide.blog",
199    ),
200    #[strum(to_string = "ghz_patch_rotations")]
201    GHZPatchRotations => (
202        "GHZ state preparation with per-qubit patch rotations.",
203        CLIFFORD,
204        "../assets/ghz_patch_rotations.blog",
205    ),
206    #[strum(to_string = "1d-yoked", serialize = "1d_yoked")]
207    OneDYoked => ("1D yoked lattice-surgery layout.", CLIFFORD, "../assets/1d_yoked.blog"),
208    #[strum(to_string = "thth")]
209    THTH => ("'-T-H-T-H-' gate sequence", NON_CLIFFORD, "../assets/thth.blog"),
210    #[strum(to_string = "three_cnots")]
211    ThreeCNOTs => ("Compressed three CNOTs.", CLIFFORD, "../assets/three_cnots.blog"),
212    #[strum(to_string = "steane_encoding")]
213    SteaneEncoding => (
214        "Compressed Steane encoding circuit.",
215        CLIFFORD,
216        "../assets/steane_encoding.blog",
217    ),
218    #[strum(to_string = "x_memory")]
219    XMemory => ("X-basis memory experiment.", CLIFFORD, "../assets/x_memory.blog"),
220    #[strum(to_string = "y_memory")]
221    YMemory => ("Y-basis memory experiment.", CLIFFORD, "../assets/y_memory.blog"),
222    #[strum(to_string = "move_rotation")]
223    MoveRotation => (
224        "Rotate boundary types by moving in spacetime.",
225        CLIFFORD,
226        "../assets/move_rotation.blog",
227    ),
228    #[strum(to_string = "stability")]
229    Stability => ("Stability experiment", CLIFFORD, "../assets/stability.blog"),
230}
231
232impl GalleryItem {
233    /// Loads the embedded BLOG graph, retaining its module hierarchy.
234    ///
235    /// Every gallery entry is self-contained.
236    ///
237    /// # Panics
238    ///
239    /// Panics if an embedded source fails to load, which would indicate a
240    /// corrupt build asset rather than a caller error.
241    pub fn build(self) -> BlockGraph {
242        self.build_with_limits(ModuleCertificationLimits::DEFAULT)
243            .expect("embedded gallery .blog files are valid")
244    }
245
246    /// Loads the embedded graph with explicit source-analysis and expansion limits.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error when the source cannot be loaded within `limits`.
251    pub fn build_with_limits(
252        self,
253        limits: ModuleCertificationLimits,
254    ) -> Result<BlockGraph, ModuleError> {
255        parse_inline_graph_with_limits(self.entry().blog, limits)
256    }
257
258    /// Iterates over all examples.
259    pub fn iter() -> impl Iterator<Item = GalleryItem> {
260        <Self as IntoEnumIterator>::iter()
261    }
262
263    /// Iterates over the examples tagged with `category`.
264    pub fn iter_by_category(category: GalleryCategory) -> impl Iterator<Item = GalleryItem> {
265        Self::iter().filter(move |entry| entry.in_category(category))
266    }
267
268    /// Returns the stable string id (the variant's canonical name).
269    pub fn id(self) -> &'static str {
270        // The id is the enum's canonical string form (its `strum` `to_string`),
271        // provided by `IntoStaticStr`, so it never duplicates the variant table.
272        self.into()
273    }
274
275    /// Returns the human-readable description.
276    pub fn description(self) -> &'static str {
277        self.entry().description
278    }
279
280    /// Returns the categories this example belongs to.
281    pub fn categories(self) -> &'static [GalleryCategory] {
282        self.entry().categories
283    }
284
285    /// Returns whether this example is tagged with `category`.
286    pub fn in_category(self, category: GalleryCategory) -> bool {
287        self.categories().contains(&category)
288    }
289}
290
291const CLIFFORD: &[GalleryCategory] = &[GalleryCategory::Clifford];
292const NON_CLIFFORD: &[GalleryCategory] = &[GalleryCategory::NonClifford];
293const FACTORY_NON_CLIFFORD: &[GalleryCategory] =
294    &[GalleryCategory::Factory, GalleryCategory::NonClifford];
295const EXTERNAL_RESOURCE_NON_CLIFFORD: &[GalleryCategory] = &[
296    GalleryCategory::ExternalResource,
297    GalleryCategory::NonClifford,
298];
299const ADDITION_RESOURCE: &[GalleryCategory] = &[
300    GalleryCategory::ExternalResource,
301    GalleryCategory::NonClifford,
302    GalleryCategory::Adaptive,
303    GalleryCategory::Arithmetic,
304    GalleryCategory::Addition,
305];
306
307#[cfg(test)]
308mod tests {
309    use std::collections::HashMap;
310
311    use bloq_utils::Pauli;
312
313    use super::*;
314    use crate::{Action, Block, Expr, Pipe};
315    use bloq_utils::Direction;
316    use glam::IVec3;
317
318    const GATE_GADGETS: [GalleryItem; 5] = [
319        GalleryItem::CNOT,
320        GalleryItem::S,
321        GalleryItem::T,
322        GalleryItem::THTH,
323        GalleryItem::ToffoliFromAndDelayedCZ,
324    ];
325
326    #[test]
327    fn test_galleries_validity() {
328        for entry in GalleryItem::iter() {
329            let source = entry.build();
330            let graph = source
331                .materialize_root_graph()
332                .expect("gallery flat projection");
333            let validation = if entry.in_category(GalleryCategory::AnalysisOnly)
334                || !source.instances.is_empty()
335            {
336                graph.validate_source()
337            } else {
338                graph.validate()
339            };
340            validation.unwrap_or_else(|err| {
341                panic!("gallery entry {:?} failed validation: {}", entry.id(), err)
342            });
343        }
344    }
345
346    #[test]
347    fn composed_sources_keep_reusable_module_hierarchies() {
348        let adder = crate::BlockGraph::from_text(bloq_test::ONE_BIT_ADDER_SOURCE).unwrap();
349        assert_eq!(adder.modules().len(), 4);
350        assert_eq!(adder.root().instances.len(), 3);
351        assert_eq!(adder.root().quantum_connections.len(), 18);
352        assert_eq!(
353            adder
354                .direct_pipe_endpoints("main")
355                .expect("adder root exists")
356                .len(),
357            4
358        );
359
360        let phase = GalleryItem::PhaseGradientK4.build();
361        assert_eq!(phase.modules().len(), 5);
362        assert_eq!(phase.root().instances.len(), 22);
363        assert!(phase.module("FinalHadamard").is_none());
364        assert!(phase.root().local_body().has_block_at(IVec3::new(0, 0, 23)));
365        assert!(phase.root().quantum_connections.iter().any(|connection| {
366            matches!(
367                connection,
368                crate::QuantumConnection::Output {
369                    block,
370                    hadamard: true,
371                    ..
372                } if *block == IVec3::new(0, 0, 23)
373            )
374        }));
375        assert_eq!(
376            phase
377                .direct_pipe_endpoints("main")
378                .expect("phase-gradient root exists")
379                .len(),
380            21
381        );
382    }
383
384    #[test]
385    fn addition_gallery_keeps_the_compact_recipe_and_register_interfaces() {
386        for item in [
387            GalleryItem::CCZInjectedAnd,
388            GalleryItem::CCZInjectedMaj,
389            GalleryItem::UMA,
390            GalleryItem::ThreeBitAdder,
391            GalleryItem::TenBitAdder,
392        ] {
393            assert!(item.in_category(GalleryCategory::Arithmetic));
394            assert!(item.in_category(GalleryCategory::Addition));
395        }
396        let and = GalleryItem::CCZInjectedAnd.build();
397        assert_eq!(and.root().local_body().blocks().count(), 38);
398        for (item, bits, blocks) in [
399            (GalleryItem::ThreeBitAdder, 3, 532),
400            (GalleryItem::TenBitAdder, 10, 1932),
401        ] {
402            let program = item.build();
403            assert_eq!(
404                program
405                    .module("InjectedAnd")
406                    .unwrap()
407                    .local_body()
408                    .to_blog_body_text(),
409                and.root().local_body().to_blog_body_text()
410            );
411            assert_eq!(
412                program
413                    .extract_definition("InjectedAnd")
414                    .unwrap()
415                    .to_blog_text(),
416                and.to_blog_text()
417            );
418            assert_eq!(program.root().instances.len(), 3 * bits - 1);
419            assert_eq!(
420                program
421                    .root()
422                    .interface
423                    .quantum_ports
424                    .iter()
425                    .filter(|port| port.resource_type == "ccz")
426                    .count(),
427                3 * (2 * bits - 1)
428            );
429            assert_eq!(
430                program
431                    .root()
432                    .interface
433                    .quantum_ports
434                    .iter()
435                    .filter(|port| port.direction == crate::PortDirection::Output)
436                    .count(),
437                2 * bits + 1
438            );
439            let graph = program.materialize_flat_graph().unwrap();
440            assert_eq!(graph.blocks().count(), blocks);
441            assert_eq!(*graph.spans().unwrap().2.end(), 14);
442        }
443    }
444
445    /// Every port of a composable gallery item admits every Pauli.
446    ///
447    /// A port's Paulis across the generators must span both bits: only then can
448    /// a surface arriving from an adjacent gadget always continue through this
449    /// one, so concatenating gadgets never strands a record.
450    #[test]
451    fn every_composable_gallery_port_admits_every_pauli() {
452        for entry in GalleryItem::iter() {
453            // These entries are resource-consuming tensor effects, not
454            // transparent gate interfaces. Their prepared-resource maps are
455            // checked exhaustively in `verify_gallery`.
456            if entry.in_category(GalleryCategory::AnalysisOnly)
457                || !entry.build().root().instances.is_empty()
458                || matches!(
459                    entry,
460                    GalleryItem::CCZInjectedAnd | GalleryItem::CCZInjectedMaj | GalleryItem::UMA
461                )
462            {
463                continue;
464            }
465            let graph = entry
466                .build()
467                .materialize_root_graph()
468                .expect("gallery flat projection");
469            for projection in graph.branch_projections().unwrap() {
470                let graph = projection.graph();
471                let stabilizers = graph.stabilizers().unwrap();
472
473                for block in graph.blocks().filter(|block| block.kind().is_port()) {
474                    let mut span = Vec::new();
475                    for generator in &stabilizers.generators {
476                        let mut bits = *generator
477                            .stabilizer
478                            .port_stabilizer
479                            .get(&block.pos())
480                            .unwrap_or(&Pauli::I) as u8;
481                        for basis in &span {
482                            bits = bits.min(bits ^ basis);
483                        }
484                        if bits != 0 {
485                            span.push(bits);
486                            span.sort_unstable_by(|lhs, rhs| rhs.cmp(lhs));
487                        }
488                    }
489                    assert_eq!(
490                        span.len(),
491                        2,
492                        "{} {:?} port {} spans only {:?}",
493                        entry.id(),
494                        projection.assignments(),
495                        block.pos(),
496                        span
497                    );
498                }
499            }
500        }
501    }
502
503    /// Every reachable branch of the §7.2 gadgets presents every Pauli tuple
504    /// on both temporal interfaces (Lemma 2's joint transparency).
505    #[test]
506    fn gate_gadget_interfaces_are_jointly_transparent_in_every_branch() {
507        for entry in GATE_GADGETS {
508            let graph = entry
509                .build()
510                .materialize_root_graph()
511                .expect("gallery flat projection");
512            let stabilizers = graph.stabilizers().unwrap();
513            let zx = &stabilizers.zx_graph;
514            let rows = stabilizers
515                .generators
516                .iter()
517                .map(|generator| &generator.stabilizer.paulis)
518                .chain(stabilizers.auxiliary_rows())
519                .collect::<Vec<_>>();
520            let (outputs, inputs): (Vec<usize>, Vec<usize>) = zx
521                .nodes()
522                .iter()
523                .filter(|node| node.kind.is_port())
524                .map(|node| node.id)
525                .partition(|&id| zx.nodes()[id].is_output_port(zx));
526            assert!(
527                !inputs.is_empty() && !outputs.is_empty(),
528                "{} ports",
529                entry.id()
530            );
531
532            let resolves = graph
533                .actions()
534                .iter()
535                .filter_map(|action| match action {
536                    Action::Resolve { target, .. } => {
537                        let node = zx
538                            .nodes()
539                            .iter()
540                            .find(|node| node.pos == *target)
541                            .expect("a resolve targets a node");
542                        let crate::NodeKind::Selective(kind) = node.kind else {
543                            panic!("a resolve targets a selective")
544                        };
545                        Some((node.id, kind, *target))
546                    }
547                    _ => None,
548                })
549                .collect::<Vec<_>>();
550            let targets = resolves
551                .iter()
552                .map(|&(_, _, target)| target)
553                .collect::<Vec<_>>();
554            let domain = graph.action_graph().resolve_value_domain(&targets).unwrap();
555            assert!(!domain.values().is_empty(), "{} has no branch", entry.id());
556            for values in domain.values() {
557                let caps = resolves
558                    .iter()
559                    .zip(values)
560                    .map(|(&(col, kind, _), &value)| {
561                        let basis = if value {
562                            kind.pauli_if_true()
563                        } else {
564                            kind.pauli_if_false()
565                        };
566                        (col, Pauli::from(basis))
567                    })
568                    .collect::<Vec<_>>();
569                for (side, ports) in [("input", &inputs), ("output", &outputs)] {
570                    let rank = admissible_interface_rank(&rows, &caps, ports);
571                    assert_eq!(
572                        rank,
573                        2 * ports.len(),
574                        "{} {side} interface is rank {rank} of {} when the caps read {caps:?}",
575                        entry.id(),
576                        2 * ports.len()
577                    );
578                }
579            }
580        }
581    }
582
583    /// Every gadget record has a canonical surface below its output ports.
584    #[test]
585    fn gate_gadget_records_close_below_their_outputs() {
586        for entry in GATE_GADGETS {
587            let graph = entry
588                .build()
589                .materialize_root_graph()
590                .expect("gallery flat projection");
591            let stabilizers = graph.stabilizers().unwrap();
592            let zx = &stabilizers.zx_graph;
593            for generator in &stabilizers.generators {
594                let Some(name) = generator.measurement_name() else {
595                    continue;
596                };
597                let reached = zx
598                    .nodes()
599                    .iter()
600                    .filter(|node| node.is_output_port(zx))
601                    .filter(|node| generator.stabilizer.paulis.get(node.id) != Pauli::I)
602                    .map(|node| node.pos)
603                    .collect::<Vec<_>>();
604                assert!(
605                    reached.is_empty(),
606                    "{} record {name} reaches output ports {reached:?}",
607                    entry.id()
608                );
609            }
610        }
611    }
612
613    /// Universal-gadget fixing rows do not touch their interfaces.
614    #[test]
615    fn universal_gate_gadget_fixings_close_off_their_ports() {
616        for &entry in &GATE_GADGETS[..4] {
617            let stabilizers = entry
618                .build()
619                .materialize_root_graph()
620                .expect("gallery flat projection")
621                .stabilizers()
622                .unwrap();
623            let zx = &stabilizers.zx_graph;
624            for generator in &stabilizers.generators {
625                if generator.kind.is_selective_fixing() {
626                    assert!(
627                        zx.nodes()
628                            .iter()
629                            .filter(|node| node.kind.is_port())
630                            .all(|node| generator.stabilizer.paulis.get(node.id) == Pauli::I),
631                        "{} fixing reaches a port",
632                        entry.id()
633                    );
634                }
635            }
636        }
637    }
638
639    /// Temporal stacks keep deadlines and add no backward dependency.
640    #[test]
641    fn stacked_gadgets_keep_deadlines_and_forward_dependencies() {
642        let stacks: &[&[GalleryItem]] = &[
643            &[GalleryItem::S, GalleryItem::T],
644            &[GalleryItem::S, GalleryItem::THTH],
645            &[GalleryItem::T, GalleryItem::S],
646            &[GalleryItem::T, GalleryItem::THTH],
647            &[GalleryItem::THTH, GalleryItem::S],
648            &[GalleryItem::THTH, GalleryItem::T],
649            &[GalleryItem::S, GalleryItem::T, GalleryItem::THTH],
650            &[GalleryItem::S, GalleryItem::THTH, GalleryItem::T],
651            &[GalleryItem::T, GalleryItem::S, GalleryItem::THTH],
652            &[GalleryItem::T, GalleryItem::THTH, GalleryItem::S],
653            &[GalleryItem::THTH, GalleryItem::S, GalleryItem::T],
654            &[GalleryItem::THTH, GalleryItem::T, GalleryItem::S],
655        ];
656        for &stack in stacks {
657            let label = stack
658                .iter()
659                .map(|item| item.id().to_string())
660                .collect::<Vec<_>>()
661                .join(" -> ");
662            let mut composite = stack[0]
663                .build()
664                .materialize_root_graph()
665                .expect("gallery flat projection");
666            let mut deadlines = record_deadlines(&composite);
667            let mut stage_of = vec![0usize; composite.actions().len()];
668            for (stage, &item) in stack.iter().enumerate().skip(1) {
669                let upper = item
670                    .build()
671                    .materialize_root_graph()
672                    .expect("gallery flat projection");
673                let shift = stack_offset(&composite, &upper);
674                deadlines.extend(
675                    record_deadlines(&upper)
676                        .into_iter()
677                        .map(|(name, top)| (name, top + shift.z)),
678                );
679                stage_of.extend(std::iter::repeat_n(stage, upper.actions().len()));
680                composite = stack_in_time(composite, upper.shift_positions(shift).unwrap());
681            }
682
683            composite
684                .validate()
685                .unwrap_or_else(|err| panic!("{label}: {err}"));
686            let (composite, stabilizers) = composite
687                .analyze_actions()
688                .unwrap_or_else(|err| panic!("{label}: {err}"));
689            let zx = &stabilizers.zx_graph;
690            for generator in &stabilizers.generators {
691                let Some(name) = generator.measurement_name() else {
692                    continue;
693                };
694                let stabilizer = &generator.stabilizer;
695                assert!(
696                    zx.nodes()
697                        .iter()
698                        .filter(|node| node.is_output_port(zx))
699                        .all(|node| stabilizer.paulis.get(node.id) == Pauli::I),
700                    "{label}: {name} reaches an output port"
701                );
702                assert!(
703                    support_top(stabilizer) <= deadlines[name],
704                    "{label}: {name} decodes at layer {} instead of {}",
705                    support_top(stabilizer),
706                    deadlines[name]
707                );
708            }
709            for (from, to, kind) in composite.action_graph().dependencies() {
710                assert!(
711                    stage_of[from] <= stage_of[to],
712                    "{label}: {kind:?} dependency {from} -> {to} points back in time"
713                );
714            }
715        }
716    }
717
718    /// Alpha-renamed repeated gadgets still satisfy (V1)-(V3).
719    #[test]
720    fn repeated_t_gadgets_compose_after_alpha_renaming() {
721        let lower = GalleryItem::T
722            .build()
723            .materialize_root_graph()
724            .expect("gallery flat projection");
725        let upper = GalleryItem::T
726            .build()
727            .materialize_root_graph()
728            .expect("gallery flat projection");
729        let shift = stack_offset(&lower, &upper);
730        let mut upper = upper.shift_positions(shift).unwrap();
731        upper
732            .set_actions(
733                upper
734                    .actions()
735                    .into_iter()
736                    .map(|action| match action {
737                        Action::Measure { target, .. } => Action::Measure {
738                            target,
739                            name: "mzz_upper".into(),
740                        },
741                        Action::Resolve { target, .. } => Action::Resolve {
742                            target,
743                            condition: Expr::Var("mzz_upper".into()),
744                        },
745                        action => action,
746                    })
747                    .collect(),
748            )
749            .unwrap();
750        let composite = stack_in_time(lower, upper);
751
752        composite.validate().unwrap();
753        composite.analyze_actions().unwrap();
754    }
755
756    /// The highest layer a surface touches — the earliest its parity decodes.
757    fn support_top(stabilizer: &crate::Stabilizer) -> i32 {
758        let nodes = stabilizer
759            .interior_nodes
760            .iter()
761            .filter(|&(_, &pauli)| pauli != Pauli::I)
762            .map(|(pos, _)| pos.z);
763        let edges = stabilizer
764            .interior_edges
765            .iter()
766            .filter(|&(_, &pauli)| pauli != Pauli::I)
767            .flat_map(|(&(from, to), _)| [from.z, to.z]);
768        nodes.chain(edges).max().expect("a surface has support")
769    }
770
771    fn record_deadlines(graph: &BlockGraph) -> HashMap<String, i32> {
772        graph
773            .stabilizers()
774            .unwrap()
775            .generators
776            .iter()
777            .filter_map(|generator| {
778                let name = generator.measurement_name()?;
779                Some((name.to_owned(), support_top(&generator.stabilizer)))
780            })
781            .collect()
782    }
783
784    /// Places `upper`'s lowest port above `lower`'s highest port.
785    fn stack_offset(lower: &BlockGraph, upper: &BlockGraph) -> IVec3 {
786        let lower_out = output_port(lower);
787        let upper_in = upper
788            .blocks()
789            .filter(|block| block.kind().is_port())
790            .min_by_key(|block| block.pos().z)
791            .expect("an input port")
792            .pos();
793        lower_out + IVec3::Z - upper_in
794    }
795
796    fn output_port(graph: &BlockGraph) -> IVec3 {
797        graph
798            .blocks()
799            .filter(|block| block.kind().is_port())
800            .max_by_key(|block| block.pos().z)
801            .expect("an output port")
802            .pos()
803    }
804
805    /// Joins an already-shifted `upper` onto `lower`.
806    fn stack_in_time(mut lower: BlockGraph, upper: BlockGraph) -> BlockGraph {
807        let seam_low = output_port(&lower);
808        let seam_high = seam_low + IVec3::Z;
809        let low_kind = lower
810            .get_block(seam_low - IVec3::Z)
811            .expect("the cube under the output port")
812            .kind();
813        let high_kind = upper
814            .get_block(seam_high + IVec3::Z)
815            .expect("the cube over the input port")
816            .kind();
817
818        // Removing the port takes its pipe with it.
819        lower.remove_block(seam_low);
820        lower.try_add_block(Block::new(seam_low, low_kind)).unwrap();
821        lower
822            .try_add_pipe(Pipe::new(seam_low - IVec3::Z, Direction::ZPLUS))
823            .unwrap();
824        for block in upper.blocks() {
825            let block = if block.pos() == seam_high {
826                Block::new(seam_high, high_kind)
827            } else {
828                block.clone()
829            };
830            lower.try_add_block(block).unwrap();
831        }
832        for pipe in upper.pipes() {
833            lower.try_add_pipe(pipe.clone()).unwrap();
834        }
835        lower
836            .try_add_pipe(Pipe::new(seam_low, Direction::ZPLUS))
837            .unwrap();
838        // One bulk replacement: adding a `measure` before its `resolve`
839        // exists would fail the per-action validation.
840        let mut actions = lower.actions();
841        actions.extend(upper.actions());
842        lower.set_actions(actions).unwrap();
843        lower
844    }
845
846    /// Rank of admissible row combinations projected onto `ports` over GF(2).
847    /// Cap-violation bits lead each vector, so later pivots span the kernel's
848    /// port projection.
849    fn admissible_interface_rank(
850        rows: &[&bloq_utils::PauliString],
851        caps: &[(usize, Pauli)],
852        ports: &[usize],
853    ) -> usize {
854        let anticommutes = |lhs: Pauli, rhs: Pauli| {
855            let (lhs, rhs) = (lhs as u8, rhs as u8);
856            ((lhs & 1) & (rhs >> 1)) ^ ((lhs >> 1) & (rhs & 1)) == 1
857        };
858        assert!(caps.len() + 2 * ports.len() <= u64::BITS as usize);
859
860        let mut basis = [0u64; u64::BITS as usize];
861        for row in rows {
862            let mut vector = 0u64;
863            for (bit, &(col, cap)) in caps.iter().enumerate() {
864                vector |= u64::from(anticommutes(row.get(col), cap)) << bit;
865            }
866            for (index, &col) in ports.iter().enumerate() {
867                vector |= u64::from(row.get(col) as u8) << (caps.len() + 2 * index);
868            }
869            while vector != 0 {
870                let pivot = &mut basis[vector.trailing_zeros() as usize];
871                if *pivot == 0 {
872                    *pivot = vector;
873                    break;
874                }
875                vector ^= *pivot;
876            }
877        }
878        basis[caps.len()..]
879            .iter()
880            .filter(|&&vector| vector != 0)
881            .count()
882    }
883
884    #[test]
885    fn test_flip_xz_basis_revalidates_all_gallery_entries() {
886        for entry in GalleryItem::iter() {
887            let source = entry.build();
888            let graph = source
889                .materialize_root_graph()
890                .expect("gallery flat projection");
891            let source_only =
892                entry.in_category(GalleryCategory::AnalysisOnly) || !source.instances.is_empty();
893            let flipped = if source_only {
894                graph.flip_xz_basis_lenient().unwrap()
895            } else {
896                graph
897                    .flip_xz_basis()
898                    .unwrap_or_else(|err| panic!("{} flip_xz_basis failed: {}", entry.id(), err))
899            };
900            let validation = if source_only {
901                flipped.validate_source()
902            } else {
903                flipped.validate()
904            };
905            validation.unwrap_or_else(|err| {
906                panic!("{} flipped graph became invalid: {}", entry.id(), err)
907            });
908        }
909    }
910
911    #[test]
912    fn test_gallery_ids_roundtrip() {
913        assert_eq!(
914            "one_bit_adder".parse::<GalleryItem>(),
915            Err(strum::ParseError::VariantNotFound)
916        );
917        for entry in GalleryItem::iter() {
918            assert_eq!(
919                entry
920                    .id()
921                    .parse::<GalleryItem>()
922                    .unwrap_or_else(|err| panic!(
923                        "gallery id {:?} failed to roundtrip: {}",
924                        entry.id(),
925                        err
926                    )),
927                entry
928            );
929        }
930    }
931}