Skip to content

Commit

Permalink
Add BackendV2 abstract class (Qiskit#5885)
Browse files Browse the repository at this point in the history
* Add BackendV2 abstract class

This commit adds a new abstract class that adds a new version of the
backend abstract class. The primary changes being made in this class are
that instead of using BackendConfiguration for immutable characteristics
of the backend those are instead exposed as read only attributes of the
Backend object itself. This also takes some time to improve how we
represent some core data about a backend. The primary example of this is
that basis gates are no longer a list of names but instead a list of
Gate objects.

* Adjust GateMap to be a dict instead of a graph

This commit reworks the GateMap class to be an internal dict of gate
instances to qargs (with optional properties) instead of building a
graph. The graph representation was inherently limited in that it
couldn't describe multiqubit gates involving > 2 qubits.

* Add fields from properties to v2 backend interface

This commit finishes by adding the defined optional methods for querying
the equivalent of the backend properties on the new backend v2
interface. The per gate properties were already defined in the gate map
class as optional fields for each gate entry so this defines methods for
the per qubit fields (t2, t1, freq, readout error and duration). While
not every backend supports these fields they're common ones that most
could implement (assuming the data is available). For any other backend
properties they can be custom fieldss in a subclass.

* Adjust GateMap structure

* Rename GateMap -> Target and drop readout error (as it'll be per instruction error)

* Add instruction durations method to target

* Add optional dt properties to backend object

* Add instruction durations method to backendv2

* Fix lint

* Rename max_experiments -> max_circuits

* Add two_q_gate option to Target.coupling_map()

* Add option for a list of qubits to t1, t2, and frequency

* Change list return to numpy array for t1 and t2

* Drop qubit frequency

* Add pulse to instruction durations

* Update units for dt and dtm

* Update qiskit/transpiler/target.py

Co-authored-by: Naoki Kanazawa <[email protected]>

* Fix type hints for t1 and t2

* Fix transpile() to actually use BackendV2 correctly

* Add transpile() test case with BackendV2

* Add repr for InstructionProperties

* Tweak docs

* Replace get_qargs_from_name with get_instructions_for_qarg

* Add tests for target

* Fix black's insane formatting choice

* Add more target tests

* Cleanup debug prints

* Make Target a mapping

* Rename get_gate_from_name to get_instruction_from_name

* Fix test issue with rename of get_gate_from_name

* Add support for running schedule() with a backend v2

* Add method to update instruction properties in target

* Fix caching

* Add tests for new target methods

* Fix lint

* Add support for qargs being None in ideal simulator case

This commit adds support for a backend setting the qargs on an
instruction to None as a shorthand for everything. This is useful for
things like ideal simulator backends that don't have any constraints on
which qubits an instruction can run on and just have a set of supported
instructions and a number of qubits supported by the simulator.

* Add test for ideal sim target instruction_schedule_map() method

* Add release note

* Add BackendV2 to autodoc

* Drop distance() and distance() matrix from Target

* Fix leftovers in doc strings

* Add __str__ for target

* Add logging when instruction_names includes instructions not on all qubits

* Documentation fixes from code review

Co-authored-by: Naoki Kanazawa <[email protected]>

* Use seconds everywhere

* Fixes from recent changes

* Add update_from_instruction_schedule_map() method

* Use defaultdict for _qarg_gate_map in target

* Explicitly add ScheduleBlock to backend.run()

* Tweak docs for target

* Add pulse channel abstract methods to BackendV2 class

* Fix lint

* Improve target's update_from_instruction_schedule_map

* Add timing constraints to the target

* Fix typos in release note

Co-authored-by: Ali Javadi-Abhari <[email protected]>

* Re-export FakeBackendV2 in qiskit.test.mock

* Update target docstring example

* Make properties optional on Target.add_instruction()

For the case of ideal simulators for each instruction they would have to
call something like add_instruction(XGate(), {None: None} which is
tedious for every instruction. This commit makes the properties argument
optional and if not specified default to {None: None} this enables ideal
simulators to just call add_instruction(XGate()) for each instruction
the simulator supports.

* Add docstring type to BackendV2.target attribute

* Make Target.instructions property return tuple and add operations

* Rename instruction property schedule -> calibration

* Add optional backend metadata as constructor kwargs

* More instruction->Operation renames

* Remove conditional and max_shots abstract properties

The conditional property isn't needed and should be included in the
target. The max_shots property should be expressed in the Option as
bounds or validation on setting.

* Add optional validators to Options object

This commit adds optional validators for the Options object. This lets a
user constructing an Options object (typically a Backend/provider
author) optionally set bounds for numeric values, types, or valid choices
for any field in the Options object. This will be validated when the
value is updated and shown in a human readable format in the __str__
output for the Options object.

* Include dt in output InstructionDurations from Target

Co-authored-by: itoko <[email protected]>

* Add QubitProperties class and qubits() backend method

This commit adds a new optional method to BackendV2 which when defined
by a BackendV2 subclass returns a QubitProperties class for the
specified Qubits. QubitProperties is a new data container class for the
properties of a qubit such as t1, t2 and frequency. This replaces the
previous optional methods t1 and t2 as the storage location for these
properties. Those methods are updated to work using qubits() directly.
The tests are also updated to validate these methods work as expected.

* Add instruction_properties() method

* Fix lint in mock backend

* Make provider backref optional and fix docstring

* Rename BackendV2.qubits() BackendV2.qubit_properties()

* Add note to QubitProperties docstring on subclassing

* Fix Options.set_validator() docstring

* Add missing **fields docstring to BackendV2 init

This is just copied from BackendV1 which has the same argument on it's
constructor.

* Remove t1/t2 methods from BackendV2 class

* Remove unused imports

* Remove properties field from Properties classes

* Apply suggestions from code review

Co-authored-by: Ali Javadi-Abhari <[email protected]>

* Rename InstructionProperties.length -> duration

* Docstring improvements and more instruction->operation

* Update releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml

Co-authored-by: Kevin Krsulich <[email protected]>

* Rename and fix incomplete basis computation

* Tweak release note wording on custom attributes of backend

Co-authored-by: Kevin Krsulich <[email protected]>

* Finish set_validator docstring on options class

* Update qiskit/providers/backend.py

Co-authored-by: Kevin Krsulich <[email protected]>

* Apply suggestions from code review

This commit makes several changes found in code review.
It fixes a typo with __slots__ for QubitProperties, fixes string formatting
for a raised error, updates a list comprehension to a more performant syntax,
and fixes release note formatting.

Co-authored-by: Jake Lishman <[email protected]>

* Return dict .keys() directly for Target properties

This commit updates the return of a couple property methods on the
Target class to return dict.keys() instead of set(dict) as the keys
return type implements the abstract Set collections interface and
returns just a read only view of the dict's keys instead of a mutable
copy. For this api the lower overhead and read-only nature are better
choices than returning a set copy.

Co-authored-by: Jake Lishman <[email protected]>

* Fix stray use of properties attr in QubitProperties

* Update stray set() return for property

* Apply suggestions from code review

Co-authored-by: Kevin Krsulich <[email protected]>

* Rename Target.get_qargs() -> qargs_for_operation_name()

* Fix lint

* Rename Target.coupling_map() -> build_coupling_map()

* Fix test name for discovery

* qarg -> qargs in target method arguments

* Add target to docs build

* Fix target docs

* Fix lint

* Tweak documentation for target to make intent and future clear

* Fix typo in QubitProperties docstring

Co-authored-by: Rathish Cholarajan <[email protected]>

Co-authored-by: Naoki Kanazawa <[email protected]>
Co-authored-by: Ali Javadi-Abhari <[email protected]>
Co-authored-by: itoko <[email protected]>
Co-authored-by: Kevin Krsulich <[email protected]>
Co-authored-by: Jake Lishman <[email protected]>
Co-authored-by: Rathish Cholarajan <[email protected]>
  • Loading branch information
7 people authored Dec 1, 2021
1 parent b807eb4 commit 3694fff
Show file tree
Hide file tree
Showing 13 changed files with 2,685 additions and 54 deletions.
46 changes: 28 additions & 18 deletions qiskit/compiler/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,34 @@ def schedule(
QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed
"""
start_time = time()
if inst_map is None:
if backend is None:
raise QiskitError(
"Must supply either a backend or InstructionScheduleMap for scheduling passes."
)
defaults = backend.defaults()
if defaults is None:
raise QiskitError(
"The backend defaults are unavailable. The backend may not support pulse."
)
inst_map = defaults.instruction_schedule_map
if meas_map is None:
if backend is None:
raise QiskitError("Must supply either a backend or a meas_map for scheduling passes.")
meas_map = backend.configuration().meas_map
if dt is None:
if backend is not None:
dt = backend.configuration().dt
if backend and getattr(backend, "version", 0) > 1:
if inst_map is None:
inst_map = backend.instruction_schedule_map
if meas_map is None:
meas_map = backend.meas_map
if dt is None:
dt = backend.dt
else:
if inst_map is None:
if backend is None:
raise QiskitError(
"Must supply either a backend or InstructionScheduleMap for scheduling passes."
)
defaults = backend.defaults()
if defaults is None:
raise QiskitError(
"The backend defaults are unavailable. The backend may not support pulse."
)
inst_map = defaults.instruction_schedule_map
if meas_map is None:
if backend is None:
raise QiskitError(
"Must supply either a backend or a meas_map for scheduling passes."
)
meas_map = backend.configuration().meas_map
if dt is None:
if backend is not None:
dt = backend.configuration().dt

schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt)
circuits = circuits if isinstance(circuits, list) else [circuits]
Expand Down
107 changes: 74 additions & 33 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,15 @@ def _check_circuits_coupling_map(circuits, transpile_args, backend):
max_qubits = parsed_coupling_map.size()

# If coupling_map is None, the limit might be in the backend (like in 1Q devices)
elif backend is not None and not backend.configuration().simulator:
max_qubits = backend.configuration().n_qubits
elif backend is not None:
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
if not backend.configuration().simulator:
max_qubits = backend.configuration().n_qubits
else:
max_qubits = backend.num_qubits

if max_qubits is not None and (num_qubits > max_qubits):
raise TranspilerError(
Expand Down Expand Up @@ -608,6 +615,11 @@ def _create_faulty_qubits_map(backend):
from working qubit in the backend to dummy qubits that are consecutive and connected."""
faulty_qubits_map = None
if backend is not None:
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version > 1:
return None
if backend.properties():
faulty_qubits = backend.properties().faulty_qubits()
faulty_edges = [gates.qubits for gates in backend.properties().faulty_gates()]
Expand Down Expand Up @@ -639,8 +651,14 @@ def _create_faulty_qubits_map(backend):
def _parse_basis_gates(basis_gates, backend, circuits):
# try getting basis_gates from user, else backend
if basis_gates is None:
if getattr(backend, "configuration", None):
basis_gates = getattr(backend.configuration(), "basis_gates", None)
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
if getattr(backend, "configuration", None):
basis_gates = getattr(backend.configuration(), "basis_gates", None)
else:
basis_gates = backend.operation_names
# basis_gates could be None, or a list of basis, e.g. ['u3', 'cx']
if basis_gates is None or (
isinstance(basis_gates, list) and all(isinstance(i, str) for i in basis_gates)
Expand All @@ -666,28 +684,34 @@ def _parse_inst_map(inst_map, backend, num_circuits):
def _parse_coupling_map(coupling_map, backend, num_circuits):
# try getting coupling_map from user, else backend
if coupling_map is None:
if getattr(backend, "configuration", None):
configuration = backend.configuration()
if hasattr(configuration, "coupling_map") and configuration.coupling_map:
faulty_map = _create_faulty_qubits_map(backend)
if faulty_map:
faulty_edges = [gate.qubits for gate in backend.properties().faulty_gates()]
functional_gates = [
edge for edge in configuration.coupling_map if edge not in faulty_edges
]
coupling_map = CouplingMap()
for qubit1, qubit2 in functional_gates:
if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None:
coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2])
if configuration.n_qubits != coupling_map.size():
warnings.warn(
"The backend has currently some qubits/edges out of service."
" This temporarily reduces the backend size from "
f"{configuration.n_qubits} to {coupling_map.size()}",
UserWarning,
)
else:
coupling_map = CouplingMap(configuration.coupling_map)
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
if getattr(backend, "configuration", None):
configuration = backend.configuration()
if hasattr(configuration, "coupling_map") and configuration.coupling_map:
faulty_map = _create_faulty_qubits_map(backend)
if faulty_map:
faulty_edges = [gate.qubits for gate in backend.properties().faulty_gates()]
functional_gates = [
edge for edge in configuration.coupling_map if edge not in faulty_edges
]
coupling_map = CouplingMap()
for qubit1, qubit2 in functional_gates:
if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None:
coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2])
if configuration.n_qubits != coupling_map.size():
warnings.warn(
"The backend has currently some qubits/edges out of service."
" This temporarily reduces the backend size from "
f"{configuration.n_qubits} to {coupling_map.size()}",
UserWarning,
)
else:
coupling_map = CouplingMap(configuration.coupling_map)
else:
coupling_map = backend.coupling_map

# coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]]
if coupling_map is None or isinstance(coupling_map, CouplingMap):
Expand Down Expand Up @@ -743,10 +767,22 @@ def _parse_backend_num_qubits(backend, num_circuits):
if backend is None:
return [None] * num_circuits
if not isinstance(backend, list):
return [backend.configuration().n_qubits] * num_circuits
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
return [backend.configuration().n_qubits] * num_circuits
else:
return [backend.num_qubits] * num_circuits
backend_num_qubits = []
for a_backend in backend:
backend_num_qubits.append(a_backend.configuration().n_qubits)
backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
backend_num_qubits.append(a_backend.configuration().n_qubits)
else:
backend_num_qubits.append(a_backend.num_qubits)
return backend_num_qubits


Expand Down Expand Up @@ -938,11 +974,16 @@ def _parse_timing_constraints(backend, timing_constraints, num_circuits):
if backend is None and timing_constraints is None:
timing_constraints = TimingConstraints()
else:
if timing_constraints is None:
# get constraints from backend
timing_constraints = getattr(backend.configuration(), "timing_constraints", {})
timing_constraints = TimingConstraints(**timing_constraints)

backend_version = getattr(backend, "version", 0)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
if timing_constraints is None:
# get constraints from backend
timing_constraints = getattr(backend.configuration(), "timing_constraints", {})
timing_constraints = TimingConstraints(**timing_constraints)
else:
timing_constraints = backend.target.timing_constraints()
return [timing_constraints] * num_circuits


Expand Down
4 changes: 4 additions & 0 deletions qiskit/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
Backend
BackendV1
BackendV2
QubitProperties
Options
-------
Expand Down Expand Up @@ -547,6 +549,8 @@ def status(self):
from qiskit.providers.provider import ProviderV1
from qiskit.providers.backend import Backend
from qiskit.providers.backend import BackendV1
from qiskit.providers.backend import BackendV2
from qiskit.providers.backend import QubitProperties
from qiskit.providers.options import Options
from qiskit.providers.job import Job
from qiskit.providers.job import JobV1
Expand Down
Loading

0 comments on commit 3694fff

Please sign in to comment.