From 85fe036fc85e3187db77383563393c6be273af53 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 7 Oct 2024 14:22:54 +0530 Subject: [PATCH 01/16] add files 1 --- .github/workflows/format.yml | 24 + pyproject.toml | 4 +- pyqasm/README.md | 32 + pyqasm/__init__.py | 9 +- pyqasm/_version.py | 17 + pyqasm/analyzer.py | 233 +++ pyqasm/elements.py | 246 +++ pyqasm/exceptions.py | 42 +- pyqasm/expressions.py | 281 ++++ pyqasm/linalg.py | 302 ++++ pyqasm/maps.py | 702 +++++++++ pyqasm/subroutines.py | 388 +++++ pyqasm/transformer.py | 352 +++++ pyqasm/unroller.py | 56 + pyqasm/validate.py | 19 +- pyqasm/validator.py | 295 ++++ pyqasm/visitor.py | 1411 ++++++++++++++++++ tests/__init__.py | 0 tests/declarations/__init__.py | 0 tests/declarations/test_classical.py | 350 +++++ tests/declarations/test_quantum.py | 129 ++ tests/resources/__init__.py | 0 tests/resources/gates.py | 9 + tests/resources/subroutines.py | 9 + tests/resources/variables.py | 264 ++++ tests/subroutines/__init__.py | 0 tests/subroutines/test_subroutines.py | 9 + tests/subroutines/test_subroutines_arrays.py | 9 + tests/test_alias.py | 306 ++++ tests/test_barrier.py | 132 ++ tests/test_expressions.py | 69 + tests/test_gates.py | 266 ++++ tests/test_if.py | 245 +++ tests/test_loop.py | 362 +++++ tests/test_measurement.py | 137 ++ tests/test_reset.py | 107 ++ tests/test_sizeof.py | 123 ++ tests/test_switch.py | 443 ++++++ tests/utils.py | 29 + tox.ini | 90 ++ 40 files changed, 7487 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/format.yml create mode 100644 pyqasm/README.md create mode 100644 pyqasm/_version.py create mode 100644 pyqasm/analyzer.py create mode 100644 pyqasm/elements.py create mode 100644 pyqasm/expressions.py create mode 100644 pyqasm/linalg.py create mode 100644 pyqasm/maps.py create mode 100644 pyqasm/subroutines.py create mode 100644 pyqasm/transformer.py create mode 100644 pyqasm/unroller.py create mode 100644 pyqasm/validator.py create mode 100644 pyqasm/visitor.py create mode 100644 tests/__init__.py create mode 100644 tests/declarations/__init__.py create mode 100644 tests/declarations/test_classical.py create mode 100644 tests/declarations/test_quantum.py create mode 100644 tests/resources/__init__.py create mode 100644 tests/resources/gates.py create mode 100644 tests/resources/subroutines.py create mode 100644 tests/resources/variables.py create mode 100644 tests/subroutines/__init__.py create mode 100644 tests/subroutines/test_subroutines.py create mode 100644 tests/subroutines/test_subroutines_arrays.py create mode 100644 tests/test_alias.py create mode 100644 tests/test_barrier.py create mode 100644 tests/test_expressions.py create mode 100644 tests/test_gates.py create mode 100644 tests/test_if.py create mode 100644 tests/test_loop.py create mode 100644 tests/test_measurement.py create mode 100644 tests/test_reset.py create mode 100644 tests/test_sizeof.py create mode 100644 tests/test_switch.py create mode 100644 tests/utils.py create mode 100644 tox.ini diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..36e5308 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,24 @@ +name: Format + +on: + pull_request: + branches: ['main'] + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install tox>=4.2.0 + - name: Check isort, black, mypy, headers + run: | + tox -e format-check \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f5f2afb..342b7f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ 'Operating System :: Unix', 'Operating System :: MacOS', ] -dependencies = ["pyqir>=0.10.0,<0.11.0", "numpy"] +dependencies = ["numpy", "openqasm3[parser]==1.0.0"] [project.urls] source = "https://github.com/qBraid/pyqasm" @@ -60,7 +60,7 @@ line_length = 100 [tool.pylint.'MESSAGES CONTROL'] max-line-length = 100 -disable = "C0414,C0415,R0914,W0511" +disable = "C0414, C0415, R0914, C0103, C0115, C0116, I1101, R0903, R0917, W0212, W0511" [tool.pylint.MASTER] ignore-paths = [ diff --git a/pyqasm/README.md b/pyqasm/README.md new file mode 100644 index 0000000..c0e232b --- /dev/null +++ b/pyqasm/README.md @@ -0,0 +1,32 @@ +# OpenQASM 3 to QIR + +## Supported conversions status table + +| openqasm3.ast Object Type | Supported | Comment | +| -------------------------------| ----------- | ---------------------- | +| QuantumMeasurementStatement | ✅ | Complete | +| QuantumReset | ✅ | Complete | +| QuantumBarrier | ✅ | Complete | +| QuantumGateDefinition | ✅ | Complete | +| QuantumGate | ✅ | Complete | +| QuantumGateModifier | ✅ | Complete (pow, inv) | +| QubitDeclaration | ✅ | Completed | +| Clbit Declarations | ✅ | Completed | +| BinaryExpression | ✅ | Completed | +| UnaryExpression | ✅ | Completed | +| ClassicalDeclaration | ✅ | Completed | +| ConstantDeclaration | ✅ | Completed | +| ClassicalAssignment | ✅ | Completed | +| AliasStatement | ✅ | Completed | +| SwitchStatement | ✅ | Completed | +| BranchingStatement | 🔜 | In progress | +| SubroutineDefinition | 🔜 | In progress | +| Looping statements(eg. for) | 🔜 | In progress | +| RangeDefinition | 🔜 | In progress | +| IODeclaration | 📋 | Planned | +| Pragma | ❓ | Unsure | +| Annotations | ❓ | Unsure | +| Pulse-level ops (e.g. delay) | ❌ | Not supported by QIR | +| Calibration ops | ❌ | Not supported by QIR | +| Duration literals | ❌ | Not supported by QIR | +| ComplexType | ❌ | Not supported by QIR | diff --git a/pyqasm/__init__.py b/pyqasm/__init__.py index 6e00552..4ec7544 100644 --- a/pyqasm/__init__.py +++ b/pyqasm/__init__.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the qBraid-SDK +# This file is part of the pyqasm # -# The qBraid-SDK is free software released under the GNU General Public License v3 +# The 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 the qBraid-SDK, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. """ Top level module containing the main PyQASM functionality. @@ -20,6 +20,7 @@ :toctree: ../stubs/ validate + unroll Exceptions ----------- @@ -41,11 +42,13 @@ __version__ = "dev" from .exceptions import PyQasmError, ValidationError +from .unroller import unroll from .validate import validate __all__ = [ "PyQasmError", "ValidationError", "validate", + "unroll", "__version__", ] diff --git a/pyqasm/_version.py b/pyqasm/_version.py new file mode 100644 index 0000000..28d532b --- /dev/null +++ b/pyqasm/_version.py @@ -0,0 +1,17 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = "0.0.1.dev0+ga5e32ff.d20241007" +__version_tuple__ = version_tuple = (0, 0, 1, "dev0", "ga5e32ff.d20241007") diff --git a/pyqasm/analyzer.py b/pyqasm/analyzer.py new file mode 100644 index 0000000..82c5236 --- /dev/null +++ b/pyqasm/analyzer.py @@ -0,0 +1,233 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=import-outside-toplevel,cyclic-import + +""" +Module with analysis functions for QASM3 visitor + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional, Union + +import numpy as np +from openqasm3.ast import ( + BinaryExpression, + DiscreteSet, + Expression, + Identifier, + IndexExpression, + IntegerLiteral, + IntType, + RangeDefinition, + UnaryExpression, +) + +from .exceptions import ValidationError, raise_qasm3_error + +if TYPE_CHECKING: + from .elements import Variable + from .expressions import Qasm3ExprEvaluator + + +class Qasm3Analyzer: + """Class with utility functions for analyzing QASM3 elements""" + + @classmethod + def analyze_classical_indices( + cls, indices: list[Any], var: Variable, expr_evaluator: Qasm3ExprEvaluator + ) -> list: + """Validate the indices for a classical variable. + + Args: + indices (list[list[Any]]): The indices to validate. + var (Variable): The variable to verify + + Raises: + ValidationError: If the indices are invalid. + + Returns: + list[list]: The list of indices. Note, we can also have a list of indices within + a list if the variable is a multi-dimensional array. + """ + indices_list = [] + var_dimensions: Optional[list[int]] = var.dims + + if var_dimensions is None or len(var_dimensions) == 0: + raise_qasm3_error( + message=f"Indexing error. Variable {var.name} is not an array", + err_type=ValidationError, + span=indices[0].span, + ) + if isinstance(indices, DiscreteSet): + indices = indices.values + + if len(indices) != len(var_dimensions): # type: ignore[arg-type] + raise_qasm3_error( + message=f"Invalid number of indices for variable {var.name}. " + f"Expected {len(var_dimensions)} but got {len(indices)}", # type: ignore[arg-type] + err_type=ValidationError, + span=indices[0].span, + ) + + def _validate_index(index, dimension, var_name, span, dim_num): + if index < 0 or index >= dimension: + raise_qasm3_error( + message=f"Index {index} out of bounds for dimension {dim_num} " + f"of variable {var_name}", + err_type=ValidationError, + span=span, + ) + + def _validate_step(start_id, end_id, step, span): + if (step < 0 and start_id < end_id) or (step > 0 and start_id > end_id): + direction = "less than" if step < 0 else "greater than" + raise_qasm3_error( + message=f"Index {start_id} is {direction} {end_id} but step" + f" is {'negative' if step < 0 else 'positive'}", + err_type=ValidationError, + span=span, + ) + + for i, index in enumerate(indices): + if not isinstance(index, (Identifier, Expression, RangeDefinition, IntegerLiteral)): + raise_qasm3_error( + message=f"Unsupported index type {type(index)} for " + f"classical variable {var.name}", + err_type=ValidationError, + span=index.span, + ) + + if isinstance(index, RangeDefinition): + assert var_dimensions is not None + + start_id = 0 + if index.start is not None: + start_id = expr_evaluator.evaluate_expression(index.start, reqd_type=IntType) + + end_id = var_dimensions[i] - 1 + if index.end is not None: + end_id = expr_evaluator.evaluate_expression(index.end, reqd_type=IntType) + + step = 1 + if index.step is not None: + step = expr_evaluator.evaluate_expression(index.step, reqd_type=IntType) + + _validate_index(start_id, var_dimensions[i], var.name, index.span, i) + _validate_index(end_id, var_dimensions[i], var.name, index.span, i) + _validate_step(start_id, end_id, step, index.span) + + indices_list.append((start_id, end_id, step)) + + if isinstance(index, (Identifier, IntegerLiteral, Expression)): + index_value = expr_evaluator.evaluate_expression(index, reqd_type=IntType) + curr_dimension = var_dimensions[i] # type: ignore[index] + _validate_index(index_value, curr_dimension, var.name, index.span, i) + + indices_list.append((index_value, index_value, 1)) + + return indices_list + + @staticmethod + def analyze_index_expression( + index_expr: IndexExpression, + ) -> tuple[str, list[Union[Any, Expression, RangeDefinition]]]: + """Analyze an index expression to get the variable name and indices. + + Args: + index_expr (IndexExpression): The index expression to analyze. + + Returns: + tuple[str, list[Any]]: The variable name and indices in openqasm objects + + """ + indices: list[Any] = [] + var_name = "" + comma_separated = False + + if isinstance(index_expr.collection, IndexExpression): + while isinstance(index_expr, IndexExpression): + if isinstance(index_expr.index, list): + indices.append(index_expr.index[0]) + index_expr = index_expr.collection + else: + comma_separated = True + indices = index_expr.index # type: ignore[assignment] + var_name = ( + index_expr.collection.name # type: ignore[attr-defined] + if comma_separated + else index_expr.name # type: ignore[attr-defined] + ) + if not comma_separated: + indices = indices[::-1] + + return var_name, indices + + @staticmethod + def find_array_element(multi_dim_arr: np.ndarray, indices: list[tuple[int, int, int]]) -> Any: + """Find the value of an array at the specified indices. + + Args: + multi_dim_arr (np.ndarray): The multi-dimensional list to search. + indices (list[tuple[int,int,int]]): The indices to search. + + Returns: + Any: The value at the specified indices. + """ + slicing = tuple( + slice(start, end + 1, step) if start != end else start for start, end, step in indices + ) + return multi_dim_arr[slicing] # type: ignore[index] + + @staticmethod + def analyse_branch_condition(condition: Any) -> bool: + """ + analyze the branching condition to determine the branch to take + + Args: + condition (Any): The condition to analyze + + Returns: + bool: The branch to take + """ + + if isinstance(condition, UnaryExpression): + if condition.op.name != "!": + raise_qasm3_error( + message=f"Unsupported unary expression '{condition.op.name}' in if condition", + err_type=ValidationError, + span=condition.span, + ) + return False + if isinstance(condition, BinaryExpression): + if condition.op.name != "==": + raise_qasm3_error( + message=f"Unsupported binary expression '{condition.op.name}' in if condition", + err_type=ValidationError, + span=condition.span, + ) + if not isinstance(condition.lhs, IndexExpression): + raise_qasm3_error( + message=f"Unsupported expression type '{type(condition.rhs)}' in if condition", + err_type=ValidationError, + span=condition.span, + ) + return condition.rhs.value != 0 # type: ignore[attr-defined] + if not isinstance(condition, IndexExpression): + raise_qasm3_error( + message=( + f"Unsupported expression type '{type(condition)}' in if condition. " + "Can only be a simple comparison" + ), + err_type=ValidationError, + span=condition.span, + ) + return True diff --git a/pyqasm/elements.py b/pyqasm/elements.py new file mode 100644 index 0000000..d61ff38 --- /dev/null +++ b/pyqasm/elements.py @@ -0,0 +1,246 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=too-many-arguments + +""" +Module defining Qasm3 Converter elements. + +""" +from abc import ABCMeta, abstractmethod +from enum import Enum +from typing import Any, Optional, Union + +import numpy as np +from openqasm3.ast import BitType, ClassicalDeclaration, Program, QubitDeclaration, Statement + + +class InversionOp(Enum): + """ + Enum for specifying the inversion action of a gate. + """ + + NO_OP = 1 + INVERT_ROTATION = 2 + + +class Context(Enum): + """ + Enum for the different contexts in Qasm. + """ + + GLOBAL = "global" + BLOCK = "block" + FUNCTION = "function" + GATE = "gate" + + +class Variable: + """ + Class representing an openqasm variable. + + Args: + name (str): Name of the variable. + base_type (Any): Base type of the variable. + base_size (int): Base size of the variable. + dims (list[int]): Dimensions of the variable. + value (Optional[Union[int, float, list]]): Value of the variable. + is_constant (bool): Flag indicating if the variable is constant. + readonly(bool): Flag indicating if the variable is readonly. + + """ + + def __init__( + self, + name: str, + base_type: Any, + base_size: int, + dims: Optional[list[int]] = None, + value: Optional[Union[int, float, np.ndarray]] = None, + is_constant: bool = False, + readonly: bool = False, + ): + self.name = name + self.base_type = base_type + self.base_size = base_size + self.dims = dims + self.value = value + self.is_constant = is_constant + self.readonly = readonly + + +class _ProgramElement(metaclass=ABCMeta): + """Abstract class for program elements""" + + @classmethod + def from_element_list(cls, elements): + """Create a list of elements from a list of elements""" + return [cls(elem) for elem in elements] + + @abstractmethod + def accept(self, visitor): + """Accept a visitor for the element""" + + +class _Register(_ProgramElement): + + def __init__(self, register: Union[QubitDeclaration, ClassicalDeclaration]): + self._register: Union[QubitDeclaration, ClassicalDeclaration] = register + + def accept(self, visitor): + visitor.visit_register(self._register) + + def __str__(self) -> str: + return f"Register({self._register})" + + +class _Statement(_ProgramElement): + + def __init__(self, statement: Statement): + self._statement = statement + + def accept(self, visitor): + visitor.visit_statement(self._statement) + + def __str__(self) -> str: + return f"Statement({self._statement})" + + +class Qasm3Module: + """ + A module representing an unrolled openqasm quantum program. + + Args: + name (str): Name of the module. + module (Module): QIR Module instance. + num_qubits (int): Number of qubits in the circuit. + num_clbits (int): Number of classical bits in the circuit. + original_program (Program): The original openqasm3 program. + elements (list[Statement]): list of openqasm3 Statements. + """ + + def __init__( + self, + name: str, + num_qubits: int, + num_clbits: int, + program: Program, + elements, + ): + self._name = name + self._num_qubits = num_qubits + self._num_clbits = num_clbits + self._elements = elements + self._unrolled_qasm = "" + self._original_program = program + + @property + def name(self) -> str: + """Returns the name of the module.""" + return self._name + + @property + def num_qubits(self) -> int: + """Returns the number of qubits in the circuit.""" + return self._num_qubits + + @property + def num_clbits(self) -> int: + """Returns the number of classical bits in the circuit.""" + return self._num_clbits + + @property + def original_program(self) -> Program: + """Returns the program AST for the original qasm supplied by the user""" + return self._original_program + + @property + def unrolled_qasm(self) -> str: + """Returns the unrolled qasm for the given module""" + return self._unrolled_qasm + + @unrolled_qasm.setter + def unrolled_qasm(self, value: str): + """Setter for the unrolled qasm""" + self._unrolled_qasm = value + + def unrolled_qasm_as_list(self): + """Returns the unrolled qasm as a list of lines""" + return self.unrolled_qasm.split("\n") + + def add_qasm_statement(self, statement: str): + """Add a qasm statement to the unrolled qasm + + Args: + statement (str): The qasm statement to add to the unrolled qasm + + Returns: + None + """ + + if len(self.unrolled_qasm) == 0: + self.unrolled_qasm = "OPENQASM 3.0;\n" + self.unrolled_qasm += 'include "stdgates.inc";\n' + # add comments about total number of qubits and clbits + self.unrolled_qasm += f"// Total number of qubits: {self.num_qubits}\n" + self.unrolled_qasm += f"// Total number of clbits: {self.num_clbits}\n" + + self.unrolled_qasm += statement + + @classmethod + def from_program(cls, program: Program): + """ + Construct a Qasm3Module from a given openqasm3.ast.Program object + """ + elements: list[Union[_Register, _Statement]] = [] + + num_qubits = 0 + num_clbits = 0 + for statement in program.statements: + if isinstance(statement, QubitDeclaration): + size = 1 + if statement.size: + size = statement.size.value # type: ignore[attr-defined] + num_qubits += size + elements.append(_Register(statement)) + + elif isinstance(statement, ClassicalDeclaration) and isinstance( + statement.type, BitType + ): + size = 1 + if statement.type.size: + size = statement.type.size.value # type: ignore[attr-defined] + num_clbits += size + elements.append(_Register(statement)) + # as bit arrays are just 0 / 1 values, we can treat them as + # classical variables too. Thus, need to add them to normal + # statements too. + elements.append(_Statement(statement)) + else: + elements.append(_Statement(statement)) + + return cls( + name="main", + num_qubits=num_qubits, + num_clbits=num_clbits, + program=program, + elements=elements, + ) + + def accept(self, visitor): + """Accept a visitor for the module + + Args: + visitor (BasicQasmVisitor): The visitor to accept + """ + for element in self._elements: + element.accept(visitor) + + # TODO: some finalizing method here probably diff --git a/pyqasm/exceptions.py b/pyqasm/exceptions.py index c444576..eb6efcc 100644 --- a/pyqasm/exceptions.py +++ b/pyqasm/exceptions.py @@ -1,18 +1,23 @@ # Copyright (C) 2024 qBraid # -# This file is part of the qBraid-SDK +# This file is part of the pyqasm # -# The qBraid-SDK is free software released under the GNU General Public License v3 +# The 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 the qBraid-SDK, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. """ Module defining base PyQASM exceptions. """ +import logging +from typing import Optional, Type + +from openqasm3.ast import Span + class PyQasmError(Exception): """Base exception for all PyQASM exceptions.""" @@ -20,3 +25,34 @@ class PyQasmError(Exception): class ValidationError(PyQasmError): """Exception raised when a OpenQASM program fails validation.""" + + +class UnrollError(PyQasmError): + """Exception raised when a OpenQASM program fails unrolling.""" + + +def raise_qasm3_error( + message: Optional[str] = None, + err_type: Type[Exception] = ValidationError, + span: Optional[Span] = None, + raised_from: Optional[Exception] = None, +) -> None: + """Raises a QASM3 conversion error with optional chaining from another exception. + + Args: + message: The error message. If not provided, a default message will be used. + err_type: The type of error to raise. + span: The span (location) in the QASM file where the error occurred. + raised_from: Optional exception from which this error was raised (chaining). + + Raises: + err_type: The error type initialized with the specified message and chained exception. + """ + if span: + logging.error( + "Error at line %s, column %s in QASM file", span.start_line, span.start_column + ) + + if raised_from: + raise err_type(message) from raised_from + raise err_type(message) diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py new file mode 100644 index 0000000..9261276 --- /dev/null +++ b/pyqasm/expressions.py @@ -0,0 +1,281 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=too-many-locals + +""" +Module containing the class for evaluating QASM3 expressions. + +""" + +from openqasm3.ast import BinaryExpression, BooleanLiteral, BoolType, DurationLiteral, FloatLiteral +from openqasm3.ast import FloatType as Qasm3FloatType +from openqasm3.ast import ( + FunctionCall, + Identifier, + ImaginaryLiteral, + IndexExpression, + IntegerLiteral, +) +from openqasm3.ast import IntType as Qasm3IntType +from openqasm3.ast import SizeOf, UnaryExpression + +from .analyzer import Qasm3Analyzer +from .exceptions import ValidationError, raise_qasm3_error +from .maps import CONSTANTS_MAP, qasm3_expression_op_map +from .validator import Qasm3Validator + + +class Qasm3ExprEvaluator: + """Class for evaluating QASM3 expressions.""" + + visitor_obj = None + + @classmethod + def set_visitor_obj(cls, visitor_obj) -> None: + cls.visitor_obj = visitor_obj + + @classmethod + def _check_var_in_scope(cls, var_name, expression): + """Checks if a variable is in scope. + + Args: + var_name: The name of the variable to check. + expression: The expression containing the variable. + Raises: + ValidationError: If the variable is undefined in the current scope. + """ + + if not cls.visitor_obj._check_in_scope(var_name): + raise_qasm3_error( + f"Undefined identifier {var_name} in expression", + ValidationError, + expression.span, + ) + + @classmethod + def _check_var_constant(cls, var_name, const_expr, expression): + """Checks if a variable is constant. + + Args: + var_name: The name of the variable to check. + const_expr: Whether the expression is a constant. + expression: The expression containing the variable. + + Raises: + ValidationError: If the variable is not a constant in the given + expression. + """ + const_var = cls.visitor_obj._get_from_visible_scope(var_name).is_constant + if const_expr and not const_var: + raise_qasm3_error( + f"Variable '{var_name}' is not a constant in given expression", + ValidationError, + expression.span, + ) + + @classmethod + def _check_var_type(cls, var_name, reqd_type, expression): + """Check the type of a variable and raise an error if it does not match the + required type. + + Args: + var_name: The name of the variable to check. + reqd_type: The required type of the variable. + expression: The expression where the variable is used. + + Raises: + ValidationError: If the variable has an invalid type for the required type. + """ + + if not Qasm3Validator.validate_variable_type( + cls.visitor_obj._get_from_visible_scope(var_name), reqd_type + ): + raise_qasm3_error( + f"Invalid type of variable {var_name} for required type {reqd_type}", + ValidationError, + expression.span, + ) + + @staticmethod + def _check_var_initialized(var_name, var_value, expression): + """Checks if a variable is initialized and raises an error if it is not. + + Args: + var_name (str): The name of the variable. + var_value: The value of the variable. + expression: The expression where the variable is used. + Raises: + ValidationError: If the variable is uninitialized. + """ + + if var_value is None: + raise_qasm3_error( + f"Uninitialized variable {var_name} in expression", + ValidationError, + expression.span, + ) + + @classmethod + def _get_var_value(cls, var_name, indices, expression): + """Retrieves the value of a variable. + + Args: + var_name (str): The name of the variable. + indices (list): The indices of the variable (if it is an array). + expression (Identifier or Expression): The expression representing the variable. + Returns: + var_value: The value of the variable. + """ + + var_value = None + if isinstance(expression, Identifier): + var_value = cls.visitor_obj._get_from_visible_scope(var_name).value + else: + validated_indices = Qasm3Analyzer.analyze_classical_indices( + indices, cls.visitor_obj._get_from_visible_scope(var_name), cls + ) + var_value = Qasm3Analyzer.find_array_element( + cls.visitor_obj._get_from_visible_scope(var_name).value, validated_indices + ) + return var_value + + @classmethod + # pylint: disable-next=too-many-return-statements, too-many-branches + def evaluate_expression(cls, expression, const_expr: bool = False, reqd_type=None): + """Evaluate an expression. Scalar types are assigned by value. + + Args: + expression (Any): The expression to evaluate. + const_expr (bool): Whether the expression is a constant. Defaults to False. + reqd_type (Any): The required type of the expression. Defaults to None. + + Returns: + Any : The result of the evaluation. + + Raises: + ValidationError: If the expression is not supported. + """ + if expression is None: + return None + + if isinstance(expression, (ImaginaryLiteral, DurationLiteral)): + raise_qasm3_error( + f"Unsupported expression type {type(expression)}", + ValidationError, + expression.span, + ) + + def _process_variable(var_name: str, indices=None): + cls._check_var_in_scope(var_name, expression) + cls._check_var_constant(var_name, const_expr, expression) + cls._check_var_type(var_name, reqd_type, expression) + var_value = cls._get_var_value(var_name, indices, expression) + Qasm3ExprEvaluator._check_var_initialized(var_name, var_value, expression) + return var_value + + if isinstance(expression, Identifier): + var_name = expression.name + if var_name in CONSTANTS_MAP: + if not reqd_type or reqd_type == Qasm3FloatType: + return CONSTANTS_MAP[var_name] + raise_qasm3_error( + f"Constant {var_name} not allowed in non-float expression", + ValidationError, + expression.span, + ) + return _process_variable(var_name) + + if isinstance(expression, IndexExpression): + var_name, indices = Qasm3Analyzer.analyze_index_expression(expression) + return _process_variable(var_name, indices) + + if isinstance(expression, SizeOf): + # has 2 elements - target and index + target = expression.target + index = expression.index + + if isinstance(target, Identifier): + var_name = target.name + cls._check_var_in_scope(var_name, expression) + dimensions = cls.visitor_obj._get_from_visible_scope( # type: ignore[union-attr] + var_name + ).dims + else: + raise_qasm3_error( + message=f"Unsupported target type {type(target)} for sizeof expression", + err_type=ValidationError, + span=expression.span, + ) + + if dimensions is None or len(dimensions) == 0: + raise_qasm3_error( + message=f"Invalid sizeof usage, variable {var_name} is not an array.", + err_type=ValidationError, + span=expression.span, + ) + + if index is None: + # get the first dimension of the array + return dimensions[0] + + index = cls.evaluate_expression(index, const_expr, reqd_type=Qasm3IntType) + assert index is not None and isinstance(index, int) + if index < 0 or index >= len(dimensions): + raise_qasm3_error( + f"Index {index} out of bounds for array {var_name} with " + f"{len(dimensions)} dimensions", + ValidationError, + expression.span, + ) + + return dimensions[index] + + if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)): + if reqd_type: + if reqd_type == BoolType and isinstance(expression, BooleanLiteral): + return expression.value + if reqd_type == Qasm3IntType and isinstance(expression, IntegerLiteral): + return expression.value + if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral): + return expression.value + raise_qasm3_error( + f"Invalid value {expression.value} with type {type(expression)} " + f"for required type {reqd_type}", + ValidationError, + expression.span, + ) + return expression.value + + if isinstance(expression, UnaryExpression): + operand = cls.evaluate_expression(expression.expression, const_expr, reqd_type) + if expression.op.name == "~" and not isinstance(operand, int): + raise_qasm3_error( + f"Unsupported expression type {type(operand)} in ~ operation", + ValidationError, + expression.span, + ) + return qasm3_expression_op_map( + "UMINUS" if expression.op.name == "-" else expression.op.name, operand + ) + if isinstance(expression, BinaryExpression): + lhs = cls.evaluate_expression(expression.lhs, const_expr, reqd_type) + rhs = cls.evaluate_expression(expression.rhs, const_expr, reqd_type) + return qasm3_expression_op_map(expression.op.name, lhs, rhs) + + if isinstance(expression, FunctionCall): + # function will not return a reqd / const type + # Reference : https://openqasm.com/language/types.html#compile-time-constants + # para : 5 + return cls.visitor_obj._visit_function_call(expression) # type: ignore[union-attr] + + raise_qasm3_error( + f"Unsupported expression type {type(expression)}", ValidationError, expression.span + ) diff --git a/pyqasm/linalg.py b/pyqasm/linalg.py new file mode 100644 index 0000000..c0de18c --- /dev/null +++ b/pyqasm/linalg.py @@ -0,0 +1,302 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=too-many-locals + +""" +Module for linear algebra functions necessary for gate decomposition. + +""" + +import cmath +import functools +import math + +import numpy as np + +MAGIC = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) + +MAGIC_CONJ_T = np.conj(MAGIC.T) + +KAK_MAGIC = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) + +KAK_MAGIC_DAG = np.conjugate(np.transpose(KAK_MAGIC)) + +KAK_GAMMA = np.array([[1, 1, 1, 1], [1, 1, -1, -1], [-1, 1, -1, 1], [1, -1, -1, 1]]) * 0.25 + + +def _helper_svd(mat): + """ + Helper function to perform SVD on a matrix. + """ + if mat.size == 0: + return np.zeros((0, 0), dtype=mat.dtype), np.array([]), np.zeros((0, 0), dtype=mat.dtype) + return np.linalg.svd(mat) + + +def _merge_dtypes(dtype1, dtype2): + return (np.zeros(0, dtype1) + np.zeros(0, dtype2)).dtype + + +def _block_diag(*blocks): + """ + Helper function to perform block diagonalization. + """ + n = sum(b.shape[0] for b in blocks) + dtype = functools.reduce(_merge_dtypes, (b.dtype for b in blocks)) + + result = np.zeros((n, n), dtype=dtype) + i = 0 + for b in blocks: + j = i + b.shape[0] + result[i:j, i:j] = b + i = j + + return result + + +def _orthogonal_diagonalize(symmetric_matrix, diagonal_matrix): + """ + Find orthogonal matrix that diagonalize mat1 and mat2. + """ + + def similar_singular(i, j): + return np.allclose(diagonal_matrix[i, i], diagonal_matrix[j, j]) + + ranges = [] + start = 0 + while start < diagonal_matrix.shape[0]: + past = start + 1 + while past < diagonal_matrix.shape[0] and similar_singular(start, past): + past += 1 + ranges.append((start, past)) + start = past + + p = np.zeros(symmetric_matrix.shape, dtype=np.float64) + for start, end in ranges: + block = symmetric_matrix[start:end, start:end] + _, res = np.linalg.eigh(block) + p[start:end, start:end] = res + + return p + + +def orthogonal_bidiagonalize(mat1, mat2): + """ + Find orthogonal matrices that diagonalize mat1 and mat2. + """ + atol = 1e-9 + base_left, base_diag, base_right = _helper_svd(mat1) + base_diag = np.diag(base_diag) + + dim = base_diag.shape[0] + rank = dim + while rank > 0 and np.all(np.less_equal(np.abs(base_diag[rank - 1, rank - 1]), atol)): + rank -= 1 + base_diag = base_diag[:rank, :rank] + + semi_corrected = np.dot(np.dot(base_left.T, np.real(mat2)), base_right.T) + + overlap = semi_corrected[:rank, :rank] + overlap_adjust = _orthogonal_diagonalize(overlap, base_diag) + + extra = semi_corrected[rank:, rank:] + extra_left_adjust, _, extra_right_adjust = _helper_svd(extra) + + left_adjust = _block_diag(overlap_adjust, extra_left_adjust) + right_adjust = _block_diag(overlap_adjust.T, extra_right_adjust) + left = np.dot(left_adjust.T, base_left.T) + right = np.dot(base_right.T, right_adjust.T) + return left, right + + +def _kronecker_fator(mat): + """ + Split U = kron(A, B) to A and B. + """ + a, b = max(((i, j) for i in range(4) for j in range(4)), key=lambda t: abs(mat[t])) + + f1 = np.zeros((2, 2), dtype=mat.dtype) + f2 = np.zeros((2, 2), dtype=mat.dtype) + for i in range(2): + for j in range(2): + f1[(a >> 1) ^ i, (b >> 1) ^ j] = mat[a ^ (i << 1), b ^ (j << 1)] + f2[(a & 1) ^ i, (b & 1) ^ j] = mat[a ^ i, b ^ j] + + with np.errstate(divide="ignore", invalid="ignore"): + f1 /= np.sqrt(np.linalg.det(f1)) or 1 + f2 /= np.sqrt(np.linalg.det(f2)) or 1 + + g = mat[a, b] / (f1[a >> 1, b >> 1] * f2[a & 1, b & 1]) + if np.real(g) < 0: + f1 *= -1 + g *= -1 + + return g, f1, f2 + + +def _so4_to_su2(mat): + """ + Decompose SO(4) matrix to SU(2) matrices. + """ + ab = np.dot(np.dot(MAGIC, mat), MAGIC_CONJ_T) + _, a, b = _kronecker_fator(ab) + + return a, b + + +def _kak_canonicalize_vector(x, y, z): + """ + Canonicalize vector for KAK decomposition. + """ + phase = [complex(1)] + left = [np.eye(2)] * 2 + right = [np.eye(2)] * 2 + v = [x, y, z] + + flippers = [ + np.array([[0, 1], [1, 0]]) * 1j, + np.array([[0, -1j], [1j, 0]]) * 1j, + np.array([[1, 0], [0, -1]]) * 1j, + ] + + swappers = [ + np.array([[1, -1j], [1j, -1]]) * 1j * np.sqrt(0.5), + np.array([[1, 1], [1, -1]]) * 1j * np.sqrt(0.5), + np.array([[0, 1 - 1j], [1 + 1j, 0]]) * 1j * np.sqrt(0.5), + ] + + def shift(k, step): + v[k] += step * np.pi / 2 + phase[0] *= 1j**step + right[0] = np.dot(flippers[k] ** (step % 4), right[0]) + right[1] = np.dot(flippers[k] ** (step % 4), right[1]) + + def negate(k1, k2): + v[k1] *= -1 + v[k2] *= -1 + phase[0] *= -1 + s = flippers[3 - k1 - k2] + left[1] = np.dot(left[1], s) + right[1] = np.dot(s, right[1]) + + def swap(k1, k2): + v[k1], v[k2] = v[k2], v[k1] + s = swappers[3 - k1 - k2] + left[0] = np.dot(left[0], s) + left[1] = np.dot(left[1], s) + right[0] = np.dot(s, right[0]) + right[1] = np.dot(s, right[1]) + + def canonical_shift(k): + while v[k] <= -np.pi / 4: + shift(k, +1) + while v[k] > np.pi / 4: + shift(k, -1) + + def sort(): + if abs(v[0]) < abs(v[1]): + swap(0, 1) + if abs(v[1]) < abs(v[2]): + swap(1, 2) + if abs(v[0]) < abs(v[1]): + swap(0, 1) + + canonical_shift(0) + canonical_shift(1) + canonical_shift(2) + sort() + + if v[0] < 0: + negate(0, 2) + if v[1] < 0: + negate(1, 2) + canonical_shift(2) + + atol = 1e-9 + if v[0] > np.pi / 4 - atol and v[2] < 0: + shift(0, -1) + negate(0, 2) + + return { + "single_qubit_operations_after": (left[1], left[0]), + "single_qubit_operations_before": (right[1], right[0]), + } + + +def _deconstruct_matrix_to_angles(mat): + """ + Decompose matrix into angles. + """ + + def _phase_matrix(angle): + return np.diag([1, np.exp(1j * angle)]) + + def _rotation_matrix(angle): + c, s = np.cos(angle), np.sin(angle) + return np.array([[c, -s], [s, c]]) + + right_phase = cmath.phase(mat[0, 1] * np.conj(mat[0, 0])) + np.pi + mat = np.dot(mat, _phase_matrix(-right_phase)) + + bottom_phase = cmath.phase(mat[1, 0] * np.conj(mat[0, 0])) + mat = np.dot(_phase_matrix(-bottom_phase), mat) + + rotation = math.atan2(abs(mat[1, 0]), abs(mat[0, 0])) + mat = np.dot(_rotation_matrix(-rotation), mat) + + diagonal_phase = cmath.phase(mat[1, 1] * np.conj(mat[0, 0])) + + return right_phase + diagonal_phase, rotation * 2, bottom_phase + + +def so_bidiagonalize(mat): + """ + Find special orthogonal L and R so that L @ mat @ R is diagonal. + """ + left, right = orthogonal_bidiagonalize(np.real(mat), np.imag(mat)) + with np.errstate(divide="ignore", invalid="ignore"): + if np.linalg.det(left) < 0: + left[0, :] *= -1 + if np.linalg.det(right) < 0: + right[:, 0] *= -1 + + diag = np.dot(np.dot(left, mat), right) + + return left, np.diag(diag), right + + +def kak_decomposition_angles(mat): + """ + Decompose matrix into KAK decomposition, return all angles. + """ + left, d, right = so_bidiagonalize(KAK_MAGIC_DAG @ mat @ KAK_MAGIC) + + a1, a0 = _so4_to_su2(left.T) + b1, b0 = _so4_to_su2(right.T) + _, x, y, z = (KAK_GAMMA @ np.angle(d).reshape(-1, 1)).flatten() + + inner_cannon = _kak_canonicalize_vector(x, y, z) + b1 = np.dot(inner_cannon["single_qubit_operations_before"][0], b1) + b0 = np.dot(inner_cannon["single_qubit_operations_before"][1], b0) + a1 = np.dot(a1, inner_cannon["single_qubit_operations_after"][0]) + a0 = np.dot(a0, inner_cannon["single_qubit_operations_after"][1]) + + pre_phase00, rotation00, post_phase00 = _deconstruct_matrix_to_angles(b1) + pre_phase01, rotation01, post_phase01 = _deconstruct_matrix_to_angles(b0) + pre_phase10, rotation10, post_phase10 = _deconstruct_matrix_to_angles(a1) + pre_phase11, rotation11, post_phase11 = _deconstruct_matrix_to_angles(a0) + + return [ + [rotation00, post_phase00, pre_phase00], + [rotation01, post_phase01, pre_phase01], + [rotation10, post_phase10, pre_phase10], + [rotation11, post_phase11, pre_phase11], + ] diff --git a/pyqasm/maps.py b/pyqasm/maps.py new file mode 100644 index 0000000..84421a9 --- /dev/null +++ b/pyqasm/maps.py @@ -0,0 +1,702 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the qBraid-SDK +# +# The qBraid-SDK 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 the qBraid-SDK, as per Section 15 of the GPL v3. + +""" +Module mapping supported QASM gates/operations to pyqir functions. + +""" +from typing import Callable, Union + +import numpy as np + +# import pyqir +from openqasm3.ast import ( + AngleType, + BitType, + BoolType, + ClassicalDeclaration, + ComplexType, + FloatType, + IntType, + QuantumGateDefinition, + QubitDeclaration, + SubroutineDefinition, + UintType, +) + +from .elements import InversionOp +from .exceptions import ValidationError +from .linalg import kak_decomposition_angles + +# Define the type for the operator functions +OperatorFunction = Union[ + Callable[[Union[int, float, bool]], Union[int, float, bool]], + Callable[[Union[int, float, bool], Union[int, float, bool]], Union[int, float, bool]], +] + +OPERATOR_MAP: dict[str, OperatorFunction] = { + "+": lambda x, y: x + y, + "-": lambda x, y: x - y, + "*": lambda x, y: x * y, + "/": lambda x, y: x / y, + "%": lambda x, y: x % y, + "==": lambda x, y: x == y, + "!=": lambda x, y: x != y, + "<": lambda x, y: x < y, + ">": lambda x, y: x > y, + "<=": lambda x, y: x <= y, + ">=": lambda x, y: x >= y, + "&&": lambda x, y: x and y, + "||": lambda x, y: x or y, + "^": lambda x, y: x ^ y, + "&": lambda x, y: x & y, + "|": lambda x, y: x | y, + "<<": lambda x, y: x << y, + ">>": lambda x, y: x >> y, + "~": lambda x: ~x, + "!": lambda x: not x, + "UMINUS": lambda x: -x, +} + + +def qasm3_expression_op_map(op_name: str, *args) -> Union[float, int, bool]: + """ + Return the result of applying the given operator to the given operands. + + Args: + op_name (str): The operator name. + *args: The operands of type Union[int, float, bool] + 1. For unary operators, a single operand (e.g., ~3) + 2. For binary operators, two operands (e.g., 3 + 2) + + Returns: + (Union[float, int, bool]): The result of applying the operator to the operands. + """ + try: + operator = OPERATOR_MAP[op_name] + return operator(*args) + except KeyError as exc: + raise ValidationError(f"Unsupported / undeclared QASM operator: {op_name}") from exc + + +def id_gate(builder, qubits): + pyqir._native.x(builder, qubits) + pyqir._native.x(builder, qubits) + + +def u3_gate( + builder, + theta: Union[int, float], + phi: Union[int, float], + lam: Union[int, float], + qubits, +): + """ + Implements the U3 gate using the following decomposition: + https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.UGate + https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.PhaseGate + + Args: + builder (pyqir._native.QirBuilder): The QIR builder. + theta (Union[int, float]): The theta angle. + phi (Union[int, float]): The phi angle. + lam (Union[int, float]): The lambda angle. + qubits: The qubits on which the gate is applied. + + Returns: + None + """ + pyqir._native.rz(builder, lam, qubits) + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) + pyqir._native.rz(builder, theta + CONSTANTS_MAP["pi"], qubits) + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) + pyqir._native.rz(builder, phi + CONSTANTS_MAP["pi"], qubits) + # global phase - e^(i*(phi+lambda)/2) is missing in the above implementation + + +def u3_inv_gate( + builder, + theta: Union[int, float], + phi: Union[int, float], + lam: Union[int, float], + qubits, +): + """ + Implements the inverse of the U3 gate using the decomposition present in + the u3_gate function. + """ + pyqir._native.rz(builder, -1.0 * (phi + CONSTANTS_MAP["pi"]), qubits) + pyqir._native.rx(builder, -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) + pyqir._native.rz(builder, -1.0 * (theta + CONSTANTS_MAP["pi"]), qubits) + pyqir._native.rx(builder, -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) + pyqir._native.rz(builder, -1.0 * lam, qubits) + + +def u2_gate(builder, phi, lam, qubits): + """ + Implements the U2 gate using the following decomposition: + https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.U2Gate + """ + u3_gate(builder, CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) + + +def u2_inv_gate(builder, phi, lam, qubits): + """ + Implements the inverse of the U2 gate using the decomposition present in + the u2_gate function. + """ + u3_inv_gate(builder, CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) + + +def sx_gate(builder, qubits): + """ + Implements the Sqrt(X) gate as a decomposition of other gates. + """ + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) + + +def sxdg_gate(builder, qubits): + """ + Implements the conjugate transpose of the Sqrt(X) gate as a decomposition of other gates. + """ + pyqir._native.rx(builder, -CONSTANTS_MAP["pi"] / 2, qubits) + + +def cv_gate(builder, qubit0, qubit1): + """ + Implements the controlled V gate as a decomposition of other gates. + """ + pyqir._native.x(builder, qubit0) + pyqir._native.h(builder, qubit1) + pyqir._native.cx(builder, qubit0, qubit1) + pyqir._native.h(builder, qubit1) + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 4, qubit1) + pyqir._native.h(builder, qubit1) + pyqir._native.cx(builder, qubit0, qubit1) + pyqir._native.t_adj(builder, qubit0) + pyqir._native.h(builder, qubit1) + pyqir._native.x(builder, qubit0) + pyqir._native.rz(builder, -CONSTANTS_MAP["pi"] / 4, qubit1) + + +def cy_gate(builder, qubit0, qubit1): + """ + Implements the CY gate as a decomposition of other gates. + """ + pyqir._native.s_adj(builder, qubit1) + pyqir._native.cx(builder, qubit0, qubit1) + pyqir._native.s(builder, qubit1) + + +def xx_gate(builder, theta, qubit0, qubit1): + """ + Implements the XX gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1] + pyqir._native.h(builder, qubits[0]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.cz(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.rx(builder, theta, qubits[0]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.cz(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[0]) + pyqir._native.h(builder, qubits[1]) + + +def xy_gate(builder, theta, qubit0, qubit1): + """ + Implements the XY gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1] + pyqir._native.rx(builder, -theta / 2, qubits[0]) + pyqir._native.ry(builder, theta / 2, qubits[1]) + pyqir._native.ry(builder, theta / 2, qubits[0]) + pyqir._native.rx(builder, theta / 2, qubits[0]) + pyqir._native.cx(builder, qubits[1], qubits[0]) + pyqir._native.ry(builder, -theta / 2, qubits[0]) + pyqir._native.ry(builder, -theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[1], qubits[0]) + pyqir._native.rx(builder, theta / 2, qubits[0]) + pyqir._native.ry(builder, -theta / 2, qubits[1]) + pyqir._native.ry(builder, theta / 2, qubits[1]) + pyqir._native.rx(builder, -theta / 2, qubits[0]) + + +def yy_gate(builder, theta, qubit0, qubit1): + """ + Implements the YY gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1] + pyqir._native.rx(builder, theta / 2, qubits[0]) + pyqir._native.rx(builder, theta / 2, qubits[1]) + pyqir._native.cz(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.rx(builder, theta, qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.cz(builder, qubits[0], qubits[1]) + pyqir._native.rx(builder, -theta / 2, qubits[0]) + pyqir._native.rx(builder, -theta / 2, qubits[1]) + + +def zz_gate(builder, theta, qubit0, qubit1): + """ + Implements the ZZ gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1] + pyqir._native.cz(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.rz(builder, theta, qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.cz(builder, qubits[0], qubits[1]) + + +def phaseshift_gate(builder, theta, qubit): + """ + Implements the phase shift gate as a decomposition of other gates. + """ + pyqir._native.h(builder, qubit) + pyqir._native.rx(builder, theta, qubit) + pyqir._native.h(builder, qubit) + + +def cswap_gate(builder, qubit0, qubit1, qubit2): + """ + Implements the CSWAP gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1, qubit2] + pyqir._native.cx(builder, qubits[2], qubits[1]) + pyqir._native.h(builder, qubits[2]) + pyqir._native.cx(builder, qubits[1], qubits[2]) + pyqir._native.t_adj(builder, qubits[2]) + pyqir._native.cx(builder, qubits[0], qubits[2]) + pyqir._native.t(builder, qubits[2]) + pyqir._native.cx(builder, qubits[1], qubits[2]) + pyqir._native.t(builder, qubits[1]) + pyqir._native.t_adj(builder, qubits[2]) + pyqir._native.cx(builder, qubits[0], qubits[2]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.t(builder, qubits[2]) + pyqir._native.t(builder, qubits[0]) + pyqir._native.t_adj(builder, qubits[1]) + pyqir._native.h(builder, qubits[2]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.cx(builder, qubits[2], qubits[1]) + + +def pswap_gate(builder, theta, qubit0, qubit1): + """ + Implements the PSWAP gate as a decomposition of other gates. + + """ + qubits = [qubit0, qubit1] + pyqir._native.swap(builder, qubits[0], qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + u3_gate(builder, 0, 0, theta, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + + +def cphaseshift_gate(builder, theta, qubit0, qubit1): + """ + Implements the controlled phase shift gate as a decomposition of other gates. + """ + qubits = [qubit0, qubit1] + pyqir._native.h(builder, qubits[0]) + pyqir._native.rx(builder, theta / 2, qubits[0]) + pyqir._native.h(builder, qubits[0]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.rx(builder, -theta / 2, qubits[0]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.h(builder, qubits[1]) + pyqir._native.rx(builder, theta / 2, qubits[1]) + pyqir._native.h(builder, qubits[1]) + + +def cphaseshift00_gate(builder, theta, qubit0, qubit1): + """ + Implements the controlled phase shift 00 gate as a decomposition of other gates. + + """ + qubits = [qubit0, qubit1] + pyqir._native.x(builder, qubits[0]) + pyqir._native.x(builder, qubits[1]) + u3_gate(builder, 0, 0, theta / 2, qubits[0]) + u3_gate(builder, 0, 0, theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + u3_gate(builder, 0, 0, -theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.x(builder, qubits[0]) + pyqir._native.x(builder, qubits[1]) + + +def cphaseshift01_gate(builder, theta, qubit0, qubit1): + """ + Implements the controlled phase shift 01 gate as a decomposition of other gates. + + """ + qubits = [qubit0, qubit1] + pyqir._native.x(builder, qubits[0]) + u3_gate(builder, 0, 0, theta / 2, qubits[1]) + u3_gate(builder, 0, 0, theta / 2, qubits[0]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + u3_gate(builder, 0, 0, -theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.x(builder, qubits[0]) + + +def cphaseshift10_gate(builder, theta, qubit0, qubit1): + """ + Implements the controlled phase shift 10 gate as a decomposition of other gates. + + """ + qubits = [qubit0, qubit1] + u3_gate(builder, 0, 0, theta / 2, qubits[0]) + pyqir._native.x(builder, qubits[1]) + u3_gate(builder, 0, 0, theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + u3_gate(builder, 0, 0, -theta / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.x(builder, qubits[1]) + + +def gpi_gate(builder, phi, qubit): + """ + Implements the gpi gate as a decomposition of other gates. + """ + theta_0 = CONSTANTS_MAP["pi"] + phi_0 = phi + lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] + u3_gate(builder, theta_0, phi_0, lambda_0, qubit) + + +def gpi2_gate(builder, phi, qubit): + """ + Implements the gpi2 gate as a decomposition of other gates. + """ + theta_0 = CONSTANTS_MAP["pi"] / 2 + phi_0 = phi + 3 * CONSTANTS_MAP["pi"] / 2 + lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] / 2 + u3_gate(builder, theta_0, phi_0, lambda_0, qubit) + + +# pylint: disable-next=too-many-arguments +def ms_gate(builder, phi0, phi1, theta, qubit0, qubit1): + """ + Implements the Molmer Sorenson gate as a decomposition of other gates. + """ + mat = np.array( + [ + [ + np.cos(np.pi * theta), + 0, + 0, + -1j * np.exp(-1j * 2 * np.pi * (phi0 + phi1)) * np.sin(np.pi * theta), + ], + [ + 0, + np.cos(np.pi * theta), + -1j * np.exp(-1j * 2 * np.pi * (phi0 - phi1)) * np.sin(np.pi * theta), + 0, + ], + [ + 0, + -1j * np.exp(1j * 2 * np.pi * (phi0 - phi1)) * np.sin(np.pi * theta), + np.cos(np.pi * theta), + 0, + ], + [ + -1j * np.exp(1j * 2 * np.pi * (phi0 + phi1)) * np.sin(np.pi * theta), + 0, + 0, + np.cos(np.pi * theta), + ], + ] + ) + angles = kak_decomposition_angles(mat) + qubits = [qubit0, qubit1] + + u3_gate(builder, angles[0][0], angles[0][1], angles[0][2], qubits[0]) + u3_gate(builder, angles[1][0], angles[1][1], angles[1][2], qubits[1]) + sx_gate(builder, qubits[0]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.rx(builder, ((1 / 2) - 2 * theta) * CONSTANTS_MAP["pi"], qubits[0]) + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits[1]) + pyqir._native.cx(builder, qubits[1], qubits[0]) + sxdg_gate(builder, qubits[1]) + pyqir._native.s(builder, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + u3_gate(builder, angles[2][0], angles[2][1], angles[2][2], qubits[0]) + u3_gate(builder, angles[3][0], angles[3][1], angles[3][2], qubits[1]) + + +def ecr_gate(builder, qubit0, qubit1): + """ + Implements the ECR gate as a decomposition of other gates. + + """ + qubits = [qubit0, qubit1] + pyqir._native.s(builder, qubits[0]) + pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits[1]) + pyqir._native.cx(builder, qubits[0], qubits[1]) + pyqir._native.x(builder, qubits[0]) + + +def prx_gate(builder, theta, phi, qubit): + """ + Implements the PRX gate as a decomposition of other gates. + """ + theta_0 = theta + phi_0 = CONSTANTS_MAP["pi"] / 2 - phi + lambda_0 = -phi_0 + u3_gate(builder, theta_0, phi_0, lambda_0, qubit) + + +# PYQIR_ONE_QUBIT_OP_MAP = { +# "i": id_gate, +# "id": id_gate, +# "h": pyqir._native.h, +# "x": pyqir._native.x, +# "y": pyqir._native.y, +# "z": pyqir._native.z, +# "s": pyqir._native.s, +# "t": pyqir._native.t, +# "sdg": pyqir._native.s_adj, +# "si": pyqir._native.s_adj, +# "tdg": pyqir._native.t_adj, +# "ti": pyqir._native.t_adj, +# "v": sx_gate, +# "sx": sx_gate, +# "vi": sxdg_gate, +# "sxdg": sxdg_gate, +# } + +# PYQIR_ONE_QUBIT_ROTATION_MAP = { +# "rx": pyqir._native.rx, +# "ry": pyqir._native.ry, +# "rz": pyqir._native.rz, +# "u": u3_gate, +# "U": u3_gate, +# "u3": u3_gate, +# "U3": u3_gate, +# "U2": u2_gate, +# "u2": u2_gate, +# "prx": prx_gate, +# "phaseshift": phaseshift_gate, +# "p": phaseshift_gate, +# "gpi": gpi_gate, +# "gpi2": gpi2_gate, +# } + +# PYQIR_TWO_QUBIT_OP_MAP = { +# "cx": pyqir._native.cx, +# "CX": pyqir._native.cx, +# "cnot": pyqir._native.cx, +# "cz": pyqir._native.cz, +# "swap": pyqir._native.swap, +# "cv": cv_gate, +# "cy": cy_gate, +# "xx": xx_gate, +# "xy": xy_gate, +# "yy": yy_gate, +# "zz": zz_gate, +# "pswap": pswap_gate, +# "cp": cphaseshift_gate, +# "cphaseshift": cphaseshift_gate, +# "cp00": cphaseshift00_gate, +# "cphaseshift00": cphaseshift00_gate, +# "cp01": cphaseshift01_gate, +# "cphaseshift01": cphaseshift01_gate, +# "cp10": cphaseshift10_gate, +# "cphaseshift10": cphaseshift10_gate, +# "ecr": ecr_gate, +# "ms": ms_gate, +# } + +# PYQIR_THREE_QUBIT_OP_MAP = { +# "ccx": pyqir._native.ccx, +# "ccnot": pyqir._native.ccx, +# "cswap": cswap_gate, +# } + + +def map_qasm_op_to_pyqir_callable(op_name: str): + """ + Map a QASM operation to a PyQIR callable. + + Args: + op_name (str): The QASM operation name. + + Returns: + tuple: A tuple containing the PyQIR callable and the number of qubits the operation acts on. + """ + try: + return PYQIR_ONE_QUBIT_OP_MAP[op_name], 1 + except KeyError: + pass + try: + return PYQIR_ONE_QUBIT_ROTATION_MAP[op_name], 1 + except KeyError: + pass + try: + return PYQIR_TWO_QUBIT_OP_MAP[op_name], 2 + except KeyError: + pass + try: + return PYQIR_THREE_QUBIT_OP_MAP[op_name], 3 + except KeyError as exc: + raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") from exc + + +PYQIR_SELF_INVERTING_ONE_QUBIT_OP_SET = {"id", "h", "x", "y", "z"} +PYQIR_ST_GATE_INV_MAP = { + "s": "sdg", + "t": "tdg", + "sdg": "s", + "tdg": "t", +} +PYQIR_ROTATION_INVERSION_ONE_QUBIT_OP_MAP = {"rx", "ry", "rz"} +PYQIR_U_INV_ROTATION_MAP = { + "U": u3_inv_gate, + "u3": u3_inv_gate, + "U3": u3_inv_gate, + "U2": u2_inv_gate, + "u2": u2_inv_gate, +} + + +def map_qasm_inv_op_to_pyqir_callable(op_name: str): + """ + Map a QASM operation to a PyQIR callable. + + Args: + op_name (str): The QASM operation name. + + Returns: + tuple: A tuple containing the PyQIR callable, the number of qubits the operation acts on, + and what is to be done with the basic gate which we are trying to invert. + """ + if op_name in PYQIR_SELF_INVERTING_ONE_QUBIT_OP_SET: + return PYQIR_ONE_QUBIT_OP_MAP[op_name], 1, InversionOp.NO_OP + if op_name in PYQIR_ST_GATE_INV_MAP: + inv_gate_name = PYQIR_ST_GATE_INV_MAP[op_name] + return PYQIR_ONE_QUBIT_OP_MAP[inv_gate_name], 1, InversionOp.NO_OP + if op_name in PYQIR_TWO_QUBIT_OP_MAP: + return PYQIR_TWO_QUBIT_OP_MAP[op_name], 2, InversionOp.NO_OP + if op_name in PYQIR_THREE_QUBIT_OP_MAP: + return PYQIR_THREE_QUBIT_OP_MAP[op_name], 3, InversionOp.NO_OP + if op_name in PYQIR_U_INV_ROTATION_MAP: + # Special handling for U gate as it is composed of multiple + # basic gates and we need to invert each of them + return PYQIR_U_INV_ROTATION_MAP[op_name], 1, InversionOp.NO_OP + if op_name in PYQIR_ROTATION_INVERSION_ONE_QUBIT_OP_MAP: + return ( + PYQIR_ONE_QUBIT_ROTATION_MAP[op_name], + 1, + InversionOp.INVERT_ROTATION, + ) + raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") + + +# 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. + + Args: + openqasm_type : The type of the variable. + type_of_rhs (type): The type to match. + + Returns: + The casted variable type. + + Raises: + ValidationError: If the cast is not possible. + """ + type_of_rhs = type(rhs_value) + + if type_of_rhs not in VARIABLE_TYPE_CAST_MAP[openqasm_type]: + raise ValidationError( + f"Cannot cast {type_of_rhs} to {openqasm_type}. " + f"Invalid assignment of type {type_of_rhs} to variable {var_name} " + f"of type {openqasm_type}" + ) + + if openqasm_type == BoolType: + return bool(rhs_value) + if openqasm_type == IntType: + return int(rhs_value) + if openqasm_type == UintType: + return int(rhs_value) % (2**base_size) + if openqasm_type == FloatType: + return float(rhs_value) + # not sure if we wanna hande array bit assignments too. + # For now, we only cater to single bit assignment. + if openqasm_type == BitType: + return bool(rhs_value) + if openqasm_type == AngleType: + return rhs_value # not sure + + +# IEEE 754 Standard for floats +# https://openqasm.com/language/types.html#floating-point-numbers +LIMITS_MAP = {"float_32": 1.70141183 * (10**38), "float_64": 10**308} + +CONSTANTS_MAP = { + "π": 3.141592653589793, + "pi": 3.141592653589793, + "ℇ": 2.718281828459045, + "euler": 2.718281828459045, + "τ": 6.283185307179586, + "tau": 6.283185307179586, +} + +VARIABLE_TYPE_MAP = { + BitType: bool, + IntType: int, + UintType: int, + BoolType: bool, + FloatType: float, + ComplexType: complex, + # AngleType: None, # not sure +} + +# Reference: https://openqasm.com/language/types.html#allowed-casts +VARIABLE_TYPE_CAST_MAP = { + BoolType: (int, float, bool, np.int64, np.float64, np.bool_), + IntType: (bool, int, float, np.int64, np.float64, np.bool_), + BitType: (bool, int, np.int64, np.bool_), + UintType: (bool, int, float, np.int64, np.uint64, np.float64, np.bool_), + FloatType: (bool, int, float, np.int64, np.float64, np.bool_), + AngleType: (float, np.float64), +} + +ARRAY_TYPE_MAP = { + BitType: np.bool_, + IntType: np.int64, + UintType: np.uint64, + FloatType: np.float64, + ComplexType: np.complex128, + BoolType: np.bool_, + AngleType: np.float64, +} + + +# Reference : https://openqasm.com/language/types.html#arrays +MAX_ARRAY_DIMENSIONS = 7 + +# Reference : https://openqasm.com/language/classical.html#the-switch-statement +# Paragraph 14 +SWITCH_BLACKLIST_STMTS = { + QubitDeclaration, + ClassicalDeclaration, + SubroutineDefinition, + QuantumGateDefinition, +} diff --git a/pyqasm/subroutines.py b/pyqasm/subroutines.py new file mode 100644 index 0000000..b8145b7 --- /dev/null +++ b/pyqasm/subroutines.py @@ -0,0 +1,388 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=too-many-arguments,too-many-locals,too-many-branches + +""" +Module containing the class for validating QASM3 subroutines. + +""" +from typing import Optional, Union + +from openqasm3.ast import ( + AccessControl, + ArrayReferenceType, + Identifier, + IndexExpression, + IntType, + QubitDeclaration, +) + +from .analyzer import Qasm3Analyzer +from .elements import Variable +from .exceptions import raise_qasm3_error +from .expressions import Qasm3ExprEvaluator +from .transformer import Qasm3Transformer +from .validator import Qasm3Validator + + +class Qasm3SubroutineProcessor: + """Class for processing QASM3 subroutines.""" + + visitor_obj = None + + @classmethod + def set_visitor_obj(cls, visitor_obj) -> None: + """Set the visitor object for the class. + + Args: + visitor_obj (BasicQasmVisitor): The visitor object to set. + """ + cls.visitor_obj = visitor_obj + + @staticmethod + def get_fn_actual_arg_name(actual_arg: Union[Identifier, IndexExpression]) -> Optional[str]: + """Get the name of the actual argument passed to a function. + + Args: + actual_arg (Union[Identifier, IndexExpression]): The actual argument passed to the + function. + + Returns: + Optional[str]: The name of the actual argument. + """ + actual_arg_name = None + if isinstance(actual_arg, Identifier): + actual_arg_name = actual_arg.name + elif isinstance(actual_arg, IndexExpression): + if isinstance(actual_arg.collection, Identifier): + actual_arg_name = actual_arg.collection.name + else: + actual_arg_name = ( + actual_arg.collection.collection.name # type: ignore[attr-defined] + ) + return actual_arg_name + + @classmethod + def process_classical_arg(cls, formal_arg, actual_arg, fn_name, span): + """Process the classical argument for a function call. + + Args: + formal_arg (FormalArgument): The formal argument of the function. + actual_arg (ActualArgument): The actual argument passed to the function. + fn_name (str): The name of the function. + span (Span): The span of the function call. + + Returns: + Variable: The variable object for the formal argument. + """ + actual_arg_name = Qasm3SubroutineProcessor.get_fn_actual_arg_name(actual_arg) + + if isinstance(formal_arg.type, ArrayReferenceType): + return cls._process_classical_arg_by_reference( + formal_arg, actual_arg, actual_arg_name, fn_name, span + ) + return cls._process_classical_arg_by_value( + formal_arg, actual_arg, actual_arg_name, fn_name, span + ) + + @classmethod + def _process_classical_arg_by_value( + cls, formal_arg, actual_arg, actual_arg_name, fn_name, span + ): + """ + Process the classical argument for a function call. + + Args: + formal_arg (FormalArgument): The formal argument of the function. + actual_arg (ActualArgument): The actual argument passed to the function. + actual_arg_name (str): The name of the actual argument. + fn_name (str): The name of the function. + span (Span): The span of the function call. + + Raises: + ValidationError: If the actual argument is a qubit register instead + of a classical argument. + ValidationError: If the actual argument is an undefined variable. + """ + # 1. variable mapping is equivalent to declaring the variable + # with the formal argument name and doing classical assignment + # in the scope of the function + if actual_arg_name: # actual arg is a variable not literal + if actual_arg_name in cls.visitor_obj._global_qreg_size_map: + raise_qasm3_error( + f"Expecting classical argument for '{formal_arg.name.name}'. " + f"Qubit register '{actual_arg_name}' found for function '{fn_name}'", + span=span, + ) + + # 2. as we have pushed the scope for fn, we need to check in parent + # scope for argument validation + if not cls.visitor_obj._check_in_scope(actual_arg_name): + raise_qasm3_error( + f"Undefined variable '{actual_arg_name}' used" + f" for function call '{fn_name}'", + span=span, + ) + actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg) + + # save this value to be updated later in scope + return Variable( + name=formal_arg.name.name, + base_type=formal_arg.type, + base_size=Qasm3ExprEvaluator.evaluate_expression(formal_arg.type.size), + dims=None, + value=actual_arg_value, + is_constant=False, + ) + + @classmethod + def _process_classical_arg_by_reference( + cls, formal_arg, actual_arg, actual_arg_name, fn_name, span + ): + """Process the classical args by reference in the QASM3 visitor. + Currently being used for array references only. + + Args: + formal_arg (Qasm3Expression): The formal argument of the function. + actual_arg (Qasm3Expression): The actual argument passed to the function. + actual_arg_name (str): The name of the actual argument. + fn_name (str): The name of the function. + span (Span): The span of the function call. + + Raises: + ValidationError: If the actual argument is - + - not an array. + - an undefined variable. + - a qubit register. + - a literal. + - having type mismatch with the formal argument. + """ + + formal_arg_base_size = Qasm3ExprEvaluator.evaluate_expression( + formal_arg.type.base_type.size + ) + array_expected_type_msg = ( + "Expecting type 'array[" + f"{formal_arg.type.base_type.__class__.__name__.lower().removesuffix('type')}" + f"[{formal_arg_base_size}],...]' for '{formal_arg.name.name}'" + f" in function '{fn_name}'. " + ) + + if actual_arg_name is None: + raise_qasm3_error( + array_expected_type_msg + + f"Literal {Qasm3ExprEvaluator.evaluate_expression(actual_arg)} " + + "found in function call", + span=span, + ) + + if actual_arg_name in cls.visitor_obj._global_qreg_size_map: + raise_qasm3_error( + array_expected_type_msg + + f"Qubit register '{actual_arg_name}' found for function call", + span=span, + ) + + # verify actual argument is defined in the parent scope of function call + if not cls.visitor_obj._check_in_scope(actual_arg_name): + raise_qasm3_error( + f"Undefined variable '{actual_arg_name}' used for function call '{fn_name}'", + span=span, + ) + + array_reference = cls.visitor_obj._get_from_visible_scope(actual_arg_name) + actual_type_string = Qasm3Transformer.get_type_string(array_reference) + + # ensure that actual argument is an array + if not array_reference.dims: + raise_qasm3_error( + array_expected_type_msg + + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", + span=span, + ) + + # The base types of the elements in array should match + actual_arg_type = array_reference.base_type + actual_arg_size = array_reference.base_size + + if formal_arg.type.base_type != actual_arg_type or formal_arg_base_size != actual_arg_size: + raise_qasm3_error( + array_expected_type_msg + + f"Variable '{actual_arg_name}' has type '{actual_type_string}'.", + span=span, + ) + + # The dimensions passed in the formal arg should be + # within limits of the actual argument + + # need to ensure that we have a positive integer as dimension + actual_dimensions = array_reference.dims + formal_dimensions_raw = formal_arg.type.dimensions + # 1. Either we will have #dim = <> + if not isinstance(formal_dimensions_raw, list): + num_formal_dimensions = Qasm3ExprEvaluator.evaluate_expression( + formal_dimensions_raw, reqd_type=IntType, const_expr=True + ) + # 2. or we will have a list of the dimensions in the formal arg + else: + num_formal_dimensions = len(formal_dimensions_raw) + + if num_formal_dimensions <= 0: + raise_qasm3_error( + f"Invalid number of dimensions {num_formal_dimensions}" + f" for '{formal_arg.name.name}' in function '{fn_name}'", + span=span, + ) + + if num_formal_dimensions > len(actual_dimensions): + raise_qasm3_error( + f"Dimension mismatch for '{formal_arg.name.name}' in function '{fn_name}'. " + f"Expected {num_formal_dimensions} dimensions but" + f" variable '{actual_arg_name}' has {len(actual_dimensions)}", + span=span, + ) + formal_dimensions = [] + + # we need to ensure that the dimensions are within the limits AND valid integers + if not isinstance(formal_dimensions_raw, list): + # the case when we have #dim identifier + formal_dimensions = actual_dimensions[:num_formal_dimensions] + else: + for idx, (formal_dim, actual_dim) in enumerate( + zip(formal_dimensions_raw, actual_dimensions) + ): + formal_dim = Qasm3ExprEvaluator.evaluate_expression( + formal_dim, reqd_type=IntType, const_expr=True + ) + if formal_dim <= 0: + raise_qasm3_error( + f"Invalid dimension size {formal_dim} for '{formal_arg.name.name}'" + f" in function '{fn_name}'", + span=span, + ) + if actual_dim < formal_dim: + raise_qasm3_error( + f"Dimension mismatch for '{formal_arg.name.name}'" + f" in function '{fn_name}'. Expected dimension {idx} with size" + f" >= {formal_dim} but got {actual_dim}", + span=span, + ) + formal_dimensions.append(formal_dim) + + readonly_arr = formal_arg.access == AccessControl.readonly + actual_array_view = array_reference.value + if isinstance(actual_arg, IndexExpression): + _, actual_indices = Qasm3Analyzer.analyze_index_expression(actual_arg) + actual_indices = Qasm3Analyzer.analyze_classical_indices( + actual_indices, array_reference, Qasm3ExprEvaluator + ) + actual_array_view = Qasm3Analyzer.find_array_element( + array_reference.value, actual_indices + ) + + return Variable( + name=formal_arg.name.name, + base_type=formal_arg.type.base_type, + base_size=formal_arg_base_size, + dims=formal_dimensions, + value=actual_array_view, # this is the VIEW of the actual array + readonly=readonly_arr, + ) + + @classmethod + def process_quantum_arg( + cls, + formal_arg, + actual_arg, + formal_qreg_size_map, + duplicate_qubit_map, + qubit_transform_map, + fn_name, + span, + ): + """ + Process a quantum argument in the QASM3 visitor. + + Args: + formal_arg (Qasm3Expression): The formal argument in the function signature. + actual_arg (Qasm3Expression): The actual argument passed to the function. + formal_qreg_size_map (dict): The map of formal quantum register sizes. + duplicate_qubit_map (dict): The map of duplicate qubit registers. + qubit_transform_map (dict): The map of qubit register transformations. + fn_name (str): The name of the function. + span (Span): The span of the function call. + + Returns: + list: The list of actual qubit ids. + + Raises: + ValidationError: If there is a mismatch in the quantum register size or + if the actual argument is not a qubit register. + + """ + actual_arg_name = Qasm3SubroutineProcessor.get_fn_actual_arg_name(actual_arg) + formal_reg_name = formal_arg.name.name + formal_qubit_size = Qasm3ExprEvaluator.evaluate_expression( + formal_arg.size, reqd_type=IntType, const_expr=True + ) + if formal_qubit_size is None: + formal_qubit_size = 1 + if formal_qubit_size <= 0: + raise_qasm3_error( + f"Invalid qubit size {formal_qubit_size} for variable '{formal_reg_name}'" + f" in function '{fn_name}'", + span=span, + ) + formal_qreg_size_map[formal_reg_name] = formal_qubit_size + + # we expect that actual arg is qubit type only + # note that we ONLY check in global scope as + # we always map the qubit arguments to the global scope + if actual_arg_name not in cls.visitor_obj._global_qreg_size_map: + raise_qasm3_error( + f"Expecting qubit argument for '{formal_reg_name}'. " + f"Qubit register '{actual_arg_name}' not found for function '{fn_name}'", + span=span, + ) + cls.visitor_obj._label_scope_level[cls.visitor_obj._curr_scope].add(formal_reg_name) + + actual_qids, actual_qubits_size = Qasm3Transformer.get_target_qubits( + actual_arg, cls.visitor_obj._global_qreg_size_map, actual_arg_name + ) + + if formal_qubit_size != actual_qubits_size: + raise_qasm3_error( + f"Qubit register size mismatch for function '{fn_name}'. " + f"Expected {formal_qubit_size} in variable '{formal_reg_name}' " + f"but got {actual_qubits_size}", + span=span, + ) + + if not Qasm3Validator.validate_unique_qubits( + duplicate_qubit_map, actual_arg_name, actual_qids + ): + raise_qasm3_error( + f"Duplicate qubit argument for register '{actual_arg_name}' " + f"in function call for '{fn_name}'", + span=span, + ) + + for idx, qid in enumerate(actual_qids): + qubit_transform_map[(formal_reg_name, idx)] = (actual_arg_name, qid) + + return Variable( + name=formal_reg_name, + base_type=QubitDeclaration, + base_size=formal_qubit_size, + dims=None, + value=None, + is_constant=False, + ) diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py new file mode 100644 index 0000000..605f1e4 --- /dev/null +++ b/pyqasm/transformer.py @@ -0,0 +1,352 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module with transformation functions for QASM3 visitor + +""" +from typing import Any, Union + +import numpy as np +from openqasm3.ast import ( + BinaryExpression, + BooleanLiteral, + DiscreteSet, + FloatLiteral, + Identifier, + IndexedIdentifier, + IndexExpression, + IntegerLiteral, + QuantumBarrier, + QuantumGate, + QuantumReset, + RangeDefinition, + UintType, + UnaryExpression, +) + +from .elements import Variable +from .exceptions import raise_qasm3_error +from .expressions import Qasm3ExprEvaluator +from .maps import VARIABLE_TYPE_MAP +from .validator import Qasm3Validator + +# mypy: disable-error-code="attr-defined, union-attr" + + +class Qasm3Transformer: + """Class with utility functions for transforming QASM3 elements""" + + visitor_obj = None + + @classmethod + def set_visitor_obj(cls, visitor_obj) -> None: + """Set the visitor object. + + Args: + visitor_obj: The visitor object. + """ + cls.visitor_obj = visitor_obj + + @staticmethod + def update_array_element( + multi_dim_arr: np.ndarray, indices: list[tuple[int, int, int]], value: Any + ) -> None: + """Update the value of an array at the specified indices. Single element only. + + Args: + multi_dim_arr (np.ndarray): The multi-dimensional array to update. + indices (list[tuple[int,int,int]]): The indices to update. + value (Any): The value to update. + + Returns: + None + """ + slicing = tuple( + slice(start, stop + 1, step) if start != stop else start + for start, stop, step in indices + ) + multi_dim_arr[slicing] = value + + @staticmethod + def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]: + """Extract the values from a discrete set. + + Args: + discrete_set (DiscreteSet): The discrete set to extract values from. + + Returns: + list[int]: The extracted values. + """ + values = [] + for value in discrete_set.values: + if not isinstance(value, IntegerLiteral): + raise_qasm3_error( + f"Unsupported discrete set value {value} in discrete set", + span=discrete_set.span, + ) + values.append(value.value) + return values + + @staticmethod + def get_qubits_from_range_definition( + range_def: RangeDefinition, qreg_size: int, is_qubit_reg: bool + ) -> list[int]: + """Get the qubits from a range definition. + Args: + range_def (RangeDefinition): The range definition to get qubits from. + qreg_size (int): The size of the register. + is_qubit_reg (bool): Whether the register is a qubit register. + Returns: + list[int]: The list of qubit identifiers. + """ + start_qid = ( + 0 + if range_def.start is None + else Qasm3ExprEvaluator.evaluate_expression(range_def.start) + ) + end_qid = ( + qreg_size + if range_def.end is None + else Qasm3ExprEvaluator.evaluate_expression(range_def.end) + ) + step = ( + 1 if range_def.step is None else Qasm3ExprEvaluator.evaluate_expression(range_def.step) + ) + Qasm3Validator.validate_register_index(start_qid, qreg_size, qubit=is_qubit_reg) + Qasm3Validator.validate_register_index(end_qid - 1, qreg_size, qubit=is_qubit_reg) + return list(range(start_qid, end_qid, step)) + + @staticmethod + def transform_gate_qubits( + gate_op: QuantumGate, qubit_map: dict[str, IndexedIdentifier] + ) -> None: + """Transform the qubits of a gate operation with a qubit map. + + Args: + gate_op (QuantumGate): The gate operation to transform. + qubit_map (dict[str, IndexedIdentifier]): The qubit map to use for transformation. + + Returns: + None + """ + for i, qubit in enumerate(gate_op.qubits): + if isinstance(qubit, IndexedIdentifier): + raise_qasm3_error( + f"Indexing '{qubit.name.name}' not supported in gate definition", + span=qubit.span, + ) + gate_qubit_name = qubit.name + assert isinstance(gate_qubit_name, str) + gate_op.qubits[i] = qubit_map[gate_qubit_name] + + @staticmethod + def transform_expression(expression, variable_map: dict[str, Union[int, float, bool]]): + """Transform an expression by replacing variables with their values. + + Args: + expression (Any): The expression to transform. + variable_map (dict): The mapping of variables to their values. + + Returns: + expression (Any): The transformed expression. + """ + if expression is None: + return None + + if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)): + return expression + + if isinstance(expression, BinaryExpression): + lhs = Qasm3Transformer.transform_expression(expression.lhs, variable_map) + rhs = Qasm3Transformer.transform_expression(expression.rhs, variable_map) + expression.lhs = lhs + expression.rhs = rhs + + if isinstance(expression, UnaryExpression): + operand = Qasm3Transformer.transform_expression(expression.expression, variable_map) + expression.expression = operand + + if isinstance(expression, Identifier): + if expression.name in variable_map: + value = variable_map[expression.name] + if isinstance(value, int): + return IntegerLiteral(value) + if isinstance(value, float): + return FloatLiteral(value) + if isinstance(value, bool): + return BooleanLiteral(value) + + return expression + + @staticmethod + def transform_gate_params( + gate_op: QuantumGate, param_map: dict[str, Union[int, float, bool]] + ) -> None: + """Transform the parameters of a gate operation with a parameter map. + + Args: + gate_op (QuantumGate): The gate operation to transform. + param_map (dict[str, Union[int, float, bool]]): The parameter map to use + for transformation. + + Returns: + None: arguments are transformed in place + """ + # gate_op.arguments is a list of "actual" arguments used in the gate call inside body + + # param map is a "global dict for this gate" which contains the binding of the params + # to the actual values used in the call + for i, actual_arg in enumerate(gate_op.arguments): + # recursively replace ALL instances of the parameter in the expression + # with the actual value + gate_op.arguments[i] = Qasm3Transformer.transform_expression(actual_arg, param_map) + + @staticmethod + def get_branch_params(condition: Any) -> tuple[int, str]: + """ + Get the branch parameters from the branching condition + + Args: + condition (Any): The condition to analyze + + Returns: + tuple[int, str]: The branch parameters + """ + if isinstance(condition, UnaryExpression): + return ( + condition.expression.index[0].value, + condition.expression.collection.name, + ) + if isinstance(condition, BinaryExpression): + return ( + condition.lhs.index[0].value, + condition.lhs.collection.name, + ) + if isinstance(condition, IndexExpression): + if isinstance(condition.index, DiscreteSet): + raise_qasm3_error( + message="DiscreteSet not supported in branching condition", + span=condition.span, + ) + if isinstance(condition.index, list): + if isinstance(condition.index[0], RangeDefinition): + raise_qasm3_error( + message="RangeDefinition not supported in branching condition", + span=condition.span, + ) + return ( + condition.index[0].value, + condition.collection.name, + ) + # default case + return -1, "" + + @classmethod + def transform_function_qubits( + cls, + q_op: Union[QuantumGate, QuantumBarrier, QuantumReset], + formal_qreg_sizes: dict[str, int], + qubit_map: dict[tuple, tuple], + ) -> list[IndexedIdentifier]: + """Transform the qubits of a function call to the actual qubits. + + Args: + visitor_obj: The visitor object. + gate_op: The quantum operation to transform. + formal_qreg_sizes (dict[str: int]): The formal qubit register sizes. + qubit_map (dict[tuple: tuple]): The mapping of formal qubits to actual qubits. + + Returns: + None + """ + expanded_op_qubits = cls.visitor_obj._get_op_bits(q_op, formal_qreg_sizes) + + transformed_qubits = [] + for qubit in expanded_op_qubits: + formal_qreg_name = qubit.name.name + formal_qreg_idx = qubit.indices[0][0].value + + # replace the formal qubit with the actual qubit + actual_qreg_name, actual_qreg_idx = qubit_map[(formal_qreg_name, formal_qreg_idx)] + transformed_qubits.append( + IndexedIdentifier( + Identifier(actual_qreg_name), + [[IntegerLiteral(actual_qreg_idx)]], + ) + ) + + return transformed_qubits + + @classmethod + def get_target_qubits( + cls, + target: Union[Identifier, IndexExpression], + qreg_size_map: dict[str, int], + target_name: str, + ) -> tuple: + """Get the target qubits of a statement. + + Args: + target (Any): The target of the statement. + qreg_size_map (dict[str: int]): The quantum register size map. + target_name (str): The name of the register. + + Returns: + tuple: The target qubits. + """ + target_qids = None + target_qubits_size = None + + if isinstance(target, Identifier): # "(q);" + target_qids = list(range(qreg_size_map[target_name])) + target_qubits_size = qreg_size_map[target_name] + + elif isinstance(target, IndexExpression): + if isinstance(target.index, DiscreteSet): # "(q[{0,1}]);" + target_qids = Qasm3Transformer.extract_values_from_discrete_set(target.index) + for qid in target_qids: + Qasm3Validator.validate_register_index( + qid, qreg_size_map[target_name], qubit=True + ) + target_qubits_size = len(target_qids) + elif isinstance(target.index[0], (IntegerLiteral, Identifier)): # "(q[0]); OR (q[i]);" + target_qids = [Qasm3ExprEvaluator.evaluate_expression(target.index[0])] + Qasm3Validator.validate_register_index( + target_qids[0], qreg_size_map[target_name], qubit=True + ) + target_qubits_size = 1 + elif isinstance(target.index[0], RangeDefinition): # "(q[0:1:2]);" + target_qids = Qasm3Transformer.get_qubits_from_range_definition( + target.index[0], + qreg_size_map[target_name], + is_qubit_reg=True, + ) + target_qubits_size = len(target_qids) + return target_qids, target_qubits_size + + @staticmethod + def get_type_string(variable: Variable) -> str: + """Get the type string for a variable.""" + base_type = variable.base_type + base_size = variable.base_size + dims = variable.dims + is_array = dims and len(dims) > 0 + type_str = "" if not is_array else "array[" + + type_str += VARIABLE_TYPE_MAP[base_type.__class__].__name__ + if base_type.__class__ == UintType: + type_str = type_str.replace("int", "uint") + if base_size: + type_str += f"[{base_size}]" + + if is_array: + type_str += f", {', '.join([str(dim) for dim in dims])}]" + return type_str diff --git a/pyqasm/unroller.py b/pyqasm/unroller.py new file mode 100644 index 0000000..449dd63 --- /dev/null +++ b/pyqasm/unroller.py @@ -0,0 +1,56 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + + +""" +Module containing OpenQASM to QIR conversion functions + +""" +from typing import Union + +import openqasm3 + +from .elements import Qasm3Module +from .exceptions import ValidationError +from .visitor import BasicQasmVisitor + + +def unroll( + program: Union[openqasm3.ast.Program, str], + **kwargs, +) -> Qasm3Module: + """Converts an OpenQASM 3 program to an unrolled qasm program + + Args: + program (openqasm3.ast.Program or str): The OpenQASM 3 program to convert. + + Returns: + Qasm3Module: An object containing unrolled qasm representation along with some useful + metadata and methods + + Raises: + TypeError: If the input is not a valid OpenQASM 3 program. + ValidationError: If the conversion fails. + """ + if isinstance(program, str): + try: + program = openqasm3.parse(program) + except openqasm3.parser.QASM3ParsingError as err: + raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err + + elif not isinstance(program, openqasm3.ast.Program): + raise TypeError("Input quantum program must be of type openqasm3.ast.Program or str.") + + module = Qasm3Module.from_program(program) + + visitor = BasicQasmVisitor(module=module, **kwargs) + module.accept(visitor) + + return module diff --git a/pyqasm/validate.py b/pyqasm/validate.py index 7582dae..b2e5fcf 100644 --- a/pyqasm/validate.py +++ b/pyqasm/validate.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the qBraid-SDK +# This file is part of the pyqasm # -# The qBraid-SDK is free software released under the GNU General Public License v3 +# The 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 the qBraid-SDK, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. """ Module for performing semantic analysis of OpenQASM 3 programs. @@ -16,9 +16,11 @@ from typing import TYPE_CHECKING, Union -from openqasm3.parser import QASM3ParsingError +import openqasm3 +from .elements import Qasm3Module from .exceptions import ValidationError +from .visitor import BasicQasmVisitor if TYPE_CHECKING: import openqasm3.ast @@ -37,9 +39,14 @@ def validate(program: Union[openqasm3.ast.Program, str]) -> None: if isinstance(program, str): try: program = openqasm3.parse(program) - except QASM3ParsingError as err: + except openqasm3.parser.QASM3ParsingError as err: raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err elif not isinstance(program, openqasm3.ast.Program): raise TypeError("Input quantum program must be of type 'str' or 'openqasm3.ast.Program'.") - raise NotImplementedError("Semantic validation is not yet implemented.") + module = Qasm3Module.from_program(program) + try: + visitor = BasicQasmVisitor(module, check_only=True) + module.accept(visitor) + except (ValidationError, TypeError, ValueError) as err: + raise ValidationError(f"Semantic validation failed: {err}") from err diff --git a/pyqasm/validator.py b/pyqasm/validator.py new file mode 100644 index 0000000..bf6a784 --- /dev/null +++ b/pyqasm/validator.py @@ -0,0 +1,295 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module with utility functions for QASM3 visitor + +""" +from typing import Any, Optional, Union + +import numpy as np +from openqasm3.ast import ArrayType, ClassicalDeclaration +from openqasm3.ast import IntType as Qasm3IntType +from openqasm3.ast import QuantumGate, QuantumGateDefinition, ReturnStatement, SubroutineDefinition + +from .elements import Variable +from .exceptions import ValidationError, raise_qasm3_error +from .maps import LIMITS_MAP, VARIABLE_TYPE_MAP, qasm_variable_type_cast + + +class Qasm3Validator: + """Class with validation functions for QASM3 visitor""" + + @staticmethod + def validate_register_index(index: Optional[int], size: int, qubit: bool = False) -> None: + """Validate the index for a register. + + Args: + index (optional, int): The index to validate. + size (int): The size of the register. + qubit (bool): Whether the register is a qubit register. + + Raises: + ValidationError: If the index is out of range. + """ + if index is None or 0 <= index < size: + return None + + raise ValidationError( + f"Index {index} out of range for register of size {size} in " + f"{'qubit' if qubit else 'clbit'}" + ) + + @staticmethod + def validate_statement_type(blacklisted_stmts: set, statement: Any, construct: str) -> None: + """Validate the type of a statement. + + Args: + blacklisted_stmts (set): The set of blacklisted statements. + statement (Any): The statement to validate. + construct (str): The construct the statement is in. + + Raises: + ValidationError: If the statement is not supported. + """ + stmt_type = statement.__class__ + if stmt_type in blacklisted_stmts: + if stmt_type != ClassicalDeclaration: + raise_qasm3_error( + f"Unsupported statement {stmt_type} in {construct} block", + span=statement.span, + ) + + if statement.type.__class__ == ArrayType: + raise_qasm3_error( + f"Unsupported statement {stmt_type} with {statement.type.__class__}" + f" in {construct} block", + span=statement.span, + ) + + @staticmethod + def validate_variable_type(variable: Optional[Variable], reqd_type: Any) -> bool: + """Validate the type of a variable. + + Args: + variable (Variable): The variable to validate. + reqd_type (Any): The required Qasm3 type of the variable. + + Returns: + bool: True if the variable is of the required type, False otherwise. + """ + if not reqd_type: + return True + if variable is None: + return False + return isinstance(variable.base_type, reqd_type) + + @staticmethod + def validate_variable_assignment_value(variable: Variable, value) -> Any: + """Validate the assignment of a value to a variable. + + Args: + variable (Variable): The variable to assign to. + value (Any): The value to assign. + + Raises: + ValidationError: If the value is not of the correct type. + + Returns: + Any: The value casted to the correct type. + """ + # check 1 - type match + qasm_type = variable.base_type.__class__ + base_size = variable.base_size + + try: + type_to_match = VARIABLE_TYPE_MAP[qasm_type] + except KeyError as err: + raise ValidationError(f"Invalid type {qasm_type} for variable {variable.name}") from err + + # For each type we will have a "castable" type set and its corresponding cast operation + type_casted_value = qasm_variable_type_cast(qasm_type, variable.name, base_size, value) + + left: Union[int, float] = 0 + right: Union[int, float] = 0 + # check 2 - range match , if bits mentioned in base size + if type_to_match == int: + base_size = variable.base_size + if qasm_type == Qasm3IntType: + left, right = ( + -1 * (2 ** (base_size - 1)), + 2 ** (base_size - 1) - 1, + ) + else: + # would be uint only so we correctly get this + left, right = 0, 2**base_size - 1 + if type_casted_value < left or type_casted_value > right: + raise_qasm3_error( + f"Value {value} out of limits for variable {variable.name} with " + f"base size {base_size}", + ) + + elif type_to_match == float: + base_size = variable.base_size + + if base_size == 32: + left, right = -1.0 * (LIMITS_MAP["float_32"]), (LIMITS_MAP["float_32"]) + else: + left, right = -1.0 * (LIMITS_MAP["float_64"]), (LIMITS_MAP["float_64"]) + + if type_casted_value < left or type_casted_value > right: + raise_qasm3_error( + f"Value {value} out of limits for variable {variable.name} with " + f"base size {base_size}", + ) + elif type_to_match == bool: + pass + else: + raise_qasm3_error( + f"Invalid type {type_to_match} for variable {variable.name}", TypeError + ) + + return type_casted_value + + @staticmethod + def validate_array_assignment_values( + variable: Variable, dimensions: list[int], values: np.ndarray + ) -> None: + """Validate the assignment of values to an array variable. + + Args: + variable (Variable): The variable to assign to. + dimensions (list[int]): The dimensions of the array. + values (np.ndarray[Any]): The values to assign. + + Raises: + ValidationError: If the values are not of the correct type. + """ + # recursively check the array + if values.shape[0] != dimensions[0]: + raise_qasm3_error( + f"Invalid dimensions for array assignment to variable {variable.name}. " + f"Expected {dimensions[0]} but got {values.shape[0]}", + ) + for i, value in enumerate(values): + if isinstance(value, np.ndarray): + Qasm3Validator.validate_array_assignment_values(variable, dimensions[1:], value) + else: + if len(dimensions) != 1: + raise_qasm3_error( + f"Invalid dimensions for array assignment to variable {variable.name}. " + f"Expected {len(dimensions)} but got 1", + ) + values[i] = Qasm3Validator.validate_variable_assignment_value(variable, value) + + @staticmethod + def validate_gate_call( + operation: QuantumGate, + gate_definition: QuantumGateDefinition, + qubits_in_op, + ) -> None: + """Validate the call of a gate operation. + + Args: + operation (QuantumGate): The gate operation to validate. + gate_definition (QuantumGateDefinition): The gate definition to validate against. + qubits_in_op (int): The number of qubits in the operation. + + Raises: + ValidationError: If the number of parameters or qubits is invalid. + """ + op_num_args = len(operation.arguments) + gate_def_num_args = len(gate_definition.arguments) + if op_num_args != gate_def_num_args: + s = "" if gate_def_num_args == 1 else "s" + raise_qasm3_error( + f"Parameter count mismatch for gate {operation.name.name}: " + f"expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.", + span=operation.span, + ) + + gate_def_num_qubits = len(gate_definition.qubits) + if qubits_in_op != gate_def_num_qubits: + s = "" if gate_def_num_qubits == 1 else "s" + raise_qasm3_error( + f"Qubit count mismatch for gate {operation.name.name}: " + f"expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.", + span=operation.span, + ) + + @staticmethod + def validate_return_statement( # pylint: disable=inconsistent-return-statements + subroutine_def: SubroutineDefinition, + return_statement: ReturnStatement, + return_value: Any, + ): + """Validate the return type of a function. + + Args: + subroutine_def (SubroutineDefinition): The subroutine definition. + return_statement (ReturnStatement): The return statement. + return_value (Any): The return value. + + Raises: + ValidationError: If the return type is invalid. + + Returns: + Any: The return value casted to the correct type + """ + + if subroutine_def.return_type is None: + if return_value is not None: + raise_qasm3_error( + f"Return type mismatch for subroutine '{subroutine_def.name.name}'." + f" Expected void but got {type(return_value)}", + span=return_statement.span, + ) + else: + if return_value is None: + raise_qasm3_error( + f"Return type mismatch for subroutine '{subroutine_def.name.name}'." + f" Expected {subroutine_def.return_type} but got void", + span=return_statement.span, + ) + base_size = 1 + if hasattr(subroutine_def.return_type, "size"): + base_size = subroutine_def.return_type.size.value + + return Qasm3Validator.validate_variable_assignment_value( + Variable( + subroutine_def.name.name + "_return", + subroutine_def.return_type, + base_size, + None, + None, + ), + return_value, + ) + + @staticmethod + def validate_unique_qubits(qubit_map: dict, reg_name: str, indices: list) -> bool: + """ + Validates that the qubits in the given register are unique. + + Args: + qubit_map (dict): Dictionary of qubits. + reg_name (str): The name of the register. + indices (list): A list of indices representing the qubits. + + Returns: + bool: True if the qubits are unique, False otherwise. + """ + if reg_name not in qubit_map: + qubit_map[reg_name] = set(indices) + else: + for idx in indices: + if idx in qubit_map[reg_name]: + return False + return True diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py new file mode 100644 index 0000000..dbb8b3a --- /dev/null +++ b/pyqasm/visitor.py @@ -0,0 +1,1411 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +# pylint: disable=too-many-instance-attributes,too-many-lines,too-many-branches + +""" +Module defining Qasm3 Visitor. + +""" +import copy +import logging +from abc import ABCMeta, abstractmethod +from collections import deque +from typing import Any, Optional, Union + +import numpy as np +import openqasm3.ast as qasm3_ast + +from .analyzer import Qasm3Analyzer +from .elements import Context, InversionOp, Qasm3Module, Variable +from .exceptions import ValidationError, raise_qasm3_error +from .expressions import Qasm3ExprEvaluator +from .maps import ( + ARRAY_TYPE_MAP, + CONSTANTS_MAP, + MAX_ARRAY_DIMENSIONS, + SWITCH_BLACKLIST_STMTS, + map_qasm_inv_op_to_pyqir_callable, + map_qasm_op_to_pyqir_callable, +) +from .subroutines import Qasm3SubroutineProcessor +from .transformer import Qasm3Transformer +from .validator import Qasm3Validator + +logger = logging.getLogger(__name__) + + +class ProgramElementVisitor(metaclass=ABCMeta): + @abstractmethod + def visit_register(self, register): + pass + + @abstractmethod + def visit_statement(self, statement): + pass + + +class BasicQasmVisitor(ProgramElementVisitor): + """A visitor for basic OpenQASM program elements. + + This class is designed to traverse and interact with elements in an OpenQASM program. + + Args: + initialize_runtime (bool): If True, quantum runtime will be initialized. Defaults to True. + record_output (bool): If True, output of the circuit will be recorded. Defaults to True. + """ + + def __init__(self, module: Qasm3Module, check_only: bool = False): + self._module: Qasm3Module = module + self._scope: deque = deque([{}]) + self._context: deque = deque([Context.GLOBAL]) + self._qubit_labels: dict[str, int] = {} + self._clbit_labels: dict[str, int] = {} + self._global_qreg_size_map: dict[str, int] = {} + self._function_qreg_size_map: deque = deque([]) # for nested functions + self._function_qreg_transform_map: deque = deque([]) # for nested functions + self._global_creg_size_map: dict[str, int] = {} + self._custom_gates: dict[str, qasm3_ast.QuantumGateDefinition] = {} + self._subroutine_defns: dict[str, qasm3_ast.SubroutineDefinition] = {} + self._check_only: bool = check_only + self._curr_scope: int = 0 + self._label_scope_level: dict[int, set] = {self._curr_scope: set()} + + self._init_utilities() + + def _init_utilities(self): + """Initialize the utilities for the visitor.""" + for class_obj in [Qasm3Transformer, Qasm3ExprEvaluator, Qasm3SubroutineProcessor]: + class_obj.set_visitor_obj(self) + + def _push_scope(self, scope: dict) -> None: + if not isinstance(scope, dict): + raise TypeError("Scope must be a dictionary") + self._scope.append(scope) + + def _push_context(self, context: Context) -> None: + if not isinstance(context, Context): + raise TypeError("Context must be an instance of Context") + self._context.append(context) + + def _pop_scope(self) -> None: + if len(self._scope) == 0: + raise IndexError("Scope list is empty, can not pop") + self._scope.pop() + + def _restore_context(self) -> None: + if len(self._context) == 0: + raise IndexError("Context list is empty, can not pop") + self._context.pop() + + def _get_parent_scope(self) -> dict: + if len(self._scope) < 2: + raise IndexError("Parent scope not available") + return self._scope[-2] + + def _get_curr_scope(self) -> dict: + if len(self._scope) == 0: + raise IndexError("No scopes available to get") + return self._scope[-1] + + def _get_curr_context(self) -> Context: + if len(self._context) == 0: + raise IndexError("No context available to get") + return self._context[-1] + + def _get_global_scope(self) -> dict: + if len(self._scope) == 0: + raise IndexError("No scopes available to get") + return self._scope[0] + + def _check_in_scope(self, var_name: str) -> bool: + """ + Checks if a variable is in scope. + + Args: + var_name (str): The name of the variable to check. + + Returns: + bool: True if the variable is in scope, False otherwise. + + NOTE: + + - According to our definition of scope, we have a NEW DICT + for each block scope also + - Since all visible variables of the immediate parent are visible + inside block scope, we have to check till we reach the boundary + contexts + - The "boundary" for a scope is either a FUNCTION / GATE context + OR the GLOBAL context + - Why then do we need a new scope for a block? + - Well, if the block redeclares a variable in its scope, then the + variable in the parent scope is shadowed. We need to remember the + original value of the shadowed variable when we exit the block scope + + """ + global_scope = self._get_global_scope() + curr_scope = self._get_curr_scope() + if self._in_global_scope(): + return var_name in global_scope + if self._in_function_scope() or self._in_gate_scope(): + if var_name in curr_scope: + return True + if var_name in global_scope: + return global_scope[var_name].is_constant + if self._in_block_scope(): + for scope, context in zip(reversed(self._scope), reversed(self._context)): + if context != Context.BLOCK: + return var_name in scope + if var_name in scope: + return True + return False + + def _get_from_visible_scope(self, var_name: str) -> Union[Variable, None]: + """ + Retrieves a variable from the visible scope. + + Args: + var_name (str): The name of the variable to retrieve. + + Returns: + Union[Variable, None]: The variable if found, None otherwise. + """ + global_scope = self._get_global_scope() + curr_scope = self._get_curr_scope() + + if self._in_global_scope(): + return global_scope.get(var_name, None) + if self._in_function_scope() or self._in_gate_scope(): + if var_name in curr_scope: + return curr_scope[var_name] + if var_name in global_scope and global_scope[var_name].is_constant: + return global_scope[var_name] + if self._in_block_scope(): + for scope, context in zip(reversed(self._scope), reversed(self._context)): + if context != Context.BLOCK: + return scope.get(var_name, None) + if var_name in scope: + return scope[var_name] + # keep on checking + return None + + def _add_var_in_scope(self, variable: Variable) -> None: + """Add a variable to the current scope. + + Args: + variable (Variable): The variable to add. + + Raises: + ValueError: If the variable already exists in the current scope. + """ + curr_scope = self._get_curr_scope() + if variable.name in curr_scope: + raise ValueError(f"Variable '{variable.name}' already exists in current scope") + curr_scope[variable.name] = variable + + def _update_var_in_scope(self, variable: Variable) -> None: + """ + Updates the variable in the current scope. + + Args: + variable (Variable): The variable to be updated. + + Raises: + ValueError: If no scope is available to update. + """ + if len(self._scope) == 0: + raise ValueError("No scope available to update") + + global_scope = self._get_global_scope() + curr_scope = self._get_curr_scope() + + if self._in_global_scope(): + global_scope[variable.name] = variable + if self._in_function_scope() or self._in_gate_scope(): + curr_scope[variable.name] = variable + if self._in_block_scope(): + for scope, context in zip(reversed(self._scope), reversed(self._context)): + if context != Context.BLOCK: + scope[variable.name] = variable + break + if variable.name in scope: + scope[variable.name] = variable + break + continue + + def _in_global_scope(self) -> bool: + return len(self._scope) == 1 and self._get_curr_context() == Context.GLOBAL + + def _in_function_scope(self) -> bool: + return len(self._scope) > 1 and self._get_curr_context() == Context.FUNCTION + + def _in_gate_scope(self) -> bool: + return len(self._scope) > 1 and self._get_curr_context() == Context.GATE + + def _in_block_scope(self) -> bool: # block scope is for if/else/for/while constructs + return len(self._scope) > 1 and self._get_curr_context() == Context.BLOCK + + def visit_register( + self, register: Union[qasm3_ast.QubitDeclaration, qasm3_ast.ClassicalDeclaration] + ) -> None: + """Visit a register element. + + Args: + register (QubitDeclaration|ClassicalDeclaration): The register name and size. + + Returns: + None + """ + logger.debug("Visiting register '%s'", str(register)) + is_qubit = isinstance(register, qasm3_ast.QubitDeclaration) + + current_size = len(self._qubit_labels) if is_qubit else len(self._clbit_labels) + if is_qubit: + register_size = ( + 1 if register.size is None else register.size.value # type: ignore[union-attr] + ) + else: + register_size = ( + 1 + if register.type.size is None # type: ignore[union-attr] + else register.type.size.value # type: ignore[union-attr] + ) + register_name = ( + register.qubit.name # type: ignore[union-attr] + if is_qubit + else register.identifier.name # type: ignore[union-attr] + ) + + size_map = self._global_qreg_size_map if is_qubit else self._global_creg_size_map + label_map = self._qubit_labels if is_qubit else self._clbit_labels + + if self._check_in_scope(register_name): + raise_qasm3_error( + f"Invalid declaration of register with name '{register_name}'", span=register.span + ) + + if is_qubit: # as bit type vars are added in classical decl handler + self._add_var_in_scope( + Variable( + register_name, + qasm3_ast.QubitDeclaration, + register_size, + None, + None, + False, + ) + ) + qasm_stmt = f"{'qubit' if is_qubit else 'bit'}[{register_size}] {register_name};\n" + self._module.add_qasm_statement(qasm_stmt) + + for i in range(register_size): + # required if indices are not used while applying a gate or measurement + size_map[f"{register_name}"] = register_size + label_map[f"{register_name}_{i}"] = current_size + i + + self._label_scope_level[self._curr_scope].add(register_name) + + logger.debug("Added labels for register '%s'", str(register)) + + def _check_if_name_in_scope(self, name: str, operation: Any) -> None: + """Check if a name is in scope to avoid duplicate declarations. + Args: + name (str): The name to check. + operation (Any): The operation to check the name in scope for. + + Returns: + bool: Whether the name is in scope. + """ + for scope_level in range(0, self._curr_scope + 1): + if name in self._label_scope_level[scope_level]: + return + raise_qasm3_error( + f"Variable {name} not in scope for operation {operation}", span=operation.span + ) + + def _get_op_bits( + self, operation: Any, reg_size_map: dict, qubits: bool = True + ) -> list[qasm3_ast.IndexedIdentifier]: + """Get the quantum / classical bits for the operation. + + Args: + operation (Any): The operation to get qubits for. + reg_size_map (dict): The size map of the registers in scope. + qubits (bool): Whether the bits are quantum bits or classical bits. Defaults to True. + Returns: + list[qasm3_ast.IndexedIdentifier] : The bits for the operation. + """ + openqasm_bits = [] + visited_bits = set() + bit_list = [] + if isinstance(operation, qasm3_ast.QuantumMeasurementStatement): + assert operation.target is not None + bit_list = [operation.measure.qubit] if qubits else [operation.target] + else: + bit_list = ( + operation.qubits if isinstance(operation.qubits, list) else [operation.qubits] + ) + + for bit in bit_list: + if isinstance(bit, qasm3_ast.IndexedIdentifier): + reg_name = bit.name.name + else: + reg_name = bit.name + + if reg_name not in reg_size_map: + raise_qasm3_error( + f"Missing register declaration for {reg_name} in operation {operation}", + span=operation.span, + ) + self._check_if_name_in_scope(reg_name, operation) + + if isinstance(bit, qasm3_ast.IndexedIdentifier): + if isinstance(bit.indices[0], qasm3_ast.DiscreteSet): + bit_ids = Qasm3Transformer.extract_values_from_discrete_set(bit.indices[0]) + elif isinstance(bit.indices[0][0], qasm3_ast.RangeDefinition): + bit_ids = Qasm3Transformer.get_qubits_from_range_definition( + bit.indices[0][0], reg_size_map[reg_name], is_qubit_reg=qubits + ) + else: + bit_id = Qasm3ExprEvaluator.evaluate_expression(bit.indices[0][0]) + Qasm3Validator.validate_register_index( + bit_id, reg_size_map[reg_name], qubit=qubits + ) + bit_ids = [bit_id] + else: + bit_ids = list(range(reg_size_map[reg_name])) + + new_bits = [ + qasm3_ast.IndexedIdentifier( + qasm3_ast.Identifier(reg_name), [[qasm3_ast.IntegerLiteral(bit_id)]] + ) + for bit_id in bit_ids + ] + # check for duplicate bits + for bit_id in new_bits: + bit_name, bit_value = bit_id.name.name, bit_id.indices[0][0].value + if tuple((bit_name, bit_value)) in visited_bits: + raise_qasm3_error( + f"Duplicate {'qubit' if qubits else 'clbit'} " + f"{bit_name}[{bit_value}] argument", + span=operation.span, + ) + visited_bits.add((bit_name, bit_value)) + + openqasm_bits.extend(new_bits) + + return openqasm_bits + + def _visit_measurement(self, statement: qasm3_ast.QuantumMeasurementStatement) -> None: + """Visit a measurement statement element. + + Args: + statement (qasm3_ast.QuantumMeasurementStatement): The measurement statement to visit. + + Returns: + None + """ + logger.debug("Visiting measurement statement '%s'", str(statement)) + + source = statement.measure.qubit + target = statement.target + assert source and target + + # # TODO: handle in-function measurements + source_name: str = ( + source.name if isinstance(source, qasm3_ast.Identifier) else source.name.name + ) + if source_name not in self._global_qreg_size_map: + raise_qasm3_error( + f"Missing register declaration for {source_name} in measurement " + f"operation {statement}", + span=statement.span, + ) + + target_name: str = ( + target.name if isinstance(target, qasm3_ast.Identifier) else target.name.name + ) + if target_name not in self._global_creg_size_map: + raise_qasm3_error( + f"Missing register declaration for {target_name} in measurement " + f"operation {statement}", + span=statement.span, + ) + + source_ids = self._get_op_bits( + statement, reg_size_map=self._global_qreg_size_map, qubits=True + ) + target_ids = self._get_op_bits( + statement, reg_size_map=self._global_creg_size_map, qubits=False + ) + + if len(source_ids) != len(target_ids): + raise_qasm3_error( + f"Register sizes of {source_name} and {target_name} do not match " + "for measurement operation", + span=statement.span, + ) + if not self._check_only: + for src_id, tgt_id in zip(source_ids, target_ids): + src_name, src_bit = src_id.name.name, src_id.indices[0][0].value + tgt_name, tgt_bit = tgt_id.name.name, tgt_id.indices[0][0].value + qasm_stmt = f"measure {src_name}[{src_bit}] -> {tgt_name}[{tgt_bit}];\n" + self._module.add_qasm_statement(qasm_stmt) + + def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None: + """Visit a reset statement element. + + Args: + statement (qasm3_ast.QuantumReset): The reset statement to visit. + + Returns: + None + """ + logger.debug("Visiting reset statement '%s'", str(statement)) + if len(self._function_qreg_size_map) > 0: # atleast in SOME function scope + # transform qubits to use the global qreg identifiers + statement.qubits = ( + Qasm3Transformer.transform_function_qubits( # type: ignore[assignment] + statement, + self._function_qreg_size_map[-1], + self._function_qreg_transform_map[-1], + ) + ) + qubit_ids = self._get_op_bits(statement, self._global_qreg_size_map, True) + + if not self._check_only: + for qid in qubit_ids: + name, bit_id = qid.name.name, qid.indices[0][0].value + qasm_stmt = f"reset {name}[{bit_id}];\n" + self._module.add_qasm_statement(qasm_stmt) + + def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None: + """Visit a barrier statement element. + + Args: + statement (qasm3_ast.QuantumBarrier): The barrier statement to visit. + + Returns: + None + """ + # if barrier is applied to ALL qubits at once, we are fine + if len(self._function_qreg_size_map) > 0: # atleast in SOME function scope + # transform qubits to use the global qreg identifiers + + # since we are changing the qubits to IndexedIdentifiers, we need to supress the + # error for the type checker + barrier.qubits = ( + Qasm3Transformer.transform_function_qubits( # type: ignore [assignment] + barrier, + self._function_qreg_size_map[-1], + self._function_qreg_transform_map[-1], + ) + ) + barrier_qubits = self._get_op_bits(barrier, self._global_qreg_size_map) + if not self._check_only: + for qubit in barrier_qubits: + qasm_stmt = f"barrier {qubit.name.name}[{qubit.indices[0][0].value}];\n" + self._module.add_qasm_statement(qasm_stmt) + + def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: + """Get the parameters for the operation. + + Args: + operation (qasm3_ast.QuantumGate): The operation to get parameters for. + + Returns: + list[float]: The parameters for the operation. + """ + param_list = [] + for param in operation.arguments: + param_value = Qasm3ExprEvaluator.evaluate_expression(param) + param_list.append(param_value) + + return param_list + + def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> None: + """Visit a gate definition element. + + Args: + definition (qasm3_ast.QuantumGateDefinition): The gate definition to visit. + + Returns: + None + """ + gate_name = definition.name.name + if gate_name in self._custom_gates: + raise_qasm3_error(f"Duplicate gate definition for {gate_name}", span=definition.span) + self._custom_gates[gate_name] = definition + + def _visit_basic_gate_operation( + self, operation: qasm3_ast.QuantumGate, inverse: bool = False + ) -> None: + """Visit a gate operation element. + + Args: + operation (qasm3_ast.QuantumGate): The gate operation to visit. + inverse (bool): Whether the operation is an inverse operation. Defaults to False. + + - if inverse is True, we apply check for different cases in the + map_qasm_inv_op_to_pyqir_callable method. + + - Only rotation and S / T gates are affected by this inversion. For S/T + gates we map them to Sdg / Tdg and vice versa. + + - For rotation gates, we map to the same gates but invert the rotation + angles. + + Returns: + None + + Raises: + ValidationError: If the number of qubits is invalid. + + """ + + logger.debug("Visiting basic gate operation '%s'", str(operation)) + op_name: str = operation.name.name + op_qubits = self._get_op_bits(operation, self._global_qreg_size_map) + inverse_action = None + # to update : qir_func is a pyqir callable, need to change this + if not inverse: + qir_func, op_qubit_count = map_qasm_op_to_pyqir_callable(op_name) + else: + # in basic gates, inverse action only affects the rotation gates + qir_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_pyqir_callable(op_name) + # to update + + op_parameters = None + + if len(op_qubits) % op_qubit_count != 0: + raise_qasm3_error( + f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}", + span=operation.span, + ) + + if len(operation.arguments) > 0: # parametric gate + op_parameters = self._get_op_parameters(operation) + if inverse_action == InversionOp.INVERT_ROTATION: + op_parameters = [-1 * param for param in op_parameters] + + if not self._check_only: + # to update + for i in range(0, len(op_qubits), op_qubit_count): + # we apply the gate on the qubit subset linearly + qubit_subset = op_qubits[i : i + op_qubit_count] + if op_parameters is not None: + qir_func(self._builder, *op_parameters, *qubit_subset) + else: + qir_func(self._builder, *qubit_subset) + # to update + + def _visit_custom_gate_operation( + self, operation: qasm3_ast.QuantumGate, inverse: bool = False + ) -> None: + """Visit a custom gate operation element recursively. + + Args: + operation (qasm3_ast.QuantumGate): The gate operation to visit. + inverse (bool): Whether the operation is an inverse operation. Defaults to False. + + If True, the gate operation is applied in reverse order and the + inverse modifier is appended to each gate call. + See https://openqasm.com/language/gates.html#inverse-modifier + for more clarity. + + Returns: + None + """ + logger.debug("Visiting custom gate operation '%s'", str(operation)) + gate_name: str = operation.name.name + gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] + op_qubits: list[qasm3_ast.IndexedIdentifier] = ( + self._get_op_bits( # type: ignore [assignment] + operation, + self._global_qreg_size_map, + ) + ) + + Qasm3Validator.validate_gate_call(operation, gate_definition, len(op_qubits)) + # we need this because the gates applied inside a gate definition use the + # VARIABLE names and not the qubits + + # so we need to update the arguments of these gate applications with the actual + # qubit identifiers and then RECURSIVELY call the visit_generic_gate_operation + qubit_map = { + formal_arg.name: actual_arg + for formal_arg, actual_arg in zip(gate_definition.qubits, op_qubits) + } + param_map = { + formal_arg.name: Qasm3ExprEvaluator.evaluate_expression(actual_arg) + for formal_arg, actual_arg in zip(gate_definition.arguments, operation.arguments) + } + + gate_definition_ops = copy.deepcopy(gate_definition.body) + if inverse: + gate_definition_ops.reverse() + + self._push_context(Context.GATE) + + for gate_op in gate_definition_ops: + if isinstance(gate_op, qasm3_ast.QuantumGate): + # necessary to avoid modifying the original gate definition + # in case the gate is reapplied + gate_op_copy = copy.deepcopy(gate_op) + if gate_op.name.name == gate_name: + raise_qasm3_error( + f"Recursive definitions not allowed for gate {gate_name}", span=gate_op.span + ) + Qasm3Transformer.transform_gate_params(gate_op_copy, param_map) + Qasm3Transformer.transform_gate_qubits(gate_op_copy, qubit_map) + # need to trickle the inverse down to the child gates + if inverse: + # span doesn't matter as we don't analyze it + gate_op_copy.modifiers.append( + qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None) + ) + self._visit_generic_gate_operation(gate_op_copy) + else: + # TODO: add control flow support + raise_qasm3_error( + f"Unsupported gate definition statement {gate_op}", span=gate_op.span + ) + + self._restore_context() + + def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple: + """Collapse the gate modifiers of a gate operation. + Some analysis is required to get this result. + The basic idea is that any power operation is multiplied and inversions are toggled. + The placement of the inverse operation does not matter. + + Args: + operation (qasm3_ast.QuantumGate): The gate operation to collapse modifiers for. + + Returns: + tuple[Any, Any]: The power and inverse values of the gate operation. + """ + power_value, inverse_value = 1, False + + for modifier in operation.modifiers: + modifier_name = modifier.modifier + if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: + current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument) + if current_power < 0: + inverse_value = not inverse_value + power_value = power_value * abs(current_power) + elif modifier_name == qasm3_ast.GateModifierName.inv: + inverse_value = not inverse_value + elif modifier_name in [ + qasm3_ast.GateModifierName.ctrl, + qasm3_ast.GateModifierName.negctrl, + ]: + raise_qasm3_error( + f"Controlled modifier gates not yet supported in gate operation {operation}", + err_type=NotImplementedError, + span=operation.span, + ) + return (power_value, inverse_value) + + def _visit_generic_gate_operation(self, operation: qasm3_ast.QuantumGate) -> None: + """Visit a gate operation element. + + Args: + operation (qasm3_ast.QuantumGate): The gate operation to visit. + + Returns: + None + """ + power_value, inverse_value = self._collapse_gate_modifiers(operation) + operation = copy.deepcopy(operation) + + # only needs to be done once for a gate operation + if not self._in_gate_scope() and len(self._function_qreg_size_map) > 0: + # we are in SOME function scope + # transform qubits to use the global qreg identifiers + operation.qubits = ( + Qasm3Transformer.transform_function_qubits( # type: ignore [assignment] + operation, + self._function_qreg_size_map[-1], + self._function_qreg_transform_map[-1], + ) + ) + # Applying the inverse first and then the power is same as + # apply the power first and then inverting the result + for _ in range(power_value): + if operation.name.name in self._custom_gates: + self._visit_custom_gate_operation(operation, inverse_value) + else: + self._visit_basic_gate_operation(operation, inverse_value) + + def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) -> None: + """ + Visit a constant declaration element. Const can only be declared for scalar + type variables and not arrays. Assignment is mandatory in constant declaration. + + Args: + statement (qasm3_ast.ConstantDeclaration): The constant declaration to visit. + + Returns: + None + """ + + var_name = statement.identifier.name + + if var_name in CONSTANTS_MAP: + raise_qasm3_error( + f"Can not declare variable with keyword name {var_name}", span=statement.span + ) + if self._check_in_scope(var_name): + raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) + init_value = Qasm3ExprEvaluator.evaluate_expression( + statement.init_expression, const_expr=True + ) + + base_type = statement.type + if isinstance(base_type, qasm3_ast.BoolType): + base_size = 1 + elif hasattr(base_type, "size"): + if base_type.size is None: + base_size = 32 # default for now + else: + base_size = Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True) + if not isinstance(base_size, int) or base_size <= 0: + raise_qasm3_error( + f"Invalid base size {base_size} for variable {var_name}", + span=statement.span, + ) + + variable = Variable(var_name, base_type, base_size, [], init_value, is_constant=True) + + # cast + validation + variable.value = Qasm3Validator.validate_variable_assignment_value(variable, init_value) + + self._add_var_in_scope(variable) + + # pylint: disable=too-many-branches + def _visit_classical_declaration(self, statement: qasm3_ast.ClassicalDeclaration) -> None: + """Visit a classical operation element. + + Args: + statement (ClassicalType): The classical operation to visit. + + Returns: + None + """ + var_name = statement.identifier.name + if var_name in CONSTANTS_MAP: + raise_qasm3_error( + f"Can not declare variable with keyword name {var_name}", span=statement.span + ) + if self._check_in_scope(var_name): + if self._in_block_scope() and var_name not in self._get_curr_scope(): + # we can re-declare variables once in block scope even if they are + # present in the parent scope + # Eg. + # int a = 10; + # { int a = 20; // valid + # } + pass + else: + raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) + + init_value = None + base_type = statement.type + final_dimensions = [] + + if isinstance(base_type, qasm3_ast.ArrayType): + dimensions = base_type.dimensions + + if len(dimensions) > MAX_ARRAY_DIMENSIONS: + raise_qasm3_error( + f"Invalid dimensions {len(dimensions)} for array declaration for {var_name}. " + f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}", + span=statement.span, + ) + + base_type = base_type.base_type + num_elements = 1 + for dim in dimensions: + dim_value = Qasm3ExprEvaluator.evaluate_expression(dim) + if not isinstance(dim_value, int) or dim_value <= 0: + raise_qasm3_error( + f"Invalid dimension size {dim_value} in array declaration for {var_name}", + span=statement.span, + ) + final_dimensions.append(dim_value) + num_elements *= dim_value + + init_value = np.full(final_dimensions, None) + + if statement.init_expression: + if isinstance(statement.init_expression, qasm3_ast.ArrayLiteral): + init_value = self._evaluate_array_initialization( + statement.init_expression, final_dimensions, base_type + ) + else: + init_value = Qasm3ExprEvaluator.evaluate_expression(statement.init_expression) + base_size = 1 + if not isinstance(base_type, qasm3_ast.BoolType): + base_size = ( + 32 + if not hasattr(base_type, "size") or base_type.size is None + else Qasm3ExprEvaluator.evaluate_expression(base_type.size) + ) + + if not isinstance(base_size, int) or base_size <= 0: + raise_qasm3_error( + f"Invalid base size {base_size} for variable {var_name}", span=statement.span + ) + + if isinstance(base_type, qasm3_ast.FloatType) and base_size not in [32, 64]: + raise_qasm3_error( + f"Invalid base size {base_size} for float variable {var_name}", span=statement.span + ) + + variable = Variable(var_name, base_type, base_size, final_dimensions, init_value) + + if statement.init_expression: + if isinstance(init_value, np.ndarray): + assert variable.dims is not None + Qasm3Validator.validate_array_assignment_values(variable, variable.dims, init_value) + else: + variable.value = Qasm3Validator.validate_variable_assignment_value( + variable, init_value + ) + + self._add_var_in_scope(variable) + + def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) -> None: + """Visit a classical assignment element. + + Args: + statement (qasm3_ast.ClassicalAssignment): The classical assignment to visit. + + Returns: + None + """ + lvalue = statement.lvalue + lvar_name = lvalue.name + if isinstance(lvar_name, qasm3_ast.Identifier): + lvar_name = lvar_name.name + + lvar = self._get_from_visible_scope(lvar_name) + if lvar is None: # we check for none here, so type errors are irrelevant afterwards + raise_qasm3_error(f"Undefined variable {lvar_name} in assignment", span=statement.span) + if lvar.is_constant: # type: ignore[union-attr] + raise_qasm3_error( + f"Assignment to constant variable {lvar_name} not allowed", span=statement.span + ) + + # rvalue will be an evaluated value (scalar, list) + # if rvalue is a list, we want a copy of it + rvalue = statement.rvalue + rvalue_raw = Qasm3ExprEvaluator.evaluate_expression( + rvalue + ) # consists of scope check and index validation + + # cast + validation + # rhs is a scalar + rvalue_eval = None + if not isinstance(rvalue_raw, np.ndarray): + rvalue_eval = Qasm3Validator.validate_variable_assignment_value( + lvar, rvalue_raw # type: ignore[arg-type] + ) + else: # rhs is a list + rvalue_dimensions = list(rvalue_raw.shape) + + # validate that the values inside rvar are valid for lvar + Qasm3Validator.validate_array_assignment_values( + variable=lvar, # type: ignore[arg-type] + dimensions=rvalue_dimensions, + values=rvalue_raw, # type: ignore[arg-type] + ) + rvalue_eval = rvalue_raw + + if lvar.readonly: # type: ignore[union-attr] + raise_qasm3_error( + f"Assignment to readonly variable '{lvar_name}' not allowed" " in function call", + span=statement.span, + ) + + # lvalue will be the variable which will HOLD this value + if isinstance(lvalue, qasm3_ast.IndexedIdentifier): + # stupid indices structure in openqasm :/ + if len(lvalue.indices[0]) > 1: # type: ignore[arg-type] + l_indices = lvalue.indices[0] + else: + l_indices = [idx[0] for idx in lvalue.indices] # type: ignore[assignment, index] + + validated_l_indices = Qasm3Analyzer.analyze_classical_indices( + l_indices, lvar, Qasm3ExprEvaluator # type: ignore[arg-type] + ) + Qasm3Transformer.update_array_element( + multi_dim_arr=lvar.value, # type: ignore[union-attr, arg-type] + indices=validated_l_indices, + value=rvalue_eval, + ) + else: + lvar.value = rvalue_eval # type: ignore[union-attr] + self._update_var_in_scope(lvar) # type: ignore[arg-type] + + def _evaluate_array_initialization( + self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: Any + ) -> np.ndarray: + """Evaluate an array initialization. + + Args: + array_literal (qasm3_ast.ArrayLiteral): The array literal to evaluate. + dimensions (list[int]): The dimensions of the array. + base_type (Any): The base type of the array. + + Returns: + np.ndarray: The evaluated array initialization. + """ + init_values = [] + for value in array_literal.values: + if isinstance(value, qasm3_ast.ArrayLiteral): + nested_array = self._evaluate_array_initialization(value, dimensions[1:], base_type) + init_values.append(nested_array) + else: + eval_value = Qasm3ExprEvaluator.evaluate_expression(value) + init_values.append(eval_value) + + return np.array(init_values, dtype=ARRAY_TYPE_MAP[base_type.__class__]) + + def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> None: + """Visit a branching statement element. + + Args: + statement (qasm3_ast.BranchingStatement): The branching statement to visit. + + Returns: + None + """ + self._push_context(Context.BLOCK) + self._push_scope({}) + self._curr_scope += 1 + self._label_scope_level[self._curr_scope] = set() + + condition = statement.condition + positive_branching = Qasm3Analyzer.analyse_branch_condition(condition) + + if_block = statement.if_block + if not statement.if_block: + raise_qasm3_error("Missing if block", span=statement.span) + else_block = statement.else_block + if not positive_branching: + if_block, else_block = else_block, if_block + + reg_id, reg_name = Qasm3Transformer.get_branch_params(condition) + + if reg_name not in self._global_creg_size_map: + raise_qasm3_error( + f"Missing register declaration for {reg_name} in {condition}", + span=statement.span, + ) + Qasm3Validator.validate_register_index( + reg_id, self._global_creg_size_map[reg_name], qubit=False + ) + + def _visit_statement_block(block): + for stmt in block: + self.visit_statement(stmt) + + # to update + if not self._check_only: + # if the condition is true, we visit the if block + pyqir._native.if_result( + self._builder, + pyqir.result(self._module.context, self._clbit_labels[f"{reg_name}_{reg_id}"]), + zero=lambda: _visit_statement_block(else_block), + one=lambda: _visit_statement_block(if_block), + ) + # to update + + del self._label_scope_level[self._curr_scope] + self._curr_scope -= 1 + self._pop_scope() + self._restore_context() + + def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None: + # Compute loop variable values + if isinstance(statement.set_declaration, qasm3_ast.RangeDefinition): + init_exp = statement.set_declaration.start + startval = Qasm3ExprEvaluator.evaluate_expression(init_exp) + range_def = statement.set_declaration + stepval = ( + 1 + if range_def.step is None + else Qasm3ExprEvaluator.evaluate_expression(range_def.step) + ) + endval = Qasm3ExprEvaluator.evaluate_expression(range_def.end) + irange = list(range(startval, endval + stepval, stepval)) + elif isinstance(statement.set_declaration, qasm3_ast.DiscreteSet): + init_exp = statement.set_declaration.values[0] + irange = [ + Qasm3ExprEvaluator.evaluate_expression(exp) + for exp in statement.set_declaration.values + ] + else: + raise ValidationError( + f"Unexpected type {type(statement.set_declaration)} of set_declaration in loop." + ) + + i: Optional[Variable] # will store iteration Variable to update to loop scope + + for ival in irange: + self._push_context(Context.BLOCK) + self._push_scope({}) + + # Initialize loop variable in loop scope + # need to re-declare as we discard the block scope in subsequent + # iterations of the loop + self._visit_classical_declaration( + qasm3_ast.ClassicalDeclaration(statement.type, statement.identifier, init_exp) + ) + i = self._get_from_visible_scope(statement.identifier.name) + + # Update scope with current value of loop Variable + if i is not None: + i.value = ival + self._update_var_in_scope(i) + + for stmt in statement.block: + self.visit_statement(stmt) + + # scope not persistent between loop iterations + self._pop_scope() + self._restore_context() + + # as we are only checking compile time errors + # not runtime errors, we can break here + if self._check_only: + break + + def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition) -> None: + """Visit a subroutine definition element. + Reference: https://openqasm.com/language/subroutines.html#subroutines + + Args: + statement (qasm3_ast.SubroutineDefinition): The subroutine definition to visit. + + Returns: + None + """ + fn_name = statement.name.name + + if fn_name in CONSTANTS_MAP: + raise_qasm3_error( + f"Subroutine name '{fn_name}' is a reserved keyword", span=statement.span + ) + + if fn_name in self._subroutine_defns: + raise_qasm3_error(f"Redefinition of subroutine '{fn_name}'", span=statement.span) + + if self._check_in_scope(fn_name): + raise_qasm3_error( + f"Can not declare subroutine with name '{fn_name}' as " + "it is already declared as a variable", + span=statement.span, + ) + + self._subroutine_defns[fn_name] = statement + + # pylint: disable=too-many-locals, too-many-statements + def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: + """Visit a function call element. + + Args: + statement (qasm3_ast.FunctionCall): The function call to visit. + Returns: + None + + """ + fn_name = statement.name.name + if fn_name not in self._subroutine_defns: + raise_qasm3_error(f"Undefined subroutine '{fn_name}' was called", span=statement.span) + + subroutine_def = self._subroutine_defns[fn_name] + + if len(statement.arguments) != len(subroutine_def.arguments): + raise_qasm3_error( + f"Parameter count mismatch for subroutine '{fn_name}'. Expected " + f"{len(subroutine_def.arguments)} but got {len(statement.arguments)} in call", + span=statement.span, + ) + + duplicate_qubit_detect_map: dict = {} + qubit_transform_map: dict = {} # {(formal arg, idx) : (actual arg, idx)} + formal_qreg_size_map: dict = {} + + quantum_vars, classical_vars = [], [] + for actual_arg, formal_arg in zip(statement.arguments, subroutine_def.arguments): + if isinstance(formal_arg, qasm3_ast.ClassicalArgument): + classical_vars.append( + Qasm3SubroutineProcessor.process_classical_arg( + formal_arg, actual_arg, fn_name, statement.span + ) + ) + else: + quantum_vars.append( + Qasm3SubroutineProcessor.process_quantum_arg( + formal_arg, + actual_arg, + formal_qreg_size_map, + duplicate_qubit_detect_map, + qubit_transform_map, + fn_name, + statement.span, + ) + ) + + self._push_scope({}) + self._curr_scope += 1 + self._label_scope_level[self._curr_scope] = set() + self._push_context(Context.FUNCTION) + + for var in quantum_vars: + self._add_var_in_scope(var) + + for var in classical_vars: + self._add_var_in_scope(var) + + # push qubit transform maps + self._function_qreg_size_map.append(formal_qreg_size_map) + self._function_qreg_transform_map.append(qubit_transform_map) + + return_statement = None + for function_op in subroutine_def.body: + if isinstance(function_op, qasm3_ast.ReturnStatement): + return_statement = copy.deepcopy(function_op) + break + self.visit_statement(copy.deepcopy(function_op)) + + if return_statement: + return_value = Qasm3ExprEvaluator.evaluate_expression(return_statement.expression) + return_value = Qasm3Validator.validate_return_statement( + subroutine_def, return_statement, return_value + ) + + # remove qubit transformation map + self._function_qreg_transform_map.pop() + self._function_qreg_size_map.pop() + + self._restore_context() + del self._label_scope_level[self._curr_scope] + self._curr_scope -= 1 + self._pop_scope() + + return return_value if subroutine_def.return_type is not None else None + + def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: + pass + + def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: + """Visit an alias statement element. + + Args: + statement (qasm3_ast.AliasStatement): The alias statement to visit. + + Returns: + None + """ + # pylint: disable=too-many-branches + target = statement.target + value = statement.value + + alias_reg_name: str = target.name + alias_reg_size: int = 0 + aliased_reg_name: str = "" + aliased_reg_size: int = 0 + + # Alias should not be redeclared earlier as a variable or a constant + if self._check_in_scope(alias_reg_name): + raise_qasm3_error(f"Re-declaration of variable '{alias_reg_name}'", span=statement.span) + self._label_scope_level[self._curr_scope].add(alias_reg_name) + + if isinstance(value, qasm3_ast.Identifier): + aliased_reg_name = value.name + elif isinstance(value, qasm3_ast.IndexExpression) and isinstance( + value.collection, qasm3_ast.Identifier + ): + aliased_reg_name = value.collection.name + else: + raise_qasm3_error(f"Unsupported aliasing {statement}", span=statement.span) + + if aliased_reg_name not in self._global_qreg_size_map: + raise_qasm3_error( + f"Qubit register {aliased_reg_name} not found for aliasing", span=statement.span + ) + aliased_reg_size = self._global_qreg_size_map[aliased_reg_name] + if isinstance(value, qasm3_ast.Identifier): # "let alias = q;" + for i in range(aliased_reg_size): + self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[ + f"{aliased_reg_name}_{i}" + ] + alias_reg_size = aliased_reg_size + elif isinstance(value, qasm3_ast.IndexExpression): + if isinstance(value.index, qasm3_ast.DiscreteSet): # "let alias = q[{0,1}];" + qids = Qasm3Transformer.extract_values_from_discrete_set(value.index) + for i, qid in enumerate(qids): + Qasm3Validator.validate_register_index( + qid, self._global_qreg_size_map[aliased_reg_name], qubit=True + ) + self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[ + f"{aliased_reg_name}_{qid}" + ] + alias_reg_size = len(qids) + elif len(value.index) != 1: # like "let alias = q[0,1];"? + raise_qasm3_error( + "An index set can be specified by a single integer (signed or unsigned), " + "a comma-separated list of integers contained in braces {a,b,c,…}, " + "or a range", + span=statement.span, + ) + elif isinstance(value.index[0], qasm3_ast.IntegerLiteral): # "let alias = q[0];" + qid = value.index[0].value + Qasm3Validator.validate_register_index( + qid, self._global_qreg_size_map[aliased_reg_name], qubit=True + ) + self._qubit_labels[f"{alias_reg_name}_0"] = value.index[0].value + alias_reg_size = 1 + elif isinstance(value.index[0], qasm3_ast.RangeDefinition): # "let alias = q[0:1:2];" + qids = Qasm3Transformer.get_qubits_from_range_definition( + value.index[0], + aliased_reg_size, + is_qubit_reg=True, + ) + for i, qid in enumerate(qids): + self._qubit_labels[f"{alias_reg_name}_{i}"] = qid + alias_reg_size = len(qids) + + self._global_qreg_size_map[alias_reg_name] = alias_reg_size + + logger.debug("Added labels for aliasing '%s'", target) + + def _visit_switch_statement(self, statement: qasm3_ast.SwitchStatement) -> None: + """Visit a switch statement element. + + Args: + statement (qasm3_ast.SwitchStatement): The switch statement to visit. + + Returns: + None + """ + # 1. analyze the target - it should ONLY be int, not casted + switch_target = statement.target + switch_target_name = "" + # either identifier or indexed expression + if isinstance(switch_target, qasm3_ast.Identifier): + switch_target_name = switch_target.name + elif isinstance(switch_target, qasm3_ast.IndexExpression): + switch_target_name, _ = Qasm3Analyzer.analyze_index_expression(switch_target) + + if not Qasm3Validator.validate_variable_type( + self._get_from_visible_scope(switch_target_name), qasm3_ast.IntType + ): + raise_qasm3_error( + f"Switch target {switch_target_name} must be of type int", span=statement.span + ) + + switch_target_val = Qasm3ExprEvaluator.evaluate_expression(switch_target) + + if len(statement.cases) == 0: + raise_qasm3_error("Switch statement must have at least one case", span=statement.span) + + # 2. handle the cases of the switch stmt + # each element in the list of the values + # should be of const int type and no duplicates should be present + + def _evaluate_case(statements): + # can not put 'context' outside + # BECAUSE the case expression CAN CONTAIN VARS from global scope + self._push_context(Context.BLOCK) + self._push_scope({}) + + for stmt in statements: + Qasm3Validator.validate_statement_type(SWITCH_BLACKLIST_STMTS, stmt, "switch") + self.visit_statement(stmt) + + self._pop_scope() + self._restore_context() + + case_fulfilled = False + for case in statement.cases: + case_list = case[0] + seen_values = set() + for case_expr in case_list: + # 3. evaluate and verify that it is a const_expression + # using vars only within the scope AND each component is either a + # literal OR type int + case_val = Qasm3ExprEvaluator.evaluate_expression( + case_expr, const_expr=True, reqd_type=qasm3_ast.IntType + ) + + if case_val in seen_values: + raise_qasm3_error( + f"Duplicate case value {case_val} in switch statement", span=case_expr.span + ) + + seen_values.add(case_val) + + if case_val == switch_target_val: + case_fulfilled = True + + if case_fulfilled: + case_stmts = case[1].statements + _evaluate_case(case_stmts) + break + + if not case_fulfilled and statement.default: + default_stmts = statement.default.statements + _evaluate_case(default_stmts) + + def visit_statement(self, statement: qasm3_ast.Statement) -> None: + """Visit a statement element. + + Args: + statement (qasm3_ast.Statement): The statement to visit. + + Returns: + None + """ + logger.debug("Visiting statement '%s'", str(statement)) + + visit_map = { + qasm3_ast.Include: lambda x: None, # No operation + qasm3_ast.QuantumMeasurementStatement: self._visit_measurement, + qasm3_ast.QuantumReset: self._visit_reset, + qasm3_ast.QuantumBarrier: self._visit_barrier, + qasm3_ast.QuantumGateDefinition: self._visit_gate_definition, + qasm3_ast.QuantumGate: self._visit_generic_gate_operation, + qasm3_ast.ClassicalDeclaration: self._visit_classical_declaration, + qasm3_ast.ClassicalAssignment: self._visit_classical_assignment, + qasm3_ast.ConstantDeclaration: self._visit_constant_declaration, + qasm3_ast.BranchingStatement: self._visit_branching_statement, + qasm3_ast.ForInLoop: self._visit_forin_loop, + qasm3_ast.AliasStatement: self._visit_alias_statement, + qasm3_ast.SwitchStatement: self._visit_switch_statement, + qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, + qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), + qasm3_ast.IODeclaration: lambda x: (_ for _ in ()).throw( + NotImplementedError("OpenQASM 3 IO declarations not yet supported") + ), + } + + visitor_function = visit_map.get(type(statement)) + + if visitor_function: + visitor_function(statement) # type: ignore[operator] + else: + raise_qasm3_error( + f"Unsupported statement of type {type(statement)}", span=statement.span + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/declarations/__init__.py b/tests/declarations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/declarations/test_classical.py b/tests/declarations/test_classical.py new file mode 100644 index 0000000..edf0a39 --- /dev/null +++ b/tests/declarations/test_classical.py @@ -0,0 +1,350 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module containing unit tests for QASM3 to QIR conversion functions. + +""" +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.validate import validate + +# from tests.qir_utils import check_attributes, check_single_qubit_rotation_op +from tests.resources.variables import ASSIGNMENT_TESTS, DECLARATION_TESTS + +# from qbraid_qir.qasm3.convert import qasm3_to_qir + + +# 1. Test scalar declarations in different ways +def test_scalar_declarations(): + """Test scalar declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + int a; + uint b; + int[2] c; + uint[3] d; + float[32] f; + float[64] g; + bit h; + bool i; + """ + + validate(qasm3_string) + + +# 2. Test const declarations in different ways +def test_const_declarations(): + """Test const declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + const int a = 5; + const uint b = 10; + const int[2*9] c = 1; + const uint[3-1] d = 2; + const bool boolean_var = true; + const float[32] f = 0.00000023; + const float[64] g = 2345623454564564564564545456456546456456456.0; + + const int a1 = 5 + a; + const uint b1 = 10 + b; + const int[2*9] c1 = 1 + 2*c + a; + const uint[6-1] d1 = 2 + d; + const bool boolean_var1 = !boolean_var; + const float[32] f1 = 0.00000023 + f; + """ + + validate(qasm3_string) + + +# 3. Test non-constant scalar assignments +def test_scalar_assignments(): + """Test scalar assignments in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + int a = 5; + uint b; + int[2*9] c = 1; + uint[3-1] d = 2; + float r; + float[32] f = 0.00000023; + float[64] g = 23456.023424983573645873465836483645876348564387; + b = 12; + r = 12.2; + """ + + validate(qasm3_string) + + +# # 4. Scalar value assignment +# def test_scalar_value_assignment(): +# """Test assigning variable values from other variables""" +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# int a = 5; +# float[32] r; +# float[32] f = 0.5; +# int b = a; +# r = 0.23; +# qubit q; +# rx(b) q; +# rx(r + f*4) q; +# """ + +# b = 5.0 +# r = 0.23 +# f = 0.5 +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 2, [0, 0], [b, r + f * 4], "rx") + + +# def test_scalar_type_casts(): +# """Test type casts on scalar variables""" +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# int[32] a = 5.1; +# float[32] r = 245; +# uint[4] b = -4; // -4 % 16 = 12 +# bit bit_var = 0; +# bool f = 0; +# bool g = 1; + +# qubit q; +# rx(a) q; +# rx(r) q; +# rx(b) q; +# rx(f) q; +# rx(g) q; + +# """ +# a = 5 +# r = 245 +# b = 12 +# f = 0 +# g = 1 + +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 1) +# check_single_qubit_rotation_op(generated_qir, 5, [0, 0, 0, 0, 0], [a, r, b, f, g], "rx") + + +# def test_array_type_casts(): +# """Test type casts on array variables""" + +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6.2} }; +# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6.2} }; +# array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, 12} }; +# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6} }; + +# qubit q; +# rx(arr_int[2][1]) q; // should be 6 +# rx(arr_uint[2][1]) q; // should be 6 +# rx(arr_bool[2][1]) q; // should be 1 (true) +# rx(arr_float32[2][1]) q; // should be 6.0 + +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 4, [0, 0, 0, 0], [6, 6, 1, 6.0], "rx") + + +# 5. Array declarations +def test_array_declarations(): + """Test array declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + array[int[32], 3, 2] arr_int; + array[uint[32-9], 3, 2] arr_uint; + array[float[32], 3, 2] arr_float32; + array[float[64], 3, 2] arr_float64; + array[bool, 3, 2] arr_bool; + """ + validate(qasm3_string) + + +# # 6. Array assignments +# def test_array_assignments(): +# """Test array assignments""" + +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] arr_int; +# array[uint[32], 3, 2] arr_uint; +# array[float[32], 3, 2] arr_float32; +# array[float[64], 3, 2] arr_float64; +# array[bool, 3, 2] arr_bool; + +# int a = 2; +# uint b = 3; +# float[32] c = 4.5; +# float[64] d = 6.7; +# bool f = true; + +# arr_int[0][1] = a*a; +# arr_int[0,1] = a*a; + +# arr_uint[0][1] = b*b; +# arr_uint[0,1] = b*b; + +# arr_float32[0][1] = c*c; +# arr_float32[0,1] = c*c; + +# arr_float64[0][1] = d*d; +# arr_float64[0,1] = d*d; + +# arr_bool[0][1] = f; +# arr_bool[0,1] = f; + +# qubit q; +# rx(arr_int[0,1]) q; +# rx(arr_uint[0][1]) q; +# rx(arr_float32[0,1]) q; +# rx(arr_float64[0][1]) q; +# rx(arr_bool[0,1]) q; +# """ + +# a = 2 +# b = 3 +# c = 4.5 +# d = 6.7 +# f = True +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op( +# generated_qir, 5, [0, 0, 0, 0, 0], [a * a, b * b, c * c, d * d, f], "rx" +# ) + + +# # 7. Test expressions which involves arrays +# def test_array_expressions(): +# """Test array expressions""" +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] arr_int; +# array[uint[32], 3, 2] arr_uint; +# array[float[32], 3, 2] arr_float32; +# array[float[64], 3, 2] arr_float64; +# array[bool, 3, 2] arr_bool; + +# int a = 2; +# uint b = 3; +# float[32] c = 4.5; +# float[64] d = 6.7; +# bool f = true; + + +# arr_int[0][1] = a*a; +# arr_int[1][0] = arr_int[0][1]; +# arr_uint[0][1] = b*b; +# arr_float32[0][1] = c*c; +# arr_float64[0][1] = d*d; + +# qubit q; +# rx(arr_int[{0,1}] + arr_uint[0][1]) q; +# rx(arr_float32[{0,1}] + arr_float64[0][1]) q; +# """ + +# a = 2 +# b = 3 +# c = 4.5 +# d = 6.7 +# f = True +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 2, [0, 0], [a * a + b * b, c * c + d * d], "rx") + + +# def test_array_initializations(): +# """Test array initializations""" + +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; +# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; +# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; +# array[float[64], 3, 2] arr_float64 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; +# array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, false} }; + +# qubit q; +# rx(arr_int[0][1]) q; +# rx(arr_uint[0][1]) q; +# rx(arr_float32[0][1]) q; +# rx(arr_float64[0][1]) q; +# rx(arr_bool[0][1]) q; +# """ + +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 5, [0, 0, 0, 0, 0], [2, 2, 2.0, 2.0, 0], "rx") + + +# def test_array_range_assignment(): +# """Test array range assignment""" + +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; +# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; +# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; + +# arr_int[0, 0:1] = arr_int[1, 0:1]; +# arr_uint[0:2, 1] = arr_uint[0:2, 0]; +# arr_float32[0:2, 1] = arr_float32[0:2, 0]; + +# qubit q; +# rx(arr_int[0][1]) q; +# rx(arr_uint[0][1]) q; +# rx(arr_float32[1][1]) q; + +# """ + +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [4, 1, 3.0], "rx") + + +@pytest.mark.parametrize("test_name", DECLARATION_TESTS.keys()) +def test_incorrect_declarations(test_name): + qasm_input, error_message = DECLARATION_TESTS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) + + +@pytest.mark.parametrize("test_name", ASSIGNMENT_TESTS.keys()) +def test_incorrect_assignments(test_name): + qasm_input, error_message = ASSIGNMENT_TESTS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py new file mode 100644 index 0000000..110765b --- /dev/null +++ b/tests/declarations/test_quantum.py @@ -0,0 +1,129 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_unrolled_qasm + + +# 1. Test qubit declarations in different ways +def test_qubit_declarations(): + """Test qubit declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit q1; + qubit[2] q2; + qreg q3[3]; + qubit[1] q4; + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 7 + // Total number of clbits: 0 + qubit[1] q1; + qubit[2] q2; + qubit[3] q3; + qubit[1] q4; + """ + + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +# 2. Test clbit declarations in different ways +def test_clbit_declarations(): + """Test clbit declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + bit c1; + bit[2] c2; + creg c3[3]; + bit[1] c4; + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 0 + // Total number of clbits: 7 + bit[1] c1; + bit[2] c2; + bit[3] c3; + bit[1] c4; + """ + + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +# 3. Test qubit and clbit declarations in different ways +def test_qubit_clbit_declarations(): + """Test qubit and clbit declarations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + // qubit declarations + qubit q1; + qubit[2] q2; + qreg q3[3]; + qubit[1] q4; + + // clbit declarations + bit c1; + bit[2] c2; + creg c3[3]; + bit[1] c4; + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 7 + // Total number of clbits: 7 + qubit[1] q1; + qubit[2] q2; + qubit[3] q3; + qubit[1] q4; + bit[1] c1; + bit[2] c2; + bit[3] c3; + bit[1] c4; + """ + + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_qubit_redeclaration_error(): + """Test redeclaration of qubit""" + with pytest.raises(ValidationError, match="Invalid declaration of register with name 'q1'"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit q1; + qubit q1; + """ + validate(qasm3_string) + + +def test_clbit_redeclaration_error(): + """Test redeclaration of clbit""" + with pytest.raises(ValidationError, match="Invalid declaration of register with name 'c1'"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + bit c1; + bit[4] c1; + """ + validate(qasm3_string) diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/gates.py b/tests/resources/gates.py new file mode 100644 index 0000000..a5caba4 --- /dev/null +++ b/tests/resources/gates.py @@ -0,0 +1,9 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. diff --git a/tests/resources/subroutines.py b/tests/resources/subroutines.py new file mode 100644 index 0000000..a5caba4 --- /dev/null +++ b/tests/resources/subroutines.py @@ -0,0 +1,9 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. diff --git a/tests/resources/variables.py b/tests/resources/variables.py new file mode 100644 index 0000000..f7a0bc0 --- /dev/null +++ b/tests/resources/variables.py @@ -0,0 +1,264 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module defining QASM3 incorrect variable tests. + +""" + +DECLARATION_TESTS = { + "keyword_redeclaration": ( + """ + OPENQASM 3.0; + include "stddgates.inc"; + int pi; + """, + "Can not declare variable with keyword name pi", + ), + "const_keyword_redeclaration": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + const int pi = 3; + """, + "Can not declare variable with keyword name pi", + ), + "variable_redeclaration": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int x; + float y = 3.4; + uint x; + """, + "Re-declaration of variable x", + ), + "variable_redeclaration_with_qubits_1": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int x; + qubit x; + """, + "Invalid declaration of register with name 'x'", + ), + "variable_redeclaration_with_qubits_2": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit x; + int x; + """, + "Re-declaration of variable x", + ), + "const_variable_redeclaration": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + const int x = 3; + const float x = 3.4; + """, + "Re-declaration of variable x", + ), + "invalid_int_size": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int[32.1] x; + """, + "Invalid base size 32.1 for variable x", + ), + "invalid_const_int_size": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + const int[32.1] x = 3; + """, + "Invalid base size 32.1 for variable x", + ), + "const_declaration_with_non_const": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int[32] x = 5; + const int[32] y = x + 5; + """, + "Variable 'x' is not a constant in given expression", + ), + "const_declaration_with_non_const_size": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + int[32] x = 5; + const int[x] y = 5; + """, + "Variable 'x' is not a constant in given expression", + ), + "invalid_float_size": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + float[23] x; + """, + "Invalid base size 23 for float variable x", + ), + "unsupported_types": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + angle x = 3.4; + """, + "Invalid type for variable x", + ), + "imaginary_variable": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + int x = 1 + 3im; + """, + "Unsupported expression type ", + ), + "invalid_array_dimensions": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 1, 2.1] x; + """, + "Invalid dimension size 2.1 in array declaration for x", + ), + "extra_array_dimensions": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 1, 2, 3, 4, 5, 6, 7, 8] x; + """, + "Invalid dimensions 8 for array declaration for x. Max allowed dimensions is 7", + ), + "dimension_mismatch_1": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 1, 2] x = {1,2,3}; + """, + "Invalid dimensions for array assignment to variable x. Expected 1 but got 3", + ), + "dimension_mismatch_2": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 3, 1, 2] x = {1,2,3}; + """, + "Invalid dimensions for array assignment to variable x. Expected 3 but got 1", + ), +} + +ASSIGNMENT_TESTS = { + "undefined_variable_assignment": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + float k; + + x = 3; + + """, + "Undefined variable x in assignment", + ), + "assignment_to_constant": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int x = 3; + x = 4; + """, + "Assignment to constant variable x not allowed", + ), + "invalid_assignment_type": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + bit x = 3.3; + """, + ( + "Cannot cast to . " + "Invalid assignment of type to variable x of type " + "" + ), + ), + "int_out_of_range": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + int[32] x = 1<<64; + """, + f"Value {2**64} out of limits for variable x with base size 32", + ), + "float32_out_of_range": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + float[32] x = 123456789123456789123456789123456789123456789.1; + """, + "Value .* out of limits for variable x with base size 32", + ), + "indexing_non_array": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + int x = 3; + x[0] = 4; + """, + "Indexing error. Variable x is not an array", + ), + "incorrect_num_dims": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 1, 2, 3] x; + x[0] = 3; + """, + "Invalid number of indices for variable x. Expected 3 but got 1", + ), + "non_nnint_index": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 3] x; + x[0.1] = 3; + """, + "Invalid value 0.1 with type for " + "required type ", + ), + "index_out_of_range": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + array[int[32], 3] x; + x[3] = 3; + """, + "Index 3 out of bounds for dimension 0 of variable x", + ), +} diff --git a/tests/subroutines/__init__.py b/tests/subroutines/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py new file mode 100644 index 0000000..a5caba4 --- /dev/null +++ b/tests/subroutines/test_subroutines.py @@ -0,0 +1,9 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. diff --git a/tests/subroutines/test_subroutines_arrays.py b/tests/subroutines/test_subroutines_arrays.py new file mode 100644 index 0000000..a5caba4 --- /dev/null +++ b/tests/subroutines/test_subroutines_arrays.py @@ -0,0 +1,9 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. diff --git a/tests/test_alias.py b/tests/test_alias.py new file mode 100644 index 0000000..19591b9 --- /dev/null +++ b/tests/test_alias.py @@ -0,0 +1,306 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module containing unit tests for unrolling OpenQASM 3 programs +with alias statements. + +""" + +# import re + +# import pytest +# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir + +# from tests.qir_utils import ( +# check_attributes, +# check_single_qubit_gate_op, +# check_three_qubit_gate_op, +# check_two_qubit_gate_op, +# ) + +# from .test_if import compare_reference_ir, resources_file + + +# def test_alias(): +# """Test converting OpenQASM 3 program with openqasm3.ast.AliasStatement.""" + +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; + +# let myqreg0 = q; +# let myqreg1 = q[1]; +# let myqreg2 = q[1:]; +# let myqreg3 = q[:4]; +# let myqreg4 = q[1:4]; +# let myqreg5 = q[1:2:4]; +# let myqreg6 = q[{0, 1}]; + +# x myqreg0[0]; +# h myqreg1; +# cx myqreg2[0], myqreg2[1]; +# cx myqreg3[2], myqreg3[3]; +# ccx myqreg4; +# swap myqreg5[0], myqreg5[1]; +# cz myqreg6; +# """ +# result = qasm3_to_qir(qasm3_alias_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 5) + +# check_single_qubit_gate_op(generated_qir, 1, [0], "x") +# check_single_qubit_gate_op(generated_qir, 1, [1], "h") +# check_two_qubit_gate_op(generated_qir, 2, [[1, 2], [2, 3]], "cx") +# check_three_qubit_gate_op(generated_qir, 1, [[1, 2, 3]], "ccx") +# check_two_qubit_gate_op(generated_qir, 1, [[1, 3]], "swap") +# check_two_qubit_gate_op(generated_qir, 1, [[0, 1]], "cz") + + +# def test_alias_update(): +# """Test converting OpenQASM 3 program with alias update.""" + +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; + +# let alias = q[1:]; +# let alias = q[2:]; + +# x alias[1]; +# """ +# result = qasm3_to_qir(qasm3_alias_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 4) + +# check_single_qubit_gate_op(generated_qir, 1, [3], "x") + + +# def test_valid_alias_redefinition(): +# """Test converting OpenQASM 3 program with redefined alias in scope.""" + +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; +# bit[5] c; +# h q; +# measure q -> c; + +# if (c[0] == 1) { +# float[32] alias = 4.3; +# } +# // valid alias +# let alias = q[2]; +# x alias; +# """ +# result = qasm3_to_qir(qasm3_alias_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 5, 5) +# check_single_qubit_gate_op(generated_qir, 1, [2], "x") + + +# def test_alias_wrong_indexing(): +# """Test converting OpenQASM 3 program with wrong alias indexing.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=re.escape( +# r"An index set can be specified by a single integer (signed or unsigned), " +# "a comma-separated list of integers contained in braces {a,b,c,…}, or a range" +# ), +# ): +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; + +# let myqreg = q[1,2]; + +# x myqreg[0]; +# """ +# _ = qasm3_to_qir(qasm3_alias_program, name="test") + + +# def test_alias_invalid_discrete_indexing(): +# """Test converting OpenQASM 3 program with invalid alias discrete indexing.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Unsupported discrete set value .*", +# ): +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; + +# let myqreg = q[{0.1}]; + +# x myqreg[0]; +# """ +# _ = qasm3_to_qir(qasm3_alias_program, name="test") + + +# def test_invalid_alias_redefinition(): +# """Test converting OpenQASM 3 program with redefined alias.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=re.escape(r"Re-declaration of variable 'alias'"), +# ): +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; +# float[32] alias = 4.2; + +# let alias = q[2]; + +# x alias; +# """ +# _ = qasm3_to_qir(qasm3_alias_program, name="test") + + +# def test_alias_defined_before(): +# """Test converting OpenQASM 3 program with alias defined before the qubit register.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=re.escape(r"Qubit register q2 not found for aliasing"), +# ): +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q1; + +# let myqreg = q2[1]; +# """ +# _ = qasm3_to_qir(qasm3_alias_program, name="test") + + +# def test_unsupported_alias(): +# """Test converting OpenQASM 3 program with unsupported alias.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Unsupported aliasing .*", +# ): +# qasm3_alias_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[5] q; + +# let myqreg = q[0] ++ q[1]; +# """ +# _ = qasm3_to_qir(qasm3_alias_program, name="test") + + +# def test_alias_in_scope_1(): +# """Test converting OpenQASM 3 program with alias in scope.""" +# qasm = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[4] q; +# bit[4] c; + +# h q; +# measure q -> c; +# if(c[0]){ +# let alias = q[0:2]; +# x alias[0]; +# cx alias[0], alias[1]; +# } + +# if(c[1] == 1){ +# cx q[1], q[2]; +# } + +# if(!c[2]){ +# h q[2]; +# } +# """ +# result = qasm3_to_qir(qasm) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 4, 4) +# simple_file = resources_file("simple_if.ll") +# compare_reference_ir(result.bitcode, simple_file) + + +# def test_alias_in_scope_2(): +# """Test converting OpenQASM 3 program with alias in scope.""" +# qasm = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[4] q; +# bit[4] c; + +# let alias = q[0:2]; + +# h q; +# measure q -> c; +# if(c[0]){ +# x alias[0]; +# cx alias[0], alias[1]; +# } + +# if(c[1] == 1){ +# cx alias[1], q[2]; +# } + +# if(!c[2]){ +# h q[2]; +# } +# """ +# result = qasm3_to_qir(qasm) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 4, 4) +# simple_file = resources_file("simple_if.ll") +# compare_reference_ir(result.bitcode, simple_file) + + +# def test_alias_out_of_scope(): +# """Test converting OpenQASM 3 program with alias out of scope.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Variable alias not in scope for operation .*", +# ): +# qasm = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[4] q; +# bit[4] c; + +# h q; +# measure q -> c; +# if(c[0]){ +# let alias = q[0:2]; +# x alias[0]; +# cx alias[0], alias[1]; +# } + +# if(c[1] == 1){ +# cx alias[1], q[2]; +# } + +# if(!c[2]){ +# h q[2]; +# } +# """ +# _ = qasm3_to_qir(qasm) diff --git a/tests/test_barrier.py b/tests/test_barrier.py new file mode 100644 index 0000000..0804cdc --- /dev/null +++ b/tests/test_barrier.py @@ -0,0 +1,132 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +""" +Module containing unit tests for the barrier operation. + +""" +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_unrolled_qasm + + +# 1. Test barrier operations in different ways +def test_barrier(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + qubit[3] q2; + qubit q3; + + // full qubits + barrier q1, q2, q3; + barrier q1[0], q1[1], q2[:], q3[0]; + + // subset of qubits + barrier q1, q2[0:2], q3[:]; + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 6 + // Total number of clbits: 0 + qubit[2] q1; + qubit[3] q2; + qubit[1] q3; + barrier q1[0]; + barrier q1[1]; + barrier q2[0]; + barrier q2[1]; + barrier q2[2]; + barrier q3[0]; + barrier q1[0]; + barrier q1[1]; + barrier q2[0]; + barrier q2[1]; + barrier q2[2]; + barrier q3[0]; + barrier q1[0]; + barrier q1[1]; + barrier q2[0]; + barrier q2[1]; + barrier q3[0]; + """ + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_barrier_in_function(): + """Test that a barrier in a function is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[4] a) { + barrier a; + return; + } + qubit[4] q; + my_function(q); + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 4 + // Total number of clbits: 0 + qubit[4] q; + barrier q[0]; + barrier q[1]; + barrier q[2]; + barrier q[3]; + """ + unrolled_qasm = unroll(qasm_str).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_incorrect_barrier(): + + undeclared = """ + OPENQASM 3; + + qubit[3] q1; + + barrier q2; + """ + + with pytest.raises(ValidationError, match=r"Missing register declaration for q2 .*"): + _ = validate(undeclared) + + out_of_bounds = """ + OPENQASM 3; + + qubit[2] q1; + + barrier q1[:4]; + """ + + with pytest.raises( + ValidationError, match="Index 3 out of range for register of size 2 in qubit" + ): + _ = validate(out_of_bounds) + + duplicate = """ + OPENQASM 3; + + qubit[2] q1; + + barrier q1, q1; + """ + + with pytest.raises(ValidationError, match=r"Duplicate qubit .*argument"): + _ = validate(duplicate) diff --git a/tests/test_expressions.py b/tests/test_expressions.py new file mode 100644 index 0000000..808b653 --- /dev/null +++ b/tests/test_expressions.py @@ -0,0 +1,69 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for expressions. + +""" +# import pytest +# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir + +# from tests.qir_utils import check_attributes, check_expressions + + +# def test_correct_expressions(): +# qasm_str = """OPENQASM 3; +# qubit q; + +# // supported +# rx(1.57) q; +# rz(3-2*3) q; +# rz(3-2*3*(8/2)) q; +# rx(-1.57) q; +# rx(4%2) q; +# rx(true) q; +# rx(!0) q; +# rx(~3) q; + +# int a = 5; +# float b = 10*a*pi; +# array[int[32], 2] c; +# c[0] = 1; +# c[1] = c[0] + 2; + + +# """ + +# result = qasm3_to_qir(qasm_str) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1, 0) +# gates = ["rx", "rz", "rz", "rx", "rx"] +# expression_values = [1.57, 3 - 2 * 3, 3 - 2 * 3 * (8 / 2), -1.57, 4 % 2] +# qubits = [0, 0, 0, 0, 0] +# check_expressions(generated_qir, 5, gates, expression_values, qubits) + + +# def test_incorrect_expressions(): +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported expression type .*"): +# qasm3_to_qir("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;") +# with pytest.raises( +# Qasm3ConversionError, match=r"Unsupported expression type .* in ~ operation" +# ): +# qasm3_to_qir("OPENQASM 3; qubit q; rx(~1.3) q;") +# with pytest.raises( +# Qasm3ConversionError, match=r"Unsupported expression type .* in ~ operation" +# ): +# qasm3_to_qir("OPENQASM 3; qubit q; rx(~1.3+5im) q;") + +# with pytest.raises(Qasm3ConversionError, match="Undefined identifier x in expression"): +# qasm3_to_qir("OPENQASM 3; qubit q; rx(x) q;") + +# with pytest.raises(Qasm3ConversionError, match="Uninitialized variable x in expression"): +# qasm3_to_qir("OPENQASM 3; qubit q; int x; rx(x) q;") diff --git a/tests/test_gates.py b/tests/test_gates.py new file mode 100644 index 0000000..75b84da --- /dev/null +++ b/tests/test_gates.py @@ -0,0 +1,266 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for unrolling quantum gates. + +""" +# import pytest +# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir + +# from tests.qasm3_qir.fixtures.gates import ( +# CUSTOM_GATE_INCORRECT_TESTS, +# SINGLE_QUBIT_GATE_INCORRECT_TESTS, +# custom_op_tests, +# double_op_tests, +# rotation_tests, +# single_op_tests, +# triple_op_tests, +# ) +# from tests.qir_utils import ( +# check_attributes, +# check_custom_qasm_gate_op, +# check_single_qubit_gate_op, +# check_single_qubit_rotation_op, +# check_three_qubit_gate_op, +# check_two_qubit_gate_op, +# ) + + +# # 7. Test gate operations in different ways +# @pytest.mark.parametrize("circuit_name", single_op_tests) +# def test_single_qubit_qasm3_gates(circuit_name, request): +# # see _generate_one_qubit_fixture for details +# qubit_list = [0, 1, 0, 0, 1] +# gate_name = circuit_name.removeprefix("Fixture_") + +# qasm3_string = request.getfixturevalue(circuit_name) +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_gate_op(generated_qir, 5, qubit_list, gate_name) + + +# @pytest.mark.parametrize("circuit_name", double_op_tests) +# def test_two_qubit_qasm3_gates(circuit_name, request): +# qubit_list = [[0, 1], [0, 1]] +# gate_name = circuit_name.removeprefix("Fixture_") + +# qasm3_string = request.getfixturevalue(circuit_name) +# result = qasm3_to_qir(qasm3_string) + +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_two_qubit_gate_op(generated_qir, 2, qubit_list, gate_name) + + +# @pytest.mark.parametrize("circuit_name", rotation_tests) +# def test_rotation_qasm3_gates(circuit_name, request): +# qubit_list = [0, 1, 0] +# param_list = [0.5, 0.5, 0.5] +# gate_name = circuit_name.removeprefix("Fixture_") + +# qasm3_string = request.getfixturevalue(circuit_name) +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_rotation_op(generated_qir, 3, qubit_list, param_list, gate_name) + + +# @pytest.mark.parametrize("circuit_name", triple_op_tests) +# def test_three_qubit_qasm3_gates(circuit_name, request): +# qubit_list = [[0, 1, 2], [0, 1, 2]] +# gate_name = circuit_name.removeprefix("Fixture_") + +# qasm3_string = request.getfixturevalue(circuit_name) +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 3, 0) +# check_three_qubit_gate_op(generated_qir, 2, qubit_list, gate_name) + + +# def test_gate_body_param_expression(): +# qasm3_str = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# gate my_gate_2(p) q { +# ry(p * 2) q; +# } + +# gate my_gate(a, b, c) q { +# rx(5 * a) q; +# rz(2 * b / a) q; +# my_gate_2(a) q; +# rx(!a) q; // not a = False +# rx(c) q; +# } + +# qubit q; +# int[32] m = 3; +# float[32] n = 6.0; +# bool o = true; +# my_gate(m, n, o) q; +# """ +# result = qasm3_to_qir(qasm3_str) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [5 * 3, 0.0, True], "rx") +# check_single_qubit_rotation_op(generated_qir, 1, [0], [2 * 6.0 / 3], "rz") +# check_single_qubit_rotation_op(generated_qir, 1, [0], [3 * 2], "ry") + + +# def test_id_gate(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# qubit q; +# id q; +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# # we have 2 X gates for id +# check_single_qubit_gate_op(generated_qir, 2, [0, 0], "x") + + +# def test_qasm_u3_gates(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# qubit[2] q1; +# u3(0.5, 0.5, 0.5) q1[0]; +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_rotation_op(generated_qir, 1, [0], [0.5, 0.5, 0.5], "u3") + + +# def test_qasm_u2_gates(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# qubit[2] q1; +# u2(0.5, 0.5) q1[0]; +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_rotation_op(generated_qir, 1, [0], [0.5, 0.5], "u2") + + +# @pytest.mark.parametrize("test_name", SINGLE_QUBIT_GATE_INCORRECT_TESTS.keys()) +# def test_incorrect_single_qubit_gates(test_name): +# qasm_input, error_message = SINGLE_QUBIT_GATE_INCORRECT_TESTS[test_name] +# with pytest.raises(Qasm3ConversionError, match=error_message): +# _ = qasm3_to_qir(qasm_input) + + +# @pytest.mark.parametrize("test_name", custom_op_tests) +# def test_custom_ops(test_name, request): +# qasm3_string = request.getfixturevalue(test_name) +# gate_type = test_name.removeprefix("Fixture_") +# result = qasm3_to_qir(qasm3_string) + +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) + +# # Check for custom gate definition +# check_custom_qasm_gate_op(generated_qir, gate_type) + + +# def test_pow_gate_modifier(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit q; +# inv @ pow(2) @ pow(4) @ h q; +# pow(-2) @ h q; +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_gate_op(generated_qir, 10, [0] * 10, "h") + + +# def test_inv_gate_modifier(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit q; +# inv @ h q; +# inv @ y q; +# inv @ rx(0.5) q; +# inv @ s q; + +# qubit[2] q2; +# inv @ cx q2; +# inv @ ccx q[0], q2; +# inv @ u2(0.5, 0.5) q2[0]; +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 3, 0) +# check_single_qubit_gate_op(generated_qir, 1, [0], "h") +# check_single_qubit_gate_op(generated_qir, 1, [0], "y") +# check_single_qubit_rotation_op(generated_qir, 1, [0], [-0.5], "rx") +# check_single_qubit_gate_op(generated_qir, 1, [0], "sdg") +# check_two_qubit_gate_op(generated_qir, 1, [[1, 2]], "cx") +# check_three_qubit_gate_op(generated_qir, 1, [[0, 1, 2]], "ccx") + + +# def test_nested_gate_modifiers(): +# complex_qir = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# gate custom2 p, q{ +# y p; +# z q; +# } +# gate custom p, q { +# pow(1) @ custom2 p, q; +# } +# pow(1) @ inv @ pow(2) @ custom q; +# pow(-1) @ custom q; +# """ +# ) +# generated_qir = str(complex_qir).splitlines() +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_gate_op(generated_qir, 2, [0, 0, 0], "y") +# check_single_qubit_gate_op(generated_qir, 2, [1, 1, 1], "z") + + +# def test_unsupported_modifiers(): +# # TO DO : add implementations, but till then we have tests +# for modifier in ["ctrl", "negctrl"]: +# with pytest.raises( +# NotImplementedError, +# match=r"Controlled modifier gates not yet supported .*", +# ): +# _ = qasm3_to_qir( +# f""" +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# {modifier} @ h q[0], q[1]; +# """ +# ) + + +# @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) +# def test_incorrect_custom_ops(test_name): +# qasm_input, error_message = CUSTOM_GATE_INCORRECT_TESTS[test_name] +# with pytest.raises(Qasm3ConversionError, match=error_message): +# _ = qasm3_to_qir(qasm_input) diff --git a/tests/test_if.py b/tests/test_if.py new file mode 100644 index 0000000..589b175 --- /dev/null +++ b/tests/test_if.py @@ -0,0 +1,245 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for the if statements. + +""" + +# import os +# from pathlib import Path + +# import pyqir +# import pytest +# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir + +# from tests.qir_utils import check_attributes, get_entry_point_body + +# RESOURCES_DIR = os.path.join( +# os.path.dirname(__file__).removesuffix("converter"), "fixtures/resources" +# ) + + +# def resources_file(filename: str) -> str: +# return str(os.path.join(RESOURCES_DIR, f"{filename}")) + + +# def compare_reference_ir(generated_bitcode: bytes, file_path: str) -> None: +# module = pyqir.Module.from_bitcode(pyqir.Context(), generated_bitcode, f"{file_path}") +# ir = str(module) +# pyqir_ir_body = get_entry_point_body(ir.splitlines()) + +# expected = Path(file_path).read_text(encoding="utf-8") +# expected_ir_body = get_entry_point_body(expected.splitlines()) +# assert pyqir_ir_body == expected_ir_body + + +# def test_simple_if(): +# qasm = """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[4] q; +# bit[4] c; +# h q; +# measure q -> c; +# if(c[0]){ +# x q[0]; +# cx q[0], q[1]; +# } + +# if(c[1] == 1){ +# cx q[1], q[2]; +# } + +# if(!c[2]){ +# h q[2]; +# } +# """ +# result = qasm3_to_qir(qasm) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 4, 4) +# simple_file = resources_file("simple_if.ll") +# compare_reference_ir(result.bitcode, simple_file) + + +# def test_complex_if(): +# qasm = """ +# OPENQASM 3; +# include "stdgates.inc"; +# gate custom a, b{ +# cx a, b; +# h a; +# } +# qubit[4] q; +# bit[4] c; +# bit[4] c0; + +# h q; +# measure q -> c0; +# if(c0[0]){ +# x q[0]; +# cx q[0], q[1]; +# if (c0[1]){ +# cx q[1], q[2]; +# } +# } +# if (c[0]){ +# custom q[2], q[3]; +# } +# """ +# result = qasm3_to_qir(qasm) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 4, 8) +# complex_if = resources_file("complex_if.ll") +# compare_reference_ir(result.bitcode, complex_if) + + +# def test_incorrect_if(): +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported expression type .*"): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c == 4){ +# cx q; +# } +# """ +# ) + +# with pytest.raises(Qasm3ConversionError, match=r"Missing if block"): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c[0]){ +# } +# """ +# ) + +# with pytest.raises(Qasm3ConversionError, match=r"Missing register declaration for c2 .*"): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c2[0]){ +# cx q; +# } +# """ +# ) + +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported unary expression .*"): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(~c[0]){ +# cx q; +# } +# """ +# ) +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported binary expression .*"): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c[0] >= 1){ +# cx q; +# } +# """ +# ) +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Unsupported expression type .* in if condition. Can only be a simple comparison", +# ): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c){ +# cx q; +# } +# """ +# ) +# with pytest.raises( +# Qasm3ConversionError, +# match=r"RangeDefinition not supported in branching condition", +# ): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c[0:1]){ +# cx q; +# } +# """ +# ) + +# with pytest.raises( +# Qasm3ConversionError, +# match=r"DiscreteSet not supported in branching condition", +# ): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3; +# include "stdgates.inc"; +# qubit[2] q; +# bit[2] c; + +# h q; +# measure q->c; + +# if(c[{0,1}]){ +# cx q; +# } +# """ +# ) diff --git a/tests/test_loop.py b/tests/test_loop.py new file mode 100644 index 0000000..53eb586 --- /dev/null +++ b/tests/test_loop.py @@ -0,0 +1,362 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for parsing and unrolling programs that contain loops. + +""" + +# import pytest +# from qbraid_qir.qasm3 import qasm3_to_qir +# from qbraid_qir.qasm3.visitor import Qasm3ConversionError + +# from tests.qir_utils import ( +# check_attributes, +# check_single_qubit_gate_op, +# check_single_qubit_rotation_op, +# ) + +# EXAMPLE_WITHOUT_LOOP = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# h q; + +# cx q[0], q[1]; +# cx q[1], q[2]; +# cx q[2], q[3]; + +# measure q->c; +# """ + + +# def test_convert_qasm3_for_loop(): +# """Test converting a QASM3 program that contains a for loop.""" +# qir_expected = qasm3_to_qir(EXAMPLE_WITHOUT_LOOP, name="test") +# qir_from_loop = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# h q; +# for int i in [0:2]{ +# cx q[i], q[i+1]; +# } +# measure q->c; +# """, +# name="test", +# ) +# assert str(qir_expected) == str(qir_from_loop) +# assert str(qir_from_loop) == EXAMPLE_QIR_OUTPUT + + +# def test_convert_qasm3_for_loop_shadow(): +# """Test for loop where loop variable shadows variable from global scope.""" +# qir_expected = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int i = 3; + +# h q; +# cx q[0], q[1]; +# cx q[1], q[2]; +# cx q[2], q[3]; +# h q[i]; +# measure q->c; +# """, +# name="test", +# ) +# qir_from_loop = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int i = 3; + +# h q; +# for int i in [0:2]{ +# cx q[i], q[i+1]; +# } +# h q[i]; +# measure q->c; +# """, +# name="test", +# ) +# assert str(qir_expected) == str(qir_from_loop) + + +# def test_convert_qasm3_for_loop_enclosing(): +# """Test for loop where variable from outer loop is accessed from inside the loop.""" +# qir_expected = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int j = 3; + +# h q; +# cx q[0], q[1]; +# h q[j]; +# cx q[1], q[2]; +# h q[j]; +# cx q[2], q[3]; +# h q[j]; +# measure q->c; +# """, +# name="test", +# ) +# qir_from_loop = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int j = 3; + +# h q; +# for int i in [0:2]{ +# cx q[i], q[i+1]; +# h q[j]; +# } +# measure q->c; +# """, +# name="test", +# ) +# assert str(qir_expected) == str(qir_from_loop) + + +# def test_convert_qasm3_for_loop_enclosing_modifying(): +# """Test for loop where variable from outer loop is modified from inside the loop.""" +# qir_expected = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int j = 0; + +# h q; +# cx q[0], q[1]; +# h q[j]; +# j += 1; +# cx q[1], q[2]; +# h q[j]; +# j += 1; +# cx q[2], q[3]; +# h q[j]; +# j += 1; + +# h q[j]; +# measure q->c; +# """, +# name="test", +# ) +# qir_from_loop = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# int j = 0; + +# h q; +# for int i in [0:2]{ +# cx q[i], q[i+1]; +# h q[j]; +# j += 1; +# } +# h q[j]; +# measure q->c; +# """, +# name="test", +# ) +# assert str(qir_expected) == str(qir_from_loop) + + +# def test_convert_qasm3_for_loop_discrete_set(): +# """Test converting a QASM3 program that contains a for loop initialized from a DiscreteSet.""" +# qir_expected = qasm3_to_qir(EXAMPLE_WITHOUT_LOOP, name="test") +# qir_from_loop = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# h q; +# for int i in {0, 1, 2} { +# cx q[i], q[i+1]; +# } +# measure q->c; +# """, +# name="test", +# ) +# assert str(qir_expected) == str(qir_from_loop) +# assert str(qir_from_loop) == EXAMPLE_QIR_OUTPUT + + +# def test_function_executed_in_loop(): +# """Test that a function executed in a loop is correctly parsed.""" +# qasm_str = """OPENQASM 3; +# include "stdgates.inc"; + +# def my_function(qubit q_arg, float[32] b) { +# rx(b) q_arg; +# return; +# } +# qubit[5] q; + +# int[32] n = 2; +# float[32] b = 3.14; + +# for int i in [0:n] { +# my_function(q[i], i*b); +# } +# """ + +# result = qasm3_to_qir(qasm_str) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 5, 0) +# check_single_qubit_rotation_op(generated_qir, 3, list(range(3)), [0, 3.14, 2 * 3.14], "rx") + + +# def test_loop_inside_function(): +# """Test that a function with a loop is correctly parsed.""" +# qasm_str = """OPENQASM 3; +# include "stdgates.inc"; + +# def my_function(qubit[3] q2) { +# for int[32] i in [0:2] { +# h q2[i]; +# } +# return; +# } +# qubit[3] q1; +# my_function(q1); +# """ + +# result = qasm3_to_qir(qasm_str) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 3, 0) +# check_single_qubit_gate_op(generated_qir, 3, [0, 1, 2], "h") + + +# def test_function_in_nested_loop(): +# """Test that a function executed in a nested loop is correctly parsed.""" +# qasm_str = """OPENQASM 3; +# include "stdgates.inc"; + +# def my_function(qubit q_arg, float[32] b) { +# rx(b) q_arg; +# return; +# } +# qubit[5] q; + +# int[32] n = 2; +# float[32] b = 3.14; + +# for int i in [0:n] { +# for int j in [0:n] { +# my_function(q[i], j*b); +# } +# } + +# my_function(q[0], 2*b); +# """ + +# result = qasm3_to_qir(qasm_str) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 5, 0) +# check_single_qubit_rotation_op( +# generated_qir, +# 9, +# [0, 0, 0, 1, 1, 1, 2, 2, 2, 0], +# [0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 2 * 3.14], +# "rx", +# ) + + +# @pytest.mark.skip(reason="Not implemented nested functions yet") +# def test_loop_in_nested_function_call(): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; +# def my_function_1(qubit q1, int[32] a){ +# for int[32] i in [0:2]{ +# rx(a*i) q1; +# } +# } + +# def my_function_2(qubit q2, int[32] b){ +# my_function_1(q2, b); +# } + +# qubit q; +# my_function_2(q, 3); +# """ +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [0, 3, 6], "rx") + + +# def test_convert_qasm3_for_loop_unsupported_type(): +# """Test correct error when converting a QASM3 program that contains a for loop initialized from +# an unsupported type.""" +# with pytest.raises( +# Qasm3ConversionError, +# match=( +# "Unexpected type " +# " of set_declaration in loop." +# ), +# ): +# _ = qasm3_to_qir( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# qubit[4] q; +# bit[4] c; + +# h q; +# for bit b in "001" { +# x q[b]; +# } +# measure q->c; +# """, +# name="test", +# ) diff --git a/tests/test_measurement.py b/tests/test_measurement.py new file mode 100644 index 0000000..51788b9 --- /dev/null +++ b/tests/test_measurement.py @@ -0,0 +1,137 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for unrolling measurement operations. + +""" +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_unrolled_qasm + + +# 6. Test measurement operations in different ways +def test_measure(): + qasm3_string = """ + OPENQASM 3; + + qubit[2] q1; + qubit[5] q2; + qubit q3; + + bit[2] c1; + bit c2; + + // supported + c1 = measure q1; + measure q1 -> c1; + c2[0] = measure q3[0]; + measure q1[:1] -> c1[1]; + measure q2[{0, 1}] -> c1[{1, 0}]; + + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 8 + // Total number of clbits: 3 + qubit[2] q1; + qubit[5] q2; + qubit[1] q3; + bit[2] c1; + bit[1] c2; + measure q1[0] -> c1[0]; + measure q1[1] -> c1[1]; + measure q1[0] -> c1[0]; + measure q1[1] -> c1[1]; + measure q3[0] -> c2[0]; + measure q1[0] -> c1[1]; + measure q2[0] -> c1[1]; + measure q2[1] -> c1[0]; + """ + + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_incorrect_measure(): + def run_test(qasm3_code, error_message): + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm3_code) + + # Test for undeclared register q2 + run_test( + """ + OPENQASM 3; + qubit[2] q1; + bit[2] c1; + c1[0] = measure q2[0]; // undeclared register + """, + r"Missing register declaration for q2 .*", + ) + + # Test for undeclared register c2 + run_test( + """ + OPENQASM 3; + qubit[2] q1; + bit[2] c1; + measure q1 -> c2; // undeclared register + """, + r"Missing register declaration for c2 .*", + ) + + # Test for size mismatch between q1 and c2 + run_test( + """ + OPENQASM 3; + qubit[2] q1; + bit[2] c1; + bit[1] c2; + c2 = measure q1; // size mismatch + """, + r"Register sizes of q1 and c2 do not match .*", + ) + + # Test for size mismatch between q1 and c2 in ranges + run_test( + """ + OPENQASM 3; + qubit[5] q1; + bit[4] c1; + bit[1] c2; + c1[:3] = measure q1; // size mismatch + """, + r"Register sizes of q1 and c1 do not match .*", + ) + + # Test for out of bounds index for q1 + run_test( + """ + OPENQASM 3; + qubit[2] q1; + bit[2] c1; + measure q1[3] -> c1[0]; // out of bounds + """, + r"Index 3 out of range for register of size 2 in qubit", + ) + + # Test for out of bounds index for c1 + run_test( + """ + OPENQASM 3; + qubit[2] q1; + bit[2] c1; + measure q1 -> c1[3]; // out of bounds + """, + r"Index 3 out of range for register of size 2 in clbit", + ) diff --git a/tests/test_reset.py b/tests/test_reset.py new file mode 100644 index 0000000..a8ce9e5 --- /dev/null +++ b/tests/test_reset.py @@ -0,0 +1,107 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for reset operation. + +""" +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_unrolled_qasm + + +# 4. Test reset operations in different ways +def test_reset_operations(): + """Test reset operations in different ways""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + // qubit declarations + qubit q1; + qubit[2] q2; + qreg q3[3]; + + // reset operations + reset q1; + reset q2[1]; + reset q3[2]; + reset q3[:2]; + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 6 + // Total number of clbits: 0 + qubit[1] q1; + qubit[2] q2; + qubit[3] q3; + reset q1[0]; + reset q2[1]; + reset q3[2]; + reset q3[0]; + reset q3[1]; + """ + + unrolled_qasm = unroll(qasm3_string).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_reset_inside_function(): + """Test that a qubit reset inside a function is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit a) { + reset a; + return; + } + qubit[3] q; + my_function(q[1]); + """ + + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + // Total number of qubits: 3 + // Total number of clbits: 0 + qubit[3] q; + reset q[1]; + """ + + unrolled_qasm = unroll(qasm_str).unrolled_qasm + check_unrolled_qasm(unrolled_qasm, expected_qasm) + + +def test_incorrect_resets(): + undeclared = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[3] q1; + + // undeclared register + reset q2[0]; + """ + with pytest.raises(ValidationError): + _ = validate(undeclared) + + index_error = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + + // out of bounds + reset q1[4]; + """ + with pytest.raises(ValidationError): + _ = validate(index_error) diff --git a/tests/test_sizeof.py b/tests/test_sizeof.py new file mode 100644 index 0000000..14ef8e1 --- /dev/null +++ b/tests/test_sizeof.py @@ -0,0 +1,123 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for sizeof operation. + +""" +# import pytest +# from qbraid_qir.qasm3 import qasm3_to_qir +# from qbraid_qir.qasm3.exceptions import Qasm3ConversionError + +# from tests.qir_utils import check_attributes, check_single_qubit_rotation_op + + +# def test_simple_sizeof(): +# """Test sizeof over an array""" +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] my_ints; + +# const uint[32] size0 = sizeof(my_ints); // this is 3 and valid + +# int[32] size1 = sizeof(my_ints); // this is 3 + +# int[32] size2 = sizeof(my_ints, 1); // this is 2 + +# int[32] size3 = sizeof(my_ints, 0); // this is 3 +# qubit[2] q; + +# rx(size0) q[0]; +# rx(size1) q[0]; +# rx(size2) q[1]; +# rx(size3) q[1]; +# """ + +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) + +# check_single_qubit_rotation_op(generated_qir, 4, [0, 0, 1, 1], [3, 3, 2, 3], "rx") + + +# def test_sizeof_multiple_types(): +# """Test sizeof over an array of bit, int and float""" +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[bit, 3, 2] my_bits; +# array[int[32], 3, 2] my_ints; +# array[float[64], 3, 2] my_floats; + +# int[32] size1 = sizeof(my_bits); // this is 3 + +# int[32] size2 = sizeof(my_ints, 1); // this is 2 + +# int[32] size3 = sizeof(my_floats, 0); // this is 3 too +# qubit[2] q; + +# rx(size1) q[0]; +# rx(size2) q[1]; +# rx(size3) q[1]; +# """ + +# result = qasm3_to_qir(qasm3_string) +# generated_qir = str(result).splitlines() +# check_attributes(generated_qir, 2, 0) + +# check_single_qubit_rotation_op(generated_qir, 2, [0, 1, 1], [3, 2, 3], "rx") + + +# def test_unsupported_target(): +# """Test sizeof over index expressions""" +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported target type .*"): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] my_ints; + +# int[32] size1 = sizeof(my_ints[0]); // this is invalid +# """ +# qasm3_to_qir(qasm3_string) + + +# def test_sizeof_on_non_array(): +# """Test sizeof on a non-array""" +# with pytest.raises( +# Qasm3ConversionError, match="Invalid sizeof usage, variable my_int is not an array." +# ): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# int[32] my_int = 3; + +# int[32] size1 = sizeof(my_int); // this is invalid +# """ +# qasm3_to_qir(qasm3_string) + + +# def test_out_of_bounds_reference(): +# """Test sizeof on an out of bounds reference""" +# with pytest.raises( +# Qasm3ConversionError, match="Index 3 out of bounds for array my_ints with 2 dimensions" +# ): +# qasm3_string = """ +# OPENQASM 3; +# include "stdgates.inc"; + +# array[int[32], 3, 2] my_ints; + +# int[32] size1 = sizeof(my_ints, 3); // this is invalid +# """ +# qasm3_to_qir(qasm3_string) diff --git a/tests/test_switch.py b/tests/test_switch.py new file mode 100644 index 0000000..6b9e397 --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,443 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. +""" +Module containing unit tests for switch statements. + +""" + +# import re + +# import pytest +# from qbraid_qir.qasm3 import qasm3_to_qir +# from qbraid_qir.qasm3.exceptions import Qasm3ConversionError + +# from tests.qir_utils import ( +# check_attributes, +# check_single_qubit_gate_op, +# check_single_qubit_rotation_op, +# ) + + +# def test_switch(): +# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement.""" + +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 5; +# qubit q; + +# switch(i) { +# case 1,3,5,7 { +# x q; +# } +# case 2,4,6,8 { +# y q; +# } +# default { +# z q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm3_switch_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1) +# check_single_qubit_gate_op(generated_qir, 1, [0], "x") + + +# def test_switch_default(): +# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and default case.""" + +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 10; +# qubit q; + +# switch(i) { +# case 1,3,5,7 { +# x q; +# } +# case 2,4,6,8 { +# y q; +# } +# default { +# z q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm3_switch_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1) +# check_single_qubit_gate_op(generated_qir, 1, [0], "z") + + +# def test_switch_identifier_case(): +# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and identifier case.""" + +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 4; +# const int j = 4; +# qubit q; + +# switch(i) { +# case 6, j { +# x q; +# } +# default { +# z q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm3_switch_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1) +# check_single_qubit_gate_op(generated_qir, 1, [0], "x") + + +# def test_switch_const_int(): +# """Test converting OpenQASM 3 program switch and constant integer case.""" + +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 4; +# const int j = 5; +# qubit q; + +# switch(i) { +# case j-1 { +# x q; +# } +# default { +# z q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm3_switch_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1) +# check_single_qubit_gate_op(generated_qir, 1, [0], "x") + + +# def test_switch_duplicate_cases(): +# """Test that switch raises error if duplicate values are present in case.""" + +# with pytest.raises( +# Qasm3ConversionError, match=re.escape("Duplicate case value 4 in switch statement") +# ): +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 4; +# qubit q; + +# switch(i) { +# case 4, 4 { +# x q; +# } +# default { +# z q; +# } +# } +# """ + +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# def test_no_case_switch(): +# """Test that switch raises error if no case is present.""" + +# with pytest.raises( +# Qasm3ConversionError, match=re.escape("Switch statement must have at least one case") +# ): +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 4; +# qubit q; + +# switch(i) { +# default { +# z q; +# } +# } +# """ + +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# def test_nested_switch(): +# """Test that switch works correctly in case of nested switch""" + +# qasm3_switch_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; + +# const int i = 1; +# qubit q; + +# switch(i) { +# case 1,3,5,7 { +# int j = 4; // definition inside scope +# switch(j) { +# case 1,3,5,7 { +# x q; +# } +# case 2,4,6,8 { +# j = 5; // assignment inside scope +# y q; // this will be executed +# } +# default { +# z q; +# } +# } +# } +# case 2,4,6,8 { +# y q; +# } +# default { +# z q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm3_switch_program, name="test") +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 1, 0) +# check_single_qubit_gate_op(generated_qir, 1, [0], "y") + + +# def test_subroutine_inside_switch(): +# """Test that a subroutine inside a switch statement is correctly parsed.""" +# qasm_str = """OPENQASM 3; +# include "stdgates.inc"; + +# def my_function(qubit q, float[32] b) { +# rx(b) q; +# return; +# } + +# qubit[2] q; +# int i = 1; +# float[32] r = 3.14; + +# switch(i) { +# case 1 { +# my_function(q[0], r); +# } +# default { +# x q; +# } +# } +# """ + +# result = qasm3_to_qir(qasm_str) +# generated_qir = str(result).splitlines() + +# check_attributes(generated_qir, 2, 0) +# check_single_qubit_rotation_op(generated_qir, 1, [0], [3.14], "rx") + + +# @pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) +# def test_invalid_scalar_switch_target(invalid_type): +# """Test that switch raises error if target is not an integer.""" + +# base_invalid_program = ( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; +# """ +# + invalid_type +# + """ i; + +# qubit q; + +# switch(i) { +# case 4 { +# x q; +# } +# default { +# z q; +# } +# } +# """ +# ) + +# with pytest.raises( +# Qasm3ConversionError, match=re.escape("Switch target i must be of type int") +# ): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# @pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) +# def test_invalid_array_switch_target(invalid_type): +# """Test that switch raises error if target is array element and not an integer.""" + +# base_invalid_program = ( +# """ +# OPENQASM 3.0; +# include "stdgates.inc"; +# array[""" +# + invalid_type +# + """, 3, 2] i; + +# qubit q; + +# switch(i[0][1]) { +# case 4 { +# x q; +# } +# default { +# z q; +# } +# } +# """ +# ) + +# with pytest.raises( +# Qasm3ConversionError, match=re.escape("Switch target i must be of type int") +# ): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# @pytest.mark.parametrize( +# "invalid_stmt", +# ["def test1() { int i = 1; }", "array[int[32], 3, 2] arr_int;", "gate test_1() q { h q;}"], +# ) +# def test_unsupported_statements_in_case(invalid_stmt): +# """Test that switch raises error if invalid statements are present in the case block""" + +# base_invalid_program = ( +# """ + +# OPENQASM 3.0; +# include "stdgates.inc"; +# qubit q; +# int i = 4; + +# switch(i) { +# case 4 { +# x q; +# """ +# + invalid_stmt +# + """ +# } +# default { +# z q; +# } +# } +# """ +# ) +# with pytest.raises(Qasm3ConversionError, match=r"Unsupported statement .*"): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# def test_non_int_expression_case(): +# """Test that switch raises error if case expression is not an integer.""" + +# base_invalid_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; +# const int i = 4; +# qubit q; + +# switch(i) { +# case 4.3, 2 { +# x q; +# } +# default { +# z q; +# } +# } +# """ + +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Invalid value 4.3 with type .* for required type ", +# ): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# def test_non_int_variable_expression(): +# """Test that switch raises error if case expression has a non-int +# variable in expression.""" + +# base_invalid_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; +# const int i = 4; +# const float f = 4.0; +# qubit q; + +# switch(i) { +# case f, 2 { +# x q; +# } +# default { +# z q; +# } +# } +# """ +# with pytest.raises( +# Qasm3ConversionError, +# match=r"Invalid type of variable .* for required type ", +# ): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") + + +# def test_non_constant_expression_case(): +# """Test that switch raises error if case expression is not a constant.""" + +# base_invalid_program = """ +# OPENQASM 3.0; +# include "stdgates.inc"; +# int i = 4; +# qubit q; +# int j = 3; +# int k = 2; + +# switch(i) { +# case j + k { +# x q; +# } +# default { +# z q; +# } +# } +# """ + +# with pytest.raises( +# Qasm3ConversionError, match=r"Variable .* is not a constant in given expression" +# ): +# qasm3_switch_program = base_invalid_program +# qasm3_to_qir(qasm3_switch_program, name="test") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..7a4966c --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,29 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + + +def check_unrolled_qasm(unrolled_qasm, expected_qasm): + """Check that the unrolled qasm matches the expected qasm. + + Args: + unrolled_qasm (str): The unrolled qasm to check. + expected_qasm (str): The expected qasm to check against + + Raises: + AssertionError: If the unrolled qasm does not match the expected qasm. + """ + # check line by line + unrolled_qasm = unrolled_qasm.split("\n") + expected_qasm = expected_qasm.split("\n") + assert len(unrolled_qasm) == len(expected_qasm) + + for unrolled_line, expected_line in zip(unrolled_qasm, expected_qasm): + print(unrolled_line, expected_line) + assert unrolled_line.strip() == expected_line.strip() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a8948c7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,90 @@ +[tox] +minversion = 4.2.0 +envlist = + unit-tests + docs + linters + format-check +skip_missing_interpreter = true + +[testenv] +commands_pre = python -m pip install . +basepython = python3 + +[testenv:unit-tests] +description = Run pytests and generate coverage report. +allowlist_externals = qbraid +commands = + pytest tests --cov=qbraid_qir --cov-config=pyproject.toml --cov-report=term --cov-report=xml {posargs} + +[testenv:docs] +description = Use sphinx to build the HTML docs. +extras = + docs +commands = + sphinx-build -W -b html docs/ docs/build/html {posargs} + +[testenv:isort] +envdir = .tox/linters +skip_install = true +deps = isort +commands = + isort . {posargs} pyqasm tests + +[testenv:pylint] +envdir = .tox/linters +skip_install = true +deps = pylint +commands = + pylint {posargs} pyqasm tests --disable=R0801,E0401,R0917 + +[testenv:black] +envdir = .tox/linters +skip_install = true +deps = black[jupyter] +commands = + black pyqasm tests {posargs} + +[testenv:mypy] +envdir = .tox/linters +skip_install = true +deps = mypy +commands = + mypy pyqasm + +[testenv:headers] +envdir = .tox/linters +skip_install = true +deps = qbraid-cli>=0.8.2 +commands = + qbraid admin headers tests pyqasm --type=gpl {posargs} + +[testenv:linters] +allowlist_externals = qbraid +envdir = .tox/linters +skip_install = true +deps = + {[testenv:isort]deps} + {[testenv:black]deps} + {[testenv:headers]deps} +commands = + {[testenv:isort]commands} + {[testenv:black]commands} + {[testenv:headers]commands} {posargs:--fix} + +[testenv:format-check] +allowlist_externals = qbraid +envdir = .tox/linters +skip_install = true +deps = + {[testenv:pylint]deps} + {[testenv:isort]deps} + {[testenv:black]deps} + {[testenv:mypy]deps} + {[testenv:headers]deps} +commands = + {[testenv:pylint]commands} + {[testenv:isort]commands} {posargs:--check-only} + {[testenv:black]commands} {posargs:--check} + {[testenv:mypy]commands} + {[testenv:headers]commands} \ No newline at end of file From fb1a0d80516579836c9a46b3cc1bfb2b6fc8a0d9 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 7 Oct 2024 18:23:20 +0530 Subject: [PATCH 02/16] add support for single qubit gates --- pyqasm/maps.py | 159 ++++++++++++++++++++++++---------------------- pyqasm/visitor.py | 13 ++-- 2 files changed, 91 insertions(+), 81 deletions(-) diff --git a/pyqasm/maps.py b/pyqasm/maps.py index 84421a9..25f0e7f 100644 --- a/pyqasm/maps.py +++ b/pyqasm/maps.py @@ -24,6 +24,7 @@ ClassicalDeclaration, ComplexType, FloatType, + IndexedIdentifier, IntType, QuantumGateDefinition, QubitDeclaration, @@ -86,17 +87,12 @@ def qasm3_expression_op_map(op_name: str, *args) -> Union[float, int, bool]: raise ValidationError(f"Unsupported / undeclared QASM operator: {op_name}") from exc -def id_gate(builder, qubits): - pyqir._native.x(builder, qubits) - pyqir._native.x(builder, qubits) - - def u3_gate( - builder, + name: str, theta: Union[int, float], phi: Union[int, float], lam: Union[int, float], - qubits, + qubit_id, ): """ Implements the U3 gate using the following decomposition: @@ -113,16 +109,18 @@ def u3_gate( Returns: None """ - pyqir._native.rz(builder, lam, qubits) - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) - pyqir._native.rz(builder, theta + CONSTANTS_MAP["pi"], qubits) - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) - pyqir._native.rz(builder, phi + CONSTANTS_MAP["pi"], qubits) + result = "" + result += one_qubit_rotation_op("rz", lam, qubit_id) + result += one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id) + result += one_qubit_rotation_op("rz", theta + CONSTANTS_MAP["pi"], qubit_id) + result += one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id) + result += one_qubit_rotation_op("rz", phi + CONSTANTS_MAP["pi"], qubit_id) + return result # global phase - e^(i*(phi+lambda)/2) is missing in the above implementation def u3_inv_gate( - builder, + name: str, theta: Union[int, float], phi: Union[int, float], lam: Union[int, float], @@ -132,41 +130,36 @@ def u3_inv_gate( Implements the inverse of the U3 gate using the decomposition present in the u3_gate function. """ - pyqir._native.rz(builder, -1.0 * (phi + CONSTANTS_MAP["pi"]), qubits) - pyqir._native.rx(builder, -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) - pyqir._native.rz(builder, -1.0 * (theta + CONSTANTS_MAP["pi"]), qubits) - pyqir._native.rx(builder, -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) - pyqir._native.rz(builder, -1.0 * lam, qubits) + result = "" + result += one_qubit_rotation_op("rz", -1.0 * (phi + CONSTANTS_MAP["pi"]), qubits) + result += one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) + result += one_qubit_rotation_op("rz", -1.0 * (theta + CONSTANTS_MAP["pi"]), qubits) + result += one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) + result += one_qubit_rotation_op("rz", -1.0 * lam, qubits) + return result -def u2_gate(builder, phi, lam, qubits): +def u2_gate(name, phi, lam, qubits): """ Implements the U2 gate using the following decomposition: https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.U2Gate """ - u3_gate(builder, CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) + return u3_gate("u3", CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) -def u2_inv_gate(builder, phi, lam, qubits): +def u2_inv_gate(name, phi, lam, qubits): """ Implements the inverse of the U2 gate using the decomposition present in the u2_gate function. """ - u3_inv_gate(builder, CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) - - -def sx_gate(builder, qubits): - """ - Implements the Sqrt(X) gate as a decomposition of other gates. - """ - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits) + return u3_inv_gate("u3_inv", CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) -def sxdg_gate(builder, qubits): +def sxdg_gate_op(name, qubit_id): """ Implements the conjugate transpose of the Sqrt(X) gate as a decomposition of other gates. """ - pyqir._native.rx(builder, -CONSTANTS_MAP["pi"] / 2, qubits) + return one_qubit_rotation_op("rx", -CONSTANTS_MAP["pi"] / 2, qubit_id) def cv_gate(builder, qubit0, qubit1): @@ -368,24 +361,24 @@ def cphaseshift10_gate(builder, theta, qubit0, qubit1): pyqir._native.x(builder, qubits[1]) -def gpi_gate(builder, phi, qubit): +def gpi_gate(name, phi, qubit_id): """ Implements the gpi gate as a decomposition of other gates. """ theta_0 = CONSTANTS_MAP["pi"] phi_0 = phi lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] - u3_gate(builder, theta_0, phi_0, lambda_0, qubit) + return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) -def gpi2_gate(builder, phi, qubit): +def gpi2_gate(name, phi, qubit_id): """ Implements the gpi2 gate as a decomposition of other gates. """ theta_0 = CONSTANTS_MAP["pi"] / 2 phi_0 = phi + 3 * CONSTANTS_MAP["pi"] / 2 lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] / 2 - u3_gate(builder, theta_0, phi_0, lambda_0, qubit) + return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) # pylint: disable-next=too-many-arguments @@ -450,51 +443,67 @@ def ecr_gate(builder, qubit0, qubit1): pyqir._native.x(builder, qubits[0]) -def prx_gate(builder, theta, phi, qubit): +def prx_gate(name, theta, phi, qubit_id): """ Implements the PRX gate as a decomposition of other gates. """ theta_0 = theta phi_0 = CONSTANTS_MAP["pi"] / 2 - phi lambda_0 = -phi_0 - u3_gate(builder, theta_0, phi_0, lambda_0, qubit) - - -# PYQIR_ONE_QUBIT_OP_MAP = { -# "i": id_gate, -# "id": id_gate, -# "h": pyqir._native.h, -# "x": pyqir._native.x, -# "y": pyqir._native.y, -# "z": pyqir._native.z, -# "s": pyqir._native.s, -# "t": pyqir._native.t, -# "sdg": pyqir._native.s_adj, -# "si": pyqir._native.s_adj, -# "tdg": pyqir._native.t_adj, -# "ti": pyqir._native.t_adj, -# "v": sx_gate, -# "sx": sx_gate, -# "vi": sxdg_gate, -# "sxdg": sxdg_gate, -# } + return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) -# PYQIR_ONE_QUBIT_ROTATION_MAP = { -# "rx": pyqir._native.rx, -# "ry": pyqir._native.ry, -# "rz": pyqir._native.rz, -# "u": u3_gate, -# "U": u3_gate, -# "u3": u3_gate, -# "U3": u3_gate, -# "U2": u2_gate, -# "u2": u2_gate, -# "prx": prx_gate, -# "phaseshift": phaseshift_gate, -# "p": phaseshift_gate, -# "gpi": gpi_gate, -# "gpi2": gpi2_gate, -# } + +def one_qubit_gate_op(gate_name: str, qubit_id: IndexedIdentifier) -> str: + qubit_name = qubit_id.name.name + qubit_idx = qubit_id.indices[0][0].value + return f"{gate_name} {qubit_name}[{qubit_idx}];\n" + + +def one_qubit_rotation_op(gate_name: str, rotation: float, qubit_id: IndexedIdentifier) -> str: + qubit_name = qubit_id.name.name + qubit_idx = qubit_id.indices[0][0].value + return f"{gate_name}({rotation}) {qubit_name}[{qubit_idx}];\n" + + +def two_qubit_gate_op(gate_name, qubit_name1, qubit_id1, qubit_name2, qubit_id2): + return f"{gate_name} {qubit_name1}[{qubit_id1}], {qubit_name2}[{qubit_id2}];\n" + + +PYQIR_ONE_QUBIT_OP_MAP = { + "id": one_qubit_gate_op, + "h": one_qubit_gate_op, + "x": one_qubit_gate_op, + "y": one_qubit_gate_op, + "z": one_qubit_gate_op, + "s": one_qubit_gate_op, + "t": one_qubit_gate_op, + "sdg": one_qubit_gate_op, + "si": one_qubit_gate_op, + "tdg": one_qubit_gate_op, + "ti": one_qubit_gate_op, + "v": one_qubit_gate_op, + "sx": one_qubit_gate_op, + "vi": sxdg_gate_op, + "sxdg": sxdg_gate_op, +} + + +PYQIR_ONE_QUBIT_ROTATION_MAP = { + "rx": one_qubit_rotation_op, + "ry": one_qubit_rotation_op, + "rz": one_qubit_rotation_op, + "u": u3_gate, + "U": u3_gate, + "u3": u3_gate, + "U3": u3_gate, + "U2": u2_gate, + "u2": u2_gate, + "prx": prx_gate, + "phaseshift": phaseshift_gate, + "p": phaseshift_gate, + "gpi": gpi_gate, + "gpi2": gpi2_gate, +} # PYQIR_TWO_QUBIT_OP_MAP = { # "cx": pyqir._native.cx, @@ -528,7 +537,7 @@ def prx_gate(builder, theta, phi, qubit): # } -def map_qasm_op_to_pyqir_callable(op_name: str): +def map_qasm_op_to_callable(op_name: str): """ Map a QASM operation to a PyQIR callable. @@ -573,7 +582,7 @@ def map_qasm_op_to_pyqir_callable(op_name: str): } -def map_qasm_inv_op_to_pyqir_callable(op_name: str): +def map_qasm_inv_op_to_callable(op_name: str): """ Map a QASM operation to a PyQIR callable. diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index dbb8b3a..d07799a 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -32,8 +32,8 @@ CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS, SWITCH_BLACKLIST_STMTS, - map_qasm_inv_op_to_pyqir_callable, - map_qasm_op_to_pyqir_callable, + map_qasm_inv_op_to_callable, + map_qasm_op_to_callable, ) from .subroutines import Qasm3SubroutineProcessor from .transformer import Qasm3Transformer @@ -576,10 +576,10 @@ def _visit_basic_gate_operation( inverse_action = None # to update : qir_func is a pyqir callable, need to change this if not inverse: - qir_func, op_qubit_count = map_qasm_op_to_pyqir_callable(op_name) + qasm_func, op_qubit_count = map_qasm_op_to_callable(op_name) else: # in basic gates, inverse action only affects the rotation gates - qir_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_pyqir_callable(op_name) + qasm_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_callable(op_name) # to update op_parameters = None @@ -601,9 +601,10 @@ def _visit_basic_gate_operation( # we apply the gate on the qubit subset linearly qubit_subset = op_qubits[i : i + op_qubit_count] if op_parameters is not None: - qir_func(self._builder, *op_parameters, *qubit_subset) + gate_qasm = qasm_func(op_name, *op_parameters, *qubit_subset) else: - qir_func(self._builder, *qubit_subset) + gate_qasm = qasm_func(op_name, *qubit_subset) + self._module.add_qasm_statement(gate_qasm) # to update def _visit_custom_gate_operation( From 98792fd1c9b8c95d72004a66dc49d219bd1ab3ce Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 8 Oct 2024 14:09:24 +0530 Subject: [PATCH 03/16] update gates --- pyqasm/_version.py | 4 +- pyqasm/elements.py | 39 +- pyqasm/maps.py | 632 ++++++++++-------- pyqasm/visitor.py | 36 +- tests/conftest.py | 11 + tests/declarations/test_quantum.py | 12 +- tests/resources/gates.py | 348 ++++++++++ tests/resources/qasm/custom_gate_complex.qasm | 23 + tests/resources/qasm/custom_gate_nested.qasm | 17 + tests/resources/qasm/custom_gate_simple.qasm | 12 + tests/test_alias.py | 218 +++--- tests/test_barrier.py | 8 +- tests/test_gates.py | 491 +++++++------- tests/test_measurement.py | 20 +- tests/test_reset.py | 8 +- tests/utils.py | 123 ++++ 16 files changed, 1294 insertions(+), 708 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/resources/qasm/custom_gate_complex.qasm create mode 100644 tests/resources/qasm/custom_gate_nested.qasm create mode 100644 tests/resources/qasm/custom_gate_simple.qasm diff --git a/pyqasm/_version.py b/pyqasm/_version.py index 28d532b..c390f01 100644 --- a/pyqasm/_version.py +++ b/pyqasm/_version.py @@ -13,5 +13,5 @@ __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE -__version__ = version = "0.0.1.dev0+ga5e32ff.d20241007" -__version_tuple__ = version_tuple = (0, 0, 1, "dev0", "ga5e32ff.d20241007") +__version__ = version = "0.0.1.dev2+gfb1a0d8.d20241008" +__version_tuple__ = version_tuple = (0, 0, 1, "dev2", "gfb1a0d8.d20241008") diff --git a/pyqasm/elements.py b/pyqasm/elements.py index d61ff38..d29475d 100644 --- a/pyqasm/elements.py +++ b/pyqasm/elements.py @@ -19,7 +19,15 @@ from typing import Any, Optional, Union import numpy as np -from openqasm3.ast import BitType, ClassicalDeclaration, Program, QubitDeclaration, Statement +from openqasm3.ast import ( + BitType, + ClassicalDeclaration, + Include, + Program, + QubitDeclaration, + Statement, +) +from openqasm3.printer import dumps class InversionOp(Enum): @@ -139,6 +147,7 @@ def __init__( self._num_clbits = num_clbits self._elements = elements self._unrolled_qasm = "" + self._unrolled_ast = Program(statements=[Include("stdgates.inc")], version="3") self._original_program = program @property @@ -164,6 +173,8 @@ def original_program(self) -> Program: @property def unrolled_qasm(self) -> str: """Returns the unrolled qasm for the given module""" + if self._unrolled_qasm == "": + self._unrolled_qasm = dumps(self._unrolled_ast) return self._unrolled_qasm @unrolled_qasm.setter @@ -171,28 +182,30 @@ def unrolled_qasm(self, value: str): """Setter for the unrolled qasm""" self._unrolled_qasm = value + @property + def unrolled_ast(self) -> Program: + """Returns the unrolled AST for the given module""" + return self._unrolled_ast + + @unrolled_ast.setter + def unrolled_ast(self, value: Program): + """Setter for the unrolled AST""" + self._unrolled_ast = value + def unrolled_qasm_as_list(self): """Returns the unrolled qasm as a list of lines""" return self.unrolled_qasm.split("\n") - def add_qasm_statement(self, statement: str): - """Add a qasm statement to the unrolled qasm + def add_qasm_statement(self, statement: Statement): + """Add a qasm statement to the unrolled ast Args: - statement (str): The qasm statement to add to the unrolled qasm + statement (str): The qasm statement to add to the unrolled ast Returns: None """ - - if len(self.unrolled_qasm) == 0: - self.unrolled_qasm = "OPENQASM 3.0;\n" - self.unrolled_qasm += 'include "stdgates.inc";\n' - # add comments about total number of qubits and clbits - self.unrolled_qasm += f"// Total number of qubits: {self.num_qubits}\n" - self.unrolled_qasm += f"// Total number of clbits: {self.num_clbits}\n" - - self.unrolled_qasm += statement + self._unrolled_ast.statements.append(statement) @classmethod def from_program(cls, program: Program): diff --git a/pyqasm/maps.py b/pyqasm/maps.py index 25f0e7f..2d328ad 100644 --- a/pyqasm/maps.py +++ b/pyqasm/maps.py @@ -9,23 +9,24 @@ # THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3. """ -Module mapping supported QASM gates/operations to pyqir functions. +Module mapping supported QASM gates/operations to lower level gate operations. """ from typing import Callable, Union import numpy as np - -# import pyqir from openqasm3.ast import ( AngleType, BitType, BoolType, ClassicalDeclaration, ComplexType, + FloatLiteral, FloatType, + Identifier, IndexedIdentifier, IntType, + QuantumGate, QuantumGateDefinition, QubitDeclaration, SubroutineDefinition, @@ -88,301 +89,334 @@ def qasm3_expression_op_map(op_name: str, *args) -> Union[float, int, bool]: def u3_gate( - name: str, theta: Union[int, float], phi: Union[int, float], lam: Union[int, float], qubit_id, -): +) -> list[QuantumGate]: """ Implements the U3 gate using the following decomposition: https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.UGate https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.PhaseGate Args: - builder (pyqir._native.QirBuilder): The QIR builder. - theta (Union[int, float]): The theta angle. - phi (Union[int, float]): The phi angle. - lam (Union[int, float]): The lambda angle. - qubits: The qubits on which the gate is applied. + name (str): The name of the gate. + theta (Union[int, float]): The theta parameter. + phi (Union[int, float]): The phi parameter. + lam (Union[int, float]): The lambda parameter. + qubit_id (IndexedIdentifier): The qubit on which to apply the gate. Returns: - None - """ - result = "" - result += one_qubit_rotation_op("rz", lam, qubit_id) - result += one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id) - result += one_qubit_rotation_op("rz", theta + CONSTANTS_MAP["pi"], qubit_id) - result += one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id) - result += one_qubit_rotation_op("rz", phi + CONSTANTS_MAP["pi"], qubit_id) + list: A list of QuantumGate objects representing the decomposition of the U3 gate. + """ + result: list[QuantumGate] = [] + result.extend(one_qubit_rotation_op("rz", lam, qubit_id)) + result.extend(one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id)) + result.extend(one_qubit_rotation_op("rz", theta + CONSTANTS_MAP["pi"], qubit_id)) + result.extend(one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit_id)) + result.extend(one_qubit_rotation_op("rz", phi + CONSTANTS_MAP["pi"], qubit_id)) return result # global phase - e^(i*(phi+lambda)/2) is missing in the above implementation def u3_inv_gate( - name: str, theta: Union[int, float], phi: Union[int, float], lam: Union[int, float], qubits, -): +) -> list[QuantumGate]: """ Implements the inverse of the U3 gate using the decomposition present in the u3_gate function. """ - result = "" - result += one_qubit_rotation_op("rz", -1.0 * (phi + CONSTANTS_MAP["pi"]), qubits) - result += one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) - result += one_qubit_rotation_op("rz", -1.0 * (theta + CONSTANTS_MAP["pi"]), qubits) - result += one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits) - result += one_qubit_rotation_op("rz", -1.0 * lam, qubits) + result: list[QuantumGate] = [] + result.extend(one_qubit_rotation_op("rz", -1.0 * (phi + CONSTANTS_MAP["pi"]), qubits)) + result.extend(one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits)) + result.extend(one_qubit_rotation_op("rz", -1.0 * (theta + CONSTANTS_MAP["pi"]), qubits)) + result.extend(one_qubit_rotation_op("rx", -1.0 * (CONSTANTS_MAP["pi"] / 2), qubits)) + result.extend(one_qubit_rotation_op("rz", -1.0 * lam, qubits)) return result -def u2_gate(name, phi, lam, qubits): +def u2_gate(phi, lam, qubits) -> list[QuantumGate]: """ Implements the U2 gate using the following decomposition: https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.U2Gate """ - return u3_gate("u3", CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) + return u3_gate(CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) -def u2_inv_gate(name, phi, lam, qubits): +def u2_inv_gate(phi, lam, qubits) -> list[QuantumGate]: """ Implements the inverse of the U2 gate using the decomposition present in the u2_gate function. """ - return u3_inv_gate("u3_inv", CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) + return u3_inv_gate(CONSTANTS_MAP["pi"] / 2, phi, lam, qubits) -def sxdg_gate_op(name, qubit_id): +def sxdg_gate_op(qubit_id) -> list[QuantumGate]: """ Implements the conjugate transpose of the Sqrt(X) gate as a decomposition of other gates. """ return one_qubit_rotation_op("rx", -CONSTANTS_MAP["pi"] / 2, qubit_id) -def cv_gate(builder, qubit0, qubit1): +def cv_gate( + qubit0: IndexedIdentifier, + qubit1: IndexedIdentifier, +) -> list[QuantumGate]: """ Implements the controlled V gate as a decomposition of other gates. """ - pyqir._native.x(builder, qubit0) - pyqir._native.h(builder, qubit1) - pyqir._native.cx(builder, qubit0, qubit1) - pyqir._native.h(builder, qubit1) - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 4, qubit1) - pyqir._native.h(builder, qubit1) - pyqir._native.cx(builder, qubit0, qubit1) - pyqir._native.t_adj(builder, qubit0) - pyqir._native.h(builder, qubit1) - pyqir._native.x(builder, qubit0) - pyqir._native.rz(builder, -CONSTANTS_MAP["pi"] / 4, qubit1) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("x", qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 4, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("tdg", qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_gate_op("x", qubit0)) + result.extend(one_qubit_rotation_op("rz", -CONSTANTS_MAP["pi"] / 4, qubit1)) + return result -def cy_gate(builder, qubit0, qubit1): +def cy_gate(qubit0: IndexedIdentifier, qubit1: IndexedIdentifier) -> list[QuantumGate]: """ Implements the CY gate as a decomposition of other gates. """ - pyqir._native.s_adj(builder, qubit1) - pyqir._native.cx(builder, qubit0, qubit1) - pyqir._native.s(builder, qubit1) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("sdg", qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("s", qubit1)) + return result -def xx_gate(builder, theta, qubit0, qubit1): +def xx_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the XX gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1] - pyqir._native.h(builder, qubits[0]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.cz(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.rx(builder, theta, qubits[0]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.cz(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[0]) - pyqir._native.h(builder, qubits[1]) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("h", qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rx", theta, qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + return result -def xy_gate(builder, theta, qubit0, qubit1): +def xy_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the XY gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1] - pyqir._native.rx(builder, -theta / 2, qubits[0]) - pyqir._native.ry(builder, theta / 2, qubits[1]) - pyqir._native.ry(builder, theta / 2, qubits[0]) - pyqir._native.rx(builder, theta / 2, qubits[0]) - pyqir._native.cx(builder, qubits[1], qubits[0]) - pyqir._native.ry(builder, -theta / 2, qubits[0]) - pyqir._native.ry(builder, -theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[1], qubits[0]) - pyqir._native.rx(builder, theta / 2, qubits[0]) - pyqir._native.ry(builder, -theta / 2, qubits[1]) - pyqir._native.ry(builder, theta / 2, qubits[1]) - pyqir._native.rx(builder, -theta / 2, qubits[0]) - - -def yy_gate(builder, theta, qubit0, qubit1): + result: list[QuantumGate] = [] + result.extend(one_qubit_rotation_op("rx", -theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("ry", theta / 2, qubit1)) + result.extend(one_qubit_rotation_op("ry", theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit0)) + result.extend(two_qubit_gate_op("cx", qubit1, qubit0)) + result.extend(one_qubit_rotation_op("ry", -theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("ry", -theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit1, qubit0)) + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("ry", -theta / 2, qubit1)) + result.extend(one_qubit_rotation_op("ry", theta / 2, qubit1)) + result.extend(one_qubit_rotation_op("rx", -theta / 2, qubit0)) + return result + + +def yy_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the YY gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1] - pyqir._native.rx(builder, theta / 2, qubits[0]) - pyqir._native.rx(builder, theta / 2, qubits[1]) - pyqir._native.cz(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.rx(builder, theta, qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.cz(builder, qubits[0], qubits[1]) - pyqir._native.rx(builder, -theta / 2, qubits[0]) - pyqir._native.rx(builder, -theta / 2, qubits[1]) + result: list[QuantumGate] = [] + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rx", theta, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + result.extend(one_qubit_rotation_op("rx", -theta / 2, qubit0)) + result.extend(one_qubit_rotation_op("rx", -theta / 2, qubit1)) + return result -def zz_gate(builder, theta, qubit0, qubit1): +def zz_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the ZZ gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1] - pyqir._native.cz(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.rz(builder, theta, qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.cz(builder, qubits[0], qubits[1]) + result: list[QuantumGate] = [] + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rz", theta, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cz", qubit0, qubit1)) + return result -def phaseshift_gate(builder, theta, qubit): +def phaseshift_gate(theta: Union[int, float], qubit: IndexedIdentifier) -> list[QuantumGate]: """ Implements the phase shift gate as a decomposition of other gates. """ - pyqir._native.h(builder, qubit) - pyqir._native.rx(builder, theta, qubit) - pyqir._native.h(builder, qubit) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("h", qubit)) + result.extend(one_qubit_rotation_op("rx", theta, qubit)) + result.extend(one_qubit_gate_op("h", qubit)) + return result -def cswap_gate(builder, qubit0, qubit1, qubit2): +def cswap_gate( + qubit0: IndexedIdentifier, qubit1: IndexedIdentifier, qubit2: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the CSWAP gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1, qubit2] - pyqir._native.cx(builder, qubits[2], qubits[1]) - pyqir._native.h(builder, qubits[2]) - pyqir._native.cx(builder, qubits[1], qubits[2]) - pyqir._native.t_adj(builder, qubits[2]) - pyqir._native.cx(builder, qubits[0], qubits[2]) - pyqir._native.t(builder, qubits[2]) - pyqir._native.cx(builder, qubits[1], qubits[2]) - pyqir._native.t(builder, qubits[1]) - pyqir._native.t_adj(builder, qubits[2]) - pyqir._native.cx(builder, qubits[0], qubits[2]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.t(builder, qubits[2]) - pyqir._native.t(builder, qubits[0]) - pyqir._native.t_adj(builder, qubits[1]) - pyqir._native.h(builder, qubits[2]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.cx(builder, qubits[2], qubits[1]) - - -def pswap_gate(builder, theta, qubit0, qubit1): + result: list[QuantumGate] = [] + result.extend(two_qubit_gate_op("cx", qubit2, qubit1)) + result.extend(one_qubit_gate_op("h", qubit2)) + result.extend(two_qubit_gate_op("cx", qubit1, qubit2)) + result.extend(one_qubit_gate_op("tdg", qubit2)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit2)) + result.extend(one_qubit_gate_op("t", qubit2)) + result.extend(two_qubit_gate_op("cx", qubit1, qubit2)) + result.extend(one_qubit_gate_op("t", qubit1)) + result.extend(one_qubit_gate_op("tdg", qubit2)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit2)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("t", qubit2)) + result.extend(one_qubit_gate_op("t", qubit0)) + result.extend(one_qubit_gate_op("tdg", qubit1)) + result.extend(one_qubit_gate_op("h", qubit2)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit2, qubit1)) + return result + + +def pswap_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the PSWAP gate as a decomposition of other gates. - """ - qubits = [qubit0, qubit1] - pyqir._native.swap(builder, qubits[0], qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - u3_gate(builder, 0, 0, theta, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) + result: list[QuantumGate] = [] + result.extend(two_qubit_gate_op("swap", qubit0, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(u3_gate(0, 0, theta, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + return result -def cphaseshift_gate(builder, theta, qubit0, qubit1): +def cphaseshift_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the controlled phase shift gate as a decomposition of other gates. """ - qubits = [qubit0, qubit1] - pyqir._native.h(builder, qubits[0]) - pyqir._native.rx(builder, theta / 2, qubits[0]) - pyqir._native.h(builder, qubits[0]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.rx(builder, -theta / 2, qubits[0]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.h(builder, qubits[1]) - pyqir._native.rx(builder, theta / 2, qubits[1]) - pyqir._native.h(builder, qubits[1]) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("h", qubit0)) + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit0)) + result.extend(one_qubit_gate_op("h", qubit0)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rx", -theta / 2, qubit0)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + result.extend(one_qubit_rotation_op("rx", theta / 2, qubit1)) + result.extend(one_qubit_gate_op("h", qubit1)) + return result -def cphaseshift00_gate(builder, theta, qubit0, qubit1): +def cphaseshift00_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the controlled phase shift 00 gate as a decomposition of other gates. - """ - qubits = [qubit0, qubit1] - pyqir._native.x(builder, qubits[0]) - pyqir._native.x(builder, qubits[1]) - u3_gate(builder, 0, 0, theta / 2, qubits[0]) - u3_gate(builder, 0, 0, theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - u3_gate(builder, 0, 0, -theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.x(builder, qubits[0]) - pyqir._native.x(builder, qubits[1]) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("x", qubit0)) + result.extend(one_qubit_gate_op("x", qubit1)) + result.extend(u3_gate(0, 0, theta / 2, qubit0)) + result.extend(u3_gate(0, 0, theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(u3_gate(0, 0, -theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("x", qubit0)) + result.extend(one_qubit_gate_op("x", qubit1)) + return result -def cphaseshift01_gate(builder, theta, qubit0, qubit1): +def cphaseshift01_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the controlled phase shift 01 gate as a decomposition of other gates. - """ - qubits = [qubit0, qubit1] - pyqir._native.x(builder, qubits[0]) - u3_gate(builder, 0, 0, theta / 2, qubits[1]) - u3_gate(builder, 0, 0, theta / 2, qubits[0]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - u3_gate(builder, 0, 0, -theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.x(builder, qubits[0]) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("x", qubit0)) + result.extend(u3_gate(0, 0, theta / 2, qubit1)) + result.extend(u3_gate(0, 0, theta / 2, qubit0)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(u3_gate(0, 0, -theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("x", qubit0)) + return result -def cphaseshift10_gate(builder, theta, qubit0, qubit1): +def cphaseshift10_gate( + theta: Union[int, float], qubit0: IndexedIdentifier, qubit1: IndexedIdentifier +) -> list[QuantumGate]: """ Implements the controlled phase shift 10 gate as a decomposition of other gates. - """ - qubits = [qubit0, qubit1] - u3_gate(builder, 0, 0, theta / 2, qubits[0]) - pyqir._native.x(builder, qubits[1]) - u3_gate(builder, 0, 0, theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - u3_gate(builder, 0, 0, -theta / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.x(builder, qubits[1]) + result: list[QuantumGate] = [] + result.extend(u3_gate(0, 0, theta / 2, qubit0)) + result.extend(one_qubit_gate_op("x", qubit1)) + result.extend(u3_gate(0, 0, theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(u3_gate(0, 0, -theta / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("x", qubit1)) + return result -def gpi_gate(name, phi, qubit_id): +def gpi_gate(phi, qubit_id) -> list[QuantumGate]: """ Implements the gpi gate as a decomposition of other gates. """ theta_0 = CONSTANTS_MAP["pi"] phi_0 = phi lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] - return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) + return u3_gate(theta_0, phi_0, lambda_0, qubit_id) -def gpi2_gate(name, phi, qubit_id): +def gpi2_gate(phi, qubit_id) -> list[QuantumGate]: """ Implements the gpi2 gate as a decomposition of other gates. """ theta_0 = CONSTANTS_MAP["pi"] / 2 phi_0 = phi + 3 * CONSTANTS_MAP["pi"] / 2 lambda_0 = -phi_0 + CONSTANTS_MAP["pi"] / 2 - return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) + return u3_gate(theta_0, phi_0, lambda_0, qubit_id) # pylint: disable-next=too-many-arguments -def ms_gate(builder, phi0, phi1, theta, qubit0, qubit1): +def ms_gate(phi0, phi1, theta, qubit0, qubit1) -> list[QuantumGate]: """ Implements the Molmer Sorenson gate as a decomposition of other gates. """ @@ -417,81 +451,119 @@ def ms_gate(builder, phi0, phi1, theta, qubit0, qubit1): angles = kak_decomposition_angles(mat) qubits = [qubit0, qubit1] - u3_gate(builder, angles[0][0], angles[0][1], angles[0][2], qubits[0]) - u3_gate(builder, angles[1][0], angles[1][1], angles[1][2], qubits[1]) - sx_gate(builder, qubits[0]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.rx(builder, ((1 / 2) - 2 * theta) * CONSTANTS_MAP["pi"], qubits[0]) - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits[1]) - pyqir._native.cx(builder, qubits[1], qubits[0]) - sxdg_gate(builder, qubits[1]) - pyqir._native.s(builder, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - u3_gate(builder, angles[2][0], angles[2][1], angles[2][2], qubits[0]) - u3_gate(builder, angles[3][0], angles[3][1], angles[3][2], qubits[1]) + result: list[QuantumGate] = [] + result.extend(u3_gate(angles[0][0], angles[0][1], angles[0][2], qubits[0])) + result.extend(u3_gate(angles[1][0], angles[1][1], angles[1][2], qubits[1])) + result.extend(one_qubit_gate_op("sx", qubits[0])) + result.extend(two_qubit_gate_op("cx", qubits[0], qubits[1])) + result.extend( + one_qubit_rotation_op("rx", ((1 / 2) - 2 * theta) * CONSTANTS_MAP["pi"], qubits[0]) + ) + result.extend(one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubits[1])) + result.extend(two_qubit_gate_op("cx", qubits[1], qubits[0])) + result.extend(sxdg_gate_op(qubits[1])) + result.extend(one_qubit_gate_op("s", qubits[1])) + result.extend(two_qubit_gate_op("cx", qubits[0], qubits[1])) + result.extend(u3_gate(angles[2][0], angles[2][1], angles[2][2], qubits[0])) + result.extend(u3_gate(angles[3][0], angles[3][1], angles[3][2], qubits[1])) + return result -def ecr_gate(builder, qubit0, qubit1): +def ccx_gate_op( + qubit0: IndexedIdentifier, qubit1: IndexedIdentifier, qubit2: IndexedIdentifier +) -> list[QuantumGate]: + return [ + QuantumGate( + modifiers=[], + name=Identifier(name="ccx"), + arguments=[], + qubits=[qubit0, qubit1, qubit2], + ) + ] + + +def ecr_gate(qubit0: IndexedIdentifier, qubit1: IndexedIdentifier) -> list[QuantumGate]: """ Implements the ECR gate as a decomposition of other gates. - """ - qubits = [qubit0, qubit1] - pyqir._native.s(builder, qubits[0]) - pyqir._native.rx(builder, CONSTANTS_MAP["pi"] / 2, qubits[1]) - pyqir._native.cx(builder, qubits[0], qubits[1]) - pyqir._native.x(builder, qubits[0]) + result: list[QuantumGate] = [] + result.extend(one_qubit_gate_op("s", qubit0)) + result.extend(one_qubit_rotation_op("rx", CONSTANTS_MAP["pi"] / 2, qubit1)) + result.extend(two_qubit_gate_op("cx", qubit0, qubit1)) + result.extend(one_qubit_gate_op("x", qubit0)) + return result -def prx_gate(name, theta, phi, qubit_id): +def prx_gate(theta, phi, qubit_id) -> list[QuantumGate]: """ Implements the PRX gate as a decomposition of other gates. """ theta_0 = theta phi_0 = CONSTANTS_MAP["pi"] / 2 - phi lambda_0 = -phi_0 - return u3_gate("u3", theta_0, phi_0, lambda_0, qubit_id) + return u3_gate(theta_0, phi_0, lambda_0, qubit_id) -def one_qubit_gate_op(gate_name: str, qubit_id: IndexedIdentifier) -> str: - qubit_name = qubit_id.name.name - qubit_idx = qubit_id.indices[0][0].value - return f"{gate_name} {qubit_name}[{qubit_idx}];\n" - - -def one_qubit_rotation_op(gate_name: str, rotation: float, qubit_id: IndexedIdentifier) -> str: - qubit_name = qubit_id.name.name - qubit_idx = qubit_id.indices[0][0].value - return f"{gate_name}({rotation}) {qubit_name}[{qubit_idx}];\n" - - -def two_qubit_gate_op(gate_name, qubit_name1, qubit_id1, qubit_name2, qubit_id2): - return f"{gate_name} {qubit_name1}[{qubit_id1}], {qubit_name2}[{qubit_id2}];\n" - - -PYQIR_ONE_QUBIT_OP_MAP = { - "id": one_qubit_gate_op, - "h": one_qubit_gate_op, - "x": one_qubit_gate_op, - "y": one_qubit_gate_op, - "z": one_qubit_gate_op, - "s": one_qubit_gate_op, - "t": one_qubit_gate_op, - "sdg": one_qubit_gate_op, - "si": one_qubit_gate_op, - "tdg": one_qubit_gate_op, - "ti": one_qubit_gate_op, - "v": one_qubit_gate_op, - "sx": one_qubit_gate_op, +def one_qubit_gate_op(gate_name: str, qubit_id: IndexedIdentifier) -> list[QuantumGate]: + return [ + QuantumGate( + modifiers=[], + name=Identifier(name=gate_name), + arguments=[], + qubits=[qubit_id], + ) + ] + + +def one_qubit_rotation_op( + gate_name: str, rotation: float, qubit_id: IndexedIdentifier +) -> list[QuantumGate]: + return [ + QuantumGate( + modifiers=[], + name=Identifier(name=gate_name), + arguments=[FloatLiteral(value=rotation)], + qubits=[qubit_id], + ) + ] + + +def two_qubit_gate_op( + gate_name: str, qubit_id1: IndexedIdentifier, qubit_id2: IndexedIdentifier +) -> list[QuantumGate]: + return [ + QuantumGate( + modifiers=[], + name=Identifier(name=gate_name.lower()), + arguments=[], + qubits=[qubit_id1, qubit_id2], + ) + ] + + +ONE_QUBIT_OP_MAP = { + "id": lambda qubit_id: one_qubit_gate_op("id", qubit_id), + "h": lambda qubit_id: one_qubit_gate_op("h", qubit_id), + "x": lambda qubit_id: one_qubit_gate_op("x", qubit_id), + "y": lambda qubit_id: one_qubit_gate_op("y", qubit_id), + "z": lambda qubit_id: one_qubit_gate_op("z", qubit_id), + "s": lambda qubit_id: one_qubit_gate_op("s", qubit_id), + "t": lambda qubit_id: one_qubit_gate_op("t", qubit_id), + "sdg": lambda qubit_id: one_qubit_gate_op("sdg", qubit_id), + "si": lambda qubit_id: one_qubit_gate_op("sdg", qubit_id), + "tdg": lambda qubit_id: one_qubit_gate_op("tdg", qubit_id), + "ti": lambda qubit_id: one_qubit_gate_op("tdg", qubit_id), + "v": lambda qubit_id: one_qubit_gate_op("sx", qubit_id), + "sx": lambda qubit_id: one_qubit_gate_op("sx", qubit_id), "vi": sxdg_gate_op, "sxdg": sxdg_gate_op, } -PYQIR_ONE_QUBIT_ROTATION_MAP = { - "rx": one_qubit_rotation_op, - "ry": one_qubit_rotation_op, - "rz": one_qubit_rotation_op, +ONE_QUBIT_ROTATION_MAP = { + "rx": lambda rotation, qubit_id: one_qubit_rotation_op("rx", rotation, qubit_id), + "ry": lambda rotation, qubit_id: one_qubit_rotation_op("ry", rotation, qubit_id), + "rz": lambda rotation, qubit_id: one_qubit_rotation_op("rz", rotation, qubit_id), "u": u3_gate, "U": u3_gate, "u3": u3_gate, @@ -505,36 +577,36 @@ def two_qubit_gate_op(gate_name, qubit_name1, qubit_id1, qubit_name2, qubit_id2) "gpi2": gpi2_gate, } -# PYQIR_TWO_QUBIT_OP_MAP = { -# "cx": pyqir._native.cx, -# "CX": pyqir._native.cx, -# "cnot": pyqir._native.cx, -# "cz": pyqir._native.cz, -# "swap": pyqir._native.swap, -# "cv": cv_gate, -# "cy": cy_gate, -# "xx": xx_gate, -# "xy": xy_gate, -# "yy": yy_gate, -# "zz": zz_gate, -# "pswap": pswap_gate, -# "cp": cphaseshift_gate, -# "cphaseshift": cphaseshift_gate, -# "cp00": cphaseshift00_gate, -# "cphaseshift00": cphaseshift00_gate, -# "cp01": cphaseshift01_gate, -# "cphaseshift01": cphaseshift01_gate, -# "cp10": cphaseshift10_gate, -# "cphaseshift10": cphaseshift10_gate, -# "ecr": ecr_gate, -# "ms": ms_gate, -# } - -# PYQIR_THREE_QUBIT_OP_MAP = { -# "ccx": pyqir._native.ccx, -# "ccnot": pyqir._native.ccx, -# "cswap": cswap_gate, -# } +TWO_QUBIT_OP_MAP = { + "cx": lambda qubit_id1, qubit_id2: two_qubit_gate_op("cx", qubit_id1, qubit_id2), + "CX": lambda qubit_id1, qubit_id2: two_qubit_gate_op("cx", qubit_id1, qubit_id2), + "cnot": lambda qubit_id1, qubit_id2: two_qubit_gate_op("cx", qubit_id1, qubit_id2), + "cz": lambda qubit_id1, qubit_id2: two_qubit_gate_op("cz", qubit_id1, qubit_id2), + "swap": lambda qubit_id1, qubit_id2: two_qubit_gate_op("swap", qubit_id1, qubit_id2), + "cv": cv_gate, + "cy": cy_gate, + "xx": xx_gate, + "xy": xy_gate, + "yy": yy_gate, + "zz": zz_gate, + "pswap": pswap_gate, + "cp": cphaseshift_gate, + "cphaseshift": cphaseshift_gate, + "cp00": cphaseshift00_gate, + "cphaseshift00": cphaseshift00_gate, + "cp01": cphaseshift01_gate, + "cphaseshift01": cphaseshift01_gate, + "cp10": cphaseshift10_gate, + "cphaseshift10": cphaseshift10_gate, + "ecr": ecr_gate, + "ms": ms_gate, +} + +THREE_QUBIT_OP_MAP = { + "ccx": ccx_gate_op, + "ccnot": ccx_gate_op, + "cswap": cswap_gate, +} def map_qasm_op_to_callable(op_name: str): @@ -548,32 +620,32 @@ def map_qasm_op_to_callable(op_name: str): tuple: A tuple containing the PyQIR callable and the number of qubits the operation acts on. """ try: - return PYQIR_ONE_QUBIT_OP_MAP[op_name], 1 + return ONE_QUBIT_OP_MAP[op_name], 1 except KeyError: pass try: - return PYQIR_ONE_QUBIT_ROTATION_MAP[op_name], 1 + return ONE_QUBIT_ROTATION_MAP[op_name], 1 except KeyError: pass try: - return PYQIR_TWO_QUBIT_OP_MAP[op_name], 2 + return TWO_QUBIT_OP_MAP[op_name], 2 except KeyError: pass try: - return PYQIR_THREE_QUBIT_OP_MAP[op_name], 3 + return THREE_QUBIT_OP_MAP[op_name], 3 except KeyError as exc: raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") from exc -PYQIR_SELF_INVERTING_ONE_QUBIT_OP_SET = {"id", "h", "x", "y", "z"} -PYQIR_ST_GATE_INV_MAP = { +SELF_INVERTING_ONE_QUBIT_OP_SET = {"id", "h", "x", "y", "z"} +ST_GATE_INV_MAP = { "s": "sdg", "t": "tdg", "sdg": "s", "tdg": "t", } -PYQIR_ROTATION_INVERSION_ONE_QUBIT_OP_MAP = {"rx", "ry", "rz"} -PYQIR_U_INV_ROTATION_MAP = { +ROTATION_INVERSION_ONE_QUBIT_OP_MAP = {"rx", "ry", "rz"} +U_INV_ROTATION_MAP = { "U": u3_inv_gate, "u3": u3_inv_gate, "U3": u3_inv_gate, @@ -593,22 +665,22 @@ def map_qasm_inv_op_to_callable(op_name: str): tuple: A tuple containing the PyQIR callable, the number of qubits the operation acts on, and what is to be done with the basic gate which we are trying to invert. """ - if op_name in PYQIR_SELF_INVERTING_ONE_QUBIT_OP_SET: - return PYQIR_ONE_QUBIT_OP_MAP[op_name], 1, InversionOp.NO_OP - if op_name in PYQIR_ST_GATE_INV_MAP: - inv_gate_name = PYQIR_ST_GATE_INV_MAP[op_name] - return PYQIR_ONE_QUBIT_OP_MAP[inv_gate_name], 1, InversionOp.NO_OP - if op_name in PYQIR_TWO_QUBIT_OP_MAP: - return PYQIR_TWO_QUBIT_OP_MAP[op_name], 2, InversionOp.NO_OP - if op_name in PYQIR_THREE_QUBIT_OP_MAP: - return PYQIR_THREE_QUBIT_OP_MAP[op_name], 3, InversionOp.NO_OP - if op_name in PYQIR_U_INV_ROTATION_MAP: + if op_name in SELF_INVERTING_ONE_QUBIT_OP_SET: + return ONE_QUBIT_OP_MAP[op_name], 1, InversionOp.NO_OP + if op_name in ST_GATE_INV_MAP: + inv_gate_name = ST_GATE_INV_MAP[op_name] + return ONE_QUBIT_OP_MAP[inv_gate_name], 1, InversionOp.NO_OP + if op_name in TWO_QUBIT_OP_MAP: + return TWO_QUBIT_OP_MAP[op_name], 2, InversionOp.NO_OP + if op_name in THREE_QUBIT_OP_MAP: + return THREE_QUBIT_OP_MAP[op_name], 3, InversionOp.NO_OP + if op_name in U_INV_ROTATION_MAP: # Special handling for U gate as it is composed of multiple # basic gates and we need to invert each of them - return PYQIR_U_INV_ROTATION_MAP[op_name], 1, InversionOp.NO_OP - if op_name in PYQIR_ROTATION_INVERSION_ONE_QUBIT_OP_MAP: + return U_INV_ROTATION_MAP[op_name], 1, InversionOp.NO_OP + if op_name in ROTATION_INVERSION_ONE_QUBIT_OP_MAP: return ( - PYQIR_ONE_QUBIT_ROTATION_MAP[op_name], + ONE_QUBIT_ROTATION_MAP[op_name], 1, InversionOp.INVERT_ROTATION, ) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index d07799a..ede2c65 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -271,12 +271,16 @@ def visit_register( register_size = ( 1 if register.size is None else register.size.value # type: ignore[union-attr] ) + if register.size is None: + register.size = qasm3_ast.IntegerLiteral(register_size) else: register_size = ( 1 if register.type.size is None # type: ignore[union-attr] else register.type.size.value # type: ignore[union-attr] ) + if register.type.size is None: + register.type.size = qasm3_ast.IntegerLiteral(register_size) register_name = ( register.qubit.name # type: ignore[union-attr] if is_qubit @@ -302,8 +306,7 @@ def visit_register( False, ) ) - qasm_stmt = f"{'qubit' if is_qubit else 'bit'}[{register_size}] {register_name};\n" - self._module.add_qasm_statement(qasm_stmt) + self._module.add_qasm_statement(register) for i in range(register_size): # required if indices are not used while applying a gate or measurement @@ -454,10 +457,10 @@ def _visit_measurement(self, statement: qasm3_ast.QuantumMeasurementStatement) - ) if not self._check_only: for src_id, tgt_id in zip(source_ids, target_ids): - src_name, src_bit = src_id.name.name, src_id.indices[0][0].value - tgt_name, tgt_bit = tgt_id.name.name, tgt_id.indices[0][0].value - qasm_stmt = f"measure {src_name}[{src_bit}] -> {tgt_name}[{tgt_bit}];\n" - self._module.add_qasm_statement(qasm_stmt) + unrolled_measure = qasm3_ast.QuantumMeasurementStatement( + measure=qasm3_ast.QuantumMeasurement(qubit=src_id), target=tgt_id + ) + self._module.add_qasm_statement(unrolled_measure) def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None: """Visit a reset statement element. @@ -482,9 +485,8 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None: if not self._check_only: for qid in qubit_ids: - name, bit_id = qid.name.name, qid.indices[0][0].value - qasm_stmt = f"reset {name}[{bit_id}];\n" - self._module.add_qasm_statement(qasm_stmt) + unrolled_reset = qasm3_ast.QuantumReset(qubits=qid) + self._module.add_qasm_statement(unrolled_reset) def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None: """Visit a barrier statement element. @@ -511,8 +513,8 @@ def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None: barrier_qubits = self._get_op_bits(barrier, self._global_qreg_size_map) if not self._check_only: for qubit in barrier_qubits: - qasm_stmt = f"barrier {qubit.name.name}[{qubit.indices[0][0].value}];\n" - self._module.add_qasm_statement(qasm_stmt) + unrolled_barrier = qasm3_ast.QuantumBarrier(qubits=[qubit]) + self._module.add_qasm_statement(unrolled_barrier) def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: """Get the parameters for the operation. @@ -574,13 +576,11 @@ def _visit_basic_gate_operation( op_name: str = operation.name.name op_qubits = self._get_op_bits(operation, self._global_qreg_size_map) inverse_action = None - # to update : qir_func is a pyqir callable, need to change this if not inverse: qasm_func, op_qubit_count = map_qasm_op_to_callable(op_name) else: # in basic gates, inverse action only affects the rotation gates qasm_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_callable(op_name) - # to update op_parameters = None @@ -596,16 +596,15 @@ def _visit_basic_gate_operation( op_parameters = [-1 * param for param in op_parameters] if not self._check_only: - # to update for i in range(0, len(op_qubits), op_qubit_count): # we apply the gate on the qubit subset linearly qubit_subset = op_qubits[i : i + op_qubit_count] if op_parameters is not None: - gate_qasm = qasm_func(op_name, *op_parameters, *qubit_subset) + unrolled_gate = qasm_func(*op_parameters, *qubit_subset) else: - gate_qasm = qasm_func(op_name, *qubit_subset) - self._module.add_qasm_statement(gate_qasm) - # to update + unrolled_gate = qasm_func(*qubit_subset) + for gate in unrolled_gate: + self._module.add_qasm_statement(gate) def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False @@ -1210,6 +1209,7 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: pass + # to edit... def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: """Visit an alias statement element. diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8124d62 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +# Copyright (C) 2024 qBraid +# +# This file is part of the pyqasm +# +# The 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 the pyqasm, as per Section 15 of the GPL v3. + +from .resources.gates import * diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py index 110765b..b23b6f8 100644 --- a/tests/declarations/test_quantum.py +++ b/tests/declarations/test_quantum.py @@ -27,10 +27,8 @@ def test_qubit_declarations(): qubit[1] q4; """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 7 - // Total number of clbits: 0 qubit[1] q1; qubit[2] q2; qubit[3] q3; @@ -53,10 +51,8 @@ def test_clbit_declarations(): bit[1] c4; """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 0 - // Total number of clbits: 7 bit[1] c1; bit[2] c2; bit[3] c3; @@ -87,10 +83,8 @@ def test_qubit_clbit_declarations(): bit[1] c4; """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 7 - // Total number of clbits: 7 qubit[1] q1; qubit[2] q2; qubit[3] q3; diff --git a/tests/resources/gates.py b/tests/resources/gates.py index a5caba4..b3ddd83 100644 --- a/tests/resources/gates.py +++ b/tests/resources/gates.py @@ -7,3 +7,351 @@ # See the LICENSE file in the project root or . # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. + +import os + +import pytest + +from pyqasm.maps import ( + ONE_QUBIT_OP_MAP, + ONE_QUBIT_ROTATION_MAP, + THREE_QUBIT_OP_MAP, + TWO_QUBIT_OP_MAP, +) + +CUSTOM_OPS = ["simple", "nested", "complex"] + +RESOURCES_DIR = os.path.join(os.path.dirname(__file__), "qasm") + + +def resources_file(filename: str) -> str: + return os.path.join(RESOURCES_DIR, f"{filename}") + + +def _fixture_name(s: str) -> str: + return f"Fixture_{s}" + + +def _validate_gate_name(gate_name: str) -> str: + if gate_name in ONE_QUBIT_OP_MAP: + return True + if gate_name in TWO_QUBIT_OP_MAP: + return True + if gate_name in ONE_QUBIT_ROTATION_MAP: + return True + if gate_name in THREE_QUBIT_OP_MAP: + return True + return False + + +def _generate_one_qubit_fixture(gate_name: str): + @pytest.fixture() + def test_fixture(): + if not _validate_gate_name(gate_name): + raise ValueError(f"Unknown qasm3 gate {gate_name}") + qasm3_string = f""" + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q; + {gate_name} q; + {gate_name} q[0]; + {gate_name} q[0:2]; + """ + return qasm3_string + + return test_fixture + + +# Generate simple single-qubit gate fixtures +for gate in ONE_QUBIT_OP_MAP: + name = _fixture_name(gate) + locals()[name] = _generate_one_qubit_fixture(gate) + + +def _generate_rotation_fixture(gate_name: str): + @pytest.fixture() + def test_fixture(): + if not _validate_gate_name(gate_name): + raise ValueError(f"Unknown qasm3 gate {gate_name}") + qasm3_string = f""" + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q; + {gate_name}(0.5) q; + {gate_name}(0.5) q[0]; + """ + return qasm3_string + + return test_fixture + + +# Generate rotation gate fixtures +for gate in ONE_QUBIT_ROTATION_MAP: + name = _fixture_name(gate) + locals()[name] = _generate_rotation_fixture(gate) + + +def _generate_two_qubit_fixture(gate_name: str): + @pytest.fixture() + def test_fixture(): + if not _validate_gate_name(gate_name): + raise ValueError(f"Unknown qasm3 gate {gate_name}") + qasm3_string = f""" + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q; + {gate_name} q[0], q[1]; + {gate_name} q; + """ + return qasm3_string + + return test_fixture + + +# Generate double-qubit gate fixtures +for gate in TWO_QUBIT_OP_MAP: + name = _fixture_name(gate) + locals()[name] = _generate_two_qubit_fixture(gate) + + +def _generate_three_qubit_fixture(gate_name: str): + @pytest.fixture() + def test_fixture(): + if not _validate_gate_name(gate_name): + raise ValueError(f"Unknown qasm3 gate {gate_name}") + qasm3_string = f""" + OPENQASM 3; + include "stdgates.inc"; + + qubit[3] q; + {gate_name} q[0], q[1], q[2]; + {gate_name} q; + """ + return qasm3_string + + return test_fixture + + +# Generate three-qubit gate fixtures +for gate in THREE_QUBIT_OP_MAP: + name = _fixture_name(gate) + locals()[name] = _generate_three_qubit_fixture(gate) + + +def _generate_custom_op_fixture(op_name: str): + print(os.getcwd()) + + @pytest.fixture() + def test_fixture(): + if not op_name in CUSTOM_OPS: + raise ValueError(f"Invalid fixture {op_name} for custom ops") + path = resources_file(f"custom_gate_{op_name}.qasm") + with open(path, "r", encoding="utf-8") as file: + return file.read() + + return test_fixture + + +for test_name in CUSTOM_OPS: + name = _fixture_name(test_name) + locals()[name] = _generate_custom_op_fixture(test_name) + +single_op_tests = [_fixture_name(s) for s in ONE_QUBIT_OP_MAP] +already_tested_single_op = ["id", "si", "ti", "v", "sx", "vi", "sxdg"] +for gate in already_tested_single_op: + single_op_tests.remove(_fixture_name(gate)) + +rotation_tests = [_fixture_name(s) for s in ONE_QUBIT_ROTATION_MAP if "u" not in s.lower()] +already_tested_rotation = ["prx", "phaseshift", "p", "gpi", "gpi2"] +for gate in already_tested_rotation: + rotation_tests.remove(_fixture_name(gate)) + +double_op_tests = [_fixture_name(s) for s in TWO_QUBIT_OP_MAP] +already_tested_double_op = [ + "cv", + "cy", + "xx", + "xy", + "yy", + "zz", + "pswap", + "cp", + "cp00", + "cp01", + "cp10", + "cphaseshift", + "cphaseshift00", + "cphaseshift01", + "cphaseshift10", + "ecr", + "ms", +] +for gate in already_tested_double_op: + double_op_tests.remove(_fixture_name(gate)) + +triple_op_tests = [_fixture_name(s) for s in THREE_QUBIT_OP_MAP] +already_tested_triple_op = ["ccnot", "cswap"] +for gate in already_tested_triple_op: + triple_op_tests.remove(_fixture_name(gate)) + +custom_op_tests = [_fixture_name(s) for s in CUSTOM_OPS] + +# qasm_input, expected_error +SINGLE_QUBIT_GATE_INCORRECT_TESTS = { + "missing_register": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + h q2; // undeclared register + """, + "Missing register declaration for q2 .*", + ), + "undeclared_1qubit_op": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u_abc(0.5, 0.5, 0.5) q1; // unsupported gate + """, + "Unsupported / undeclared QASM operation: u_abc", + ), + "undeclared_1qubit_op_with_indexing": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u_abc(0.5, 0.5, 0.5) q1[0], q1[1]; // unsupported gate + """, + "Unsupported / undeclared QASM operation: u_abc", + ), + "undeclared_3qubit_op": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[3] q1; + u_abc(0.5, 0.5, 0.5) q1[0], q1[1], q1[2]; // unsupported gate + """, + "Unsupported / undeclared QASM operation: u_abc", + ), + "invalid_gate_application": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[3] q1; + cx q1; // invalid application of gate, as we apply it to 3 qubits in blocks of 2 + """, + "Invalid number of qubits 3 for operation .*", + ), + "unsupported_parameter_type": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + rx(a) q1; // unsupported parameter type + """, + "Undefined identifier a in.*", + ), +} + +# qasm_input, expected_error +CUSTOM_GATE_INCORRECT_TESTS = { + "undeclared_custom": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + custom_gate q1; // undeclared gate + """, + "Unsupported / undeclared QASM operation: custom_gate", + ), + "parameter_mismatch": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + gate custom_gate(a,b) p, q{ + rx(a) p; + ry(b) q; + } + + qubit[2] q1; + custom_gate(0.5) q1; // parameter count mismatch + """, + "Parameter count mismatch for gate custom_gate: expected 2 arguments, but got 1 instead.", + ), + "qubit_mismatch": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + gate custom_gate(a,b) p, q{ + rx(a) p; + ry(b) q; + } + + qubit[3] q1; + custom_gate(0.5, 0.5) q1; // qubit count mismatch + """, + "Qubit count mismatch for gate custom_gate: expected 2 qubits, but got 3 instead.", + ), + "indexing_not_supported": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + gate custom_gate(a,b) p, q{ + rx(a) p; + ry(b) q[0]; + } + + qubit[2] q1; + custom_gate(0.5, 0.5) q1; // indexing not supported + """, + "Indexing .* not supported in gate definition", + ), + "recursive_definition": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + gate custom_gate(a,b) p, q{ + custom_gate(a,b) p, q; + } + + qubit[2] q1; + custom_gate(0.5, 0.5) q1; // recursive definition + """, + "Recursive definitions not allowed .*", + ), + "duplicate_definition": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + gate custom_gate(a,b) p, q{ + rx(a) p; + ry(b) q; + } + + gate custom_gate(a,b) p, q{ + rx(a) p; + ry(b) q; + } + + qubit[2] q1; + custom_gate(0.5, 0.5) q1; // duplicate definition + """, + "Duplicate gate definition for custom_gate", + ), +} diff --git a/tests/resources/qasm/custom_gate_complex.qasm b/tests/resources/qasm/custom_gate_complex.qasm new file mode 100644 index 0000000..5652971 --- /dev/null +++ b/tests/resources/qasm/custom_gate_complex.qasm @@ -0,0 +1,23 @@ +OPENQASM 3; +include "stdgates.inc"; + +gate custom1 a{ + h a; + x a; + rx(0.5) a; +} + +gate custom2(p) b { + custom1 b; + ry(p) b; +} + +gate custom3(p, q) c, d { + custom2(p) c; + rz(q) c; + cx c, d; +} + +qubit[2] q; + +custom3(0.1, 0.2) q[0:]; \ No newline at end of file diff --git a/tests/resources/qasm/custom_gate_nested.qasm b/tests/resources/qasm/custom_gate_nested.qasm new file mode 100644 index 0000000..918c5d9 --- /dev/null +++ b/tests/resources/qasm/custom_gate_nested.qasm @@ -0,0 +1,17 @@ +OPENQASM 3.0; +include "stdgates.inc"; + +gate custom2(g) s { + h s; + rz(g) s; +} +gate custom(a,b,c) p, q{ + custom2(a) q; + h p; + cx p,q; + rx(a) q; + ry(0.5/0.1) q; +} + +qubit[2] q; +custom(2 + 3 - 1/5, 0.1, 0.3) q[0], q[1]; diff --git a/tests/resources/qasm/custom_gate_simple.qasm b/tests/resources/qasm/custom_gate_simple.qasm new file mode 100644 index 0000000..acd4a0c --- /dev/null +++ b/tests/resources/qasm/custom_gate_simple.qasm @@ -0,0 +1,12 @@ +OPENQASM 3; +include "stdgates.inc"; + +gate custom(a) p, q { + h p; + z q; + rx(a) p; + cx p,q; +} + +qubit[2] q; +custom(0.1+1) q[0], q[1]; \ No newline at end of file diff --git a/tests/test_alias.py b/tests/test_alias.py index 19591b9..c0ab01c 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -14,17 +14,18 @@ """ -# import re +import re -# import pytest -# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir +import pytest -# from tests.qir_utils import ( -# check_attributes, -# check_single_qubit_gate_op, -# check_three_qubit_gate_op, -# check_two_qubit_gate_op, -# ) +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import ( + check_single_qubit_gate_op, + check_three_qubit_gate_op, + check_two_qubit_gate_op, +) # from .test_if import compare_reference_ir, resources_file @@ -54,17 +55,16 @@ # swap myqreg5[0], myqreg5[1]; # cz myqreg6; # """ -# result = qasm3_to_qir(qasm3_alias_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 5) - -# check_single_qubit_gate_op(generated_qir, 1, [0], "x") -# check_single_qubit_gate_op(generated_qir, 1, [1], "h") -# check_two_qubit_gate_op(generated_qir, 2, [[1, 2], [2, 3]], "cx") -# check_three_qubit_gate_op(generated_qir, 1, [[1, 2, 3]], "ccx") -# check_two_qubit_gate_op(generated_qir, 1, [[1, 3]], "swap") -# check_two_qubit_gate_op(generated_qir, 1, [[0, 1]], "cz") +# result = unroll(qasm3_alias_program) +# assert result.num_qubits == 5 +# assert result.num_clbits == 0 +# print(result.unrolled_qasm) +# check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") +# check_single_qubit_gate_op(result.unrolled_ast, 1, [1], "h") +# check_two_qubit_gate_op(result.unrolled_ast, 2, [[1, 2], [2, 3]], "cx") +# check_three_qubit_gate_op(result.unrolled_ast, 1, [[1, 2, 3]], "ccx") +# check_two_qubit_gate_op(result.unrolled_ast, 1, [[1, 3]], "swap") +# check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") # def test_alias_update(): @@ -81,12 +81,10 @@ # x alias[1]; # """ -# result = qasm3_to_qir(qasm3_alias_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 4) - -# check_single_qubit_gate_op(generated_qir, 1, [3], "x") +# result = unroll(qasm3_alias_program) +# assert result.num_qubits == 4 +# assert result.num_clbits == 0 +# check_single_qubit_gate_op(result.unrolled_ast, 1, [3], "x") # def test_valid_alias_redefinition(): @@ -108,106 +106,106 @@ # let alias = q[2]; # x alias; # """ -# result = qasm3_to_qir(qasm3_alias_program, name="test") -# generated_qir = str(result).splitlines() +# result = unroll(qasm3_alias_program) +# assert result.num_qubits == 5 +# assert result.num_clbits == 5 -# check_attributes(generated_qir, 5, 5) -# check_single_qubit_gate_op(generated_qir, 1, [2], "x") +# check_single_qubit_gate_op(result.unrolled_ast, 1, [2], "x") -# def test_alias_wrong_indexing(): -# """Test converting OpenQASM 3 program with wrong alias indexing.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=re.escape( -# r"An index set can be specified by a single integer (signed or unsigned), " -# "a comma-separated list of integers contained in braces {a,b,c,…}, or a range" -# ), -# ): -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_alias_wrong_indexing(): + """Test converting OpenQASM 3 program with wrong alias indexing.""" + with pytest.raises( + ValidationError, + match=re.escape( + r"An index set can be specified by a single integer (signed or unsigned), " + "a comma-separated list of integers contained in braces {a,b,c,…}, or a range" + ), + ): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[5] q; + qubit[5] q; -# let myqreg = q[1,2]; + let myqreg = q[1,2]; -# x myqreg[0]; -# """ -# _ = qasm3_to_qir(qasm3_alias_program, name="test") + x myqreg[0]; + """ + _ = validate(qasm3_alias_program) -# def test_alias_invalid_discrete_indexing(): -# """Test converting OpenQASM 3 program with invalid alias discrete indexing.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=r"Unsupported discrete set value .*", -# ): -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_alias_invalid_discrete_indexing(): + """Test converting OpenQASM 3 program with invalid alias discrete indexing.""" + with pytest.raises( + ValidationError, + match=r"Unsupported discrete set value .*", + ): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[5] q; + qubit[5] q; -# let myqreg = q[{0.1}]; + let myqreg = q[{0.1}]; -# x myqreg[0]; -# """ -# _ = qasm3_to_qir(qasm3_alias_program, name="test") + x myqreg[0]; + """ + _ = validate(qasm3_alias_program) -# def test_invalid_alias_redefinition(): -# """Test converting OpenQASM 3 program with redefined alias.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=re.escape(r"Re-declaration of variable 'alias'"), -# ): -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_invalid_alias_redefinition(): + """Test converting OpenQASM 3 program with redefined alias.""" + with pytest.raises( + ValidationError, + match=re.escape(r"Re-declaration of variable 'alias'"), + ): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[5] q; -# float[32] alias = 4.2; + qubit[5] q; + float[32] alias = 4.2; -# let alias = q[2]; + let alias = q[2]; -# x alias; -# """ -# _ = qasm3_to_qir(qasm3_alias_program, name="test") + x alias; + """ + _ = validate(qasm3_alias_program) -# def test_alias_defined_before(): -# """Test converting OpenQASM 3 program with alias defined before the qubit register.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=re.escape(r"Qubit register q2 not found for aliasing"), -# ): -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_alias_defined_before(): + """Test converting OpenQASM 3 program with alias defined before the qubit register.""" + with pytest.raises( + ValidationError, + match=re.escape(r"Qubit register q2 not found for aliasing"), + ): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[5] q1; + qubit[5] q1; -# let myqreg = q2[1]; -# """ -# _ = qasm3_to_qir(qasm3_alias_program, name="test") + let myqreg = q2[1]; + """ + _ = validate(qasm3_alias_program) -# def test_unsupported_alias(): -# """Test converting OpenQASM 3 program with unsupported alias.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=r"Unsupported aliasing .*", -# ): -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_unsupported_alias(): + """Test converting OpenQASM 3 program with unsupported alias.""" + with pytest.raises( + ValidationError, + match=r"Unsupported aliasing .*", + ): + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[5] q; + qubit[5] q; -# let myqreg = q[0] ++ q[1]; -# """ -# _ = qasm3_to_qir(qasm3_alias_program, name="test") + let myqreg = q[0] ++ q[1]; + """ + _ = validate(qasm3_alias_program) # def test_alias_in_scope_1(): @@ -234,10 +232,10 @@ # h q[2]; # } # """ -# result = qasm3_to_qir(qasm) -# generated_qir = str(result).splitlines() +# result = validate(qasm) +# result.unrolled_ast = str(result).splitlines() -# check_attributes(generated_qir, 4, 4) +# check_attributes(result.unrolled_ast, 4, 4) # simple_file = resources_file("simple_if.ll") # compare_reference_ir(result.bitcode, simple_file) @@ -267,10 +265,10 @@ # h q[2]; # } # """ -# result = qasm3_to_qir(qasm) -# generated_qir = str(result).splitlines() +# result = validate(qasm) +# result.unrolled_ast = str(result).splitlines() -# check_attributes(generated_qir, 4, 4) +# check_attributes(result.unrolled_ast, 4, 4) # simple_file = resources_file("simple_if.ll") # compare_reference_ir(result.bitcode, simple_file) @@ -278,7 +276,7 @@ # def test_alias_out_of_scope(): # """Test converting OpenQASM 3 program with alias out of scope.""" # with pytest.raises( -# Qasm3ConversionError, +# ValidationError, # match=r"Variable alias not in scope for operation .*", # ): # qasm = """ @@ -303,4 +301,4 @@ # h q[2]; # } # """ -# _ = qasm3_to_qir(qasm) +# _ = validate(qasm) diff --git a/tests/test_barrier.py b/tests/test_barrier.py index 0804cdc..46693e0 100644 --- a/tests/test_barrier.py +++ b/tests/test_barrier.py @@ -38,10 +38,8 @@ def test_barrier(): barrier q1, q2[0:2], q3[:]; """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 6 - // Total number of clbits: 0 qubit[2] q1; qubit[3] q2; qubit[1] q3; @@ -80,10 +78,8 @@ def my_function(qubit[4] a) { my_function(q); """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 4 - // Total number of clbits: 0 qubit[4] q; barrier q[0]; barrier q[1]; diff --git a/tests/test_gates.py b/tests/test_gates.py index 75b84da..e28f6b7 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -11,256 +11,241 @@ Module containing unit tests for unrolling quantum gates. """ -# import pytest -# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir - -# from tests.qasm3_qir.fixtures.gates import ( -# CUSTOM_GATE_INCORRECT_TESTS, -# SINGLE_QUBIT_GATE_INCORRECT_TESTS, -# custom_op_tests, -# double_op_tests, -# rotation_tests, -# single_op_tests, -# triple_op_tests, -# ) -# from tests.qir_utils import ( -# check_attributes, -# check_custom_qasm_gate_op, -# check_single_qubit_gate_op, -# check_single_qubit_rotation_op, -# check_three_qubit_gate_op, -# check_two_qubit_gate_op, -# ) - - -# # 7. Test gate operations in different ways -# @pytest.mark.parametrize("circuit_name", single_op_tests) -# def test_single_qubit_qasm3_gates(circuit_name, request): -# # see _generate_one_qubit_fixture for details -# qubit_list = [0, 1, 0, 0, 1] -# gate_name = circuit_name.removeprefix("Fixture_") - -# qasm3_string = request.getfixturevalue(circuit_name) -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_gate_op(generated_qir, 5, qubit_list, gate_name) - - -# @pytest.mark.parametrize("circuit_name", double_op_tests) -# def test_two_qubit_qasm3_gates(circuit_name, request): -# qubit_list = [[0, 1], [0, 1]] -# gate_name = circuit_name.removeprefix("Fixture_") - -# qasm3_string = request.getfixturevalue(circuit_name) -# result = qasm3_to_qir(qasm3_string) - -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_two_qubit_gate_op(generated_qir, 2, qubit_list, gate_name) - - -# @pytest.mark.parametrize("circuit_name", rotation_tests) -# def test_rotation_qasm3_gates(circuit_name, request): -# qubit_list = [0, 1, 0] -# param_list = [0.5, 0.5, 0.5] -# gate_name = circuit_name.removeprefix("Fixture_") - -# qasm3_string = request.getfixturevalue(circuit_name) -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_rotation_op(generated_qir, 3, qubit_list, param_list, gate_name) - - -# @pytest.mark.parametrize("circuit_name", triple_op_tests) -# def test_three_qubit_qasm3_gates(circuit_name, request): -# qubit_list = [[0, 1, 2], [0, 1, 2]] -# gate_name = circuit_name.removeprefix("Fixture_") - -# qasm3_string = request.getfixturevalue(circuit_name) -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 3, 0) -# check_three_qubit_gate_op(generated_qir, 2, qubit_list, gate_name) - - -# def test_gate_body_param_expression(): -# qasm3_str = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# gate my_gate_2(p) q { -# ry(p * 2) q; -# } - -# gate my_gate(a, b, c) q { -# rx(5 * a) q; -# rz(2 * b / a) q; -# my_gate_2(a) q; -# rx(!a) q; // not a = False -# rx(c) q; -# } - -# qubit q; -# int[32] m = 3; -# float[32] n = 6.0; -# bool o = true; -# my_gate(m, n, o) q; -# """ -# result = qasm3_to_qir(qasm3_str) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [5 * 3, 0.0, True], "rx") -# check_single_qubit_rotation_op(generated_qir, 1, [0], [2 * 6.0 / 3], "rz") -# check_single_qubit_rotation_op(generated_qir, 1, [0], [3 * 2], "ry") - - -# def test_id_gate(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# qubit q; -# id q; -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# # we have 2 X gates for id -# check_single_qubit_gate_op(generated_qir, 2, [0, 0], "x") - - -# def test_qasm_u3_gates(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# qubit[2] q1; -# u3(0.5, 0.5, 0.5) q1[0]; -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_rotation_op(generated_qir, 1, [0], [0.5, 0.5, 0.5], "u3") - - -# def test_qasm_u2_gates(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# qubit[2] q1; -# u2(0.5, 0.5) q1[0]; -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_rotation_op(generated_qir, 1, [0], [0.5, 0.5], "u2") - - -# @pytest.mark.parametrize("test_name", SINGLE_QUBIT_GATE_INCORRECT_TESTS.keys()) -# def test_incorrect_single_qubit_gates(test_name): -# qasm_input, error_message = SINGLE_QUBIT_GATE_INCORRECT_TESTS[test_name] -# with pytest.raises(Qasm3ConversionError, match=error_message): -# _ = qasm3_to_qir(qasm_input) - - -# @pytest.mark.parametrize("test_name", custom_op_tests) -# def test_custom_ops(test_name, request): -# qasm3_string = request.getfixturevalue(test_name) -# gate_type = test_name.removeprefix("Fixture_") -# result = qasm3_to_qir(qasm3_string) - -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) - -# # Check for custom gate definition -# check_custom_qasm_gate_op(generated_qir, gate_type) - - -# def test_pow_gate_modifier(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit q; -# inv @ pow(2) @ pow(4) @ h q; -# pow(-2) @ h q; -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_gate_op(generated_qir, 10, [0] * 10, "h") - - -# def test_inv_gate_modifier(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit q; -# inv @ h q; -# inv @ y q; -# inv @ rx(0.5) q; -# inv @ s q; - -# qubit[2] q2; -# inv @ cx q2; -# inv @ ccx q[0], q2; -# inv @ u2(0.5, 0.5) q2[0]; -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 3, 0) -# check_single_qubit_gate_op(generated_qir, 1, [0], "h") -# check_single_qubit_gate_op(generated_qir, 1, [0], "y") -# check_single_qubit_rotation_op(generated_qir, 1, [0], [-0.5], "rx") -# check_single_qubit_gate_op(generated_qir, 1, [0], "sdg") -# check_two_qubit_gate_op(generated_qir, 1, [[1, 2]], "cx") -# check_three_qubit_gate_op(generated_qir, 1, [[0, 1, 2]], "ccx") - - -# def test_nested_gate_modifiers(): -# complex_qir = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# gate custom2 p, q{ -# y p; -# z q; -# } -# gate custom p, q { -# pow(1) @ custom2 p, q; -# } -# pow(1) @ inv @ pow(2) @ custom q; -# pow(-1) @ custom q; -# """ -# ) -# generated_qir = str(complex_qir).splitlines() -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_gate_op(generated_qir, 2, [0, 0, 0], "y") -# check_single_qubit_gate_op(generated_qir, 2, [1, 1, 1], "z") - - -# def test_unsupported_modifiers(): -# # TO DO : add implementations, but till then we have tests -# for modifier in ["ctrl", "negctrl"]: -# with pytest.raises( -# NotImplementedError, -# match=r"Controlled modifier gates not yet supported .*", -# ): -# _ = qasm3_to_qir( -# f""" -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# {modifier} @ h q[0], q[1]; -# """ -# ) - - -# @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) -# def test_incorrect_custom_ops(test_name): -# qasm_input, error_message = CUSTOM_GATE_INCORRECT_TESTS[test_name] -# with pytest.raises(Qasm3ConversionError, match=error_message): -# _ = qasm3_to_qir(qasm_input) +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.resources.gates import ( + CUSTOM_GATE_INCORRECT_TESTS, + SINGLE_QUBIT_GATE_INCORRECT_TESTS, + custom_op_tests, + double_op_tests, + rotation_tests, + single_op_tests, + triple_op_tests, +) +from tests.utils import ( + check_custom_qasm_gate_op, + check_single_qubit_gate_op, + check_single_qubit_rotation_op, + check_three_qubit_gate_op, + check_two_qubit_gate_op, +) + + +# 7. Test gate operations in different ways +@pytest.mark.parametrize("circuit_name", single_op_tests) +def test_single_qubit_qasm3_gates(circuit_name, request): + # see _generate_one_qubit_fixture for details + qubit_list = [0, 1, 0, 0, 1] + gate_name = circuit_name.removeprefix("Fixture_") + + qasm3_string = request.getfixturevalue(circuit_name) + result = unroll(qasm3_string) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 5, qubit_list, gate_name) + + +@pytest.mark.parametrize("circuit_name", double_op_tests) +def test_two_qubit_qasm3_gates(circuit_name, request): + qubit_list = [[0, 1], [0, 1]] + gate_name = circuit_name.removeprefix("Fixture_") + + qasm3_string = request.getfixturevalue(circuit_name) + result = unroll(qasm3_string) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_two_qubit_gate_op(result.unrolled_ast, 2, qubit_list, gate_name) + + +@pytest.mark.parametrize("circuit_name", rotation_tests) +def test_rotation_qasm3_gates(circuit_name, request): + qubit_list = [0, 1, 0] + param_list = [0.5, 0.5, 0.5] + gate_name = circuit_name.removeprefix("Fixture_") + + qasm3_string = request.getfixturevalue(circuit_name) + result = unroll(qasm3_string) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_rotation_op(result.unrolled_ast, 3, qubit_list, param_list, gate_name) + + +@pytest.mark.parametrize("circuit_name", triple_op_tests) +def test_three_qubit_qasm3_gates(circuit_name, request): + qubit_list = [[0, 1, 2], [0, 1, 2]] + gate_name = circuit_name.removeprefix("Fixture_") + + qasm3_string = request.getfixturevalue(circuit_name) + result = unroll(qasm3_string) + assert result.num_qubits == 3 + assert result.num_clbits == 0 + check_three_qubit_gate_op(result.unrolled_ast, 2, qubit_list, gate_name) + + +def test_gate_body_param_expression(): + qasm3_str = """ + OPENQASM 3; + include "stdgates.inc"; + + gate my_gate_2(p) q { + ry(p * 2) q; + } + + gate my_gate(a, b, c) q { + rx(5 * a) q; + rz(2 * b / a) q; + my_gate_2(a) q; + rx(!a) q; // not a = False + rx(c) q; + } + + qubit q; + int[32] m = 3; + float[32] n = 6.0; + bool o = true; + my_gate(m, n, o) q; + """ + result = unroll(qasm3_str) + assert result.num_qubits == 1 + assert result.num_clbits == 0 + + check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 0, 0], [5 * 3, 0.0, True], "rx") + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [2 * 6.0 / 3], "rz") + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [3 * 2], "ry") + + +def test_qasm_u3_gates(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u3(0.5, 0.5, 0.5) q1[0]; + """ + result = unroll(qasm3_string) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [0.5, 0.5, 0.5], "u3") + + +def test_qasm_u2_gates(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u2(0.5, 0.5) q1[0]; + """ + result = unroll(qasm3_string) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [0.5, 0.5], "u2") + + +@pytest.mark.parametrize("test_name", SINGLE_QUBIT_GATE_INCORRECT_TESTS.keys()) +def test_incorrect_single_qubit_gates(test_name): + qasm_input, error_message = SINGLE_QUBIT_GATE_INCORRECT_TESTS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) + + +@pytest.mark.parametrize("test_name", custom_op_tests) +def test_custom_ops(test_name, request): + qasm3_string = request.getfixturevalue(test_name) + gate_type = test_name.removeprefix("Fixture_") + result = unroll(qasm3_string) + + assert result.num_qubits == 2 + assert result.num_clbits == 0 + + # Check for custom gate definition + check_custom_qasm_gate_op(result.unrolled_ast, gate_type) + + +def test_pow_gate_modifier(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit q; + inv @ pow(2) @ pow(4) @ h q; + pow(-2) @ h q; + """ + result = unroll(qasm3_string) + assert result.num_qubits == 1 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 10, [0] * 10, "h") + + +def test_inv_gate_modifier(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit q; + inv @ h q; + inv @ y q; + inv @ rx(0.5) q; + inv @ s q; + + qubit[2] q2; + inv @ cx q2; + inv @ ccx q[0], q2; + """ + result = unroll(qasm3_string) + print(result.unrolled_qasm) + assert result.num_qubits == 3 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "y") + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [-0.5], "rx") + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "sdg") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx") + check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 0, 1]], "ccx") + + +def test_nested_gate_modifiers(): + qasm_str = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + gate custom2 p, q{ + y p; + z q; + } + gate custom p, q { + pow(1) @ custom2 p, q; + } + pow(1) @ inv @ pow(2) @ custom q; + pow(-1) @ custom q; + """ + result = unroll(qasm_str) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 3, [1, 1, 1], "z") + check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 0, 0], "y") + + +def test_unsupported_modifiers(): + # TO DO : add implementations, but till then we have tests + for modifier in ["ctrl", "negctrl"]: + with pytest.raises( + NotImplementedError, + match=r"Controlled modifier gates not yet supported .*", + ): + _ = validate( + f""" + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + {modifier} @ h q[0], q[1]; + """ + ) + + +@pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) +def test_incorrect_custom_ops(test_name): + qasm_input, error_message = CUSTOM_GATE_INCORRECT_TESTS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) diff --git a/tests/test_measurement.py b/tests/test_measurement.py index 51788b9..a6ee701 100644 --- a/tests/test_measurement.py +++ b/tests/test_measurement.py @@ -40,23 +40,21 @@ def test_measure(): """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 8 - // Total number of clbits: 3 qubit[2] q1; qubit[5] q2; qubit[1] q3; bit[2] c1; bit[1] c2; - measure q1[0] -> c1[0]; - measure q1[1] -> c1[1]; - measure q1[0] -> c1[0]; - measure q1[1] -> c1[1]; - measure q3[0] -> c2[0]; - measure q1[0] -> c1[1]; - measure q2[0] -> c1[1]; - measure q2[1] -> c1[0]; + c1[0] = measure q1[0]; + c1[1] = measure q1[1]; + c1[0] = measure q1[0]; + c1[1] = measure q1[1]; + c2[0] = measure q3[0]; + c1[1] = measure q1[0]; + c1[1] = measure q2[0]; + c1[0] = measure q2[1]; """ unrolled_qasm = unroll(qasm3_string).unrolled_qasm diff --git a/tests/test_reset.py b/tests/test_reset.py index a8ce9e5..cb3e65b 100644 --- a/tests/test_reset.py +++ b/tests/test_reset.py @@ -38,10 +38,8 @@ def test_reset_operations(): reset q3[:2]; """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 6 - // Total number of clbits: 0 qubit[1] q1; qubit[2] q2; qubit[3] q3; @@ -69,10 +67,8 @@ def my_function(qubit a) { my_function(q[1]); """ - expected_qasm = """OPENQASM 3.0; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; - // Total number of qubits: 3 - // Total number of clbits: 0 qubit[3] q; reset q[1]; """ diff --git a/tests/utils.py b/tests/utils.py index 7a4966c..981108e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,6 +8,10 @@ # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. +import openqasm3.ast as qasm3_ast + +from pyqasm.maps import CONSTANTS_MAP + def check_unrolled_qasm(unrolled_qasm, expected_qasm): """Check that the unrolled qasm matches the expected qasm. @@ -27,3 +31,122 @@ def check_unrolled_qasm(unrolled_qasm, expected_qasm): for unrolled_line, expected_line in zip(unrolled_qasm, expected_qasm): print(unrolled_line, expected_line) assert unrolled_line.strip() == expected_line.strip() + + +def check_single_qubit_gate_op(unrolled_ast, num_gates, qubit_list, gate_name): + qubit_id, gate_count = 0, 0 + for stmt in unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.QuantumGate) and stmt.name.name == gate_name: + assert len(stmt.qubits) == 1 + assert stmt.qubits[0].indices[0][0].value == qubit_list[qubit_id] + qubit_id += 1 + gate_count += 1 + + assert gate_count == num_gates + + +def check_two_qubit_gate_op(unrolled_ast, num_gates, qubit_list, gate_name): + qubit_id, gate_count = 0, 0 + if gate_name == "cnot": + gate_name = "cx" + for stmt in unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.QuantumGate) and stmt.name.name == gate_name.lower(): + assert len(stmt.qubits) == 2 + assert stmt.qubits[0].indices[0][0].value == qubit_list[qubit_id][0] + assert stmt.qubits[1].indices[0][0].value == qubit_list[qubit_id][1] + qubit_id += 1 + gate_count += 1 + + assert gate_count == num_gates + + +def check_three_qubit_gate_op(unrolled_ast, num_gates, qubit_list, gate_name): + qubit_id, gate_count = 0, 0 + for stmt in unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.QuantumGate) and stmt.name.name == gate_name.lower(): + assert len(stmt.qubits) == 3 + assert stmt.qubits[0].indices[0][0].value == qubit_list[qubit_id][0] + assert stmt.qubits[1].indices[0][0].value == qubit_list[qubit_id][1] + assert stmt.qubits[2].indices[0][0].value == qubit_list[qubit_id][2] + qubit_id += 1 + gate_count += 1 + + assert gate_count == num_gates + + +def check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list): + theta, phi, lam = param_list + op_count = 0 + q_id = 0 + pi = CONSTANTS_MAP["pi"] + u3_param_list = [lam, pi / 2, theta + pi, pi / 2, phi + pi] + u3_gate_list = ["rz", "rx", "rz", "rx", "rz"] + u3_gates_id = 0 + + for stmt in unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.QuantumGate) and stmt.name.name == u3_gate_list[u3_gates_id]: + assert len(stmt.qubits) == 1 + assert stmt.qubits[0].indices[0][0].value == qubit_list[q_id] + assert stmt.arguments[0].value == u3_param_list[u3_gates_id] + u3_gates_id += 1 + if u3_gates_id == 5: + u3_gates_id = 0 + op_count += 1 + q_id += 1 + + assert op_count == num_gates + + +def check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_list, gate_name): + if gate_name == "u3": + check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list) + return + elif gate_name == "u2": + param_list = [CONSTANTS_MAP["pi"] / 2, param_list[0], param_list[1]] + check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list) + return + qubit_id, param_id, gate_count = 0, 0, 0 + for stmt in unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.QuantumGate) and stmt.name.name == gate_name: + assert len(stmt.qubits) == 1 + assert stmt.qubits[0].indices[0][0].value == qubit_list[qubit_id] + assert stmt.arguments[0].value == param_list[param_id] + qubit_id += 1 + param_id += 1 + gate_count += 1 + assert gate_count == num_gates + + +def _validate_simple_custom_gate(unrolled_ast): + check_single_qubit_gate_op(unrolled_ast, 1, [0], "h") + check_single_qubit_gate_op(unrolled_ast, 1, [1], "z") + check_single_qubit_rotation_op(unrolled_ast, 1, [0], [1.1], "rx") + check_two_qubit_gate_op(unrolled_ast, 1, [(0, 1)], "cx") + + +def _validate_nested_custom_gate(unrolled_ast): + check_single_qubit_gate_op(unrolled_ast, 2, [1, 0], "h") + check_single_qubit_rotation_op(unrolled_ast, 1, [1], [4.8], "rz") + check_two_qubit_gate_op(unrolled_ast, 1, [[0, 1]], "cx") + check_single_qubit_rotation_op(unrolled_ast, 1, [1], [4.8], "rx") + check_single_qubit_rotation_op(unrolled_ast, 1, [1], [5], "ry") + + +def _validate_complex_custom_gate(unrolled_ast): + check_single_qubit_gate_op(unrolled_ast, 1, [0], "h") + check_single_qubit_gate_op(unrolled_ast, 1, [0], "x") + check_single_qubit_rotation_op(unrolled_ast, 1, [0], [0.5], "rx") + check_single_qubit_rotation_op(unrolled_ast, 1, [0], [0.1], "ry") + check_single_qubit_rotation_op(unrolled_ast, 1, [0], [0.2], "rz") + check_two_qubit_gate_op(unrolled_ast, 1, [[0, 1]], "cx") + + +def check_custom_qasm_gate_op(unrolled_ast, test_type): + test_function_map = { + "simple": _validate_simple_custom_gate, + "nested": _validate_nested_custom_gate, + "complex": _validate_complex_custom_gate, + } + if test_type not in test_function_map: + raise ValueError(f"Unknown test type {test_type}") + test_function_map[test_type](unrolled_ast) From 0ae83ad9ce2db7cae610c10fbdef46e28d9ebcac Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 8 Oct 2024 14:12:12 +0530 Subject: [PATCH 04/16] update gitignore --- .gitignore | 2 ++ pyqasm/_version.py | 17 ----------------- 2 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 pyqasm/_version.py diff --git a/.gitignore b/.gitignore index 82f9275..cd27709 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +pyqasm/_version.py + # PyInstaller # Usually these files are written by a python script from a template diff --git a/pyqasm/_version.py b/pyqasm/_version.py deleted file mode 100644 index c390f01..0000000 --- a/pyqasm/_version.py +++ /dev/null @@ -1,17 +0,0 @@ -# file generated by setuptools_scm -# don't change, don't track in version control -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple, Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] -else: - VERSION_TUPLE = object - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE - -__version__ = version = "0.0.1.dev2+gfb1a0d8.d20241008" -__version_tuple__ = version_tuple = (0, 0, 1, "dev2", "gfb1a0d8.d20241008") From 65afbe11097a16aadf4a7e2768493759b3608ec3 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 8 Oct 2024 14:32:58 +0530 Subject: [PATCH 05/16] update expr and sizeof --- tests/test_expressions.py | 104 ++++++++++++------------- tests/test_sizeof.py | 156 +++++++++++++++++++------------------- tests/utils.py | 1 + 3 files changed, 126 insertions(+), 135 deletions(-) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 808b653..875781e 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -11,59 +11,51 @@ Module containing unit tests for expressions. """ -# import pytest -# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir - -# from tests.qir_utils import check_attributes, check_expressions - - -# def test_correct_expressions(): -# qasm_str = """OPENQASM 3; -# qubit q; - -# // supported -# rx(1.57) q; -# rz(3-2*3) q; -# rz(3-2*3*(8/2)) q; -# rx(-1.57) q; -# rx(4%2) q; -# rx(true) q; -# rx(!0) q; -# rx(~3) q; - -# int a = 5; -# float b = 10*a*pi; -# array[int[32], 2] c; -# c[0] = 1; -# c[1] = c[0] + 2; - - -# """ - -# result = qasm3_to_qir(qasm_str) -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1, 0) -# gates = ["rx", "rz", "rz", "rx", "rx"] -# expression_values = [1.57, 3 - 2 * 3, 3 - 2 * 3 * (8 / 2), -1.57, 4 % 2] -# qubits = [0, 0, 0, 0, 0] -# check_expressions(generated_qir, 5, gates, expression_values, qubits) - - -# def test_incorrect_expressions(): -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported expression type .*"): -# qasm3_to_qir("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;") -# with pytest.raises( -# Qasm3ConversionError, match=r"Unsupported expression type .* in ~ operation" -# ): -# qasm3_to_qir("OPENQASM 3; qubit q; rx(~1.3) q;") -# with pytest.raises( -# Qasm3ConversionError, match=r"Unsupported expression type .* in ~ operation" -# ): -# qasm3_to_qir("OPENQASM 3; qubit q; rx(~1.3+5im) q;") - -# with pytest.raises(Qasm3ConversionError, match="Undefined identifier x in expression"): -# qasm3_to_qir("OPENQASM 3; qubit q; rx(x) q;") - -# with pytest.raises(Qasm3ConversionError, match="Uninitialized variable x in expression"): -# qasm3_to_qir("OPENQASM 3; qubit q; int x; rx(x) q;") +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_single_qubit_rotation_op + + +def test_correct_expressions(): + qasm_str = """OPENQASM 3; + qubit q; + + // supported + rx(1.57) q; + rz(3-2*3) q; + rz(3-2*3*(8/2)) q; + rx(-1.57) q; + rx(4%2) q; + + int a = 5; + float b = 10*a*pi; + array[int[32], 2] c; + c[0] = 1; + c[1] = c[0] + 2; + """ + + result = unroll(qasm_str) + assert result.num_qubits == 1 + assert result.num_clbits == 0 + rx_expression_values = [1.57, -1.57, 0] + rz_expression_values = [-3, -21.0] + check_single_qubit_rotation_op(result.unrolled_ast, 3, [0] * 3, rx_expression_values, "rx") + check_single_qubit_rotation_op(result.unrolled_ast, 2, [0] * 2, rz_expression_values, "rz") + + +def test_incorrect_expressions(): + with pytest.raises(ValidationError, match=r"Unsupported expression type .*"): + validate("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;") + with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): + validate("OPENQASM 3; qubit q; rx(~1.3) q;") + with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): + validate("OPENQASM 3; qubit q; rx(~1.3+5im) q;") + + with pytest.raises(ValidationError, match="Undefined identifier x in expression"): + validate("OPENQASM 3; qubit q; rx(x) q;") + + with pytest.raises(ValidationError, match="Uninitialized variable x in expression"): + validate("OPENQASM 3; qubit q; int x; rx(x) q;") diff --git a/tests/test_sizeof.py b/tests/test_sizeof.py index 14ef8e1..6914842 100644 --- a/tests/test_sizeof.py +++ b/tests/test_sizeof.py @@ -11,113 +11,111 @@ Module containing unit tests for sizeof operation. """ -# import pytest -# from qbraid_qir.qasm3 import qasm3_to_qir -# from qbraid_qir.qasm3.exceptions import Qasm3ConversionError +import pytest -# from tests.qir_utils import check_attributes, check_single_qubit_rotation_op +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_single_qubit_rotation_op -# def test_simple_sizeof(): -# """Test sizeof over an array""" -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; +def test_simple_sizeof(): + """Test sizeof over an array""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; -# array[int[32], 3, 2] my_ints; + array[int[32], 3, 2] my_ints; -# const uint[32] size0 = sizeof(my_ints); // this is 3 and valid + const uint[32] size0 = sizeof(my_ints); // this is 3 and valid -# int[32] size1 = sizeof(my_ints); // this is 3 + int[32] size1 = sizeof(my_ints); // this is 3 -# int[32] size2 = sizeof(my_ints, 1); // this is 2 + int[32] size2 = sizeof(my_ints, 1); // this is 2 -# int[32] size3 = sizeof(my_ints, 0); // this is 3 -# qubit[2] q; + int[32] size3 = sizeof(my_ints, 0); // this is 3 + qubit[2] q; -# rx(size0) q[0]; -# rx(size1) q[0]; -# rx(size2) q[1]; -# rx(size3) q[1]; -# """ + rx(size0) q[0]; + rx(size1) q[0]; + rx(size2) q[1]; + rx(size3) q[1]; + """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) + result = unroll(qasm3_string) + assert result.num_qubits == 2 -# check_single_qubit_rotation_op(generated_qir, 4, [0, 0, 1, 1], [3, 3, 2, 3], "rx") + check_single_qubit_rotation_op(result.unrolled_ast, 4, [0, 0, 1, 1], [3, 3, 2, 3], "rx") -# def test_sizeof_multiple_types(): -# """Test sizeof over an array of bit, int and float""" -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; +def test_sizeof_multiple_types(): + """Test sizeof over an array of bit, int and float""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; -# array[bit, 3, 2] my_bits; -# array[int[32], 3, 2] my_ints; -# array[float[64], 3, 2] my_floats; + array[bit, 3, 2] my_bits; + array[int[32], 3, 2] my_ints; + array[float[64], 3, 2] my_floats; -# int[32] size1 = sizeof(my_bits); // this is 3 + int[32] size1 = sizeof(my_bits); // this is 3 -# int[32] size2 = sizeof(my_ints, 1); // this is 2 + int[32] size2 = sizeof(my_ints, 1); // this is 2 -# int[32] size3 = sizeof(my_floats, 0); // this is 3 too -# qubit[2] q; + int[32] size3 = sizeof(my_floats, 0); // this is 3 too + qubit[2] q; -# rx(size1) q[0]; -# rx(size2) q[1]; -# rx(size3) q[1]; -# """ + rx(size1) q[0]; + rx(size2) q[1]; + rx(size3) q[1]; + """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 2, 0) + result = unroll(qasm3_string) + assert result.num_qubits == 2 + check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 1, 1], [3, 2, 3], "rx") -# check_single_qubit_rotation_op(generated_qir, 2, [0, 1, 1], [3, 2, 3], "rx") +def test_unsupported_target(): + """Test sizeof over index expressions""" + with pytest.raises(ValidationError, match=r"Unsupported target type .*"): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; -# def test_unsupported_target(): -# """Test sizeof over index expressions""" -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported target type .*"): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; + array[int[32], 3, 2] my_ints; -# array[int[32], 3, 2] my_ints; + int[32] size1 = sizeof(my_ints[0]); // this is invalid + """ + validate(qasm3_string) -# int[32] size1 = sizeof(my_ints[0]); // this is invalid -# """ -# qasm3_to_qir(qasm3_string) +def test_sizeof_on_non_array(): + """Test sizeof on a non-array""" + with pytest.raises( + ValidationError, match="Invalid sizeof usage, variable my_int is not an array." + ): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; -# def test_sizeof_on_non_array(): -# """Test sizeof on a non-array""" -# with pytest.raises( -# Qasm3ConversionError, match="Invalid sizeof usage, variable my_int is not an array." -# ): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; + int[32] my_int = 3; -# int[32] my_int = 3; + int[32] size1 = sizeof(my_int); // this is invalid + """ + validate(qasm3_string) -# int[32] size1 = sizeof(my_int); // this is invalid -# """ -# qasm3_to_qir(qasm3_string) +def test_out_of_bounds_reference(): + """Test sizeof on an out of bounds reference""" + with pytest.raises( + ValidationError, match="Index 3 out of bounds for array my_ints with 2 dimensions" + ): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; -# def test_out_of_bounds_reference(): -# """Test sizeof on an out of bounds reference""" -# with pytest.raises( -# Qasm3ConversionError, match="Index 3 out of bounds for array my_ints with 2 dimensions" -# ): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; + array[int[32], 3, 2] my_ints; -# array[int[32], 3, 2] my_ints; - -# int[32] size1 = sizeof(my_ints, 3); // this is invalid -# """ -# qasm3_to_qir(qasm3_string) + int[32] size1 = sizeof(my_ints, 3); // this is invalid + """ + validate(qasm3_string) diff --git a/tests/utils.py b/tests/utils.py index 981108e..532f77f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -114,6 +114,7 @@ def check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_li qubit_id += 1 param_id += 1 gate_count += 1 + print(gate_count, num_gates) assert gate_count == num_gates From 30f7aa81786ac14febd849c68d1d238870a59793 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 8 Oct 2024 18:23:42 +0530 Subject: [PATCH 06/16] update subroutine tests --- tests/conftest.py | 1 + tests/resources/subroutines.py | 409 +++++++++ tests/subroutines/test_subroutines.py | 316 +++++++ tests/subroutines/test_subroutines_arrays.py | 149 ++++ tests/test_switch.py | 839 +++++++++---------- 5 files changed, 1290 insertions(+), 424 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8124d62..ac13e9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,3 +9,4 @@ # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. from .resources.gates import * +from .resources.subroutines import * diff --git a/tests/resources/subroutines.py b/tests/resources/subroutines.py index a5caba4..a1c5a9d 100644 --- a/tests/resources/subroutines.py +++ b/tests/resources/subroutines.py @@ -7,3 +7,412 @@ # See the LICENSE file in the project root or . # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. + + +""" +Module defining subroutine tests. + +""" + +SUBROUTINE_INCORRECT_TESTS = { + "undeclared_call": ( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit q; + my_function(1); + """, + "Undefined subroutine 'my_function' was called", + ), + "redefinition_raises_error": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) -> int[32] { + h q; + return; + } + def my_function(qubit q) -> float[32] { + x q; + return; + } + qubit q; + """, + "Redefinition of subroutine 'my_function'", + ), + "redefinition_raises_error_2": ( + """ + OPENQASM 3; + include "stdgates.inc"; + def my_function(qubit q) { + int[32] q = 1; + return; + } + qubit q; + my_function(q); + """, + "Re-declaration of variable q", + ), + "incorrect_param_count_1": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q, qubit r) { + h q; + return; + } + qubit q; + my_function(q); + """, + "Parameter count mismatch for subroutine 'my_function'. Expected 2 but got 1 in call", + ), + "incorrect_param_count_2": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(int[32] q) { + h q; + return; + } + qubit q; + my_function(q, q); + """, + "Parameter count mismatch for subroutine 'my_function'. Expected 1 but got 2 in call", + ), + "return_value_mismatch": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) { + h q; + int[32] a = 1; + return a; + } + qubit q; + my_function(q); + """, + "Return type mismatch for subroutine 'my_function'.", + ), + "return_value_mismatch_2": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) -> int[32] { + h q; + int[32] a = 1; + return ; + } + qubit q; + my_function(q); + """, + "Return type mismatch for subroutine 'my_function'.", + ), + "subroutine_keyword_naming": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def pi(qubit q) { + h q; + return; + } + qubit q; + pi(q); + """, + "Subroutine name 'pi' is a reserved keyword", + ), + "qubit_size_arg_mismatch": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[3] q) { + h q; + return; + } + qubit[2] q; + my_function(q); + """, + "Qubit register size mismatch for function 'my_function'.", + ), + "subroutine_var_name_conflict": ( + """ + OPENQASM 3; + include "stdgates.inc"; + const int a = 4; + def a(qubit q) { + h q; + return; + } + qubit q; + a(q); + """, + r"Can not declare subroutine with name 'a' .*", + ), + "undeclared_register_usage": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) { + h q; + return; + } + qubit q; + int b; + my_function(b); + """, + "Expecting qubit argument for 'q'. Qubit register 'b' not found for function 'my_function'", + ), + "test_invalid_qubit_size": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[-3] q) { + h q; + return; + } + qubit[4] q; + my_function(q); + """, + "Invalid qubit size -3 for variable 'q' in function 'my_function'", + ), + "test_type_mismatch_for_function": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(int[32] a, qubit q) { + h q; + return; + } + qubit q; + int[32] b = 4; + my_function(q, b); + """, + "Expecting classical argument for 'a'. Qubit register 'q' found for function 'my_function'", + ), + "test_duplicate_qubit_args": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[3] p, qubit[1] q) { + h q; + return; + } + qubit[4] q; + my_function(q[0:3], q[2]); + """, + r"Duplicate qubit argument for register 'q' in function call for 'my_function'", + ), + "undefined_variable_in_actual_arg_1": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(int [32] a) { + h q; + return; + } + qubit q; + my_function(b); + """, + "Undefined variable 'b' used for function call 'my_function'", + ), + "undefined_array_arg_in_function_call": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(readonly array[int[32], 1, 2] a) { + return; + } + my_function(b); + """, + "Undefined variable 'b' used for function call 'my_function'", + ), +} + +SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS = { + "non_array_raises_error": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { + return; + } + qubit q; + int[8] arr; + my_function(q, arr); + """, + r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." + r" Variable 'arr' has type 'int\[8\]'.", + ), + "literal_raises_error": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { + return; + } + qubit q; + my_function(q, 5); + """, + r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." + r" Literal 5 found in function call", + ), + "type_mismatch_in_array": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { + return; + } + qubit q; + array[uint[32], 2, 2] arr; + my_function(q, arr); + """, + r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." + r" Variable 'arr' has type 'array\[uint\[32\], 2, 2\]'.", + ), + "dimension_count_mismatch_1": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { + return; + } + qubit q; + array[int[8], 2] arr; + my_function(q, arr); + """, + r"Dimension mismatch for 'my_arr' in function 'my_function'. Expected 2 dimensions" + r" but variable 'arr' has 1", + ), + "dimension_count_mismatch_2": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], #dim = 4] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2] arr; + my_function(q, arr); + """, + r"Dimension mismatch for 'my_arr' in function 'my_function'. Expected 4 dimensions " + r"but variable 'arr' has 2", + ), + "qubit_passed_as_array": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(mutable array[int[8], 2, 2] my_arr) { + return; + } + qubit[2] q; + my_function(q); + """, + r"Expecting type 'array\[int\[8\],...\]' for 'my_arr' in function 'my_function'." + r" Qubit register 'q' found for function call", + ), + "invalid_dimension_number": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], #dim = -3] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2, 2] arr; + my_function(q, arr); + """, + r"Invalid number of dimensions -3 for 'my_arr' in function 'my_function'", + ), + "invalid_non_int_dimensions_1": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, mutable array[int[8], #dim = 2.5] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2] arr; + my_function(q, arr); + """, + r"Invalid value 2.5 with type for required type " + r"", + ), + "invalid_non_int_dimensions_2": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2.5, 2] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2] arr; + my_function(q, arr); + """, + r"Invalid value 2.5 with type for required type" + r" ", + ), + "extra_dimensions_for_array": ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, mutable array[int[8], 4, 2] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2] arr; + my_function(q, arr); + """, + r"Dimension mismatch for 'my_arr' in function 'my_function'. " + r"Expected dimension 0 with size >= 4 but got 2", + ), + "invalid_array_dimensions_formal_arg": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(readonly array[int[32], -1, 2] a) { + return; + } + array[int[32], 1, 2] b; + my_function(b); + """, + r"Invalid dimension size -1 for 'a' in function 'my_function'", + ), + "invalid_array_mutation_for_readonly_arg": ( + """ + OPENQASM 3; + include "stdgates.inc"; + + def my_function(readonly array[int[32], 1, 2] a) { + a[1][0] = 5; + return; + } + array[int[32], 1, 2] b; + my_function(b); + """, + r"Assignment to readonly variable 'a' not allowed in function call", + ), +} diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py index a5caba4..b7986c2 100644 --- a/tests/subroutines/test_subroutines.py +++ b/tests/subroutines/test_subroutines.py @@ -7,3 +7,319 @@ # See the LICENSE file in the project root or . # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. + +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.resources.subroutines import SUBROUTINE_INCORRECT_TESTS +from tests.utils import check_single_qubit_gate_op, check_single_qubit_rotation_op + + +def test_function_declaration(): + """Test that a function declaration is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + def my_function(qubit q) { + h q; + return; + } + qubit q; + my_function(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_simple_function_call(): + """Test that a simple function call is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, float[32] b) { + rx(b) a; + float[64] c = 2*b; + rx(c) a; + return; + } + qubit q; + float[32] r = 3.14; + my_function(q, r); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 2, [0, 0], [3.14, 6.28], "rx") + + +def test_const_visible_in_function_call(): + """Test that a constant is visible in a function call.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + const float[32] pi2 = 3.14; + + def my_function(qubit q) { + rx(pi2) q; + return; + } + qubit q; + my_function(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [3.14], "rx") + + +def test_update_variable_in_function(): + """Test that variable update works correctly in a function.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) { + float[32] a = 3.14; + a = 2*a; + rx(a) q; + return; + } + qubit q; + my_function(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [6.28], "rx") + + +def test_function_call_in_expression(): + """Test that a function call in an expression is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) -> bool{ + h q; + return true; + } + qubit q; + bool b = my_function(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_multiple_function_calls(): + """Test that multiple function calls are correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(int[32] a, qubit q_arg) { + h q_arg; + rx (a) q_arg; + return; + } + qubit[3] q; + my_function(2, q[2]); + my_function(1, q[1]); + my_function(0, q[0]); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 3 + + check_single_qubit_gate_op(result.unrolled_ast, 3, [2, 1, 0], "h") + check_single_qubit_rotation_op(result.unrolled_ast, 3, [2, 1, 0], [2, 1, 0], "rx") + + +def test_function_call_with_return(): + """Test that a function call with a return value is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) -> float[32] { + h q; + return 3.14; + } + qubit q; + float[32] r = my_function(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + + +def test_return_values_from_function(): + """Test that the values returned from a function are used correctly in other function.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) -> float[32] { + h q; + return 3.14; + } + def my_function_2(qubit q, float[32] r) { + rx(r) q; + return; + } + qubit[2] q; + float[32] r1 = my_function(q[0]); + my_function_2(q[0], r1); + + array[float[32], 1, 1] r2 = {{3.14}}; + my_function_2(q[1], r2[0,0]); + + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 2 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + check_single_qubit_rotation_op(result.unrolled_ast, 2, [0, 1], [3.14, 3.14], "rx") + + +def test_function_call_with_custom_gate(): + """Test that a function call with a custom gate is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + gate my_gate(a) q2 { + rx(a) q2; + } + + def my_function(qubit a, float[32] b) { + float[64] c = 2*b; + my_gate(b) a; + my_gate(c) a; + return; + } + qubit q; + float[32] r = 3.14; + my_function(q, r); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 2, [0, 0], [3.14, 6.28], "rx") + + +@pytest.mark.skip(reason="Not implemented nested functions yet") +def test_function_call_from_within_fn(): + """Test that a function call from within another function is correctly converted.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q1) { + h q1; + return; + } + + def my_function_2(qubit[2] q2) { + my_function(q2[1]); + return; + } + qubit[2] q; + my_function_2(q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 2 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [1], "h") + + +@pytest.mark.parametrize("data_type", ["int[32] a = 1;", "float[32] a = 1.0;", "bit a = 0;"]) +def test_return_value_mismatch(data_type): + """Test that returning a value of incorrect type raises error.""" + qasm_str = ( + """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q) { + h q; + """ + + data_type + + """ + return a; + } + qubit q; + my_function(q); + """ + ) + + with pytest.raises( + ValidationError, match=r"Return type mismatch for subroutine 'my_function'.*" + ): + validate(qasm_str) + + +@pytest.mark.parametrize("keyword", ["pi", "euler", "tau"]) +def test_subroutine_keyword_naming(keyword): + """Test that using a keyword as a subroutine name raises error.""" + qasm_str = f"""OPENQASM 3; + include "stdgates.inc"; + + def {keyword}(qubit q) {{ + h q; + return; + }} + qubit q; + {keyword}(q); + """ + + with pytest.raises(ValidationError, match=f"Subroutine name '{keyword}' is a reserved keyword"): + validate(qasm_str) + + +@pytest.mark.parametrize("qubit_params", ["q", "q[:2]", "q[{0,1}]"]) +def test_qubit_size_arg_mismatch(qubit_params): + """Test that passing a qubit of different size raises error.""" + qasm_str = ( + """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[3] q) { + h q; + return; + } + qubit[2] q; + my_function(""" + + qubit_params + + """); + """ + ) + + with pytest.raises( + ValidationError, + match="Qubit register size mismatch for function 'my_function'. " + "Expected 3 in variable 'q' but got 2", + ): + validate(qasm_str) + + +@pytest.mark.parametrize("test_name", SUBROUTINE_INCORRECT_TESTS.keys()) +def test_incorrect_custom_ops(test_name): + qasm_input, error_message = SUBROUTINE_INCORRECT_TESTS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) diff --git a/tests/subroutines/test_subroutines_arrays.py b/tests/subroutines/test_subroutines_arrays.py index a5caba4..cd24ca2 100644 --- a/tests/subroutines/test_subroutines_arrays.py +++ b/tests/subroutines/test_subroutines_arrays.py @@ -7,3 +7,152 @@ # See the LICENSE file in the project root or . # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. + + +""" +Module containing unit tests for parsing, unrolling, and +converting OpenQASM3 programs that contain arrays in subroutines. + +""" + +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.resources.subroutines import SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS +from tests.utils import check_single_qubit_rotation_op + + +def test_simple_function_call(): + """Test that a simple function call is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(qubit a, readonly array[int[8], 2, 2] my_arr) { + return; + } + qubit q; + array[int[8], 2, 2] arr; + my_function(q, arr); + + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + +def test_passing_array_to_function(): + """Test that passing an array to a function is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(readonly array[int[8], 2, 2] my_arr, qubit q) { + rx(my_arr[0][0]) q; + rx(my_arr[0][1]) q; + rx(my_arr[1][0]) q; + rx(my_arr[1][1]) q; + + return; + } + qubit my_q; + array[int[8], 2, 2] arr = { {1, 2}, {3, 4} }; + my_function(arr, my_q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, 4, qubit_list=[0, 0, 0, 0], param_list=[1, 2, 3, 4], gate_name="rx" + ) + + +def test_passing_subarray_to_function(): + """Test that passing a smaller subarray to a function is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(readonly array[int[8], 2, 2] my_arr, qubit q) { + rx(my_arr[0][0]) q; + rx(my_arr[0][1]) q; + return; + } + qubit my_q; + array[int[8], 4, 3] arr_1 = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} }; + array[int[8], 2, 2] arr_2 = { {1, 2}, {3, 4} }; + my_function(arr_1[1:2][1:2], my_q); + my_function(arr_2[0:1, :], my_q); + + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, 4, qubit_list=[0] * 4, param_list=[5, 6, 1, 2], gate_name="rx" + ) + + +def test_passing_array_with_dim_identifier(): + """Test that passing an array with a dimension identifier is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(readonly array[int[8], #dim = 2] my_arr, qubit q) { + rx(my_arr[0][0]) q; + rx(my_arr[0][1]) q; + return; + } + qubit my_q; + array[int[8], 2, 2, 2] arr = { { {1, 2}, {3, 4} }, { {5, 6}, {7, 8} } }; + my_function(arr[0, :, :], my_q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, 2, qubit_list=[0] * 2, param_list=[1, 2], gate_name="rx" + ) + + +def test_pass_multiple_arrays_to_function(): + """Test that passing multiple arrays to a function is correctly parsed.""" + qasm_str = """OPENQASM 3.0; + include "stdgates.inc"; + + def my_function(readonly array[int[8], 2, 2] my_arr1, + readonly array[int[8], 2, 2] my_arr2, + qubit q) { + for int[8] i in [0:1] { + rx(my_arr1[i][0]) q; + rx(my_arr2[i][1]) q; + } + + return; + } + qubit my_q; + array[int[8], 2, 2] arr_1 = { {1, 2}, {3, 4} }; + array[int[8], 2, 2] arr_2 = { {5, 6}, {7, 8} }; + my_function(arr_1, arr_2, my_q); + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, 4, qubit_list=[0] * 4, param_list=[1, 6, 3, 8], gate_name="rx" + ) + + +@pytest.mark.parametrize("test_name", SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS.keys()) +def test_incorrect_custom_ops_with_arrays(test_name): + qasm_input, error_message = SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS[test_name] + with pytest.raises(ValidationError, match=error_message): + _ = validate(qasm_input) diff --git a/tests/test_switch.py b/tests/test_switch.py index 6b9e397..5da2bd1 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -12,432 +12,423 @@ """ -# import re +import re -# import pytest -# from qbraid_qir.qasm3 import qasm3_to_qir -# from qbraid_qir.qasm3.exceptions import Qasm3ConversionError +import pytest -# from tests.qir_utils import ( -# check_attributes, -# check_single_qubit_gate_op, -# check_single_qubit_rotation_op, -# ) +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_single_qubit_gate_op, check_single_qubit_rotation_op -# def test_switch(): -# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement.""" - -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 5; -# qubit q; - -# switch(i) { -# case 1,3,5,7 { -# x q; -# } -# case 2,4,6,8 { -# y q; -# } -# default { -# z q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm3_switch_program, name="test") -# generated_qir = str(result).splitlines() +def test_switch(): + """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement.""" + + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 5; + qubit q; + + switch(i) { + case 1,3,5,7 { + x q; + } + case 2,4,6,8 { + y q; + } + default { + z q; + } + } + """ + + result = unroll(qasm3_switch_program) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") -# check_attributes(generated_qir, 1) -# check_single_qubit_gate_op(generated_qir, 1, [0], "x") - - -# def test_switch_default(): -# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and default case.""" - -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 10; -# qubit q; - -# switch(i) { -# case 1,3,5,7 { -# x q; -# } -# case 2,4,6,8 { -# y q; -# } -# default { -# z q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm3_switch_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1) -# check_single_qubit_gate_op(generated_qir, 1, [0], "z") - - -# def test_switch_identifier_case(): -# """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and identifier case.""" - -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 4; -# const int j = 4; -# qubit q; - -# switch(i) { -# case 6, j { -# x q; -# } -# default { -# z q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm3_switch_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1) -# check_single_qubit_gate_op(generated_qir, 1, [0], "x") - - -# def test_switch_const_int(): -# """Test converting OpenQASM 3 program switch and constant integer case.""" - -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 4; -# const int j = 5; -# qubit q; - -# switch(i) { -# case j-1 { -# x q; -# } -# default { -# z q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm3_switch_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1) -# check_single_qubit_gate_op(generated_qir, 1, [0], "x") - - -# def test_switch_duplicate_cases(): -# """Test that switch raises error if duplicate values are present in case.""" - -# with pytest.raises( -# Qasm3ConversionError, match=re.escape("Duplicate case value 4 in switch statement") -# ): -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 4; -# qubit q; - -# switch(i) { -# case 4, 4 { -# x q; -# } -# default { -# z q; -# } -# } -# """ - -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# def test_no_case_switch(): -# """Test that switch raises error if no case is present.""" - -# with pytest.raises( -# Qasm3ConversionError, match=re.escape("Switch statement must have at least one case") -# ): -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 4; -# qubit q; - -# switch(i) { -# default { -# z q; -# } -# } -# """ - -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# def test_nested_switch(): -# """Test that switch works correctly in case of nested switch""" - -# qasm3_switch_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# const int i = 1; -# qubit q; - -# switch(i) { -# case 1,3,5,7 { -# int j = 4; // definition inside scope -# switch(j) { -# case 1,3,5,7 { -# x q; -# } -# case 2,4,6,8 { -# j = 5; // assignment inside scope -# y q; // this will be executed -# } -# default { -# z q; -# } -# } -# } -# case 2,4,6,8 { -# y q; -# } -# default { -# z q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm3_switch_program, name="test") -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_gate_op(generated_qir, 1, [0], "y") - - -# def test_subroutine_inside_switch(): -# """Test that a subroutine inside a switch statement is correctly parsed.""" -# qasm_str = """OPENQASM 3; -# include "stdgates.inc"; - -# def my_function(qubit q, float[32] b) { -# rx(b) q; -# return; -# } - -# qubit[2] q; -# int i = 1; -# float[32] r = 3.14; - -# switch(i) { -# case 1 { -# my_function(q[0], r); -# } -# default { -# x q; -# } -# } -# """ - -# result = qasm3_to_qir(qasm_str) -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 2, 0) -# check_single_qubit_rotation_op(generated_qir, 1, [0], [3.14], "rx") - - -# @pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) -# def test_invalid_scalar_switch_target(invalid_type): -# """Test that switch raises error if target is not an integer.""" - -# base_invalid_program = ( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# """ -# + invalid_type -# + """ i; - -# qubit q; - -# switch(i) { -# case 4 { -# x q; -# } -# default { -# z q; -# } -# } -# """ -# ) - -# with pytest.raises( -# Qasm3ConversionError, match=re.escape("Switch target i must be of type int") -# ): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# @pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) -# def test_invalid_array_switch_target(invalid_type): -# """Test that switch raises error if target is array element and not an integer.""" - -# base_invalid_program = ( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# array[""" -# + invalid_type -# + """, 3, 2] i; - -# qubit q; - -# switch(i[0][1]) { -# case 4 { -# x q; -# } -# default { -# z q; -# } -# } -# """ -# ) - -# with pytest.raises( -# Qasm3ConversionError, match=re.escape("Switch target i must be of type int") -# ): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# @pytest.mark.parametrize( -# "invalid_stmt", -# ["def test1() { int i = 1; }", "array[int[32], 3, 2] arr_int;", "gate test_1() q { h q;}"], -# ) -# def test_unsupported_statements_in_case(invalid_stmt): -# """Test that switch raises error if invalid statements are present in the case block""" - -# base_invalid_program = ( -# """ - -# OPENQASM 3.0; -# include "stdgates.inc"; -# qubit q; -# int i = 4; - -# switch(i) { -# case 4 { -# x q; -# """ -# + invalid_stmt -# + """ -# } -# default { -# z q; -# } -# } -# """ -# ) -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported statement .*"): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# def test_non_int_expression_case(): -# """Test that switch raises error if case expression is not an integer.""" - -# base_invalid_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# const int i = 4; -# qubit q; - -# switch(i) { -# case 4.3, 2 { -# x q; -# } -# default { -# z q; -# } -# } -# """ - -# with pytest.raises( -# Qasm3ConversionError, -# match=r"Invalid value 4.3 with type .* for required type ", -# ): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# def test_non_int_variable_expression(): -# """Test that switch raises error if case expression has a non-int -# variable in expression.""" - -# base_invalid_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# const int i = 4; -# const float f = 4.0; -# qubit q; - -# switch(i) { -# case f, 2 { -# x q; -# } -# default { -# z q; -# } -# } -# """ -# with pytest.raises( -# Qasm3ConversionError, -# match=r"Invalid type of variable .* for required type ", -# ): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") - - -# def test_non_constant_expression_case(): -# """Test that switch raises error if case expression is not a constant.""" - -# base_invalid_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# int i = 4; -# qubit q; -# int j = 3; -# int k = 2; - -# switch(i) { -# case j + k { -# x q; -# } -# default { -# z q; -# } -# } -# """ - -# with pytest.raises( -# Qasm3ConversionError, match=r"Variable .* is not a constant in given expression" -# ): -# qasm3_switch_program = base_invalid_program -# qasm3_to_qir(qasm3_switch_program, name="test") + +def test_switch_default(): + """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and default case.""" + + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 10; + qubit q; + + switch(i) { + case 1,3,5,7 { + x q; + } + case 2,4,6,8 { + y q; + } + default { + z q; + } + } + """ + + result = unroll(qasm3_switch_program) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "z") + + +def test_switch_identifier_case(): + """Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement and identifier case.""" + + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 4; + const int j = 4; + qubit q; + + switch(i) { + case 6, j { + x q; + } + default { + z q; + } + } + """ + + result = unroll(qasm3_switch_program) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") + + +def test_switch_const_int(): + """Test converting OpenQASM 3 program switch and constant integer case.""" + + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 4; + const int j = 5; + qubit q; + + switch(i) { + case j-1 { + x q; + } + default { + z q; + } + } + """ + + result = unroll(qasm3_switch_program) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") + + +def test_switch_duplicate_cases(): + """Test that switch raises error if duplicate values are present in case.""" + + with pytest.raises( + ValidationError, match=re.escape("Duplicate case value 4 in switch statement") + ): + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 4; + qubit q; + + switch(i) { + case 4, 4 { + x q; + } + default { + z q; + } + } + """ + + validate(qasm3_switch_program) + + +def test_no_case_switch(): + """Test that switch raises error if no case is present.""" + + with pytest.raises( + ValidationError, match=re.escape("Switch statement must have at least one case") + ): + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 4; + qubit q; + + switch(i) { + default { + z q; + } + } + """ + + validate(qasm3_switch_program) + + +def test_nested_switch(): + """Test that switch works correctly in case of nested switch""" + + qasm3_switch_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + const int i = 1; + qubit q; + + switch(i) { + case 1,3,5,7 { + int j = 4; // definition inside scope + switch(j) { + case 1,3,5,7 { + x q; + } + case 2,4,6,8 { + j = 5; // assignment inside scope + y q; // this will be executed + } + default { + z q; + } + } + } + case 2,4,6,8 { + y q; + } + default { + z q; + } + } + """ + + result = unroll(qasm3_switch_program) + + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "y") + + +def test_subroutine_inside_switch(): + """Test that a subroutine inside a switch statement is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit q, float[32] b) { + rx(b) q; + return; + } + + qubit[2] q; + int i = 1; + float[32] r = 3.14; + + switch(i) { + case 1 { + my_function(q[0], r); + } + default { + x q; + } + } + """ + + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 2 + + check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [3.14], "rx") + + +@pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) +def test_invalid_scalar_switch_target(invalid_type): + """Test that switch raises error if target is not an integer.""" + + base_invalid_program = ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + """ + + invalid_type + + """ i; + + qubit q; + + switch(i) { + case 4 { + x q; + } + default { + z q; + } + } + """ + ) + + with pytest.raises(ValidationError, match=re.escape("Switch target i must be of type int")): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) + + +@pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"]) +def test_invalid_array_switch_target(invalid_type): + """Test that switch raises error if target is array element and not an integer.""" + + base_invalid_program = ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + array[""" + + invalid_type + + """, 3, 2] i; + + qubit q; + + switch(i[0][1]) { + case 4 { + x q; + } + default { + z q; + } + } + """ + ) + + with pytest.raises(ValidationError, match=re.escape("Switch target i must be of type int")): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) + + +@pytest.mark.parametrize( + "invalid_stmt", + ["def test1() { int i = 1; }", "array[int[32], 3, 2] arr_int;", "gate test_1() q { h q;}"], +) +def test_unsupported_statements_in_case(invalid_stmt): + """Test that switch raises error if invalid statements are present in the case block""" + + base_invalid_program = ( + """ + + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + int i = 4; + + switch(i) { + case 4 { + x q; + """ + + invalid_stmt + + """ + } + default { + z q; + } + } + """ + ) + with pytest.raises(ValidationError, match=r"Unsupported statement .*"): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) + + +def test_non_int_expression_case(): + """Test that switch raises error if case expression is not an integer.""" + + base_invalid_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + const int i = 4; + qubit q; + + switch(i) { + case 4.3, 2 { + x q; + } + default { + z q; + } + } + """ + + with pytest.raises( + ValidationError, + match=r"Invalid value 4.3 with type .* for required type ", + ): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) + + +def test_non_int_variable_expression(): + """Test that switch raises error if case expression has a non-int + variable in expression.""" + + base_invalid_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + const int i = 4; + const float f = 4.0; + qubit q; + + switch(i) { + case f, 2 { + x q; + } + default { + z q; + } + } + """ + with pytest.raises( + ValidationError, + match=r"Invalid type of variable .* for required type ", + ): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) + + +def test_non_constant_expression_case(): + """Test that switch raises error if case expression is not a constant.""" + + base_invalid_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + int i = 4; + qubit q; + int j = 3; + int k = 2; + + switch(i) { + case j + k { + x q; + } + default { + z q; + } + } + """ + + with pytest.raises(ValidationError, match=r"Variable .* is not a constant in given expression"): + qasm3_switch_program = base_invalid_program + validate(qasm3_switch_program) From 224a7c0b483ee219b9f28f6fc5f7f2a28d72e610 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Tue, 8 Oct 2024 18:40:53 +0530 Subject: [PATCH 07/16] add for --- tests/test_loop.py | 453 ++++++++++++++++++++------------------------- 1 file changed, 202 insertions(+), 251 deletions(-) diff --git a/tests/test_loop.py b/tests/test_loop.py index 53eb586..0363bdd 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -12,173 +12,116 @@ """ -# import pytest -# from qbraid_qir.qasm3 import qasm3_to_qir -# from qbraid_qir.qasm3.visitor import Qasm3ConversionError +import pytest -# from tests.qir_utils import ( -# check_attributes, -# check_single_qubit_gate_op, -# check_single_qubit_rotation_op, -# ) +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import ( + check_single_qubit_gate_op, + check_single_qubit_rotation_op, + check_two_qubit_gate_op, +) -# EXAMPLE_WITHOUT_LOOP = """ -# OPENQASM 3.0; -# include "stdgates.inc"; +EXAMPLE_WITHOUT_LOOP = """ +OPENQASM 3.0; +include "stdgates.inc"; -# qubit[4] q; -# bit[4] c; +qubit[4] q; +bit[4] c; -# h q; +h q; -# cx q[0], q[1]; -# cx q[1], q[2]; -# cx q[2], q[3]; +cx q[0], q[1]; +cx q[1], q[2]; +cx q[2], q[3]; -# measure q->c; -# """ +measure q->c; +""" -# def test_convert_qasm3_for_loop(): -# """Test converting a QASM3 program that contains a for loop.""" -# qir_expected = qasm3_to_qir(EXAMPLE_WITHOUT_LOOP, name="test") -# qir_from_loop = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_convert_qasm3_for_loop(): + """Test converting a QASM3 program that contains a for loop.""" + result = unroll( + """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[4] q; -# bit[4] c; - -# h q; -# for int i in [0:2]{ -# cx q[i], q[i+1]; -# } -# measure q->c; -# """, -# name="test", -# ) -# assert str(qir_expected) == str(qir_from_loop) -# assert str(qir_from_loop) == EXAMPLE_QIR_OUTPUT + qubit[4] q; + bit[4] c; + h q; + for int i in [0:2]{ + cx q[i], q[i+1]; + } + measure q->c; + """, + ) + assert result.num_qubits == 4 + assert result.num_clbits == 4 -# def test_convert_qasm3_for_loop_shadow(): -# """Test for loop where loop variable shadows variable from global scope.""" -# qir_expected = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; + check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 2, 3], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") -# qubit[4] q; -# bit[4] c; -# int i = 3; +def test_convert_qasm3_for_loop_shadow(): + """Test for loop where loop variable shadows variable from global scope.""" + result = unroll( + """ + OPENQASM 3.0; + include "stdgates.inc"; -# h q; -# cx q[0], q[1]; -# cx q[1], q[2]; -# cx q[2], q[3]; -# h q[i]; -# measure q->c; -# """, -# name="test", -# ) -# qir_from_loop = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; + qubit[4] q; + bit[4] c; -# qubit[4] q; -# bit[4] c; + int i = 3; -# int i = 3; + h q; + for int i in [0:2]{ + cx q[i], q[i+1]; + } + h q[i]; + measure q->c; + """, + ) + assert result.num_clbits == 4 + assert result.num_qubits == 4 -# h q; -# for int i in [0:2]{ -# cx q[i], q[i+1]; -# } -# h q[i]; -# measure q->c; -# """, -# name="test", -# ) -# assert str(qir_expected) == str(qir_from_loop) + check_single_qubit_gate_op(result.unrolled_ast, 5, [0, 1, 2, 3, 3], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") -# def test_convert_qasm3_for_loop_enclosing(): -# """Test for loop where variable from outer loop is accessed from inside the loop.""" -# qir_expected = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; +def test_convert_qasm3_for_loop_enclosing(): + """Test for loop where variable from outer loop is accessed from inside the loop.""" + result = unroll( + """ + OPENQASM 3.0; + include "stdgates.inc"; -# qubit[4] q; -# bit[4] c; + qubit[4] q; + bit[4] c; -# int j = 3; + int j = 3; -# h q; -# cx q[0], q[1]; -# h q[j]; -# cx q[1], q[2]; -# h q[j]; -# cx q[2], q[3]; -# h q[j]; -# measure q->c; -# """, -# name="test", -# ) -# qir_from_loop = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; + h q; + for int i in [0:2]{ + cx q[i], q[i+1]; + h q[j]; + } + measure q->c; + """, + ) -# qubit[4] q; -# bit[4] c; + assert result.num_clbits == 4 + assert result.num_qubits == 4 -# int j = 3; - -# h q; -# for int i in [0:2]{ -# cx q[i], q[i+1]; -# h q[j]; -# } -# measure q->c; -# """, -# name="test", -# ) -# assert str(qir_expected) == str(qir_from_loop) + check_single_qubit_gate_op(result.unrolled_ast, 7, [0, 1, 2, 3, 3, 3, 3], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") # def test_convert_qasm3_for_loop_enclosing_modifying(): # """Test for loop where variable from outer loop is modified from inside the loop.""" -# qir_expected = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# qubit[4] q; -# bit[4] c; - -# int j = 0; - -# h q; -# cx q[0], q[1]; -# h q[j]; -# j += 1; -# cx q[1], q[2]; -# h q[j]; -# j += 1; -# cx q[2], q[3]; -# h q[j]; -# j += 1; - -# h q[j]; -# measure q->c; -# """, -# name="test", -# ) -# qir_from_loop = qasm3_to_qir( +# result = unroll( # """ # OPENQASM 3.0; # include "stdgates.inc"; @@ -197,80 +140,87 @@ # h q[j]; # measure q->c; # """, -# name="test", # ) -# assert str(qir_expected) == str(qir_from_loop) +# assert result.num_clbits == 4 +# assert result.num_qubits == 4 +# check_single_qubit_gate_op(result.unrolled_ast, 7, [0, 1, 2, 3, 0, 1, 2], "h") +# check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") -# def test_convert_qasm3_for_loop_discrete_set(): -# """Test converting a QASM3 program that contains a for loop initialized from a DiscreteSet.""" -# qir_expected = qasm3_to_qir(EXAMPLE_WITHOUT_LOOP, name="test") -# qir_from_loop = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; -# qubit[4] q; -# bit[4] c; +def test_convert_qasm3_for_loop_discrete_set(): + """Test converting a QASM3 program that contains a for loop initialized from a DiscreteSet.""" + result = unroll( + """ + OPENQASM 3.0; + include "stdgates.inc"; -# h q; -# for int i in {0, 1, 2} { -# cx q[i], q[i+1]; -# } -# measure q->c; -# """, -# name="test", -# ) -# assert str(qir_expected) == str(qir_from_loop) -# assert str(qir_from_loop) == EXAMPLE_QIR_OUTPUT + qubit[4] q; + bit[4] c; + h q; + for int i in {0, 1, 2} { + cx q[i], q[i+1]; + } + measure q->c; + """, + ) + assert result.num_clbits == 4 + assert result.num_qubits == 4 -# def test_function_executed_in_loop(): -# """Test that a function executed in a loop is correctly parsed.""" -# qasm_str = """OPENQASM 3; -# include "stdgates.inc"; + check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 2, 3], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") -# def my_function(qubit q_arg, float[32] b) { -# rx(b) q_arg; -# return; -# } -# qubit[5] q; -# int[32] n = 2; -# float[32] b = 3.14; +def test_function_executed_in_loop(): + """Test that a function executed in a loop is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; -# for int i in [0:n] { -# my_function(q[i], i*b); -# } -# """ + def my_function(qubit q_arg, float[32] b) { + rx(b) q_arg; + return; + } + qubit[5] q; -# result = qasm3_to_qir(qasm_str) -# generated_qir = str(result).splitlines() + int[32] n = 2; + float[32] b = 3.14; -# check_attributes(generated_qir, 5, 0) -# check_single_qubit_rotation_op(generated_qir, 3, list(range(3)), [0, 3.14, 2 * 3.14], "rx") + for int i in [0:n] { + my_function(q[i], i*b); + } + """ + result = unroll(qasm_str) + assert result.num_qubits == 5 + assert result.num_clbits == 0 -# def test_loop_inside_function(): -# """Test that a function with a loop is correctly parsed.""" -# qasm_str = """OPENQASM 3; -# include "stdgates.inc"; + check_single_qubit_rotation_op( + result.unrolled_ast, 3, list(range(3)), [0, 3.14, 2 * 3.14], "rx" + ) -# def my_function(qubit[3] q2) { -# for int[32] i in [0:2] { -# h q2[i]; -# } -# return; -# } -# qubit[3] q1; -# my_function(q1); -# """ -# result = qasm3_to_qir(qasm_str) -# generated_qir = str(result).splitlines() +def test_loop_inside_function(): + """Test that a function with a loop is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; + + def my_function(qubit[3] q2) { + for int[32] i in [0:2] { + h q2[i]; + } + return; + } + qubit[3] q1; + my_function(q1); + """ -# check_attributes(generated_qir, 3, 0) -# check_single_qubit_gate_op(generated_qir, 3, [0, 1, 2], "h") + result = unroll(qasm_str) + + assert result.num_qubits == 3 + assert result.num_clbits == 0 + + check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 1, 2], "h") # def test_function_in_nested_loop(): @@ -296,12 +246,13 @@ # my_function(q[0], 2*b); # """ -# result = qasm3_to_qir(qasm_str) -# generated_qir = str(result).splitlines() +# result = unroll(qasm_str) + +# assert result.num_qubits == 5 +# assert result.num_clbits == 0 -# check_attributes(generated_qir, 5, 0) # check_single_qubit_rotation_op( -# generated_qir, +# result.unrolled_ast, # 9, # [0, 0, 0, 1, 1, 1, 2, 2, 2, 0], # [0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 2 * 3.14], @@ -309,54 +260,54 @@ # ) -# @pytest.mark.skip(reason="Not implemented nested functions yet") -# def test_loop_in_nested_function_call(): -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# def my_function_1(qubit q1, int[32] a){ -# for int[32] i in [0:2]{ -# rx(a*i) q1; -# } -# } - -# def my_function_2(qubit q2, int[32] b){ -# my_function_1(q2, b); -# } - -# qubit q; -# my_function_2(q, 3); -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [0, 3, 6], "rx") - - -# def test_convert_qasm3_for_loop_unsupported_type(): -# """Test correct error when converting a QASM3 program that contains a for loop initialized from -# an unsupported type.""" -# with pytest.raises( -# Qasm3ConversionError, -# match=( -# "Unexpected type " -# " of set_declaration in loop." -# ), -# ): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# qubit[4] q; -# bit[4] c; - -# h q; -# for bit b in "001" { -# x q[b]; -# } -# measure q->c; -# """, -# name="test", -# ) +@pytest.mark.skip(reason="Not implemented nested functions yet") +def test_loop_in_nested_function_call(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + def my_function_1(qubit q1, int[32] a){ + for int[32] i in [0:2]{ + rx(a*i) q1; + } + } + + def my_function_2(qubit q2, int[32] b){ + my_function_1(q2, b); + } + + qubit q; + my_function_2(q, 3); + """ + result = unroll(qasm3_string) + + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 0, 0], [0, 3, 6], "rx") + + +def test_convert_qasm3_for_loop_unsupported_type(): + """Test correct error when converting a QASM3 program that contains a for loop initialized from + an unsupported type.""" + with pytest.raises( + ValidationError, + match=( + "Unexpected type " + " of set_declaration in loop." + ), + ): + _ = validate( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[4] q; + bit[4] c; + + h q; + for bit b in "001" { + x q[b]; + } + measure q->c; + """, + ) From fd6ef69af323f3411540801d8ef8a13a4e9d0259 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 9 Oct 2024 11:54:42 +0530 Subject: [PATCH 08/16] fix for test + bug for assignment operator --- pyqasm/visitor.py | 13 ++++- tests/conftest.py | 3 + tests/resources/gates.py | 4 ++ tests/test_barrier.py | 6 +- tests/test_expressions.py | 2 + tests/test_gates.py | 4 +- tests/test_loop.py | 113 +++++++++++++++++++------------------- tests/test_measurement.py | 2 +- tests/test_reset.py | 4 +- tests/utils.py | 1 - 10 files changed, 84 insertions(+), 68 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index ede2c65..441113a 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -193,7 +193,7 @@ def _get_from_visible_scope(self, var_name: str) -> Union[Variable, None]: return scope.get(var_name, None) if var_name in scope: return scope[var_name] - # keep on checking + # keep on checking otherwise return None def _add_var_in_scope(self, variable: Variable) -> None: @@ -904,18 +904,25 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) raise_qasm3_error( f"Assignment to constant variable {lvar_name} not allowed", span=statement.span ) + binary_op = None + if statement.op != qasm3_ast.AssignmentOperator["="]: + # eg. j += 1 -> broken down to j = j + 1 + binary_op = statement.op.name.removesuffix("=") + binary_op = qasm3_ast.BinaryOperator[binary_op] # rvalue will be an evaluated value (scalar, list) # if rvalue is a list, we want a copy of it rvalue = statement.rvalue + if binary_op is not None: + rvalue = qasm3_ast.BinaryExpression(lhs=lvalue, op=binary_op, rhs=rvalue) rvalue_raw = Qasm3ExprEvaluator.evaluate_expression( rvalue ) # consists of scope check and index validation # cast + validation - # rhs is a scalar rvalue_eval = None if not isinstance(rvalue_raw, np.ndarray): + # rhs is a scalar rvalue_eval = Qasm3Validator.validate_variable_assignment_value( lvar, rvalue_raw # type: ignore[arg-type] ) @@ -932,7 +939,7 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) if lvar.readonly: # type: ignore[union-attr] raise_qasm3_error( - f"Assignment to readonly variable '{lvar_name}' not allowed" " in function call", + f"Assignment to readonly variable '{lvar_name}' not allowed in function call", span=statement.span, ) diff --git a/tests/conftest.py b/tests/conftest.py index ac13e9b..e298a43 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,5 +8,8 @@ # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. +"""Module containing imports for unit test resources +""" + from .resources.gates import * from .resources.subroutines import * diff --git a/tests/resources/gates.py b/tests/resources/gates.py index b3ddd83..4b9c754 100644 --- a/tests/resources/gates.py +++ b/tests/resources/gates.py @@ -8,6 +8,10 @@ # # THERE IS NO WARRANTY for the pyqasm, as per Section 15 of the GPL v3. +""" +Module defining QASM3 gate tests. + +""" import os import pytest diff --git a/tests/test_barrier.py b/tests/test_barrier.py index 46693e0..dde5369 100644 --- a/tests/test_barrier.py +++ b/tests/test_barrier.py @@ -101,7 +101,7 @@ def test_incorrect_barrier(): """ with pytest.raises(ValidationError, match=r"Missing register declaration for q2 .*"): - _ = validate(undeclared) + validate(undeclared) out_of_bounds = """ OPENQASM 3; @@ -114,7 +114,7 @@ def test_incorrect_barrier(): with pytest.raises( ValidationError, match="Index 3 out of range for register of size 2 in qubit" ): - _ = validate(out_of_bounds) + validate(out_of_bounds) duplicate = """ OPENQASM 3; @@ -125,4 +125,4 @@ def test_incorrect_barrier(): """ with pytest.raises(ValidationError, match=r"Duplicate qubit .*argument"): - _ = validate(duplicate) + validate(duplicate) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 875781e..21c7ff0 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -49,8 +49,10 @@ def test_correct_expressions(): def test_incorrect_expressions(): with pytest.raises(ValidationError, match=r"Unsupported expression type .*"): validate("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;") + with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): validate("OPENQASM 3; qubit q; rx(~1.3) q;") + with pytest.raises(ValidationError, match=r"Unsupported expression type .* in ~ operation"): validate("OPENQASM 3; qubit q; rx(~1.3+5im) q;") diff --git a/tests/test_gates.py b/tests/test_gates.py index e28f6b7..4eb90a3 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -234,7 +234,7 @@ def test_unsupported_modifiers(): NotImplementedError, match=r"Controlled modifier gates not yet supported .*", ): - _ = validate( + validate( f""" OPENQASM 3; include "stdgates.inc"; @@ -248,4 +248,4 @@ def test_unsupported_modifiers(): def test_incorrect_custom_ops(test_name): qasm_input, error_message = CUSTOM_GATE_INCORRECT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) diff --git a/tests/test_loop.py b/tests/test_loop.py index 0363bdd..b1c739b 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -119,33 +119,35 @@ def test_convert_qasm3_for_loop_enclosing(): check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") -# def test_convert_qasm3_for_loop_enclosing_modifying(): -# """Test for loop where variable from outer loop is modified from inside the loop.""" -# result = unroll( -# """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# qubit[4] q; -# bit[4] c; - -# int j = 0; - -# h q; -# for int i in [0:2]{ -# cx q[i], q[i+1]; -# h q[j]; -# j += 1; -# } -# h q[j]; -# measure q->c; -# """, -# ) -# assert result.num_clbits == 4 -# assert result.num_qubits == 4 - -# check_single_qubit_gate_op(result.unrolled_ast, 7, [0, 1, 2, 3, 0, 1, 2], "h") -# check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") +def test_convert_qasm3_for_loop_enclosing_modifying(): + """Test for loop where variable from outer loop is modified from inside the loop.""" + result = unroll( + """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[4] q; + bit[4] c; + + int j = 0; + + h q; + for int i in [0:2]{ + cx q[i], q[i+1]; + h q[j]; + j += i; + } + h q[j]; + measure q->c; + """, + ) + print(result.unrolled_qasm) + + assert result.num_clbits == 4 + assert result.num_qubits == 4 + + check_single_qubit_gate_op(result.unrolled_ast, 8, [0, 1, 2, 3, 0, 0, 1, 3], "h") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") def test_convert_qasm3_for_loop_discrete_set(): @@ -223,41 +225,40 @@ def my_function(qubit[3] q2) { check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 1, 2], "h") -# def test_function_in_nested_loop(): -# """Test that a function executed in a nested loop is correctly parsed.""" -# qasm_str = """OPENQASM 3; -# include "stdgates.inc"; - -# def my_function(qubit q_arg, float[32] b) { -# rx(b) q_arg; -# return; -# } -# qubit[5] q; +def test_function_in_nested_loop(): + """Test that a function executed in a nested loop is correctly parsed.""" + qasm_str = """OPENQASM 3; + include "stdgates.inc"; -# int[32] n = 2; -# float[32] b = 3.14; + def my_function(qubit q_arg, float[32] b) { + rx(b) q_arg; + return; + } + qubit[5] q; -# for int i in [0:n] { -# for int j in [0:n] { -# my_function(q[i], j*b); -# } -# } + int[32] n = 2; + float[32] b = 3.14; -# my_function(q[0], 2*b); -# """ + for int i in [0:n] { + for int j in [0:n] { + my_function(q[i], j*b); + } + } -# result = unroll(qasm_str) + my_function(q[0], 2*b); + """ -# assert result.num_qubits == 5 -# assert result.num_clbits == 0 + result = unroll(qasm_str) + assert result.num_qubits == 5 + assert result.num_clbits == 0 -# check_single_qubit_rotation_op( -# result.unrolled_ast, -# 9, -# [0, 0, 0, 1, 1, 1, 2, 2, 2, 0], -# [0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 2 * 3.14], -# "rx", -# ) + check_single_qubit_rotation_op( + result.unrolled_ast, + 10, + [0, 0, 0, 1, 1, 1, 2, 2, 2, 0], + [0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 0, 3.14, 2 * 3.14, 2 * 3.14], + "rx", + ) @pytest.mark.skip(reason="Not implemented nested functions yet") diff --git a/tests/test_measurement.py b/tests/test_measurement.py index a6ee701..7647b1c 100644 --- a/tests/test_measurement.py +++ b/tests/test_measurement.py @@ -64,7 +64,7 @@ def test_measure(): def test_incorrect_measure(): def run_test(qasm3_code, error_message): with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm3_code) + validate(qasm3_code) # Test for undeclared register q2 run_test( diff --git a/tests/test_reset.py b/tests/test_reset.py index cb3e65b..d241f5c 100644 --- a/tests/test_reset.py +++ b/tests/test_reset.py @@ -88,7 +88,7 @@ def test_incorrect_resets(): reset q2[0]; """ with pytest.raises(ValidationError): - _ = validate(undeclared) + validate(undeclared) index_error = """ OPENQASM 3; @@ -100,4 +100,4 @@ def test_incorrect_resets(): reset q1[4]; """ with pytest.raises(ValidationError): - _ = validate(index_error) + validate(index_error) diff --git a/tests/utils.py b/tests/utils.py index 532f77f..981108e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -114,7 +114,6 @@ def check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_li qubit_id += 1 param_id += 1 gate_count += 1 - print(gate_count, num_gates) assert gate_count == num_gates From 20bbadacff89a39a798467d73f5757daccd70744 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 9 Oct 2024 12:15:14 +0530 Subject: [PATCH 09/16] add contributing --- CONTRIBUTING.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d218d9..41e1061 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,115 @@ -# Contributing \ No newline at end of file +# Contributing + +Welcome! We're delighted that you're interested in contributing. Your help is essential for keeping the project great. + +## Getting Started + +Before you start working on a new feature or a fix, here's how you can contribute: + +1. **Fork the repository**: Visit the GitHub page of our project and use the "Fork" button to create a copy of the project in your own GitHub account. +2. **Create a Development Branch**: After forking, clone the repository to your local machine and create a new branch for your development. Use a descriptive name for your branch, such as `feature-` or `bugfix-`. +3. **Commit Your Changes**: Make your changes in your development branch and commit them. Be sure to write clear, concise commit messages. +4. **Push to Your Fork**: Push your changes to your forked repository on GitHub. +5. **Create a Pull Request**: Go to the original project repository and click on "Pull Requests", then click the "New Pull Request" button + +### Development install + +```shell +git clone https://github.com/qBraid/pyqasm.git +cd pyqasm +pip install -e . +``` + +## Pull requests + +Before submitting a pull request (PR), ensure your contributions comply with the [Developer's Certificate of Origin](https://developercertificate.org/), confirming your right to submit the work under this project's license. Contributors are encouraged to [sign commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits), however, it is not required. + +For code changes, please ensure that: +1. All new code includes corresponding unit tests and satisfies code coverage. +2. Docstrings are thorough and accurate for both new and updated features. +3. All integration tests, including docs and linters, are passing. +4. New functions and classes are annotated with Python type hints to support `py.typed`. + +Once PR has been validated by the maintainers, please ensure to update the [`CHANGELOG.md`](https://github.com/qBraid/pyqasm/blob/main/CHANGELOG.md) with a description of the PR and its link. You can refer to the [past release changelogs](https://github.com/qBraid/pyqasm/releases/tag/v0.0.0) as an example. Please make sure to correctly classify your work under one of the available contribution types. + +### Run tests + +Workflow: [`main.yml`](.github/workflows/main.yml) + +- [ ] All unit tests are passing +- [ ] New/modified code has corresponding unit tests and satisfies ``codecov`` checks. + +Install pytest: + +```shell +pip install pytest pytest-cov +``` + +Run unit tests: + +```shell +pytest tests +``` + +Generate a coverage report and verify that project and diff ``codecov`` are both upheld: + +```bash +pytest --cov=pyqasm --cov-report=term tests/ +``` + +### Build docs + +Workflow: [`docs.yml`](.github/workflows/docs.yml) + +- [ ] Docs builds are passing. +- [ ] New/modified code has appropriate docstrings. +- [ ] Tree stubs are updated, if applicable. +- [ ] README and/or example notebooks are updated, if applicable. + +Static docs pages (e.g. User Guide) are written using reStructuredText (reST), which is the default plaintext markup language used by [Sphinx](https://docs.readthedocs.io/en/stable/intro/getting-started-with-sphinx.html). It's pretty straightforward once you get the hang of it. If you're unfamiliar, [reStructuredText Primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#restructuredtext-primer) is a good place to start. + +Use [Google Style Python Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) +to specify attributes, arguments, exceptions, returns, and other related info. The docstrings are compiled into HTML using Sphinx, so to add relative links, in-line markup, bulleted lists, code-blocks, or do other types of formatting inside of docstrings, use the `reST` syntax mentioned (linked) above. + +Install sphinx and other docs requirements: + +```shell +pip install -e '.[docs]' +``` + +Then, build docs with: + +```shell +cd docs +make html +``` + +View docs in local browser window: + +```shell +open build/html/index.html +``` + +### Code style + +Workflow: [`format.yml`](.github/workflows/format.yml) + +- [ ] Formatting/linters checks pass +- [ ] All files have appropriate licensing header + +For code style, our project uses a combination of [isort](https://github.com/PyCQA/isort), [pylint](https://github.com/pylint-dev/pylint), +and [black](https://github.com/psf/black). Specific configurations for these tools should be added to [`pyproject.toml`](pyproject.toml). + +Install linters: + +```shell +pip install 'black[jupyter]' isort pylint +``` + +Run the following and make changes as needed to satisfy format checks: + +```shell +black pyqasm tests examples +isort pyqasm tests +pylint pyqasm tests +``` From 4cff33285f12b940340e5443ab29547e137e2ff2 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 9 Oct 2024 13:11:25 +0530 Subject: [PATCH 10/16] add alias suport --- pyqasm/visitor.py | 50 +++++++++++----- tests/resources/gates.py | 1 + tests/test_alias.py | 121 +++++++++++++++++++-------------------- 3 files changed, 98 insertions(+), 74 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 441113a..3967c4b 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -68,7 +68,9 @@ def __init__(self, module: Qasm3Module, check_only: bool = False): self._context: deque = deque([Context.GLOBAL]) self._qubit_labels: dict[str, int] = {} self._clbit_labels: dict[str, int] = {} + self._alias_qubit_labels: dict[tuple(str, int), tuple(str, int)] = {} self._global_qreg_size_map: dict[str, int] = {} + self._global_alias_size_map: dict[str, int] = {} self._function_qreg_size_map: deque = deque([]) # for nested functions self._function_qreg_transform_map: deque = deque([]) # for nested functions self._global_creg_size_map: dict[str, int] = {} @@ -348,6 +350,8 @@ def _get_op_bits( openqasm_bits = [] visited_bits = set() bit_list = [] + replace_alias = False + if isinstance(operation, qasm3_ast.QuantumMeasurementStatement): assert operation.target is not None bit_list = [operation.measure.qubit] if qubits else [operation.target] @@ -363,10 +367,15 @@ def _get_op_bits( reg_name = bit.name if reg_name not in reg_size_map: - raise_qasm3_error( - f"Missing register declaration for {reg_name} in operation {operation}", - span=operation.span, - ) + # check for aliasing + if qubits and reg_name in self._global_alias_size_map: + replace_alias = True + reg_size_map = self._global_alias_size_map + else: + raise_qasm3_error( + f"Missing register declaration for {reg_name} in operation {operation}", + span=operation.span, + ) self._check_if_name_in_scope(reg_name, operation) if isinstance(bit, qasm3_ast.IndexedIdentifier): @@ -385,6 +394,14 @@ def _get_op_bits( else: bit_ids = list(range(reg_size_map[reg_name])) + if replace_alias: + original_reg_name, _ = self._alias_qubit_labels[(reg_name, bit_ids[0])] + bit_ids = [ + self._alias_qubit_labels[(reg_name, bit_id)][1] # gives (original_reg, index) + for bit_id in bit_ids + ] + reg_name = original_reg_name + new_bits = [ qasm3_ast.IndexedIdentifier( qasm3_ast.Identifier(reg_name), [[qasm3_ast.IntegerLiteral(bit_id)]] @@ -1235,6 +1252,14 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: aliased_reg_name: str = "" aliased_reg_size: int = 0 + # this will only build a global alias map + + # whenever we are referring to qubits , we will first check in the global map of registers + # if the register is present, we will use the global map to get the qubit labels + # if not, we will check the alias map for the labels + + # see self._get_op_bits for details + # Alias should not be redeclared earlier as a variable or a constant if self._check_in_scope(alias_reg_name): raise_qasm3_error(f"Re-declaration of variable '{alias_reg_name}'", span=statement.span) @@ -1256,9 +1281,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: aliased_reg_size = self._global_qreg_size_map[aliased_reg_name] if isinstance(value, qasm3_ast.Identifier): # "let alias = q;" for i in range(aliased_reg_size): - self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[ - f"{aliased_reg_name}_{i}" - ] + self._alias_qubit_labels[(alias_reg_name, i)] = (aliased_reg_name, i) alias_reg_size = aliased_reg_size elif isinstance(value, qasm3_ast.IndexExpression): if isinstance(value.index, qasm3_ast.DiscreteSet): # "let alias = q[{0,1}];" @@ -1267,9 +1290,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: Qasm3Validator.validate_register_index( qid, self._global_qreg_size_map[aliased_reg_name], qubit=True ) - self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[ - f"{aliased_reg_name}_{qid}" - ] + self._alias_qubit_labels[(alias_reg_name, i)] = (aliased_reg_name, qid) alias_reg_size = len(qids) elif len(value.index) != 1: # like "let alias = q[0,1];"? raise_qasm3_error( @@ -1283,7 +1304,10 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: Qasm3Validator.validate_register_index( qid, self._global_qreg_size_map[aliased_reg_name], qubit=True ) - self._qubit_labels[f"{alias_reg_name}_0"] = value.index[0].value + self._alias_qubit_labels[(alias_reg_name, 0)] = ( + aliased_reg_name, + value.index[0].value, + ) alias_reg_size = 1 elif isinstance(value.index[0], qasm3_ast.RangeDefinition): # "let alias = q[0:1:2];" qids = Qasm3Transformer.get_qubits_from_range_definition( @@ -1292,10 +1316,10 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: is_qubit_reg=True, ) for i, qid in enumerate(qids): - self._qubit_labels[f"{alias_reg_name}_{i}"] = qid + self._alias_qubit_labels[(alias_reg_name, i)] = (aliased_reg_name, qid) alias_reg_size = len(qids) - self._global_qreg_size_map[alias_reg_name] = alias_reg_size + self._global_alias_size_map[alias_reg_name] = alias_reg_size logger.debug("Added labels for aliasing '%s'", target) diff --git a/tests/resources/gates.py b/tests/resources/gates.py index 4b9c754..cd29630 100644 --- a/tests/resources/gates.py +++ b/tests/resources/gates.py @@ -231,6 +231,7 @@ def test_fixture(): include "stdgates.inc"; qubit[2] q1; + ms(0,0,0) q1; u_abc(0.5, 0.5, 0.5) q1[0], q1[1]; // unsupported gate """, "Unsupported / undeclared QASM operation: u_abc", diff --git a/tests/test_alias.py b/tests/test_alias.py index c0ab01c..f5de582 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -30,61 +30,60 @@ # from .test_if import compare_reference_ir, resources_file -# def test_alias(): -# """Test converting OpenQASM 3 program with openqasm3.ast.AliasStatement.""" - -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# qubit[5] q; - -# let myqreg0 = q; -# let myqreg1 = q[1]; -# let myqreg2 = q[1:]; -# let myqreg3 = q[:4]; -# let myqreg4 = q[1:4]; -# let myqreg5 = q[1:2:4]; -# let myqreg6 = q[{0, 1}]; - -# x myqreg0[0]; -# h myqreg1; -# cx myqreg2[0], myqreg2[1]; -# cx myqreg3[2], myqreg3[3]; -# ccx myqreg4; -# swap myqreg5[0], myqreg5[1]; -# cz myqreg6; -# """ -# result = unroll(qasm3_alias_program) -# assert result.num_qubits == 5 -# assert result.num_clbits == 0 -# print(result.unrolled_qasm) -# check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") -# check_single_qubit_gate_op(result.unrolled_ast, 1, [1], "h") -# check_two_qubit_gate_op(result.unrolled_ast, 2, [[1, 2], [2, 3]], "cx") -# check_three_qubit_gate_op(result.unrolled_ast, 1, [[1, 2, 3]], "ccx") -# check_two_qubit_gate_op(result.unrolled_ast, 1, [[1, 3]], "swap") -# check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") - - -# def test_alias_update(): -# """Test converting OpenQASM 3 program with alias update.""" - -# qasm3_alias_program = """ -# OPENQASM 3.0; -# include "stdgates.inc"; - -# qubit[4] q; - -# let alias = q[1:]; -# let alias = q[2:]; - -# x alias[1]; -# """ -# result = unroll(qasm3_alias_program) -# assert result.num_qubits == 4 -# assert result.num_clbits == 0 -# check_single_qubit_gate_op(result.unrolled_ast, 1, [3], "x") +def test_alias(): + """Test converting OpenQASM 3 program with openqasm3.ast.AliasStatement.""" + + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[5] q; + + let myqreg0 = q; + let myqreg1 = q[1]; + let myqreg2 = q[1:]; + let myqreg3 = q[:4]; + let myqreg4 = q[1:4]; + let myqreg5 = q[1:2:4]; + let myqreg6 = q[{0, 1}]; + + x myqreg0[0]; + h myqreg1; + cx myqreg2[0], myqreg2[1]; + cx myqreg3[2], myqreg3[3]; + ccx myqreg4; + swap myqreg5[0], myqreg5[1]; + cz myqreg6; + """ + result = unroll(qasm3_alias_program) + assert result.num_qubits == 5 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "x") + check_single_qubit_gate_op(result.unrolled_ast, 1, [1], "h") + check_two_qubit_gate_op(result.unrolled_ast, 2, [[1, 2], [2, 3]], "cx") + check_three_qubit_gate_op(result.unrolled_ast, 1, [[1, 2, 3]], "ccx") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[1, 3]], "swap") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") + + +def test_alias_update(): + """Test converting OpenQASM 3 program with alias update.""" + + qasm3_alias_program = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[4] q; + + let alias = q[1:]; + let alias = q[2:]; + + x alias[1]; + """ + result = unroll(qasm3_alias_program) + assert result.num_qubits == 4 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 1, [3], "x") # def test_valid_alias_redefinition(): @@ -132,7 +131,7 @@ def test_alias_wrong_indexing(): x myqreg[0]; """ - _ = validate(qasm3_alias_program) + validate(qasm3_alias_program) def test_alias_invalid_discrete_indexing(): @@ -151,7 +150,7 @@ def test_alias_invalid_discrete_indexing(): x myqreg[0]; """ - _ = validate(qasm3_alias_program) + validate(qasm3_alias_program) def test_invalid_alias_redefinition(): @@ -171,7 +170,7 @@ def test_invalid_alias_redefinition(): x alias; """ - _ = validate(qasm3_alias_program) + validate(qasm3_alias_program) def test_alias_defined_before(): @@ -188,7 +187,7 @@ def test_alias_defined_before(): let myqreg = q2[1]; """ - _ = validate(qasm3_alias_program) + validate(qasm3_alias_program) def test_unsupported_alias(): @@ -205,7 +204,7 @@ def test_unsupported_alias(): let myqreg = q[0] ++ q[1]; """ - _ = validate(qasm3_alias_program) + validate(qasm3_alias_program) # def test_alias_in_scope_1(): @@ -301,4 +300,4 @@ def test_unsupported_alias(): # h q[2]; # } # """ -# _ = validate(qasm) +# validate(qasm) From 855a317410bb0b8ee54076c0117bc4a1107ee44d Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Thu, 10 Oct 2024 16:49:08 +0530 Subject: [PATCH 11/16] restructure unroller with support for if --- pyqasm/analyzer.py | 47 ------- pyqasm/elements.py | 60 +-------- pyqasm/expressions.py | 69 +++++++--- pyqasm/transformer.py | 13 +- pyqasm/visitor.py | 301 +++++++++++++++++++++++++----------------- 5 files changed, 241 insertions(+), 249 deletions(-) diff --git a/pyqasm/analyzer.py b/pyqasm/analyzer.py index 82c5236..1771fe7 100644 --- a/pyqasm/analyzer.py +++ b/pyqasm/analyzer.py @@ -20,7 +20,6 @@ import numpy as np from openqasm3.ast import ( - BinaryExpression, DiscreteSet, Expression, Identifier, @@ -28,7 +27,6 @@ IntegerLiteral, IntType, RangeDefinition, - UnaryExpression, ) from .exceptions import ValidationError, raise_qasm3_error @@ -186,48 +184,3 @@ def find_array_element(multi_dim_arr: np.ndarray, indices: list[tuple[int, int, slice(start, end + 1, step) if start != end else start for start, end, step in indices ) return multi_dim_arr[slicing] # type: ignore[index] - - @staticmethod - def analyse_branch_condition(condition: Any) -> bool: - """ - analyze the branching condition to determine the branch to take - - Args: - condition (Any): The condition to analyze - - Returns: - bool: The branch to take - """ - - if isinstance(condition, UnaryExpression): - if condition.op.name != "!": - raise_qasm3_error( - message=f"Unsupported unary expression '{condition.op.name}' in if condition", - err_type=ValidationError, - span=condition.span, - ) - return False - if isinstance(condition, BinaryExpression): - if condition.op.name != "==": - raise_qasm3_error( - message=f"Unsupported binary expression '{condition.op.name}' in if condition", - err_type=ValidationError, - span=condition.span, - ) - if not isinstance(condition.lhs, IndexExpression): - raise_qasm3_error( - message=f"Unsupported expression type '{type(condition.rhs)}' in if condition", - err_type=ValidationError, - span=condition.span, - ) - return condition.rhs.value != 0 # type: ignore[attr-defined] - if not isinstance(condition, IndexExpression): - raise_qasm3_error( - message=( - f"Unsupported expression type '{type(condition)}' in if condition. " - "Can only be a simple comparison" - ), - err_type=ValidationError, - span=condition.span, - ) - return True diff --git a/pyqasm/elements.py b/pyqasm/elements.py index d29475d..377efce 100644 --- a/pyqasm/elements.py +++ b/pyqasm/elements.py @@ -14,7 +14,6 @@ Module defining Qasm3 Converter elements. """ -from abc import ABCMeta, abstractmethod from enum import Enum from typing import Any, Optional, Union @@ -84,43 +83,6 @@ def __init__( self.readonly = readonly -class _ProgramElement(metaclass=ABCMeta): - """Abstract class for program elements""" - - @classmethod - def from_element_list(cls, elements): - """Create a list of elements from a list of elements""" - return [cls(elem) for elem in elements] - - @abstractmethod - def accept(self, visitor): - """Accept a visitor for the element""" - - -class _Register(_ProgramElement): - - def __init__(self, register: Union[QubitDeclaration, ClassicalDeclaration]): - self._register: Union[QubitDeclaration, ClassicalDeclaration] = register - - def accept(self, visitor): - visitor.visit_register(self._register) - - def __str__(self) -> str: - return f"Register({self._register})" - - -class _Statement(_ProgramElement): - - def __init__(self, statement: Statement): - self._statement = statement - - def accept(self, visitor): - visitor.visit_statement(self._statement) - - def __str__(self) -> str: - return f"Statement({self._statement})" - - class Qasm3Module: """ A module representing an unrolled openqasm quantum program. @@ -140,12 +102,12 @@ def __init__( num_qubits: int, num_clbits: int, program: Program, - elements, + statements, ): self._name = name self._num_qubits = num_qubits self._num_clbits = num_clbits - self._elements = elements + self._statements = statements self._unrolled_qasm = "" self._unrolled_ast = Program(statements=[Include("stdgates.inc")], version="3") self._original_program = program @@ -212,7 +174,7 @@ def from_program(cls, program: Program): """ Construct a Qasm3Module from a given openqasm3.ast.Program object """ - elements: list[Union[_Register, _Statement]] = [] + statements: list[Statement] = [] num_qubits = 0 num_clbits = 0 @@ -222,8 +184,6 @@ def from_program(cls, program: Program): if statement.size: size = statement.size.value # type: ignore[attr-defined] num_qubits += size - elements.append(_Register(statement)) - elif isinstance(statement, ClassicalDeclaration) and isinstance( statement.type, BitType ): @@ -231,20 +191,14 @@ def from_program(cls, program: Program): if statement.type.size: size = statement.type.size.value # type: ignore[attr-defined] num_clbits += size - elements.append(_Register(statement)) - # as bit arrays are just 0 / 1 values, we can treat them as - # classical variables too. Thus, need to add them to normal - # statements too. - elements.append(_Statement(statement)) - else: - elements.append(_Statement(statement)) + statements.append(statement) return cls( name="main", num_qubits=num_qubits, num_clbits=num_clbits, program=program, - elements=elements, + statements=statements, ) def accept(self, visitor): @@ -253,7 +207,5 @@ def accept(self, visitor): Args: visitor (BasicQasmVisitor): The visitor to accept """ - for element in self._elements: - element.accept(visitor) - + self.unrolled_ast.statements.extend(visitor.visit_basic_block(self._statements)) # TODO: some finalizing method here probably diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py index 9261276..a49b00f 100644 --- a/pyqasm/expressions.py +++ b/pyqasm/expressions.py @@ -15,7 +15,14 @@ """ -from openqasm3.ast import BinaryExpression, BooleanLiteral, BoolType, DurationLiteral, FloatLiteral +from openqasm3.ast import ( + BinaryExpression, + BooleanLiteral, + BoolType, + DurationLiteral, + Expression, + FloatLiteral, +) from openqasm3.ast import FloatType as Qasm3FloatType from openqasm3.ast import ( FunctionCall, @@ -149,7 +156,9 @@ def _get_var_value(cls, var_name, indices, expression): @classmethod # pylint: disable-next=too-many-return-statements, too-many-branches - def evaluate_expression(cls, expression, const_expr: bool = False, reqd_type=None): + def evaluate_expression( + cls, expression, const_expr: bool = False, reqd_type=None, validate_only: bool = False + ): """Evaluate an expression. Scalar types are assigned by value. Args: @@ -173,19 +182,24 @@ def evaluate_expression(cls, expression, const_expr: bool = False, reqd_type=Non expression.span, ) + def _check_and_return_value(value): + if validate_only: + return None + return value + def _process_variable(var_name: str, indices=None): cls._check_var_in_scope(var_name, expression) cls._check_var_constant(var_name, const_expr, expression) cls._check_var_type(var_name, reqd_type, expression) var_value = cls._get_var_value(var_name, indices, expression) Qasm3ExprEvaluator._check_var_initialized(var_name, var_value, expression) - return var_value + return _check_and_return_value(var_value) if isinstance(expression, Identifier): var_name = expression.name if var_name in CONSTANTS_MAP: if not reqd_type or reqd_type == Qasm3FloatType: - return CONSTANTS_MAP[var_name] + return _check_and_return_value(CONSTANTS_MAP[var_name]) raise_qasm3_error( f"Constant {var_name} not allowed in non-float expression", ValidationError, @@ -224,7 +238,7 @@ def _process_variable(var_name: str, indices=None): if index is None: # get the first dimension of the array - return dimensions[0] + return _check_and_return_value(dimensions[0]) index = cls.evaluate_expression(index, const_expr, reqd_type=Qasm3IntType) assert index is not None and isinstance(index, int) @@ -235,24 +249,24 @@ def _process_variable(var_name: str, indices=None): ValidationError, expression.span, ) - - return dimensions[index] + return _check_and_return_value(dimensions[index]) if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)): if reqd_type: + if reqd_type == BoolType and isinstance(expression, BooleanLiteral): - return expression.value + return _check_and_return_value(expression.value) if reqd_type == Qasm3IntType and isinstance(expression, IntegerLiteral): - return expression.value + return _check_and_return_value(expression.value) if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral): - return expression.value + return _check_and_return_value(expression.value) raise_qasm3_error( f"Invalid value {expression.value} with type {type(expression)} " f"for required type {reqd_type}", ValidationError, expression.span, ) - return expression.value + return _check_and_return_value(expression.value) if isinstance(expression, UnaryExpression): operand = cls.evaluate_expression(expression.expression, const_expr, reqd_type) @@ -262,20 +276,43 @@ def _process_variable(var_name: str, indices=None): ValidationError, expression.span, ) - return qasm3_expression_op_map( - "UMINUS" if expression.op.name == "-" else expression.op.name, operand - ) + op_name = "UMINUS" if expression.op.name == "-" else expression.op.name + return _check_and_return_value(qasm3_expression_op_map(op_name, operand)) if isinstance(expression, BinaryExpression): lhs = cls.evaluate_expression(expression.lhs, const_expr, reqd_type) rhs = cls.evaluate_expression(expression.rhs, const_expr, reqd_type) - return qasm3_expression_op_map(expression.op.name, lhs, rhs) + return _check_and_return_value(qasm3_expression_op_map(expression.op.name, lhs, rhs)) if isinstance(expression, FunctionCall): # function will not return a reqd / const type # Reference : https://openqasm.com/language/types.html#compile-time-constants # para : 5 - return cls.visitor_obj._visit_function_call(expression) # type: ignore[union-attr] + return _check_and_return_value(cls.visitor_obj._visit_function_call(expression)) # type: ignore[union-attr] raise_qasm3_error( f"Unsupported expression type {type(expression)}", ValidationError, expression.span ) + + @classmethod + def classical_register_in_expr(cls, expr: Expression) -> bool: + """ + Check if a classical register is present in the expression + + Args: + expr (Expression): The expression to check + + Returns: + bool: True if a classical register is present, False otherwise + """ + if isinstance(expr, Identifier): + return expr.name in cls.visitor_obj._global_creg_size_map + if isinstance(expr, IndexExpression): + var_name, _ = Qasm3Analyzer.analyze_index_expression(expr) + return var_name in cls.visitor_obj._global_creg_size_map + if isinstance(expr, BinaryExpression): + return Qasm3Analyzer.classical_register_in_expr( + expr.lhs + ) or Qasm3Analyzer.classical_register_in_expr(expr.rhs) + if isinstance(expr, UnaryExpression): + return Qasm3Analyzer.classical_register_in_expr(expr.expression) + return False diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py index 605f1e4..a61c5b8 100644 --- a/pyqasm/transformer.py +++ b/pyqasm/transformer.py @@ -218,17 +218,15 @@ def get_branch_params(condition: Any) -> tuple[int, str]: condition (Any): The condition to analyze Returns: - tuple[int, str]: The branch parameters + tuple[int, str, Any]: register_idx, register_name, value of RHS """ if isinstance(condition, UnaryExpression): - return ( - condition.expression.index[0].value, - condition.expression.collection.name, - ) + return (condition.expression.index[0].value, condition.expression.collection.name, None) if isinstance(condition, BinaryExpression): return ( condition.lhs.index[0].value, condition.lhs.collection.name, + Qasm3ExprEvaluator.evaluate_expression(condition.rhs) != 0, ) if isinstance(condition, IndexExpression): if isinstance(condition.index, DiscreteSet): @@ -242,10 +240,7 @@ def get_branch_params(condition: Any) -> tuple[int, str]: message="RangeDefinition not supported in branching condition", span=condition.span, ) - return ( - condition.index[0].value, - condition.collection.name, - ) + return (condition.index[0].value, condition.collection.name, None) # default case return -1, "" diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 3967c4b..fbbe26a 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -16,7 +16,6 @@ """ import copy import logging -from abc import ABCMeta, abstractmethod from collections import deque from typing import Any, Optional, Union @@ -42,17 +41,7 @@ logger = logging.getLogger(__name__) -class ProgramElementVisitor(metaclass=ABCMeta): - @abstractmethod - def visit_register(self, register): - pass - - @abstractmethod - def visit_statement(self, statement): - pass - - -class BasicQasmVisitor(ProgramElementVisitor): +class BasicQasmVisitor: """A visitor for basic OpenQASM program elements. This class is designed to traverse and interact with elements in an OpenQASM program. @@ -68,7 +57,7 @@ def __init__(self, module: Qasm3Module, check_only: bool = False): self._context: deque = deque([Context.GLOBAL]) self._qubit_labels: dict[str, int] = {} self._clbit_labels: dict[str, int] = {} - self._alias_qubit_labels: dict[tuple(str, int), tuple(str, int)] = {} + self._alias_qubit_labels: dict[tuple[str, int], tuple[str, int]] = {} self._global_qreg_size_map: dict[str, int] = {} self._global_alias_size_map: dict[str, int] = {} self._function_qreg_size_map: deque = deque([]) # for nested functions @@ -254,10 +243,8 @@ def _in_gate_scope(self) -> bool: def _in_block_scope(self) -> bool: # block scope is for if/else/for/while constructs return len(self._scope) > 1 and self._get_curr_context() == Context.BLOCK - def visit_register( - self, register: Union[qasm3_ast.QubitDeclaration, qasm3_ast.ClassicalDeclaration] - ) -> None: - """Visit a register element. + def _visit_qubit_register(self, register: qasm3_ast.QubitDeclaration) -> None: + """Visit a Qubit / Classical declaration statement. Args: register (QubitDeclaration|ClassicalDeclaration): The register name and size. @@ -266,59 +253,44 @@ def visit_register( None """ logger.debug("Visiting register '%s'", str(register)) - is_qubit = isinstance(register, qasm3_ast.QubitDeclaration) - current_size = len(self._qubit_labels) if is_qubit else len(self._clbit_labels) - if is_qubit: - register_size = ( - 1 if register.size is None else register.size.value # type: ignore[union-attr] - ) - if register.size is None: - register.size = qasm3_ast.IntegerLiteral(register_size) - else: - register_size = ( - 1 - if register.type.size is None # type: ignore[union-attr] - else register.type.size.value # type: ignore[union-attr] - ) - if register.type.size is None: - register.type.size = qasm3_ast.IntegerLiteral(register_size) - register_name = ( - register.qubit.name # type: ignore[union-attr] - if is_qubit - else register.identifier.name # type: ignore[union-attr] + current_size = len(self._qubit_labels) + register_size = ( + 1 if register.size is None else register.size.value # type: ignore[union-attr] ) + if register.size is None: + register.size = qasm3_ast.IntegerLiteral(register_size) + register_name = register.qubit.name # type: ignore[union-attr] - size_map = self._global_qreg_size_map if is_qubit else self._global_creg_size_map - label_map = self._qubit_labels if is_qubit else self._clbit_labels + size_map = self._global_qreg_size_map + label_map = self._qubit_labels if self._check_in_scope(register_name): raise_qasm3_error( f"Invalid declaration of register with name '{register_name}'", span=register.span ) - if is_qubit: # as bit type vars are added in classical decl handler - self._add_var_in_scope( - Variable( - register_name, - qasm3_ast.QubitDeclaration, - register_size, - None, - None, - False, - ) + self._add_var_in_scope( + Variable( + register_name, + qasm3_ast.QubitDeclaration, + register_size, + None, + None, + False, ) - self._module.add_qasm_statement(register) + ) + size_map[f"{register_name}"] = register_size for i in range(register_size): # required if indices are not used while applying a gate or measurement - size_map[f"{register_name}"] = register_size label_map[f"{register_name}_{i}"] = current_size + i self._label_scope_level[self._curr_scope].add(register_name) - logger.debug("Added labels for register '%s'", str(register)) + return [register] + def _check_if_name_in_scope(self, name: str, operation: Any) -> None: """Check if a name is in scope to avoid duplicate declarations. Args: @@ -423,7 +395,9 @@ def _get_op_bits( return openqasm_bits - def _visit_measurement(self, statement: qasm3_ast.QuantumMeasurementStatement) -> None: + def _visit_measurement( + self, statement: qasm3_ast.QuantumMeasurementStatement + ) -> list[qasm3_ast.QuantumMeasurementStatement]: """Visit a measurement statement element. Args: @@ -472,14 +446,17 @@ def _visit_measurement(self, statement: qasm3_ast.QuantumMeasurementStatement) - "for measurement operation", span=statement.span, ) + unrolled_measurements = [] if not self._check_only: for src_id, tgt_id in zip(source_ids, target_ids): unrolled_measure = qasm3_ast.QuantumMeasurementStatement( measure=qasm3_ast.QuantumMeasurement(qubit=src_id), target=tgt_id ) - self._module.add_qasm_statement(unrolled_measure) + # self._module.add_qasm_statement(unrolled_measure) + unrolled_measurements.append(unrolled_measure) + return unrolled_measurements - def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None: + def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.QuantumReset]: """Visit a reset statement element. Args: @@ -501,11 +478,14 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None: qubit_ids = self._get_op_bits(statement, self._global_qreg_size_map, True) if not self._check_only: + unrolled_resets = [] for qid in qubit_ids: unrolled_reset = qasm3_ast.QuantumReset(qubits=qid) - self._module.add_qasm_statement(unrolled_reset) + # self._module.add_qasm_statement(unrolled_reset) + unrolled_resets.append(unrolled_reset) + return unrolled_resets - def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None: + def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> list[qasm3_ast.QuantumBarrier]: """Visit a barrier statement element. Args: @@ -529,9 +509,12 @@ def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None: ) barrier_qubits = self._get_op_bits(barrier, self._global_qreg_size_map) if not self._check_only: + unrolled_barriers = [] for qubit in barrier_qubits: unrolled_barrier = qasm3_ast.QuantumBarrier(qubits=[qubit]) - self._module.add_qasm_statement(unrolled_barrier) + # self._module.add_qasm_statement(unrolled_barrier) + unrolled_barriers.append(unrolled_barrier) + return unrolled_barriers def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: """Get the parameters for the operation. @@ -563,6 +546,8 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> raise_qasm3_error(f"Duplicate gate definition for {gate_name}", span=definition.span) self._custom_gates[gate_name] = definition + return [] + def _visit_basic_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False ) -> None: @@ -573,7 +558,7 @@ def _visit_basic_gate_operation( inverse (bool): Whether the operation is an inverse operation. Defaults to False. - if inverse is True, we apply check for different cases in the - map_qasm_inv_op_to_pyqir_callable method. + map_qasm_inv_op_to_callable method. - Only rotation and S / T gates are affected by this inversion. For S/T gates we map them to Sdg / Tdg and vice versa. @@ -611,17 +596,21 @@ def _visit_basic_gate_operation( op_parameters = self._get_op_parameters(operation) if inverse_action == InversionOp.INVERT_ROTATION: op_parameters = [-1 * param for param in op_parameters] - - if not self._check_only: - for i in range(0, len(op_qubits), op_qubit_count): - # we apply the gate on the qubit subset linearly - qubit_subset = op_qubits[i : i + op_qubit_count] - if op_parameters is not None: - unrolled_gate = qasm_func(*op_parameters, *qubit_subset) - else: - unrolled_gate = qasm_func(*qubit_subset) - for gate in unrolled_gate: - self._module.add_qasm_statement(gate) + if self._check_only: + return [] + + for i in range(0, len(op_qubits), op_qubit_count): + # we apply the gate on the qubit subset linearly + qubit_subset = op_qubits[i : i + op_qubit_count] + if op_parameters is not None: + unrolled_gate = qasm_func(*op_parameters, *qubit_subset) + else: + unrolled_gate = qasm_func(*qubit_subset) + unrolled_gates = [] + for gate in unrolled_gate: + # self._module.add_qasm_statement(gate) + unrolled_gates.append(gate) + return unrolled_gates def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False @@ -670,7 +659,7 @@ def _visit_custom_gate_operation( gate_definition_ops.reverse() self._push_context(Context.GATE) - + result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, qasm3_ast.QuantumGate): # necessary to avoid modifying the original gate definition @@ -688,7 +677,7 @@ def _visit_custom_gate_operation( gate_op_copy.modifiers.append( qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None) ) - self._visit_generic_gate_operation(gate_op_copy) + result.extend(self._visit_generic_gate_operation(gate_op_copy)) else: # TODO: add control flow support raise_qasm3_error( @@ -696,6 +685,10 @@ def _visit_custom_gate_operation( ) self._restore_context() + if self._check_only: + return [] + + return result def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple: """Collapse the gate modifiers of a gate operation. @@ -756,11 +749,16 @@ def _visit_generic_gate_operation(self, operation: qasm3_ast.QuantumGate) -> Non ) # Applying the inverse first and then the power is same as # apply the power first and then inverting the result + result = [] for _ in range(power_value): if operation.name.name in self._custom_gates: - self._visit_custom_gate_operation(operation, inverse_value) + result.extend(self._visit_custom_gate_operation(operation, inverse_value)) else: - self._visit_basic_gate_operation(operation, inverse_value) + result.extend(self._visit_basic_gate_operation(operation, inverse_value)) + if self._check_only: + return [] + + return result def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) -> None: """ @@ -807,8 +805,12 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) self._add_var_in_scope(variable) + return [] + # pylint: disable=too-many-branches - def _visit_classical_declaration(self, statement: qasm3_ast.ClassicalDeclaration) -> None: + def _visit_classical_declaration( + self, statement: qasm3_ast.ClassicalDeclaration + ) -> Optional[qasm3_ast.Statement]: """Visit a classical operation element. Args: @@ -900,6 +902,19 @@ def _visit_classical_declaration(self, statement: qasm3_ast.ClassicalDeclaration self._add_var_in_scope(variable) + if isinstance(base_type, qasm3_ast.BitType): + + # handle classical register declaration + self._global_creg_size_map[var_name] = base_size + current_classical_size = len(self._clbit_labels) + for i in range(base_size): + self._clbit_labels[f"{var_name}_{i}"] = current_classical_size + i + self._label_scope_level[self._curr_scope].add(var_name) + + return [statement] + + return [] + def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) -> None: """Visit a classical assignment element. @@ -980,6 +995,8 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) lvar.value = rvalue_eval # type: ignore[union-attr] self._update_var_in_scope(lvar) # type: ignore[arg-type] + return [] + def _evaluate_array_initialization( self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: Any ) -> np.ndarray: @@ -1018,48 +1035,63 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> self._curr_scope += 1 self._label_scope_level[self._curr_scope] = set() + result = [] condition = statement.condition - positive_branching = Qasm3Analyzer.analyse_branch_condition(condition) + if Qasm3ExprEvaluator.classical_register_in_expr(condition): + # leave this condition as is, and start unrolling the block - if_block = statement.if_block - if not statement.if_block: - raise_qasm3_error("Missing if block", span=statement.span) - else_block = statement.else_block - if not positive_branching: - if_block, else_block = else_block, if_block + # here, the lhs CAN only be a classical register as QCs won't have + # ability to evaluate expressions in the condition - reg_id, reg_name = Qasm3Transformer.get_branch_params(condition) + reg_id, reg_name, rhs_value = Qasm3Transformer.get_branch_params(condition) - if reg_name not in self._global_creg_size_map: - raise_qasm3_error( - f"Missing register declaration for {reg_name} in {condition}", - span=statement.span, + if reg_name not in self._global_creg_size_map: + raise_qasm3_error( + f"Missing register declaration for {reg_name} in {condition}", + span=statement.span, + ) + Qasm3Validator.validate_register_index( + reg_id, self._global_creg_size_map[reg_name], qubit=False ) - Qasm3Validator.validate_register_index( - reg_id, self._global_creg_size_map[reg_name], qubit=False - ) - - def _visit_statement_block(block): - for stmt in block: - self.visit_statement(stmt) - # to update - if not self._check_only: - # if the condition is true, we visit the if block - pyqir._native.if_result( - self._builder, - pyqir.result(self._module.context, self._clbit_labels[f"{reg_name}_{reg_id}"]), - zero=lambda: _visit_statement_block(else_block), - one=lambda: _visit_statement_block(if_block), + new_if_block = qasm3_ast.BranchingStatement( + condition=qasm3_ast.BinaryExpression( + op=qasm3_ast.BinaryOperator["=="], + lhs=qasm3_ast.IndexExpression( + collection=qasm3_ast.Identifier(name=reg_name), + index=[qasm3_ast.IntegerLiteral(reg_id)], + ), + rhs=qasm3_ast.BooleanLiteral(rhs_value), + ), + if_block=self.visit_basic_block(statement.if_block), + else_block=self.visit_basic_block(statement.else_block), ) - # to update + result.extend(new_if_block) + + else: + # here we can unroll the block depending on the condition + positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition) != 0 + + if_block = statement.if_block + if not statement.if_block: + raise_qasm3_error("Missing if block", span=statement.span) + else_block = statement.else_block + + block_to_visit = if_block if positive_branching else else_block + + result.extend(self.visit_basic_block(block_to_visit)) del self._label_scope_level[self._curr_scope] self._curr_scope -= 1 self._pop_scope() self._restore_context() - def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None: + if self._check_only: + return [] + + return result + + def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.Statement]: # Compute loop variable values if isinstance(statement.set_declaration, qasm3_ast.RangeDefinition): init_exp = statement.set_declaration.start @@ -1085,6 +1117,7 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None: i: Optional[Variable] # will store iteration Variable to update to loop scope + result = [] for ival in irange: self._push_context(Context.BLOCK) self._push_scope({}) @@ -1102,8 +1135,7 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None: i.value = ival self._update_var_in_scope(i) - for stmt in statement.block: - self.visit_statement(stmt) + result.extend(self.visit_basic_block(statement.block)) # scope not persistent between loop iterations self._pop_scope() @@ -1112,7 +1144,8 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None: # as we are only checking compile time errors # not runtime errors, we can break here if self._check_only: - break + return [] + return result def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition) -> None: """Visit a subroutine definition element. @@ -1143,6 +1176,8 @@ def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition self._subroutine_defns[fn_name] = statement + return [] + # pylint: disable=too-many-locals, too-many-statements def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: """Visit a function call element. @@ -1207,11 +1242,12 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: self._function_qreg_transform_map.append(qubit_transform_map) return_statement = None + result = [] for function_op in subroutine_def.body: if isinstance(function_op, qasm3_ast.ReturnStatement): return_statement = copy.deepcopy(function_op) break - self.visit_statement(copy.deepcopy(function_op)) + result.extend(self.visit_statement(copy.deepcopy(function_op))) if return_statement: return_value = Qasm3ExprEvaluator.evaluate_expression(return_statement.expression) @@ -1228,12 +1264,11 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: self._curr_scope -= 1 self._pop_scope() - return return_value if subroutine_def.return_type is not None else None + return return_value if subroutine_def.return_type is not None else result def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: pass - # to edit... def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: """Visit an alias statement element. @@ -1323,7 +1358,11 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: logger.debug("Added labels for aliasing '%s'", target) - def _visit_switch_statement(self, statement: qasm3_ast.SwitchStatement) -> None: + return [] + + def _visit_switch_statement( + self, statement: qasm3_ast.SwitchStatement + ) -> list[qasm3_ast.Statement]: """Visit a switch statement element. Args: @@ -1362,13 +1401,16 @@ def _evaluate_case(statements): # BECAUSE the case expression CAN CONTAIN VARS from global scope self._push_context(Context.BLOCK) self._push_scope({}) - + result = [] for stmt in statements: Qasm3Validator.validate_statement_type(SWITCH_BLACKLIST_STMTS, stmt, "switch") - self.visit_statement(stmt) + result.extend(self.visit_statement(stmt)) self._pop_scope() self._restore_context() + if self._check_only: + return [] + return result case_fulfilled = False for case in statement.cases: @@ -1394,14 +1436,13 @@ def _evaluate_case(statements): if case_fulfilled: case_stmts = case[1].statements - _evaluate_case(case_stmts) - break + return _evaluate_case(case_stmts) if not case_fulfilled and statement.default: default_stmts = statement.default.statements - _evaluate_case(default_stmts) + return _evaluate_case(default_stmts) - def visit_statement(self, statement: qasm3_ast.Statement) -> None: + def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Statement]: """Visit a statement element. Args: @@ -1411,12 +1452,13 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> None: None """ logger.debug("Visiting statement '%s'", str(statement)) - + result = [] visit_map = { - qasm3_ast.Include: lambda x: None, # No operation + qasm3_ast.Include: lambda x: [], # No operation qasm3_ast.QuantumMeasurementStatement: self._visit_measurement, qasm3_ast.QuantumReset: self._visit_reset, qasm3_ast.QuantumBarrier: self._visit_barrier, + qasm3_ast.QubitDeclaration: self._visit_qubit_register, qasm3_ast.QuantumGateDefinition: self._visit_gate_definition, qasm3_ast.QuantumGate: self._visit_generic_gate_operation, qasm3_ast.ClassicalDeclaration: self._visit_classical_declaration, @@ -1428,16 +1470,29 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> None: qasm3_ast.SwitchStatement: self._visit_switch_statement, qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), - qasm3_ast.IODeclaration: lambda x: (_ for _ in ()).throw( - NotImplementedError("OpenQASM 3 IO declarations not yet supported") - ), + qasm3_ast.IODeclaration: lambda x: [], } visitor_function = visit_map.get(type(statement)) if visitor_function: - visitor_function(statement) # type: ignore[operator] + result.extend(visitor_function(statement)) # type: ignore[operator] else: raise_qasm3_error( f"Unsupported statement of type {type(statement)}", span=statement.span ) + return result + + def visit_basic_block(self, stmt_list: list[qasm3_ast.Statement]) -> list[qasm3_ast.Statement]: + """Visit a basic block of statements. + + Args: + stmt_list (list[qasm3_ast.Statement]): The list of statements to visit. + + Returns: + list[qasm3_ast.Statement]: The list of unrolled statements. + """ + result = [] + for stmt in stmt_list: + result.extend(self.visit_statement(stmt)) + return result From eef81ff1f78bf9be939108540141c07edb5fd9e8 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Oct 2024 12:47:37 +0530 Subject: [PATCH 12/16] fix tests for if and some bugs --- pyqasm/expressions.py | 13 +- pyqasm/transformer.py | 44 ++- pyqasm/visitor.py | 68 ++-- tests/declarations/test_classical.py | 483 +++++++++++++------------- tests/declarations/test_quantum.py | 4 +- tests/subroutines/test_subroutines.py | 5 + tests/test_if.py | 483 ++++++++++++++------------ tests/test_loop.py | 2 + tests/utils.py | 5 +- 9 files changed, 605 insertions(+), 502 deletions(-) diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py index a49b00f..d3c1aa9 100644 --- a/pyqasm/expressions.py +++ b/pyqasm/expressions.py @@ -287,8 +287,9 @@ def _process_variable(var_name: str, indices=None): # function will not return a reqd / const type # Reference : https://openqasm.com/language/types.html#compile-time-constants # para : 5 - return _check_and_return_value(cls.visitor_obj._visit_function_call(expression)) # type: ignore[union-attr] - + return _check_and_return_value( + cls.visitor_obj._visit_function_call(expression) # type: ignore[union-attr] + ) raise_qasm3_error( f"Unsupported expression type {type(expression)}", ValidationError, expression.span ) @@ -310,9 +311,9 @@ def classical_register_in_expr(cls, expr: Expression) -> bool: var_name, _ = Qasm3Analyzer.analyze_index_expression(expr) return var_name in cls.visitor_obj._global_creg_size_map if isinstance(expr, BinaryExpression): - return Qasm3Analyzer.classical_register_in_expr( - expr.lhs - ) or Qasm3Analyzer.classical_register_in_expr(expr.rhs) + return cls.classical_register_in_expr(expr.lhs) or cls.classical_register_in_expr( + expr.rhs + ) if isinstance(expr, UnaryExpression): - return Qasm3Analyzer.classical_register_in_expr(expr.expression) + return cls.classical_register_in_expr(expr.expression) return False diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py index a61c5b8..e489dc7 100644 --- a/pyqasm/transformer.py +++ b/pyqasm/transformer.py @@ -17,6 +17,7 @@ import numpy as np from openqasm3.ast import ( BinaryExpression, + BinaryOperator, BooleanLiteral, DiscreteSet, FloatLiteral, @@ -24,12 +25,16 @@ IndexedIdentifier, IndexExpression, IntegerLiteral, +) +from openqasm3.ast import IntType as Qasm3IntType +from openqasm3.ast import ( QuantumBarrier, QuantumGate, QuantumReset, RangeDefinition, UintType, UnaryExpression, + UnaryOperator, ) from .elements import Variable @@ -218,14 +223,45 @@ def get_branch_params(condition: Any) -> tuple[int, str]: condition (Any): The condition to analyze Returns: - tuple[int, str, Any]: register_idx, register_name, value of RHS + tuple[Optional[int], str, Any]: register_idx, register_name, value of RHS """ + if isinstance(condition, Identifier): + raise_qasm3_error( + message="Only simple comparison supported in branching condition with " + "classical register", + span=condition.span, + ) if isinstance(condition, UnaryExpression): - return (condition.expression.index[0].value, condition.expression.collection.name, None) + print(condition) + if condition.op != UnaryOperator["!"]: + raise_qasm3_error( + message="Only '!' supported in branching condition with classical register", + span=condition.span, + ) + return ( + condition.expression.index[0].value, + condition.expression.collection.name, + False, + ) if isinstance(condition, BinaryExpression): + if condition.op != BinaryOperator["=="]: + raise_qasm3_error( + message="Only '==' supported in branching condition with classical register", + span=condition.span, + ) + + if isinstance(condition.lhs, Identifier): + # full register eg. if(c == 5) + return ( + None, + condition.lhs.name, + # do not evaluate to bool + Qasm3ExprEvaluator.evaluate_expression(condition.rhs, reqd_type=Qasm3IntType), + ) return ( condition.lhs.index[0].value, condition.lhs.collection.name, + # evaluate to bool Qasm3ExprEvaluator.evaluate_expression(condition.rhs) != 0, ) if isinstance(condition, IndexExpression): @@ -240,9 +276,9 @@ def get_branch_params(condition: Any) -> tuple[int, str]: message="RangeDefinition not supported in branching condition", span=condition.span, ) - return (condition.index[0].value, condition.collection.name, None) + return (condition.index[0].value, condition.collection.name, True) # eg. if(c[0]) # default case - return -1, "" + return None, "", None @classmethod def transform_function_qubits( diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index fbbe26a..7359f19 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -477,13 +477,12 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.Quan ) qubit_ids = self._get_op_bits(statement, self._global_qreg_size_map, True) + unrolled_resets = [] if not self._check_only: - unrolled_resets = [] for qid in qubit_ids: unrolled_reset = qasm3_ast.QuantumReset(qubits=qid) - # self._module.add_qasm_statement(unrolled_reset) unrolled_resets.append(unrolled_reset) - return unrolled_resets + return unrolled_resets def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> list[qasm3_ast.QuantumBarrier]: """Visit a barrier statement element. @@ -598,7 +597,7 @@ def _visit_basic_gate_operation( op_parameters = [-1 * param for param in op_parameters] if self._check_only: return [] - + result = [] for i in range(0, len(op_qubits), op_qubit_count): # we apply the gate on the qubit subset linearly qubit_subset = op_qubits[i : i + op_qubit_count] @@ -610,7 +609,11 @@ def _visit_basic_gate_operation( for gate in unrolled_gate: # self._module.add_qasm_statement(gate) unrolled_gates.append(gate) - return unrolled_gates + result.extend(unrolled_gates) + + if self._check_only: + return [] + return result def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False @@ -828,10 +831,8 @@ def _visit_classical_declaration( if self._in_block_scope() and var_name not in self._get_curr_scope(): # we can re-declare variables once in block scope even if they are # present in the parent scope - # Eg. - # int a = 10; - # { int a = 20; // valid - # } + # Eg. int a = 10; + # { int a = 20;} // is valid pass else: raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) @@ -899,11 +900,9 @@ def _visit_classical_declaration( variable.value = Qasm3Validator.validate_variable_assignment_value( variable, init_value ) - self._add_var_in_scope(variable) if isinstance(base_type, qasm3_ast.BitType): - # handle classical register declaration self._global_creg_size_map[var_name] = base_size current_classical_size = len(self._clbit_labels) @@ -911,6 +910,12 @@ def _visit_classical_declaration( self._clbit_labels[f"{var_name}_{i}"] = current_classical_size + i self._label_scope_level[self._curr_scope].add(var_name) + if hasattr(statement.type, "size"): + statement.type.size = ( + qasm3_ast.IntegerLiteral(1) + if statement.type.size is None + else qasm3_ast.IntegerLiteral(base_size) + ) return [statement] return [] @@ -1037,6 +1042,10 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> result = [] condition = statement.condition + + if not statement.if_block: + raise_qasm3_error("Missing if block", span=statement.span) + if Qasm3ExprEvaluator.classical_register_in_expr(condition): # leave this condition as is, and start unrolling the block @@ -1050,34 +1059,41 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> f"Missing register declaration for {reg_name} in {condition}", span=statement.span, ) - Qasm3Validator.validate_register_index( - reg_id, self._global_creg_size_map[reg_name], qubit=False + if reg_id is not None: + Qasm3Validator.validate_register_index( + reg_id, self._global_creg_size_map[reg_name], qubit=False + ) + + new_lhs = ( + qasm3_ast.IndexExpression( + collection=qasm3_ast.Identifier(name=reg_name), + index=[qasm3_ast.IntegerLiteral(reg_id)], + ) + if reg_id is not None + else qasm3_ast.Identifier(name=reg_name) + ) + + new_rhs = ( + qasm3_ast.BooleanLiteral(rhs_value) + if isinstance(rhs_value, bool) + else qasm3_ast.IntegerLiteral(rhs_value) ) new_if_block = qasm3_ast.BranchingStatement( condition=qasm3_ast.BinaryExpression( op=qasm3_ast.BinaryOperator["=="], - lhs=qasm3_ast.IndexExpression( - collection=qasm3_ast.Identifier(name=reg_name), - index=[qasm3_ast.IntegerLiteral(reg_id)], - ), - rhs=qasm3_ast.BooleanLiteral(rhs_value), + lhs=new_lhs, + rhs=new_rhs, ), if_block=self.visit_basic_block(statement.if_block), else_block=self.visit_basic_block(statement.else_block), ) - result.extend(new_if_block) + result.append(new_if_block) else: # here we can unroll the block depending on the condition positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition) != 0 - - if_block = statement.if_block - if not statement.if_block: - raise_qasm3_error("Missing if block", span=statement.span) - else_block = statement.else_block - - block_to_visit = if_block if positive_branching else else_block + block_to_visit = statement.if_block if positive_branching else statement.else_block result.extend(self.visit_basic_block(block_to_visit)) diff --git a/tests/declarations/test_classical.py b/tests/declarations/test_classical.py index edf0a39..8c6f1f0 100644 --- a/tests/declarations/test_classical.py +++ b/tests/declarations/test_classical.py @@ -15,10 +15,10 @@ import pytest from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll from pyqasm.validate import validate - -# from tests.qir_utils import check_attributes, check_single_qubit_rotation_op from tests.resources.variables import ASSIGNMENT_TESTS, DECLARATION_TESTS +from tests.utils import check_single_qubit_rotation_op # from qbraid_qir.qasm3.convert import qasm3_to_qir @@ -87,85 +87,98 @@ def test_scalar_assignments(): validate(qasm3_string) -# # 4. Scalar value assignment -# def test_scalar_value_assignment(): -# """Test assigning variable values from other variables""" -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# int a = 5; -# float[32] r; -# float[32] f = 0.5; -# int b = a; -# r = 0.23; -# qubit q; -# rx(b) q; -# rx(r + f*4) q; -# """ - -# b = 5.0 -# r = 0.23 -# f = 0.5 -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 2, [0, 0], [b, r + f * 4], "rx") - - -# def test_scalar_type_casts(): -# """Test type casts on scalar variables""" -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# int[32] a = 5.1; -# float[32] r = 245; -# uint[4] b = -4; // -4 % 16 = 12 -# bit bit_var = 0; -# bool f = 0; -# bool g = 1; - -# qubit q; -# rx(a) q; -# rx(r) q; -# rx(b) q; -# rx(f) q; -# rx(g) q; - -# """ -# a = 5 -# r = 245 -# b = 12 -# f = 0 -# g = 1 - -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 1) -# check_single_qubit_rotation_op(generated_qir, 5, [0, 0, 0, 0, 0], [a, r, b, f, g], "rx") - - -# def test_array_type_casts(): -# """Test type casts on array variables""" - -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; -# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6.2} }; -# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6.2} }; -# array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, 12} }; -# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6} }; - -# qubit q; -# rx(arr_int[2][1]) q; // should be 6 -# rx(arr_uint[2][1]) q; // should be 6 -# rx(arr_bool[2][1]) q; // should be 1 (true) -# rx(arr_float32[2][1]) q; // should be 6.0 - -# """ -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 4, [0, 0, 0, 0], [6, 6, 1, 6.0], "rx") +# 4. Scalar value assignment +def test_scalar_value_assignment(): + """Test assigning variable values from other variables""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + int a = 5; + float[32] r; + float[32] f = 0.5; + int b = a; + r = 0.23; + qubit q; + rx(b) q; + rx(r + f*4) q; + """ + + b = 5.0 + r = 0.23 + f = 0.5 + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 2, [0, 0], [b, r + f * 4], "rx") + + +def test_scalar_type_casts(): + """Test type casts on scalar variables""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + int[32] a = 5.1; + float[32] r = 245; + uint[4] b = -4; // -4 % 16 = 12 + bit bit_var = 0; + bool f = 0; + bool g = 1; + + qubit q; + rx(a) q; + rx(r) q; + rx(b) q; + rx(f) q; + rx(g) q; + + """ + a = 5 + r = 245 + b = 12 + f = 0 + g = 1 + + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 5, [0, 0, 0, 0, 0], [a, r, b, f, g], "rx") + + +def test_array_type_casts(): + """Test type casts on array variables""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6.2} }; + array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6.2} }; + array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, 12} }; + array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6} }; + + qubit q; + rx(arr_int[2][1]) q; // should be 6 + rx(arr_uint[2][1]) q; // should be 6 + rx(arr_bool[2][1]) q; // should be 1 (true) + rx(arr_float32[2][1]) q; // should be 6.0 + + """ + arr_int_val = 6 + arr_uint_val = 6 + arr_bool_val = 1 + arr_float32_val = 6.0 + + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, + 4, + [0, 0, 0, 0], + [arr_int_val, arr_uint_val, arr_bool_val, arr_float32_val], + "rx", + ) # 5. Array declarations @@ -183,157 +196,163 @@ def test_array_declarations(): validate(qasm3_string) -# # 6. Array assignments -# def test_array_assignments(): -# """Test array assignments""" - -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# array[int[32], 3, 2] arr_int; -# array[uint[32], 3, 2] arr_uint; -# array[float[32], 3, 2] arr_float32; -# array[float[64], 3, 2] arr_float64; -# array[bool, 3, 2] arr_bool; - -# int a = 2; -# uint b = 3; -# float[32] c = 4.5; -# float[64] d = 6.7; -# bool f = true; - -# arr_int[0][1] = a*a; -# arr_int[0,1] = a*a; - -# arr_uint[0][1] = b*b; -# arr_uint[0,1] = b*b; - -# arr_float32[0][1] = c*c; -# arr_float32[0,1] = c*c; - -# arr_float64[0][1] = d*d; -# arr_float64[0,1] = d*d; - -# arr_bool[0][1] = f; -# arr_bool[0,1] = f; - -# qubit q; -# rx(arr_int[0,1]) q; -# rx(arr_uint[0][1]) q; -# rx(arr_float32[0,1]) q; -# rx(arr_float64[0][1]) q; -# rx(arr_bool[0,1]) q; -# """ - -# a = 2 -# b = 3 -# c = 4.5 -# d = 6.7 -# f = True -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op( -# generated_qir, 5, [0, 0, 0, 0, 0], [a * a, b * b, c * c, d * d, f], "rx" -# ) - - -# # 7. Test expressions which involves arrays -# def test_array_expressions(): -# """Test array expressions""" -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# array[int[32], 3, 2] arr_int; -# array[uint[32], 3, 2] arr_uint; -# array[float[32], 3, 2] arr_float32; -# array[float[64], 3, 2] arr_float64; -# array[bool, 3, 2] arr_bool; - -# int a = 2; -# uint b = 3; -# float[32] c = 4.5; -# float[64] d = 6.7; -# bool f = true; - - -# arr_int[0][1] = a*a; -# arr_int[1][0] = arr_int[0][1]; -# arr_uint[0][1] = b*b; -# arr_float32[0][1] = c*c; -# arr_float64[0][1] = d*d; - -# qubit q; -# rx(arr_int[{0,1}] + arr_uint[0][1]) q; -# rx(arr_float32[{0,1}] + arr_float64[0][1]) q; -# """ - -# a = 2 -# b = 3 -# c = 4.5 -# d = 6.7 -# f = True -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 2, [0, 0], [a * a + b * b, c * c + d * d], "rx") - - -# def test_array_initializations(): -# """Test array initializations""" - -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; -# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; -# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; -# array[float[64], 3, 2] arr_float64 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; -# array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, false} }; - -# qubit q; -# rx(arr_int[0][1]) q; -# rx(arr_uint[0][1]) q; -# rx(arr_float32[0][1]) q; -# rx(arr_float64[0][1]) q; -# rx(arr_bool[0][1]) q; -# """ - -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 5, [0, 0, 0, 0, 0], [2, 2, 2.0, 2.0, 0], "rx") - - -# def test_array_range_assignment(): -# """Test array range assignment""" - -# qasm3_string = """ -# OPENQASM 3; -# include "stdgates.inc"; - -# array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; -# array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; -# array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; - -# arr_int[0, 0:1] = arr_int[1, 0:1]; -# arr_uint[0:2, 1] = arr_uint[0:2, 0]; -# arr_float32[0:2, 1] = arr_float32[0:2, 0]; - -# qubit q; -# rx(arr_int[0][1]) q; -# rx(arr_uint[0][1]) q; -# rx(arr_float32[1][1]) q; - -# """ - -# result = qasm3_to_qir(qasm3_string) -# generated_qir = str(result).splitlines() -# check_attributes(generated_qir, 1, 0) -# check_single_qubit_rotation_op(generated_qir, 3, [0, 0, 0], [4, 1, 3.0], "rx") +# 6. Array assignments +def test_array_assignments(): + """Test array assignments""" + + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + array[int[32], 3, 2] arr_int; + array[uint[32], 3, 2] arr_uint; + array[float[32], 3, 2] arr_float32; + array[float[64], 3, 2] arr_float64; + array[bool, 3, 2] arr_bool; + + int a = 2; + uint b = 3; + float[32] c = 4.5; + float[64] d = 6.7; + bool f = true; + + arr_int[0][1] = a*a; + arr_int[0,1] = a*a; + + arr_uint[0][1] = b*b; + arr_uint[0,1] = b*b; + + arr_float32[0][1] = c*c; + arr_float32[0,1] = c*c; + + arr_float64[0][1] = d*d; + arr_float64[0,1] = d*d; + + arr_bool[0][1] = f; + arr_bool[0,1] = f; + + qubit q; + rx(arr_int[0,1]) q; + rx(arr_uint[0][1]) q; + rx(arr_float32[0,1]) q; + rx(arr_float64[0][1]) q; + rx(arr_bool[0,1]) q; + """ + + a = 2 + b = 3 + c = 4.5 + d = 6.7 + f = True + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + check_single_qubit_rotation_op( + result.unrolled_ast, 5, [0, 0, 0, 0, 0], [a * a, b * b, c * c, d * d, f], "rx" + ) + + +# 7. Test expressions which involves arrays +def test_array_expressions(): + """Test array expressions""" + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + array[int[32], 3, 2] arr_int; + array[uint[32], 3, 2] arr_uint; + array[float[32], 3, 2] arr_float32; + array[float[64], 3, 2] arr_float64; + array[bool, 3, 2] arr_bool; + + int a = 2; + uint b = 3; + float[32] c = 4.5; + float[64] d = 6.7; + bool f = true; + + + arr_int[0][1] = a*a; + arr_int[1][0] = arr_int[0][1]; + arr_uint[0][1] = b*b; + arr_float32[0][1] = c*c; + arr_float64[0][1] = d*d; + + qubit q; + rx(arr_int[{0,1}] + arr_uint[0][1]) q; + rx(arr_float32[{0,1}] + arr_float64[0][1]) q; + """ + + a = 2 + b = 3 + c = 4.5 + d = 6.7 + f = True + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + check_single_qubit_rotation_op( + result.unrolled_ast, 2, [0, 0], [a * a + b * b, c * c + d * d], "rx" + ) + + +def test_array_initializations(): + """Test array initializations""" + + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; + array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; + array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; + array[float[64], 3, 2] arr_float64 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; + array[bool, 3, 2] arr_bool = { {true, false}, {true, false}, {true, false} }; + + qubit q; + rx(arr_int[0][1]) q; + rx(arr_uint[0][1]) q; + rx(arr_float32[0][1]) q; + rx(arr_float64[0][1]) q; + rx(arr_bool[0][1]) q; + """ + + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op( + result.unrolled_ast, 5, [0, 0, 0, 0, 0], [2, 2, 2.0, 2.0, 0], "rx" + ) + + +def test_array_range_assignment(): + """Test array range assignment""" + + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + array[int[32], 3, 2] arr_int = { {1, 2}, {3, 4}, {5, 6} }; + array[uint[32], 3, 2] arr_uint = { {1, 2}, {3, 4}, {5, 6} }; + array[float[32], 3, 2] arr_float32 = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} }; + + arr_int[0, 0:1] = arr_int[1, 0:1]; + arr_uint[0:2, 1] = arr_uint[0:2, 0]; + arr_float32[0:2, 1] = arr_float32[0:2, 0]; + + qubit q; + rx(arr_int[0][1]) q; + rx(arr_uint[0][1]) q; + rx(arr_float32[1][1]) q; + + """ + + result = unroll(qasm3_string) + assert result.num_clbits == 0 + assert result.num_qubits == 1 + + check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 0, 0], [4, 1, 3.0], "rx") @pytest.mark.parametrize("test_name", DECLARATION_TESTS.keys()) diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py index b23b6f8..cccbc33 100644 --- a/tests/declarations/test_quantum.py +++ b/tests/declarations/test_quantum.py @@ -113,7 +113,9 @@ def test_qubit_redeclaration_error(): def test_clbit_redeclaration_error(): """Test redeclaration of clbit""" - with pytest.raises(ValidationError, match="Invalid declaration of register with name 'c1'"): + with pytest.raises( + ValidationError, match="Semantic validation failed: Re-declaration of variable c1" + ): qasm3_string = """ OPENQASM 3; include "stdgates.inc"; diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py index b7986c2..8b4b235 100644 --- a/tests/subroutines/test_subroutines.py +++ b/tests/subroutines/test_subroutines.py @@ -112,6 +112,10 @@ def my_function(qubit q) -> bool{ return true; } qubit q; + // do I only care about the return value? + // or we also need to add the gate to the unrolled_ast? + + // how to do this? bool b = my_function(q); """ @@ -119,6 +123,7 @@ def my_function(qubit q) -> bool{ assert result.num_clbits == 0 assert result.num_qubits == 1 + print(result.unrolled_qasm) check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") diff --git a/tests/test_if.py b/tests/test_if.py index 589b175..5ce1429 100644 --- a/tests/test_if.py +++ b/tests/test_if.py @@ -12,234 +12,255 @@ """ -# import os -# from pathlib import Path - -# import pyqir -# import pytest -# from qbraid_qir.qasm3 import Qasm3ConversionError, qasm3_to_qir - -# from tests.qir_utils import check_attributes, get_entry_point_body - -# RESOURCES_DIR = os.path.join( -# os.path.dirname(__file__).removesuffix("converter"), "fixtures/resources" -# ) - - -# def resources_file(filename: str) -> str: -# return str(os.path.join(RESOURCES_DIR, f"{filename}")) - - -# def compare_reference_ir(generated_bitcode: bytes, file_path: str) -> None: -# module = pyqir.Module.from_bitcode(pyqir.Context(), generated_bitcode, f"{file_path}") -# ir = str(module) -# pyqir_ir_body = get_entry_point_body(ir.splitlines()) - -# expected = Path(file_path).read_text(encoding="utf-8") -# expected_ir_body = get_entry_point_body(expected.splitlines()) -# assert pyqir_ir_body == expected_ir_body - - -# def test_simple_if(): -# qasm = """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[4] q; -# bit[4] c; -# h q; -# measure q -> c; -# if(c[0]){ -# x q[0]; -# cx q[0], q[1]; -# } - -# if(c[1] == 1){ -# cx q[1], q[2]; -# } - -# if(!c[2]){ -# h q[2]; -# } -# """ -# result = qasm3_to_qir(qasm) -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 4, 4) -# simple_file = resources_file("simple_if.ll") -# compare_reference_ir(result.bitcode, simple_file) - - -# def test_complex_if(): -# qasm = """ -# OPENQASM 3; -# include "stdgates.inc"; -# gate custom a, b{ -# cx a, b; -# h a; -# } -# qubit[4] q; -# bit[4] c; -# bit[4] c0; - -# h q; -# measure q -> c0; -# if(c0[0]){ -# x q[0]; -# cx q[0], q[1]; -# if (c0[1]){ -# cx q[1], q[2]; -# } -# } -# if (c[0]){ -# custom q[2], q[3]; -# } -# """ -# result = qasm3_to_qir(qasm) -# generated_qir = str(result).splitlines() - -# check_attributes(generated_qir, 4, 8) -# complex_if = resources_file("complex_if.ll") -# compare_reference_ir(result.bitcode, complex_if) - - -# def test_incorrect_if(): -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported expression type .*"): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c == 4){ -# cx q; -# } -# """ -# ) - -# with pytest.raises(Qasm3ConversionError, match=r"Missing if block"): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c[0]){ -# } -# """ -# ) - -# with pytest.raises(Qasm3ConversionError, match=r"Missing register declaration for c2 .*"): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c2[0]){ -# cx q; -# } -# """ -# ) - -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported unary expression .*"): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(~c[0]){ -# cx q; -# } -# """ -# ) -# with pytest.raises(Qasm3ConversionError, match=r"Unsupported binary expression .*"): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c[0] >= 1){ -# cx q; -# } -# """ -# ) -# with pytest.raises( -# Qasm3ConversionError, -# match=r"Unsupported expression type .* in if condition. Can only be a simple comparison", -# ): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c){ -# cx q; -# } -# """ -# ) -# with pytest.raises( -# Qasm3ConversionError, -# match=r"RangeDefinition not supported in branching condition", -# ): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c[0:1]){ -# cx q; -# } -# """ -# ) - -# with pytest.raises( -# Qasm3ConversionError, -# match=r"DiscreteSet not supported in branching condition", -# ): -# _ = qasm3_to_qir( -# """ -# OPENQASM 3; -# include "stdgates.inc"; -# qubit[2] q; -# bit[2] c; - -# h q; -# measure q->c; - -# if(c[{0,1}]){ -# cx q; -# } -# """ -# ) +import pytest + +from pyqasm.exceptions import ValidationError +from pyqasm.unroller import unroll +from pyqasm.validate import validate +from tests.utils import check_unrolled_qasm + + +def test_simple_if(): + qasm = """OPENQASM 3; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + h q; + measure q -> c; + if(c[0]){ + x q[0]; + cx q[0], q[1]; + } + if(c[1] == 1){ + cx q[1], q[2]; + } + if(c == 5){ + x q[3]; + } + """ + expected_qasm = """OPENQASM 3; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + h q[0]; + h q[1]; + h q[2]; + h q[3]; + c[0] = measure q[0]; + c[1] = measure q[1]; + c[2] = measure q[2]; + c[3] = measure q[3]; + if (c[0] == true) { + x q[0]; + cx q[0], q[1]; + } + if (c[1] == true) { + cx q[1], q[2]; + } + if (c == 5) { + x q[3]; + } + """ + + result = unroll(qasm) + assert result.num_clbits == 4 + assert result.num_qubits == 4 + + check_unrolled_qasm(result.unrolled_qasm, expected_qasm) + + +def test_complex_if(): + qasm = """OPENQASM 3; + include "stdgates.inc"; + gate custom a, b{ + cx a, b; + h a; + } + qubit[4] q; + bit[4] c; + bit[4] c0; + h q; + measure q -> c0; + if(c0[0]){ + x q[0]; + cx q[0], q[1]; + if (c0[1]){ + cx q[1], q[2]; + } + } + if (c[0]){ + custom q[2], q[3]; + } + + // this block will be removed and only statements + // will be present + array[int[32], 8] arr; + arr[0] = 1; + if(arr[0] >= 1){ + h q[0]; + h q[1]; + } + """ + expected_qasm = """OPENQASM 3; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + bit[4] c0; + h q[0]; + h q[1]; + h q[2]; + h q[3]; + c0[0] = measure q[0]; + c0[1] = measure q[1]; + c0[2] = measure q[2]; + c0[3] = measure q[3]; + if (c0[0] == true) { + x q[0]; + cx q[0], q[1]; + if (c0[1] == true) { + cx q[1], q[2]; + } + } + if (c[0] == true) { + cx q[2], q[3]; + h q[2]; + } + h q[0]; + h q[1]; + """ + result = unroll(qasm) + assert result.num_clbits == 8 + assert result.num_qubits == 4 + print(result.unrolled_qasm) + check_unrolled_qasm(result.unrolled_qasm, expected_qasm) + + +def test_incorrect_if(): + + with pytest.raises(ValidationError, match=r"Missing if block"): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c[0]){ + } + """ + ) + + with pytest.raises( + ValidationError, match=r"Semantic validation failed: Undefined identifier c2 in expression" + ): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c2[0]){ + cx q; + } + """ + ) + + with pytest.raises(ValidationError, match=r"Semantic validation failed: Only '!' supported .*"): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(~c[0]){ + cx q; + } + """ + ) + with pytest.raises( + ValidationError, match=r"Semantic validation failed: Only '==' supported .*" + ): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c[0] >= 1){ + cx q; + } + """ + ) + with pytest.raises( + ValidationError, + match=r"Semantic validation failed: Only simple comparison supported .*", + ): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c){ + cx q; + } + """ + ) + with pytest.raises( + ValidationError, + match=r"RangeDefinition not supported in branching condition", + ): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c[0:1]){ + cx q; + } + """ + ) + + with pytest.raises( + ValidationError, + match=r"DiscreteSet not supported in branching condition", + ): + validate( + """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + + h q; + measure q->c; + + if(c[{0,1}]){ + cx q; + } + """ + ) diff --git a/tests/test_loop.py b/tests/test_loop.py index b1c739b..a401833 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -60,6 +60,8 @@ def test_convert_qasm3_for_loop(): assert result.num_qubits == 4 assert result.num_clbits == 4 + print(result.unrolled_qasm) + check_single_qubit_gate_op(result.unrolled_ast, 4, [0, 1, 2, 3], "h") check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") diff --git a/tests/utils.py b/tests/utils.py index 981108e..6036c0e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -24,8 +24,9 @@ def check_unrolled_qasm(unrolled_qasm, expected_qasm): AssertionError: If the unrolled qasm does not match the expected qasm. """ # check line by line - unrolled_qasm = unrolled_qasm.split("\n") - expected_qasm = expected_qasm.split("\n") + unrolled_qasm = [line for line in unrolled_qasm.split("\n") if line.strip()] + expected_qasm = [line for line in expected_qasm.split("\n") if line.strip()] + assert len(unrolled_qasm) == len(expected_qasm) for unrolled_line, expected_line in zip(unrolled_qasm, expected_qasm): From e472f57903b45de2d2bc65376f083003fb1b5cf2 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Oct 2024 13:12:45 +0530 Subject: [PATCH 13/16] fix headers --- pyqasm/__init__.py | 6 +++--- pyqasm/analyzer.py | 6 +++--- pyqasm/elements.py | 6 +++--- pyqasm/exceptions.py | 6 +++--- pyqasm/expressions.py | 6 +++--- pyqasm/linalg.py | 6 +++--- pyqasm/maps.py | 6 +++--- pyqasm/subroutines.py | 6 +++--- pyqasm/transformer.py | 6 +++--- pyqasm/unroller.py | 7 +++---- pyqasm/validate.py | 6 +++--- pyqasm/validator.py | 6 +++--- pyqasm/visitor.py | 6 +++--- tests/conftest.py | 6 +++--- tests/declarations/test_classical.py | 7 +++---- tests/declarations/test_quantum.py | 7 ++++--- tests/resources/gates.py | 6 +++--- tests/resources/subroutines.py | 7 +++---- tests/resources/variables.py | 6 +++--- tests/subroutines/test_subroutines.py | 6 +++--- tests/subroutines/test_subroutines_arrays.py | 7 +++---- tests/test_alias.py | 6 +++--- tests/test_barrier.py | 6 +++--- tests/test_expressions.py | 7 ++++--- tests/test_gates.py | 7 ++++--- tests/test_if.py | 19 ++++++++----------- tests/test_loop.py | 7 ++++--- tests/test_measurement.py | 7 ++++--- tests/test_reset.py | 7 ++++--- tests/test_sizeof.py | 7 ++++--- tests/test_switch.py | 7 ++++--- tests/utils.py | 6 +++--- 32 files changed, 109 insertions(+), 108 deletions(-) diff --git a/pyqasm/__init__.py b/pyqasm/__init__.py index 4ec7544..87e9668 100644 --- a/pyqasm/__init__.py +++ b/pyqasm/__init__.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Top level module containing the main PyQASM functionality. diff --git a/pyqasm/analyzer.py b/pyqasm/analyzer.py index 1771fe7..421ddd8 100644 --- a/pyqasm/analyzer.py +++ b/pyqasm/analyzer.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=import-outside-toplevel,cyclic-import diff --git a/pyqasm/elements.py b/pyqasm/elements.py index 377efce..dab6f55 100644 --- a/pyqasm/elements.py +++ b/pyqasm/elements.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=too-many-arguments diff --git a/pyqasm/exceptions.py b/pyqasm/exceptions.py index eb6efcc..db6ccd2 100644 --- a/pyqasm/exceptions.py +++ b/pyqasm/exceptions.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module defining base PyQASM exceptions. diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py index d3c1aa9..858f21e 100644 --- a/pyqasm/expressions.py +++ b/pyqasm/expressions.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=too-many-locals diff --git a/pyqasm/linalg.py b/pyqasm/linalg.py index c0de18c..43e1b6f 100644 --- a/pyqasm/linalg.py +++ b/pyqasm/linalg.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=too-many-locals diff --git a/pyqasm/maps.py b/pyqasm/maps.py index 2d328ad..7c35bfa 100644 --- a/pyqasm/maps.py +++ b/pyqasm/maps.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the qBraid-SDK +# This file is part of pyqasm # -# The qBraid-SDK is free software released under the GNU General Public License v3 +# 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 the qBraid-SDK, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module mapping supported QASM gates/operations to lower level gate operations. diff --git a/pyqasm/subroutines.py b/pyqasm/subroutines.py index b8145b7..1f374d1 100644 --- a/pyqasm/subroutines.py +++ b/pyqasm/subroutines.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=too-many-arguments,too-many-locals,too-many-branches diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py index e489dc7..349040b 100644 --- a/pyqasm/transformer.py +++ b/pyqasm/transformer.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the 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 transformation functions for QASM3 visitor diff --git a/pyqasm/unroller.py b/pyqasm/unroller.py index 449dd63..6ccba62 100644 --- a/pyqasm/unroller.py +++ b/pyqasm/unroller.py @@ -1,13 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. - +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module containing OpenQASM to QIR conversion functions diff --git a/pyqasm/validate.py b/pyqasm/validate.py index b2e5fcf..9a46fa9 100644 --- a/pyqasm/validate.py +++ b/pyqasm/validate.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module for performing semantic analysis of OpenQASM 3 programs. diff --git a/pyqasm/validator.py b/pyqasm/validator.py index bf6a784..47582d8 100644 --- a/pyqasm/validator.py +++ b/pyqasm/validator.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the 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 utility functions for QASM3 visitor diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 7359f19..110d68d 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. # pylint: disable=too-many-instance-attributes,too-many-lines,too-many-branches diff --git a/tests/conftest.py b/tests/conftest.py index e298a43..e37c1bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """Module containing imports for unit test resources """ diff --git a/tests/declarations/test_classical.py b/tests/declarations/test_classical.py index 8c6f1f0..a8a7b6e 100644 --- a/tests/declarations/test_classical.py +++ b/tests/declarations/test_classical.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module containing unit tests for QASM3 to QIR conversion functions. @@ -121,7 +121,6 @@ def test_scalar_type_casts(): int[32] a = 5.1; float[32] r = 245; uint[4] b = -4; // -4 % 16 = 12 - bit bit_var = 0; bool f = 0; bool g = 1; diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py index cccbc33..4e94681 100644 --- a/tests/declarations/test_quantum.py +++ b/tests/declarations/test_quantum.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + import pytest from pyqasm.exceptions import ValidationError diff --git a/tests/resources/gates.py b/tests/resources/gates.py index cd29630..edc00af 100644 --- a/tests/resources/gates.py +++ b/tests/resources/gates.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module defining QASM3 gate tests. diff --git a/tests/resources/subroutines.py b/tests/resources/subroutines.py index a1c5a9d..f6c2738 100644 --- a/tests/resources/subroutines.py +++ b/tests/resources/subroutines.py @@ -1,13 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. - +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module defining subroutine tests. diff --git a/tests/resources/variables.py b/tests/resources/variables.py index f7a0bc0..972789b 100644 --- a/tests/resources/variables.py +++ b/tests/resources/variables.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module defining QASM3 incorrect variable tests. diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py index 8b4b235..33fb5db 100644 --- a/tests/subroutines/test_subroutines.py +++ b/tests/subroutines/test_subroutines.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. import pytest diff --git a/tests/subroutines/test_subroutines_arrays.py b/tests/subroutines/test_subroutines_arrays.py index cd24ca2..761cc70 100644 --- a/tests/subroutines/test_subroutines_arrays.py +++ b/tests/subroutines/test_subroutines_arrays.py @@ -1,13 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. - +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module containing unit tests for parsing, unrolling, and diff --git a/tests/test_alias.py b/tests/test_alias.py index f5de582..9bb8d1a 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module containing unit tests for unrolling OpenQASM 3 programs diff --git a/tests/test_barrier.py b/tests/test_barrier.py index dde5369..3af10ef 100644 --- a/tests/test_barrier.py +++ b/tests/test_barrier.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. """ Module containing unit tests for the barrier operation. diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 21c7ff0..02dc35f 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for expressions. diff --git a/tests/test_gates.py b/tests/test_gates.py index 4eb90a3..fc2da3b 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for unrolling quantum gates. diff --git a/tests/test_if.py b/tests/test_if.py index 5ce1429..0df2080 100644 --- a/tests/test_if.py +++ b/tests/test_if.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for the if statements. @@ -153,9 +154,7 @@ def test_incorrect_if(): """ ) - with pytest.raises( - ValidationError, match=r"Semantic validation failed: Undefined identifier c2 in expression" - ): + with pytest.raises(ValidationError, match=r"Undefined identifier c2 in expression"): validate( """ OPENQASM 3; @@ -172,7 +171,7 @@ def test_incorrect_if(): """ ) - with pytest.raises(ValidationError, match=r"Semantic validation failed: Only '!' supported .*"): + with pytest.raises(ValidationError, match=r"Only '!' supported .*"): validate( """ OPENQASM 3; @@ -188,9 +187,7 @@ def test_incorrect_if(): } """ ) - with pytest.raises( - ValidationError, match=r"Semantic validation failed: Only '==' supported .*" - ): + with pytest.raises(ValidationError, match=r"Only '==' supported .*"): validate( """ OPENQASM 3; @@ -208,7 +205,7 @@ def test_incorrect_if(): ) with pytest.raises( ValidationError, - match=r"Semantic validation failed: Only simple comparison supported .*", + match=r"Only simple comparison supported .*", ): validate( """ diff --git a/tests/test_loop.py b/tests/test_loop.py index a401833..5ecf6b3 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# 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 loops. diff --git a/tests/test_measurement.py b/tests/test_measurement.py index 7647b1c..d8509ea 100644 --- a/tests/test_measurement.py +++ b/tests/test_measurement.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for unrolling measurement operations. diff --git a/tests/test_reset.py b/tests/test_reset.py index d241f5c..8b9b5ae 100644 --- a/tests/test_reset.py +++ b/tests/test_reset.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for reset operation. diff --git a/tests/test_sizeof.py b/tests/test_sizeof.py index 6914842..ed58595 100644 --- a/tests/test_sizeof.py +++ b/tests/test_sizeof.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for sizeof operation. diff --git a/tests/test_switch.py b/tests/test_switch.py index 5da2bd1..e8dfe3f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,12 +1,13 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. + """ Module containing unit tests for switch statements. diff --git a/tests/utils.py b/tests/utils.py index 6036c0e..c4231be 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,12 +1,12 @@ # Copyright (C) 2024 qBraid # -# This file is part of the pyqasm +# This file is part of pyqasm # -# The pyqasm is free software released under the GNU General Public License v3 +# 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 the pyqasm, as per Section 15 of the GPL v3. +# THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. import openqasm3.ast as qasm3_ast From 63ce200d9ca6b7cf73750de16a5dee5d2fd35bce Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Oct 2024 14:30:09 +0530 Subject: [PATCH 14/16] complete decoupling --- pyqasm/analyzer.py | 8 +-- pyqasm/expressions.py | 43 ++++++++++----- pyqasm/maps.py | 8 +-- pyqasm/subroutines.py | 14 ++--- pyqasm/transformer.py | 16 +++--- pyqasm/visitor.py | 75 +++++++++++++++++---------- tests/declarations/test_classical.py | 1 - tests/subroutines/test_subroutines.py | 50 +++++++++++++++--- tests/test_loop.py | 2 +- 9 files changed, 144 insertions(+), 73 deletions(-) diff --git a/pyqasm/analyzer.py b/pyqasm/analyzer.py index 421ddd8..80be059 100644 --- a/pyqasm/analyzer.py +++ b/pyqasm/analyzer.py @@ -109,15 +109,15 @@ def _validate_step(start_id, end_id, step, span): start_id = 0 if index.start is not None: - start_id = expr_evaluator.evaluate_expression(index.start, reqd_type=IntType) + start_id = expr_evaluator.evaluate_expression(index.start, reqd_type=IntType)[0] end_id = var_dimensions[i] - 1 if index.end is not None: - end_id = expr_evaluator.evaluate_expression(index.end, reqd_type=IntType) + end_id = expr_evaluator.evaluate_expression(index.end, reqd_type=IntType)[0] step = 1 if index.step is not None: - step = expr_evaluator.evaluate_expression(index.step, reqd_type=IntType) + step = expr_evaluator.evaluate_expression(index.step, reqd_type=IntType)[0] _validate_index(start_id, var_dimensions[i], var.name, index.span, i) _validate_index(end_id, var_dimensions[i], var.name, index.span, i) @@ -126,7 +126,7 @@ def _validate_step(start_id, end_id, step, span): indices_list.append((start_id, end_id, step)) if isinstance(index, (Identifier, IntegerLiteral, Expression)): - index_value = expr_evaluator.evaluate_expression(index, reqd_type=IntType) + index_value = expr_evaluator.evaluate_expression(index, reqd_type=IntType)[0] curr_dimension = var_dimensions[i] # type: ignore[index] _validate_index(index_value, curr_dimension, var.name, index.span, i) diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py index 858f21e..b56f07d 100644 --- a/pyqasm/expressions.py +++ b/pyqasm/expressions.py @@ -158,7 +158,7 @@ def _get_var_value(cls, var_name, indices, expression): # pylint: disable-next=too-many-return-statements, too-many-branches def evaluate_expression( cls, expression, const_expr: bool = False, reqd_type=None, validate_only: bool = False - ): + ) -> tuple: """Evaluate an expression. Scalar types are assigned by value. Args: @@ -167,13 +167,14 @@ def evaluate_expression( reqd_type (Any): The required type of the expression. Defaults to None. Returns: - Any : The result of the evaluation. + tuple[Any, list[Statement]] : The result of the evaluation. Raises: ValidationError: If the expression is not supported. """ + statements = [] if expression is None: - return None + return None, [] if isinstance(expression, (ImaginaryLiteral, DurationLiteral)): raise_qasm3_error( @@ -184,8 +185,8 @@ def evaluate_expression( def _check_and_return_value(value): if validate_only: - return None - return value + return None, statements + return value, statements def _process_variable(var_name: str, indices=None): cls._check_var_in_scope(var_name, expression) @@ -240,7 +241,9 @@ def _process_variable(var_name: str, indices=None): # get the first dimension of the array return _check_and_return_value(dimensions[0]) - index = cls.evaluate_expression(index, const_expr, reqd_type=Qasm3IntType) + index, stmts = cls.evaluate_expression(index, const_expr, reqd_type=Qasm3IntType) + statements.extend(stmts) + assert index is not None and isinstance(index, int) if index < 0 or index >= len(dimensions): raise_qasm3_error( @@ -253,7 +256,6 @@ def _process_variable(var_name: str, indices=None): if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)): if reqd_type: - if reqd_type == BoolType and isinstance(expression, BooleanLiteral): return _check_and_return_value(expression.value) if reqd_type == Qasm3IntType and isinstance(expression, IntegerLiteral): @@ -269,7 +271,9 @@ def _process_variable(var_name: str, indices=None): return _check_and_return_value(expression.value) if isinstance(expression, UnaryExpression): - operand = cls.evaluate_expression(expression.expression, const_expr, reqd_type) + operand, returned_stats = cls.evaluate_expression( + expression.expression, const_expr, reqd_type + ) if expression.op.name == "~" and not isinstance(operand, int): raise_qasm3_error( f"Unsupported expression type {type(operand)} in ~ operation", @@ -277,19 +281,30 @@ def _process_variable(var_name: str, indices=None): expression.span, ) op_name = "UMINUS" if expression.op.name == "-" else expression.op.name + statements.extend(returned_stats) return _check_and_return_value(qasm3_expression_op_map(op_name, operand)) + if isinstance(expression, BinaryExpression): - lhs = cls.evaluate_expression(expression.lhs, const_expr, reqd_type) - rhs = cls.evaluate_expression(expression.rhs, const_expr, reqd_type) - return _check_and_return_value(qasm3_expression_op_map(expression.op.name, lhs, rhs)) + lhs_value, lhs_statements = cls.evaluate_expression( + expression.lhs, const_expr, reqd_type + ) + statements.extend(lhs_statements) + rhs_value, rhs_statements = cls.evaluate_expression( + expression.rhs, const_expr, reqd_type + ) + statements.extend(rhs_statements) + return _check_and_return_value( + qasm3_expression_op_map(expression.op.name, lhs_value, rhs_value) + ) if isinstance(expression, FunctionCall): # function will not return a reqd / const type # Reference : https://openqasm.com/language/types.html#compile-time-constants # para : 5 - return _check_and_return_value( - cls.visitor_obj._visit_function_call(expression) # type: ignore[union-attr] - ) + ret_value, ret_stmts = cls.visitor_obj._visit_function_call(expression) + statements.extend(ret_stmts) + return _check_and_return_value(ret_value) + raise_qasm3_error( f"Unsupported expression type {type(expression)}", ValidationError, expression.span ) diff --git a/pyqasm/maps.py b/pyqasm/maps.py index 7c35bfa..0e744d0 100644 --- a/pyqasm/maps.py +++ b/pyqasm/maps.py @@ -611,13 +611,13 @@ def two_qubit_gate_op( def map_qasm_op_to_callable(op_name: str): """ - Map a QASM operation to a PyQIR callable. + Map a QASM operation to a callable. Args: op_name (str): The QASM operation name. Returns: - tuple: A tuple containing the PyQIR callable and the number of qubits the operation acts on. + tuple: A tuple containing the callable and the number of qubits the operation acts on. """ try: return ONE_QUBIT_OP_MAP[op_name], 1 @@ -656,13 +656,13 @@ def map_qasm_op_to_callable(op_name: str): def map_qasm_inv_op_to_callable(op_name: str): """ - Map a QASM operation to a PyQIR callable. + Map a QASM operation to a callable. Args: op_name (str): The QASM operation name. Returns: - tuple: A tuple containing the PyQIR callable, the number of qubits the operation acts on, + tuple: A tuple containing the callable, the number of qubits the operation acts on, and what is to be done with the basic gate which we are trying to invert. """ if op_name in SELF_INVERTING_ONE_QUBIT_OP_SET: diff --git a/pyqasm/subroutines.py b/pyqasm/subroutines.py index 1f374d1..830f090 100644 --- a/pyqasm/subroutines.py +++ b/pyqasm/subroutines.py @@ -131,13 +131,13 @@ def _process_classical_arg_by_value( f" for function call '{fn_name}'", span=span, ) - actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg) + actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0] # save this value to be updated later in scope return Variable( name=formal_arg.name.name, base_type=formal_arg.type, - base_size=Qasm3ExprEvaluator.evaluate_expression(formal_arg.type.size), + base_size=Qasm3ExprEvaluator.evaluate_expression(formal_arg.type.size)[0], dims=None, value=actual_arg_value, is_constant=False, @@ -168,7 +168,7 @@ def _process_classical_arg_by_reference( formal_arg_base_size = Qasm3ExprEvaluator.evaluate_expression( formal_arg.type.base_type.size - ) + )[0] array_expected_type_msg = ( "Expecting type 'array[" f"{formal_arg.type.base_type.__class__.__name__.lower().removesuffix('type')}" @@ -179,7 +179,7 @@ def _process_classical_arg_by_reference( if actual_arg_name is None: raise_qasm3_error( array_expected_type_msg - + f"Literal {Qasm3ExprEvaluator.evaluate_expression(actual_arg)} " + + f"Literal {Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0]} " + "found in function call", span=span, ) @@ -230,7 +230,7 @@ def _process_classical_arg_by_reference( if not isinstance(formal_dimensions_raw, list): num_formal_dimensions = Qasm3ExprEvaluator.evaluate_expression( formal_dimensions_raw, reqd_type=IntType, const_expr=True - ) + )[0] # 2. or we will have a list of the dimensions in the formal arg else: num_formal_dimensions = len(formal_dimensions_raw) @@ -261,7 +261,7 @@ def _process_classical_arg_by_reference( ): formal_dim = Qasm3ExprEvaluator.evaluate_expression( formal_dim, reqd_type=IntType, const_expr=True - ) + )[0] if formal_dim <= 0: raise_qasm3_error( f"Invalid dimension size {formal_dim} for '{formal_arg.name.name}'" @@ -332,7 +332,7 @@ def process_quantum_arg( formal_reg_name = formal_arg.name.name formal_qubit_size = Qasm3ExprEvaluator.evaluate_expression( formal_arg.size, reqd_type=IntType, const_expr=True - ) + )[0] if formal_qubit_size is None: formal_qubit_size = 1 if formal_qubit_size <= 0: diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py index 349040b..7ceb950 100644 --- a/pyqasm/transformer.py +++ b/pyqasm/transformer.py @@ -115,15 +115,17 @@ def get_qubits_from_range_definition( start_qid = ( 0 if range_def.start is None - else Qasm3ExprEvaluator.evaluate_expression(range_def.start) + else Qasm3ExprEvaluator.evaluate_expression(range_def.start)[0] ) end_qid = ( qreg_size if range_def.end is None - else Qasm3ExprEvaluator.evaluate_expression(range_def.end) + else Qasm3ExprEvaluator.evaluate_expression(range_def.end)[0] ) step = ( - 1 if range_def.step is None else Qasm3ExprEvaluator.evaluate_expression(range_def.step) + 1 + if range_def.step is None + else Qasm3ExprEvaluator.evaluate_expression(range_def.step)[0] ) Qasm3Validator.validate_register_index(start_qid, qreg_size, qubit=is_qubit_reg) Qasm3Validator.validate_register_index(end_qid - 1, qreg_size, qubit=is_qubit_reg) @@ -256,13 +258,15 @@ def get_branch_params(condition: Any) -> tuple[int, str]: None, condition.lhs.name, # do not evaluate to bool - Qasm3ExprEvaluator.evaluate_expression(condition.rhs, reqd_type=Qasm3IntType), + Qasm3ExprEvaluator.evaluate_expression(condition.rhs, reqd_type=Qasm3IntType)[ + 0 + ], ) return ( condition.lhs.index[0].value, condition.lhs.collection.name, # evaluate to bool - Qasm3ExprEvaluator.evaluate_expression(condition.rhs) != 0, + Qasm3ExprEvaluator.evaluate_expression(condition.rhs)[0] != 0, ) if isinstance(condition, IndexExpression): if isinstance(condition.index, DiscreteSet): @@ -349,7 +353,7 @@ def get_target_qubits( ) target_qubits_size = len(target_qids) elif isinstance(target.index[0], (IntegerLiteral, Identifier)): # "(q[0]); OR (q[i]);" - target_qids = [Qasm3ExprEvaluator.evaluate_expression(target.index[0])] + target_qids = [Qasm3ExprEvaluator.evaluate_expression(target.index[0])[0]] Qasm3Validator.validate_register_index( target_qids[0], qreg_size_map[target_name], qubit=True ) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 110d68d..91b3630 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -358,7 +358,7 @@ def _get_op_bits( bit.indices[0][0], reg_size_map[reg_name], is_qubit_reg=qubits ) else: - bit_id = Qasm3ExprEvaluator.evaluate_expression(bit.indices[0][0]) + bit_id = Qasm3ExprEvaluator.evaluate_expression(bit.indices[0][0])[0] Qasm3Validator.validate_register_index( bit_id, reg_size_map[reg_name], qubit=qubits ) @@ -526,7 +526,7 @@ def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: """ param_list = [] for param in operation.arguments: - param_value = Qasm3ExprEvaluator.evaluate_expression(param) + param_value = Qasm3ExprEvaluator.evaluate_expression(param)[0] param_list.append(param_value) return param_list @@ -653,7 +653,7 @@ def _visit_custom_gate_operation( for formal_arg, actual_arg in zip(gate_definition.qubits, op_qubits) } param_map = { - formal_arg.name: Qasm3ExprEvaluator.evaluate_expression(actual_arg) + formal_arg.name: Qasm3ExprEvaluator.evaluate_expression(actual_arg)[0] for formal_arg, actual_arg in zip(gate_definition.arguments, operation.arguments) } @@ -710,7 +710,7 @@ def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple: for modifier in operation.modifiers: modifier_name = modifier.modifier if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: - current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument) + current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] if current_power < 0: inverse_value = not inverse_value power_value = power_value * abs(current_power) @@ -774,7 +774,7 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) Returns: None """ - + statements = [] var_name = statement.identifier.name if var_name in CONSTANTS_MAP: @@ -783,9 +783,10 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) ) if self._check_in_scope(var_name): raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span) - init_value = Qasm3ExprEvaluator.evaluate_expression( + init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( statement.init_expression, const_expr=True ) + statements.extend(stmts) base_type = statement.type if isinstance(base_type, qasm3_ast.BoolType): @@ -794,7 +795,9 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) if base_type.size is None: base_size = 32 # default for now else: - base_size = Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True) + base_size = Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[ + 0 + ] if not isinstance(base_size, int) or base_size <= 0: raise_qasm3_error( f"Invalid base size {base_size} for variable {var_name}", @@ -808,7 +811,7 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) self._add_var_in_scope(variable) - return [] + return statements # pylint: disable=too-many-branches def _visit_classical_declaration( @@ -822,6 +825,7 @@ def _visit_classical_declaration( Returns: None """ + statements = [] var_name = statement.identifier.name if var_name in CONSTANTS_MAP: raise_qasm3_error( @@ -854,7 +858,7 @@ def _visit_classical_declaration( base_type = base_type.base_type num_elements = 1 for dim in dimensions: - dim_value = Qasm3ExprEvaluator.evaluate_expression(dim) + dim_value = Qasm3ExprEvaluator.evaluate_expression(dim)[0] if not isinstance(dim_value, int) or dim_value <= 0: raise_qasm3_error( f"Invalid dimension size {dim_value} in array declaration for {var_name}", @@ -871,13 +875,16 @@ def _visit_classical_declaration( statement.init_expression, final_dimensions, base_type ) else: - init_value = Qasm3ExprEvaluator.evaluate_expression(statement.init_expression) + init_value, stmts = Qasm3ExprEvaluator.evaluate_expression( + statement.init_expression + ) + statements.extend(stmts) base_size = 1 if not isinstance(base_type, qasm3_ast.BoolType): base_size = ( 32 if not hasattr(base_type, "size") or base_type.size is None - else Qasm3ExprEvaluator.evaluate_expression(base_type.size) + else Qasm3ExprEvaluator.evaluate_expression(base_type.size)[0] ) if not isinstance(base_size, int) or base_size <= 0: @@ -916,9 +923,9 @@ def _visit_classical_declaration( if statement.type.size is None else qasm3_ast.IntegerLiteral(base_size) ) - return [statement] + statements.append(statement) - return [] + return statements def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) -> None: """Visit a classical assignment element. @@ -929,6 +936,7 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) Returns: None """ + statements = [] lvalue = statement.lvalue lvar_name = lvalue.name if isinstance(lvar_name, qasm3_ast.Identifier): @@ -952,9 +960,10 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) rvalue = statement.rvalue if binary_op is not None: rvalue = qasm3_ast.BinaryExpression(lhs=lvalue, op=binary_op, rhs=rvalue) - rvalue_raw = Qasm3ExprEvaluator.evaluate_expression( + rvalue_raw, rhs_stmts = Qasm3ExprEvaluator.evaluate_expression( rvalue ) # consists of scope check and index validation + statements.extend(rhs_stmts) # cast + validation rvalue_eval = None @@ -1000,7 +1009,7 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) lvar.value = rvalue_eval # type: ignore[union-attr] self._update_var_in_scope(lvar) # type: ignore[arg-type] - return [] + return statements def _evaluate_array_initialization( self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: Any @@ -1021,7 +1030,7 @@ def _evaluate_array_initialization( nested_array = self._evaluate_array_initialization(value, dimensions[1:], base_type) init_values.append(nested_array) else: - eval_value = Qasm3ExprEvaluator.evaluate_expression(value) + eval_value = Qasm3ExprEvaluator.evaluate_expression(value)[0] init_values.append(eval_value) return np.array(init_values, dtype=ARRAY_TYPE_MAP[base_type.__class__]) @@ -1092,7 +1101,7 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> else: # here we can unroll the block depending on the condition - positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition) != 0 + positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition)[0] != 0 block_to_visit = statement.if_block if positive_branching else statement.else_block result.extend(self.visit_basic_block(block_to_visit)) @@ -1111,19 +1120,19 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.St # Compute loop variable values if isinstance(statement.set_declaration, qasm3_ast.RangeDefinition): init_exp = statement.set_declaration.start - startval = Qasm3ExprEvaluator.evaluate_expression(init_exp) + startval = Qasm3ExprEvaluator.evaluate_expression(init_exp)[0] range_def = statement.set_declaration stepval = ( 1 if range_def.step is None - else Qasm3ExprEvaluator.evaluate_expression(range_def.step) + else Qasm3ExprEvaluator.evaluate_expression(range_def.step)[0] ) - endval = Qasm3ExprEvaluator.evaluate_expression(range_def.end) + endval = Qasm3ExprEvaluator.evaluate_expression(range_def.end)[0] irange = list(range(startval, endval + stepval, stepval)) elif isinstance(statement.set_declaration, qasm3_ast.DiscreteSet): init_exp = statement.set_declaration.values[0] irange = [ - Qasm3ExprEvaluator.evaluate_expression(exp) + Qasm3ExprEvaluator.evaluate_expression(exp)[0] for exp in statement.set_declaration.values ] else: @@ -1141,8 +1150,10 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.St # Initialize loop variable in loop scope # need to re-declare as we discard the block scope in subsequent # iterations of the loop - self._visit_classical_declaration( - qasm3_ast.ClassicalDeclaration(statement.type, statement.identifier, init_exp) + result.extend( + self._visit_classical_declaration( + qasm3_ast.ClassicalDeclaration(statement.type, statement.identifier, init_exp) + ) ) i = self._get_from_visible_scope(statement.identifier.name) @@ -1266,10 +1277,13 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: result.extend(self.visit_statement(copy.deepcopy(function_op))) if return_statement: - return_value = Qasm3ExprEvaluator.evaluate_expression(return_statement.expression) + return_value, stmts = Qasm3ExprEvaluator.evaluate_expression( + return_statement.expression + ) return_value = Qasm3Validator.validate_return_statement( subroutine_def, return_statement, return_value ) + result.extend(stmts) # remove qubit transformation map self._function_qreg_transform_map.pop() @@ -1280,7 +1294,7 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: self._curr_scope -= 1 self._pop_scope() - return return_value if subroutine_def.return_type is not None else result + return return_value, result def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: pass @@ -1403,7 +1417,7 @@ def _visit_switch_statement( f"Switch target {switch_target_name} must be of type int", span=statement.span ) - switch_target_val = Qasm3ExprEvaluator.evaluate_expression(switch_target) + switch_target_val = Qasm3ExprEvaluator.evaluate_expression(switch_target)[0] if len(statement.cases) == 0: raise_qasm3_error("Switch statement must have at least one case", span=statement.span) @@ -1438,7 +1452,7 @@ def _evaluate_case(statements): # literal OR type int case_val = Qasm3ExprEvaluator.evaluate_expression( case_expr, const_expr=True, reqd_type=qasm3_ast.IntType - ) + )[0] if case_val in seen_values: raise_qasm3_error( @@ -1492,7 +1506,12 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat visitor_function = visit_map.get(type(statement)) if visitor_function: - result.extend(visitor_function(statement)) # type: ignore[operator] + # these are special, they return a tuple of return value and list of statements + if isinstance(statement, qasm3_ast.ExpressionStatement): + _, ret_stmts = visitor_function(statement) + result.extend(ret_stmts) + else: + result.extend(visitor_function(statement)) # type: ignore[operator] else: raise_qasm3_error( f"Unsupported statement of type {type(statement)}", span=statement.span diff --git a/tests/declarations/test_classical.py b/tests/declarations/test_classical.py index a8a7b6e..a054868 100644 --- a/tests/declarations/test_classical.py +++ b/tests/declarations/test_classical.py @@ -286,7 +286,6 @@ def test_array_expressions(): b = 3 c = 4.5 d = 6.7 - f = True result = unroll(qasm3_string) assert result.num_clbits == 0 assert result.num_qubits == 1 diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py index 33fb5db..442ce11 100644 --- a/tests/subroutines/test_subroutines.py +++ b/tests/subroutines/test_subroutines.py @@ -8,6 +8,11 @@ # # 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 subroutines. + +""" + import pytest from pyqasm.exceptions import ValidationError @@ -107,24 +112,53 @@ def test_function_call_in_expression(): qasm_str = """OPENQASM 3; include "stdgates.inc"; - def my_function(qubit q) -> bool{ + def my_function(qubit[4] q) -> bool{ h q; return true; } - qubit q; - // do I only care about the return value? - // or we also need to add the gate to the unrolled_ast? - - // how to do this? + qubit[4] q; + + // this is to be explained bool b = my_function(q); """ result = unroll(qasm_str) assert result.num_clbits == 0 - assert result.num_qubits == 1 + assert result.num_qubits == 4 print(result.unrolled_qasm) - check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h") + check_single_qubit_gate_op(result.unrolled_ast, 4, list(range(4)), "h") + + +def test_classical_quantum_function(): + """Test that a function with classical and quantum instructions is correctly unrolled""" + qasm_str = """ + OPENQASM 3; + include "stdgates.inc"; + def my_function(qubit q, int[32] iter) -> int[32]{ + h q; + if(iter>2) + x q; + if (iter>3) + y q; + return iter + 1; + } + qubit[4] q; + int[32] new_var ; + for int i in [0:3] + { + new_var = my_function(q[i] , i); + } + + if(new_var>2) + h q[0]; + """ + result = unroll(qasm_str) + assert result.num_clbits == 0 + assert result.num_qubits == 4 + + check_single_qubit_gate_op(result.unrolled_ast, 5, list(range(4)) + [0], "h") + check_single_qubit_gate_op(result.unrolled_ast, 1, [3], "x") def test_multiple_function_calls(): diff --git a/tests/test_loop.py b/tests/test_loop.py index 5ecf6b3..e8ad0e2 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -300,7 +300,7 @@ def test_convert_qasm3_for_loop_unsupported_type(): " of set_declaration in loop." ), ): - _ = validate( + validate( """ OPENQASM 3.0; include "stdgates.inc"; From 55627d6aaca973906e984e2337646ea1a4d6b1b6 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Oct 2024 18:09:03 +0530 Subject: [PATCH 15/16] format check --- pyproject.toml | 2 +- pyqasm/expressions.py | 21 +++--- pyqasm/transformer.py | 4 +- pyqasm/visitor.py | 72 ++++++++++++-------- tests/conftest.py | 3 +- tests/declarations/test_classical.py | 4 +- tests/declarations/test_quantum.py | 5 ++ tests/subroutines/test_subroutines.py | 2 +- tests/subroutines/test_subroutines_arrays.py | 2 +- tests/test_gates.py | 2 +- tests/test_reset.py | 2 +- tests/utils.py | 6 +- tox.ini | 8 +-- 13 files changed, 77 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 342b7f2..213d37a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ source = "https://github.com/qBraid/pyqasm" tracker = "https://github.com/qBraid/pyqasm/issues" [project.optional-dependencies] -test = ["pytest", "pytest-cov", "autoqasm>=0.1.0"] +test = ["pytest", "pytest-cov"] lint = ["black", "isort", "pylint", "mypy", "qbraid-cli>=0.8.5"] docs = ["sphinx>=7.3.7,<8.1.0", "sphinx-autodoc-typehints>=1.24,<2.5", "sphinx-rtd-theme~=2.0.0", "docutils<0.22", "sphinx-copybutton"] diff --git a/pyqasm/expressions.py b/pyqasm/expressions.py index b56f07d..0b1fb7c 100644 --- a/pyqasm/expressions.py +++ b/pyqasm/expressions.py @@ -32,7 +32,7 @@ IntegerLiteral, ) from openqasm3.ast import IntType as Qasm3IntType -from openqasm3.ast import SizeOf, UnaryExpression +from openqasm3.ast import SizeOf, Statement, UnaryExpression from .analyzer import Qasm3Analyzer from .exceptions import ValidationError, raise_qasm3_error @@ -155,8 +155,8 @@ def _get_var_value(cls, var_name, indices, expression): return var_value @classmethod - # pylint: disable-next=too-many-return-statements, too-many-branches - def evaluate_expression( + # pylint: disable-next=too-many-return-statements, too-many-branches, too-many-statements + def evaluate_expression( # type: ignore[return] cls, expression, const_expr: bool = False, reqd_type=None, validate_only: bool = False ) -> tuple: """Evaluate an expression. Scalar types are assigned by value. @@ -172,7 +172,7 @@ def evaluate_expression( Raises: ValidationError: If the expression is not supported. """ - statements = [] + statements: list[Statement] = [] if expression is None: return None, [] @@ -213,7 +213,6 @@ def _process_variable(var_name: str, indices=None): return _process_variable(var_name, indices) if isinstance(expression, SizeOf): - # has 2 elements - target and index target = expression.target index = expression.index @@ -238,8 +237,7 @@ def _process_variable(var_name: str, indices=None): ) if index is None: - # get the first dimension of the array - return _check_and_return_value(dimensions[0]) + return _check_and_return_value(dimensions[0]) # return first dimension index, stmts = cls.evaluate_expression(index, const_expr, reqd_type=Qasm3IntType) statements.extend(stmts) @@ -299,9 +297,8 @@ def _process_variable(var_name: str, indices=None): if isinstance(expression, FunctionCall): # function will not return a reqd / const type - # Reference : https://openqasm.com/language/types.html#compile-time-constants - # para : 5 - ret_value, ret_stmts = cls.visitor_obj._visit_function_call(expression) + # Reference : https://openqasm.com/language/types.html#compile-time-constants, para: 5 + ret_value, ret_stmts = cls.visitor_obj._visit_function_call(expression) # type: ignore statements.extend(ret_stmts) return _check_and_return_value(ret_value) @@ -321,10 +318,10 @@ def classical_register_in_expr(cls, expr: Expression) -> bool: bool: True if a classical register is present, False otherwise """ if isinstance(expr, Identifier): - return expr.name in cls.visitor_obj._global_creg_size_map + return expr.name in cls.visitor_obj._global_creg_size_map # type: ignore if isinstance(expr, IndexExpression): var_name, _ = Qasm3Analyzer.analyze_index_expression(expr) - return var_name in cls.visitor_obj._global_creg_size_map + return var_name in cls.visitor_obj._global_creg_size_map # type: ignore if isinstance(expr, BinaryExpression): return cls.classical_register_in_expr(expr.lhs) or cls.classical_register_in_expr( expr.rhs diff --git a/pyqasm/transformer.py b/pyqasm/transformer.py index 7ceb950..2409597 100644 --- a/pyqasm/transformer.py +++ b/pyqasm/transformer.py @@ -12,7 +12,7 @@ Module with transformation functions for QASM3 visitor """ -from typing import Any, Union +from typing import Any, Optional, Union import numpy as np from openqasm3.ast import ( @@ -217,7 +217,7 @@ def transform_gate_params( gate_op.arguments[i] = Qasm3Transformer.transform_expression(actual_arg, param_map) @staticmethod - def get_branch_params(condition: Any) -> tuple[int, str]: + def get_branch_params(condition: Any) -> tuple[Optional[int], str, Optional[bool]]: """ Get the branch parameters from the branching condition diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 91b3630..c81ba88 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -243,7 +243,9 @@ def _in_gate_scope(self) -> bool: def _in_block_scope(self) -> bool: # block scope is for if/else/for/while constructs return len(self._scope) > 1 and self._get_curr_context() == Context.BLOCK - def _visit_qubit_register(self, register: qasm3_ast.QubitDeclaration) -> None: + def _visit_qubit_register( + self, register: qasm3_ast.QubitDeclaration + ) -> list[qasm3_ast.QubitDeclaration]: """Visit a Qubit / Classical declaration statement. Args: @@ -256,7 +258,7 @@ def _visit_qubit_register(self, register: qasm3_ast.QubitDeclaration) -> None: current_size = len(self._qubit_labels) register_size = ( - 1 if register.size is None else register.size.value # type: ignore[union-attr] + 1 if register.size is None else register.size.value # type: ignore[attr-defined] ) if register.size is None: register.size = qasm3_ast.IntegerLiteral(register_size) @@ -307,6 +309,7 @@ def _check_if_name_in_scope(self, name: str, operation: Any) -> None: f"Variable {name} not in scope for operation {operation}", span=operation.span ) + # pylint: disable=too-many-locals def _get_op_bits( self, operation: Any, reg_size_map: dict, qubits: bool = True ) -> list[qasm3_ast.IndexedIdentifier]: @@ -452,7 +455,6 @@ def _visit_measurement( unrolled_measure = qasm3_ast.QuantumMeasurementStatement( measure=qasm3_ast.QuantumMeasurement(qubit=src_id), target=tgt_id ) - # self._module.add_qasm_statement(unrolled_measure) unrolled_measurements.append(unrolled_measure) return unrolled_measurements @@ -507,13 +509,14 @@ def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> list[qasm3_ast.Qu ) ) barrier_qubits = self._get_op_bits(barrier, self._global_qreg_size_map) + unrolled_barriers = [] if not self._check_only: - unrolled_barriers = [] for qubit in barrier_qubits: - unrolled_barrier = qasm3_ast.QuantumBarrier(qubits=[qubit]) - # self._module.add_qasm_statement(unrolled_barrier) + unrolled_barrier = qasm3_ast.QuantumBarrier( + qubits=[qubit] # type: ignore[list-item] + ) unrolled_barriers.append(unrolled_barrier) - return unrolled_barriers + return unrolled_barriers def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: """Get the parameters for the operation. @@ -531,7 +534,7 @@ def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: return param_list - def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> None: + def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> list[None]: """Visit a gate definition element. Args: @@ -549,7 +552,7 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> def _visit_basic_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False - ) -> None: + ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. Args: @@ -607,7 +610,6 @@ def _visit_basic_gate_operation( unrolled_gate = qasm_func(*qubit_subset) unrolled_gates = [] for gate in unrolled_gate: - # self._module.add_qasm_statement(gate) unrolled_gates.append(gate) result.extend(unrolled_gates) @@ -617,7 +619,7 @@ def _visit_basic_gate_operation( def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False - ) -> None: + ) -> list[qasm3_ast.QuantumGate]: """Visit a custom gate operation element recursively. Args: @@ -727,7 +729,9 @@ def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple: ) return (power_value, inverse_value) - def _visit_generic_gate_operation(self, operation: qasm3_ast.QuantumGate) -> None: + def _visit_generic_gate_operation( + self, operation: qasm3_ast.QuantumGate + ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. Args: @@ -763,7 +767,9 @@ def _visit_generic_gate_operation(self, operation: qasm3_ast.QuantumGate) -> Non return result - def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) -> None: + def _visit_constant_declaration( + self, statement: qasm3_ast.ConstantDeclaration + ) -> list[qasm3_ast.Statement]: """ Visit a constant declaration element. Const can only be declared for scalar type variables and not arrays. Assignment is mandatory in constant declaration. @@ -813,10 +819,10 @@ def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) return statements - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches, too-many-statements, too-many-locals def _visit_classical_declaration( self, statement: qasm3_ast.ClassicalDeclaration - ) -> Optional[qasm3_ast.Statement]: + ) -> list[qasm3_ast.Statement]: """Visit a classical operation element. Args: @@ -927,14 +933,16 @@ def _visit_classical_declaration( return statements - def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) -> None: + def _visit_classical_assignment( + self, statement: qasm3_ast.ClassicalAssignment + ) -> list[qasm3_ast.Statement]: """Visit a classical assignment element. Args: statement (qasm3_ast.ClassicalAssignment): The classical assignment to visit. Returns: - None + list[qasm3_ast.Statement]: The list of statements generated by the assignment. """ statements = [] lvalue = statement.lvalue @@ -949,7 +957,7 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) raise_qasm3_error( f"Assignment to constant variable {lvar_name} not allowed", span=statement.span ) - binary_op = None + binary_op: Union[str, None, qasm3_ast.BinaryOperator] = None if statement.op != qasm3_ast.AssignmentOperator["="]: # eg. j += 1 -> broken down to j = j + 1 binary_op = statement.op.name.removesuffix("=") @@ -959,7 +967,9 @@ def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) # if rvalue is a list, we want a copy of it rvalue = statement.rvalue if binary_op is not None: - rvalue = qasm3_ast.BinaryExpression(lhs=lvalue, op=binary_op, rhs=rvalue) + rvalue = qasm3_ast.BinaryExpression( + lhs=lvalue, op=binary_op, rhs=rvalue # type: ignore[arg-type] + ) rvalue_raw, rhs_stmts = Qasm3ExprEvaluator.evaluate_expression( rvalue ) # consists of scope check and index validation @@ -1035,7 +1045,9 @@ def _evaluate_array_initialization( return np.array(init_values, dtype=ARRAY_TYPE_MAP[base_type.__class__]) - def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> None: + def _visit_branching_statement( + self, statement: qasm3_ast.BranchingStatement + ) -> list[qasm3_ast.Statement]: """Visit a branching statement element. Args: @@ -1081,7 +1093,7 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> if reg_id is not None else qasm3_ast.Identifier(name=reg_name) ) - + assert isinstance(rhs_value, (bool, int)) new_rhs = ( qasm3_ast.BooleanLiteral(rhs_value) if isinstance(rhs_value, bool) @@ -1104,7 +1116,7 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition)[0] != 0 block_to_visit = statement.if_block if positive_branching else statement.else_block - result.extend(self.visit_basic_block(block_to_visit)) + result.extend(self.visit_basic_block(block_to_visit)) # type: ignore[arg-type] del self._label_scope_level[self._curr_scope] self._curr_scope -= 1 @@ -1114,7 +1126,7 @@ def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> if self._check_only: return [] - return result + return result # type: ignore[return-value] def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.Statement]: # Compute loop variable values @@ -1174,7 +1186,7 @@ def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> list[qasm3_ast.St return [] return result - def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition) -> None: + def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition) -> list[None]: """Visit a subroutine definition element. Reference: https://openqasm.com/language/subroutines.html#subroutines @@ -1206,7 +1218,9 @@ def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition return [] # pylint: disable=too-many-locals, too-many-statements - def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: + def _visit_function_call( + self, statement: qasm3_ast.FunctionCall + ) -> tuple[Any, list[qasm3_ast.Statement]]: """Visit a function call element. Args: @@ -1299,7 +1313,7 @@ def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None: def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None: pass - def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: + def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> list[None]: """Visit an alias statement element. Args: @@ -1390,7 +1404,7 @@ def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None: return [] - def _visit_switch_statement( + def _visit_switch_statement( # type: ignore[return] self, statement: qasm3_ast.SwitchStatement ) -> list[qasm3_ast.Statement]: """Visit a switch statement element. @@ -1399,7 +1413,7 @@ def _visit_switch_statement( statement (qasm3_ast.SwitchStatement): The switch statement to visit. Returns: - None + list[qasm3_ast.Statement]: The list of statements generated by the switch statement. """ # 1. analyze the target - it should ONLY be int, not casted switch_target = statement.target @@ -1508,7 +1522,7 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat if visitor_function: # these are special, they return a tuple of return value and list of statements if isinstance(statement, qasm3_ast.ExpressionStatement): - _, ret_stmts = visitor_function(statement) + _, ret_stmts = visitor_function(statement) # type: ignore[operator] result.extend(ret_stmts) else: result.extend(visitor_function(statement)) # type: ignore[operator] diff --git a/tests/conftest.py b/tests/conftest.py index e37c1bb..e0d3c21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,5 +11,6 @@ """Module containing imports for unit test resources """ +# pylint: disable=wildcard-import, unused-wildcard-import + from .resources.gates import * -from .resources.subroutines import * diff --git a/tests/declarations/test_classical.py b/tests/declarations/test_classical.py index a054868..7b0de56 100644 --- a/tests/declarations/test_classical.py +++ b/tests/declarations/test_classical.py @@ -357,11 +357,11 @@ def test_array_range_assignment(): def test_incorrect_declarations(test_name): qasm_input, error_message = DECLARATION_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) @pytest.mark.parametrize("test_name", ASSIGNMENT_TESTS.keys()) def test_incorrect_assignments(test_name): qasm_input, error_message = ASSIGNMENT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py index 4e94681..2d9e999 100644 --- a/tests/declarations/test_quantum.py +++ b/tests/declarations/test_quantum.py @@ -8,6 +8,11 @@ # # 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 +declarations. + +""" import pytest from pyqasm.exceptions import ValidationError diff --git a/tests/subroutines/test_subroutines.py b/tests/subroutines/test_subroutines.py index 442ce11..6f44e15 100644 --- a/tests/subroutines/test_subroutines.py +++ b/tests/subroutines/test_subroutines.py @@ -361,4 +361,4 @@ def my_function(qubit[3] q) { def test_incorrect_custom_ops(test_name): qasm_input, error_message = SUBROUTINE_INCORRECT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) diff --git a/tests/subroutines/test_subroutines_arrays.py b/tests/subroutines/test_subroutines_arrays.py index 761cc70..713b7bd 100644 --- a/tests/subroutines/test_subroutines_arrays.py +++ b/tests/subroutines/test_subroutines_arrays.py @@ -154,4 +154,4 @@ def my_function(readonly array[int[8], 2, 2] my_arr1, def test_incorrect_custom_ops_with_arrays(test_name): qasm_input, error_message = SUBROUTINE_INCORRECT_TESTS_WITH_ARRAYS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) diff --git a/tests/test_gates.py b/tests/test_gates.py index fc2da3b..2753214 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -150,7 +150,7 @@ def test_qasm_u2_gates(): def test_incorrect_single_qubit_gates(test_name): qasm_input, error_message = SINGLE_QUBIT_GATE_INCORRECT_TESTS[test_name] with pytest.raises(ValidationError, match=error_message): - _ = validate(qasm_input) + validate(qasm_input) @pytest.mark.parametrize("test_name", custom_op_tests) diff --git a/tests/test_reset.py b/tests/test_reset.py index 8b9b5ae..a545abe 100644 --- a/tests/test_reset.py +++ b/tests/test_reset.py @@ -68,7 +68,7 @@ def my_function(qubit a) { my_function(q[1]); """ - expected_qasm = """OPENQASM 3; + expected_qasm = """OPENQASM 3; include "stdgates.inc"; qubit[3] q; reset q[1]; diff --git a/tests/utils.py b/tests/utils.py index c4231be..f33663d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,6 +8,10 @@ # # THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3. +""" +Module containing utility functions for unit tests. + +""" import openqasm3.ast as qasm3_ast from pyqasm.maps import CONSTANTS_MAP @@ -102,7 +106,7 @@ def check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_li if gate_name == "u3": check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list) return - elif gate_name == "u2": + if gate_name == "u2": param_list = [CONSTANTS_MAP["pi"] / 2, param_list[0], param_list[1]] check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list) return diff --git a/tox.ini b/tox.ini index a8948c7..8f9cf1a 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ basepython = python3 description = Run pytests and generate coverage report. allowlist_externals = qbraid commands = - pytest tests --cov=qbraid_qir --cov-config=pyproject.toml --cov-report=term --cov-report=xml {posargs} + pytest tests --cov=pyqasm --cov-config=pyproject.toml --cov-report=term --cov-report=xml {posargs} [testenv:docs] description = Use sphinx to build the HTML docs. @@ -57,7 +57,7 @@ envdir = .tox/linters skip_install = true deps = qbraid-cli>=0.8.2 commands = - qbraid admin headers tests pyqasm --type=gpl {posargs} + qbraid admin headers tests pyqasm --type=gpl -p pyqasm {posargs} [testenv:linters] allowlist_externals = qbraid @@ -81,10 +81,10 @@ deps = {[testenv:isort]deps} {[testenv:black]deps} {[testenv:mypy]deps} - {[testenv:headers]deps} + ; {[testenv:headers]deps} commands = {[testenv:pylint]commands} {[testenv:isort]commands} {posargs:--check-only} {[testenv:black]commands} {posargs:--check} {[testenv:mypy]commands} - {[testenv:headers]commands} \ No newline at end of file + ; {[testenv:headers]commands} \ No newline at end of file From 0ea1838856007a50dfb8cf59115be36de0189b01 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Fri, 11 Oct 2024 18:12:28 +0530 Subject: [PATCH 16/16] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7d4f3..63f39ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,4 +30,5 @@ Types of changes: ### Added - Setup basic project/repo template ([#1](https://github.com/qBraid/pyqasm/pull/1)) -- Configured PyPI workflows ([#2](https://github.com/qBraid/pyqasm/pull/2)) \ No newline at end of file +- Configured PyPI workflows ([#2](https://github.com/qBraid/pyqasm/pull/2)) +- Add initial implementation ([#3](https://github.com/qBraid/pyqasm/pull/3)) \ No newline at end of file