From 90d092e9396c4c0c305c7d8a77f82fd6eb1e1b3c Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Thu, 27 Feb 2025 17:37:11 +0800 Subject: [PATCH 1/9] fix(mapping): Update return types and SWAP gate handling --- cqlib/mapping/cir_dg.py | 20 ++++---------------- cqlib/mapping/mapping.py | 30 ++++++++++++++++-------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/cqlib/mapping/cir_dg.py b/cqlib/mapping/cir_dg.py index 608e42a..dbd5b3b 100644 --- a/cqlib/mapping/cir_dg.py +++ b/cqlib/mapping/cir_dg.py @@ -69,9 +69,6 @@ def add_gate_to_cir(cir, gate): if name == 'u1': cir.add_u1(q[0]) - - - class xor_tensor(): def __init__(self, q): self.name = 'xor' @@ -368,26 +365,17 @@ class DG(DiGraph): gates = self.get_node_gates(node) for gate in gates: if gate[0].lower() == 'swap': - cir_qcis.append(Instruction(name='y2m', num_qubits=1, params=[]), gate[1][1]) - cir_qcis.append(Instruction(name='cz', num_qubits=2, params=[]), gate[1]) - cir_qcis.append(Instruction(name='y2p', num_qubits=1, params=[]), gate[1][1]) - - cir_qcis.append(Instruction(name='y2m', num_qubits=1, params=[]), gate[1][0]) - cir_qcis.append(Instruction(name='cz', num_qubits=2, params=[]), tuple(reversed(gate[1]))) - cir_qcis.append(Instruction(name='y2p', num_qubits=1, params=[]), gate[1][0]) - - cir_qcis.append(Instruction(name='y2m', num_qubits=1, params=[]), gate[1][1]) - cir_qcis.append(Instruction(name='cz', num_qubits=2, params=[]), gate[1]) - cir_qcis.append(Instruction(name='y2p', num_qubits=1, params=[]), gate[1][1]) + instruction_temp = Instruction(name=gate[0].upper(), num_qubits=2, params=[]) + cir_qcis.append(instruction_temp, gate[1]) else: # format parameters param_format = [format_para(para_temp) for para_temp in gate[2]] - instruction_temp = Instruction(name=gate[0], + instruction_temp = Instruction(name=gate[0].upper(), num_qubits=len(gate[1]), params=param_format) cir_qcis.append(instruction_temp, gate[1]) if add_barrier: - cir_qcis.barrier(cir_qcis.qubits) + cir_qcis.barrier(*cir_qcis.qubits) circuit.execute_front_layer() return cir_qcis diff --git a/cqlib/mapping/mapping.py b/cqlib/mapping/mapping.py index f302760..3e39e55 100644 --- a/cqlib/mapping/mapping.py +++ b/cqlib/mapping/mapping.py @@ -16,7 +16,7 @@ from .cir_dg import DG import networkx as nx import numpy as np from .monte_carlo_tree import MCTree -from cqlib import BasePlatform +from cqlib import BasePlatform, Circuit method_init_mapping = 'Topgraph' @@ -130,18 +130,19 @@ def transpile_qcis(qcis_str, platform: BasePlatform, initial_layout=None, object Returns ------- - qasm_transpiled : string - qasm string after transpilation - layout : dict - mapping from virtual to physical qubit - e.g.: - {0:1, 1:2, 2:3, 3:4, 4:0} - swap_mapping : dict - mapping from initial physical qubit to final physical qubit after a series of swaps - e.g.: - {0:1, 1:2, 2:3, 3:4, 4:0} - mapping_virtual_to_final: - mapping from virtual to final physical qubit + res: list + circuit : Circuit + New circuit object. + layout : dict + mapping from virtual to physical qubit + e.g.: + {0:1, 1:2, 2:3, 3:4, 4:0} + swap_mapping : dict + mapping from initial physical qubit to final physical qubit after a series of swaps + e.g.: + {0:1, 1:2, 2:3, 3:4, 4:0} + mapping_virtual_to_final: + mapping from virtual to final physical qubit raised: TranspileError: @@ -212,4 +213,5 @@ def transpile_qcis(qcis_str, platform: BasePlatform, initial_layout=None, object r_qubit_mapping = {v: k for k, v in qubit_mapping.items()} initial_layout = {r_qubit_mapping[k]: v for k, v in initial_layout.items()} mapping_virtual_to_final = {r_qubit_mapping[k]: v for k, v in mapping_virtual_to_final.items()} - return qcis_circuit, initial_layout, swap_mapping, mapping_virtual_to_final + c = Circuit.load(qcis_circuit.as_str().upper()) + return c, initial_layout, swap_mapping, mapping_virtual_to_final -- Gitee From 3347a2c12c7634413d89b4da8d33c98041267ddd Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Thu, 27 Feb 2025 17:41:39 +0800 Subject: [PATCH 2/9] test(tests/mapping): Add transpile test case --- tests/mapping/test_transpile.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/mapping/test_transpile.py diff --git a/tests/mapping/test_transpile.py b/tests/mapping/test_transpile.py new file mode 100644 index 0000000..84cd886 --- /dev/null +++ b/tests/mapping/test_transpile.py @@ -0,0 +1,53 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. + +""" +transpile testcase +""" + +import os +import warnings + +import pytest + +from cqlib.mapping import transpile_qcis +from cqlib import TianYanPlatform, Circuit + +warnings.filterwarnings("ignore") + + +@pytest.fixture +def pf(): + """ new TianYanPlatform object""" + login_key = os.getenv('LOGIN_KEY', '') + quantum_machine = os.getenv('QUANTUM_MACHINE', 'tianyan176-2') + return TianYanPlatform(login_key=login_key, machine_name=quantum_machine) + + +# pylint: disable=redefined-outer-name +def test_base(pf): + """ test base """ + circuit = Circuit(3) + circuit.cz(0, 1) + circuit.cz(0, 2) + circuit.cz(1, 2) + circuit.measure_all() + + topo_check = pf.qcis_check_regular(circuit.qcis) + assert topo_check is False + + result = transpile_qcis(circuit.as_str(), pf) + new_circuit = result[0] + + topo_check = pf.qcis_check_regular(new_circuit.qcis) + assert topo_check is True -- Gitee From 008068b90437e22db11499d61cccc85dbf5af8f5 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Fri, 28 Feb 2025 11:58:56 +0800 Subject: [PATCH 3/9] feat(cqlib/visualization/circuit/mpl): Add Matplotlib quantum circuit drawer --- cqlib/visualization/circuit/__init__.py | 5 +- cqlib/visualization/circuit/mpl.py | 356 ++++++++++++++++++++++++ 2 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 cqlib/visualization/circuit/mpl.py diff --git a/cqlib/visualization/circuit/__init__.py b/cqlib/visualization/circuit/__init__.py index fe04304..9617dff 100644 --- a/cqlib/visualization/circuit/__init__.py +++ b/cqlib/visualization/circuit/__init__.py @@ -16,8 +16,9 @@ Quantum circuit visualization """ from .text import draw_text, TextDrawer +from .mpl import MatplotlibDrawer, draw_mpl __all__ = [ - 'draw_text', - 'TextDrawer' + 'draw_text', 'TextDrawer', + 'draw_mpl', 'MatplotlibDrawer', ] diff --git a/cqlib/visualization/circuit/mpl.py b/cqlib/visualization/circuit/mpl.py new file mode 100644 index 0000000..24d6328 --- /dev/null +++ b/cqlib/visualization/circuit/mpl.py @@ -0,0 +1,356 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. + +""" +Matplotlib quantum circuit drawer module +""" + +import logging +from collections.abc import Sequence +from enum import IntEnum + +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from matplotlib.patches import Rectangle + +from cqlib.circuits.bit import Bit +from cqlib.circuits.circuit import Circuit +from cqlib.circuits.qubit import Qubit +from cqlib.circuits.instruction_data import InstructionData +from cqlib.circuits.parameter import Parameter +from cqlib.circuits.barrier import Barrier +from cqlib.circuits.gates import SWAP, CZ +from cqlib.circuits.gates.gate import ControlledGate +from cqlib.exceptions import CqlibError + +from .base import BaseDrawer, BoxChar + +logger = logging.getLogger('cqlib.vis') + + +class ZOrder(IntEnum): + """ + Defines z-order layers for rendering circuit components. + + Ensures proper layering of visual elements (e.g., lines under boxes). + + Members: + LINE (int): Layer for quantum wires and connectors (z=10) + BOX (int): Layer for gate background boxes (z=20) + GATE (int): Layer for gate symbols and text (z=30) + """ + LINE = 10 + BOX = 20 + GATE = 30 + + +# pylint: disable=too-many-instance-attributes +class MatplotlibDrawer(BaseDrawer): + """ + Quantum circuit visualizer using Matplotlib. + + Renders circuit diagrams with customizable styling and layout. Supports + multi-qubit gates, parameterized operations, and classical registers. + """ + figure_styles = { + 'figure_color': 'white', + 'axes_color': 'white', + 'show_axis': '0', + 'margin_top': 0.88, + 'margin_bottom': 0.11, + 'margin_left': 0.125, + 'margin_right': 0.9, + 'moment_width': 0.3, + 'gate_width': 1, + 'gate_height': 1.2, + } + gate_styles = {} + + # pylint: disable=too-many-arguments,too-many-positional-arguments + def __init__( + self, + circuit: Circuit, + qubit_order: list[int | Qubit] = None, + figure_styles: dict[str, str | bool | float | tuple[float | int]] = None, + gate_styles: dict[str, str | bool | float | tuple[float | int]] = None, + title: str = None, + fonts: str | list[str] = None, + filename: str = None + ): + """ + + Args: + circuit (Circuit): Quantum circuit to visualize. + qubit_order (list[int | Qubit], optional): Custom display order for qubits. + Defaults to circuit's natural ordering. + figure_styles (dict): Overrides for default figure styles (colors, margins, etc.). + gate_styles (dict): Overrides for default gate rendering styles. + title (str, optional): Title displayed above the circuit. + fonts (str | list[str], optional): Font family/families for text rendering. + filename (str, optional): + """ + super().__init__(circuit, qubit_order) + if figure_styles: + self.figure_styles.update(figure_styles) + if gate_styles: + self.gate_styles.update(gate_styles) + if fonts and isinstance(fonts, str): + fonts = [fonts] + self._fonts = fonts + self._title = title + self._filename = filename + self._fig = None + self._ax = None + self._fig_rows = 1 + self._line_width = 1 + self._current_x = 0 + self.qubit_mapping = {q: i * 2 for i, q in enumerate(self.sorted_qubits)} + + def drawer(self) -> Figure: + """ + Generates complete circuit visualization. + + Processes circuit structure, computes layout, and renders all components. + + Returns: + Figure: Matplotlib figure containing circuit diagram + """ + lines = self._make_lines() + + self._fig, self._ax = self.setup_figure() + for i, qubit in enumerate(self.sorted_qubits): + self._ax.text(-0.6, i * 2, str(qubit), ha='right', va='center', + fontsize=12, color='black') + self._ax.hlines(i * 2, xmin=-self.figure_styles['moment_width'], + xmax=self._line_width, color='gray', linewidth=1) + self._current_x = self.figure_styles['moment_width'] + for moment in lines: + for column in moment: + start = self._current_x + for col in column: + self.draw_column(col) + self._current_x += self.figure_styles['gate_width'] + if len(column) > 1: + self._ax.axvspan( + xmin=start - self.figure_styles['gate_width'] / 2 - 0.1, + xmax=self._current_x - self.figure_styles['gate_width'] / 2 + 0.1, + color='lightgray', alpha=0.2, zorder=0 + ) + self._current_x += self.figure_styles['moment_width'] + if self._filename: + self._fig.savefig(self._filename) + return self._fig + + def _make_lines(self): + """ + Organizes circuit moments into columns for rendering. Internal layout logic. + """ + lines = [] + + for moment in self.generate_moment(): + moment_lines = [] + columns = self.moment_to_columns(moment) + self._line_width += (self.figure_styles['moment_width'] + + len(columns) * self.figure_styles['gate_width']) + moment_lines.append(columns) + lines.append(moment_lines) + return lines + + def setup_figure(self) -> tuple[Figure, Axes]: + """ + Configures Matplotlib figure and axes with specified styles. + + Returns: + tuple[Figure, Axes]: Configured figure and axes objects. + """ + fig, ax = plt.subplots(figsize=(self._line_width, len(self.sorted_qubits))) + fig.set_facecolor(self.figure_styles['figure_color']) + ax.set_facecolor(self.figure_styles['axes_color']) + plt.subplots_adjust( + left=self.figure_styles['margin_left'], + right=self.figure_styles['margin_right'], + top=self.figure_styles['margin_top'], + bottom=self.figure_styles['margin_bottom'] + ) + ax.set_xlim(-1, self._line_width - 1) + ax.set_ylim(-1, len(self.sorted_qubits) * 2 - 1) + ax.invert_yaxis() + if self._title: + ax.set_title(self._title) + + ax.axis(self.figure_styles['show_axis'] in [1, True, 'true', '1']) + if self._fonts: + plt.rcParams['font.sans-serif'] = self._fonts + plt.rcParams['axes.unicode_minus'] = False + + return fig, ax + + def draw_column(self, column: list[InstructionData]): + """ + Renders a single column of instructions onto the axes. + + Args: + column (list[InstructionData]): Group of gates to draw vertically aligned. + """ + for ins in column: + if len(ins.qubits) == 1: + self._draw_single_gate(ins) + else: + self._draw_multi_gate(ins) + + def _draw_single_gate(self, instruction_data: InstructionData): + """ + Renders single-qubit gate with parameter display. + + Args: + instruction_data (InstructionData): Gate and associated qubit(s). + """ + qubit_no = self.qubit_line_no(instruction_data.qubits[0]) + rect = Rectangle( + (self._current_x - self.figure_styles['gate_width'] / 2, + qubit_no - self.figure_styles['gate_height'] / 2), + self.figure_styles['gate_width'], self.figure_styles['gate_height'], + fc='white', ec='gray', lw=2, zorder=ZOrder.BOX + ) + self._ax.add_patch(rect) + ins = instruction_data.instruction + if ins.params: + t = [] + for p in ins.params: + if isinstance(p, Parameter): + t.append(str(p.symbol)) + else: + t.append(str(p)) + t = f"{ins.name}\n({','.join(t)})" + else: + t = ins.name + self._ax.text(self._current_x, qubit_no, t, ha='center', va='center', + fontsize=12, zorder=ZOrder.GATE) + + def _draw_measure(self, qubit: Qubit): + pass + + def _draw_multi_gate(self, instruction_data: InstructionData): + """ + Handles multi-qubit gates (SWAP, CZ, controlled gates) by delegating to specific methods. + """ + qs = instruction_data.qubits + instruction = instruction_data.instruction + if isinstance(instruction, SWAP): + self._draw_swap(qs) + elif isinstance(instruction, CZ): + self._draw_cz(qs) + elif isinstance(instruction, Barrier): + self._draw_barrier(qs) + elif isinstance(instruction, ControlledGate): + self._draw_controlled_gate(instruction_data) + + def _draw_swap(self, qs: Sequence[Bit]): + for q in qs: + self._ax.plot( + [self._current_x - 0.3, self._current_x + 0.3], + [self.qubit_line_no(q) + 0.3, self.qubit_line_no(q) - 0.3], + color='gray', + linewidth=1.5, + zorder=ZOrder.GATE, + ) + self._ax.plot( + [self._current_x - 0.3, self._current_x + 0.3], + [self.qubit_line_no(q) - 0.3, self.qubit_line_no(q) + 0.3], + color='gray', + linewidth=1.5, + zorder=ZOrder.GATE, + ) + + self._ax.vlines( + self._current_x, + ymin=self.qubit_line_no(qs[0]), + ymax=self.qubit_line_no(qs[1]), + color='gray', linewidth=1 + ) + + def _draw_barrier(self, qs: Sequence[Bit]): + for q in qs: + self._ax.vlines( + self._current_x, + ymin=self.qubit_line_no(q) - 1, + ymax=self.qubit_line_no(q) + 1, + linestyles='dashed', + color='gray', + linewidth=2 + ) + + def _draw_cz(self, qs: Sequence[Bit]): + for q in qs: + self._ax.text( + self._current_x, + self.qubit_line_no(q), + BoxChar.DOT.value, + ha='center', + va='center', + fontsize=12, + zorder=ZOrder.GATE, + ) + self._ax.vlines( + self._current_x, + ymin=self.qubit_line_no(qs[0]), + ymax=self.qubit_line_no(qs[1]), + color='gray', linewidth=1 + ) + + def _draw_controlled_gate(self, instruction_data: InstructionData): + """ + Renders controlled gates with connection lines. + + Args: + instruction_data: Must contain a ControlledGate instance + """ + if not isinstance(instruction_data.instruction, ControlledGate): + raise CqlibError(f"Invalid instruction type for controlled gate. {instruction_data}") + qubit_nos = [] + for i, qubit in enumerate(instruction_data.qubits): + qubit_nos.append(self.qubit_line_no(qubit)) + if i in instruction_data.instruction.control_index: + self._ax.text( + self._current_x, + self.qubit_line_no(qubit), + BoxChar.DOT.value, + ha='center', + va='center', + fontsize=12, + zorder=ZOrder.GATE, + ) + else: + self._draw_single_gate( + InstructionData(instruction_data.instruction.base_gate, [qubit])) + self._ax.vlines(self._current_x, ymin=min(qubit_nos), ymax=max(qubit_nos), + color='gray', linewidth=1, zorder=ZOrder.LINE) + + +def draw_mpl( + circuit: Circuit, + title: str = None, + filename: str = None +) -> Figure: + """ + Quick visualization entry point. + Args: + circuit(Circuit): Quantum circuit to draw + title (str): Optional diagram title + filename (str): + + Returns: + Figure: Ready-to-display circuit visualization + """ + return MatplotlibDrawer(circuit, title=title, filename=filename).drawer() -- Gitee From 3513309e7c72922a69e92116ce4cbe3a58fc00bc Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Fri, 28 Feb 2025 14:53:20 +0800 Subject: [PATCH 4/9] fix(pulse_circuit): Add type checks for qubit and coupler_qubit --- cqlib/pulse/pulse_circuit.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cqlib/pulse/pulse_circuit.py b/cqlib/pulse/pulse_circuit.py index 02fbac5..8aec60b 100644 --- a/cqlib/pulse/pulse_circuit.py +++ b/cqlib/pulse/pulse_circuit.py @@ -94,6 +94,9 @@ class PulseCircuit(Circuit): """ if isinstance(couple_qubit, int): couple_qubit = CouplerQubit(couple_qubit) + if not isinstance(couple_qubit, CouplerQubit): + raise CqlibError(f"Invalid type for coupler qubit. Expected CouplerQubit or int, " + f"got {type(couple_qubit).__name__} instead.") self.append_pulse(G(length, coupling_strength), couple_qubit) def pxy(self, qubit: Qubit | int, waveform: Waveform): @@ -106,6 +109,9 @@ class PulseCircuit(Circuit): """ if isinstance(qubit, int): qubit = Qubit(qubit) + if not isinstance(qubit, Qubit): + raise CqlibError(f"Invalid type for qubit. Expected Qubit or int, " + f"got {type(qubit).__name__} instead.") self.append_pulse(PXY(waveform=waveform), qubit) def pz(self, qubit: CouplerQubit | Qubit, waveform: Waveform): -- Gitee From 0485b9c7cb10a9b2f1338ace70cd73fbc9f7a639 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Fri, 28 Feb 2025 14:53:54 +0800 Subject: [PATCH 5/9] refactor(cqlib/pulse/waveform.py): Add waveform type defaults to dataclasses --- cqlib/pulse/waveform.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cqlib/pulse/waveform.py b/cqlib/pulse/waveform.py index 9ecd8f6..c766d36 100644 --- a/cqlib/pulse/waveform.py +++ b/cqlib/pulse/waveform.py @@ -20,7 +20,7 @@ interface to configure and validate waveform parameters across different waveform types. """ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import IntEnum, unique from math import pi @@ -45,7 +45,7 @@ class WaveformType(IntEnum): NUMERIC = 3 -@dataclass +@dataclass(kw_only=True) class Waveform: """ Base dataclass for waveform parameter validation and string representation. @@ -189,6 +189,7 @@ class Waveform: @dataclass class CosineWaveform(Waveform): """Configuration parameters for cosine-shaped waveforms.""" + waveform: WaveformType = field(default=WaveformType.COSINE) def __post_init__(self): self.validate() @@ -202,6 +203,7 @@ class FlattopWaveform(Waveform): Additional Attributes: edge (int): Length of rising/falling edges (must be < total length/2). """ + waveform: WaveformType = field(default=WaveformType.FLATTOP) edge: int = None def __post_init__(self): @@ -230,6 +232,7 @@ class SlepianWaveform(Waveform): - lam2: - lam3: """ + waveform: WaveformType = field(default=WaveformType.SLEPIAN) thf: float = None thi: float = None lam2: float = None @@ -251,6 +254,7 @@ class NumericWaveform(Waveform): - params: List of floats, Values must be in [0.0, 1.0], Minimum length of 3 samples; """ + waveform: WaveformType = field(default=WaveformType.NUMERIC) data_list: list[float] = None def __post_init__(self): -- Gitee From 2b3f9c6a35795d15eeb1ff9b506488f621139878 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Tue, 4 Mar 2025 17:44:10 +0800 Subject: [PATCH 6/9] feat(cqlib/visualization): Add and update circuit drawer parameters and styles --- cqlib/visualization/__init__.py | 9 +- cqlib/visualization/circuit/base.py | 18 +- cqlib/visualization/circuit/mpl.py | 299 +++++++++++++----- cqlib/visualization/circuit/style.py | 79 +++++ .../visualization/circuit/styles/default.json | 202 ++++++++++++ cqlib/visualization/circuit/styles/gray.json | 14 + cqlib/visualization/circuit/text.py | 10 +- 7 files changed, 538 insertions(+), 93 deletions(-) create mode 100644 cqlib/visualization/circuit/style.py create mode 100644 cqlib/visualization/circuit/styles/default.json create mode 100644 cqlib/visualization/circuit/styles/gray.json diff --git a/cqlib/visualization/__init__.py b/cqlib/visualization/__init__.py index 361407e..166d24c 100644 --- a/cqlib/visualization/__init__.py +++ b/cqlib/visualization/__init__.py @@ -15,7 +15,12 @@ visualization module defines methods to visualize qcis circuits and plotting experiment results. """ -# from .circuit import draw_circuit -from .circuit import draw_text +from .circuit import draw_text, draw_mpl, TextDrawer, MatplotlibDrawer from .gplot import draw_gplot from .result import draw_histogram + +__all__ = [ + 'draw_text', 'draw_mpl', 'TextDrawer', 'MatplotlibDrawer', + 'draw_gplot', + 'draw_histogram' +] diff --git a/cqlib/visualization/circuit/base.py b/cqlib/visualization/circuit/base.py index 3a90389..d3239d3 100644 --- a/cqlib/visualization/circuit/base.py +++ b/cqlib/visualization/circuit/base.py @@ -62,8 +62,22 @@ class BaseDrawer(ABC): """ Abstract Quantum circuit Drawer. """ + MIN_LINE_WIDTH = 10 + DEFAULT_LINE_WIDTH = 30 + + def __init__( + self, + circuit: Circuit, + qubit_order: list[int | Bit] | None = None, + ): + """ + Initialization Parameters - def __init__(self, circuit: Circuit, qubit_order: list[int | Bit] = None): + Args: + circuit(Circuit): Quantum circuit to be visualized + qubit_order(list[int | Bit] | None): Optional list specifying the display + order of qubits + """ self.circuit = circuit self.qubit_order = qubit_order self.sorted_qubits: list[Bit] = [] @@ -165,5 +179,5 @@ class BaseDrawer(ABC): """ start draw lines""" @abstractmethod - def draw_column(self, column: list[InstructionData]) -> list[list[str]]: + def draw_column(self, column) -> list[list[str]]: """ draw column lines""" diff --git a/cqlib/visualization/circuit/mpl.py b/cqlib/visualization/circuit/mpl.py index 24d6328..64dfc9a 100644 --- a/cqlib/visualization/circuit/mpl.py +++ b/cqlib/visualization/circuit/mpl.py @@ -22,19 +22,21 @@ from enum import IntEnum from matplotlib import pyplot as plt from matplotlib.axes import Axes from matplotlib.figure import Figure -from matplotlib.patches import Rectangle +from matplotlib.patches import Rectangle, Arc +from cqlib.circuits.barrier import Barrier from cqlib.circuits.bit import Bit from cqlib.circuits.circuit import Circuit -from cqlib.circuits.qubit import Qubit from cqlib.circuits.instruction_data import InstructionData -from cqlib.circuits.parameter import Parameter -from cqlib.circuits.barrier import Barrier from cqlib.circuits.gates import SWAP, CZ from cqlib.circuits.gates.gate import ControlledGate +from cqlib.circuits.measure import Measure +from cqlib.circuits.parameter import Parameter +from cqlib.circuits.qubit import Qubit from cqlib.exceptions import CqlibError from .base import BaseDrawer, BoxChar +from .style import Style logger = logging.getLogger('cqlib.vis') @@ -72,39 +74,41 @@ class MatplotlibDrawer(BaseDrawer): 'margin_left': 0.125, 'margin_right': 0.9, 'moment_width': 0.3, - 'gate_width': 1, + 'gate_width': 0.8, 'gate_height': 1.2, } - gate_styles = {} # pylint: disable=too-many-arguments,too-many-positional-arguments def __init__( self, circuit: Circuit, - qubit_order: list[int | Qubit] = None, - figure_styles: dict[str, str | bool | float | tuple[float | int]] = None, - gate_styles: dict[str, str | bool | float | tuple[float | int]] = None, - title: str = None, - fonts: str | list[str] = None, - filename: str = None + qubit_order: list[int | Qubit] | None = None, + figure_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, + gate_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, + title: str | None = None, + fonts: str | list[str] | None = None, + filename: str | None = None, + style: str = 'default', ): """ + Initialization Parameters Args: circuit (Circuit): Quantum circuit to visualize. - qubit_order (list[int | Qubit], optional): Custom display order for qubits. + qubit_order (list[int | Qubit] | None): Custom display order for qubits. Defaults to circuit's natural ordering. - figure_styles (dict): Overrides for default figure styles (colors, margins, etc.). - gate_styles (dict): Overrides for default gate rendering styles. - title (str, optional): Title displayed above the circuit. - fonts (str | list[str], optional): Font family/families for text rendering. - filename (str, optional): + figure_styles (dict | None): Overrides for default figure styles. + gate_styles (dict | None): Overrides for default gate rendering styles. + title (str | None): Title displayed above the circuit. + fonts (str | list[str] | None): Font family/families for text rendering. + filename (str, optional): Optional filename to save the diagram to. """ super().__init__(circuit, qubit_order) + + self.gate_style = Style(style, gate_styles) if figure_styles: self.figure_styles.update(figure_styles) - if gate_styles: - self.gate_styles.update(gate_styles) + if fonts and isinstance(fonts, str): fonts = [fonts] self._fonts = fonts @@ -112,9 +116,11 @@ class MatplotlibDrawer(BaseDrawer): self._filename = filename self._fig = None self._ax = None - self._fig_rows = 1 - self._line_width = 1 + self._real_line_width = 1 self._current_x = 0 + self._current_col_index = 0 + self._current_moment_index = 0 + self.column_width = [] self.qubit_mapping = {q: i * 2 for i, q in enumerate(self.sorted_qubits)} def drawer(self) -> Figure: @@ -133,23 +139,28 @@ class MatplotlibDrawer(BaseDrawer): self._ax.text(-0.6, i * 2, str(qubit), ha='right', va='center', fontsize=12, color='black') self._ax.hlines(i * 2, xmin=-self.figure_styles['moment_width'], - xmax=self._line_width, color='gray', linewidth=1) - self._current_x = self.figure_styles['moment_width'] + xmax=self._real_line_width, color='gray', linewidth=1) + self._current_x = 0 for moment in lines: - for column in moment: - start = self._current_x - for col in column: - self.draw_column(col) - self._current_x += self.figure_styles['gate_width'] - if len(column) > 1: - self._ax.axvspan( - xmin=start - self.figure_styles['gate_width'] / 2 - 0.1, - xmax=self._current_x - self.figure_styles['gate_width'] / 2 + 0.1, - color='lightgray', alpha=0.2, zorder=0 - ) + start = self._current_x + for column in moment['moment']: + column_width = column['width'] + self._current_x += column_width / 2 + self.draw_column(column['column']) + self._current_x += column_width / 2 + # Draw light gray background area when multiple parallel gate + # columns exist in the same moment + if len(moment['moment']) > 1: + self._ax.axvspan( + xmin=start - 0.1, + xmax=self._current_x + 0.1, + color='lightgray', + alpha=0.2, + zorder=0 + ) self._current_x += self.figure_styles['moment_width'] if self._filename: - self._fig.savefig(self._filename) + self._fig.savefig(self._filename, transparent=None, dpi='figure', ) return self._fig def _make_lines(self): @@ -159,12 +170,32 @@ class MatplotlibDrawer(BaseDrawer): lines = [] for moment in self.generate_moment(): - moment_lines = [] - columns = self.moment_to_columns(moment) - self._line_width += (self.figure_styles['moment_width'] - + len(columns) * self.figure_styles['gate_width']) - moment_lines.append(columns) - lines.append(moment_lines) + columns = [] + max_width = 0 + for column in self.moment_to_columns(moment): + col_width = 0 + instructions = [] + for ins in column: + ins_width = self.figure_styles['gate_width'] + if ins.instruction.params: + t = self._params_str(ins) + ins_width = max(ins_width, len(t) / 5.0 * self.figure_styles['gate_width']) + instructions.append({ + 'width': ins_width, + 'instruction': ins + }) + col_width = max(ins_width, col_width) + self._real_line_width += col_width + columns.append({ + 'width': col_width, + 'column': instructions + }) + max_width += col_width + self._real_line_width += self.figure_styles['moment_width'] + lines.append({ + 'width': max_width, + 'moment': columns + }) return lines def setup_figure(self) -> tuple[Figure, Axes]: @@ -174,7 +205,7 @@ class MatplotlibDrawer(BaseDrawer): Returns: tuple[Figure, Axes]: Configured figure and axes objects. """ - fig, ax = plt.subplots(figsize=(self._line_width, len(self.sorted_qubits))) + fig, ax = plt.subplots(figsize=(self._real_line_width, len(self.sorted_qubits))) fig.set_facecolor(self.figure_styles['figure_color']) ax.set_facecolor(self.figure_styles['axes_color']) plt.subplots_adjust( @@ -183,7 +214,7 @@ class MatplotlibDrawer(BaseDrawer): top=self.figure_styles['margin_top'], bottom=self.figure_styles['margin_bottom'] ) - ax.set_xlim(-1, self._line_width - 1) + ax.set_xlim(-1, self._real_line_width - 1) ax.set_ylim(-1, len(self.sorted_qubits) * 2 - 1) ax.invert_yaxis() if self._title: @@ -196,20 +227,27 @@ class MatplotlibDrawer(BaseDrawer): return fig, ax - def draw_column(self, column: list[InstructionData]): + def draw_column(self, column: list[dict]): """ Renders a single column of instructions onto the axes. Args: column (list[InstructionData]): Group of gates to draw vertically aligned. """ - for ins in column: + for item in column: + ins_width = item['width'] + ins = item['instruction'] if len(ins.qubits) == 1: - self._draw_single_gate(ins) + self._draw_single_gate(ins, ins_width) else: - self._draw_multi_gate(ins) + self._draw_multi_gate(ins, ins_width) - def _draw_single_gate(self, instruction_data: InstructionData): + def _draw_single_gate( + self, + instruction_data: InstructionData, + width: float, + gate_style=None + ): """ Renders single-qubit gate with parameter display. @@ -217,33 +255,78 @@ class MatplotlibDrawer(BaseDrawer): instruction_data (InstructionData): Gate and associated qubit(s). """ qubit_no = self.qubit_line_no(instruction_data.qubits[0]) + if gate_style: + gs = gate_style + else: + gs = self.gate_style[instruction_data.instruction.name] + rect = Rectangle( - (self._current_x - self.figure_styles['gate_width'] / 2, - qubit_no - self.figure_styles['gate_height'] / 2), - self.figure_styles['gate_width'], self.figure_styles['gate_height'], - fc='white', ec='gray', lw=2, zorder=ZOrder.BOX + (self._current_x - width / 2, qubit_no - self.figure_styles['gate_height'] / 2), + width, + self.figure_styles['gate_height'], + fc=gs['background_color'], + ec=gs['border_color'], + lw=2, + zorder=ZOrder.BOX ) self._ax.add_patch(rect) ins = instruction_data.instruction + + if isinstance(ins, Measure): + self._draw_measure(qubit_no) + return if ins.params: - t = [] - for p in ins.params: - if isinstance(p, Parameter): - t.append(str(p.symbol)) - else: - t.append(str(p)) - t = f"{ins.name}\n({','.join(t)})" + t = f"{ins.name}\n{self._params_str(instruction_data)}" else: t = ins.name - self._ax.text(self._current_x, qubit_no, t, ha='center', va='center', - fontsize=12, zorder=ZOrder.GATE) - def _draw_measure(self, qubit: Qubit): - pass + self._ax.text( + self._current_x, + qubit_no, + t, + ha='center', + va='center', + fontsize=gs['font_size'], + color=gs['text_color'], + zorder=ZOrder.GATE + ) + + def _draw_measure(self, qubit_no: int): + """ + Draws a measurement symbol on the circuit diagram. - def _draw_multi_gate(self, instruction_data: InstructionData): + Args: + qubit_no (int): Line number for the qubit where measurement is performed. """ - Handles multi-qubit gates (SWAP, CZ, controlled gates) by delegating to specific methods. + gs = self.gate_style['M'] + self._ax.add_patch(Arc( + (self._current_x, qubit_no + 0.3 * self.figure_styles['gate_height']), + width=0.5 * self.figure_styles['gate_width'], + height=0.8 * self.figure_styles['gate_width'], + theta1=180, + theta2=0, + lw=2, + color=gs['text_color'], + zorder=ZOrder.GATE, + )) + + self._ax.arrow( + x=self._current_x - 0.1 * self.figure_styles['gate_width'], + y=qubit_no + 0.5, + dx=0.2 * self.figure_styles['gate_width'], + dy=-0.6 * self.figure_styles['gate_height'], + head_width=self.figure_styles['gate_width'] / 9.0, + zorder=ZOrder.GATE, + color=gs['text_color'], + length_includes_head=False + ) + + def _draw_multi_gate(self, instruction_data: InstructionData, width: float): + """ + Draws multi-qubit gates with connection lines. + Args: + instruction_data (InstructionData): Gate and associated qubits. + width (float): Width of the gate column. """ qs = instruction_data.qubits instruction = instruction_data.instruction @@ -254,21 +337,22 @@ class MatplotlibDrawer(BaseDrawer): elif isinstance(instruction, Barrier): self._draw_barrier(qs) elif isinstance(instruction, ControlledGate): - self._draw_controlled_gate(instruction_data) + self._draw_controlled_gate(instruction_data, width) def _draw_swap(self, qs: Sequence[Bit]): + gs = self.gate_style['SWAP'] for q in qs: self._ax.plot( [self._current_x - 0.3, self._current_x + 0.3], [self.qubit_line_no(q) + 0.3, self.qubit_line_no(q) - 0.3], - color='gray', - linewidth=1.5, + color=gs['text_color'], + linewidth=gs['line_width'], zorder=ZOrder.GATE, ) self._ax.plot( [self._current_x - 0.3, self._current_x + 0.3], [self.qubit_line_no(q) - 0.3, self.qubit_line_no(q) + 0.3], - color='gray', + color=gs['text_color'], linewidth=1.5, zorder=ZOrder.GATE, ) @@ -277,21 +361,24 @@ class MatplotlibDrawer(BaseDrawer): self._current_x, ymin=self.qubit_line_no(qs[0]), ymax=self.qubit_line_no(qs[1]), - color='gray', linewidth=1 + color=gs['line_color'], + linewidth=gs['line_width'] ) def _draw_barrier(self, qs: Sequence[Bit]): + gs = self.gate_style['B'] for q in qs: self._ax.vlines( self._current_x, ymin=self.qubit_line_no(q) - 1, ymax=self.qubit_line_no(q) + 1, linestyles='dashed', - color='gray', - linewidth=2 + color=gs['line_color'], + linewidth=gs['line_width'] ) def _draw_cz(self, qs: Sequence[Bit]): + gs = self.gate_style['CZ'] for q in qs: self._ax.text( self._current_x, @@ -299,6 +386,7 @@ class MatplotlibDrawer(BaseDrawer): BoxChar.DOT.value, ha='center', va='center', + color=gs['line_color'], fontsize=12, zorder=ZOrder.GATE, ) @@ -306,16 +394,18 @@ class MatplotlibDrawer(BaseDrawer): self._current_x, ymin=self.qubit_line_no(qs[0]), ymax=self.qubit_line_no(qs[1]), - color='gray', linewidth=1 + color=gs['line_color'], + linewidth=1 ) - def _draw_controlled_gate(self, instruction_data: InstructionData): + def _draw_controlled_gate(self, instruction_data: InstructionData, width: float): """ Renders controlled gates with connection lines. Args: instruction_data: Must contain a ControlledGate instance """ + gs = self.gate_style[instruction_data.instruction.name] if not isinstance(instruction_data.instruction, ControlledGate): raise CqlibError(f"Invalid instruction type for controlled gate. {instruction_data}") qubit_nos = [] @@ -328,29 +418,72 @@ class MatplotlibDrawer(BaseDrawer): BoxChar.DOT.value, ha='center', va='center', + color=gs['line_color'], fontsize=12, zorder=ZOrder.GATE, ) else: self._draw_single_gate( - InstructionData(instruction_data.instruction.base_gate, [qubit])) - self._ax.vlines(self._current_x, ymin=min(qubit_nos), ymax=max(qubit_nos), - color='gray', linewidth=1, zorder=ZOrder.LINE) + InstructionData(instruction_data.instruction.base_gate, [qubit]), + gate_style=gs, + width=width + ) + self._ax.vlines( + self._current_x, + ymin=min(qubit_nos), + ymax=max(qubit_nos), + color=gs['line_color'], + linewidth=1, + zorder=ZOrder.LINE + ) + + @staticmethod + def _params_str(ins: InstructionData): + if ins.instruction.params: + t = [] + for p in ins.instruction.params: + if isinstance(p, Parameter): + t.append(str(p.symbol)) + else: + t.append(str(p)) + t = ','.join(t) + else: + t = '' + return t +# pylint: disable=too-many-arguments, too-many-positional-arguments def draw_mpl( circuit: Circuit, - title: str = None, - filename: str = None + title: str | None = None, + filename: str | None = None, + qubit_order: list[int | Qubit] | None = None, + figure_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, + gate_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, + fonts: str | list[str] | None = None, ) -> Figure: """ Quick visualization entry point. + Args: circuit(Circuit): Quantum circuit to draw - title (str): Optional diagram title - filename (str): + title (str | None): Optional diagram title + filename (str | None): Optional filename to save the diagram + qubit_order (list[int | Qubit] | None): Custom display order for qubits. + Defaults to circuit's natural ordering. + figure_styles (dict | None): Overrides for default figure styles (colors, margins, etc.). + gate_styles (dict | None): Overrides for default gate rendering styles. + fonts (str | list[str] | None): Font family/families for text rendering. Returns: Figure: Ready-to-display circuit visualization """ - return MatplotlibDrawer(circuit, title=title, filename=filename).drawer() + return MatplotlibDrawer( + circuit, + title=title, + filename=filename, + figure_styles=figure_styles, + qubit_order=qubit_order, + gate_styles=gate_styles, + fonts=fonts + ).drawer() diff --git a/cqlib/visualization/circuit/style.py b/cqlib/visualization/circuit/style.py new file mode 100644 index 0000000..14c0246 --- /dev/null +++ b/cqlib/visualization/circuit/style.py @@ -0,0 +1,79 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. + +""" +Handles circuit visualization styles by loading from JSON files and allowing customizations. + +This module provides the Style class which manages gate display styles loaded from +JSON files in the styles directory, with optional runtime customizations. +""" +import json +from pathlib import Path + + +class Style: + """ + Manages visualization styles for quantum circuit gates. + + Styles are loaded from JSON files and can be customized at runtime. Each style + defines graphical parameters for different types of quantum gates. + + Args: + style: Name of the base style JSON file (without extension) + gate_styles: Optional dictionary of style overrides to merge with base style + + Attributes: + _style: Combined dictionary of all style parameters + """ + + def __init__(self, style: str, gate_styles: dict | None = None): + """Initialize style with base configuration and optional customizations.""" + self._style = self.load_style(style) + if gate_styles: + self._style.update(gate_styles) + + @staticmethod + def load_style(style): + """ + Load style configuration from JSON file in styles directory. + + Args: + style: Name of the style file (without .json extension) + + Returns: + dict: Style parameters loaded from JSON file + + Raises: + ValueError: If the specified style file doesn't exist + """ + path = Path(__file__).parent / "styles" / f'{style}.json' + if not path.exists(): + raise ValueError(f"Style {style} not found.") + with open(path, 'r', encoding='utf-8') as f: + gate_styles = json.load(f) + return gate_styles + + def __getitem__(self, item: str): + """Retrieve style parameters for a specific gate type. + + Falls back to 'default' style if gate-specific parameters aren't found. + + Args: + item: Name of the gate type to retrieve style for + + Returns: + dict: Style parameters for the requested gate type + """ + if item in self._style: + return self._style[item] + return self._style['default'] diff --git a/cqlib/visualization/circuit/styles/default.json b/cqlib/visualization/circuit/styles/default.json new file mode 100644 index 0000000..056341f --- /dev/null +++ b/cqlib/visualization/circuit/styles/default.json @@ -0,0 +1,202 @@ +{ + "default": { + "border_color": "None", + "background_color": "red", + "font_size": 12, + "text_color": "black", + "line_color": "black" + }, + "H": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "I": { + "border_color": "none", + "background_color": "#002D9C", + "font_size": 12, + "text_color": "white" + }, + "RX": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white" + }, + "CRX": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white", + "line_color": "#9F1853" + }, + "RXY": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white" + }, + "RY": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white" + }, + "CRY": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white", + "line_color": "#9F1853" + }, + "RZ": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "CRZ": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white", + "line_color": "#33B1FF" + }, + "S": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "SD": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "SWAP": { + "border_color": "none", + "background_color": "#002D9C", + "line_width": 2, + "text_color": "#002D9C", + "line_color": "#002D9C" + }, + "T": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "TD": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "U": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "white" + }, + "X": { + "border_color": "none", + "background_color": "#002D9C", + "font_size": 12, + "text_color": "white" + }, + "CX": { + "border_color": "none", + "background_color": "#002D9C", + "font_size": 12, + "text_color": "white", + "line_color": "#002D9C" + }, + "CCX": { + "border_color": "none", + "background_color": "#002D9C", + "font_size": 12, + "text_color": "white", + "line_color": "#002D9C" + }, + "X2P": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "X2M": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "XY": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "XY2P": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "XY2M": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "red" + }, + "Y": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "black" + }, + "Y2M": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "Y2P": { + "border_color": "none", + "background_color": "red", + "font_size": 12, + "text_color": "black" + }, + "CY": { + "border_color": "none", + "background_color": "#9F1853", + "font_size": 12, + "text_color": "black", + "line_color": "#9F1853" + }, + "Z": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white" + }, + "CZ": { + "border_color": "none", + "background_color": "#33B1FF", + "font_size": 12, + "text_color": "white", + "line_color": "#33B1FF" + }, + "M": { + "border_color": "gray", + "background_color": "white", + "text_color": "gray", + "line_color": "gray" + }, + "B": { + "border_color": "none", + "line_color": "blue", + "line_width": 5 + } +} \ No newline at end of file diff --git a/cqlib/visualization/circuit/styles/gray.json b/cqlib/visualization/circuit/styles/gray.json new file mode 100644 index 0000000..ed15beb --- /dev/null +++ b/cqlib/visualization/circuit/styles/gray.json @@ -0,0 +1,14 @@ +{ + "default": { + "border_color": "gray", + "background_color": "white", + "font_size": 12, + "text_color": "black", + "line_color": "black" + }, + "B": { + "border_color": "gray", + "line_color": "black", + "line_width": 5 + } +} \ No newline at end of file diff --git a/cqlib/visualization/circuit/text.py b/cqlib/visualization/circuit/text.py index 5bbaeec..6f17ce1 100644 --- a/cqlib/visualization/circuit/text.py +++ b/cqlib/visualization/circuit/text.py @@ -36,22 +36,20 @@ class TextDrawer(BaseDrawer): """ Renders quantum circuits as text diagrams using box-drawing characters """ - MIN_LINE_WIDTH = 10 - DEFAULT_LINE_WIDTH = 30 def __init__( self, circuit: Circuit, - qubit_order: list[int | Qubit] = None, - line_width: int = None, + qubit_order: list[int | Qubit] | None = None, + line_width: int | None = None, ): """ Initialization Parameters Args: circuit (Circuit): Quantum circuit to visualize - qubit_order (list[int | Qubit]): Custom ordering of qubits (optional) - line_width (int): Force specific output width (default: auto-detect terminal width) + qubit_order (list[int | Qubit] | None): Custom ordering of qubits. + line_width(int | None): Optional width of the visualization output """ super().__init__(circuit, qubit_order) self.line_width = line_width -- Gitee From 050f1faa2c26c6e3406a624a5879921910f42c9c Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Tue, 4 Mar 2025 17:46:01 +0800 Subject: [PATCH 7/9] chore(cqlib/visualization): remove deprecated circuit visualization module --- cqlib/visualization/circuit.py | 432 ---------------------- tests/visualization/test_visualization.py | 41 -- 2 files changed, 473 deletions(-) delete mode 100644 cqlib/visualization/circuit.py diff --git a/cqlib/visualization/circuit.py b/cqlib/visualization/circuit.py deleted file mode 100644 index a28c5e0..0000000 --- a/cqlib/visualization/circuit.py +++ /dev/null @@ -1,432 +0,0 @@ -# This code is part of cqlib. -# -# Copyright (C) 2024 China Telecom Quantum Group, QuantumCTek Co., Ltd., -# Center for Excellence in Quantum Information and Quantum Physics. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE 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 ..exceptions import VisualizationError -import os -import re -from typing import Optional -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.image as mpimg -from matplotlib.patches import Circle, Rectangle -from collections import defaultdict - -""" -Quantum circuit visualization -""" -LINE_GATE = {"CZ", "ISWAP", "SWAP", "CCZ", "CCX"} -PARAM_GATE = {"RZ", "RY", "RXY", "RX", "U3", "XY2M", "XY2P"} -MULTIPLE_QUBIT_GATE = {'B', 'M'} - - -def _processing_circuit(qcis_str): - """ - Parses and processes circuit strings in preparation for drawing - """ - draw_data = defaultdict(list) - qcis_list = qcis_str.upper().split("\n") - # 双比特门的x坐标集合, 在这个集合里面的x轴坐标除了已添加的双比特门,不能再添加其他门 - c_line_number = [] - for index, qcis in enumerate(qcis_list): - if c_line_number: - for key, value in draw_data.items(): - if len(value) - 1 < max(c_line_number): - for _ in range(max(c_line_number) - len(value)): - value.append(None) - value.append("") - if qcis: - try: - qcis_split = qcis.split(" ") - line_num = validate_and_extract_bit(qcis_split[1]) - bit_gate_name = qcis_split[0] - # 特殊处理,此类型属于连线类型的门 - if any( - substr in bit_gate_name - for substr in LINE_GATE - ): - # 此类型属于三个点连线 - if bit_gate_name in ["CCZ", "CCX"]: - line_num = validate_and_extract_bit(qcis_split[3]) - another_line_number = ( - ("".join(filter(str.isdigit, qcis_split[1]))) - + " " - + ("".join(filter(str.isdigit, qcis_split[2]))) - ) - else: # 此类型属于两个点连线 - line_num = validate_and_extract_bit(qcis_split[2]) - another_line_number = int( - "".join(filter(str.isdigit, qcis_split[1])) - ) - bit_gate_name = " ".join([bit_gate_name, str(another_line_number)]) - _multiple_gate_process( - draw_data=draw_data, - bit_gate_name=bit_gate_name, - line_num=line_num, - c_line_number=c_line_number, - ) - # 特殊处理 此类型属于带有参数的门 - elif any( - substr in bit_gate_name - for substr in PARAM_GATE - ): - if "RXY" in bit_gate_name: - bit_gate_name = " ".join( - [bit_gate_name, qcis_split[2], qcis_split[3]] - ) - elif "U3" in bit_gate_name: - bit_gate_name = " ".join( - [bit_gate_name, qcis_split[2], qcis_split[3], qcis_split[4]] - ) - else: - bit_gate_name = " ".join([bit_gate_name, qcis_split[2]]) - _normal_gate_process( - draw_data=draw_data, - bit_gate_name=bit_gate_name, - line_num=line_num, - c_line_number=c_line_number, - ) - elif bit_gate_name in MULTIPLE_QUBIT_GATE: - line_num_list = [validate_and_extract_bit(qcis_split[i]) for i in range(1, len(qcis_split))] - line_num_gate_index_dict = {} - for line_num in line_num_list: - gate_line_index = _normal_gate_process( - draw_data=draw_data, - bit_gate_name=bit_gate_name, - line_num=line_num, - c_line_number=c_line_number, - ) - line_num_gate_index_dict[line_num] = gate_line_index - if bit_gate_name == 'B': - _align_barrier(draw_data, line_num_gate_index_dict) - else: - _normal_gate_process( - draw_data=draw_data, - bit_gate_name=bit_gate_name, - line_num=line_num, - c_line_number=c_line_number, - ) - except Exception as e: - raise e - # raise VisualizationError( - # f"线路可视化错误---线路第{index}行:'{qcis}' 存在问题" - # ) - simplify_draw_data(draw_data) - return draw_data - - -def validate_and_extract_bit(qcis_bit: Optional[str]): - """ - Verify qcis_bit string format is correct and extract the number of bit lines - """ - pattern = r"^Q(\d+)$" - match = re.match(pattern, qcis_bit) - if match: - number = match.group(1) - return int(number) - else: - raise Exception() - - -def _align_barrier(draw_data, line_num_gate_index_dict): - max_gate_index = max(line_num_gate_index_dict.values()) - for line_num, gate_index in line_num_gate_index_dict.items(): - if gate_index < max_gate_index: - for _ in range(max_gate_index - gate_index): - draw_data[line_num].insert(gate_index, '') - - -def simplify_draw_data(draw_data): - min_len = np.inf - min_line_num = None - for line_num, line_data in draw_data.items(): - if len(line_data) < min_len: - min_len = len(line_data) - min_line_num = line_num - for i in range(min_len - 1, -1, -1): - if all(not (line_data[i]) for line_data in draw_data.values()): - for line_data in draw_data.values(): - line_data.pop(i) - - -def _normal_gate_process(draw_data, bit_gate_name, line_num, c_line_number): - """ - processes bit gates with no special drawing, such as X, Y, M gates.... - """ - line_data = draw_data.get(line_num) - if line_data: - for line_data_index, value in enumerate(line_data): - if value is None and line_data_index not in c_line_number: - line_data[line_data_index] = bit_gate_name - return line_data_index - draw_data[line_num].append(bit_gate_name) - return len(draw_data[line_num]) - 1 - - -def _multiple_gate_process(draw_data, bit_gate_name, line_num, c_line_number): - """ - processes bits that need to be wired, e.g. cz, swap,ccz gate..... - """ - # 目前最长的一条线的长度(连线类型的比特门放到此长度之后) - line_max_size = 0 - for data in draw_data.values(): - if len(data) > line_max_size: - line_max_size = len(data) - line_data = draw_data.get(line_num) - if line_data: - if len(line_data) < line_max_size: - for _ in range(line_max_size - len(line_data)): - line_data.append("") - else: - line_data = [] - for _ in range(line_max_size): - line_data.append("") - draw_data[line_num] = line_data - - for index in range(0, len(line_data)): - if line_data[index] is None: - line_data[index] = "" - draw_data[line_num].append(bit_gate_name) - - another_line_number_list = bit_gate_name.split(" ") - for index in range(1, len(another_line_number_list)): - another_line_number = int(another_line_number_list[index]) - another_line_data = draw_data.get(another_line_number) - if another_line_data: - if len(another_line_data) < line_max_size: - for _ in range(line_max_size - len(another_line_data)): - another_line_data.append("") - else: - another_line_data = draw_data[another_line_number] = [] - for _ in range(line_max_size): - draw_data[another_line_number].append("") - draw_data[another_line_number].append("") - - for i in range(0, len(another_line_data)): - if another_line_data[i] is None: - another_line_data[i] = "" - c_line_number.append(line_max_size) - - -def draw_circuit( - qcis_str: Optional[str], - file_name: Optional[str] = None, - scale: Optional[float] = 1.0, - interactive: Optional[bool] = False, -): - """Draw a quantum circuit based on matplotlib. - - Args: - qcis_str: A visual qcis string is required - file_name: The address to save the image.Default to 'None' - scale: Scale of image to draw .The value must be greater than 0,if it is greater than 1.0, it is enlarged, and if it is less than 1.0, it is shrunk. Default to '1.0' - interactive:Displays the created image.Default to 'False' - - Returns: - matplotlib.figure.Figure: a matplotlib figure object for the qcis circuit diagram - - Raises: - VisualizationError: There is a problem with the line format - ValueError: 'scale' parameter range error - """ - draw_data = _processing_circuit(qcis_str) - if not draw_data: - raise VisualizationError("解析qcis线路失败") - if scale <= 0: - raise ValueError("scale参数值不能小于等于0") - total_num_line = len(draw_data) - x_len = max([len(value) for value in draw_data.values()]) - y_len = total_num_line + 2 - # 设置画布大小 - plt.figure(figsize=((x_len + 1), y_len), dpi=150 * scale) - plt.xlim(0, (x_len + 1)) - plt.ylim(0, y_len) - y_values = range(1, total_num_line + 1) - qubit_index_list = sorted([key for key in draw_data]) - line_num_list = range(0, total_num_line) - map_q2line = dict(zip(qubit_index_list, line_num_list)) - for y, line_num in zip(reversed(y_values), line_num_list): - q_index = qubit_index_list[line_num] - line_data = draw_data.get(q_index) - # 画一个水平线 - plt.axhline(y=y, color="black", linestyle="-", zorder=0, linewidth=3.5) - # 在水平线左边添加文字信息 - rect = Rectangle( - (0, (y - 0.25)), 0.6, 0.505, edgecolor="none", facecolor="white" - ) - plt.gca().add_patch(rect) - plt.annotate( - f"q[{q_index}]", - (0.1 - (q_index // 10) * 0.1, y), - color="black", - fontsize=13, - fontweight="bold", - ha="left", - va="center", - ) - if line_data: - for index, data in enumerate(line_data): - index = index + 0.75 - if not data or "two_gate" in data: - continue - image_name = data - - # 特殊处理-cz iswap swap - if any( - substr in data for substr in LINE_GATE - ): - data_split = data.split(" ") - another_line = map_q2line[int(data_split[1])] - image_name = data_split[0] - another_line1 = None - color = "#D6271D" - if image_name in ["CCZ", "CCX"]: - another_line1 = map_q2line[int(data_split[2])] - color = "#1A4DF2" - # 画一条竖线 - plt.axvline( - x=(index + 0.3), - ymin=(y - (another_line - line_num)) / y_len, - ymax=y / y_len, - color=color, - linewidth=3.5, - ) - if another_line1 is not None: - plt.axvline( - x=(index + 0.3), - ymin=(y - (another_line1 - line_num)) / y_len, - ymax=y / y_len, - color=color, - linewidth=3.5, - ) - ##读取图片 - current_dir = os.path.dirname(os.path.abspath(__file__)) - image_folder = "image" - image_path = os.path.join( - current_dir, image_folder, f"{image_name}.png" - ) - img = mpimg.imread(image_path) - ##将图片置于水平线上 - plt.imshow( - img, - extent=[index, (index + 0.6), (y - 0.3), (y + 0.3)], - aspect="auto", - zorder=3, - alpha=1, - ) - # iswap和swap每个比特都是图片 - if image_name in ["ISWAP", "SWAP"]: - - plt.imshow( - img, - extent=[ - index, - (index + 0.6), - ((y - (another_line - line_num)) - 0.3), - ((y - (another_line - line_num)) + 0.3), - ], - aspect="auto", - zorder=3, - alpha=1, - ) - else: - # 画一个圆 - circle = Circle( - ((index + 0.3), (y - (another_line - line_num))), - 0.12, - color=color, - ) - plt.gca().add_patch(circle) - if another_line1 is not None: - circle = Circle( - ((index + 0.3), (y - (another_line1 - line_num))), - 0.12, - color=color, - ) - plt.gca().add_patch(circle) - - # 特殊处理-rx ry rxy rz u3 - elif any(substr in data for substr in PARAM_GATE): - bit_gate_name = data.split(" ")[0] - if "RXY" in data: - angle1 = str(round(float(data.split(" ")[1]), 4)) - angle2 = str(round(float(data.split(" ")[2]), 4)) - angle = angle1 + "_" + angle2 - elif "U3" in data: - angle1 = str(round(float(data.split(" ")[1]), 4)) - angle2 = str(round(float(data.split(" ")[2]), 4)) - angle3 = str(round(float(data.split(" ")[3]), 4)) - angle = angle1 + "_" + angle2 + "\n" + "_" + angle3 - else: - angle = str(round(float(data.split(" ")[1]), 4)) - if bit_gate_name == "RZ": - color = "#D6271D" - else: - color = "#1A4DF2" - rect = Rectangle( - ((index - 0.1), (y - 0.3)), - 0.8, - 0.6, - linewidth=1, - edgecolor="none", - facecolor=color, - ) - plt.gca().add_patch(rect) - plt.annotate( - bit_gate_name, - ((index + 0.3), (y + 0.1)), - color="white", - fontsize=18, - ha="center", - va="center", - ) - if len(angle) > 12: - fontsize = 8 - elif 8 < len(angle) <= 12: - fontsize = 10 - else: - fontsize = 12 - - plt.annotate( - angle, - ((index + 0.3), (y - 0.15)), - color="white", - fontsize=fontsize, - ha="center", - va="center", - ) - - else: - # 读取图片 - current_dir = os.path.dirname(os.path.abspath(__file__)) - image_folder = "image" - image_path = os.path.join( - current_dir, image_folder, f"{image_name}.png" - ) - img = mpimg.imread(image_path) - # 将图片置于水平线上 - plt.imshow( - img, - extent=[index, (index + 0.6), (y - 0.3), (y + 0.3)], - aspect="auto", - zorder=3, - alpha=1, - ) - plt.axis("off") - - plt.subplots_adjust(top=1, bottom=0, left=0, right=1) - if file_name is not None: - plt.savefig(f"{file_name}.png") - fig = plt.gcf() - if interactive: - plt.show() - return fig diff --git a/tests/visualization/test_visualization.py b/tests/visualization/test_visualization.py index 180e624..54a4732 100644 --- a/tests/visualization/test_visualization.py +++ b/tests/visualization/test_visualization.py @@ -21,49 +21,10 @@ sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) ) -from cqlib.visualization.circuit import draw_circuit from cqlib.visualization.gplot import draw_gplot from cqlib.visualization.result import draw_histogram -def test_draw_circuit(): - s1 = """ -x q1 -y q1 -RXy q1 1.26 1.33 -ry q0 0.33 -rx q3 0.23 -td q2 -cz q1 q2 -cz q3 q8 -xy2m q1 3.1415 -xy2p q8 1.732 -rz q11 2.13 -iswap q1 q2 -sd q2 -s q8 -h q5 -swap q2 q8 -b q4 q2 q5 -cz q3 q4 -s q9 -t q11 -x2p q0 -ccz q2 q8 q11 -x2m q0 -y2p q0 -y2m q0 -ccx q1 q0 q2 -i q9 -rz q0 1.63 -m q1 q3 -u3 Q3 -3.14 -3.14 -3.14 -m q11 -""" - a = draw_circuit(s1, file_name="circuit", interactive=False) - return a - - def test_draw_gplot(): # ['T1', 'T2', 'gate_error', 'two_gate_error', 'Readout_Error'] a = draw_gplot( @@ -92,5 +53,3 @@ def test_draw_histogram(): {"2": 10, "9": 1.6}, ] draw_histogram(a, file_name='hist', interactive=False, fig_size=(20, 15)) - - -- Gitee From bd4854c1ddb1723af64f64dd641e6abfa86693b5 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 5 Mar 2025 14:04:43 +0800 Subject: [PATCH 8/9] fix(tests/pulse): Rename params to data_list in waveform tests --- tests/pulse/__init__.py | 12 ++++++++++++ tests/pulse/test_instruction.py | 12 ++++++------ tests/pulse/test_waveform.py | 22 ++++++++++++++++++---- 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 tests/pulse/__init__.py diff --git a/tests/pulse/__init__.py b/tests/pulse/__init__.py new file mode 100644 index 0000000..c32c814 --- /dev/null +++ b/tests/pulse/__init__.py @@ -0,0 +1,12 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. diff --git a/tests/pulse/test_instruction.py b/tests/pulse/test_instruction.py index 0fe0221..a9ce7be 100644 --- a/tests/pulse/test_instruction.py +++ b/tests/pulse/test_instruction.py @@ -11,7 +11,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" Tests for pulse instruction""" +""" Tests for pulse instruction """ import pytest @@ -47,12 +47,12 @@ class TestPXY: def test_numeric(self): waveform = Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, phase=2, - drag_alpha=0.2, params=[0.1, 0.2, 0.3]) + drag_alpha=0.2, data_list=[0.1, 0.2, 0.3]) with pytest.raises(CqlibError) as exec_info: PXY(waveform=waveform) assert 'The length of its list must >=6 and must be even.' in exec_info.value.args[0] waveform = Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, phase=2, - drag_alpha=0.2, params=[0.1, 0.2, 0.3, 0.11, 0.22, 0.33]) + drag_alpha=0.2, data_list=[0.1, 0.2, 0.3, 0.11, 0.22, 0.33]) pxy = PXY(waveform=waveform) assert str(pxy) == 'PXY(3,10,0.1,2,0.2,0.1,0.2,0.3,0.11,0.22,0.33)' @@ -78,12 +78,12 @@ class TestPZ: def test_numeric(self): waveform = Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, - params=[0.1, 0.2, 0.3]) + data_list=[0.1, 0.2, 0.3]) pz = PZ(waveform=waveform) assert str(pz) == 'PZ(3,10,0.1,0.1,0.2,0.3)' with pytest.raises(CqlibError) as exec_info: - Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, params=[0.1, 0.2]) + Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, data_list=[0.1, 0.2]) assert ('The numerical list of data pulses is in the form of [d0, d1, ..., dn]' ', and the list length must be >= 3') in exec_info.value.args[0] @@ -109,7 +109,7 @@ class TestPZ0: def test_numeric(self): waveform = Waveform.create(WaveformType.NUMERIC, length=10, amplitude=0.1, - params=[0.1, 0.2, 0.3]) + data_list=[0.1, 0.2, 0.3]) pz0 = PZ0(waveform=waveform) assert str(pz0) == 'PZ0(3,10,0.1,0.1,0.2,0.3)' diff --git a/tests/pulse/test_waveform.py b/tests/pulse/test_waveform.py index 16f2dfc..ab4df6f 100644 --- a/tests/pulse/test_waveform.py +++ b/tests/pulse/test_waveform.py @@ -13,9 +13,10 @@ """test waveform""" -# pylint: disable=no-member +# pylint: disable=no-value-for-parameter -from cqlib.pulse import Waveform, WaveformType +from cqlib.pulse import Waveform, WaveformType, CosineWaveform, FlattopWaveform, \ + SlepianWaveform, NumericWaveform def test_cosine(): @@ -26,6 +27,9 @@ def test_cosine(): assert cos.data == [WaveformType.COSINE, length, amplitude] assert str(cos) == f'{WaveformType.COSINE.value} {length} {amplitude}' + cos = CosineWaveform(length=length, amplitude=amplitude) + assert str(cos) == f'{WaveformType.COSINE.value} {length} {amplitude}' + def test_flattop(): """test flattop waveform""" @@ -35,6 +39,8 @@ def test_flattop(): waveform = Waveform.create(WaveformType.FLATTOP, length=length, amplitude=amplitude, edge=edge) assert waveform.data == [WaveformType.FLATTOP, length, amplitude, edge] assert str(waveform) == f'{WaveformType.FLATTOP.value} {length} {amplitude} {edge}' + waveform = FlattopWaveform(length=length, amplitude=amplitude, edge=edge) + assert str(waveform) == f'{WaveformType.FLATTOP.value} {length} {amplitude} {edge}' def test_slepian(): @@ -48,16 +54,24 @@ def test_slepian(): assert str(waveform) == (f'{WaveformType.SLEPIAN.value} {length} ' f'{amplitude} {thf} {thi} {lam2} {lam3}') + waveform = SlepianWaveform(length=length, amplitude=amplitude, + thf=thf, thi=thi, lam2=lam2, lam3=lam3) + assert str(waveform) == (f'{WaveformType.SLEPIAN.value} {length} ' + f'{amplitude} {thf} {thi} {lam2} {lam3}') + def test_numeric(): """test numeric waveform""" length = 10 amplitude = 1 - params = 0.1, 0.2, 0.3, 0.4 + params = [0.1, 0.2, 0.3, 0.4] waveform = Waveform.create(WaveformType.NUMERIC, length=length, amplitude=amplitude, - params=params) + data_list=params) d = [WaveformType.NUMERIC.value, length, amplitude] d.extend(params) assert waveform.data == d assert str(waveform) == (f'{WaveformType.NUMERIC.value} {length} ' f'{amplitude} {" ".join(map(str, params))}') + waveform = NumericWaveform(length=length, amplitude=amplitude, data_list=params) + assert str(waveform) == (f'{WaveformType.NUMERIC.value} {length} ' + f'{amplitude} {" ".join(map(str, params))}') -- Gitee From 8fe7fe20f4dbccf9953959bc67d409d335d40d5f Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 5 Mar 2025 16:39:25 +0800 Subject: [PATCH 9/9] chore(tests/visualization): Remove obsolete files and add new test modules --- cqlib/visualization/circuit/mpl.py | 10 +- cqlib/visualization/circuit/text.py | 7 +- requirements-dev.txt | 3 +- tests/visualization/.gitignore | 1 - tests/visualization/circuit/__init__.py | 12 + .../circuit/baseline/test_barrier.png | Bin 0 -> 4024 bytes .../circuit/baseline/test_basic.png | Bin 0 -> 5917 bytes .../circuit/baseline/test_gates_0.png | Bin 0 -> 24574 bytes .../circuit/baseline/test_gates_1.png | Bin 0 -> 20978 bytes .../circuit/baseline/test_gray.png | Bin 0 -> 6046 bytes .../circuit/baseline/test_long_theta.png | Bin 0 -> 12900 bytes .../circuit/baseline/test_moment.png | Bin 0 -> 8730 bytes .../circuit/baseline/test_parameter.png | Bin 0 -> 7599 bytes .../circuit/baseline/test_phy_qubits.png | Bin 0 -> 8189 bytes .../circuit/baseline/test_rotation_gates.png | Bin 0 -> 11309 bytes .../circuit/baseline/test_swap.png | Bin 0 -> 14240 bytes .../circuit/baseline/test_title.png | Bin 0 -> 7545 bytes tests/visualization/circuit/test_plt.py | 135 +++++++ tests/visualization/circuit/test_text.py | 335 ++++++++++++++++++ 19 files changed, 496 insertions(+), 7 deletions(-) delete mode 100644 tests/visualization/.gitignore create mode 100644 tests/visualization/circuit/__init__.py create mode 100644 tests/visualization/circuit/baseline/test_barrier.png create mode 100644 tests/visualization/circuit/baseline/test_basic.png create mode 100644 tests/visualization/circuit/baseline/test_gates_0.png create mode 100644 tests/visualization/circuit/baseline/test_gates_1.png create mode 100644 tests/visualization/circuit/baseline/test_gray.png create mode 100644 tests/visualization/circuit/baseline/test_long_theta.png create mode 100644 tests/visualization/circuit/baseline/test_moment.png create mode 100644 tests/visualization/circuit/baseline/test_parameter.png create mode 100644 tests/visualization/circuit/baseline/test_phy_qubits.png create mode 100644 tests/visualization/circuit/baseline/test_rotation_gates.png create mode 100644 tests/visualization/circuit/baseline/test_swap.png create mode 100644 tests/visualization/circuit/baseline/test_title.png create mode 100644 tests/visualization/circuit/test_plt.py create mode 100644 tests/visualization/circuit/test_text.py diff --git a/cqlib/visualization/circuit/mpl.py b/cqlib/visualization/circuit/mpl.py index 64dfc9a..2552b51 100644 --- a/cqlib/visualization/circuit/mpl.py +++ b/cqlib/visualization/circuit/mpl.py @@ -218,7 +218,7 @@ class MatplotlibDrawer(BaseDrawer): ax.set_ylim(-1, len(self.sorted_qubits) * 2 - 1) ax.invert_yaxis() if self._title: - ax.set_title(self._title) + ax.set_title(self._title, loc='center') ax.axis(self.figure_styles['show_axis'] in [1, True, 'true', '1']) if self._fonts: @@ -372,8 +372,9 @@ class MatplotlibDrawer(BaseDrawer): self._current_x, ymin=self.qubit_line_no(q) - 1, ymax=self.qubit_line_no(q) + 1, - linestyles='dashed', + # linestyles='dashed', color=gs['line_color'], + linestyles=(0, (10, 3)), linewidth=gs['line_width'] ) @@ -461,6 +462,7 @@ def draw_mpl( figure_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, gate_styles: dict[str, str | bool | float | tuple[float | int]] | None = None, fonts: str | list[str] | None = None, + style: str = 'default', ) -> Figure: """ Quick visualization entry point. @@ -474,6 +476,7 @@ def draw_mpl( figure_styles (dict | None): Overrides for default figure styles (colors, margins, etc.). gate_styles (dict | None): Overrides for default gate rendering styles. fonts (str | list[str] | None): Font family/families for text rendering. + style (str): Style name for consistent visual appearance. Returns: Figure: Ready-to-display circuit visualization @@ -485,5 +488,6 @@ def draw_mpl( figure_styles=figure_styles, qubit_order=qubit_order, gate_styles=gate_styles, - fonts=fonts + fonts=fonts, + style=style ).drawer() diff --git a/cqlib/visualization/circuit/text.py b/cqlib/visualization/circuit/text.py index 6f17ce1..54e0f89 100644 --- a/cqlib/visualization/circuit/text.py +++ b/cqlib/visualization/circuit/text.py @@ -265,8 +265,11 @@ class TextDrawer(BaseDrawer): Returns: int: display width in characters. """ - if self.line_width and self.line_width > self.MIN_LINE_WIDTH: - return self.line_width + if self.line_width: + if self.line_width > self.MIN_LINE_WIDTH: + return self.line_width + if self.line_width < 0: + return float('inf') # try to get terminal size width, _ = shutil.get_terminal_size() if not width: diff --git a/requirements-dev.txt b/requirements-dev.txt index 2fdfbc8..25ed76d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,4 +7,5 @@ antlr4-tools>=0.2.1 delocate poetry-core pipx -psutil \ No newline at end of file +psutil +pytest-mpl \ No newline at end of file diff --git a/tests/visualization/.gitignore b/tests/visualization/.gitignore deleted file mode 100644 index e33609d..0000000 --- a/tests/visualization/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.png diff --git a/tests/visualization/circuit/__init__.py b/tests/visualization/circuit/__init__.py new file mode 100644 index 0000000..185105c --- /dev/null +++ b/tests/visualization/circuit/__init__.py @@ -0,0 +1,12 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. diff --git a/tests/visualization/circuit/baseline/test_barrier.png b/tests/visualization/circuit/baseline/test_barrier.png new file mode 100644 index 0000000000000000000000000000000000000000..cc9f3fbf6ddf470acd7570bb2fc76fef76a22be3 GIT binary patch literal 4024 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 Xqvemc+OdL@Y6x5vp^|_1!IOUj4eT7D literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_gates_0.png b/tests/visualization/circuit/baseline/test_gates_0.png new file mode 100644 index 0000000000000000000000000000000000000000..9f5a4e1346eaed3726d795c52260618055d0d5b0 GIT binary patch literal 24574 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 literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_long_theta.png b/tests/visualization/circuit/baseline/test_long_theta.png new file mode 100644 index 0000000000000000000000000000000000000000..55dc2cdec1450a26461316365ddc6f1bf938fcbb GIT binary patch literal 12900 zcmeIZcTm(_*CpB{5s<6`5(NPf1OX)nQ9zL-IR}Xno1AGtBq)+Za+Iv(oN18IAQ>bl z$(bgn26|3EZ_Qixn>%ywH-AjkOw~|DK~ukY&faUUz4qeMTcuaTg!c#`5D2l{YiU&o z1RMN{#dY%r_)&FD^a%VAagot>QG;5zxW98UhbX>taj=ED*jkw|xtTjTTS4vlxp|*( zKjC6}@8aU%EW*QM_wOUPp-z@O-<-6az*PtwUh6nRAf)dw-&pyQc~%ez@1UHtl)6XC z&a9`0x<(p$-`#xveiW0A|3_cq1}3`?69UYH%o(+i#pgtig_N=$SHC2k3VXlw#1w6- z_m(!uqD<%MhpC&H+f3y_;?G|*5#6W#{>WSP7?s6_Xa0gBE}QA}(2F5Gr2cfQZk{k2 z^}~5Uc-*XF(l!))eXp&nhmez#TX_&dn3k&!w}; z`4bhu;kET&ZkU#7mvZ=zeZuw+ea&*0jI59{THZW1ruh~mt^K~FZMq2vTYjBoy zyZ>#7^#8|)?A8fe0SXF=uHV0%TseaCFndazSy=dGv_R`{(lWiwdi0T2ys3xo&DYY> zo2iqw7L(=XW)>FMQL(Wyo}R)C4E49_K6`)`JUj?DC=?30yt*>bNXmdXC`Ql;|2o@W`V<=4GCiHVx989q z$v7~S^{RkQPgV6-p-#n3Qc}eDcsv%)jna2*H>lJf%kJ?SW5XJMUwixS5^H?vx+CJU z{#;+5uA`#^(wioptYzpKq-EqAYjn9t#a-M#aT~JvzAx$7(Z)y!6n+(FndTXMI_DkP z9m8H>HNtjrwDHo%r*ULIhE37r`tn@t_>WrSVZUf?Lj%9hnM1YR^lGLu6Z7IdIXO8< zrR^kzySqC?OkBL6=`j=Wt3#g6zu>|O2~PsRk>%z+cVE1GNslu0SimzeHKmb+i4hPG zNWXe@*hVT@YSKZt+Lw%w=V`)iY5zpIG1nx@1Ha@Gb=!6reFbvTK|YQP%lOwv0`6kz z;%o+gkpw{~16XS={o28eMN%>fieDe_h)T7&dXoi7{WS_S7j}31D+VOCg!&PP5X}2H zuPODXijHR-hlSR@F*HnD8i2uI+=bGxH&_`YW5ovY>gu=4%F0G}Bg4X29zFV8WiyUl z>W7AjxNX0F`!;IC(S6R79fD6nmp|^+QF{=uZGFw@Y_4MRp3!Fy3!;{%KYhe7Z*=W6ZanT&{t5n_2b8nggy&CKE9hoLy;%=g*(%M)Zr9mzHq;o-w{ywm@+XmL=`* zVBaCu59|6J%ZWuT?o}Da8NK-s@@Mfkk+)*61wUKNr>H10A75V%4vrt-mQSgTjOLS# z-ak9m=8E%@jQmq*E&2AUo^oBQ@5Fe!2K(L{Sy@?}G{v8$Rl2m>`U1(}A3k80S5$Ct zakX1lneZhFxo+Nu%r*#KK2R?h)#ZVQj2`dK72k$f4`s_y{%UKJd;L0pzxLxtTxJ%Q z{euH$F0R5UR&Gu|nzpvKt(-B=o!N%?zGT61lxMbuv%Z0U7-ru_Szn7+Xmim?!ct5T z5z%RByJsKNQfubC!r~t54c9qa*1K%H0HL?p)coPY2d{JS6#l&ZOQRylTYMJTFUGAO z@NB26!yAyz4Zp98^lHAQr1S!yz*-+IpqlqRrH|(}aJqN1*89{RaeXmX12qzH-Bd@e zMe8=6Bo%NveB!ih3AN!esQ(5oo;LfdNNCdeg$DA4UNo*|9+nJ{QDhA3AqU61p-h=u)KC01i*!f5`vL+2sv`8re(;3< zNIr5~S#@(pc!0fcK3}CJVU*A}H+udY@>sW$>A{1K85vAYPEHeWKS^-Xj3a{H%-lRW zJsl||AuOCI6Lxo~%=A?-3H{~~bhB@9bv1XsO&0`)6zXJ0r_qB~S!^Fy-0R5rV7VJ) zUJO{VSB$>WhK3BBnuSavBKQ0Olj_@oREBqZjBW|3=auWW6(t}c$-+OjrT zA8>LKK>!dTIvAOrKK<4jh)+pN+Y^9;KhfyP4`L#-vho|i2e4)X8B&4wR$mfixI&8M zPIqT@>RmW!XlT^c)%UvD)87J!O@=n>tY|YbG6HORB`bRa!06>(8|jQ6`ieVW_lq%w zan=P51Tv4KA!;vU9rx|qvdcbl@H@^(p0efNmVi;iORT$G+K?6y2|s6%tE;l<621u% zz9L}dB6es6Z1Yb}KKx-khRpbXqGbOX9o@=V()hy6>;bfKKe#}ig1n9c?6r2CQ_j-j ze2I}HgU8>1dx*QYsi@Rc-WfSEKX{PcdD7I_Nd3(Ec57>E1daUu{yr`~J|#K1V~j-5 zA8B!j;QO&c9Z&)dE@iS+CQIK1npztnBIs?J&TzPln(6!zjDC$uYflmtZZU{W@bdp` zY)mPEU)T3n5G{8;gaB3)^XHGcQi>4PgyrpU2J!l_F!-qT%JyUhWc(@Kq#57-;7d{v z%0!Ui;o;W`3XT20S3taB&I6k$d`(A1rL{A>Dp>`}6XXUdgHPX@v%9-xSJeH8aW6Bk zwzm*6a&r9SzRpB`OUT~INv=C`&ehAv{C#a21ae{+XwjF1b;1w|F1}^`YEubQGwI69 z%jpiUrYo%i8ydWekistOWX9>N4l7=e@n2?qXB`xV>X_633jdTo1qj*Pnort+tk;jse37ocbO;deqh1i<@CLUCmuATO0zYPN;qb zSZfF-c^BU-ds&fXVEzOkhu>-WHA3Xt38eyFCfde=FLAT+O~y>pKeuc5|AWKCJRGXS zb$4|j9l6PO?@Nb?qborTF&-rPB6}?K{gRn~d!5CWZd*^>jG5 zfAPWg6_fhVot_{NNReTt=b$CPQN@H2FVieGSzTYH-!{W8%rbbXs;corW?`6_t(@_Eau0`C zI+4HfArr&o1xekjuR7@UEhW1Iu^F24zV!WDdn<>n@Z84yY$?gW(AqR?|1LZ0`E)H~ z;p9&ZHB{eyMFwFrWV$nxiD&oue>~*K5?Z5>ytd+y^!C2D!$9A=5t~ep+>x-uZ8exG zb^N6d()rdKtmn%DvDH?R!uZNK4o;Ib-d|2oLB zjjvWD+NSPP5%T|pRn)}AsabI2GMR4Ox7D->{PR$?+g6~$c9IXUbu2*sGaDP}NJ$NX z^<^YIqo@D$Yf1c7RTVLmf>rum8tRg4zmc)hsY1A&uQ()~zL%dXyIrpU^KZ|gK&IofaI|16Yfpm60zTu8hns|Gx(3#wF(iCQJZ`J0RzTCWdX5^X9 z;`IeK1582*aULX?cPhX1`rJ!!UhEpb(0t`qifD(7M%WOYgiit}*}?{woS?U;%YTFg za#pt0qN*GjZhjW>EUyPn>^vp7r+gB2wL9sTxBg4!q^T|?)Z2^vYHvVa<4G5HcL<(u;qPW2mtJqDpK=6k2t(-59^#Yu z$|LBiJP@6(=H0_xi3)6htF%Skf4O&X;ADGxa&n_(etupfhky_=zT6ef@`X_{Ej09I zV^b3WDXHb<**=}P*Pjicu#R6T!p_tJw$z}NXahB2AHmp=`=0k(8?s|*nmOTQDfsUK zvdv%el2$n6tI)}URy=9>xaK7i#auKeSaB3(%#XbqOX6e1(X-hl~^s0 z<58{i4j$OT;wEUaG9o}FdkG5Y*~v82b2ZV@1KD(%Am+&jAUabyU7{U<5RK^T?_WKI zqs2jq%d#9u<1W!{nf7(`{r zBp?vCzl4a1ihAzi@)QKSnVH#Oxw$;-a{txEDH8c>Gp9`WcmRp{E||BfD6<$mjEHZr z(cXccZo|iGjg~ER%FPHMlxzwPj;v0s$B;j*jh=@Oxwy<0eg$c0784Q>yaOy(8$f$s zGX4Y-T4g&~UY`rvT~;2RfqhF!Nl7YAX2~2th;7D;SwYh>F)?9%isnrnt}eO%Wv4vA0HeZ$|)&^YkRVK<6<=~6 zcvh|W`^?X}y1Mdfs*8xv8bNiJm6F1moSeLU_pV8mE;u|z#Ffy07qk=Fk7ElpZJI89 z+J61A_ew6#guD%7Q%oXbU{HGT;zc-u^}z!&8k&vDiJ*UX5PB%P%_4L1CGA>=?*;O; z_IJaAmJpH7+Y`LQBO~;Omj#Dode9-T3nK2j3V`mtAI_F*8rORJ_AQU(ys-1?AK0MX z3()b&exU|SOEgh!N9#WC>mGZhS7Xl#C^R4<^%?b>#{fQ+Qbhp8-sJcUExeYKdk3hfw2Ta|Sr?gZjlJ*4 zR>3k7IiRL2t^wT+Y-5ZwYDKqP)$W==H4cPPhxyCwEBYS)9kz7TVhYv$(_$_Kn z-&p>uDX7(Wxt|jhYwip`_I&}oiIrcW7ifn+B6AxRf5$h_p~NnI)~{z?zd*S#Wovmi2I5Sf z{Aw8OZ-S%z@uWwuzLcv9olCPbJftw|5z8y}iFWKm2qBE&6*I!D;<0 z@jZIoSeKOX`Qh%62Ohq$uy+9e{%-V0PL&_qC4JZ%ZzsfNLJ0n==z14)7*(dClPEH+c5q^yf zdY(Z`6DN_gZL>&qv?$uD_x9&6qdH3F(G1N8Czlscsn}9Z5E)zdOox2??7= zbEUP^UQK5Y`@Ac2GPA#Yu?sodKkm?0t)N3xHWtvfjWrtX)L{W6{qoU$OkXV-a%Ge* z-P2G{K}PerA(ATY57*ZvcZV~m(OW~0V`%MofkZ>h?2P>`8BfJzui(5-8<2Vmwux@u z%mCmGLZ9sRZD}PX64WV@{mGLk(cRj>!a^>PWAq4icJ_+*L(D)z=oeCl zsdx-s#I{X#rmC`Yr_kTMug}+Lvx&Cd78q$hTl$mcU7w2wC3j zp&neDE@{k9XB*&(r}=%n9NAc=7nKjYcHKH&KTbyM?-gCUg)3J)2dhO5UfU@$m+Yh> z32CHxJHGrX2QF;K*`&UHG{w^>Oj?YhtR^g2G8uk1Ll#_oM!&`Ao!V=Ffb8uZukN~A zI4mG2Q?w6=WhUd_57n?V8G3ywZSXFIZQE!}(O-8kVhNR*;RMmtomJgp%hp0!+sp#^ zH^-BS7_TN=L$&Wp7~;BH%WT%5N>h5UlfwsLskMj4avx82TjDm5>Q>JG;!5W+1IWuh z{*w}(L9h3c6!G>C)&@l`cW>q%^0!ep?-DN}Q{|+r1t1{qR^}p&*iHv&P5_Ha3utyp zfgQ% zHG1C`xA?-$Rg7;GdBKyvBskZ}#=-Fkh=*$09_ObbAX^g>sO#N!-hir+s(I{vx@7s;Qh6Q%q zr`#@)Xp{DfnsotJ>2NG2(S@fy^$M9snLz@iB!y~__ftRaq^2|c4nXV3q{lOsl*9L4 zq%G0f9;WS3@0ZkA+YaceNDXfmJ!2R#?PT%5J3l;+O4JvzBMtf_FL=;3=Dv=;crZ3A z*)yl9BWx6Lrd$paTkt@3=g^j=G(1PfGqQ74P9ID%{XpV_L3H#bOh40H& zGA$>!JBnW!XIQzn^0Gw4PPFWd_BZ#PLGuHLlg5)zBZA(g=&KbvG2c;vD_sGW&DH=q z7*84_y0pDa-1(F@xCoXi(1!2ivLFJv=x2yzUBNaAPYJIor1N*4zw8QyUMob>WMQ%! za@%rE3&oFocF236oF_V}HZFkG3_EjI+DXOj?M^)M+p&7#UBhlO-(3Lf@7(;`8i?Tw z^{A`M%h2%f93wc)!$ZC4qnY`nx5muuEIpN<|KQ+T(6K52X#t3S#)qqY`t>fKhKE&w zWFrHENd(>?woV{r_0PpJi~+T*0@MYd@#W{||BX?W_zu(~fD{$K%euR}Cv2;&(P5uI zTc0gL`2`AMKOE6Dey}zaE^N$57!Dg3m|o}y)QH0f-4cXGja{jY4IezBs+&rfnX&dM zpQFdAk8(cyvb0G0Rz>}H-LWJE#YjZUdla3OjZWQ22VC{{xFlpS`(?JNUd+jY^mQX? z$b4AdgMkkXHSBDpr@DmC48|!TkJq-*`QFcdTqE1s=J7`~>QlsVwy6R#3w`o-WT>_| z+cj>-Pfgq3d+v~@neEdw?fCshmuS+9>+VWjMkBi60&yw3w%5AO!qhYGlOb_`h)$}L zsFB^SK=@UnLX*&oJn_A7xj{mwz0<*n5#}HY$pS@FSN8Vy7-@N9B#)Aka%rOZU0bj< zl;`!83TX>*f{-IKP+J-gesfC8${uvmLTl@@r;|cT1b3SaJ@77uHm|RxXQq?+jkarl zti3-RG^ku3j~-aM4g?*D{hMTl<~cD>C*Cxe#JA-{xeFgrZ}b^yo_FW2A;Y&-E#gNj zGOUn~?UijltCt!B3)7h#5Z1c*hJrSe9kjc-%Ns zRZ*3lI>nzjykJ*KUr}{p!?T)nNf!8UIbyZUAQ3oE-?x6+K#-uuCwzD=fEc*RjsGG2 zI^~0CO{ZYxhDMvtH1A2QNXae8_(*Q|EO80jb*-lt*-YKDCQEUtA9YcMS9w3hp{B7h zF>OHarKdu}t}gala7h0MRcF-JCVVDD2}MukN_%^H@>>6S_#rTmo<9yC2muKR8#D7F zn=j#yPuMNQNzZKVR#@~oxzdEcK#!;8jNk){-nmjgmrjQ2>LF()pva7=1pm zKHJL0_5VfWNq21{1~~hZx;k>+Y$}5^8rceayR-3i;Rx^T^SqHd)8|xTm%a~qYUyt6 z)>aT1BiTyiBG(vlk5mIjioRx4pI|8kt}3#kTCAG=PT>2IH*-Mw=Q~PA%Uli^WoLH5 z7C5_W9JaGcYD)#lZLDbAZ>_&rlWV)VF`DMuW}#xi;Ur>|ynsV}d0j$ZQbC$xGF>w7 ze2@PvAVJ-Cl7-+)H_caHBq1uZw*G{8IP9dKnXLER&Rv5TbC&#V;DAz|&JkfsVf?h1 zM^cIGWu9rHw2Gz#E7jx5_4&ykcaeV~Np6*=KAVvGwxW;Nx>U2W^|~ms*|n;lwVNny<*h0Kf>9OF_u_brNLrR6wVJ9l>0`Rv0kCur z46L;T;CzdZKRg~c;s<)NI7cbgLrB--rFHzR*M>DCumO*DCWT%5TF<#gqag-<*@e}G z-KQd#vOC_Ui6dKpNp&m#&Ok!bA8dYa5TbO6_@g>vULmL~LLnigeL zmd9)f9}{^70v^fcykH(NRrq{I0`$nS(%KP%L0F7|j|qLwmJ zxRW*iO3$p>j%3H*)9XNRt+m01bkUuG30T5$WK)Dzl;pcI@%LlT2$dzFd8S{-cOQ7} zC?U2g&{Ho#oO}GOm>)R|;@wVh^(1kVYYv)W>3#s9erzrVSEec(0hjzG&e+%`xCDcy z&l*=m*P7vA*oQ=kM0fW}@*d$)7IK>)VjjmmebY|~+-T2^T;Z3Ig!7yuy`7%x0HxQu-EkH1pR7x09B4uL<#0{dgB$}e&$5xYay<*I2@j$eHlK|{M&wKna52i zTuIrmfzn1t`0>T)CFy$NK($Ij;#zI2L(by14_`F{^Gc7ww$}}lG%>v#?hoW&K33bIb+uaZ?rFzYiZ^}oKB}S4zTtJIHNbbWQO>0N z7Knx~+GtDLJ9kp_n}2kst4$A;Y4>o_1#G<;pR!2I3H?$OB2)t-^+q3&Ml{0Xyd`0Y ze~UYHuC2$5dqzx_78iliTD-h4zP5b z=CevKyxp1d4bF&bMz>ZF9KBsDmk}(u(!qc#Z`PLVj);Z&cfGBZGl;m;k@Ia?XOG0q zFl6B@({JA@lEdeGu%NIBZ7vb!8^Bj_MeBQgClH4-sH1}n6~87oTRX5Z?UM6L0mW3G znLg82qf-QMeBW9Zl~MKK$QCpv*5cgY0mN{8tezG6(NFcZHf*d2Op6JT&EIWJxw42H zaZD7ARQ3Adf7jh`H?1Zvwqhyg^xU$<^4$6lQv0xCZE~-jai8!AY^}-;H7)>eT}cOa z8a>LN^aBp!c7_|ql^*3K4-3dhx6nwIPLhk$pDZ}`T3U+}_)On3v>te7X1 zS5ySz@MbM>T^$fz?R8=eDhvYzuW#-9hmw7m3g$ftIsLiau=N;aZQCQFSjgvx_VPGG z{T<0!0wFtNirA`a}kysh+YuY>|j%lE^-_@|> z2INPSdzDR4P?uy=_M};D^kfdGfTShQC6!8kcs^RKs`O+P+QB_=q~% zSFucB^&^E|sEo->MMSI&8jT5Q64*-aGR%n0$j$qlpWm753rxA&h_!xnbafW|)bZy? zgN||47OsoW!J5DCUqeMZS->8m(sqO{7UCo9;=gV;(GHQ$_XVJ@^Gv-9P^hI1q`cB& ztZY!u7stzK8Z7%%Zo6pNF)TUY`qu@u4E5DVe9gkNfJ0XLWrks$ei14fqhqDc1!D4Zw#O=3}lZSx^UT;>NXq}Hf_tY-+_e)1!&|9xhM0p__)X;PFHZ3_GuCU$VUHnzCfX zoj6Z?cROuhW%&R~kEo`nluyRT|5~?2ZI0LR5plwFv5+2J>%zieWOywW+n98N%n`~1 z)L7q9=z*(U#Y%?#%sIl3#e9~Vo7Md9>Pjns)j^9h2u8QHI*zlRhT4KLHjSto7f@nd zb>Gw+eDVhV0Kv;Al>&BCPe9KGd=1a;VCr~i4=Vzh(MEYXe7^DB10P@2f`gg;{--9t zOZ>Iw^Rlti)5PPCBU`_rwhqD&{eoTiy^W`ePe5xUEiOdnpRJ+yke>p^F;hTz{x=rMfjfG!JpUJ`4jk;lJ50$ z>4y-2fF-KqKa}Cl^+67xx)tWE93}96zJ@11-^jtHh_^LH&X`XWd(hbqCl#GAgy7=? z85q+YhoJ8bw7P4G<`QC?T|1`YKYJiA=FK@$pIY`01;+H=11>bW_T zJToVU>qkJ^LRLn-HN8ts?V;Fnpef-9{r!zY&}SEjpyNHxSlM1|dIpSZ51fJ`+w2Pq zl%2ArHC~-ew-g;F%mkm!ga=t*g@$&y>Xf%%+lKsi zMsbyrc;)|b7WW&GOiC~Erz5uO_@5zAodxh1SO6LPXmhk@G^fiIGZXqxP2&m?0`oWV zAa&fw!hSl^_6*5=!IS!~o|fc5RE?H>$dqxCCnjJai@tRI9{Vq6)Bl9Q=|Aa%`d@UI z81QnuHm!14c)@iytf0ps(cd4E=6j-p(SWtSLzq;R|EtR@G*n$4>v?t>7&2gEW9tJq zSD7{!7))c1tO8@;AC_2HSiLF2BctX|6Mz?^VD06yK!%k6f7Xe2j+W)~1tmyG$inRHzj28Gw{-2jUz3G#Kk()K{Vc3UVzKQvXn_ zZ{NO^`kXuEg~5}PX|1fRxC`G%>&uBln8WpfOF}-LyJ&b8TBR$esOYMq!Mtd_iA83$ z4D3y^N=iyqC@^gLk)Qwm?`P>{%?t)G%dsEKkOB%}RD3)F_<TMxJk4ETdXfk{28M=)+{JID^`*oiu9X&j_b@6W=m9(bqsj=J5c?Z>=?B2o zfU^gb^D;sY>-CgKn(+vKarEBy zNFJ?qTv{K>!iP4Wv!TxrlKT6+?JaJ|IpXIfhE2eb0LJ3d_IRJsu1}zIVuVCQIzaFr z0iF)vfx#oXYh0#{aex`a*DglvM}Yt_SfnQyK`%OH>>8%73miKTQt_jxFitT32m%(J zLW735{QQT&cUCaVNJaGuc)h^Hq{*5VSe<%%6~M4zvp^x*I#t;DhX38iy6;QVLxq4# zg;`QE{piTW>ttJJ$q|?~vU77y1drLzypx0q`#C_J4N?G#isEJXf&$GV>r!s*ib*ipw~BI>I|eh8z_b$JV@XM&13EJYa1NUBmD$gV zK-iSiW-_Y$uMREE%$}Zk%517>YbSs)iPeAKs+D>Jy|T7;2($!;{Y9B-``I@h9v;5& zTcz$uLjmaAjPY7bu$G~rlBy~Nn5ZoQch&ORn$htD#%ENh!#`C6RR)pq>C2bG(@6+` zT;S>|H5|A1w+f-_u!*6X6S_6cRo#~ni%K?TrZ{BPk zF%*oxadQ&@Q>E>xsxV-Rf}K>P57j{Dy|!WxPEXf<0P>U7rK zs0mkHP_RaJ)Zo$Z#Q)okm0wO_{Qt0EQ5pF}^E97*AZL*Uz@=xtkPhOfVbQe9t}r75 zV{MF1N!gxQ-Q86JYY!x1POxfiBO@baP>^*HjHwLBY*=sA)q#(aUC3$qW}Z?i_O>#i zew`C*llQ5`+1`SO$9g(0dDb9$$Mva2FExNd#7v#@IRJq=hXriSf;+(Sbh10Ubws=g zlnj60EC0>08*d>|Hqijc7CXY|SuPaWC;>8=?fq?iYByCmb?O7&6LJd_%(6b#hi=cq(`RP%(|iu(#A%Q;HCQ<5U{ec zHlD90Dz2F7_}z=NgLbfzWY@`#V`;AhQ!P$Lom z@MOt`JNaxW%gHSOGsp-sKUC`BznoQk?z{SNTsprBN}4w}owB3LCT$B1o07J-ZCkF@ z;T&qYg8*Ic&DQ|n_ZJ~<*_y0i2e!js)6=x2B|2qRBe}GN`qK9Jg#UI^&3P;lYsE8% z_r{)X`uh5M9(1!45EA~}7Z(;5_P-Jch>TQD?FTNb)`>Dx`Vnjpmb*9UnTI`qt!Cx} z2&Q>IbkovtzDgFxomGU?D>cS~z%NEiiZ^^fkh61hla!u{>3iCfTcvRU#Tf?95X0+DTel2b92Mk6v8Nb=jux`-_l`0jS z6ls@X`y&{Q=w($6! z%Y#`oa}6!VH<7_zfcrUAUk;EbdsBe3m9kr5U9P}0RTn;L9?e7^y}5q(A9K?We85Bg zTfY>uD%fAg^4#}`Tt`$8peb6zli?* f|6O_=jT0Wf-Du!9WDow~10pA*BwhT%_``nzY5Bn> literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_moment.png b/tests/visualization/circuit/baseline/test_moment.png new file mode 100644 index 0000000000000000000000000000000000000000..e22199ebe5b75e31f956fe59c7be7dda12e2635d GIT binary patch literal 8730 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 literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_parameter.png b/tests/visualization/circuit/baseline/test_parameter.png new file mode 100644 index 0000000000000000000000000000000000000000..915cc11ea20bbfb5b6dfa6d701bb9173956f57a0 GIT binary patch literal 7599 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

P4yZ;MW`pGvs6pq+!Dk@m*WAvDGX2?b53hoW~n0 zKdB#S_>;)|4rp^*ikPbPGQ6|@A1UjyJ=dzV6&x+xYRSyP(wC>1Xx!d*AOYi$cD3HY zbsCrXNqV8qjk<7IT4zWRX93t0FP*ja)S+iAWe5)Tr z`acuj%Q=e9$S`+0xX0L zGbNa}q414`Py09|Y%hWWRN9Ie^6E=D(2sirH8+?aC=pAUXf@J>lz!+uv|{Z4H~gjJ zH|2VnMtT?H!CWcS0j=F(HQ}6uFL00VTzif0j1eo(NSmp#`}O;GQ%=r3|HB=%I4S3c z*fKHTCLJEtlHQFk^~DLo;oOB2ngE_aD;$$*p3&^&=Negl?)Nae$v`9}D5m^&|Lk$@ zXd?7%e@p<~Ojus(+A0YO4btEtp0&hAg-11*>uUl`(IizAmNzZ*IZwp8|LVU=7|m6S zY5F+Tvj4*jl9!)fyOs*naCc8n0x-CNzje+fHl`}HiSJFRMV&a%CBtH63TA4xWf!92 z=zB1P`Zd`b?o(sJbz+#Hqj{#%qd@A?ehFoaIeOuH!S!W?EHDLG^)RSnxWP!=Cpgvs zg&&K4_wEMRP7$pxg%NR;9bNNv`B815*}Bft+OWa3U#op*|DI*Zcn<&T!cj(`rzMeO znUl4z_Z{)U;t$ogUAeVX+6MT|M>}C|F+bwKo z(*CPH%+3a*Ri5HfcgV#($KgB$Vu<$=ys%^-?+klL!R~s zUYLhKU$V-@J-m8$0*?;(Gi9u7MiMVFF!4)>6Bu6FS$8dOw!6t^^^gxgCcoP*5RVlL zfJeRCs0sxD_j+Q%{yVpW3mneN@A6$L~ z;(+mW(Hd|t3*MT%8^Dn0r}y9cDhQeNnzEoY;?+KPH@+Ll?1?KVn9ET6=(7Ve<#Ak2 z4!1#pmJ>na!7k^2i~&aw==ZzI=NB~HPrfgm-#cNpL@Zl=u1Z3+PuB`tvO*rsDg?k2 z>mN7!g$*u$aj0gq2xCzrT(Yf%l+qfif7zKvI{U2 Tn({NaJB4Vf>Zp`IunzfO1oqUZ literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_phy_qubits.png b/tests/visualization/circuit/baseline/test_phy_qubits.png new file mode 100644 index 0000000000000000000000000000000000000000..3c31484cf902f0c806f0a93e81e89ee65de1bdfd GIT binary patch literal 8189 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 literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_rotation_gates.png b/tests/visualization/circuit/baseline/test_rotation_gates.png new file mode 100644 index 0000000000000000000000000000000000000000..592eb5edfbc61908e70b2c49a12088f552d6c266 GIT binary patch literal 11309 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 literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/baseline/test_title.png b/tests/visualization/circuit/baseline/test_title.png new file mode 100644 index 0000000000000000000000000000000000000000..fbdc0fa0f681ac06a2c9c6efb0acdb886137a99c GIT binary patch literal 7545 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 KsNimxKl?Yf2BW|L literal 0 HcmV?d00001 diff --git a/tests/visualization/circuit/test_plt.py b/tests/visualization/circuit/test_plt.py new file mode 100644 index 0000000..eab83ce --- /dev/null +++ b/tests/visualization/circuit/test_plt.py @@ -0,0 +1,135 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. + +""" Test matplotlib visualization. + +run: +```bash +pytest .\\tests\\visualization\\circuit\\test_plt.py::TestCircuitPltVis --mpl +``` +""" + +# pylint: disable=missing-function-docstring +from math import pi + +import matplotlib +import pytest + +from cqlib import Circuit, Parameter +from cqlib.visualization.circuit import draw_mpl + + +class TestCircuitPltVis: + """ Test matplotlib visualization.""" + + def setup_method(self): + matplotlib.use('Agg') + + @pytest.mark.mpl_image_compare + def test_basic(self): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + figure = draw_mpl(circuit) + return figure + + @pytest.mark.mpl_image_compare + def test_gray(self): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + figure = draw_mpl(circuit, style='gray') + return figure + + @pytest.mark.mpl_image_compare + def test_parameter(self): + theta = Parameter('theta') + circuit = Circuit(2, parameters=[theta]) + circuit.h(0) + circuit.rx(1, theta) + circuit.cry(0, 1, theta) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_rotation_gates(self): + circuit = Circuit(3) + circuit.h(0) + circuit.rx(1, 0.1) + circuit.ry(2, 0.2) + circuit.rz(0, 0.3) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_moment(self): + circuit = Circuit(3) + circuit.h(1) + circuit.cx(0, 2) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_long_theta(self): + circuit = Circuit(3) + circuit.h(0) + circuit.rx(1, pi) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_phy_qubits(self): + circuit = Circuit([1, 7, 13]) + circuit.h(1) + circuit.cx(7, 13) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_swap(self): + circuit = Circuit(3) + circuit.swap(0, 1) + circuit.swap(1, 2) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_gates_0(self): + theta = Parameter('theta') + circuit = Circuit(5, parameters=[theta]) + circuit.h(0) + circuit.i(1, 100) + circuit.rx(2, 0.123) + circuit.crx(3, 4, theta) + circuit.rxy(0, 1, 0.123) + circuit.ry(1, 0.123) + circuit.cry(2, 3, 0.123) + circuit.rz(4, theta) + circuit.measure_all() + return draw_mpl(circuit) + + @pytest.mark.mpl_image_compare + def test_gates_1(self): + theta = Parameter('theta') + phi = Parameter('phi') + circuit = Circuit(5, parameters=[theta, phi]) + circuit.s(0) + circuit.sd(1) + circuit.swap(2, 3) + circuit.t(4) + circuit.td(0) + circuit.u(1, theta, phi, 0.123) + circuit.measure_all() + return draw_mpl(circuit) diff --git a/tests/visualization/circuit/test_text.py b/tests/visualization/circuit/test_text.py new file mode 100644 index 0000000..b3fc85f --- /dev/null +++ b/tests/visualization/circuit/test_text.py @@ -0,0 +1,335 @@ +# This code is part of cqlib. +# +# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE 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. + +""" test text visualization """ +from math import pi + +from cqlib.circuits.circuit import Circuit +from cqlib.circuits.parameter import Parameter +from cqlib.visualization.circuit.text import draw_text + +# pylint: disable=missing-function-docstring, trailing-whitespace + + +class TestCircuitTextVis: + """ test text visualization """ + + def test_basic(self): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ───H──■──M─ + │ + Q1: ──────X──M─ + + +""" + assert text == resp + + def test_parameter(self): + theta = Parameter('theta') + circuit = Circuit(2, parameters=[theta]) + circuit.h(0) + circuit.rx(1, theta) + circuit.cx(0, 1) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ───────H──────■──M─ + │ + Q1: ───RX(theta)──X──M─ + + +""" + assert text == resp + + def test_rotation_gates(self): + circuit = Circuit(3) + circuit.h(0) + circuit.rx(1, 0.1) + circuit.ry(2, 0.2) + circuit.rz(0, 0.3) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ──────H─────RZ(0.3)──M─ + + Q1: ───RX(0.1)─────M─────── + + Q2: ───RY(0.2)─────M─────── + + +""" + assert text == resp + + def test_barrier(self): + circuit = Circuit(3) + circuit.barrier(0, 2) + circuit.barrier(0, 1, 2) + text = draw_text(circuit) + resp = """ + Q0: ───│──│─ + │ + Q1: ──────│─ + │ │ + Q2: ───│──│─ + + +""" + assert text == resp + + def test_moment(self): + circuit = Circuit(3) + circuit.h(1) + circuit.cx(0, 2) + circuit.measure_all() + text = draw_text(circuit) + resp = """ ┌──┐ + Q0: ────■──M─ + │ + Q1: ───H┼──M─ + │ + Q2: ────X──M─ + └──┘ + +""" + assert text == resp + + def test_long_theta(self): + circuit = Circuit(3) + circuit.h(0) + circuit.rx(1, pi) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ─────────────H────────────M─ + + Q1: ───RX(3.141592653589793)──M─ + + Q2: ─────────────M────────────── + + +""" + assert text == resp + + def test_phy_qubits(self): + circuit = Circuit([1, 7, 13]) + circuit.h(1) + circuit.cx(7, 13) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q1: ───H──M─ + + Q7: ───■──M─ + │ + Q13: ───X──M─ + + +""" + assert text == resp + + def test_swap(self): + circuit = Circuit(3) + circuit.swap(0, 1) + circuit.swap(1, 2) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ───X──M──── + │ + Q1: ───X──X──M─ + │ + Q2: ──────X──M─ + + +""" + assert text == resp + + def test_width(self): + circuit = Circuit(3) + for _ in range(10): + circuit.h(0) + circuit.rx(2, pi / 2) + circuit.ry(1, 0.2) + circuit.barrier(*circuit.qubits) + circuit.measure_all() + text = draw_text(circuit, line_width=80) + # pylint: disable=line-too-long + resp = """ » + Q0: ─────────────H─────────────│────────────H─────────────│────────────H────────────» + │ │ » + Q1: ──────────RY(0.2)──────────│─────────RY(0.2)──────────│─────────RY(0.2)─────────» + │ │ » + Q2: ───RX(1.5707963267948966)──│──RX(1.5707963267948966)──│──RX(1.5707963267948966)─» + » + +« » +« Q0: ───│────────────H─────────────│────────────H─────────────│─» +« │ │ │ » +« Q1: ───│─────────RY(0.2)──────────│─────────RY(0.2)──────────│─» +« │ │ │ » +« Q2: ───│──RX(1.5707963267948966)──│──RX(1.5707963267948966)──│─» +« » + +« » +« Q0: ─────────────H─────────────│────────────H─────────────│────────────H────────────» +« │ │ » +« Q1: ──────────RY(0.2)──────────│─────────RY(0.2)──────────│─────────RY(0.2)─────────» +« │ │ » +« Q2: ───RX(1.5707963267948966)──│──RX(1.5707963267948966)──│──RX(1.5707963267948966)─» +« » + +« +« Q0: ───│────────────H─────────────│────────────H─────────────│──M─ +« │ │ │ +« Q1: ───│─────────RY(0.2)──────────│─────────RY(0.2)──────────│──M─ +« │ │ │ +« Q2: ───│──RX(1.5707963267948966)──│──RX(1.5707963267948966)──│──M─ +« + +""" + assert text == resp + + def test_one_line(self): + circuit = Circuit(5) + for _ in range(5): + circuit.x(0) + circuit.swap(3, 4) + circuit.rxy(1, 0.12, pi) + circuit.barrier(*circuit.qubits) + circuit.measure_all() + text = draw_text(circuit, line_width=-1) + resp = """ + Q0: ────────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│──M─ + │ │ │ │ │ + Q1: ───RXY(0.12,3.141592653589793)──│──RXY(0.12,3.141592653589793)──│──RXY(0.12,3.141592653589793)──│──RXY(0.12,3.141592653589793)──│──RXY(0.12,3.141592653589793)──│──M─ + │ │ │ │ │ + Q2: ────────────────────────────────│───────────────────────────────│───────────────────────────────│───────────────────────────────│───────────────────────────────│──M─ + │ │ │ │ │ + Q3: ────────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│──M─ + │ │ │ │ │ │ │ │ │ │ + Q4: ────────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│───────────────X───────────────│──M─ + + +""" + assert text == resp + + def test_i(self): + circuit = Circuit(3) + circuit.i(0, 100) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ───I(100)──M─ + + Q1: ─────M─────── + + Q2: ─────M─────── + + +""" + assert text == resp + + def test_gates_0(self): + theta = Parameter('theta') + circuit = Circuit(5, parameters=[theta]) + circuit.h(0) + circuit.i(1, 100) + circuit.rx(2, 0.123) + circuit.crx(3, 4, theta) + circuit.rxy(0, 1, 0.123) + circuit.ry(1, 0.123) + circuit.cry(2, 3, 0.123) + circuit.rz(4, theta) + circuit.measure_all() + text = draw_text(circuit, line_width=100) + resp = """ + Q0: ───────H──────RXY(1,0.123)──M─ + + Q1: ─────I(100)────RY(0.123)────M─ + + Q2: ───RX(0.123)───────■────────M─ + │ + Q3: ───────■───────RY(0.123)────M─ + │ + Q4: ───RX(theta)───RZ(theta)────M─ + + +""" + assert text == resp + + def test_gates_1(self): + theta = Parameter('theta') + phi = Parameter('phi') + circuit = Circuit(5, parameters=[theta, phi]) + circuit.s(0) + circuit.sd(1) + circuit.swap(2, 3) + circuit.t(4) + circuit.td(0) + circuit.u(1, theta, phi, 0.123) + circuit.measure_all() + text = draw_text(circuit) + resp = """ + Q0: ───S───────────TD──────────M─ + + Q1: ───SD──U(theta,phi,0.123)──M─ + + Q2: ───X───────────M───────────── + │ + Q3: ───X───────────M───────────── + + Q4: ───T───────────M───────────── + + +""" + assert text == resp + + def test_gates_2(self): + theta = Parameter('theta') + phi = Parameter('phi') + circuit = Circuit(5, parameters=[theta, phi]) + circuit.x(0) + circuit.cx(1, 2) + circuit.ccx(3, 4, 0) + circuit.x2p(1) + circuit.x2m(2) + circuit.xy(3, theta) + circuit.xy2p(4, theta) + circuit.xy2m(0, theta) + circuit.y(1) + circuit.y2p(2) + circuit.y2m(3) + circuit.cy(4, 0) + circuit.z(0) + circuit.cz(1, 2) + circuit.measure_all() + text = draw_text(circuit) + resp = """ ┌────┐ ┌────┐ + Q0: ───X──X─────XY2M(theta)──Y─────Z──M─ + │ │ + Q1: ───■──┼X2P───────Y───────┼─■───M──── + │ │ │ │ + Q2: ───X──┼X2M──────Y2P──────┼─■───M──── + │ │ + Q3: ──────■──────XY(theta)───┼Y2M──M──── + │ │ + Q4: ──────■─────XY2P(theta)──■─────M──── + └────┘ └────┘ + +""" + assert text == resp -- Gitee