diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index 7b2fecc4..abe9bedc 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -35,9 +35,8 @@ def __init__(self, name, gate_args, gate_regs): def _get_qiskit_gates(): """ - Create and return a dictionary containing custom gates needed - for "qiskit" mode. These include a subset of gates usually defined - in the file "qelib1.inc". + Create and return a dictionary containing a few commonly used qiskit gates + that are not predefined in qutip-qip. Returns a dictionary mapping gate names to QuTiP gates. """ @@ -179,17 +178,22 @@ class QasmProcessor: Class which holds variables used in processing QASM code. """ - def __init__(self, commands, mode="qiskit", version="2.0"): + def __init__(self, commands, mode="default", version="2.0"): self.qubit_regs = {} self.cbit_regs = {} self.num_qubits = 0 self.num_cbits = 0 - self.qasm_gates = {} + self.qasm_gates = {} # Custom defined QASM gates + if mode not in ["default", "external_only", "predefined_only"]: + warnings.warn( + "Unknown parsing mode, using the default mode instead." + ) + mode = "default" self.mode = mode self.version = version self.predefined_gates = set(["CX", "U"]) - if self.mode == "qiskit": + if self.mode != "external_only": self.qiskitgates = set( [ "u3", @@ -221,6 +225,8 @@ def __init__(self, commands, mode="qiskit", version="2.0"): self.qiskitgates ) + # A set of available gates, including both predefined gate and + # custom defined gates from `qelib1.inc` (added later). self.gate_names = deepcopy(self.predefined_gates) for gate in self.predefined_gates: self.qasm_gates[gate] = QasmGate( @@ -245,7 +251,11 @@ def _process_includes(self): filename = command[1].strip('"') - if self.mode == "qiskit" and filename == "qelib1.inc": + if self.mode == "predefined_only": + warnings.warn( + "Ignoring external gate definition" + " in the predefined_only mode." + ) continue if os.path.exists(filename): @@ -264,8 +274,14 @@ def _process_includes(self): ) prev_index = curr_index + 1 else: - raise ValueError(command[1] + ": such a file does not exist") + if self.mode == "default": + warnings.warn(command[1] + "not found, ignored.") + else: + raise ValueError( + command[1] + ": such a file does not exist" + ) + # Insert the custom gate configurations to the list of commands expanded_commands += self.commands[prev_index:] self.commands = expanded_commands @@ -275,8 +291,10 @@ def _initialize_pass(self): each user-defined gate, process register declarations. """ - gate_defn_mode = False - open_bracket_mode = False + gate_defn_mode = False # If in the middle of defining a custom gate + open_bracket_mode = ( + False # If in the middle of defining a decomposition + ) unprocessed = [] @@ -290,6 +308,7 @@ def _initialize_pass(self): else: raise SyntaxError("QASM: incorrect bracket formatting") elif open_bracket_mode: + # Define the decomposition of custom QASM gate if command[0] == "{": raise SyntaxError("QASM: incorrect bracket formatting") elif command[0] == "}": @@ -312,11 +331,11 @@ def _initialize_pass(self): gate_added = self.qasm_gates[name] curr_gate.gates_inside.append([name, gate_args, gate_regs]) elif command[0] == "gate": + # Custom definition of gates. gate_name = command[1] gate_args, gate_regs = _gate_processor(command[1:]) curr_gate = QasmGate(gate_name, gate_args, gate_regs) gate_defn_mode = True - elif command[0] == "qreg": groups = re.match(r"(.*)\[(.*)\]", "".join(command[1:])) if groups: @@ -647,7 +666,7 @@ def _add_qiskit_gates( classical_controls=classical_controls, classical_control_value=classical_control_value, ) - if name == "cx": + elif name == "cx": qc.add_gate( "CNOT", targets=int(regs[1]), @@ -715,7 +734,7 @@ def _add_predefined_gates( classical_controls=classical_controls, classical_control_value=classical_control_value, ) - elif name in self.qiskitgates and self.mode == "qiskit": + elif name in self.qiskitgates and self.mode != "external_only": self._add_qiskit_gates( qc, name, @@ -808,7 +827,7 @@ def _final_pass(self, qc): """ custom_gates = {} - if self.mode == "qiskit": + if self.mode != "external_only": custom_gates = _get_qiskit_gates() for command in self.commands: @@ -847,7 +866,7 @@ def _final_pass(self, qc): raise SyntaxError(err) -def read_qasm(qasm_input, mode="qiskit", version="2.0", strmode=False): +def read_qasm(qasm_input, mode="default", version="2.0", strmode=False): """ Read OpenQASM intermediate representation (https://github.com/Qiskit/openqasm) and return @@ -860,10 +879,13 @@ def read_qasm(qasm_input, mode="qiskit", version="2.0", strmode=False): File location or String Input for QASM file to be imported. In case of string input, the parameter strmode must be True. mode : str - QASM mode to be read in. When mode is "qiskit", - the "qelib1.inc" include is automatically included, - without checking externally. Otherwise, each include is - processed. + Parsing mode for the qasm file. + - "default": For predefined gates in qutip-qip, use the predefined + version, otherwise use the custom gate defined in qelib1.inc. + The predefined gate can usually be further processed (e.g. decomposed) + within qutip-qip. + - "predefined_only": Use only the predefined gates in qutip-qip. + - "external_only": Use only the gate defined in qelib1.inc, except for CX and QASMU gate. version : str QASM version of the QASM file. Only version 2.0 is currently supported. strmode : bool diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 95434c8a..c0aa21ce 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -1,11 +1,13 @@ import pytest import numpy as np from pathlib import Path +import warnings +import qutip from qutip_qip.qasm import read_qasm, circuit_to_qasm_str from qutip_qip.circuit import QubitCircuit from qutip import tensor, rand_ket, basis, rand_dm, identity -from qutip_qip.operations import cnot, ry, Measurement +from qutip_qip.operations import cnot, ry, Measurement, swap @pytest.mark.parametrize(["filename", "error", "error_message"], [ @@ -127,7 +129,10 @@ def test_export_import(): qc.add_gate("T", targets=[0]) # qc.add_gate("CSIGN", targets=[0], controls=[1]) - read_qc = read_qasm(circuit_to_qasm_str(qc), strmode=True) + # The generated code by default has a inclusion statement of + # qelib1.inc, which will trigger a warning when read. + with warnings.catch_warnings(): + read_qc = read_qasm(circuit_to_qasm_str(qc), strmode=True) props = qc.propagators() read_props = read_qc.propagators() @@ -145,3 +150,66 @@ def test_read_qasm(): qc = read_qasm(filepath) qc2 = read_qasm(filepath2) assert True + + +def test_parsing_mode(tmp_path): + mode = "qiskit" + qasm_input_string = ( + 'OPENQASM 2.0;\n\ncreg c[2];' + '\nqreg q[2];cx q[0],q[1];\n' + ) + with pytest.warns(UserWarning) as record_warning: + read_qasm( + qasm_input_string, + mode=mode, + strmode=True, + ) + assert "Unknown parsing mode" in record_warning[0].message.args[0] + + mode = "predefined_only" + qasm_input_string = ( + 'OPENQASM 2.0;\ninclude "qelib1.inc"\n\ncreg c[2];' + '\nqreg q[2];swap q[0],q[1];\n' + ) + with pytest.raises(SyntaxError): + with pytest.warns(UserWarning) as record_warning: + circuit = read_qasm( + qasm_input_string, + mode=mode, + strmode=True, + ) + assert ( + "Ignoring external gate definition in the predefined_only mode." + in record_warning[0].message.args[0] + ) + + mode = "external_only" + file_path = tmp_path / "custom_swap.inc" + file_path.write_text( + "gate cx c,t { CX c,t; }\n" + "gate swap a,b { cx a,b; cx b,a; cx a,b; }\n" + ) + qasm_input_string = ( + 'OPENQASM 2.0;\ninclude "' + + str(file_path) + + '"\ncreg c[2];' + '\nqreg q[2];swap q[0],q[1];\n' + ) + circuit = read_qasm( + qasm_input_string, + mode=mode, + strmode=True, + ) + propagator = circuit.compute_unitary() + + fidelity = qutip.average_gate_fidelity(propagator, swap()) + pytest.approx(fidelity, 1.0) + + circuit = read_qasm( + qasm_input_string, + strmode=True, + ) + propagator = circuit.compute_unitary() + + fidelity = qutip.average_gate_fidelity(propagator, swap()) + pytest.approx(fidelity, 1.0)