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

Cleaner specification for RB experiments #1030

Closed
wants to merge 3 commits into from
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
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,17 @@ def _circuit_compose(
return self


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


def _synthesize_clifford_circuit(
circuit: QuantumCircuit, basis_gates: Tuple[str]
) -> QuantumCircuit:
# synthesizes clifford circuits using given basis gates, for use during
# custom transpilation during RB circuit generation.
return transpile(circuit, basis_gates=list(basis_gates), optimization_level=1)
return transpile(
circuit,
basis_gates=list(basis_gates),
coupling_map=[[0, 1]] if circuit.num_qubits == 2 else None,
optimization_level=1,
)


@lru_cache(maxsize=None)
Expand Down Expand Up @@ -560,6 +554,7 @@ def _create_cliff_2q_layer_2():
_create_cliff_2q_layer_1(),
_create_cliff_2q_layer_2(),
)
# _NUM_LAYER_0 = 36
_NUM_LAYER_1 = 20
_NUM_LAYER_2 = 16

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""
Interleaved RB Experiment class.
"""
import copy
import itertools
import warnings
from typing import Union, Iterable, Optional, List, Sequence, Tuple
Expand All @@ -20,14 +21,11 @@
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 import Options
from qiskit_experiments.framework.backend_timing import BackendTiming
from .clifford_utils import _truncate_inactive_qubits
from .clifford_utils import num_from_1q_circuit, num_from_2q_circuit
from .interleaved_rb_analysis import InterleavedRBAnalysis
from .standard_rb import StandardRB, SequenceElementType
Expand Down Expand Up @@ -59,7 +57,7 @@ class InterleavedRB(StandardRB):

def __init__(
self,
interleaved_element: Union[QuantumCircuit, Gate, Delay, Clifford],
interleaved_element: Union[QuantumCircuit, Gate, Delay],
physical_qubits: Sequence[int],
lengths: Iterable[int],
backend: Optional[Backend] = None,
Expand All @@ -71,10 +69,9 @@ def __init__(
"""Initialize an interleaved randomized benchmarking experiment.

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.
interleaved_element: The Clifford element to interleave,
given either as a gate, delay or circuit.
It must consists only of basis gates if a backend is supplied.
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 @@ -186,8 +183,7 @@ def circuits(self) -> List[QuantumCircuit]:
A list of :class:`QuantumCircuit`.

Raises:
QiskitError: If the ``interleaved_element`` provided to the constructor
cannot be transpiled.
QiskitError: If ``interleaved_element`` contains any gate not in backend's basis gates.
"""
# Convert interleaved element to transpiled circuit operation and store it for speed
self.__set_up_interleaved_op()
Expand Down Expand Up @@ -233,37 +229,16 @@ def _to_instruction(
return super()._to_instruction(elem, basis_gates)

def __set_up_interleaved_op(self) -> None:
# Convert interleaved element to transpiled circuit operation and store it for speed
self._interleaved_op = self._interleaved_element
self._interleaved_op = copy.deepcopy(self._interleaved_element)
# Validate if interleaved element consists only of basis gates
basis_gates = self._get_basis_gates()
# Convert interleaved element to circuit
if isinstance(self._interleaved_op, Clifford):
self._interleaved_op = self._interleaved_op.to_circuit()

if isinstance(self._interleaved_op, QuantumCircuit):
interleaved_circ = self._interleaved_op
elif isinstance(self._interleaved_op, Gate):
interleaved_circ = QuantumCircuit(self.num_qubits, name=self._interleaved_op.name)
interleaved_circ.append(self._interleaved_op, list(range(self.num_qubits)))
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
if len(interleaved_circ) == 1:
self._interleaved_op = interleaved_circ.data[0].operation
else:
self._interleaved_op = interleaved_circ
if basis_gates:
ops = [self._interleaved_op]
if isinstance(self._interleaved_op, QuantumCircuit):
ops = [i.operation for i in self._interleaved_op]
for op in ops:
if op.name not in basis_gates:
raise QiskitError(f"interleaved_element contains a non-basis gate: {op.name}")

# Store interleaved operation as Instruction
if isinstance(self._interleaved_op, QuantumCircuit):
Expand Down
33 changes: 19 additions & 14 deletions qiskit_experiments/library/randomized_benchmarking/standard_rb.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,16 @@ def _get_basis_gates(self) -> Optional[Tuple[str, ...]]:

- Return None if this experiment is an RB with 3 or more qubits.
- Return None if no basis gates are supplied via ``backend`` or ``transpile_options``.
- Return None if all 2q-gates supported on the physical qubits of the backend are one-way
directed (e.g. cx(0, 1) is supported but cx(1, 0) is not supported).

In all those case when None are returned, basis transformation will be skipped in the
circuit generation step (i.e. :meth:`circuits`) and it will be done in the successive
transpilation step (i.e. :meth:`_transpiled_circuits`) that calls :func:`transpile`.

Returns:
Sorted basis gate names.

Raises:
QiskitError: If ``backend`` has no 2q-basis-gate on ``physical_qubits`` in 2Q RB case.
"""
# 3 or more qubits case: Return None (skip basis transformation in circuit generation)
if self.num_qubits > 2:
Expand All @@ -236,30 +237,34 @@ def _get_basis_gates(self) -> Optional[Tuple[str, ...]]:
if not basis_gates and self.backend:
if isinstance(self.backend, BackendV2):
basis_gates = self.backend.operation_names
elif isinstance(self.backend, BackendV1):
elif isinstance(self.backend, BackendV1): # TODO: remove after BackendV2 simulator
basis_gates = self.backend.configuration().basis_gates
return tuple(sorted(basis_gates)) if basis_gates else None

def is_bidirectional(coupling_map):
return len(coupling_map.reduce(self.physical_qubits).get_edges()) == 2

# 2 qubits case: Return all basis gates except for one-way directed 2q-gates.
# Return None if there is no bidirectional 2q-gates in basis gates.
# 2 qubits case: Return all basis gates except for one-way directed 2q-gate
# not defined on the physical qubits.
if self.num_qubits == 2:
basis_gates = self.transpile_options.get("basis_gates", [])
if not basis_gates and self.backend:
if isinstance(self.backend, BackendV2) and self.backend.target:
has_bidirectional_2q_gates = False
has_2q_gate = False
for op_name in self.backend.target:
if self.backend.target.operation_from_name(op_name).num_qubits == 2:
if is_bidirectional(self.backend.target.build_coupling_map(op_name)):
has_bidirectional_2q_gates = True
else:
continue
basis_gates.append(op_name)
if not has_bidirectional_2q_gates:
basis_gates = None
elif isinstance(self.backend, BackendV1):
op_coupling = self.backend.target.build_coupling_map(op_name)
if self.physical_qubits in op_coupling.get_edges():
basis_gates.append(op_name)
has_2q_gate = True
else:
basis_gates.append(op_name)
if not has_2q_gate:
raise QiskitError(
f"Backend {self.backend.name} has no 2q-basis-gate on "
f"physical qubit {self.physical_qubits}."
)
elif isinstance(self.backend, BackendV1): # TODO: remove after BackendV2 simulator
cmap = self.backend.configuration().coupling_map
if cmap is None or is_bidirectional(CouplingMap(cmap)):
basis_gates = self.backend.configuration().basis_gates
Expand Down
69 changes: 66 additions & 3 deletions test/library/randomized_benchmarking/test_interleaved_rb.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,15 +266,15 @@ def test_interleaved_circuit_is_decomposed(self):
self.assertTrue(all(not inst.operation.name.startswith("Clifford") for inst in qc))

def test_interleaving_cnot_gate_with_non_supported_direction(self):
"""Test if cx(0, 1) can be interleaved for backend that support only cx(1, 0)."""
"""Test if, for backend that support only cx(1, 0), cx(1, 0) can be interleaved
but cx(0, 1) cannot."""
my_backend = FakeManilaV2()
del my_backend.target["cx"][(0, 1)] # make support only cx(1, 0)

exp = rb.InterleavedRB(
interleaved_element=CXGate(),
physical_qubits=(0, 1),
physical_qubits=(1, 0), # supported
lengths=[3],
num_samples=4,
backend=my_backend,
seed=1234,
)
Expand All @@ -286,6 +286,69 @@ def test_interleaving_cnot_gate_with_non_supported_direction(self):
if inst.operation.name == "cx":
self.assertEqual(inst.qubits, expected_qubits)

exp = rb.InterleavedRB(
interleaved_element=CXGate(),
physical_qubits=(0, 1), # not supported
lengths=[3],
backend=my_backend,
)
with self.assertRaises(QiskitError):
exp._transpiled_circuits()

def test_interleaving_circuit_with_gates_with_non_supported_direction(self):
"""Test if, for backend that support only cx(2, 1),
circuit only with cx(2, 1) can be interleaved but circuit with cx(1, 2) cannot."""
my_backend = FakeManilaV2()
del my_backend.target["cx"][(1, 2)] # make support only cx(2, 1)

circ = QuantumCircuit(2)
circ.sx(0)
circ.cx(0, 1)
exp = rb.InterleavedRB(
interleaved_element=circ,
physical_qubits=(2, 1), # supported
lengths=[3],
backend=my_backend,
)
transpiled = exp._transpiled_circuits()
for qc in transpiled:
self.assertTrue(qc.count_ops().get("cx", 0) > 0)
expected_qubits = (qc.qubits[2], qc.qubits[1])
for inst in qc:
if inst.operation.name == "cx":
self.assertEqual(inst.qubits, expected_qubits)

exp = rb.InterleavedRB(
interleaved_element=circ,
physical_qubits=(1, 2), # not supported
lengths=[3],
backend=my_backend,
)
with self.assertRaises(QiskitError):
exp._transpiled_circuits()

def test_interleaved_element_contains_non_basis_gates(self):
"""Raise if interleaved_element contains any non basis gate"""
with self.assertRaises(QiskitError):
rb.InterleavedRB(
interleaved_element=CZGate(), # CZ gate is not in Manila's basis gates
physical_qubits=[0, 1],
lengths=[1, 2, 3],
backend=self.backend,
).circuits()

qc_cnot = QuantumCircuit(2)
qc_cnot.h(1)
qc_cnot.cz(0, 1)
qc_cnot.h(1)
with self.assertRaises(QiskitError):
rb.InterleavedRB(
interleaved_element=qc_cnot, # Equivalent with CNOT but it contains CZ (non basis)
physical_qubits=[0, 1],
lengths=[1, 2, 3],
backend=self.backend,
).circuits()


class TestRunInterleavedRB(QiskitExperimentsTestCase, RBTestMixin):
"""Test for running InterleavedRB."""
Expand Down
4 changes: 2 additions & 2 deletions test/library/randomized_benchmarking/test_standard_rb.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ def test_calibrations_via_custom_backend(self):
def test_backend_with_directed_basis_gates(self):
"""Test if correct circuits are generated from backend with directed basis gates."""
my_backend = copy.deepcopy(self.backend)
del my_backend.target["cx"][(1, 2)] # make cx on {1, 2} one-sided
del my_backend.target["cx"][(1, 2)] # make support only cx(2, 1)

exp = rb.StandardRB(physical_qubits=(1, 2), lengths=[3], num_samples=4, backend=my_backend)
exp = rb.StandardRB(physical_qubits=(2, 1), lengths=[3], num_samples=4, backend=my_backend)
transpiled = exp._transpiled_circuits()
for qc in transpiled:
self.assertTrue(qc.count_ops().get("cx", 0) > 0)
Expand Down