# 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 unique, Enum
from typing import List, NamedTuple, Tuple
from math import pi
from lark import Lark, Transformer, Tree
from pytket.circuit import Circuit, OpType, CircBox
# 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
Wire = NamedTuple("Wire", [("i", int)])
ControlWire = NamedTuple("ControlWire", [("wire", Wire), ("negative", bool)])
Control = NamedTuple(
"Control", [("controlled", List[ControlWire]), ("no_control", bool)]
)
@unique
class TypeAssignment_Type(Enum):
Qbit = 1
Cbit = 2
TypeAssignment = NamedTuple(
"TypeAssignment", [("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
Program = NamedTuple(
"Program",
[
("inputs", List[TypeAssignment]),
("gates", List[Gate]),
("outputs", List[TypeAssignment]),
],
)
@unique
class Subroutine_Control(Enum):
yes = 1
no = 2
classically = 3
Subroutine = NamedTuple(
"Subroutine",
[
("name", str),
("shape", str),
("controllable", Subroutine_Control),
("circuit", Program),
],
)
Start = NamedTuple("Start", [("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("Unknown QGate operation: {}".format(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 None
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
elif op in ["Swap", "W"]:
return arity == 2
else:
# 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, "r") 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