Skip to content

Commit a370af5

Browse files
Add get_check_qubits helper (#37)
* first pass * Clean up docstrings in layout module * dont filter 3rd party warnings * Add a reno * Clean up docstring * Fix commentary in guide
1 parent 42f812d commit a370af5

4 files changed

Lines changed: 190 additions & 76 deletions

File tree

docs/guides/low_overhead_error_detection_using_spacetime_codes.ipynb

Lines changed: 76 additions & 66 deletions
Large diffs are not rendered by default.

python/qiskit_paulice/layout.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,64 @@
1616

1717
from collections.abc import Sequence
1818

19+
from qiskit.providers import BackendV2
1920
from qiskit.transpiler import CouplingMap
2021

2122

23+
def get_check_qubits(
24+
backend: BackendV2 | CouplingMap, layout: Sequence[int]
25+
) -> tuple[list[int], list[int]]:
26+
"""Pair qubits in ``layout`` with neighboring ancillas.
27+
28+
Generate equal-length lists of target and ancilla qubits, such that
29+
target qubit ``i`` is adjacent to ancilla qubit ``i``in the coupling map.
30+
Target and ancilla qubits may appear at most one time in their
31+
respective lists.
32+
33+
Args:
34+
backend: The target backend, or a :class:`~qiskit.transpiler.CouplingMap`
35+
describing its connectivity.
36+
layout: A list of physical qubit indices.
37+
38+
Returns:
39+
A length-2 tuple of lists, ``(target_qubits, ancilla_qubits)``. ``target_qubits[i]``
40+
pairs with ``ancilla_qubits[i]``.
41+
"""
42+
coupling_map = getattr(backend, "coupling_map", backend)
43+
ancilla_to_payload = get_low_overhead_ancillas(coupling_map, layout)
44+
45+
# Give each ancilla its first not-yet-claimed neighbor, so every target and
46+
# ancilla is used at most once. Sorting makes the choice deterministic.
47+
matched: dict[int, int] = {} # target qubit -> ancilla
48+
for ancilla in sorted(ancilla_to_payload):
49+
for target in sorted(ancilla_to_payload[ancilla]):
50+
if target not in matched:
51+
matched[target] = ancilla
52+
break
53+
54+
targets = sorted(matched)
55+
return [int(t) for t in targets], [int(matched[t]) for t in targets]
56+
57+
2258
def get_low_overhead_ancillas(
2359
coupling_map: CouplingMap, layout: Sequence[int]
2460
) -> dict[int, list[int]]:
25-
"""Find ancilla qubits adjacent to the layout qubits in the coupling graph.
26-
27-
This function identifies physical qubits that are not in the layout but are
28-
connected to one or more layout qubits via the coupling map.
61+
"""Create a mapping from ancillas to ``layout`` qubits to which they are adjacent.
2962
3063
Args:
31-
coupling_map: A qubit connectivity graph
32-
layout: Physical qubit indices for which to find adjacent ancillas
64+
coupling_map: A qubit connectivity graph.
65+
layout: Physical qubit indices occupied by the payload circuit.
3366
3467
Returns:
35-
A dictionary mapping ancilla qubit indices to lists of layout qubit indices
36-
to which it is adjacent.
68+
A mapping from ancilla indices to the list of ``layout`` qubits to which it is
69+
adjacent. An ancilla bordering several layout qubits maps to all of them, and
70+
a layout qubit bordering several ancillas appears in each of their lists.
3771
"""
3872
layout_set = set(layout)
3973
ancilla_targets: dict[int, list[int]] = {}
4074

4175
for qubit in layout:
42-
for neighbor in coupling_map.neighbors(qubit):
76+
for neighbor in sorted(coupling_map.neighbors(qubit)):
4377
if neighbor not in layout_set:
4478
if neighbor not in ancilla_targets:
4579
ancilla_targets[neighbor] = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
Added new :func:`qiskit_paulice.layout.get_check_qubits` function, which provides a straightforward way to find target/ancilla qubit pairs, which can be used to implement spacetime Pauli checks.

test/test_layout.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import unittest
1818

1919
from qiskit.transpiler import CouplingMap
20-
from qiskit_paulice.layout import get_low_overhead_ancillas
20+
from qiskit_paulice.layout import get_check_qubits, get_low_overhead_ancillas
2121

2222

2323
class TestGetLowOverheadAncillas(unittest.TestCase):
@@ -58,3 +58,69 @@ def test_ancilla_shared_by_multiple_layout_qubits(self):
5858
self.assertEqual(list(result.keys()), [1])
5959
# Order tracks the layout iteration order.
6060
self.assertEqual(result[1], [0, 2])
61+
62+
def test_key_order_is_deterministic(self):
63+
"""Keys are sorted, so `neighbors` iteration instability can't leak out."""
64+
# Edges added so a single layout qubit's neighbors are not in index order.
65+
cm = CouplingMap([(5, 2), (5, 8), (5, 1), (1, 5), (2, 5), (8, 5)])
66+
self.assertEqual(list(get_low_overhead_ancillas(cm, [5])), [1, 2, 8])
67+
68+
69+
class _FakeBackend:
70+
"""Minimal stand-in exposing the ``coupling_map`` attribute the wrapper reads."""
71+
72+
def __init__(self, coupling_map: CouplingMap):
73+
self.coupling_map = coupling_map
74+
75+
76+
class TestGetCheckQubits(unittest.TestCase):
77+
"""Tests covering :func:`get_check_qubits`."""
78+
79+
def test_line_pairs_each_target_with_its_ancilla(self):
80+
# Line 0-1-2-3-4, payload on the interior: 0 checks 1, 4 checks 3.
81+
targets, ancillas = get_check_qubits(CouplingMap.from_line(5), [1, 2, 3])
82+
self.assertEqual(targets, [1, 3])
83+
self.assertEqual(ancillas, [0, 4])
84+
85+
def test_accepts_backend_like_object(self):
86+
# Anything exposing `.coupling_map` works the same as passing the map.
87+
cm = CouplingMap.from_line(5)
88+
self.assertEqual(
89+
get_check_qubits(_FakeBackend(cm), [1, 2, 3]),
90+
get_check_qubits(cm, [1, 2, 3]),
91+
)
92+
93+
def test_each_ancilla_takes_a_distinct_target(self):
94+
# Ancilla 0 neighbors only target 1; ancilla 3 neighbors targets 1 and 2.
95+
# Walking ancillas in order, 0 claims 1 and 3 falls through to 2, so both
96+
# targets get a check rather than competing for target 1.
97+
cm = CouplingMap([(0, 1), (1, 0), (3, 1), (1, 3), (3, 2), (2, 3)])
98+
targets, ancillas = get_check_qubits(cm, [1, 2])
99+
self.assertEqual(targets, [1, 2])
100+
self.assertEqual(ancillas, [0, 3])
101+
102+
def test_pairs_are_valid_and_unique(self):
103+
# Star centered on 2 plus a tail: every pair must use a distinct ancilla
104+
# outside the layout that genuinely neighbors its target.
105+
cm = CouplingMap.from_line(6)
106+
layout = [1, 2, 3, 4]
107+
targets, ancillas = get_check_qubits(cm, layout)
108+
self.assertEqual(len(targets), len(ancillas))
109+
self.assertEqual(len(set(targets)), len(targets))
110+
self.assertEqual(len(set(ancillas)), len(ancillas))
111+
for t, a in zip(targets, ancillas, strict=True):
112+
self.assertIn(t, layout)
113+
self.assertNotIn(a, layout)
114+
self.assertIn(a, list(cm.neighbors(t)))
115+
116+
def test_ancilla_with_no_free_target_is_dropped(self):
117+
# Ancillas 0 and 2 both border only target 1; the first claims it and the
118+
# second is left unmatched, so only one pair comes back.
119+
cm = CouplingMap([(0, 1), (1, 0), (2, 1), (1, 2)])
120+
self.assertEqual(get_check_qubits(cm, [1]), ([1], [0]))
121+
122+
def test_empty_layout_returns_empty_lists(self):
123+
self.assertEqual(get_check_qubits(CouplingMap.from_line(5), []), ([], []))
124+
125+
def test_full_layout_returns_empty_lists(self):
126+
self.assertEqual(get_check_qubits(CouplingMap.from_line(5), list(range(5))), ([], []))

0 commit comments

Comments
 (0)