From c2aaf7feb5637cf2a44d294777d97210fbb3a12c Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Thu, 11 Sep 2025 13:45:52 -0700 Subject: [PATCH 1/6] fixing issue 217 --- examples/unroll_example.py | 2 +- src/pyqasm/modules/base.py | 72 +++++++++++++++++++++ src/pyqasm/modules/qasm2.py | 73 ++++++++++++++++++++- src/pyqasm/modules/qasm3.py | 69 +++++++++++++++++++- tests/qasm3/test_merge.py | 122 ++++++++++++++++++++++++++++++++++++ 5 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 tests/qasm3/test_merge.py diff --git a/examples/unroll_example.py b/examples/unroll_example.py index 8d7f375b..b7ae33ad 100755 --- a/examples/unroll_example.py +++ b/examples/unroll_example.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=invalid-name +# pylint: disable=invalid-name, cyclic-import """ Script demonstrating how to unroll a QASM 3 program using pyqasm. diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 3b21330a..785fd708 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -19,6 +19,7 @@ from __future__ import annotations import functools +import re from abc import ABC, abstractmethod from collections import Counter from copy import deepcopy @@ -761,3 +762,74 @@ def accept(self, visitor): Args: visitor (QasmVisitor): The visitor to accept """ + + @abstractmethod + def merge( + self, + other: "QasmModule", + device_qubits: Optional[int] = None, + ) -> "QasmModule": + + """Merge this module with another module. + + Implemented by concrete subclasses to avoid version mixing and + import-time cycles. Implementations should ensure both operands + are normalized to the same version prior to merging. + """ + + + def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): + """Offset qubit indices for a given statement in-place by ``offset``. + Handles gates, measurements, resets, and barriers (including slice forms). + """ + if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): + bit = stmt.measure.qubit + if isinstance(bit, qasm3_ast.IndexedIdentifier): + for group in bit.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumGate): + for q in stmt.qubits: + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumReset): + q = stmt.qubits + if isinstance(q, qasm3_ast.IndexedIdentifier): + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumBarrier): + qubits = stmt.qubits + if len(qubits) == 0: + return + first = qubits[0] + if isinstance(first, qasm3_ast.IndexedIdentifier): + for group in first.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + elif isinstance(first, qasm3_ast.Identifier): + # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] + name = first.name + if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): + slice_str = name[len("__PYQASM_QUBITS__"):] + # Parse slice forms [S:E], [:E], or [S:] + m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) + if m: + start_s, end_s = m.group(1), m.group(2) + if start_s is None and end_s is not None: + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[:{end_v}]" + elif start_s is not None and end_s is None: + start_v = int(start_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:]" + elif start_s is not None and end_s is not None: + start_v = int(start_s) + offset + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" \ No newline at end of file diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index f4b0de9d..79b838ce 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -26,7 +26,7 @@ from pyqasm.exceptions import ValidationError from pyqasm.modules.base import QasmModule from pyqasm.modules.qasm3 import Qasm3Module - +from pyqasm.modules.base import offset_statement_qubits class Qasm2Module(QasmModule): """ @@ -108,3 +108,74 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self.unrolled_ast.statements = final_stmt_list + + def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModule: + """Merge two modules and return a QASM2 result without mixing versions. + - If ``other`` is QASM3, it is merged into this module's semantics, and + any standard gate includes are mapped to ``qelib1.inc``. + - The merged program keeps version "2.0" and prints as QASM2. + """ + if not isinstance(other, QasmModule): + raise TypeError(f"Expected QasmModule instance, got {type(other).__name__}") + + left_mod = self.copy() + right_mod = other.copy() + + # Unroll with qubit consolidation so both sides use __PYQASM_QUBITS__ + unroll_kwargs: dict[str, object] = {"consolidate_qubits": True} + if device_qubits is not None: + unroll_kwargs["device_qubits"] = device_qubits + + left_mod.unroll(**unroll_kwargs) + right_mod.unroll(**unroll_kwargs) + + left_qubits = left_mod.num_qubits + total_qubits = left_qubits + right_mod.num_qubits + + merged_program = Program(statements=[], version="2.0") + + # Unique includes first; map stdgates.inc -> qelib1.inc for QASM2 + include_names: list[str] = [] + for module in (left_mod, right_mod): + for stmt in module.unrolled_ast.statements: + if isinstance(stmt, Include): + fname = stmt.filename + if fname == "stdgates.inc": + fname = "qelib1.inc" + if fname not in include_names: + include_names.append(fname) + for name in include_names: + merged_program.statements.append(Include(filename=name)) + + # Consolidated qubit declaration (converted to qreg on print) + merged_program.statements.append( + qasm3_ast.QubitDeclaration( + size=qasm3_ast.IntegerLiteral(value=total_qubits), + qubit=qasm3_ast.Identifier(name="__PYQASM_QUBITS__"), + ) + ) + + # Append left ops (skip decls and includes) + for stmt in left_mod.unrolled_ast.statements: + if isinstance(stmt, (qasm3_ast.QubitDeclaration, Include)): + continue + merged_program.statements.append(deepcopy(stmt)) + + # Append right ops with index offset + for stmt in right_mod.unrolled_ast.statements: + if isinstance(stmt, (qasm3_ast.QubitDeclaration, Include)): + continue + stmt_copy = deepcopy(stmt) + offset_statement_qubits(stmt_copy, left_qubits) + merged_program.statements.append(stmt_copy) + + merged_module = Qasm2Module( + name=f"{left_mod.name}_merged_{right_mod.name}", + program=merged_program, + ) + merged_module.unrolled_ast = Program(statements=list(merged_program.statements), version="2.0") + merged_module._external_gates = list({*left_mod._external_gates, *right_mod._external_gates}) + merged_module._user_operations = list(left_mod.history) + list(right_mod.history) + merged_module._user_operations.append(f"merge(other={right_mod.name})") + merged_module.validate() + return merged_module diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 8ed08d51..d24a494e 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,10 +16,11 @@ Defines a module for handling OpenQASM 3.0 programs. """ +import openqasm3.ast as qasm3_ast from openqasm3.ast import Program from openqasm3.printer import dumps -from pyqasm.modules.base import QasmModule +from pyqasm.modules.base import QasmModule, offset_statement_qubits class Qasm3Module(QasmModule): @@ -52,3 +53,69 @@ def accept(self, visitor): final_stmt_list = visitor.finalize(unrolled_stmt_list) self._unrolled_ast.statements = final_stmt_list + + def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModule: + """Merge two modules as OpenQASM 3.0 without mixing versions. + If ``other`` is QASM2, it will be converted to QASM3 before merging. + The merged program keeps version "3.0". + """ + if not isinstance(other, QasmModule): + raise TypeError(f"Expected QasmModule instance, got {type(other).__name__}") + + # Convert right to QASM3 if it supports conversion; otherwise copy + convert = getattr(other, "to_qasm3", None) + right_mod = convert(as_str=False) if callable(convert) else other.copy() # type: ignore[assignment] + + left_mod = self.copy() + + # Unroll with consolidation so both use __PYQASM_QUBITS__ + unroll_kwargs: dict[str, object] = {"consolidate_qubits": True} + if device_qubits is not None: + unroll_kwargs["device_qubits"] = device_qubits + + left_mod.unroll(**unroll_kwargs) + right_mod.unroll(**unroll_kwargs) + + left_qubits = left_mod.num_qubits + total_qubits = left_qubits + right_mod.num_qubits + + merged_program = Program(statements=[], version="3.0") + + # Unique includes first + include_names: list[str] = [] + for module in (left_mod, right_mod): + for stmt in module.unrolled_ast.statements: + if isinstance(stmt, qasm3_ast.Include) and stmt.filename not in include_names: + include_names.append(stmt.filename) + for name in include_names: + merged_program.statements.append(qasm3_ast.Include(filename=name)) + + # Consolidated qubit declaration + merged_program.statements.append( + qasm3_ast.QubitDeclaration( + size=qasm3_ast.IntegerLiteral(value=total_qubits), + qubit=qasm3_ast.Identifier(name="__PYQASM_QUBITS__"), + ) + ) + + # Append left ops + for stmt in left_mod.unrolled_ast.statements: + if isinstance(stmt, (qasm3_ast.QubitDeclaration, qasm3_ast.Include)): + continue + merged_program.statements.append(stmt) + + # Append right ops with index offset + for stmt in right_mod.unrolled_ast.statements: + if isinstance(stmt, (qasm3_ast.QubitDeclaration, qasm3_ast.Include)): + continue + # right_mod is a copy, so it's safe to modify statements in place + offset_statement_qubits(stmt, left_qubits) + merged_program.statements.append(stmt) + + merged_module = Qasm3Module(name=f"{left_mod.name}_merged_{right_mod.name}", program=merged_program) + merged_module.unrolled_ast = Program(statements=list(merged_program.statements), version="3.0") + merged_module._external_gates = list({*left_mod._external_gates, *right_mod._external_gates}) + merged_module._user_operations = list(left_mod.history) + list(right_mod.history) + merged_module._user_operations.append(f"merge(other={right_mod.name})") + merged_module.validate() + return merged_module \ No newline at end of file diff --git a/tests/qasm3/test_merge.py b/tests/qasm3/test_merge.py new file mode 100644 index 00000000..0a7ace86 --- /dev/null +++ b/tests/qasm3/test_merge.py @@ -0,0 +1,122 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for QasmModule.merge(). +""" + +from pyqasm.entrypoint import loads +from pyqasm.modules import QasmModule + + +def _qasm3(qasm: str) -> QasmModule: + return loads(qasm) + + +def test_merge_basic_gates_and_offsets(): + qasm_a = ( + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[2] q;\n" + "x q[0];\n" + "cx q[0], q[1];\n" + ) + qasm_b = ( + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[3] r;\n" + "h r[0];\n" + "cx r[1], r[2];\n" + ) + + mod_a = _qasm3(qasm_a) + mod_b = _qasm3(qasm_b) + + merged = mod_a.merge(mod_b) + + # Unrolled representation should have a single consolidated qubit declaration of size 5 + text = str(merged) + assert "qubit[5] __PYQASM_QUBITS__;" in text + + lines = [l.strip() for l in text.splitlines() if l.strip()] + # Keep only gate lines for comparison; skip version/includes/declarations + gate_lines = [ + l + for l in lines + if l[0].isalpha() + and not l.startswith("include") + and not l.startswith("OPENQASM") + and not l.startswith("qubit") + ] + assert gate_lines[0].startswith("x __PYQASM_QUBITS__[0]") + assert gate_lines[1].startswith("cx __PYQASM_QUBITS__[0], __PYQASM_QUBITS__[1]") + assert any(l.startswith("h __PYQASM_QUBITS__[2]") for l in gate_lines) + assert any(l.startswith("cx __PYQASM_QUBITS__[3], __PYQASM_QUBITS__[4]") for l in gate_lines) + + +def test_merge_with_measurements_and_barriers(): + # Module A: 1 qubit + classical 1; has barrier and measure + qasm_a = ( + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[1] qa; bit[1] ca;\n" + "h qa[0];\n" + "barrier qa;\n" + "ca[0] = measure qa[0];\n" + ) + # Module B: 2 qubits + classical 2 + qasm_b = ( + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[2] qb; bit[2] cb;\n" + "x qb[1];\n" + "cb[1] = measure qb[1];\n" + ) + + mod_a = _qasm3(qasm_a) + mod_b = _qasm3(qasm_b) + + merged = mod_a.merge(mod_b) + merged_text = str(merged) + + assert "qubit[3] __PYQASM_QUBITS__;" in merged_text + assert "measure __PYQASM_QUBITS__[2];" in merged_text + assert "barrier __PYQASM_QUBITS__" in merged_text + + +def test_merge_qasm2_with_qasm3(): + qasm2 = ( + "OPENQASM 2.0;\n" + "include \"qelib1.inc\";\n" + "qreg q[1];\n" + "h q[0];\n" + ) + qasm3 = ( + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[2] r;\n" + "x r[0];\n" + ) + + mod2 = loads(qasm2) + mod3 = loads(qasm3) + + merged = mod2.merge(mod3) + text = str(merged) + # Since we are merging starting from a QASM2 module, the merged output + # should remain in QASM2 syntax (qreg), not QASM3 (qubit). + assert "OPENQASM 2.0;" in text + assert "include \"qelib1.inc\";" in text + assert "qreg __PYQASM_QUBITS__[3];" in text + assert "x __PYQASM_QUBITS__[1];" in text \ No newline at end of file From 0720d2bef77d62973252a0fb59a9e6e104218142 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 15 Sep 2025 01:33:09 -0400 Subject: [PATCH 2/6] lint changes --- src/pyqasm/modules/base.py | 111 ++++++++++++++++++------------------ src/pyqasm/modules/qasm2.py | 87 +++++++++++++++++++++++----- src/pyqasm/modules/qasm3.py | 93 ++++++++++++++++++++++++++---- tests/qasm3/test_merge.py | 2 +- 4 files changed, 210 insertions(+), 83 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 785fd708..7dbc51ca 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -769,7 +769,7 @@ def merge( other: "QasmModule", device_qubits: Optional[int] = None, ) -> "QasmModule": - + """Merge this module with another module. Implemented by concrete subclasses to avoid version mixing and @@ -777,59 +777,58 @@ def merge( are normalized to the same version prior to merging. """ - - def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): - """Offset qubit indices for a given statement in-place by ``offset``. - Handles gates, measurements, resets, and barriers (including slice forms). - """ - if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): - bit = stmt.measure.qubit - if isinstance(bit, qasm3_ast.IndexedIdentifier): - for group in bit.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumGate): - for q in stmt.qubits: - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumReset): - q = stmt.qubits - if isinstance(q, qasm3_ast.IndexedIdentifier): - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] +def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # pylint: disable=too-many-branches + """Offset qubit indices for a given statement in-place by ``offset``. + Handles gates, measurements, resets, and barriers (including slice forms). + """ + if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): + bit = stmt.measure.qubit + if isinstance(bit, qasm3_ast.IndexedIdentifier): + for group in bit.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumGate): + for q in stmt.qubits: + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumReset): + q = stmt.qubits + if isinstance(q, qasm3_ast.IndexedIdentifier): + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumBarrier): + qubits = stmt.qubits + if len(qubits) == 0: return - - if isinstance(stmt, qasm3_ast.QuantumBarrier): - qubits = stmt.qubits - if len(qubits) == 0: - return - first = qubits[0] - if isinstance(first, qasm3_ast.IndexedIdentifier): - for group in first.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - elif isinstance(first, qasm3_ast.Identifier): - # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] - name = first.name - if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__"):] - # Parse slice forms [S:E], [:E], or [S:] - m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) - if m: - start_s, end_s = m.group(1), m.group(2) - if start_s is None and end_s is not None: - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[:{end_v}]" - elif start_s is not None and end_s is None: - start_v = int(start_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:]" - elif start_s is not None and end_s is not None: - start_v = int(start_s) + offset - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" \ No newline at end of file + first = qubits[0] + if isinstance(first, qasm3_ast.IndexedIdentifier): + for group in first.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + elif isinstance(first, qasm3_ast.Identifier): + # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] + name = first.name + if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): + slice_str = name[len("__PYQASM_QUBITS__"):] + # Parse slice forms [S:E], [:E], or [S:] + m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) + if m: + start_s, end_s = m.group(1), m.group(2) + if start_s is None and end_s is not None: + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[:{end_v}]" + elif start_s is not None and end_s is None: + start_v = int(start_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:]" + elif start_s is not None and end_s is not None: + start_v = int(start_s) + offset + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index 79b838ce..f2ec1a52 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -26,7 +26,61 @@ from pyqasm.exceptions import ValidationError from pyqasm.modules.base import QasmModule from pyqasm.modules.qasm3 import Qasm3Module -from pyqasm.modules.base import offset_statement_qubits + +try: + from pyqasm.modules.base import offset_statement_qubits # type: ignore +except Exception: # pylint: disable=broad-except + def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches + """Offset qubit indices for a given statement in-place by ``offset``.""" + if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): + bit = stmt.measure.qubit + if isinstance(bit, qasm3_ast.IndexedIdentifier): + for group in bit.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumGate): + for q in stmt.qubits: + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumReset): + q = stmt.qubits + if isinstance(q, qasm3_ast.IndexedIdentifier): + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumBarrier): + qubits = stmt.qubits + if len(qubits) == 0: + return + first = qubits[0] + if isinstance(first, qasm3_ast.IndexedIdentifier): + for group in first.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + elif isinstance(first, qasm3_ast.Identifier): + name = first.name + if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): + slice_str = name[len("__PYQASM_QUBITS__"):] + m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) + if m: + start_s, end_s = m.group(1), m.group(2) + if start_s is None and end_s is not None: + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[:{end_v}]" + elif start_s is not None and end_s is None: + start_v = int(start_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:]" + elif start_s is not None and end_s is not None: + start_v = int(start_s) + offset + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" class Qasm2Module(QasmModule): """ @@ -130,27 +184,25 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu right_mod.unroll(**unroll_kwargs) left_qubits = left_mod.num_qubits - total_qubits = left_qubits + right_mod.num_qubits merged_program = Program(statements=[], version="2.0") # Unique includes first; map stdgates.inc -> qelib1.inc for QASM2 - include_names: list[str] = [] + include_names: set[str] = set() for module in (left_mod, right_mod): for stmt in module.unrolled_ast.statements: if isinstance(stmt, Include): fname = stmt.filename if fname == "stdgates.inc": fname = "qelib1.inc" - if fname not in include_names: - include_names.append(fname) - for name in include_names: - merged_program.statements.append(Include(filename=name)) + include_names.add(fname) + for fname in include_names: + merged_program.statements.append(Include(filename=fname)) # Consolidated qubit declaration (converted to qreg on print) merged_program.statements.append( qasm3_ast.QubitDeclaration( - size=qasm3_ast.IntegerLiteral(value=total_qubits), + size=qasm3_ast.IntegerLiteral(value=left_qubits + right_mod.num_qubits), qubit=qasm3_ast.Identifier(name="__PYQASM_QUBITS__"), ) ) @@ -165,17 +217,24 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu for stmt in right_mod.unrolled_ast.statements: if isinstance(stmt, (qasm3_ast.QubitDeclaration, Include)): continue - stmt_copy = deepcopy(stmt) - offset_statement_qubits(stmt_copy, left_qubits) - merged_program.statements.append(stmt_copy) + stmt = deepcopy(stmt) + offset_statement_qubits(stmt, left_qubits) + merged_program.statements.append(stmt) merged_module = Qasm2Module( name=f"{left_mod.name}_merged_{right_mod.name}", program=merged_program, ) - merged_module.unrolled_ast = Program(statements=list(merged_program.statements), version="2.0") - merged_module._external_gates = list({*left_mod._external_gates, *right_mod._external_gates}) - merged_module._user_operations = list(left_mod.history) + list(right_mod.history) + merged_module.unrolled_ast = Program( + statements=list(merged_program.statements), + version="2.0", + ) + merged_module._external_gates = list( + { *left_mod._external_gates, *right_mod._external_gates } + ) + merged_module._user_operations = ( + list(left_mod.history) + list(right_mod.history) + ) merged_module._user_operations.append(f"merge(other={right_mod.name})") merged_module.validate() return merged_module diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index d24a494e..c138b279 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,11 +16,68 @@ Defines a module for handling OpenQASM 3.0 programs. """ +import re import openqasm3.ast as qasm3_ast from openqasm3.ast import Program from openqasm3.printer import dumps -from pyqasm.modules.base import QasmModule, offset_statement_qubits +from pyqasm.modules.base import QasmModule + +# Backward-compat: older installed versions may not export offset_statement_qubits +try: + from pyqasm.modules.base import offset_statement_qubits # type: ignore +except Exception: # pylint: disable=broad-except + def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches + """Offset qubit indices for a given statement in-place by ``offset``.""" + if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): + bit = stmt.measure.qubit + if isinstance(bit, qasm3_ast.IndexedIdentifier): + for group in bit.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumGate): + for q in stmt.qubits: + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumReset): + q = stmt.qubits + if isinstance(q, qasm3_ast.IndexedIdentifier): + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumBarrier): + qubits = stmt.qubits + if len(qubits) == 0: + return + first = qubits[0] + if isinstance(first, qasm3_ast.IndexedIdentifier): + for group in first.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + elif isinstance(first, qasm3_ast.Identifier): + name = first.name + if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): + slice_str = name[len("__PYQASM_QUBITS__"):] + m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) + if m: + start_s, end_s = m.group(1), m.group(2) + if start_s is None and end_s is not None: + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[:{end_v}]" + elif start_s is not None and end_s is None: + start_v = int(start_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:]" + elif start_s is not None and end_s is not None: + start_v = int(start_s) + offset + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" class Qasm3Module(QasmModule): @@ -64,7 +121,9 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu # Convert right to QASM3 if it supports conversion; otherwise copy convert = getattr(other, "to_qasm3", None) - right_mod = convert(as_str=False) if callable(convert) else other.copy() # type: ignore[assignment] + right_mod = ( + convert(as_str=False) if callable(convert) else other.copy() + ) # type: ignore[assignment] left_mod = self.copy() @@ -82,13 +141,13 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu merged_program = Program(statements=[], version="3.0") # Unique includes first - include_names: list[str] = [] + include_names: set[str] = set() for module in (left_mod, right_mod): for stmt in module.unrolled_ast.statements: - if isinstance(stmt, qasm3_ast.Include) and stmt.filename not in include_names: - include_names.append(stmt.filename) - for name in include_names: - merged_program.statements.append(qasm3_ast.Include(filename=name)) + if isinstance(stmt, qasm3_ast.Include): + include_names.add(stmt.filename) + for fname in include_names: + merged_program.statements.append(qasm3_ast.Include(filename=fname)) # Consolidated qubit declaration merged_program.statements.append( @@ -112,10 +171,20 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu offset_statement_qubits(stmt, left_qubits) merged_program.statements.append(stmt) - merged_module = Qasm3Module(name=f"{left_mod.name}_merged_{right_mod.name}", program=merged_program) - merged_module.unrolled_ast = Program(statements=list(merged_program.statements), version="3.0") - merged_module._external_gates = list({*left_mod._external_gates, *right_mod._external_gates}) - merged_module._user_operations = list(left_mod.history) + list(right_mod.history) + merged_module = Qasm3Module( + name=f"{left_mod.name}_merged_{right_mod.name}", + program=merged_program, + ) + merged_module.unrolled_ast = Program( + statements=list(merged_program.statements), + version="3.0", + ) + merged_module._external_gates = list( + { *left_mod._external_gates, *right_mod._external_gates } + ) + merged_module._user_operations = ( + list(left_mod.history) + list(right_mod.history) + ) merged_module._user_operations.append(f"merge(other={right_mod.name})") merged_module.validate() - return merged_module \ No newline at end of file + return merged_module diff --git a/tests/qasm3/test_merge.py b/tests/qasm3/test_merge.py index 0a7ace86..9a2d3fe3 100644 --- a/tests/qasm3/test_merge.py +++ b/tests/qasm3/test_merge.py @@ -119,4 +119,4 @@ def test_merge_qasm2_with_qasm3(): assert "OPENQASM 2.0;" in text assert "include \"qelib1.inc\";" in text assert "qreg __PYQASM_QUBITS__[3];" in text - assert "x __PYQASM_QUBITS__[1];" in text \ No newline at end of file + assert "x __PYQASM_QUBITS__[1];" in text From 6ad6a635c4c00d57a97da61c27b20218769e25c2 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 15 Sep 2025 03:29:56 -0400 Subject: [PATCH 3/6] lint change --- src/pyqasm/modules/qasm3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index c138b279..38fe8859 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -17,6 +17,7 @@ """ import re + import openqasm3.ast as qasm3_ast from openqasm3.ast import Program from openqasm3.printer import dumps From e1af14ff4090691bf22e55e1d72853cf469d2705 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 15 Sep 2025 03:32:59 -0400 Subject: [PATCH 4/6] lint changes --- src/pyqasm/modules/base.py | 8 +++++--- src/pyqasm/modules/qasm2.py | 10 +++++----- src/pyqasm/modules/qasm3.py | 9 ++++----- tests/qasm3/test_merge.py | 32 +++++++------------------------- 4 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 7dbc51ca..ed79c6d4 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -769,7 +769,6 @@ def merge( other: "QasmModule", device_qubits: Optional[int] = None, ) -> "QasmModule": - """Merge this module with another module. Implemented by concrete subclasses to avoid version mixing and @@ -777,7 +776,10 @@ def merge( are normalized to the same version prior to merging. """ -def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # pylint: disable=too-many-branches + +def offset_statement_qubits( + stmt: qasm3_ast.Statement, offset: int +): # pylint: disable=too-many-branches """Offset qubit indices for a given statement in-place by ``offset``. Handles gates, measurements, resets, and barriers (including slice forms). """ @@ -817,7 +819,7 @@ def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # pylint: # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] name = first.name if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__"):] + slice_str = name[len("__PYQASM_QUBITS__") :] # Parse slice forms [S:E], [:E], or [S:] m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) if m: diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index f2ec1a52..c8798246 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -30,6 +30,7 @@ try: from pyqasm.modules.base import offset_statement_qubits # type: ignore except Exception: # pylint: disable=broad-except + def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches """Offset qubit indices for a given statement in-place by ``offset``.""" if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): @@ -67,7 +68,7 @@ def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ig elif isinstance(first, qasm3_ast.Identifier): name = first.name if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__"):] + slice_str = name[len("__PYQASM_QUBITS__") :] m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) if m: start_s, end_s = m.group(1), m.group(2) @@ -82,6 +83,7 @@ def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ig end_v = int(end_s) + offset first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" + class Qasm2Module(QasmModule): """ A module representing an openqasm2 quantum program. @@ -230,11 +232,9 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu version="2.0", ) merged_module._external_gates = list( - { *left_mod._external_gates, *right_mod._external_gates } - ) - merged_module._user_operations = ( - list(left_mod.history) + list(right_mod.history) + {*left_mod._external_gates, *right_mod._external_gates} ) + merged_module._user_operations = list(left_mod.history) + list(right_mod.history) merged_module._user_operations.append(f"merge(other={right_mod.name})") merged_module.validate() return merged_module diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 38fe8859..f4bcbb6c 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -28,6 +28,7 @@ try: from pyqasm.modules.base import offset_statement_qubits # type: ignore except Exception: # pylint: disable=broad-except + def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches """Offset qubit indices for a given statement in-place by ``offset``.""" if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): @@ -65,7 +66,7 @@ def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ig elif isinstance(first, qasm3_ast.Identifier): name = first.name if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__"):] + slice_str = name[len("__PYQASM_QUBITS__") :] m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) if m: start_s, end_s = m.group(1), m.group(2) @@ -181,11 +182,9 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu version="3.0", ) merged_module._external_gates = list( - { *left_mod._external_gates, *right_mod._external_gates } - ) - merged_module._user_operations = ( - list(left_mod.history) + list(right_mod.history) + {*left_mod._external_gates, *right_mod._external_gates} ) + merged_module._user_operations = list(left_mod.history) + list(right_mod.history) merged_module._user_operations.append(f"merge(other={right_mod.name})") merged_module.validate() return merged_module diff --git a/tests/qasm3/test_merge.py b/tests/qasm3/test_merge.py index 9a2d3fe3..e231c1f4 100644 --- a/tests/qasm3/test_merge.py +++ b/tests/qasm3/test_merge.py @@ -26,18 +26,10 @@ def _qasm3(qasm: str) -> QasmModule: def test_merge_basic_gates_and_offsets(): qasm_a = ( - "OPENQASM 3.0;\n" - "include \"stdgates.inc\";\n" - "qubit[2] q;\n" - "x q[0];\n" - "cx q[0], q[1];\n" + "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[2] q;\n" "x q[0];\n" "cx q[0], q[1];\n" ) qasm_b = ( - "OPENQASM 3.0;\n" - "include \"stdgates.inc\";\n" - "qubit[3] r;\n" - "h r[0];\n" - "cx r[1], r[2];\n" + "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[3] r;\n" "h r[0];\n" "cx r[1], r[2];\n" ) mod_a = _qasm3(qasm_a) @@ -69,7 +61,7 @@ def test_merge_with_measurements_and_barriers(): # Module A: 1 qubit + classical 1; has barrier and measure qasm_a = ( "OPENQASM 3.0;\n" - "include \"stdgates.inc\";\n" + 'include "stdgates.inc";\n' "qubit[1] qa; bit[1] ca;\n" "h qa[0];\n" "barrier qa;\n" @@ -78,7 +70,7 @@ def test_merge_with_measurements_and_barriers(): # Module B: 2 qubits + classical 2 qasm_b = ( "OPENQASM 3.0;\n" - "include \"stdgates.inc\";\n" + 'include "stdgates.inc";\n' "qubit[2] qb; bit[2] cb;\n" "x qb[1];\n" "cb[1] = measure qb[1];\n" @@ -96,18 +88,8 @@ def test_merge_with_measurements_and_barriers(): def test_merge_qasm2_with_qasm3(): - qasm2 = ( - "OPENQASM 2.0;\n" - "include \"qelib1.inc\";\n" - "qreg q[1];\n" - "h q[0];\n" - ) - qasm3 = ( - "OPENQASM 3.0;\n" - "include \"stdgates.inc\";\n" - "qubit[2] r;\n" - "x r[0];\n" - ) + qasm2 = "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' "qreg q[1];\n" "h q[0];\n" + qasm3 = "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[2] r;\n" "x r[0];\n" mod2 = loads(qasm2) mod3 = loads(qasm3) @@ -117,6 +99,6 @@ def test_merge_qasm2_with_qasm3(): # Since we are merging starting from a QASM2 module, the merged output # should remain in QASM2 syntax (qreg), not QASM3 (qubit). assert "OPENQASM 2.0;" in text - assert "include \"qelib1.inc\";" in text + assert 'include "qelib1.inc";' in text assert "qreg __PYQASM_QUBITS__[3];" in text assert "x __PYQASM_QUBITS__[1];" in text From 3a2bd47aba773079a0aca40901563123c8eae157 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 15 Sep 2025 03:44:23 -0400 Subject: [PATCH 5/6] test merge lint fix --- tests/qasm3/test_merge.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/qasm3/test_merge.py b/tests/qasm3/test_merge.py index e231c1f4..eaf06ebe 100644 --- a/tests/qasm3/test_merge.py +++ b/tests/qasm3/test_merge.py @@ -26,10 +26,22 @@ def _qasm3(qasm: str) -> QasmModule: def test_merge_basic_gates_and_offsets(): qasm_a = ( - "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[2] q;\n" "x q[0];\n" "cx q[0], q[1];\n" + """ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +x q[0]; +cx q[0], q[1]; +""" ) qasm_b = ( - "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[3] r;\n" "h r[0];\n" "cx r[1], r[2];\n" + """ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] r; +h r[0]; +cx r[1], r[2]; +""" ) mod_a = _qasm3(qasm_a) @@ -88,8 +100,18 @@ def test_merge_with_measurements_and_barriers(): def test_merge_qasm2_with_qasm3(): - qasm2 = "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' "qreg q[1];\n" "h q[0];\n" - qasm3 = "OPENQASM 3.0;\n" 'include "stdgates.inc";\n' "qubit[2] r;\n" "x r[0];\n" + qasm2 = ( + "OPENQASM 2.0;\n" + 'include "qelib1.inc";\n' + "qreg q[1];\n" + "h q[0];\n" + ) + qasm3 = ( + "OPENQASM 3.0;\n" + 'include "stdgates.inc";\n' + "qubit[2] r;\n" + "x r[0];\n" + ) mod2 = loads(qasm2) mod3 = loads(qasm3) From c10c526d7bb00fc938a3acf55b0af09087f48543 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Tue, 16 Sep 2025 11:41:40 -0700 Subject: [PATCH 6/6] comment changes --- src/pyqasm/modules/base.py | 116 +++++++++++++++++++----------------- src/pyqasm/modules/qasm2.py | 58 +----------------- src/pyqasm/modules/qasm3.py | 64 +------------------- 3 files changed, 63 insertions(+), 175 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index ed79c6d4..c9abc4a1 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -776,61 +776,65 @@ def merge( are normalized to the same version prior to merging. """ + @staticmethod + def offset_statement_qubits( + stmt: qasm3_ast.Statement, offset: int + ): # pylint: disable=too-many-branches + """Offset qubit indices for a given statement in-place by ``offset``. + Handles gates, measurements, resets, and barriers (including slice forms). + """ + if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): + bit = stmt.measure.qubit + if isinstance(bit, qasm3_ast.IndexedIdentifier): + for group in bit.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return -def offset_statement_qubits( - stmt: qasm3_ast.Statement, offset: int -): # pylint: disable=too-many-branches - """Offset qubit indices for a given statement in-place by ``offset``. - Handles gates, measurements, resets, and barriers (including slice forms). - """ - if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): - bit = stmt.measure.qubit - if isinstance(bit, qasm3_ast.IndexedIdentifier): - for group in bit.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumGate): - for q in stmt.qubits: - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumReset): - q = stmt.qubits - if isinstance(q, qasm3_ast.IndexedIdentifier): - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumBarrier): - qubits = stmt.qubits - if len(qubits) == 0: + if isinstance(stmt, qasm3_ast.QuantumGate): + for q in stmt.qubits: + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] return - first = qubits[0] - if isinstance(first, qasm3_ast.IndexedIdentifier): - for group in first.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - elif isinstance(first, qasm3_ast.Identifier): - # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] - name = first.name - if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__") :] - # Parse slice forms [S:E], [:E], or [S:] - m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) - if m: - start_s, end_s = m.group(1), m.group(2) - if start_s is None and end_s is not None: - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[:{end_v}]" - elif start_s is not None and end_s is None: - start_v = int(start_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:]" - elif start_s is not None and end_s is not None: - start_v = int(start_s) + offset - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" + + if isinstance(stmt, qasm3_ast.QuantumReset): + q = stmt.qubits + if isinstance(q, qasm3_ast.IndexedIdentifier): + for group in q.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + return + + if isinstance(stmt, qasm3_ast.QuantumBarrier): + qubits = stmt.qubits + if len(qubits) == 0: + return + first = qubits[0] + if isinstance(first, qasm3_ast.IndexedIdentifier): + for group in first.indices: + for ind in group: + ind.value += offset # type: ignore[attr-defined] + elif isinstance(first, qasm3_ast.Identifier): + # Handle forms: __PYQASM_QUBITS__[:E], [S:], [S:E] + name = first.name + if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): + slice_str = name[len("__PYQASM_QUBITS__") :] + # Parse slice forms [S:E], [:E], or [S:] + m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) + if m: + start_s, end_s = m.group(1), m.group(2) + if start_s is None and end_s is not None: + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[:{end_v}]" + elif start_s is not None and end_s is None: + start_v = int(start_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:]" + elif start_s is not None and end_s is not None: + start_v = int(start_s) + offset + end_v = int(end_s) + offset + first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" + +def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): + """Backward-compat wrapper to the class staticmethod.""" + return QasmModule.offset_statement_qubits(stmt, offset) diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index c8798246..0796f3b9 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -27,62 +27,6 @@ from pyqasm.modules.base import QasmModule from pyqasm.modules.qasm3 import Qasm3Module -try: - from pyqasm.modules.base import offset_statement_qubits # type: ignore -except Exception: # pylint: disable=broad-except - - def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches - """Offset qubit indices for a given statement in-place by ``offset``.""" - if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): - bit = stmt.measure.qubit - if isinstance(bit, qasm3_ast.IndexedIdentifier): - for group in bit.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumGate): - for q in stmt.qubits: - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumReset): - q = stmt.qubits - if isinstance(q, qasm3_ast.IndexedIdentifier): - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumBarrier): - qubits = stmt.qubits - if len(qubits) == 0: - return - first = qubits[0] - if isinstance(first, qasm3_ast.IndexedIdentifier): - for group in first.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - elif isinstance(first, qasm3_ast.Identifier): - name = first.name - if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__") :] - m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) - if m: - start_s, end_s = m.group(1), m.group(2) - if start_s is None and end_s is not None: - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[:{end_v}]" - elif start_s is not None and end_s is None: - start_v = int(start_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:]" - elif start_s is not None and end_s is not None: - start_v = int(start_s) + offset - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" - class Qasm2Module(QasmModule): """ @@ -220,7 +164,7 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu if isinstance(stmt, (qasm3_ast.QubitDeclaration, Include)): continue stmt = deepcopy(stmt) - offset_statement_qubits(stmt, left_qubits) + QasmModule.offset_statement_qubits(stmt, left_qubits) merged_program.statements.append(stmt) merged_module = Qasm2Module( diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index f4bcbb6c..ea0d6054 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,71 +16,12 @@ Defines a module for handling OpenQASM 3.0 programs. """ -import re - import openqasm3.ast as qasm3_ast from openqasm3.ast import Program from openqasm3.printer import dumps from pyqasm.modules.base import QasmModule -# Backward-compat: older installed versions may not export offset_statement_qubits -try: - from pyqasm.modules.base import offset_statement_qubits # type: ignore -except Exception: # pylint: disable=broad-except - - def offset_statement_qubits(stmt: qasm3_ast.Statement, offset: int): # type: ignore[override] # pylint: disable=too-many-branches - """Offset qubit indices for a given statement in-place by ``offset``.""" - if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): - bit = stmt.measure.qubit - if isinstance(bit, qasm3_ast.IndexedIdentifier): - for group in bit.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumGate): - for q in stmt.qubits: - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumReset): - q = stmt.qubits - if isinstance(q, qasm3_ast.IndexedIdentifier): - for group in q.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - return - - if isinstance(stmt, qasm3_ast.QuantumBarrier): - qubits = stmt.qubits - if len(qubits) == 0: - return - first = qubits[0] - if isinstance(first, qasm3_ast.IndexedIdentifier): - for group in first.indices: - for ind in group: - ind.value += offset # type: ignore[attr-defined] - elif isinstance(first, qasm3_ast.Identifier): - name = first.name - if name.startswith("__PYQASM_QUBITS__[") and name.endswith("]"): - slice_str = name[len("__PYQASM_QUBITS__") :] - m = re.match(r"\[(?:(\d+)?:(\d+)?)\]", slice_str) - if m: - start_s, end_s = m.group(1), m.group(2) - if start_s is None and end_s is not None: - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[:{end_v}]" - elif start_s is not None and end_s is None: - start_v = int(start_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:]" - elif start_s is not None and end_s is not None: - start_v = int(start_s) + offset - end_v = int(end_s) + offset - first.name = f"__PYQASM_QUBITS__[{start_v}:{end_v}]" - class Qasm3Module(QasmModule): """ @@ -122,9 +63,8 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu raise TypeError(f"Expected QasmModule instance, got {type(other).__name__}") # Convert right to QASM3 if it supports conversion; otherwise copy - convert = getattr(other, "to_qasm3", None) right_mod = ( - convert(as_str=False) if callable(convert) else other.copy() + other.to_qasm3() if hasattr(other, "to_qasm3") else other.copy() ) # type: ignore[assignment] left_mod = self.copy() @@ -170,7 +110,7 @@ def merge(self, other: QasmModule, device_qubits: int | None = None) -> QasmModu if isinstance(stmt, (qasm3_ast.QubitDeclaration, qasm3_ast.Include)): continue # right_mod is a copy, so it's safe to modify statements in place - offset_statement_qubits(stmt, left_qubits) + QasmModule.offset_statement_qubits(stmt, left_qubits) merged_program.statements.append(stmt) merged_module = Qasm3Module(