Source code for deepquantum.qasm3

"""Converter between QubitCircuit and QASM3"""

import re
from typing import Any

import numpy as np
import torch

from .circuit import QubitCircuit
from .gate import (
    Barrier,
    CNOT,
    Fredkin,
    Hadamard,
    PauliX,
    PauliY,
    PauliZ,
    PhaseShift,
    Rx,
    Rxx,
    Ry,
    Ryy,
    Rz,
    Rzz,
    SDaggerGate,
    SGate,
    Swap,
    TDaggerGate,
    TGate,
    Toffoli,
    U3Gate,
)
from .operation import Channel, Gate, Layer, Operation

# ==============================================================================
#                 DeepQuantum Circuit to OpenQASM 3.0 Converter
# ==============================================================================


def _op_to_qasm3(op: Operation) -> str:
    """Helper function to convert a single deepquantum operation to an OpenQASM 3.0 string."""
    if isinstance(op, Layer):
        return '\n'.join([_op_to_qasm3(gate) for gate in op.gates])

    if isinstance(op, Barrier):
        qubits_str = ', '.join([f'q[{w}]' for w in op.wires])
        return f'barrier {qubits_str};'

    if not isinstance(op, Gate):
        return f'// Unsupported operation type: {op.__class__.__name__}'

    if isinstance(op, Channel):
        return f'// Quantum channels like {op.name} are not part of the OpenQASM 3.0 core specification.'

    # Gate name mapping
    name_map = {
        U3Gate: 'u',
        PhaseShift: 'p',
        PauliX: 'x',
        PauliY: 'y',
        PauliZ: 'z',
        Hadamard: 'h',
        SGate: 's',
        SDaggerGate: 'sdg',
        TGate: 't',
        TDaggerGate: 'tdg',
        Rx: 'rx',
        Ry: 'ry',
        Rz: 'rz',
        Swap: 'swap',
        CNOT: 'cx',
        Toffoli: 'ccx',
        Fredkin: 'cswap',
        Rxx: 'rxx',
        Ryy: 'ryy',
        Rzz: 'rzz',
    }

    qasm_name = name_map.get(type(op))
    if not qasm_name:
        return f'// Unsupported gate: {op.name}'

    # Parameters
    param_str = ''
    if hasattr(op, 'npara') and op.npara > 0:
        params = []
        if isinstance(op, U3Gate):
            params = [op.theta.item(), op.phi.item(), op.lambd.item()]
        elif hasattr(op, 'theta'):
            # Works for Rx, Ry, Rz, PhaseShift, Rxx, Ryy, Rzz
            params = [op.theta.item()]

        # Handle inverse operation for parametric gates
        if hasattr(op, 'inv_mode') and op.inv_mode:
            params = [-p for p in params]

        param_str = f'({", ".join(map(str, params))})'

    # Qubits and Controls
    controls = op.controls
    targets = op.wires

    # Implicit controls in gates like CNOT, Toffoli
    if isinstance(op, (CNOT, Toffoli, Fredkin)):
        qubits_str = ', '.join([f'q[{w}]' for w in targets])
        return f'{qasm_name} {qubits_str};'

    all_qubits = controls + targets
    qubits_str = ', '.join([f'q[{w}]' for w in all_qubits])

    # Control modifiers
    ctrl_modifiers = 'ctrl @ ' * len(controls)

    return f'{ctrl_modifiers}{qasm_name}{param_str} {qubits_str};'


[docs] def cir_to_qasm3(circuit: QubitCircuit) -> str: """Converts ``QubitCircuit`` to an OpenQASM 3.0 string. Args: circuit: The DeepQuantum circuit to convert. Returns: A string containing the OpenQASM 3.0 representation of the circuit. """ qasm_parts = ['OPENQASM 3.0;', 'include "stdgates.inc";'] num_qubits = circuit.nqubit qasm_parts.append(f'qubit[{num_qubits}] q;') # Declare classical bits if any measurements are defined if circuit.wires_measure: # Declare enough bits to cover all measured qubits max_measured_wire = max(circuit.wires_measure) if circuit.wires_measure else -1 num_classical_bits = max_measured_wire + 1 if num_classical_bits > 0: qasm_parts.append(f'bit[{num_classical_bits}] c;') # Convert operations for op in circuit.operators: qasm_line = _op_to_qasm3(op) if qasm_line: qasm_parts.append(qasm_line) # Add measurements if circuit.wires_measure: qasm_parts.append('\n// Measurements') for wire in sorted(circuit.wires_measure): qasm_parts.append(f'c[{wire}] = measure q[{wire}];') return '\n'.join(qasm_parts)
# ============================================================================== # OpenQASM 3.0 to DeepQuantum Circuit Converter # ==============================================================================
[docs] class GateDefinition: """Gate definition in OpenQASM 3.0""" def __init__(self, name: str, params: list[str], qubits: list[str], body: list[str]): self.name, self.params, self.qubits, self.body = name, params, qubits, body
[docs] def qasm3_to_cir(qasm_string: str) -> QubitCircuit: """Converts a full-featured OpenQASM 3.0 string to ``QubitCircuit``. Supports: `def`, `inv @`, `ctrl @`, and floating-point/negative `pow() @`. """ lines = [line.split('//')[0].strip() for line in qasm_string.strip().splitlines() if line.strip()] if not any(line.startswith('OPENQASM 3.0') for line in lines): raise ValueError('Input is not a valid OpenQASM 3.0 string (Header missing).') gate_definitions: dict[str, GateDefinition] = {} main_body_lines = [] i = 0 while i < len(lines): line = lines[i] if line.startswith('def '): header_line = line open_braces = header_line.count('{') body_start_line_offset = 1 if open_braces == 0: if i + 1 < len(lines) and lines[i + 1].strip() == '{': open_braces = 1 body_start_line_offset = 2 else: main_body_lines.append(header_line) i += 1 continue body_start_index = i + body_start_line_offset brace_scan_index = i + body_start_line_offset - 1 while brace_scan_index < len(lines) and open_braces > 0: brace_scan_index += 1 if brace_scan_index >= len(lines): break open_braces += lines[brace_scan_index].count('{') open_braces -= lines[brace_scan_index].count('}') body_lines = [l_.strip() for l_ in lines[body_start_index:brace_scan_index]] i = brace_scan_index + 1 try: header_content = header_line[3:].strip() if header_content.endswith('{'): header_content = header_content[:-1].strip() paren_match = re.match(r'(\w+)\s*\((.*?)\)\s*(.*)', header_content) if paren_match: name, params_str, qubits_str = paren_match.groups() else: name_match = re.match(r'(\w+)\s*(.*)', header_content) name, qubits_str = name_match.groups() params_str = '' params = [p.strip() for p in params_str.split(',')] if params_str else [] qubits = [q.strip() for q in qubits_str.strip().split(',')] if qubits_str.strip() else [] qubits = [q for q in qubits if q] gate_definitions[name] = GateDefinition(name, params, qubits, body_lines) except Exception: print(f"Warning: Could not parse gate definition header: '{header_line}'") else: main_body_lines.append(line) i += 1 num_qubits = 0 for line in main_body_lines: match = re.search(r'qubit\[(\d+)\]', line) if match: num_qubits = int(match.group(1)) break if num_qubits == 0: raise ValueError('Qubit declaration not found or zero qubits specified.') circuit = QubitCircuit(nqubit=num_qubits) # --- Helper Function to get gate matrix --- def get_gate_matrix( gate_name: str, params_str: str, gate_qubits_str: list[str], scope: dict[str, Any] ) -> torch.Tensor: """Dynamically builds the unitary matrix for a given gate call.""" n_gate_qubits = len(gate_qubits_str) # Create a fake QASM program to parse local_qubit_def = f'qubit[{n_gate_qubits}] q;' local_qubits_str = ', '.join([f'q[{i}]' for i in range(n_gate_qubits)]) param_part = f'({params_str})' if params_str else '' fake_line = f'{gate_name}{param_part} {local_qubits_str};' # Build a temporary full QASM string for the sub-parser temp_qasm = ['OPENQASM 3.0;', local_qubit_def] # We need the definitions to be available to the sub-parser for name, definition in gate_definitions.items(): params_def = f'({",".join(definition.params)})' if definition.params else '' qubits_def = ','.join(definition.qubits) body_def = '\n '.join(definition.body) temp_qasm.append(f'def {name}{params_def} {qubits_def} {{\n {body_def}\n}}') temp_qasm.append(fake_line) temp_circ = qasm3_to_cir('\n'.join(temp_qasm)) return temp_circ.get_unitary() def _process_qasm_lines( lines_to_process: list[str], circuit_obj: QubitCircuit, scope: dict[str, Any] | None = None, external_controls: list[int] | None = None, is_inverted: bool = False, ): if scope is None: scope = {} if external_controls is None: external_controls = [] gate_pattern = re.compile(r'((?:(?:inv|ctrl|pow\s*\(.*?\))\s*@\s*)*)(\w+)(?:\((.*?)\))?\s+(.*?);') processing_order = reversed(lines_to_process) if is_inverted else lines_to_process for line in processing_order: line = line.strip() if not line or line.startswith(('OPENQASM', 'include', 'qubit', 'bit', 'defcal')): continue if 'measure' in line: for m in re.findall(r'q\[(\d+)\]', line): if int(m) not in circuit_obj.wires_measure: circuit_obj.wires_measure.append(int(m)) continue if line.startswith('barrier'): qubits_str = line.replace('barrier', '').replace(';', '').strip() wires = [int(q.strip()[2:-1]) for q in qubits_str.split(',')] if qubits_str else [] circuit_obj.barrier(wires=wires if wires else None) continue match = gate_pattern.match(line) if not match: print(f"Warning: Could not parse line: '{line}'") continue modifiers_str, gate_name, params_str, qubits_str = match.groups() num_inv = modifiers_str.count('inv') num_ctrls = modifiers_str.count('ctrl') pow_match = re.search(r'pow\s*\((.*?)\)', modifiers_str) effectively_inverted = is_inverted ^ (num_inv % 2 == 1) power = 1.0 if pow_match: try: power = float(eval(pow_match.group(1), {'pi': np.pi, 'np': np})) except Exception: print(f"Warning: Could not parse power expression '{pow_match.group(1)}'.") continue if effectively_inverted: power = -power call_qubits_all_str = [q.strip() for q in qubits_str.split(',')] if int(power) != power: inline_controls_str = call_qubits_all_str[:num_ctrls] gate_qubits_actual_str = call_qubits_all_str[num_ctrls:] total_controls = external_controls + [int(q[2:-1]) for q in inline_controls_str] gate_qubits = [int(q[2:-1]) for q in gate_qubits_actual_str] try: base_unitary = get_gate_matrix(gate_name, params_str, gate_qubits_actual_str, scope) eigvals, eigvecs = torch.linalg.eig(base_unitary.to(dtype=torch.cfloat)) eigvals_pow = eigvals**power final_unitary = eigvecs @ torch.diag(eigvals_pow) @ torch.linalg.inv(eigvecs) circuit_obj.any(final_unitary, wires=gate_qubits, controls=total_controls) except Exception as e: print(f"Warning: Cannot apply non-integer power to gate from line '{line}'. Error: {e}") continue continue for _ in range(int(power)) if power >= 0 else range(int(-power)): loop_inverted = effectively_inverted if power >= 0 else not effectively_inverted if gate_name in gate_definitions: definition = gate_definitions[gate_name] inline_controls_str = call_qubits_all_str[:num_ctrls] gate_qubits_actual_str = call_qubits_all_str[num_ctrls:] inline_controls = [int(q[2:-1]) for q in inline_controls_str] total_external_controls = external_controls + inline_controls if len(gate_qubits_actual_str) != len(definition.qubits): print(f"Warning: Mismatched qubit count for gate '{gate_name}'.") continue qubit_map = dict(zip(definition.qubits, gate_qubits_actual_str, strict=True)) eval_scope = {'pi': np.pi, 'np': np} eval_scope.update(scope) call_params_evaluated = ( [eval(p.strip(), eval_scope) for p in params_str.split(',')] if params_str else [] ) if len(call_params_evaluated) != len(definition.params): print(f"Warning: Mismatched parameter count for gate '{gate_name}'.") continue new_scope = scope.copy() new_scope.update(zip(definition.params, call_params_evaluated, strict=True)) expanded_body = [] for body_line in definition.body: new_line = body_line for formal_param in definition.params: new_line = re.sub( r'\b' + re.escape(formal_param) + r'\b', str(new_scope.get(formal_param, formal_param)), new_line, ) for formal_qubit, actual_qubit in qubit_map.items(): new_line = re.sub(r'\b' + re.escape(formal_qubit) + r'\b', actual_qubit, new_line) expanded_body.append(new_line) _process_qasm_lines( expanded_body, circuit_obj, new_scope, external_controls=total_external_controls, is_inverted=loop_inverted, ) else: _apply_builtin_gate( circuit_obj, gate_name, params_str, call_qubits_all_str, num_ctrls, external_controls, loop_inverted, ) def _apply_builtin_gate( circuit_obj, gate_name, params_str, call_qubits_all_str, num_ctrls, external_controls, is_inverted ): try: qubit_indices = [int(q[2:-1]) for q in call_qubits_all_str] params = [float(eval(p, {'pi': np.pi})) for p in params_str.split(',')] if params_str else [] inline_controls = qubit_indices[:num_ctrls] all_qubits_after_mods = qubit_indices[num_ctrls:] total_controls = external_controls + inline_controls if is_inverted: if gate_name in ['rx', 'ry', 'rz', 'p', 'rxx', 'ryy', 'rzz']: params = [-p for p in params] elif gate_name == 'u': params = [-params[0], -params[2], -params[1]] elif gate_name == 's': gate_name = 'sdg' elif gate_name == 'sdg': gate_name = 's' elif gate_name == 't': gate_name = 'tdg' elif gate_name == 'tdg': gate_name = 't' if gate_name == 'cx': final_controls = total_controls + [all_qubits_after_mods[0]] target = all_qubits_after_mods[1] if len(final_controls) == 1: circuit_obj.cnot(control=final_controls[0], target=target) else: circuit_obj.x(wires=target, controls=final_controls) elif gate_name == 'cz': final_controls = total_controls + [all_qubits_after_mods[0]] target = all_qubits_after_mods[1] circuit_obj.z(wires=target, controls=final_controls) elif gate_name == 'ccx': final_controls = total_controls + all_qubits_after_mods[0:2] target = all_qubits_after_mods[2] if len(final_controls) == 2: circuit_obj.toffoli(control1=final_controls[0], control2=final_controls[1], target=target) else: circuit_obj.x(wires=target, controls=final_controls) elif gate_name == 'cswap': final_controls = total_controls + [all_qubits_after_mods[0]] targets = all_qubits_after_mods[1:3] if len(final_controls) == 1: circuit_obj.fredkin(control=final_controls[0], target1=targets[0], target2=targets[1]) else: circuit_obj.swap(wires=targets, controls=final_controls) else: targets = all_qubits_after_mods target_arg = targets[0] if len(targets) == 1 else targets if gate_name == 'h': circuit_obj.h(wires=target_arg, controls=total_controls) elif gate_name == 'x': circuit_obj.x(wires=target_arg, controls=total_controls) elif gate_name == 'y': circuit_obj.y(wires=target_arg, controls=total_controls) elif gate_name == 'z': circuit_obj.z(wires=target_arg, controls=total_controls) elif gate_name == 's': circuit_obj.s(wires=target_arg, controls=total_controls) elif gate_name == 'sdg': circuit_obj.sdg(wires=target_arg, controls=total_controls) elif gate_name == 't': circuit_obj.t(wires=target_arg, controls=total_controls) elif gate_name == 'tdg': circuit_obj.tdg(wires=target_arg, controls=total_controls) elif gate_name == 'swap': circuit_obj.swap(wires=target_arg, controls=total_controls) elif gate_name == 'rx': circuit_obj.rx(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'ry': circuit_obj.ry(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'rz': circuit_obj.rz(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'p': circuit_obj.p(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'u': circuit_obj.u3(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'rxx': circuit_obj.rxx(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'ryy': circuit_obj.ryy(wires=target_arg, inputs=params, controls=total_controls) elif gate_name == 'rzz': circuit_obj.rzz(wires=target_arg, inputs=params, controls=total_controls) else: print(f"Warning: Unsupported built-in gate '{gate_name}'") except Exception as e: print(f"Warning: Error applying gate '{gate_name}'. Qubits: {call_qubits_all_str}. Error: {e}") _process_qasm_lines(main_body_lines, circuit) circuit.wires_measure.sort() return circuit