bloqade_lanes_bytecode_core/
atom_state.rs

1//! Atom state tracking for qubit-to-location mappings.
2//!
3//! [`AtomStateData`] is an immutable state object that tracks where qubits
4//! are located in the architecture as atoms move through transport lanes.
5//! It is the core data structure used by the IR analysis pipeline to simulate
6//! atom movement, detect collisions, and identify CZ gate pairings.
7
8use std::collections::HashMap;
9use std::hash::{Hash, Hasher};
10
11use crate::arch::addr::{LaneAddr, LocationAddr, ZoneAddr};
12use crate::arch::types::ArchSpec;
13
14/// Tracks qubit-to-location mappings as atoms move through the architecture.
15///
16/// This is an immutable value type: all mutation methods (`add_atoms`,
17/// `apply_moves`) return a new instance rather than modifying in place.
18///
19/// The two primary maps (`locations_to_qubit` and `qubit_to_locations`) are
20/// kept in sync as a bidirectional index. When a move causes two atoms to
21/// occupy the same site, both are removed from the location maps and recorded
22/// in `collision`.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct AtomStateData {
25    /// Reverse index: given a physical location, which qubit (if any) is there?
26    pub locations_to_qubit: HashMap<LocationAddr, u32>,
27    /// Forward index: given a qubit id, where is it currently located?
28    pub qubit_to_locations: HashMap<u32, LocationAddr>,
29    /// Cumulative record of qubits that have collided since this state was
30    /// created (via constructors or `add_atoms`). Updated by `apply_moves` —
31    /// new collisions are added to existing entries. Key is the moving qubit,
32    /// value is the qubit it displaced. Collided qubits are removed from
33    /// both location maps.
34    pub collision: HashMap<u32, u32>,
35    /// The lane each qubit used in the most recent `apply_moves`.
36    /// Only populated for qubits that moved in the last step.
37    pub prev_lanes: HashMap<u32, LaneAddr>,
38    /// Cumulative number of moves each qubit has undergone across
39    /// all `apply_moves` calls in the state's history.
40    pub move_count: HashMap<u32, u32>,
41}
42
43impl Hash for AtomStateData {
44    fn hash<H: Hasher>(&self, state: &mut H) {
45        // Hash each field with a discriminant tag and length prefix to prevent
46        // cross-field collisions (e.g. entries from one map aliasing another).
47        fn hash_sorted_map<H: Hasher, K: Ord + Hash, V: Hash>(
48            state: &mut H,
49            tag: u8,
50            entries: &mut [(K, V)],
51        ) {
52            tag.hash(state);
53            entries.len().hash(state);
54            entries.sort_by(|a, b| a.0.cmp(&b.0));
55            for (k, v) in entries.iter() {
56                k.hash(state);
57                v.hash(state);
58            }
59        }
60
61        let mut loc_entries: Vec<_> = self
62            .locations_to_qubit
63            .iter()
64            .map(|(k, v)| (k.encode(), *v))
65            .collect();
66        hash_sorted_map(state, 0, &mut loc_entries);
67
68        let mut qubit_entries: Vec<_> = self
69            .qubit_to_locations
70            .iter()
71            .map(|(k, v)| (*k, v.encode()))
72            .collect();
73        hash_sorted_map(state, 1, &mut qubit_entries);
74
75        let mut collision_entries: Vec<_> = self.collision.iter().map(|(k, v)| (*k, *v)).collect();
76        hash_sorted_map(state, 2, &mut collision_entries);
77
78        let mut lane_entries: Vec<_> = self
79            .prev_lanes
80            .iter()
81            .map(|(k, v)| (*k, v.encode_u64()))
82            .collect();
83        hash_sorted_map(state, 3, &mut lane_entries);
84
85        let mut count_entries: Vec<_> = self.move_count.iter().map(|(k, v)| (*k, *v)).collect();
86        hash_sorted_map(state, 4, &mut count_entries);
87    }
88}
89
90impl AtomStateData {
91    /// Create an empty state with no qubits or locations.
92    pub fn new() -> Self {
93        Self {
94            locations_to_qubit: HashMap::new(),
95            qubit_to_locations: HashMap::new(),
96            collision: HashMap::new(),
97            prev_lanes: HashMap::new(),
98            move_count: HashMap::new(),
99        }
100    }
101
102    /// Create a state from a list of `(qubit_id, location)` pairs.
103    ///
104    /// Builds both the forward (qubit → location) and reverse (location → qubit)
105    /// maps. All other fields (collision, prev_lanes, move_count) are empty.
106    pub fn from_locations(locations: &[(u32, LocationAddr)]) -> Self {
107        let mut locations_to_qubit = HashMap::new();
108        let mut qubit_to_locations = HashMap::new();
109
110        for &(qubit, loc) in locations {
111            qubit_to_locations.insert(qubit, loc);
112            locations_to_qubit.insert(loc, qubit);
113        }
114
115        Self {
116            locations_to_qubit,
117            qubit_to_locations,
118            collision: HashMap::new(),
119            prev_lanes: HashMap::new(),
120            move_count: HashMap::new(),
121        }
122    }
123
124    /// Add atoms at new locations, returning a new state.
125    ///
126    /// Each `(qubit_id, location)` pair is added to the bidirectional maps.
127    /// Returns `Err` if any qubit id already exists in this state or any
128    /// location is already occupied by another qubit.
129    ///
130    /// The returned state inherits no collision, prev_lanes, or move_count
131    /// data — those fields are reset to empty.
132    pub fn add_atoms(&self, locations: &[(u32, LocationAddr)]) -> Result<Self, &'static str> {
133        let mut qubit_to_locations = self.qubit_to_locations.clone();
134        let mut locations_to_qubit = self.locations_to_qubit.clone();
135
136        for &(qubit, loc) in locations {
137            if qubit_to_locations.contains_key(&qubit) {
138                return Err("Attempted to add atom that already exists");
139            }
140            if locations_to_qubit.contains_key(&loc) {
141                return Err("Attempted to add atom to occupied location");
142            }
143            qubit_to_locations.insert(qubit, loc);
144            locations_to_qubit.insert(loc, qubit);
145        }
146
147        Ok(Self {
148            locations_to_qubit,
149            qubit_to_locations,
150            collision: HashMap::new(),
151            prev_lanes: HashMap::new(),
152            move_count: HashMap::new(),
153        })
154    }
155
156    /// Apply a sequence of lane moves and return the resulting state.
157    ///
158    /// For each lane, resolves its source and destination locations via
159    /// [`ArchSpec::lane_endpoints`]. If a qubit exists at the source, it is
160    /// moved to the destination. If the destination is already occupied,
161    /// both qubits are removed from the location maps and recorded in
162    /// `collision`. Lanes whose source has no qubit are skipped.
163    ///
164    /// Returns `None` if any lane cannot be resolved to endpoints (invalid
165    /// bus, word, or site). The `prev_lanes` field is reset to contain only
166    /// the lanes used in this call; `move_count` is accumulated.
167    pub fn apply_moves(&self, lanes: &[LaneAddr], arch_spec: &ArchSpec) -> Option<Self> {
168        let mut qubit_to_locations = self.qubit_to_locations.clone();
169        let mut locations_to_qubit = self.locations_to_qubit.clone();
170        let mut collisions = self.collision.clone();
171        let mut move_count = self.move_count.clone();
172        let mut prev_lanes: HashMap<u32, LaneAddr> = HashMap::new();
173
174        for lane in lanes {
175            let (src, dst) = arch_spec.lane_endpoints(lane)?;
176
177            let qubit = match locations_to_qubit.remove(&src) {
178                Some(q) => q,
179                None => continue,
180            };
181
182            *move_count.entry(qubit).or_insert(0) += 1;
183            prev_lanes.insert(qubit, *lane);
184
185            if let Some(other_qubit) = locations_to_qubit.remove(&dst) {
186                qubit_to_locations.remove(&qubit);
187                qubit_to_locations.remove(&other_qubit);
188                collisions.insert(qubit, other_qubit);
189            } else {
190                qubit_to_locations.insert(qubit, dst);
191                locations_to_qubit.insert(dst, qubit);
192            }
193        }
194
195        Some(Self {
196            locations_to_qubit,
197            qubit_to_locations,
198            prev_lanes,
199            collision: collisions,
200            move_count,
201        })
202    }
203
204    /// Look up which qubit (if any) occupies the given location.
205    pub fn get_qubit(&self, location: &LocationAddr) -> Option<u32> {
206        self.locations_to_qubit.get(location).copied()
207    }
208
209    /// Find CZ gate control/target qubit pairings within a zone.
210    ///
211    /// Iterates over all qubits whose current location is in the given zone
212    /// and checks whether the CZ pair site (via [`ArchSpec::get_blockaded_location`])
213    /// is also occupied. If both sites are occupied, the qubits form a
214    /// control/target pair. If the pair site is empty or doesn't exist, the
215    /// qubit is unpaired.
216    ///
217    /// Returns `(controls, targets, unpaired)` where `controls[i]` and
218    /// `targets[i]` are paired for CZ. Results are sorted by qubit id for
219    /// deterministic ordering. Returns `None` if the zone id is invalid.
220    pub fn get_qubit_pairing(
221        &self,
222        zone: &ZoneAddr,
223        arch_spec: &ArchSpec,
224    ) -> Option<(Vec<u32>, Vec<u32>, Vec<u32>)> {
225        let zone_data = arch_spec.zone_by_id(zone.zone_id)?;
226        let word_ids: std::collections::HashSet<u32> = zone_data.words.iter().copied().collect();
227
228        let mut controls = Vec::new();
229        let mut targets = Vec::new();
230        let mut unpaired = Vec::new();
231        let mut visited = std::collections::HashSet::new();
232
233        // Sort by qubit id for deterministic iteration order
234        let mut sorted_qubits: Vec<_> = self.qubit_to_locations.iter().collect();
235        sorted_qubits.sort_by_key(|(qubit, _)| **qubit);
236
237        for (qubit, loc) in &sorted_qubits {
238            let qubit = **qubit;
239            let loc = **loc;
240            if visited.contains(&qubit) {
241                continue;
242            }
243            visited.insert(qubit);
244
245            if !word_ids.contains(&loc.word_id) {
246                continue;
247            }
248
249            let blockaded = match arch_spec.get_blockaded_location(&loc) {
250                Some(b) => b,
251                None => {
252                    unpaired.push(qubit);
253                    continue;
254                }
255            };
256
257            let target_qubit = match self.get_qubit(&blockaded) {
258                Some(t) => t,
259                None => {
260                    unpaired.push(qubit);
261                    continue;
262                }
263            };
264
265            controls.push(qubit);
266            targets.push(target_qubit);
267            visited.insert(target_qubit);
268        }
269
270        Some((controls, targets, unpaired))
271    }
272}
273
274impl Default for AtomStateData {
275    fn default() -> Self {
276        Self::new()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::arch::example_arch_spec;
284
285    #[test]
286    fn new_state_is_empty() {
287        let state = AtomStateData::new();
288        assert!(state.locations_to_qubit.is_empty());
289        assert!(state.qubit_to_locations.is_empty());
290        assert!(state.collision.is_empty());
291        assert!(state.prev_lanes.is_empty());
292        assert!(state.move_count.is_empty());
293    }
294
295    #[test]
296    fn from_locations_creates_bidirectional_map() {
297        let locs = vec![
298            (
299                0,
300                LocationAddr {
301                    word_id: 0,
302                    site_id: 0,
303                },
304            ),
305            (
306                1,
307                LocationAddr {
308                    word_id: 1,
309                    site_id: 0,
310                },
311            ),
312        ];
313        let state = AtomStateData::from_locations(&locs);
314        assert_eq!(
315            state.get_qubit(&LocationAddr {
316                word_id: 0,
317                site_id: 0
318            }),
319            Some(0)
320        );
321        assert_eq!(
322            state.get_qubit(&LocationAddr {
323                word_id: 1,
324                site_id: 0
325            }),
326            Some(1)
327        );
328    }
329
330    #[test]
331    fn add_atoms_succeeds_and_fields_match() {
332        let state = AtomStateData::new();
333        let loc0 = LocationAddr {
334            word_id: 0,
335            site_id: 0,
336        };
337        let loc1 = LocationAddr {
338            word_id: 1,
339            site_id: 0,
340        };
341        let new_state = state.add_atoms(&[(0, loc0), (1, loc1)]).unwrap();
342
343        assert_eq!(new_state.qubit_to_locations.len(), 2);
344        assert_eq!(new_state.qubit_to_locations[&0], loc0);
345        assert_eq!(new_state.qubit_to_locations[&1], loc1);
346        assert_eq!(new_state.locations_to_qubit[&loc0], 0);
347        assert_eq!(new_state.locations_to_qubit[&loc1], 1);
348        assert!(new_state.collision.is_empty());
349        assert!(new_state.prev_lanes.is_empty());
350        assert!(new_state.move_count.is_empty());
351    }
352
353    #[test]
354    fn add_atoms_duplicate_qubit_fails() {
355        let state = AtomStateData::from_locations(&[(
356            0,
357            LocationAddr {
358                word_id: 0,
359                site_id: 0,
360            },
361        )]);
362        let result = state.add_atoms(&[(
363            0,
364            LocationAddr {
365                word_id: 1,
366                site_id: 0,
367            },
368        )]);
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn add_atoms_occupied_location_fails() {
374        let state = AtomStateData::from_locations(&[(
375            0,
376            LocationAddr {
377                word_id: 0,
378                site_id: 0,
379            },
380        )]);
381        let result = state.add_atoms(&[(
382            1,
383            LocationAddr {
384                word_id: 0,
385                site_id: 0,
386            },
387        )]);
388        assert!(result.is_err());
389    }
390
391    #[test]
392    fn apply_moves_basic() {
393        let spec = example_arch_spec();
394        let state = AtomStateData::from_locations(&[
395            (
396                0,
397                LocationAddr {
398                    word_id: 0,
399                    site_id: 0,
400                },
401            ),
402            (
403                1,
404                LocationAddr {
405                    word_id: 1,
406                    site_id: 0,
407                },
408            ),
409        ]);
410
411        // Site bus 0 moves site 0 -> site 5 (forward)
412        let lane = LaneAddr {
413            direction: crate::arch::addr::Direction::Forward,
414            move_type: crate::arch::addr::MoveType::SiteBus,
415            word_id: 0,
416            site_id: 0,
417            bus_id: 0,
418        };
419
420        let new_state = state.apply_moves(&[lane], &spec).unwrap();
421        assert_eq!(
422            new_state.get_qubit(&LocationAddr {
423                word_id: 0,
424                site_id: 5
425            }),
426            Some(0)
427        );
428        assert_eq!(
429            new_state.get_qubit(&LocationAddr {
430                word_id: 0,
431                site_id: 0
432            }),
433            None
434        );
435        assert_eq!(*new_state.move_count.get(&0).unwrap(), 1);
436    }
437
438    #[test]
439    fn apply_moves_collision() {
440        let spec = example_arch_spec();
441        let state = AtomStateData::from_locations(&[
442            (
443                0,
444                LocationAddr {
445                    word_id: 0,
446                    site_id: 0,
447                },
448            ),
449            (
450                1,
451                LocationAddr {
452                    word_id: 0,
453                    site_id: 5,
454                },
455            ),
456        ]);
457
458        let lane = LaneAddr {
459            direction: crate::arch::addr::Direction::Forward,
460            move_type: crate::arch::addr::MoveType::SiteBus,
461            word_id: 0,
462            site_id: 0,
463            bus_id: 0,
464        };
465
466        let new_state = state.apply_moves(&[lane], &spec).unwrap();
467        assert!(new_state.collision.contains_key(&0));
468        assert_eq!(*new_state.collision.get(&0).unwrap(), 1);
469        assert!(new_state.qubit_to_locations.is_empty());
470    }
471
472    #[test]
473    fn apply_moves_verifies_all_fields() {
474        let spec = example_arch_spec();
475        let loc_0_0 = LocationAddr {
476            word_id: 0,
477            site_id: 0,
478        };
479        let loc_0_5 = LocationAddr {
480            word_id: 0,
481            site_id: 5,
482        };
483        let loc_1_0 = LocationAddr {
484            word_id: 1,
485            site_id: 0,
486        };
487        let state = AtomStateData::from_locations(&[(0, loc_0_0), (1, loc_1_0)]);
488
489        let lane = LaneAddr {
490            direction: crate::arch::addr::Direction::Forward,
491            move_type: crate::arch::addr::MoveType::SiteBus,
492            word_id: 0,
493            site_id: 0,
494            bus_id: 0,
495        };
496
497        let new_state = state.apply_moves(&[lane], &spec).unwrap();
498
499        // Qubit 0 moved from (0,0) to (0,5)
500        assert_eq!(new_state.qubit_to_locations[&0], loc_0_5);
501        assert_eq!(new_state.locations_to_qubit[&loc_0_5], 0);
502        // Qubit 1 didn't move
503        assert_eq!(new_state.qubit_to_locations[&1], loc_1_0);
504        assert_eq!(new_state.locations_to_qubit[&loc_1_0], 1);
505        // Old location is empty
506        assert!(!new_state.locations_to_qubit.contains_key(&loc_0_0));
507        // prev_lanes only has the moved qubit
508        assert_eq!(new_state.prev_lanes.len(), 1);
509        assert_eq!(new_state.prev_lanes[&0], lane);
510        // move_count incremented
511        assert_eq!(new_state.move_count[&0], 1);
512        // No collision
513        assert!(new_state.collision.is_empty());
514    }
515
516    #[test]
517    fn apply_moves_collision_verifies_all_fields() {
518        let spec = example_arch_spec();
519        let state = AtomStateData::from_locations(&[
520            (
521                0,
522                LocationAddr {
523                    word_id: 0,
524                    site_id: 0,
525                },
526            ),
527            (
528                1,
529                LocationAddr {
530                    word_id: 0,
531                    site_id: 5,
532                },
533            ),
534        ]);
535
536        let lane = LaneAddr {
537            direction: crate::arch::addr::Direction::Forward,
538            move_type: crate::arch::addr::MoveType::SiteBus,
539            word_id: 0,
540            site_id: 0,
541            bus_id: 0,
542        };
543
544        let new_state = state.apply_moves(&[lane], &spec).unwrap();
545
546        // Both qubits removed from location maps
547        assert!(new_state.qubit_to_locations.is_empty());
548        assert!(new_state.locations_to_qubit.is_empty());
549        // Collision recorded
550        assert_eq!(new_state.collision[&0], 1);
551        // prev_lanes has the moving qubit's lane
552        assert_eq!(new_state.prev_lanes[&0], lane);
553        // move_count incremented for moving qubit
554        assert_eq!(new_state.move_count[&0], 1);
555    }
556
557    #[test]
558    fn apply_moves_skips_empty_source() {
559        let spec = example_arch_spec();
560        // Only qubit at (1,0), no qubit at (0,0)
561        let state = AtomStateData::from_locations(&[(
562            1,
563            LocationAddr {
564                word_id: 1,
565                site_id: 0,
566            },
567        )]);
568
569        let lane = LaneAddr {
570            direction: crate::arch::addr::Direction::Forward,
571            move_type: crate::arch::addr::MoveType::SiteBus,
572            word_id: 0,
573            site_id: 0,
574            bus_id: 0,
575        };
576
577        let new_state = state.apply_moves(&[lane], &spec).unwrap();
578        // Nothing changed — lane source had no qubit
579        assert_eq!(new_state.qubit_to_locations.len(), 1);
580        assert!(new_state.prev_lanes.is_empty());
581        assert!(new_state.move_count.is_empty());
582    }
583
584    #[test]
585    fn apply_moves_invalid_lane_returns_none() {
586        let spec = example_arch_spec();
587        let state = AtomStateData::from_locations(&[(
588            0,
589            LocationAddr {
590                word_id: 0,
591                site_id: 0,
592            },
593        )]);
594
595        let bad_lane = LaneAddr {
596            direction: crate::arch::addr::Direction::Forward,
597            move_type: crate::arch::addr::MoveType::SiteBus,
598            word_id: 0,
599            site_id: 0,
600            bus_id: 99, // invalid bus
601        };
602
603        assert!(state.apply_moves(&[bad_lane], &spec).is_none());
604    }
605
606    #[test]
607    fn apply_moves_accumulates_move_count() {
608        let spec = example_arch_spec();
609        let state = AtomStateData::from_locations(&[(
610            0,
611            LocationAddr {
612                word_id: 0,
613                site_id: 0,
614            },
615        )]);
616
617        // Move forward: site 0 -> site 5
618        let lane_fwd = LaneAddr {
619            direction: crate::arch::addr::Direction::Forward,
620            move_type: crate::arch::addr::MoveType::SiteBus,
621            word_id: 0,
622            site_id: 0,
623            bus_id: 0,
624        };
625        let state2 = state.apply_moves(&[lane_fwd], &spec).unwrap();
626        assert_eq!(state2.move_count[&0], 1);
627
628        // Move backward: site 5 -> site 0
629        // site_id is always the forward source (0), direction flips endpoints
630        let lane_bwd = LaneAddr {
631            direction: crate::arch::addr::Direction::Backward,
632            move_type: crate::arch::addr::MoveType::SiteBus,
633            word_id: 0,
634            site_id: 0,
635            bus_id: 0,
636        };
637        let state3 = state2.apply_moves(&[lane_bwd], &spec).unwrap();
638        assert_eq!(state3.move_count[&0], 2);
639    }
640
641    #[test]
642    fn get_qubit_empty_location() {
643        let state = AtomStateData::from_locations(&[(
644            0,
645            LocationAddr {
646                word_id: 0,
647                site_id: 0,
648            },
649        )]);
650        assert_eq!(
651            state.get_qubit(&LocationAddr {
652                word_id: 1,
653                site_id: 0
654            }),
655            None
656        );
657    }
658
659    #[test]
660    fn get_qubit_pairing_all_unpaired() {
661        let spec = example_arch_spec();
662        // Place qubits at sites 0, 1, 2 — site 0 pairs with 5, site 1 with 6,
663        // site 2 with 7, but none of the pair sites are occupied.
664        let state = AtomStateData::from_locations(&[
665            (
666                0,
667                LocationAddr {
668                    word_id: 0,
669                    site_id: 0,
670                },
671            ),
672            (
673                1,
674                LocationAddr {
675                    word_id: 0,
676                    site_id: 1,
677                },
678            ),
679            (
680                2,
681                LocationAddr {
682                    word_id: 1,
683                    site_id: 0,
684                },
685            ),
686        ]);
687
688        let zone = ZoneAddr { zone_id: 0 };
689        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
690
691        assert!(controls.is_empty());
692        assert!(targets.is_empty());
693        assert_eq!(unpaired.len(), 3);
694    }
695
696    #[test]
697    fn get_qubit_pairing_with_pairs() {
698        let spec = example_arch_spec();
699        // Site 0 pairs with site 5 in the same word. Place qubits at both.
700        // Also place unpaired qubit at site 1 (pair site 6 is empty).
701        let state = AtomStateData::from_locations(&[
702            (
703                0,
704                LocationAddr {
705                    word_id: 0,
706                    site_id: 0,
707                },
708            ),
709            (
710                1,
711                LocationAddr {
712                    word_id: 0,
713                    site_id: 5,
714                },
715            ),
716            (
717                2,
718                LocationAddr {
719                    word_id: 1,
720                    site_id: 0,
721                },
722            ),
723            (
724                3,
725                LocationAddr {
726                    word_id: 1,
727                    site_id: 5,
728                },
729            ),
730            (
731                4,
732                LocationAddr {
733                    word_id: 0,
734                    site_id: 1,
735                },
736            ),
737        ]);
738
739        let zone = ZoneAddr { zone_id: 0 };
740        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
741
742        // Qubits 0+1 and 2+3 should be paired
743        assert_eq!(controls.len(), 2);
744        assert_eq!(targets.len(), 2);
745        use std::collections::HashSet;
746        let control_set: HashSet<u32> = controls.iter().copied().collect();
747        let target_set: HashSet<u32> = targets.iter().copied().collect();
748        assert_eq!(control_set, HashSet::from([0, 2]));
749        assert_eq!(target_set, HashSet::from([1, 3]));
750        // Qubit 4 is unpaired (site 1, pair site 6 is empty)
751        assert_eq!(unpaired, vec![4]);
752    }
753
754    #[test]
755    fn get_qubit_pairing_invalid_zone() {
756        let spec = example_arch_spec();
757        let state = AtomStateData::new();
758        let zone = ZoneAddr { zone_id: 99 };
759        assert!(state.get_qubit_pairing(&zone, &spec).is_none());
760    }
761
762    #[test]
763    fn get_qubit_pairing_skips_qubits_outside_zone() {
764        let spec = example_arch_spec();
765        // Zone 0 contains words [0, 1]. Place a qubit at word 99 (out of zone).
766        // But word 99 doesn't exist in the spec, so this qubit won't match any zone.
767        let state = AtomStateData::from_locations(&[(
768            0,
769            LocationAddr {
770                word_id: 0,
771                site_id: 0,
772            },
773        )]);
774
775        // Use zone 0 — qubit at (0,0) is in zone but has no CZ pair
776        let zone = ZoneAddr { zone_id: 0 };
777        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
778
779        assert!(controls.is_empty());
780        assert!(targets.is_empty());
781        assert_eq!(unpaired, vec![0]);
782    }
783
784    #[test]
785    fn default_is_empty() {
786        let state = AtomStateData::default();
787        assert!(state.locations_to_qubit.is_empty());
788        assert!(state.qubit_to_locations.is_empty());
789    }
790
791    #[test]
792    fn clone_produces_equal_state() {
793        let state = AtomStateData::from_locations(&[
794            (
795                0,
796                LocationAddr {
797                    word_id: 0,
798                    site_id: 0,
799                },
800            ),
801            (
802                1,
803                LocationAddr {
804                    word_id: 1,
805                    site_id: 0,
806                },
807            ),
808        ]);
809        let cloned = state.clone();
810        assert_eq!(state, cloned);
811    }
812
813    #[test]
814    fn hash_is_deterministic() {
815        use std::collections::hash_map::DefaultHasher;
816
817        let state1 = AtomStateData::from_locations(&[
818            (
819                0,
820                LocationAddr {
821                    word_id: 0,
822                    site_id: 0,
823                },
824            ),
825            (
826                1,
827                LocationAddr {
828                    word_id: 1,
829                    site_id: 0,
830                },
831            ),
832        ]);
833        let state2 = AtomStateData::from_locations(&[
834            (
835                1,
836                LocationAddr {
837                    word_id: 1,
838                    site_id: 0,
839                },
840            ),
841            (
842                0,
843                LocationAddr {
844                    word_id: 0,
845                    site_id: 0,
846                },
847            ),
848        ]);
849
850        let mut h1 = DefaultHasher::new();
851        let mut h2 = DefaultHasher::new();
852        state1.hash(&mut h1);
853        state2.hash(&mut h2);
854        assert_eq!(h1.finish(), h2.finish());
855    }
856}