# 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 for conversions between Cirq Circuits and QASM strings
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Optional
import cirq
import pyqasm
from cirq import ops, value
from qbraid._version import __version__ as qbraid_version
from qbraid.transpiler.annotations import weight
from ._utils import _validate_resolved_parameters
if TYPE_CHECKING:
from qbraid.programs.typer import Qasm2StringType
_CREG = re.compile(r"^creg\s+(?P<name>\w+)\s*\[(?P<size>\d+)\]\s*;\s*$")
_BIT_INDEX = re.compile(r"^(?P<register>.+)_(?P<index>\d+)$")
def _order_cregs_by_key(qasm: str) -> str:
"""Emit ``creg`` declarations in measurement-key order rather than moment order.
Cirq writes one ``creg`` per measurement key in the order the measurements are
scheduled, so an idle qubit whose measurement packs into an earlier moment gets its
register declared first. Nothing is wrong with the QASM -- each register is named and
its ``measure`` is correct -- but consumers that flatten the registers into one
readout region do so in declaration order (``openqasm3_to_pyquil``, matching how
``RigettiJob`` reads them back, #1314). Declaration order therefore has to reflect the
keys, or the flattened bits come out permuted relative to the source.
Registers are sorted the way ``cirq_to_pyquil`` orders its merged measurements: by the
``(register, index)`` parsed from the key, so ``m_c_0`` precedes ``m_c_2``. Names that
do not carry an index keep their relative order, and the declarations are rewritten in
place so surrounding statements are untouched.
"""
lines = qasm.splitlines(keepends=True)
positions = [i for i, line in enumerate(lines) if _CREG.match(line)]
if len(positions) < 2:
return qasm
def sort_key(index: int) -> tuple:
name = _CREG.match(lines[index])["name"]
match = _BIT_INDEX.match(name)
if match is None:
return (1, "", 0, index)
return (0, match["register"], int(match["index"]), index)
reordered = [lines[i] for i in sorted(positions, key=sort_key)]
for slot, line in zip(positions, reordered, strict=True):
lines[slot] = line
return "".join(lines)
@value.value_equality
class ZPowGate(cirq.ZPowGate):
"""A single qubit gate for rotations around the
Z axis of the Bloch sphere.
"""
def _qasm_(self, args: cirq.QasmArgs, qubits: tuple[cirq.Qid, ...]) -> Optional[str]:
args.validate_version("2.0")
if self._global_shift == 0:
if self._exponent == 0.25:
return args.format("t {0};\n", qubits[0])
if self._exponent == -0.25:
return args.format("tdg {0};\n", qubits[0])
if self._exponent == 0.5:
return args.format("s {0};\n", qubits[0])
if self._exponent == -0.5:
return args.format("sdg {0};\n", qubits[0])
if self._exponent == 1:
return args.format("z {0};\n", qubits[0])
return args.format("p({0:half_turns}) {1};\n", self._exponent, qubits[0])
return args.format("rz({0:half_turns}) {1};\n", self._exponent, qubits[0])
def map_zpow_and_unroll(circuit: cirq.Circuit) -> cirq.Circuit:
"""Map ZPowGate to RZ and unroll circuit"""
def _map_zpow(op: cirq.Operation, _: int) -> cirq.OP_TREE:
if isinstance(op.gate, cirq.ZPowGate):
yield ZPowGate(exponent=op.gate.exponent, global_shift=op.gate.global_shift)(
op.qubits[0]
)
else:
yield op
return cirq.map_operations_and_unroll(circuit, _map_zpow)
def _to_qasm_output(
circuit: cirq.Circuit,
header: Optional[str] = None,
precision: int = 10,
qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT,
) -> cirq.QasmOutput:
"""Returns a QASM object equivalent to the circuit.
Args:
header: A multi-line string that is placed in a comment at the top
of the QASM. Defaults to a cirq version specifier.
precision: Number of digits to use when representing numbers.
qubit_order: Determines how qubits are ordered in the QASM
register.
"""
if header is None:
header = f"Generated from qBraid v{qbraid_version}"
qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(circuit.all_qubits())
return cirq.QasmOutput(
operations=circuit.all_operations(),
qubits=qubits,
header=header,
precision=precision,
version="2.0",
)
[docs]
@weight(1)
def cirq_to_qasm2(
circuit: cirq.Circuit,
header: Optional[str] = None,
precision: int = 10,
qubit_order: cirq.QubitOrderOrList = ops.QubitOrder.DEFAULT,
) -> Qasm2StringType:
"""Returns a QASM string representing the input Cirq circuit.
Args:
circuit: Cirq circuit to convert to a QASM string.
Returns:
Qasm2StringType: QASM string equivalent to the input Cirq circuit.
Raises:
ProgramConversionError: If the circuit contains unresolved parameters.
"""
_validate_resolved_parameters(circuit, "OpenQASM 2")
circuit = map_zpow_and_unroll(circuit)
qasm = _order_cregs_by_key(str(_to_qasm_output(circuit, header, precision, qubit_order)))
# format the qasm before returning
return pyqasm.dumps(pyqasm.loads(qasm))