QuantumArchitect-MCP / src /core /circuit_parser.py
Deminiko
Initial commit: QuantumArchitect-MCP quantum circuit MCP server with Gradio UI
6ce350d
"""
Circuit Parser - Converts QASM strings and other formats into the internal DAG representation.
"""
import re
from typing import Any
from .dag_representation import CircuitDAG
from .exceptions import CircuitParseError, InvalidQASMError
class CircuitParser:
"""
Parser for quantum circuit representations.
Supports OpenQASM 2.0/3.0 and JSON circuit dictionaries.
"""
# Standard gate patterns for QASM 2.0
QASM2_GATE_PATTERN = re.compile(
r'^(\w+)(?:\(([\d\.,\s\*\/\+\-\w]+)\))?\s+([\w\[\],\s]+);$'
)
# Qubit/register patterns
QREG_PATTERN = re.compile(r'^qreg\s+(\w+)\[(\d+)\];$')
CREG_PATTERN = re.compile(r'^creg\s+(\w+)\[(\d+)\];$')
QUBIT_REF_PATTERN = re.compile(r'(\w+)\[(\d+)\]')
# Single qubit gates (no parameters)
SINGLE_GATES = {'x', 'y', 'z', 'h', 's', 't', 'sdg', 'tdg', 'id', 'sx', 'sxdg'}
# Parameterized single qubit gates
PARAM_SINGLE_GATES = {'rx', 'ry', 'rz', 'p', 'u1', 'u2', 'u3', 'u'}
# Two qubit gates
TWO_QUBIT_GATES = {'cx', 'cz', 'cy', 'swap', 'iswap', 'cnot', 'cp', 'crx', 'cry', 'crz'}
# Three qubit gates
THREE_QUBIT_GATES = {'ccx', 'cswap', 'toffoli', 'fredkin'}
@classmethod
def parse_qasm(cls, qasm_string: str) -> CircuitDAG:
"""
Parse OpenQASM 2.0/3.0 string into CircuitDAG.
Args:
qasm_string: Valid QASM code
Returns:
CircuitDAG representation
"""
lines = qasm_string.strip().split('\n')
# Track quantum and classical registers
qregs: dict[str, tuple[int, int]] = {} # name -> (start_index, size)
cregs: dict[str, tuple[int, int]] = {}
total_qubits = 0
total_cbits = 0
# First pass: find registers
for line_num, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith('//') or line.startswith('OPENQASM') or line.startswith('include'):
continue
qreg_match = cls.QREG_PATTERN.match(line)
if qreg_match:
name, size = qreg_match.groups()
qregs[name] = (total_qubits, int(size))
total_qubits += int(size)
continue
creg_match = cls.CREG_PATTERN.match(line)
if creg_match:
name, size = creg_match.groups()
cregs[name] = (total_cbits, int(size))
total_cbits += int(size)
continue
if total_qubits == 0:
raise InvalidQASMError("No quantum registers defined", qasm_string[:200])
# Create DAG
dag = CircuitDAG(total_qubits, total_cbits)
# Second pass: parse gates
for line_num, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith('//') or line.startswith('OPENQASM') or line.startswith('include'):
continue
if cls.QREG_PATTERN.match(line) or cls.CREG_PATTERN.match(line):
continue
# Parse gate
try:
cls._parse_gate_line(line, dag, qregs, cregs, line_num)
except Exception as e:
raise CircuitParseError(str(e), line_num)
return dag
@classmethod
def _parse_gate_line(
cls,
line: str,
dag: CircuitDAG,
qregs: dict[str, tuple[int, int]],
cregs: dict[str, tuple[int, int]],
line_num: int
) -> None:
"""Parse a single gate line and add to DAG."""
# Handle barrier
if line.startswith('barrier'):
qubit_str = line[7:].strip().rstrip(';')
qubits = cls._resolve_qubits(qubit_str, qregs)
dag.add_gate('barrier', qubits)
return
# Handle measurement
if line.startswith('measure'):
parts = line[7:].strip().rstrip(';').split('->')
if len(parts) != 2:
raise CircuitParseError(f"Invalid measure syntax: {line}", line_num)
qubits = cls._resolve_qubits(parts[0].strip(), qregs)
cbits = cls._resolve_cbits(parts[1].strip(), cregs)
for q, c in zip(qubits, cbits):
dag.add_gate('measure', [q], classical_bits=[c])
return
# Handle reset
if line.startswith('reset'):
qubit_str = line[5:].strip().rstrip(';')
qubits = cls._resolve_qubits(qubit_str, qregs)
for q in qubits:
dag.add_gate('reset', [q])
return
# General gate pattern
gate_match = cls.QASM2_GATE_PATTERN.match(line)
if not gate_match:
raise CircuitParseError(f"Cannot parse line: {line}", line_num)
gate_name = gate_match.group(1).lower()
params_str = gate_match.group(2)
qubits_str = gate_match.group(3)
# Parse parameters
params: list[float] = []
if params_str:
try:
params = [cls._eval_param(p.strip()) for p in params_str.split(',')]
except Exception:
raise CircuitParseError(f"Invalid parameters: {params_str}", line_num)
# Parse qubits
qubits = cls._resolve_qubits(qubits_str, qregs)
# Validate gate
if gate_name in cls.SINGLE_GATES:
if len(qubits) != 1:
raise CircuitParseError(
f"Gate {gate_name} expects 1 qubit, got {len(qubits)}", line_num
)
elif gate_name in cls.PARAM_SINGLE_GATES:
if len(qubits) != 1:
raise CircuitParseError(
f"Gate {gate_name} expects 1 qubit, got {len(qubits)}", line_num
)
elif gate_name in cls.TWO_QUBIT_GATES:
if len(qubits) != 2:
raise CircuitParseError(
f"Gate {gate_name} expects 2 qubits, got {len(qubits)}", line_num
)
elif gate_name in cls.THREE_QUBIT_GATES:
if len(qubits) != 3:
raise CircuitParseError(
f"Gate {gate_name} expects 3 qubits, got {len(qubits)}", line_num
)
dag.add_gate(gate_name, qubits, params)
@classmethod
def _resolve_qubits(cls, qubit_str: str, qregs: dict[str, tuple[int, int]]) -> list[int]:
"""Resolve qubit references to absolute indices."""
qubits: list[int] = []
for match in cls.QUBIT_REF_PATTERN.finditer(qubit_str):
reg_name, idx = match.groups()
if reg_name not in qregs:
raise CircuitParseError(f"Unknown register: {reg_name}")
start_idx, size = qregs[reg_name]
qubit_idx = int(idx)
if qubit_idx >= size:
raise CircuitParseError(
f"Qubit index {qubit_idx} out of range for register {reg_name}[{size}]"
)
qubits.append(start_idx + qubit_idx)
return qubits
@classmethod
def _resolve_cbits(cls, cbit_str: str, cregs: dict[str, tuple[int, int]]) -> list[int]:
"""Resolve classical bit references to absolute indices."""
cbits: list[int] = []
for match in cls.QUBIT_REF_PATTERN.finditer(cbit_str):
reg_name, idx = match.groups()
if reg_name not in cregs:
raise CircuitParseError(f"Unknown classical register: {reg_name}")
start_idx, size = cregs[reg_name]
cbit_idx = int(idx)
if cbit_idx >= size:
raise CircuitParseError(
f"Classical bit index {cbit_idx} out of range for register {reg_name}[{size}]"
)
cbits.append(start_idx + cbit_idx)
return cbits
@classmethod
def _eval_param(cls, param_str: str) -> float:
"""Safely evaluate a parameter expression."""
import math
# Allow pi and basic math
safe_dict = {
'pi': math.pi,
'PI': math.pi,
'e': math.e,
'sqrt': math.sqrt,
'sin': math.sin,
'cos': math.cos,
'tan': math.tan,
'exp': math.exp,
}
# Clean the expression
param_str = param_str.replace('^', '**')
return float(eval(param_str, {"__builtins__": {}}, safe_dict))
@classmethod
def parse_json_circuit(cls, circuit_dict: dict[str, Any]) -> CircuitDAG:
"""
Parse a JSON circuit dictionary into CircuitDAG.
Expected format:
{
"num_qubits": 2,
"num_classical_bits": 2,
"gates": [
{"name": "h", "qubits": [0]},
{"name": "cx", "qubits": [0, 1]},
{"name": "measure", "qubits": [0], "classical_bits": [0]}
]
}
"""
num_qubits = circuit_dict.get('num_qubits', 0)
num_cbits = circuit_dict.get('num_classical_bits', 0)
if num_qubits == 0:
raise CircuitParseError("num_qubits must be specified and > 0")
dag = CircuitDAG(num_qubits, num_cbits)
for gate_dict in circuit_dict.get('gates', []):
name = gate_dict.get('name', '').lower()
qubits = gate_dict.get('qubits', [])
params = gate_dict.get('params', [])
classical_bits = gate_dict.get('classical_bits', [])
if not name:
raise CircuitParseError("Gate must have a 'name' field")
if not qubits:
raise CircuitParseError(f"Gate '{name}' must have 'qubits' field")
dag.add_gate(name, qubits, params, classical_bits)
return dag