Source code for pytket.quipper.quipper

# Copyright 2019-2024 Cambridge Quantum Computing
#
# 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.

from enum import Enum, unique
from math import pi
from typing import NamedTuple

from lark import Lark, Transformer, Tree

from pytket.circuit import CircBox, Circuit, OpType

# The Lark grammar, transformer and type definitions below are adapted from the
# code in Eddie Schoute's `quippy` project
# (https://github.com/eddieschoute/quippy). The main enhancements are support
# for multi-qubit gates and correct handling of negative controls on qubit 0.


# Types
class Wire(NamedTuple):
    i: int


class ControlWire(NamedTuple):
    wire: Wire
    negative: bool


class Control(NamedTuple):
    controlled: list[ControlWire]
    no_control: bool


@unique
class TypeAssignment_Type(Enum):
    Qbit = 1
    Cbit = 2


class TypeAssignment(NamedTuple):
    wire: Wire
    type: TypeAssignment_Type


class Gate:
    pass


@unique
class QGate_Op(Enum):
    Not = 1  # Pauli X
    H = 2  # Hadamard
    MultiNot = 3  # Multi-target not
    Y = 4  # Pauli Y
    Z = 5  # Pauli Z
    S = 6  # Clifford S
    T = 7  # Clifford T = sqrt(S)
    E = 8  # Clifford E = H S (omega)^3
    Omega = 9  # scalar = exp(i pi / 4)
    V = 10  # V = sqrt(X)
    Swap = 11
    W = 12  # W is self-inverse and diagonalizes Swap
    IX = 13  # i X


class QGate(
    Gate,
    NamedTuple(
        "QGate",
        [
            ("op", QGate_Op),
            ("inverted", bool),
            ("wires", list[Wire]),
            ("control", Control),
        ],
    ),
):
    pass


@unique
class QRot_Op(Enum):
    ExpZt = 1  # exp(−i Z t)
    R = 2  # R(2 / 2^t) (notation is confusing but see Monad.hs)


class QRot(
    Gate,
    NamedTuple(
        "QRot",
        [("op", QRot_Op), ("inverted", bool), ("timestep", float), ("wire", Wire)],
    ),
):
    pass


class QInit(Gate, NamedTuple("QInit", [("value", bool), ("wire", Wire)])):
    pass


class CInit(Gate, NamedTuple("CInit", [("value", bool), ("wire", Wire)])):
    pass


class QTerm(Gate, NamedTuple("QTerm", [("value", bool), ("wire", Wire)])):
    pass


class CTerm(Gate, NamedTuple("CTerm", [("value", bool), ("wire", Wire)])):
    pass


class QMeas(Gate, NamedTuple("QMeas", [("wire", Wire)])):
    pass


class QDiscard(Gate, NamedTuple("QDiscard", [("wire", Wire)])):
    pass


class CDiscard(Gate, NamedTuple("CDiscard", [("wire", Wire)])):
    pass


class SubroutineCall(
    Gate,
    NamedTuple(
        "SubroutineCall",
        [
            ("repetitions", int),
            ("name", str),
            ("shape", str),
            ("inverted", bool),
            ("inputs", list[Wire]),
            ("outputs", list[Wire]),
            ("control", Control),
        ],
    ),
):
    pass


class Comment(
    Gate,
    NamedTuple(
        "Comment",
        [
            ("comment", str),
            ("inverted", bool),
            ("wire_comments", list[tuple[Wire, str]]),
        ],
    ),
):
    pass


class Program(NamedTuple):
    inputs: list[TypeAssignment]
    gates: list[Gate]
    outputs: list[TypeAssignment]


@unique
class Subroutine_Control(Enum):
    yes = 1
    no = 2
    classically = 3


class Subroutine(NamedTuple):
    name: str
    shape: str
    controllable: Subroutine_Control
    circuit: Program


class Start(NamedTuple):
    circuit: Program
    subroutines: list[Subroutine]


# Transformer
class QuipperTransformer(Transformer):
    def int(self, t: list) -> int:
        return int(t[0])

    def float(self, t: list) -> float:
        return float(t[0])

    def string(self, t: list) -> str:
        return str(t[0][1:-1])

    def wire(self, t: list) -> Wire:
        return Wire(t[0])

    wire_list = list

    def wire_string_list(self, t: list) -> list[tuple[Wire, str]]:
        wires = (el for i, el in enumerate(t) if i % 2 == 0)
        labels = (el for i, el in enumerate(t) if i % 2 == 1)
        return list(zip(wires, labels))

    def pos_control_wire(self, t: list) -> ControlWire:
        return ControlWire(t[0], False)

    control_wire_list = list

    def neg_control_wire(self, t: list) -> ControlWire:
        return ControlWire(t[0], True)

    def type_assignment(self, t: list) -> TypeAssignment:
        ty = TypeAssignment_Type.Qbit if t[1] == "Qbit" else TypeAssignment_Type.Cbit
        return TypeAssignment(t[0], ty)

    def arity(self, t: list) -> list[Tree]:
        return list(t)

    def qgate(self, t: list) -> QGate:
        ops = QGate_Op
        n = t[0]
        if n == "not" or n == "x" or n == "X":
            op = ops.Not
        elif n == "H":
            op = ops.H
        elif n == "multinot":
            op = ops.MultiNot
        elif n == "Y":
            op = ops.Y
        elif n == "Z":
            op = ops.Z
        elif n == "S":
            op = ops.S
        elif n == "E":
            op = ops.E
        elif n == "T":
            op = ops.T
        elif n == "V":
            op = ops.V
        elif n == "swap":
            op = ops.Swap
        elif n == "omega":
            op = ops.Omega
        elif n == "iX":
            op = ops.IX
        elif n == "W":
            op = ops.W
        else:
            raise RuntimeError(f"Unknown QGate operation: {n}")
        return QGate(op=op, inverted=len(t[1].children) > 0, wires=t[2], control=t[3])

    def qrot1(self, t: list) -> QRot:
        return QRot(
            op=QRot_Op.ExpZt, timestep=t[0], inverted=len(t[1].children) > 0, wire=t[2]
        )

    def qrot2(self, t: list) -> QRot:
        return QRot(
            op=QRot_Op.R, timestep=t[0], inverted=len(t[1].children) > 0, wire=t[2]
        )

    def qinit(self, t: list) -> QInit:
        return QInit(value=(t[0] == "QInit1"), wire=t[1])

    def cinit(self, t: list) -> CInit:
        return CInit(value=(t[0] == "CInit1"), wire=t[1])

    def qterm(self, t: list) -> QTerm:
        return QTerm(value=(t[0] == "QTerm1"), wire=t[1])

    def cterm(self, t: list) -> CTerm:
        return CTerm(value=(t[0] == "CTerm1"), wire=t[1])

    def qmeas(self, t: list) -> QMeas:
        return QMeas(wire=t[0])

    def qdiscard(self, t: list) -> QDiscard:
        return QDiscard(wire=t[0])

    def cdiscard(self, t: list) -> CDiscard:
        return CDiscard(wire=t[0])

    def subroutine_call(self, t: list) -> SubroutineCall:
        repetitions = 1
        if t[0] is not None:
            assert isinstance(t[0], int)
            repetitions = t[0]
        return SubroutineCall(
            repetitions=repetitions,
            name=t[1],
            shape=t[2],
            inverted=len(t[3].children) > 0,
            inputs=t[4],
            outputs=t[5],
            control=t[6],
        )

    def comment(self, t: list) -> Comment:
        wire_comments = t[2] if len(t) > 2 else []
        return Comment(
            comment=t[0], inverted=len(t[1].children) > 0, wire_comments=wire_comments
        )

    def control_app(self, t: list) -> Control:
        if not t:
            return Control(controlled=list(), no_control=False)
        if len(t) == 2:
            return Control(controlled=t[0], no_control=True)
        if t[0] == "with nocontrol":
            return Control(controlled=list(), no_control=True)
        return Control(controlled=t[0], no_control=False)

    def circuit(self, t: list) -> Program:
        return Program(inputs=t[0], gates=t[1:-1], outputs=t[-1])

    def subroutine(self, t: list) -> Subroutine:
        if t[2] == "yes":
            controllable = Subroutine_Control.yes
        elif t[2] == "no":
            controllable = Subroutine_Control.no
        else:
            controllable = Subroutine_Control.classically
        return Subroutine(
            name=t[0], shape=t[1], controllable=controllable, circuit=t[3]
        )

    def start(self, t: list) -> Start:
        circuit = t.pop(0)
        return Start(circuit, list(t))


# Utility function
def allowed(op: str, arity: int) -> bool:
    if op in ["Not", "IX", "H", "Y", "Z", "S", "T", "E", "Omega", "V"]:
        return arity == 1
    if op in ["Swap", "W"]:
        return arity == 2
    # MultiNot
    return True


# Class for constructing a pytket Circuit from a parsed Quipper program
class CircuitMaker:
    def __init__(self, subr: list[Subroutine]) -> None:
        self.subrd = dict((s.name, s) for s in subr)
        if len(self.subrd) != len(subr):
            raise TypeError("Repeated subroutine names")

    def make_circuit(self, circ: Program) -> Circuit:
        inps, outs, gates = circ.inputs, circ.outputs, circ.gates
        if inps != outs:
            raise TypeError("Inputs don't match outputs")
        qbits = [inp.wire.i for inp in inps if inp.type.name == "Qbit"]
        cbits = [inp.wire.i for inp in inps if inp.type.name == "Cbit"]
        n_qbits = len(qbits)
        n_cbits = len(cbits)
        # Construct mappings from wire labels to tket indices.
        tkqbits = dict((qbits[i], i) for i in range(n_qbits))
        tkcbits = dict((cbits[i], i) for i in range(n_cbits))
        # Construct circuit in tket.
        c = Circuit(n_qbits, n_cbits)
        for gate in gates:
            if isinstance(gate, SubroutineCall):
                s = self.subrd[gate.name]
                if gate.shape != s.shape:
                    # Just a sanity check, otherwise we assume 'shape' OK.
                    raise TypeError("Mismatched shape in subroutine call")
                if gate.inputs != gate.outputs:
                    raise TypeError("Mismatched outputs in subroutine call")
                if gate.control.controlled:
                    raise NotImplementedError("Controls on subroutine")
                subcirc = self.make_circuit(s.circuit)  # recurse
                if gate.inverted:
                    subcirc = subcirc.dagger()
                cbox = CircBox(subcirc)
                for _ in range(gate.repetitions):
                    c.add_circbox(cbox, [wire.i for wire in gate.inputs])
            elif isinstance(gate, QGate):
                # The `nocontrol' flag is irrelevant here.
                op = gate.op.name
                qctrls, neg_qctrls, cctrls = [], [], []
                for w in gate.control.controlled:
                    idx = w.children[0].wire.i  # type: ignore
                    is_neg = w.children[0].negative  # type: ignore
                    if idx in qbits:
                        tkqidx = tkqbits[idx]
                        qctrls.append(tkqidx)
                        if is_neg:
                            neg_qctrls.append(tkqidx)
                    elif idx in cbits:
                        tkcidx = tkcbits[idx]
                        cctrls.append(tkcidx)
                if cctrls:
                    raise NotImplementedError("Classical controls")
                inv = gate.inverted
                wires = [tkqbits[wire.i] for wire in gate.wires]  # all must be qubits
                n_wires = len(wires)
                if not allowed(op, n_wires):
                    raise TypeError("'%s' gate with %d wires" % (op, n_wires))
                n_ctrls = len(qctrls)
                # Negative control values must be handled using NOT gates
                # either side.
                for ctrl in neg_qctrls:
                    c.X(ctrl)
                if op == "Not":
                    if n_ctrls == 0:
                        c.X(wires[0])
                    elif n_ctrls == 1:
                        c.CX(qctrls[0], wires[0])
                    elif n_ctrls == 2:
                        c.CCX(qctrls[0], qctrls[1], wires[0])
                    else:
                        c.add_gate(OpType.CnX, qctrls + wires)
                elif op == "IX":
                    if n_ctrls == 0:
                        c.X(wires[0])  # ignore phase
                    elif n_ctrls == 1:
                        c.S(qctrls[0])
                        c.CX(qctrls[0], wires[0])
                    else:
                        raise NotImplementedError("IX with more than 1 control")
                elif op == "MultiNot":
                    # Implement as a series of (controlled) X gates.
                    for wire in wires:
                        if n_ctrls == 0:
                            c.X(wire)
                        elif n_ctrls == 1:
                            c.CX(qctrls[0], wire)
                        elif n_ctrls == 2:
                            c.CCX(qctrls[0], qctrls[1], wire)
                        else:
                            c.add_gate(OpType.CnX, qctrls + [wire])
                elif op == "H":
                    if n_ctrls == 0:
                        c.H(wires[0])
                    elif n_ctrls == 1:
                        c.CH(qctrls[0], wires[0])
                    else:
                        raise NotImplementedError("H with more than 1 control")
                elif op == "Y":
                    if n_ctrls == 0:
                        c.Y(wires[0])
                    elif n_ctrls == 1:
                        c.CY(qctrls[0], wires[0])
                    else:
                        raise NotImplementedError("Y with more than 1 control")
                elif op == "Z":
                    if n_ctrls == 0:
                        c.Z(wires[0])
                    elif n_ctrls == 1:
                        c.CZ(qctrls[0], wires[0])
                    else:
                        raise NotImplementedError("Z with more than 1 control")
                elif op == "S":
                    if n_ctrls == 0:
                        if inv:
                            c.Sdg(wires[0])
                        else:
                            c.S(wires[0])
                    elif n_ctrls == 1:
                        # S = U1(1/2)
                        if inv:
                            c.add_gate(OpType.CU1, -0.5, [qctrls[0], wires[0]])
                        else:
                            c.add_gate(OpType.CU1, 0.5, [qctrls[0], wires[0]])
                    else:
                        raise NotImplementedError("S with more than 1 control")
                elif op == "T":
                    if n_ctrls == 0:
                        if inv:
                            c.Tdg(wires[0])
                        else:
                            c.T(wires[0])
                    elif n_ctrls == 1:
                        # T = U1(1/4)
                        if inv:
                            c.add_gate(OpType.CU1, -0.25, [qctrls[0], wires[0]])
                        else:
                            c.add_gate(OpType.CU1, 0.25, [qctrls[0], wires[0]])
                    else:
                        raise NotImplementedError("T with more than 1 control")
                elif op == "E":
                    # E = H S^3 omega^3 = H Sdg up to a scalar.
                    if n_ctrls == 0:
                        if inv:
                            c.H(wires[0])
                            c.S(wires[0])
                        else:
                            c.Sdg(wires[0])
                            c.H(wires[0])
                    else:
                        raise NotImplementedError("Controlled E")
                elif op == "Omega":
                    if n_ctrls == 0:
                        pass  # ignore phase
                    elif n_ctrls == 1:
                        c.Rz(0.25, qctrls[0])
                    else:
                        raise NotImplementedError("Omega with more than 1 control")
                elif op == "V":
                    if n_ctrls == 0:
                        if inv:
                            c.add_gate(OpType.Vdg, wires)
                        else:
                            c.add_gate(OpType.V, wires)
                    else:
                        raise NotImplementedError("Controlled V")
                elif op == "Swap":
                    if n_ctrls == 0:
                        c.SWAP(wires[0], wires[1])
                    elif n_ctrls == 1:
                        c.CSWAP(qctrls[0], wires[0], wires[1])
                    else:
                        raise NotImplementedError("SWAP with more than 2 controls")
                elif op == "W":
                    # W is self-inverse.
                    # Is there a simpler way to synthesize this?
                    if n_ctrls == 0:
                        c.Rz(1.25, wires[0])
                        c.Ry(1.0, wires[0])
                        c.T(wires[1])
                        c.Ry(1.0, wires[1])
                        c.CX(wires[1], wires[0])
                        c.Ry(-0.25, wires[1])
                        c.CX(wires[0], wires[1])
                        c.Ry(0.25, wires[1])
                        c.CX(wires[1], wires[0])
                        c.T(wires[0])
                        c.Ry(1.0, wires[0])
                        c.Rz(1.25, wires[1])
                        c.Ry(1.0, wires[1])
                    else:
                        raise NotImplementedError("Controlled W")
                else:
                    raise TypeError("Unknown op type: %s" % op)
                # Apply the NOT gates again for the negative controls.
                for ctrl in neg_qctrls:
                    c.X(ctrl)
            elif isinstance(gate, QRot):
                op = gate.op.name
                inv = gate.inverted
                t = gate.timestep
                wire = tkqbits[gate.wire.i]  # must be quantum
                if op == "ExpZt":
                    if inv:
                        c.Rz(-2 * t / pi, wire)
                    else:
                        c.Rz(2 * t / pi, wire)
                elif op == "R":
                    if inv:
                        c.Rz(-2 / t, wire)
                    else:
                        c.Rz(2 / t, wire)
                else:
                    raise TypeError("Unknown op type: %s" % op)
            # QInit, QTerm, CInit, CTerm represent 'temporary' wires that
            # only occupy part of a circuit (and can be initialized with 0/1).
            # Not supported in pytket.
            elif isinstance(gate, QInit):
                wire = tkqbits[gate.wire.i]  # must be quantum
                raise NotImplementedError("QInit")
            elif isinstance(gate, CInit):
                wire = tkcbits[gate.wire.i]  # must be clasical
                raise NotImplementedError("CInit")
            elif isinstance(gate, QTerm):
                wire = tkqbits[gate.wire.i]  # must be quantum
                raise NotImplementedError("QTerm")
            elif isinstance(gate, CTerm):
                wire = tkcbits[gate.wire.i]  # must be clasical
                raise NotImplementedError("CTerm")
            elif isinstance(gate, QMeas):
                # QMeas turns a qubit into a bit.
                # Not supported in pytket.
                wire = tkqbits[gate.wire.i]  # must be quantum
                raise NotImplementedError("QMeas")
            elif isinstance(gate, QDiscard):
                # QDiscard discards a qubit.
                # Not supported in pytket.
                wire = tkqbits[gate.wire.i]  # must be quantum
                raise NotImplementedError("QDiscard")
            elif isinstance(gate, CDiscard):
                # CDiscard discards a bit.
                # Not supported in pytket.
                wire = tkcbits[gate.wire.i]  # must be classical
                raise NotImplementedError("CDiscard")
            elif isinstance(gate, Comment):
                pass
            else:
                raise TypeError("Unknown gate type: %s" % type(gate))
        return c


[docs] def circuit_from_quipper(input_file: str) -> Circuit: # pylint: disable=C0301 """Generate a pytket :py:class:`Circuit` given a program in Quipper ASCII format. Limitations (due to current limitations of `pytket`): * Subroutines must be defined over the full set of qubits. * Global phases are ignored. * Only limited support for controlled gates (depending on the gate type). * No support for 'QInit' and 'QTerm'. All qubits must run from the begining to the end of the circuit. * No support for the legacy keywords ('QNot', 'QMultinot', 'QHad', 'QSwap', 'QW'). These are now represented in Quipper as types of 'QGate'. * No support for 'QMeas' (which in Quipper converts a Q wire to a C wire). * No support for: 'GPhase' (global phase), 'QPrep', 'QUnprep', 'QDiscard', or 'DTerm'. * No support for classical operations ('CNot', 'CGate', 'CSwap', 'CInit', 'CTerm', 'CDiscard'). :param input_file: name of file to read """ # Read Quipper program from file. with open(input_file) as f: quip = f.read() # Parse the circuit using the QuipperTransformer. x = Lark( """ start : circuit subroutine* _NEWLINE* circuit : "Inputs:" arity (gate _NEWLINE)* "Outputs:" arity subroutine: _NEWLINE "Subroutine:" string _NEWLINE "Shape:" string _NEWLINE "Controllable:" SUB_CONTROL _NEWLINE circuit SUB_CONTROL : "yes" | "no" | "classically" arity : type_assignment ("," type_assignment)* ","? _NEWLINE type_assignment : wire ":" TYPE TYPE: "Qbit" | "Cbit" control_app : controlled? NO_CONTROL? ?controlled : "with controls=[" control_wire_list "]" NO_CONTROL : "with nocontrol" ?gate : qgate | qrot1 | qrot2 | gphase | cnot | cgate | cswap | qprep | qunprep | qinit | cinit | qterm | cterm | qmeas | qdiscard | cdiscard | dterm | subroutine_call | comment !inversion : "*"? qgate : "QGate[" string "]" inversion "(" wire_list ")" control_app qrot1 : "QRot[\\"exp(-i%Z)\\"," float "]" inversion "(" wire ")" qrot2 : "QRot[\\"R(2pi/%)\\"," int "]" inversion "(" wire ")" gphase : "Gphase() with t=" float control_app "with anchors=[" wire_list "]" cnot : "CNot(" wire ")" control_app cgate : "CGate[" string "]" inversion "(" wire_list ")" NO_CONTROL? cswap : "CSwap(" wire "," wire ")" control_app qprep : "QPrep(" wire ")" NO_CONTROL? qunprep : "QUnprep(" wire ")" NO_CONTROL? qinit : QINIT_STATE "(" wire ")" NO_CONTROL? QINIT_STATE : "QInit0" | "QInit1" cinit : CINIT_STATE "(" wire ")" NO_CONTROL? CINIT_STATE : "CInit0" | "CInit1" qterm : QTERM_STATE "(" wire ")" NO_CONTROL? QTERM_STATE : "QTerm0" | "QTerm1" cterm : CTERM_STATE "(" wire ")" NO_CONTROL? CTERM_STATE : "CTerm0" | "CTerm1" qmeas : "QMeas(" wire ")" qdiscard : "QDiscard(" wire ")" cdiscard : "CDiscard(" wire ")" dterm : DTERM_STATE "(" wire ")" DTERM_STATE : "DTerm0" | "DTerm1" subroutine_call : "Subroutine" ["(x" int ")"] "[" string ", shape" string "]" inversion "(" wire_list ") -> (" wire_list ")" control_app comment : "Comment[" string "]" inversion "(" wire_string_list? ")" wire_string_list: wire ":" string ("," wire ":" string)* wire : int wire_list : wire ("," wire)* pos_control_wire : "+" wire neg_control_wire : "-" wire control_wire : pos_control_wire | neg_control_wire control_wire_list : control_wire ("," control_wire)* %import common.WS_INLINE -> WS %ignore WS %import common.CR %import common.LF _NEWLINE: CR? LF %import common.ESCAPED_STRING string: ESCAPED_STRING %import common.SIGNED_FLOAT float : SIGNED_FLOAT %import common.INT int : INT """, parser="lalr", transformer=QuipperTransformer(), ).parse(quip) # Load the subroutine list. maker = CircuitMaker(x.subroutines) # type: ignore # Make the tket circuit. return maker.make_circuit(x.circuit) # type: ignore