Source code for qbraid.transpiler.conversions.openqasm3.openqasm3_to_pyquil

# 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 defining OpenQASM 3 to pyQuil conversion function.

"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

import pyqasm
from openqasm3 import ast
from qbraid_core._import import LazyLoader

from qbraid.transpiler.annotations import weight
from qbraid.transpiler.exceptions import ProgramConversionError

pyquil = LazyLoader("pyquil", globals(), "pyquil")
pyquil_gates = LazyLoader("pyquil_gates", globals(), "pyquil.gates")
pyquil_quilbase = LazyLoader("pyquil_quilbase", globals(), "pyquil.quilbase")

if TYPE_CHECKING:
    from pyquil import Program

    from qbraid.programs.typer import QasmStringType

# OpenQASM 3 delay time units -> seconds (pyQuil's DELAY duration is in seconds).
# "dt" is backend-dependent and has no fixed conversion, so it is unsupported.
_TIME_UNIT_SECONDS = {"ns": 1e-9, "us": 1e-6, "ms": 1e-3, "s": 1.0}

# OpenQASM gate name -> pyQuil gate constructor name. pyQuil's signature is
# uniform: fn(*params, *qubits), so a single dispatch handles every entry.
_GATE_MAP = {
    # one-qubit, no parameters
    "i": "I",
    "id": "I",
    "x": "X",
    "y": "Y",
    "z": "Z",
    "h": "H",
    "s": "S",
    "t": "T",
    # one-qubit, parameterized
    "rx": "RX",
    "ry": "RY",
    "rz": "RZ",
    "p": "PHASE",
    "phase": "PHASE",
    # two-qubit, no parameters
    "cx": "CNOT",
    "cnot": "CNOT",
    "cz": "CZ",
    "swap": "SWAP",
    "iswap": "ISWAP",
    # two-qubit, parameterized
    "cp": "CPHASE",
    "cphase": "CPHASE",
    "rxx": "RXX",
    "ryy": "RYY",
    "rzz": "RZZ",
    "xy": "XY",
    # three-qubit
    "ccx": "CCNOT",
    "toffoli": "CCNOT",
    "cswap": "CSWAP",
}

# Multi-qubit gates that pyQuil supports natively. Marking them external to
# ``pyqasm.unroll`` keeps them intact (mapped directly via ``_GATE_MAP``) instead
# of decomposing them into long RZ/RX/CNOT sequences, giving exact, compact
# output. Limited to multi-qubit gates so single-qubit gate modifiers (e.g.
# ``ctrl @ x``) still decompose normally.
_NATIVE_EXTERNAL_GATES = ["cp", "iswap", "rxx", "ryy", "rzz", "xy", "cswap"]


def _flat_qubit(qubit: ast.IndexedIdentifier, offsets: dict[str, int]) -> int:
    """Map an (unrolled) qubit reference to a flat pyQuil integer index.

    After ``pyqasm.unroll()`` every qubit reference is an ``IndexedIdentifier``
    (single-qubit registers are normalized to ``name[0]``).
    """
    return offsets[qubit.name.name] + qubit.indices[0][0].value


def _duration_seconds(duration: ast.DurationLiteral) -> float:
    """Convert an OpenQASM 3 delay duration to seconds for pyQuil's DELAY."""
    unit = duration.unit.name
    if unit not in _TIME_UNIT_SECONDS:
        raise ProgramConversionError(f"Unsupported delay unit: {unit}")
    return duration.value * _TIME_UNIT_SECONDS[unit]


def _literal_bit(node: ast.Expression) -> int:
    """Read a 0/1 from the literal side of a branch comparison."""
    if isinstance(node, ast.BooleanLiteral):
        return 1 if node.value else 0
    if isinstance(node, ast.IntegerLiteral) and node.value in (0, 1):
        return node.value
    raise ProgramConversionError("Unsupported branch comparison value (expected 0 or 1).")


def _branch_target(
    condition: ast.Expression, clbit_offsets: dict[str, int], ro: object
) -> tuple[object, int]:
    """Resolve a branch condition ``c[i] == 0|1`` to ``(memory_ref, expected_bit)``."""
    if not isinstance(condition, ast.BinaryExpression) or condition.op.name != "==":
        raise ProgramConversionError("Unsupported branch condition (expected '==').")
    lhs, rhs = condition.lhs, condition.rhs
    if isinstance(lhs, (ast.BooleanLiteral, ast.IntegerLiteral)):
        lhs, rhs = rhs, lhs
    if not isinstance(lhs, ast.IndexExpression) or ro is None:
        raise ProgramConversionError("Unsupported branch condition target.")
    name = lhs.collection.name
    if name not in clbit_offsets:
        raise ProgramConversionError(f"Unknown classical register in branch: {name}")
    ro_index = clbit_offsets[name] + lhs.index[0].value
    return ro[ro_index], _literal_bit(rhs)


[docs] @weight(1.0) # pylint: disable-next=too-many-statements def openqasm3_to_pyquil(program: QasmStringType | ast.Program) -> Program: """Returns a pyQuil Program equivalent to the input OpenQASM 3 program. Supports the standard gate set (including gate modifiers and controlled gates, which ``pyqasm`` decomposes during unrolling), measurement, ``barrier`` (-> pyQuil ``FENCE``), ``reset`` (-> ``RESET``), ``delay`` (-> ``DELAY``), and ``if (c == 0|1)`` classical feedforward (-> conditional ``JUMP-WHEN``). Declared-but-idle qubits are padded with identity so the operator dimension matches the source register width. Args: program (str or openqasm3.ast.Program): OpenQASM 3 program to convert. Returns: pyquil.Program: pyQuil Program equivalent to the input program. Raises: ProgramConversionError: If the program is malformed or contains a gate/statement that is not supported by the conversion. """ try: module = pyqasm.loads(program) module.validate() except Exception as e: raise ProgramConversionError("QASM program is not well-formed.") from e module.unroll(external_gates=_NATIVE_EXTERNAL_GATES) unrolled = module.unrolled_ast quil = pyquil.Program() # pass 1: build flat qubit + classical-bit offset maps qubit_offsets: dict[str, int] = {} clbit_offsets: dict[str, int] = {} num_qubits = 0 num_clbits = 0 for statement in unrolled.statements: if isinstance(statement, ast.QubitDeclaration): size = statement.size.value if statement.size is not None else 1 qubit_offsets[statement.qubit.name] = num_qubits num_qubits += size elif isinstance(statement, ast.ClassicalDeclaration) and isinstance( statement.type, ast.BitType ): size = statement.type.size.value if statement.type.size is not None else 1 clbit_offsets[statement.identifier.name] = num_clbits num_clbits += size ro = quil.declare("ro", "BIT", num_clbits) if num_clbits else None # track which declared qubits are acted on, so idle ones can be padded with I # (keeps the operator dimension equal to the declared register width) used_qubits: set[int] = set() def flat(qubit: ast.IndexedIdentifier | ast.Identifier) -> int: index = _flat_qubit(qubit, qubit_offsets) used_qubits.add(index) return index # pylint: disable-next=too-many-statements def emit(statement: ast.Statement, prog: Program) -> None: # statements with no observable pyQuil equivalent that are safe to drop: # QuantumPhase = global phase (gphase), unobservable (pyqasm emits it when # decomposing gates such as rzz; equivalence holds up to global phase). if isinstance( statement, (ast.QubitDeclaration, ast.ClassicalDeclaration, ast.Include, ast.QuantumPhase), ): return if isinstance(statement, ast.QuantumBarrier): prog.inst(pyquil_quilbase.Fence([flat(q) for q in statement.qubits])) elif isinstance(statement, ast.QuantumReset): prog.inst(pyquil_gates.RESET(flat(statement.qubits))) elif isinstance(statement, ast.DelayInstruction): qubits = [flat(q) for q in statement.qubits] prog.inst(pyquil_quilbase.DelayQubits(qubits, _duration_seconds(statement.duration))) elif isinstance(statement, ast.QuantumGate): # pyqasm.unroll() strips gate modifiers and decomposes non-basis gates # (e.g. sxdg, u, u3, controlled/inverse forms) into the gates below. name = statement.name.name.lower() params = [arg.value for arg in statement.arguments] qubits = [flat(q) for q in statement.qubits] if name == "sdg": prog.inst(pyquil_gates.S(*qubits).dagger()) elif name == "tdg": prog.inst(pyquil_gates.T(*qubits).dagger()) elif name == "sx": prog.inst(pyquil_gates.RX(math.pi / 2, *qubits)) elif name in _GATE_MAP: prog.inst(getattr(pyquil_gates, _GATE_MAP[name])(*params, *qubits)) else: raise ProgramConversionError(f"Unsupported gate: {name}") elif isinstance(statement, ast.QuantumMeasurementStatement): src = flat(statement.measure.qubit) if statement.target is not None and ro is not None: target = statement.target ro_index = clbit_offsets[target.name.name] + target.indices[0][0].value prog.inst(pyquil_gates.MEASURE(src, ro[ro_index])) else: prog.inst(pyquil_gates.MEASURE(src, None)) elif isinstance(statement, ast.BranchingStatement): # classical feedforward: if (c[i] == 1) { ... } else { ... } -> JUMP-WHEN reg, expected = _branch_target(statement.condition, clbit_offsets, ro) then_block, else_block = statement.if_block, statement.else_block if expected == 0: # run the if-body when the bit is 0 => swap into the else slot then_block, else_block = else_block, then_block then_prog = pyquil.Program() for inner in then_block or []: emit(inner, then_prog) else_prog = None if else_block: else_prog = pyquil.Program() for inner in else_block: emit(inner, else_prog) prog.if_then(reg, then_prog, else_prog) else: raise ProgramConversionError(f"Unsupported statement: {statement}") for statement in unrolled.statements: emit(statement, quil) # pad idle declared qubits with identity so the operator spans the full register for index in range(num_qubits): if index not in used_qubits: quil += pyquil_gates.I(index) return quil