Skip to content

Commit

Permalink
Merge pull request #151 from dlyongemallo/qasm3
Browse files Browse the repository at this point in the history
Add basic support for OpenQASM 3.
  • Loading branch information
jvdwetering authored Sep 7, 2023
2 parents 71458b0 + 6f9478d commit f418da9
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 21 deletions.
59 changes: 41 additions & 18 deletions pyzx/circuit/qasmparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,29 @@


import math
import re
from fractions import Fraction
from typing import List, Dict, Tuple, Optional

from . import Circuit
from .gates import Gate, qasm_gate_table, ZPhase, XPhase, CRZ, YPhase
from .gates import Gate, qasm_gate_table, ZPhase, XPhase, CRZ, YPhase, NOT
from ..utils import settings


class QASMParser(object):
"""Class for parsing QASM source files into circuit descriptions."""

def __init__(self) -> None:
self.qasm_version:int = settings.default_qasm_version
self.gates: List[Gate] = []
self.customgates: Dict[str,Circuit] = {}
self.custom_gates: Dict[str,Circuit] = {}
self.registers: Dict[str,Tuple[int,int]] = {}
self.qubit_count: int = 0
self.circuit: Optional[Circuit] = None

def parse(self, s: str, strict:bool=True) -> Circuit:
self.gates = []
self.customgates = {}
self.custom_gates = {}
self.registers = {}
self.qubit_count = 0
self.circuit = None
Expand All @@ -47,12 +51,15 @@ def parse(self, s: str, strict:bool=True) -> Circuit:
else: t = s.strip()
if t: r.append(t)

if r[0].startswith("OPENQASM"):
match = re.fullmatch(r"OPENQASM ([23])(\.\d+)?;", r[0])
if match and match.group(1):
self.qasm_version = int(match.group(1))
r.pop(0)
elif strict:
raise TypeError("File does not start with OPENQASM descriptor")
raise TypeError("File does not start with supported OPENQASM descriptor.")

if r[0].startswith('include "qelib1.inc";'):
if self.qasm_version == 2 and r[0].startswith('include "qelib1.inc";') or \
self.qasm_version == 3 and r[0].startswith('include "stdgates.inc";'):
r.pop(0)
elif strict:
raise TypeError("File is not importing standard library")
Expand Down Expand Up @@ -108,11 +115,19 @@ def parse_custom_gate(self, data: str) -> None:
for c in commands:
for g in self.parse_command(c, registers):
circ.add_gate(g)
self.customgates[name] = circ
self.custom_gates[name] = circ

def extract_command_name(self, c: str) -> List[str]:
if self.qasm_version == 3:
# Convert some OpenQASM 3 commands into OpenQASM 2 format.
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)
return c.split(" ",1)

def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gate]:
gates: List[Gate] = []
name, rest = c.split(" ",1)
name, rest = self.extract_command_name(c)
if name in ("barrier","creg","measure", "id"): return gates
if name in ("opaque", "if"):
raise TypeError("Unsupported operation {}".format(c))
Expand All @@ -130,7 +145,7 @@ def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gat
if "[" in a:
regname, valp = a.split("[",1)
val = int(valp[:-1])
if not regname in registers: raise TypeError("Invalid register {}".format(regname))
if regname not in registers: raise TypeError("Invalid register {}".format(regname))
qubit_values.append([registers[regname][0]+val])
else:
if is_range:
Expand All @@ -147,20 +162,20 @@ def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gat
qubit_values[i] = [qubit_values[i][0]]*dim
for j in range(dim):
argset = [q[j] for q in qubit_values]
if name in self.customgates:
circ = self.customgates[name]
if name in self.custom_gates:
circ = self.custom_gates[name]
if len(argset) != circ.qubits:
raise TypeError("Argument amount does not match gate spec: {}".format(c))
for g in circ.gates:
gates.append(g.reposition(argset))
continue
if name in ("x", "z", "s", "t", "h", "sdg", "tdg"):
if name in ("sdg", "tdg"):
if name in ("sdg", "tdg"):
g = qasm_gate_table[name](argset[0],adjoint=True) # type: ignore # mypy can't handle -
else: g = qasm_gate_table[name](argset[0]) # type: ignore # - Gate subclasses with different numbers of parameters
gates.append(g)
continue
if name.startswith(("rx", "ry", "rz", "u1", "crz")):
if name.startswith(("rx", "ry", "rz", "p", "u1", "crz")):
i = name.find('(')
j = name.find(')')
if i == -1 or j == -1: raise TypeError("Invalid specification {}".format(name))
Expand All @@ -175,12 +190,19 @@ def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gat
# except: raise TypeError("Invalid specification {}".format(name))
# phase = Fraction(phasep).limit_denominator(100000000)
phase = self.parse_phase_arg(valp)
if name.startswith('rx'): g = XPhase(argset[0],phase=phase)
elif name.startswith('crz'): g = CRZ(argset[0],argset[1],phase=phase)
elif name.startswith('rz'): g = ZPhase(argset[0],phase=phase)
elif name.startswith("ry"): g = YPhase(argset[0],phase=phase)
if name.startswith('rx'): gates.append(XPhase(argset[0],phase=phase))
elif name.startswith('crz'): gates.append(CRZ(argset[0],argset[1],phase=phase))
elif self.qasm_version == 2 and name.startswith('rz') or \
self.qasm_version == 3 and name.startswith('p') or \
name.startswith('u1'):
gates.append(ZPhase(argset[0],phase=phase))
elif self.qasm_version == 3 and name.startswith('rz'):
gates.append(ZPhase(argset[0],phase=phase/2))
gates.append(NOT(argset[0]))
gates.append(ZPhase(argset[0],phase=-phase/2))
gates.append(NOT(argset[0]))
elif name.startswith("ry"): gates.append(YPhase(argset[0],phase=phase))
else: raise TypeError("Invalid specification {}".format(name))
gates.append(g)
continue
if name.startswith('u2') or name.startswith('u3'): # see https://arxiv.org/pdf/1707.03429.pdf
i = name.find('(')
Expand Down Expand Up @@ -239,6 +261,7 @@ def parse_phase_arg(self, val):
phase = Fraction(phase).limit_denominator(100000000)
return phase


def qasm(s: str) -> Circuit:
"""Parses a string representing a program in QASM, and outputs a `Circuit`."""
p = QASMParser()
Expand Down
3 changes: 2 additions & 1 deletion pyzx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Settings(object): # namespace class
topt_command: Optional[List[str]] = None # Argument-separated command to run TOpt such as ["wsl", "./TOpt"]
show_labels: bool = False
tikz_classes: Dict[str,str] = tikz_classes
default_qasm_version: int = 2

settings = Settings()

Expand Down Expand Up @@ -218,4 +219,4 @@ def is_pauli(phase):
"""
if phase == 0 or phase == 1:
return True
return getattr(phase, 'is_pauli', False)
return getattr(phase, 'is_pauli', False)
54 changes: 52 additions & 2 deletions tests/test_circuit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PyZX - Python library for quantum circuit rewriting
# PyZX - Python library for quantum circuit rewriting
# and optimization using the ZX-calculus
# Copyright (C) 2018 - Aleks Kissinger and John van de Wetering

Expand Down Expand Up @@ -27,7 +27,8 @@

try:
import numpy as np
from pyzx.tensor import tensorfy, compare_tensors
from pyzx.tensor import tensorfy, compare_tensors, find_scalar_correction
import math
except ImportError:
np = None

Expand Down Expand Up @@ -137,5 +138,54 @@ def test_parser_state_reset(self):
self.assertEqual(len(c2.gates), 1)
self.assertTrue(c1.verify_equality(c2))

def test_parse_qasm3(self):
qasm3 = Circuit.from_qasm("""
OPENQASM 3;
include "stdgates.inc";
qubit[3] q;
cx q[0], q[1];
s q[2];
cx q[2], q[1];
""")
self.assertEqual(self.c.qubits, qasm3.qubits)
self.assertListEqual(self.c.gates, qasm3.gates)

def test_qasm3_p_gate(self):
qasm2 = Circuit.from_qasm("""
OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
rz(pi/2) q[0];
""")
qasm3 = Circuit.from_qasm("""
OPENQASM 3;
include "stdgates.inc";
qubit[1] q;
p(pi/2) q[0];
""")
self.assertEqual(qasm2.qubits, qasm3.qubits)
self.assertListEqual(qasm2.gates, qasm3.gates)

def test_qasm3_rz_gate(self):
# `rz` differs by a global phase between OpenQASM 2 and 3.
qasm2 = Circuit.from_qasm("""
OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
rz(pi/2) q[0];
""")
qasm3 = Circuit.from_qasm("""
OPENQASM 3;
include "stdgates.inc";
qubit[1] q;
rz(pi/2) q[0];
""")
t2 = qasm2.to_matrix()
t3 = qasm3.to_matrix()
self.assertFalse(compare_tensors(t2, t3, True))
self.assertTrue(compare_tensors(t2, t3, False))
sqrt_half = math.sqrt(1/2)
self.assertAlmostEqual(find_scalar_correction(t2, t3), complex(sqrt_half, sqrt_half))

if __name__ == '__main__':
unittest.main()

0 comments on commit f418da9

Please sign in to comment.