1use std::ops::BitXor;
2
3use bloq_utils::Pauli;
4use glam::IVec2;
5use rustc_hash::FxHashMap;
6use smallvec::SmallVec;
7
8use crate::{CircuitError, CoordinateOverflowError, checked_translate_coordinate};
9
10const PAULI_MAP_INLINE_CAP: usize = 4;
11type PauliMapEntries = SmallVec<[(IVec2, Pauli); PAULI_MAP_INLINE_CAP]>;
12
13#[derive(Debug, PartialEq, Eq, Default, serde::Serialize)]
16pub struct PauliMap {
17 entries: PauliMapEntries,
18}
19
20impl<'de> serde::Deserialize<'de> for PauliMap {
26 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
27 #[derive(serde::Deserialize)]
28 #[serde(rename = "PauliMap")]
29 struct Wire {
30 entries: PauliMapEntries,
31 }
32
33 let Wire { entries } = Wire::deserialize(deserializer)?;
34 if !entries
35 .windows(2)
36 .all(|pair| (pair[0].0.x, pair[0].0.y) < (pair[1].0.x, pair[1].0.y))
37 {
38 return Err(serde::de::Error::custom(
39 "PauliMap entries must be strictly sorted by (x, y)",
40 ));
41 }
42 if entries.iter().any(|&(_, pauli)| pauli == Pauli::I) {
43 return Err(serde::de::Error::custom(
44 "PauliMap entries must not contain identity Paulis",
45 ));
46 }
47 Ok(Self { entries })
48 }
49}
50
51impl Clone for PauliMap {
55 fn clone(&self) -> Self {
56 Self {
57 entries: PauliMapEntries::from_slice(&self.entries),
58 }
59 }
60}
61
62impl std::hash::Hash for PauliMap {
68 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
69 self.hash_translated(IVec2::ZERO, state);
70 }
71}
72
73impl PauliMap {
74 pub(crate) fn hash_translated<H: std::hash::Hasher>(&self, offset: IVec2, state: &mut H) {
78 state.write_usize(self.entries.len());
79 for &(coord, pauli) in &self.entries {
80 let coord = coord + offset;
81 let packed = ((coord.x as u32 as u64) << 32) | (coord.y as u32 as u64);
82 state.write_u64(packed ^ ((pauli as u64) << 30));
83 }
84 }
85}
86
87impl FromIterator<(IVec2, Pauli)> for PauliMap {
88 fn from_iter<T: IntoIterator<Item = (IVec2, Pauli)>>(iter: T) -> Self {
89 let mut map = Self::empty();
90 for (coord, pauli) in iter {
91 map.insert(coord, pauli);
92 }
93 map
94 }
95}
96
97impl std::fmt::Display for PauliMap {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 if self.is_empty() {
100 return write!(f, "I");
101 }
102 let mut entries = self.iter().collect::<Vec<_>>();
103 entries.sort_unstable_by_key(|&(coord, _)| (coord.y, coord.x));
104 for (index, (coord, pauli)) in entries.into_iter().enumerate() {
105 if index > 0 {
106 write!(f, "*")?;
107 }
108 write!(f, "{pauli}({},{})", coord.x, coord.y)?;
109 }
110 Ok(())
111 }
112}
113
114impl PauliMap {
115 pub(crate) fn entry_before(&self, coord: IVec2) -> Option<(IVec2, Pauli)> {
117 let end = self
118 .entries
119 .partition_point(|&(entry, _)| (entry.x, entry.y) < (coord.x, coord.y));
120 end.checked_sub(1).map(|index| self.entries[index])
121 }
122
123 pub fn product_phase(&self, other: &Self) -> u8 {
125 self.iter().fold(0, |phase, (coord, &left)| {
126 (phase
127 + match (left, other.get(coord).copied().unwrap_or(Pauli::I)) {
128 (Pauli::X, Pauli::Y) | (Pauli::Y, Pauli::Z) | (Pauli::Z, Pauli::X) => 1,
129 (Pauli::Y, Pauli::X) | (Pauli::Z, Pauli::Y) | (Pauli::X, Pauli::Z) => 3,
130 _ => 0,
131 })
132 % 4
133 })
134 }
135
136 #[must_use]
143 pub fn translated(&self, offset: IVec2) -> Self {
144 self.try_translated(offset)
145 .expect("translated PauliMap coordinates must fit in the i32 lattice")
146 }
147
148 pub fn try_translated(&self, offset: IVec2) -> Result<Self, CoordinateOverflowError> {
155 let mut entries = PauliMapEntries::from_slice(&self.entries);
158 for (coord, _) in &mut entries {
159 *coord = checked_translate_coordinate(*coord, offset)?;
160 }
161 Ok(Self { entries })
162 }
163
164 #[must_use]
175 pub fn from_unique_entries(entries: impl IntoIterator<Item = (IVec2, Pauli)>) -> Self {
176 let mut entries: PauliMapEntries = entries.into_iter().collect();
177 entries.sort_unstable_by_key(|&(coord, _)| (coord.x, coord.y));
178 assert!(
179 entries
180 .windows(2)
181 .all(|pair| (pair[0].0.x, pair[0].0.y) < (pair[1].0.x, pair[1].0.y)),
182 "entries must have pairwise-distinct coordinates"
183 );
184 assert!(
185 entries.iter().all(|&(_, pauli)| pauli != Pauli::I),
186 "entries must not contain identity Paulis"
187 );
188 Self { entries }
189 }
190
191 #[must_use]
193 pub fn empty() -> Self {
194 Self {
195 entries: SmallVec::new(),
196 }
197 }
198
199 #[must_use]
201 pub fn len(&self) -> usize {
202 self.entries.len()
203 }
204
205 #[must_use]
207 pub fn is_empty(&self) -> bool {
208 self.entries.is_empty()
209 }
210
211 pub fn iter(&self) -> PauliMapIter<'_> {
213 PauliMapIter {
214 inner: self.entries.iter(),
215 }
216 }
217
218 #[must_use]
224 pub fn representative_coord(&self) -> Option<IVec2> {
225 self.iter().next().map(|(coord, _)| *coord)
226 }
227
228 pub fn get(&self, coord: &IVec2) -> Option<&Pauli> {
230 self.find_index(*coord)
231 .ok()
232 .map(|index| &self.entries[index].1)
233 }
234
235 pub fn insert(&mut self, coord: IVec2, pauli: Pauli) -> Option<Pauli> {
240 match self.find_index(coord) {
241 Ok(index) => {
242 if pauli == Pauli::I {
243 Some(self.entries.remove(index).1)
244 } else {
245 Some(std::mem::replace(&mut self.entries[index].1, pauli))
246 }
247 }
248 Err(index) => {
249 if pauli != Pauli::I {
250 self.entries.insert(index, (coord, pauli));
251 }
252 None
253 }
254 }
255 }
256
257 pub fn to_pauli_string(
266 &self,
267 layout: &FxHashMap<IVec2, u32>,
268 num_qubits: usize,
269 ) -> Result<String, CircuitError> {
270 let mut pauli_string = "_".repeat(num_qubits);
271 for (coord, pauli) in self {
272 let index = *layout
273 .get(coord)
274 .ok_or(CircuitError::QubitNotFoundInLayout(*coord))?
275 as usize;
276 if index >= num_qubits {
277 return Err(CircuitError::QubitIndexOutOfRange { index, num_qubits });
278 }
279 pauli_string.replace_range(
280 index..index + 1,
281 match pauli {
282 Pauli::I => "I",
283 Pauli::X => "X",
284 Pauli::Y => "Y",
285 Pauli::Z => "Z",
286 },
287 );
288 }
289 Ok(pauli_string)
290 }
291
292 fn find_index(&self, coord: IVec2) -> Result<usize, usize> {
293 self.entries
294 .binary_search_by_key(&(coord.x, coord.y), |(entry, _)| (entry.x, entry.y))
295 }
296}
297
298impl BitXor for &PauliMap {
299 type Output = PauliMap;
300
301 fn bitxor(self, rhs: Self) -> Self::Output {
303 let (a, b) = (&self.entries, &rhs.entries);
304 let mut entries = PauliMapEntries::with_capacity(a.len() + b.len());
305 let (mut i, mut j) = (0, 0);
306 while i < a.len() && j < b.len() {
307 let ((a_coord, a_pauli), (b_coord, b_pauli)) = (a[i], b[j]);
308 match (a_coord.x, a_coord.y).cmp(&(b_coord.x, b_coord.y)) {
309 std::cmp::Ordering::Less => {
310 entries.push((a_coord, a_pauli));
311 i += 1;
312 }
313 std::cmp::Ordering::Greater => {
314 entries.push((b_coord, b_pauli));
315 j += 1;
316 }
317 std::cmp::Ordering::Equal => {
318 let pauli = a_pauli ^ b_pauli;
319 if pauli != Pauli::I {
320 entries.push((a_coord, pauli));
321 }
322 i += 1;
323 j += 1;
324 }
325 }
326 }
327 entries.extend_from_slice(&a[i..]);
328 entries.extend_from_slice(&b[j..]);
329 PauliMap { entries }
330 }
331}
332
333#[derive(Debug)]
336pub struct PauliMapIter<'a> {
337 inner: std::slice::Iter<'a, (IVec2, Pauli)>,
338}
339
340impl<'a> Iterator for PauliMapIter<'a> {
341 type Item = (&'a IVec2, &'a Pauli);
342
343 fn next(&mut self) -> Option<Self::Item> {
344 self.inner.next().map(|(coord, pauli)| (coord, pauli))
345 }
346
347 fn size_hint(&self) -> (usize, Option<usize>) {
348 self.inner.size_hint()
349 }
350}
351
352impl ExactSizeIterator for PauliMapIter<'_> {}
353
354impl std::iter::FusedIterator for PauliMapIter<'_> {}
355
356impl DoubleEndedIterator for PauliMapIter<'_> {
357 fn next_back(&mut self) -> Option<Self::Item> {
358 self.inner.next_back().map(|(coord, pauli)| (coord, pauli))
359 }
360}
361
362impl IntoIterator for PauliMap {
363 type Item = (IVec2, Pauli);
364 type IntoIter = smallvec::IntoIter<[(IVec2, Pauli); PAULI_MAP_INLINE_CAP]>;
365
366 fn into_iter(self) -> Self::IntoIter {
367 self.entries.into_iter()
368 }
369}
370
371impl<'a> IntoIterator for &'a PauliMap {
372 type Item = (&'a IVec2, &'a Pauli);
373 type IntoIter = PauliMapIter<'a>;
374
375 fn into_iter(self) -> Self::IntoIter {
376 self.iter()
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 fn map(entries: impl IntoIterator<Item = (IVec2, Pauli)>) -> PauliMap {
385 entries.into_iter().collect()
386 }
387
388 #[test]
391 fn deserialize_rejects_unsorted_and_identity_entries() {
392 #[derive(serde::Serialize)]
393 #[serde(rename = "PauliMap")]
394 struct RawMap {
395 entries: Vec<(IVec2, Pauli)>,
396 }
397 let encode = |entries: Vec<(IVec2, Pauli)>| {
398 postcard::to_allocvec(&RawMap { entries }).expect("test wire encodes into a Vec")
399 };
400
401 let unsorted = encode(vec![
402 (IVec2::new(1, 0), Pauli::X),
403 (IVec2::new(0, 0), Pauli::Z),
404 ]);
405 postcard::from_bytes::<PauliMap>(&unsorted).unwrap_err();
406
407 let identity = encode(vec![(IVec2::new(0, 0), Pauli::I)]);
408 postcard::from_bytes::<PauliMap>(&identity).unwrap_err();
409
410 let valid = map([(IVec2::new(0, 0), Pauli::X), (IVec2::new(1, 0), Pauli::Z)]);
411 let bytes = postcard::to_allocvec(&valid).expect("map encodes into a Vec");
412 let restored: PauliMap = postcard::from_bytes(&bytes).expect("sorted entries decode");
413 assert_eq!(restored, valid);
414 }
415
416 #[test]
417 fn unique_entries_preserve_map_invariants() {
418 for entries in [
419 [(IVec2::ZERO, Pauli::X), (IVec2::ZERO, Pauli::Z)],
420 [(IVec2::ZERO, Pauli::X), (IVec2::X, Pauli::I)],
421 ] {
422 std::panic::catch_unwind(|| PauliMap::from_unique_entries(entries))
423 .expect_err("duplicate coordinates and identity entries must panic");
424 }
425 let map = PauliMap::from_unique_entries([(IVec2::X, Pauli::Z), (IVec2::ZERO, Pauli::X)]);
426 let mut entries: crate::PauliMapIter<'_> = map.iter();
427 assert_eq!(entries.len(), 2);
428 assert_eq!(entries.next(), Some((&IVec2::ZERO, &Pauli::X)));
429 assert_eq!(entries.len(), 1);
430 assert_eq!(entries.next_back(), Some((&IVec2::X, &Pauli::Z)));
431 assert_eq!(entries.len(), 0);
432 assert_eq!(entries.next(), None);
433 }
434
435 #[test]
436 fn xor_composes_paulis() {
437 let x = map([(IVec2::ZERO, Pauli::X)]);
438 let z = map([(IVec2::ZERO, Pauli::Z)]);
439
440 assert_eq!(&x ^ &z, map([(IVec2::ZERO, Pauli::Y)]));
441 assert!((&x ^ &x).is_empty());
442 }
443
444 #[test]
445 fn pauli_string_error_reports_empty_layout_size() {
446 let coord = IVec2::ZERO;
447 let map = map([(coord, Pauli::X)]);
448 let layout: FxHashMap<_, _> = [(coord, 0)].into_iter().collect();
449
450 assert_eq!(
451 map.to_pauli_string(&layout, 0),
452 Err(CircuitError::QubitIndexOutOfRange {
453 index: 0,
454 num_qubits: 0,
455 })
456 );
457 }
458
459 #[test]
460 fn pauli_string_uses_layout_indices_and_identity_gaps() {
461 let map = map([
462 (IVec2::ZERO, Pauli::Z),
463 (IVec2::X, Pauli::X),
464 (IVec2::Y, Pauli::Y),
465 ]);
466 let layout = FxHashMap::from_iter([(IVec2::ZERO, 3), (IVec2::X, 0), (IVec2::Y, 2)]);
467 assert_eq!(map.to_pauli_string(&layout, 5), Ok("X_YZ_".into()));
468 assert_eq!(
469 map.to_pauli_string(&FxHashMap::default(), 5),
470 Err(CircuitError::QubitNotFoundInLayout(IVec2::ZERO))
471 );
472 assert_eq!(
473 PauliMap::empty().to_pauli_string(&layout, 0),
474 Ok(String::new())
475 );
476 }
477
478 #[test]
479 fn checked_translation_rejects_coordinate_overflow() {
480 let map = map([(IVec2::new(i32::MAX, 0), Pauli::X)]);
481
482 assert_eq!(
483 map.try_translated(IVec2::X),
484 Err(CoordinateOverflowError {
485 coordinate: IVec2::new(i32::MAX, 0),
486 offset: IVec2::X,
487 })
488 );
489 }
490
491 #[test]
492 fn pauli_map_deduplicates_and_sorts_entries() {
493 let map = vec![
494 (IVec2::new(2, 0), Pauli::Z),
495 (IVec2::new(0, 1), Pauli::X),
496 (IVec2::new(2, 0), Pauli::I),
497 (IVec2::new(1, 0), Pauli::Y),
498 ]
499 .into_iter()
500 .collect::<PauliMap>();
501
502 assert_eq!(
503 map.into_iter().collect::<Vec<_>>(),
504 vec![(IVec2::new(0, 1), Pauli::X), (IVec2::new(1, 0), Pauli::Y)]
505 );
506 }
507}