Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial implementation #3

Merged
merged 16 commits into from
Oct 11, 2024
24 changes: 24 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = [
Expand Down
32 changes: 32 additions & 0 deletions pyqasm/README.md
Original file line number Diff line number Diff line change
@@ -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 |
9 changes: 6 additions & 3 deletions pyqasm/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# 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.
Expand All @@ -20,6 +20,7 @@
:toctree: ../stubs/

validate
unroll

Exceptions
-----------
Expand All @@ -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__",
]
17 changes: 17 additions & 0 deletions pyqasm/_version.py
Original file line number Diff line number Diff line change
@@ -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")
233 changes: 233 additions & 0 deletions pyqasm/analyzer.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# 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
Loading
Loading