# Copyright 2021-2024 Cambridge Quantum Computing Ltd.## 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."""Quantum category================Lambeq's internal representation of the quantum category. This work isbased on DisCoPy (https://discopy.org/) which is released under theBSD 3-Clause 'New' or 'Revised' License.Notes-----In lambeq, gates are represented as the transpose of their matrixaccording to the standard convention in quantum computing. This makescomposition of gates using the tensornetwork library easier."""from__future__importannotationsfromcollections.abcimportCallablefromdataclassesimportdataclass,field,replacefromfunctoolsimportpartialfromtypingimportcast,Dict,Optional,Tuple,Unionimportnumpyasnpimporttensornetworkastnfromtyping_extensionsimportAny,Selffromlambeq.backendimportFunctor,grammar,Symbol,tensorfromlambeq.backend.numerical_backendimportbackend,get_backendfromlambeq.backend.symbolimportlambdifyfromlambeq.core.utilsimportfast_deepcopyquantum=grammar.Category('quantum')
[docs]@quantumclassTy(tensor.Dim):"""A type in the quantum category."""
[docs]def__init__(self,name:str|None=None,objects:list[Self]|None=None):"""Initialise a type in the quantum category. Parameters ---------- name : str, optional The name of the type, by default None objects : list[Ty], optional The objects defining a complex type, by default None """ifobjects:super().__init__(objects=objects)self.name=Noneself.label=Noneelse:ifnameisNone:super().__init__()else:super().__init__(2)self.label=name
[docs]@quantumclassBox(tensor.Box):"""A box in the quantum category."""name:strdom:Tycod:Tydata:float|np.ndarray|Nonez:intis_mixed:boolself_adjoint:bool
[docs]def__init__(self,name:str,dom:Ty,cod:Ty,data:float|np.ndarray|None=None,z:int=0,is_mixed:bool=False,self_adjoint:bool=False):"""Initialise a box in the quantum category. Parameters ---------- name : str Name of the box. dom : Ty Domain of the box. cod : Ty Codomain of the box. data : float | np.ndarray, optional Array defining the tensor of the box, by default None z : int, optional The winding number, by default 0 is_mixed : bool, optional Whether the box is mixed, by default False self_adjoint : bool, optional Whether the box is self-adjoint, by default False """self.name=nameself.dom=domself.cod=codself.data=dataself.z=zself.is_mixed=is_mixedself.self_adjoint=self_adjoint
[docs]defdagger(self)->Daggered|Box:"""Return the dagger of the box."""ifself.self_adjoint:returnselfreturnDaggered(self)
def__hash__(self)->int:returnsuper().__hash__()
[docs]@dataclass@quantumclassLayer(tensor.Layer):"""A Layer in a quantum Diagram. Parameters ---------- box : Box The box of the layer. left : Ty The wire type to the left of the box. right : Ty The wire type to the right of the box. """left:Tybox:Boxright:Ty
[docs]@dataclass@quantumclassDiagram(tensor.Diagram):"""A diagram in the quantum category. Parameters ---------- dom : Ty The type of the input wires. cod : Ty The type of the output wires. layers : list[Layer] The layers of the diagram. """dom:Tycod:Tylayers:list[Layer]# type: ignore[assignment]def__getattr__(self,name:str)->Any:try:gate=GATES[name]ifcallable(gate):returnpartial(self.apply_parametrized_gate,gate)returnpartial(self.apply_gate,gate)exceptKeyError:returnsuper().__getattr__(name)# type: ignore[misc]
@propertydefis_mixed(self)->bool:"""Whether the diagram is mixed. A diagram is mixed if it contains a mixed box or if it has both classical and quantum wires. """dom_n_cod=self.dom@self.codmixed_boundary=bitindom_n_codandqubitindom_n_codreturnmixed_boundaryorany(box.is_mixedforboxinself.boxes)@propertydefis_circuital(self)->bool:"""Checks if this diagram is a 'circuital' quantum diagram. Circuital means: 1. All initial layers are qubits 2. All post selections are at the end Allows for mixed_circuit measurements. Returns ------- bool Whether this diagram is a circuital diagram. """ifself.dom:returnFalselayers=self.layersnum_qubits=sum([1forlayerinlayersifisinstance(layer.box,Ket)])qubit_layers=layers[:num_qubits]ifnotall([isinstance(layer.box,Ket)forlayerinqubit_layers]):returnFalseforqubit_layerinqubit_layers:iflen(qubit_layer.right):returnFalse# Check there are no gates in between post-selections.measure_idx=[ifori,layerinenumerate(layers[num_qubits:])ifisinstance(layer.box,(Discard,Bra))]ifnotmeasure_idx:returnTruemmax=max(measure_idx)mmin=min(measure_idx)fori,gateinenumerate(layers[num_qubits:]):ifnotisinstance(gate.box,(Discard,Bra,Measure)):ifi>mminandi<mmax:returnFalsereturnTrue
[docs]defeval(self,*others,backend=None,mixed=False,contractor=tn.contractors.auto,**params):"""Evaluate the circuit represented by the diagram. Be aware that this method is only suitable for small circuits with a small number of qubits (depending on hardware resources). Parameters ---------- others : :class:`lambeq.backend.quantum.Diagram` Other circuits to process in batch if backend is set to tket. backend : pytket.Backend, optional Backend on which to run the circuit, if none then we apply tensor contraction. mixed : bool, optional Whether the circuit is mixed, by default False contractor : Callable, optional The contractor to use, by default tn.contractors.auto Returns ------- np.ndarray or list of np.ndarray The result of the circuit simulation. """ifbackendisNone:returncontractor(*self.to_tn(mixed=mixed)).tensorcircuits=[circuit.to_tk()forcircuitin(self,)+others]results,counts=[],circuits[0].get_counts(*circuits[1:],backend=backend,**params)fori,circuitinenumerate(circuits):n_bits=len(circuit.post_processing.dom)result=np.zeros((n_bits*(2,)))forbitstring,countincounts[i].items():result[bitstring]=countifcircuit.post_processing:post_result=circuit.post_processing.eval().astype(float)ifresult.shapeandpost_result.shape:result=np.tensordot(result,post_result,-1)else:result*post_resultresults.append(result)returnresultsiflen(results)>1elseresults[0]
[docs]definit_and_discard(self):"""Return circuit with empty domain and only bits as codomain. """circuit=selfifcircuit.dom:init=Id().tensor(*(Ket(0)ifx==qubitelseBit(0)forxincircuit.dom))circuit=init>>circuitifcircuit.cod!=bit**len(circuit.cod):discards=Id().tensor(*(Discard()ifx==qubitelseId(bit)forxincircuit.cod))circuit=circuit>>discardsreturncircuit
[docs]defto_tk(self):"""Export to t|ket>. Returns ------- tk_circuit : lambeq.backend.converters.tk.Circuit A :class:`lambeq.backend.converters.tk.Circuit`. Notes ----- * No measurements are performed. * SWAP gates are treated as logical swaps. * If the circuit contains scalars or a :class:`Bra`, then :code:`tk_circuit` will hold attributes :code:`post_selection` and :code:`scalar`. Examples -------- >>> from lambeq.backend.quantum import * >>> bell_test = H @ Id(qubit) >> CX >> Measure() @ Measure() >>> bell_test.to_tk() tk.Circuit(2, 2).H(0).CX(0, 1).Measure(0, 0).Measure(1, 1) >>> circuit0 = (Sqrt(2) @ H @ Rx(0.5) >> CX >> ... Measure() @ Discard()) >>> circuit0.to_tk() tk.Circuit(2, 1).H(0).Rx(1.0, 1).CX(0, 1).Measure(0, 0).scale(2) >>> circuit1 = Ket(1, 0) >> CX >> Id(qubit) @ Ket(0) @ Id(qubit) >>> circuit1.to_tk() tk.Circuit(3).X(0).CX(0, 2) >>> circuit2 = X @ Id(qubit ** 2) \\ ... >> Id(qubit) @ SWAP >> CX @ Id(qubit) >> Id(qubit) @ SWAP >>> circuit2.to_tk() tk.Circuit(3).X(0).SWAP(1, 2).CX(0, 1).SWAP(1, 2) >>> circuit3 = Ket(0, 0)\\ ... >> H @ Id(qubit)\\ ... >> CX\\ ... >> Id(qubit) @ Bra(0) >>> circuit3.to_tk() tk.Circuit(2, 1).H(0).CX(0, 1).Measure(1, 0).post_select({1: 0}) """fromlambeq.backend.converters.tkimportto_tkreturnto_tk(self)
[docs]defto_pennylane(self,probabilities=False,backend_config=None,diff_method='best'):""" Export lambeq circuit to PennylaneCircuit. Parameters ---------- probabilties : bool, default: False If True, the PennylaneCircuit will return the normalized probabilties of measuring the computational basis states when run. If False, it returns the unnormalized quantum states in the computational basis. backend_config : dict, default: None A dictionary of PennyLane backend configration options, including the provider (e.g. IBM or Honeywell), the device, the number of shots, etc. See the `PennyLane plugin documentation <https://pennylane.ai/plugins/>`_ for more details. diff_method : str, default: "best" The differentiation method to use to obtain gradients for the PennyLane circuit. Some gradient methods are only compatible with simulated circuits. See the `PennyLane documentation <https://docs.pennylane.ai/en/stable/introduction/interfaces.html>`_ for more details. Returns ------- :class:`lambeq.backend.pennylane.PennylaneCircuit` """fromlambeq.backend.pennylaneimportto_pennylanereturnto_pennylane(self,probabilities=probabilities,backend_config=backend_config,diff_method=diff_method)
[docs]defto_tn(self,mixed=False):"""Send a diagram to a mixed :code:`tensornetwork`. Parameters ---------- mixed : bool, default: False Whether to perform mixed (also known as density matrix) evaluation of the circuit. Returns ------- nodes : :class:`tensornetwork.Node` Nodes of the network. output_edge_order : list of :class:`tensornetwork.Edge` Output edges of the network. """ifnotmixedandnotself.is_mixed:returnsuper().to_tn(dtype=complex)diag=Id(self.dom)forleft,box,rightinself.layers:subdiag=boxifhasattr(box,'decompose'):subdiag=box.decompose()diag>>=Id(left)@subdiag@Id(right)c_nodes=[tn.CopyNode(2,2,f'c_input_{i}',dtype=complex)foriinrange(list(diag.dom).count(bit))]q_nodes1=[tn.CopyNode(2,2,f'q1_input_{i}',dtype=complex)foriinrange(list(diag.dom).count(qubit))]q_nodes2=[tn.CopyNode(2,2,f'q2_input_{i}',dtype=complex)foriinrange(list(diag.dom).count(qubit))]inputs=[n[0]forninc_nodes+q_nodes1+q_nodes2]c_scan=[n[1]forninc_nodes]q_scan1=[n[1]forninq_nodes1]q_scan2=[n[1]forninq_nodes2]nodes=c_nodes+q_nodes1+q_nodes2forleft,box,_indiag.layers:c_offset=list(left).count(bit)q_offset=list(left).count(qubit)ifisinstance(box,Swap)andbox.is_classical:c_scan[q_offset],c_scan[q_offset+1]=(c_scan[q_offset+1],c_scan[q_offset])elifisinstance(box,Discard):tn.connect(q_scan1[q_offset],q_scan2[q_offset])delq_scan1[q_offset]delq_scan2[q_offset]elifbox.is_mixed:ifisinstance(box,(Measure,Encode)):node=tn.CopyNode(3,2,'cq_'+str(box),dtype=complex)elifisinstance(box,(MixedState)):node=tn.CopyNode(2,2,'cq_'+str(box),dtype=complex)else:node=tn.Node(box.data+0j,'cq_'+str(box))c_dom=list(box.dom).count(bit)q_dom=list(box.dom).count(qubit)c_cod=list(box.cod).count(bit)q_cod=list(box.cod).count(qubit)foriinrange(c_dom):tn.connect(c_scan[c_offset+i],node[i])foriinrange(q_dom):tn.connect(q_scan1[q_offset+i],node[c_dom+i])tn.connect(q_scan2[q_offset+i],node[c_dom+q_dom+i])cq_dom=c_dom+2*q_domc_edges=node[cq_dom:cq_dom+c_cod]q_edges1=node[cq_dom+c_cod:cq_dom+c_cod+q_cod]q_edges2=node[cq_dom+c_cod+q_cod:]c_scan[c_offset:c_offset+c_dom]=c_edgesq_scan1[q_offset:q_offset+q_dom]=q_edges1q_scan2[q_offset:q_offset+q_dom]=q_edges2nodes.append(node)else:# Purely quantum boxifisinstance(box,Swap):forscanin(q_scan1,q_scan2):(scan[q_offset],scan[q_offset+1])=(scan[q_offset+1],scan[q_offset])else:utensor=box.arraynode1=tn.Node(utensor+0j,'q1_'+str(box))withbackend()asnp:node2=tn.Node(np.conj(utensor)+0j,'q2_'+str(box))foriinrange(len(box.dom)):tn.connect(q_scan1[q_offset+i],node1[i])tn.connect(q_scan2[q_offset+i],node2[i])q_scan1[q_offset:q_offset+len(box.dom)]=node1[len(box.dom):]q_scan2[q_offset:q_offset+len(box.dom)]=node2[len(box.dom):]nodes.extend([node1,node2])outputs=c_scan+q_scan1+q_scan2returnnodes,inputs+outputs
__hash__:Callable[[],int]=tensor.Diagram.__hash__
[docs]classSelfConjugate(Box):"""A self-conjugate box is equal to its own conjugate."""
[docs]@Diagram.register_special_box('cap')defgenerate_cap(left:Ty,right:Ty,is_reversed=False)->Diagram:"""Generate a cap diagram. Parameters ---------- left : Ty The left type of the cap. right : Ty The right type of the cap. is_reversed : bool, optional Unused, by default False Returns ------- Diagram The cap diagram. """assertleft==rightatomic_cap=Ket(0)@Ket(0)>>H@Sqrt(2)@qubit>>Controlled(X)d=Id()foriinrange(len(left)):d=d.then_at(atomic_cap,i)returnd
[docs]@Diagram.register_special_box('cup')defgenerate_cup(left:Ty,right:Ty,is_reversed=False)->Diagram:"""Generate a cup diagram. Parameters ---------- left : Ty The left type of the cup. right : Ty The right type of the cup. is_reversed : bool, optional Unused, by default False Returns ------- Diagram The cup diagram. """assertleft==rightatomic_cup=Controlled(X)>>H@Sqrt(2)@qubit>>Bra(0)@Bra(0)d=Id(left@right)foriinrange(len(left)):d=d.then_at(atomic_cup,len(left)-i-1)returnd
[docs]@Diagram.register_special_box('spider')defgenerate_spider(type:Ty,n_legs_in:int,n_legs_out:int)->Diagram:i,o=n_legs_in,n_legs_outifi==o==1:returnId(type)iftype==Ty():returnId()iftype!=qubit:raiseNotImplementedError('Multi-qubit spiders are not presently'' supported.')if(i,o)==(1,0):returncast(Diagram,Sqrt(2)@H>>Bra(0))if(i,o)==(2,1):returncast(Diagram,CX>>Id(qubit)@Bra(0))ifo>i:returngenerate_spider(type,o,i).dagger()ifo!=1:returngenerate_spider(type,i,1)>>generate_spider(type,1,o)ifi%2:return(generate_spider(type,i-1,1)@Id(type)>>generate_spider(type,2,1))half_spiders=generate_spider(type,i//2,1)returnhalf_spiders@half_spiders>>generate_spider(type,2,1)
[docs]@Diagram.register_special_box('swap')classSwap(tensor.Swap,SelfConjugate,Box):"""A swap box in the quantum category."""type:Tyn_legs_in:intn_legs_out:intname:strdom:Tycod:Tyz:int=0
[docs]def__init__(self,left:Ty,right:Ty):"""Initialise a swap box. Parameters ---------- left : Ty The left type of the swap. right : Ty The right type of the swap. """Box.__init__(self,'SWAP',left@right,right@left,np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]))tensor.Swap.__init__(self,left,right)
[docs]classKet(SelfConjugate,Box):"""A ket in the quantum category. A ket is a box that initializes a qubit to a given state. """def__new__(cls,*bitstring:int):iflen(bitstring)<=1:returnsuper(Ket,cls).__new__(cls)returnId().tensor(*[cls(bit)forbitinbitstring])
[docs]def__init__(self,bit:int)->None:"""Initialise a ket box. Parameters ---------- bit : int The state of the qubit (either 0 or 1). """assertbitin{0,1}self.bit=bitsuper().__init__(str(bit),Ty(),qubit,np.eye(2)[bit].T)
[docs]classBra(SelfConjugate,Box):"""A bra in the quantum category. A bra is a box that measures a qubit in the computational basis and post-selects on a given state. """def__new__(cls,*bitstring:int):iflen(bitstring)<=1:returnsuper(Bra,cls).__new__(cls)returnId().tensor(*[cls(bit)forbitinbitstring])
[docs]def__init__(self,bit:int):"""Initialise a bra box. Parameters ---------- bit : int The state of the qubit to post-select on (either 0 or 1). """assertbitin{0,1}self.bit=bitsuper().__init__(str(bit),qubit,Ty(),np.eye(2)[bit])
[docs]classParametrized(Box):"""A parametrized box in the quantum category. A parametrized box is a unitary gate that can be parametrized by a real number. Parameters ---------- name : str The name of the box. dom : Ty The domain of the box. cod : Ty The codomain of the box. data : float The parameterised unitary of the box. is_mixed : bool, default: False Whether the box is mixed self_adjoint : bool, default: False Whether the box is self-adjoint """name:strdom:Tycod:Tydata:floatis_mixed:bool=Falseself_adjoint:bool=False
[docs]deflambdify(self,*symbols,**kwargs):"""Return a lambda function that evaluates the box."""returnlambda*xs:type(self)(lambdify(symbols,self.data)(*xs))
@propertydefmodules(self):ifself.free_symbols:raiseRuntimeError('Attempting to access modules for a symbolic expression. '+'Eval of a symbolic expression is not supported.')else:returnget_backend()
[docs]classRotation(Parametrized):"""Single qubit gate defining a rotation around the bloch sphere."""
[docs]classRx(AntiConjugate,Rotation):"""Single qubit gate defining a rotation aound the x-axis."""@propertydefarray(self):withbackend()asnp:half_theta=np.pi*self.phasesin=self.modules.sin(half_theta)cos=self.modules.cos(half_theta)I_arr=np.eye(2)X_arr=np.array([[0,1],[1,0]])returncos*I_arr-1j*sin*X_arr
[docs]classRy(SelfConjugate,Rotation):"""Single qubit gate defining a rotation aound the y-axis."""@propertydefarray(self):withbackend()asnp:half_theta=np.pi*self.phasesin=self.modules.sin(half_theta)cos=self.modules.cos(half_theta)I_arr=np.eye(2)Y_arr=np.array([[0,1j],[-1j,0]])returncos*I_arr-1j*sin*Y_arr
[docs]classRz(AntiConjugate,Rotation):"""Single qubit gate defining a rotation aound the z-axis."""@propertydefarray(self):withbackend()asnp:half_theta=self.modules.pi*self.phaseexp1=np.e**(-1j*half_theta)exp2=np.e**(1j*half_theta)P_0=np.array([[1,0],[0,0]])P_1=np.array([[0,0],[0,1]])returnexp1*P_0+exp2*P_1
[docs]classControlled(Parametrized):"""A gate that applies a unitary controlled by a qubit's state."""
[docs]def__init__(self,controlled:Box,distance=1):"""Initialise a controlled box. Parameters ---------- controlled : Box The box to be controlled. distance : int, optional The distance between the control and the target, by default 1 """assertdistanceself.distance=distanceself.controlled=controlledwidth=len(controlled.dom)+abs(distance)super().__init__(f'C{controlled}',qubit**width,qubit**width,controlled.data,controlled.is_mixed)
def__hash__(self)->int:returnhash((self.controlled,self.distance))def__setattr__(self,__name:str,__value:Any)->None:if__name=='data':self.controlled.data=__valuereturnsuper().__setattr__(__name,__value)@propertydefphase(self)->float:ifisinstance(self.controlled,Rotation):returnself.controlled.phaseelse:raiseAttributeError('Controlled gate has no phase.')
[docs]defdecompose(self)->Diagram|Box:"""Split a box (distance >1) into distance 1 box + swaps."""ifself.distance==1:returnselfn_qubits=len(self.dom)skipped_qbs=n_qubits-(1+len(self.controlled.dom))ifself.distance>0:pattern=[0,*range(skipped_qbs+1,n_qubits),*range(1,skipped_qbs+1)]else:pattern=[n_qubits-1,*range(n_qubits-1)]perm:Diagram=Diagram.permutation(self.dom,pattern)diagram=(perm>>type(self)(self.controlled)@Id(qubit**skipped_qbs)>>perm.dagger())returndiagram
[docs]deflambdify(self,*symbols,**kwargs):"""Return a lambda function that evaluates the box."""c_fn=self.controlled.lambdify(*symbols)returnlambda*xs:type(self)(c_fn(*xs),distance=self.distance)
[docs]@dataclassclassScalar(Box):"""A scalar amplifies a quantum state by a given factor."""data:float|np.ndarrayname:str=field(init=False)dom:Ty=field(default=Ty(),init=False)cod:Ty=field(default=Ty(),init=False)is_mixed:bool=field(default=False,init=False)self_adjoint:bool=field(default=False,init=False)z:int=field(default=0,init=False)def__post_init__(self)->None:self.name=f'{self.data:.3f}'@propertydefarray(self):withbackend()asnp:returnnp.array(self.data)__hash__:Callable[[Box],int]=Box.__hash__
[docs]@dataclassclassDaggered(tensor.Daggered,Box):"""A daggered gate reverses the box's effect on a quantum state. Parameters ---------- box : Box The box to be daggered. """box:Boxname:str=field(init=False)dom:Ty=field(init=False)cod:Ty=field(init=False)data:float|np.ndarray|None=field(default=None,init=False)is_mixed:bool=field(default=False,init=False)self_adjoint:bool=field(default=False,init=False)z:int=field(init=False)def__post_init__(self)->None:self.name=self.box.name+'†'self.dom=self.box.codself.cod=self.box.domself.data=self.box.dataself.z=0self.is_mixed=self.box.is_mixeddef__setattr__(self,__name:str,__value:Any)->None:if__name=='data':self.box.data=__valuereturnsuper().__setattr__(__name,__value)
[docs]classBit(Box):"""Classical state for a given bit."""def__new__(cls,*bitstring:int):iflen(bitstring)<=1:returnsuper(Bit,cls).__new__(cls)returnId().tensor(*[cls(bit)forbitinbitstring])
[docs]def__init__(self,bit_value:int)->None:"""Initialise a ket box. Parameters ---------- bit_value : int The state of the qubit (either 0 or 1). """assertbit_valuein{0,1}self.bit=bit_valuesuper().__init__(str(bit_value),Ty(),bit,np.eye(2)[bit_value].T)
[docs]defto_circuital(diagram:Diagram)->Diagram:"""Takes a :py:class:`lambeq.quantum.Diagram`, returns a modified :py:class:`lambeq.quantum.Diagram` which is easier to convert to tket and other circuit simulators Parameters ---------- diagram : :py:class:`~lambeq.backend.quantum.Diagram` The :py:class:`Circuits <lambeq.backend.quantum.Diagram>` to be converted to a tket circuit. The returned circuit diagram has all qubits at the top with layer depth equal to qubit index, followed by gates, and then post-selection measurements at the bottom. Returns ------- :py:class:`lambeq.quantum.Diagram` Circuital diagram compatible with circuital_to_dict. """# bits and qubits are lists of register indices, at layer i we want# len(bits) == circuit[:i].cod.count(bit) and same for qubits# Necessary to ensure editing boxes is localized.circuit=fast_deepcopy(diagram)qubits:list[Layer]=[]gates:list[Layer]=[]measures:list[Layer]=[]postselect:list[Layer]=[]circuit=circuit.init_and_discard()# Cleans up any '1' kets and converts them to X|0> -> |1>defremove_ketbra1(_,box:Box)->Diagram|Box:ob_map:dict[Box,Diagram]ob_map={Ket(1):Ket(0)>>X,# type: ignore[dict-item]Bra(1):X>>Bra(0)}# type: ignore[dict-item]returnob_map.get(box,box)defadd_qubit(qubits:list[Layer],layer:Layer,offset:int,gates:list[Layer])->Tuple[list[Layer],list[Layer]]:""" Adds a qubit to the qubit list. Shifts all the gates to accommodate new qubit. Assumes we only add one qubit at a time. """forqubit_layerinqubits:from_left=len(qubit_layer.left)iffrom_left>=offset:qubit_layer.left=qubit_layer.left.insert(layer.box.cod,offset)layer.right=Ty()ifoffset>0:layer.left=qubit**offsetelse:layer.left=Ty()qubits.insert(offset,layer)returnqubits,pull_qubit_through(offset,gates,dom=layer.box.cod)[0]defconstruct_measurements(last_layer:Layer,post_selects:list[Layer])->list[Layer]:# Change to accommodate measurements beforetotal_qubits=(len(last_layer.left)+len(last_layer.box.cod)+len(last_layer.right))bit_idx=list(range(total_qubits))q_idx={}forlayerinpost_selects:# Find the qubit for each post selectionq_idx[bit_idx[len(layer.left)]]=layerbit_idx.remove(bit_idx[len(layer.left)])# Inserting to the left is always trivialtotal_layer=([*last_layer.left]+[*last_layer.box.cod]+[*last_layer.right])new_postselects=[]forkeyinsorted(q_idx.keys()):bits_left=sum([1foriinbit_idxifi<key])q_idx[key].left=bit**bits_leftq_idx[key].right=q_idx[key].right._fromiter(total_layer[key+1:])new_postselects.append(q_idx[key])returnnew_postselectsdefpull_bit_through(q_idx:int,gates:list[Layer],layer:Layer)->tuple[list[Layer],int]:""" Inserts a qubit type into every layer at the appropriate index q_idx: idx - index of where to insert the gate. """fori,gate_layerinenumerate(gates):# noqa: B007l_size=len(gate_layer.left)c_size=len(gate_layer.box.cod)d_size=len(gate_layer.box.dom)# Inserting to the left is always trivialifq_idx==l_size:breakelifq_idx<l_size:gate_layer.left=gate_layer.left.replace(qubit,q_idx)# Qubit on right of gate. Handles 1 qubit gates by l(dom) = 1elifq_idx>l_size+len(gate_layer.box.dom)-1:# Index relative to the 1st qubit on rightr_rel=q_idx-(l_size+len(gate_layer.box.dom))# Insert on right. Update relative index from the leftgate_layer.right=gate_layer.right.replace(qubit,r_rel)q_idx=r_rel+l_size+len(gate_layer.box.cod)elifc_size==d_size:# Initial control qubit boxbox=gate_layer.boxbox.dom=box.dom.replace(qubit,q_idx-l_size)box.cod=box.cod.replace(qubit,q_idx-l_size)else:raiseNotImplementedError('Cannot pull bit through 'f'box {gate_layer}')# Insert layer back into list and remove from the originallayer=build_left_right(q_idx,layer,[gates[i-1]])gates.insert(i,layer)returngates,q_idxdefpull_qubit_through(q_idx:int,gates:list[Layer],dom:Ty=qubit)->tuple[list[Layer],int]:# noqa: E501""" Inserts a qubit type into every layer at the appropriate index q_idx: idx - index of where to insert the gate. """new_gates=[]forgate_layeringates:l_size=len(gate_layer.left)# Inserting to the left is always trivialifq_idx<=l_size:gate_layer.left=gate_layer.left.insert(dom,q_idx)new_gates.append(gate_layer)# Qubit on right of gate. Handles 1 qubit gates by l(dom) = 1elifq_idx>l_size+len(gate_layer.box.dom)-1:# Index relative to the 1st qubit on rightr_rel=q_idx-(l_size+len(gate_layer.box.dom))# Insert on right. Update relative index from the leftgate_layer.right=gate_layer.right.insert(dom,r_rel)q_idx=r_rel+l_size+len(gate_layer.box.cod)new_gates.append(gate_layer)else:ifisinstance(gate_layer.box,Controlled):gate_qubits=[len(gate_layer.left)+jforjinrange(len(gate_layer.box.dom))]# Initial control qubit boxdists=[0]curr_box:Box|Controlled=gate_layer.boxwhileisinstance(curr_box,Controlled):# Compute relative index control qubitsdists.append(curr_box.distance+sum(dists))curr_box=curr_box.controlledprev_pos=-1*min(dists)+gate_qubits[0]curr_box=gate_layer.boxwhileisinstance(curr_box,Controlled):curr_pos=prev_pos+curr_box.distanceifprev_pos<q_idxandq_idx<=curr_pos:curr_box.distance=curr_box.distance+1elifq_idx<=prev_posandq_idx>curr_pos:curr_box.distance=curr_box.distance-1prev_pos=curr_poscurr_box=curr_box.controlledbox=gate_layer.boxbox.dom=box.dom.insert(dom,q_idx-l_size)box.cod=box.cod.insert(dom,q_idx-l_size)new_gates.append(gate_layer)ifisinstance(gate_layer.box,Swap):""" Replace single swap with a series of swaps Swaps are 2 wide, so if a qubit is pulled through we have to use the pulled qubit as an temp ancillary. """new_gates.append(Layer(gate_layer.left,Swap(qubit,qubit),dom>>gate_layer.right))new_gates.append(Layer(dom>>gate_layer.left,Swap(qubit,qubit),gate_layer.right))new_gates.append(Layer(gate_layer.left,Swap(qubit,qubit),dom>>gate_layer.right))returnnew_gates,q_idxdefbuild_left_right(q_idx:int,layer:Layer,layers:list[Layer])->Layer:""" We assume that the left and right are constructable from the last gate and the left position of the bra. (We type check at the end.) Rebuild left and right based on the last layer """iflen(layers)==0:returnlayergate_layer=layers[-1]total_layer=([*gate_layer.left]+[*gate_layer.box.cod]+[*gate_layer.right])# Assumes you're only inserting one qubit at a timetotal_layer[q_idx]=layer.box.codifq_idx==0ornottotal_layer[:q_idx]:layer.left=Ty()else:layer.left=layer.left._fromiter(total_layer[:q_idx])ifq_idx==len(total_layer)-1ornottotal_layer[q_idx+1:]:layer.right=Ty()else:layer.right=layer.right._fromiter(total_layer[q_idx+1:])returnlayercircuit=Functor(target_category=quantum,ob=lambda_,x:x,ar=remove_ketbra1)(circuit)# type: ignore [arg-type]layers=circuit.layersfori,layerinenumerate(layers):ifisinstance(layer.box,Ket):qubits,gates=add_qubit(qubits,layer,len(layer.left),gates)elifisinstance(layer.box,(Bra,Discard)):q_idx=len(layer.left)layers[i+1:],q_idx=pull_qubit_through(q_idx,layers[i+1:])layer=build_left_right(q_idx,layer,layers[i+1:])postselect.insert(0,layer)else:gates.append(layer)ifgates:postselect=construct_measurements(gates[-1],postselect)# Rebuild the diagramdiags=[Diagram(dom=layer.dom,cod=layer.cod,layers=[layer])# type: ignore [arg-type] # noqa: E501forlayerinqubits+gates+postselect+measures]layerD=diags[0]fordiagramindiags[1:]:layerD=layerD>>diagramreturnlayerD
[docs]@dataclassclassGate:"""Gate information for backend circuit construction. Parameters ---------- name : str Arbitrary name / id gtype : str Type for backend conversion, e.g., 'Rx', 'X', etc. qubits : list[int] List of qubits the gate acts on. phase : Union[float, Symbol, None] = 0 Phase parameter for gate. dagger : bool = False Whether to dagger the gate. control : Optional[list[int]] = None For control gates, list of all the control qubits. gate_q : Optional[int] = None For control gates, the gates being controlled. """name:strgtype:strqubits:list[int]phase:Union[float,Symbol,None]=0dagger:bool=Falsecontrol:Optional[list[int]]=Nonegate_q:Optional[int]=None
[docs]@classmethoddeffrom_box(cls,box:Box,offset:int,use_sympy:bool=False)->Gate:"""Constructs Gate for backend circuit construction from a Box. Parameters ---------- box : Box Box to convert to a Gate. offset : int Qubit index on the leftmost part of the Gate. use_sympy : bool Use `sympy.Symbol` for the gate params, otherwise use `lambeq.backend.Symbol`. """name=box.namegtype=box.name.split('(')[0]qubits=[offset+jforjinrange(len(box.dom))]phase=Nonedagger=Falsecontrol=Nonegate_q=Noneifisinstance(box,Daggered):box=box.dagger()dagger=Truegtype=box.name.split('(')[0]ifisinstance(box,(Rx,Ry,Rz)):phase=box.phaseifuse_sympyandisinstance(box.phase,Symbol):# Tket uses sympy, lambeq uses custom symbolphase=box.phase.to_sympy()elifisinstance(box,Controlled):# reverse the distance orderdists=[]curr_box:Box|Controlled=boxwhileisinstance(curr_box,Controlled):dists.append(curr_box.distance)curr_box=curr_box.controlleddists.reverse()# Index of the controlled qubit is the last entry in rel_idxrel_idx=[0]fordistindists:ifdist>0:# Add control to the left, offset by distancerel_idx=[0]+[i+distforiinrel_idx]else:# Add control to the right, don't offsetright_most_idx=max(rel_idx)rel_idx.insert(-1,right_most_idx-dist)i_qubits=[qubits[i]foriinrel_idx]qubits=i_qubitscontrol=sorted(qubits[:-1])gate_q=qubits[-1]ifgtypein('CRx','CRz'):phase=box.phaseifuse_sympyandisinstance(box.phase,Symbol):# Tket uses sympy, lambeq uses custom symbolphase=box.phase.to_sympy()elifisinstance(box,Scalar):gtype='Scalar'phase=box.arrayreturnGate(name,gtype,qubits,phase,dagger,control,gate_q)
[docs]@dataclassclassCircuitInfo:"""Info for constructing circuits with backends. Parameters ---------- total_qubits : int Total number of qubits in the circuit. gates : list[:py:class:`~lambeq.backend.quantum.Gate`] List containing gates, in topological ordering. bitmap: dict[int, int] Dictionary mapping qubit index to bit index for measurements, postselection, etc. postmap: dict[int, int] Dictionary mapping qubit index to post selection value. discards: list[int] List of discarded qubit indeces. """total_qubits:intgates:list[Gate]bitmap:dict[int,int]postmap:dict[int,int]discards:list[int]
[docs]defreadoff_circuital(diagram:Diagram,use_sympy:bool=False)->CircuitInfo:"""Takes a circuital :py:class:`lambeq.quantum.Diagram`, returns a :py:class:`~lambeq.backend.quantum.CircuitInfo` which is used by quantum backends to construct circuits. This checks if the diagram is circuital before converting. Parameters ---------- diagram : :py:class:`~lambeq.backend.quantum.Diagram` The :py:class:`Circuits <lambeq.backend.quantum.Diagram>` to be converted to dictionary. use_sympy : bool, default=False Flag to use `sympy.Symbol` instead of `lambeq.backend.Symbol` for the parameters. Returns ------- :py:class:`~lambeq.backend.quantum.CircuitInfo` """assertdiagram.is_circuitallayers=diagram.layerstotal_qubits=sum([1forlayerinlayersifisinstance(layer.box,Ket)])available_qubits=list(range(total_qubits))gates:list[Gate]=[]bitmap:dict={}postmap:dict={}discards:list[int]=[]forlayerinlayers:ifisinstance(layer.box,Ket):passelifisinstance(layer.box,Measure):qi=available_qubits[layer.left.count(qubit)]available_qubits.remove(qi)bitmap[qi]=len(bitmap)elifisinstance(layer.box,Bra):qi=available_qubits[layer.left.count(qubit)]available_qubits.remove(qi)bitmap[qi]=len(bitmap)postmap[qi]=layer.box.bitelifisinstance(layer.box,Discard):qi=available_qubits[layer.left.count(qubit)]available_qubits.remove(qi)discards.append(qi)else:qi=len(layer.left)gates.append(Gate.from_box(layer.box,qi,use_sympy=use_sympy))returnCircuitInfo(total_qubits,gates,bitmap,postmap,discards)