Skip to content

Commit 5aabc66

Browse files
authored
[Costs] Qubit Counts (#969)
* qubit counts * fixes * SymbolicInt * lint * merge conflicts * fixing the fixes
1 parent b9429f6 commit 5aabc66

5 files changed

Lines changed: 276 additions & 62 deletions

File tree

qualtran/resource_counting/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@
3232
from ._costing import GeneralizerT, get_cost_value, get_cost_cache, query_costs, CostKey, CostValT
3333

3434
from ._success_prob import SuccessProb
35+
from ._qubit_counts import QubitCount
3536

3637
from . import generalizers

qualtran/resource_counting/_qubit_counting.py

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright 2023 Google LLC
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 logging
16+
from typing import Callable, Set
17+
18+
import networkx as nx
19+
from attrs import frozen
20+
21+
from qualtran import Bloq, Connection, DanglingT, DecomposeNotImplementedError, DecomposeTypeError
22+
from qualtran._infra.composite_bloq import _binst_to_cxns
23+
from qualtran.symbolics import smax, SymbolicInt
24+
25+
from ._call_graph import get_bloq_callee_counts
26+
from ._costing import CostKey
27+
28+
logger = logging.getLogger(__name__)
29+
30+
31+
def _cbloq_max_width(
32+
binst_graph: nx.DiGraph, _bloq_max_width: Callable[[Bloq], SymbolicInt] = lambda b: 0
33+
) -> SymbolicInt:
34+
"""Get the maximum width of a composite bloq.
35+
36+
Specifically, we treat each binst in series. The width at each inter-bloq time point
37+
is the sum of the bitsizes of all the connections that are "in play". The width at each
38+
during-a-binst time point is the sum of the binst width (which is provided by the
39+
`_bloq_max_width` callable) and the bystander connections that are "in play". The max
40+
width is the maximum over all the time points.
41+
42+
If the dataflow graph has more than one connected component, we treat each component
43+
independently.
44+
"""
45+
max_width: SymbolicInt = 0
46+
in_play: Set[Connection] = set()
47+
48+
for cc in nx.weakly_connected_components(binst_graph):
49+
for binst in nx.topological_sort(binst_graph.subgraph(cc)):
50+
pred_cxns, succ_cxns = _binst_to_cxns(binst, binst_graph=binst_graph)
51+
52+
# Remove inbound connections from those that are 'in play'.
53+
for cxn in pred_cxns:
54+
in_play.remove(cxn)
55+
56+
if not isinstance(binst, DanglingT):
57+
# During the application of the binst, we have "observer" connections that have
58+
# width as well as the width from the binst itself. We consider the case where
59+
# the bloq may have a max_width greater than the max of its left/right registers.
60+
during_size = _bloq_max_width(binst.bloq) + sum(s.shape for s in in_play)
61+
max_width = smax(max_width, during_size)
62+
63+
# After the binst, its successor connections are 'in play'.
64+
in_play.update(succ_cxns)
65+
after_size = sum(s.shape for s in in_play)
66+
max_width = smax(max_width, after_size)
67+
68+
return max_width
69+
70+
71+
@frozen
72+
class QubitCount(CostKey[SymbolicInt]):
73+
"""A cost estimating the number of qubits required to implement a bloq.
74+
75+
The number of qubits is bounded from below by the number of qubits implied by the signature.
76+
If a bloq has no callees, the size implied by the signature will be returned. Otherwise,
77+
this CostKey will try to compute the number of qubits by inspecting the decomposition.
78+
79+
In the decomposition, each (sub)bloq is considered to be executed sequentially. The "width"
80+
of the circuit (i.e. the number of qubits) at each sequence point is the number of qubits
81+
required by the subbloq (computed recursively) plus any "bystander" idling wires.
82+
83+
This is an estimate for the number of qubits required by an algorithm. Specifically:
84+
- Bloqs are assumed to be executed sequentially, minimizing the number of qubits potentially
85+
at the expense of greater circuit depth or execution time.
86+
- We do not consider "tetris-ing" subbloqs. In a decomposition, each subbloq is assumed
87+
to be using all of its qubits for the duration of its execution. This could potentially
88+
overestimate the total number of qubits.
89+
90+
This Min-Max style estimate can provide a good balance between accuracy and scalability
91+
of the accounting. To fully account for each qubit and manage space-vs-time trade-offs,
92+
you must comprehensively decompose your algorithm to a `cirq.Circuit` of basic gates and
93+
use a `cirq.QubitManager` to manage trade-offs. This may be computationally expensive for
94+
large algorithms.
95+
"""
96+
97+
def compute(
98+
self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], SymbolicInt]
99+
) -> SymbolicInt:
100+
"""Compute an estimate of the number of qubits used by `bloq`.
101+
102+
See the class docstring for more information.
103+
"""
104+
# Base case: No callees; use the signature
105+
min_bloq_size = bloq.signature.n_qubits()
106+
callees = get_bloq_callee_counts(bloq)
107+
if len(callees) == 0:
108+
logger.info("Computing %s for %s from signature", self, bloq)
109+
return min_bloq_size
110+
111+
# Compute the number of qubits ("width") from the bloq's decomposition. We forward
112+
# the `get_callee_cost` function so this can recurse into subbloqs.
113+
try:
114+
cbloq = bloq.decompose_bloq()
115+
logger.info("Computing %s for %s from its decomposition", self, bloq)
116+
return _cbloq_max_width(cbloq._binst_graph, get_callee_cost)
117+
except (DecomposeNotImplementedError, DecomposeTypeError):
118+
pass
119+
120+
# No decomposition specified, but callees present. Take the simple maximum of
121+
# all the callees' sizes. This is likely an under-estimate.
122+
tot: int = min_bloq_size
123+
logger.info("Computing %s for %s from %d callee(s)", self, bloq, len(callees))
124+
for callee, n in callees:
125+
tot = smax(tot, get_callee_cost(callee))
126+
return tot
127+
128+
def zero(self) -> SymbolicInt:
129+
"""Zero cost is zero qubits."""
130+
return 0
131+
132+
def __str__(self):
133+
return 'qubit count'

qualtran/resource_counting/_qubit_counting_test.py renamed to qualtran/resource_counting/_qubit_counts_test.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import pytest
1516
import sympy
1617

18+
import qualtran.testing as qlt_testing
19+
from qualtran import QAny
20+
from qualtran.bloqs.basic_gates import Swap, TwoBitSwap
21+
from qualtran.bloqs.bookkeeping import Allocate, Free
1722
from qualtran.bloqs.for_testing.interior_alloc import InteriorAlloc
1823
from qualtran.bloqs.for_testing.with_decomposition import (
1924
TestIndependentParallelCombo,
2025
TestSerialCombo,
2126
)
2227
from qualtran.drawing import show_bloq
23-
from qualtran.resource_counting._qubit_counting import _cbloq_max_width
28+
from qualtran.resource_counting import get_cost_cache, QubitCount
29+
from qualtran.resource_counting._qubit_counts import _cbloq_max_width
30+
from qualtran.resource_counting.generalizers import ignore_split_join
2431

2532

2633
def test_max_width_interior_alloc_symb():
@@ -54,3 +61,20 @@ def test_max_width_simple():
5461
show_bloq(TestSerialCombo().decompose_bloq())
5562
max_width = _cbloq_max_width(TestSerialCombo().decompose_bloq()._binst_graph)
5663
assert max_width == 1
64+
65+
66+
def test_qubit_count_cost():
67+
bloq = InteriorAlloc(n=10)
68+
qubit_counts = get_cost_cache(bloq, QubitCount(), generalizer=ignore_split_join)
69+
assert qubit_counts == {
70+
InteriorAlloc(n=10): 30,
71+
Allocate(QAny(10)): 10,
72+
Free(QAny(10)): 10,
73+
Swap(10): 20,
74+
TwoBitSwap(): 2,
75+
}
76+
77+
78+
@pytest.mark.notebook
79+
def test_notebook():
80+
qlt_testing.execute_notebook("qubit_counts")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "c3a2e0b1-d7f3-4761-aaf7-9fe1b4202c63",
6+
"metadata": {},
7+
"source": [
8+
"# Qubit Counts\n",
9+
"\n",
10+
"The number of qubits is an important cost for running a quantum algorithm. The provided `QubitCounts()` cost key can efficiently estimate the qubit count of even large-scale algorithms by exploiting the hierarchical structure of bloq decomposition.\n",
11+
"\n",
12+
"\n",
13+
"The number of qubits is bounded from below by the number of qubits implied by the signature.\n",
14+
"If a bloq has no callees, the size implied by the signature will be returned. Otherwise,\n",
15+
"`QubitCounts()` will try to compute the number of qubits by inspecting the decomposition.\n",
16+
"\n",
17+
"In the decomposition, each (sub)bloq is considered to be executed sequentially. The \"width\"\n",
18+
"of the circuit (i.e. the number of qubits) at each sequence point is the number of qubits\n",
19+
"required by the subbloq (computed recursively) plus any \"bystander\" idling wires.\n",
20+
"\n",
21+
"This is an estimate for the number of qubits required by an algorithm. Specifically:\n",
22+
" - Bloqs are assumed to be executed sequentially, minimizing the number of qubits potentially\n",
23+
" at the expense of greater circuit depth or execution time.\n",
24+
" - We do not consider \"tetris-ing\" subbloqs. In a decomposition, each subbloq is assumed\n",
25+
" to be using all of its qubits for the duration of its execution. This could potentially\n",
26+
" overestimate the total number of qubits.\n",
27+
"\n",
28+
"This Min-Max style estimate can provide a good balance between accuracy and scalability\n",
29+
"of the accounting. To fully account for each qubit and manage space-vs-time trade-offs,\n",
30+
"you must comprehensively decompose your algorithm to a `cirq.Circuit` of basic gates and\n",
31+
"use a `cirq.QubitManager` to manage trade-offs. This may be computationally expensive for\n",
32+
"large algorithms."
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"id": "ddb82393-8bb0-42d1-9b84-6b3e22455f7e",
39+
"metadata": {},
40+
"outputs": [],
41+
"source": [
42+
"import sympy\n",
43+
"\n",
44+
"from qualtran.drawing import show_bloq\n",
45+
"\n",
46+
"from qualtran.bloqs.for_testing.interior_alloc import InteriorAlloc\n",
47+
"from qualtran.resource_counting import get_cost_value, query_costs, QubitCount"
48+
]
49+
},
50+
{
51+
"cell_type": "markdown",
52+
"id": "58f0823f-f76f-4adb-8f2a-a25d1e2ee070",
53+
"metadata": {},
54+
"source": [
55+
"For illustrative purposes, we use a bloq that has two $n$ bit registers, but allocates an additional $n$ bit register as part of its decomposition. Looking purely at the signature, you would conclude that the bloq uses $2n$ qubits; but by looking at the decomposition we can see that at its maximum circuit width it uses $3n$ qubits. "
56+
]
57+
},
58+
{
59+
"cell_type": "code",
60+
"execution_count": null,
61+
"id": "2a5768b5-7f2e-4851-bc15-3be8a66df4f5",
62+
"metadata": {},
63+
"outputs": [],
64+
"source": [
65+
"n = sympy.Symbol('n', positive=True, integer=True)\n",
66+
"bloq = InteriorAlloc(n=n)\n",
67+
"show_bloq(bloq)\n",
68+
"show_bloq(bloq.decompose_bloq(), 'musical_score')"
69+
]
70+
},
71+
{
72+
"cell_type": "code",
73+
"execution_count": null,
74+
"id": "329c02d8-35b0-4ed8-881b-0bb5e6856813",
75+
"metadata": {},
76+
"outputs": [],
77+
"source": [
78+
"get_cost_value(bloq, QubitCount())"
79+
]
80+
},
81+
{
82+
"cell_type": "code",
83+
"execution_count": null,
84+
"id": "21a84a95-1ed6-4ce1-8c43-512621efdbdf",
85+
"metadata": {},
86+
"outputs": [],
87+
"source": [
88+
"from qualtran.drawing import GraphvizCallGraph\n",
89+
"\n",
90+
"g, _ = bloq.call_graph()\n",
91+
"costs = query_costs(bloq, [QubitCount()])\n",
92+
"GraphvizCallGraph(g, costs).get_svg()"
93+
]
94+
}
95+
],
96+
"metadata": {
97+
"kernelspec": {
98+
"display_name": "Python 3 (ipykernel)",
99+
"language": "python",
100+
"name": "python3"
101+
},
102+
"language_info": {
103+
"codemirror_mode": {
104+
"name": "ipython",
105+
"version": 3
106+
},
107+
"file_extension": ".py",
108+
"mimetype": "text/x-python",
109+
"name": "python",
110+
"nbconvert_exporter": "python",
111+
"pygments_lexer": "ipython3",
112+
"version": "3.11.8"
113+
}
114+
},
115+
"nbformat": 4,
116+
"nbformat_minor": 5
117+
}

0 commit comments

Comments
 (0)