From 064c274a20066d8815d85223e2906b2c7835c129 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Fri, 13 Sep 2024 07:36:22 -0400 Subject: [PATCH] initial commit --- .github/workflows/ci.yml | 4 +- qiskit_ibm_runtime/visualization/__init__.py | 0 .../visualization/plot_execution_spans.py | 112 ++++++++++++++++++ requirements-visual.txt | 1 + tox.ini | 1 + 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 qiskit_ibm_runtime/visualization/__init__.py create mode 100644 qiskit_ibm_runtime/visualization/plot_execution_spans.py create mode 100644 requirements-visual.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca1485bb2..14e9cf554 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -c constraints.txt -r requirements-dev.txt -e . + pip install -c constraints.txt -r requirements-dev.txt -r requirements-visual.txt -e . - name: Run black run: make style - name: Run lint @@ -103,7 +103,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -c constraints.txt -r requirements-dev.txt -e . + pip install -c constraints.txt -r requirements-dev.txt -r requirements-visual.txt -e . - name: Run unit tests run: make unit-test-coverage - name: Report coverage to coveralls.io diff --git a/qiskit_ibm_runtime/visualization/__init__.py b/qiskit_ibm_runtime/visualization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qiskit_ibm_runtime/visualization/plot_execution_spans.py b/qiskit_ibm_runtime/visualization/plot_execution_spans.py new file mode 100644 index 000000000..d26dfc460 --- /dev/null +++ b/qiskit_ibm_runtime/visualization/plot_execution_spans.py @@ -0,0 +1,112 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from __future__ import annotations + +from functools import partial +from typing import TYPE_CHECKING + +import plotly.graph_objects as go +import numpy as np + +if TYPE_CHECKING: + from ..execution_span import ExecutionSpans + +HOVER_TEMPLATE = "
".join( + [ + "ExecutionSpans{id}[{idx}]", + "   Start: {span.start:%Y-%m-%d %H:%M:%S.%f}", + "   Stop: {span.stop:%Y-%m-%d %H:%M:%S.%f}", + "   Size: {span.size}", + "   Pub Indexes: {idxs}", + ] +) + + +def _get_idxs(span, limit=10): + if len(idxs := span.pub_idxs) <= limit: + return str(idxs) + else: + return f"[{', '.join(map(str, idxs[:limit]))}, ...]" + + +def _get_id(span, multiple): + return f"<{hex(id(span))}>" if multiple else "" + + +def plot_execution_spans(*list_of_spans: ExecutionSpans, common_start: bool = False) -> go.Figure: + """Plot one or more :class:`~.ExecutionSpans` on a bar plot. + + Args: + list_of_spans: One or more :class:`~.ExecutionSpans`. + common_start: Whether to shift all collections of spans so that their first span's start is + at :math:`t=0`. + + Returns: + A plotly figure. + """ + fig = go.Figure() + + get_id = partial(_get_id, multiple=len(list_of_spans) > 1) + + for spans in list_of_spans: + if not spans: + continue + spans = spans.sort(inplace=False) + + starts = np.array([span.start.replace(tzinfo=None) for span in spans], dtype=np.datetime64) + stops = np.array([span.stop.replace(tzinfo=None) for span in spans], dtype=np.datetime64) + # plotly wants durations to be numeric in units of ms + durations = (stops - starts) / np.timedelta64(1, "ms") + total_sizes = np.cumsum([span.size for span in spans]) + + if common_start: + # plotly doesn't natively support using date formatting with time deltas, + # so the commonly recommended hack is to plot absolute times past the epoch + # but format the ticks to not include the year + offsets = starts - np.datetime64(spans.start.replace(tzinfo=None)) + starts = np.datetime64("1970-01-01") + offsets + + texts = [ + HOVER_TEMPLATE.format(span=span, idx=idx, idxs=_get_idxs(span), id=get_id(span)) + for idx, span in enumerate(spans) + ] + + # Add a trace for each time segment + fig.add_trace( + go.Bar( + y=list(map(int, total_sizes)), + x=durations, + orientation="h", + width=0.4, + base=starts, + text=texts, + hoverinfo="text", + textposition="none", + ) + ) + + xaxis_format = dict(title="Time", type="date") + if common_start: + xaxis_format["tickformat"] = "%H:%M:%S.%f" + + # Update layout for better visualization + fig.update_layout( + xaxis=xaxis_format, + barmode="group", + yaxis=dict(title="Execution Spans", type="category"), + bargap=0.3, + showlegend=False, + margin=dict(l=70, r=20, t=20, b=70), + ) + + return fig diff --git a/requirements-visual.txt b/requirements-visual.txt new file mode 100644 index 000000000..0e77b4b6a --- /dev/null +++ b/requirements-visual.txt @@ -0,0 +1 @@ +plotly>=5.23.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 35580b13c..d355fe248 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ passenv = deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt + -r{toxinidir}/requirements-visual.txt commands = python -m unittest -v