diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index b472d463c808..a206af3cf0ab 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -16,7 +16,6 @@ import dill -from qiskit.visualization import pass_manager_drawer from qiskit.tools.parallel import parallel_map from qiskit.circuit import QuantumCircuit from .basepasses import BasePass @@ -301,6 +300,8 @@ def draw(self, filename=None, style=None, raw=False): Raises: ImportError: when nxpd or pydot not installed. """ + from qiskit.visualization import pass_manager_drawer + return pass_manager_drawer(self, filename=filename, style=style, raw=raw) def passes(self) -> List[Dict[str, BasePass]]: diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index 5e6aea5c3ba8..52c4ff39ab29 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -14,13 +14,25 @@ import math from typing import List + import numpy as np +import retworkx as rx + from qiskit.exceptions import QiskitError from qiskit.utils import optionals as _optionals from .exceptions import VisualizationError from .utils import matplotlib_close_if_inline +def _get_backend_interface_version(backend): + backend_interface_version = getattr(backend, "version", None) + # Handle deprecated BaseBackend based backends which have a version() + # method + if not isinstance(backend_interface_version, int): + backend_interface_version = 0 + return backend_interface_version + + @_optionals.HAS_MATPLOTLIB.require_in_call def plot_gate_map( backend, @@ -36,6 +48,7 @@ def plot_gate_map( font_color="w", ax=None, filename=None, + qubit_coordinates=None, ): """Plots the gate map of a device. @@ -54,6 +67,11 @@ def plot_gate_map( font_color (str): The font color for the qubit labels. ax (Axes): A Matplotlib axes instance. filename (str): file path to save image to. + qubit_coordinates (Sequence): An optional sequence input (list or array being the + most common) of 2d coordinates for each qubit. The length of the + sequence much match the number of qubits on the backend. The sequence + should be the planar coordinates in a 0-based square grid where each + qubit is located. Returns: Figure: A Matplotlib figure instance. @@ -82,9 +100,6 @@ def plot_gate_map( backend = accountProvider.get_backend('ibmq_vigo') plot_gate_map(backend) """ - if backend.configuration().simulator: - raise QiskitError("Requires a device backend, not simulator.") - qubit_coordinates_map = {} qubit_coordinates_map[1] = [[0, 0]] @@ -338,14 +353,182 @@ def plot_gate_map( [8, 10], ] - config = backend.configuration() - num_qubits = config.n_qubits - coupling_map = config.coupling_map - qubit_coordinates = qubit_coordinates_map.get(num_qubits) + qubit_coordinates_map[127] = [ + [0, 0], + [0, 1], + [0, 2], + [0, 3], + [0, 4], + [0, 5], + [0, 6], + [0, 7], + [0, 8], + [0, 9], + [0, 10], + [0, 11], + [0, 12], + [0, 13], + [1, 0], + [1, 4], + [1, 8], + [1, 12], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + [2, 4], + [2, 5], + [2, 6], + [2, 7], + [2, 8], + [2, 9], + [2, 10], + [2, 11], + [2, 12], + [2, 13], + [2, 14], + [3, 2], + [3, 6], + [3, 10], + [3, 14], + [4, 0], + [4, 1], + [4, 2], + [4, 3], + [4, 4], + [4, 5], + [4, 6], + [4, 7], + [4, 8], + [4, 9], + [4, 10], + [4, 11], + [4, 12], + [4, 13], + [4, 14], + [5, 0], + [5, 4], + [5, 8], + [5, 12], + [6, 0], + [6, 1], + [6, 2], + [6, 3], + [6, 4], + [6, 5], + [6, 6], + [6, 7], + [6, 8], + [6, 9], + [6, 10], + [6, 11], + [6, 12], + [6, 13], + [6, 14], + [7, 2], + [7, 6], + [7, 10], + [7, 14], + [8, 0], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [8, 6], + [8, 7], + [8, 8], + [8, 9], + [8, 10], + [8, 11], + [8, 12], + [8, 13], + [8, 14], + [9, 0], + [9, 4], + [9, 8], + [9, 12], + [10, 0], + [10, 1], + [10, 2], + [10, 3], + [10, 4], + [10, 5], + [10, 6], + [10, 7], + [10, 8], + [10, 9], + [10, 10], + [10, 11], + [10, 12], + [10, 13], + [10, 14], + [11, 2], + [11, 6], + [11, 10], + [11, 14], + [12, 1], + [12, 2], + [12, 3], + [12, 4], + [12, 5], + [12, 6], + [12, 7], + [12, 8], + [12, 9], + [12, 10], + [12, 11], + [12, 12], + [12, 13], + [12, 14], + ] + + backend_version = _get_backend_interface_version(backend) + if backend_version <= 1: + from qiskit.transpiler.coupling import CouplingMap + + if backend.configuration().simulator: + raise QiskitError("Requires a device backend, not simulator.") + config = backend.configuration() + num_qubits = config.n_qubits + coupling_map = CouplingMap(config.coupling_map) + name = backend.name() + else: + num_qubits = backend.num_qubits + coupling_map = backend.coupling_map + name = backend.name + if qubit_coordinates is None and ("ibm" in name or "fake" in name): + qubit_coordinates = qubit_coordinates_map.get(num_qubits, None) + + if qubit_coordinates is None: + # Replace with planar_layout() when retworkx offers it + qubit_coordinates_rx = rx.spring_layout(coupling_map.graph, seed=1234) + scaling_factor = 10 ** int(math.log10(num_qubits) + 1) + qubit_coordinates = [ + ( + int(scaling_factor * qubit_coordinates_rx[i][0]), + int(scaling_factor * qubit_coordinates_rx[i][1]), + ) + for i in range(num_qubits) + ] + + if any(x[0] < 0 or x[1] < 0 for x in qubit_coordinates): + min_entry = min(qubit_coordinates, key=lambda x: min(x[0], x[1])) + negative_offset = 0 - min(min_entry) + qubit_coordinates = [ + (x[0] + negative_offset, x[1] + negative_offset) for x in qubit_coordinates + ] + + if len(qubit_coordinates) != num_qubits: + raise QiskitError( + f"The number of specified qubit coordinates {len(qubit_coordinates)} " + f"does not match the device number of qubits: {num_qubits}" + ) + return plot_coupling_map( num_qubits, qubit_coordinates, - coupling_map, + coupling_map.get_edges(), figsize, plot_directed, label_qubits, @@ -565,7 +748,7 @@ def plot_coupling_map( return None -def plot_circuit_layout(circuit, backend, view="virtual"): +def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None): """Plot the layout of a circuit transpiled for a given target backend. @@ -573,6 +756,11 @@ def plot_circuit_layout(circuit, backend, view="virtual"): circuit (QuantumCircuit): Input quantum circuit. backend (BaseBackend): Target backend. view (str): Layout view: either 'virtual' or 'physical'. + qubit_coordinates (Sequence): An optional sequence input (list or array being the + most common) of 2d coordinates for each qubit. The length of the + sequence much mast the number of qubits on the backend. The sequence + should be the planar coordinates in a 0-based square grid where each + qubit is located. Returns: Figure: A matplotlib figure showing layout. @@ -614,7 +802,13 @@ def plot_circuit_layout(circuit, backend, view="virtual"): if circuit._layout is None: raise QiskitError("Circuit has no layout. Perhaps it has not been transpiled.") - num_qubits = backend.configuration().n_qubits + backend_version = _get_backend_interface_version(backend) + if backend_version <= 1: + num_qubits = backend.configuration().n_qubits + cmap = backend.configuration().coupling_map + else: + num_qubits = backend.num_qubits + cmap = backend.coupling_map qubits = [] qubit_labels = [None] * num_qubits @@ -649,27 +843,36 @@ def plot_circuit_layout(circuit, backend, view="virtual"): for k in qubits: qcolors[k] = "k" - cmap = backend.configuration().coupling_map - lcolors = ["#648fff"] * len(cmap) for idx, edge in enumerate(cmap): if edge[0] in qubits and edge[1] in qubits: lcolors[idx] = "k" - fig = plot_gate_map(backend, qubit_color=qcolors, qubit_labels=qubit_labels, line_color=lcolors) + fig = plot_gate_map( + backend, + qubit_color=qcolors, + qubit_labels=qubit_labels, + line_color=lcolors, + qubit_coordinates=qubit_coordinates, + ) return fig @_optionals.HAS_MATPLOTLIB.require_in_call @_optionals.HAS_SEABORN.require_in_call -def plot_error_map(backend, figsize=(12, 9), show_title=True): +def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=None): """Plots the error map of a given backend. Args: backend (IBMQBackend): Given backend. figsize (tuple): Figure size in inches. show_title (bool): Show the title or not. + qubit_coordinates (Sequence): An optional sequence input (list or array being the + most common) of 2d coordinates for each qubit. The length of the + sequence much mast the number of qubits on the backend. The sequence + should be the planar coordinates in a 0-based square grid where each + qubit is located. Returns: Figure: A matplotlib figure showing error map. @@ -705,24 +908,78 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True): color_map = sns.cubehelix_palette(reverse=True, as_cmap=True) - props = backend.properties().to_dict() - config = backend.configuration().to_dict() - - num_qubits = config["n_qubits"] - - # sx error rates - single_gate_errors = [0] * num_qubits - for gate in props["gates"]: - if gate["gate"] == "sx": - _qubit = gate["qubits"][0] - for param in gate["parameters"]: - if param["name"] == "gate_error": - single_gate_errors[_qubit] = param["value"] - break - else: - raise VisualizationError( - f"Backend '{backend}' did not supply an error for the 'sx' gate." - ) + backend_version = _get_backend_interface_version(backend) + if backend_version <= 1: + num_qubits = backend.configuration().n_qubits + cmap = backend.configuration().coupling_map + props = backend.properties().to_dict() + single_gate_errors = [0] * num_qubits + read_err = [0] * num_qubits + cx_errors = [] + # sx error rates + for gate in props["gates"]: + if gate["gate"] == "sx": + _qubit = gate["qubits"][0] + for param in gate["parameters"]: + if param["name"] == "gate_error": + single_gate_errors[_qubit] = param["value"] + break + else: + raise VisualizationError( + f"Backend '{backend}' did not supply an error for the 'sx' gate." + ) + if cmap: + directed = False + if num_qubits < 20: + for edge in cmap: + if not [edge[1], edge[0]] in cmap: + directed = True + break + + for line in cmap: + for item in props["gates"]: + if item["qubits"] == line: + cx_errors.append(item["parameters"][0]["value"]) + break + for qubit in range(num_qubits): + for item in props["qubits"][qubit]: + if item["name"] == "readout_error": + read_err.append(item["value"]) + + else: + num_qubits = backend.num_qubits + cmap = backend.coupling_map + two_q_error_map = {} + single_gate_errors = [0] * num_qubits + read_err = [0] * num_qubits + cx_errors = [] + for gate, prop_dict in backend.target.items(): + if prop_dict is None or None in prop_dict: + continue + for qargs, inst_props in prop_dict.items(): + if gate == "measure": + if inst_props.error is not None: + read_err[qargs[0]] = inst_props.error + elif len(qargs) == 1: + if inst_props.error is not None: + single_gate_errors[qargs[0]] = max( + single_gate_errors[qargs[0]], inst_props.error + ) + elif len(qargs) == 2: + if inst_props.error is not None: + two_q_error_map[qargs] = max( + two_q_error_map.get(qargs, 0), inst_props.error + ) + if cmap: + directed = False + if num_qubits < 20: + for edge in cmap: + if not [edge[1], edge[0]] in cmap: + directed = True + break + for line in cmap: + err = two_q_error_map.get(tuple(line), 0) + cx_errors.append(err) # Convert to percent single_gate_errors = 100 * np.asarray(single_gate_errors) @@ -733,26 +990,9 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True): ) q_colors = [color_map(single_norm(err)) for err in single_gate_errors] - cmap = config["coupling_map"] - directed = False line_colors = [] if cmap: - directed = False - if num_qubits < 20: - for edge in cmap: - if not [edge[1], edge[0]] in cmap: - directed = True - break - - cx_errors = [] - for line in cmap: - for item in props["gates"]: - if item["qubits"] == line: - cx_errors.append(item["parameters"][0]["value"]) - break - else: - continue # Convert to percent cx_errors = 100 * np.asarray(cx_errors) @@ -761,15 +1001,6 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True): cx_norm = matplotlib.colors.Normalize(vmin=min(cx_errors), vmax=max(cx_errors)) line_colors = [color_map(cx_norm(err)) for err in cx_errors] - # Measurement errors - - read_err = [] - - for qubit in range(num_qubits): - for item in props["qubits"][qubit]: - if item["name"] == "readout_error": - read_err.append(item["value"]) - read_err = 100 * np.asarray(read_err) avg_read_err = np.mean(read_err) max_read_err = np.max(read_err) @@ -799,6 +1030,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True): line_width=5, plot_directed=directed, ax=main_ax, + qubit_coordinates=qubit_coordinates, ) main_ax.axis("off") main_ax.set_aspect(1) diff --git a/releasenotes/notes/update-plot-gate-map-9ed6ad5490bafbbf.yaml b/releasenotes/notes/update-plot-gate-map-9ed6ad5490bafbbf.yaml new file mode 100644 index 000000000000..981e04a9be9f --- /dev/null +++ b/releasenotes/notes/update-plot-gate-map-9ed6ad5490bafbbf.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + The :func:`~.plot_gate_map` visualization function and the functions built + on top of it, :func:`~.plot_error_map` and :func:`~.plot_circuit_layout`, + have a new keyword argument, ``qubit_coordinates``. This argument takes + a sequence of 2D coordinates to use for plotting each qubit in the backend + being visualized. If specified this sequence must have a length equal to + the number of qubits on the backend and it will be used instead of the + default behavior. + - | + The :func:`~.plot_gate_map` visualization function and the functions built + on top of it, :func:`~.plot_error_map` and :func:`~.plot_circuit_layout`, + now are able to plot any backend not just those with the number of qubits + equal to one of the IBM backends. This relies on + the retworkx ``spring_layout()`` + `function `__ + to generate the layout for the visualization. If the default layout doesn't + work with a backend's particular coupling graph you can use the + ``qubit_coordinates`` function to set a custom layout. + - | + The :func:`~.plot_gate_map` visualization function and the functions built + on top of it, :func:`~.plot_error_map` and :func:`~.plot_circuit_layout`, + are now able to function with a :class:`~.BackendV2` based backend. + Previously, these functions only worked with :class:`~.BaseBackend` or + :class:`~.BackendV1` based backends. diff --git a/test/python/visualization/references/16_plot_circuit_layout.png b/test/python/visualization/references/16_plot_circuit_layout.png index 72aaf3b3f5b9..7f6c34d19c17 100644 Binary files a/test/python/visualization/references/16_plot_circuit_layout.png and b/test/python/visualization/references/16_plot_circuit_layout.png differ