diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index 86a3fbe9f858..6acd2f8b0ab7 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -17,12 +17,13 @@ import numpy as np import rustworkx as rx +from rustworkx.visualization import graphviz_draw from qiskit.exceptions import QiskitError from qiskit.utils import optionals as _optionals from qiskit.providers.exceptions import BackendPropertyError +from qiskit.transpiler.coupling import CouplingMap from .exceptions import VisualizationError -from .utils import matplotlib_close_if_inline def _get_backend_interface_version(backend): @@ -42,7 +43,7 @@ def plot_gate_map( qubit_color=None, qubit_labels=None, line_color=None, - font_color="w", + font_color="white", ax=None, filename=None, qubit_coordinates=None, @@ -93,8 +94,6 @@ def plot_gate_map( """ qubit_coordinates_map = {} - qubit_coordinates_map[1] = [[0, 0]] - qubit_coordinates_map[5] = [[1, 0], [0, 1], [1, 1], [1, 2], [2, 1]] qubit_coordinates_map[7] = [[0, 0], [0, 1], [0, 2], [1, 1], [2, 0], [2, 1], [2, 2]] @@ -912,8 +911,6 @@ def plot_gate_map( 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() @@ -927,31 +924,12 @@ def plot_gate_map( 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 rustworkx 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]), + if 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}" ) - 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, @@ -972,6 +950,7 @@ def plot_gate_map( @_optionals.HAS_MATPLOTLIB.require_in_call +@_optionals.HAS_GRAPHVIZ.require_in_call def plot_coupling_map( num_qubits: int, qubit_coordinates: List[List[int]], @@ -985,7 +964,7 @@ def plot_coupling_map( qubit_color=None, qubit_labels=None, line_color=None, - font_color="w", + font_color="white", ax=None, filename=None, ): @@ -1014,7 +993,7 @@ def plot_coupling_map( Figure: A Matplotlib figure instance. Raises: - MissingOptionalLibraryError: if matplotlib not installed. + MissingOptionalLibraryError: If matplotlib or graphviz is not installed. QiskitError: If length of qubit labels does not match number of qubits. Example: @@ -1030,20 +1009,14 @@ def plot_coupling_map( plot_coupling_map(num_qubits, qubit_coordinates, coupling_map) """ import matplotlib.pyplot as plt - import matplotlib.patches as mpatches + from .utils import matplotlib_close_if_inline input_axes = False if ax: input_axes = True - if font_size is None: - font_size = 12 - if qubit_size is None: - qubit_size = 24 - if num_qubits > 20: - qubit_size = 28 - font_size = 10 + qubit_size = 30 if qubit_labels is None: qubit_labels = list(range(num_qubits)) @@ -1051,128 +1024,96 @@ def plot_coupling_map( if len(qubit_labels) != num_qubits: raise QiskitError("Length of qubit labels does not equal number of qubits.") - if qubit_coordinates is not None: - grid_data = qubit_coordinates - else: - if not input_axes: - fig, ax = plt.subplots(figsize=(5, 5)) - ax.axis("off") - if filename: - fig.savefig(filename) - return fig - - x_max = max(d[1] for d in grid_data) - y_max = max(d[0] for d in grid_data) - max_dim = max(x_max, y_max) - - if figsize is None: - if num_qubits == 1 or (x_max / max_dim > 0.33 and y_max / max_dim > 0.33): - figsize = (5, 5) - else: - figsize = (9, 3) - - if ax is None: - fig, ax = plt.subplots(figsize=figsize) - ax.axis("off") + if not label_qubits: + qubit_labels = [""] * num_qubits # set coloring if qubit_color is None: qubit_color = ["#648fff"] * num_qubits if line_color is None: - line_color = ["#648fff"] * len(coupling_map) if coupling_map else [] - - # Add lines for couplings - if num_qubits != 1: - for ind, edge in enumerate(coupling_map): - is_symmetric = False - if edge[::-1] in coupling_map: - is_symmetric = True - y_start = grid_data[edge[0]][0] - x_start = grid_data[edge[0]][1] - y_end = grid_data[edge[1]][0] - x_end = grid_data[edge[1]][1] - - if is_symmetric: - if y_start == y_end: - x_end = (x_end - x_start) / 2 + x_start - - elif x_start == x_end: - y_end = (y_end - y_start) / 2 + y_start + line_color = ["#648fff"] * len(coupling_map) - else: - x_end = (x_end - x_start) / 2 + x_start - y_end = (y_end - y_start) / 2 + y_start - ax.add_artist( - plt.Line2D( - [x_start, x_end], - [-y_start, -y_end], - color=line_color[ind], - linewidth=line_width, - zorder=0, - ) - ) - if plot_directed: - dx = x_end - x_start - dy = y_end - y_start - if is_symmetric: - x_arrow = x_start + dx * 0.95 - y_arrow = -y_start - dy * 0.95 - dx_arrow = dx * 0.01 - dy_arrow = -dy * 0.01 - head_width = 0.15 - else: - x_arrow = x_start + dx * 0.5 - y_arrow = -y_start - dy * 0.5 - dx_arrow = dx * 0.2 - dy_arrow = -dy * 0.2 - head_width = 0.2 - ax.add_patch( - mpatches.FancyArrow( - x_arrow, - y_arrow, - dx_arrow, - dy_arrow, - head_width=head_width, - length_includes_head=True, - edgecolor=None, - linewidth=0, - facecolor=line_color[ind], - zorder=1, - ) - ) - - # Add circles for qubits - for var, idx in enumerate(grid_data): - _idx = [idx[1], -idx[0]] - ax.add_artist( - mpatches.Ellipse( - _idx, - qubit_size / 48, - qubit_size / 48, # This is here so that the changes - color=qubit_color[var], - zorder=1, - ) - ) # to how qubits are plotted does - if label_qubits: # not affect qubit size kwarg. - ax.text( - *_idx, - s=qubit_labels[var], - horizontalalignment="center", - verticalalignment="center", - color=font_color, - size=font_size, - weight="bold", - ) - ax.set_xlim([-1, x_max + 1]) - ax.set_ylim([-(y_max + 1), 1]) - ax.set_aspect("equal") + if num_qubits == 1: + graph = rx.PyDiGraph() + graph.add_node(0) + else: + graph = CouplingMap(coupling_map).graph + + if not plot_directed: + graph = graph.to_undirected(multigraph=False) + + for node in graph.node_indices(): + graph[node] = node + + for edge_index in graph.edge_indices(): + graph.update_edge_by_index(edge_index, edge_index) + + # pixel-to-inch conversion + px = 1.15 / plt.rcParams["figure.dpi"] + + if qubit_coordinates: + qubit_coordinates = [coordinates[::-1] for coordinates in qubit_coordinates] + + if font_size is None: + max_characters = max(1, max(len(str(x)) for x in qubit_labels)) + font_size = max(int(20 / max_characters), 1) + + def color_node(node): + if qubit_coordinates: + out_dict = { + "label": str(qubit_labels[node]), + "color": f'"{qubit_color[node]}"', + "fillcolor": f'"{qubit_color[node]}"', + "style": "filled", + "shape": "circle", + "pos": f'"{qubit_coordinates[node][0]},{qubit_coordinates[node][1]}"', + "pin": "True", + } + else: + out_dict = { + "label": str(qubit_labels[node]), + "color": f'"{qubit_color[node]}"', + "fillcolor": f'"{qubit_color[node]}"', + "style": "filled", + "shape": "circle", + } + out_dict["fontcolor"] = f'"{font_color}"' + out_dict["fontsize"] = str(font_size) + out_dict["height"] = str(qubit_size * px) + out_dict["fixedsize"] = "True" + out_dict["fontname"] = '"DejaVu Sans"' + return out_dict + + def color_edge(edge): + out_dict = { + "color": f'"{line_color[edge]}"', + "fillcolor": f'"{line_color[edge]}"', + "penwidth": str(line_width), + } + return out_dict + + plot = graphviz_draw( + graph, + method="neato", + node_attr_fn=color_node, + edge_attr_fn=color_edge, + filename=filename, + ) + + if filename: + return None + + if not input_axes: + if figsize is None: + width, height = plot.size + figsize = (width * px, height * px) + fig, ax = plt.subplots(figsize=figsize) + ax.axis("off") + ax.imshow(plot) if not input_axes: matplotlib_close_if_inline(fig) - if filename: - fig.savefig(filename) return fig - return None def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None): @@ -1232,7 +1173,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None cmap_len = cmap.graph.num_edges() qubits = [] - qubit_labels = [None] * num_qubits + qubit_labels = [""] * num_qubits bit_locations = { bit: {"register": register, "index": index} @@ -1248,27 +1189,27 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None bit_register = bit_locations[key]["register"] if bit_register is None or bit_register.name != "ancilla": qubits.append(val) - qubit_labels[val] = bit_locations[key]["index"] + qubit_labels[val] = str(bit_locations[key]["index"]) elif view == "physical": for key, val in circuit._layout.initial_layout.get_physical_bits().items(): bit_register = bit_locations[val]["register"] if bit_register is None or bit_register.name != "ancilla": qubits.append(key) - qubit_labels[key] = key + qubit_labels[key] = str(key) else: raise VisualizationError("Layout view must be 'virtual' or 'physical'.") qcolors = ["#648fff"] * num_qubits for k in qubits: - qcolors[k] = "k" + qcolors[k] = "black" lcolors = ["#648fff"] * cmap_len for idx, edge in enumerate(cmap): if edge[0] in qubits and edge[1] in qubits: - lcolors[idx] = "k" + lcolors[idx] = "black" fig = plot_gate_map( backend, @@ -1282,7 +1223,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None @_optionals.HAS_MATPLOTLIB.require_in_call @_optionals.HAS_SEABORN.require_in_call -def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates=None): +def plot_error_map(backend, figsize=(15, 12), show_title=True, qubit_coordinates=None): """Plots the error map of a given backend. Args: @@ -1300,7 +1241,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates= Raises: VisualizationError: The backend does not provide gate errors for the 'sx' gate. - MissingOptionalLibraryError: If seaborn is not installed + MissingOptionalLibraryError: If matplotlib or seaborn is not installed. Example: .. plot:: @@ -1313,10 +1254,11 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates= backend = FakeVigoV2() plot_error_map(backend) """ - import seaborn as sns import matplotlib import matplotlib.pyplot as plt from matplotlib import gridspec, ticker + import seaborn as sns + from .utils import matplotlib_close_if_inline color_map = sns.cubehelix_palette(reverse=True, as_cmap=True) @@ -1406,7 +1348,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates= single_norm = matplotlib.colors.Normalize( vmin=min(single_gate_errors), vmax=max(single_gate_errors) ) - q_colors = [color_map(single_norm(err)) for err in single_gate_errors] + q_colors = [matplotlib.colors.to_hex(color_map(single_norm(err))) for err in single_gate_errors] directed = False line_colors = [] @@ -1417,7 +1359,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates= avg_cx_err = np.mean(cx_errors) 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] + line_colors = [matplotlib.colors.to_hex(color_map(cx_norm(err))) for err in cx_errors] read_err = 100 * np.asarray(read_err) avg_read_err = np.mean(read_err) @@ -1450,6 +1392,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True, qubit_coordinates= ax=main_ax, qubit_coordinates=qubit_coordinates, ) + main_ax.axis("off") main_ax.set_aspect(1) if cmap: diff --git a/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml b/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml new file mode 100644 index 000000000000..4ef11ae430d7 --- /dev/null +++ b/releasenotes/notes/update-gate-map-visualizations-6ea907a0502fdc1a.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + The visualizations from the :func:`~.plot_gate_map`, :func:`~.plot_coupling_map`. + :func:`~.plot_error_map`, and :func:`~.plot_circuit_layout` functions have been significantly + improved for rendering layouts of backends with large numbers of qubits. This was accomplished + by leveraging `graphviz `__ through rustworkx's ``graphviz_draw()`` function + to perform a more sophisticated algorithmic graph layout that scales for large numbers of + qubits. + +upgrade: + - | + The visualization functions: :func:`~.plot_gate_map`, :func:`~.plot_coupling_map`. + :func:`~.plot_error_map`, and :func:`~.plot_circuit_layout` now depend on + `graphviz `__ being installed to function. This change was + necessary to enable visualizing backends with larger numbers of qubits. This + additional external requirement is in addition to the existing optional dependencies + these functions previously required. You find details on how to install + graphviz here: https://graphviz.org/download/ diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index 29f9d7286b36..c4b7c3aabf32 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -55,6 +55,8 @@ class TestGateMap(QiskitVisualizationTestCase): ) @data(*backends) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") def test_plot_gate_map(self, backend): """tests plotting of gate map of a device (20 qubit, 16 qubit, 14 qubit and 5 qubit)""" n = backend.configuration().n_qubits @@ -67,6 +69,8 @@ def test_plot_gate_map(self, backend): plt.close(fig) @data(*backends) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") def test_plot_circuit_layout(self, backend): """tests plot_circuit_layout for each device""" layout_length = int(backend._configuration.n_qubits / 2) @@ -83,9 +87,11 @@ def test_plot_circuit_layout(self, backend): with BytesIO() as img_buffer: fig.savefig(img_buffer, format="png") img_buffer.seek(0) - self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.1) + self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") def test_plot_gate_map_no_backend(self): """tests plotting of gate map without a device""" n_qubits = 8 @@ -101,6 +107,9 @@ def test_plot_gate_map_no_backend(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_backend_v1(self): """Test plotting error map with fake backend v1.""" backend = FakeKolkata() @@ -112,6 +121,9 @@ def test_plot_error_map_backend_v1(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_backend_v2(self): """Test plotting error map with fake backend v2.""" backend = FakeKolkataV2() @@ -123,6 +135,9 @@ def test_plot_error_map_backend_v2(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_over_100_qubit(self): """Test plotting error map with large fake backend.""" backend = FakeWashington() @@ -134,6 +149,9 @@ def test_plot_error_map_over_100_qubit(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_SEABORN, "Seaborn not installed") def test_plot_error_map_over_100_qubit_backend_v2(self): """Test plotting error map with large fake backendv2.""" backend = FakeWashingtonV2() diff --git a/test/visual/mpl/graph/references/16_qubit_gate_map.png b/test/visual/mpl/graph/references/16_qubit_gate_map.png index 632fe59ce182..fe9cb3645bcc 100644 Binary files a/test/visual/mpl/graph/references/16_qubit_gate_map.png and b/test/visual/mpl/graph/references/16_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/1_qubit_gate_map.png b/test/visual/mpl/graph/references/1_qubit_gate_map.png index 9d770a0d6597..d04aca637ddf 100644 Binary files a/test/visual/mpl/graph/references/1_qubit_gate_map.png and b/test/visual/mpl/graph/references/1_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/27_qubit_gate_map.png b/test/visual/mpl/graph/references/27_qubit_gate_map.png index b81ee4619395..5baadad4eb94 100644 Binary files a/test/visual/mpl/graph/references/27_qubit_gate_map.png and b/test/visual/mpl/graph/references/27_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/5_qubit_gate_map.png b/test/visual/mpl/graph/references/5_qubit_gate_map.png index 8c50538a795c..1bc197fa0556 100644 Binary files a/test/visual/mpl/graph/references/5_qubit_gate_map.png and b/test/visual/mpl/graph/references/5_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/65_qubit_gate_map.png b/test/visual/mpl/graph/references/65_qubit_gate_map.png index 88f540e69bb9..8cba4e5a4be4 100644 Binary files a/test/visual/mpl/graph/references/65_qubit_gate_map.png and b/test/visual/mpl/graph/references/65_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/7_qubit_gate_map.png b/test/visual/mpl/graph/references/7_qubit_gate_map.png index 543e9159a317..deaec94cd598 100644 Binary files a/test/visual/mpl/graph/references/7_qubit_gate_map.png and b/test/visual/mpl/graph/references/7_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/coupling_map.png b/test/visual/mpl/graph/references/coupling_map.png index f5e51efea127..dcdb1fe10dc9 100644 Binary files a/test/visual/mpl/graph/references/coupling_map.png and b/test/visual/mpl/graph/references/coupling_map.png differ diff --git a/test/visual/mpl/graph/references/figsize.png b/test/visual/mpl/graph/references/figsize.png index 25ece4b5696d..1bc197fa0556 100644 Binary files a/test/visual/mpl/graph/references/figsize.png and b/test/visual/mpl/graph/references/figsize.png differ diff --git a/test/visual/mpl/graph/references/font_color.png b/test/visual/mpl/graph/references/font_color.png index 6fef2274d9dc..4296aa5610cb 100644 Binary files a/test/visual/mpl/graph/references/font_color.png and b/test/visual/mpl/graph/references/font_color.png differ diff --git a/test/visual/mpl/graph/references/line_color.png b/test/visual/mpl/graph/references/line_color.png index a28d508afa36..11b09840fa8f 100644 Binary files a/test/visual/mpl/graph/references/line_color.png and b/test/visual/mpl/graph/references/line_color.png differ diff --git a/test/visual/mpl/graph/references/qubit_color.png b/test/visual/mpl/graph/references/qubit_color.png index caf5631ff2d3..0fe7329cc690 100644 Binary files a/test/visual/mpl/graph/references/qubit_color.png and b/test/visual/mpl/graph/references/qubit_color.png differ diff --git a/test/visual/mpl/graph/references/qubit_labels.png b/test/visual/mpl/graph/references/qubit_labels.png index 6a42e1176390..f80464a72b8e 100644 Binary files a/test/visual/mpl/graph/references/qubit_labels.png and b/test/visual/mpl/graph/references/qubit_labels.png differ diff --git a/test/visual/mpl/graph/references/qubit_size.png b/test/visual/mpl/graph/references/qubit_size.png index d66a174d1032..d7259766319a 100644 Binary files a/test/visual/mpl/graph/references/qubit_size.png and b/test/visual/mpl/graph/references/qubit_size.png differ