Source code for uniqc.qasm.qasm_base_parser

"""OpenQASM 2.0 base parser module.

This module provides the base parser for OpenQASM 2.0 quantum circuit representation,
including parsing qreg/creg definitions, quantum operations, and measurement statements.

Key exports:
    OpenQASM2_BaseParser: Base parser class for OpenQASM 2.0 circuits.
"""

__all__ = ["OpenQASM2_BaseParser"]
from typing import List, Tuple
from uniqc.circuit_builder.qcircuit import Circuit
from uniqc.originir.originir_base_parser import OriginIR_BaseParser
from uniqc.circuit_builder.translate_qasm2_oir import get_opcode_from_QASM2
from .qasm_line_parser import OpenQASM2_LineParser
from .exceptions import NotSupportedGateError, RegisterDefinitionError, RegisterNotFoundError, RegisterOutOfRangeError


[docs] class OpenQASM2_BaseParser: """Parser for OpenQASM 2.0 quantum circuit representation. Attributes: qregs: List of quantum register tuples (name, size). cregs: List of classical register tuples (name, size). n_qubit: Total number of qubits. n_cbit: Total number of classical bits. program_body: List of operation opcodes. measure_qubits: List of measurement tuples (qubit, cbit). """ def __init__(self): self.qregs = list() self.cregs = list() self.n_qubit = None self.n_cbit = None self.program_body = list() # contain the opcodes self.raw_qasm = None self.formatted_qasm = None # for qasm statement collection self.collected_qregs_str = list() self.collected_cregs_str = list() self.collected_measurements_str = list() self.program_body_str = list() # contain strs of the program body # for measurement mapping self.measure_qubits : List[Tuple[int, int]] = list() def _format_and_check(self): '''Format the original qasm code and check if it is valid. Currently, this is a simple parser, so that "gate" defines are not supported, and 'if' statements are not supported. A canonical format of qasm code is like: OPENQASM 2.0; include "qelib1.inc"; qreg q[n]; <other qreg definitions> creg c[m]; <other creg definitions> <program_body> These rules will be applied in the formatted qasm code. 1. OPENQASM 2.0 and include "qelib1.inc" will be removed. 2. qreg definitions are collected together; so as creg definitions. 3. program body is line-wise, separated by semicolons 4. measurements must be at the end of the program body. 5. barriers will be ignored. ''' if self.raw_qasm is None: raise ValueError("No raw qasm code provided.") # check if there is "gate" definitions if 'gate' in self.raw_qasm and '{' in self.raw_qasm: raise NotSupportedGateError("Gate definitions are not supported yet.") # TODO: Current check is too naive - uses substring match which causes false positives # (e.g., "Unified" contains "if"). Should use regex to match QASM if keyword: r'\bif\s*\(' # check if there is "if" statements if 'if' in self.raw_qasm: raise NotSupportedGateError("If statements are not supported yet.") collected_qregs = list() collected_cregs = list() collected_measurements = list() program_body = list() # split all codes by semicolons codes = self.raw_qasm.split(';') for code in codes: # strip leading and trailing whitespaces code = code.strip() # remove comments and OPENQASM/include statements if code.startswith('//'): continue elif code == '': continue elif code.startswith('include'): continue elif code.startswith('OPENQASM'): continue elif code.startswith('barrier'): continue # handle qreg and creg definitions elif code.startswith('qreg'): collected_qregs.append(code) elif code.startswith('creg'): collected_cregs.append(code) elif code.startswith('measure'): collected_measurements.append(code) else: program_body.append(code) ret_qasm = ('{};\n' '{};\n' '{};\n' '{};'.format( ';\n'.join(collected_qregs), ';\n'.join(collected_cregs), ';\n'.join(program_body), ';\n'.join(collected_measurements) )) return ret_qasm, collected_qregs, collected_cregs, program_body, collected_measurements @staticmethod def _compute_id(regs, reg_name, reg_id): id = 0 for stored_reg_name, stored_reg_size in regs: if stored_reg_name == reg_name: if reg_id >= stored_reg_size: raise RegisterOutOfRangeError() return id + reg_id id += stored_reg_size raise RegisterNotFoundError() def _get_qubit_id(self, qreg_name, qreg_id): try: qubit_id = OpenQASM2_BaseParser._compute_id(self.qregs, qreg_name, qreg_id) return qubit_id except RegisterNotFoundError: raise RegisterNotFoundError('Cannot find qreg {}, (defined = {})'.format( qreg_name, self.collected_qregs_str )) except RegisterOutOfRangeError: raise RegisterOutOfRangeError('qreg {}[{}] out of range.)'.format( qreg_name, qreg_id )) def _get_cbit_id(self, creg_name, creg_id): try: cbit_id = OpenQASM2_BaseParser._compute_id(self.cregs, creg_name, creg_id) return cbit_id except RegisterNotFoundError: raise RegisterNotFoundError('Cannot find creg {}, (defined = {})'.format( creg_name, self.collected_cregs_str )) except RegisterOutOfRangeError: raise RegisterOutOfRangeError('creg {}[{}] out of range.)'.format( creg_name, creg_id )) @staticmethod def _check_regs(collected_regs, reg_handler): # check whether qregs have the same name names = set() regs = list() total_size = 0 if len(collected_regs) == 0: raise RegisterDefinitionError("Register is empty") for reg_str in collected_regs: name, size = reg_handler(reg_str) if name in names: raise RegisterDefinitionError("Duplicate name") names.add(name) regs.append((name, size)) total_size += size return total_size, regs def _process_measurements(self): for measurement in self.collected_measurements_str: qreg_name, qreg_id, creg_name, creg_id = OpenQASM2_LineParser.handle_measure(measurement) qid = self._get_qubit_id(qreg_name, qreg_id) cid = self._get_cbit_id(creg_name, creg_id) self.measure_qubits.append((qid, cid))
[docs] def parse(self, raw_qasm): """Parse an OpenQASM 2.0 string and populate internal state. Args: raw_qasm: OpenQASM 2.0 string to parse. """ self.raw_qasm = raw_qasm # format, and check if QASM code is valid # also return the collected statements (self.formatted_qasm, self.collected_qregs_str, self.collected_cregs_str, self.program_body_str, self.collected_measurements_str) = self._format_and_check() # process the total number of qubit try: self.n_qubit, self.qregs = OpenQASM2_BaseParser._check_regs(self.collected_qregs_str, OpenQASM2_LineParser.handle_qreg) except RegisterDefinitionError as e: raise RegisterDefinitionError("QReg Definition Error.\n" f"Internal error: \n{str(e)}") # process the total number of cbit try: self.n_cbit, self.cregs = OpenQASM2_BaseParser._check_regs(self.collected_cregs_str, OpenQASM2_LineParser.handle_creg) except RegisterDefinitionError as e: raise RegisterDefinitionError("CReg Definition Error.\n" f"Internal error: \n{str(e)}") # process measurements self._process_measurements() # process program body for line in self.program_body_str: operation, qubits, cbits, parameters = OpenQASM2_LineParser.parse_line(line) if operation is None: continue # transform the qubit from regname+index to qubit_id # Note: register's validity is checked through _get_qubit_id if qubits: if isinstance(qubits, list): qubits = [self._get_qubit_id(qubit[0], qubit[1]) for qubit in qubits] else: qubits = self._get_qubit_id(qubits[0], qubits[1]) if cbits: if isinstance(cbits, list): cbits = [self._get_cbit_id(cbit[0], cbit[1]) for cbit in cbits] else: cbits = self._get_cbit_id(cbits[0], cbits[1]) # convert parameter to a scalar value if parameters and isinstance(parameters, list) and len(parameters) == 1: parameters = parameters[0] # transform into opcodes # opcodes = (operation,qubits,cbit,parameter,dagger_flag,control_qubits_set) opcode = get_opcode_from_QASM2(operation, qubits, cbits, parameters) # check if opcode is correctely converted if opcode is None: raise NotImplementedError("Opcode is not converted correctly for " f"line: {line}.\n" f"operation: {operation}" f"qubits: {qubits}" f"cbits: {cbits}" f"parameters: {parameters}" ) self.program_body.append(opcode)
[docs] def to_originir(self): """Convert parsed OpenQASM data to OriginIR string. Returns: str: OriginIR string representation, including any ``MEASURE`` statements collected from the QASM source. """ oir_parser = OriginIR_BaseParser() oir_parser.n_qubit = self.n_qubit oir_parser.n_cbit = self.n_cbit oir_parser.program_body = self.program_body # Without this, OriginIR output silently drops all measurements. oir_parser.measure_qubits = list(self.measure_qubits) return oir_parser.to_extended_originir()
[docs] def to_circuit(self) -> Circuit: """ The function coverts OpenQASM string into uniqc.Circuit object. In the initilization phase, we need to notice that OriginIR-based quantum circuit doe not need to specify how many qregs and cregs used. Parameters: - qasm_str: The quantum circuit of intersts in the OpenQASM format. Returns: - origin_qcirc: The quantum circuit of intersts in the OriginIR format. """ # Create an empty Circuit object origincircuit = Circuit() # Split the QASM string into lines and parse each line for opcode in self.program_body: # unpack as 6 paramters (operation, qubits, params, cbits, dagger, control_qubits) origincircuit.add_gate(*opcode) # add measurements to the circuit, sort by cbit id measure_list = sorted(self.measure_qubits, key=lambda x: x[1]) for qubit, cbit in measure_list: origincircuit.measure(qubit) return origincircuit