Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 80 additions & 36 deletions qiskit_experiments/library/randomized_benchmarking/clifford_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
"""
Utilities for using the Clifford group in randomized benchmarking
"""

import copy
import itertools
import os
from functools import lru_cache
from numbers import Integral
from typing import Optional, Union, Tuple, Sequence
from typing import Optional, Union, Tuple, Sequence, FrozenSet

import numpy as np
import scipy.sparse
Expand All @@ -30,6 +30,7 @@
from qiskit.compiler import transpile
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import Clifford, random_clifford
from qiskit.transpiler import Target
from qiskit_experiments.warnings import deprecated_function


Expand Down Expand Up @@ -100,40 +101,83 @@ def _circuit_compose(
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
class ReducedTarget(Target):
"""
A target class reduced to represent subsystem with specified physical qubits.

Note that this class must be treated as an immutable class since it implements
``__hash__`` function so that its object can be a cache key,
even though it is technically mutable.
Also note that this class may not contain some data necessary to schedule circuits
or transpile pulse gates, different from the parent Target class, and hence
it works only with normal circuits.
This class must be instantiated by reducing an original Target into physical qubits.
In the reduction, the qubits are remapped. That means, for example, when a Target is
reduced into physical qubits (3, 2), the resulting ReducedTarget will have
virtual qubits (0, 1): Qubit 3 is mapped to 0 and qubit 2 to 1.
"""

def __init__(self, target: Target, physical_qubits: Tuple[int, ...]):
description = None
if target.description:
description = f"{target.description} reduced to qubits {physical_qubits}"
super().__init__(
description=description,
num_qubits=len(physical_qubits),
)
supported_instructions = set()
for op_name, qargs_dic in target.items():
new_prop_dic = {}
for qargs, inst_prop in qargs_dic.items():
if qargs is None:
new_prop_dic[None] = None
supported_instructions.add((op_name, None))
elif set(qargs).issubset(physical_qubits):
new_prop = copy.copy(inst_prop)
if new_prop and new_prop.calibration:
new_prop.calibration = None
reduced_qargs = tuple(physical_qubits.index(q) for q in qargs)
new_prop_dic[reduced_qargs] = new_prop
supported_instructions.add((op_name, reduced_qargs))
if new_prop_dic:
super().add_instruction(target.operation_from_name(op_name), new_prop_dic)
self._supported_instructions = frozenset(supported_instructions)

def __hash__(self):
return hash(self._supported_instructions)

def __eq__(self, other):
return (
isinstance(other, ReducedTarget)
and self._supported_instructions == other._supported_instructions
)

@property
def supported_instructions(self) -> FrozenSet[Tuple[str, Optional[Tuple[int, ...]]]]:
"""Set of instructions supported in this target.
An instruction is a pair of operation name and qubit arguments, e.g. ("cx", (0, 1)).
"""
return self._supported_instructions

def add_instruction(self, instruction, properties=None, name=None):
"""Not supported for ReducedTarget (immutable)"""
raise NotImplementedError("Not supported for ReducedTarget (immutable).")


# 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,
def _translate_basis(circuit: QuantumCircuit, target: ReducedTarget) -> QuantumCircuit:
# The function that translates clifford circuits into those with gates defined in given target,
# which should be commonly used during custom transpilation in the RB circuit generation.
return transpile(circuit, basis_gates=list(basis_gates), optimization_level=0)
return transpile(circuit, target=target, optimization_level=0)


@lru_cache(maxsize=None)
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()
def _clifford_1q_int_to_instruction(num: Integral, target: Optional[ReducedTarget]) -> Instruction:
return CliffordUtils.clifford_1_qubit_circuit(num, target).to_instruction()


@lru_cache(maxsize=11520)
def _clifford_2q_int_to_instruction(
num: Integral, basis_gates: Optional[Tuple[str]]
) -> Instruction:
return CliffordUtils.clifford_2_qubit_circuit(num, basis_gates).to_instruction()
def _clifford_2q_int_to_instruction(num: Integral, target: Optional[ReducedTarget]) -> Instruction:
return CliffordUtils.clifford_2_qubit_circuit(num, target).to_instruction()


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

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

if basis_gates:
qc = _transform_clifford_circuit(qc, basis_gates)
if target:
qc = _translate_basis(qc, target)

return qc

@classmethod
@lru_cache(maxsize=11520)
def clifford_2_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None):
def clifford_2_qubit_circuit(cls, num, target: Optional[ReducedTarget] = None):
"""Return the 2-qubit clifford circuit corresponding to `num`
where `num` is between 0 and 11519.
"""
qc = QuantumCircuit(2, name=f"Clifford-2Q({num})")
for layer, idx in enumerate(_layer_indices_from_num(num)):
if basis_gates:
layer_circ = _transformed_clifford_layer(layer, idx, basis_gates)
if target:
layer_circ = _transformed_clifford_layer(layer, idx, target)
else:
layer_circ = _CLIFFORD_LAYER[layer][idx]
# qc.compose(layer_circ, inplace=True)
Expand Down Expand Up @@ -577,11 +621,11 @@ def _create_cliff_2q_layer_2():

@lru_cache(maxsize=None)
def _transformed_clifford_layer(
layer: int, index: Integral, basis_gates: Tuple[str, ...]
layer: int, index: Integral, target: ReducedTarget
) -> QuantumCircuit:
# Return the index-th quantum circuit of the layer translated with the basis_gates.
# Return the index-th quantum circuit of the layer translated with the target.
# The result is cached for speed.
return _transform_clifford_circuit(_CLIFFORD_LAYER[layer][index], basis_gates)
return _translate_basis(_CLIFFORD_LAYER[layer][index], target)


def _num_from_layer_indices(triplet: Tuple[Integral, Integral, Integral]) -> Integral:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,17 @@
Interleaved RB Experiment class.
"""
import warnings
from typing import Union, Iterable, Optional, List, Sequence, Tuple
from typing import Union, Iterable, Optional, List, Sequence

from numpy.random import Generator
from numpy.random.bit_generator import BitGenerator, SeedSequence

from qiskit.circuit import QuantumCircuit, Instruction, Gate, Delay
from qiskit.compiler import transpile
from qiskit.exceptions import QiskitError
from qiskit.providers.backend import Backend
from qiskit.quantum_info import Clifford
from qiskit.transpiler.exceptions import TranspilerError
from qiskit_experiments.framework.backend_timing import BackendTiming
from .clifford_utils import _truncate_inactive_qubits
from .clifford_utils import _translate_basis, ReducedTarget
from .clifford_utils import num_from_1q_circuit, num_from_2q_circuit
from .interleaved_rb_analysis import InterleavedRBAnalysis
from .rb_experiment import StandardRB, SequenceElementType
Expand Down Expand Up @@ -67,8 +65,8 @@ def __init__(
Args:
interleaved_element: The element to interleave,
given either as a Clifford element, gate, delay or circuit.
If the element contains any non-basis gates,
it will be transpiled with ``transpiled_options`` of this experiment.
If the element contains any gates not defined in the ``backend``,
it will be internally translated into that with gates supported in the backend.
If it is/contains a delay, its duration and unit must comply with
the timing constraints of the ``backend``.
(:class:``~qiskit_experiments.framework.backend_timing.BackendTiming`
Expand Down Expand Up @@ -192,16 +190,16 @@ def circuits(self) -> List[QuantumCircuit]:
return reference_circuits + interleaved_circuits

def _to_instruction(
self, elem: SequenceElementType, basis_gates: Optional[Tuple[str]] = None
self, elem: SequenceElementType, target: Optional[ReducedTarget] = None
) -> Instruction:
if elem is self._interleaved_elem:
return self._interleaved_op

return super()._to_instruction(elem, basis_gates)
return super()._to_instruction(elem, target)

def __set_up_interleaved_op(self) -> None:
# Convert interleaved element to transpiled circuit operation and store it for speed
basis_gates = self._get_basis_gates()
target = self._get_reduced_target()
# Convert interleaved element to circuit
if isinstance(self._interleaved_op, Clifford):
self._interleaved_op = self._interleaved_op.to_circuit()
Expand All @@ -214,18 +212,18 @@ def __set_up_interleaved_op(self) -> None:
else: # Delay
interleaved_circ = []

if basis_gates and any(i.operation.name not in basis_gates for i in interleaved_circ):
# Transpile circuit with non-basis gates and remove idling qubits
try:
interleaved_circ = transpile(
interleaved_circ, self.backend, **vars(self.transpile_options)
)
except TranspilerError as err:
raise QiskitError("Failed to transpile interleaved_element.") from err
interleaved_circ = _truncate_inactive_qubits(
interleaved_circ, active_qubits=interleaved_circ.qubits[: self.num_qubits]
)
# Convert transpiled circuit to operation
need_to_translate = False
if target:
for inst in interleaved_circ:
qargs = tuple(interleaved_circ.find_bit(q).index for q in inst.qubits)
if (inst.operation.name, qargs) not in target.supported_instructions:
need_to_translate = True
break

if need_to_translate:
# Translate basis of circuit with target
interleaved_circ = _translate_basis(interleaved_circ, target)
# Convert translated circuit to operation
if len(interleaved_circ) == 1:
self._interleaved_op = interleaved_circ.data[0].operation
else:
Expand Down
51 changes: 16 additions & 35 deletions qiskit_experiments/library/randomized_benchmarking/rb_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
from collections import defaultdict
from numbers import Integral
from typing import Union, Iterable, Optional, List, Sequence, Tuple
from typing import Union, Iterable, Optional, List, Sequence

import numpy as np
from numpy.random import Generator, default_rng
Expand All @@ -32,6 +32,7 @@
from qiskit_experiments.framework.restless_mixin import RestlessMixin
from .clifford_utils import (
CliffordUtils,
ReducedTarget,
compose_1q,
compose_2q,
inverse_1q,
Expand Down Expand Up @@ -140,7 +141,6 @@ def _default_experiment_options(cls) -> Options:
seed=None,
full_sampling=None,
)

return options

def _set_backend(self, backend: Backend):
Expand Down Expand Up @@ -191,32 +191,16 @@ def _sample_sequences(self) -> List[Sequence[SequenceElementType]]:

return sequences

def _get_basis_gates(self) -> Optional[Tuple[str, ...]]:
"""Get sorted basis gates to use in basis transformation during circuit generation.
def _get_reduced_target(self) -> Optional[ReducedTarget]:
"""Get a reduced target to use in basis transformation during circuit generation.

Returns:
Sorted basis gate names.
Reduced target or None if there is no corresponding target.
"""
basis_gates = self.transpile_options.get("basis_gates", None)
if not basis_gates and self.backend:
if isinstance(self.backend, BackendV2):
# Only the "global basis gates" are returned for v2 backend.
# Some non-global basis gates may be usable for some physical qubits. However,
# they are conservatively removed here because the basis gates are agnostic to
# the direction of each gate.
basis_gates = self.backend.operation_names
non_globals = self.backend.target.get_non_global_operation_names(
strict_direction=True
)
if non_globals:
basis_gates = set(basis_gates) - set(non_globals)
else:
basis_gates = self.backend.configuration().basis_gates

if basis_gates is not None:
basis_gates = tuple(sorted(basis_gates))

return basis_gates
if isinstance(self.backend, BackendV2) and self.backend.target is not None:
return ReducedTarget(self.backend.target, self.physical_qubits)

return None

def _sequences_to_circuits(
self, sequences: List[Sequence[SequenceElementType]]
Expand All @@ -226,7 +210,7 @@ def _sequences_to_circuits(
Returns:
A list of RB circuits.
"""
basis_gates = self._get_basis_gates()
target = self._get_reduced_target()
# Circuit generation
circuits = []
for i, seq in enumerate(sequences):
Expand All @@ -239,15 +223,15 @@ def _sequences_to_circuits(
circ = QuantumCircuit(self.num_qubits)
circ.append(Barrier(self.num_qubits), circ.qubits)
for elem in seq:
circ.append(self._to_instruction(elem, basis_gates), circ.qubits)
circ.append(self._to_instruction(elem, target), circ.qubits)
circ.append(Barrier(self.num_qubits), circ.qubits)

# Compute inverse, compute only the difference from the previous shorter sequence
prev_elem = self.__compose_clifford_seq(prev_elem, seq[len(prev_seq) :])
prev_seq = seq
inv = self.__adjoint_clifford(prev_elem)

circ.append(self._to_instruction(inv, basis_gates), circ.qubits)
circ.append(self._to_instruction(inv, target), circ.qubits)
circ.measure_all() # includes insertion of the barrier before measurement
circuits.append(circ)
return circuits
Expand All @@ -264,14 +248,14 @@ def __sample_sequence(self, length: int, rng: Generator) -> Sequence[SequenceEle
return [random_clifford(self.num_qubits, rng).to_circuit() for _ in range(length)]

def _to_instruction(
self, elem: SequenceElementType, basis_gates: Optional[Tuple[str, ...]] = None
self, elem: SequenceElementType, target: Optional[ReducedTarget] = None
) -> Instruction:
# Switching for speed up
if isinstance(elem, Integral):
if self.num_qubits == 1:
return _clifford_1q_int_to_instruction(elem, basis_gates)
return _clifford_1q_int_to_instruction(elem, target)
if self.num_qubits == 2:
return _clifford_2q_int_to_instruction(elem, basis_gates)
return _clifford_2q_int_to_instruction(elem, target)

return elem.to_instruction()

Expand Down Expand Up @@ -310,10 +294,7 @@ def __adjoint_clifford(self, op: SequenceElementType) -> SequenceElementType:
def _transpiled_circuits(self) -> List[QuantumCircuit]:
"""Return a list of experiment circuits, transpiled."""
has_custom_transpile_option = (
any(
opt not in {"basis_gates", "optimization_level"}
for opt in vars(self.transpile_options)
)
any(opt != "optimization_level" for opt in vars(self.transpile_options))
or self.transpile_options.get("optimization_level", 0) != 0
)
if self.num_qubits > 2 or has_custom_transpile_option:
Expand Down
Loading