From 7fac964d87dcc7dd857f819de060dc11ccf4755b Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 02:44:17 +0000 Subject: [PATCH 01/29] basic one qubit drawer --- pyproject.toml | 2 +- src/pyqasm/printer.py | 131 ++++++++++++++++++++++++++++++++++++ tests/qasm3/test_printer.py | 42 ++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/pyqasm/printer.py create mode 100644 tests/qasm3/test_printer.py diff --git a/pyproject.toml b/pyproject.toml index ae164a32..333580ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", ] -dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] +dependencies = ["numpy", "matplotlib", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.urls] "Source Code" = "https://github.com/qBraid/pyqasm" diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py new file mode 100644 index 00000000..15eed564 --- /dev/null +++ b/src/pyqasm/printer.py @@ -0,0 +1,131 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of pyqasm +# +# Pyqasm is free software released under the GNU General Public License v3 +# or later. You can redistribute and/or modify it under the terms of the GPL v3. +# See the LICENSE file in the project root or . +# +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + +""" +Module with analysis functions for QASM visitor + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional, Union +from pyqasm.modules import Qasm2Module, Qasm3Module, QasmModule +from pyqasm.maps import ONE_QUBIT_OP_MAP, TWO_QUBIT_OP_MAP, THREE_QUBIT_OP_MAP, FOUR_QUBIT_OP_MAP, FIVE_QUBIT_OP_MAP +from pyqasm.expressions import Qasm3ExprEvaluator +import matplotlib as mpl +from matplotlib import pyplot as plt +import openqasm3.ast as ast + +def draw(module: QasmModule, output="mpl"): + if isinstance(module, Qasm2Module): + module: Qasm3Module = module.to_qasm3() + if output == "mpl": + _draw_mpl(module) + else: + raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") + +def _draw_mpl(module: Qasm3Module) -> plt.Figure: + module.unroll() + module.remove_includes() + module.remove_barriers() + module.remove_measurements() + + n_lines = module._num_qubits + module._num_clbits + statements = module._statements + + fig, ax = plt.subplots(figsize=(10, n_lines * 0.7)) + line_nums = dict() + line_num = -1 + max_depth = 0 + + for clbit_reg in module._classical_registers.keys(): + size = module._classical_registers[clbit_reg] + line_num += size + for i in range(size): + line_nums[(clbit_reg, i)] = line_num + line_num -= 1 + line_num += size + + for qubit_reg in module._qubit_registers.keys(): + size = module._qubit_registers[qubit_reg] + line_num += size + for i in range(size): + line_nums[(qubit_reg, i)] = line_num + depth = module._qubit_depths[(qubit_reg, i)]._total_ops() + max_depth = max(max_depth, depth) + line_num -= 1 + line_num += size + + + for qubit_reg in module._qubit_registers.keys(): + for i in range(size): + line_num = line_nums[(qubit_reg, i)] + depth = module._qubit_depths[(qubit_reg, i)]._total_ops() + _draw_mpl_qubit((qubit_reg, i), ax, line_num, max_depth) + + for clbit_reg in module._classical_registers.keys(): + for i in range(size): + line_num = line_nums[(clbit_reg, i)] + _draw_mpl_bit((clbit_reg, i), ax, line_num, max_depth) + + depths = dict() + for k in line_nums.keys(): + depths[k] = -1 + + # Draw gates + for i, statement in enumerate(statements): + if "Declaration" in str(type(statement)): continue + if isinstance(statement, ast.QuantumGate): + qubits = [(q.name.name,Qasm3ExprEvaluator.evaluate_expression(q.indices[0][0])[0]) for q in statement.qubits] + draw_depth = 1 + max([depths[q] for q in qubits]) + for q in qubits: + depths[q] = draw_depth + _draw_mpl_gate(statement, ax, [line_nums[q] for q in qubits], draw_depth) + elif isinstance(statement, ast.QuantumMeasurement): + pass + else: + raise NotImplementedError(f"Unsupported statement: {statement}") + + # Configure plot + ax.set_ylim(-0.5, n_lines - 0.5) + ax.set_xlim(-1, len(statements)) + ax.axis('off') + ax.set_title('Quantum Circuit') + + plt.tight_layout() + return fig + +def _draw_mpl_bit(bit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): + ax.hlines(y=line_num, xmin=0, xmax=max_depth, color='gray', linestyle='-') + ax.text(-0.5, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') + +def _draw_mpl_qubit(qubit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): + ax.hlines(y=line_num, xmin=0, xmax=max_depth, color='black', linestyle='-') + ax.text(-0.5, line_num, f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') + +def _draw_mpl_gate(gate: ast.QuantumGate, ax: plt.Axes, lines: list[int], depth: int): + print("DRAW", gate.name.name, lines, depth) + if gate.name.name in ONE_QUBIT_OP_MAP: + ax.text(depth, lines[0], gate.name.name, ha='center', va='center', + bbox=dict(facecolor='white', edgecolor='black')) + elif gate.name.name in TWO_QUBIT_OP_MAP: + pass + # q1_idx = module.qubits.index(qubits[0]) + # q2_idx = module.qubits.index(qubits[1]) + # min_idx = min(q1_idx, q2_idx) + # max_idx = max(q1_idx, q2_idx) + + # # Draw vertical connection + # ax.vlines(x=i, ymin=min_idx, ymax=max_idx, color='black') + # # Draw gate symbol + # ax.text(i, (min_idx + max_idx)/2, gate_name, ha='center', va='center', + # bbox=dict(facecolor='white', edgecolor='black')) + else: + pass + \ No newline at end of file diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py new file mode 100644 index 00000000..d95c2660 --- /dev/null +++ b/tests/qasm3/test_printer.py @@ -0,0 +1,42 @@ +import pytest +from pyqasm.entrypoint import loads, load +from pyqasm.printer import _draw_mpl + +import matplotlib.pyplot as plt +import random +from qbraid import random_circuit, transpile + +def test_simple_circuit_drawing(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit[2] b; + h q[0]; + z q[1]; + y q[0]; + rz(pi/0.1) q[0]; + cx q[0], q[1]; + b = measure q; + """ + + circuit = loads(qasm) + fig = _draw_mpl(circuit) + + ax = fig.gca() + plt.savefig("test.png") + assert len(ax.texts) > 0 + plt.close(fig) + assert False + +@pytest.mark.skip(reason="Not implemented drawing for all gates") +def test_random_qiskit_circuit(): + qiskit_circuit = random_circuit("qiskit", measure=random.choice([True, False])) + qasm_str = transpile(qiskit_circuit, random.choice(["qasm2", "qasm3"])) + + module = load(qasm_str) + fig = module.draw() + + assert isinstance(fig, plt.Figure) + ax = fig.gca() + assert len(ax.texts) > 0 + plt.close(fig) From 40688bf8efe53f466c5dff6e50f795cebd43974e Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 03:44:07 +0000 Subject: [PATCH 02/29] controlled gates + swap gate --- src/pyqasm/maps.py | 13 +++++++ src/pyqasm/printer.py | 76 +++++++++++++++++++++++-------------- tests/qasm3/test_printer.py | 44 ++++++++++----------- 3 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index 89752a72..c5dd22e8 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1235,6 +1235,19 @@ def map_qasm_inv_op_to_callable(op_name: str): ) raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") +REV_CTRL_GATE_MAP = { + "cx": "x", + "cy": "y", + "cz": "z", + "crx": "rx", + "cry": "ry", + "crz": "rz", + "cp": "p", + "ch": "h", + "cu": "u", + "cswap": "swap", + "ccx": "cx", +} # pylint: disable=inconsistent-return-statements def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value): diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 15eed564..ac3d4513 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -16,17 +16,20 @@ from typing import TYPE_CHECKING, Any, Optional, Union from pyqasm.modules import Qasm2Module, Qasm3Module, QasmModule -from pyqasm.maps import ONE_QUBIT_OP_MAP, TWO_QUBIT_OP_MAP, THREE_QUBIT_OP_MAP, FOUR_QUBIT_OP_MAP, FIVE_QUBIT_OP_MAP +from pyqasm.maps import ONE_QUBIT_OP_MAP, ONE_QUBIT_ROTATION_MAP, TWO_QUBIT_OP_MAP, THREE_QUBIT_OP_MAP, FOUR_QUBIT_OP_MAP, FIVE_QUBIT_OP_MAP, REV_CTRL_GATE_MAP from pyqasm.expressions import Qasm3ExprEvaluator import matplotlib as mpl from matplotlib import pyplot as plt import openqasm3.ast as ast +DEFAULT_GATE_COLOR = '#d4b6e8' +HADAMARD_GATE_COLOR = '#f0a6a6' + def draw(module: QasmModule, output="mpl"): if isinstance(module, Qasm2Module): module: Qasm3Module = module.to_qasm3() if output == "mpl": - _draw_mpl(module) + return _draw_mpl(module) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") @@ -39,7 +42,6 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: n_lines = module._num_qubits + module._num_clbits statements = module._statements - fig, ax = plt.subplots(figsize=(10, n_lines * 0.7)) line_nums = dict() line_num = -1 max_depth = 0 @@ -62,6 +64,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: line_num -= 1 line_num += size + fig, ax = plt.subplots(figsize=(10, n_lines * 0.7)) for qubit_reg in module._qubit_registers.keys(): for i in range(size): @@ -82,50 +85,67 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: for i, statement in enumerate(statements): if "Declaration" in str(type(statement)): continue if isinstance(statement, ast.QuantumGate): + args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] qubits = [(q.name.name,Qasm3ExprEvaluator.evaluate_expression(q.indices[0][0])[0]) for q in statement.qubits] draw_depth = 1 + max([depths[q] for q in qubits]) for q in qubits: depths[q] = draw_depth - _draw_mpl_gate(statement, ax, [line_nums[q] for q in qubits], draw_depth) + _draw_mpl_gate(statement, ax, [line_nums[q] for q in qubits], draw_depth, args) elif isinstance(statement, ast.QuantumMeasurement): pass + elif isinstance(statement, ast.QuantumBarrier): + pass + elif isinstance(statement, ast.QuantumReset): + pass else: raise NotImplementedError(f"Unsupported statement: {statement}") - # Configure plot ax.set_ylim(-0.5, n_lines - 0.5) - ax.set_xlim(-1, len(statements)) + ax.set_xlim(-0.5, max_depth) ax.axis('off') - ax.set_title('Quantum Circuit') plt.tight_layout() return fig def _draw_mpl_bit(bit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=0, xmax=max_depth, color='gray', linestyle='-') - ax.text(-0.5, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') + ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth, color='gray', linestyle='-') + ax.text(-0.25, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') def _draw_mpl_qubit(qubit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=0, xmax=max_depth, color='black', linestyle='-') - ax.text(-0.5, line_num, f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') + ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth, color='black', linestyle='-') + ax.text(-0.25, line_num, f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') -def _draw_mpl_gate(gate: ast.QuantumGate, ax: plt.Axes, lines: list[int], depth: int): +def _draw_mpl_gate(gate: ast.QuantumGate, ax: plt.Axes, lines: list[int], depth: int, args: list[Any]): print("DRAW", gate.name.name, lines, depth) - if gate.name.name in ONE_QUBIT_OP_MAP: - ax.text(depth, lines[0], gate.name.name, ha='center', va='center', - bbox=dict(facecolor='white', edgecolor='black')) + if gate.name.name in ONE_QUBIT_OP_MAP or gate.name.name in ONE_QUBIT_ROTATION_MAP: + _draw_mpl_one_qubit_gate(gate, ax, lines[0], depth, args) elif gate.name.name in TWO_QUBIT_OP_MAP: - pass - # q1_idx = module.qubits.index(qubits[0]) - # q2_idx = module.qubits.index(qubits[1]) - # min_idx = min(q1_idx, q2_idx) - # max_idx = max(q1_idx, q2_idx) - - # # Draw vertical connection - # ax.vlines(x=i, ymin=min_idx, ymax=max_idx, color='black') - # # Draw gate symbol - # ax.text(i, (min_idx + max_idx)/2, gate_name, ha='center', va='center', - # bbox=dict(facecolor='white', edgecolor='black')) + if gate.name.name in REV_CTRL_GATE_MAP: + gate.name.name = REV_CTRL_GATE_MAP[gate.name.name] + _draw_mpl_one_qubit_gate(gate, ax, lines[1], depth, args) + _draw_mpl_control(ax, lines[0], lines[1], depth) + elif gate.name.name == 'swap': + _draw_mpl_swap(ax, lines[0], lines[1], depth) + else: + raise NotImplementedError(f"Unsupported gate: {gate.name.name}") else: - pass - \ No newline at end of file + raise NotImplementedError(f"Unsupported gate: {gate.name.name}") + +def _draw_mpl_one_qubit_gate(gate: ast.QuantumGate, ax: plt.Axes, line: int, depth: int, args: list[Any]): + color = DEFAULT_GATE_COLOR + if gate.name.name == 'h': + color = HADAMARD_GATE_COLOR + text = gate.name.name.upper() + if len(args) > 0: + text += f"\n({', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])})" + ax.text(depth, line, text, ha='center', va='center', + bbox=dict(facecolor=color, edgecolor='none')) + +def _draw_mpl_control(ax: plt.Axes, ctrl: int, target: int, depth: int): + ax.vlines(x=depth, ymin=min(ctrl, target), ymax=max(ctrl, target), color='black', linestyle='-') + ax.plot(depth, ctrl, 'ko', markersize=8, markerfacecolor='black') + +def _draw_mpl_swap(ax: plt.Axes, ctrl: int, target: int, depth: int): + ax.vlines(x=depth, ymin=min(ctrl, target), ymax=max(ctrl, target), color='black', linestyle='-') + ax.plot(depth, ctrl, 'x', markersize=8, color='black') + ax.plot(depth, target, 'x', markersize=8, color='black') \ No newline at end of file diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index d95c2660..34df2d89 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -1,12 +1,18 @@ import pytest from pyqasm.entrypoint import loads, load -from pyqasm.printer import _draw_mpl +from pyqasm.printer import draw import matplotlib.pyplot as plt import random from qbraid import random_circuit, transpile -def test_simple_circuit_drawing(): +def _check_fig(circuit, fig): + ax = fig.gca() + # plt.savefig("test.png") + assert len(ax.texts) > 0 + plt.close(fig) + +def test_simple(): qasm = """OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; @@ -14,29 +20,17 @@ def test_simple_circuit_drawing(): h q[0]; z q[1]; y q[0]; - rz(pi/0.1) q[0]; + rz(pi/1.1) q[0]; cx q[0], q[1]; + swap q[0], q[1]; b = measure q; """ - - circuit = loads(qasm) - fig = _draw_mpl(circuit) - - ax = fig.gca() - plt.savefig("test.png") - assert len(ax.texts) > 0 - plt.close(fig) - assert False - -@pytest.mark.skip(reason="Not implemented drawing for all gates") -def test_random_qiskit_circuit(): - qiskit_circuit = random_circuit("qiskit", measure=random.choice([True, False])) - qasm_str = transpile(qiskit_circuit, random.choice(["qasm2", "qasm3"])) - - module = load(qasm_str) - fig = module.draw() - - assert isinstance(fig, plt.Figure) - ax = fig.gca() - assert len(ax.texts) > 0 - plt.close(fig) + circ = loads(qasm) + fig = draw(circ) + _check_fig(circ, fig) + +def test_random(): + circ = random_circuit("qasm3", measure=random.choice([True, False])) + module = loads(circ) + fig = draw(module) + _check_fig(circ, fig) From 197f30972cb5d21608b34522df8ca60d59890091 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 04:05:33 +0000 Subject: [PATCH 03/29] .draw() on module --- src/pyqasm/modules/base.py | 4 ++++ src/pyqasm/modules/qasm2.py | 5 +++++ src/pyqasm/modules/qasm3.py | 6 +++++- src/pyqasm/printer.py | 40 ++++++++++++++++++++++++------------- tests/qasm3/test_printer.py | 13 ++++++------ 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 488ced71..b27f4822 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -551,3 +551,7 @@ def accept(self, visitor): Args: visitor (QasmVisitor): The visitor to accept """ + + @abstractmethod + def draw(self): + """Draw the module""" diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index f17e55eb..2eed4699 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -23,6 +23,7 @@ from pyqasm.exceptions import ValidationError from pyqasm.modules.base import QasmModule from pyqasm.modules.qasm3 import Qasm3Module +from pyqasm.printer import draw class Qasm2Module(QasmModule): @@ -105,3 +106,7 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self.unrolled_ast.statements = final_stmt_list + + def draw(self): + """Draw the module""" + return draw(self.to_qasm3()) diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index fb193c40..c2d12426 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,7 +16,7 @@ from openqasm3.printer import dumps from pyqasm.modules.base import QasmModule - +from pyqasm.printer import draw class Qasm3Module(QasmModule): """ @@ -48,3 +48,7 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self._unrolled_ast.statements = final_stmt_list + + def draw(self): + """Draw the module""" + return draw(self) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index ac3d4513..932ab181 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -15,19 +15,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional, Union -from pyqasm.modules import Qasm2Module, Qasm3Module, QasmModule from pyqasm.maps import ONE_QUBIT_OP_MAP, ONE_QUBIT_ROTATION_MAP, TWO_QUBIT_OP_MAP, THREE_QUBIT_OP_MAP, FOUR_QUBIT_OP_MAP, FIVE_QUBIT_OP_MAP, REV_CTRL_GATE_MAP from pyqasm.expressions import Qasm3ExprEvaluator import matplotlib as mpl from matplotlib import pyplot as plt import openqasm3.ast as ast +if TYPE_CHECKING: + from pyqasm.modules.base import Qasm3Module + DEFAULT_GATE_COLOR = '#d4b6e8' HADAMARD_GATE_COLOR = '#f0a6a6' -def draw(module: QasmModule, output="mpl"): - if isinstance(module, Qasm2Module): - module: Qasm3Module = module.to_qasm3() +def draw(module: Qasm3Module, output="mpl"): if output == "mpl": return _draw_mpl(module) else: @@ -86,13 +86,15 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: if "Declaration" in str(type(statement)): continue if isinstance(statement, ast.QuantumGate): args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] - qubits = [(q.name.name,Qasm3ExprEvaluator.evaluate_expression(q.indices[0][0])[0]) for q in statement.qubits] + qubits = [_identifier_to_key(q) for q in statement.qubits] draw_depth = 1 + max([depths[q] for q in qubits]) for q in qubits: depths[q] = draw_depth _draw_mpl_gate(statement, ax, [line_nums[q] for q in qubits], draw_depth, args) - elif isinstance(statement, ast.QuantumMeasurement): - pass + elif isinstance(statement, ast.QuantumMeasurementStatement): + qubit_key = _identifier_to_key(statement.measure.qubit) + target_key = _identifier_to_key(statement.target) + _draw_mpl_measurement(ax, line_nums[qubit_key], line_nums[target_key], draw_depth) elif isinstance(statement, ast.QuantumBarrier): pass elif isinstance(statement, ast.QuantumReset): @@ -107,6 +109,12 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: plt.tight_layout() return fig +def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tuple[str, int]: + if isinstance(identifier, ast.Identifier): + return identifier.name, -1 + else: + return identifier.name.name, Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0] + def _draw_mpl_bit(bit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth, color='gray', linestyle='-') ax.text(-0.25, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') @@ -141,11 +149,15 @@ def _draw_mpl_one_qubit_gate(gate: ast.QuantumGate, ax: plt.Axes, line: int, dep ax.text(depth, line, text, ha='center', va='center', bbox=dict(facecolor=color, edgecolor='none')) -def _draw_mpl_control(ax: plt.Axes, ctrl: int, target: int, depth: int): - ax.vlines(x=depth, ymin=min(ctrl, target), ymax=max(ctrl, target), color='black', linestyle='-') - ax.plot(depth, ctrl, 'ko', markersize=8, markerfacecolor='black') +def _draw_mpl_control(ax: plt.Axes, ctrl_line: int, target_line: int, depth: int): + ax.vlines(x=depth, ymin=min(ctrl_line, target_line), ymax=max(ctrl_line, target_line), color='black', linestyle='-') + ax.plot(depth, ctrl_line, 'ko', markersize=8, markerfacecolor='black') -def _draw_mpl_swap(ax: plt.Axes, ctrl: int, target: int, depth: int): - ax.vlines(x=depth, ymin=min(ctrl, target), ymax=max(ctrl, target), color='black', linestyle='-') - ax.plot(depth, ctrl, 'x', markersize=8, color='black') - ax.plot(depth, target, 'x', markersize=8, color='black') \ No newline at end of file +def _draw_mpl_swap(ax: plt.Axes, ctrl_line: int, target_line: int, depth: int): + ax.vlines(x=depth, ymin=min(ctrl_line, target_line), ymax=max(ctrl_line, target_line), color='black', linestyle='-') + ax.plot(depth, ctrl_line, 'x', markersize=8, color='black') + ax.plot(depth, target_line, 'x', markersize=8, color='black') + +def _draw_mpl_measurement(ax: plt.Axes, qbit_line: int, cbit_line: int, depth: int): + ax.plot(depth, qbit_line, 'x', markersize=8, color='black') + ax.plot(depth, cbit_line, 'x', markersize=8, color='black') diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index 34df2d89..85899a4b 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -6,9 +6,9 @@ import random from qbraid import random_circuit, transpile -def _check_fig(circuit, fig): +def _check_fig(circ, fig): ax = fig.gca() - # plt.savefig("test.png") + plt.savefig("test.png") assert len(ax.texts) > 0 plt.close(fig) @@ -26,11 +26,12 @@ def test_simple(): b = measure q; """ circ = loads(qasm) - fig = draw(circ) + fig = circ.draw() _check_fig(circ, fig) def test_random(): - circ = random_circuit("qasm3", measure=random.choice([True, False])) - module = loads(circ) - fig = draw(module) + circ = random_circuit("qiskit", measure=random.choice([True, False])) + qasm_str = transpile(circ, random.choice(["qasm2", "qasm3"])) + module = loads(qasm_str) + fig = module.draw() _check_fig(circ, fig) From cd6d5eb3a915cb5155a0f45f3117499203d8fad0 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 04:17:58 +0000 Subject: [PATCH 04/29] measurement --- src/pyqasm/printer.py | 15 +++++++++------ tests/qasm3/test_printer.py | 1 - 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 932ab181..4ebd77ef 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -37,7 +37,6 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: module.unroll() module.remove_includes() module.remove_barriers() - module.remove_measurements() n_lines = module._num_qubits + module._num_clbits statements = module._statements @@ -94,6 +93,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) target_key = _identifier_to_key(statement.target) + draw_depth = 1 + max([depths[q] for q in qubits]) _draw_mpl_measurement(ax, line_nums[qubit_key], line_nums[target_key], draw_depth) elif isinstance(statement, ast.QuantumBarrier): pass @@ -103,7 +103,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: raise NotImplementedError(f"Unsupported statement: {statement}") ax.set_ylim(-0.5, n_lines - 0.5) - ax.set_xlim(-0.5, max_depth) + ax.set_xlim(-0.5, max_depth + 0.5) ax.axis('off') plt.tight_layout() @@ -116,11 +116,11 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu return identifier.name.name, Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0] def _draw_mpl_bit(bit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth, color='gray', linestyle='-') + ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth+0.25, color='gray', linestyle='-') ax.text(-0.25, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') def _draw_mpl_qubit(qubit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth, color='black', linestyle='-') + ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth+0.25, color='black', linestyle='-') ax.text(-0.25, line_num, f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') def _draw_mpl_gate(gate: ast.QuantumGate, ax: plt.Axes, lines: list[int], depth: int, args: list[Any]): @@ -159,5 +159,8 @@ def _draw_mpl_swap(ax: plt.Axes, ctrl_line: int, target_line: int, depth: int): ax.plot(depth, target_line, 'x', markersize=8, color='black') def _draw_mpl_measurement(ax: plt.Axes, qbit_line: int, cbit_line: int, depth: int): - ax.plot(depth, qbit_line, 'x', markersize=8, color='black') - ax.plot(depth, cbit_line, 'x', markersize=8, color='black') + ax.text(depth, qbit_line, 'M', ha='center', va='center', + bbox=dict(facecolor='gray', edgecolor='none')) + ax.vlines(x=depth-0.01, ymin=min(qbit_line, cbit_line), ymax=max(qbit_line, cbit_line), color='gray', linestyle='-') + ax.vlines(x=depth+0.01, ymin=min(qbit_line, cbit_line), ymax=max(qbit_line, cbit_line), color='gray', linestyle='-') + ax.plot(depth, cbit_line+0.05, 'v', markersize=8, color='gray') diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index 85899a4b..62db78e7 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -8,7 +8,6 @@ def _check_fig(circ, fig): ax = fig.gca() - plt.savefig("test.png") assert len(ax.texts) > 0 plt.close(fig) From c79b46d1f7e9f6eb828f06294c4eea80969b652f Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 01:54:20 -0800 Subject: [PATCH 05/29] moment based drawing --- src/pyqasm/printer.py | 175 ++++++++++++++++++++++++------------ test.png | Bin 0 -> 10508 bytes tests/qasm3/test_printer.py | 3 +- 3 files changed, 121 insertions(+), 57 deletions(-) create mode 100644 test.png diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 4ebd77ef..1c540c57 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -27,6 +27,11 @@ DEFAULT_GATE_COLOR = '#d4b6e8' HADAMARD_GATE_COLOR = '#f0a6a6' +GATE_BOX_WIDTH, GATE_BOX_HEIGHT = 0.6, 0.6 +GATE_SPACING = 0.2 +LINE_SPACING = 0.6 +TEXT_MARGIN = 0.6 + def draw(module: Qasm3Module, output="mpl"): if output == "mpl": return _draw_mpl(module) @@ -41,6 +46,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: n_lines = module._num_qubits + module._num_clbits statements = module._statements + # compute line numbers per qubit + max depth line_nums = dict() line_num = -1 max_depth = 0 @@ -62,51 +68,71 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: max_depth = max(max_depth, depth) line_num -= 1 line_num += size - - fig, ax = plt.subplots(figsize=(10, n_lines * 0.7)) - - for qubit_reg in module._qubit_registers.keys(): - for i in range(size): - line_num = line_nums[(qubit_reg, i)] - depth = module._qubit_depths[(qubit_reg, i)]._total_ops() - _draw_mpl_qubit((qubit_reg, i), ax, line_num, max_depth) - for clbit_reg in module._classical_registers.keys(): - for i in range(size): - line_num = line_nums[(clbit_reg, i)] - _draw_mpl_bit((clbit_reg, i), ax, line_num, max_depth) - + # compute moments depths = dict() for k in line_nums.keys(): depths[k] = -1 - # Draw gates - for i, statement in enumerate(statements): + moments = [] + for statement in statements: if "Declaration" in str(type(statement)): continue if isinstance(statement, ast.QuantumGate): - args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] qubits = [_identifier_to_key(q) for q in statement.qubits] - draw_depth = 1 + max([depths[q] for q in qubits]) + depth = 1 + max([depths[q] for q in qubits]) for q in qubits: - depths[q] = draw_depth - _draw_mpl_gate(statement, ax, [line_nums[q] for q in qubits], draw_depth, args) + depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) target_key = _identifier_to_key(statement.target) - draw_depth = 1 + max([depths[q] for q in qubits]) - _draw_mpl_measurement(ax, line_nums[qubit_key], line_nums[target_key], draw_depth) + depth = 1 + max(depths[qubit_key], depths[target_key]) + for k in [qubit_key, target_key]: + depths[k] = depth elif isinstance(statement, ast.QuantumBarrier): pass elif isinstance(statement, ast.QuantumReset): pass else: raise NotImplementedError(f"Unsupported statement: {statement}") - - ax.set_ylim(-0.5, n_lines - 0.5) - ax.set_xlim(-0.5, max_depth + 0.5) + + if depth >= len(moments): + moments.append([]) + moments[depth].append(statement) + + width = 0 + for moment in moments: + width += _mpl_get_moment_width(moment) + width += TEXT_MARGIN + + fig, ax = plt.subplots(figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))) + ax.set_ylim(-GATE_BOX_HEIGHT/2, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1) - GATE_BOX_HEIGHT/2) + ax.set_xlim(0, width) ax.axis('off') + # ax.set_aspect('equal') + # plt.tight_layout() + + x = 0 + for k in module._qubit_registers.keys(): + for i in range(module._qubit_registers[k]): + line_num = line_nums[(k, i)] + _mpl_draw_qubit_label((k, i), line_num, ax, x) + for k in module._classical_registers.keys(): + for i in range(module._classical_registers[k]): + line_num = line_nums[(k, i)] + _mpl_draw_clbit_label((k, i), line_num, ax, x) + x += TEXT_MARGIN + x0 = x + for moment in moments: + dx = _mpl_get_moment_width(moment) + _mpl_draw_lines(dx, line_nums, ax, x) + x += dx + x = x0 + for moment in moments: + dx = _mpl_get_moment_width(moment) + for statement in moment: + _mpl_draw_statement(statement, line_nums, ax, x) + x += dx - plt.tight_layout() return fig def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tuple[str, int]: @@ -115,52 +141,89 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu else: return identifier.name.name, Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0] -def _draw_mpl_bit(bit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth+0.25, color='gray', linestyle='-') - ax.text(-0.25, line_num, f'{bit[0]}[{bit[1]}]', ha='right', va='center') +def _mpl_line_to_y(line_num: int) -> float: + return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) -def _draw_mpl_qubit(qubit: tuple[str, int], ax: plt.Axes, line_num: int, max_depth: int): - ax.hlines(y=line_num, xmin=-0.125, xmax=max_depth+0.25, color='black', linestyle='-') - ax.text(-0.25, line_num, f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') +def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): + ax.text(x, _mpl_line_to_y(line_num), f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') -def _draw_mpl_gate(gate: ast.QuantumGate, ax: plt.Axes, lines: list[int], depth: int, args: list[Any]): - print("DRAW", gate.name.name, lines, depth) +def _mpl_draw_clbit_label(clbit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): + ax.text(x, _mpl_line_to_y(line_num), f'{clbit[0]}[{clbit[1]}]', ha='right', va='center') + +def _mpl_draw_lines(width, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float): + for k in line_nums.keys(): + y = _mpl_line_to_y(line_nums[k]) + ax.hlines(xmin=x-width/2, xmax=x+width/2, y=y, color='black', linestyle='-', zorder=-10) + +def _mpl_get_moment_width(moment: list[ast.QuantumStatement]) -> float: + return max([_mpl_get_statement_width(s) for s in moment]) + +def _mpl_get_statement_width(statement: ast.QuantumStatement) -> float: + return GATE_BOX_WIDTH + GATE_SPACING + +def _mpl_draw_statement(statement: ast.QuantumStatement, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float): + if isinstance(statement, ast.QuantumGate): + args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] + lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] + _mpl_draw_gate(statement, args, lines, ax, x) + elif isinstance(statement, ast.QuantumMeasurementStatement): + qubit_key = _identifier_to_key(statement.measure.qubit) + target_key = _identifier_to_key(statement.target) + _mpl_draw_measurement(line_nums[qubit_key], line_nums[target_key], ax, x) + else: + raise NotImplementedError(f"Unsupported statement: {statement}") + +def _mpl_draw_gate(gate: ast.QuantumGate, args: list[Any], lines: list[int], ax: plt.Axes, x: float): + print("DRAW", gate.name.name, lines, x) if gate.name.name in ONE_QUBIT_OP_MAP or gate.name.name in ONE_QUBIT_ROTATION_MAP: - _draw_mpl_one_qubit_gate(gate, ax, lines[0], depth, args) + _draw_mpl_one_qubit_gate(gate, args, lines[0], ax, x) elif gate.name.name in TWO_QUBIT_OP_MAP: if gate.name.name in REV_CTRL_GATE_MAP: gate.name.name = REV_CTRL_GATE_MAP[gate.name.name] - _draw_mpl_one_qubit_gate(gate, ax, lines[1], depth, args) - _draw_mpl_control(ax, lines[0], lines[1], depth) + _draw_mpl_one_qubit_gate(gate, args, lines[1], ax, x) + _draw_mpl_control(lines[0], lines[1], ax, x) elif gate.name.name == 'swap': - _draw_mpl_swap(ax, lines[0], lines[1], depth) + _draw_mpl_swap(lines[0], lines[1], ax, x) else: raise NotImplementedError(f"Unsupported gate: {gate.name.name}") else: raise NotImplementedError(f"Unsupported gate: {gate.name.name}") - -def _draw_mpl_one_qubit_gate(gate: ast.QuantumGate, ax: plt.Axes, line: int, depth: int, args: list[Any]): + +# TODO: switch to moment based system. go progressively, calculating required width for each moment, center the rest. this makes position calculations not to bad. if we overflow, start a new figure. + +def _draw_mpl_one_qubit_gate(gate: ast.QuantumGate, args: list[Any], line: int, ax: plt.Axes, x: float): color = DEFAULT_GATE_COLOR if gate.name.name == 'h': color = HADAMARD_GATE_COLOR text = gate.name.name.upper() if len(args) > 0: text += f"\n({', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])})" - ax.text(depth, line, text, ha='center', va='center', - bbox=dict(facecolor=color, edgecolor='none')) - -def _draw_mpl_control(ax: plt.Axes, ctrl_line: int, target_line: int, depth: int): - ax.vlines(x=depth, ymin=min(ctrl_line, target_line), ymax=max(ctrl_line, target_line), color='black', linestyle='-') - ax.plot(depth, ctrl_line, 'ko', markersize=8, markerfacecolor='black') -def _draw_mpl_swap(ax: plt.Axes, ctrl_line: int, target_line: int, depth: int): - ax.vlines(x=depth, ymin=min(ctrl_line, target_line), ymax=max(ctrl_line, target_line), color='black', linestyle='-') - ax.plot(depth, ctrl_line, 'x', markersize=8, color='black') - ax.plot(depth, target_line, 'x', markersize=8, color='black') - -def _draw_mpl_measurement(ax: plt.Axes, qbit_line: int, cbit_line: int, depth: int): - ax.text(depth, qbit_line, 'M', ha='center', va='center', - bbox=dict(facecolor='gray', edgecolor='none')) - ax.vlines(x=depth-0.01, ymin=min(qbit_line, cbit_line), ymax=max(qbit_line, cbit_line), color='gray', linestyle='-') - ax.vlines(x=depth+0.01, ymin=min(qbit_line, cbit_line), ymax=max(qbit_line, cbit_line), color='gray', linestyle='-') - ax.plot(depth, cbit_line+0.05, 'v', markersize=8, color='gray') + y = _mpl_line_to_y(line) + rect = plt.Rectangle((x - GATE_BOX_WIDTH/2, y - GATE_BOX_HEIGHT/2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor=color, edgecolor='none') + ax.add_patch(rect) + ax.text(x, y, text, ha='center', va='center') + +def _draw_mpl_control(ctrl_line: int, target_line: int, ax: plt.Axes, x: float): + y1 = _mpl_line_to_y(ctrl_line) + y2 = _mpl_line_to_y(target_line) + ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color='black', linestyle='-', zorder=-1) + ax.plot(x, y1, 'ko', markersize=8, markerfacecolor='black') + +def _draw_mpl_swap(line1: int, line2: int, ax: plt.Axes, x: float): + y1 = _mpl_line_to_y(line1) + y2 = _mpl_line_to_y(line2) + ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color='black', linestyle='-') + ax.plot(x, y1, 'x', markersize=8, color='black') + ax.plot(x, y2, 'x', markersize=8, color='black') + +def _mpl_draw_measurement(qbit_line: int, cbit_line: int, ax: plt.Axes, x: float): + y1 = _mpl_line_to_y(qbit_line) + y2 = _mpl_line_to_y(cbit_line) + + rect = plt.Rectangle((x - GATE_BOX_WIDTH/2, y1 - GATE_BOX_HEIGHT/2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor='gray', edgecolor='none') + ax.add_patch(rect) + ax.text(x, y1, 'M', ha='center', va='center') + ax.vlines(x=x-0.025, ymin=min(y1, y2), ymax=max(y1, y2), color='gray', linestyle='-', zorder=-1) + ax.vlines(x=x+0.025, ymin=min(y1, y2), ymax=max(y1, y2), color='gray', linestyle='-', zorder=-1) + ax.plot(x, y2+0.1, 'v', markersize=16, color='gray') diff --git a/test.png b/test.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed4c331b3c6ee6ef96aae9face67bf38d7a2682 GIT binary patch literal 10508 zcmeHt2T;@7x^LVTtZ>{?6hyWKDJrNm0WpFi0@4XRL{MsI(u))~A_@W$K$Ors385DW zkRV9!CA82xgn)pd1;|_3=e&F8y_q*}?wpx>?%cV|F#iOS_5ZJLec!i!Wqr_8S2}Z& z`y>PcIivjau{H#97z=?Mx_SH<_zQm%e;c^SxGEaB>Nr}udYCy|K-A1!o$MT4?QG2d zaJO)Fv2k<|6S;F+l)B96{hqS4L<&R~=iPEQS8AP_b)=I^0w zXqF8GBIv37SYFpNo;vDf0wWzT)}+#|1ZzHieED%j_~WUOWE-fO>gMtV1zBa!QO6-)o8BSjwc&E^Z_Mk*M+Qfk*Ym;u4F}Ly;tE)wA6X)Wi%e#EZc+3x2lO#n z!zVF=`U`;)sw84+xD=TC#n7w&d~gDIpLzLxTv=Gkm&NbixPMm@{;s5_Hbczyj+7Fr zgfUKMAEUb4kC&Ee64@RMrO%C07P78FWm4_H1JszAfQ8i5DzSe>vqB_CuUENypER;JhKyGBjkUlWNIjd+ztXhleYnc^RuW``=#f6ufu)hdhay zbm58yN0-w{30t;H*%d7<&DtZo12a?07?$BD?6y%c2{|~~bF*5_j}WV5TuQ`8XU$UZ zrGOW&xNkoffC=ksX;SgFx}v(BVWFfgmQCx?ept}(O#>PDibD*0xl@!o|Elg(Fp^jI z7WmqP!CP415f+x#=4NGeb#)aD4bfLbo8C-AdVNE~aPEU>UMnjrxq}6RTYkuJXAw(P z)B|!<`db`U961%ryh|F3TF=Np=2G8K6l)$Q7_k6ai!rlo`;yYvr|ae8V?5xyHR@qI zke48=pO;H^B*Hi5+Hdp;$jBJwB7>8*gk2&!O(ylP?oG15sN;ymuU}cS zTOP=ihen4Mhw3>RWq!;(6^FO}^VpW3w%M6DWz;d6r^03VGvd^GYZiSYcj0x<`I#9( z5fSxjpDn#$R^Gl!56AlYdLaRU#_o(mg8Xw*kilaEx!et{t*QqH2mP}f^PO0i`HrQ& zoG4RY8qAlmzr`)%nvK!JVa{HC@)UKGGxR3sDUYyQRwi&0lBjd`=lu9c+9Ewzr04*ct#qWFY_m`|UQ z3tu$a)`xI}eEjI@`W52v+XfHBF1Wx}>Fd|8At52!3JQN#RaLR`^5VjHq=fI^uWIV` z(9bi;85uFD-kC!?dwA$uTW6!9qG)>?oyK*-ZBiih227w;oz`bo#qdpjZoe%pwtBFU zF*gUlzdOUzm!`xrs6uBloN*(Ij)#rFtKqSZ%Bxt;N3y z8{W^$o~9nJ3uYVmIa$XGKL+7>?swQCEnDn@6Y@$fcgOfRLO_Gl&$XE}3mto(b04dn zgId}jAY7F7>6a(f^q+SGMG$?&jHIrhpfEExmk9!YLs0N3D*E&1%s>`4j4yptakG-X zQYm54!ah4Y+uYu+hWhoLg)iHz?wzFcQ3Jeli}NcH`@!c?TcxEG>HzKH&NtV6-i#7D zT&uD2=C9Rp1X|L*Xv{9-9!Ri|_wTzfcORh6)JT3AhdlQIfhaV%$t`L31uU#?wYS^O z&W=q>w2-sA$iXH3efrkGuf<*vn%in|;>tc-E2LD#&|kHuL=4ppbnSnynLB13Z_hH}<#u5E7igEy|*Kg~+5yA{p} zsQ2~d=y=(M9j+o+My?Pk`TfapyX5>3Z}$MB&0H*fH^}yT&munBKWEUss_eSn;pfKk-^Q7&`=!{lX$n$xps6u zd9_1!wXY*V*4{9#uIjGyjKn}WvM0UGaao7k(9lq2uuLVt-Q=orOK0amR41!0&DJjj zQJ|I8k=QpAls3CGmH8@AfU6>*O0{CCM^bw-%t+9TymW<3d!3w5kS$PRiP$q}D_dt5 z^GG*(Tc))hf#C4Q#d)}ml2Zf6#)?e*d53q8ZS}jiw0g$Iw8Y#%f*!IoR$U2Zk>{oJ z9?R#dpKFi;{z0yM43YZqk7?9B1Ix=9syN>fvp3T^RBY91UbwK(ohF%^Yfdh*2pt(6 zA(eE;T%Am!4y8OIUhIqi65A&@!&-_YX{B94r%Rwe3yF%BH&H)wZn3@FkU-fpg^Y#oR5_@2UkJ1sP3EPjLvMDGaN3tK?vb z9|B&$%*|7;T)oPx$s5oE3M#<{b(~%uer1f4H#*Wy9?B>8e8byaz(3VK$*xaKW52pd z66_6&GmFzQ&NJ`ZDe`N0H%jJoV+18kYOEkH^!}1IGHxMIlpZ^196Q_F(OQb%K1WBhld=!!(|YPz3EDJ_#L3bM{OjFiNA34iTV<%f zo{s5xtaP{P%N*@rvD|))94UT&#qZz0m)$tduU+A6L8FofcH`^8;oy`q!R0r~m) zJrVnUyoHXX+`9)8QbEJ<%22mi2|g&gf4#mu$+|HQXGB=xP}Y8P(sgIuLL%&VSMiHE ztp5A~D&2*vhhJI3wolMj2aFg{cpUriggCPQnd7pN1KeRaJHyo9C$b}*;V1KRe*E=K zRSwahBXD}8xCOu8;)A4(~=!hV3sWg}3uRbKIuP+t!?%i<7B`|0W z=e;xM1>cCk@e7(QhbY}GK5-tJP1G~L%8#a%0$}%Rae!R9jgwyDx$}61Ar0TAANFCF zPQ|Gws+T$l(8>{|j4soRM48OJ)}3L0>-mx5JWOJ9u-n%u#uLG|3WI(ttg6Nsm2c3-;I?OnpIo|hsf@wGL{Tm0mGs{He*Dxa zo#{`v?MEx*eetZQ(9lrRIL@qj)423E=dR8G5H%`8>&|XWEZb}*e8JTb4{}v^y!zc# zr{;y$5J+4rTg1vWWp9@bnNq@rsd7)B;<;a>tt3WHd%p!jFxgV`1JaK%{aEqLdqM>- zlT2M~%b76JC_EPO%7JF96zil@MW@|>F=DeCk&`_aj2J2?+6C{-;hovwI7Wjwe^nJ+ z?doqTrMM88v9eEJzY2xcUr}ypYMO9Jsk!wQl(I#{Rj^{Ej{5=Om!P>(R@yM^lf~bX zaTbWSo;CnAmulHrAdrVUKLiXXj@|En@it9WFtWqm>$n^<1X9JF7JpG8-KYOio)fMk zFE9TlEff+E@`sq14(KGP=*Y;+8X6ib`bUy;!9Jh7{OaZZkg5MXLix(g^^lT`+G4WaEfSiv?S6PXHRb%pNxy5yo@)&87llF{~Y9`wx zp+DO&h+9kMD~cq{+2o(sapKA3+Y+oNinmsnJg(q!EhgcmrKPa2u*!)OC)U^3wOw6H zBppVwfBtj=M9H)`A}cG))N7VQxhE^h&-+mp3;Ow9+hj{+GgxpPNFMMWEIuzP!Z;$G`7sYAs|larI- zBJI~Ztm1nI=mKtb=q35>e@zM zh5g&5V+V@kD-0vYgAM-u)pKGJ1#~CmxRKwRlelr+qeqW!lJ79@?fzW?>;5S+G9^Pb z*7p0KN0$Z*(`svLOU&y}!tt)n-5EpAIM_5`Yd`yxl8E^uRiRRSTZJnT*yQAgWT-y^ zRbtUtiJ6+rA)F1&9~x_J&MBE?_U+rJmGr0+T`7-R+S_3O=jKvgzkc0UVD^Yb4_4CF z)~4-PN2-SvH;DEWi#|b8KbQC2yY}o>-AaET)hF9*^m_d!Z4+}n2}6+CB#WZbqhAEG zFCdiiM2L)Uc_5^qvm3@jLsUP<&fC{Jl;7MuB^U-hIK94ce8lL>cz~~`w>PM(!Lw!@ z{^o@SA-=w=pU?3RcO16A>WZMK?|QE~_?9z})VetvOxEkfcG z@)_{&Y|y=AKr9pFeDt_POf;RHi>?j*YIp`%TRp*VW_k)Cst&eV-xA8cIr)dc!J`iK zjwR2nKva4j)yJ0fF2ktUrs5K1ZCj1FMcX`8P}cjB-B4^*&e4{L>2nm0OP37epjsDLGJZ7 zKNC6GEuBET+RWl)jxW>~D>YD^4nk^fANw^XrPs<|$J8$|JmDTjjXYp8+8@I$x|S!t&)uZ*NqR@&sr7 z-m!opd4tk(q4lZXzTE?2f#HDtSk(@GKm4M!c^$?sDcIkF4Zh+bFr_mk6YwS!3M06}vtKRV!jzzZ#iy*tG4Z%}Hd}Ge<|m6c4BY zZaH)aV_N#oXzW$2p2!FjXZfsNVO9~3v+#tlMet%*N?>hW9S08&c4Fc==!GEr)f&Tj zqsq%YK9m(Y&#K6+{W#as(a{euJoprnoXSu2Sxs*ruj5qUn|a6Ol*?R#Fv8utckiwL zHlhHypeB=uzfVuUHaiQic=IV%zd7P}cS?Wz?$dY-3_F}E&b)ttY%S*S{|3NIBYl!U zgK!WWVOHWpubMO@^E3ZSHT0Vd9uf^rVe5BwJ;8hwhgTO3|NAPk|MgVqzvT3b@FS2f zmg7DGyzpAJ6TStOIlD>5)%cIXg`%r|`zoZQH4()5Rdk(atmsFnnt#BNH~*BRu42}* z@xoO}-+#25gqVzcvvfB-hEpUVCA2~smA=my%lhe1j-=<|f_8m>HeZ)mHC2N0?sT8E zI4pQ*PHMt{i5?&U>v{H}bP1QeE3>oni@`~R-st(pCZKeldv}n;O*#eBC7y^YyJyrM zSE5ZKzjnR8A8mQl%ShwQM?F9_!$tTW-T_;}v=e~20ou+U*lP$rC=(xqZ{5$XtX=H| zjLFW|*E8u326pwK7~%kZ&1QEhi-TQLBhW`~F&lL#`fnRNPJdYq7ECo;c&AZxc5cZM zK0h^;YIx4HZxe1z`!yoyTV@q(8HTvV#l^*sz<4D1ZQWBOnZjoQecEWZ!(8qB`o}*HKf0)UD}V=3%!^E;;-OF}e#2CaS3=)V z+AzeJ5oZz)r#&RUSFkTC5-WAHO1&@awLtN$l=u~?#fssq2?LBgyw0Wu2v^!MEiiW7 z$~+JrlFPI?ZU2p&QMnr|Me@{V_1H>d{`y-Z>F{P*rr^l}J2mkH5P`!Kb5!tFSRA10M-J%vebFD zMZq^UEiEQ44pUR}P$~NKgv|QJhQxF8w+$T^jjZ>B+BTN%Wmj)qr|ft&z(GBn`|Tc zMD|-xk&ad)dIvs;docF<20cQ!*0T-}>UV2@2dO}hxu%@tyZLpcYC|1oT&_OuyOr?n z^o3OUz+*sBSL=TONEA?>N8DI%M*|`XRy5}QpVkOv@MN00wFB>owUI1W$X86sL4>EUx^>p{Q?>gf2 z7$Mu<5*z{gGi;20PYY>29*ag-rnNp2YK!|hB{9IZDrQti|0q&~NC@!WT_3G06F%ks zV~^klGPUta?y%OgXCF~0RP>iGf>0<_E%Cmg&*rk0r|0$=`}vUy7u%6+JE{0Yyv=kKtpRhnMSC2l^D!;Ldo<_|4wA)qY2UR?Vl& zLH)A`g2~+Ki(x-G9USt8OKe`i z=clLB0C4*3$wMx89Qbtk(XR;qy+CJs@<*#^>>@qdooFvHR@UG-esRuY zD0^Yr9{F*r6sX(Li}deyZ%NKqgoKnMBO|>hgSf=4yZ%s$Jv?#!^+sJRsAIfoQpJ;e z+y_$M;0f;+o3i~pn9&f7E+*ak;2@}<_a6#0utLO$`PqWg@UwTuoQ{B)zJ2?)3E+uJ zk5xU*t{6w7-ku)UpA+(>j^l(u0#N*c8T)pwBLNG1kHM<`j~`E?i7e0YSNUS8_nSJJ zd&D{;U`fFTN;q^GEWrPh_X95?5GECT`M6CiS9OMWd~E98-NpPMs8o(y$c=5*{x!87 z^I=c&sp&I+)1x++7g`S%n1y`!pha6EfnJs+q?6e+RI#1KVn0rRpFMk)Th=|-dus)E zP9QJ7Wol(b1gLQ6zLru*z_l2vXe%H+u;kyu`0gvB*fqtTcGo46{>K7%^Dekc{0CLT}y%IydMTD(3{oMy9odhdmkLJMcuX1ihVn zuyTQ68@FYjhRzMH&+WSI>YuMGwOdvlT8oEO#zFlF!==b909@Uc`jkQ0z(P51g;0wD zxv{XYSX}jIR08qXV!s!wFDeT=lUzdBI}pPSl{^E6P)nLK2W9sGoJFYW)eW_@#sB6wlzv3HwXzvD`SrYiuw=%55=mFV@P*s{H+!ey5B!e*y&PjZ}s*_<~M@>F+XLq;2rNONX)WQyvQd^S$)k^|^1f7f5zW~D2< z>sj_^RyZ<#_~S^TIYLqe!8JJG`?|j@G+^iA$^!V9f^SDlagnWd)@O|fuPefC9Dc5f zt6k|XM~A~VN(Zd+3SA^NH(Ht^2TaEEl%wtv*BaZKT3UK~GE__5D0)KFVf(v9BGx~| zBAAY=xLcnI@BJc#&Wpy@)+wCQrPsNG)UAJ)#J95s)nqc*K0G?1OgTUu>omVq!Ah+GBqW z`!`$x>Vd`sSxpjlN67E{lLQ$b?I^DB&)86O!Kp&*+tVPu;%48G9?7CXqwa|L! zN8_l@L^)&7=9MBYm6};VoRC96Y8QrK(koZ+*d|GIS-y^`X`*GgkGy|Q?^8H=t1G~v z?pJ?hJ@kRyzf0?RJKixZiE%QviI)y24|nviTFYfjU;>2VjLfh+-|OnYQt8jFaC>xC zliRq=F0v9`>2~lMpa|A|XH6G#Q|+`*vVD0lr7&+hRKJ~3W>wY5m2xrLiC*r$jT0m` zkzMI6u=4s$Pj~59zkaOyD7YnEGJ$5QZGK{t^&9Q~FU}KxgP4$kDU$Ca)KCuf%lPau z!Y?J|LBac=J-Q|l{{0oF7A5n3wxntL$wj7SA5ug_4ddOAzH9Ydc+a)ecFLg^bb_o0 zrWvtPBH~}kxB+BclLggOA7fX|C;dnFU;hedrwk?ReJJxS((1fV+qy!2D`A}(l@&p0 zio7bB&vKhL>yYeVIInE3f4)xEw&PR?@ETtfqi-Fm!3hH)z7#niQr5m$qI2?>=xaG7 zjq$okE@2Z5HrZj!{fvwBa#Mo`s*dfd(OA`PVjglJ569%g_g+n9`bR*@G5?qLAJl32 z?f<3y%by@J`p_s;DqwD8jw8{Bf!6vkC^{hl2TU2`*vg11jYfYu4Apm~B+S$|Z-LTd zcZ->kouNHCht1h98ti3Rv9Yl!>*uu})--T;FMB-IixT!9%evc>*4^@-qE6~2x5kQML1#+j6+=|ZY{g!q@E>cl z2>|FbJ2_4QnYq2~NpYsel{0eGdK+%hrb48FR3RXxFQwdLHU2sm|Hi*?QK;rK@Y1(t zsX_g?p(+!AYH|{j_SlZ@P4>+I8(;YLZRSKJu`Gi5%ps-RZKOge9j1oRroV+|7l#&??k645kyD1h<^zZJNDY@!xWc8G=gT-PlIMH$6~Q)g-4I< zjo36VIR)~ACF;vi<)V_y^Sio9j7`m8Q(zwV_6@G>N?Q=eRE_duJ}|nH6EzITdz9i^ zAs4&LG>QMGC1DNR3E)^brc{PqlJR~>XQPrp*5g09jSW9R{oS2-l>&V*MsW7XPv89> z2FobQa+8(ySkayDNN8P3I{Gccb+Jb=Sew-#LT{|MDp0|}F`0PlJdVk4n9#7^v0ZEY z{beTgP~JVLxuh6wjg89E!I72ssrpoCiGEI2xDdmc;6|{tureZ>IF1D$kq{KmLh5am zchI1IJG8&sA`RXlP|ZlDT(5aKC@3h$Yh%u+(j7N^jP}ivN3@gtYqzajf-+0Y>HH#s zaYsiy*-X-0xPZOzb>3}zAn6nh4Gvxqb`M-A>vd_e%BV6Cy22re{EHl6aWXaDVat5v z?!b4S}AGVXn=KA}m&zPJw3MSBz@}q~jFk9<*74>i ze0<7Xm8%3S*wIII86lEm`ge$}u|45it-aHwPK&2;{`}3`UWAQSHVH2yqV`wr+lh{x% z;O(i3HHr#vzJfhetbGQ7=vq%Vgxx4t8h*#dx3chifb(SGrK=a=3Ljh@c)$!GX 0 plt.close(fig) + # assert False def test_simple(): qasm = """OPENQASM 3.0; @@ -18,7 +20,6 @@ def test_simple(): bit[2] b; h q[0]; z q[1]; - y q[0]; rz(pi/1.1) q[0]; cx q[0], q[1]; swap q[0], q[1]; From cbd9293defe9077277da6cd10de7fb8e9c5271b1 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Sat, 11 Jan 2025 02:49:30 -0800 Subject: [PATCH 06/29] pad label --- src/pyqasm/printer.py | 6 +++--- test.png | Bin 10508 -> 2023 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 1c540c57..e9a67128 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -31,6 +31,7 @@ GATE_SPACING = 0.2 LINE_SPACING = 0.6 TEXT_MARGIN = 0.6 +FRAME_PADDING = 0.2 def draw(module: Qasm3Module, output="mpl"): if output == "mpl": @@ -105,8 +106,8 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: width += TEXT_MARGIN fig, ax = plt.subplots(figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))) - ax.set_ylim(-GATE_BOX_HEIGHT/2, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1) - GATE_BOX_HEIGHT/2) - ax.set_xlim(0, width) + ax.set_ylim(-GATE_BOX_HEIGHT/2-FRAME_PADDING/2, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1) - GATE_BOX_HEIGHT/2 + FRAME_PADDING/2) + ax.set_xlim(-FRAME_PADDING/2, width) ax.axis('off') # ax.set_aspect('equal') # plt.tight_layout() @@ -174,7 +175,6 @@ def _mpl_draw_statement(statement: ast.QuantumStatement, line_nums: dict[tuple[s raise NotImplementedError(f"Unsupported statement: {statement}") def _mpl_draw_gate(gate: ast.QuantumGate, args: list[Any], lines: list[int], ax: plt.Axes, x: float): - print("DRAW", gate.name.name, lines, x) if gate.name.name in ONE_QUBIT_OP_MAP or gate.name.name in ONE_QUBIT_ROTATION_MAP: _draw_mpl_one_qubit_gate(gate, args, lines[0], ax, x) elif gate.name.name in TWO_QUBIT_OP_MAP: diff --git a/test.png b/test.png index 6ed4c331b3c6ee6ef96aae9face67bf38d7a2682..d002ba26a09fb1e243928b7aedc403f39bd8b73f 100644 GIT binary patch literal 2023 zcma)7X*Ao37XMo+_P&%F!qB2aYl&SOj7Dkgdm{{LomiqFiKX_ScI_yoec@62T2xbN zE2=^fIx2RWsI^kmRt@no@5B2#bI#q)J$LzVfA{>-Y^`qz@k96l01z@WHMR!;4rO+% z&&$KMb8{&!YzM=eAn^{E-|^v|SYN=(6CaGm;L-kG7s7n8IDgDtO;vR@RW;=cet3K^ z4hDq={l7vLgGE7eu}Ca?3SY3PGY$aGdj6#xRfd)R0043@Gln}x6fowZY#k>#c(yUl z;b#uM7Ay5yUzgShtraaPX|m6@PqREHW42M;lL@azcuj1-e>E>k_%}oG=CiDc5PD*Q z$T*^=w4^7K!>Y8;OSG{x?8)GmaUbL!_YWYHnIZy#xMdd1x1&yCHHRBlDV?i&nnUEy za2ZI-TN(h)=8pa-eBF4j7>P8N7ZMW#n4((0Iaetdo>xS{x?`(+ZlN>k!|ZH^TYDJ1VNwnTgRG7gkyl2DOaTi7 ztWcUzQOw~zqTsqap@vL|B%em+F;_86IKvb z=+1~R3Jnb}VbLWv7i`n2)pvk+asv56zlQU&&w|y|B~N#E>43n%knK-!M<=J!iVB`p zN+GUobbY*19k*h+wtpI>QCU{@>D%&2 zB(mLXWeD%VV6%^mbh-?5To}mSBc{ZsEmU{fIC*ULP=>UmuaQ*X85$n$&0bMeRn=YR z+zbG|zP^bI;ca0Ih^(9(DDw;mbcm6>hAm5t;M&{U<279I_&WER>r20-AISwhz*J#h za%`!#se<{zPwV@;8lfkLOWe^)UiupLP69=5MW}j~dV&q8$6oUdxE*HAqGzPMc{Cnkx^0VK{GE|>N$hl8SiB2+eVku>G&vz32hoK zFK;@8W^b^tfEDG5TirFx?4f?nRH8B~H(F@Ab7zZ4q+&+@?Ii9LK7+2xy2C*UoZ4=h z&qv*fXY4NPiK6bWhIdV9GrHTGIT=YKOXyDFlp9m}8n*i}$lseKf`Yi-*fNI5cRH^@ zn`>scrkdj<)9Yu!(i#ZHRn^0tJEJAX-NI-U#aqLzW&_Z{gAKzPTI8KR8|%LJFVUMZ zOKK|$&^~Iq7s>mc%0(2?&dzRZ9bZ*jdjmN&e_Fv{Fj}$8{O3eOw%Ql>*2g`Yr^hBI zL50>J;A-sgRb{aZ3sn%IJ->kxjs5fJ07JLOiBKQ=&>olTL_<~0rb12^Jp!>eKS!SK zZ^h9`BZ1jJhAkrKI}*I=DYV!5IosB^pN=`182EM`o;$B2tU!?xi5z}bH#y6DS=q$y zkL+whM~8kUM4KNDhc`~6w|7EfIrH=Kl6!lNe0+Sm14t#+)qLgULT-mlk@(t_DiNXoh5F5Fhz^!a$eT~uA2sn zV>PLs;}j!#%OgUg#SI%I=GLT@&usqE>uIQm>t}B9W5L$%Edo=dIHgBjL|LB)P=kQO z>H_e=t7JOe3(v+P8(nIUtbxHnI0{uc9>dAWY3t^eb-NaEQB_r#Kp+f^j^?vegn;00 zn^CQiCl0Qitjk@eUYnDz48=s#Hw@zJEZRt&cz74(19BltfJ=_J+UIZieZJn!qhAG6ZtK*@ANi9|~q>T=w?&Ia-M66 z!Wb6JRQB5y{kO8R{*8^PrVwRi*+)DA8L4GGy?>MHkK+)RI-v%Y3?{QV7ceuiHhym8 Gk?>zW6VGS> literal 10508 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 Date: Mon, 13 Jan 2025 16:56:56 -0800 Subject: [PATCH 07/29] multi control --- src/pyqasm/maps.py | 4 +- src/pyqasm/modules/qasm3.py | 1 + src/pyqasm/printer.py | 166 +++++++++++++++++++++++++----------- tests/qasm3/test_printer.py | 35 +++++--- 4 files changed, 146 insertions(+), 60 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index c5dd22e8..a5394bbc 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1235,9 +1235,10 @@ def map_qasm_inv_op_to_callable(op_name: str): ) raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") + REV_CTRL_GATE_MAP = { "cx": "x", - "cy": "y", + "cy": "y", "cz": "z", "crx": "rx", "cry": "ry", @@ -1249,6 +1250,7 @@ def map_qasm_inv_op_to_callable(op_name: str): "ccx": "cx", } + # pylint: disable=inconsistent-return-statements def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value): """Cast the variable type to the type to match, if possible. diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index c2d12426..ee656776 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -18,6 +18,7 @@ from pyqasm.modules.base import QasmModule from pyqasm.printer import draw + class Qasm3Module(QasmModule): """ A module representing an openqasm3 quantum program. diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index e9a67128..d1297f19 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -15,17 +15,27 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional, Union -from pyqasm.maps import ONE_QUBIT_OP_MAP, ONE_QUBIT_ROTATION_MAP, TWO_QUBIT_OP_MAP, THREE_QUBIT_OP_MAP, FOUR_QUBIT_OP_MAP, FIVE_QUBIT_OP_MAP, REV_CTRL_GATE_MAP -from pyqasm.expressions import Qasm3ExprEvaluator + import matplotlib as mpl -from matplotlib import pyplot as plt import openqasm3.ast as ast +from matplotlib import pyplot as plt + +from pyqasm.expressions import Qasm3ExprEvaluator +from pyqasm.maps import ( + FIVE_QUBIT_OP_MAP, + FOUR_QUBIT_OP_MAP, + ONE_QUBIT_OP_MAP, + ONE_QUBIT_ROTATION_MAP, + REV_CTRL_GATE_MAP, + THREE_QUBIT_OP_MAP, + TWO_QUBIT_OP_MAP, +) if TYPE_CHECKING: from pyqasm.modules.base import Qasm3Module -DEFAULT_GATE_COLOR = '#d4b6e8' -HADAMARD_GATE_COLOR = '#f0a6a6' +DEFAULT_GATE_COLOR = "#d4b6e8" +HADAMARD_GATE_COLOR = "#f0a6a6" GATE_BOX_WIDTH, GATE_BOX_HEIGHT = 0.6, 0.6 GATE_SPACING = 0.2 @@ -33,12 +43,14 @@ TEXT_MARGIN = 0.6 FRAME_PADDING = 0.2 + def draw(module: Qasm3Module, output="mpl"): if output == "mpl": return _draw_mpl(module) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") + def _draw_mpl(module: Qasm3Module) -> plt.Figure: module.unroll() module.remove_includes() @@ -46,7 +58,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: n_lines = module._num_qubits + module._num_clbits statements = module._statements - + # compute line numbers per qubit + max depth line_nums = dict() line_num = -1 @@ -70,18 +82,19 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: line_num -= 1 line_num += size - # compute moments + # compute moments depths = dict() for k in line_nums.keys(): depths[k] = -1 moments = [] for statement in statements: - if "Declaration" in str(type(statement)): continue - if isinstance(statement, ast.QuantumGate): + if "Declaration" in str(type(statement)): + continue + if isinstance(statement, ast.QuantumGate): qubits = [_identifier_to_key(q) for q in statement.qubits] depth = 1 + max([depths[q] for q in qubits]) - for q in qubits: + for q in qubits: depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) @@ -95,7 +108,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: pass else: raise NotImplementedError(f"Unsupported statement: {statement}") - + if depth >= len(moments): moments.append([]) moments[depth].append(statement) @@ -105,10 +118,18 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: width += _mpl_get_moment_width(moment) width += TEXT_MARGIN - fig, ax = plt.subplots(figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))) - ax.set_ylim(-GATE_BOX_HEIGHT/2-FRAME_PADDING/2, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1) - GATE_BOX_HEIGHT/2 + FRAME_PADDING/2) - ax.set_xlim(-FRAME_PADDING/2, width) - ax.axis('off') + fig, ax = plt.subplots( + figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1)) + ) + ax.set_ylim( + -GATE_BOX_HEIGHT / 2 - FRAME_PADDING / 2, + n_lines * GATE_BOX_HEIGHT + + LINE_SPACING * (n_lines - 1) + - GATE_BOX_HEIGHT / 2 + + FRAME_PADDING / 2, + ) + ax.set_xlim(-FRAME_PADDING / 2, width) + ax.axis("off") # ax.set_aspect('equal') # plt.tight_layout() @@ -124,7 +145,7 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: x += TEXT_MARGIN x0 = x for moment in moments: - dx = _mpl_get_moment_width(moment) + dx = _mpl_get_moment_width(moment) _mpl_draw_lines(dx, line_nums, ax, x) x += dx x = x0 @@ -133,36 +154,51 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: for statement in moment: _mpl_draw_statement(statement, line_nums, ax, x) x += dx - + return fig + def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tuple[str, int]: if isinstance(identifier, ast.Identifier): return identifier.name, -1 else: - return identifier.name.name, Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0] + return ( + identifier.name.name, + Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0], + ) + def _mpl_line_to_y(line_num: int) -> float: return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) + def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): - ax.text(x, _mpl_line_to_y(line_num), f'{qubit[0]}[{qubit[1]}]', ha='right', va='center') + ax.text(x, _mpl_line_to_y(line_num), f"{qubit[0]}[{qubit[1]}]", ha="right", va="center") + def _mpl_draw_clbit_label(clbit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): - ax.text(x, _mpl_line_to_y(line_num), f'{clbit[0]}[{clbit[1]}]', ha='right', va='center') + ax.text(x, _mpl_line_to_y(line_num), f"{clbit[0]}[{clbit[1]}]", ha="right", va="center") + def _mpl_draw_lines(width, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float): for k in line_nums.keys(): y = _mpl_line_to_y(line_nums[k]) - ax.hlines(xmin=x-width/2, xmax=x+width/2, y=y, color='black', linestyle='-', zorder=-10) + ax.hlines( + xmin=x - width / 2, xmax=x + width / 2, y=y, color="black", linestyle="-", zorder=-10 + ) + def _mpl_get_moment_width(moment: list[ast.QuantumStatement]) -> float: return max([_mpl_get_statement_width(s) for s in moment]) + def _mpl_get_statement_width(statement: ast.QuantumStatement) -> float: return GATE_BOX_WIDTH + GATE_SPACING -def _mpl_draw_statement(statement: ast.QuantumStatement, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float): + +def _mpl_draw_statement( + statement: ast.QuantumStatement, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float +): if isinstance(statement, ast.QuantumGate): args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] @@ -174,56 +210,88 @@ def _mpl_draw_statement(statement: ast.QuantumStatement, line_nums: dict[tuple[s else: raise NotImplementedError(f"Unsupported statement: {statement}") -def _mpl_draw_gate(gate: ast.QuantumGate, args: list[Any], lines: list[int], ax: plt.Axes, x: float): - if gate.name.name in ONE_QUBIT_OP_MAP or gate.name.name in ONE_QUBIT_ROTATION_MAP: + +def _mpl_draw_gate( + gate: ast.QuantumGate, args: list[Any], lines: list[int], ax: plt.Axes, x: float +): + name = gate.name.name + if name in REV_CTRL_GATE_MAP: + i = 0 + while name in REV_CTRL_GATE_MAP: + name = REV_CTRL_GATE_MAP[name] + _draw_mpl_control(lines[i], lines[-1], ax, x) + i += 1 + lines = lines[i:] + gate.name.name = name + + if name in ONE_QUBIT_OP_MAP or name in ONE_QUBIT_ROTATION_MAP: _draw_mpl_one_qubit_gate(gate, args, lines[0], ax, x) - elif gate.name.name in TWO_QUBIT_OP_MAP: - if gate.name.name in REV_CTRL_GATE_MAP: - gate.name.name = REV_CTRL_GATE_MAP[gate.name.name] - _draw_mpl_one_qubit_gate(gate, args, lines[1], ax, x) - _draw_mpl_control(lines[0], lines[1], ax, x) - elif gate.name.name == 'swap': + elif name in TWO_QUBIT_OP_MAP: + if name == "swap": _draw_mpl_swap(lines[0], lines[1], ax, x) else: - raise NotImplementedError(f"Unsupported gate: {gate.name.name}") + raise NotImplementedError(f"Unsupported gate: {name}") else: - raise NotImplementedError(f"Unsupported gate: {gate.name.name}") + raise NotImplementedError(f"Unsupported gate: {name}") -# TODO: switch to moment based system. go progressively, calculating required width for each moment, center the rest. this makes position calculations not to bad. if we overflow, start a new figure. -def _draw_mpl_one_qubit_gate(gate: ast.QuantumGate, args: list[Any], line: int, ax: plt.Axes, x: float): +# TODO: switch to moment based system. go progressively, calculating required width for each moment, center the rest. this makes position calculations not to bad. if we overflow, start a new figure. + + +def _draw_mpl_one_qubit_gate( + gate: ast.QuantumGate, args: list[Any], line: int, ax: plt.Axes, x: float +): color = DEFAULT_GATE_COLOR - if gate.name.name == 'h': + if gate.name.name == "h": color = HADAMARD_GATE_COLOR text = gate.name.name.upper() if len(args) > 0: text += f"\n({', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])})" - + y = _mpl_line_to_y(line) - rect = plt.Rectangle((x - GATE_BOX_WIDTH/2, y - GATE_BOX_HEIGHT/2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor=color, edgecolor='none') + rect = plt.Rectangle( + (x - GATE_BOX_WIDTH / 2, y - GATE_BOX_HEIGHT / 2), + GATE_BOX_WIDTH, + GATE_BOX_HEIGHT, + facecolor=color, + edgecolor="none", + ) ax.add_patch(rect) - ax.text(x, y, text, ha='center', va='center') + ax.text(x, y, text, ha="center", va="center") + def _draw_mpl_control(ctrl_line: int, target_line: int, ax: plt.Axes, x: float): y1 = _mpl_line_to_y(ctrl_line) y2 = _mpl_line_to_y(target_line) - ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color='black', linestyle='-', zorder=-1) - ax.plot(x, y1, 'ko', markersize=8, markerfacecolor='black') - + ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color="black", linestyle="-", zorder=-1) + ax.plot(x, y1, "ko", markersize=8, markerfacecolor="black") + + def _draw_mpl_swap(line1: int, line2: int, ax: plt.Axes, x: float): y1 = _mpl_line_to_y(line1) y2 = _mpl_line_to_y(line2) - ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color='black', linestyle='-') - ax.plot(x, y1, 'x', markersize=8, color='black') - ax.plot(x, y2, 'x', markersize=8, color='black') + ax.vlines(x=x, ymin=min(y1, y2), ymax=max(y1, y2), color="black", linestyle="-") + ax.plot(x, y1, "x", markersize=8, color="black") + ax.plot(x, y2, "x", markersize=8, color="black") + def _mpl_draw_measurement(qbit_line: int, cbit_line: int, ax: plt.Axes, x: float): y1 = _mpl_line_to_y(qbit_line) y2 = _mpl_line_to_y(cbit_line) - rect = plt.Rectangle((x - GATE_BOX_WIDTH/2, y1 - GATE_BOX_HEIGHT/2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor='gray', edgecolor='none') + rect = plt.Rectangle( + (x - GATE_BOX_WIDTH / 2, y1 - GATE_BOX_HEIGHT / 2), + GATE_BOX_WIDTH, + GATE_BOX_HEIGHT, + facecolor="gray", + edgecolor="none", + ) ax.add_patch(rect) - ax.text(x, y1, 'M', ha='center', va='center') - ax.vlines(x=x-0.025, ymin=min(y1, y2), ymax=max(y1, y2), color='gray', linestyle='-', zorder=-1) - ax.vlines(x=x+0.025, ymin=min(y1, y2), ymax=max(y1, y2), color='gray', linestyle='-', zorder=-1) - ax.plot(x, y2+0.1, 'v', markersize=16, color='gray') + ax.text(x, y1, "M", ha="center", va="center") + ax.vlines( + x=x - 0.025, ymin=min(y1, y2), ymax=max(y1, y2), color="gray", linestyle="-", zorder=-1 + ) + ax.vlines( + x=x + 0.025, ymin=min(y1, y2), ymax=max(y1, y2), color="gray", linestyle="-", zorder=-1 + ) + ax.plot(x, y2 + 0.1, "v", markersize=16, color="gray") diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index 8207d4e2..d51de655 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -1,11 +1,22 @@ -import pytest -from pyqasm.entrypoint import loads, load -from pyqasm.printer import draw +# Copyright (C) 2024 qBraid +# +# This file is part of pyqasm +# +# Pyqasm is free software released under the GNU General Public License v3 +# or later. You can redistribute and/or modify it under the terms of the GPL v3. +# See the LICENSE file in the project root or . +# +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. -import matplotlib.pyplot as plt import random + +import matplotlib.pyplot as plt +import pytest from qbraid import random_circuit, transpile +from pyqasm.entrypoint import loads + + def _check_fig(circ, fig): ax = fig.gca() # plt.savefig("test.png") @@ -13,25 +24,29 @@ def _check_fig(circ, fig): plt.close(fig) # assert False + def test_simple(): qasm = """OPENQASM 3.0; include "stdgates.inc"; - qubit[2] q; - bit[2] b; + qubit[3] q; + bit[3] b; h q[0]; z q[1]; rz(pi/1.1) q[0]; cx q[0], q[1]; swap q[0], q[1]; + ccx q[0], q[1], q[2]; b = measure q; """ circ = loads(qasm) fig = circ.draw() - _check_fig(circ, fig) - -def test_random(): + _check_fig(circ, fig) + + +@pytest.mark.parametrize("_", range(10)) +def test_random(_): circ = random_circuit("qiskit", measure=random.choice([True, False])) qasm_str = transpile(circ, random.choice(["qasm2", "qasm3"])) module = loads(qasm_str) fig = module.draw() - _check_fig(circ, fig) + _check_fig(circ, fig) \ No newline at end of file From 0ff03afb10ce1785933ba434f1fd3a551313b566 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 02:19:47 +0000 Subject: [PATCH 08/29] custom test --- pyproject.toml | 4 +++- src/pyqasm/printer.py | 13 +++++++++++-- test.png | Bin 2023 -> 0 bytes tests/qasm3/test_printer.py | 16 ++++++++++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) delete mode 100644 test.png diff --git a/pyproject.toml b/pyproject.toml index 333580ef..53719f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,8 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", ] -dependencies = ["numpy", "matplotlib", "openqasm3[parser]>=1.0.0,<2.0.0"] + +dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.urls] "Source Code" = "https://github.com/qBraid/pyqasm" @@ -43,6 +44,7 @@ cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] test = ["pytest", "pytest-cov"] lint = ["black", "isort", "pylint", "mypy", "qbraid-cli>=0.8.5"] docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<2.6", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] +visualization = ["matplotlib"] [tool.setuptools_scm] write_to = "src/pyqasm/_version.py" diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index d1297f19..f139f1a7 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -16,9 +16,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union -import matplotlib as mpl import openqasm3.ast as ast -from matplotlib import pyplot as plt from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import ( @@ -31,8 +29,16 @@ TWO_QUBIT_OP_MAP, ) +try: + from matplotlib import pyplot as plt + mpl_installed = True +except ImportError as e: + mpl_installed = False + if TYPE_CHECKING: from pyqasm.modules.base import Qasm3Module + + DEFAULT_GATE_COLOR = "#d4b6e8" HADAMARD_GATE_COLOR = "#f0a6a6" @@ -45,6 +51,9 @@ def draw(module: Qasm3Module, output="mpl"): + if not mpl_installed: + raise ImportError("matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'") + if output == "mpl": return _draw_mpl(module) else: diff --git a/test.png b/test.png deleted file mode 100644 index d002ba26a09fb1e243928b7aedc403f39bd8b73f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2023 zcma)7X*Ao37XMo+_P&%F!qB2aYl&SOj7Dkgdm{{LomiqFiKX_ScI_yoec@62T2xbN zE2=^fIx2RWsI^kmRt@no@5B2#bI#q)J$LzVfA{>-Y^`qz@k96l01z@WHMR!;4rO+% z&&$KMb8{&!YzM=eAn^{E-|^v|SYN=(6CaGm;L-kG7s7n8IDgDtO;vR@RW;=cet3K^ z4hDq={l7vLgGE7eu}Ca?3SY3PGY$aGdj6#xRfd)R0043@Gln}x6fowZY#k>#c(yUl z;b#uM7Ay5yUzgShtraaPX|m6@PqREHW42M;lL@azcuj1-e>E>k_%}oG=CiDc5PD*Q z$T*^=w4^7K!>Y8;OSG{x?8)GmaUbL!_YWYHnIZy#xMdd1x1&yCHHRBlDV?i&nnUEy za2ZI-TN(h)=8pa-eBF4j7>P8N7ZMW#n4((0Iaetdo>xS{x?`(+ZlN>k!|ZH^TYDJ1VNwnTgRG7gkyl2DOaTi7 ztWcUzQOw~zqTsqap@vL|B%em+F;_86IKvb z=+1~R3Jnb}VbLWv7i`n2)pvk+asv56zlQU&&w|y|B~N#E>43n%knK-!M<=J!iVB`p zN+GUobbY*19k*h+wtpI>QCU{@>D%&2 zB(mLXWeD%VV6%^mbh-?5To}mSBc{ZsEmU{fIC*ULP=>UmuaQ*X85$n$&0bMeRn=YR z+zbG|zP^bI;ca0Ih^(9(DDw;mbcm6>hAm5t;M&{U<279I_&WER>r20-AISwhz*J#h za%`!#se<{zPwV@;8lfkLOWe^)UiupLP69=5MW}j~dV&q8$6oUdxE*HAqGzPMc{Cnkx^0VK{GE|>N$hl8SiB2+eVku>G&vz32hoK zFK;@8W^b^tfEDG5TirFx?4f?nRH8B~H(F@Ab7zZ4q+&+@?Ii9LK7+2xy2C*UoZ4=h z&qv*fXY4NPiK6bWhIdV9GrHTGIT=YKOXyDFlp9m}8n*i}$lseKf`Yi-*fNI5cRH^@ zn`>scrkdj<)9Yu!(i#ZHRn^0tJEJAX-NI-U#aqLzW&_Z{gAKzPTI8KR8|%LJFVUMZ zOKK|$&^~Iq7s>mc%0(2?&dzRZ9bZ*jdjmN&e_Fv{Fj}$8{O3eOw%Ql>*2g`Yr^hBI zL50>J;A-sgRb{aZ3sn%IJ->kxjs5fJ07JLOiBKQ=&>olTL_<~0rb12^Jp!>eKS!SK zZ^h9`BZ1jJhAkrKI}*I=DYV!5IosB^pN=`182EM`o;$B2tU!?xi5z}bH#y6DS=q$y zkL+whM~8kUM4KNDhc`~6w|7EfIrH=Kl6!lNe0+Sm14t#+)qLgULT-mlk@(t_DiNXoh5F5Fhz^!a$eT~uA2sn zV>PLs;}j!#%OgUg#SI%I=GLT@&usqE>uIQm>t}B9W5L$%Edo=dIHgBjL|LB)P=kQO z>H_e=t7JOe3(v+P8(nIUtbxHnI0{uc9>dAWY3t^eb-NaEQB_r#Kp+f^j^?vegn;00 zn^CQiCl0Qitjk@eUYnDz48=s#Hw@zJEZRt&cz74(19BltfJ=_J+UIZieZJn!qhAG6ZtK*@ANi9|~q>T=w?&Ia-M66 z!Wb6JRQB5y{kO8R{*8^PrVwRi*+)DA8L4GGy?>MHkK+)RI-v%Y3?{QV7ceuiHhym8 Gk?>zW6VGS> diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index d51de655..c7528b43 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -10,7 +10,6 @@ import random -import matplotlib.pyplot as plt import pytest from qbraid import random_circuit, transpile @@ -21,7 +20,6 @@ def _check_fig(circ, fig): ax = fig.gca() # plt.savefig("test.png") assert len(ax.texts) > 0 - plt.close(fig) # assert False @@ -43,6 +41,20 @@ def test_simple(): _check_fig(circ, fig) +def test_custom_gate(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + gate custom a { + h a; + z a; + } + custom q; + """ + circ = loads(qasm) + fig = circ.draw() + _check_fig(circ, fig) + @pytest.mark.parametrize("_", range(10)) def test_random(_): circ = random_circuit("qiskit", measure=random.choice([True, False])) From 56ae34388f996fae3a5ccfcbe54a0ea9d5b70968 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 15:55:12 -0800 Subject: [PATCH 09/29] fix double draw --- src/pyqasm/printer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index f139f1a7..0b8952ef 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -55,6 +55,7 @@ def draw(module: Qasm3Module, output="mpl"): raise ImportError("matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'") if output == "mpl": + plt.ioff() return _draw_mpl(module) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") From dbc5f3e5e86a79371cf36d6af064381e49f330d2 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 16:04:18 -0800 Subject: [PATCH 10/29] angle font --- src/pyqasm/printer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 0b8952ef..02f065c6 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -255,8 +255,7 @@ def _draw_mpl_one_qubit_gate( if gate.name.name == "h": color = HADAMARD_GATE_COLOR text = gate.name.name.upper() - if len(args) > 0: - text += f"\n({', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])})" + y = _mpl_line_to_y(line) rect = plt.Rectangle( @@ -267,7 +266,13 @@ def _draw_mpl_one_qubit_gate( edgecolor="none", ) ax.add_patch(rect) - ax.text(x, y, text, ha="center", va="center") + + if len(args) > 0: + args_text = f"{', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])}" + ax.text(x, y + GATE_BOX_HEIGHT/8, text, ha="center", va="center", fontsize=12) + ax.text(x, y - GATE_BOX_HEIGHT/4, args_text, ha="center", va="center", fontsize=8) + else: + ax.text(x, y, text, ha="center", va="center", fontsize=12) def _draw_mpl_control(ctrl_line: int, target_line: int, ax: plt.Axes, x: float): From d9c27f85bba7acc448bcff0ad6de1260e79fec42 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 16:17:22 -0800 Subject: [PATCH 11/29] idle wires option --- src/pyqasm/modules/base.py | 2 +- src/pyqasm/modules/qasm3.py | 4 ++-- src/pyqasm/printer.py | 28 ++++++++++++++++++++-------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index b27f4822..eb84ef1b 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -553,5 +553,5 @@ def accept(self, visitor): """ @abstractmethod - def draw(self): + def draw(self, **kwargs): """Draw the module""" diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index ee656776..c002a961 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -50,6 +50,6 @@ def accept(self, visitor): self._unrolled_ast.statements = final_stmt_list - def draw(self): + def draw(self, **kwargs): """Draw the module""" - return draw(self) + return draw(self, **kwargs) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 02f065c6..274081fa 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -50,23 +50,25 @@ FRAME_PADDING = 0.2 -def draw(module: Qasm3Module, output="mpl"): +def draw(module: Qasm3Module, output="mpl", **kwargs): if not mpl_installed: raise ImportError("matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'") if output == "mpl": plt.ioff() - return _draw_mpl(module) + plt.close('all') + return _draw_mpl(module, **kwargs) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") -def _draw_mpl(module: Qasm3Module) -> plt.Figure: +def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: module.unroll() module.remove_includes() module.remove_barriers() + + idle_wires = kwargs.get("idle_wires", True) - n_lines = module._num_qubits + module._num_clbits statements = module._statements # compute line numbers per qubit + max depth @@ -127,6 +129,14 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: for moment in moments: width += _mpl_get_moment_width(moment) width += TEXT_MARGIN + + if not idle_wires: + # remove all lines that are not used + line_nums = {k: v for k, v in line_nums.items() if depths[k] >= 0} + for i, k in enumerate(line_nums.keys()): + line_nums[k] = i + + n_lines = max(line_nums.values()) + 1 fig, ax = plt.subplots( figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1)) @@ -146,12 +156,14 @@ def _draw_mpl(module: Qasm3Module) -> plt.Figure: x = 0 for k in module._qubit_registers.keys(): for i in range(module._qubit_registers[k]): - line_num = line_nums[(k, i)] - _mpl_draw_qubit_label((k, i), line_num, ax, x) + if (k, i) in line_nums: + line_num = line_nums[(k, i)] + _mpl_draw_qubit_label((k, i), line_num, ax, x) for k in module._classical_registers.keys(): for i in range(module._classical_registers[k]): - line_num = line_nums[(k, i)] - _mpl_draw_clbit_label((k, i), line_num, ax, x) + if (k, i) in line_nums: + line_num = line_nums[(k, i)] + _mpl_draw_clbit_label((k, i), line_num, ax, x) x += TEXT_MARGIN x0 = x for moment in moments: From 0076925249c2a799ee8d17ae4fd7c7f79682e165 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 16:29:32 -0800 Subject: [PATCH 12/29] add barrier support --- src/pyqasm/printer.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 274081fa..485c192e 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -65,7 +65,6 @@ def draw(module: Qasm3Module, output="mpl", **kwargs): def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: module.unroll() module.remove_includes() - module.remove_barriers() idle_wires = kwargs.get("idle_wires", True) @@ -115,7 +114,10 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: for k in [qubit_key, target_key]: depths[k] = depth elif isinstance(statement, ast.QuantumBarrier): - pass + qubits = [_identifier_to_key(q) for q in statement.qubits] + depth = 1 + max([depths[q] for q in qubits]) + for q in qubits: + depths[q] = depth elif isinstance(statement, ast.QuantumReset): pass else: @@ -229,6 +231,9 @@ def _mpl_draw_statement( qubit_key = _identifier_to_key(statement.measure.qubit) target_key = _identifier_to_key(statement.target) _mpl_draw_measurement(line_nums[qubit_key], line_nums[target_key], ax, x) + elif isinstance(statement, ast.QuantumBarrier): + lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] + _mpl_draw_barrier(lines, ax, x) else: raise NotImplementedError(f"Unsupported statement: {statement}") @@ -322,3 +327,19 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, ax: plt.Axes, x: float x=x + 0.025, ymin=min(y1, y2), ymax=max(y1, y2), color="gray", linestyle="-", zorder=-1 ) ax.plot(x, y2 + 0.1, "v", markersize=16, color="gray") + + +def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): + for line in lines: + y = _mpl_line_to_y(line) + ax.vlines(x=x, ymin=y - GATE_BOX_HEIGHT/2 - LINE_SPACING/2, ymax=y + GATE_BOX_HEIGHT/2 + LINE_SPACING/2, color="black", linestyle="--") + rect = plt.Rectangle( + (x - GATE_BOX_WIDTH / 4, y - GATE_BOX_HEIGHT / 2 - LINE_SPACING/2), + GATE_BOX_WIDTH / 2, + GATE_BOX_HEIGHT + LINE_SPACING, + facecolor="lightgray", + edgecolor="none", + alpha=0.5, + zorder=-1 + ) + ax.add_patch(rect) From 77d68979af2b4a30d517a0de45e76238842e7bfe Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 17:33:19 -0800 Subject: [PATCH 13/29] group cregs + measure style --- src/pyqasm/printer.py | 72 ++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 485c192e..0fce95d5 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -72,16 +72,15 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: # compute line numbers per qubit + max depth line_nums = dict() + sizes = dict() line_num = -1 max_depth = 0 - for clbit_reg in module._classical_registers.keys(): - size = module._classical_registers[clbit_reg] - line_num += size - for i in range(size): - line_nums[(clbit_reg, i)] = line_num - line_num -= 1 - line_num += size + # classical registers are condensed into a single line + for k in module._classical_registers.keys(): + line_num += 1 + line_nums[(k, -1)] = line_num + sizes[(k, -1)] = module._classical_registers[k] for qubit_reg in module._qubit_registers.keys(): size = module._qubit_registers[qubit_reg] @@ -109,7 +108,7 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) - target_key = _identifier_to_key(statement.target) + target_key = _identifier_to_key(statement.target)[0], -1 depth = 1 + max(depths[qubit_key], depths[target_key]) for k in [qubit_key, target_key]: depths[k] = depth @@ -131,7 +130,7 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: for moment in moments: width += _mpl_get_moment_width(moment) width += TEXT_MARGIN - + if not idle_wires: # remove all lines that are not used line_nums = {k: v for k, v in line_nums.items() if depths[k] >= 0} @@ -161,16 +160,15 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: if (k, i) in line_nums: line_num = line_nums[(k, i)] _mpl_draw_qubit_label((k, i), line_num, ax, x) + for k in module._classical_registers.keys(): - for i in range(module._classical_registers[k]): - if (k, i) in line_nums: - line_num = line_nums[(k, i)] - _mpl_draw_clbit_label((k, i), line_num, ax, x) + _mpl_draw_creg_label(k, module._classical_registers[k], line_nums[(k, -1)], ax, x) + x += TEXT_MARGIN x0 = x - for moment in moments: + for i,moment in enumerate(moments): dx = _mpl_get_moment_width(moment) - _mpl_draw_lines(dx, line_nums, ax, x) + _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=i==0) x += dx x = x0 for moment in moments: @@ -195,21 +193,30 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu def _mpl_line_to_y(line_num: int) -> float: return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) - def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{qubit[0]}[{qubit[1]}]", ha="right", va="center") +def _mpl_draw_creg_label(creg: str, size: int, line_num: int, ax: plt.Axes, x: float): + ax.text(x, _mpl_line_to_y(line_num), f"{creg[0]}", ha="right", va="center") -def _mpl_draw_clbit_label(clbit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): - ax.text(x, _mpl_line_to_y(line_num), f"{clbit[0]}[{clbit[1]}]", ha="right", va="center") - - -def _mpl_draw_lines(width, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float): +def _mpl_draw_lines(width, line_nums: dict[tuple[str, int], int], sizes: dict[tuple[str, int], int], ax: plt.Axes, x: float, start=True): for k in line_nums.keys(): y = _mpl_line_to_y(line_nums[k]) - ax.hlines( - xmin=x - width / 2, xmax=x + width / 2, y=y, color="black", linestyle="-", zorder=-10 - ) + if k[1] == -1: + gap = GATE_BOX_HEIGHT/15 + ax.hlines( + xmin=x - width / 2, xmax=x + width / 2, y=y+gap/2, color="black", linestyle="-", zorder=-10 + ) + ax.hlines( + xmin=x - width / 2, xmax=x + width / 2, y=y-gap/2, color="black", linestyle="-", zorder=-10 + ) + if start: + ax.plot([x - width/2 + gap, x - width/2 + 2*gap], [y-2*gap, y+2*gap], color="black", zorder=-10) + ax.text(x - width/2 + 3*gap, y+3*gap, f"{sizes[k]}", fontsize=8) + else: + ax.hlines( + xmin=x - width / 2, xmax=x + width / 2, y=y, color="black", linestyle="-", zorder=-10 + ) def _mpl_get_moment_width(moment: list[ast.QuantumStatement]) -> float: @@ -230,7 +237,7 @@ def _mpl_draw_statement( elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) target_key = _identifier_to_key(statement.target) - _mpl_draw_measurement(line_nums[qubit_key], line_nums[target_key], ax, x) + _mpl_draw_measurement(line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x) elif isinstance(statement, ast.QuantumBarrier): lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] _mpl_draw_barrier(lines, ax, x) @@ -307,26 +314,29 @@ def _draw_mpl_swap(line1: int, line2: int, ax: plt.Axes, x: float): ax.plot(x, y2, "x", markersize=8, color="black") -def _mpl_draw_measurement(qbit_line: int, cbit_line: int, ax: plt.Axes, x: float): +def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes, x: float): y1 = _mpl_line_to_y(qbit_line) y2 = _mpl_line_to_y(cbit_line) - + + color = "#A0A0A0" + gap = GATE_BOX_WIDTH/3 rect = plt.Rectangle( (x - GATE_BOX_WIDTH / 2, y1 - GATE_BOX_HEIGHT / 2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, - facecolor="gray", + facecolor=color, edgecolor="none", ) ax.add_patch(rect) ax.text(x, y1, "M", ha="center", va="center") ax.vlines( - x=x - 0.025, ymin=min(y1, y2), ymax=max(y1, y2), color="gray", linestyle="-", zorder=-1 + x=x - gap/10, ymin=min(y1, y2)+gap, ymax=max(y1, y2), color=color, linestyle="-", zorder=-1 ) ax.vlines( - x=x + 0.025, ymin=min(y1, y2), ymax=max(y1, y2), color="gray", linestyle="-", zorder=-1 + x=x + gap/10, ymin=min(y1, y2)+gap, ymax=max(y1, y2), color=color, linestyle="-", zorder=-1 ) - ax.plot(x, y2 + 0.1, "v", markersize=16, color="gray") + ax.plot(x, y2 + gap, "v", markersize=12, color=color) + ax.text(x + gap, y2 + gap, str(idx), color=color, ha="left", va="bottom", fontsize=8) def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): From 5d61b1a2b7225a23cf0e4e16fa07f2fac9cf0c19 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 17:57:52 -0800 Subject: [PATCH 14/29] sections + idle wires order --- src/pyqasm/printer.py | 102 +++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 0fce95d5..1c95a0f0 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -43,6 +43,7 @@ DEFAULT_GATE_COLOR = "#d4b6e8" HADAMARD_GATE_COLOR = "#f0a6a6" +MAX_WIDTH = 12 GATE_BOX_WIDTH, GATE_BOX_HEIGHT = 0.6, 0.6 GATE_SPACING = 0.2 LINE_SPACING = 0.6 @@ -126,56 +127,75 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: moments.append([]) moments[depth].append(statement) - width = 0 - for moment in moments: - width += _mpl_get_moment_width(moment) - width += TEXT_MARGIN - if not idle_wires: # remove all lines that are not used line_nums = {k: v for k, v in line_nums.items() if depths[k] >= 0} for i, k in enumerate(line_nums.keys()): line_nums[k] = i + + sections = [[]] + width = TEXT_MARGIN + for moment in moments: + w = _mpl_get_moment_width(moment) + if width + w < MAX_WIDTH: + width += w + else: + width = TEXT_MARGIN + width = w + sections.append([]) + sections[-1].append(moment) + + if len(sections) >= 1: + width = MAX_WIDTH + n_lines = max(line_nums.values()) + 1 - fig, ax = plt.subplots( - figsize=(width, n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1)) + fig, axs = plt.subplots( + len(sections), + 1, + sharex=True, + figsize=(width, len(sections)*(n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))) ) - ax.set_ylim( - -GATE_BOX_HEIGHT / 2 - FRAME_PADDING / 2, - n_lines * GATE_BOX_HEIGHT - + LINE_SPACING * (n_lines - 1) - - GATE_BOX_HEIGHT / 2 - + FRAME_PADDING / 2, - ) - ax.set_xlim(-FRAME_PADDING / 2, width) - ax.axis("off") - # ax.set_aspect('equal') - # plt.tight_layout() - - x = 0 - for k in module._qubit_registers.keys(): - for i in range(module._qubit_registers[k]): - if (k, i) in line_nums: - line_num = line_nums[(k, i)] - _mpl_draw_qubit_label((k, i), line_num, ax, x) - - for k in module._classical_registers.keys(): - _mpl_draw_creg_label(k, module._classical_registers[k], line_nums[(k, -1)], ax, x) - - x += TEXT_MARGIN - x0 = x - for i,moment in enumerate(moments): - dx = _mpl_get_moment_width(moment) - _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=i==0) - x += dx - x = x0 - for moment in moments: - dx = _mpl_get_moment_width(moment) - for statement in moment: - _mpl_draw_statement(statement, line_nums, ax, x) - x += dx + if len(sections) == 1: + axs = [axs] + + for ax in axs: + ax.set_ylim( + -GATE_BOX_HEIGHT / 2 - FRAME_PADDING / 2, + n_lines * GATE_BOX_HEIGHT + + LINE_SPACING * (n_lines - 1) + - GATE_BOX_HEIGHT / 2 + + FRAME_PADDING / 2, + ) + ax.set_xlim(-FRAME_PADDING / 2, width) + ax.axis("off") + + for sidx,moments in enumerate(sections): + ax = axs[sidx] + x = 0 + if sidx == 0: + for k in module._qubit_registers.keys(): + for i in range(module._qubit_registers[k]): + if (k, i) in line_nums: + line_num = line_nums[(k, i)] + _mpl_draw_qubit_label((k, i), line_num, ax, x) + + for k in module._classical_registers.keys(): + _mpl_draw_creg_label(k, module._classical_registers[k], line_nums[(k, -1)], ax, x) + + x += TEXT_MARGIN + x0 = x + for i,moment in enumerate(moments): + dx = _mpl_get_moment_width(moment) + _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=(i == 0 and sidx == 0)) + x += dx + x = x0 + for moment in moments: + dx = _mpl_get_moment_width(moment) + for statement in moment: + _mpl_draw_statement(statement, line_nums, ax, x) + x += dx return fig From ada25132d25e0c22c074627c923f2f03c8e60621 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 17:58:20 -0800 Subject: [PATCH 15/29] overflow wrap + idle wires order fix --- src/pyqasm/printer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 1c95a0f0..b1490ef9 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -43,7 +43,7 @@ DEFAULT_GATE_COLOR = "#d4b6e8" HADAMARD_GATE_COLOR = "#f0a6a6" -MAX_WIDTH = 12 +FIG_MAX_WIDTH = 12 GATE_BOX_WIDTH, GATE_BOX_HEIGHT = 0.6, 0.6 GATE_SPACING = 0.2 LINE_SPACING = 0.6 @@ -126,19 +126,19 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: if depth >= len(moments): moments.append([]) moments[depth].append(statement) - + if not idle_wires: # remove all lines that are not used - line_nums = {k: v for k, v in line_nums.items() if depths[k] >= 0} - for i, k in enumerate(line_nums.keys()): - line_nums[k] = i + ks = sorted(line_nums.keys(), key=lambda k: line_nums[k]) + ks = [k for k in ks if depths[k] > 0] + line_nums = { k:i for i,k in enumerate(ks) } sections = [[]] width = TEXT_MARGIN for moment in moments: w = _mpl_get_moment_width(moment) - if width + w < MAX_WIDTH: + if width + w < FIG_MAX_WIDTH: width += w else: width = TEXT_MARGIN @@ -146,8 +146,8 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: sections.append([]) sections[-1].append(moment) - if len(sections) >= 1: - width = MAX_WIDTH + if len(sections) > 1: + width = FIG_MAX_WIDTH n_lines = max(line_nums.values()) + 1 From 4a7848c716b24eca4df9a58e2b585558c71c0747 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 18:02:00 -0800 Subject: [PATCH 16/29] linting --- src/pyqasm/printer.py | 118 +++++++++++++++++++++++++----------- tests/qasm3/test_printer.py | 4 +- 2 files changed, 85 insertions(+), 37 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index b1490ef9..714bc2f7 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -1,5 +1,4 @@ -# Copyright (C) 2024 qBraid -# +# Copyright (C) 2025 qBraid# # This file is part of pyqasm # # Pyqasm is free software released under the GNU General Public License v3 @@ -31,13 +30,13 @@ try: from matplotlib import pyplot as plt + mpl_installed = True except ImportError as e: mpl_installed = False if TYPE_CHECKING: from pyqasm.modules.base import Qasm3Module - DEFAULT_GATE_COLOR = "#d4b6e8" @@ -53,11 +52,13 @@ def draw(module: Qasm3Module, output="mpl", **kwargs): if not mpl_installed: - raise ImportError("matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'") + raise ImportError( + "matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'" + ) if output == "mpl": plt.ioff() - plt.close('all') + plt.close("all") return _draw_mpl(module, **kwargs) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") @@ -66,7 +67,7 @@ def draw(module: Qasm3Module, output="mpl", **kwargs): def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: module.unroll() module.remove_includes() - + idle_wires = kwargs.get("idle_wires", True) statements = module._statements @@ -126,15 +127,15 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: if depth >= len(moments): moments.append([]) moments[depth].append(statement) - + if not idle_wires: - # remove all lines that are not used + # remove all lines that are not used ks = sorted(line_nums.keys(), key=lambda k: line_nums[k]) ks = [k for k in ks if depths[k] > 0] - line_nums = { k:i for i,k in enumerate(ks) } + line_nums = {k: i for i, k in enumerate(ks)} sections = [[]] - + width = TEXT_MARGIN for moment in moments: w = _mpl_get_moment_width(moment) @@ -145,7 +146,7 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: width = w sections.append([]) sections[-1].append(moment) - + if len(sections) > 1: width = FIG_MAX_WIDTH @@ -155,11 +156,11 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: len(sections), 1, sharex=True, - figsize=(width, len(sections)*(n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))) + figsize=(width, len(sections) * (n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))), ) if len(sections) == 1: axs = [axs] - + for ax in axs: ax.set_ylim( -GATE_BOX_HEIGHT / 2 - FRAME_PADDING / 2, @@ -171,7 +172,7 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: ax.set_xlim(-FRAME_PADDING / 2, width) ax.axis("off") - for sidx,moments in enumerate(sections): + for sidx, moments in enumerate(sections): ax = axs[sidx] x = 0 if sidx == 0: @@ -186,7 +187,7 @@ def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: x += TEXT_MARGIN x0 = x - for i,moment in enumerate(moments): + for i, moment in enumerate(moments): dx = _mpl_get_moment_width(moment) _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=(i == 0 and sidx == 0)) x += dx @@ -213,29 +214,59 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu def _mpl_line_to_y(line_num: int) -> float: return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) + def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{qubit[0]}[{qubit[1]}]", ha="right", va="center") + def _mpl_draw_creg_label(creg: str, size: int, line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{creg[0]}", ha="right", va="center") -def _mpl_draw_lines(width, line_nums: dict[tuple[str, int], int], sizes: dict[tuple[str, int], int], ax: plt.Axes, x: float, start=True): + +def _mpl_draw_lines( + width, + line_nums: dict[tuple[str, int], int], + sizes: dict[tuple[str, int], int], + ax: plt.Axes, + x: float, + start=True, +): for k in line_nums.keys(): y = _mpl_line_to_y(line_nums[k]) if k[1] == -1: - gap = GATE_BOX_HEIGHT/15 + gap = GATE_BOX_HEIGHT / 15 ax.hlines( - xmin=x - width / 2, xmax=x + width / 2, y=y+gap/2, color="black", linestyle="-", zorder=-10 + xmin=x - width / 2, + xmax=x + width / 2, + y=y + gap / 2, + color="black", + linestyle="-", + zorder=-10, ) ax.hlines( - xmin=x - width / 2, xmax=x + width / 2, y=y-gap/2, color="black", linestyle="-", zorder=-10 + xmin=x - width / 2, + xmax=x + width / 2, + y=y - gap / 2, + color="black", + linestyle="-", + zorder=-10, ) if start: - ax.plot([x - width/2 + gap, x - width/2 + 2*gap], [y-2*gap, y+2*gap], color="black", zorder=-10) - ax.text(x - width/2 + 3*gap, y+3*gap, f"{sizes[k]}", fontsize=8) + ax.plot( + [x - width / 2 + gap, x - width / 2 + 2 * gap], + [y - 2 * gap, y + 2 * gap], + color="black", + zorder=-10, + ) + ax.text(x - width / 2 + 3 * gap, y + 3 * gap, f"{sizes[k]}", fontsize=8) else: ax.hlines( - xmin=x - width / 2, xmax=x + width / 2, y=y, color="black", linestyle="-", zorder=-10 + xmin=x - width / 2, + xmax=x + width / 2, + y=y, + color="black", + linestyle="-", + zorder=-10, ) @@ -257,7 +288,9 @@ def _mpl_draw_statement( elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) target_key = _identifier_to_key(statement.target) - _mpl_draw_measurement(line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x) + _mpl_draw_measurement( + line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x + ) elif isinstance(statement, ast.QuantumBarrier): lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] _mpl_draw_barrier(lines, ax, x) @@ -299,7 +332,6 @@ def _draw_mpl_one_qubit_gate( if gate.name.name == "h": color = HADAMARD_GATE_COLOR text = gate.name.name.upper() - y = _mpl_line_to_y(line) rect = plt.Rectangle( @@ -310,11 +342,11 @@ def _draw_mpl_one_qubit_gate( edgecolor="none", ) ax.add_patch(rect) - + if len(args) > 0: args_text = f"{', '.join([f'{a:.3f}' if isinstance(a, float) else str(a) for a in args])}" - ax.text(x, y + GATE_BOX_HEIGHT/8, text, ha="center", va="center", fontsize=12) - ax.text(x, y - GATE_BOX_HEIGHT/4, args_text, ha="center", va="center", fontsize=8) + ax.text(x, y + GATE_BOX_HEIGHT / 8, text, ha="center", va="center", fontsize=12) + ax.text(x, y - GATE_BOX_HEIGHT / 4, args_text, ha="center", va="center", fontsize=8) else: ax.text(x, y, text, ha="center", va="center", fontsize=12) @@ -337,9 +369,9 @@ def _draw_mpl_swap(line1: int, line2: int, ax: plt.Axes, x: float): def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes, x: float): y1 = _mpl_line_to_y(qbit_line) y2 = _mpl_line_to_y(cbit_line) - + color = "#A0A0A0" - gap = GATE_BOX_WIDTH/3 + gap = GATE_BOX_WIDTH / 3 rect = plt.Rectangle( (x - GATE_BOX_WIDTH / 2, y1 - GATE_BOX_HEIGHT / 2), GATE_BOX_WIDTH, @@ -350,10 +382,20 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes ax.add_patch(rect) ax.text(x, y1, "M", ha="center", va="center") ax.vlines( - x=x - gap/10, ymin=min(y1, y2)+gap, ymax=max(y1, y2), color=color, linestyle="-", zorder=-1 + x=x - gap / 10, + ymin=min(y1, y2) + gap, + ymax=max(y1, y2), + color=color, + linestyle="-", + zorder=-1, ) ax.vlines( - x=x + gap/10, ymin=min(y1, y2)+gap, ymax=max(y1, y2), color=color, linestyle="-", zorder=-1 + x=x + gap / 10, + ymin=min(y1, y2) + gap, + ymax=max(y1, y2), + color=color, + linestyle="-", + zorder=-1, ) ax.plot(x, y2 + gap, "v", markersize=12, color=color) ax.text(x + gap, y2 + gap, str(idx), color=color, ha="left", va="bottom", fontsize=8) @@ -362,14 +404,20 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): for line in lines: y = _mpl_line_to_y(line) - ax.vlines(x=x, ymin=y - GATE_BOX_HEIGHT/2 - LINE_SPACING/2, ymax=y + GATE_BOX_HEIGHT/2 + LINE_SPACING/2, color="black", linestyle="--") + ax.vlines( + x=x, + ymin=y - GATE_BOX_HEIGHT / 2 - LINE_SPACING / 2, + ymax=y + GATE_BOX_HEIGHT / 2 + LINE_SPACING / 2, + color="black", + linestyle="--", + ) rect = plt.Rectangle( - (x - GATE_BOX_WIDTH / 4, y - GATE_BOX_HEIGHT / 2 - LINE_SPACING/2), + (x - GATE_BOX_WIDTH / 4, y - GATE_BOX_HEIGHT / 2 - LINE_SPACING / 2), GATE_BOX_WIDTH / 2, GATE_BOX_HEIGHT + LINE_SPACING, facecolor="lightgray", edgecolor="none", alpha=0.5, - zorder=-1 + zorder=-1, ) - ax.add_patch(rect) + ax.add_patch(rect) \ No newline at end of file diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index c7528b43..3597c82b 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -1,5 +1,4 @@ -# Copyright (C) 2024 qBraid -# +# Copyright (C) 2025 qBraid# # This file is part of pyqasm # # Pyqasm is free software released under the GNU General Public License v3 @@ -55,6 +54,7 @@ def test_custom_gate(): fig = circ.draw() _check_fig(circ, fig) + @pytest.mark.parametrize("_", range(10)) def test_random(_): circ = random_circuit("qiskit", measure=random.choice([True, False])) From 5575332c6f17236c8ed7b7f79c3796e12984a0fb Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 16 Jan 2025 19:29:57 -0800 Subject: [PATCH 17/29] fix kwargs --- src/pyqasm/modules/base.py | 2 +- src/pyqasm/modules/qasm3.py | 4 ++-- src/pyqasm/printer.py | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index eb84ef1b..a013e766 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -553,5 +553,5 @@ def accept(self, visitor): """ @abstractmethod - def draw(self, **kwargs): + def draw(self, idle_wires=True): """Draw the module""" diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index c002a961..6a805b31 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -50,6 +50,6 @@ def accept(self, visitor): self._unrolled_ast.statements = final_stmt_list - def draw(self, **kwargs): + def draw(self, idle_wires=True): """Draw the module""" - return draw(self, **kwargs) + return draw(self, idle_wires=idle_wires) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index b27eaf13..92af057c 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -47,7 +47,7 @@ FRAME_PADDING = 0.2 -def draw(module: Qasm3Module, output="mpl", **kwargs): +def draw(module: Qasm3Module, output="mpl", idle_wires=True): if not mpl_installed: raise ImportError( "matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'" @@ -56,17 +56,15 @@ def draw(module: Qasm3Module, output="mpl", **kwargs): if output == "mpl": plt.ioff() plt.close("all") - return _draw_mpl(module, **kwargs) + return _draw_mpl(module, idle_wires=idle_wires) else: raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") -def _draw_mpl(module: Qasm3Module, **kwargs) -> plt.Figure: +def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: module.unroll() module.remove_includes() - idle_wires = kwargs.get("idle_wires", True) - statements = module._statements # compute line numbers per qubit + max depth From f6d4d77700f2f4401612d546a57e50da5187b9fa Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 17 Jan 2025 03:02:50 -0800 Subject: [PATCH 18/29] phase placeholder --- src/pyqasm/printer.py | 9 +++++++++ tests/qasm3/test_printer.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 92af057c..f75cd261 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -109,6 +109,12 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: depth = 1 + max(depths[qubit_key], depths[target_key]) for k in [qubit_key, target_key]: depths[k] = depth + elif isinstance(statement, ast.QuantumPhase): + qubits = [_identifier_to_key(q) for q in statement.qubits] + if len(qubits) > 0: + depth = 1 + max([depths[q] for q in qubits]) + for q in qubits: + depths[q] = depth elif isinstance(statement, ast.QuantumBarrier): qubits = [_identifier_to_key(q) for q in statement.qubits] depth = 1 + max([depths[q] for q in qubits]) @@ -284,6 +290,9 @@ def _mpl_draw_statement( _mpl_draw_measurement( line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x ) + elif isinstance(statement, ast.QuantumPhase): + # TODO: draw gphase + pass elif isinstance(statement, ast.QuantumBarrier): lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] _mpl_draw_barrier(lines, ax, x) diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index 3597c82b..198b45ee 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -55,7 +55,7 @@ def test_custom_gate(): _check_fig(circ, fig) -@pytest.mark.parametrize("_", range(10)) +@pytest.mark.parametrize("_", range(100)) def test_random(_): circ = random_circuit("qiskit", measure=random.choice([True, False])) qasm_str = transpile(circ, random.choice(["qasm2", "qasm3"])) From 11eeb84b7f19a5822e3442f2c691c252a8eb2338 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Mon, 20 Jan 2025 13:37:14 -0500 Subject: [PATCH 19/29] global phase --- src/pyqasm/printer.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index f75cd261..8e2dc466 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -93,6 +93,9 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: depths = dict() for k in line_nums.keys(): depths[k] = -1 + + global_phase = sum([Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] for s in statements if isinstance(s, ast.QuantumPhase)]) + statements = [s for s in statements if not isinstance(s, ast.QuantumPhase)] moments = [] for statement in statements: @@ -109,12 +112,6 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: depth = 1 + max(depths[qubit_key], depths[target_key]) for k in [qubit_key, target_key]: depths[k] = depth - elif isinstance(statement, ast.QuantumPhase): - qubits = [_identifier_to_key(q) for q in statement.qubits] - if len(qubits) > 0: - depth = 1 + max([depths[q] for q in qubits]) - for q in qubits: - depths[q] = depth elif isinstance(statement, ast.QuantumBarrier): qubits = [_identifier_to_key(q) for q in statement.qubits] depth = 1 + max([depths[q] for q in qubits]) @@ -175,6 +172,7 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: ax = axs[sidx] x = 0 if sidx == 0: + if global_phase != 0: _mpl_draw_global_phase(global_phase, ax, x) for k in module._qubit_registers.keys(): for i in range(module._qubit_registers[k]): if (k, i) in line_nums: @@ -213,11 +211,11 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu def _mpl_line_to_y(line_num: int) -> float: return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) +def _mpl_draw_global_phase(global_phase: float, ax: plt.Axes, x: float): + ax.text(x, -0.75, f"Global Phase: {global_phase:.3f}", ha="left", va="center") def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{qubit[0]}[{qubit[1]}]", ha="right", va="center") - - def _mpl_draw_creg_label(creg: str, size: int, line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{creg[0]}", ha="right", va="center") @@ -290,9 +288,6 @@ def _mpl_draw_statement( _mpl_draw_measurement( line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x ) - elif isinstance(statement, ast.QuantumPhase): - # TODO: draw gphase - pass elif isinstance(statement, ast.QuantumBarrier): lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] _mpl_draw_barrier(lines, ax, x) From 8d8bfec86034864d6f97ba2ebd58a50833b36c48 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 14 Feb 2025 02:19:55 -0500 Subject: [PATCH 20/29] reset + labels + linting --- src/pyqasm/expressions.py | 6 +- src/pyqasm/maps/gates.py | 2 +- src/pyqasm/modules/qasm2.py | 4 +- src/pyqasm/printer.py | 255 +++++++++++++++++++--------- tests/qasm2/test_rotation_gates.py | 2 +- tests/qasm3/conftest.py | 3 +- tests/qasm3/test_printer.py | 24 ++- tests/qasm3/test_transformations.py | 2 +- 8 files changed, 199 insertions(+), 99 deletions(-) diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 705540c4..7c6e75af 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -29,7 +29,11 @@ IntegerLiteral, ) from openqasm3.ast import IntType as Qasm3IntType -from openqasm3.ast import SizeOf, Statement, UnaryExpression +from openqasm3.ast import ( + SizeOf, + Statement, + UnaryExpression, +) from pyqasm.analyzer import Qasm3Analyzer from pyqasm.exceptions import ValidationError, raise_qasm3_error diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index 1a9b4798..5022d111 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -1169,6 +1169,7 @@ def map_qasm_inv_op_to_callable(op_name: str): ) raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") + REV_CTRL_GATE_MAP = { "cx": "x", "cy": "y", @@ -1182,4 +1183,3 @@ def map_qasm_inv_op_to_callable(op_name: str): "cswap": "swap", "ccx": "cx", } - diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index 2df24abb..37e2e463 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -107,6 +107,6 @@ def accept(self, visitor): self.unrolled_ast.statements = final_stmt_list - def draw(self): + def draw(self, idle_wires=True): """Draw the module""" - return draw(self.to_qasm3()) + return draw(self.to_qasm3(), idle_wires=idle_wires) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 8e2dc466..acdd94ec 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -1,11 +1,12 @@ -# Copyright (C) 2025 qBraid# -# This file is part of pyqasm +# Copyright (C) 2025 qBraid # -# Pyqasm is free software released under the GNU General Public License v3 +# This file is part of PyQASM +# +# PyQASM is free software released under the GNU General Public License v3 # or later. You can redistribute and/or modify it under the terms of the GPL v3. # See the LICENSE file in the project root or . # -# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. """ Module with analysis functions for QASM visitor @@ -15,7 +16,8 @@ from typing import TYPE_CHECKING, Any -import openqasm3.ast as ast +from matplotlib import pyplot as plt +from openqasm3 import ast from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps.gates import ( @@ -26,60 +28,95 @@ ) try: - from matplotlib import pyplot as plt - - mpl_installed = True -except ImportError as e: - mpl_installed = False + MPL_INSTALLED = True +except ImportError: + MPL_INSTALLED = False if TYPE_CHECKING: from pyqasm.modules.base import Qasm3Module - +# Constants DEFAULT_GATE_COLOR = "#d4b6e8" HADAMARD_GATE_COLOR = "#f0a6a6" FIG_MAX_WIDTH = 12 -GATE_BOX_WIDTH, GATE_BOX_HEIGHT = 0.6, 0.6 +GATE_BOX_WIDTH = 0.6 +GATE_BOX_HEIGHT = 0.6 GATE_SPACING = 0.2 LINE_SPACING = 0.6 TEXT_MARGIN = 0.6 FRAME_PADDING = 0.2 -def draw(module: Qasm3Module, output="mpl", idle_wires=True): - if not mpl_installed: +def draw(module: Qasm3Module, output: str = "mpl", idle_wires: bool = True) -> plt.Figure: + """Draw the quantum circuit. + + Args: + module: The quantum module to draw + output: The output format, currently only "mpl" supported + idle_wires: Whether to show idle wires + + Returns: + The matplotlib figure + """ + if not MPL_INSTALLED: raise ImportError( - "matplotlib needs to be installed prior to running pyqasm.draw(). You can install matplotlib with:\n'pip install pyqasm[visualization]'" + "matplotlib needs to be installed prior to running pyqasm.draw(). " + "Install with: 'pip install pyqasm[visualization]'" ) - if output == "mpl": - plt.ioff() - plt.close("all") - return _draw_mpl(module, idle_wires=idle_wires) - else: + if output != "mpl": raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") + plt.ioff() + plt.close("all") + return _draw_mpl(module, idle_wires=idle_wires) + -def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: +def _draw_mpl(module: Qasm3Module, idle_wires: bool = True) -> plt.Figure: + """Internal matplotlib drawing implementation.""" module.unroll() module.remove_includes() - statements = module._statements + line_nums, sizes = _compute_line_nums(module) + + global_phase = sum( + Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] + for s in module._statements + if isinstance(s, ast.QuantumPhase) + ) + statements = [s for s in module._statements if not isinstance(s, ast.QuantumPhase)] + + # Compute moments + moments, depths = _compute_moments(statements, line_nums) + + if not idle_wires: + # remove all lines that are not used + ks = sorted(line_nums.keys(), key=lambda k: line_nums[k]) + ks = [k for k in ks if depths[k] > 0] + line_nums = {k: i for i, k in enumerate(ks)} + + fig = _mpl_draw(module, moments, line_nums, sizes, global_phase) + return fig + - # compute line numbers per qubit + max depth - line_nums = dict() - sizes = dict() +def _compute_line_nums( + module: Qasm3Module, +) -> tuple[dict[tuple[str, int], int], dict[tuple[str, int], int], int]: + """Compute line number and register size lookup table for the circuit.""" + line_nums = {} + sizes = {} line_num = -1 max_depth = 0 - # classical registers are condensed into a single line - for k in module._classical_registers.keys(): + # Classical registers condensed to single line + for k in module._classical_registers: line_num += 1 line_nums[(k, -1)] = line_num sizes[(k, -1)] = module._classical_registers[k] - for qubit_reg in module._qubit_registers.keys(): + # Calculate qubit lines and depths + for qubit_reg in module._qubit_registers: size = module._qubit_registers[qubit_reg] line_num += size for i in range(size): @@ -89,13 +126,15 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: line_num -= 1 line_num += size - # compute moments - depths = dict() - for k in line_nums.keys(): + return line_nums, sizes + + +def _compute_moments( + statements: list[ast.QuantumStatement], line_nums: dict[tuple[str, int], int] +) -> tuple[list[list[ast.QuantumStatement]], dict[tuple[str, int], int]]: + depths = {} + for k in line_nums: depths[k] = -1 - - global_phase = sum([Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] for s in statements if isinstance(s, ast.QuantumPhase)]) - statements = [s for s in statements if not isinstance(s, ast.QuantumPhase)] moments = [] for statement in statements: @@ -103,7 +142,7 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: continue if isinstance(statement, ast.QuantumGate): qubits = [_identifier_to_key(q) for q in statement.qubits] - depth = 1 + max([depths[q] for q in qubits]) + depth = 1 + max(depths[q] for q in qubits) for q in qubits: depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): @@ -114,9 +153,13 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: depths[k] = depth elif isinstance(statement, ast.QuantumBarrier): qubits = [_identifier_to_key(q) for q in statement.qubits] - depth = 1 + max([depths[q] for q in qubits]) + depth = 1 + max(depths[q] for q in qubits) for q in qubits: depths[q] = depth + elif isinstance(statement, ast.QuantumReset): + qubit_key = _identifier_to_key(statement.qubits) + depth = 1 + depths[qubit_key] + depths[qubit_key] = depth else: raise NotImplementedError(f"Unsupported statement: {statement}") @@ -124,12 +167,22 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: moments.append([]) moments[depth].append(statement) - if not idle_wires: - # remove all lines that are not used - ks = sorted(line_nums.keys(), key=lambda k: line_nums[k]) - ks = [k for k in ks if depths[k] > 0] - line_nums = {k: i for i, k in enumerate(ks)} + return moments, depths + +def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tuple[str, int]: + if isinstance(identifier, ast.Identifier): + return identifier.name, -1 + + return ( + identifier.name.name, + Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0], + ) + + +def _compute_sections( + moments: list[list[ast.QuantumStatement]], +) -> list[list[ast.QuantumStatement]]: sections = [[]] width = TEXT_MARGIN @@ -146,8 +199,29 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: if len(sections) > 1: width = FIG_MAX_WIDTH + return sections, width + + +def _mpl_draw( + module: Qasm3Module, + moments: list[list[ast.QuantumStatement]], + line_nums: dict[tuple[str, int], int], + sizes: dict[tuple[str, int], int], + global_phase: float, +): + sections, width = _compute_sections(moments) n_lines = max(line_nums.values()) + 1 + fig, axs = _mpl_setup_figure(sections, width, n_lines) + + for sidx, ms in enumerate(sections): + ax = axs[sidx] + _mpl_draw_section(module, ms, line_nums, sizes, ax, global_phase) + + return fig + + +def _mpl_setup_figure(sections: list[list[ast.QuantumStatement]], width: float, n_lines: int): fig, axs = plt.subplots( len(sections), 1, @@ -168,58 +242,61 @@ def _draw_mpl(module: Qasm3Module, idle_wires=True) -> plt.Figure: ax.set_xlim(-FRAME_PADDING / 2, width) ax.axis("off") - for sidx, moments in enumerate(sections): - ax = axs[sidx] - x = 0 - if sidx == 0: - if global_phase != 0: _mpl_draw_global_phase(global_phase, ax, x) - for k in module._qubit_registers.keys(): - for i in range(module._qubit_registers[k]): - if (k, i) in line_nums: - line_num = line_nums[(k, i)] - _mpl_draw_qubit_label((k, i), line_num, ax, x) - - for k in module._classical_registers.keys(): - _mpl_draw_creg_label(k, module._classical_registers[k], line_nums[(k, -1)], ax, x) - - x += TEXT_MARGIN - x0 = x - for i, moment in enumerate(moments): - dx = _mpl_get_moment_width(moment) - _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=(i == 0 and sidx == 0)) - x += dx - x = x0 - for moment in moments: - dx = _mpl_get_moment_width(moment) - for statement in moment: - _mpl_draw_statement(statement, line_nums, ax, x) - x += dx + return fig, axs - return fig +# pylint: disable=too-many-arguments +def _mpl_draw_section( + module: Qasm3Module, + moments: list[list[ast.QuantumStatement]], + line_nums: dict[tuple[str, int], int], + sizes: dict[tuple[str, int], int], + ax: plt.Axes, + global_phase: float, +): + x = 0 + if global_phase != 0: + _mpl_draw_global_phase(global_phase, ax, x) + for k in module._qubit_registers.keys(): + for i in range(module._qubit_registers[k]): + if (k, i) in line_nums: + line_num = line_nums[(k, i)] + _mpl_draw_qubit_label((k, i), line_num, ax, x) -def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tuple[str, int]: - if isinstance(identifier, ast.Identifier): - return identifier.name, -1 - else: - return ( - identifier.name.name, - Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0], - ) + for k in module._classical_registers.keys(): + _mpl_draw_creg_label(k, line_nums[(k, -1)], ax, x) + + x += TEXT_MARGIN + x0 = x + for i, moment in enumerate(moments): + dx = _mpl_get_moment_width(moment) + _mpl_draw_lines(dx, line_nums, sizes, ax, x, start=i == 0) + x += dx + x = x0 + for moment in moments: + dx = _mpl_get_moment_width(moment) + for statement in moment: + _mpl_draw_statement(statement, line_nums, ax, x) + x += dx def _mpl_line_to_y(line_num: int) -> float: return line_num * (GATE_BOX_HEIGHT + LINE_SPACING) + def _mpl_draw_global_phase(global_phase: float, ax: plt.Axes, x: float): ax.text(x, -0.75, f"Global Phase: {global_phase:.3f}", ha="left", va="center") + def _mpl_draw_qubit_label(qubit: tuple[str, int], line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{qubit[0]}[{qubit[1]}]", ha="right", va="center") -def _mpl_draw_creg_label(creg: str, size: int, line_num: int, ax: plt.Axes, x: float): + + +def _mpl_draw_creg_label(creg: str, line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{creg[0]}", ha="right", va="center") +# pylint: disable=too-many-arguments def _mpl_draw_lines( width, line_nums: dict[tuple[str, int], int], @@ -268,10 +345,10 @@ def _mpl_draw_lines( def _mpl_get_moment_width(moment: list[ast.QuantumStatement]) -> float: - return max([_mpl_get_statement_width(s) for s in moment]) + return max(_mpl_get_statement_width(s) for s in moment) -def _mpl_get_statement_width(statement: ast.QuantumStatement) -> float: +def _mpl_get_statement_width(_: ast.QuantumStatement) -> float: return GATE_BOX_WIDTH + GATE_SPACING @@ -291,6 +368,8 @@ def _mpl_draw_statement( elif isinstance(statement, ast.QuantumBarrier): lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] _mpl_draw_barrier(lines, ax, x) + elif isinstance(statement, ast.QuantumReset): + _mpl_draw_reset(line_nums[_identifier_to_key(statement.qubits)], ax, x) else: raise NotImplementedError(f"Unsupported statement: {statement}") @@ -319,9 +398,6 @@ def _mpl_draw_gate( raise NotImplementedError(f"Unsupported gate: {name}") -# TODO: switch to moment based system. go progressively, calculating required width for each moment, center the rest. this makes position calculations not to bad. if we overflow, start a new figure. - - def _draw_mpl_one_qubit_gate( gate: ast.QuantumGate, args: list[Any], line: int, ax: plt.Axes, x: float ): @@ -417,4 +493,17 @@ def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): alpha=0.5, zorder=-1, ) - ax.add_patch(rect) \ No newline at end of file + ax.add_patch(rect) + + +def _mpl_draw_reset(line: int, ax: plt.Axes, x: float): + y = _mpl_line_to_y(line) + rect = plt.Rectangle( + (x - GATE_BOX_WIDTH / 2, y - GATE_BOX_HEIGHT / 2), + GATE_BOX_WIDTH, + GATE_BOX_HEIGHT, + facecolor="lightgray", + edgecolor="none", + ) + ax.add_patch(rect) + ax.text(x, y, "∣0⟩", ha="center", va="center", fontsize=12) diff --git a/tests/qasm2/test_rotation_gates.py b/tests/qasm2/test_rotation_gates.py index e08aabb0..05ba4a34 100644 --- a/tests/qasm2/test_rotation_gates.py +++ b/tests/qasm2/test_rotation_gates.py @@ -9,7 +9,7 @@ # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. """ -Module containing unit tests for parsing and unrolling programs that contain quantum +Module containing unit tests for parsing and unrolling programs that contain quantum rotations in qasm2 format. """ diff --git a/tests/qasm3/conftest.py b/tests/qasm3/conftest.py index a81d6a03..68ed0652 100644 --- a/tests/qasm3/conftest.py +++ b/tests/qasm3/conftest.py @@ -8,8 +8,7 @@ # # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. -"""Module containing imports for unit test resources -""" +"""Module containing imports for unit test resources""" # pylint: disable=wildcard-import, unused-wildcard-import diff --git a/tests/qasm3/test_printer.py b/tests/qasm3/test_printer.py index 198b45ee..4b934a34 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/qasm3/test_printer.py @@ -1,11 +1,16 @@ -# Copyright (C) 2025 qBraid# -# This file is part of pyqasm +# Copyright (C) 2025 qBraid # -# Pyqasm is free software released under the GNU General Public License v3 +# This file is part of PyQASM +# +# PyQASM is free software released under the GNU General Public License v3 # or later. You can redistribute and/or modify it under the terms of the GPL v3. # See the LICENSE file in the project root or . # -# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. + +""" +Tests for the QASM printer module. +""" import random @@ -15,11 +20,14 @@ from pyqasm.entrypoint import loads -def _check_fig(circ, fig): +def _check_fig(_, fig): + """Verify the matplotlib figure contains expected elements. + + Args: + fig: a matplotlib figure + """ ax = fig.gca() - # plt.savefig("test.png") assert len(ax.texts) > 0 - # assert False def test_simple(): @@ -61,4 +69,4 @@ def test_random(_): qasm_str = transpile(circ, random.choice(["qasm2", "qasm3"])) module = loads(qasm_str) fig = module.draw() - _check_fig(circ, fig) \ No newline at end of file + _check_fig(circ, fig) diff --git a/tests/qasm3/test_transformations.py b/tests/qasm3/test_transformations.py index 2313edca..ca422203 100644 --- a/tests/qasm3/test_transformations.py +++ b/tests/qasm3/test_transformations.py @@ -9,7 +9,7 @@ # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. """ -Module containing unit tests for transformations on qasm3 programs +Module containing unit tests for transformations on qasm3 programs """ From 6750d22079d08e5dac2df9edae649b3c4c406245 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 14 Feb 2025 15:32:32 -0600 Subject: [PATCH 21/29] build, static type checking, user interface --- docs/conf.py | 4 +- src/pyqasm/__init__.py | 3 + src/pyqasm/maps/gates.py | 2 +- src/pyqasm/modules/base.py | 4 - src/pyqasm/modules/qasm2.py | 5 - src/pyqasm/modules/qasm3.py | 5 - src/pyqasm/printer.py | 225 ++++++++++++------ .../test_printer.py => test_mpl_draw.py} | 52 ++-- tox.ini | 2 +- 9 files changed, 197 insertions(+), 105 deletions(-) rename tests/{qasm3/test_printer.py => test_mpl_draw.py} (59%) diff --git a/docs/conf.py b/docs/conf.py index 2d088fc3..e70eb0f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ # -- Project information ----------------------------------------------------- project = "qBraid" -copyright = "2024, qBraid Development Team" +copyright = "2025, qBraid Development Team" author = "qBraid Development Team" # The full version, including alpha/beta/rc tags @@ -41,7 +41,7 @@ # set_type_checking_flag = True autodoc_member_order = "bysource" autoclass_content = "both" -autodoc_mock_imports = ["openqasm3"] +autodoc_mock_imports = ["openqasm3", "matplotlib"] napoleon_numpy_docstring = False todo_include_todos = True mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS-MML_HTMLorMML" diff --git a/src/pyqasm/__init__.py b/src/pyqasm/__init__.py index 990e65b6..11cf953c 100644 --- a/src/pyqasm/__init__.py +++ b/src/pyqasm/__init__.py @@ -23,6 +23,7 @@ loads dump dumps + draw Classes --------- @@ -57,6 +58,7 @@ from .entrypoint import dump, dumps, load, loads from .exceptions import PyQasmError, QasmParsingError, ValidationError from .modules import Qasm2Module, Qasm3Module, QasmModule +from .printer import draw __all__ = [ "PyQasmError", @@ -66,6 +68,7 @@ "loads", "dump", "dumps", + "draw", "QasmModule", "Qasm2Module", "Qasm3Module", diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index 6defee3f..af2a7a79 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -1250,4 +1250,4 @@ def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): # TODO: decompose controls if not built in raise ValidationError( f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controls" - ) \ No newline at end of file + ) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 8fa52fca..c0a796d0 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -602,7 +602,3 @@ def accept(self, visitor): Args: visitor (QasmVisitor): The visitor to accept """ - - @abstractmethod - def draw(self, idle_wires=True): - """Draw the module""" diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index 37e2e463..3c30d8eb 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -23,7 +23,6 @@ from pyqasm.exceptions import ValidationError from pyqasm.modules.base import QasmModule from pyqasm.modules.qasm3 import Qasm3Module -from pyqasm.printer import draw class Qasm2Module(QasmModule): @@ -106,7 +105,3 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self.unrolled_ast.statements = final_stmt_list - - def draw(self, idle_wires=True): - """Draw the module""" - return draw(self.to_qasm3(), idle_wires=idle_wires) diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 76cb0d41..0592aa17 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,7 +16,6 @@ from openqasm3.printer import dumps from pyqasm.modules.base import QasmModule -from pyqasm.printer import draw class Qasm3Module(QasmModule): @@ -49,7 +48,3 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self._unrolled_ast.statements = final_stmt_list - - def draw(self, idle_wires=True): - """Draw the module""" - return draw(self, idle_wires=idle_wires) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index acdd94ec..2e615bc3 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -8,15 +8,17 @@ # # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. +# pylint: disable=import-outside-toplevel + """ Module with analysis functions for QASM visitor """ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal -from matplotlib import pyplot as plt from openqasm3 import ast from pyqasm.expressions import Qasm3ExprEvaluator @@ -27,13 +29,11 @@ TWO_QUBIT_OP_MAP, ) -try: - MPL_INSTALLED = True -except ImportError: - MPL_INSTALLED = False - if TYPE_CHECKING: - from pyqasm.modules.base import Qasm3Module + import matplotlib.pyplot as plt + + from pyqasm.modules.base import QasmModule + # Constants DEFAULT_GATE_COLOR = "#d4b6e8" @@ -46,46 +46,81 @@ LINE_SPACING = 0.6 TEXT_MARGIN = 0.6 FRAME_PADDING = 0.2 +BOX_STYLE = "round,pad=0.02,rounding_size=0.05" + +Declaration = ( + ast.CalibrationGrammarDeclaration + | ast.ClassicalDeclaration + | ast.ConstantDeclaration + | ast.ExternDeclaration + | ast.IODeclaration + | ast.QubitDeclaration +) +QuantumStatement = ( + ast.QuantumGate | ast.QuantumMeasurementStatement | ast.QuantumBarrier | ast.QuantumReset +) + +QubitIdentifier = ast.Identifier | ast.IndexedIdentifier -def draw(module: Qasm3Module, output: str = "mpl", idle_wires: bool = True) -> plt.Figure: +def draw( + program: str | QasmModule, + output: Literal["mpl"] = "mpl", + idle_wires: bool = True, + **kwargs: Any, +) -> None: """Draw the quantum circuit. Args: - module: The quantum module to draw - output: The output format, currently only "mpl" supported - idle_wires: Whether to show idle wires + module (QasmModule): The quantum module to draw + output (str): The output format. Defaults to "mpl". + idle_wires (bool): Whether to show idle wires. Defaults to True. Returns: - The matplotlib figure + None: The drawing is displayed or saved to a file. """ - if not MPL_INSTALLED: - raise ImportError( - "matplotlib needs to be installed prior to running pyqasm.draw(). " - "Install with: 'pip install pyqasm[visualization]'" - ) + if isinstance(program, str): + from pyqasm.entrypoint import loads - if output != "mpl": - raise NotImplementedError(f"{output} drawing for Qasm3Module is unsupported") + program = loads(program) - plt.ioff() - plt.close("all") - return _draw_mpl(module, idle_wires=idle_wires) + if output == "mpl": + mpl_draw(program, idle_wires=idle_wires, **kwargs) + else: + raise ValueError(f"Unsupported output format: {output}") -def _draw_mpl(module: Qasm3Module, idle_wires: bool = True) -> plt.Figure: +def mpl_draw( + program: str | QasmModule, idle_wires: bool = True, filename: str | Path | None = None +) -> plt.Figure: """Internal matplotlib drawing implementation.""" - module.unroll() - module.remove_includes() + if isinstance(program, str): + from pyqasm.entrypoint import loads - line_nums, sizes = _compute_line_nums(module) + program = loads(program) - global_phase = sum( - Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] - for s in module._statements - if isinstance(s, ast.QuantumPhase) - ) - statements = [s for s in module._statements if not isinstance(s, ast.QuantumPhase)] + try: + # pylint: disable-next=unused-import + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "matplotlib needs to be installed prior to running pyqasm.mpl_draw(). " + "You can install matplotlib with:\n'pip install matplotlib'" + ) from e + + program.unroll() + program.remove_includes() + + line_nums, sizes = _compute_line_nums(program) + + global_phase = 0 + statements: list[ast.Statement] = [] + + for s in program._statements: + if isinstance(s, ast.QuantumPhase): + global_phase += Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] + else: + statements.append(s) # Compute moments moments, depths = _compute_moments(statements, line_nums) @@ -96,13 +131,17 @@ def _draw_mpl(module: Qasm3Module, idle_wires: bool = True) -> plt.Figure: ks = [k for k in ks if depths[k] > 0] line_nums = {k: i for i, k in enumerate(ks)} - fig = _mpl_draw(module, moments, line_nums, sizes, global_phase) + fig = _mpl_draw(program, moments, line_nums, sizes, global_phase) + + if filename is not None: + raise NotImplementedError("Saving to file not yet supported.") + return fig def _compute_line_nums( - module: Qasm3Module, -) -> tuple[dict[tuple[str, int], int], dict[tuple[str, int], int], int]: + module: QasmModule, +) -> tuple[dict[tuple[str, int], int], dict[tuple[str, int], int]]: """Compute line number and register size lookup table for the circuit.""" line_nums = {} sizes = {} @@ -129,17 +168,20 @@ def _compute_line_nums( return line_nums, sizes +# pylint: disable-next=too-many-branches def _compute_moments( - statements: list[ast.QuantumStatement], line_nums: dict[tuple[str, int], int] -) -> tuple[list[list[ast.QuantumStatement]], dict[tuple[str, int], int]]: + statements: list[ast.Statement], line_nums: dict[tuple[str, int], int] +) -> tuple[list[list[QuantumStatement]], dict[tuple[str, int], int]]: depths = {} for k in line_nums: depths[k] = -1 - moments = [] + moments: list[list[QuantumStatement]] = [] for statement in statements: - if "Declaration" in str(type(statement)): + if isinstance(statement, Declaration): continue + if not isinstance(statement, QuantumStatement): + raise ValueError(f"Unsupported statement: {statement}") if isinstance(statement, ast.QuantumGate): qubits = [_identifier_to_key(q) for q in statement.qubits] depth = 1 + max(depths[q] for q in qubits) @@ -147,12 +189,23 @@ def _compute_moments( depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) + if statement.target is None: + raise NotImplementedError("Stand-alone measurement statements not yet supported.") target_key = _identifier_to_key(statement.target)[0], -1 depth = 1 + max(depths[qubit_key], depths[target_key]) for k in [qubit_key, target_key]: depths[k] = depth elif isinstance(statement, ast.QuantumBarrier): - qubits = [_identifier_to_key(q) for q in statement.qubits] + qubits = [] + for expr in statement.qubits: + # https://github.com/openqasm/openqasm/issues/461 + if not isinstance(expr, QubitIdentifier): + raise ValueError( + f"Unsupported qubit type '{type(expr).__name__}' in " + f"'{type(statement).__name__}' statement. " + f"Expected a qubit of type {QubitIdentifier}." + ) + qubits.append(_identifier_to_key(expr)) depth = 1 + max(depths[q] for q in qubits) for q in qubits: depths[q] = depth @@ -160,11 +213,10 @@ def _compute_moments( qubit_key = _identifier_to_key(statement.qubits) depth = 1 + depths[qubit_key] depths[qubit_key] = depth - else: - raise NotImplementedError(f"Unsupported statement: {statement}") if depth >= len(moments): moments.append([]) + moments[depth].append(statement) return moments, depths @@ -174,16 +226,20 @@ def _identifier_to_key(identifier: ast.Identifier | ast.IndexedIdentifier) -> tu if isinstance(identifier, ast.Identifier): return identifier.name, -1 - return ( - identifier.name.name, - Qasm3ExprEvaluator.evaluate_expression(identifier.indices[0][0])[0], - ) + indices = identifier.indices + if len(indices) >= 1 and isinstance(indices[0], list) and len(indices[0]) >= 1: + return ( + identifier.name.name, + Qasm3ExprEvaluator.evaluate_expression(indices[0][0])[0], + ) + + raise ValueError(f"Unsupported identifier: {identifier}") def _compute_sections( - moments: list[list[ast.QuantumStatement]], -) -> list[list[ast.QuantumStatement]]: - sections = [[]] + moments: list[list[QuantumStatement]], +) -> tuple[list[list[list[QuantumStatement]]], float]: + sections: list[list[list[QuantumStatement]]] = [[]] width = TEXT_MARGIN for moment in moments: @@ -203,15 +259,14 @@ def _compute_sections( def _mpl_draw( - module: Qasm3Module, - moments: list[list[ast.QuantumStatement]], + module: QasmModule, + moments: list[list[QuantumStatement]], line_nums: dict[tuple[str, int], int], sizes: dict[tuple[str, int], int], global_phase: float, ): sections, width = _compute_sections(moments) n_lines = max(line_nums.values()) + 1 - fig, axs = _mpl_setup_figure(sections, width, n_lines) for sidx, ms in enumerate(sections): @@ -221,15 +276,20 @@ def _mpl_draw( return fig -def _mpl_setup_figure(sections: list[list[ast.QuantumStatement]], width: float, n_lines: int): - fig, axs = plt.subplots( +def _mpl_setup_figure( + sections: list[list[list[QuantumStatement]]], width: float, n_lines: int +) -> tuple[plt.Figure, list[plt.Axes]]: + import matplotlib.pyplot as plt + + fig_ax_tuple: tuple[plt.Figure, list[plt.Axes] | plt.Axes] = plt.subplots( len(sections), 1, sharex=True, figsize=(width, len(sections) * (n_lines * GATE_BOX_HEIGHT + LINE_SPACING * (n_lines - 1))), ) - if len(sections) == 1: - axs = [axs] + + fig, axs = fig_ax_tuple + axs = axs if isinstance(axs, list) else [axs] for ax in axs: ax.set_ylim( @@ -245,16 +305,16 @@ def _mpl_setup_figure(sections: list[list[ast.QuantumStatement]], width: float, return fig, axs -# pylint: disable=too-many-arguments +# pylint: disable-next=too-many-arguments def _mpl_draw_section( - module: Qasm3Module, - moments: list[list[ast.QuantumStatement]], + module: QasmModule, + moments: list[list[QuantumStatement]], line_nums: dict[tuple[str, int], int], sizes: dict[tuple[str, int], int], ax: plt.Axes, global_phase: float, ): - x = 0 + x = 0.0 if global_phase != 0: _mpl_draw_global_phase(global_phase, ax, x) for k in module._qubit_registers.keys(): @@ -296,7 +356,7 @@ def _mpl_draw_creg_label(creg: str, line_num: int, ax: plt.Axes, x: float): ax.text(x, _mpl_line_to_y(line_num), f"{creg[0]}", ha="right", va="center") -# pylint: disable=too-many-arguments +# pylint: disable-next=too-many-arguments def _mpl_draw_lines( width, line_nums: dict[tuple[str, int], int], @@ -344,16 +404,16 @@ def _mpl_draw_lines( ) -def _mpl_get_moment_width(moment: list[ast.QuantumStatement]) -> float: +def _mpl_get_moment_width(moment: list[QuantumStatement]) -> float: return max(_mpl_get_statement_width(s) for s in moment) -def _mpl_get_statement_width(_: ast.QuantumStatement) -> float: +def _mpl_get_statement_width(_: QuantumStatement) -> float: return GATE_BOX_WIDTH + GATE_SPACING def _mpl_draw_statement( - statement: ast.QuantumStatement, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float + statement: QuantumStatement, line_nums: dict[tuple[str, int], int], ax: plt.Axes, x: float ): if isinstance(statement, ast.QuantumGate): args = [Qasm3ExprEvaluator.evaluate_expression(arg)[0] for arg in statement.arguments] @@ -361,12 +421,22 @@ def _mpl_draw_statement( _mpl_draw_gate(statement, args, lines, ax, x) elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) - target_key = _identifier_to_key(statement.target) - _mpl_draw_measurement( - line_nums[qubit_key], line_nums[(target_key[0], -1)], target_key[1], ax, x - ) + if statement.target is None: + raise NotImplementedError("Stand-alone measurement statements not yet supported.") + name, idx = _identifier_to_key(statement.target) + _mpl_draw_measurement(line_nums[qubit_key], line_nums[(name, -1)], idx, ax, x) elif isinstance(statement, ast.QuantumBarrier): - lines = [line_nums[_identifier_to_key(q)] for q in statement.qubits] + lines = [] + for q in statement.qubits: + # https://github.com/openqasm/openqasm/issues/461 + if not isinstance(q, QubitIdentifier): + raise ValueError( + f"Unsupported qubit type '{type(q).__name__}' in " + f"'{type(statement).__name__}' statement. " + f"Expected a qubit of type {QubitIdentifier}." + ) + lines.append(line_nums[_identifier_to_key(q)]) + _mpl_draw_barrier(lines, ax, x) elif isinstance(statement, ast.QuantumReset): _mpl_draw_reset(line_nums[_identifier_to_key(statement.qubits)], ax, x) @@ -401,18 +471,22 @@ def _mpl_draw_gate( def _draw_mpl_one_qubit_gate( gate: ast.QuantumGate, args: list[Any], line: int, ax: plt.Axes, x: float ): + from matplotlib.patches import FancyBboxPatch + color = DEFAULT_GATE_COLOR if gate.name.name == "h": color = HADAMARD_GATE_COLOR text = gate.name.name.upper() y = _mpl_line_to_y(line) - rect = plt.Rectangle( + + rect = FancyBboxPatch( (x - GATE_BOX_WIDTH / 2, y - GATE_BOX_HEIGHT / 2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor=color, edgecolor="none", + boxstyle=BOX_STYLE, ) ax.add_patch(rect) @@ -440,17 +514,20 @@ def _draw_mpl_swap(line1: int, line2: int, ax: plt.Axes, x: float): def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes, x: float): + from matplotlib.patches import FancyBboxPatch + y1 = _mpl_line_to_y(qbit_line) y2 = _mpl_line_to_y(cbit_line) color = "#A0A0A0" gap = GATE_BOX_WIDTH / 3 - rect = plt.Rectangle( + rect = FancyBboxPatch( (x - GATE_BOX_WIDTH / 2, y1 - GATE_BOX_HEIGHT / 2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor=color, edgecolor="none", + boxstyle=BOX_STYLE, ) ax.add_patch(rect) ax.text(x, y1, "M", ha="center", va="center") @@ -475,6 +552,8 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): + import matplotlib.pyplot as plt + for line in lines: y = _mpl_line_to_y(line) ax.vlines( @@ -497,6 +576,8 @@ def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): def _mpl_draw_reset(line: int, ax: plt.Axes, x: float): + import matplotlib.pyplot as plt + y = _mpl_line_to_y(line) rect = plt.Rectangle( (x - GATE_BOX_WIDTH / 2, y - GATE_BOX_HEIGHT / 2), diff --git a/tests/qasm3/test_printer.py b/tests/test_mpl_draw.py similarity index 59% rename from tests/qasm3/test_printer.py rename to tests/test_mpl_draw.py index 4b934a34..01b7584e 100644 --- a/tests/qasm3/test_printer.py +++ b/tests/test_mpl_draw.py @@ -12,12 +12,12 @@ Tests for the QASM printer module. """ -import random - import pytest -from qbraid import random_circuit, transpile from pyqasm.entrypoint import loads +from pyqasm.printer import mpl_draw + +pytest.importorskip("matplotlib", reason="Matplotlib not installed.") def _check_fig(_, fig): @@ -30,11 +30,15 @@ def _check_fig(_, fig): assert len(ax.texts) > 0 -def test_simple(): - qasm = """OPENQASM 3.0; +def test_draw_qasm3_simple(): + """Test drawing a simple QASM 3.0 circuit.""" + qasm = """ + OPENQASM 3.0; include "stdgates.inc"; + qubit[3] q; bit[3] b; + h q[0]; z q[1]; rz(pi/1.1) q[0]; @@ -44,14 +48,17 @@ def test_simple(): b = measure q; """ circ = loads(qasm) - fig = circ.draw() + fig = mpl_draw(circ) _check_fig(circ, fig) -def test_custom_gate(): - qasm = """OPENQASM 3.0; +def test_draw_qasm3_custom_gate(): + qasm = """ + OPENQASM 3.0; include "stdgates.inc"; + qubit q; + gate custom a { h a; z a; @@ -59,14 +66,29 @@ def test_custom_gate(): custom q; """ circ = loads(qasm) - fig = circ.draw() + fig = mpl_draw(circ) _check_fig(circ, fig) -@pytest.mark.parametrize("_", range(100)) -def test_random(_): - circ = random_circuit("qiskit", measure=random.choice([True, False])) - qasm_str = transpile(circ, random.choice(["qasm2", "qasm3"])) - module = loads(qasm_str) - fig = module.draw() +def test_draw_qasm2_simple(): + """Test drawing a simple QASM 2.0 circuit.""" + qasm = """ + OPENQASM 2.0; + include "qelib1.inc"; + + qreg q[4]; + creg c[4]; + + h q[0]; + cx q[0], q[1]; + x q[2]; + rz(pi/4) q[3]; + cx q[1], q[2]; + ccx q[0], q[1], q[3]; + u3(pi/2, pi/4, pi/8) q[2]; + barrier q; + measure q -> c; + """ + circ = loads(qasm) + fig = mpl_draw(circ) _check_fig(circ, fig) diff --git a/tox.ini b/tox.ini index be4f5d53..821a602e 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = skip_missing_interpreter = true [testenv] -commands_pre = python -m pip install . +commands_pre = python -m pip install '.[visualization]' basepython = python3 [testenv:unit-tests] From c2cb5dee1b162f6128a5f6bde68a38210b652077 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 18 Feb 2025 17:15:58 -0500 Subject: [PATCH 22/29] standalone measurement --- src/pyqasm/printer.py | 60 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 2e615bc3..a98906b4 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -102,6 +102,8 @@ def mpl_draw( try: # pylint: disable-next=unused-import import matplotlib.pyplot as plt + + plt.ioff() except ImportError as e: raise ImportError( "matplotlib needs to be installed prior to running pyqasm.mpl_draw(). " @@ -134,7 +136,7 @@ def mpl_draw( fig = _mpl_draw(program, moments, line_nums, sizes, global_phase) if filename is not None: - raise NotImplementedError("Saving to file not yet supported.") + plt.savefig(filename) return fig @@ -188,12 +190,13 @@ def _compute_moments( for q in qubits: depths[q] = depth elif isinstance(statement, ast.QuantumMeasurementStatement): - qubit_key = _identifier_to_key(statement.measure.qubit) - if statement.target is None: - raise NotImplementedError("Stand-alone measurement statements not yet supported.") - target_key = _identifier_to_key(statement.target)[0], -1 - depth = 1 + max(depths[qubit_key], depths[target_key]) - for k in [qubit_key, target_key]: + keys = [_identifier_to_key(statement.measure.qubit)] + if statement.target: + target_key = _identifier_to_key(statement.target)[0], -1 + keys.append(target_key) + print(keys) + depth = 1 + max(depths[k] for k in keys) + for k in keys: depths[k] = depth elif isinstance(statement, ast.QuantumBarrier): qubits = [] @@ -422,7 +425,8 @@ def _mpl_draw_statement( elif isinstance(statement, ast.QuantumMeasurementStatement): qubit_key = _identifier_to_key(statement.measure.qubit) if statement.target is None: - raise NotImplementedError("Stand-alone measurement statements not yet supported.") + _mpl_draw_measurement(line_nums[qubit_key], -1, -1, ax, x) + return name, idx = _identifier_to_key(statement.target) _mpl_draw_measurement(line_nums[qubit_key], line_nums[(name, -1)], idx, ax, x) elif isinstance(statement, ast.QuantumBarrier): @@ -517,7 +521,6 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes from matplotlib.patches import FancyBboxPatch y1 = _mpl_line_to_y(qbit_line) - y2 = _mpl_line_to_y(cbit_line) color = "#A0A0A0" gap = GATE_BOX_WIDTH / 3 @@ -531,24 +534,27 @@ def _mpl_draw_measurement(qbit_line: int, cbit_line: int, idx: int, ax: plt.Axes ) ax.add_patch(rect) ax.text(x, y1, "M", ha="center", va="center") - ax.vlines( - x=x - gap / 10, - ymin=min(y1, y2) + gap, - ymax=max(y1, y2), - color=color, - linestyle="-", - zorder=-1, - ) - ax.vlines( - x=x + gap / 10, - ymin=min(y1, y2) + gap, - ymax=max(y1, y2), - color=color, - linestyle="-", - zorder=-1, - ) - ax.plot(x, y2 + gap, "v", markersize=12, color=color) - ax.text(x + gap, y2 + gap, str(idx), color=color, ha="left", va="bottom", fontsize=8) + + if cbit_line >= 0 and idx >= 0: + y2 = _mpl_line_to_y(cbit_line) + ax.vlines( + x=x - gap / 10, + ymin=min(y1, y2) + gap, + ymax=max(y1, y2), + color=color, + linestyle="-", + zorder=-1, + ) + ax.vlines( + x=x + gap / 10, + ymin=min(y1, y2) + gap, + ymax=max(y1, y2), + color=color, + linestyle="-", + zorder=-1, + ) + ax.plot(x, y2 + gap, "v", markersize=12, color=color) + ax.text(x + gap, y2 + gap, str(idx), color=color, ha="left", va="bottom", fontsize=8) def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): From 0528696e67fb8b1e385a4d640c53349ee1bb1bee Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Thu, 20 Feb 2025 16:59:42 -0600 Subject: [PATCH 23/29] add tests --- src/pyqasm/printer.py | 12 ++-- tests/visualization/images/bell.png | Bin 0 -> 19923 bytes tests/visualization/images/misc.png | Bin 0 -> 51484 bytes tests/{ => visualization}/test_mpl_draw.py | 63 +++++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 tests/visualization/images/bell.png create mode 100644 tests/visualization/images/misc.png rename tests/{ => visualization}/test_mpl_draw.py (53%) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index a98906b4..7207cf3a 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -116,7 +116,7 @@ def mpl_draw( line_nums, sizes = _compute_line_nums(program) global_phase = 0 - statements: list[ast.Statement] = [] + statements: list[ast.Statement | ast.Pragma] = [] for s in program._statements: if isinstance(s, ast.QuantumPhase): @@ -136,7 +136,7 @@ def mpl_draw( fig = _mpl_draw(program, moments, line_nums, sizes, global_phase) if filename is not None: - plt.savefig(filename) + plt.savefig(filename, bbox_inches="tight", dpi=300) return fig @@ -172,7 +172,7 @@ def _compute_line_nums( # pylint: disable-next=too-many-branches def _compute_moments( - statements: list[ast.Statement], line_nums: dict[tuple[str, int], int] + statements: list[ast.Statement | ast.Pragma], line_nums: dict[tuple[str, int], int] ) -> tuple[list[list[QuantumStatement]], dict[tuple[str, int], int]]: depths = {} for k in line_nums: @@ -194,7 +194,6 @@ def _compute_moments( if statement.target: target_key = _identifier_to_key(statement.target)[0], -1 keys.append(target_key) - print(keys) depth = 1 + max(depths[k] for k in keys) for k in keys: depths[k] = depth @@ -582,15 +581,16 @@ def _mpl_draw_barrier(lines: list[int], ax: plt.Axes, x: float): def _mpl_draw_reset(line: int, ax: plt.Axes, x: float): - import matplotlib.pyplot as plt + from matplotlib.patches import FancyBboxPatch y = _mpl_line_to_y(line) - rect = plt.Rectangle( + rect = FancyBboxPatch( (x - GATE_BOX_WIDTH / 2, y - GATE_BOX_HEIGHT / 2), GATE_BOX_WIDTH, GATE_BOX_HEIGHT, facecolor="lightgray", edgecolor="none", + boxstyle=BOX_STYLE, ) ax.add_patch(rect) ax.text(x, y, "∣0⟩", ha="center", va="center", fontsize=12) diff --git a/tests/visualization/images/bell.png b/tests/visualization/images/bell.png new file mode 100644 index 0000000000000000000000000000000000000000..d862faef2557ad6a15139cc1b9ad97c3fcd0442d GIT binary patch literal 19923 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 literal 0 HcmV?d00001 diff --git a/tests/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py similarity index 53% rename from tests/test_mpl_draw.py rename to tests/visualization/test_mpl_draw.py index 01b7584e..102df6f9 100644 --- a/tests/test_mpl_draw.py +++ b/tests/visualization/test_mpl_draw.py @@ -12,8 +12,12 @@ Tests for the QASM printer module. """ +from pathlib import Path + import pytest +from matplotlib.testing.compare import compare_images +from pyqasm import draw from pyqasm.entrypoint import loads from pyqasm.printer import mpl_draw @@ -92,3 +96,62 @@ def test_draw_qasm2_simple(): circ = loads(qasm) fig = mpl_draw(circ) _check_fig(circ, fig) + + +def test_draw_bell(): + """Test drawing a simple Bell state circuit.""" + qasm3 = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] b; + h q; + cnot q[0], q[1]; + b = measure q; + """ + images_dir = Path(__file__).parent / "images" + expected_img = images_dir / "bell.png" + test_img = images_dir / "bell-test.png" + diff_img = images_dir / "bell-failed-diff.png" + + try: + draw(qasm3, output="mpl", filename=test_img) + + assert compare_images(str(test_img), str(expected_img), tol=0.001) is None + finally: + for img in [test_img, diff_img]: + if img.exists(): + img.unlink() + + +def test_draw_misc_ops(): + """Test drawing a circuit with various operations.""" + qasm3 = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[3] q; + h q; + ccnot q[0], q[1], q[2]; + rz(2*pi) q[0]; + ry(pi/4) q[1]; + rx(pi) q[2]; + swap q[0], q[2]; + swap q[1], q[2]; + id q[0]; + barrier q; + measure q; + reset q; + """ + images_dir = Path(__file__).parent / "images" + expected_img = images_dir / "misc.png" + test_img = images_dir / "misc-test.png" + diff_img = images_dir / "misc-failed-diff.png" + + try: + draw(qasm3, output="mpl", filename=test_img) + + assert compare_images(str(test_img), str(expected_img), tol=0.001) is None + finally: + for img in [test_img, diff_img]: + if img.exists(): + img.unlink() From 02ca6cb4ed3530e6ba6bf7dfe9f2c1aa74f53f01 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Thu, 20 Feb 2025 17:02:33 -0600 Subject: [PATCH 24/29] add mpl to test extra --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa79121b..b87a6c08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.optional-dependencies] cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] -test = ["pytest", "pytest-cov"] +test = ["pytest", "pytest-cov", "matplotlib"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.8.5"] docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<3.1", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] visualization = ["matplotlib"] From 705cc48aecdd7ebf2f2f9ebff5c09dfc2b943145 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Thu, 20 Feb 2025 17:10:00 -0600 Subject: [PATCH 25/29] try pytest-mpl --- pyproject.toml | 2 +- tests/visualization/test_mpl_draw.py | 97 ++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b87a6c08..db9247a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.optional-dependencies] cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] -test = ["pytest", "pytest-cov", "matplotlib"] +test = ["pytest", "pytest-cov", "matplotlib", "pytest-mpl"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.8.5"] docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<3.1", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] visualization = ["matplotlib"] diff --git a/tests/visualization/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py index 102df6f9..973b93d1 100644 --- a/tests/visualization/test_mpl_draw.py +++ b/tests/visualization/test_mpl_draw.py @@ -12,15 +12,17 @@ Tests for the QASM printer module. """ -from pathlib import Path +# from pathlib import Path import pytest -from matplotlib.testing.compare import compare_images from pyqasm import draw from pyqasm.entrypoint import loads from pyqasm.printer import mpl_draw +# from matplotlib.testing.compare import compare_images + + pytest.importorskip("matplotlib", reason="Matplotlib not installed.") @@ -98,6 +100,66 @@ def test_draw_qasm2_simple(): _check_fig(circ, fig) +# def test_draw_bell(): +# """Test drawing a simple Bell state circuit.""" +# qasm3 = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] b; +# h q; +# cnot q[0], q[1]; +# b = measure q; +# """ +# images_dir = Path(__file__).parent / "images" +# expected_img = images_dir / "bell.png" +# test_img = images_dir / "bell-test.png" +# diff_img = images_dir / "bell-failed-diff.png" + +# try: +# draw(qasm3, output="mpl", filename=test_img) + +# assert compare_images(str(test_img), str(expected_img), tol=0.001) is None +# finally: +# for img in [test_img, diff_img]: +# if img.exists(): +# img.unlink() + + +# def test_draw_misc_ops(): +# """Test drawing a circuit with various operations.""" +# qasm3 = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[3] q; +# h q; +# ccnot q[0], q[1], q[2]; +# rz(2*pi) q[0]; +# ry(pi/4) q[1]; +# rx(pi) q[2]; +# swap q[0], q[2]; +# swap q[1], q[2]; +# id q[0]; +# barrier q; +# measure q; +# reset q; +# """ +# images_dir = Path(__file__).parent / "images" +# expected_img = images_dir / "misc.png" +# test_img = images_dir / "misc-test.png" +# diff_img = images_dir / "misc-failed-diff.png" + +# try: +# draw(qasm3, output="mpl", filename=test_img) + +# assert compare_images(str(test_img), str(expected_img), tol=0.001) is None +# finally: +# for img in [test_img, diff_img]: +# if img.exists(): +# img.unlink() + + +@pytest.mark.mpl_image_compare(baseline_dir="images", filename="bell.png") def test_draw_bell(): """Test drawing a simple Bell state circuit.""" qasm3 = """ @@ -109,21 +171,11 @@ def test_draw_bell(): cnot q[0], q[1]; b = measure q; """ - images_dir = Path(__file__).parent / "images" - expected_img = images_dir / "bell.png" - test_img = images_dir / "bell-test.png" - diff_img = images_dir / "bell-failed-diff.png" - - try: - draw(qasm3, output="mpl", filename=test_img) - - assert compare_images(str(test_img), str(expected_img), tol=0.001) is None - finally: - for img in [test_img, diff_img]: - if img.exists(): - img.unlink() + fig = draw(qasm3, output="mpl") + return fig +@pytest.mark.mpl_image_compare(baseline_dir="images", filename="misc.png") def test_draw_misc_ops(): """Test drawing a circuit with various operations.""" qasm3 = """ @@ -142,16 +194,5 @@ def test_draw_misc_ops(): measure q; reset q; """ - images_dir = Path(__file__).parent / "images" - expected_img = images_dir / "misc.png" - test_img = images_dir / "misc-test.png" - diff_img = images_dir / "misc-failed-diff.png" - - try: - draw(qasm3, output="mpl", filename=test_img) - - assert compare_images(str(test_img), str(expected_img), tol=0.001) is None - finally: - for img in [test_img, diff_img]: - if img.exists(): - img.unlink() + fig = draw(qasm3, output="mpl") + return fig From d5ae2527ebd386afbbba092d4f809735eaf7ba47 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Thu, 20 Feb 2025 17:14:56 -0600 Subject: [PATCH 26/29] clean up tests --- pyproject.toml | 2 +- tests/visualization/test_mpl_draw.py | 64 ---------------------------- 2 files changed, 1 insertion(+), 65 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index db9247a0..2a3dd46d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.optional-dependencies] cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] -test = ["pytest", "pytest-cov", "matplotlib", "pytest-mpl"] +test = ["pytest", "pytest-cov", "pytest-mpl", "matplotlib"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.8.5"] docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<3.1", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] visualization = ["matplotlib"] diff --git a/tests/visualization/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py index 973b93d1..bd644f5a 100644 --- a/tests/visualization/test_mpl_draw.py +++ b/tests/visualization/test_mpl_draw.py @@ -12,17 +12,12 @@ Tests for the QASM printer module. """ -# from pathlib import Path - import pytest from pyqasm import draw from pyqasm.entrypoint import loads from pyqasm.printer import mpl_draw -# from matplotlib.testing.compare import compare_images - - pytest.importorskip("matplotlib", reason="Matplotlib not installed.") @@ -100,65 +95,6 @@ def test_draw_qasm2_simple(): _check_fig(circ, fig) -# def test_draw_bell(): -# """Test drawing a simple Bell state circuit.""" -# qasm3 = """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] b; -# h q; -# cnot q[0], q[1]; -# b = measure q; -# """ -# images_dir = Path(__file__).parent / "images" -# expected_img = images_dir / "bell.png" -# test_img = images_dir / "bell-test.png" -# diff_img = images_dir / "bell-failed-diff.png" - -# try: -# draw(qasm3, output="mpl", filename=test_img) - -# assert compare_images(str(test_img), str(expected_img), tol=0.001) is None -# finally: -# for img in [test_img, diff_img]: -# if img.exists(): -# img.unlink() - - -# def test_draw_misc_ops(): -# """Test drawing a circuit with various operations.""" -# qasm3 = """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[3] q; -# h q; -# ccnot q[0], q[1], q[2]; -# rz(2*pi) q[0]; -# ry(pi/4) q[1]; -# rx(pi) q[2]; -# swap q[0], q[2]; -# swap q[1], q[2]; -# id q[0]; -# barrier q; -# measure q; -# reset q; -# """ -# images_dir = Path(__file__).parent / "images" -# expected_img = images_dir / "misc.png" -# test_img = images_dir / "misc-test.png" -# diff_img = images_dir / "misc-failed-diff.png" - -# try: -# draw(qasm3, output="mpl", filename=test_img) - -# assert compare_images(str(test_img), str(expected_img), tol=0.001) is None -# finally: -# for img in [test_img, diff_img]: -# if img.exists(): -# img.unlink() - - @pytest.mark.mpl_image_compare(baseline_dir="images", filename="bell.png") def test_draw_bell(): """Test drawing a simple Bell state circuit.""" From 61516c003076ed84a1cb8fd0d4397df685eaad00 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Mon, 17 Mar 2025 14:05:01 -0500 Subject: [PATCH 27/29] update headers --- src/pyqasm/printer.py | 16 ++++++++++------ tests/visualization/test_mpl_draw.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 7207cf3a..5f908f1e 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -1,12 +1,16 @@ -# Copyright (C) 2025 qBraid +# Copyright 2025 qBraid # -# This file is part of PyQASM +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# PyQASM is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . +# http://www.apache.org/licenses/LICENSE-2.0 # -# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # pylint: disable=import-outside-toplevel diff --git a/tests/visualization/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py index bd644f5a..543a3e11 100644 --- a/tests/visualization/test_mpl_draw.py +++ b/tests/visualization/test_mpl_draw.py @@ -1,12 +1,16 @@ -# Copyright (C) 2025 qBraid +# Copyright 2025 qBraid # -# This file is part of PyQASM +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# PyQASM is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . +# http://www.apache.org/licenses/LICENSE-2.0 # -# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Tests for the QASM printer module. From 1efb3e8e599453e42aee1968d6b985d10c0825ea Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Mon, 17 Mar 2025 14:08:04 -0500 Subject: [PATCH 28/29] update docstring and pyproject format --- pyproject.toml | 1 - src/pyqasm/printer.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19147e9a..37369811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", ] - dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.urls] diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index 5f908f1e..4560e59b 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -15,7 +15,7 @@ # pylint: disable=import-outside-toplevel """ -Module with analysis functions for QASM visitor +Functions for drawing quantum circuits. """ from __future__ import annotations From d44430063b8133452cc825dfbc987b9c6724f6d6 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Mon, 17 Mar 2025 14:09:14 -0500 Subject: [PATCH 29/29] rm extra space added in maps --- src/pyqasm/maps/gates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index 84227e59..b07a0b12 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - # pylint: disable=too-many-lines """