Source code for qilisdk.utils.visualization.schedule_renderers

# Copyright 2025 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from __future__ import annotations

from typing import TYPE_CHECKING

import matplotlib.pyplot as plt

if TYPE_CHECKING:
    from qilisdk.analog.schedule import Schedule
    from qilisdk.common.variables import Number

from qilisdk.utils.visualization.style import ScheduleStyle


[docs] class MatplotlibScheduleRenderer: """Render a Schedule using matplotlib, with theme support.""" def __init__(self, schedule: Schedule, ax: plt.Axes | None = None, *, style: ScheduleStyle | None = None) -> None:
[docs] self.schedule = schedule
[docs] self.style = style or ScheduleStyle()
[docs] self.ax = ax or self._make_axes(self.style.dpi, self.style)
[docs] def plot(self, ax: plt.Axes | None = None) -> None: """ Plot the schedule coefficients for each Hamiltonian over time. Args: ax (plt.Axes | None): The matplotlib axes to plot on. Default is None. """ style = self.style theme = style.theme facecolor = theme.background title_color = theme.on_background label_color = theme.on_background legend_facecolor = theme.surface legend_edgecolor = theme.border tick_color = theme.on_background # Set axes and figure background to theme self.ax.set_facecolor(facecolor) if hasattr(ax, "figure"): self.ax.figure.set_facecolor(facecolor) plots: dict[str, list[Number]] = {} T = self.schedule.T dt = self.schedule.dt hamiltonians = self.schedule.hamiltonians times = [i * dt for i in range(int(T / dt))] for h in hamiltonians: plots[h] = [] for _t in range(int(T / dt)): t = _t * dt for h in hamiltonians: plots[h].append(self.schedule.get_coefficient(t, h)) # Generate gradient colors between primary and accent def hex_to_rgb(hex_color: str) -> tuple[int, ...]: hex_color = hex_color.lstrip("#") return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) def rgb_to_hex(rgb: tuple[int, ...]) -> str: return "#{:02x}{:02x}{:02x}".format(*rgb) def gradient_colors(start_hex: str, end_hex: str, n: int) -> list[str]: start_rgb = hex_to_rgb(start_hex) end_rgb = hex_to_rgb(end_hex) colors = [] for i in range(n): ratio = i / max(n - 1, 1) rgb = tuple(int(start_rgb[j] + (end_rgb[j] - start_rgb[j]) * ratio) for j in range(3)) colors.append(rgb_to_hex(rgb)) return colors n_hams = len(hamiltonians) grad_colors = gradient_colors(theme.primary, theme.accent, n_hams) for idx, h in enumerate(hamiltonians): line_style = style.line_styles.get(h, style.default_line_style) marker = style.marker # If no color specified, use gradient color if "color" not in line_style: color = grad_colors[idx] line_style = {**line_style, "color": color} self.ax.plot(times, plots[h], label=h, marker=marker, markersize=style.marker_size, **line_style) if style.grid: grid_style = dict(style.grid_style) if "color" not in grid_style: grid_style["color"] = theme.surface_muted self.ax.grid(**grid_style) leg = self.ax.legend( loc=style.legend_loc, fontsize=style.legend_fontsize, frameon=style.legend_frame, facecolor=legend_facecolor, edgecolor=legend_edgecolor, ) # Set legend text color to match theme text color if leg: for text in leg.get_texts(): text.set_color(title_color) self.ax.set_title( self.style.title or "Schedule Plot", fontsize=style.title_fontsize, color=title_color, fontweight=style.fontweight, family=style.fontfamily, ) self.ax.set_xlabel( style.xlabel, fontsize=style.label_fontsize, color=label_color, fontweight=style.fontweight, family=style.fontfamily, ) self.ax.set_ylabel( style.ylabel, fontsize=style.label_fontsize, color=label_color, fontweight=style.fontweight, family=style.fontfamily, ) self.ax.tick_params(axis="x", labelsize=style.xtick_fontsize, colors=tick_color) self.ax.tick_params(axis="y", labelsize=style.ytick_fontsize, colors=tick_color) if style.tight_layout: plt.tight_layout() plt.draw()
[docs] def save(self, filename: str) -> None: # thin wrapper """Save current figure to disk. Args: filename: Path to save the figure (e.g., 'circuit.png'). """ self.ax.figure.savefig(filename, bbox_inches="tight") # type: ignore[union-attr]
@staticmethod def _make_axes(dpi: int, style: ScheduleStyle) -> plt.Axes: """ Create a new figure and axes with the given DPI. Args: style: Optional style configuration (for DPI). Returns: A newly created Matplotlib Axes. """ _, ax = plt.subplots(figsize=style.figsize, dpi=style.dpi, facecolor=style.theme.background) return ax