Skip to content

Commit 3fd4813

Browse files
authored
Optimizations to randomized benchmarking (#40)
* Do not use QuantumCircuit.copy to avoid overheads * Using precomputed clifford data for 1 and 2 qubits * Improvements to the Clifford-generation optimization * Testing an alternative way to generate the inverse circuit * Testing clifford generation from circuit instead of saving in a file * Removed storing cliffords in json, added LRU cache for circuit generation * Linting and doc * Linting after merge * Adding W gate synthesis to the clifford utils
1 parent a733fe7 commit 3fd4813

File tree

3 files changed

+249
-11
lines changed

3 files changed

+249
-11
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2021.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
"""
13+
Utilities for using the Clifford group in randomized benchmarking
14+
"""
15+
16+
from typing import Optional, Union
17+
from functools import lru_cache
18+
from numpy.random import Generator, default_rng
19+
from qiskit import QuantumCircuit, QuantumRegister
20+
from qiskit.circuit import Gate
21+
from qiskit.circuit.library import SdgGate, HGate, SGate
22+
from qiskit.quantum_info import Clifford, random_clifford
23+
24+
25+
class VGate(Gate):
26+
"""V Gate used in Clifford synthesis."""
27+
28+
def __init__(self):
29+
"""Create new V Gate."""
30+
super().__init__("v", 1, [])
31+
32+
def _define(self):
33+
"""V Gate definition."""
34+
q = QuantumRegister(1, "q")
35+
qc = QuantumCircuit(q)
36+
qc.data = [(SdgGate(), [q[0]], []), (HGate(), [q[0]], [])]
37+
self.definition = qc
38+
39+
40+
class WGate(Gate):
41+
"""W Gate used in Clifford synthesis."""
42+
43+
def __init__(self):
44+
"""Create new W Gate."""
45+
super().__init__("w", 1, [])
46+
47+
def _define(self):
48+
"""W Gate definition."""
49+
q = QuantumRegister(1, "q")
50+
qc = QuantumCircuit(q)
51+
qc.data = [(HGate(), [q[0]], []), (SGate(), [q[0]], [])]
52+
self.definition = qc
53+
54+
55+
def v(self, q):
56+
"""Apply V to q."""
57+
return self.append(VGate(), [q], [])
58+
59+
60+
def w(self, q):
61+
"""Apply W to q."""
62+
return self.append(WGate(), [q], [])
63+
64+
65+
QuantumCircuit.v = v
66+
QuantumCircuit.v = w
67+
68+
69+
class CliffordUtils:
70+
"""Utilities for generating 1 and 2 qubit clifford circuits and elements"""
71+
72+
NUM_CLIFFORD_1_QUBIT = 24
73+
NUM_CLIFFORD_2_QUBIT = 11520
74+
CLIFFORD_1_QUBIT_SIG = (2, 3, 4)
75+
CLIFFORD_2_QUBIT_SIGS = [
76+
(2, 2, 3, 3, 4, 4),
77+
(2, 2, 3, 3, 3, 3, 4, 4),
78+
(2, 2, 3, 3, 3, 3, 4, 4),
79+
(2, 2, 3, 3, 4, 4),
80+
]
81+
82+
def clifford_1_qubit(self, num):
83+
"""Return the 1-qubit clifford element corresponding to `num`
84+
where `num` is between 0 and 23.
85+
"""
86+
return Clifford(self.clifford_1_qubit_circuit(num))
87+
88+
def clifford_2_qubit(self, num):
89+
"""Return the 2-qubit clifford element corresponding to `num`
90+
where `num` is between 0 and 11519.
91+
"""
92+
return Clifford(self.clifford_2_qubit_circuit(num))
93+
94+
def random_cliffords(
95+
self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None
96+
):
97+
"""Generate a list of random clifford elements"""
98+
if num_qubits > 2:
99+
return random_clifford(num_qubits, seed=rng)
100+
101+
if rng is None:
102+
rng = default_rng()
103+
104+
if isinstance(rng, int):
105+
rng = default_rng(rng)
106+
107+
if num_qubits == 1:
108+
samples = rng.integers(24, size=size)
109+
return [Clifford(self.clifford_1_qubit_circuit(i)) for i in samples]
110+
else:
111+
samples = rng.integers(11520, size=size)
112+
return [Clifford(self.clifford_2_qubit_circuit(i)) for i in samples]
113+
114+
def random_clifford_circuits(
115+
self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None
116+
):
117+
"""Generate a list of random clifford circuits"""
118+
if num_qubits > 2:
119+
return [random_clifford(num_qubits, seed=rng).to_circuit() for _ in range(size)]
120+
121+
if rng is None:
122+
rng = default_rng()
123+
124+
if isinstance(rng, int):
125+
rng = default_rng(rng)
126+
127+
if num_qubits == 1:
128+
samples = rng.integers(24, size=size)
129+
return [self.clifford_1_qubit_circuit(i) for i in samples]
130+
else:
131+
samples = rng.integers(11520, size=size)
132+
return [self.clifford_2_qubit_circuit(i) for i in samples]
133+
134+
@lru_cache(maxsize=24)
135+
def clifford_1_qubit_circuit(self, num):
136+
"""Return the 1-qubit clifford circuit corresponding to `num`
137+
where `num` is between 0 and 23.
138+
"""
139+
# pylint: disable=unbalanced-tuple-unpacking
140+
# This is safe since `_unpack_num` returns list the size of the sig
141+
(i, j, p) = self._unpack_num(num, self.CLIFFORD_1_QUBIT_SIG)
142+
qc = QuantumCircuit(1)
143+
if i == 1:
144+
qc.h(0)
145+
if j == 1:
146+
qc.v(0)
147+
if j == 2:
148+
qc.w(0)
149+
if p == 1:
150+
qc.x(0)
151+
if p == 2:
152+
qc.y(0)
153+
if p == 3:
154+
qc.z(0)
155+
return qc
156+
157+
@lru_cache(maxsize=11520)
158+
def clifford_2_qubit_circuit(self, num):
159+
"""Return the 2-qubit clifford circuit corresponding to `num`
160+
where `num` is between 0 and 11519.
161+
"""
162+
vals = self._unpack_num_multi_sigs(num, self.CLIFFORD_2_QUBIT_SIGS)
163+
qc = QuantumCircuit(2)
164+
if vals[0] == 0 or vals[0] == 3:
165+
(form, i0, i1, j0, j1, p0, p1) = vals
166+
else:
167+
(form, i0, i1, j0, j1, k0, k1, p0, p1) = vals
168+
if i0 == 1:
169+
qc.h(0)
170+
if i1 == 1:
171+
qc.h(1)
172+
if j0 == 1:
173+
qc.v(0)
174+
if j0 == 2:
175+
qc.w(0)
176+
if j1 == 1:
177+
qc.v(1)
178+
if j1 == 2:
179+
qc.w(1)
180+
if form in (1, 2, 3):
181+
qc.cx(0, 1)
182+
if form in (2, 3):
183+
qc.cx(1, 0)
184+
if form == 3:
185+
qc.cx(0, 1)
186+
if form in (1, 2):
187+
if k0 == 1:
188+
qc.v(0)
189+
if k0 == 2:
190+
qc.w(0)
191+
if k1 == 1:
192+
qc.v(1)
193+
if k1 == 2:
194+
qc.v(1)
195+
qc.v(1)
196+
if p0 == 1:
197+
qc.x(0)
198+
if p0 == 2:
199+
qc.y(0)
200+
if p0 == 3:
201+
qc.z(0)
202+
if p1 == 1:
203+
qc.x(1)
204+
if p1 == 2:
205+
qc.y(1)
206+
if p1 == 3:
207+
qc.z(1)
208+
return qc
209+
210+
def _unpack_num(self, num, sig):
211+
r"""Returns a tuple :math:`(a_1, \ldots, a_n)` where
212+
:math:`0 \le a_i \le \sigma_i` where
213+
sig=:math:`(\sigma_1, \ldots, \sigma_n)` and num is the sequential
214+
number of the tuple
215+
"""
216+
res = []
217+
for k in sig:
218+
res.append(num % k)
219+
num //= k
220+
return res
221+
222+
def _unpack_num_multi_sigs(self, num, sigs):
223+
"""Returns the result of `_unpack_num` on one of the
224+
signatures in `sigs`
225+
"""
226+
for i, sig in enumerate(sigs):
227+
sig_size = 1
228+
for k in sig:
229+
sig_size *= k
230+
if num < sig_size:
231+
return [i] + self._unpack_num(num, sig)
232+
num -= sig_size
233+
return None

qiskit_experiments/randomized_benchmarking/rb_analysis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from typing import Optional, List
1717

18-
from qiskit_experiments.base_analysis import BaseAnalysis
18+
from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData
1919
from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data
2020
from qiskit_experiments.analysis.data_processing import (
2121
level2_probability,
@@ -35,7 +35,7 @@ class RBAnalysis(BaseAnalysis):
3535
# pylint: disable = arguments-differ, invalid-name
3636
def _run_analysis(
3737
self,
38-
experiment_data: "ExperimentData",
38+
experiment_data: ExperimentData,
3939
p0: Optional[List[float]] = None,
4040
plot: bool = True,
4141
ax: Optional["AxesSubplot"] = None,

qiskit_experiments/randomized_benchmarking/rb_experiment.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919

2020
from qiskit import QuantumCircuit
2121
from qiskit.providers import Backend
22-
from qiskit.quantum_info import Clifford, random_clifford
22+
from qiskit.quantum_info import Clifford
2323

2424
from qiskit_experiments.base_experiment import BaseExperiment
2525
from .rb_analysis import RBAnalysis
26+
from .clifford_utils import CliffordUtils
2627

2728

2829
class RBExperiment(BaseExperiment):
@@ -61,6 +62,7 @@ def __init__(
6162
self._lengths = list(lengths)
6263
self._num_samples = num_samples
6364
self._full_sampling = full_sampling
65+
self._clifford_utils = CliffordUtils()
6466
super().__init__(qubits)
6567

6668
# pylint: disable = arguments-differ
@@ -112,7 +114,7 @@ def _sample_circuits(
112114
"""
113115
circuits = []
114116
for length in lengths if self._full_sampling else [lengths[-1]]:
115-
elements = [random_clifford(self.num_qubits, seed=seed) for _ in range(length)]
117+
elements = self._clifford_utils.random_clifford_circuits(self.num_qubits, length, seed)
116118
element_lengths = [len(elements)] if self._full_sampling else lengths
117119
circuits += self._generate_circuit(elements, element_lengths)
118120
return circuits
@@ -136,18 +138,21 @@ def _generate_circuit(
136138
qubits = list(range(self.num_qubits))
137139
circuits = []
138140

139-
circ = QuantumCircuit(self.num_qubits)
140-
circ.barrier(qubits)
141+
circs = [QuantumCircuit(self.num_qubits) for _ in range(len(lengths))]
142+
for circ in circs:
143+
circ.barrier(qubits)
141144
circ_op = Clifford(np.eye(2 * self.num_qubits))
142145

143-
for current_length, group_elt in enumerate(elements):
144-
circ_op = circ_op.compose(group_elt)
145-
circ.append(group_elt, qubits)
146-
circ.barrier(qubits)
146+
for current_length, group_elt_circ in enumerate(elements):
147+
group_elt_gate = group_elt_circ.to_gate()
148+
circ_op = circ_op.compose(Clifford(group_elt_circ))
149+
for circ in circs:
150+
circ.append(group_elt_gate, qubits)
151+
circ.barrier(qubits)
147152
if current_length + 1 in lengths:
148153
# copy circuit and add inverse
149154
inv = circ_op.adjoint()
150-
rb_circ = circ.copy()
155+
rb_circ = circs.pop()
151156
rb_circ.append(inv, qubits)
152157
rb_circ.barrier(qubits)
153158
rb_circ.metadata = {

0 commit comments

Comments
 (0)