-
Notifications
You must be signed in to change notification settings - Fork 134
[WIP] New algorithm for RB using transpiled Cliffords #851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d81eed
c836831
86cad33
9a873dc
76ff366
ab7d4f3
274432a
890d890
8dc9aa1
f0c3cc8
10a7e9f
aea856a
eec3721
dd0b62b
b9c9f41
92f5580
3e9c386
4557572
0673e23
c543fa4
3c2dca8
b9e0884
f899247
2bc7469
bbaf218
402bf69
67f07ca
d72beb1
7f9778f
af42386
95125ed
d5949f4
a9e3146
de1d6ff
b79b5bf
96046a5
352539e
b10b1c6
0cef2b7
eab49ec
16fe2e1
e2fe641
6c4662e
2654c1a
a27ded2
6c38ad1
8c47f94
5882832
5d0b893
bbb91ea
3788e0f
ce72dd9
57a6ab0
eed14b7
93dfec7
4408e31
04622e3
89b7602
7415163
f7d6609
b0a5e24
968a7fc
b05f7df
b4f4eb7
d75adc3
60a41f0
506eaa7
6d42ef1
535ffb0
db4b782
ed2824a
b8c4fb6
d1c304b
a2dd0a7
7a0acfc
059904f
1cafa16
66ceb08
a4ff280
0eda2e7
e6b13c5
1efbd0c
1fdecee
08e3a27
60505a8
14411d4
2990180
0d63efa
e90a5e5
b91b4a0
30654f4
632fa52
0923667
5cf5c5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,13 +13,26 @@ | |
| Utilities for using the Clifford group in randomized benchmarking | ||
| """ | ||
|
|
||
| from typing import Optional, Union | ||
| import os | ||
| from typing import List | ||
| from functools import lru_cache | ||
| from numpy.random import Generator, default_rng | ||
| from math import isclose | ||
| import numpy as np | ||
|
|
||
| from qiskit import QuantumCircuit, QuantumRegister | ||
| from qiskit.circuit import Gate | ||
| from qiskit.circuit.library import SdgGate, HGate, SGate, SXdgGate | ||
| from qiskit.quantum_info import Clifford, random_clifford | ||
| from qiskit.exceptions import QiskitError | ||
| from qiskit.quantum_info import Clifford | ||
|
|
||
| from .clifford_data import ( | ||
| CLIFF_SINGLE_GATE_MAP_1Q, | ||
| CLIFF_SINGLE_GATE_MAP_2Q, | ||
| CLIFF_COMPOSE_DATA_1Q, | ||
| CLIFF_COMPOSE_DATA_2Q, | ||
| CLIFF_INVERSE_DATA_1Q, | ||
| CLIFF_INVERSE_DATA_2Q, | ||
| ) | ||
|
|
||
|
|
||
| class VGate(Gate): | ||
|
|
@@ -64,67 +77,35 @@ class CliffordUtils: | |
| (2, 2, 3, 3, 3, 3, 4, 4), | ||
| (2, 2, 3, 3, 4, 4), | ||
| ] | ||
| GENERAL_CLIFF_LIST = ["id", "h", "sdg", "s", "x", "sx", "sxdg", "y", "z", "cx"] | ||
| TRANSPILED_CLIFF_LIST = ["sx", "rz", "cx"] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumes a particular IBM-ish backend. Other providers may use different basis set depending on their hardware architecture, or, even us may want to use different set, e.g. "ecr" rather than "cx" which are locally equivalent.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is difficult to make the code efficient without optimizing it for a specific set of basis gates.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but Qiskit is backend agnostic. This violates very important policy of Qiskit. |
||
| CLIFF_SINGLE_GATE_MAP = {1: CLIFF_SINGLE_GATE_MAP_1Q, 2: CLIFF_SINGLE_GATE_MAP_2Q} | ||
| CLIFF_COMPOSE_DATA = {1: CLIFF_COMPOSE_DATA_1Q, 2: CLIFF_COMPOSE_DATA_2Q} | ||
| CLIFF_INVERSE_DATA = {1: CLIFF_INVERSE_DATA_1Q, 2: CLIFF_INVERSE_DATA_2Q} | ||
|
|
||
| def clifford_1_qubit(self, num): | ||
| @classmethod | ||
| def clifford_1_qubit(cls, num): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was planning to deprecate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could remove
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the method does not access any class-relevant data (i.e. doesn't use the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but still these are just a collection of functions. I wonder why these must be class methods, i.e. from qiskit_experiments.library.randomized_benchmarking import clifford_utils
clifford_utils.clifford_1_qubits(...)v.s. from qiskit_experiments.library.randomized_benchmarking.clifford_utils import CliffordUtils
CliffordUtils.clifford_1_qubits(...) |
||
| """Return the 1-qubit clifford element corresponding to `num` | ||
| where `num` is between 0 and 23. | ||
| """ | ||
| return Clifford(self.clifford_1_qubit_circuit(num), validate=False) | ||
| return Clifford(cls.clifford_1_qubit_circuit(num), validate=False) | ||
|
|
||
| def clifford_2_qubit(self, num): | ||
| @classmethod | ||
| def clifford_2_qubit(cls, 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), validate=False) | ||
|
|
||
| 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), validate=False) for i in samples] | ||
| else: | ||
| samples = rng.integers(11520, size=size) | ||
| return [Clifford(self.clifford_2_qubit_circuit(i), validate=False) for i in samples] | ||
|
merav-aharoni marked this conversation as resolved.
|
||
|
|
||
| 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] | ||
|
merav-aharoni marked this conversation as resolved.
|
||
| return Clifford(cls.clifford_2_qubit_circuit(num), validate=False) | ||
|
|
||
| @classmethod | ||
| @lru_cache(maxsize=24) | ||
| def clifford_1_qubit_circuit(self, num): | ||
| def clifford_1_qubit_circuit(cls, 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) | ||
| (i, j, p) = cls._unpack_num(num, cls.CLIFFORD_1_QUBIT_SIG) | ||
| qr = QuantumRegister(1) | ||
| qc = QuantumCircuit(qr) | ||
| if i == 1: | ||
|
|
@@ -141,12 +122,13 @@ def clifford_1_qubit_circuit(self, num): | |
| qc.z(0) | ||
| return qc | ||
|
|
||
| @classmethod | ||
| @lru_cache(maxsize=11520) | ||
| def clifford_2_qubit_circuit(self, num): | ||
| def clifford_2_qubit_circuit(cls, 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) | ||
| vals = cls._unpack_num_multi_sigs(num, cls.CLIFFORD_2_QUBIT_SIGS) | ||
| qr = QuantumRegister(2) | ||
| qc = QuantumCircuit(qr) | ||
| if vals[0] == 0 or vals[0] == 3: | ||
|
|
@@ -195,7 +177,8 @@ def clifford_2_qubit_circuit(self, num): | |
| qc.z(1) | ||
| return qc | ||
|
|
||
| def _unpack_num(self, num, sig): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In lines 206-207 above - can replace: by:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this equivalent? I can't see it. I also tried running and comparing the two, and didn't get the same.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| @staticmethod | ||
| def _unpack_num(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 | ||
|
|
@@ -207,7 +190,8 @@ def _unpack_num(self, num, sig): | |
| num //= k | ||
| return res | ||
|
|
||
| def _unpack_num_multi_sigs(self, num, sigs): | ||
| @classmethod | ||
| def _unpack_num_multi_sigs(cls, num, sigs): | ||
| """Returns the result of `_unpack_num` on one of the | ||
| signatures in `sigs` | ||
| """ | ||
|
|
@@ -216,6 +200,104 @@ def _unpack_num_multi_sigs(self, num, sigs): | |
| for k in sig: | ||
| sig_size *= k | ||
| if num < sig_size: | ||
| return [i] + self._unpack_num(num, sig) | ||
| return [i] + cls._unpack_num(num, sig) | ||
| num -= sig_size | ||
| return None | ||
|
|
||
| @classmethod | ||
| def num_from_clifford_single_gate(cls, inst, qubits, rb_num_qubits, basis_gates): | ||
| """ | ||
| This method does the reverse of clifford_1_qubit_circuit and clifford_2_qubit_circuit - | ||
| given a clifford, it returns the corresponding integer, with the mapping | ||
| defined in the above method. | ||
| The mapping is in the context of the basis_gates. Therefore, we define here | ||
| the possible supersets of basis gates, and verify that the given inst belong to | ||
| one of these sets. | ||
| """ | ||
| name = inst.name | ||
| gates_with_delay = basis_gates.copy() | ||
| gates_with_delay.append("delay") | ||
|
|
||
| if not name in gates_with_delay: | ||
| raise QiskitError("Instruction {} is not in the basis gates".format(inst.name)) | ||
| if set(basis_gates).issubset(set(cls.GENERAL_CLIFF_LIST)): | ||
| if name == "delay": | ||
| return 0 | ||
| map_index = name | ||
|
|
||
| if set(basis_gates).issubset(set(cls.TRANSPILED_CLIFF_LIST)): | ||
| if name in {"sx", "cx"}: | ||
| map_index = name | ||
| elif name == "delay": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can do this check once in the beginning of the method |
||
| return 0 | ||
| elif name == "rz": | ||
| # The next two are identical up to a phase, which makes no difference | ||
| # for the associated Cliffords | ||
| if isclose(inst.params[0], np.pi) or isclose(inst.params[0], -np.pi): | ||
| map_index = "z" | ||
| elif isclose(inst.params[0], np.pi / 2): | ||
| map_index = "s" | ||
| elif isclose(inst.params[0], -np.pi / 2): | ||
| map_index = "sdg" | ||
| else: | ||
| raise QiskitError("wrong param {} for rz in clifford".format(inst.params[0])) | ||
| else: | ||
| raise QiskitError( | ||
| "Instruction {} could not be converted to Clifford gate".format(name) | ||
| ) | ||
|
|
||
| return cls.CLIFF_SINGLE_GATE_MAP[rb_num_qubits][(map_index, str(qubits))] | ||
|
|
||
| @classmethod | ||
| def compose_num_with_clifford( | ||
| cls, num_qubits: int, composed_num: int, qc: QuantumCircuit, basis_gates: List[str] | ||
| ) -> int: | ||
| """Compose a number that represents a Clifford, with a single-gate Clifford, and return the | ||
| number that represents the resulting Clifford.""" | ||
|
|
||
| # The numbers corresponding to single gate Cliffords are not in sequence - | ||
| # see num_from_1q_clifford_single_gate. To compute the index in | ||
| # the array CLIFF_COMPOSE_DATA_1Q, we map the numbers to [0, 8]. | ||
| map_clifford_num_to_array_index = {} | ||
| num_single_gate_cliffs = len(cls.CLIFF_SINGLE_GATE_MAP[num_qubits]) | ||
| for k in list(cls.CLIFF_SINGLE_GATE_MAP[num_qubits]): | ||
| map_clifford_num_to_array_index[cls.CLIFF_SINGLE_GATE_MAP[num_qubits][k]] = list( | ||
| cls.CLIFF_SINGLE_GATE_MAP[num_qubits].keys() | ||
| ).index(k) | ||
| if num_qubits == 1: | ||
| for inst, qargs, _ in qc: | ||
| num = cls.num_from_clifford_single_gate( | ||
| inst=inst, qubits=[0], rb_num_qubits=1, basis_gates=basis_gates | ||
| ) | ||
| index = num_single_gate_cliffs * composed_num + map_clifford_num_to_array_index[num] | ||
| composed_num = cls.CLIFF_COMPOSE_DATA[num_qubits][index] | ||
| else: | ||
| for inst, qargs, _ in qc: | ||
| if inst.num_qubits == 2: | ||
| qubits = [qc.find_bit(qargs[0]).index, qc.find_bit(qargs[1]).index] | ||
| else: | ||
| qubits = [qc.find_bit(qargs[0]).index] | ||
| num = cls.num_from_clifford_single_gate( | ||
| inst=inst, qubits=qubits, rb_num_qubits=2, basis_gates=basis_gates | ||
| ) | ||
| index = num_single_gate_cliffs * composed_num + map_clifford_num_to_array_index[num] | ||
| composed_num = cls.CLIFF_COMPOSE_DATA[num_qubits][index] | ||
| return composed_num | ||
|
|
||
| @classmethod | ||
| def clifford_inverse_by_num(cls, num: int, num_qubits: int): | ||
| """Return the number of the inverse Clifford to the input num""" | ||
| return cls.CLIFF_INVERSE_DATA[num_qubits][num] | ||
|
|
||
| @staticmethod | ||
| def file_name(num_qubits, basis_gates): | ||
| """Return the name of the file containing the transpiled Cliffords""" | ||
| suffix = "" | ||
| for n in basis_gates[0:-1]: | ||
| suffix += "_" + n | ||
| if num_qubits == 2: | ||
| suffix += "_" + basis_gates[-1] | ||
| circs_file_name = "/transpiled_circs_" + str(num_qubits) + "q" + suffix + ".qpy" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another possibility: |
||
| root_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| transpiled_circs_file = root_dir + circs_file_name | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
| return transpiled_circs_file | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use this set as a default if the user does not pass this option?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible. The question is whether we think it is better to have a default, or to make sure the user specifies their needs. @nkanazawa1989 , @ShellyGarion ? what do you think?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RB experiment itself should be general. Experiment must define a protocol, not executable (target) code.