Skip to main content

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        // In the zone-centric model, all zones share the same words.
226        // Filter qubits by checking if their zone_id matches the requested zone.
227        let _zone_data = arch_spec.zone_by_id(zone.zone_id)?;
228        let zone_id = zone.zone_id;
229
230        let mut controls = Vec::new();
231        let mut targets = Vec::new();
232        let mut unpaired = Vec::new();
233        let mut visited = std::collections::HashSet::new();
234
235        // Sort by qubit id for deterministic iteration order
236        let mut sorted_qubits: Vec<_> = self.qubit_to_locations.iter().collect();
237        sorted_qubits.sort_by_key(|(qubit, _)| **qubit);
238
239        for (qubit, loc) in &sorted_qubits {
240            let qubit = **qubit;
241            let loc = **loc;
242            if visited.contains(&qubit) {
243                continue;
244            }
245            visited.insert(qubit);
246
247            if loc.zone_id != zone_id {
248                continue;
249            }
250
251            let blockaded = match arch_spec.get_cz_partner(&loc) {
252                Some(b) => b,
253                None => {
254                    unpaired.push(qubit);
255                    continue;
256                }
257            };
258
259            let target_qubit = match self.get_qubit(&blockaded) {
260                Some(t) => t,
261                None => {
262                    unpaired.push(qubit);
263                    continue;
264                }
265            };
266
267            controls.push(qubit);
268            targets.push(target_qubit);
269            visited.insert(target_qubit);
270        }
271
272        Some((controls, targets, unpaired))
273    }
274}
275
276impl Default for AtomStateData {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::arch::addr::{SiteRef, WordRef, ZonedWordRef};
286    use crate::arch::types::{Bus, Grid, Mode, Word, Zone};
287    use crate::version::Version;
288
289    /// Build the same two-zone spec used by the arch module tests.
290    /// Zone 0 has site bus 0 (site 0 -> site 1) and word bus 0 (word 0 -> word 1).
291    /// Entangling pair: zones [0, 1].
292    fn make_test_spec() -> crate::arch::ArchSpec {
293        let grid0 = Grid::from_positions(&[0.0, 5.0, 10.0], &[0.0, 3.0]);
294        let grid1 = Grid::from_positions(&[0.0, 7.5, 15.0], &[0.0, 4.0]);
295
296        crate::arch::ArchSpec {
297            version: Version::new(2, 0),
298            words: vec![
299                Word {
300                    sites: vec![[0, 0], [0, 1]],
301                },
302                Word {
303                    sites: vec![[1, 0], [1, 1]],
304                },
305            ],
306            zones: vec![
307                Zone {
308                    name: String::new(),
309                    grid: grid0,
310                    site_buses: vec![Bus {
311                        src: vec![SiteRef(0)],
312                        dst: vec![SiteRef(1)],
313                    }],
314                    word_buses: vec![Bus {
315                        src: vec![WordRef(0)],
316                        dst: vec![WordRef(1)],
317                    }],
318                    words_with_site_buses: vec![0, 1],
319                    sites_with_word_buses: vec![0],
320                    entangling_pairs: vec![[0, 1]],
321                },
322                Zone {
323                    name: String::new(),
324                    grid: grid1,
325                    site_buses: vec![],
326                    word_buses: vec![],
327                    words_with_site_buses: vec![],
328                    sites_with_word_buses: vec![],
329                    entangling_pairs: vec![],
330                },
331            ],
332            zone_buses: vec![Bus {
333                src: vec![ZonedWordRef {
334                    zone_id: 0,
335                    word_id: 0,
336                }],
337                dst: vec![ZonedWordRef {
338                    zone_id: 1,
339                    word_id: 0,
340                }],
341            }],
342            modes: vec![Mode {
343                name: "full".to_string(),
344                zones: vec![0, 1],
345                bitstring_order: vec![],
346            }],
347            paths: None,
348            feed_forward: false,
349            atom_reloading: false,
350            blockade_radius: None,
351        }
352    }
353
354    #[test]
355    fn new_state_is_empty() {
356        let state = AtomStateData::new();
357        assert!(state.locations_to_qubit.is_empty());
358        assert!(state.qubit_to_locations.is_empty());
359        assert!(state.collision.is_empty());
360        assert!(state.prev_lanes.is_empty());
361        assert!(state.move_count.is_empty());
362    }
363
364    #[test]
365    fn from_locations_creates_bidirectional_map() {
366        let locs = vec![
367            (
368                0,
369                LocationAddr {
370                    zone_id: 0,
371                    word_id: 0,
372                    site_id: 0,
373                },
374            ),
375            (
376                1,
377                LocationAddr {
378                    zone_id: 0,
379                    word_id: 1,
380                    site_id: 0,
381                },
382            ),
383        ];
384        let state = AtomStateData::from_locations(&locs);
385        assert_eq!(
386            state.get_qubit(&LocationAddr {
387                zone_id: 0,
388                word_id: 0,
389                site_id: 0
390            }),
391            Some(0)
392        );
393        assert_eq!(
394            state.get_qubit(&LocationAddr {
395                zone_id: 0,
396                word_id: 1,
397                site_id: 0
398            }),
399            Some(1)
400        );
401    }
402
403    #[test]
404    fn add_atoms_succeeds_and_fields_match() {
405        let state = AtomStateData::new();
406        let loc0 = LocationAddr {
407            zone_id: 0,
408            word_id: 0,
409            site_id: 0,
410        };
411        let loc1 = LocationAddr {
412            zone_id: 0,
413            word_id: 1,
414            site_id: 0,
415        };
416        let new_state = state.add_atoms(&[(0, loc0), (1, loc1)]).unwrap();
417
418        assert_eq!(new_state.qubit_to_locations.len(), 2);
419        assert_eq!(new_state.qubit_to_locations[&0], loc0);
420        assert_eq!(new_state.qubit_to_locations[&1], loc1);
421        assert_eq!(new_state.locations_to_qubit[&loc0], 0);
422        assert_eq!(new_state.locations_to_qubit[&loc1], 1);
423        assert!(new_state.collision.is_empty());
424        assert!(new_state.prev_lanes.is_empty());
425        assert!(new_state.move_count.is_empty());
426    }
427
428    #[test]
429    fn add_atoms_duplicate_qubit_fails() {
430        let state = AtomStateData::from_locations(&[(
431            0,
432            LocationAddr {
433                zone_id: 0,
434                word_id: 0,
435                site_id: 0,
436            },
437        )]);
438        let result = state.add_atoms(&[(
439            0,
440            LocationAddr {
441                zone_id: 0,
442                word_id: 1,
443                site_id: 0,
444            },
445        )]);
446        assert!(result.is_err());
447    }
448
449    #[test]
450    fn add_atoms_occupied_location_fails() {
451        let state = AtomStateData::from_locations(&[(
452            0,
453            LocationAddr {
454                zone_id: 0,
455                word_id: 0,
456                site_id: 0,
457            },
458        )]);
459        let result = state.add_atoms(&[(
460            1,
461            LocationAddr {
462                zone_id: 0,
463                word_id: 0,
464                site_id: 0,
465            },
466        )]);
467        assert!(result.is_err());
468    }
469
470    #[test]
471    fn apply_moves_basic() {
472        let spec = make_test_spec();
473        // Zone 0 site bus 0: site 0 -> site 1
474        let state = AtomStateData::from_locations(&[
475            (
476                0,
477                LocationAddr {
478                    zone_id: 0,
479                    word_id: 0,
480                    site_id: 0,
481                },
482            ),
483            (
484                1,
485                LocationAddr {
486                    zone_id: 0,
487                    word_id: 1,
488                    site_id: 0,
489                },
490            ),
491        ]);
492
493        // Site bus 0 moves site 0 -> site 1 (forward) in zone 0
494        let lane = LaneAddr {
495            direction: crate::arch::addr::Direction::Forward,
496            move_type: crate::arch::addr::MoveType::SiteBus,
497            zone_id: 0,
498            word_id: 0,
499            site_id: 0,
500            bus_id: 0,
501        };
502
503        let new_state = state.apply_moves(&[lane], &spec).unwrap();
504        assert_eq!(
505            new_state.get_qubit(&LocationAddr {
506                zone_id: 0,
507                word_id: 0,
508                site_id: 1
509            }),
510            Some(0)
511        );
512        assert_eq!(
513            new_state.get_qubit(&LocationAddr {
514                zone_id: 0,
515                word_id: 0,
516                site_id: 0
517            }),
518            None
519        );
520        assert_eq!(*new_state.move_count.get(&0).unwrap(), 1);
521    }
522
523    #[test]
524    fn apply_moves_collision() {
525        let spec = make_test_spec();
526        // Place qubit 0 at site 0 and qubit 1 at site 1 (the destination of site bus 0)
527        let state = AtomStateData::from_locations(&[
528            (
529                0,
530                LocationAddr {
531                    zone_id: 0,
532                    word_id: 0,
533                    site_id: 0,
534                },
535            ),
536            (
537                1,
538                LocationAddr {
539                    zone_id: 0,
540                    word_id: 0,
541                    site_id: 1,
542                },
543            ),
544        ]);
545
546        let lane = LaneAddr {
547            direction: crate::arch::addr::Direction::Forward,
548            move_type: crate::arch::addr::MoveType::SiteBus,
549            zone_id: 0,
550            word_id: 0,
551            site_id: 0,
552            bus_id: 0,
553        };
554
555        let new_state = state.apply_moves(&[lane], &spec).unwrap();
556        assert!(new_state.collision.contains_key(&0));
557        assert_eq!(*new_state.collision.get(&0).unwrap(), 1);
558        assert!(new_state.qubit_to_locations.is_empty());
559    }
560
561    #[test]
562    fn apply_moves_verifies_all_fields() {
563        let spec = make_test_spec();
564        let loc_0_0 = LocationAddr {
565            zone_id: 0,
566            word_id: 0,
567            site_id: 0,
568        };
569        let loc_0_1 = LocationAddr {
570            zone_id: 0,
571            word_id: 0,
572            site_id: 1,
573        };
574        let loc_1_0 = LocationAddr {
575            zone_id: 0,
576            word_id: 1,
577            site_id: 0,
578        };
579        let state = AtomStateData::from_locations(&[(0, loc_0_0), (1, loc_1_0)]);
580
581        let lane = LaneAddr {
582            direction: crate::arch::addr::Direction::Forward,
583            move_type: crate::arch::addr::MoveType::SiteBus,
584            zone_id: 0,
585            word_id: 0,
586            site_id: 0,
587            bus_id: 0,
588        };
589
590        let new_state = state.apply_moves(&[lane], &spec).unwrap();
591
592        // Qubit 0 moved from (0,0,0) to (0,0,1)
593        assert_eq!(new_state.qubit_to_locations[&0], loc_0_1);
594        assert_eq!(new_state.locations_to_qubit[&loc_0_1], 0);
595        // Qubit 1 didn't move
596        assert_eq!(new_state.qubit_to_locations[&1], loc_1_0);
597        assert_eq!(new_state.locations_to_qubit[&loc_1_0], 1);
598        // Old location is empty
599        assert!(!new_state.locations_to_qubit.contains_key(&loc_0_0));
600        // prev_lanes only has the moved qubit
601        assert_eq!(new_state.prev_lanes.len(), 1);
602        assert_eq!(new_state.prev_lanes[&0], lane);
603        // move_count incremented
604        assert_eq!(new_state.move_count[&0], 1);
605        // No collision
606        assert!(new_state.collision.is_empty());
607    }
608
609    #[test]
610    fn apply_moves_collision_verifies_all_fields() {
611        let spec = make_test_spec();
612        let state = AtomStateData::from_locations(&[
613            (
614                0,
615                LocationAddr {
616                    zone_id: 0,
617                    word_id: 0,
618                    site_id: 0,
619                },
620            ),
621            (
622                1,
623                LocationAddr {
624                    zone_id: 0,
625                    word_id: 0,
626                    site_id: 1,
627                },
628            ),
629        ]);
630
631        let lane = LaneAddr {
632            direction: crate::arch::addr::Direction::Forward,
633            move_type: crate::arch::addr::MoveType::SiteBus,
634            zone_id: 0,
635            word_id: 0,
636            site_id: 0,
637            bus_id: 0,
638        };
639
640        let new_state = state.apply_moves(&[lane], &spec).unwrap();
641
642        // Both qubits removed from location maps
643        assert!(new_state.qubit_to_locations.is_empty());
644        assert!(new_state.locations_to_qubit.is_empty());
645        // Collision recorded
646        assert_eq!(new_state.collision[&0], 1);
647        // prev_lanes has the moving qubit's lane
648        assert_eq!(new_state.prev_lanes[&0], lane);
649        // move_count incremented for moving qubit
650        assert_eq!(new_state.move_count[&0], 1);
651    }
652
653    #[test]
654    fn apply_moves_skips_empty_source() {
655        let spec = make_test_spec();
656        // Only qubit at (0,1,0), no qubit at (0,0,0)
657        let state = AtomStateData::from_locations(&[(
658            1,
659            LocationAddr {
660                zone_id: 0,
661                word_id: 1,
662                site_id: 0,
663            },
664        )]);
665
666        let lane = LaneAddr {
667            direction: crate::arch::addr::Direction::Forward,
668            move_type: crate::arch::addr::MoveType::SiteBus,
669            zone_id: 0,
670            word_id: 0,
671            site_id: 0,
672            bus_id: 0,
673        };
674
675        let new_state = state.apply_moves(&[lane], &spec).unwrap();
676        // Nothing changed — lane source had no qubit
677        assert_eq!(new_state.qubit_to_locations.len(), 1);
678        assert!(new_state.prev_lanes.is_empty());
679        assert!(new_state.move_count.is_empty());
680    }
681
682    #[test]
683    fn apply_moves_invalid_lane_returns_none() {
684        let spec = make_test_spec();
685        let state = AtomStateData::from_locations(&[(
686            0,
687            LocationAddr {
688                zone_id: 0,
689                word_id: 0,
690                site_id: 0,
691            },
692        )]);
693
694        let bad_lane = LaneAddr {
695            direction: crate::arch::addr::Direction::Forward,
696            move_type: crate::arch::addr::MoveType::SiteBus,
697            zone_id: 0,
698            word_id: 0,
699            site_id: 0,
700            bus_id: 99, // invalid bus
701        };
702
703        assert!(state.apply_moves(&[bad_lane], &spec).is_none());
704    }
705
706    #[test]
707    fn apply_moves_accumulates_move_count() {
708        let spec = make_test_spec();
709        let state = AtomStateData::from_locations(&[(
710            0,
711            LocationAddr {
712                zone_id: 0,
713                word_id: 0,
714                site_id: 0,
715            },
716        )]);
717
718        // Move forward: site 0 -> site 1
719        let lane_fwd = LaneAddr {
720            direction: crate::arch::addr::Direction::Forward,
721            move_type: crate::arch::addr::MoveType::SiteBus,
722            zone_id: 0,
723            word_id: 0,
724            site_id: 0,
725            bus_id: 0,
726        };
727        let state2 = state.apply_moves(&[lane_fwd], &spec).unwrap();
728        assert_eq!(state2.move_count[&0], 1);
729
730        // Move backward: site 1 -> site 0
731        // site_id is always the forward source (0), direction flips endpoints
732        let lane_bwd = LaneAddr {
733            direction: crate::arch::addr::Direction::Backward,
734            move_type: crate::arch::addr::MoveType::SiteBus,
735            zone_id: 0,
736            word_id: 0,
737            site_id: 0,
738            bus_id: 0,
739        };
740        let state3 = state2.apply_moves(&[lane_bwd], &spec).unwrap();
741        assert_eq!(state3.move_count[&0], 2);
742    }
743
744    #[test]
745    fn get_qubit_empty_location() {
746        let state = AtomStateData::from_locations(&[(
747            0,
748            LocationAddr {
749                zone_id: 0,
750                word_id: 0,
751                site_id: 0,
752            },
753        )]);
754        assert_eq!(
755            state.get_qubit(&LocationAddr {
756                zone_id: 0,
757                word_id: 1,
758                site_id: 0
759            }),
760            None
761        );
762    }
763
764    #[test]
765    fn get_qubit_pairing_all_unpaired() {
766        let spec = make_test_spec();
767        // Zone 0 entangling_pairs: [[0, 1]] — word 0 paired with word 1.
768        // Place both qubits in word 0 only — no qubit in word 1, so all unpaired.
769        let state = AtomStateData::from_locations(&[
770            (
771                0,
772                LocationAddr {
773                    zone_id: 0,
774                    word_id: 0,
775                    site_id: 0,
776                },
777            ),
778            (
779                1,
780                LocationAddr {
781                    zone_id: 0,
782                    word_id: 0,
783                    site_id: 1,
784                },
785            ),
786        ]);
787
788        let zone = ZoneAddr { zone_id: 0 };
789        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
790
791        assert!(controls.is_empty());
792        assert!(targets.is_empty());
793        assert_eq!(unpaired.len(), 2);
794    }
795
796    #[test]
797    fn get_qubit_pairing_with_pairs() {
798        let spec = make_test_spec();
799        // Zone 0 entangling_pairs: [[0, 1]] — word 0 paired with word 1.
800        // Place qubit 0 at (zone 0, word 0, site 0) and qubit 1 at (zone 0, word 1, site 0)
801        // -> paired (same zone, partner words, same site).
802        // Place qubit 2 at (zone 0, word 0, site 1) without partner at (zone 0, word 1, site 1)
803        // -> unpaired.
804        let state = AtomStateData::from_locations(&[
805            (
806                0,
807                LocationAddr {
808                    zone_id: 0,
809                    word_id: 0,
810                    site_id: 0,
811                },
812            ),
813            (
814                1,
815                LocationAddr {
816                    zone_id: 0,
817                    word_id: 1,
818                    site_id: 0,
819                },
820            ),
821            (
822                2,
823                LocationAddr {
824                    zone_id: 0,
825                    word_id: 0,
826                    site_id: 1,
827                },
828            ),
829        ]);
830
831        let zone = ZoneAddr { zone_id: 0 };
832        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
833
834        // Qubits 0 and 1 should be paired (word 0 and word 1 at site 0 in zone 0)
835        assert_eq!(controls.len(), 1);
836        assert_eq!(targets.len(), 1);
837        use std::collections::HashSet;
838        let control_set: HashSet<u32> = controls.iter().copied().collect();
839        let target_set: HashSet<u32> = targets.iter().copied().collect();
840        assert_eq!(control_set, HashSet::from([0]));
841        assert_eq!(target_set, HashSet::from([1]));
842        // Qubit 2 is unpaired (zone 0 word 0 site 1, partner word 1 site 1 is empty)
843        assert_eq!(unpaired, vec![2]);
844    }
845
846    #[test]
847    fn get_qubit_pairing_invalid_zone() {
848        let spec = make_test_spec();
849        let state = AtomStateData::new();
850        let zone = ZoneAddr { zone_id: 99 };
851        assert!(state.get_qubit_pairing(&zone, &spec).is_none());
852    }
853
854    #[test]
855    fn get_qubit_pairing_skips_qubits_outside_zone() {
856        let spec = make_test_spec();
857        // Zone 0 entangling_pairs: [[0, 1]] — word 0 paired with word 1.
858        // Place a qubit only at word 0 — partner word 1 has no qubit.
859        let state = AtomStateData::from_locations(&[(
860            0,
861            LocationAddr {
862                zone_id: 0,
863                word_id: 0,
864                site_id: 0,
865            },
866        )]);
867
868        // Use zone 0 — qubit at (0,0,0), partner at (0,1,0) is empty
869        let zone = ZoneAddr { zone_id: 0 };
870        let (controls, targets, unpaired) = state.get_qubit_pairing(&zone, &spec).unwrap();
871
872        assert!(controls.is_empty());
873        assert!(targets.is_empty());
874        assert_eq!(unpaired, vec![0]);
875    }
876
877    #[test]
878    fn default_is_empty() {
879        let state = AtomStateData::default();
880        assert!(state.locations_to_qubit.is_empty());
881        assert!(state.qubit_to_locations.is_empty());
882    }
883
884    #[test]
885    fn clone_produces_equal_state() {
886        let state = AtomStateData::from_locations(&[
887            (
888                0,
889                LocationAddr {
890                    zone_id: 0,
891                    word_id: 0,
892                    site_id: 0,
893                },
894            ),
895            (
896                1,
897                LocationAddr {
898                    zone_id: 0,
899                    word_id: 1,
900                    site_id: 0,
901                },
902            ),
903        ]);
904        let cloned = state.clone();
905        assert_eq!(state, cloned);
906    }
907
908    #[test]
909    fn hash_is_deterministic() {
910        use std::collections::hash_map::DefaultHasher;
911
912        let state1 = AtomStateData::from_locations(&[
913            (
914                0,
915                LocationAddr {
916                    zone_id: 0,
917                    word_id: 0,
918                    site_id: 0,
919                },
920            ),
921            (
922                1,
923                LocationAddr {
924                    zone_id: 0,
925                    word_id: 1,
926                    site_id: 0,
927                },
928            ),
929        ]);
930        let state2 = AtomStateData::from_locations(&[
931            (
932                1,
933                LocationAddr {
934                    zone_id: 0,
935                    word_id: 1,
936                    site_id: 0,
937                },
938            ),
939            (
940                0,
941                LocationAddr {
942                    zone_id: 0,
943                    word_id: 0,
944                    site_id: 0,
945                },
946            ),
947        ]);
948
949        let mut h1 = DefaultHasher::new();
950        let mut h2 = DefaultHasher::new();
951        state1.hash(&mut h1);
952        state2.hash(&mut h2);
953        assert_eq!(h1.finish(), h2.finish());
954    }
955}