Skip to content

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

Closed
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
4 changes: 3 additions & 1 deletion qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,3 +156,4 @@
)
from .json import ExperimentEncoder, ExperimentDecoder
from .restless_mixin import RestlessMixin
from .transpile_mixin import SimpleCircuitExtenderMixin
98 changes: 98 additions & 0 deletions qiskit_experiments/framework/transpile_mixin.py
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:
Copy link
Collaborator

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 like layout_and_translate_only = True and if BaseExperiment._transpiled_circuits could just do the code here in that case.

"""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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you at least want to set optimization_level? Better than that, could we create a pass manager with just BasisTranslator here?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the equivalent of this make sense in Qiskit as a SimpleLayout pass? I have not understood why the simplest layout in Qiskit has to be four stages and involve ancilla expansion like it does.

9 changes: 7 additions & 2 deletions qiskit_experiments/library/characterization/t1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions qiskit_experiments/library/characterization/t2ramsey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
21 changes: 21 additions & 0 deletions releasenotes/notes/add-transpiler-mixin-9b30296518e5f4ba.yaml
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]
164 changes: 164 additions & 0 deletions test/framework/test_transpile_mixin.py
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)
6 changes: 3 additions & 3 deletions test/library/characterization/test_t1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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):
Expand Down
Loading