Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Types of changes:
- 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 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))
Expand Down Expand Up @@ -55,6 +56,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

Expand Down
Binary file modified docs/_static/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ 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"]
visualization = ["pillow<11.4.0", "matplotlib", "tabulate"]
pulse = ["openpulse[parser]>=1.0.1"]

[tool.setuptools.package-data]
Expand Down
5 changes: 3 additions & 2 deletions src/pyqasm/cli/unroll.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions src/pyqasm/cli/utils.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions src/pyqasm/cli/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
120 changes: 103 additions & 17 deletions src/pyqasm/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
Definition of the base Qasm module
"""

from __future__ import annotations

import functools
from abc import ABC, abstractmethod
from collections import Counter
from copy import deepcopy
from typing import Optional

Expand All @@ -32,7 +36,33 @@
from pyqasm.visitor import QasmVisitor, ScopeManager


class QasmModule(ABC): # pylint: disable=too-many-instance-attributes
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, too-many-public-methods
"""Abstract class for a Qasm module

Args:
Expand All @@ -59,13 +89,19 @@ 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"]
self._device_cycle_time: Optional[int] = None

@property
def name(self) -> str:
"""Returns the name of the module."""
return self._name

@property
def history(self) -> list[str]:
Comment thread
TheGupta2012 marked this conversation as resolved.
"""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."""
Expand Down Expand Up @@ -148,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

Expand Down Expand Up @@ -207,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

Expand Down Expand Up @@ -237,6 +275,7 @@ def remove_barriers(self, in_place: bool = True) -> Optional["QasmModule"]:

return curr_module

@track_user_operation
def remove_includes(self, in_place=True) -> Optional["QasmModule"]:
"""Remove the include statements from the module

Expand All @@ -263,6 +302,7 @@ def remove_includes(self, in_place=True) -> Optional["QasmModule"]:

return curr_module

@track_user_operation
def depth(self, decompose_native_gates=True):
"""Calculate the depth of the unrolled openqasm program.

Expand Down Expand Up @@ -454,6 +494,7 @@ def remove_idle_qubits(self, in_place: bool = True):

return qasm_module

@track_user_operation
def reverse_qubit_order(self, in_place=True):
"""Reverse the order of qubits in the module.

Expand Down Expand Up @@ -514,6 +555,7 @@ 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:
Expand All @@ -534,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.

Expand Down Expand Up @@ -575,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.

Expand Down Expand Up @@ -624,25 +668,67 @@ def rebase(self, target_basis_set, in_place=True):

return qasm_module

@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.)
def _get_gate_counts(self) -> dict[str, int]:
"""Return a dictionary of gate counts in the unrolled program.

Returns:
bool: True if the file should be skipped, False otherwise.
dict[str, int]: A dictionary of gate counts.
"""
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
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.
"""
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

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)))

# 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],
["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],
["-" * 15, "-" * 15, "-" * 15], # Separator
["Gate Counts", "Self", "Other"],
["-" * 15, "-" * 15, "-" * 15], # Separator
]
Comment thread
TheGupta2012 marked this conversation as resolved.

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",
)
)

def __str__(self) -> str:
"""Return the string representation of the QASM program
Expand Down
Loading