diff --git a/qiskit_experiments/framework/__init__.py b/qiskit_experiments/framework/__init__.py index 2f4304ebf2..ce96bb7052 100644 --- a/qiskit_experiments/framework/__init__.py +++ b/qiskit_experiments/framework/__init__.py @@ -125,8 +125,9 @@ BackendData BackendTiming RestlessMixin + SimpleCircuitExtenderMixin -""" + """ from qiskit.providers.options import Options from qiskit_experiments.framework.backend_data import BackendData from qiskit_experiments.framework.analysis_result import AnalysisResult @@ -155,3 +156,4 @@ ) from .json import ExperimentEncoder, ExperimentDecoder from .restless_mixin import RestlessMixin +from .transpile_mixin import SimpleCircuitExtenderMixin diff --git a/qiskit_experiments/framework/transpile_mixin.py b/qiskit_experiments/framework/transpile_mixin.py new file mode 100644 index 0000000000..26d655ca94 --- /dev/null +++ b/qiskit_experiments/framework/transpile_mixin.py @@ -0,0 +1,98 @@ +# 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. + +"""Transpile mixin class.""" + +from __future__ import annotations +from typing import Protocol + +from qiskit import QuantumCircuit, QuantumRegister, transpile +from qiskit.providers import Backend + + +class TranspileMixInProtocol(Protocol): + """A protocol to define a class that can be mixed with transpiler mixins.""" + + @property + def physical_qubits(self): + """Return the device qubits for the experiment.""" + + @property + def backend(self) -> Backend | None: + """Return the backend for the experiment""" + + def circuits(self) -> list[QuantumCircuit]: + """Return a list of experiment circuits. + + Returns: + A list of :class:`~qiskit.circuit.QuantumCircuit`. + + .. note:: + These circuits should be on qubits ``[0, .., N-1]`` for an + *N*-qubit experiment. The circuits mapped to physical qubits + are obtained via the internal :meth:`_transpiled_circuits` method. + """ + + def _transpiled_circuits(self) -> list[QuantumCircuit]: + ... + + +class SimpleCircuitExtenderMixin: + """A transpiler mixin class that maps virtual qubit index to physical. + + When the backend is not set, the experiment class naively assumes + there are max(physical_qubits) + 1 qubits in the quantum circuits. + """ + + def _transpiled_circuits( + self: TranspileMixInProtocol, + ) -> list: + if hasattr(self.backend, "target"): + # V2 backend model + # This model assumes qubit dependent instruction set, + # but we assume experiment mixed with this class doesn't have such architecture. + basis_gates = set(self.backend.target.operation_names) + n_qubits = self.backend.target.num_qubits + elif hasattr(self.backend, "configuration"): + # V1 backend model + basis_gates = set(self.backend.configuration().basis_gates) + n_qubits = self.backend.configuration().n_qubits + else: + # Backend is not set. Naively guess qubit size. + basis_gates = None + n_qubits = max(self.physical_qubits) + 1 + return [self._index_mapper(c, basis_gates, n_qubits) for c in self.circuits()] + + def _index_mapper( + self: TranspileMixInProtocol, + v_circ: QuantumCircuit, + basis_gates: set[str] | None, + n_qubits: int, + ) -> QuantumCircuit: + if basis_gates is not None and not basis_gates.issuperset( + set(v_circ.count_ops().keys()) - {"barrier"} + ): + # In Qiskit provider model barrier is not included in target. + # Use standard circuit transpile when circuit is not ISA. + return transpile( + v_circ, + backend=self.backend, + initial_layout=list(self.physical_qubits), + ) + p_qregs = QuantumRegister(n_qubits) + v_p_map = {q: p_qregs[self.physical_qubits[i]] for i, q in enumerate(v_circ.qubits)} + p_circ = QuantumCircuit(p_qregs, *v_circ.cregs) + p_circ.metadata = v_circ.metadata + for inst, v_qubits, clbits in v_circ.data: + p_qubits = list(map(v_p_map.get, v_qubits)) + p_circ._append(inst, p_qubits, clbits) + return p_circ diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 6f3c02cfc1..7f3c7b5577 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -19,11 +19,16 @@ from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from qiskit_experiments.framework import ( + SimpleCircuitExtenderMixin, + BackendTiming, + BaseExperiment, + Options, +) from qiskit_experiments.library.characterization.analysis.t1_analysis import T1Analysis -class T1(BaseExperiment): +class T1(SimpleCircuitExtenderMixin, BaseExperiment): r"""An experiment to measure the qubit relaxation time. # section: overview diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index b4b06be794..79a80975e6 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -21,11 +21,16 @@ from qiskit import QuantumCircuit from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from qiskit_experiments.framework import ( + SimpleCircuitExtenderMixin, + BackendTiming, + BaseExperiment, + Options, +) from qiskit_experiments.library.characterization.analysis.t2ramsey_analysis import T2RamseyAnalysis -class T2Ramsey(BaseExperiment): +class T2Ramsey(SimpleCircuitExtenderMixin, BaseExperiment): r"""An experiment to measure the Ramsey frequency and the qubit dephasing time sensitive to inhomogeneous broadening. diff --git a/releasenotes/notes/add-transpiler-mixin-9b30296518e5f4ba.yaml b/releasenotes/notes/add-transpiler-mixin-9b30296518e5f4ba.yaml new file mode 100644 index 0000000000..f956c76d6a --- /dev/null +++ b/releasenotes/notes/add-transpiler-mixin-9b30296518e5f4ba.yaml @@ -0,0 +1,21 @@ +--- +developer: + - | + Add a mixin class :class:`~.SimpleCircuitExtenderMixin` that automatically + implements the :meth:`.BaseExperiment._transpiled_circuits` method for + simple experiments that require neither gate translation nor routing, + i.e. experiment that directly creates ISA circuits. + This bypasses the call to the Qiskit transpiler, which makes your experiment run more performant. + For example: + + .. code-block::python + + from qiskit_experiment.framework import BaseExperiment, SimpleCircuitExtenderMixin + + class MyExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + + def circuits(self): + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + return [qc] diff --git a/test/framework/test_transpile_mixin.py b/test/framework/test_transpile_mixin.py new file mode 100644 index 0000000000..17ec198101 --- /dev/null +++ b/test/framework/test_transpile_mixin.py @@ -0,0 +1,164 @@ +# 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. + +"""Tests for transpile mixin.""" + +from test.base import QiskitExperimentsTestCase +from test.fake_experiment import FakeAnalysis + +from qiskit import QuantumCircuit +from qiskit.providers.fake_provider import GenericBackendV2 + +from qiskit_experiments.framework import SimpleCircuitExtenderMixin, BaseExperiment +from qiskit_experiments.framework.composite import ParallelExperiment + + +class TestSimpleCircuitExtender(QiskitExperimentsTestCase): + """A test for SimpleCircuitExtender MixIn.""" + + def test_transpiled_single_qubit_circuits(self): + """Test fast-transpile with single qubit circuit.""" + + class _MockExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + def circuits(self) -> list: + qc1 = QuantumCircuit(1, 1) + qc1.x(0) + qc1.measure(0, 0) + qc1.metadata = {"test_val": "123"} + + qc2 = QuantumCircuit(1, 1) + qc2.sx(0) + qc2.measure(0, 0) + qc2.metadata = {"test_val": "456"} + return [qc1, qc2] + + num_qubits = 10 + + mock_backend = GenericBackendV2(num_qubits, basis_gates=["x", "sx", "measure"]) + exp = _MockExperiment((3,), backend=mock_backend) + test_circs = exp._transpiled_circuits() + + self.assertEqual(len(test_circs), 2) + c0, c1 = test_circs + + # output size + self.assertEqual(len(c0.qubits), num_qubits) + self.assertEqual(len(c1.qubits), num_qubits) + + # metadata + self.assertDictEqual(c0.metadata, {"test_val": "123"}) + + # qubit index of X gate + self.assertEqual(c0.qubits.index(c0.data[0][1][0]), 3) + + # creg index of measure + self.assertEqual(c0.clbits.index(c0.data[1][2][0]), 0) + + # metadata + self.assertDictEqual(c1.metadata, {"test_val": "456"}) + + # qubit index of SX gate + self.assertEqual(c1.qubits.index(c1.data[0][1][0]), 3) + + # creg index of measure + self.assertEqual(c1.clbits.index(c1.data[1][2][0]), 0) + + def test_transpiled_two_qubit_circuits(self): + """Test fast-transpile with two qubit circuit.""" + + class _MockExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + def circuits(self) -> list: + qc = QuantumCircuit(2, 2) + qc.cx(0, 1) + qc.measure(0, 0) + qc.measure(1, 1) + return [qc] + + num_qubits = 10 + + mock_backend = GenericBackendV2(num_qubits, basis_gates=["cx", "measure"]) + exp = _MockExperiment((9, 2), backend=mock_backend) + test_circ = exp._transpiled_circuits()[0] + + self.assertEqual(len(test_circ.qubits), num_qubits) + + # qubit index of CX control qubit + self.assertEqual(test_circ.qubits.index(test_circ.data[0][1][0]), 9) + + # qubit index of CX target qubit + self.assertEqual(test_circ.qubits.index(test_circ.data[0][1][1]), 2) + + # creg index of measure + self.assertEqual(test_circ.clbits.index(test_circ.data[1][2][0]), 0) + self.assertEqual(test_circ.clbits.index(test_circ.data[2][2][0]), 1) + + def test_empty_backend(self): + """Test fast-transpile without backend.""" + + class _MockExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + def circuits(self) -> list: + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + + return [qc] + + exp = _MockExperiment((10,)) + test_circ = exp._transpiled_circuits()[0] + + self.assertEqual(len(test_circ.qubits), 11) + + # qubit index of X gate + self.assertEqual(test_circ.qubits.index(test_circ.data[0][1][0]), 10) + + def test_empty_backend_with_parallel(self): + """Test fast-transpile without backend. Circuit qubit location must not overlap.""" + + class _MockExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + def __init__(self, physical_qubits): + super().__init__(physical_qubits, FakeAnalysis()) + + def circuits(self) -> list: + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + + return [qc] + + exp1 = _MockExperiment((3,)) + exp2 = _MockExperiment((15,)) + pexp = ParallelExperiment([exp1, exp2], flatten_results=True) + test_circ = pexp._transpiled_circuits()[0] + + self.assertEqual(len(test_circ.qubits), 16) + + # qubit index of X gate + self.assertEqual(test_circ.qubits.index(test_circ.data[0][1][0]), 3) + self.assertEqual(test_circ.qubits.index(test_circ.data[2][1][0]), 15) + + def test_circuit_non_isa(self): + """Test fast-transpile with non-ISA circuit. It should use standard transpile.""" + + class _MockExperiment(SimpleCircuitExtenderMixin, BaseExperiment): + def circuits(self) -> list: + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + + return [qc] + + mock_backend = GenericBackendV2(1, basis_gates=["sx", "rz", "measure"]) + exp = _MockExperiment((0,), backend=mock_backend) + test_circ = exp._transpiled_circuits()[0] + + # gate is translated into sx-sx-measure + self.assertEqual(len(test_circ.data), 3) diff --git a/test/library/characterization/test_t1.py b/test/library/characterization/test_t1.py index 7f2d46f6ec..5c8d9db1e4 100644 --- a/test/library/characterization/test_t1.py +++ b/test/library/characterization/test_t1.py @@ -252,7 +252,7 @@ def test_t1_parallel_exp_transpile(self): instruction_durations = [] for i in range(num_qubits): instruction_durations += [ - ("rx", [i], (i + 1) * 10, "ns"), + ("x", [i], (i + 1) * 10, "ns"), ("measure", [i], (i + 1) * 1000, "ns"), ] coupling_map = [[i - 1, i] for i in range(1, num_qubits)] @@ -272,14 +272,14 @@ def test_t1_parallel_exp_transpile(self): for circ in circs: self.assertEqual(circ.num_qubits, 2) op_counts = circ.count_ops() - self.assertEqual(op_counts.get("rx"), 2) + self.assertEqual(op_counts.get("x"), 2) self.assertEqual(op_counts.get("delay"), 2) tcircs = parexp._transpiled_circuits() for circ in tcircs: self.assertEqual(circ.num_qubits, num_qubits) op_counts = circ.count_ops() - self.assertEqual(op_counts.get("rx"), 2) + self.assertEqual(op_counts.get("x"), 2) self.assertEqual(op_counts.get("delay"), 2) def test_experiment_config(self):