-
Notifications
You must be signed in to change notification settings - Fork 127
Fast transpile with MixIn #1455
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
Changes from all commits
cba0538
f283af9
b0bba26
b24ff22
34fcfed
bb597da
c3367da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't you at least want to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we quickly check for custom pulse gates as well? I think that is the one other pass that users sometimes want for QE (though in the future that will probably go away). |
||
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 | ||
Comment on lines
+91
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would the equivalent of this make sense in Qiskit as a |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we want to make this a feature of
BaseExperiment
rather than a mixin. I am not sure what the variable should be called but experiments could set something likelayout_and_translate_only = True
and ifBaseExperiment._transpiled_circuits
could just do the code here in that case.