1use std::fmt;
2
3use bloq_graph::{
4 Basis, Block, BlockGraph, BlockKind, Direction, UDirection, checked_add_position,
5};
6
7use crate::CompileError;
8use glam::IVec3;
9
10pub(crate) type BlockSignatureMap = crate::FxMap<IVec3, BlockSignature>;
11
12#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
19pub struct Connectivity(u16);
20
21impl Connectivity {
22 pub(crate) const ISOLATED: Self = Self(0);
24
25 pub const fn has_pipe(self, dir: Direction) -> bool {
27 self.0 & (1 << (dir.index() * 2)) != 0
28 }
29
30 pub const fn has_hadamard(self, dir: Direction) -> bool {
33 self.0 & (2 << (dir.index() * 2)) != 0
34 }
35
36 pub(crate) const fn with_pipe(self, dir: Direction) -> Self {
38 Self(self.0 | (1 << (dir.index() * 2)))
39 }
40
41 pub(crate) const fn with_plain_pipe(self, dir: Direction) -> Self {
43 let shift = dir.index() * 2;
44 Self((self.0 & !(0b11 << shift)) | (1 << shift))
45 }
46
47 pub(crate) fn hadamard_wall_axes(self) -> (bool, bool) {
51 let on = |axis: [Direction; 2]| axis.into_iter().any(|dir| self.has_hadamard(dir));
52 (
53 on([Direction::XPLUS, Direction::XMINUS]),
54 on([Direction::YPLUS, Direction::YMINUS]),
55 )
56 }
57
58 pub(crate) const fn with_hadamard(self, dir: Direction) -> Self {
60 Self(self.0 | (0b11 << (dir.index() * 2)))
61 }
62
63 pub fn spatial_pipes(self) -> impl Iterator<Item = Direction> {
65 Direction::iter().filter(move |d| d.is_spatial() && self.has_pipe(*d))
66 }
67
68 pub fn pipe_dirs(self) -> impl Iterator<Item = Direction> {
70 Direction::iter().filter(move |d| self.has_pipe(*d))
71 }
72
73 pub const fn is_isolated(self) -> bool {
75 self.0 == 0
76 }
77
78 pub(crate) const fn has_open_edge(self, dir: Direction) -> bool {
84 self.has_pipe(dir) && !self.has_hadamard(dir)
85 }
86}
87
88impl fmt::Debug for Connectivity {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 if *self == Self::ISOLATED {
91 return write!(f, "Connectivity(ISOLATED)");
92 }
93 write!(f, "Connectivity(")?;
94 let mut first = true;
95 for dir in Direction::iter() {
96 if self.has_pipe(dir) {
97 if !first {
98 write!(f, " ")?;
99 }
100 first = false;
101 if self.has_hadamard(dir) {
102 write!(f, "{dir}H")?;
103 } else {
104 write!(f, "{dir}")?;
105 }
106 }
107 }
108 write!(f, ")")
109 }
110}
111
112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub(crate) enum LayerSchedule {
119 #[default]
121 Compact,
122 Padded,
124 Extended,
128 ExtendedY,
131}
132
133impl LayerSchedule {
134 pub(crate) const fn is_extended(self) -> bool {
135 matches!(self, Self::Extended | Self::ExtendedY)
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub(crate) struct BlockSignature {
145 pub kind: BlockKind,
146 pub rounds: Option<u32>,
156 pub connectivity: Connectivity,
157 pub boundary_basis: Option<Basis>,
162 pub layer_schedule: Option<LayerSchedule>,
165 pub surgery_side: Option<Direction>,
171}
172
173impl BlockSignature {
174 pub(crate) fn temporal_neighbor_boundary_basis(
178 block: &Block,
179 graph: &BlockGraph,
180 ) -> Option<Basis> {
181 let block_pos = block.pos();
182 for dir in [Direction::ZPLUS, Direction::ZMINUS] {
183 let Ok(neighbor_endpoint) = checked_add_position(block_pos, dir.to_ivec3()) else {
184 continue;
185 };
186 let pipe = graph.get_pipe(block_pos, neighbor_endpoint);
187 if let Some(neighbor) = graph.get_endpoint_block(neighbor_endpoint)
188 && let BlockKind::Cube(kind) = neighbor.kind()
189 && let Some(pipe) = pipe
190 {
191 let basis = kind.y();
192 return Some(if pipe.is_hadamard() {
193 basis.flip()
194 } else {
195 basis
196 });
197 }
198 if let Some(pipe) = pipe {
199 return graph.infer_pipe_basis_from_endpoint(pipe, block_pos)[1];
200 }
201 }
202 None
203 }
204
205 pub(crate) fn template_connectivity(block: &Block, graph: &BlockGraph) -> Connectivity {
206 Self::connectivity(block, graph, false)
207 }
208
209 pub(crate) fn graph_connectivity(block: &Block, graph: &BlockGraph) -> Connectivity {
210 Self::connectivity(block, graph, true)
211 }
212
213 fn connectivity(block: &Block, graph: &BlockGraph, preserve_hadamard: bool) -> Connectivity {
214 if let BlockKind::Walking(kind) = block.kind() {
215 return Self::two_layer_endpoint_connectivity(
216 block.pos(),
217 kind.end_position(block.pos()),
218 graph,
219 preserve_hadamard,
220 );
221 }
222 if let BlockKind::PatchRotation(kind) = block.kind() {
223 return Self::two_layer_endpoint_connectivity(
224 block.pos(),
225 kind.end_position(block.pos()),
226 graph,
227 preserve_hadamard,
228 );
229 }
230
231 let mut connectivity = Connectivity::ISOLATED;
232 for dir in Direction::iter() {
233 let endpoint = block.endpoint_for_direction(dir);
234 let Ok(neighbor_pos) = checked_add_position(endpoint, dir.to_ivec3()) else {
235 continue;
236 };
237 if let Some(pipe) = graph.get_pipe(endpoint, neighbor_pos) {
238 connectivity = if pipe.is_hadamard() && (preserve_hadamard || dir.is_spatial()) {
244 connectivity.with_hadamard(dir)
245 } else {
246 connectivity.with_pipe(dir)
247 };
248 }
249 }
250 connectivity
251 }
252
253 fn two_layer_endpoint_connectivity(
254 start: IVec3,
255 end: IVec3,
256 graph: &BlockGraph,
257 preserve_hadamard: bool,
258 ) -> Connectivity {
259 let mut connectivity = Connectivity::ISOLATED;
260 for (endpoint, dir) in [(start, Direction::ZMINUS), (end, Direction::ZPLUS)] {
261 let Ok(neighbor_pos) = checked_add_position(endpoint, dir.to_ivec3()) else {
262 continue;
263 };
264 if let Some(pipe) = graph.get_pipe(endpoint, neighbor_pos) {
265 connectivity = if preserve_hadamard && pipe.is_hadamard() {
266 connectivity.with_hadamard(dir)
267 } else {
268 connectivity.with_pipe(dir)
269 };
270 }
271 }
272 connectivity
273 }
274}
275
276pub(crate) fn cube_rounds(block: &Block, distance: u32) -> Result<u32, CompileError> {
284 let height = block.height();
285 let rounds = height.rounds(distance);
286 match u32::try_from(rounds) {
287 Ok(rounds) if rounds >= 2 => Ok(rounds),
288 _ => Err(CompileError::CubeHeightTooShort {
289 pos: block.pos(),
290 height,
291 distance,
292 rounds,
293 }),
294 }
295}
296
297pub(crate) fn derive_block_signature(
298 graph: &BlockGraph,
299 block: &Block,
300 distance: u32,
301 layer_schedule: LayerSchedule,
302 surgery_side: Option<Direction>,
303) -> Result<BlockSignature, CompileError> {
304 let kind = block.kind();
305 let is_cube = matches!(kind, BlockKind::Cube(_));
306 let boundary_basis = matches!(
307 kind,
308 BlockKind::Y
309 | BlockKind::Measurement(_)
310 | BlockKind::Port
311 | BlockKind::Selective(_)
312 | BlockKind::T
313 )
314 .then(|| BlockSignature::temporal_neighbor_boundary_basis(block, graph))
315 .flatten();
316 Ok(BlockSignature {
317 kind,
318 rounds: is_cube.then(|| cube_rounds(block, distance)).transpose()?,
319 connectivity: BlockSignature::template_connectivity(block, graph),
320 boundary_basis,
321 layer_schedule: is_cube.then_some(layer_schedule),
322 surgery_side,
323 })
324}
325
326pub(crate) fn block_signatures(
327 graph: &BlockGraph,
328 layer_schedules: &LayerScheduleMap,
329 distance: u32,
330) -> Result<BlockSignatureMap, CompileError> {
331 let t_surgery_sides = t_surgery_sides(graph)?;
332
333 graph
334 .blocks()
335 .map(|block| {
336 let signature = derive_block_signature(
337 graph,
338 block,
339 distance,
340 layer_schedule_at(layer_schedules, block.pos()),
341 t_surgery_sides.get(&block.pos()).copied(),
342 )?;
343 Ok((block.pos(), signature))
344 })
345 .collect()
346}
347
348pub(crate) fn relocatable_block_signatures(
353 graph: &BlockGraph,
354 positions: &crate::FxSet<IVec3>,
355 distance: u32,
356) -> Result<BlockSignatureMap, CompileError> {
357 graph
358 .blocks()
359 .filter(|block| positions.contains(&block.pos()))
360 .map(|block| {
361 let kind = block.kind();
362 let mut signature = derive_block_signature(
363 graph,
364 block,
365 distance,
366 LayerSchedule::default(),
367 (kind == BlockKind::T)
368 .then_some(crate::block::fixed_bulk::t::SurgeryLayout::SIDE_PRIORITY[0]),
369 )?;
370 signature.layer_schedule = match kind {
371 BlockKind::Cube(cube) => Some(match signature.connectivity.hadamard_wall_axes() {
372 (true, _) => LayerSchedule::Extended,
373 (false, true) => LayerSchedule::ExtendedY,
374 (false, false) if cube.is_spatial() => LayerSchedule::Padded,
375 (false, false) => LayerSchedule::Compact,
376 }),
377 _ => None,
378 };
379 Ok((block.pos(), signature))
380 })
381 .collect()
382}
383
384fn t_surgery_sides(graph: &BlockGraph) -> Result<crate::FxMap<IVec3, Direction>, CompileError> {
394 let t_positions: Vec<IVec3> = graph
395 .blocks()
396 .filter(|block| block.kind() == BlockKind::T)
397 .map(Block::pos)
398 .collect();
399 assign_linked_t_surgery_sides(t_positions, occupied_branch_positions(graph)?)
400}
401
402pub(crate) fn occupied_branch_positions(
404 graph: &BlockGraph,
405) -> Result<crate::FxSet<IVec3>, CompileError> {
406 let mut occupied = graph.occupied_positions().collect::<crate::FxSet<_>>();
407 for region in graph.branch_regions()? {
408 for block in region.on_false().blocks().chain(region.on_true().blocks()) {
409 occupied.extend(
410 block
411 .reserved_offsets()
412 .into_iter()
413 .map(|offset| block.pos() + offset),
414 );
415 }
416 }
417 Ok(occupied)
418}
419
420pub(crate) fn assign_linked_t_surgery_sides(
421 mut t_positions: Vec<IVec3>,
422 reserved: crate::FxSet<IVec3>,
423) -> Result<crate::FxMap<IVec3, Direction>, CompileError> {
424 if t_positions.is_empty() {
425 return Ok(crate::FxMap::default());
426 }
427 t_positions.sort_by_key(|pos| (pos.z, pos.x, pos.y));
428
429 let candidates = t_positions
430 .iter()
431 .map(|&position| {
432 crate::block::fixed_bulk::t::SurgeryLayout::SIDE_PRIORITY
433 .into_iter()
434 .filter_map(|side| {
435 let cell = checked_add_position(position, side.to_ivec3()).ok()?;
436 (!reserved.contains(&cell)).then_some((cell, side))
437 })
438 .collect::<Vec<_>>()
439 })
440 .collect::<Vec<_>>();
441 let mut owner = crate::FxMap::<IVec3, usize>::default();
442 for (index, &position) in t_positions.iter().enumerate() {
443 if !augment_surgery_matching(index, &candidates, &mut crate::FxSet::default(), &mut owner) {
444 return Err(CompileError::TBlockNeedsFreeNeighbor { pos: position });
445 }
446 }
447 Ok(owner
448 .into_iter()
449 .map(|(cell, index)| {
450 let side = candidates[index]
451 .iter()
452 .find_map(|&(candidate, side)| (candidate == cell).then_some(side))
453 .expect("matched spill cell belongs to its T candidate set");
454 (t_positions[index], side)
455 })
456 .collect())
457}
458
459fn augment_surgery_matching(
460 index: usize,
461 candidates: &[Vec<(IVec3, Direction)>],
462 visited: &mut crate::FxSet<IVec3>,
463 owner: &mut crate::FxMap<IVec3, usize>,
464) -> bool {
465 for &(cell, _) in &candidates[index] {
466 if !visited.insert(cell) {
467 continue;
468 }
469 let previous = owner.get(&cell).copied();
470 if previous
471 .is_some_and(|previous| !augment_surgery_matching(previous, candidates, visited, owner))
472 {
473 continue;
474 }
475 owner.insert(cell, index);
476 return true;
477 }
478 false
479}
480
481pub(crate) type LayerScheduleMap = crate::FxMap<IVec3, LayerSchedule>;
482
483pub(crate) fn layer_schedule_at(schedules: &LayerScheduleMap, pos: IVec3) -> LayerSchedule {
485 schedules.get(&pos).copied().unwrap_or_default()
486}
487
488pub(crate) fn join_component_layer_schedules(
495 graph: &BlockGraph,
496) -> Result<LayerScheduleMap, CompileError> {
497 if !graph
499 .blocks()
500 .any(|block| matches!(block.kind(), BlockKind::Cube(kind) if kind.is_spatial()))
501 && !graph
502 .pipes()
503 .any(|pipe| pipe.is_hadamard() && pipe.dir().is_spatial())
504 {
505 return Ok(LayerScheduleMap::default());
506 }
507 let mut schedules = LayerScheduleMap::default();
508 let mut mixed = Vec::new();
509
510 for (layer, components) in graph.cube_layer_components() {
515 let root: crate::FxMap<IVec3, IVec3> = components
516 .iter()
517 .flat_map(|members| members.iter().map(|&pos| (pos, members[0])))
518 .collect();
519
520 let mut demanded = crate::FxMap::<IVec3, LayerSchedule>::default();
523 let mut demand = |component: IVec3, schedule: LayerSchedule| {
524 let slot = demanded.entry(component).or_default();
525 *slot = (*slot).max(schedule);
526 };
527 for (&pos, &component) in &root {
528 if matches!(
529 graph.get_block(pos).map(Block::kind),
530 Some(BlockKind::Cube(kind)) if kind.is_spatial()
531 ) {
532 demand(component, LayerSchedule::Padded);
533 }
534 }
535
536 let mut wall_axes = crate::FxMap::<IVec3, (bool, bool)>::default();
537 for pipe in graph.layer(layer).spacelike_pipes() {
538 if !pipe.is_hadamard() {
539 continue;
540 }
541 let (src, dst) = pipe.endpoints();
542 let (Some(src), Some(dst)) =
543 (graph.get_endpoint_block(src), graph.get_endpoint_block(dst))
544 else {
545 continue;
546 };
547 let (Some(&component), Some(_)) = (root.get(&src.pos()), root.get(&dst.pos())) else {
548 continue;
549 };
550 let axes = wall_axes.entry(component).or_default();
551 match pipe.dir().as_udirection() {
552 UDirection::X => axes.0 = true,
553 UDirection::Y => axes.1 = true,
554 UDirection::Z => unreachable!("spatial Hadamard walls are not temporal"),
555 }
556 }
557 for (component, (has_x, has_y)) in wall_axes {
558 match (has_x, has_y) {
559 (true, true) => mixed.push(component),
560 (true, false) => demand(component, LayerSchedule::Extended),
561 (false, true) => demand(component, LayerSchedule::ExtendedY),
562 (false, false) => unreachable!("wall axis map is non-empty"),
563 }
564 }
565
566 for (&pos, component) in &root {
567 if let Some(&schedule) = demanded.get(component) {
568 schedules.insert(pos, schedule);
569 }
570 }
571 }
572
573 match mixed.into_iter().min_by_key(glam::IVec3::to_array) {
574 Some(pos) => Err(CompileError::MixedSpatialHadamardUnsupported { pos }),
575 None => Ok(schedules),
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use bloq_graph::{Pipe, WalkingBoundaryKind, WalkingKind};
583 use rstest::rstest;
584
585 use crate::{CompileConfig, CompileContext};
586
587 #[test]
592 fn quarter_turn_spatial_hadamard_is_rejected() {
593 let graph = BlockGraph::from_blog_text(
594 "BLOG 1.0\n\n\
595 0: ZZX [0, 0, 0]\n\
596 1: XZZ [0, 1, 0]\n\
597 2: ZXZ [1, 0, 0]\n\
598 3: Port [0, 1, 1]\n\
599 4: Port [1, 0, 1]\n\
600 [0, 0, 0] -H> +Y\n\
601 [0, 0, 0] -H> +X\n\
602 [0, 1, 0] -> +Z\n\
603 [1, 0, 0] -> +Z\n",
604 )
605 .expect("quarter-turn graph parses");
606 let ctx = CompileContext::new(CompileConfig::new(3));
607
608 assert!(matches!(
609 ctx.compile_and_validate(&graph),
610 Err(CompileError::MixedSpatialHadamardUnsupported { pos }) if pos == IVec3::ZERO
611 ));
612 assert!(matches!(
613 ctx.compile(&graph),
614 Err(CompileError::MixedSpatialHadamardUnsupported { pos }) if pos == IVec3::ZERO
615 ));
616 }
617
618 #[test]
619 fn spatial_cube_pads_its_whole_component() {
620 let mut graph = BlockGraph::new();
621 graph.add_block(Block::new(
622 IVec3::new(0, 0, 0),
623 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
624 ));
625 graph.add_block(Block::new(
626 IVec3::new(1, 0, 0),
627 BlockKind::Cube(bloq_graph::CubeKind::ZZX),
628 ));
629 graph.add_block(Block::new(IVec3::new(2, 0, 0), BlockKind::Y));
630 graph.add_block(Block::new(
631 IVec3::new(1, 1, 0),
632 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
633 ));
634 graph.add_pipe(Pipe::new(IVec3::new(1, 0, 0), Direction::YPLUS));
635
636 let regular = IVec3::new(0, 0, 0);
637 let spatial = IVec3::new(1, 0, 0);
638 let connected_regular = IVec3::new(1, 1, 0);
639 let y = IVec3::new(2, 0, 0);
640
641 let signatures = block_signatures(
644 &graph,
645 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
646 3,
647 )
648 .expect("valid graph");
649
650 assert_eq!(
651 signatures
652 .get(®ular)
653 .expect("regular cube signature")
654 .layer_schedule,
655 Some(LayerSchedule::Compact)
656 );
657 assert_eq!(
658 signatures
659 .get(&spatial)
660 .expect("spatial cube signature")
661 .layer_schedule,
662 Some(LayerSchedule::Padded)
663 );
664 assert_eq!(
665 signatures
666 .get(&connected_regular)
667 .expect("spatial-connected regular cube signature")
668 .layer_schedule,
669 Some(LayerSchedule::Padded)
670 );
671 assert_eq!(
672 signatures
673 .get(&y)
674 .expect("Y block signature")
675 .layer_schedule,
676 None
677 );
678 }
679
680 fn cube(pos: IVec3, kind: bloq_graph::CubeKind) -> Block {
682 Block::new(pos, BlockKind::Cube(kind))
683 }
684
685 #[test]
686 fn spatial_hadamard_extends_its_whole_component() {
687 use bloq_graph::CubeKind::{XZX, ZXZ};
688
689 let mut graph = BlockGraph::new();
690 graph.add_block(cube(IVec3::ZERO, XZX));
692 graph.add_block(cube(IVec3::new(1, 0, 0), XZX));
693 graph.add_block(cube(IVec3::new(1, 1, 0), ZXZ));
694 graph.add_block(cube(IVec3::new(5, 0, 0), ZXZ));
696 graph.add_pipe(Pipe::new(IVec3::new(1, 0, 0), Direction::YPLUS));
697 assert!(join_component_layer_schedules(&graph).unwrap().is_empty());
698 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS).with_hadamard());
699
700 let schedules =
701 join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls");
702 for pos in [IVec3::ZERO, IVec3::new(1, 0, 0), IVec3::new(1, 1, 0)] {
703 assert_eq!(
704 layer_schedule_at(&schedules, pos),
705 LayerSchedule::Extended,
706 "{pos} shares the wall's component"
707 );
708 }
709 assert_eq!(
710 layer_schedule_at(&schedules, IVec3::new(5, 0, 0)),
711 LayerSchedule::Compact
712 );
713 }
714
715 #[test]
716 fn spatial_hadamard_outranks_a_spatial_cube_in_the_same_component() {
717 use bloq_graph::CubeKind::{XXZ, XZX};
718
719 let mut graph = BlockGraph::new();
720 graph.add_block(cube(IVec3::ZERO, XZX));
721 graph.add_block(cube(IVec3::new(1, 0, 0), XXZ));
722 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS).with_hadamard());
723
724 let schedules =
725 join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls");
726 assert_eq!(
727 layer_schedule_at(&schedules, IVec3::new(1, 0, 0)),
728 LayerSchedule::Extended,
729 "the spatial cube's padded demand must not win over the wall's"
730 );
731 assert_eq!(
732 layer_schedule_at(&schedules, IVec3::ZERO),
733 LayerSchedule::Extended
734 );
735 }
736
737 #[rstest]
741 #[case("d/2", 3, 2)]
742 #[case("d/2", 7, 4)]
743 #[case("3d/2", 7, 11)]
744 #[case("3d+2", 7, 23)]
745 #[case("d-1", 7, 6)]
746 fn cube_rounds_resolve_the_height_at_the_code_distance(
747 #[case] height: &str,
748 #[case] distance: u32,
749 #[case] expected: u32,
750 ) {
751 let cube = Block::new(IVec3::ZERO, BlockKind::Cube(bloq_graph::CubeKind::ZXZ))
752 .with_height(height.parse().expect("valid height"))
753 .expect("cube accepts a height");
754
755 assert_eq!(
756 cube_rounds(&cube, distance).expect("height resolves"),
757 expected
758 );
759 }
760
761 #[rstest]
762 #[case("d-2", 3)]
763 #[case("d/2-1", 3)]
764 #[case("d-9", 7)]
765 fn cube_rounds_reject_a_height_below_two_rounds(#[case] height: &str, #[case] distance: u32) {
766 let cube = Block::new(
767 IVec3::new(1, 2, 3),
768 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
769 )
770 .with_height(height.parse().expect("valid height"))
771 .expect("cube accepts a height");
772
773 let error = cube_rounds(&cube, distance).expect_err("too short to compile");
774 let CompileError::CubeHeightTooShort { pos, rounds, .. } = error else {
775 panic!("expected a too-short height error, got {error}");
776 };
777 assert_eq!(pos, IVec3::new(1, 2, 3));
778 assert!(rounds < 2, "{rounds}");
779 }
780
781 #[test]
782 fn tall_cube_signature_checks_top_temporal_endpoint() {
783 let mut graph = BlockGraph::new();
784 graph.add_block(
785 Block::new(
786 IVec3::new(0, 0, 0),
787 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
788 )
789 .with_height("2d".parse().expect("valid height"))
790 .unwrap(),
791 );
792 graph.add_block(Block::new(IVec3::new(0, 0, 2), BlockKind::Port));
793 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 1), Direction::ZPLUS));
794
795 let signature = block_signatures(
796 &graph,
797 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
798 3,
799 )
800 .expect("valid graph")[&IVec3::ZERO];
801
802 assert_eq!(signature.rounds, Some(6));
803 assert!(signature.connectivity.has_pipe(Direction::ZPLUS));
804 }
805
806 #[test]
807 fn tall_spatial_cube_does_not_mark_unconnected_overlapping_layer_cube() {
808 let mut graph = BlockGraph::new();
809 graph.add_block(Block::new(
810 IVec3::new(0, 0, 1),
811 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
812 ));
813 graph.add_block(
814 Block::new(
815 IVec3::new(1, 0, 0),
816 BlockKind::Cube(bloq_graph::CubeKind::ZZX),
817 )
818 .with_height("2d".parse().expect("valid height"))
819 .unwrap(),
820 );
821
822 let signatures = block_signatures(
823 &graph,
824 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
825 3,
826 )
827 .expect("valid graph");
828
829 assert_eq!(
830 signatures[&IVec3::new(0, 0, 1)].layer_schedule,
831 Some(LayerSchedule::Compact)
832 );
833 assert_eq!(
834 signatures[&IVec3::new(1, 0, 0)].layer_schedule,
835 Some(LayerSchedule::Padded)
836 );
837 }
838
839 #[test]
840 fn y_boundary_basis_flips_across_hadamard_edge() {
841 let mut graph = BlockGraph::new();
842 graph.add_block(Block::new(IVec3::new(0, 0, 0), BlockKind::Y));
843 graph.add_block(Block::new(
844 IVec3::new(0, 0, 1),
845 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
846 ));
847 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS).with_hadamard());
848
849 assert_eq!(
850 block_signatures(
851 &graph,
852 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
853 3
854 )
855 .expect("valid graph")[&IVec3::new(0, 0, 0)]
856 .boundary_basis,
857 Some(Basis::Z)
858 );
859 }
860
861 #[test]
862 fn measurement_signature_inherits_boundary_basis_across_hadamard_edge() {
863 let mut graph = BlockGraph::new();
864 graph.add_block(Block::new(
865 IVec3::ZERO,
866 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
867 ));
868 graph.add_block(Block::new(IVec3::Z, BlockKind::Measurement(Basis::X)));
869 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS).with_hadamard());
870
871 let signatures = block_signatures(
872 &graph,
873 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
874 3,
875 )
876 .expect("valid graph");
877 assert_eq!(signatures[&IVec3::Z].boundary_basis, Some(Basis::Z));
878 assert!(
879 signatures[&IVec3::Z]
880 .connectivity
881 .has_pipe(Direction::ZMINUS)
882 );
883 }
884
885 #[test]
886 fn y_boundary_basis_resolves_tall_cube_top_endpoint() {
887 let mut graph = BlockGraph::new();
888 graph.add_block(
889 Block::new(
890 IVec3::new(0, 0, 0),
891 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
892 )
893 .with_height("2d".parse().expect("valid height"))
894 .unwrap(),
895 );
896 graph.add_block(Block::new(IVec3::new(0, 0, 2), BlockKind::Y));
897 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 1), Direction::ZPLUS));
898
899 assert_eq!(
900 block_signatures(
901 &graph,
902 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
903 3
904 )
905 .expect("valid graph")[&IVec3::new(0, 0, 2)]
906 .boundary_basis,
907 Some(Basis::X)
908 );
909 }
910
911 #[test]
912 fn y_boundary_basis_ignores_adjacent_unpiped_cube() {
913 let mut graph = BlockGraph::new();
914 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Y));
915 graph.add_block(Block::new(
916 IVec3::Z,
917 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
918 ));
919
920 assert_eq!(
921 block_signatures(
922 &graph,
923 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
924 3
925 )
926 .expect("valid graph")[&IVec3::ZERO]
927 .boundary_basis,
928 None
929 );
930 }
931
932 #[test]
933 fn y_boundary_basis_falls_back_to_graph_pipe_inference() {
934 let mut graph = BlockGraph::new();
935 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Y));
936 graph.add_block(Block::new(IVec3::Z, BlockKind::Y));
937 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
938
939 assert_eq!(
940 graph.infer_pipe_basis(graph.pipes().next().unwrap())[1],
941 Some(Basis::Z)
942 );
943 assert_eq!(
944 block_signatures(
945 &graph,
946 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
947 3
948 )
949 .expect("valid graph")[&IVec3::ZERO]
950 .boundary_basis,
951 Some(Basis::Z)
952 );
953 }
954
955 #[test]
956 fn walking_connectivity_checks_virtual_end_endpoint() {
957 let mut graph = BlockGraph::new();
958 let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(1, 1)).unwrap();
959 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Walking(walking)));
960 graph.add_block(Block::new(IVec3::new(1, 1, 2), BlockKind::Port));
961 graph.add_pipe(Pipe::new(
962 walking.end_position(IVec3::ZERO),
963 Direction::ZPLUS,
964 ));
965
966 let signature = block_signatures(
967 &graph,
968 &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"),
969 3,
970 )
971 .expect("valid graph")[&IVec3::ZERO];
972
973 assert!(signature.connectivity.has_pipe(Direction::ZPLUS));
974 assert!(!signature.connectivity.has_pipe(Direction::ZMINUS));
975 }
976
977 fn t_graph(pos: IVec3, neighbors: &[IVec3]) -> BlockGraph {
980 let mut graph = BlockGraph::new();
981 graph.add_block(Block::new(pos, BlockKind::T));
982 graph.add_block(Block::new(
983 pos + IVec3::Z,
984 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
985 ));
986 graph.add_pipe(Pipe::new(pos, Direction::ZPLUS));
987 for &neighbor in neighbors {
988 graph.add_block(Block::new(
989 neighbor,
990 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
991 ));
992 }
993 graph
994 }
995
996 fn t_side(graph: &BlockGraph, pos: IVec3) -> Option<Direction> {
997 block_signatures(
998 graph,
999 &join_component_layer_schedules(graph).expect("no mixed spatial Hadamard walls"),
1000 3,
1001 )
1002 .expect("valid graph")[&pos]
1003 .surgery_side
1004 }
1005
1006 #[test]
1007 fn t_surgery_side_picks_first_free_in_priority_order() {
1008 let pos = IVec3::ZERO;
1009 assert_eq!(t_side(&t_graph(pos, &[]), pos), Some(Direction::YMINUS));
1011 assert_eq!(
1013 t_side(&t_graph(pos, &[IVec3::new(0, -1, 0)]), pos),
1014 Some(Direction::XPLUS)
1015 );
1016 assert_eq!(
1018 t_side(
1019 &t_graph(pos, &[IVec3::new(0, -1, 0), IVec3::new(1, 0, 0)]),
1020 pos
1021 ),
1022 Some(Direction::YPLUS)
1023 );
1024 }
1025
1026 #[test]
1027 fn signature_neighbor_steps_do_not_wrap_at_coordinate_limits() {
1028 let pos = IVec3::new(i32::MAX, i32::MIN, 0);
1029 let graph = t_graph(pos, &[]);
1030
1031 assert_eq!(
1032 t_side(&graph, pos),
1033 Some(Direction::YPLUS),
1034 "-Y and +X overflow, so +Y is the first available spill side"
1035 );
1036
1037 let isolated = Block::new(IVec3::new(i32::MAX, 0, 0), BlockKind::Port);
1038 assert!(BlockSignature::template_connectivity(&isolated, &BlockGraph::new()).is_isolated());
1039 }
1040
1041 #[test]
1042 fn t_surgery_side_reservation_is_exclusive_between_t_blocks() {
1043 let mut graph = t_graph(IVec3::ZERO, &[IVec3::new(0, -1, 0), IVec3::new(2, -1, 0)]);
1047 graph.add_block(Block::new(IVec3::new(2, 0, 0), BlockKind::T));
1048 graph.add_block(Block::new(
1049 IVec3::new(2, 0, 1),
1050 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
1051 ));
1052 graph.add_pipe(Pipe::new(IVec3::new(2, 0, 0), Direction::ZPLUS));
1053 for boxed in [
1056 IVec3::new(0, 1, 0),
1057 IVec3::new(-1, 0, 0),
1058 IVec3::new(3, 0, 0),
1059 IVec3::new(2, 1, 0),
1060 ] {
1061 graph.add_block(Block::new(
1062 boxed,
1063 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
1064 ));
1065 }
1066
1067 assert!(matches!(
1068 block_signatures(&graph, &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"), 3).unwrap_err(),
1069 CompileError::TBlockNeedsFreeNeighbor { pos } if pos == IVec3::new(2, 0, 0)
1070 ));
1071 }
1072
1073 #[test]
1074 fn t_surgery_matching_reassigns_for_a_later_blocks_only_option() {
1075 let mut graph = t_graph(IVec3::ZERO, &[IVec3::new(0, -1, 0)]);
1081 graph.add_block(Block::new(IVec3::new(2, 0, 0), BlockKind::T));
1082 graph.add_block(Block::new(
1083 IVec3::new(2, 0, 1),
1084 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
1085 ));
1086 graph.add_pipe(Pipe::new(IVec3::new(2, 0, 0), Direction::ZPLUS));
1087 for boxed in [
1088 IVec3::new(2, -1, 0),
1089 IVec3::new(3, 0, 0),
1090 IVec3::new(2, 1, 0),
1091 ] {
1092 graph.add_block(Block::new(
1093 boxed,
1094 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
1095 ));
1096 }
1097
1098 assert_eq!(t_side(&graph, IVec3::ZERO), Some(Direction::YPLUS));
1099 assert_eq!(t_side(&graph, IVec3::new(2, 0, 0)), Some(Direction::XMINUS));
1100 }
1101
1102 #[test]
1103 fn t_block_boxed_on_all_sides_needs_free_neighbor() {
1104 let pos = IVec3::ZERO;
1105 let graph = t_graph(
1106 pos,
1107 &[
1108 IVec3::new(0, -1, 0),
1109 IVec3::new(1, 0, 0),
1110 IVec3::new(0, 1, 0),
1111 IVec3::new(-1, 0, 0),
1112 ],
1113 );
1114 assert!(matches!(
1115 block_signatures(&graph, &join_component_layer_schedules(&graph).expect("no mixed spatial Hadamard walls"), 3).unwrap_err(),
1116 CompileError::TBlockNeedsFreeNeighbor { pos: p } if p == pos
1117 ));
1118 }
1119
1120 #[test]
1121 fn t_boundary_basis_inherits_and_flips_across_hadamard_edge() {
1122 let plain = t_graph(IVec3::ZERO, &[]);
1123 assert_eq!(
1124 block_signatures(
1125 &plain,
1126 &join_component_layer_schedules(&plain).expect("no mixed spatial Hadamard walls"),
1127 3
1128 )
1129 .expect("valid graph")[&IVec3::ZERO]
1130 .boundary_basis,
1131 Some(Basis::X) );
1133
1134 let mut hadamard = BlockGraph::new();
1135 hadamard.add_block(Block::new(IVec3::ZERO, BlockKind::T));
1136 hadamard.add_block(Block::new(
1137 IVec3::Z,
1138 BlockKind::Cube(bloq_graph::CubeKind::ZXZ),
1139 ));
1140 hadamard.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS).with_hadamard());
1141 assert_eq!(
1142 block_signatures(
1143 &hadamard,
1144 &join_component_layer_schedules(&hadamard)
1145 .expect("no mixed spatial Hadamard walls"),
1146 3
1147 )
1148 .expect("valid graph")[&IVec3::ZERO]
1149 .boundary_basis,
1150 Some(Basis::Z)
1151 );
1152 }
1153}