Skip to content

Commit b49e278

Browse files
committed
qubit counts
1 parent b5b9ccd commit b49e278

File tree

5 files changed

+269
-62
lines changed

5 files changed

+269
-62
lines changed

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 ._bloq_counts import BloqCount, QECGatesCost
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 abc
16+
import logging
17+
from typing import Callable, Dict, Generic, Sequence, Set, Tuple, TYPE_CHECKING, Union
18+
19+
import networkx as nx
20+
import sympy
21+
from attrs import frozen
22+
23+
from qualtran import Bloq, Connection, DanglingT, DecomposeNotImplementedError, DecomposeTypeError
24+
from qualtran._infra.composite_bloq import _binst_to_cxns
25+
26+
from ._call_graph import get_bloq_callee_counts
27+
from ._costing import CostKey
28+
from .symbolic_counting_utils import smax
29+
30+
logger = logging.getLogger(__name__)
31+
32+
33+
def _cbloq_max_width(
34+
binst_graph: nx.DiGraph, _bloq_max_width: Callable[[Bloq], int] = lambda b: 0
35+
) -> Union[int, sympy.Expr]:
36+
"""Get the maximum width of a composite bloq.
37+
38+
Specifically, we treat each binst in series. The width at each inter-bloq time point
39+
is the sum of the bitsizes of all the connections that are "in play". The width at each
40+
during-a-binst time point is the sum of the binst width (which is provided by the
41+
`_bloq_max_width` callable) and the bystander connections that are "in play". The max
42+
width is the maximum over all the time points.
43+
44+
If the dataflow graph has more than one connected component, we treat each component
45+
independently.
46+
"""
47+
max_width: Union[int, sympy.Expr] = 0
48+
in_play: Set[Connection] = set()
49+
50+
for cc in nx.weakly_connected_components(binst_graph):
51+
for binst in nx.topological_sort(binst_graph.subgraph(cc)):
52+
pred_cxns, succ_cxns = _binst_to_cxns(binst, binst_graph=binst_graph)
53+
54+
# Remove inbound connections from those that are 'in play'.
55+
for cxn in pred_cxns:
56+
in_play.remove(cxn)
57+
58+
if not isinstance(binst, DanglingT):
59+
# During the application of the binst, we have "observer" connections that have
60+
# width as well as the width from the binst itself. We consider the case where
61+
# the bloq may have a max_width greater than the max of its left/right registers.
62+
during_size = _bloq_max_width(binst.bloq) + sum(s.shape for s in in_play)
63+
max_width = smax(max_width, during_size)
64+
65+
# After the binst, its successor connections are 'in play'.
66+
in_play.update(succ_cxns)
67+
after_size = sum(s.shape for s in in_play)
68+
max_width = smax(max_width, after_size)
69+
70+
return max_width
71+
72+
73+
@frozen
74+
class QubitCount(CostKey[int]):
75+
"""A cost estimating the number of qubits required to implement a bloq.
76+
77+
The number of qubits is bounded from below by the number of qubits implied by the signature.
78+
If a bloq has no callees, the size implied by the signature will be returned. Otherwise,
79+
this CostKey will try to compute the number of qubits by inspecting the decomposition.
80+
81+
In the decomposition, each (sub)bloq is considered to be executed sequentially. The "width"
82+
of the circuit (i.e. the number of qubits) at each sequence point is the number of qubits
83+
required by the subbloq (computed recursively) plus any "bystander" idling wires.
84+
85+
This is an estimate for the number of qubits required by an algorithm. Specifically:
86+
- Bloqs are assumed to be executed sequentially, minimizing the number of qubits potentially
87+
at the expense of greater circuit depth or execution time.
88+
- We do not consider "tetris-ing" subbloqs. In a decomposition, each subbloq is assumed
89+
to be using all of its qubits for the duration of its execution. This could potentially
90+
overestimate the total number of qubits.
91+
92+
This Min-Max style estimate can provide a good balance between accuracy and scalability
93+
of the accounting. To fully account for each qubit and manage space-vs-time trade-offs,
94+
you must comprehensively decompose your algorithm to a `cirq.Circuit` of basic gates and
95+
use a `cirq.QubitManager` to manage trade-offs. This may be computationally expensive for
96+
large algorithms.
97+
"""
98+
99+
def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], int]) -> int:
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) -> int:
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: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414

1515
import sympy
1616

17+
from qualtran import QAny
18+
from qualtran.bloqs.basic_gates import Swap, TwoBitSwap
1719
from qualtran.bloqs.for_testing.interior_alloc import InteriorAlloc
1820
from qualtran.bloqs.for_testing.with_decomposition import (
1921
TestIndependentParallelCombo,
2022
TestSerialCombo,
2123
)
24+
from qualtran.bloqs.util_bloqs import Allocate, Free
2225
from qualtran.drawing import show_bloq
23-
from qualtran.resource_counting._qubit_counting import _cbloq_max_width
26+
from qualtran.resource_counting import get_cost_cache, QubitCount
27+
from qualtran.resource_counting._qubit_counts import _cbloq_max_width
28+
from qualtran.resource_counting.generalizers import ignore_split_join
2429

2530

2631
def test_max_width_interior_alloc_symb():
@@ -54,3 +59,15 @@ def test_max_width_simple():
5459
show_bloq(TestSerialCombo().decompose_bloq())
5560
max_width = _cbloq_max_width(TestSerialCombo().decompose_bloq()._binst_graph)
5661
assert max_width == 1
62+
63+
64+
def test_qubit_count_cost():
65+
bloq = InteriorAlloc(n=10)
66+
qubit_counts = get_cost_cache(bloq, QubitCount(), generalizer=ignore_split_join)
67+
assert qubit_counts == {
68+
InteriorAlloc(n=10): 30,
69+
Allocate(QAny(10)): 10,
70+
Free(QAny(10)): 10,
71+
Swap(10): 20,
72+
TwoBitSwap(): 2,
73+
}
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.7"
113+
}
114+
},
115+
"nbformat": 4,
116+
"nbformat_minor": 5
117+
}

0 commit comments

Comments
 (0)