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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 2 additions & 151 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,172 +15,23 @@ Types of changes:
## Unreleased

### Added
- Added support for classical declarations with measurement ([#120](https://github.com/qBraid/pyqasm/pull/120)). Usage example -

```python
In [1]: from pyqasm import loads, dumps

In [2]: module = loads(
...: """OPENQASM 3.0;
...: qubit q;
...: bit b = measure q;
...: """)

In [3]: module.unroll()

In [4]: dumps(module).splitlines()
Out[4]: ['OPENQASM 3.0;', 'qubit[1] q;', 'bit[1] b;', 'b[0] = measure q[0];']
```

- Added support for `gphase`, `toffoli`, `not`, `c3sx` and `c4x` gates ([#86](https://github.com/qBraid/pyqasm/pull/86))
- Added a `remove_includes` method to `QasmModule` to remove include statements from the generated QASM code ([#100](https://github.com/qBraid/pyqasm/pull/100)). Usage example -

```python
In [1]: from pyqasm import loads

In [2]: module = loads(
...: """OPENQASM 3.0;
...: include "stdgates.inc";
...: include "random.qasm";
...:
...: qubit[2] q;
...: h q;
...: """)

In [3]: module.remove_includes()
Out[3]: <pyqasm.modules.qasm3.Qasm3Module at 0x10442b190>

In [4]: from pyqasm import dumps

In [5]: dumps(module).splitlines()
Out[5]: ['OPENQASM 3.0;', 'qubit[2] q;', 'h q;']
```
- Added support for unrolling multi-bit branching with `==`, `>=`, `<=`, `>`, and `<` ([#112](https://github.com/qBraid/pyqasm/pull/112)). Usage example -
```python
In [1]: from pyqasm import loads

In [2]: module = loads(
...: """OPENQASM 3.0;
...: include "stdgates.inc";
...: qubit[1] q;
...: bit[4] c;
...: if(c == 3){
...: h q[0];
...: }
...: """)

In [3]: module.unroll()

In [4]: dumps(module)
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[4] c;
if (c[0] == false) {
if (c[1] == false) {
if (c[2] == true) {
if (c[3] == true) {
h q[0];
}
}
}
}
```
- Add formatting check for Unix style line endings i.e. `\n`. For any other line endings, errors are raised. ([#130](https://github.com/qBraid/pyqasm/pull/130))
- Add `rebase` method to the `QasmModule`. Users now have the ability to rebase the quantum programs to any of the available `pyqasm.elements.BasisSet` ([#123](https://github.com/qBraid/pyqasm/pull/123)). Usage example -

```python
In [9] : import pyqasm

In [10]: qasm_input = """ OPENQASM 3.0;
...: include "stdgates.inc";
...: qubit[2] q;
...: bit[2] c;
...:
...: h q;
...: x q;
...: cz q[0], q[1];
...:
...: c = measure q;
...: """

In [11]: module = pyqasm.loads(qasm_input)

In [12]: from pyqasm.elements import BasisSet

In [13]: module.rebase(target_basis_set=BasisSet.ROTATIONAL_CX)
Out[13]: <pyqasm.modules.qasm3.Qasm3Module at 0x103744e10>

In [14]: print(pyqasm.dumps(module))
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
ry(1.5707963267948966) q[0];
rx(3.141592653589793) q[0];
ry(1.5707963267948966) q[1];
rx(3.141592653589793) q[1];
rx(3.141592653589793) q[0];
rx(3.141592653589793) q[1];
ry(1.5707963267948966) q[1];
rx(3.141592653589793) q[1];
cx q[0], q[1];
ry(1.5707963267948966) q[1];
rx(3.141592653589793) q[1];
c[0] = measure q[0];
c[1] = measure q[1];
```

Current support for `BasisSet.CLIFFORD_T` decompositions is limited to non-parameterized gates only.
- Added `.gitattributes` file to specify unix-style line endings(`\n`) for all files ([#123](https://github.com/qBraid/pyqasm/pull/123))
- Added support for `ctrl` modifiers. QASM3 programs with `ctrl @` modifiers can now be loaded as `QasmModule` objects ([#121](https://github.com/qBraid/pyqasm/pull/121)). Usage example -

```python
In [18]: import pyqasm

In [19]: qasm3_string = """
...: OPENQASM 3.0;
...: include "stdgates.inc";
...: qubit[3] q;
...: gate custom a, b, c {
...: ctrl @ x a, b;
...: ctrl(2) @ x a, b, c;
...: }
...: custom q[0], q[1], q[2];
...: """

In [20]: module = pyqasm.loads(qasm3_string)

In [21]: module.unroll()

In [22]: print(pyqasm.dumps(module))
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
cx q[0], q[1];
ccx q[0], q[1], q[2];
```

### Improved / Modified
- Refactored the initialization of `QasmModule` to remove default include statements. Only user supplied include statements are now added to the generated QASM code ([#86](https://github.com/qBraid/pyqasm/pull/86))
- Update the `pre-release.yml` workflow to multi-platform builds. Added the pre-release version bump to the `pre_build.sh` script. ([#99](https://github.com/qBraid/pyqasm/pull/99))
- Bumped qBraid-CLI dep in `tox.ini` to fix `qbraid headers` command formatting bug ([#129](https://github.com/qBraid/pyqasm/pull/129))
- Re-wrote the `QasmAnalyzer.extract_qasm_version` method so that it extracts the program version just by looking at the [first non-comment line](https://github.com/openqasm/openqasm/blob/bb923eb9a84fdffe1ba6fc3c20d0b47a131523d9/source/language/comments.rst#version-string), instead of parsing the entire program ([#140](https://github.com/qBraid/pyqasm/pull/140)).

### Deprecated

### Removed
- Unix-style line endings check in GitHub actions was removed in lieu of the `.gitattributes` file ([#123](https://github.com/qBraid/pyqasm/pull/123))

### Fixed
- Fixed bugs in implementations of `gpi2` and `prx` gates ([#86](https://github.com/qBraid/pyqasm/pull/86))

### Dependencies
- Update sphinx-autodoc-typehints requirement from <2.6,>=1.24 to >=1.24,<3.1 ([#119](https://github.com/qBraid/pyqasm/pull/119))

## Past Release Notes

Archive of changelog entries from previous releases:

- [v0.2.0](https://github.com/qBraid/pyqasm/releases/tag/v0.2.0)
- [v0.1.0](https://github.com/qBraid/pyqasm/releases/tag/v0.1.0)
- [v0.1.0-alpha](https://github.com/qBraid/pyqasm/releases/tag/v0.1.0-alpha)
- [v0.0.3](https://github.com/qBraid/pyqasm/releases/tag/v0.0.3)
Expand Down
41 changes: 21 additions & 20 deletions src/pyqasm/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"""
from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any, Optional, Union

import numpy as np
from openqasm3 import parse
from openqasm3.ast import (
DiscreteSet,
Expression,
Expand All @@ -28,7 +28,6 @@
QuantumMeasurementStatement,
RangeDefinition,
)
from openqasm3.parser import QASM3ParsingError

from pyqasm.exceptions import QasmParsingError, ValidationError, raise_qasm3_error

Expand Down Expand Up @@ -209,27 +208,29 @@ def get_op_bit_list(operation):
)
return bit_list

@staticmethod
def extract_qasm_version(qasm: str) -> int: # type: ignore # pylint: disable=R1710
@staticmethod # pylint: disable-next=inconsistent-return-statements
def extract_qasm_version(qasm: str) -> float: # type: ignore[return]
"""
Parses an OpenQASM program string to determine its major version, either 2 or 3.
Extracts the OpenQASM version from a given OpenQASM string.

Args:
qasm (str): The OpenQASM program string.
qasm (str): The OpenQASM program as a string.

Returns:
int: The OpenQASM version as an integer.

Raises:
QasmError: If the string does not represent a valid OpenQASM program.
The semantic version as a float.
"""
try:
# TODO: optimize this to just check the start of the program for version
parsed_program = parse(qasm)
assert parsed_program.version is not None
version = int(float(parsed_program.version))
return version
except (QASM3ParsingError, ValueError, TypeError):
raise_qasm3_error(
"Could not determine the OpenQASM version.", err_type=QasmParsingError
)
qasm = re.sub(r"//.*", "", qasm)
qasm = re.sub(r"/\*.*?\*/", "", qasm, flags=re.DOTALL)

lines = qasm.strip().splitlines()

for line in lines:
line = line.strip()
if line.startswith("OPENQASM"):
match = re.match(r"OPENQASM\s+(\d+)(?:\.(\d+))?;", line)
if match:
major = int(match.group(1))
minor = int(match.group(2)) if match.group(2) else 0
return float(f"{major}.{minor}")

raise_qasm3_error("Could not determine the OpenQASM version.", err_type=QasmParsingError)
77 changes: 62 additions & 15 deletions tests/qasm3/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,68 @@
from pyqasm.exceptions import QasmParsingError


def test_extract_version():
"""Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement."""
@pytest.mark.parametrize(
"qasm_input, expected_version",
[
(
"""
OPENQASM 3.0;
qubit q;
""",
3.0,
),
(
"""
OPENQASM 2;
qubit q;
""",
2.0,
),
(
"""
// Single-line comment
OPENQASM 1.5; // Inline comment
qubit q;
""",
1.5,
),
(
"""
/*
Block comment before the version string
describing the program.
*/
OPENQASM 3.2;
qubit q;
""",
3.2,
),
],
)
def test_extract_qasm_version_valid(qasm_input, expected_version):
"""Test valid OpenQASM version extraction with various inputs."""
assert Qasm3Analyzer.extract_qasm_version(qasm_input) == expected_version

qasm3_program = """
OPENQASM 3.0;
qubit q;
"""
assert Qasm3Analyzer.extract_qasm_version(qasm3_program) == 3


def test_invalid_raises_raises_err():
"""Test converting OpenQASM 3 program with openqasm3.ast.SwitchStatement."""

qasm3_program = """
random string
"""
@pytest.mark.parametrize(
"qasm_input",
[
"""
random string
""",
"""
OPENQASM three.point.zero;
qubit q;
""",
"""
OPENQASM 2.0
qubit q;
""",
"",
" \n \t ",
],
)
def test_extract_qasm_version_invalid(qasm_input):
"""Test invalid OpenQASM inputs that should raise QasmParsingError."""
with pytest.raises(QasmParsingError):
Qasm3Analyzer.extract_qasm_version(qasm3_program)
Qasm3Analyzer.extract_qasm_version(qasm_input)