Skip to content

Commit 96b3842

Browse files
authored
Move qubit management transformers from Cirq-FT to Cirq-core (#6319)
* Move qubit management transformers from Cirq-FT to Cirq-core * Fix mypy error
1 parent 105d975 commit 96b3842

8 files changed

+442
-389
lines changed

cirq-core/cirq/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@
358358
is_negligible_turn,
359359
LineInitialMapper,
360360
MappingManager,
361+
map_clean_and_borrowable_qubits,
361362
map_moments,
362363
map_operations,
363364
map_operations_and_unroll,

cirq-core/cirq/transformers/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494
merge_single_qubit_moments_to_phxz,
9595
)
9696

97+
from cirq.transformers.qubit_management_transformers import map_clean_and_borrowable_qubits
98+
9799
from cirq.transformers.synchronize_terminal_measurements import synchronize_terminal_measurements
98100

99101
from cirq.transformers.transformer_api import (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright 2023 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Dict, Optional, Set, Tuple, TYPE_CHECKING
16+
17+
from cirq import circuits, ops
18+
19+
if TYPE_CHECKING:
20+
import cirq
21+
22+
23+
def _get_qubit_mapping_first_and_last_moment(
24+
circuit: 'cirq.AbstractCircuit',
25+
) -> Dict['cirq.Qid', Tuple[int, int]]:
26+
"""Computes `(first_moment_idx, last_moment_idx)` tuple for each qubit in the input circuit.
27+
28+
Args:
29+
circuit: An input cirq circuit to analyze.
30+
31+
Returns:
32+
A dict mapping each qubit `q` in the input circuit to a tuple of integers
33+
`(first_moment_idx, last_moment_idx)` where
34+
- first_moment_idx: Index of leftmost moment containing an operation that acts on `q`.
35+
- last_moment_idx: Index of rightmost moment containing an operation that acts on `q`.
36+
"""
37+
ret = {q: (len(circuit), 0) for q in circuit.all_qubits()}
38+
for i, moment in enumerate(circuit):
39+
for q in moment.qubits:
40+
ret[q] = (min(ret[q][0], i), max(ret[q][1], i))
41+
return ret
42+
43+
44+
def _is_temp(q: 'cirq.Qid') -> bool:
45+
return isinstance(q, (ops.CleanQubit, ops.BorrowableQubit))
46+
47+
48+
def map_clean_and_borrowable_qubits(
49+
circuit: 'cirq.AbstractCircuit', *, qm: Optional['cirq.QubitManager'] = None
50+
) -> 'cirq.Circuit':
51+
"""Uses `qm: QubitManager` to map all `CleanQubit`/`BorrowableQubit`s to system qubits.
52+
53+
`CleanQubit` and `BorrowableQubit` are internal qubit types that are used as placeholder qubits
54+
to record a clean / dirty ancilla allocation request.
55+
56+
This transformer uses the `QubitManager` provided in the input to:
57+
- Allocate clean ancilla qubits by delegating to `qm.qalloc` for all `CleanQubit`s.
58+
- Allocate dirty qubits for all `BorrowableQubit` types via the following two steps:
59+
1. First analyse the input circuit and check if there are any suitable system qubits
60+
that can be borrowed, i.e. ones which do not have any overlapping operations
61+
between circuit[start_index : end_index] where `(start_index, end_index)` is the
62+
lifespan of temporary borrowable qubit under consideration. If yes, borrow the system
63+
qubits to replace the temporary `BorrowableQubit`.
64+
2. If no system qubits can be borrowed, delegate the request to `qm.qborrow`.
65+
66+
Notes:
67+
1. The borrow protocol can be made more efficient by also considering the newly
68+
allocated clean ancilla qubits in step-1 before delegating to `qm.borrow`, but this
69+
optimization is left as a future improvement.
70+
2. As of now, the transformer does not preserve moment structure and defaults to
71+
inserting all mapped operations in a resulting circuit using EARLIEST strategy. The reason
72+
is that preserving moment structure forces additional constraints on the qubit allocation
73+
strategy (i.e. if two operations `op1` and `op2` are in the same moment, then we cannot
74+
reuse ancilla across `op1` and `op2`). We leave it upto the user to force such constraints
75+
using the qubit manager instead of making it part of the transformer.
76+
3. However, for borrowable system qubits managed by the transformer, we do not reuse qubits
77+
within the same moment.
78+
4. Since this is not implemented using the cirq transformers infrastructure, we currently
79+
do not support recursive mapping within sub-circuits and that is left as a future TODO.
80+
81+
Args:
82+
circuit: Input `cirq.Circuit` containing temporarily allocated
83+
`CleanQubit`/`BorrowableQubit`s.
84+
qm: An instance of `cirq.QubitManager` specifying the strategy to use for allocating /
85+
/ deallocating new ancilla qubits to replace the temporary qubits.
86+
87+
Returns:
88+
An updated `cirq.Circuit` with all `CleanQubit`/`BorrowableQubit` mapped to either existing
89+
system qubits or new ancilla qubits allocated using the `qm` qubit manager.
90+
"""
91+
if qm is None:
92+
qm = ops.GreedyQubitManager(prefix="ancilla")
93+
94+
allocated_qubits = {q for q in circuit.all_qubits() if _is_temp(q)}
95+
qubits_lifespan = _get_qubit_mapping_first_and_last_moment(circuit)
96+
all_qubits = frozenset(circuit.all_qubits() - allocated_qubits)
97+
trivial_map = {q: q for q in all_qubits}
98+
# `allocated_map` maintains the mapping of all temporary qubits seen so far, mapping each of
99+
# them to either a newly allocated managed ancilla or an existing borrowed system qubit.
100+
allocated_map: Dict['cirq.Qid', 'cirq.Qid'] = {}
101+
to_free: Set['cirq.Qid'] = set()
102+
last_op_idx = -1
103+
104+
def map_func(op: 'cirq.Operation', idx: int) -> 'cirq.OP_TREE':
105+
nonlocal last_op_idx, to_free
106+
assert isinstance(qm, ops.QubitManager)
107+
108+
for q in sorted(to_free):
109+
is_managed_qubit = allocated_map[q] not in all_qubits
110+
if idx > last_op_idx or is_managed_qubit:
111+
# is_managed_qubit: if `q` is mapped to a newly allocated qubit managed by the qubit
112+
# manager, we can free it immediately after the previous operation ends. This
113+
# assumes that a managed qubit is not considered by the transformer as part of
114+
# borrowing qubits (first point of the notes above).
115+
# idx > last_op_idx: if `q` is mapped to a system qubit, which is not managed by the
116+
# qubit manager, we free it only at the end of the moment.
117+
if is_managed_qubit:
118+
qm.qfree([allocated_map[q]])
119+
allocated_map.pop(q)
120+
to_free.remove(q)
121+
122+
last_op_idx = idx
123+
124+
# To check borrowable qubits, we manually manage only the original system qubits
125+
# that are not managed by the qubit manager. If any of the system qubits cannot be
126+
# borrowed, we defer to the qubit manager to allocate a new clean qubit for us.
127+
# This is a heuristic and can be improved by also checking if any allocated but not
128+
# yet freed managed qubit can be borrowed for the shorter scope, but we ignore the
129+
# optimization for the sake of simplicity here.
130+
borrowable_qubits = set(all_qubits) - set(allocated_map.values())
131+
132+
op_temp_qubits = (q for q in op.qubits if _is_temp(q))
133+
for q in op_temp_qubits:
134+
# Get the lifespan of this temporarily allocated ancilla qubit `q`.
135+
st, en = qubits_lifespan[q]
136+
assert st <= idx <= en
137+
if en == idx:
138+
# Mark that this temporarily allocated qubit can be freed after this moment ends.
139+
to_free.add(q)
140+
if q in allocated_map or st < idx:
141+
# The qubit already has a mapping iff we have seen it before.
142+
assert st < idx and q in allocated_map
143+
# This line is actually covered by
144+
# `test_map_clean_and_borrowable_qubits_deallocates_only_once` but pytest-cov seems
145+
# to not recognize it and hence the pragma: no cover.
146+
continue # pragma: no cover
147+
148+
# This is the first time we are seeing this temporary qubit and need to find a mapping.
149+
if isinstance(q, ops.CleanQubit):
150+
# Allocate a new clean qubit if `q` using the qubit manager.
151+
allocated_map[q] = qm.qalloc(1)[0]
152+
elif isinstance(q, ops.BorrowableQubit):
153+
# For each of the system qubits that can be borrowed, check whether they have a
154+
# conflicting operation in the range [st, en]; which is the scope for which the
155+
# borrower needs the borrowed qubit for.
156+
start_frontier = {q: st for q in borrowable_qubits}
157+
end_frontier = {q: en + 1 for q in borrowable_qubits}
158+
ops_in_between = circuit.findall_operations_between(start_frontier, end_frontier)
159+
# Filter the set of borrowable qubits which do not have any conflicting operations.
160+
filtered_borrowable_qubits = borrowable_qubits - set(
161+
q for _, op in ops_in_between for q in op.qubits
162+
)
163+
if filtered_borrowable_qubits:
164+
# Allocate a borrowable qubit and remove it from the pool of available qubits.
165+
allocated_map[q] = min(filtered_borrowable_qubits)
166+
borrowable_qubits.remove(allocated_map[q])
167+
else:
168+
# Use the qubit manager to get a new borrowable qubit, since we couldn't find
169+
# one from the original system qubits.
170+
allocated_map[q] = qm.qborrow(1)[0]
171+
else:
172+
assert False, f"Unknown temporary qubit type {q}"
173+
174+
# Return the transformed operation / decomposed op-tree.
175+
return op.transform_qubits({**allocated_map, **trivial_map})
176+
177+
return circuits.Circuit(map_func(op, idx) for idx, m in enumerate(circuit) for op in m)

0 commit comments

Comments
 (0)