Skip to content
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

Improve custom transpilation for faster 1Q/2Q RB #922

Merged
Merged
146 changes: 128 additions & 18 deletions qiskit_experiments/library/randomized_benchmarking/clifford_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@
Utilities for using the Clifford group in randomized benchmarking
"""

from typing import List, Tuple, Optional, Union
import itertools
from functools import lru_cache
from numbers import Integral
from math import isclose
import itertools
from numbers import Integral
from typing import List
from typing import Optional, Union, Tuple, Sequence

import numpy as np
from numpy.random import Generator, default_rng

from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit import Gate, Instruction
from qiskit.circuit.library import SdgGate, HGate, SGate
from qiskit.circuit import QuantumCircuit, QuantumRegister, CircuitInstruction, Qubit
from qiskit.circuit.library import SdgGate, HGate, SGate, XGate, YGate, ZGate
from qiskit.compiler import transpile
from qiskit.providers.backend import Backend
from qiskit.exceptions import QiskitError
from qiskit.providers.backend import Backend
from qiskit.quantum_info import Clifford, random_clifford

from .clifford_data import (
CLIFF_SINGLE_GATE_MAP_1Q,
CLIFF_SINGLE_GATE_MAP_2Q,
Expand All @@ -41,14 +42,116 @@
)


# Transpilation utilities
def _transpile_clifford_circuit(
circuit: QuantumCircuit, physical_qubits: Sequence[int]
) -> QuantumCircuit:
# Simplified transpile, which only decomposes Clifford circuits and layout qubits
itoko marked this conversation as resolved.
Show resolved Hide resolved
return _apply_qubit_layout(_decompose_clifford_ops(circuit), physical_qubits=physical_qubits)


def _decompose_clifford_ops(circuit: QuantumCircuit) -> QuantumCircuit:
merav-aharoni marked this conversation as resolved.
Show resolved Hide resolved
# Simplified QuantumCircuit.decompose, which decomposes only Clifford ops
# Note that the resulting circuit depends on the input circuit,
# that means the changes on the input circuit may affect the resulting circuit.
# For example, the resulting circuit shares the parameter_table of the input circuit,
res = circuit.copy_empty_like()
res._parameter_table = circuit._parameter_table
for inst in circuit:
if inst.operation.name.startswith("Clifford"): # Decompose
rule = inst.operation.definition.data
if len(rule) == 1 and len(inst.qubits) == len(rule[0].qubits):
if inst.operation.definition.global_phase:
res.global_phase += inst.operation.definition.global_phase
res._data.append(
CircuitInstruction(
operation=rule[0].operation,
qubits=inst.qubits,
clbits=inst.clbits,
)
)
else:
_circuit_compose(res, inst.operation.definition, qubits=inst.qubits)
else: # Keep the original instruction
res._data.append(inst)
return res


def _apply_qubit_layout(circuit: QuantumCircuit, physical_qubits: Sequence[int]) -> QuantumCircuit:
# Mapping qubits in circuit to physical qubits (layout)
res = QuantumCircuit(1 + max(physical_qubits), name=circuit.name, metadata=circuit.metadata)
res.add_bits(circuit.clbits)
for reg in circuit.cregs:
res.add_register(reg)
_circuit_compose(res, circuit, qubits=physical_qubits)
res._parameter_table = circuit._parameter_table
return res


def _circuit_compose(
self: QuantumCircuit, other: QuantumCircuit, qubits: Sequence[Union[Qubit, int]]
) -> QuantumCircuit:
# Simplified QuantumCircuit.compose with clbits=None, front=False, inplace=True, wrap=False
# without any validation, parameter_table/calibrations updates and copy of operations
# The input circuit `self` is changed inplace.
qubit_map = {
other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q) for i, q in enumerate(qubits)
}
for instr in other:
self._data.append(
CircuitInstruction(
operation=instr.operation,
qubits=[qubit_map[q] for q in instr.qubits],
clbits=instr.clbits,
),
)
self.global_phase += other.global_phase
return self


def _truncate_inactive_qubits(
circ: QuantumCircuit, active_qubits: Sequence[Qubit]
) -> QuantumCircuit:
new_data = []
for inst in circ:
if all(q in active_qubits for q in inst.qubits):
new_data.append(inst)

res = QuantumCircuit(active_qubits, name=circ.name)
res._calibrations = circ.calibrations
res._data = new_data
res._metadata = circ.metadata
return res


# TODO: Naming: transform? translate? synthesis?
def _transform_clifford_circuit(circuit: QuantumCircuit, basis_gates: Tuple[str]) -> QuantumCircuit:
# The function that synthesis clifford circuits with given basis gates,
# which should be commonly used during custom transpilation in the RB circuit generation.
return transpile(circuit, basis_gates=list(basis_gates), optimization_level=0)
merav-aharoni marked this conversation as resolved.
Show resolved Hide resolved


@lru_cache(maxsize=None)
def _clifford_1q_int_to_instruction(num: Integral) -> Instruction:
return CliffordUtils.clifford_1_qubit_circuit(num).to_instruction()
def _clifford_1q_int_to_instruction(
num: Integral, basis_gates: Optional[Tuple[str]]
) -> Instruction:
return CliffordUtils.clifford_1_qubit_circuit(num, basis_gates).to_instruction()


@lru_cache(maxsize=11520)
def _clifford_2q_int_to_instruction(num: Integral) -> Instruction:
return CliffordUtils.clifford_2_qubit_circuit(num).to_instruction()
def _clifford_2q_int_to_instruction(
num: Integral, basis_gates: Optional[Tuple[str]]
) -> Instruction:
utils = __get_clifford_utils_2q(basis_gates)
return utils.transpiled_cliff_from_layer_nums(
utils.layer_indices_from_num(num)
).to_instruction()
# return CliffordUtils.clifford_2_qubit_circuit(num, basis_gates).to_instruction()


@lru_cache(maxsize=None)
def __get_clifford_utils_2q(basis_gates: Optional[Tuple[str]]):
return CliffordUtils(2, basis_gates)


# The classes VGate and WGate are not actually used in the code - we leave them here to give
Expand Down Expand Up @@ -173,7 +276,7 @@ def random_clifford_circuits(

@classmethod
@lru_cache(maxsize=24)
def clifford_1_qubit_circuit(cls, num):
def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str]] = None):
"""Return the 1-qubit clifford circuit corresponding to `num`
where `num` is between 0 and 23.
"""
Expand All @@ -193,11 +296,14 @@ def clifford_1_qubit_circuit(cls, num):
if p == 3:
qc.z(0)

if basis_gates:
qc = _transform_clifford_circuit(qc, basis_gates)

merav-aharoni marked this conversation as resolved.
Show resolved Hide resolved
return qc

@classmethod
@lru_cache(maxsize=11520)
def clifford_2_qubit_circuit(cls, num):
def clifford_2_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str]] = None):
"""Return the 2-qubit clifford circuit corresponding to `num`
where `num` is between 0 and 11519.
"""
Expand Down Expand Up @@ -251,6 +357,9 @@ def clifford_2_qubit_circuit(cls, num):
if p1 == 3:
qc.z(1)

if basis_gates:
qc = _transform_clifford_circuit(qc, basis_gates)

return qc

@staticmethod
Expand Down Expand Up @@ -488,14 +597,14 @@ def _transpile_cliff_layer_2(self):
Number of Cliffords == 16."""
if self._transpiled_cliff_layer[2] != []:
return
pauli = ["i", "x", "y", "z"]

pauli = ("i", XGate(), YGate(), ZGate())
for p0, p1 in itertools.product(pauli, pauli):
qr = QuantumRegister(2)
qc = QuantumCircuit(qr)
qc = QuantumCircuit(2)
if p0 != "i":
qc._append(Gate(p0, 1, []), [qr[0]], [])
qc.append(p0, [0])
if p1 != "i":
qc._append(Gate(p1, 1, []), [qr[1]], [])
qc.append(p1, [1])

transpiled = transpile(
qc, optimization_level=1, basis_gates=self.basis_gates, backend=self._backend
Expand Down Expand Up @@ -555,6 +664,7 @@ def transpiled_cliff_from_layer_nums(self, triplet: Tuple) -> QuantumCircuit:
qc = q0.copy()
qc.compose(q1, inplace=True)
qc.compose(q2, inplace=True)
qc.name = f"Clifford-2Q({self.num_from_layer_indices(triplet)})"
return qc

@staticmethod
Expand Down
Loading