Skip to content

Commit

Permalink
Bug fixes and improvements for LayerError visualization tool (#1973)
Browse files Browse the repository at this point in the history
* modifications

* tests

* better docs

* oops

* Update qiskit_ibm_runtime/utils/noise_learner_result.py

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

* Update qiskit_ibm_runtime/visualization/draw_layer_error_map.py

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

---------

Co-authored-by: joshuasn <[email protected]>
  • Loading branch information
SamFerracin and joshuasn authored Oct 16, 2024
1 parent c92b839 commit 0f659ec
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 28 deletions.
38 changes: 38 additions & 0 deletions qiskit_ibm_runtime/utils/noise_learner_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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,
Expand Down
46 changes: 32 additions & 14 deletions qiskit_ibm_runtime/visualization/draw_layer_error_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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))
Expand All @@ -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():
Expand Down Expand Up @@ -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",
Expand All @@ -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,
},
Expand Down
9 changes: 7 additions & 2 deletions qiskit_ibm_runtime/visualization/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -55,14 +57,17 @@ 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.
"""
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
Expand Down
18 changes: 6 additions & 12 deletions test/unit/test_noise_learner_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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,
Expand All @@ -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)

0 comments on commit 0f659ec

Please sign in to comment.