Skip to content

Commit

Permalink
New algorithm for RB building Cliffords by layers (qiskit-community#892)
Browse files Browse the repository at this point in the history
* New algorithm for generating Clifford circuits for single qubit. We generate once all 24 transpiled Cliffords. Then, for every rb_circuit, select Cliffords at random and compose them to a circuit

* Changed basis for transpilation to match the one in single_qubit_test. Added parameter to all calls to compose to use inplace=True.  Removed redundant method generate_all_transpiled_clifford_circuits

* Modified the methods in create_clifford_map so that compose does not use front=True, because I assume front=False when creating the circuits

* Changed generation of generation of random numbers to be identical to the previous version of rb_experiment.

* Test to run on device

* added methods for new algorithm to generate rb circuits: build_rb_circuits, generate_1q_transpiled_clifford_circuits

* Added the method _layout_for_rb_single_qubit and added test_full_sampling_single_qubit

* In test_full_sampling_single_qubit fixed num_samples to be 1, because otherwise randomization is not identical in the two experiments

* Tidied up build_rb_circuits

* Added documentation and moved methods

* Added test_single_qubit_parallel

* Changed name _format_data to format_data because the method wasn't being called by the child class CurveAnalaysis. Added parameter to rb_experiment to determine whether to use the old algorithm or new one

* Fixed handling of num_samples>1. Cleaned out prints. Reverted previous change regarding _format_data

* Added assertExperimentDone to test_single-qubit_parallel. Fixed parameters for test_full_sampling_single_qubit

* Modified assertAllIdentity to support circuits with rz gates

* Changed name _new_rb to _transpiled_rb. Also changed default to be False, so that all tests will pass

* removed fast_rb.py

* removed rb_on_device.py

* Removed temporary 'import time'

* Added support for interleaved rb single qubit

* Fixed handling of interleaved element

* Fixed bug caused by change of interface of _buil_rb_circuits after adding support for interleave

* added test_number_to_clifford_mapping and fixed the method num_from_1_qubit_clifford

* Added support for computation of the Clifford to number mapping of a circuit

* Moved setting of interleaved metadata to be under 'if is_interleaved'

* Added transpilation of interleaved element before creating the rb circuits. Transformed interleaved element into a transpiled clifford circuit. Added relevant tests

* Fixed incorrect parameter 'qubits' in test_non_clifford_interleaved_element

* Added support for 'delay' as interleaved element

* Moved setting of basis gates to circuits(), because in __init__ the transpile_options are not available yet

* Cleaned up setting of tranpile_options in the test. Added call to generate_1q_transpiled_clifford_circuits in InterleavedRB.circuits()

* Changed the clifford compose mapping so that the rhs includes only single-gate cliffords. The purpose is to reduce the size of the mapping table

* Changed all methods in CliffordUtils to be classmethod

* Fixes following changes in CliffordUtils

* Changed structure of CLIFF_COMPOSE_DATA to be an array instead of a dict, for performance reasons. The index in the array is computed in compose_num_with_clifford

* Improved _layout_for_rb_single_qubit to be more robust

* Documentation, black, pylint

* black

* Removed the parameter transpiled_rb used for choosing whether to use old algorithm or new one.

* Cleaning up

* Generated transpiled clifford circuits for 2 qubits, and stored in a file. Adding load from this file in circuits()

* re-generated transpiled circuits

* Added support  for compose_num_with_clifford_2q. Added suitable test - test_number_to_clifford_mapping_2q.

* Created method load_transpiled_cliff_circuits

* Added support for two sets of basis gates and their corresponding transpiled clifford files

* United the two class variables _transpiled_cliff_circuits_1q and _transpiled_cliff_circuits_2q to a single dict

* Added setting of transpile_options to all tests

* Fixed error messages

* United methods compose_num_with_clifford and num_from_clifford_single_gate for 1 and 2 qubits. Interface changes in StandardRB towards uniting functionality for 1 and 2 qubits

* Unified the format for CLIFF_SINGLE_GATE_MAP_1Q and CLIFF_SINGLE_GATE_MAP_2Q

* Added method clifford_inverse_by_num to unite handling of 1 and 2 qubit-rb

* Fixes to support 1 and 2 qubits in _build_rb_circuits

* Fixed more places where the code assumed 1 qubit rb

* Fix for handling of delay. Changed test_interleaving_circuit_with_delay to suit transpiled circuits

* changed basis gates in test, because rz is not supported in conversion to Clifford

* Extended lenths for test, because test was failing for statistical reasons

* changed name of method after addition of 2q

* black and lint

* black, lint and documentation

* black, lint, cleaning

* Removed legacy code. Additional cleaning

* Removed more legacy code. More cleaning

* Removed pylint messages for the data file

* pylint, and use method CliffordUtils.file_name

* Removed additional legacy code

* Release notes

* Added missing ()

* Fixed error message

* Added QiskitError to documentation

* black

* Fixed error message

* Changed varible name

* attempt to fix documentation failure in CI

* pylint

* Added transpile options to every experiment

* Made setting transpile_options mandatory, because if basis_gates are taken from backend, then the names of the transpiled clifford files don't match

* Split test_rb_utils into itself and test_clifford_utils

* Added tests for composing a clifford with a number

* Added test for inverse clifford by num

* Changed random to rng because of failure on Windows

* Removed dependency on Aer for transpile. Removed the method transpile_single_clifford and use transpile() directly instead

* Changed parameter backend to be optional, as it was before

* Improved format for CLIFF_SINGLE_GATE_MAP, CLIFF_COMPOSE_DATA, CLIFF_INVERSE_DATA

* Changed usage from cliff.__repr__ to repr(cliff)

* Fixed a bug where transpiled_circuits were loaded multiple times

* New algorithm that constructs the Cliffords by layers

* Added support for 1 qubits, for full sampling and for interleaved rb

* Removed unused files. Black

* Added 2 parameters to CliffordUtils.__init__: num_qubits and basis_gates

* Removed all parameters num_qubits and use self.num-qubits instead

* Removed usage of specific gate sets

* moved interleaved element out of StandardRB

* Split _build_rb_circuits into small methods to make it more modular

* Added tests for CliffordUtils

* Added support for cz

* Added backend to CliffordInit.__init__. Added transpile to initial circuit created in _build methods

* Modified test_correct_1q_depolarization for it to pass

* black and pylint

* Fixed bug with cz - since it is a symmetrical operation, we store only (cz, [0,1]) in all the Clifford lists.

* Fixed the parameters in cliffordUtils methods. Since num_qubits was added as a class member, no need to pass it as a parameter.

* Data updated by changes in create_clifford_map.py from previous commit

* Fixed two tests. test_correct_1q_depolarization was failing due to too strict assertion. test_interleaving_circuit_with_delay needed to be changed because now returns transpiled circuits.

* Changed usage of VGate and WGate to their component gates: s, h, sdg.

* Moved transpiled clifford structures outside init to make them static in the class

* Added test for two qubit rb in parallel

* Moved structures back inside __init__ because the previous change caused a bug

* Added back the old code to create RB circuits for more than 2 qubits

* Changed InterleavedRB.circuits to call its super() when number of qubits > 2. Added default set of basis gates

* Added test for three qubit RB

* Documentation

* black and pylint

* Disabled black and pylint errors on clifford_data.py

* Changed _clifford_utils to be a static class member. Added a couple of short methods to reduce code duplication

* Updated release notes

* Fixed failing test

* Fixed bug: _transpile_cliff_layer_0 was ignoring the backend

* Documentation

* Reverted changes in tutorial as it is not mandatory to supply basis_gates any more

* Cleaning up

* Cleaning up

* black and doc

* Fixed bug found in tutorial - added transpile at the beginning of building an interleaved circuit

* Added header

* black

* Cleaning up temporary comments

* documentation

* Changed order of several methods

* Removed -interleave(). It was previously removed in main, but I forgot it in the code

* Removed method _set_interleaved_element that was removed in a previous PR and I missed it

* Name changed and spaces

* Removed empty spaces

* typo

* Fixed transpilation of interleaved element. Made a few structural changes to the surrounding code
  • Loading branch information
merav-aharoni authored and itoko committed Nov 29, 2022
1 parent 86f7282 commit 762ee51
Show file tree
Hide file tree
Showing 9 changed files with 2,464 additions and 967 deletions.

Large diffs are not rendered by default.

372 changes: 360 additions & 12 deletions qiskit_experiments/library/randomized_benchmarking/clifford_utils.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# 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.
"""
This is a script used to create the data in clifford_data.py.
Every Clifford is represented by a number. We store a list of the compositions of Cliffords represented
as numbers. For example, if Clifford1.compose(Clifford2) == Clifford3, then we conceptually,
we store {(1, 2) : 3}. We don't actually store the map, but only the results of compose in a list,
because this is more efficient in performance. The result is found using the indices of the input
Cliffords.
Similarly, we store for each number representing a Clifford, the number representing the
inverse Clifford.
For compose, we don't actually store the full compose table of all-cliffords X all-cliffords.
Instead, we define an list of single-gate-cliffords. This comprises all Cliffords that consist
of a single gate. There are 9 such Cliffords for 1-qubit, and 21 such Cliffords for 2-qubits.
It is sufficient to store the compose table of all-cliffords X single-gate-cliffords,
since for every Clifford on the right hand side, we can break it down into single gate Cliffords,
and do the composition one gate at a time. This greatly reduces the storage space for the array of
composition results (from O(n^2) to O(n)), where n is the number of Cliffords.
"""
import itertools

from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import (
IGate,
HGate,
SXdgGate,
SGate,
XGate,
SXGate,
YGate,
ZGate,
SdgGate,
CXGate,
CZGate,
)
from qiskit.quantum_info.operators.symplectic import Clifford
from qiskit_experiments.library.randomized_benchmarking.clifford_utils import CliffordUtils

gate_list_1q = [
IGate(),
HGate(),
SXdgGate(),
SGate(),
XGate(),
SXGate(),
YGate(),
ZGate(),
SdgGate(),
]
asymm_gates_2q = [
CXGate(),
]
symm_gates_2q = [CZGate()]


class CliffordNumMapping:
"""Class that creates creates all the structures with the mappings between Cliffords
and numbers."""

basis_gates = ["h", "s", "sdg", "x", "cx"]
single_gate_clifford_map_1q = {}
single_gate_clifford_map_2q = {}
num_to_cliff_1q = {}
cliff_to_num_1q = {}
num_to_cliff_2q = {}
cliff_to_num_2q = {}
layers_num_to_cliff_num_2q = {}
cliff_num_to_layers_2q = {}

@classmethod
def gen_nums_single_gate_cliffs_1q(cls):
"""
Generates a dict mapping numbers to Cliffords and the reverse dict.
Based on these structures, we build a mapping from every single-gate-clifford to its number.
The mapping actually looks like {(gate, '[0]'): num}, where [0] represents qubit 0.
The qubit is added to be consistent with the format for 2 qubits.
"""
clifford_utils = CliffordUtils(1, cls.basis_gates)
for i in range(clifford_utils.NUM_CLIFFORD_1_QUBIT):
cliff = clifford_utils.clifford_1_qubit(i)
cls.num_to_cliff_1q[i] = cliff
cls.cliff_to_num_1q[repr(cliff)] = i

for gate in gate_list_1q:
qc = QuantumCircuit(1)
qc.append(gate, [0])
cliff = Clifford(qc)
if repr(cliff) in cls.cliff_to_num_1q.keys():
num = cls.cliff_to_num_1q[repr(cliff)]
# qubit_as_str is not really necessary. It is only added to be consistent
# with the representation for 2 qubits
qubit_as_str = "[0]"
cls.single_gate_clifford_map_1q[(gate.name, qubit_as_str)] = num
else:
print("not found")
cliff_data_file.write(f"CLIFF_SINGLE_GATE_MAP_1Q = {cls.single_gate_clifford_map_1q}\n")

@classmethod
def gen_nums_single_gate_cliffs_2q(cls):
"""
Generates a dict mapping numbers to Cliffords and the reverse dict.
Based on these structures, we build a mapping from every single-gate-clifford to its number.
The mapping actually looks like {(gate, '[qubits]'): num}.
"""
clifford_utils = CliffordUtils(2, cls.basis_gates)
for i in range(clifford_utils.NUM_CLIFFORD_2_QUBIT):
cliff = clifford_utils.clifford_2_qubit(i)
cls.num_to_cliff_2q[i] = cliff
cls.cliff_to_num_2q[repr(cliff)] = i

for gate, qubit in itertools.product(gate_list_1q, [0, 1]):
qc = QuantumCircuit(2)
qc.append(gate, [qubit])
cliff = Clifford(qc)
if repr(cliff) in cls.cliff_to_num_2q:
num = cls.cliff_to_num_2q[repr(cliff)]
# qubit_as_str is not really necessary. It is only added to be consistent
# with the representation for 2 qubits
qubit_as_str = "[" + str(qubit) + "]"
cls.single_gate_clifford_map_2q[(gate.name, qubit_as_str)] = num
else:
print("not found")

for gate, qubits in itertools.product(asymm_gates_2q, [[0, 1], [1, 0]]):
qc = QuantumCircuit(2)
qc.append(gate, qubits)
cliff = Clifford(qc)
if repr(cliff) in cls.cliff_to_num_2q.keys():
num = cls.cliff_to_num_2q[repr(cliff)]
else:
print("not found")
direction = "[0, 1]" if qubits == [0, 1] else "[1, 0]"
cls.single_gate_clifford_map_2q[(gate.name, direction)] = num

for gate in symm_gates_2q:
qc = QuantumCircuit(2)
qc.append(gate, [0, 1])
cliff = Clifford(qc)
if repr(cliff) in cls.cliff_to_num_2q.keys():
num = cls.cliff_to_num_2q[repr(cliff)]
else:
print("not found")
cls.single_gate_clifford_map_2q[(gate.name, "[0, 1]")] = num

cliff_data_file.write(f"CLIFF_SINGLE_GATE_MAP_2Q = {cls.single_gate_clifford_map_2q}\n")

@classmethod
def create_compose_map_1q(cls):
"""Creates the data in compose data in CLIFF_COMPOSE_DATA and
the inverse data in CLIFF_INVERSE_DATA"""
products = {}
for i in range(CliffordUtils.NUM_CLIFFORD_1_QUBIT):
cliff1 = cls.num_to_cliff_1q[i]
for gate in cls.single_gate_clifford_map_1q:
cliff2 = cls.num_to_cliff_1q[cls.single_gate_clifford_map_1q[gate]]
cliff = cliff1.compose(cliff2)
products[(i, gate)] = cls.cliff_to_num_1q[repr(cliff)]

invs = {}
for i in range(CliffordUtils.NUM_CLIFFORD_1_QUBIT):
cliff1 = cls.num_to_cliff_1q[i]
cliff = cliff1.adjoint()
invs[i] = cls.cliff_to_num_1q[repr(cliff)]

cliff_data_file.write("CLIFF_COMPOSE_DATA_1Q = [")
for i in products:
cliff_data_file.write(f"{products[i]},")
cliff_data_file.write("]")
cliff_data_file.write("\n")

cliff_data_file.write("CLIFF_INVERSE_DATA_1Q = [")
for i in invs:
cliff_data_file.write(f"{invs[i]},")
cliff_data_file.write("]")
cliff_data_file.write("\n")

@classmethod
def create_compose_map_2q(cls):
"""Creates the data in CLIFF_COMPOSE_DATA and CLIFF_INVERSE_DATA"""
products = {}
for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT):
cliff1 = cls.num_to_cliff_2q[i]
for gate in cls.single_gate_clifford_map_2q:
cliff2 = cls.num_to_cliff_2q[cls.single_gate_clifford_map_2q[gate]]
cliff = cliff1.compose(cliff2)
products[(i, gate)] = cls.cliff_to_num_2q[repr(cliff)]

invs = {}
for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT):
cliff1 = cls.num_to_cliff_2q[i]
cliff = cliff1.adjoint()
invs[i] = cls.cliff_to_num_2q[repr(cliff)]

cliff_data_file.write("CLIFF_COMPOSE_DATA_2Q = [")
for i in products:
cliff_data_file.write(f"{products[i]},")
cliff_data_file.write("]")
cliff_data_file.write("\n")

cliff_data_file.write("CLIFF_INVERSE_DATA_2Q = [")
for i in invs:
cliff_data_file.write(f"{invs[i]},")
cliff_data_file.write("]")
cliff_data_file.write("\n")

@classmethod
def map_layers_to_cliffords_2q(cls):
"""Creates a map from a triplet describing the indices in the layers, to the
number of the corresponding Clifford"""
clifford_utils = CliffordUtils(2, cls.basis_gates)
clifford_utils.transpile_2q_cliff_layers()
length = [len(clifford_utils._transpiled_cliff_layer[i]) for i in [0, 1, 2]]
for n0, n1, n2 in itertools.product(range(length[0]), range(length[1]), range(length[2])):
cliff = Clifford(clifford_utils.transpiled_cliff_from_layer_nums((n0, n1, n2)))

num = cls.cliff_to_num_2q[repr(cliff)]
cls.layers_num_to_cliff_num_2q[(n0, n1, n2)] = num
cls.cliff_num_to_layers_2q[num] = (n0, n1, n2)

cliff_data_file.write("CLIFF_LAYERS_TO_NUM_2Q = [")
for i in cls.layers_num_to_cliff_num_2q:
cliff_data_file.write(f"{cls.layers_num_to_cliff_num_2q[i]},")
cliff_data_file.write("]\n")

cliff_data_file.write("CLIFF_NUM_TO_LAYERS_2Q = [")
for i in range(len(cls.cliff_num_to_layers_2q)):
cliff_data_file.write(f"{cls.cliff_num_to_layers_2q[i]},")
cliff_data_file.write("]\n")

@classmethod
def create_clifford_data(cls):
"""Creates all the data for compose and inverse."""
cls.gen_nums_single_gate_cliffs_1q()
cls.gen_nums_single_gate_cliffs_2q()
cls.create_compose_map_1q()
cls.create_compose_map_2q()


with open("clifford_data.py", "w") as cliff_data_file:
cliff_data_file.write("# fmt: off\n")
cliff_data_file.write("# pylint: skip-file\n\n")
CliffordNumMapping.create_clifford_data()
CliffordNumMapping.map_layers_to_cliffords_2q()
cliff_data_file.write("\n# fmt: on\n")
Loading

0 comments on commit 762ee51

Please sign in to comment.