From f352b3cc4fd2b04f4d687415d517890801473000 Mon Sep 17 00:00:00 2001
From: Toshinari Itoko <15028342+itoko@users.noreply.github.com>
Date: Wed, 1 May 2024 22:07:52 +0900
Subject: [PATCH] Add layer fidelity experiment (#1322)
### Summary
Add a new experiment class to measure layer fidelity, which is a
holistic benchmark to characterize the full quality of the devices at
scale (https://arxiv.org/abs/2311.05933)
Example notebook: [run_lf_qiskit_experiments_large.ipynb
(Gist)](https://gist.github.com/itoko/28c7cc117614c67e2a1899a3757d4ad1)
### Experimental features:
- Exceptionally `circuits()` method returns circuits on physical qubits
(not virtual qubits as usual)
- Add `reason` as an extra analysis result entry to tell users why the
`quality` of the analysis was "bad"
### Follow-up items:
- Add API for customizing DD (e.g. register DD sequence generator by
name and specify the name in experiment_options)
```
def dd_func1(delay_length, backend) -> list[Instruction];
LayerFidelity.dd_functions = {
"dd1": dd_func1,
"dd2": dd_func2,
}
```
### Features decided not to include:
- `full_sampling` option (`StandardRB` has). `LayerFidelity` behaves as
if setting always `full_sampling==True` to avoid correlation between
sequences, which RB theory does not consider.
- `replicate_in_parallel` option that allows to use a common direct RB
sequence for all qubit pairs. It turned out that
`replicate_in_parallel==True` may underestimate some types of errors,
suggesting it should always be set to `False`.
### Issues to be addressed separately
- Poor interface for querying figures: No pointer to a relevant figure
(or data used for fitting) is stored in `AnalysisResult` (i.e. users who
find a bad fitting result in `AnalysisResult` cannot easily look at a
figure relevant to the result)
==> For now, you can query a figure by its name; e.g.
`exp_data.figure("DirectRB_Q83_Q82")`
### Dependencies:
- [x] #1288 for Clifford synthesis with unidirectional 2q gates (e.g.
IBM Eagle processors)
---
.../measurement/readout_mitigation.rst | 2 +-
.../randomized_benchmarking/__init__.py | 4 +
.../randomized_benchmarking/clifford_utils.py | 22 +-
.../data/clifford_tensor_1q.npz | Bin 0 -> 1355 bytes
.../data/generate_clifford_data.py | 18 +-
.../randomized_benchmarking/layer_fidelity.py | 478 ++++++++++++++++++
.../layer_fidelity_analysis.py | 353 +++++++++++++
.../layer-fidelity-1e09dea9e5b69515.yaml | 25 +
.../test_layer_fidelity.py | 219 ++++++++
9 files changed, 1113 insertions(+), 8 deletions(-)
create mode 100644 qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz
create mode 100644 qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py
create mode 100644 qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py
create mode 100644 releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml
create mode 100644 test/library/randomized_benchmarking/test_layer_fidelity.py
diff --git a/docs/manuals/measurement/readout_mitigation.rst b/docs/manuals/measurement/readout_mitigation.rst
index cc145b47f1..1a6b8d54d7 100644
--- a/docs/manuals/measurement/readout_mitigation.rst
+++ b/docs/manuals/measurement/readout_mitigation.rst
@@ -106,7 +106,7 @@ Mitigation example
.. jupyter-execute::
qc = QuantumCircuit(num_qubits)
- qc.h(0)
+ qc.sx(0)
for i in range(1, num_qubits):
qc.cx(i - 1, i)
qc.measure_all()
diff --git a/qiskit_experiments/library/randomized_benchmarking/__init__.py b/qiskit_experiments/library/randomized_benchmarking/__init__.py
index 371f0a3679..6ec3daa157 100644
--- a/qiskit_experiments/library/randomized_benchmarking/__init__.py
+++ b/qiskit_experiments/library/randomized_benchmarking/__init__.py
@@ -25,6 +25,7 @@
StandardRB
InterleavedRB
+ LayerFidelity
Analysis
@@ -36,6 +37,7 @@
RBAnalysis
InterleavedRBAnalysis
+ LayerFidelityAnalysis
Synthesis
=========
@@ -61,3 +63,5 @@
from .clifford_utils import CliffordUtils
from .rb_utils import RBUtils
from .clifford_synthesis import RBDefaultCliffordSynthesis
+from .layer_fidelity import LayerFidelity
+from .layer_fidelity_analysis import LayerFidelityAnalysis
diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py
index a081db7006..666f8b0cc9 100644
--- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py
+++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py
@@ -46,6 +46,7 @@
_valid_sparse_indices = _clifford_compose_2q_data["valid_sparse_indices"]
# map a clifford number to the index of _CLIFFORD_COMPOSE_2Q_DENSE
_clifford_num_to_dense_index = {idx: ii for ii, idx in enumerate(_valid_sparse_indices)}
+_CLIFFORD_TENSOR_1Q = np.load(f"{_DATA_FOLDER}/clifford_tensor_1q.npz")["table"]
# Transpilation utilities
def _transpile_clifford_circuit(
@@ -486,7 +487,11 @@ def inverse_1q(num: Integral) -> Integral:
def num_from_1q_circuit(qc: QuantumCircuit) -> Integral:
- """Convert a given 1-qubit Clifford circuit to the corresponding integer."""
+ """Convert a given 1-qubit Clifford circuit to the corresponding integer.
+
+ Note: The circuit must consist of gates in :const:`_CLIFF_SINGLE_GATE_MAP_1Q`,
+ RZGate, Delay and Barrier.
+ """
num = 0
for inst in qc:
rhs = _num_from_1q_gate(op=inst.operation)
@@ -497,7 +502,7 @@ def num_from_1q_circuit(qc: QuantumCircuit) -> Integral:
def _num_from_1q_gate(op: Instruction) -> int:
"""
Convert a given 1-qubit clifford operation to the corresponding integer.
- Note that supported operations are limited to ones in :const:`CLIFF_SINGLE_GATE_MAP_1Q` or Rz gate.
+ Note that supported operations are limited to ones in :const:`_CLIFF_SINGLE_GATE_MAP_1Q` or Rz gate.
Args:
op: operation to be converted.
@@ -556,7 +561,11 @@ def inverse_2q(num: Integral) -> Integral:
def num_from_2q_circuit(qc: QuantumCircuit) -> Integral:
- """Convert a given 2-qubit Clifford circuit to the corresponding integer."""
+ """Convert a given 2-qubit Clifford circuit to the corresponding integer.
+
+ Note: The circuit must consist of gates in :const:`_CLIFF_SINGLE_GATE_MAP_2Q`,
+ RZGate, Delay and Barrier.
+ """
lhs = 0
for rhs in _clifford_2q_nums_from_2q_circuit(qc):
lhs = _CLIFFORD_COMPOSE_2Q_DENSE[lhs, _clifford_num_to_dense_index[rhs]]
@@ -568,7 +577,7 @@ def _num_from_2q_gate(
) -> int:
"""
Convert a given 1-qubit clifford operation to the corresponding integer.
- Note that supported operations are limited to ones in `CLIFF_SINGLE_GATE_MAP_2Q` or Rz gate.
+ Note that supported operations are limited to ones in `_CLIFF_SINGLE_GATE_MAP_2Q` or Rz gate.
Args:
op: operation of instruction to be converted.
@@ -730,3 +739,8 @@ def _layer_indices_from_num(num: Integral) -> Tuple[Integral, Integral, Integral
idx1 = num % _NUM_LAYER_1
idx0 = num // _NUM_LAYER_1
return idx0, idx1, idx2
+
+
+def _tensor_1q_nums(first: Integral, second: Integral) -> Integral:
+ """Return the 2-qubit Clifford integer that is the tensor product of 1-qubit Cliffords."""
+ return _CLIFFORD_TENSOR_1Q[first, second]
diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz
new file mode 100644
index 0000000000000000000000000000000000000000..7725682f70de413190f39651eba0279b168346d4
GIT binary patch
literal 1355
zcmZ{keM}Q)9LL*LoD2y@aQG599hr!zjWWX2(AzXy6LA4!jM>oMkSt`wjS**v?ez}C
zH>T1lDk9EvZbp_R>lQPcMX#rz6xU*%Z)Cpo)
7
zg@7U|G^!j)E&kL`JxbI{vG+u*i}YBOrAP>o3)K^>AZdxK)ibED({!T}6kri{>mP~4
zc=Cw7fLpoaJp5m6W5S3$10AVVPB4-E_#sLtQU``KIlZ;zQb4p~Ps@9xO9A?DOFkAF
z!cX$CM&kNrIS+}Lub||I{bh1C#ns^!K7)3}^Y7WTOQ&4Q4)@`0m^h2@IOI36U=!Yo
z#!5_skY*p)svr}I2eeDM$>#(!A*~#6h^yVbkVc2^Ds2r>NJq-wnzW9c9f`Yd+n^8P
zP!4?>4&~4XaZR3)wZ+1}4KqN{!q>a6?+5t|SD~~!qcAWpze0xQ;isrrq5GPaGqHx>
zMZ0!zRz6lk40g)-2xrDmv$0vkbq|mRf3JCmc8(FA23e1Uc{vk;%AQK0RK4nw
zGtkII{2&wEN@Jt;^U}G%Wh?U_(d-cBsqIK~1JPRu4A^iX6pQbxy)Jnr6+URip3y%@
z^>l9BeJ7E}1YgFFF~eo}VKka;x;Z2p*pN{YjoPo80i?6!MQE@?X>rD&*bkPF!D>yMN~@~S
kzm#M6zy9}co747Yy|(|?4XY~BrfLs*ILqLvPIkn90J2Q^Qvd(}
literal 0
HcmV?d00001
diff --git a/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py
index 7ebd45dcab..5193dc35d8 100644
--- a/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py
+++ b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py
@@ -59,7 +59,7 @@ def _hash_cliff(cliff):
def gen_clifford_inverse_1q():
- """Generate table data for integer 1Q Clifford inversion"""
+ """Generate data for integer 1Q Clifford inversion table"""
invs = np.empty(NUM_CLIFFORD_1Q, dtype=int)
for i, cliff_i in _CLIFF_1Q.items():
invs[i] = _TO_INT_1Q[_hash_cliff(cliff_i.adjoint())]
@@ -68,7 +68,7 @@ def gen_clifford_inverse_1q():
def gen_clifford_compose_1q():
- """Generate table data for integer 1Q Clifford composition."""
+ """Generate data for integer 1Q Clifford composition table"""
products = np.empty((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int)
for i, cliff_i in _CLIFF_1Q.items():
for j, cliff_j in _CLIFF_1Q.items():
@@ -83,7 +83,7 @@ def gen_clifford_compose_1q():
def gen_clifford_inverse_2q():
- """Generate table data for integer 2Q Clifford inversion"""
+ """Generate data for integer 2Q Clifford inversion table"""
invs = np.empty(NUM_CLIFFORD_2Q, dtype=int)
for i, cliff_i in _CLIFF_2Q.items():
invs[i] = _TO_INT_2Q[_hash_cliff(cliff_i.adjoint())]
@@ -191,6 +191,16 @@ def gen_cliff_single_2q_gate_map():
return table
+def gen_clifford_tensor_1q():
+ """Generate data for 2Q integer Clifford table of the tensor product of 1Q integer Cliffords."""
+ products = np.empty((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int)
+ for i, cliff_i in _CLIFF_1Q.items():
+ for j, cliff_j in _CLIFF_1Q.items():
+ cliff = cliff_i.tensor(cliff_j)
+ products[i, j] = _TO_INT_2Q[_hash_cliff(cliff)]
+ return products
+
+
if __name__ == "__main__":
if _CLIFF_SINGLE_GATE_MAP_1Q != gen_cliff_single_1q_gate_map():
raise Exception(
@@ -212,3 +222,5 @@ def gen_cliff_single_2q_gate_map():
table=_CLIFFORD_COMPOSE_2Q_DENSE,
valid_sparse_indices=valid_sparse_indices,
)
+
+ np.savez_compressed("clifford_tensor_1q.npz", table=gen_clifford_tensor_1q())
diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py
new file mode 100644
index 0000000000..40c5247f87
--- /dev/null
+++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py
@@ -0,0 +1,478 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2023.
+#
+# 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.
+"""
+Layer Fidelity RB Experiment class.
+"""
+import functools
+import logging
+from collections import defaultdict
+from typing import Union, Iterable, Optional, List, Sequence, Tuple, Dict
+
+from numpy.random import Generator, default_rng
+from numpy.random.bit_generator import BitGenerator, SeedSequence
+
+from qiskit.circuit import QuantumCircuit, CircuitInstruction, Barrier, Gate
+from qiskit.circuit.library import get_standard_gate_name_mapping
+from qiskit.exceptions import QiskitError
+from qiskit.providers import BackendV2Converter
+from qiskit.providers.backend import Backend, BackendV1, BackendV2
+from qiskit.quantum_info import Clifford
+from qiskit.pulse.instruction_schedule_map import CalibrationPublisher
+
+from qiskit_experiments.framework import BaseExperiment, Options
+from qiskit_experiments.framework.configs import ExperimentConfig
+from qiskit_experiments.framework.restless_mixin import RestlessMixin
+
+from .clifford_utils import (
+ CliffordUtils,
+ DEFAULT_SYNTHESIS_METHOD,
+ compose_1q,
+ compose_2q,
+ inverse_1q,
+ inverse_2q,
+ num_from_2q_circuit,
+ _tensor_1q_nums,
+ _clifford_1q_int_to_instruction,
+ _clifford_2q_int_to_instruction,
+ _decompose_clifford_ops,
+)
+from .layer_fidelity_analysis import LayerFidelityAnalysis
+
+LOG = logging.getLogger(__name__)
+
+
+GATE_NAME_MAP = get_standard_gate_name_mapping()
+NUM_1Q_CLIFFORD = CliffordUtils.NUM_CLIFFORD_1_QUBIT
+
+
+class LayerFidelity(BaseExperiment, RestlessMixin):
+ r"""A holistic benchmarking experiment to characterize the full quality of the devices at scale.
+
+ # section: overview
+ Layer Fidelity (LF) is a method to estimate the fidelity of
+ a connecting set of two-qubit gates over :math:`N` qubits by measuring gate errors
+ using simultaneous direct randomized benchmarking (RB) in disjoint layers.
+ LF can easily be expressed as a layer size independent quantity, error per layered gate (EPLG):
+ :math:`EPLG = 1 - LF^{1/N_{2Q}}` where :math:`N_{2Q}` is number of 2-qubit gates in the layers.
+
+ Each of the 2-qubit (or 1-qubit) direct RBs yields the decaying probabilities
+ to get back to the ground state for an increasing sequence length (i.e. number of layers),
+ fits the exponential curve to estimate the decay rate, and calculates
+ the process fidelity of the subsystem from the rate.
+ LF is calculated as the product of the 2-qubit (or 1-qubit) process fidelities.
+ See Ref. [1] for details.
+
+ # section: analysis_ref
+ :class:`LayerFidelityAnalysis`
+
+ # section: reference
+ .. ref_arxiv:: 1 2311.05933
+ """
+
+ def __init__(
+ self,
+ physical_qubits: Sequence[int],
+ two_qubit_layers: Sequence[Sequence[Tuple[int, int]]],
+ lengths: Iterable[int],
+ backend: Optional[Backend] = None,
+ num_samples: int = 6,
+ seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None,
+ two_qubit_gate: Optional[str] = None,
+ one_qubit_basis_gates: Optional[Sequence[str]] = None,
+ ):
+ """Initialize a layer fidelity experiment.
+
+ Args:
+ physical_qubits: List of physical qubits for the experiment.
+ two_qubit_layers: List of two-qubit gate layers to run on. Each two-qubit
+ gate layer must be given as a list of directed qubit pairs.
+ lengths: A list of layer lengths (the number of depth points).
+ backend: The backend to run the experiment on. Note that either ``backend`` or
+ ``two_qubit_gate`` and ``one_qubit_basis_gates`` must be set at instantiation.
+ num_samples: Number of samples (i.e. circuits) to generate for each layer length.
+ seed: Optional, seed used to initialize ``numpy.random.default_rng``.
+ when generating circuits. The ``default_rng`` will be initialized
+ with this seed value every time :meth:~.LayerFidelity.circuits` is called.
+ two_qubit_gate: Optional, 2q-gate name (e.g. "cx", "cz", "ecr")
+ of which the two qubit layers consist.
+ If not specified (but ``backend`` is supplied),
+ one of 2q-gates supported in the backend is automatically set.
+ one_qubit_basis_gates: Optional, 1q-gates to use for implementing 1q-Clifford operations.
+ If not specified (but ``backend`` is supplied),
+ all 1q-gates supported in the backend are automatically set.
+
+ Raises:
+ QiskitError: If any invalid argument is supplied.
+ """
+ # Compute full layers
+ full_layers = []
+ for two_q_layer in two_qubit_layers:
+ qubits_in_layer = {q for qpair in two_q_layer for q in qpair}
+ if len(qubits_in_layer) != 2 * len(two_q_layer):
+ raise QiskitError("two_qubit_layers have a layer with gates on non-disjoint qubits")
+ for q in qubits_in_layer:
+ if q not in physical_qubits:
+ raise QiskitError(f"Qubit {q} in two_qubit_layers is not in physical_qubits")
+ layer = two_q_layer + [(q,) for q in physical_qubits if q not in qubits_in_layer]
+ full_layers.append(layer)
+
+ # Initialize base experiment
+ super().__init__(
+ physical_qubits, analysis=LayerFidelityAnalysis(full_layers), backend=backend
+ )
+ # assert isinstance(backend, BackendV2)
+
+ # Verify parameters
+ if len(set(lengths)) != len(lengths):
+ raise QiskitError(f"The lengths list {lengths} should not contain duplicate elements.")
+ if num_samples <= 0:
+ raise QiskitError(f"The number of samples {num_samples} should be positive.")
+
+ if two_qubit_gate is None:
+ if self.backend is None:
+ raise QiskitError("two_qubit_gate or backend must be supplied.")
+ # Try to set default two_qubit_gate from backend
+ for op in self.backend.target.operations:
+ if isinstance(op, Gate) and op.num_qubits == 2:
+ two_qubit_gate = op.name
+ LOG.info("%s is set for two_qubit_gate", op.name)
+ break
+ if not two_qubit_gate:
+ raise QiskitError("two_qubit_gate is not provided and failed to set from backend.")
+ else:
+ if self.backend is None and two_qubit_gate not in GATE_NAME_MAP:
+ raise QiskitError(f"Unknown two_qubit_gate: {two_qubit_gate}.")
+
+ if one_qubit_basis_gates is None:
+ if self.backend is None:
+ raise QiskitError("one_qubit_basis_gates or backend must be supplied.")
+ # Try to set default one_qubit_basis_gates from backend
+ one_qubit_basis_gates = []
+ for op in self.backend.target.operations:
+ if isinstance(op, Gate) and op.num_qubits == 1:
+ one_qubit_basis_gates.append(op.name)
+ LOG.info("%s is set for one_qubit_basis_gates", str(one_qubit_basis_gates))
+ if not one_qubit_basis_gates:
+ raise QiskitError(
+ "one_qubit_basis_gates is not provided and failed to set from backend."
+ )
+ else:
+ if self.backend is None:
+ for gate in one_qubit_basis_gates:
+ if gate not in GATE_NAME_MAP:
+ raise QiskitError(f"Unknown gate in one_qubit_basis_gates: {gate}.")
+
+ # Set configurable options
+ self.set_experiment_options(
+ lengths=sorted(lengths),
+ num_samples=num_samples,
+ seed=seed,
+ two_qubit_layers=two_qubit_layers,
+ two_qubit_gate=two_qubit_gate,
+ one_qubit_basis_gates=tuple(one_qubit_basis_gates),
+ )
+
+ # Verify two_qubit_gate and one_qubit_basis_gates
+ self.__validate_basis_gates()
+
+ @classmethod
+ def _default_experiment_options(cls) -> Options:
+ """Default experiment options.
+
+ Experiment Options:
+ two_qubit_layers (List[List[Tuple[int, int]]]): List of two-qubit gate layers to run on.
+ Each two-qubit gate layer must be given as a list of directed qubit pairs.
+ lengths (List[int]): A list of layer lengths.
+ num_samples (int): Number of samples to generate for each layer length.
+ seed (None or int or SeedSequence or BitGenerator or Generator): A seed
+ used to initialize ``numpy.random.default_rng`` when generating circuits.
+ The ``default_rng`` will be initialized with this seed value every time
+ :meth:`circuits` is called.
+ two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr")
+ of which the two qubit layers consist.
+ one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Cliffords.
+ clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use
+ for building circuits of RB sequences.
+ """
+ options = super()._default_experiment_options()
+ options.update_options(
+ lengths=None,
+ num_samples=None,
+ seed=None,
+ two_qubit_layers=None,
+ two_qubit_gate=None,
+ one_qubit_basis_gates=None,
+ clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD,
+ )
+ return options
+
+ def set_experiment_options(self, **fields):
+ """Set the experiment options.
+
+ Args:
+ fields: The fields to update the options
+
+ Raises:
+ AttributeError: If the field passed in is not a supported options
+ """
+ for field in fields:
+ if field in {"two_qubit_layers"}:
+ if (
+ hasattr(self._experiment_options, field)
+ and self._experiment_options[field] is not None
+ ):
+ raise AttributeError(f"Options field {field} is not allowed to update.")
+ super().set_experiment_options(**fields)
+
+ @classmethod
+ def _default_transpile_options(cls) -> Options:
+ """Default transpiler options for transpiling RB circuits."""
+ return Options(optimization_level=1)
+
+ def set_transpile_options(self, **fields):
+ """Transpile options is not supported for LayerFidelity experiments.
+
+ Raises:
+ QiskitError: If `set_transpile_options` is called.
+ """
+ raise QiskitError(
+ "Custom transpile options is not supported for LayerFidelity experiments."
+ )
+
+ def _set_backend(self, backend: Backend):
+ """Set the backend V2 for RB experiments since RB experiments only support BackendV2.
+ If BackendV1 is provided, it is converted to V2 and stored.
+ """
+ if isinstance(backend, BackendV1):
+ super()._set_backend(BackendV2Converter(backend, add_delay=True))
+ else:
+ super()._set_backend(backend)
+ self.__validate_basis_gates()
+
+ def __validate_basis_gates(self) -> None:
+ if not self.backend:
+ return
+ opts = self.experiment_options
+ # validate two_qubit_gate if it is set
+ if opts.two_qubit_gate:
+ if opts.two_qubit_gate not in self.backend.target.operation_names:
+ raise QiskitError(f"two_qubit_gate {opts.two_qubit_gate} is not in backend.target")
+ for two_q_layer in opts.two_qubit_layers:
+ for qpair in two_q_layer:
+ if not self.backend.target.instruction_supported(opts.two_qubit_gate, qpair):
+ raise QiskitError(f"{opts.two_qubit_gate}{qpair} is not in backend.target")
+ # validate one_qubit_basis_gates if it is set
+ for gate in opts.one_qubit_basis_gates or []:
+ if gate not in self.backend.target.operation_names:
+ raise QiskitError(f"{gate} in one_qubit_basis_gates is not in backend.target")
+ for gate in opts.one_qubit_basis_gates or []:
+ for q in self.physical_qubits:
+ if not self.backend.target.instruction_supported(gate, (q,)):
+ raise QiskitError(f"{gate}({q}) is not in backend.target")
+
+ def __residual_qubits(self, two_qubit_layer):
+ qubits_in_layer = {q for qpair in two_qubit_layer for q in qpair}
+ return [q for q in self.physical_qubits if q not in qubits_in_layer]
+
+ def circuits(self) -> List[QuantumCircuit]:
+ r"""Return a list of physical circuits to measure layer fidelity.
+
+ Returns:
+ A list of :class:`QuantumCircuit`\s.
+ """
+ return list(self.circuits_generator())
+
+ def circuits_generator(self) -> Iterable[QuantumCircuit]:
+ r"""Return a generator of physical circuits to measure layer fidelity.
+
+ Returns:
+ A generator of :class:`QuantumCircuit`\s.
+ """
+ opts = self.experiment_options
+ residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers]
+ rng = default_rng(seed=opts.seed)
+ # define functions and variables for speed
+ _to_gate_1q = functools.partial(
+ _clifford_1q_int_to_instruction,
+ basis_gates=opts.one_qubit_basis_gates,
+ synthesis_method=opts.clifford_synthesis_method,
+ )
+ _to_gate_2q = functools.partial(
+ _clifford_2q_int_to_instruction,
+ basis_gates=(opts.two_qubit_gate,) + opts.one_qubit_basis_gates,
+ coupling_tuple=((0, 1),),
+ synthesis_method=opts.clifford_synthesis_method,
+ )
+ if self.backend:
+ gate2q = self.backend.target.operation_from_name(opts.two_qubit_gate)
+ else:
+ gate2q = GATE_NAME_MAP[opts.two_qubit_gate]
+ gate2q_cliff = num_from_2q_circuit(Clifford(gate2q).to_circuit())
+ # Circuit generation
+ num_qubits = max(self.physical_qubits) + 1
+ for i_sample in range(opts.num_samples):
+ for i_set, (two_qubit_layer, one_qubits) in enumerate(
+ zip(opts.two_qubit_layers, residal_qubits_by_layer)
+ ):
+ num_2q_gates = len(two_qubit_layer)
+ num_1q_gates = len(one_qubits)
+ composite_qubits = two_qubit_layer + [(q,) for q in one_qubits]
+ composite_clbits = [(2 * c, 2 * c + 1) for c in range(num_2q_gates)]
+ composite_clbits.extend(
+ [(c,) for c in range(2 * num_2q_gates, 2 * num_2q_gates + num_1q_gates)]
+ )
+ for length in opts.lengths:
+ circ = QuantumCircuit(num_qubits, num_qubits)
+ barrier_inst = CircuitInstruction(Barrier(num_qubits), circ.qubits)
+ self.__circuit_body(
+ circ,
+ length,
+ two_qubit_layer,
+ one_qubits,
+ rng,
+ _to_gate_1q,
+ _to_gate_2q,
+ gate2q,
+ gate2q_cliff,
+ barrier_inst,
+ )
+ # add the measurements
+ circ._append(barrier_inst)
+ for qubits, clbits in zip(composite_qubits, composite_clbits):
+ circ.measure(qubits, clbits)
+ # store composite structure in metadata
+ circ.metadata = {
+ "experiment_type": "BatchExperiment",
+ "composite_metadata": [
+ {
+ "experiment_type": "ParallelExperiment",
+ "composite_index": list(range(len(composite_qubits))),
+ "composite_metadata": [
+ {
+ "experiment_type": "SubLayerFidelity",
+ "physical_qubits": qpair,
+ "sample": i_sample,
+ "xval": length,
+ }
+ for qpair in two_qubit_layer
+ ]
+ + [
+ {
+ "experiment_type": "SubLayerFidelity",
+ "physical_qubits": (q,),
+ "sample": i_sample,
+ "xval": length,
+ }
+ for q in one_qubits
+ ],
+ "composite_qubits": composite_qubits,
+ "composite_clbits": composite_clbits,
+ }
+ ],
+ "composite_index": [i_set],
+ }
+ yield circ
+
+ @staticmethod
+ def __circuit_body(
+ circ,
+ length,
+ two_qubit_layer,
+ one_qubits,
+ rng,
+ _to_gate_1q,
+ _to_gate_2q,
+ gate2q,
+ gate2q_cliff,
+ barrier_inst,
+ ):
+ # initialize cliffords and a ciruit (0: identity clifford)
+ cliffs_2q = [0] * len(two_qubit_layer)
+ cliffs_1q = [0] * len(one_qubits)
+ for _ in range(length):
+ # sample random 1q-Clifford layer
+ for j, qpair in enumerate(two_qubit_layer):
+ # sample product of two 1q-Cliffords as 2q interger Clifford
+ samples = rng.integers(NUM_1Q_CLIFFORD, size=2)
+ cliffs_2q[j] = compose_2q(cliffs_2q[j], _tensor_1q_nums(*samples))
+ # For Clifford 1 (x) Clifford 2, in its circuit representation,
+ # Clifford 1 acts on the 2nd qubit and Clifford 2 acts on the 1st qubit.
+ # That's why the qpair is reversed here.
+ for sample, q in zip(samples, reversed(qpair)):
+ circ._append(_to_gate_1q(sample), (circ.qubits[q],), ())
+ for k, q in enumerate(one_qubits):
+ sample = rng.integers(NUM_1Q_CLIFFORD)
+ cliffs_1q[k] = compose_1q(cliffs_1q[k], sample)
+ circ._append(_to_gate_1q(sample), (circ.qubits[q],), ())
+ circ._append(barrier_inst)
+ # add two qubit gates
+ for j, qpair in enumerate(two_qubit_layer):
+ circ._append(gate2q, tuple(circ.qubits[q] for q in qpair), ())
+ cliffs_2q[j] = compose_2q(cliffs_2q[j], gate2q_cliff)
+ # TODO: add dd if necessary
+ for k, q in enumerate(one_qubits):
+ # TODO: add dd if necessary
+ pass
+ circ._append(barrier_inst)
+ # add the last inverse
+ for j, qpair in enumerate(two_qubit_layer):
+ inv = inverse_2q(cliffs_2q[j])
+ circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ())
+ for k, q in enumerate(one_qubits):
+ inv = inverse_1q(cliffs_1q[k])
+ circ._append(_to_gate_1q(inv), (circ.qubits[q],), ())
+ return circ
+
+ def _transpiled_circuits(self) -> List[QuantumCircuit]:
+ """Return a list of experiment circuits, transpiled."""
+ transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()]
+ # Set custom calibrations provided in backend
+ if isinstance(self.backend, BackendV2):
+ instructions = [] # (op_name, qargs) for each element where qargs mean qubit tuple
+ for two_qubit_layer in self.experiment_options.two_qubit_layers:
+ for qpair in two_qubit_layer:
+ instructions.append((self.experiment_options.two_qubit_gate, tuple(qpair)))
+ for q in self.physical_qubits:
+ for gate_1q in self.experiment_options.one_qubit_basis_gates:
+ instructions.append((gate_1q, (q,)))
+
+ common_calibrations = defaultdict(dict)
+ for op_name, qargs in instructions:
+ inst_prop = self.backend.target[op_name].get(qargs, None)
+ if inst_prop is None:
+ continue
+ schedule = inst_prop.calibration
+ if schedule is None:
+ continue
+ publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT)
+ if publisher != CalibrationPublisher.BACKEND_PROVIDER:
+ common_calibrations[op_name][(qargs, tuple())] = schedule
+
+ for circ in transpiled:
+ circ.calibrations = common_calibrations
+
+ return transpiled
+
+ def _metadata(self):
+ metadata = super()._metadata()
+ metadata["two_qubit_layers"] = self.experiment_options.two_qubit_layers
+ return metadata
+
+ @classmethod
+ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "LayerFidelity":
+ """Initialize an experiment from experiment config"""
+ if isinstance(config, dict):
+ config = ExperimentConfig(**dict)
+ ret = cls(*config.args, **config.kwargs)
+ if config.run_options:
+ ret.set_run_options(**config.run_options)
+ return ret
diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py
new file mode 100644
index 0000000000..8508863ad8
--- /dev/null
+++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py
@@ -0,0 +1,353 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2023.
+#
+# 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.
+"""
+Analysis classes for Layer Fidelity RB.
+"""
+from typing import List, Tuple, Union
+
+import logging
+import traceback
+import lmfit
+import numpy as np
+
+import qiskit_experiments.curve_analysis as curve
+import qiskit_experiments.database_service.device_component as device
+from qiskit_experiments.exceptions import AnalysisError
+from qiskit_experiments.framework import CompositeAnalysis, AnalysisResultData, ExperimentData
+from qiskit_experiments.framework.containers import FigureType, ArtifactData
+
+LOG = logging.getLogger(__name__)
+
+
+class _ProcessFidelityAnalysis(curve.CurveAnalysis):
+ r"""A class to estimate process fidelity from one of 1Q/2Q simultaneous direct RB experiments
+
+ # section: overview
+ This analysis takes only a single series.
+ This series is fit by the exponential decay function.
+ From the fit :math:`\alpha` value this analysis estimates the process fidelity:
+ .. math:: F = \frac{1+(d^2-1)\alpha}{d^2}
+
+ # section: fit_model
+ .. math::
+
+ F(x) = a \alpha^x + b
+
+ # section: fit_parameters
+ defpar a:
+ desc: Height of decay curve.
+ init_guess: Determined by :math:`1 - b`.
+ bounds: [0, 1]
+ defpar b:
+ desc: Base line.
+ init_guess: Determined by :math:`(1/2)^n` where :math:`n` is number of qubit.
+ bounds: [0, 1]
+ defpar \alpha:
+ desc: Depolarizing parameter.
+ init_guess: Determined by :func:`~.guess.rb_decay`.
+ bounds: [0, 1]
+
+ # section: reference
+ .. ref_arxiv:: 1 2311.05933
+ """
+
+ def __init__(self, physical_qubits):
+ super().__init__(
+ models=[
+ lmfit.models.ExpressionModel(
+ expr="a * alpha ** x + b",
+ name="rb_decay",
+ )
+ ]
+ )
+ self._physical_qubits = physical_qubits
+ self.set_options(
+ outcome="0" * len(physical_qubits),
+ figure_names="DirectRB_Q" + "_Q".join(map(str, physical_qubits)) + ".svg",
+ )
+ self.plotter.set_figure_options(
+ figure_title=f"Simultaneous Direct RB on Qubit{physical_qubits}",
+ )
+
+ @classmethod
+ def _default_options(cls):
+ """Default analysis options."""
+ default_options = super()._default_options()
+ default_options.plotter.set_figure_options(
+ xlabel="Layers",
+ ylabel="Ground State Population",
+ )
+ default_options.plot_raw_data = True
+ default_options.result_parameters = ["alpha"]
+ default_options.average_method = "sample"
+
+ return default_options
+
+ def _generate_fit_guesses(
+ self,
+ user_opt: curve.FitOptions,
+ curve_data: curve.ScatterTable,
+ ) -> Union[curve.FitOptions, List[curve.FitOptions]]:
+ """Create algorithmic initial fit guess from analysis options and curve data.
+
+ Args:
+ user_opt: Fit options filled with user provided guess and bounds.
+ curve_data: Formatted data collection to fit.
+
+ Returns:
+ List of fit options that are passed to the fitter function.
+ """
+ user_opt.bounds.set_if_empty(
+ a=(0, 1),
+ alpha=(0, 1),
+ b=(0, 1),
+ )
+
+ b_guess = 1 / 2 ** len(self._physical_qubits)
+ alpha_guess = curve.guess.rb_decay(curve_data.x, curve_data.y, b=b_guess)
+ a_guess = (curve_data.y[0] - b_guess) / (alpha_guess ** curve_data.x[0])
+
+ user_opt.p0.set_if_empty(
+ b=b_guess,
+ a=a_guess,
+ alpha=alpha_guess,
+ )
+
+ return user_opt
+
+ def _create_analysis_results(
+ self,
+ fit_data: curve.CurveFitResult,
+ quality: str,
+ **metadata,
+ ) -> List[AnalysisResultData]:
+ """Create analysis results for important fit parameters.
+
+ Args:
+ fit_data: Fit outcome.
+ quality: Quality of fit outcome.
+
+ Returns:
+ List of analysis result data.
+ """
+ outcomes = super()._create_analysis_results(fit_data, quality, **metadata)
+ num_qubits = len(self._physical_qubits)
+ d = 2**num_qubits
+
+ # Calculate process fidelity
+ alpha = fit_data.ufloat_params["alpha"]
+ pf = (1 + (d * d - 1) * alpha) / (d * d)
+
+ quality, reason = self._evaluate_quality_with_reason(fit_data)
+
+ metadata["qubits"] = self._physical_qubits
+ metadata["reason"] = reason
+ metadata.update(fit_data.params)
+ outcomes.append(
+ AnalysisResultData(
+ name="ProcessFidelity",
+ value=pf,
+ chisq=fit_data.reduced_chisq,
+ quality=quality,
+ extra=metadata,
+ )
+ )
+ return outcomes
+
+ def _run_analysis(
+ self, experiment_data: ExperimentData
+ ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]:
+ try:
+ return super()._run_analysis(experiment_data)
+ except Exception: # pylint: disable=broad-except
+ LOG.error(
+ "%s(%s) failed: %s",
+ self.__class__.__name__,
+ str(self._physical_qubits),
+ traceback.format_exc(),
+ )
+ failed_result = AnalysisResultData(
+ name="ProcessFidelity",
+ value=None,
+ quality="bad",
+ extra={"qubits": self._physical_qubits, "reason": "analysis_failure"},
+ )
+ return [failed_result], []
+
+ def _get_experiment_components(self, experiment_data: ExperimentData):
+ """Set physical qubits to the experiment components."""
+ return [device.Qubit(qubit) for qubit in self._physical_qubits]
+
+ def _evaluate_quality_with_reason(
+ self,
+ fit_data: curve.CurveFitResult,
+ ) -> Tuple[str, Union[str, None]]:
+ """Evaluate quality of the fit result and the reason if it is no good.
+
+ Args:
+ fit_data: Fit outcome.
+
+ Returns:
+ Pair of strings that represent quality ("good" or "bad") and its reason if "bad".
+ """
+ # Too large SPAM
+ y_intercept = fit_data.params["a"] + fit_data.params["b"]
+ if y_intercept < 0.7:
+ return "bad", "large_spam"
+ # Convergence to a bad value (probably due to bad readout)
+ ideal_limit = 1 / (2 ** len(self._physical_qubits))
+ if fit_data.params["b"] <= 0 or abs(fit_data.params["b"] - ideal_limit) > 0.3:
+ return "bad", "biased_tail"
+ # Too good fidelity (negative decay)
+ if fit_data.params["alpha"] < 0:
+ return "bad", "negative_decay"
+ # Large residual errors in terms of reduced Chi-square
+ if fit_data.reduced_chisq > 3.0:
+ return "bad", "large_chisq"
+ # Too good Chi-square
+ if fit_data.reduced_chisq == 0:
+ return "bad", "zero_chisq"
+ return "good", None
+
+
+class _SingleLayerFidelityAnalysis(CompositeAnalysis):
+ """A class to estimate a process fidelity per disjoint layer."""
+
+ def __init__(self, layer, analyses=None):
+ if analyses:
+ if len(layer) != len(analyses):
+ raise AnalysisError("'analyses' must have the same length with 'layer'")
+ else:
+ analyses = [_ProcessFidelityAnalysis(qubits) for qubits in layer]
+
+ super().__init__(analyses, flatten_results=True)
+ self._layer = layer
+
+ def _run_analysis(
+ self, experiment_data: ExperimentData
+ ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]:
+ try:
+ # Run composite analysis and extract sub-experiments results
+ analysis_results, figures = super()._run_analysis(experiment_data)
+ # Calculate single layer fidelity from process fidelities of subsystems
+ pf_results = [res for res in analysis_results if res.name == "ProcessFidelity"]
+ pfs = [res.value for res in pf_results]
+ slf = np.prod(pfs)
+ quality_slf = "good" if all(sub.quality == "good" for sub in pf_results) else "bad"
+ slf_result = AnalysisResultData(
+ name="SingleLF",
+ value=slf,
+ quality=quality_slf,
+ extra={"qubits": [q for qubits in self._layer for q in qubits]},
+ )
+ # Return combined results
+ analysis_results = [slf_result] + analysis_results
+ return analysis_results, figures
+ except Exception: # pylint: disable=broad-except
+ LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc())
+ failed_result = AnalysisResultData(
+ name="SingleLF",
+ value=None,
+ quality="bad",
+ extra={
+ "qubits": [q for qubits in self._layer for q in qubits],
+ "reason": "analysis_failure",
+ },
+ )
+ return [failed_result] + analysis_results, figures
+
+ def _get_experiment_components(self, experiment_data: ExperimentData):
+ """Set physical qubits to the experiment components."""
+ return [device.Qubit(q) for qubits in self._layer for q in qubits]
+
+
+class LayerFidelityAnalysis(CompositeAnalysis):
+ r"""A class to analyze layer fidelity experiments.
+
+ # section: overview
+ It estimates Layer Fidelity and EPLG (error per layered gate)
+ by fitting the exponential curve to estimate the decay rate, hence the process fidelity,
+ for each 2-qubit (or 1-qubit) direct randomized benchmarking result.
+ See Ref. [1] for details.
+
+ # section: reference
+ .. ref_arxiv:: 1 2311.05933
+ """
+
+ def __init__(self, layers, analyses=None):
+ if analyses:
+ if len(layers) != len(analyses):
+ raise AnalysisError("'analyses' must have the same length with 'layers'")
+ else:
+ analyses = [_SingleLayerFidelityAnalysis(a_layer) for a_layer in layers]
+
+ super().__init__(analyses, flatten_results=True)
+ self.num_layers = len(layers)
+ self.num_2q_gates = sum(1 if len(qs) == 2 else 0 for lay in layers for qs in lay)
+
+ def _run_analysis(
+ self, experiment_data: ExperimentData
+ ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]:
+ r"""Run analysis for Layer Fidelity experiment.
+
+ It invokes :meth:`CompositeAnalysis._run_analysis` that will recursively invoke
+ ``_run_analysis`` of the sub-experiments (1Q/2Q simultaneous direct RBs for each layer).
+ Based on the results, it computes Layer Fidelity and EPLG (error per layered gate).
+
+ Args:
+ experiment_data: the experiment data to analyze.
+
+ Returns:
+ A pair ``(analysis_results, figures)`` where ``analysis_results``
+ is a list of :class:`AnalysisResultData` objects, and ``figures``
+ is a list of any figures for the experiment.
+ If an analysis fails, an analysis result with ``None`` value will be returned.
+ """
+ try:
+ # Run composite analysis and extract sub-experiments results
+ analysis_results, figures = super()._run_analysis(experiment_data)
+ # Calculate full layer fidelity from single layer fidelities
+ slf_results = [res for res in analysis_results if res.name == "SingleLF"]
+ slfs = [res.value for res in slf_results]
+ lf = np.prod(slfs)
+ quality_lf = "good" if all(sub.quality == "good" for sub in slf_results) else "bad"
+ lf_result = AnalysisResultData(
+ name="LF",
+ value=lf,
+ quality=quality_lf,
+ )
+ eplg = 1 - (lf ** (1 / self.num_2q_gates))
+ eplg_result = AnalysisResultData(
+ name="EPLG",
+ value=eplg,
+ quality=quality_lf,
+ )
+ # Return combined results
+ analysis_results = [lf_result, eplg_result] + analysis_results
+ return analysis_results, figures
+ except Exception: # pylint: disable=broad-except
+ LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc())
+ failed_results = [
+ AnalysisResultData(
+ name="LF",
+ value=None,
+ quality="bad",
+ extra={"reason": "analysis_failure"},
+ ),
+ AnalysisResultData(
+ name="EPLG",
+ value=None,
+ quality="bad",
+ extra={"reason": "analysis_failure"},
+ ),
+ ]
+ return failed_results + analysis_results, figures
diff --git a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml
new file mode 100644
index 0000000000..b72f42a615
--- /dev/null
+++ b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml
@@ -0,0 +1,25 @@
+---
+features:
+ - |
+ Add a new experiment class :class:`.LayerFidelity` to measure
+ `layer fidelity and EPLG (error per layered gate) `_,
+ which is a holistic benchmark to characterize the full quality of the devices at scale.
+
+ It has an experimental feature: its :meth:`circuits`
+ exceptionally returns circuits on physical qubits (not virtual qubits as usual).
+ Its analysis class :class:`.LayerFidelityAnalysis` returns :class:`.AnalysisResultData`
+ which contains several ``extra`` entries to help additional analyses: e.g.
+ ``qubits`` to ease the query of subanalysis results and
+ ``reason`` to tell users why the ``quality`` of the analysis was ``"bad"``.
+
+ For example, the syntax for pulling out the individual fidelities looks like below.
+
+ .. code-block:: python
+
+ df = exp_data.analysis_results(dataframe=True)
+ df[(df.name=="ProcessFidelity") & (df.qubits==(59, 60))].value
+
+ See `an example notebook
+ `_
+ for more examples such as how to select a best possible qubit chain to measure and
+ how to plot EPLG as a function of (sub)chain length.
\ No newline at end of file
diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py
new file mode 100644
index 0000000000..00f7ea28fb
--- /dev/null
+++ b/test/library/randomized_benchmarking/test_layer_fidelity.py
@@ -0,0 +1,219 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2023.
+#
+# 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.
+
+"""Test for layer fidelity experiments."""
+from test.base import QiskitExperimentsTestCase
+from test.library.randomized_benchmarking.mixin import RBTestMixin
+import copy
+import numpy as np
+from ddt import ddt, data, unpack
+
+from qiskit.circuit.library import SXGate
+from qiskit.exceptions import QiskitError
+from qiskit.pulse import Schedule
+from qiskit_ibm_runtime.fake_provider import FakeManilaV2
+from qiskit_experiments.library.randomized_benchmarking import LayerFidelity, LayerFidelityAnalysis
+
+
+@ddt
+class TestLayerFidelity(QiskitExperimentsTestCase, RBTestMixin):
+ """Test for LayerFidelity without running the experiments."""
+
+ # ### Tests for configuration ###
+ def test_experiment_config(self):
+ """Test converting to and from config works"""
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+ loaded_exp = LayerFidelity.from_config(exp.config())
+ self.assertNotEqual(exp, loaded_exp)
+ self.assertEqualExtended(exp, loaded_exp)
+
+ def test_invalid_two_qubit_layers(self):
+ """Test raise error when creating experiment with invalid configs."""
+ valid_kwargs = {
+ "lengths": [10, 20, 30],
+ "two_qubit_gate": "cx",
+ "one_qubit_basis_gates": ["rz", "sx", "x"],
+ }
+ # not disjoit
+ with self.assertRaises(QiskitError):
+ LayerFidelity(
+ physical_qubits=(0, 1, 2, 3), two_qubit_layers=[[(0, 1), (1, 2)]], **valid_kwargs
+ )
+ # no 2q-gate on the qubits (FakeManilaV2 has no cx gate on (0, 3))
+ with self.assertRaises(QiskitError):
+ LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(0, 3)]],
+ backend=FakeManilaV2(),
+ **valid_kwargs,
+ )
+
+ def test_roundtrip_serializable(self):
+ """Test round trip JSON serialization"""
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+ self.assertRoundTripSerializable(exp, strict_type=False)
+
+ def test_circuit_roundtrip_serializable(self):
+ """Test circuits round trip JSON serialization"""
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+ self.assertRoundTripSerializable(exp._transpiled_circuits())
+
+ def test_analysis_config(self):
+ """ "Test converting analysis to and from config works"""
+ analysis = LayerFidelityAnalysis(layers=[[(1, 0), (2, 3)], [(1, 2), (0,), (3,)]])
+ loaded = LayerFidelityAnalysis.from_config(analysis.config())
+ self.assertNotEqual(analysis, loaded)
+ self.assertEqual(analysis.config(), loaded.config())
+
+ # ### Tests for circuit generation ###
+ @data(
+ [(1, 2), [[(1, 2)]]],
+ [(1, 3, 4), [[(3, 4)]]],
+ [(4, 3, 2, 1, 0), [[(0, 1), (3, 2)], [(1, 2), (3, 4)]]],
+ )
+ @unpack
+ def test_generate_circuits(self, qubits, two_qubit_layers):
+ """Test RB circuit generation"""
+ exp = LayerFidelity(
+ physical_qubits=qubits,
+ two_qubit_layers=two_qubit_layers,
+ lengths=[1, 2, 3],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+ circuits = exp.circuits()
+ self.assertAllIdentity(circuits)
+
+ def test_return_same_circuit_for_same_config(self):
+ """Test if setting the same seed returns the same circuits."""
+ exp1 = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+
+ exp2 = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ two_qubit_gate="cx",
+ one_qubit_basis_gates=["rz", "sx", "x"],
+ )
+
+ circs1 = exp1.circuits()
+ circs2 = exp2.circuits()
+
+ self.assertEqual(circs1[0].decompose(), circs2[0].decompose())
+ self.assertEqual(circs1[1].decompose(), circs2[1].decompose())
+ self.assertEqual(circs1[2].decompose(), circs2[2].decompose())
+
+ # ### Tests for transpiled circuit generation ###
+ def test_calibrations_via_custom_backend(self):
+ """Test if calibrations given as custom backend show up in transpiled circuits."""
+ qubits = (2,)
+ my_sched = Schedule(name="custom_sx_gate")
+ my_backend = copy.deepcopy(FakeManilaV2())
+ my_backend.target["sx"][qubits].calibration = my_sched
+
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ backend=my_backend,
+ )
+ transpiled = exp._transpiled_circuits()
+ for qc in transpiled:
+ self.assertTrue(qc.calibrations)
+ self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], [])))
+ self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched)
+
+ def test_backend_with_directed_basis_gates(self):
+ """Test if correct circuits are generated from backend with directed basis gates."""
+ my_backend = copy.deepcopy(FakeManilaV2())
+ del my_backend.target["cx"][(1, 2)] # make cx on {1, 2} one-sided
+
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(2, 1)]],
+ lengths=[10, 20, 30],
+ seed=42,
+ num_samples=1,
+ backend=my_backend,
+ )
+ transpiled = exp._transpiled_circuits()
+ for qc in transpiled[3:]: # check only the second layer
+ 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)
+
+
+class TestRunLayerFidelity(QiskitExperimentsTestCase, RBTestMixin):
+ """Test for running LayerFidelity on noisy simulator."""
+
+ def test_run_layer_fidelity(self):
+ """Test layer fidelity RB. Use default basis gates."""
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[1, 4, 16, 64, 256],
+ seed=42,
+ backend=FakeManilaV2(),
+ )
+ expdata = exp.run()
+ self.assertExperimentDone(expdata)
+
+ lf = expdata.analysis_results("LF").value.n
+ slfs = [res.value.n for res in expdata.analysis_results("SingleLF")]
+ self.assertAlmostEqual(lf, np.prod(slfs))
+
+ def test_expdata_serialization(self):
+ """Test serializing experiment data works."""
+ exp = LayerFidelity(
+ physical_qubits=(0, 1, 2, 3),
+ two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]],
+ lengths=[1, 4, 16, 64, 256],
+ seed=42,
+ backend=FakeManilaV2(),
+ )
+ expdata = exp.run()
+ self.assertExperimentDone(expdata)
+ self.assertRoundTripSerializable(expdata)
+ self.assertRoundTripPickle(expdata)