Skip to content

Commit fee056e

Browse files
authored
Move GreedyQubitManager from Cirq-FT to Cirq-Core (#6309)
* Move GreedyQubitManager from Cirq-FT to Cirq-Core * Mark Cirq-FT qubit manager as deprecated * Fix tests * Fix coverage test
1 parent 87f77be commit fee056e

22 files changed

+241
-183
lines changed

cirq-core/cirq/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@
224224
givens,
225225
GlobalPhaseGate,
226226
global_phase_operation,
227+
GreedyQubitManager,
227228
H,
228229
HPowGate,
229230
I,
@@ -301,6 +302,7 @@
301302
ry,
302303
rz,
303304
S,
305+
SimpleQubitManager,
304306
SingleQubitCliffordGate,
305307
SingleQubitPauliStringGateOperation,
306308
SQRT_ISWAP,

cirq-core/cirq/ops/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122

123123
from cirq.ops.qubit_manager import BorrowableQubit, CleanQubit, QubitManager, SimpleQubitManager
124124

125+
from cirq.ops.greedy_qubit_manager import GreedyQubitManager
126+
125127
from cirq.ops.qubit_order import QubitOrder
126128

127129
from cirq.ops.qubit_order_or_list import QubitOrderOrList
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 Iterable, List, Set, TYPE_CHECKING
16+
17+
from cirq.ops import named_qubit, qid_util, qubit_manager
18+
19+
if TYPE_CHECKING:
20+
import cirq
21+
22+
23+
class GreedyQubitManager(qubit_manager.QubitManager):
24+
"""Greedy allocator that maximizes/minimizes qubit reuse based on a configurable parameter.
25+
26+
GreedyQubitManager can be configured, using `maximize_reuse` flag, to work in one of two modes:
27+
- Minimize qubit reuse (maximize_reuse=False): For a fixed width, this mode uses a FIFO (First
28+
in First out) strategy s.t. next allocated qubit is one which was freed the earliest.
29+
- Maximize qubit reuse (maximize_reuse=True): For a fixed width, this mode uses a LIFO (Last in
30+
First out) strategy s.t. the next allocated qubit is one which was freed the latest.
31+
32+
If the requested qubits are more than the set of free qubits, the qubit manager automatically
33+
resizes the size of the managed qubit pool and adds new free qubits, that have their last
34+
freed time to be -infinity.
35+
36+
For borrowing qubits, the qubit manager simply delegates borrow requests to `self.qalloc`, thus
37+
always allocating new clean qubits.
38+
"""
39+
40+
def __init__(self, prefix: str, *, size: int = 0, maximize_reuse: bool = False):
41+
"""Initializes `GreedyQubitManager`
42+
43+
Args:
44+
prefix: The prefix to use for naming new clean ancillas allocated by the qubit manager.
45+
The i'th allocated qubit is of the type `cirq.NamedQubit(f'{prefix}_{i}')`.
46+
size: The initial size of the pool of ancilla qubits managed by the qubit manager. The
47+
qubit manager can automatically resize itself when the allocation request
48+
exceeds the number of available qubits.
49+
maximize_reuse: Flag to control a FIFO vs LIFO strategy, defaults to False (FIFO).
50+
"""
51+
self._prefix = prefix
52+
self._used_qubits: Set['cirq.Qid'] = set()
53+
self._free_qubits: List['cirq.Qid'] = []
54+
self._size = 0
55+
self.maximize_reuse = maximize_reuse
56+
self.resize(size)
57+
58+
def _allocate_qid(self, name: str, dim: int) -> 'cirq.Qid':
59+
return qid_util.q(name) if dim == 2 else named_qubit.NamedQid(name, dimension=dim)
60+
61+
def resize(self, new_size: int, dim: int = 2) -> None:
62+
if new_size <= self._size:
63+
return
64+
new_qubits: List['cirq.Qid'] = [
65+
self._allocate_qid(f'{self._prefix}_{s}', dim) for s in range(self._size, new_size)
66+
]
67+
self._free_qubits = new_qubits + self._free_qubits
68+
self._size = new_size
69+
70+
def qalloc(self, n: int, dim: int = 2) -> List['cirq.Qid']:
71+
if not n:
72+
return []
73+
self.resize(self._size + n - len(self._free_qubits), dim=dim)
74+
ret_qubits = self._free_qubits[-n:] if self.maximize_reuse else self._free_qubits[:n]
75+
self._free_qubits = self._free_qubits[:-n] if self.maximize_reuse else self._free_qubits[n:]
76+
self._used_qubits.update(ret_qubits)
77+
return ret_qubits
78+
79+
def qfree(self, qubits: Iterable['cirq.Qid']) -> None:
80+
qs = list(dict(zip(qubits, qubits)).keys())
81+
assert self._used_qubits.issuperset(qs), "Only managed qubits currently in-use can be freed"
82+
self._used_qubits = self._used_qubits.difference(qs)
83+
self._free_qubits.extend(qs)
84+
85+
def qborrow(self, n: int, dim: int = 2) -> List['cirq.Qid']:
86+
return self.qalloc(n, dim)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
import cirq
16+
17+
18+
class GateAllocInDecompose(cirq.Gate):
19+
def __init__(self, num_alloc: int = 1):
20+
self.num_alloc = num_alloc
21+
22+
def _num_qubits_(self) -> int:
23+
return 1
24+
25+
def _decompose_with_context_(self, qubits, context):
26+
assert context is not None
27+
qm = context.qubit_manager
28+
for q in qm.qalloc(self.num_alloc):
29+
yield cirq.CNOT(qubits[0], q)
30+
qm.qfree([q])
31+
32+
33+
def test_greedy_qubit_manager():
34+
def make_circuit(qm: cirq.QubitManager):
35+
q = cirq.LineQubit.range(2)
36+
g = GateAllocInDecompose(1)
37+
context = cirq.DecompositionContext(qubit_manager=qm)
38+
circuit = cirq.Circuit(
39+
cirq.decompose_once(g.on(q[0]), context=context),
40+
cirq.decompose_once(g.on(q[1]), context=context),
41+
)
42+
return circuit
43+
44+
qm = cirq.GreedyQubitManager(prefix="ancilla", size=1)
45+
# Qubit manager with only 1 managed qubit. Will always repeat the same qubit.
46+
circuit = make_circuit(qm)
47+
cirq.testing.assert_has_diagram(
48+
circuit,
49+
"""
50+
0: ───────────@───────
51+
52+
1: ───────────┼───@───
53+
│ │
54+
ancilla_0: ───X───X───
55+
""",
56+
)
57+
58+
qm = cirq.GreedyQubitManager(prefix="ancilla", size=2)
59+
# Qubit manager with 2 managed qubits and maximize_reuse=False, tries to minimize adding
60+
# additional data dependencies by minimizing qubit reuse.
61+
circuit = make_circuit(qm)
62+
cirq.testing.assert_has_diagram(
63+
circuit,
64+
"""
65+
┌──┐
66+
0: ────────────@─────
67+
68+
1: ────────────┼@────
69+
││
70+
ancilla_0: ────X┼────
71+
72+
ancilla_1: ─────X────
73+
└──┘
74+
""",
75+
)
76+
77+
qm = cirq.GreedyQubitManager(prefix="ancilla", size=2, maximize_reuse=True)
78+
# Qubit manager with 2 managed qubits and maximize_reuse=True, tries to maximize reuse by
79+
# potentially adding new data dependencies.
80+
circuit = make_circuit(qm)
81+
cirq.testing.assert_has_diagram(
82+
circuit,
83+
"""
84+
0: ───────────@───────
85+
86+
1: ───────────┼───@───
87+
│ │
88+
ancilla_1: ───X───X───
89+
""",
90+
)
91+
92+
93+
def test_greedy_qubit_manager_preserves_order():
94+
qm = cirq.GreedyQubitManager(prefix="anc")
95+
ancillae = [cirq.q(f"anc_{i}") for i in range(100)]
96+
assert qm.qalloc(100) == ancillae
97+
qm.qfree(ancillae)
98+
assert qm.qalloc(100) == ancillae

cirq-core/cirq/protocols/json_test_data/spec.py

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@
8989
'LineInitialMapper',
9090
'MappingManager',
9191
'RouteCQC',
92+
# Qubit Managers,
93+
'SimpleQubitManager',
94+
'GreedyQubitManager',
9295
# global objects
9396
'CONTROL_TAG',
9497
'PAULI_BASIS',

cirq-ft/cirq_ft/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
)
4646
from cirq_ft.infra import (
4747
GateWithRegisters,
48-
GreedyQubitManager,
4948
Register,
5049
Signature,
5150
SelectionRegister,

cirq-ft/cirq_ft/algos/apply_gate_to_lth_target_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
@pytest.mark.parametrize("selection_bitsize,target_bitsize", [[3, 5], [3, 7], [4, 5]])
2424
def test_apply_gate_to_lth_qubit(selection_bitsize, target_bitsize):
25-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
25+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
2626
gate = cirq_ft.ApplyGateToLthQubit(
2727
cirq_ft.SelectionRegister('selection', selection_bitsize, target_bitsize), lambda _: cirq.X
2828
)

cirq-ft/cirq_ft/algos/arithmetic_gates_test.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import cirq_ft
1919
import numpy as np
2020
import pytest
21-
from cirq_ft.infra import bit_tools, GreedyQubitManager
21+
from cirq_ft.infra import bit_tools
2222

2323

2424
def identity_map(n: int):
@@ -174,7 +174,7 @@ def test_add(a: int, b: int, num_bits: int):
174174
num_anc = num_bits - 1
175175
gate = cirq_ft.AdditionGate(num_bits)
176176
qubits = cirq.LineQubit.range(2 * num_bits)
177-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
177+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
178178
context = cirq.DecompositionContext(greedy_mm)
179179
circuit = cirq.Circuit(cirq.decompose_once(gate.on(*qubits), context=context))
180180
ancillas = sorted(circuit.all_qubits())[-num_anc:]
@@ -258,7 +258,7 @@ def test_add_truncated():
258258
num_anc = num_bits - 1
259259
gate = cirq_ft.AdditionGate(num_bits)
260260
qubits = cirq.LineQubit.range(2 * num_bits)
261-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
261+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
262262
context = cirq.DecompositionContext(greedy_mm)
263263
circuit = cirq.Circuit(cirq.decompose_once(gate.on(*qubits), context=context))
264264
ancillas = sorted(circuit.all_qubits() - frozenset(qubits))
@@ -272,7 +272,7 @@ def test_add_truncated():
272272
num_anc = num_bits - 1
273273
gate = cirq_ft.AdditionGate(num_bits)
274274
qubits = cirq.LineQubit.range(2 * num_bits)
275-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
275+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
276276
context = cirq.DecompositionContext(greedy_mm)
277277
circuit = cirq.Circuit(cirq.decompose_once(gate.on(*qubits), context=context))
278278
ancillas = sorted(circuit.all_qubits() - frozenset(qubits))
@@ -290,7 +290,7 @@ def test_subtract(a, b, num_bits):
290290
num_anc = num_bits - 1
291291
gate = cirq_ft.AdditionGate(num_bits)
292292
qubits = cirq.LineQubit.range(2 * num_bits)
293-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
293+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
294294
context = cirq.DecompositionContext(greedy_mm)
295295
circuit = cirq.Circuit(cirq.decompose_once(gate.on(*qubits), context=context))
296296
ancillas = sorted(circuit.all_qubits())[-num_anc:]
@@ -340,7 +340,7 @@ def test_decompose_less_than_equal_gate(P: int, n: int, Q: int, m: int):
340340
circuit = cirq.Circuit(
341341
cirq.decompose_once(
342342
cirq_ft.LessThanEqualGate(n, m).on(*cirq.LineQubit.range(n + m + 1)),
343-
context=cirq.DecompositionContext(GreedyQubitManager(prefix='_c')),
343+
context=cirq.DecompositionContext(cirq.GreedyQubitManager(prefix='_c')),
344344
)
345345
)
346346
qubit_order = tuple(sorted(circuit.all_qubits()))

cirq-ft/cirq_ft/algos/programmable_rotation_gate_array_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def construct_prga_with_identity(*args, **kwargs) -> cirq_ft.ProgrammableRotatio
6363
def test_programmable_rotation_gate_array(angles, kappa, constructor):
6464
rotation_gate = cirq.X
6565
programmable_rotation_gate = constructor(*angles, kappa=kappa, rotation_gate=rotation_gate)
66-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a")
66+
greedy_mm = cirq.GreedyQubitManager(prefix="_a")
6767
g = cirq_ft.testing.GateHelper(
6868
programmable_rotation_gate, context=cirq.DecompositionContext(greedy_mm)
6969
)

cirq-ft/cirq_ft/algos/qrom_test.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
@pytest.mark.parametrize("num_controls", [0, 1, 2])
3030
def test_qrom_1d(data, num_controls):
3131
qrom = cirq_ft.QROM.build(*data, num_controls=num_controls)
32-
greedy_mm = cirq_ft.GreedyQubitManager('a', maximize_reuse=True)
32+
greedy_mm = cirq.GreedyQubitManager('a', maximize_reuse=True)
3333
g = cirq_ft.testing.GateHelper(qrom, context=cirq.DecompositionContext(greedy_mm))
3434
decomposed_circuit = cirq.Circuit(cirq.decompose(g.operation, context=g.context))
3535
inverse = cirq.Circuit(cirq.decompose(g.operation**-1, context=g.context))
@@ -121,7 +121,7 @@ def test_t_complexity(data):
121121
def _assert_qrom_has_diagram(qrom: cirq_ft.QROM, expected_diagram: str):
122122
gh = cirq_ft.testing.GateHelper(qrom)
123123
op = gh.operation
124-
context = cirq.DecompositionContext(qubit_manager=cirq_ft.GreedyQubitManager(prefix="anc"))
124+
context = cirq.DecompositionContext(qubit_manager=cirq.GreedyQubitManager(prefix="anc"))
125125
circuit = cirq.Circuit(cirq.decompose_once(op, context=context))
126126
selection = [
127127
*itertools.chain.from_iterable(gh.quregs[reg.name] for reg in qrom.selection_registers)
@@ -208,7 +208,7 @@ def test_qrom_multi_dim(data, num_controls):
208208
target_bitsizes=target_bitsizes,
209209
num_controls=num_controls,
210210
)
211-
greedy_mm = cirq_ft.GreedyQubitManager('a', maximize_reuse=True)
211+
greedy_mm = cirq.GreedyQubitManager('a', maximize_reuse=True)
212212
g = cirq_ft.testing.GateHelper(qrom, context=cirq.DecompositionContext(greedy_mm))
213213
decomposed_circuit = cirq.Circuit(cirq.decompose(g.operation, context=g.context))
214214
inverse = cirq.Circuit(cirq.decompose(g.operation**-1, context=g.context))

cirq-ft/cirq_ft/algos/qubitization_walk_operator_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_qubitization_walk_operator(num_sites: int, eps: float):
6363
L_state = np.zeros(2 ** len(g.quregs['selection']))
6464
L_state[: len(ham_coeff)] = np.sqrt(ham_coeff / qubitization_lambda)
6565

66-
greedy_mm = cirq_ft.GreedyQubitManager('ancilla', maximize_reuse=True)
66+
greedy_mm = cirq.GreedyQubitManager('ancilla', maximize_reuse=True)
6767
walk_circuit = cirq_ft.map_clean_and_borrowable_qubits(walk_circuit, qm=greedy_mm)
6868
assert len(walk_circuit.all_qubits()) < 23
6969
qubit_order = cirq.QubitOrder.explicit(

cirq-ft/cirq_ft/algos/reflection_using_prepare_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def keep(op: cirq.Operation):
4444

4545

4646
def greedily_allocate_ancilla(circuit: cirq.AbstractCircuit) -> cirq.Circuit:
47-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="ancilla", maximize_reuse=True)
47+
greedy_mm = cirq.GreedyQubitManager(prefix="ancilla", maximize_reuse=True)
4848
circuit = cirq_ft.map_clean_and_borrowable_qubits(circuit, qm=greedy_mm)
4949
assert len(circuit.all_qubits()) < 30
5050
return circuit

cirq-ft/cirq_ft/algos/select_swap_qrom_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_select_swap_qrom(data, block_size):
2929
selection_q, selection_r = selection[: qrom.selection_q], selection[qrom.selection_q :]
3030
targets = [qubit_regs[f"target{i}"] for i in range(len(data))]
3131

32-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
32+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
3333
context = cirq.DecompositionContext(greedy_mm)
3434
qrom_circuit = cirq.Circuit(cirq.decompose(qrom.on_registers(**qubit_regs), context=context))
3535

cirq-ft/cirq_ft/algos/selected_majorana_fermion_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def test_selected_majorana_fermion_gate_decomposed_diagram():
100100
cirq_ft.SelectionRegister('selection', selection_bitsize, target_bitsize),
101101
target_gate=cirq.X,
102102
)
103-
greedy_mm = cirq_ft.GreedyQubitManager(prefix="_a", maximize_reuse=True)
103+
greedy_mm = cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True)
104104
g = cirq_ft.testing.GateHelper(gate)
105105
context = cirq.DecompositionContext(greedy_mm)
106106
circuit = cirq.Circuit(cirq.decompose_once(g.operation, context=context))

cirq-ft/cirq_ft/algos/unary_iteration_gate.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,13 @@ def unary_iteration(
182182
Users can write multi-dimensional coherent for loops as follows:
183183
184184
>>> import cirq
185-
>>> from cirq_ft import unary_iteration, GreedyQubitManager
185+
>>> from cirq_ft import unary_iteration
186186
>>> N, M = 5, 7
187187
>>> target = [[cirq.q(f't({i}, {j})') for j in range(M)] for i in range(N)]
188188
>>> selection = [[cirq.q(f's({i}, {j})') for j in range(3)] for i in range(3)]
189189
>>> circuit = cirq.Circuit()
190190
>>> i_ops = []
191-
>>> qm = GreedyQubitManager("ancilla", maximize_reuse=True)
191+
>>> qm = cirq.GreedyQubitManager("ancilla", maximize_reuse=True)
192192
>>> for i_optree, i_ctrl, i in unary_iteration(0, N, i_ops, [], selection[0], qm):
193193
... circuit.append(i_optree)
194194
... j_ops = []

0 commit comments

Comments
 (0)