Skip to content
This repository has been archived by the owner on Jul 13, 2022. It is now read-only.

Commit

Permalink
Optimizations to randomized benchmarking (qiskit-community#40)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gadial authored May 26, 2021
1 parent 94d6ef6 commit a41490d
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 11 deletions.
233 changes: 233 additions & 0 deletions qiskit_experiments/randomized_benchmarking/clifford_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Utilities for using the Clifford group in randomized benchmarking
"""

from typing import Optional, Union
from functools import lru_cache
from numpy.random import Generator, default_rng
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit import Gate
from qiskit.circuit.library import SdgGate, HGate, SGate
from qiskit.quantum_info import Clifford, random_clifford


class VGate(Gate):
"""V Gate used in Clifford synthesis."""

def __init__(self):
"""Create new V Gate."""
super().__init__("v", 1, [])

def _define(self):
"""V Gate definition."""
q = QuantumRegister(1, "q")
qc = QuantumCircuit(q)
qc.data = [(SdgGate(), [q[0]], []), (HGate(), [q[0]], [])]
self.definition = qc


class WGate(Gate):
"""W Gate used in Clifford synthesis."""

def __init__(self):
"""Create new W Gate."""
super().__init__("w", 1, [])

def _define(self):
"""W Gate definition."""
q = QuantumRegister(1, "q")
qc = QuantumCircuit(q)
qc.data = [(HGate(), [q[0]], []), (SGate(), [q[0]], [])]
self.definition = qc


def v(self, q):
"""Apply V to q."""
return self.append(VGate(), [q], [])


def w(self, q):
"""Apply W to q."""
return self.append(WGate(), [q], [])


QuantumCircuit.v = v
QuantumCircuit.v = w


class CliffordUtils:
"""Utilities for generating 1 and 2 qubit clifford circuits and elements"""

NUM_CLIFFORD_1_QUBIT = 24
NUM_CLIFFORD_2_QUBIT = 11520
CLIFFORD_1_QUBIT_SIG = (2, 3, 4)
CLIFFORD_2_QUBIT_SIGS = [
(2, 2, 3, 3, 4, 4),
(2, 2, 3, 3, 3, 3, 4, 4),
(2, 2, 3, 3, 3, 3, 4, 4),
(2, 2, 3, 3, 4, 4),
]

def clifford_1_qubit(self, num):
"""Return the 1-qubit clifford element corresponding to `num`
where `num` is between 0 and 23.
"""
return Clifford(self.clifford_1_qubit_circuit(num))

def clifford_2_qubit(self, num):
"""Return the 2-qubit clifford element corresponding to `num`
where `num` is between 0 and 11519.
"""
return Clifford(self.clifford_2_qubit_circuit(num))

def random_cliffords(
self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None
):
"""Generate a list of random clifford elements"""
if num_qubits > 2:
return random_clifford(num_qubits, seed=rng)

if rng is None:
rng = default_rng()

if isinstance(rng, int):
rng = default_rng(rng)

if num_qubits == 1:
samples = rng.integers(24, size=size)
return [Clifford(self.clifford_1_qubit_circuit(i)) for i in samples]
else:
samples = rng.integers(11520, size=size)
return [Clifford(self.clifford_2_qubit_circuit(i)) for i in samples]

def random_clifford_circuits(
self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None
):
"""Generate a list of random clifford circuits"""
if num_qubits > 2:
return [random_clifford(num_qubits, seed=rng).to_circuit() for _ in range(size)]

if rng is None:
rng = default_rng()

if isinstance(rng, int):
rng = default_rng(rng)

if num_qubits == 1:
samples = rng.integers(24, size=size)
return [self.clifford_1_qubit_circuit(i) for i in samples]
else:
samples = rng.integers(11520, size=size)
return [self.clifford_2_qubit_circuit(i) for i in samples]

@lru_cache(maxsize=24)
def clifford_1_qubit_circuit(self, num):
"""Return the 1-qubit clifford circuit corresponding to `num`
where `num` is between 0 and 23.
"""
# pylint: disable=unbalanced-tuple-unpacking
# This is safe since `_unpack_num` returns list the size of the sig
(i, j, p) = self._unpack_num(num, self.CLIFFORD_1_QUBIT_SIG)
qc = QuantumCircuit(1)
if i == 1:
qc.h(0)
if j == 1:
qc.v(0)
if j == 2:
qc.w(0)
if p == 1:
qc.x(0)
if p == 2:
qc.y(0)
if p == 3:
qc.z(0)
return qc

@lru_cache(maxsize=11520)
def clifford_2_qubit_circuit(self, num):
"""Return the 2-qubit clifford circuit corresponding to `num`
where `num` is between 0 and 11519.
"""
vals = self._unpack_num_multi_sigs(num, self.CLIFFORD_2_QUBIT_SIGS)
qc = QuantumCircuit(2)
if vals[0] == 0 or vals[0] == 3:
(form, i0, i1, j0, j1, p0, p1) = vals
else:
(form, i0, i1, j0, j1, k0, k1, p0, p1) = vals
if i0 == 1:
qc.h(0)
if i1 == 1:
qc.h(1)
if j0 == 1:
qc.v(0)
if j0 == 2:
qc.w(0)
if j1 == 1:
qc.v(1)
if j1 == 2:
qc.w(1)
if form in (1, 2, 3):
qc.cx(0, 1)
if form in (2, 3):
qc.cx(1, 0)
if form == 3:
qc.cx(0, 1)
if form in (1, 2):
if k0 == 1:
qc.v(0)
if k0 == 2:
qc.w(0)
if k1 == 1:
qc.v(1)
if k1 == 2:
qc.v(1)
qc.v(1)
if p0 == 1:
qc.x(0)
if p0 == 2:
qc.y(0)
if p0 == 3:
qc.z(0)
if p1 == 1:
qc.x(1)
if p1 == 2:
qc.y(1)
if p1 == 3:
qc.z(1)
return qc

def _unpack_num(self, num, sig):
r"""Returns a tuple :math:`(a_1, \ldots, a_n)` where
:math:`0 \le a_i \le \sigma_i` where
sig=:math:`(\sigma_1, \ldots, \sigma_n)` and num is the sequential
number of the tuple
"""
res = []
for k in sig:
res.append(num % k)
num //= k
return res

def _unpack_num_multi_sigs(self, num, sigs):
"""Returns the result of `_unpack_num` on one of the
signatures in `sigs`
"""
for i, sig in enumerate(sigs):
sig_size = 1
for k in sig:
sig_size *= k
if num < sig_size:
return [i] + self._unpack_num(num, sig)
num -= sig_size
return None
4 changes: 2 additions & 2 deletions qiskit_experiments/randomized_benchmarking/rb_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from typing import Optional, List

from qiskit_experiments.base_analysis import BaseAnalysis
from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData
from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data
from qiskit_experiments.analysis.data_processing import (
level2_probability,
Expand All @@ -35,7 +35,7 @@ class RBAnalysis(BaseAnalysis):
# pylint: disable = arguments-differ, invalid-name
def _run_analysis(
self,
experiment_data: "ExperimentData",
experiment_data: ExperimentData,
p0: Optional[List[float]] = None,
plot: bool = True,
ax: Optional["AxesSubplot"] = None,
Expand Down
23 changes: 14 additions & 9 deletions qiskit_experiments/randomized_benchmarking/rb_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@

from qiskit import QuantumCircuit
from qiskit.providers import Backend
from qiskit.quantum_info import Clifford, random_clifford
from qiskit.quantum_info import Clifford

from qiskit_experiments.base_experiment import BaseExperiment
from .rb_analysis import RBAnalysis
from .clifford_utils import CliffordUtils


class RBExperiment(BaseExperiment):
Expand Down Expand Up @@ -61,6 +62,7 @@ def __init__(
self._lengths = list(lengths)
self._num_samples = num_samples
self._full_sampling = full_sampling
self._clifford_utils = CliffordUtils()
super().__init__(qubits)

# pylint: disable = arguments-differ
Expand Down Expand Up @@ -112,7 +114,7 @@ def _sample_circuits(
"""
circuits = []
for length in lengths if self._full_sampling else [lengths[-1]]:
elements = [random_clifford(self.num_qubits, seed=seed) for _ in range(length)]
elements = self._clifford_utils.random_clifford_circuits(self.num_qubits, length, seed)
element_lengths = [len(elements)] if self._full_sampling else lengths
circuits += self._generate_circuit(elements, element_lengths)
return circuits
Expand All @@ -136,18 +138,21 @@ def _generate_circuit(
qubits = list(range(self.num_qubits))
circuits = []

circ = QuantumCircuit(self.num_qubits)
circ.barrier(qubits)
circs = [QuantumCircuit(self.num_qubits) for _ in range(len(lengths))]
for circ in circs:
circ.barrier(qubits)
circ_op = Clifford(np.eye(2 * self.num_qubits))

for current_length, group_elt in enumerate(elements):
circ_op = circ_op.compose(group_elt)
circ.append(group_elt, qubits)
circ.barrier(qubits)
for current_length, group_elt_circ in enumerate(elements):
group_elt_gate = group_elt_circ.to_gate()
circ_op = circ_op.compose(Clifford(group_elt_circ))
for circ in circs:
circ.append(group_elt_gate, qubits)
circ.barrier(qubits)
if current_length + 1 in lengths:
# copy circuit and add inverse
inv = circ_op.adjoint()
rb_circ = circ.copy()
rb_circ = circs.pop()
rb_circ.append(inv, qubits)
rb_circ.barrier(qubits)
rb_circ.metadata = {
Expand Down

0 comments on commit a41490d

Please sign in to comment.