Skip to content

Commit

Permalink
Undo breaking API change in unitary synthesis plugin interface (#10066)…
Browse files Browse the repository at this point in the history
… (#10075)

* Undo breaking API change in unitary synthesis plugin interface

This commit reverts an unecessary breaking API change that was
introduced in #9175. In that PR the data type for two arguments,
gate_lengths and gate_errors, that was potentially passed to
plugins was changed. This change was well intentioned as it was
used by the default plugin as part of the changes to the default
plugin made in that PR. But this neglected the downstream effects
for any existing plugin interface users relying on the input to
those fields. Making this change to existing fields would break
any plugins that were using the old data format and is a violation
of the Qiskit stability and deprecation policy. This commit reverts
that change so that gate_lengths and gate_errors contain the same data
format as in previous releases. Instead to facilitate the new
workflow a new fields, gate_lengths_by_qubit and gate_errors_by_qubit,
were added that leverage the new format introduced by #9175. By adding
this as new optional fields existing users can continue to work as
before, but the default plugin which requires the different data
format can use the new data type.

* Update docstring

* Fix copy paste error

(cherry picked from commit 72e7481)

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
mergify[bot] and mtreinish committed May 3, 2023
1 parent 16bcd4c commit 35f7b67
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 20 deletions.
72 changes: 66 additions & 6 deletions qiskit/transpiler/passes/synthesis/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ def supports_gate_lengths(self):
def supports_gate_errors(self):
return False
@property
def supports_gate_lengths_by_qubit(self):
return False
@property
def supports_gate_errors_by_qubit(self):
return False
@property
def min_qubits(self):
return None
Expand Down Expand Up @@ -340,17 +348,69 @@ def supports_pulse_optimize(self):
"""
pass

@property
def supports_gate_lengths_by_qubit(self):
"""Return whether the plugin supports taking ``gate_lengths_by_qubit``
This differs from ``supports_gate_lengths``/``gate_lengths`` by using a different
view of the same data. Instead of being keyed by gate name this is keyed by qubit
and uses :class:`~.Gate` instances to represent gates (instead of gate names)
``gate_lengths_by_qubit`` will be a dictionary in the form of
``{(qubits,): [Gate, length]}``. For example::
{
(0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0],
(0, 1): [CXGate(): 0.012012477900732316]
}
where the ``length`` value is in units of seconds.
Do note that this dictionary might not be complete or could be empty
as it depends on the target backend reporting gate lengths on every
gate for each qubit.
This defaults to False
"""
return False

@property
def supports_gate_errors_by_qubit(self):
"""Return whether the plugin supports taking ``gate_errors_by_qubit``
This differs from ``supports_gate_errors``/``gate_errors`` by using a different
view of the same data. Instead of being keyed by gate name this is keyed by qubit
and uses :class:`~.Gate` instances to represent gates (instead of gate names).
``gate_errors_by_qubit`` will be a dictionary in the form of
``{(qubits,): [Gate, error]}``. For example::
{
(0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0],
(0, 1): [CXGate(): 0.012012477900732316]
}
Do note that this dictionary might not be complete or could be empty
as it depends on the target backend reporting gate errors on every
gate for each qubit. The gate error rates reported in ``gate_errors``
are provided by the target device ``Backend`` object and the exact
meaning might be different depending on the backend.
This defaults to False
"""
return False

@property
@abc.abstractmethod
def supports_gate_lengths(self):
"""Return whether the plugin supports taking ``gate_lengths``
``gate_lengths`` will be a dictionary in the form of
``{(qubits,): [Gate, length]}``. For example::
``{gate_name: {(qubit_1, qubit_2): length}}``. For example::
{
(0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0],
(0, 1): [CXGate(): 0.012012477900732316]
'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126},
'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07}
}
where the ``length`` value is in units of seconds.
Expand All @@ -367,11 +427,11 @@ def supports_gate_errors(self):
"""Return whether the plugin supports taking ``gate_errors``
``gate_errors`` will be a dictionary in the form of
``{(qubits,): [Gate, error]}``. For example::
``{gate_name: {(qubit_1, qubit_2): error}}``. For example::
{
(0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0],
(0, 1): [CXGate(): 0.012012477900732316]
'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126},
'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07}
}
Do note that this dictionary might not be complete or could be empty
Expand Down
79 changes: 75 additions & 4 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
plugin_method = DefaultUnitarySynthesis()
plugin_kwargs = {"config": self._plugin_config}
_gate_lengths = _gate_errors = None
_gate_lengths_by_qubit = _gate_errors_by_qubit = None

if self.method == "default":
# If the method is the default, we only need to evaluate one set of keyword arguments.
Expand Down Expand Up @@ -417,6 +418,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
if method.supports_gate_errors:
_gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target)
kwargs["gate_errors"] = _gate_errors
if method.supports_gate_lengths_by_qubit:
_gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit(
self._backend_props, self._target
)
kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit
if method.supports_gate_errors_by_qubit:
_gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit(
self._backend_props, self._target
)
kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit
supported_bases = method.supported_bases
if supported_bases is not None:
kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases)
Expand Down Expand Up @@ -479,6 +490,58 @@ def _recurse(dag):


def _build_gate_lengths(props=None, target=None):
"""Builds a ``gate_lengths`` dictionary from either ``props`` (BackendV1)
or ``target`` (BackendV2).
The dictionary has the form:
{gate_name: {(qubits,): duration}}
"""
gate_lengths = {}
if target is not None:
for gate, prop_dict in target.items():
gate_lengths[gate] = {}
for qubit, gate_props in prop_dict.items():
if gate_props is not None and gate_props.duration is not None:
gate_lengths[gate][qubit] = gate_props.duration
elif props is not None:
for gate in props._gates:
gate_lengths[gate] = {}
for k, v in props._gates[gate].items():
length = v.get("gate_length")
if length:
gate_lengths[gate][k] = length[0]
if not gate_lengths[gate]:
del gate_lengths[gate]
return gate_lengths


def _build_gate_errors(props=None, target=None):
"""Builds a ``gate_error`` dictionary from either ``props`` (BackendV1)
or ``target`` (BackendV2).
The dictionary has the form:
{gate_name: {(qubits,): error_rate}}
"""
gate_errors = {}
if target is not None:
for gate, prop_dict in target.items():
gate_errors[gate] = {}
for qubit, gate_props in prop_dict.items():
if gate_props is not None and gate_props.error is not None:
gate_errors[gate][qubit] = gate_props.error
if props is not None:
for gate in props._gates:
gate_errors[gate] = {}
for k, v in props._gates[gate].items():
error = v.get("gate_error")
if error:
gate_errors[gate][k] = error[0]
if not gate_errors[gate]:
del gate_errors[gate]
return gate_errors


def _build_gate_lengths_by_qubit(props=None, target=None):
"""
Builds a `gate_lengths` dictionary from either `props` (BackendV1)
or `target (BackendV2)`.
Expand Down Expand Up @@ -511,7 +574,7 @@ def _build_gate_lengths(props=None, target=None):
return gate_lengths


def _build_gate_errors(props=None, target=None):
def _build_gate_errors_by_qubit(props=None, target=None):
"""
Builds a `gate_error` dictionary from either `props` (BackendV1)
or `target (BackendV2)`.
Expand Down Expand Up @@ -565,10 +628,18 @@ def supports_pulse_optimize(self):

@property
def supports_gate_lengths(self):
return True
return False

@property
def supports_gate_errors(self):
return False

@property
def supports_gate_lengths_by_qubit(self):
return True

@property
def supports_gate_errors_by_qubit(self):
return True

@property
Expand Down Expand Up @@ -734,8 +805,8 @@ def run(self, unitary, **options):
coupling_map = options["coupling_map"][0]
natural_direction = options["natural_direction"]
pulse_optimize = options["pulse_optimize"]
gate_lengths = options["gate_lengths"]
gate_errors = options["gate_errors"]
gate_lengths = options["gate_lengths_by_qubit"]
gate_errors = options["gate_errors_by_qubit"]
qubits = options["coupling_map"][1]
target = options["target"]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ features:
(e.g. RZ-RX-RZ or RZ-RY-RZ), generic unitary basis (e.g. U), and a
few others.
For a two-qubit decomposition, it can target any supercontrolled basis
(e.g. CNOT, iSWAP, B) or multiple controlled basis
(e.g. CNOT, iSWAP, B) or multiple controlled basis
(e.g. CZ, CH, ZZ^.5, ZX^.2, etc.).
upgrade:
- |
The interface for :class:`~.UnitarySynthesisPlugin` has been changed so
now `gate_lengths` and `gate_errors` have the form
``{(qubits,): [Gate, length]}``. This allows gates that have the same
name but different parameters to be represented separately.
The interface for :class:`~.UnitarySynthesisPlugin` has two new
optional properties ``supports_gate_lengths_by_qubit`` and
``supports_gate_errors_by_qubit`` which when set will add the
fields ``gate_lengths_by_qubit`` and ``gate_errors_by_qubit``
respectively to the input options to the plugin's ``run()`` method.
These new fields are an alternative view of the data provided by
``gate_lengths`` and ``gate_errors`` but instead have the form:
``{(qubits,): [Gate, length]}`` (where ``Gate`` is the instance
of :class:`~.Gate` for that definition). This allows plugins to
reason about working with gates of the same type but but that
have different parameters set.
8 changes: 4 additions & 4 deletions test/python/transpiler/test_unitary_synthesis_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ def test_all_keywords_passed_to_default_on_fallback(self):
expected_kwargs = [
"basis_gates",
"coupling_map",
"gate_errors",
"gate_lengths",
"gate_errors_by_qubit",
"gate_lengths_by_qubit",
"natural_direction",
"pulse_optimize",
]
Expand Down Expand Up @@ -294,8 +294,8 @@ def test_config_not_passed_to_default_on_fallback(self):
expected_kwargs = [
"basis_gates",
"coupling_map",
"gate_errors",
"gate_lengths",
"gate_errors_by_qubit",
"gate_lengths_by_qubit",
"natural_direction",
"pulse_optimize",
]
Expand Down

0 comments on commit 35f7b67

Please sign in to comment.