"""Translation utilities between OpenQASM 2.0 and OriginIR.
This module provides bidirectional translation between OpenQASM 2.0 and OriginIR
quantum circuit representations. It includes direct mapping dictionaries,
opcode conversion functions, and multi-controlled gate decomposition.
Key exports:
OriginIR_QASM2_dict: Mapping from OriginIR to QASM2 operations.
QASM2_OriginIR_dict: Mapping from QASM2 to OriginIR operations.
direct_mapping_qasm2_to_oir: Function for direct QASM2 to OriginIR lookup.
get_opcode_from_QASM2: Convert QASM2 operation to OriginIR opcode.
get_QASM2_from_opcode: Convert OriginIR opcode to QASM2 operation.
decompose_mcx_qasm_text: Decompose multi-controlled X gates for QASM2.
decompose_mcu_qasm_text: Decompose multi-controlled single-qubit gates for QASM2.
"""
import math
from typing import List, Optional, Tuple, Union
from .qasm_spec import available_qasm_gates
__all__ = [
"OriginIR_QASM2_dict",
"QASM2_OriginIR_dict",
"direct_mapping_qasm2_to_oir",
"get_opcode_from_QASM2",
"get_QASM2_from_opcode",
"decompose_mcx_qasm_text",
"decompose_mcu_qasm_text",
]
qasm2_oir_mapping = {
("id", "I"),
("h", "H"),
("x", "X"),
("y", "Y"),
("z", "Z"),
("s", "S"),
("sx", "SX"),
("t", "T"),
("cx", "CNOT"),
("cz", "CZ"),
("swap", "SWAP"),
("ccx", "TOFFOLI"),
("cswap", "CSWAP"),
("rx", "RX"),
("ry", "RY"),
("rz", "RZ"),
("u1", "U1"),
("u2", "U2"),
("u3", "U3"),
("rxx", "XX"),
("ryy", "YY"),
("rzz", "ZZ"),
}
# direct mapping from OriginIR opcode to QASM2 operation
QASM2_OriginIR_dict = {qasm: oir for (qasm, oir) in qasm2_oir_mapping}
# direct mapping from QASM2 operation to OriginIR
OriginIR_QASM2_dict = {oir: qasm for (qasm, oir) in qasm2_oir_mapping}
[docs]
def direct_mapping_qasm2_to_oir(qasm2_operation):
"""
Return the corresponding OriginIR by given QASM2 operation.
Return None when there is no direct-mapping.
Note: There are also operations that do not sastify "direct mapping"
from QASM -> OIR or OIR -> QASM.
"""
return QASM2_OriginIR_dict.get(qasm2_operation, None)
[docs]
def get_opcode_from_QASM2(operation, qubits, cbits, parameters):
"""Here list all supported operations of OpenQASM2.0 and its corresponding operation
in OriginIR in UnifiedQuantum.
Opcode Definition:
opcodes = (operation,qubits,cbit,parameter,dagger_flag,control_qubits_set)
"""
# 1-qubit gates
if operation == "id":
return ("I", qubits, cbits, parameters, False, None)
elif operation == "h":
return ("H", qubits, cbits, parameters, False, None)
elif operation == "x":
return ("X", qubits, cbits, parameters, False, None)
elif operation == "y":
return ("Y", qubits, cbits, parameters, False, None)
elif operation == "z":
return ("Z", qubits, cbits, parameters, False, None)
elif operation == "s":
return ("S", qubits, cbits, parameters, False, None)
elif operation == "sdg":
return ("S", qubits, cbits, parameters, True, None)
elif operation == "sx":
return ("SX", qubits, cbits, parameters, False, None)
elif operation == "sxdg":
return ("SX", qubits, cbits, parameters, True, None)
elif operation == "t":
return ("T", qubits, cbits, parameters, False, None)
elif operation == "tdg":
return ("T", qubits, cbits, parameters, True, None)
# 2-qubit gates
elif operation == "cx":
return ("CNOT", qubits, cbits, parameters, False, None)
elif operation == "cy":
return ("Y", qubits[1], cbits, parameters, False, [qubits[0]])
elif operation == "cz":
return ("CZ", qubits, cbits, parameters, False, None)
elif operation == "swap":
return ("SWAP", qubits, cbits, parameters, False, None)
elif operation == "ch":
return ("H", qubits, cbits, parameters, False, [qubits[0]])
# 3-qubit gates
elif operation == "ccx":
return ("TOFFOLI", qubits, cbits, parameters, False, None)
elif operation == "cswap":
return ("CSWAP", qubits, cbits, parameters, False, None)
# 4-qubit gates
elif operation == "c3x":
return ("X", qubits[3], cbits, parameters, False, qubits[0:3])
# 1-qubit 1-parameter gates
elif operation == "rx":
return ("RX", qubits, cbits, parameters, False, None)
elif operation == "ry":
return ("RY", qubits, cbits, parameters, False, None)
elif operation == "rz":
return ("RZ", qubits, cbits, parameters, False, None)
elif operation == "u1":
return ("U1", qubits, cbits, parameters, False, None)
# 1-qubit 2-parameter gates
elif operation == "u2":
return ("U2", qubits, cbits, parameters, False, None)
elif operation == "u0":
return ("U0", qubits, cbits, parameters, False, None)
# 1-qubit 3-parameter gates
elif operation == "u" or operation == "u3":
return ("U3", qubits, cbits, parameters, False, None)
# 2-qubit 1-parameter gates
elif operation == "rxx":
return ("XX", qubits, cbits, parameters, False, None)
elif operation == "ryy":
return ("YY", qubits, cbits, parameters, False, None)
elif operation == "rzz":
return ("ZZ", qubits, cbits, parameters, False, None)
elif operation == "cu1":
return ("U1", qubits[1], cbits, parameters, False, [qubits[0]])
elif operation == "crx":
return ("RX", qubits[1], cbits, parameters, False, [qubits[0]])
elif operation == "cry":
return ("RY", qubits[1], cbits, parameters, False, [qubits[0]])
elif operation == "crz":
return ("RZ", qubits[1], cbits, parameters, False, [qubits[0]])
# 2-qubit 3-parameter gates
elif operation == "cu3":
return ("U3", qubits[1], cbits, parameters, False, [qubits[0]])
return None
[docs]
def decompose_mcx_qasm_text(controls: List[int], target: int, qubit_num: int) -> str:
"""Decompose an n-control X (MCX) gate into QASM 2.0 Toffoli-ladder statements.
Uses a clean-ancilla ladder (Barenco et al. 1995, adapted): n-2 workspace
qubits are borrowed from existing circuit qubits not involved in the gate.
The workspace qubits must be in state ``|0⟩`` at the call site; they are
restored to ``|0⟩`` after the decomposition.
Args:
controls: Ordered list of n ≥ 4 control qubit indices.
target: Target qubit index.
qubit_num: Total number of qubits declared in the circuit (``qreg q[N]``).
Returns:
Multi-line QASM 2.0 string (Toffoli gates only; no semicolon-separated
single line — the returned string may contain ``\\n``).
Raises:
NotImplementedError: Not enough workspace qubits are available in the
circuit. Use OriginIR export (which supports arbitrary-width
controlled gates natively) or add workspace qubits to the circuit.
"""
n = len(controls)
assert n >= 4, f"decompose_mcx_qasm_text requires n>=4, got {n}"
n_workspace = n - 2
used = set(controls) | {target}
workspace = [q for q in range(qubit_num) if q not in used][:n_workspace]
if len(workspace) < n_workspace:
raise NotImplementedError(
f"MCX with {n} controls needs {n_workspace} workspace qubits, "
f"but only {len(workspace)} are available in a {qubit_num}-qubit circuit. "
"Add workspace qubits to the circuit, or use OriginIR export which "
"supports arbitrary-width multi-controlled gates natively."
)
lines: List[str] = []
# Forward ladder: compute AND(c0, c1, …, c_{n-2}) into workspace[-1].
lines.append(f"ccx q[{controls[0]}], q[{controls[1]}], q[{workspace[0]}];")
for i in range(1, n_workspace):
lines.append(f"ccx q[{controls[i + 1]}], q[{workspace[i - 1]}], q[{workspace[i]}];")
# Apply to target using the last control and the accumulated AND.
lines.append(f"ccx q[{controls[-1]}], q[{workspace[-1]}], q[{target}];")
# Uncompute workspace (reverse order).
for i in range(n_workspace - 1, 0, -1):
lines.append(f"ccx q[{controls[i + 1]}], q[{workspace[i - 1]}], q[{workspace[i]}];")
lines.append(f"ccx q[{controls[0]}], q[{controls[1]}], q[{workspace[0]}];")
return "\n".join(lines)
[docs]
def get_QASM2_from_opcode(
opcode,
) -> Tuple[str, Union[int, List[int]], Union[int, List[int]], Union[float, List[float]]]:
"""
Return the corresponding QASM2 operation by given OriginIR operation.
Note: Only a subset of QASM2 operations are supported in UnifiedQuantum.
"""
(operation, qubits, cbits, parameters, dagger_flag, control_qubits_set) = opcode
# check if the operation is supported
if operation not in OriginIR_QASM2_dict:
raise NotImplementedError(f"The operation {operation} is not supported in UnifiedQuantum.")
operation_qasm2 = OriginIR_QASM2_dict[operation]
# Dagger flag is supported only when operation is S or T or SX
# These gates will skip the dagger flag:
# I(id), H(h), X(x), Y(y), Z(z), CNOT(cx), CZ(cz), SWAP(swap), TOFFOLI(ccx)
# These gates will invert the angle parameter:
# RX(rx), RY(ry), RZ(rz), U1(u1), XX(xx), YY(yy), ZZ(zz),
if dagger_flag:
if operation_qasm2 in ["s", "t", "sx"]:
operation_qasm2 += "dg"
elif operation_qasm2 in ["id", "h", "x", "y", "z", "cx", "cz", "swap", "ccx"]:
pass
elif operation_qasm2 in ["rx", "ry", "rz", "u1", "rxx", "ryy", "rzz"]:
parameters = -parameters
elif operation_qasm2 == "u3":
# U3(θ,φ,λ)† = U3(-θ,-λ,-φ)
if isinstance(parameters, list):
theta, phi, lam = parameters[0], parameters[1], parameters[2]
else:
theta, phi, lam = parameters
parameters = [-theta, -lam, -phi]
else:
raise NotImplementedError(f"The operation {operation} with dagger flag is not supported in UnifiedQuantum.")
# When there is no control qubits, return
if not control_qubits_set:
return operation_qasm2, qubits, cbits, parameters
# Work with a copy so we never mutate the original opcode's control list.
ctrl_list = list(control_qubits_set)
# When there are 1 control qubit, add the control keyword
if len(ctrl_list) == 1:
operation_qasm2 = "c" + operation_qasm2
# check whether this operation is still supported
if operation_qasm2 not in available_qasm_gates:
raise NotImplementedError(
f"The operation {operation_qasm2} with control qubit is not supported in UnifiedQuantum."
)
qubits_out = ctrl_list + (list(qubits) if isinstance(qubits, list) else [qubits])
return operation_qasm2, qubits_out, cbits, parameters
# When there are 2 control qubits, add the control keyword
if len(ctrl_list) == 2:
operation_qasm2 = "cc" + operation_qasm2
# check whether this operation is still supported
if operation_qasm2 not in available_qasm_gates:
raise NotImplementedError(
f"The operation {operation_qasm2} with control qubit is not supported in UnifiedQuantum."
)
qubits_out = ctrl_list + (list(qubits) if isinstance(qubits, list) else [qubits])
return operation_qasm2, qubits_out, cbits, parameters
# When there are 3 control qubits, add the control keyword
if len(ctrl_list) == 3:
operation_qasm2 = "c3" + operation_qasm2
# check whether this operation is still supported
if operation_qasm2 not in available_qasm_gates:
raise NotImplementedError(
f"The operation {operation_qasm2} with control qubit is not supported in UnifiedQuantum."
)
qubits_out = ctrl_list + (list(qubits) if isinstance(qubits, list) else [qubits])
return operation_qasm2, qubits_out, cbits, parameters
# n >= 4 controls: signal to opcode_to_line_qasm that decomposition is needed.
# Supported single-qubit gates are decomposed via conjugation or ABC method.
_mcu_supported_gates = {
"x",
"z",
"y",
"s",
"sdg",
"rz",
"rx",
"u1",
"u3",
"ry",
"sx",
"sxdg",
"h",
}
if operation_qasm2 not in _mcu_supported_gates:
raise NotImplementedError(
f"QASM 2.0 export does not support the gate '{operation}' with "
f"{len(ctrl_list)} control qubits. Use OriginIR export instead."
)
# Return a sentinel that opcode_to_line_qasm will intercept.
# dagger_flag has already been applied to operation_qasm2/parameters above,
# so we pass False here to avoid double-applying.
target_qubit = qubits if not isinstance(qubits, list) else qubits[0]
return "_MCU_DECOMP_", (ctrl_list, target_qubit, operation_qasm2, parameters, False), None, None
def _qasm_gate(name: str, qubit: int, param: Optional[float] = None) -> str:
"""Format a single QASM 2.0 gate line."""
if param is not None:
return f"{name}({param}) q[{qubit}];"
return f"{name} q[{qubit}];"
def _scalar(params: Union[float, List[float]]) -> float:
"""Extract a scalar from a parameter that may be a float or a single-element list."""
if isinstance(params, list):
return float(params[0])
return float(params)
def _abc_decompose(
phi: float,
theta: float,
lam: float,
controls: List[int],
target: int,
qubit_num: int,
) -> str:
"""Decompose a multi-controlled gate via the ABC method (Barenco et al.).
Given a gate with SU(2) ZYZ decomposition V = RZ(phi)·RY(theta)·RZ(lam),
compute A, B, C such that CBA = I and AXBXC = V, then emit:
A → MCX → B → MCX → C (on the target qubit).
"""
mcx_lines = decompose_mcx_qasm_text(controls, target, qubit_num)
# A = RZ(phi - lam)
# B = RZ(pi - lam) · RY(theta/2) · RZ((lam - phi)/2 - pi)
# C = RZ((lam - phi)/2) · RY(theta/2) · RZ(lam)
a_phi_lam = phi - lam
b_rz1 = math.pi - lam
b_ry = theta / 2
b_rz2 = (lam - phi) / 2 - math.pi
c_rz1 = (lam - phi) / 2
c_ry = theta / 2
c_rz2 = lam
lines: List[str] = []
# A
if abs(a_phi_lam) > 1e-15:
lines.append(_qasm_gate("rz", target, a_phi_lam))
# MCX
lines.append(mcx_lines)
# B
if abs(b_rz1) > 1e-15:
lines.append(_qasm_gate("rz", target, b_rz1))
if abs(b_ry) > 1e-15:
lines.append(_qasm_gate("ry", target, b_ry))
if abs(b_rz2) > 1e-15:
lines.append(_qasm_gate("rz", target, b_rz2))
# MCX
lines.append(mcx_lines)
# C
if abs(c_rz1) > 1e-15:
lines.append(_qasm_gate("rz", target, c_rz1))
if abs(c_ry) > 1e-15:
lines.append(_qasm_gate("ry", target, c_ry))
if abs(c_rz2) > 1e-15:
lines.append(_qasm_gate("rz", target, c_rz2))
return "\n".join(lines)
[docs]
def decompose_mcu_qasm_text(
controls: List[int],
target: int,
qubit_num: int,
gate_qasm: str,
params: Optional[Union[float, List[float]]],
) -> str:
"""Decompose an n-control single-qubit gate into QASM 2.0 statements.
Uses two strategies:
- **Tier 1 (conjugation, 1 MCX)**: For gates where G = U·X·U†.
Supported: X, Z, Y, S, Sdg, RZ, RX, U1.
- **Tier 2 (ABC method, 2 MCX)**: For gates requiring the general
Barenco decomposition A·MCX·B·MCX·C.
Supported: U3, RY, SX, H.
Args:
controls: Ordered list of n ≥ 4 control qubit indices.
target: Target qubit index.
qubit_num: Total number of qubits declared in the circuit.
gate_qasm: QASM 2.0 gate name (already dagger-adjusted).
params: Gate parameter(s), already dagger-adjusted if applicable.
Returns:
Multi-line QASM 2.0 string.
Raises:
NotImplementedError: Gate is not supported for decomposition.
"""
n = len(controls)
assert n >= 4, f"decompose_mcu_qasm_text requires n>=4, got {n}"
mcx_lines = decompose_mcx_qasm_text(controls, target, qubit_num)
# --- Tier 1: simple conjugation G = U · X · U† (1 MCX) ---
if gate_qasm == "x":
return mcx_lines
if gate_qasm == "z":
return f"h q[{target}];\n{mcx_lines}\nh q[{target}];"
if gate_qasm == "y":
return f"sdg q[{target}];\nh q[{target}];\n{mcx_lines}\nh q[{target}];\ns q[{target}];"
if gate_qasm == "s":
return f"h q[{target}];\nt q[{target}];\n{mcx_lines}\ntdg q[{target}];\nh q[{target}];"
if gate_qasm == "sdg":
return f"h q[{target}];\ntdg q[{target}];\n{mcx_lines}\nt q[{target}];\nh q[{target}];"
if gate_qasm == "rz":
half = _scalar(params) / 2
return f"rz({half}) q[{target}];\nh q[{target}];\n{mcx_lines}\nh q[{target}];\nrz({half}) q[{target}];"
if gate_qasm == "rx":
half = _scalar(params) / 2
return (
f"h q[{target}];\nrz({half}) q[{target}];\nh q[{target}];\n"
f"{mcx_lines}\n"
f"h q[{target}];\nrz({half}) q[{target}];\nh q[{target}];"
)
if gate_qasm == "u1":
half = _scalar(params) / 2
return f"rz({half}) q[{target}];\nh q[{target}];\n{mcx_lines}\nh q[{target}];\nrz({half}) q[{target}];"
# --- Tier 2: ABC decomposition (2 MCX) ---
if gate_qasm == "u3":
p = params if isinstance(params, list) else [params]
theta, phi, lam = p[0], p[1], p[2]
# U3(theta, phi, lam) — ZYZ decomposition: phi, theta, lam
return _abc_decompose(phi, theta, lam, controls, target, qubit_num)
if gate_qasm == "ry":
theta = _scalar(params)
# RY(theta) — ZYZ: phi=0, theta=theta, lam=0
return _abc_decompose(0.0, theta, 0.0, controls, target, qubit_num)
if gate_qasm == "sx":
# SX — SU(2) part ZYZ: phi=-pi/2, theta=pi/2, lam=pi/2
return _abc_decompose(
-math.pi / 2,
math.pi / 2,
math.pi / 2,
controls,
target,
qubit_num,
)
if gate_qasm == "sxdg":
# SXdg — SU(2) part ZYZ: phi=-pi/2, theta=-pi/2, lam=pi/2
return _abc_decompose(
-math.pi / 2,
-math.pi / 2,
math.pi / 2,
controls,
target,
qubit_num,
)
if gate_qasm == "h":
# H — SU(2) part ZYZ: phi=0, theta=pi, lam=0
return _abc_decompose(0.0, math.pi, 0.0, controls, target, qubit_num)
raise NotImplementedError(
f"QASM 2.0 export does not support decomposing '{gate_qasm}' with "
f"{n} control qubits. Use OriginIR export instead."
)