Repository: Quantomatic/pyzx

Issue:
cp gate support for QASMParser
Hi @jvdwetering, I faced the problem of phase(p), and controlled phase(cp) gate when parsing the qasm string. I thought I might try working on this issue to add p and cp gate support. Your guidance and input would be greatly appreciated.

Discussion:

Patch:
diff --git a/pyzx/circuit/gates.py b/pyzx/circuit/gates.py
index ebd2630b..0bada393 100644
--- a/pyzx/circuit/gates.py
+++ b/pyzx/circuit/gates.py
@@ -436,6 +436,30 @@ class XPhase(Gate):
         gates.append(HAD(self.target))
         return gates
 
+class SX(XPhase):
+    name = 'SX'
+    qasm_name = 'sx'
+    qasm_name_adjoint = 'sxdg'
+    def __init__(self, target: int, adjoint:bool=False) -> None:
+        super().__init__(target, Fraction(1,2)*(-1 if adjoint else 1))
+        self.adjoint = adjoint
+
+class CSX(Gate):
+    name = 'CSX'
+    qasm_name = 'csx'
+    def __init__(self, control: int, target: int) -> None:
+        self.target = target
+        self.control = control
+
+    def to_basic_gates(self):
+        return [HAD(self.target)] + \
+               CPhase(self.control,self.target,Fraction(1,2)).to_basic_gates() + \
+               [HAD(self.target)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
 class YPhase(Gate):
     name = 'YPhase'
     qasm_name = 'ry'
@@ -474,6 +498,22 @@ class Y(YPhase):
     def __init__(self, target: int) -> None:
         super().__init__(target, phase = Fraction(1,1))
 
+class CY(Gate):
+    name = 'CY'
+    qasm_name = 'cy'
+    def __init__(self, control: int, target: int) -> None:
+        self.target = target
+        self.control = control
+
+    def to_basic_gates(self):
+        return [S(self.target,adjoint=True),
+                CNOT(self.control,self.target),
+                S(self.target)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
 class NOT(XPhase):
     name = 'NOT'
     qasm_name = 'x'
@@ -568,7 +608,7 @@ class CZ(Gate):
 
 class XCX(CZ):
     '''This class represents the X-controlled-X gate.'''
-    name = 'CX'
+    name = 'XCX'
     qasm_name = 'undefined'
     qc_name = 'undefined'
     quipper_name = 'X'
@@ -598,10 +638,48 @@ class SWAP(CZ):
         for gate in self.to_basic_gates():
             gate.to_graph(g, q_mapper, c_mapper)
 
+class CRX(Gate):
+    name = 'CRX'
+    qasm_name = 'crx'
+    print_phase = True
+    def __init__(self, control: int, target: int, phase: FractionLike) -> None:
+        self.target = target
+        self.control = control
+        self.phase = phase
+
+    def to_basic_gates(self):
+        return [ZPhase(self.target,Fraction(1,2)),
+                CNOT(self.control,self.target)] + \
+               U3(self.target,-self.phase/2,0,0).to_basic_gates() + \
+               [CNOT(self.control,self.target)] + \
+               U3(self.target,self.phase/2,Fraction(-1,2),0).to_basic_gates()
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class CRY(Gate):
+    name = 'CRY'
+    qasm_name = 'cry'
+    print_phase = True
+    def __init__(self, control: int, target: int, phase: FractionLike) -> None:
+        self.target = target
+        self.control = control
+        self.phase = phase
+
+    def to_basic_gates(self):
+        return YPhase(self.target,self.phase/2).to_basic_gates() + \
+               [CNOT(self.control,self.target)] + \
+               YPhase(self.target,-self.phase/2).to_basic_gates() + \
+               [CNOT(self.control,self.target)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
 class CRZ(Gate):
     name = 'CRZ'
     qasm_name = 'crz'
-    quipper_name = 'undefined'
     print_phase = True
     def __init__(self, control: int, target: int, phase: FractionLike) -> None:
         self.target = target
@@ -621,11 +699,68 @@ class CRZ(Gate):
                 ZPhase(self.target, phase2),
                 CNOT(self.control, self.target)]
 
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class RXX(Gate):
+    name = 'RXX'
+    qasm_name = 'rxx'
+    print_phase = True
+    def __init__(self, control: int, target: int, phase: FractionLike) -> None:
+        self.target = target
+        self.control = control
+        self.phase = phase
+
+    def to_basic_gates(self):
+        return U3(self.control,Fraction(1,2),self.phase,0).to_basic_gates() + \
+               [HAD(self.target),
+                CNOT(self.control,self.target),
+                ZPhase(self.target,-self.phase),
+                CNOT(self.control,self.target),
+                HAD(self.target)] + \
+               U2(self.control,Fraction(-1,1),(Fraction(1,1)-self.phase)%2).to_basic_gates()
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class RZZ(Gate):
+    name = 'RZZ'
+    qasm_name = 'rzz'
+    print_phase = True
+    def __init__(self, control: int, target: int, phase: FractionLike) -> None:
+        self.target = target
+        self.control = control
+        self.phase = phase
+
+    def to_basic_gates(self):
+        return [CNOT(control=self.control,target=self.target),
+                ZPhase(self.target,phase=self.phase),
+                CNOT(control=self.control,target=self.target)]
 
     def to_graph(self, g, q_mapper, c_mapper):
         for gate in self.to_basic_gates():
             gate.to_graph(g, q_mapper, c_mapper)
 
+class CPhase(CRZ):
+    name = 'CPhase'
+    qasm_name = 'cp'
+
+    def to_basic_gates(self):
+        phase1 = self.phase / 2
+        phase2 = -self.phase / 2
+        try:
+            phase1 = Fraction(phase1) % 2
+            phase2 = Fraction(phase2) % 2
+        except Exception:
+            pass
+        return [ZPhase(self.control, phase1),
+                CNOT(self.control, self.target),
+                ZPhase(self.target, phase2),
+                CNOT(self.control, self.target),
+                ZPhase(self.target, phase1)]
+
 class CHAD(Gate):
     name = 'CHAD'
     qasm_name = 'ch'
@@ -887,6 +1022,113 @@ class Tofolli(CCZ):
         CCZ.to_graph(self, g, q_mapper, c_mapper)
         HAD(t).to_graph(g, q_mapper, c_mapper)
 
+class CSWAP(CCZ):
+    name = 'CSWAP'
+    qasm_name = 'cswap'
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, CSWAP): return False
+        if self.index != other.index: return False
+        if self.ctrl1 != other.ctrl1: return False
+        if set([self.ctrl2,self.target]) == set([other.ctrl2,other.target]):
+            return True
+        return False
+
+    def to_basic_gates(self):
+        c, t1, t2 = self.ctrl1, self.ctrl2, self.target
+        return [CNOT(control=t2,target=t1)] + \
+               Tofolli(c,t1,t2).to_basic_gates() + \
+               [CNOT(control=t2,target=t1)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class U2(Gate):  # See https://arxiv.org/pdf/1707.03429.pdf
+    name = 'U2'
+    qasm_name = 'u2'
+    print_phase = True
+    def __init__(self, target: int, theta: FractionLike, phi: FractionLike) -> None:
+        self.target = target
+        self.theta = theta
+        self.phi = phi
+
+    def to_basic_gates(self):
+        return [ZPhase(self.target,phase=(self.phi-Fraction(1,2))%2),
+                XPhase(self.target,phase=Fraction(1,2)),
+                ZPhase(self.target,phase=(self.theta+Fraction(1,2))%2)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class U3(Gate):  # See equation (5) of https://arxiv.org/pdf/1707.03429.pdf
+    name = 'U3'
+    qasm_name = 'u3'
+    print_phase = True
+    def __init__(self, target: int, theta: FractionLike, phi: FractionLike, rho: FractionLike) -> None:
+        self.target = target
+        self.theta = theta
+        self.phi = phi
+        self.rho = rho
+
+    def to_basic_gates(self):
+        return [ZPhase(self.target,phase=self.rho),
+                XPhase(self.target,phase=Fraction(1,2)),
+                ZPhase(self.target,phase=(self.theta+1)%2),
+                XPhase(self.target,phase=Fraction(1,2)),
+                ZPhase(self.target,phase=(self.phi+3)%2)]
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+class CU3(Gate):
+    name = 'CU3'
+    qasm_name = 'cu3'
+    print_phase = True
+
+    def __init__(self, control: int, target: int, theta: FractionLike, phi: FractionLike, rho: FractionLike) -> None:
+        self.control = control
+        self.target = target
+        self.theta = theta
+        self.phi = phi
+        self.rho = rho
+
+    def to_basic_gates(self):
+        return [ZPhase(self.control,phase=(self.rho+self.phi)/2),
+                ZPhase(self.target, phase=(self.rho-self.phi)/2),
+                CNOT(self.control,self.target)] + \
+               U3(self.target,-self.theta/2,0,-(self.phi+self.rho)/2).to_basic_gates() + \
+               [CNOT(self.control, self.target)] + \
+               U3(self.target,self.theta/2,self.phi,0).to_basic_gates()
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
+
+
+class CU(Gate):
+    name = 'CU'
+    qasm_name = 'cu'
+    print_phase = True
+
+    def __init__(self, control: int, target: int, theta: FractionLike, phi: FractionLike, rho: FractionLike,
+            gamma: FractionLike) -> None:
+        self.control = control
+        self.target = target
+        self.theta = theta
+        self.phi = phi
+        self.rho = rho
+        self.gamma = gamma
+
+    def to_basic_gates(self):
+        return [ZPhase(self.control,phase=self.gamma)] + \
+               CU3(self.control,self.target,self.theta,self.phi,self.rho).to_basic_gates()
+
+    def to_graph(self, g, q_mapper, c_mapper):
+        for gate in self.to_basic_gates():
+            gate.to_graph(g, q_mapper, c_mapper)
 
 class InitAncilla(Gate):
     name = 'InitAncilla'
@@ -975,8 +1217,14 @@ class Measurement(Gate):
 gate_types: Dict[str,Type[Gate]] = {
     "XPhase": XPhase,
     "NOT": NOT,
-    "ZPhase": ZPhase,
+    "SX": SX,
+    "CSX": CSX,
     "YPhase": YPhase,
+    "Y": Y,
+    "CY": CY,
+    "ZPhase": ZPhase,
+    "CPhase": CPhase,
+    "CP": CPhase,
     "Z": Z,
     "S": S,
     "T": T,
@@ -985,12 +1233,22 @@ gate_types: Dict[str,Type[Gate]] = {
     "ParityPhase": ParityPhase,
     "XCX": XCX,
     "SWAP": SWAP,
+    "CSWAP": CSWAP,
     "CRZ": CRZ,
     "HAD": HAD,
     "H": HAD,
     "CHAD": CHAD,
     "TOF": Tofolli,
     "CCZ": CCZ,
+    "U2": U2,
+    "U3": U3,
+    "CU3": CU3,
+    "CU": CU,
+    "CRX": CRX,
+    "CRY": CRY,
+    "RZZ": RZZ,
+    "RXX": RXX,
+    "FSim": FSim,
     "InitAncilla": InitAncilla,
     "PostSelect": PostSelect,
     "DiscardBit": DiscardBit,
@@ -1005,14 +1263,35 @@ qasm_gate_table: Dict[str, Type[Gate]] = {
     "t": T,
     "sdg": S,
     "tdg": T,
+    "sx": SX,
+    "sxdg": SX,
+    "csx": CSX,
     "h": HAD,
+    "rx": XPhase,
+    "ry": YPhase,
+    "rz": ZPhase,
+    "cp": CPhase,
+    "cphase": CPhase,
+    "cu1": CPhase,
+    "p": ZPhase,
+    "u1": ZPhase,
+    "u2": U2,
+    "u3": U3,
+    "cu3": CU3,
+    "cu": CU,
     "cx": CNOT,
-    "CX": CNOT,
+    "CX": CNOT,  # needed for backwards compatibility with older versions of qiskit
+    "cy": CY,
     "cz": CZ,
     "ch": CHAD,
+    "crx": CRX,
+    "cry": CRY,
     "crz": CRZ,
+    "rxx": RXX,
+    "rzz": RZZ,
     "ccx": Tofolli,
     "ccz": CCZ,
     "swap": SWAP,
+    "cswap": CSWAP,
     "measure": Measurement,
 }
diff --git a/pyzx/circuit/qasmparser.py b/pyzx/circuit/qasmparser.py
index a690cafc..b524c795 100644
--- a/pyzx/circuit/qasmparser.py
+++ b/pyzx/circuit/qasmparser.py
@@ -21,7 +21,7 @@ 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, NOT
+from .gates import Gate, qasm_gate_table, XPhase, YPhase, ZPhase, NOT, U2, U3
 from ..utils import settings
 
 
@@ -72,7 +72,7 @@ class QASMParser(object):
             j = data.find("}", i)
             self.parse_custom_gate(data[i:j+1])
             data = data[:i] + data[j+1:]
-        #parse the regular commands
+        # parse the regular commands
         commands = [s.strip() for s in data.split(";") if s.strip()]
         for c in commands:
             self.gates.extend(self.parse_command(c, self.registers))
@@ -117,21 +117,31 @@ class QASMParser(object):
                 circ.add_gate(g)
         self.custom_gates[name] = circ
 
-    def extract_command_name(self, c: str) -> List[str]:
+    def extract_command_parts(self, c: str) -> Tuple[str,List[Fraction],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)
+        name, rest = c.split(" ",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(',')
+            phases = [self.parse_phase_arg(val) for val in vals]
+            name = name[:left_bracket]
+        return name, phases, args
 
     def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gate]:
         gates: List[Gate] = []
-        name, rest = self.extract_command_name(c)
+        name, phases, args = self.extract_command_parts(c)
         if name in ("barrier","creg","measure", "id"): return gates
         if name in ("opaque", "if"):
             raise TypeError("Unsupported operation {}".format(c))
-        args = [s.strip() for s in rest.split(",") if s.strip()]
         if name == "qreg":
             regname, sizep = args[0].split("[",1)
             size = int(sizep[:-1])
@@ -168,72 +178,46 @@ class QASMParser(object):
                     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', 'y', 'z', 's', 't', 'h', '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
+            elif name in ('x', 'y', 'z', 's', 't', 'h', 'sx'):
+                if len(phases) != 0: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](argset[0])  # type: ignore # mypy can't handle Gate subclasses with different number of parameters
                 gates.append(g)
-                continue
-            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))
-                valp = name[i+1:j]
-                # try:
-                #     phasep = float(valp)/math.pi
-                # except ValueError:
-                #     if valp.find('pi') == -1: raise TypeError("Invalid specification {}".format(name))
-                #     valp = valp.replace('pi', '')
-                #     valp = valp.replace('*','')
-                #     try: phasep = float(valp)
-                #     except: raise TypeError("Invalid specification {}".format(name))
-                # phase = Fraction(phasep).limit_denominator(100000000)
-                phase = self.parse_phase_arg(valp)
-                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))
-                continue
-            if name.startswith('u2') or name.startswith('u3'): # see https://arxiv.org/pdf/1707.03429.pdf
-                i = name.find('(')
-                j = name.find(')')
-                if i == -1 or j == -1: raise TypeError("Invalid specification {}".format(name))
-                vals = name[i+1:j].split(',')
-                phases = [self.parse_phase_arg(val) for val in vals]
-                if name.startswith('u2'):
-                    if len(phases) != 2: raise TypeError("Invalid specification {}".format(name))
-                    gates.append(ZPhase(argset[0],phase=(phases[1]-Fraction(1,2))%2))
-                    gates.append(XPhase(argset[0],phase=Fraction(1,2)))
-                    gates.append(ZPhase(argset[0],phase=(phases[0]+Fraction(1,2))%2))
-                    continue
-                else:
-                    # See equation (5) of https://arxiv.org/pdf/1707.03429.pdf
-                    if len(phases) != 3: raise TypeError("Invalid specification {}".format(name))
-                    gates.append(ZPhase(argset[0],phase=phases[2]))
-                    gates.append(XPhase(argset[0],phase=Fraction(1,2)))
-                    gates.append(ZPhase(argset[0],phase=(phases[0]+1)%2))
-                    gates.append(XPhase(argset[0],phase=Fraction(1,2)))
-                    gates.append(ZPhase(argset[0],phase=(phases[1]+3)%2))
-                    continue
-            if name in ('cx', 'CX', 'cz', 'ch', 'swap'):
-                g = qasm_gate_table[name](control=argset[0],target=argset[1]) # type: ignore
+            elif name in ('sdg', 'tdg', 'sxdg'):
+                if len(phases) != 0: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](argset[0],adjoint=True)  # type: ignore
+                gates.append(g)
+            elif name in ('rx', 'ry', 'rz', 'p', 'u1'):
+                if len(phases) != 1: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](argset[0],phase=phases[0])  # type: ignore
+                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 == 'u3':
+                if len(phases) != 3: raise TypeError("Invalid specification {}".format(c))
+                gates.append(U3(argset[0],phases[0],phases[1],phases[2]))
+            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
                 gates.append(g)
-                continue
-            if name in ('ccx', 'ccz'):
-                g = qasm_gate_table[name](ctrl1=argset[0],ctrl2=argset[1],target=argset[2]) # type: ignore
+            elif name in ('crx', 'cry', 'crz', 'cp', 'cphase', 'cu1', 'rxx', 'rzz'):
+                if len(phases) != 1: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](argset[0],argset[1],phase=phases[0])  # type: ignore
                 gates.append(g)
-                continue
-            raise TypeError("Unknown gate name: {}".format(c))
+            elif name in ('ccx', 'ccz', 'cswap'):
+                if len(phases) != 0: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](ctrl1=argset[0],ctrl2=argset[1],target=argset[2])  # type: ignore
+                gates.append(g)
+            elif name == 'cu3':
+                if len(phases) != 3: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](control=argset[0],target=argset[1],theta=phases[0],phi=phases[1],rho=phases[2])  # type: ignore
+                gates.append(g)
+            elif name == 'cu':
+                if len(phases) != 4: raise TypeError("Invalid specification {}".format(c))
+                g = qasm_gate_table[name](control=argset[0],target=argset[1],theta=phases[0],phi=phases[1],rho=phases[2],gamma=phases[3])  # type: ignore
+                gates.append(g)
+            else:
+                raise TypeError("Invalid specification: {}".format(c))
         return gates
 
     def parse_phase_arg(self, val):


Test patch:
diff --git a/tests/test_circuit.py b/tests/test_circuit.py
index ac1c8db1..114f24f9 100644
--- a/tests/test_circuit.py
+++ b/tests/test_circuit.py
@@ -27,7 +27,7 @@ mydir = os.path.dirname(__file__)
 
 try:
     import numpy as np
-    from pyzx.tensor import tensorfy, compare_tensors, find_scalar_correction
+    from pyzx.tensor import tensorfy, compare_tensors
     import math
 except ImportError:
     np = None
@@ -123,69 +123,5 @@ class TestCircuit(unittest.TestCase):
         self.assertTrue(c1.verify_equality(c2,up_to_swaps=True))
         self.assertFalse(c1.verify_equality(c2,up_to_swaps=False))
 
-    def test_parser_state_reset(self):
-        from pyzx.circuit.qasmparser import QASMParser
-        s = """
-        OPENQASM 2.0;
-        include "qelib1.inc";
-        qreg q[1];
-        h q[0];
-        """
-        p = QASMParser()
-        c1 = p.parse(s)
-        c2 = p.parse(s)
-        self.assertEqual(c2.qubits, 1)
-        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()
diff --git a/tests/test_qasm.py b/tests/test_qasm.py
new file mode 100644
index 00000000..885786db
--- /dev/null
+++ b/tests/test_qasm.py
@@ -0,0 +1,238 @@
+# PyZX - Python library for quantum circuit rewriting
+#        and optimization using the ZX-calculus
+# Copyright (C) 2018 - Aleks Kissinger and John van de Wetering
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#    http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import unittest
+
+try:
+    import numpy as np
+    from pyzx.tensor import compare_tensors
+    import math
+except ImportError:
+    np = None
+
+try:
+    from qiskit import quantum_info, transpile
+    from qiskit.circuit import QuantumCircuit
+    from qiskit.qasm3 import loads
+except ImportError:
+    QuantumCircuit = None
+
+from pyzx.simplify import full_reduce
+from pyzx.extract import extract_circuit
+from pyzx.circuit import Circuit
+
+
+@unittest.skipUnless(np, "numpy needs to be installed for this to run")
+class TestQASM(unittest.TestCase):
+
+    def test_parser_state_reset(self):
+        from pyzx.circuit.qasmparser import QASMParser
+        s = """
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[1];
+        h q[0];
+        """
+        p = QASMParser()
+        c1 = p.parse(s)
+        c2 = p.parse(s)
+        self.assertEqual(c2.qubits, 1)
+        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];
+        """)
+        c = Circuit(3)
+        c.add_gate("CNOT", 0, 1)
+        c.add_gate("S", 2)
+        c.add_gate("CNOT", 2, 1)
+        self.assertEqual(c.qubits, qasm3.qubits)
+        self.assertListEqual(c.gates, qasm3.gates)
+
+    def test_p_same_as_rz(self):
+        """Test that the `p` gate is identical to the `rz` gate.
+
+        In the OpenQASM 3 spec, they differ by a global phase. However,
+        they are treated in pyzx identically, since the global phase doesn't
+        matter.
+        """
+        p_qasm = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[1];
+        p(pi/2) q[0];
+        """)
+        rz_qasm = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[1];
+        rz(pi/2) q[0];
+        """)
+        self.assertEqual(p_qasm.qubits, rz_qasm.qubits)
+        self.assertListEqual(p_qasm.gates, rz_qasm.gates)
+
+    def test_cp_differs_from_crz(self):
+        """Test that the `cp` gate and the `crz` gates are different.
+
+        The difference in phase between `p` and `rz` matters when the gates are
+        controlled.
+        """
+        crz_qasm = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[2];
+        crz(pi/2) q[0],q[1];
+        """)
+        cp_qasm = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[2];
+        cp(pi/2) q[0],q[1];
+        """)
+        crz_t = crz_qasm.to_matrix()
+        cp_t = cp_qasm.to_matrix()
+        self.assertFalse(compare_tensors(crz_t, cp_t, False))
+
+    def test_rzz(self):
+        """Regression test for issue #158.
+
+        The qiskit transpiler may rewrite `crz` into `rz` and `rzz` in some
+        situations. The following circuits should be equivalent."""
+        before = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[2];
+        crz(pi/2) q[0],q[1];
+        """)
+
+        after = Circuit.from_qasm("""
+        OPENQASM 2.0;
+        include "qelib1.inc";
+        qreg q[2];
+        rz(pi/4) q[1];
+        rzz(-pi/4) q[0],q[1];
+        """)
+        self.assertTrue(compare_tensors(
+            before.to_matrix(), after.to_matrix(), True))
+
+    @unittest.skipUnless(QuantumCircuit, "qiskit needs to be installed for this test")
+    def test_qasm_qiskit_semantics(self):
+        """Verify/document qasm gate semantics when imported into pyzx.
+
+        pyzx's implementation of qasm gates differs from qiskit in global
+        phases which are not significant, but which has sometimes led to user
+        confusion. This unit test documents those differences.
+        """
+
+        def compare_gate_matrix_with_qiskit(gates, num_qubits: int, num_angles: int, qasm_versions=None):
+            for gate in gates:
+                for qasm_version in qasm_versions if qasm_versions else [2, 3]:
+                    header = "OPENQASM 2.0;\ninclude \"qelib1.inc\";\n" if qasm_version == 2 \
+                        else "OPENQASM 3;\ninclude \"stdgates.inc\";\n"
+                    params = "" if num_angles == 0 else "({})".format(
+                        ",".join(["pi/2"] * num_angles))
+                    setup = header + f"qreg q[{num_qubits}];\n{gate}{params} "
+                    qasm = setup + \
+                        ", ".join(
+                            [f"q[{i}]" for i in range(num_qubits)]) + ";\n"
+                    c = Circuit.from_qasm(qasm)
+                    pyzx_matrix = c.to_matrix()
+
+                    for g in c.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")
+
+                    # qiskit uses little-endian ordering
+                    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)
+                    qiskit_matrix = quantum_info.Operator(qc).data
+                    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}")
+
+        # Test standard gates common to both OpenQASM 2 and 3.
+        compare_gate_matrix_with_qiskit(
+            ['x', 'y', 'z', 'h', 's', 'sdg', 't', 'tdg', 'sx'], 1, 0)
+        compare_gate_matrix_with_qiskit(['u1', 'p', 'rx', 'ry', 'rz'], 1, 1)
+        compare_gate_matrix_with_qiskit(['u2'], 1, 2)
+        compare_gate_matrix_with_qiskit(['u3'], 1, 3)
+        compare_gate_matrix_with_qiskit(
+            ['cx', 'CX', 'cy', 'cz', 'ch', 'swap'], 2, 0)
+        compare_gate_matrix_with_qiskit(['crx', 'cry', 'crz', 'cp'], 2, 1)
+        # 'ccz' not tested because not a standard qasm gate
+        compare_gate_matrix_with_qiskit(['ccx', 'cswap'], 3, 0)
+        compare_gate_matrix_with_qiskit(['cu'], 2, 4)
+
+        # Test standard gates added to OpenQASM 3.
+        compare_gate_matrix_with_qiskit(['cphase'], 2, 1, [3])
+
+        # Test standard gates removed from OpenQASM 3.
+        compare_gate_matrix_with_qiskit(['sxdg'], 1, 0, [2])
+        compare_gate_matrix_with_qiskit(['csx'], 2, 0, [2])
+        compare_gate_matrix_with_qiskit(['cu1', 'rxx', 'rzz'], 2, 1, [2])
+        compare_gate_matrix_with_qiskit(['cu3'], 2, 3, [2])
+
+    @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.
+
+        Transpile a circuit in qiskit, simplify it using pyzx, and verify that
+        it produces the same unitary in qiskit.
+        """
+        qc = QuantumCircuit(4)
+        qc.ccx(2, 1, 0)
+        qc.ccz(0, 1, 2)
+        qc.h(1)
+        qc.ccx(1, 2, 3)
+        qc.t(1)
+        qc.ccz(0, 1, 2)
+        qc.h(1)
+        qc.t(0)
+        qc.ccz(2, 1, 0)
+        qc.s(1)
+        qc.ccx(2, 1, 0)
+        qc.crz(0.2*np.pi, 0, 1)
+        qc.rz(0.8*np.pi, 1)
+        qc.cry(0.4*np.pi, 2, 1)
+        qc.crx(0.02*np.pi, 2, 0)
+
+        qc1 = transpile(qc)
+        t1 = quantum_info.Operator(qc1).data
+
+        c = Circuit.from_qasm(qc1.qasm())
+        g = c.to_graph()
+        full_reduce(g)
+        qasm = extract_circuit(g).to_basic_gates().to_qasm()
+
+        qc2 = QuantumCircuit().from_qasm_str(qasm)
+        t2 = quantum_info.Operator(qc2).data
+
+        self.assertTrue(compare_tensors(t1, t2))
+
+
+if __name__ == '__main__':
+    unittest.main()