# 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."""Tensor category===============Lambeq's internal representation of the tensor category. This work isbased on DisCoPy (https://discopy.org/) which is released under theBSD 3-Clause 'New' or 'Revised' License."""from__future__importannotationsfromcollections.abcimportCallable,Iterablefromdataclassesimportdataclass,field,replacefromfunctoolsimportcached_propertyimportmathfromtypingimportMappingimportnumpyasnpimporttensornetworkastnfromtyping_extensionsimportAny,Selffromlambeq.backendimportgrammarfromlambeq.backend.numerical_backendimportbackend,get_backendfromlambeq.backend.symbolimportlambdify,Symboltensor=grammar.Category('tensor')
[docs]@tensor('Ty')@dataclass(init=False)classDim(grammar.Ty):"""Dimension in the tensor category. Attributes ---------- dim : tuple of int Tuple of dimensions represented by the object. product: int Product of contained dimensions. """objects:list[Self]# type: ignore[assignment,misc]
[docs]def__init__(self,*dim:int,objects:list[Self]|None=None)->None:"""Initialise a Dim type. Parameters ---------- dim : list[int] List of dimensions to initialise. objects: list[Self] or None, default None List of `Dim`s, to prepare a non-atomic `Dim` object. """ifobjects:assertnotlen(dim)super().__init__(objects=objects)# type: ignore[arg-type]else:dims:list[int]=list(filter(lambdax:x>1,dim))ifnotlen(dims):super().__init__()iflen(dims)==1:super().__init__(str(dims[0]))else:super().__init__(objects=[Dim(d)fordindims])
[docs]@dataclass(init=False)@tensorclassBox(grammar.Box):"""Box (tensor) in the the tensor category. Attributes ---------- data : np.array or float or None Data used to represent the `array` attribute. Typically either the array itself, or a symbolic array. array : np.array or float Tensor which the box represents. free_symbols : set of lambeq.backend.symbol.Symbol In case of a symbolic tensor, set of symbols in the box's data. """name:strdom:Dimcod:Dimdata:float|np.ndarray|Nonez:int
[docs]def__init__(self,name:str,dom:Dim,cod:Dim,data:float|np.ndarray|None=None,z:int=0):"""Initialise a `tensor.Box` type. Parameters ---------- name : str Name for the box. dom : Dim Dimension of the box's domain. cod : Dim Dimension of the box's codomain. data : float or np.ndarray, optional The concrete tensor the box represents. z : int, optional Winding number of the box, indicating conjugation. Starts at 0 if not provided. """self.name=nameself.dom=domself.cod=codself.data=dataself.z=z
def__eq__(self,other):return(self.name==other.nameandself.dom==other.domandself.cod==other.codandnp.equal(self.data,other.data).all())@propertydefarray(self):ifself.dataisnotNone:ifself.z%2:ret_arr=self._conjugate_array()else:ret_arr=get_backend().array(self.data)returnret_arr.reshape(self.dom.dim+self.cod.dim)def_adjoint_array(self):"""Returns the adjoint of the box's data"""arr=self.arraysource=range(len(self.dom@self.cod))target=[i+len(self.cod)ifi<len(self.dom)elsei-len(self.dom)foriinrange(len(self.dom@self.cod))]withbackend()asnp:returnnp.conjugate(np.moveaxis(arr,source,target))def_conjugate_array(self):"""Returns the diagrammtic conjugate of the box's data"""dom,cod=self.dom,self.codwithbackend()asnp:array=np.moveaxis(self.data,range(len(dom@cod)),[len(dom)-i-1foriinrange(len(dom@cod))])returnnp.conjugate(array)
[docs]defdagger(self):"""Get the dagger (adjoint) of the box. Returns ------- Box Dagger of the box. """returnDaggered(self)
[docs]defrotate(self,z:int):"""Get the result of conjugating the box `z` times. Parameters ---------- z : int Winding count. The number of conjugations to apply to the box. Returns ------- Box The box conjugated z times. """returnreplace(self,dom=self.dom.rotate(z),cod=self.cod.rotate(z),z=(self.z+z)%2)
@cached_propertydeffree_symbols(self)->set[Symbol]:defrecursive_free_symbols(data)->set[Symbol]:ifisinstance(data,Mapping):data=data.values()ifisinstance(data,Iterable):ifnothasattr(data,'shape')ordata.shape!=():returnset().union(*map(recursive_free_symbols,data))# Remove scale before adding to setreturn{data.unscaled}ifisinstance(data,Symbol)elseset()returnrecursive_free_symbols(self.data)
[docs]deflambdify(self,*symbols:'Symbol',**kwargs)->Callable:"""Get a lambdified version of a symbolic box. Returns a function which when provided appropriate parameters, initialises a concrete box. Parameters ---------- symbols : list of Symbols List of symbols in the box in the order in which their assigned values will appear in the concretisation call. kwargs: Additional parameters to pass to `lambdify`. Returns ------- Callable[..., Box]: A lambda function which when invoked with appropriate parameters, returns a concrete version of the box. """ifnotany(xinself.free_symbolsforxinsymbols):returnlambda*xs:selfreturnlambda*xs:type(self)(self.name,self.dom,self.cod,lambdify(symbols,self.data)(*xs))
[docs]defeval(self,contractor=tn.contractors.auto,dtype:type|None=None):"""Evaluate the tensor diagram. Parameters ---------- contractor : tn contractor `tensornetwork` contractor for chosen contraction algorithm. dtype : type, optional Data type of the resulting array. Defaults to `np.float32`. Returns ------- numpy.ndarray n-dimension array representing the contracted tensor. """returncontractor(*self.to_tn(dtype=dtype)).tensor
[docs]defto_tn(self,dtype:type|None=None):"""Convert the diagram to a `tensornetwork` TN. Parameters ---------- dtype : type, optional Data type of the resulting array. Defaults to `np.float32`. Returns ------- tuple[list[tn.Node], list[tn.Edge]] `tensornetwork` representation of the diagram. An edge object is returned for each dangling edge in the network. """ifdtypeisNone:dtype=np.float32backend=get_backend().namenodes=[tn.CopyNode(2,dim,dtype=dtype,backend=backend)fordiminself.dom.dim]inputs=[node[0]fornodeinnodes]scan=[node[1]fornodeinnodes]diag=self.category.Diagram.id(self.dom)forlayerinself.layers:left,box,right=layer.unpack()subdiag=boxifhasattr(box,'decompose'):subdiag=box.decompose()diag>>=(self.category.Diagram.id(left)
@Diagram.register_special_box('cap')classCap(grammar.Cap,Box):"""A Cap in the tensor category."""left:Dimright:Dimdom:Dimcod:Dimz:int=0is_reversed:bool=False
[docs]def__init__(self,left:Dim,right:Dim,is_reversed:bool=False):"""Initialise a tensor Cap. Parameters ---------- left : Dim Dimension (type) of the left leg of the cap. Must be the conjugate of `right`. right : Dim Dimension (type) of the right leg of the cap. Must be the conjugate of `left`. is_reversed : bool, default False Ignored parameter, since left and right conjugates are equivalent in the tensor category. Necessary to inherit from `grammar.Cap` appropriately. """super().__init__(left,right)arr=np.zeros(left.product**2)arr[0]=1arr[-1]=1self.data=arr
__hash__=Box.__hash____repr__=Box.__repr__
[docs]@Diagram.register_special_box('cup')classCup(grammar.Cup,Box):"""A Cup in the tensor category."""left:Dimright:Dimname:strdom:Dimcod:Dimz:int=0is_reversed:bool=False
[docs]def__init__(self,left:Dim,right:Dim,is_reversed:bool=False):"""Initialise a tensor Cup. Parameters ---------- left : Dim Dimension (type) of the left leg of the cup. Must be the conjugate of `right`. right : Dim Dimension (type) of the right leg of the cup. Must be the conjugate of `left`. is_reversed : bool, default False Ignored parameter, since left and right conjugates are equivalent in the tensor category. Necessary to inherit from `grammar.Cup` appropriately. """super().__init__(left,right)arr=np.zeros(left.product**2)arr[0]=1arr[-1]=1self.data=arr
__hash__=Box.__hash____repr__=Box.__repr__
[docs]@Diagram.register_special_box('swap')classSwap(grammar.Swap,Box):"""A Swap in the tensor category."""left:Dimright:Dimname:strdom:Dimcod:Dimz:int=0
[docs]def__init__(self,left:Dim,right:Dim):"""Initialise a tensor Swap. Parameters ---------- left : Dim Dimension (type) of the left input of the swap. right : Dim Dimension (type) of the right input of the swap. """grammar.Swap.__init__(self,left,right)Box.__init__(self,'SWAP',left@right,right@left)
[docs]@Diagram.register_special_box('spider')classSpider(grammar.Spider,Box):"""A Spider in the tensor category. Concretely represented by a copy node. """type:Dimn_legs_in:intn_legs_out:intname:strdom:Dimcod:Dimz:int=0
[docs]def__init__(self,type:Dim,n_legs_in:int,n_legs_out:int):"""Initialise a tensor Spider. Parameters ---------- type : Dim Dimension (type) of each leg of the spider. n_legs_in : int Number of input legs of the spider. n_legs_out : int Number of input legs of the spider. """Box.__init__(self,'SPIDER',type**n_legs_in,type**n_legs_out)grammar.Spider.__init__(self,type,n_legs_in,n_legs_out)
[docs]@dataclassclassDaggered(grammar.Daggered,Box):"""A daggered box. Attributes ---------- box : Box The box to be daggered. """box:Boxname:str=field(init=False)dom:Dim=field(init=False)cod:Dim=field(init=False)data:float|np.ndarray|None=field(default=None,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=self.box.zdef__setattr__(self,__name:str,__value:Any)->None:if__name=='data':self.box.data=__valuereturnsuper().__setattr__(__name,__value)