From f46c1a6dea86ae46dabac7247bcd4edf76e2b1c1 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 16 Jul 2025 00:07:10 -0700 Subject: [PATCH 1/8] issue 216 added --- src/pyqasm/modules/base.py | 69 +++++++++++++++++++++++++++++ tests/visualization/test_compare.py | 64 ++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tests/visualization/test_compare.py diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 0258b5d8..c7ed8b13 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -17,11 +17,13 @@ """ from abc import ABC, abstractmethod +from collections import Counter from copy import deepcopy from typing import Optional import openqasm3.ast as qasm3_ast from openqasm3.ast import BranchingStatement, Program, QuantumGate +from tabulate import tabulate from pyqasm.analyzer import Qasm3Analyzer from pyqasm.decomposer import Decomposer @@ -59,12 +61,18 @@ def __init__(self, name: str, program: Program): self._decompose_native_gates: Optional[bool] = None self._device_qubits: Optional[int] = None self._consolidate_qubits: Optional[bool] = False + self._user_operations: list[str] = ["load"] @property def name(self) -> str: """Returns the name of the module.""" return self._name + @property + def history(self) -> list[str]: + """Returns the user operations performed on the module.""" + return self._user_operations + @property def num_qubits(self) -> int: """Returns the number of qubits in the circuit.""" @@ -156,6 +164,7 @@ def remove_measurements(self, in_place: bool = True) -> Optional["QasmModule"]: Returns: QasmModule: The module with the measurements removed if in_place is False """ + self._user_operations.append("remove_measurements") stmt_list = ( self._statements if len(self._unrolled_ast.statements) == 0 @@ -233,6 +242,7 @@ def remove_barriers(self, in_place: bool = True) -> Optional["QasmModule"]: curr_module._has_barriers = False curr_module._statements = stmts_without_barriers curr_module._unrolled_ast.statements = stmts_without_barriers + curr_module._user_operations.append("remove_barriers") return curr_module @@ -259,6 +269,7 @@ def remove_includes(self, in_place=True) -> Optional["QasmModule"]: curr_module._statements = stmts_without_includes curr_module._unrolled_ast.statements = stmts_without_includes + curr_module._user_operations.append("remove_includes") return curr_module @@ -275,6 +286,7 @@ def depth(self, decompose_native_gates=True): """ # 1. Since the program will be unrolled before its execution on a QC, it makes sense to # calculate the depth of the unrolled program. + self._user_operations.append(f"depth(decompose_native_gates={decompose_native_gates})") # We are performing operations in place, thus we need to calculate depth # at "each instance of the function call". @@ -450,6 +462,7 @@ def remove_idle_qubits(self, in_place: bool = True): # the original ast will need to be updated to the unrolled ast as if we call the # unroll operation again, it will incorrectly choose the original ast WITH THE IDLE QUBITS qasm_module._statements = qasm_module._unrolled_ast.statements + qasm_module._user_operations.append("remove_idle_qubits") return qasm_module @@ -468,6 +481,7 @@ def reverse_qubit_order(self, in_place=True): qasm_module = self if in_place else self.copy() qasm_module.unroll() + qasm_module._user_operations.append("reverse_qubit_order") new_qubit_mappings = {} for register, size in self._qubit_registers.items(): @@ -517,6 +531,7 @@ def validate(self): """Validate the module""" if self._validated_program is True: return + self._user_operations.append("validate") try: self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) @@ -558,6 +573,7 @@ def unroll(self, **kwargs): if not kwargs: kwargs = {} + self._user_operations.append(f"unroll({kwargs})") try: self.num_qubits, self.num_clbits = 0, 0 if ext_gates := kwargs.get("external_gates"): @@ -588,6 +604,7 @@ def rebase(self, target_basis_set, in_place=True): """ if target_basis_set not in DECOMPOSITION_RULES: raise ValueError(f"Target basis set '{target_basis_set}' is not defined.") + self._user_operations.append(f"rebase({target_basis_set})") qasm_module = self if in_place else self.copy() @@ -623,6 +640,58 @@ def rebase(self, target_basis_set, in_place=True): return qasm_module + def get_gate_counts(self) -> dict[str, int]: + """Return a dictionary of gate counts in the unrolled program. + + Returns: + dict[str, int]: A dictionary of gate counts. + """ + if not self._unrolled_ast.statements: + self.unroll() + + gate_nodes = [ + s for s in self._unrolled_ast.statements if isinstance(s, qasm3_ast.QuantumGate) + ] + return dict(Counter(gate.name.name for gate in gate_nodes)) + + def compare(self, other_module: "QasmModule"): + """Compare two QasmModule objects across multiple attributes. + + Args: + other_module (QasmModule): The module to compare with. + """ + self_counts = self.get_gate_counts() + other_counts = other_module.get_gate_counts() + all_gates = sorted(list(set(self_counts) | set(other_counts))) + + # Format lists into multi-line strings for better readability + self_history_str = "\n".join(map(str, self.history)) + other_history_str = "\n".join(map(str, other_module.history)) + self_ext_gates_str = "\n".join(self._external_gates) + other_ext_gates_str = "\n".join(other_module._external_gates) + + table_data = [ + ["Qubits", self.num_qubits, other_module.num_qubits], + ["Classical Bits", self.num_clbits, other_module.num_clbits], + ["Depth", self.depth(), other_module.depth()], + ["External Gates", self_ext_gates_str, other_ext_gates_str], + ["History", self_history_str, other_history_str], + ["-" * 15, "-" * 15, "-" * 15], # Separator + ["Gate Counts", "Self", "Other"], + ["-" * 15, "-" * 15, "-" * 15], # Separator + ] + + for gate in all_gates: + table_data.append([gate, self_counts.get(gate, 0), other_counts.get(gate, 0)]) + + print( + tabulate( + table_data, + headers=["Attribute", "Self", "Other"], + tablefmt="grid", + ) + ) + @staticmethod def skip_qasm_files_with_tag(content: str, mode: str) -> bool: """Check if a file should be skipped for a given mode (e.g., 'unroll', 'validate'). diff --git a/tests/visualization/test_compare.py b/tests/visualization/test_compare.py new file mode 100644 index 00000000..90f62435 --- /dev/null +++ b/tests/visualization/test_compare.py @@ -0,0 +1,64 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test the compare() method of the QasmModule class. +""" + +import pytest +from pyqasm.entrypoint import loads +from pyqasm.elements import BasisSet + +def test_compare_method_output(capsys): + """ + Tests the output of the QasmModule.compare() method to ensure it + correctly identifies and displays differences between two modules. + """ + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[2] q; + h q[0]; + cx q[0], q[1]; + barrier q; + """ + + module_a = loads(qasm_str) + module_b = loads(qasm_str) + + module_a.unroll() + module_b.unroll(unroll_barriers=False, external_gates=['cx']) + module_b.rebase(BasisSet.ROTATIONAL_CX) + + module_a.compare(module_b) + captured = capsys.readouterr() + stdout = captured.out + + print(stdout) + + assert "Attribute" in stdout + assert "Self" in stdout + assert "Other" in stdout + assert "Qubits" in stdout + assert "Depth" in stdout + assert "History" in stdout + assert "External Gates" in stdout + assert "Gate Counts" in stdout + assert "unroll({})" in stdout + assert "unroll({'unroll_barriers': False, 'external_gates': ['cx']})" in stdout + assert "rebase(BasisSet.ROTATIONAL_CX)" in stdout + assert "h" in stdout + assert "cx" in stdout + assert "barrier" in stdout \ No newline at end of file From 978ec59d9e34372d19b350c4326222d8c1b3a47d Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 16 Jul 2025 23:22:40 -0700 Subject: [PATCH 2/8] fix 216 changes --- pyproject.toml | 2 +- src/pyqasm/modules/base.py | 9 ++++- tests/visualization/test_compare.py | 58 ++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8578e539..c96bde3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] test = ["pytest", "pytest-cov", "pytest-mpl", "pillow<11.4.0", "matplotlib"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.10.2"] docs = ["sphinx>=7.3.7,<8.3.0", "sphinx-autodoc-typehints>=1.24,<3.2", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] -visualization = ["pillow<11.4.0", "matplotlib"] +visualization = ["pillow<11.4.0", "matplotlib", "tabulate"] pulse = ["openpulse[parser]>=1.0.1"] [tool.setuptools.package-data] diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index c7ed8b13..21fcb3bd 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -16,6 +16,8 @@ Definition of the base Qasm module """ +from __future__ import annotations + from abc import ABC, abstractmethod from collections import Counter from copy import deepcopy @@ -654,12 +656,15 @@ def get_gate_counts(self) -> dict[str, int]: ] return dict(Counter(gate.name.name for gate in gate_nodes)) - def compare(self, other_module: "QasmModule"): + def compare(self, other_module: QasmModule): """Compare two QasmModule objects across multiple attributes. Args: other_module (QasmModule): The module to compare with. """ + if not isinstance(other_module, QasmModule): + raise TypeError(f"Expected QasmModule instance, got {type(other_module).__name__}") + self_counts = self.get_gate_counts() other_counts = other_module.get_gate_counts() all_gates = sorted(list(set(self_counts) | set(other_counts))) @@ -673,6 +678,8 @@ def compare(self, other_module: "QasmModule"): table_data = [ ["Qubits", self.num_qubits, other_module.num_qubits], ["Classical Bits", self.num_clbits, other_module.num_clbits], + ["Measurements", self.has_measurements(), other_module.has_measurements()], + ["Barriers", self.has_barriers(), other_module.has_barriers()], ["Depth", self.depth(), other_module.depth()], ["External Gates", self_ext_gates_str, other_ext_gates_str], ["History", self_history_str, other_history_str], diff --git a/tests/visualization/test_compare.py b/tests/visualization/test_compare.py index 90f62435..0b32c8b7 100644 --- a/tests/visualization/test_compare.py +++ b/tests/visualization/test_compare.py @@ -61,4 +61,60 @@ def test_compare_method_output(capsys): assert "rebase(BasisSet.ROTATIONAL_CX)" in stdout assert "h" in stdout assert "cx" in stdout - assert "barrier" in stdout \ No newline at end of file + assert "barrier" in stdout + + +def test_compare_with_measurements_and_barriers(capsys): + """Test compare method with modules that have measurements and barriers.""" + qasm_with_both = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[2] q; + bit[2] c; + h q[0]; + barrier q; + cx q[0], q[1]; + c = measure q; + """ + + qasm_without_both = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[2] q; + h q[0]; + cx q[0], q[1]; + """ + + module_with_both = loads(qasm_with_both) + module_without_both = loads(qasm_without_both) + + module_with_both.compare(module_without_both) + captured = capsys.readouterr() + stdout = captured.out + + assert "Measurements" in stdout + assert "Barriers" in stdout + assert "True" in stdout + assert "False" in stdout + assert "Classical Bits" in stdout + + +def test_compare_invalid_type(): + """Test compare method with invalid input type raises TypeError.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[2] q; + h q[0]; + """ + + module = loads(qasm_str) + + with pytest.raises(TypeError, match="Expected QasmModule instance, got str"): + module.compare("not a module") + + with pytest.raises(TypeError, match="Expected QasmModule instance, got int"): + module.compare(42) \ No newline at end of file From d284f36686624f32db30e06dae25aee9e039eae1 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 21 Jul 2025 22:17:53 -0700 Subject: [PATCH 3/8] added decorator, changelogs, and test --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- src/pyqasm/modules/base.py | 44 ++++++++++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c39ac958..a4986b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Types of changes: ## Unreleased ### Added +- A `track_user_operation` decorator to automatically log method calls in `QasmModule`, reducing code duplication and improving maintainability. ([#216](https://github.com/qBraid/pyqasm/pull/216)) - A new discussion template for issues in pyqasm ([#213](https://github.com/qBraid/pyqasm/pull/213)) - A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214)) - Added `unroll` command support in PYQASM CLI with options skipping files, overwriting originals files, and specifying output paths.([#224](https://github.com/qBraid/pyqasm/pull/224)) @@ -22,6 +23,7 @@ Types of changes: - Added `slots=True` parameter to the data classes in `elements.py` to improve memory efficiency ([#218](https://github.com/qBraid/pyqasm/pull/218)) - Updated the documentation to include core features in the `README` ([#219](https://github.com/qBraid/pyqasm/pull/219)) - Added support to `device qubit` resgister consolidation.([#222](https://github.com/qBraid/pyqasm/pull/222)) +- The `QasmModule.compare` method now displays barrier and measurement statistics, and includes instance validation for safer comparisons. ([#216](https://github.com/qBraid/pyqasm/pull/216)) ### Deprecated @@ -34,6 +36,7 @@ Types of changes: ### Dependencies - Add `pillow<11.3.0` dependency for test and visualization to avoid CI errors in Linux builds ([#226](https://github.com/qBraid/pyqasm/pull/226)) +- Added `tabulate` to the testing dependencies to support new comparison table tests. ([#216](https://github.com/qBraid/pyqasm/pull/216)) ### Other diff --git a/pyproject.toml b/pyproject.toml index c96bde3b..1e09c7d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,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", "pytest-mpl", "pillow<11.4.0", "matplotlib"] +test = ["pytest", "pytest-cov", "pytest-mpl", "pillow<11.4.0", "matplotlib", "tabulate"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.10.2"] docs = ["sphinx>=7.3.7,<8.3.0", "sphinx-autodoc-typehints>=1.24,<3.2", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] visualization = ["pillow<11.4.0", "matplotlib", "tabulate"] diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 21fcb3bd..27818806 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -18,6 +18,7 @@ from __future__ import annotations +import functools from abc import ABC, abstractmethod from collections import Counter from copy import deepcopy @@ -36,6 +37,32 @@ from pyqasm.visitor import QasmVisitor +def track_user_operation(func): + """Decorator to track user operations on a QasmModule.""" + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + """Wrapper that logs the operation and its arguments.""" + func_name = func.__name__ + log_message = func_name + + if func_name == "depth": + decompose_native_gates = kwargs.get("decompose_native_gates", True) + if args: + decompose_native_gates = args[0] + log_message = f"depth(decompose_native_gates={decompose_native_gates})" + elif func_name == "unroll": + log_message = f"unroll({kwargs or {}})" + elif func_name == "rebase": + target_basis_set = args[0] + log_message = f"rebase({target_basis_set})" + + self._user_operations.append(log_message) + return func(self, *args, **kwargs) + + return wrapper + + class QasmModule(ABC): # pylint: disable=too-many-instance-attributes """Abstract class for a Qasm module @@ -157,6 +184,7 @@ def has_measurements(self) -> bool: break return self._has_measurements + @track_user_operation def remove_measurements(self, in_place: bool = True) -> Optional["QasmModule"]: """Remove the measurement operations @@ -166,7 +194,6 @@ def remove_measurements(self, in_place: bool = True) -> Optional["QasmModule"]: Returns: QasmModule: The module with the measurements removed if in_place is False """ - self._user_operations.append("remove_measurements") stmt_list = ( self._statements if len(self._unrolled_ast.statements) == 0 @@ -217,6 +244,7 @@ def has_barriers(self) -> bool: break return self._has_barriers + @track_user_operation def remove_barriers(self, in_place: bool = True) -> Optional["QasmModule"]: """Remove the barrier operations @@ -244,10 +272,10 @@ def remove_barriers(self, in_place: bool = True) -> Optional["QasmModule"]: curr_module._has_barriers = False curr_module._statements = stmts_without_barriers curr_module._unrolled_ast.statements = stmts_without_barriers - curr_module._user_operations.append("remove_barriers") return curr_module + @track_user_operation def remove_includes(self, in_place=True) -> Optional["QasmModule"]: """Remove the include statements from the module @@ -271,10 +299,10 @@ def remove_includes(self, in_place=True) -> Optional["QasmModule"]: curr_module._statements = stmts_without_includes curr_module._unrolled_ast.statements = stmts_without_includes - curr_module._user_operations.append("remove_includes") return curr_module + @track_user_operation def depth(self, decompose_native_gates=True): """Calculate the depth of the unrolled openqasm program. @@ -288,7 +316,6 @@ def depth(self, decompose_native_gates=True): """ # 1. Since the program will be unrolled before its execution on a QC, it makes sense to # calculate the depth of the unrolled program. - self._user_operations.append(f"depth(decompose_native_gates={decompose_native_gates})") # We are performing operations in place, thus we need to calculate depth # at "each instance of the function call". @@ -464,10 +491,10 @@ def remove_idle_qubits(self, in_place: bool = True): # the original ast will need to be updated to the unrolled ast as if we call the # unroll operation again, it will incorrectly choose the original ast WITH THE IDLE QUBITS qasm_module._statements = qasm_module._unrolled_ast.statements - qasm_module._user_operations.append("remove_idle_qubits") return qasm_module + @track_user_operation def reverse_qubit_order(self, in_place=True): """Reverse the order of qubits in the module. @@ -483,7 +510,6 @@ def reverse_qubit_order(self, in_place=True): qasm_module = self if in_place else self.copy() qasm_module.unroll() - qasm_module._user_operations.append("reverse_qubit_order") new_qubit_mappings = {} for register, size in self._qubit_registers.items(): @@ -529,11 +555,11 @@ def reverse_qubit_order(self, in_place=True): # 4. return the module return qasm_module + @track_user_operation def validate(self): """Validate the module""" if self._validated_program is True: return - self._user_operations.append("validate") try: self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) @@ -550,6 +576,7 @@ def validate(self): raise err self._validated_program = True + @track_user_operation def unroll(self, **kwargs): """Unroll the module into basic qasm operations. @@ -575,7 +602,6 @@ def unroll(self, **kwargs): if not kwargs: kwargs = {} - self._user_operations.append(f"unroll({kwargs})") try: self.num_qubits, self.num_clbits = 0, 0 if ext_gates := kwargs.get("external_gates"): @@ -592,6 +618,7 @@ def unroll(self, **kwargs): self._unrolled_ast = Program(statements=[], version=self.original_program.version) raise err + @track_user_operation def rebase(self, target_basis_set, in_place=True): """Rebase the AST to use a specified target basis set. @@ -606,7 +633,6 @@ def rebase(self, target_basis_set, in_place=True): """ if target_basis_set not in DECOMPOSITION_RULES: raise ValueError(f"Target basis set '{target_basis_set}' is not defined.") - self._user_operations.append(f"rebase({target_basis_set})") qasm_module = self if in_place else self.copy() From c0116e6a4bfd535e25929058e71ff25af4cca8f0 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 23 Jul 2025 22:01:32 -0700 Subject: [PATCH 4/8] lazy import --- src/pyqasm/modules/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 27818806..4bfb4d77 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -26,7 +26,6 @@ import openqasm3.ast as qasm3_ast from openqasm3.ast import BranchingStatement, Program, QuantumGate -from tabulate import tabulate from pyqasm.analyzer import Qasm3Analyzer from pyqasm.decomposer import Decomposer @@ -688,9 +687,16 @@ def compare(self, other_module: QasmModule): Args: other_module (QasmModule): The module to compare with. """ + try: + from tabulate import tabulate + except ImportError as exc: + raise ImportError( + "tabulate is required for the compare method. " + ) from exc + if not isinstance(other_module, QasmModule): raise TypeError(f"Expected QasmModule instance, got {type(other_module).__name__}") - + self_counts = self.get_gate_counts() other_counts = other_module.get_gate_counts() all_gates = sorted(list(set(self_counts) | set(other_counts))) From 0516eb05e061216ae46ae37aceca8a7653516c50 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 1 Aug 2025 21:36:06 +0530 Subject: [PATCH 5/8] fix format ci --- docs/conf.py | 2 +- src/pyqasm/cli/unroll.py | 5 ++-- src/pyqasm/cli/utils.py | 38 +++++++++++++++++++++++++++++ src/pyqasm/cli/validate.py | 5 ++-- src/pyqasm/modules/base.py | 34 +++++--------------------- tests/visualization/test_compare.py | 8 +++--- tox.ini | 4 ++- 7 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 src/pyqasm/cli/utils.py diff --git a/docs/conf.py b/docs/conf.py index e70eb0f0..6d71ede8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ # set_type_checking_flag = True autodoc_member_order = "bysource" autoclass_content = "both" -autodoc_mock_imports = ["openqasm3", "matplotlib"] +autodoc_mock_imports = ["openqasm3", "matplotlib", "tabulate"] 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/cli/unroll.py b/src/pyqasm/cli/unroll.py index d50e4ab3..4d7f74fd 100644 --- a/src/pyqasm/cli/unroll.py +++ b/src/pyqasm/cli/unroll.py @@ -29,7 +29,8 @@ from pyqasm import dumps, load from pyqasm.exceptions import QasmParsingError, UnrollError, ValidationError -from pyqasm.modules.base import QasmModule + +from .utils import skip_qasm_files_with_tag logger = logging.getLogger(__name__) logger.propagate = False @@ -57,7 +58,7 @@ def unroll_qasm_file(file_path: str) -> None: if file_path in skip_files: return - if QasmModule.skip_qasm_files_with_tag(content, "unroll"): + if skip_qasm_files_with_tag(content, "unroll"): skip_files.append(file_path) return diff --git a/src/pyqasm/cli/utils.py b/src/pyqasm/cli/utils.py new file mode 100644 index 00000000..f59afd4f --- /dev/null +++ b/src/pyqasm/cli/utils.py @@ -0,0 +1,38 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module containing utility functions for the CLI scripts + +""" + + +def skip_qasm_files_with_tag(content: str, mode: str) -> bool: + """Check if a file should be skipped for a given mode (e.g., 'unroll', 'validate'). + + Args: + content (str): The file content. + mode (str): The operation mode ('unroll', 'validate', etc.) + + Returns: + bool: True if the file should be skipped, False otherwise. + """ + skip_tag = f"// pyqasm disable: {mode}" + generic_skip_tag = "// pyqasm: ignore" + for line in content.splitlines(): + if skip_tag in line or generic_skip_tag in line: + return True + if "OPENQASM" in line: + break + return False diff --git a/src/pyqasm/cli/validate.py b/src/pyqasm/cli/validate.py index c8e7a2f2..ea8c23f7 100644 --- a/src/pyqasm/cli/validate.py +++ b/src/pyqasm/cli/validate.py @@ -26,7 +26,8 @@ from pyqasm import load from pyqasm.exceptions import QasmParsingError, UnrollError, ValidationError -from pyqasm.modules.base import QasmModule + +from .utils import skip_qasm_files_with_tag logger = logging.getLogger(__name__) logger.propagate = False @@ -62,7 +63,7 @@ def validate_qasm_file(file_path: str) -> None: if file_path in skip_files: return - if QasmModule.skip_qasm_files_with_tag(content, "validate"): + if skip_qasm_files_with_tag(content, "validate"): skip_files.append(file_path) return diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index ed899223..e84274b3 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -62,7 +62,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class QasmModule(ABC): # pylint: disable=too-many-instance-attributes +class QasmModule(ABC): # pylint: disable=too-many-instance-attributes, too-many-public-methods """Abstract class for a Qasm module Args: @@ -667,7 +667,7 @@ def rebase(self, target_basis_set, in_place=True): return qasm_module - def get_gate_counts(self) -> dict[str, int]: + def _get_gate_counts(self) -> dict[str, int]: """Return a dictionary of gate counts in the unrolled program. Returns: @@ -687,18 +687,16 @@ def compare(self, other_module: QasmModule): Args: other_module (QasmModule): The module to compare with. """ - try: + try: # pylint: disable-next=import-outside-toplevel from tabulate import tabulate except ImportError as exc: - raise ImportError( - "tabulate is required for the compare method. " - ) from exc + raise ImportError("tabulate is required for the compare method. ") from exc if not isinstance(other_module, QasmModule): raise TypeError(f"Expected QasmModule instance, got {type(other_module).__name__}") - self_counts = self.get_gate_counts() - other_counts = other_module.get_gate_counts() + self_counts = self._get_gate_counts() + other_counts = other_module._get_gate_counts() all_gates = sorted(list(set(self_counts) | set(other_counts))) # Format lists into multi-line strings for better readability @@ -731,26 +729,6 @@ def compare(self, other_module: QasmModule): ) ) - @staticmethod - def skip_qasm_files_with_tag(content: str, mode: str) -> bool: - """Check if a file should be skipped for a given mode (e.g., 'unroll', 'validate'). - - Args: - content (str): The file content. - mode (str): The operation mode ('unroll', 'validate', etc.) - - Returns: - bool: True if the file should be skipped, False otherwise. - """ - skip_tag = f"// pyqasm disable: {mode}" - generic_skip_tag = "// pyqasm: ignore" - for line in content.splitlines(): - if skip_tag in line or generic_skip_tag in line: - return True - if "OPENQASM" in line: - break - return False - def __str__(self) -> str: """Return the string representation of the QASM program diff --git a/tests/visualization/test_compare.py b/tests/visualization/test_compare.py index 0b32c8b7..98fa849b 100644 --- a/tests/visualization/test_compare.py +++ b/tests/visualization/test_compare.py @@ -17,8 +17,10 @@ """ import pytest -from pyqasm.entrypoint import loads + from pyqasm.elements import BasisSet +from pyqasm.entrypoint import loads + def test_compare_method_output(capsys): """ @@ -39,7 +41,7 @@ def test_compare_method_output(capsys): module_b = loads(qasm_str) module_a.unroll() - module_b.unroll(unroll_barriers=False, external_gates=['cx']) + module_b.unroll(unroll_barriers=False, external_gates=["cx"]) module_b.rebase(BasisSet.ROTATIONAL_CX) module_a.compare(module_b) @@ -117,4 +119,4 @@ def test_compare_invalid_type(): module.compare("not a module") with pytest.raises(TypeError, match="Expected QasmModule instance, got int"): - module.compare(42) \ No newline at end of file + module.compare(42) diff --git a/tox.ini b/tox.ini index 9c488a8a..6f6f4b90 100644 --- a/tox.ini +++ b/tox.ini @@ -51,7 +51,9 @@ commands = [testenv:mypy] envdir = .tox/linters skip_install = true -deps = mypy +deps = + mypy + types-tabulate commands = mypy src examples From 3dc32875c708232b6460617b85d8491e4e6ec523 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 1 Aug 2025 21:41:33 +0530 Subject: [PATCH 6/8] format --- docs/_static/logo.png | Bin 154811 -> 154809 bytes src/pyqasm/modules/base.py | 1 - 2 files changed, 1 deletion(-) diff --git a/docs/_static/logo.png b/docs/_static/logo.png index eaa9312e37fa17f1f3b9145f0855000be170cb10..f41c51fc31fd28c8b0635001833a0dd19f9fd426 100644 GIT binary patch delta 24 gcmdnJlXK@zPL|F9KlhESlXo}I+`WC~Zbqer0En3j6#xJL delta 27 jcmdnFlXLe@PS(x str: """Returns the name of the module.""" From b7886f19ea33f01df4291314e30a56a2920473a3 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 5 Aug 2025 08:25:50 +0530 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f29d161..dd6996a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,18 +15,17 @@ Types of changes: ## Unreleased ### Added -- A `track_user_operation` decorator to automatically log method calls in `QasmModule`, reducing code duplication and improving maintainability. ([#216](https://github.com/qBraid/pyqasm/pull/216)) - A new discussion template for issues in pyqasm ([#213](https://github.com/qBraid/pyqasm/pull/213)) - A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214)) - Added `unroll` command support in PYQASM CLI with options skipping files, overwriting originals files, and specifying output paths.([#224](https://github.com/qBraid/pyqasm/pull/224)) -- Added `.github/copilot-instructions.md` to the repository to document coding standards and design principles for pyqasm. This file provides detailed guidance on documentation, static typing, formatting, error handling, and adherence to the QASM specification for all code contributions. ([#234](https://github.com/qBraid/pyqasm/pull/234)) +- Added `.github/copilot-instructions.md` to the repository to document coding standards and design principles for pyqasm. This file provides detailed guidance on documentation, static typing, formatting, error handling, and adherence to the QASM specification for all code contributions. ([#234](https://github.com/qBraid/pyqasm/pull/234))s +- Added a new `QasmModule.compare` method to compare two QASM modules, providing a detailed report of differences in gates, qubits, and measurements. This method is useful for comparing two identifying differences in QASM programs, their structure and operations. ([#233](https://github.com/qBraid/pyqasm/pull/233)) ### Improved / Modified - Added `slots=True` parameter to the data classes in `elements.py` to improve memory efficiency ([#218](https://github.com/qBraid/pyqasm/pull/218)) - Updated the documentation to include core features in the `README` ([#219](https://github.com/qBraid/pyqasm/pull/219)) - Added support to `device qubit` resgister consolidation.([#222](https://github.com/qBraid/pyqasm/pull/222)) - Updated the scoping of variables in `QasmVisitor` using a `ScopeManager`. This change is introduced to ensure that the `QasmVisitor` and the `PulseVisitor` can share the same `ScopeManager` instance, allowing for consistent variable scoping across different visitors. No change in the user API is expected. ([#232](https://github.com/qBraid/pyqasm/pull/232)) -- The `QasmModule.compare` method now displays barrier and measurement statistics, and includes instance validation for safer comparisons. ([#216](https://github.com/qBraid/pyqasm/pull/216)) - Added `Duration`,`Stretch` type, `Delay` and `Box` support for `OPENQASM3` code in pyqasm. ([#231](https://github.com/qBraid/pyqasm/pull/231)) ###### Example: ```qasm From 78a6b6d9e68ca0b64ee1ae3ea18079c394bff6b6 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 5 Aug 2025 08:33:28 +0530 Subject: [PATCH 8/8] fix typo in changelog [no ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6996a3..7b48491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Types of changes: - A new discussion template for issues in pyqasm ([#213](https://github.com/qBraid/pyqasm/pull/213)) - A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214)) - Added `unroll` command support in PYQASM CLI with options skipping files, overwriting originals files, and specifying output paths.([#224](https://github.com/qBraid/pyqasm/pull/224)) -- Added `.github/copilot-instructions.md` to the repository to document coding standards and design principles for pyqasm. This file provides detailed guidance on documentation, static typing, formatting, error handling, and adherence to the QASM specification for all code contributions. ([#234](https://github.com/qBraid/pyqasm/pull/234))s +- Added `.github/copilot-instructions.md` to the repository to document coding standards and design principles for pyqasm. This file provides detailed guidance on documentation, static typing, formatting, error handling, and adherence to the QASM specification for all code contributions. ([#234](https://github.com/qBraid/pyqasm/pull/234)) - Added a new `QasmModule.compare` method to compare two QASM modules, providing a detailed report of differences in gates, qubits, and measurements. This method is useful for comparing two identifying differences in QASM programs, their structure and operations. ([#233](https://github.com/qBraid/pyqasm/pull/233)) ### Improved / Modified