From 0e843bd15f264204bef172fff15efcf63b9cd88a Mon Sep 17 00:00:00 2001 From: David Yonge-Mallo Date: Thu, 9 May 2024 05:43:49 +0200 Subject: [PATCH 1/2] Fix parsing of OpenQASM code with spaces between phase parameters. Add round-trip tests from pyzx to qiskit back to pyzx using both OpenQASM 2 and 3. Fixes #225. --- pyzx/circuit/qasmparser.py | 5 ++-- tests/test_qasm.py | 56 +++++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/pyzx/circuit/qasmparser.py b/pyzx/circuit/qasmparser.py index 81fdaf27..8f8f5448 100644 --- a/pyzx/circuit/qasmparser.py +++ b/pyzx/circuit/qasmparser.py @@ -123,12 +123,13 @@ def extract_command_parts(self, c: str) -> Tuple[str,List[Fraction],List[str]]: c = re.sub(r"^bit\[(\d+)] (\w+)$", r"creg \2[\1]", c) c = re.sub(r"^qubit\[(\d+)] (\w+)$", r"qreg \2[\1]", c) c = re.sub(r"^(\w+)\[(\d+)] = measure (\w+)\[(\d+)]$", r"measure \3[\4] -> \1[\2]", c) - name, rest = c.split(" ",1) + right_bracket = c.find(")") + name, rest = c.split(" ", 1) if right_bracket == -1\ + else [c[:right_bracket+1], c[right_bracket+1:]] args = [s.strip() for s in rest.split(",") if s.strip()] left_bracket = name.find('(') phases = [] if left_bracket != -1: - right_bracket = name.find(')') if right_bracket == -1: raise TypeError(f"Mismatched bracket: {name}.") vals = name[left_bracket+1:right_bracket].split(',') diff --git a/tests/test_qasm.py b/tests/test_qasm.py index cdf7c24a..07bd472c 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -40,8 +40,8 @@ try: from qiskit import quantum_info, transpile from qiskit.circuit import QuantumCircuit - from qiskit.qasm2 import dumps - from qiskit.qasm3 import loads + from qiskit.qasm2 import dumps as dumps2 + from qiskit.qasm3 import loads as loads3, dumps as dumps3 except ImportError: QuantumCircuit = None @@ -232,10 +232,10 @@ def compare_gate_matrix_with_qiskit(gates, num_qubits: int, num_angles: int, qas qasm = setup + \ ", ".join( [f"q[{i}]" for i in range(num_qubits)]) + ";\n" - c = Circuit.from_qasm(qasm) - pyzx_matrix = c.to_matrix() + pyzx_circuit = Circuit.from_qasm(qasm) + pyzx_matrix = pyzx_circuit.to_matrix() - for g in c.gates: + for g in pyzx_circuit.gates: for b in g.to_basic_gates(): self.assertListEqual(b.to_basic_gates(), [b], f"\n{gate}.to_basic_gates() contains non-basic gate") @@ -244,16 +244,28 @@ def compare_gate_matrix_with_qiskit(gates, num_qubits: int, num_angles: int, qas qiskit_qasm = setup + \ ", ".join([f"q[{i}]" for i in reversed( range(num_qubits))]) + ";\n" - qc = QuantumCircuit.from_qasm_str( - qiskit_qasm) if qasm_version == 2 else loads(qiskit_qasm) + qc = QuantumCircuit.from_qasm_str(qiskit_qasm) if qasm_version == 2 else loads3(qiskit_qasm) qiskit_matrix = quantum_info.Operator(qc).data + + # Check that pyzx and qiskit produce the same tensor from the same qasm, modulo qubit endianness. self.assertTrue(compare_tensors(pyzx_matrix, qiskit_matrix, False), f"Gate: {gate}\nqasm:\n{qasm}\npyzx_matrix:\n{pyzx_matrix}\nqiskit_matrix:\n{qiskit_matrix}") - s = c.to_qasm(qasm_version) - round_trip = Circuit.from_qasm(s) - self.assertEqual(c.qubits, round_trip.qubits) - self.assertListEqual(c.gates, round_trip.gates) + # Check internal round-trip (pyzx to qasm to pyzx) results in the same circuit. + qasm_from_pyzx = pyzx_circuit.to_qasm(qasm_version) + pyzx_round_trip = Circuit.from_qasm(qasm_from_pyzx) + self.assertEqual(pyzx_circuit.qubits, pyzx_round_trip.qubits) + self.assertListEqual(pyzx_circuit.gates, pyzx_round_trip.gates) + + # Check external round-trip (pyzx to qasm to qiskit to qasm to pyzx) results in the same circuit. + # Note that the endianness is reversed when going out and again when coming back in, so the overall + # result is no change. + qiskit_from_qasm = (QuantumCircuit.from_qasm_str(qasm_from_pyzx) if qasm_version == 2 + else loads3(qasm_from_pyzx)) + pyzx_from_qiskit = Circuit.from_qasm(dumps2(qiskit_from_qasm) if qasm_version == 2 + else dumps3(qiskit_from_qasm)) + self.assertEqual(pyzx_circuit.qubits, pyzx_from_qiskit.qubits) + self.assertListEqual(pyzx_circuit.gates, pyzx_from_qiskit.gates) # Test standard gates common to both qelib1.inc (OpenQASM 2) and stdgates.inc (OpenQASM 3). compare_gate_matrix_with_qiskit( @@ -305,16 +317,28 @@ def test_qiskit_transpile_pyzx_optimization_round_trip(self): qc1 = transpile(qc) t1 = quantum_info.Operator(qc1).data - c = Circuit.from_qasm(dumps(qc1)) - g = c.to_graph() - full_reduce(g) - qasm = extract_circuit(g).to_basic_gates().to_qasm() + # Test round-trip for OpenQASM 2. + c2 = Circuit.from_qasm(dumps2(qc1)) + g2 = c2.to_graph() + full_reduce(g2) + qasm2 = extract_circuit(g2).to_basic_gates().to_qasm(2) - qc2 = QuantumCircuit().from_qasm_str(qasm) + qc2 = QuantumCircuit().from_qasm_str(qasm2) t2 = quantum_info.Operator(qc2).data self.assertTrue(compare_tensors(t1, t2)) + # Test round-trip for OpenQASM 3. + c3 = Circuit.from_qasm(dumps3(qc1)) + g3 = c3.to_graph() + full_reduce(g3) + qasm3 = extract_circuit(g3).to_basic_gates().to_qasm(3) + + qc3 = loads3(qasm3) + t3 = quantum_info.Operator(qc3).data + + self.assertTrue(compare_tensors(t1, t3)) + if __name__ == '__main__': unittest.main() From 4f986293ea0d13b187826b7ac4a63989b8b0cff6 Mon Sep 17 00:00:00 2001 From: David Yonge-Mallo Date: Thu, 9 May 2024 08:45:21 +0200 Subject: [PATCH 2/2] Add `U` as alias for `u3` gate. Required for compatibility with OpenQASM 3. Together with #218, fixes #227. --- pyzx/circuit/gates.py | 1 + pyzx/circuit/qasmparser.py | 8 ++++---- tests/test_qasm.py | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyzx/circuit/gates.py b/pyzx/circuit/gates.py index a6cf7de8..97c68b88 100644 --- a/pyzx/circuit/gates.py +++ b/pyzx/circuit/gates.py @@ -1288,6 +1288,7 @@ def to_graph(self, g, q_mapper, c_mapper): "u2": U2, "u3": U3, "u": U3, + "U": U3, # needed for OpenQASM 3 in Qiskit 1.0 and up "cu3": CU3, "cu": CU, "cx": CNOT, diff --git a/pyzx/circuit/qasmparser.py b/pyzx/circuit/qasmparser.py index 8f8f5448..a92341ea 100644 --- a/pyzx/circuit/qasmparser.py +++ b/pyzx/circuit/qasmparser.py @@ -21,7 +21,7 @@ from typing import List, Dict, Tuple, Optional from . import Circuit -from .gates import Gate, qasm_gate_table, U2, U3 +from .gates import Gate, qasm_gate_table from ..utils import settings @@ -193,10 +193,10 @@ def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gat gates.append(g) elif name == 'u2': if len(phases) != 2: raise TypeError("Invalid specification {}".format(c)) - gates.append(U2(argset[0],phases[0],phases[1])) - elif name in ('u3', 'u'): + gates.append(qasm_gate_table[name](argset[0], phases[0], phases[1])) # type: ignore + elif name in ('u3', 'u', 'U'): if len(phases) != 3: raise TypeError("Invalid specification {}".format(c)) - gates.append(U3(argset[0],phases[0],phases[1],phases[2])) + gates.append(qasm_gate_table[name](argset[0], phases[0], phases[1], phases[2])) # type: ignore elif name in ('cx', 'CX', 'cy', 'cz', 'ch', 'csx', 'swap'): if len(phases) != 0: raise TypeError("Invalid specification {}".format(c)) g = qasm_gate_table[name](control=argset[0],target=argset[1]) # type: ignore diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 07bd472c..128569f5 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -290,6 +290,9 @@ def compare_gate_matrix_with_qiskit(gates, num_qubits: int, num_angles: int, qas compare_gate_matrix_with_qiskit(['cu3'], 2, 3, [2]) compare_gate_matrix_with_qiskit(['u'], 1, 3, [2]) + # Test native OpenQASM 3 gate. + compare_gate_matrix_with_qiskit(['U'], 1, 3, [3]) + @unittest.skipUnless(QuantumCircuit, "qiskit needs to be installed for this test") def test_qiskit_transpile_pyzx_optimization_round_trip(self): """Regression test for issue #102.