diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2d47eb..9e43a5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Types of changes: ### Improved / Modified - Added `slots=True` parameter to the data classes in `elements.py` to improve memory efficiency ([#218](https://github.com/qBraid/pyqasm/pull/218)) - Updated the documentation to include core features in the `README` ([#219](https://github.com/qBraid/pyqasm/pull/219)) +- Added support to `device qubit` resgister consolidation.([#222](https://github.com/qBraid/pyqasm/pull/222)) ### Deprecated diff --git a/src/pyqasm/elements.py b/src/pyqasm/elements.py index 3e4936c7..ac084c75 100644 --- a/src/pyqasm/elements.py +++ b/src/pyqasm/elements.py @@ -89,6 +89,7 @@ class Variable: # pylint: disable=too-many-instance-attributes base_size (int): Base size of the variable. dims (Optional[List[int]]): Dimensions of the variable. value (Optional[int | float | np.ndarray]): Value of the variable. + span (Any): Span of the variable. is_constant (bool): Flag indicating if the variable is constant. is_register (bool): Flag indicating if the variable is a register. readonly (bool): Flag indicating if the variable is readonly. @@ -99,6 +100,7 @@ class Variable: # pylint: disable=too-many-instance-attributes base_size: int dims: Optional[list[int]] = None value: Optional[int | float | np.ndarray] = None + span: Any = None is_constant: bool = False is_register: bool = False readonly: bool = False diff --git a/src/pyqasm/entrypoint.py b/src/pyqasm/entrypoint.py index af083021..5facd8f9 100644 --- a/src/pyqasm/entrypoint.py +++ b/src/pyqasm/entrypoint.py @@ -30,7 +30,7 @@ import openqasm3.ast -def load(filename: str) -> QasmModule: +def load(filename: str, **kwargs) -> QasmModule: """Loads an OpenQASM program into a `QasmModule` object. Args: @@ -44,15 +44,18 @@ def load(filename: str) -> QasmModule: raise TypeError("Input 'filename' must be of type 'str'.") with open(filename, "r", encoding="utf-8") as file: program = file.read() - return loads(program) + return loads(program, **kwargs) -def loads(program: openqasm3.ast.Program | str) -> QasmModule: +def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule: """Loads an OpenQASM program into a `QasmModule` object. Args: program (openqasm3.ast.Program or str): The OpenQASM program to validate. + **kwargs: Additional arguments to pass to the loads function. + device_qubits (int): Number of physical qubits available on the target device. + Raises: TypeError: If the input is not a string or an `openqasm3.ast.Program` instance. ValidationError: If the program fails parsing or semantic validation. @@ -79,7 +82,9 @@ def loads(program: openqasm3.ast.Program | str) -> QasmModule: qasm_module = Qasm3Module if program.version.startswith("3") else Qasm2Module module = qasm_module("main", program) - + # Store device_qubits on the module for later use + if dev_qbts := kwargs.get("device_qubits"): + module._device_qubits = dev_qbts return module diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py index 62926e13..d5642ac4 100644 --- a/src/pyqasm/expressions.py +++ b/src/pyqasm/expressions.py @@ -392,6 +392,7 @@ def _check_type_size(expression, var_name, var_format, base_type): dims=[], value=var_value, is_constant=const_expr, + span=expression.span, ) cast_var_value = Qasm3Validator.validate_variable_assignment_value( variable, var_value, expression diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 3b18db71..137975d6 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -57,6 +57,8 @@ def __init__(self, name: str, program: Program): self._unrolled_ast = Program(statements=[]) self._external_gates: list[str] = [] self._decompose_native_gates: Optional[bool] = None + self._device_qubits: Optional[int] = None + self._consolidate_qubits: Optional[bool] = False @property def name(self) -> str: @@ -519,6 +521,13 @@ def validate(self): self.num_qubits, self.num_clbits = 0, 0 visitor = QasmVisitor(self, check_only=True) self.accept(visitor) + # Implicit validation: check total qubits if device_qubits is set and not consolidating + if self._device_qubits: + if self.num_qubits > self._device_qubits: + raise ValidationError( + # pylint: disable-next=line-too-long + f"Total qubits '{self.num_qubits}' exceed device qubits '{self._device_qubits}'." + ) except (ValidationError, NotImplementedError) as err: self.num_qubits, self.num_clbits = -1, -1 raise err @@ -534,6 +543,9 @@ def unroll(self, **kwargs): max_loop_iters (int): Max number of iterations for unrolling loops. Defaults to 1e9. check_only (bool): If True, only check the program without executing it. Defaults to False. + device_qubits (int): Number of physical qubits available on the target device. + consolidate_qubits (bool): If True, consolidate all quantum registers into + single register. Raises: ValidationError: If the module fails validation during unrolling. @@ -545,12 +557,15 @@ def unroll(self, **kwargs): """ if not kwargs: kwargs = {} + try: self.num_qubits, self.num_clbits = 0, 0 if ext_gates := kwargs.get("external_gates"): self._external_gates = ext_gates else: self._external_gates = [] + if consolidate_qbts := kwargs.get("consolidate_qubits"): + self._consolidate_qubits = consolidate_qbts visitor = QasmVisitor(module=self, **kwargs) self.accept(visitor) except (ValidationError, UnrollError) as err: diff --git a/src/pyqasm/subroutines.py b/src/pyqasm/subroutines.py index 4f4096e3..894a2b4f 100644 --- a/src/pyqasm/subroutines.py +++ b/src/pyqasm/subroutines.py @@ -156,6 +156,7 @@ def _process_classical_arg_by_value( dims=None, value=actual_arg_value, is_constant=False, + span=fn_call.span, ) @classmethod # pylint: disable-next=too-many-arguments,too-many-locals,too-many-branches @@ -346,6 +347,7 @@ def _process_classical_arg_by_reference( dims=formal_dimensions, value=actual_array_view, # this is the VIEW of the actual array readonly=readonly_arr, + span=fn_call.span, ) @classmethod # pylint: disable-next=too-many-arguments @@ -454,4 +456,5 @@ def process_quantum_arg( # pylint: disable=too-many-locals dims=None, value=None, is_constant=False, + span=fn_call.span, ) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 119ac358..2c9087c7 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -17,7 +17,7 @@ """ from copy import deepcopy -from typing import Any, NamedTuple, Optional +from typing import Any, NamedTuple, Optional, Sequence, cast import numpy as np from openqasm3.ast import ( @@ -37,9 +37,11 @@ QASMNode, QuantumBarrier, QuantumGate, + QuantumMeasurementStatement, QuantumPhase, QuantumReset, RangeDefinition, + Statement, UintType, UnaryExpression, UnaryOperator, @@ -438,3 +440,132 @@ def get_type_string(variable: Variable) -> str: if is_array: type_str += f", {', '.join([str(dim) for dim in dims])}]" return type_str + + @staticmethod + def consolidate_qubit_registers( # pylint: disable=too-many-branches, too-many-locals, too-many-statements + unrolled_stmts: Sequence[Statement] | Statement, + qubit_register_offsets: dict[str, int], + global_qreg_size_map: dict[str, int], + device_qubits: int | None, + ) -> Sequence[Statement] | Statement: + """Transform statements by mapping qubit registers to device qubit register indices + + Args: + unrolled_stmts : The statements or single statement to transform. + qubit_register_offsets (dict): Mapping from register name to its + offset in the global qubit array. + global_qreg_size_map (dict): original global qubit register mapping. + device_qubits (int): Total number of device qubits + + Returns: + The transformed statements or statement with qubit registers mapped to device indices. + """ + if device_qubits is None: + device_qubits = sum(global_qreg_size_map.values()) + + def _get_pyqasm_device_qubit_index( + reg: str, idx: int, qubit_reg_offsets: dict[str, int], global_qreg: dict[str, int] + ): + _offsets = qubit_reg_offsets + _n_qubits = global_qreg[reg] + if not 0 <= idx < _n_qubits: + raise IndexError(f"{reg}[{idx}] out of range (0..{_n_qubits-1})") + return _offsets[reg] + idx + + if isinstance(unrolled_stmts, QuantumBarrier): + _qubit_id = cast(Identifier, unrolled_stmts.qubits[0]) # type: ignore[union-attr] + if not isinstance(_qubit_id, IndexedIdentifier): + _start = _get_pyqasm_device_qubit_index( + _qubit_id.name, 0, qubit_register_offsets, global_qreg_size_map + ) + _end = _get_pyqasm_device_qubit_index( + _qubit_id.name, + global_qreg_size_map[_qubit_id.name] - 1, + qubit_register_offsets, + global_qreg_size_map, + ) + if _start == 0: + _qubit_id.name = f"__PYQASM_QUBITS__[:{_end+1}]" + elif _end == device_qubits - 1: + _qubit_id.name = f"__PYQASM_QUBITS__[{_start}:]" + else: + _qubit_id.name = f"__PYQASM_QUBITS__[{_start}:{_end+1}]" + else: + _qubit_str = cast(str, unrolled_stmts.qubits[0].name) # type: ignore[union-attr] + _qubit_ind = cast( + list, unrolled_stmts.qubits[0].indices + ) # type: ignore[union-attr] + for multi_ind in _qubit_ind: + for ind in multi_ind: + pyqasm_ind = _get_pyqasm_device_qubit_index( + _qubit_str.name, ind.value, qubit_register_offsets, global_qreg_size_map + ) + ind.value = pyqasm_ind + _qubit_str.name = "__PYQASM_QUBITS__" + + if isinstance(unrolled_stmts, list): # pylint: disable=too-many-nested-blocks + if isinstance(unrolled_stmts[0], QuantumMeasurementStatement): + for stmt in unrolled_stmts: + _qubit_id = cast( + Identifier, stmt.measure.qubit.name + ) # type: ignore[union-attr] + _qubit_ind = cast(list, stmt.measure.qubit.indices) # type: ignore[union-attr] + for multiple_ind in _qubit_ind: + for ind in multiple_ind: + _pyqasm_val = _get_pyqasm_device_qubit_index( + _qubit_id.name, + ind.value, + qubit_register_offsets, + global_qreg_size_map, + ) + ind.value = _pyqasm_val + _qubit_id.name = "__PYQASM_QUBITS__" + + if isinstance(unrolled_stmts[0], QuantumReset): + for stmt in unrolled_stmts: + _qubit_str = cast(str, stmt.qubits.name.name) # type: ignore[union-attr] + _qubit_ind = cast(list, stmt.qubits.indices) # type: ignore[union-attr] + for multiple_ind in _qubit_ind: + for ind in multiple_ind: + _pyqasm_val = _get_pyqasm_device_qubit_index( + _qubit_str, ind.value, qubit_register_offsets, global_qreg_size_map + ) + ind.value = _pyqasm_val + stmt.qubits.name.name = "__PYQASM_QUBITS__" # type: ignore[union-attr] + + if isinstance(unrolled_stmts[0], QuantumBarrier): + for stmt in unrolled_stmts: + _qubit_ind_id = cast( + IndexedIdentifier, stmt.qubits[0] + ) # type: ignore[union-attr] + _original_qubit_name = _qubit_ind_id.name.name + for multiple_ind in _qubit_ind_id.indices: + for ind in multiple_ind: # type: ignore[union-attr] + ind_val = cast(IntegerLiteral, ind) # type: ignore[union-attr] + pyqasm_val = _get_pyqasm_device_qubit_index( + _original_qubit_name, + ind_val.value, + qubit_register_offsets, + global_qreg_size_map, + ) + ind_val.value = pyqasm_val + _qubit_ind_id.name.name = "__PYQASM_QUBITS__" + + if isinstance(unrolled_stmts[0], QuantumGate): + for stmt in unrolled_stmts: + stmt_qubits: list[IndexedIdentifier] = [] + for qubit in stmt.qubits: + pyqasm_val = _get_pyqasm_device_qubit_index( + qubit.name.name, + qubit.indices[0][0].value, + qubit_register_offsets, + global_qreg_size_map, + ) + stmt_qubits.append( + IndexedIdentifier( + Identifier("__PYQASM_QUBITS__"), [[IntegerLiteral(pyqasm_val)]] + ) + ) + stmt.qubits = stmt_qubits + + return unrolled_stmts diff --git a/src/pyqasm/validator.py b/src/pyqasm/validator.py index 90d0ac44..14c42e55 100644 --- a/src/pyqasm/validator.py +++ b/src/pyqasm/validator.py @@ -335,6 +335,7 @@ def validate_return_statement( # pylint: disable=inconsistent-return-statements base_size, None, None, + span=return_statement.span, ), return_value, op_node=return_statement, diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index ef71d358..6fb16d83 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -20,9 +20,9 @@ """ import copy import logging -from collections import deque +from collections import OrderedDict, deque from functools import partial -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, cast import numpy as np import openqasm3.ast as qasm3_ast @@ -77,6 +77,8 @@ class QasmVisitor: external_gates (list[str]): List of gates that should not be unrolled. unroll_barriers (bool): If True, barriers will be unrolled. Defaults to True. check_only (bool): If True, only check the program without executing it. Defaults to False. + device_qubits (int): Number of physical qubits available on the target device. + consolidate_qubits (bool): If True, consolidate all quantum registers into single register. """ def __init__( # pylint: disable=too-many-arguments @@ -86,6 +88,7 @@ def __init__( # pylint: disable=too-many-arguments external_gates: list[str] | None = None, unroll_barriers: bool = True, max_loop_iters: int = int(1e9), + consolidate_qubits: bool = False, ): self._module = module self._scope: deque = deque([{}]) @@ -113,6 +116,10 @@ def __init__( # pylint: disable=too-many-arguments self._measurement_set: set[str] = set() self._init_utilities() self._loop_limit = max_loop_iters + self._consolidate_qubits: bool = consolidate_qubits + self._in_generic_gate_op_scope: int = 0 + self._qubit_register_offsets: OrderedDict = OrderedDict() + self._qubit_register_max_offset = 0 def _init_utilities(self): """Initialize the utilities for the visitor.""" @@ -338,7 +345,14 @@ def _visit_quantum_register( self._add_var_in_scope( Variable( - register_name, qasm3_ast.QubitDeclaration, register_size, None, None, False, True + register_name, + qasm3_ast.QubitDeclaration, + register_size, + None, + None, + register.span, + False, + True, ) ) size_map[f"{register_name}"] = register_size @@ -352,6 +366,13 @@ def _visit_quantum_register( self._module._add_qubit_register(register_name, register_size) + # _qubit_register_offsets maps each original quantum register to its + # starting index in the consolidated register, enabling correct + # translation of qubit indices after consolidation. + if self._consolidate_qubits: + self._qubit_register_offsets[register_name] = self._qubit_register_max_offset + self._qubit_register_max_offset += register_size + logger.debug("Added labels for register '%s'", str(register)) if self._check_only: @@ -559,7 +580,47 @@ def _check_variable_cast_type( span=statement.span, ) - def _visit_measurement( # pylint: disable=too-many-locals + def _qubit_register_consolidation( + self, unrolled_stmts: list, total_qubits: int + ) -> list[qasm3_ast.Statement]: + """ + Consolidate all quantum registers into a single register '__PYQASM_QUBITS__'. + + Args: + unrolled_stmts (list): The list of statements to process and modify in-place. + + Raises: + ValidationError: If the total number of qubits exceeds the available device qubits, + or if the reserved register '__PYQASM_QUBITS__' is already declared + in the original QASM program. + """ + if total_qubits > self._module._device_qubits: # type: ignore + raise_qasm3_error( + # pylint: disable-next=line-too-long + f"Total qubits '({total_qubits})' exceed device qubits '({self._module._device_qubits})'.", + ) + + global_scope = self._get_global_scope() + for var, val in global_scope.items(): + if var == "__PYQASM_QUBITS__": + raise_qasm3_error( + "Variable '__PYQASM_QUBITS__' is already defined", + span=val.span, + ) + + pyqasm_reg_id = qasm3_ast.Identifier("__PYQASM_QUBITS__") + pyqasm_reg_size = qasm3_ast.IntegerLiteral(self._module._device_qubits) # type: ignore + pyqasm_reg_stmt = qasm3_ast.QubitDeclaration(pyqasm_reg_id, pyqasm_reg_size) + + _valid_statements: list[qasm3_ast.Statement] = [] + _valid_statements.append(pyqasm_reg_stmt) + for stmt in unrolled_stmts: + if not isinstance(stmt, qasm3_ast.QubitDeclaration): + _valid_statements.append(stmt) + + return _valid_statements + + def _visit_measurement( # pylint: disable=too-many-locals, too-many-branches self, statement: qasm3_ast.QuantumMeasurementStatement ) -> list[qasm3_ast.QuantumMeasurementStatement]: """Visit a measurement statement element. @@ -659,6 +720,17 @@ def _visit_measurement( # pylint: disable=too-many-locals unrolled_measurements.append(unrolled_measure) + if self._consolidate_qubits: + unrolled_measurements = cast( + list[qasm3_ast.QuantumMeasurementStatement], + Qasm3Transformer.consolidate_qubit_registers( + unrolled_measurements, + self._qubit_register_offsets, + self._global_qreg_size_map, + self._module._device_qubits, + ), + ) + if self._check_only: return [] @@ -699,12 +771,25 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.Quan unrolled_resets.append(unrolled_reset) + if self._consolidate_qubits: + unrolled_resets = cast( + list[qasm3_ast.QuantumReset], + Qasm3Transformer.consolidate_qubit_registers( + unrolled_resets, + self._qubit_register_offsets, + self._global_qreg_size_map, + self._module._device_qubits, + ), + ) + if self._check_only: return [] return unrolled_resets - def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> list[qasm3_ast.QuantumBarrier]: + def _visit_barrier( # pylint: disable=too-many-locals, too-many-branches + self, barrier: qasm3_ast.QuantumBarrier + ) -> list[qasm3_ast.QuantumBarrier]: """Visit a barrier statement element. Args: @@ -749,8 +834,29 @@ def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> list[qasm3_ast.Qu return [] if not self._unroll_barriers: + if self._consolidate_qubits: + barrier = cast( + qasm3_ast.QuantumBarrier, + Qasm3Transformer.consolidate_qubit_registers( + barrier, + self._qubit_register_offsets, + self._global_qreg_size_map, + self._module._device_qubits, + ), + ) return [barrier] + if self._consolidate_qubits: + unrolled_barriers = cast( + list[qasm3_ast.QuantumBarrier], + Qasm3Transformer.consolidate_qubit_registers( + unrolled_barriers, + self._qubit_register_offsets, + self._global_qreg_size_map, + self._module._device_qubits, + ), + ) + return unrolled_barriers def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]: @@ -1244,7 +1350,7 @@ def _visit_phase_operation( return [operation] - def _visit_generic_gate_operation( # pylint: disable=too-many-branches + def _visit_generic_gate_operation( # pylint: disable=too-many-branches, too-many-statements self, operation: qasm3_ast.QuantumGate | qasm3_ast.QuantumPhase, ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, @@ -1261,6 +1367,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches negctrls = [] if ctrls is None: ctrls = [] + self._in_generic_gate_op_scope += 1 # only needs to be done once for a gate operation if ( @@ -1364,6 +1471,17 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls ] result = negs + result + negs # type: ignore + self._in_generic_gate_op_scope -= 1 + if self._consolidate_qubits and not self._in_generic_gate_op_scope: + result = cast( + list[qasm3_ast.QuantumGate | qasm3_ast.QuantumPhase], + Qasm3Transformer.consolidate_qubit_registers( + result, + self._qubit_register_offsets, + self._global_qreg_size_map, + self._module._device_qubits, + ), + ) if self._check_only: return [] @@ -1418,7 +1536,9 @@ def _visit_constant_declaration( statement.init_expression, validate_only=True ) self._check_variable_cast_type(statement, val_type, var_name, base_type, base_size, True) - variable = Variable(var_name, base_type, base_size, [], init_value, is_constant=True) + variable = Variable( + var_name, base_type, base_size, [], init_value, is_constant=True, span=statement.span + ) # cast + validation variable.value = Qasm3Validator.validate_variable_assignment_value( @@ -1549,6 +1669,7 @@ def _visit_classical_declaration( final_dimensions, init_value, is_register=isinstance(base_type, qasm3_ast.BitType), + span=statement.span, ) # validate the assignment @@ -2460,6 +2581,11 @@ def finalize(self, unrolled_stmts): """ # remove the gphase qubits if they use ALL qubits + if self._consolidate_qubits: + total_qubits = sum(self._global_qreg_size_map.values()) + if self._module._device_qubits is None: + self._module._device_qubits = total_qubits + unrolled_stmts = self._qubit_register_consolidation(unrolled_stmts, total_qubits) for stmt in unrolled_stmts: # Rule 1 if isinstance(stmt, qasm3_ast.QuantumPhase): diff --git a/tests/qasm3/test_device_qubits.py b/tests/qasm3/test_device_qubits.py new file mode 100644 index 00000000..9261e00b --- /dev/null +++ b/tests/qasm3/test_device_qubits.py @@ -0,0 +1,286 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module containing unit tests for the qubit register consolidation. + +""" + +import pytest + +from pyqasm.entrypoint import dumps, loads +from pyqasm.exceptions import ValidationError +from tests.utils import check_unrolled_qasm + + +def test_reset(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + qreg q2[3]; + reset q2; + reset q[1]; + """ + expected_qasm = """OPENQASM 3.0; + qubit[5] __PYQASM_QUBITS__; + include "stdgates.inc"; + reset __PYQASM_QUBITS__[2]; + reset __PYQASM_QUBITS__[3]; + reset __PYQASM_QUBITS__[4]; + reset __PYQASM_QUBITS__[1]; + """ + + result = loads(qasm, device_qubits=5) + result.unroll(consolidate_qubits=True) + check_unrolled_qasm(dumps(result), expected_qasm) + + +def test_barrier(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + qreg q2[3]; + barrier q2; + barrier q[1]; + """ + expected_qasm = """OPENQASM 3.0; + qubit[5] __PYQASM_QUBITS__; + include "stdgates.inc"; + barrier __PYQASM_QUBITS__[2]; + barrier __PYQASM_QUBITS__[3]; + barrier __PYQASM_QUBITS__[4]; + barrier __PYQASM_QUBITS__[1]; + """ + result = loads(qasm, device_qubits=5) + result.unroll(consolidate_qubits=True) + check_unrolled_qasm(dumps(result), expected_qasm) + + +def test_unrolled_barrier(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + qreg q2[3]; + qubit[2] q3; + barrier q[0]; + barrier q2; + barrier q; + barrier q3; + """ + expected_qasm = """OPENQASM 3.0; + qubit[7] __PYQASM_QUBITS__; + include "stdgates.inc"; + barrier __PYQASM_QUBITS__[0]; + barrier __PYQASM_QUBITS__[2:5]; + barrier __PYQASM_QUBITS__[:2]; + barrier __PYQASM_QUBITS__[5:]; + """ + result = loads(qasm, device_qubits=7) + result.unroll(unroll_barriers=False, consolidate_qubits=True) + check_unrolled_qasm(dumps(result), expected_qasm) + + +def test_measurement(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + qreg q2[3]; + bit[3] c; + measure q2 -> c; + c[0] = measure q[0]; + c = measure q[:3]; + c = measure q2; + measure q2[1] -> c[2]; + """ + expected_qasm = """OPENQASM 3.0; + qubit[7] __PYQASM_QUBITS__; + include "stdgates.inc"; + bit[3] c; + c[0] = measure __PYQASM_QUBITS__[4]; + c[1] = measure __PYQASM_QUBITS__[5]; + c[2] = measure __PYQASM_QUBITS__[6]; + c[0] = measure __PYQASM_QUBITS__[0]; + c[0] = measure __PYQASM_QUBITS__[0]; + c[1] = measure __PYQASM_QUBITS__[1]; + c[2] = measure __PYQASM_QUBITS__[2]; + c[0] = measure __PYQASM_QUBITS__[4]; + c[1] = measure __PYQASM_QUBITS__[5]; + c[2] = measure __PYQASM_QUBITS__[6]; + c[2] = measure __PYQASM_QUBITS__[5]; + """ + result = loads(qasm, device_qubits=7) + result.unroll(consolidate_qubits=True) + check_unrolled_qasm(dumps(result), expected_qasm) + + +def test_gates(): + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] data; + qubit[2] ancilla; + bit[3] c; + x data[3]; + cx data[0], ancilla[1]; + crx (0.1) ancilla[0], data[2]; + gate custom_rccx a, b, c{ + rccx a, b, c; + } + custom_rccx ancilla[0], data[1], data[0]; + if(c[0]){ + x data[0]; + cx data[1], ancilla[1]; + } + if(c[1] == 1){ + cx ancilla[0], data[2]; + } + """ + expected_qasm = """OPENQASM 3.0; + qubit[6] __PYQASM_QUBITS__; + include "stdgates.inc"; + bit[3] c; + x __PYQASM_QUBITS__[3]; + cx __PYQASM_QUBITS__[0], __PYQASM_QUBITS__[5]; + rz(1.5707963267948966) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.141592653589793) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.141592653589793) __PYQASM_QUBITS__[2]; + cx __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[2]; + rz(0) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.0915926535897933) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.141592653589793) __PYQASM_QUBITS__[2]; + cx __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[2]; + rz(0) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.191592653589793) __PYQASM_QUBITS__[2]; + rx(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(1.5707963267948966) __PYQASM_QUBITS__[2]; + rz(3.141592653589793) __PYQASM_QUBITS__[0]; + rx(1.5707963267948966) __PYQASM_QUBITS__[0]; + rz(4.71238898038469) __PYQASM_QUBITS__[0]; + rx(1.5707963267948966) __PYQASM_QUBITS__[0]; + rz(3.141592653589793) __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + rx(0.7853981633974483) __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + cx __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + rx(-0.7853981633974483) __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + cx __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + rx(0.7853981633974483) __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + cx __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + rx(-0.7853981633974483) __PYQASM_QUBITS__[0]; + h __PYQASM_QUBITS__[0]; + rz(3.141592653589793) __PYQASM_QUBITS__[0]; + rx(1.5707963267948966) __PYQASM_QUBITS__[0]; + rz(4.71238898038469) __PYQASM_QUBITS__[0]; + rx(1.5707963267948966) __PYQASM_QUBITS__[0]; + rz(3.141592653589793) __PYQASM_QUBITS__[0]; + if (c[0] == true) { + x __PYQASM_QUBITS__[0]; + cx __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[5]; + } + if (c[1] == true) { + cx __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[2]; + } + """ + result = loads(qasm, device_qubits=6) + result.unroll(consolidate_qubits=True) + check_unrolled_qasm(dumps(result), expected_qasm) + + +def test_validate(caplog): + with pytest.raises(ValidationError, match=r"Total qubits '4' exceed device qubits '3'."): + with caplog.at_level("ERROR"): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + bit[4] c; + for int i in [0:2] { + h q[0]; + } + """ + loads(qasm3_string, device_qubits=3).validate() + + +@pytest.mark.parametrize( + "qasm_code, error_message", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] data; + qubit[3] ancilla; + """, + r"Total qubits '(7)' exceed device qubits '(6)'.", + ), + ], +) # pylint: disable-next= too-many-arguments +def test_incorrect_device_qubits(qasm_code, error_message, caplog): + with pytest.raises(ValidationError) as err: + with caplog.at_level("ERROR"): + loads(qasm_code, device_qubits=6).unroll(consolidate_qubits=True) + assert error_message in str(err.value) + + +@pytest.mark.parametrize( + "qasm_code,error_message,error_span", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] data; + qubit[2] __PYQASM_QUBITS__; + """, + r"Variable '__PYQASM_QUBITS__' is already defined", + r"Error at line 5, column 12", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[6] data; + bit[2] __PYQASM_QUBITS__; + """, + r"Variable '__PYQASM_QUBITS__' is already defined", + r"Error at line 5, column 12", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[6] data; + bit[2] class_data; + int __PYQASM_QUBITS__; + """, + r"Variable '__PYQASM_QUBITS__' is already defined", + r"Error at line 6, column 12", + ), + ], +) # pylint: disable-next= too-many-arguments +def test_incorrect_qubit_reg(qasm_code, error_message, error_span, caplog): + with pytest.raises(ValidationError) as err: + with caplog.at_level("ERROR"): + loads(qasm_code, device_qubits=6).unroll(consolidate_qubits=True) + assert error_message in str(err.value) + assert error_span in caplog.text