diff --git a/qiskit_ibm_runtime/utils/noise_learner_result.py b/qiskit_ibm_runtime/utils/noise_learner_result.py index aa799953f..f49ff17dc 100644 --- a/qiskit_ibm_runtime/utils/noise_learner_result.py +++ b/qiskit_ibm_runtime/utils/noise_learner_result.py @@ -223,9 +223,11 @@ def draw_map( embedding: Union[Embedding, BackendV2], colorscale: str = "Bluered", color_no_data: str = "lightgray", + color_out_of_scale: str = "lightgreen", num_edge_segments: int = 16, edge_width: float = 4, height: int = 500, + highest_rate: Optional[float] = None, background_color: str = "white", radius: float = 0.25, width: int = 800, @@ -238,12 +240,46 @@ def draw_map( to draw the layer error on, or a backend to generate an :class:`~.Embedding` for. colorscale: The colorscale used to show the rates of this layer error. color_no_data: The color used for qubits and edges for which no data is available. + color_out_of_scale: The color used for rates with value greater than ``highest_rate``. num_edge_segments: The number of equal-sized segments that edges are made of. edge_width: The line width of the edges in pixels. height: The height of the returned figure. + highest_rate: The highest rate, used to normalize all other rates before choosing their + colors. If ``None``, it defaults to the highest value found in the ``layer_error``. background_color: The background color. radius: The radius of the pie charts representing the qubits. width: The width of the returned figure. + + .. code:: python + + from qiskit import QuantumCircuit + from qiskit.quantum_info import PauliList + from qiskit_ibm_runtime.utils.embeddings import Embedding + from qiskit_ibm_runtime.utils.noise_learner_result import LayerError, PauliLindbladError + + # A five-qubit 1-D embedding with nearest neighbouring connectivity + coordinates1 = [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)] + coupling_map1 = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)] + embedding1 = Embedding(coordinates1, coupling_map1) + + # A six-qubit horseshoe-shaped embedding with nearest neighbouring connectivity + coordinates2 = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] + coupling_map2 = [(0, 1), (1, 2), (0, 3), (3, 4), (4, 5)] + embedding2 = Embedding(coordinates2, coupling_map2) + + # A LayerError object + circuit = QuantumCircuit(4) + qubits = [1, 2, 3, 4] + generators = PauliList(["IIIX", "IIXI", "IXII", "YIII", "ZIII", "XXII", "ZZII"]) + rates = [0.01, 0.01, 0.01, 0.005, 0.02, 0.01, 0.01] + error = PauliLindbladError(generators, rates) + layer_error = LayerError(circuit, qubits, error) + + # Draw the layer error on embedding1 + layer_error.draw_map(embedding1) + + # Draw the layer error on embedding2 + layer_error.draw_map(embedding2) """ # pylint: disable=import-outside-toplevel, cyclic-import @@ -254,9 +290,11 @@ def draw_map( embedding, colorscale, color_no_data, + color_out_of_scale, num_edge_segments, edge_width, height, + highest_rate, background_color, radius, width, diff --git a/qiskit_ibm_runtime/visualization/draw_layer_error_map.py b/qiskit_ibm_runtime/visualization/draw_layer_error_map.py index f59933874..b286751b2 100644 --- a/qiskit_ibm_runtime/visualization/draw_layer_error_map.py +++ b/qiskit_ibm_runtime/visualization/draw_layer_error_map.py @@ -13,7 +13,7 @@ """Functions to visualize :class:`~.NoiseLearnerResult` objects.""" from __future__ import annotations -from typing import Dict, Tuple, Union, TYPE_CHECKING +from typing import Dict, Optional, Tuple, Union, TYPE_CHECKING import numpy as np from qiskit.providers.backend import BackendV2 @@ -31,9 +31,11 @@ def draw_layer_error_map( embedding: Union[Embedding, BackendV2], colorscale: str = "Bluered", color_no_data: str = "lightgray", + color_out_of_scale: str = "lightgreen", num_edge_segments: int = 16, edge_width: float = 4, height: int = 500, + highest_rate: Optional[float] = None, background_color: str = "white", radius: float = 0.25, width: int = 800, @@ -47,9 +49,12 @@ def draw_layer_error_map( to draw the layer error on, or a backend to generate an :class:`~.Embedding` for. colorscale: The colorscale used to show the rates of ``layer_error``. color_no_data: The color used for qubits and edges for which no data is available. + color_out_of_scale: The color used for rates with value greater than ``highest_rate``. num_edge_segments: The number of equal-sized segments that edges are made of. edge_width: The line width of the edges in pixels. height: The height of the returned figure. + highest_rate: The highest rate, used to normalize all other rates before choosing their + colors. If ``None``, it defaults to the highest value found in the ``layer_error``. background_color: The background color. radius: The radius of the pie charts representing the qubits. width: The width of the returned figure. @@ -83,24 +88,28 @@ def draw_layer_error_map( # A set of unique edges ``(i, j)``, with ``i < j``. edges = set(tuple(sorted(edge)) for edge in list(coupling_map)) - # The highest rate, used to normalize all other rates before choosing their colors. - high_scale = 0 + # The highest rate + max_rate = 0 # Initialize a dictionary of one-qubit errors + qubits = layer_error.qubits error_1q = layer_error.error.restrict_num_bodies(1) - rates_1q: Dict[int, Dict[str, float]] = {qubit: {} for qubit in layer_error.qubits} + rates_1q: Dict[int, Dict[str, float]] = {qubit: {} for qubit in qubits} for pauli, rate in zip(error_1q.generators, error_1q.rates): - qubit = np.where(pauli.x | pauli.z)[0][0] - rates_1q[qubit][str(pauli[qubit])] = rate - high_scale = max(high_scale, rate) + qubit_idx = np.where(pauli.x | pauli.z)[0][0] + rates_1q[qubits[qubit_idx]][str(pauli[qubit_idx])] = rate + max_rate = max(max_rate, rate) # Initialize a dictionary of two-qubit errors error_2q = layer_error.error.restrict_num_bodies(2) - rates_2q: Dict[Tuple[int, ...], Dict[str, float]] = {qubits: {} for qubits in edges} + rates_2q: Dict[Tuple[int, ...], Dict[str, float]] = {edge: {} for edge in edges} for pauli, rate in zip(error_2q.generators, error_2q.rates): - qubits = tuple(sorted([i for i, q in enumerate(pauli) if str(q) != "I"])) - rates_2q[qubits][str(pauli[[qubits[0], qubits[1]]])] = rate - high_scale = max(high_scale, rate) + err_idxs = tuple(sorted([i for i, q in enumerate(pauli) if str(q) != "I"])) + edge = (qubits[err_idxs[0]], qubits[err_idxs[1]]) + rates_2q[edge][str(pauli[[err_idxs[0], err_idxs[1]]])] = rate + max_rate = max(max_rate, rate) + + highest_rate = highest_rate if highest_rate else max_rate # A discreet colorscale that contains 1000 hues. discreet_colorscale = sample_colorscale(colorscale, np.linspace(0, 1, 1000)) @@ -122,7 +131,10 @@ def draw_layer_error_map( for i in range(num_edge_segments) ] color = [ - get_rgb_color(discreet_colorscale, v / high_scale, color_no_data) for v in all_vals + get_rgb_color( + discreet_colorscale, v / highest_rate, color_no_data, color_out_of_scale + ) + for v in all_vals ] hoverinfo_2q = "" for pauli, rate in rates_2q[(q1, q2)].items(): @@ -172,7 +184,9 @@ def draw_layer_error_map( hoverinfo = "" for pauli, angle in [("Z", -30), ("X", 90), ("Y", 210)]: rate = rates_1q.get(qubit, {}).get(pauli, 0) - fillcolor = get_rgb_color(discreet_colorscale, rate / high_scale, color_no_data) + fillcolor = get_rgb_color( + discreet_colorscale, rate / highest_rate, color_no_data, color_out_of_scale + ) shapes += [ { "type": "path", @@ -191,12 +205,16 @@ def draw_layer_error_map( fig.add_annotation(x=x + 0.3, y=y + 0.4, text=f"{qubit}", showarrow=False) # Add the hoverinfo for the pie charts + marker_colors = [] + for qubit in rates_1q: + max_qubit_rate = max(rates_1q[qubit].values()) + marker_colors.append(max_qubit_rate if max_qubit_rate <= highest_rate else highest_rate) nodes = go.Scatter( x=xs, y=ys, mode="markers", marker={ - "color": list({qubit: max(rates_1q[qubit].values()) for qubit in rates_1q}.values()), + "color": marker_colors, "colorscale": colorscale, "showscale": True, }, diff --git a/qiskit_ibm_runtime/visualization/utils.py b/qiskit_ibm_runtime/visualization/utils.py index a701bdad2..5397e9869 100644 --- a/qiskit_ibm_runtime/visualization/utils.py +++ b/qiskit_ibm_runtime/visualization/utils.py @@ -46,7 +46,9 @@ def pie_slice(angle_st: float, angle_end: float, x: float, y: float, radius: flo return path -def get_rgb_color(discreet_colorscale: List[str], val: float, default: str) -> str: +def get_rgb_color( + discreet_colorscale: List[str], val: float, default: str, color_out_of_scale: str +) -> str: r""" Maps a float to an RGB color based on a discreet colorscale that contains exactly ``1000`` hues. @@ -55,6 +57,7 @@ def get_rgb_color(discreet_colorscale: List[str], val: float, default: str) -> s discreet_colorscale: A discreet colorscale. val: A value to map to a color. default: A default color returned when ``val`` is ``0``. + color_out_of_scale: The color that is returned when ``val`` is larger than ``1``. Raises: ValueError: If the colorscale contains more or less than ``1000`` hues. @@ -62,7 +65,9 @@ def get_rgb_color(discreet_colorscale: List[str], val: float, default: str) -> s if len(discreet_colorscale) != 1000: raise ValueError("Invalid ``discreet_colorscale.``") - if val >= 1: + if val > 1: + return color_out_of_scale + if val == 1: return discreet_colorscale[-1] if val == 0: return default diff --git a/test/unit/test_noise_learner_result.py b/test/unit/test_noise_learner_result.py index 022995a4f..728c8a02a 100644 --- a/test/unit/test_noise_learner_result.py +++ b/test/unit/test_noise_learner_result.py @@ -13,13 +13,13 @@ """Tests for the classes used to instantiate noise learner results.""" from unittest import skipIf -from ddt import ddt, data +from ddt import ddt from qiskit import QuantumCircuit from qiskit.quantum_info import PauliList from qiskit_aer import AerSimulator -from qiskit_ibm_runtime.fake_provider.local_service import QiskitRuntimeLocalService +from qiskit_ibm_runtime.fake_provider import FakeKyiv from qiskit_ibm_runtime.utils.noise_learner_result import PauliLindbladError, LayerError from ..ibm_test_case import IBMTestCase @@ -95,9 +95,6 @@ class TestLayerError(IBMTestCase): def setUp(self): super().setUp() - # A local service - self.service = QiskitRuntimeLocalService() - # A set of circuits c1 = QuantumCircuit(2) c1.cx(0, 1) @@ -121,7 +118,7 @@ def setUp(self): # Another set of errors used in the visualization tests circuit = QuantumCircuit(4) - qubits = [0, 1, 2, 3] + qubits = [1, 2, 3, 4] generators = PauliList(["IIIX", "IIXI", "IXII", "YIII", "ZIII", "XXII", "ZZII"]) rates = [0.01, 0.01, 0.01, 0.005, 0.02, 0.01, 0.01] self.layer_error_viz = LayerError(circuit, qubits, PauliLindbladError(generators, rates)) @@ -167,15 +164,12 @@ def test_no_coupling_map(self): self.layer_error_viz.draw_map(AerSimulator()) @skipIf(not PLOTLY_INSTALLED, reason="Plotly is not installed") - @data(["fake_hanoi", 44], ["fake_kyiv", 160]) - def test_plotting(self, inputs): + def test_plotting(self): r""" Tests the `draw_map` function to make sure that it produces the right figure. """ - backend_name, n_traces = inputs - backend = self.service.backend(backend_name) fig = self.layer_error_viz.draw_map( - backend, + embedding=FakeKyiv(), color_no_data="blue", colorscale="reds", radius=0.2, @@ -184,4 +178,4 @@ def test_plotting(self, inputs): ) self.assertIsInstance(fig, go.Figure) - self.assertEqual(len(fig.data), n_traces) + self.assertEqual(len(fig.data), 160)