# Copyright 2018-2022 Xanadu Quantum Technologies Inc.# 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."""The Pauli arithmetic abstract reduced representation classes"""# pylint:disable=protected-accessfromcopyimportcopyfromfunctoolsimportlru_cache,reduceimportnumpyasnpfromscipyimportsparseimportpennylaneasqmlfrompennylaneimportmathfrompennylane.opsimportIdentity,PauliX,PauliY,PauliZ,Prod,SProd,Sumfrompennylane.typingimportTensorLikefrompennylane.wiresimportWires,WiresLikeI="I"X="X"Y="Y"Z="Z"op_map={I:Identity,X:PauliX,Y:PauliY,Z:PauliZ,}op_to_str_map={Identity:I,PauliX:X,PauliY:Y,PauliZ:Z,}matI=np.eye(2)matX=np.array([[0,1],[1,0]])matY=np.array([[0,-1j],[1j,0]])matZ=np.array([[1,0],[0,-1]])mat_map={I:matI,X:matX,Y:matY,Z:matZ,}anticom_map={I:{I:0,X:0,Y:0,Z:0},X:{I:0,X:0,Y:1,Z:1},Y:{I:0,X:1,Y:0,Z:1},Z:{I:0,X:1,Y:1,Z:0},}@lru_cachedef_make_operation(op,wire):returnop_map[op](wire)@lru_cachedef_cached_sparse_data(op):"""Returns the sparse data and indices of a Pauli operator."""ifop=="I":data=np.array([1.0,1.0],dtype=np.complex128)indices=np.array([0,1],dtype=np.int64)elifop=="X":data=np.array([1.0,1.0],dtype=np.complex128)indices=np.array([1,0],dtype=np.int64)elifop=="Y":data=np.array([-1.0j,1.0j],dtype=np.complex128)indices=np.array([1,0],dtype=np.int64)else:# op == "Z"data=np.array([1.0,-1.0],dtype=np.complex128)indices=np.array([0,1],dtype=np.int64)returndata,indices@lru_cache(maxsize=2)def_cached_arange(n):"Caches `np.arange` output to speed up sparse calculations."returnnp.arange(n)pauli_to_sparse_int={I:0,X:1,Y:1,Z:0}# (I, Z) and (X, Y) have the same sparsitydef_ps_to_sparse_index(pauli_words,wires):"""Represent the Pauli words sparse structure in a matrix of shape n_words x n_wires."""indices=np.zeros((len(pauli_words),len(wires)))fori,pwinenumerate(pauli_words):ifnotpw.wires:continuewire_indices=np.array(wires.indices(pw.wires))indices[i,wire_indices]=[pauli_to_sparse_int[pw[w]]forwinpw.wires]returnindices_map_I={I:(1,I),X:(1,X),Y:(1,Y),Z:(1,Z),}_map_X={I:(1,X),X:(1,I),Y:(1.0j,Z),Z:(-1.0j,Y),}_map_Y={I:(1,Y),X:(-1.0j,Z),Y:(1,I),Z:(1j,X),}_map_Z={I:(1,Z),X:(1j,Y),Y:(-1.0j,X),Z:(1,I),}mul_map={I:_map_I,X:_map_X,Y:_map_Y,Z:_map_Z}
[docs]classPauliWord(dict):r""" Immutable dictionary used to represent a Pauli Word, associating wires with their respective operators. Can be constructed from a standard dictionary. .. note:: An empty :class:`~.PauliWord` will be treated as the multiplicative identity (i.e identity on all wires). Its matrix is the identity matrix (trivially the :math:`1\times 1` one matrix when no ``wire_order`` is passed to ``PauliWord({}).to_mat()``). **Examples** Initializing a Pauli word: >>> w = PauliWord({"a": 'X', 2: 'Y', 3: 'Z'}) >>> w X(a) @ Y(2) @ Z(3) When multiplying Pauli words together, we obtain a :class:`~PauliSentence` with the resulting ``PauliWord`` as a key and the corresponding coefficient as its value. >>> w1 = PauliWord({0:"X", 1:"Y"}) >>> w2 = PauliWord({1:"X", 2:"Z"}) >>> w1 @ w2 -1j * Z(1) @ Z(2) @ X(0) We can multiply scalars to Pauli words or add/subtract them, resulting in a :class:`~PauliSentence` instance. >>> 0.5 * w1 - 1.5 * w2 + 2 0.5 * X(0) @ Y(1) + -1.5 * X(1) @ Z(2) + 2 * I """# this allows scalar multiplication from left with numpy arrays np.array(0.5) * pw1# taken from [stackexchange](https://stackoverflow.com/questions/40694380/forcing-multiplication-to-use-rmul-instead-of-numpy-array-mul-or-byp/44634634#44634634)__array_priority__=1000def__missing__(self,key):"""If the wire is not in the Pauli word, then no operator acts on it, so return the Identity."""returnIdef__init__(self,mapping):"""Strip identities from PauliWord on init!"""forwire,opinmapping.copy().items():ifop==I:delmapping[wire]super().__init__(mapping)@propertydefpauli_rep(self):"""Trivial pauli_rep"""returnPauliSentence({self:1.0})def__reduce__(self):"""Defines how to pickle and unpickle a PauliWord. Otherwise, un-pickling would cause __setitem__ to be called, which is forbidden on PauliWord. For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__ """return(PauliWord,(dict(self),))def__copy__(self):"""Copy the PauliWord instance."""returnPauliWord(dict(self.items()))def__deepcopy__(self,memo):res=self.__copy__()memo[id(self)]=resreturnresdef__setitem__(self,key,item):"""Restrict setting items after instantiation."""raiseTypeError("PauliWord object does not support assignment")
[docs]defupdate(self,__m,**kwargs)->None:"""Restrict updating PW after instantiation."""raiseTypeError("PauliWord object does not support assignment")
def__hash__(self):returnhash(frozenset(self.items()))def_matmul(self,other):"""Private matrix multiplication that returns (pauli_word, coeff) tuple for more lightweight processing"""base,iterator,swapped=((self,other,False)iflen(self)>=len(other)else(other,self,True))result=copy(dict(base))coeff=1forwire,terminiterator.items():ifwireinbase:factor,new_op=mul_map[term][base[wire]]ifswappedelsemul_map[base[wire]][term]ifnew_op==I:delresult[wire]else:coeff*=factorresult[wire]=new_opelifterm!=I:result[wire]=termreturnPauliWord(result),coeffdef__matmul__(self,other):"""Multiply two Pauli words together using the matrix product if wires overlap and the tensor product otherwise. Empty Pauli words are treated as the Identity operator on all wires. Args: other (PauliWord): The Pauli word to multiply with Returns: PauliSentence: coeff * new_word """ifisinstance(other,PauliSentence):returnPauliSentence({self:1.0})@othernew_word,coeff=self._matmul(other)returnPauliSentence({new_word:coeff})def__mul__(self,other):"""Multiply a PauliWord by a scalar Args: other (Scalar): The scalar to multiply the PauliWord with Returns: PauliSentence """ifisinstance(other,TensorLike):ifnotqml.math.ndim(other)==0:raiseValueError(f"Attempting to multiply a PauliWord with an array of dimension {qml.math.ndim(other)}")returnPauliSentence({self:other})raiseTypeError(f"PauliWord can only be multiplied by numerical data. Attempting to multiply by {other} of type {type(other)}")__rmul__=__mul__def__add__(self,other):"""Add PauliWord instances and scalars to PauliWord. Returns a PauliSentence."""# Note that the case of PauliWord + PauliSentence is covered in PauliSentenceifisinstance(other,PauliWord):ifother==self:returnPauliSentence({self:2.0})returnPauliSentence({self:1.0,other:1.0})ifisinstance(other,TensorLike):# Scalars are interepreted as scalar * IdentityIdWord=PauliWord({})ifIdWord==self:returnPauliSentence({self:1.0+other})returnPauliSentence({self:1.0,IdWord:other})returnNotImplemented__radd__=__add__def__iadd__(self,other):"""Inplace addition"""returnself+otherdef__sub__(self,other):"""Subtract other PauliSentence, PauliWord, or scalar"""returnself+-1*otherdef__rsub__(self,other):"""Subtract other PauliSentence, PauliWord, or scalar"""return-1*self+otherdef__truediv__(self,other):"""Divide a PauliWord by a scalar"""ifisinstance(other,TensorLike):returnself*(1/other)raiseTypeError(f"PauliWord can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}")
[docs]defcommutes_with(self,other):"""Fast check if two PauliWords commute with each other"""wires=set(self)&set(other)ifnotwires:returnTrueanticom_count=sum(anticom_map[self[wire]][other[wire]]forwireinwires)return(anticom_count%2)==0
def_commutator(self,other):"""comm between two PauliWords, returns tuple (new_word, coeff) for faster arithmetic"""# This may be helpful to developers that need a more lightweight comm between pauli words# without creating PauliSentence classesifself.commutes_with(other):returnPauliWord({}),0.0new_word,coeff=self._matmul(other)returnnew_word,2*coeff
[docs]defcommutator(self,other):""" Compute commutator between a ``PauliWord`` :math:`P` and other operator :math:`O` .. math:: [P, O] = P O - O P When the other operator is a :class:`~PauliWord` or :class:`~PauliSentence`, this method is faster than computing ``P @ O - O @ P``. It is what is being used in :func:`~commutator` when setting ``pauli=True``. Args: other (Union[Operator, PauliWord, PauliSentence]): Second operator Returns: ~PauliSentence: The commutator result in form of a :class:`~PauliSentence` instances. **Examples** You can compute commutators between :class:`~PauliWord` instances. >>> pw = PauliWord({0:"X"}) >>> pw.commutator(PauliWord({0:"Y"})) 2j * Z(0) You can also compute the commutator with other operator types if they have a Pauli representation. >>> pw.commutator(qml.Y(0)) 2j * Z(0) """ifisinstance(other,PauliWord):new_word,coeff=self._commutator(other)ifcoeff==0:returnPauliSentence({})returnPauliSentence({new_word:coeff})ifisinstance(other,qml.operation.Operator):op_self=PauliSentence({self:1.0})returnop_self.commutator(other)ifisinstance(other,PauliSentence):# for infix method, this would be handled by __ror__return-1.0*other.commutator(self)raiseNotImplementedError(f"Cannot compute natively a commutator between PauliWord and {other} of type {type(other)}")
def__str__(self):"""String representation of a PauliWord."""iflen(self)==0:return"I"return" @ ".join(f"{op}({w})"forw,opinself.items())def__repr__(self):"""Terminal representation for PauliWord"""returnstr(self)@propertydefwires(self):"""Track wires in a PauliWord."""returnWires(self)
[docs]defto_mat(self,wire_order=None,format="dense",coeff=1.0):"""Returns the matrix representation. Keyword Args: wire_order (iterable or None): The order of qubits in the tensor product. format (str): The format of the matrix. It is "dense" by default. Use "csr" for sparse. coeff (float): Coefficient multiplying the resulting matrix. Returns: (Union[NumpyArray, ScipySparseArray]): Matrix representation of the Pauli word. Raises: ValueError: Can't get the matrix of an empty PauliWord. """wire_order=self.wiresifwire_orderisNoneelseWires(wire_order)ifnotwire_order.contains_wires(self.wires):raiseValueError("Can't get the matrix for the specified wire order because it "f"does not contain all the Pauli word's wires {self.wires}")iflen(self)==0:n=len(wire_order)ifwire_orderisnotNoneelse0return(np.diag([coeff]*2**n)ifformat=="dense"elsecoeff*sparse.eye(2**n,format=format,dtype="complex128"))ifformat=="dense":returncoeff*reduce(math.kron,(mat_map[self[w]]forwinwire_order))returnself._to_sparse_mat(wire_order,coeff)
def_to_sparse_mat(self,wire_order,coeff):"""Compute the sparse matrix of the Pauli word times a coefficient, given a wire order. See pauli_sparse_matrices.md for the technical details of the implementation."""matrix_size=2**len(wire_order)matrix=sparse.csr_matrix((matrix_size,matrix_size),dtype="complex128")# Avoid checks and copies in __init__ by directly setting the attributes of an empty matrixmatrix.data=self._get_csr_data(wire_order,coeff)matrix.indices=self._get_csr_indices(wire_order)matrix.indptr=_cached_arange(matrix_size+1)# Non-zero entries by row (starting from 0)returnmatrixdef_get_csr_data(self,wire_order,coeff):"""Computes the sparse matrix data of the Pauli word times a coefficient, given a wire order."""full_word=[self[wire]forwireinwire_order]matrix_size=2**len(wire_order)iflen(self)==0:returnnp.full(matrix_size,coeff,dtype=np.complex128)data=np.empty(matrix_size,dtype=np.complex128)# Non-zero valuescurrent_size=2data[:current_size],_=_cached_sparse_data(full_word[-1])data[:current_size]*=coeff# Multiply initial term better than the full matrixforsinfull_word[-2::-1]:ifs=="I":data[current_size:2*current_size]=data[:current_size]elifs=="X":data[current_size:2*current_size]=data[:current_size]elifs=="Y":data[current_size:2*current_size]=1j*data[:current_size]data[:current_size]*=-1jelifs=="Z":data[current_size:2*current_size]=-data[:current_size]current_size*=2returndatadef_get_csr_data_2(self,wire_order,coeff):"""Computes the sparse matrix data of the Pauli word times a coefficient, given a wire order."""full_word=[self[wire]forwireinwire_order]nwords=len(full_word)ifnwords<2:returnnp.array([1.0]),self._get_csr_data(wire_order,coeff)outer=self._get_csr_data(wire_order[:nwords//2],1.0)inner=self._get_csr_data(wire_order[nwords//2:],coeff)returnouter,innerdef_get_csr_indices(self,wire_order):"""Computes the sparse matrix indices of the Pauli word times a coefficient, given a wire order."""full_word=[self[wire]forwireinwire_order]matrix_size=2**len(wire_order)iflen(self)==0:return_cached_arange(matrix_size)indices=np.empty(matrix_size,dtype=np.int64)# Column index of non-zero valuescurrent_size=2_,indices[:current_size]=_cached_sparse_data(full_word[-1])forsinfull_word[-2::-1]:ifs=="I":indices[current_size:2*current_size]=indices[:current_size]+current_sizeelifs=="X":indices[current_size:2*current_size]=indices[:current_size]indices[:current_size]+=current_sizeelifs=="Y":indices[current_size:2*current_size]=indices[:current_size]indices[:current_size]+=current_sizeelifs=="Z":indices[current_size:2*current_size]=indices[:current_size]+current_sizecurrent_size*=2returnindices
[docs]defoperation(self,wire_order:WiresLike=()):"""Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliWord."""iflen(self)==0:returnIdentity(wires=wire_order)ifqml.capture.enabled():# cant use lru_cache with program capturefactors=[op_map[op](wire)forwire,opinself.items()]else:factors=[_make_operation(op,wire)forwire,opinself.items()]pauli_rep=PauliSentence({self:1})returnfactors[0]iflen(factors)==1elseProd(*factors,_pauli_rep=pauli_rep)
[docs]defmap_wires(self,wire_map:dict)->"PauliWord":"""Return a new PauliWord with the wires mapped."""returnself.__class__({wire_map.get(w,w):opforw,opinself.items()})
pw_id=PauliWord({})# empty pauli word to be re-used
[docs]classPauliSentence(dict):r"""Dictionary representing a linear combination of Pauli words, with the keys as :class:`~pennylane.pauli.PauliWord` instances and the values correspond to coefficients. .. note:: An empty :class:`~.PauliSentence` will be treated as the additive identity (i.e ``0 * Identity()``). Its matrix is the all-zero matrix (trivially the :math:`1\times 1` zero matrix when no ``wire_order`` is passed to ``PauliSentence({}).to_mat()``). **Examples** >>> ps = PauliSentence({ ... PauliWord({0:'X', 1:'Y'}): 1.23, ... PauliWord({2:'Z', 0:'Y'}): -0.45j ... }) >>> ps 1.23 * X(0) @ Y(1) + (-0-0.45j) * Z(2) @ Y(0) Combining Pauli words automatically results in Pauli sentences that can be used to construct more complicated operators. >>> w1 = PauliWord({0:"X", 1:"Y"}) >>> w2 = PauliWord({1:"X", 2:"Z"}) >>> ps = 0.5 * w1 - 1.5 * w2 + 2 >>> ps + PauliWord({3:"Z"}) - 1 0.5 * X(0) @ Y(1) + -1.5 * X(1) @ Z(2) + 1 * I + 1.0 * Z(3) Note that while the empty :class:`~PauliWord` ``PauliWord({})`` respresents the identity, the empty ``PauliSentence`` represents 0 >>> PauliSentence({}) 0 * I We can compute commutators using the ``PauliSentence.commutator()`` method >>> op1 = PauliWord({0:"X", 1:"X"}) >>> op2 = PauliWord({0:"Y"}) + PauliWord({1:"Y"}) >>> op1.commutator(op2) 2j * Z(0) @ X(1) + 2j * X(0) @ Z(1) Or, alternatively, use :func:`~commutator`. >>> qml.commutator(op1, op2, pauli=True) Note that we need to specify ``pauli=True`` as :func:`~.commutator` returns PennyLane operators by default. """# this allows scalar multiplication from left with numpy arrays np.array(0.5) * ps1# taken from [stackexchange](https://stackoverflow.com/questions/40694380/forcing-multiplication-to-use-rmul-instead-of-numpy-array-mul-or-byp/44634634#44634634)__array_priority__=1000@propertydefpauli_rep(self):"""Trivial pauli_rep"""returnselfdef__missing__(self,key):"""If the PauliWord is not in the sentence then the coefficient associated with it should be 0."""return0.0
[docs]deftrace(self):r"""Return the normalized trace of the ``PauliSentence`` instance .. math:: \frac{1}{2^n} \text{tr}\left( P \right). The normalized trace does not scale with the number of qubits :math:`n`. >>> PauliSentence({PauliWord({0:"I", 1:"I"}): 0.5}).trace() 0.5 >>> PauliSentence({PauliWord({}): 0.5}).trace() 0.5 """returnself.get(pw_id,0.0)
def__add__(self,other):"""Add a PauliWord, scalar or other PauliSentence to a PauliSentence. Empty Pauli sentences are treated as the additive identity (i.e 0 * Identity on all wires). The non-empty Pauli sentence is returned. """ifisinstance(other,PauliSentence):smaller_ps,larger_ps=((self,copy(other))iflen(self)<len(other)else(other,copy(self)))forkeyinsmaller_ps:larger_ps[key]+=smaller_ps[key]returnlarger_psifisinstance(other,PauliWord):res=copy(self)ifotherinres:res[other]+=1.0else:res[other]=1.0returnresifisinstance(other,TensorLike):# Scalars are interepreted as scalar * Identityres=copy(self)IdWord=PauliWord({})ifIdWordinres:res[IdWord]+=otherelse:res[IdWord]=otherreturnresraiseTypeError(f"Cannot add {other} of type {type(other)} to PauliSentence")__radd__=__add__def__iadd__(self,other):"""Inplace addition of two Pauli sentence together by adding terms of other to self"""ifisinstance(other,PauliSentence):forkeyinother:ifkeyinself:self[key]+=other[key]else:self[key]=other[key]returnselfifisinstance(other,PauliWord):ifotherinself:self[other]+=1.0else:self[other]=1.0returnselfifisinstance(other,TensorLike):IdWord=PauliWord({})ifIdWordinself:self[IdWord]+=otherelse:self[IdWord]=otherreturnselfraiseTypeError(f"Cannot add {other} of type {type(other)} to PauliSentence")def__sub__(self,other):"""Subtract other PauliSentence, PauliWord, or scalar"""returnself+-1*otherdef__rsub__(self,other):"""Subtract other PauliSentence, PauliWord, or scalar"""return-1*self+otherdef__copy__(self):"""Copy the PauliSentence instance."""copied_ps={}forpw,coeffinself.items():copied_ps[copy(pw)]=coeffreturnPauliSentence(copied_ps)def__deepcopy__(self,memo):res=self.__copy__()memo[id(self)]=resreturnresdef__matmul__(self,other):"""Matrix / tensor product between two PauliSentences by iterating over each sentence and multiplying the Pauli words pair-wise"""ifisinstance(other,PauliWord):other=PauliSentence({other:1.0})final_ps=PauliSentence()iflen(self)==0orlen(other)==0:returnfinal_psforpw1inself:forpw2inother:prod_pw,coeff=pw1._matmul(pw2)final_ps[prod_pw]=final_ps[prod_pw]+coeff*self[pw1]*other[pw2]returnfinal_psdef__mul__(self,other):"""Multiply a PauliWord by a scalar Args: other (Scalar): The scalar to multiply the PauliWord with Returns: PauliSentence """ifisinstance(other,TensorLike):ifnotqml.math.ndim(other)==0:raiseValueError(f"Attempting to multiply a PauliSentence with an array of dimension {qml.math.ndim(other)}")returnPauliSentence({key:other*valueforkey,valueinself.items()})raiseTypeError(f"PauliSentence can only be multiplied by numerical data. Attempting to multiply by {other} of type {type(other)}")__rmul__=__mul__def__truediv__(self,other):"""Divide a PauliSentence by a scalar"""ifisinstance(other,TensorLike):returnself*(1/other)raiseTypeError(f"PauliSentence can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}")
[docs]defcommutator(self,other):""" Compute commutator between a ``PauliSentence`` :math:`P` and other operator :math:`O` .. math:: [P, O] = P O - O P When the other operator is a :class:`~PauliWord` or :class:`~PauliSentence`, this method is faster than computing ``P @ O - O @ P``. It is what is being used in :func:`~commutator` when setting ``pauli=True``. Args: other (Union[Operator, PauliWord, PauliSentence]): Second operator Returns: ~PauliSentence: The commutator result in form of a :class:`~PauliSentence` instances. **Examples** You can compute commutators between :class:`~PauliSentence` instances. >>> pw1 = PauliWord({0:"X"}) >>> pw2 = PauliWord({1:"X"}) >>> ps1 = PauliSentence({pw1: 1., pw2: 2.}) >>> ps2 = PauliSentence({pw1: 0.5j, pw2: 1j}) >>> ps1.commutator(ps2) 0 * I You can also compute the commutator with other operator types if they have a Pauli representation. >>> ps1.commutator(qml.Y(0)) 2j * Z(0)"""final_ps=PauliSentence()ifisinstance(other,PauliWord):forpw1inself:comm_pw,coeff=pw1._commutator(other)iflen(comm_pw)!=0:final_ps[comm_pw]+=coeff*self[pw1]returnfinal_psifnotisinstance(other,PauliSentence):ifother.pauli_repisNone:raiseNotImplementedError(f"Cannot compute a native commutator of a Pauli word or sentence with the operator {other} of type {type(other)}."f"You can try to use qml.commutator(op1, op2, pauli=False) instead.")other=qml.pauli.pauli_sentence(other)forpw1inself:forpw2inother:comm_pw,coeff=pw1._commutator(pw2)iflen(comm_pw)!=0:final_ps[comm_pw]+=coeff*self[pw1]*other[pw2]returnfinal_ps
def__str__(self):"""String representation of the PauliSentence."""iflen(self)==0:return"0 * I"return"\n+ ".join(f"{coeff} * {str(pw)}"forpw,coeffinself.items())def__repr__(self):"""Terminal representation for PauliSentence"""returnstr(self)@propertydefwires(self):"""Track wires of the PauliSentence."""returnWires.all_wires((pw.wiresforpwinself.keys()))
[docs]defto_mat(self,wire_order=None,format="dense",buffer_size=None):"""Returns the matrix representation. Keyword Args: wire_order (iterable or None): The order of qubits in the tensor product. format (str): The format of the matrix. It is "dense" by default. Use "csr" for sparse. buffer_size (int or None): The maximum allowed memory in bytes to store intermediate results in the calculation of sparse matrices. It defaults to ``2 ** 30`` bytes that make 1GB of memory. In general, larger buffers allow faster computations. Returns: (Union[NumpyArray, ScipySparseArray]): Matrix representation of the Pauli sentence. Raises: ValueError: Can't get the matrix of an empty PauliSentence. """wire_order=self.wiresifwire_orderisNoneelseWires(wire_order)iflen(self)==0:n=len(wire_order)ifwire_orderisnotNoneelse0ifformat=="dense":returnnp.zeros((2**n,2**n))returnsparse.csr_matrix((2**n,2**n),dtype="complex128").asformat(format)ifformat=="dense":returnself._to_dense_mat(wire_order)returnself._to_sparse_mat(wire_order,buffer_size=buffer_size).asformat(format)
def_to_sparse_mat(self,wire_order,buffer_size=None):"""Compute the sparse matrix of the Pauli sentence by efficiently adding the Pauli words that it is composed of. See pauli_sparse_matrices.md for the technical details."""pauli_words=list(self)# Ensure consistent orderingn_wires=len(wire_order)matrix_size=2**n_wiresmatrix=sparse.csr_matrix((matrix_size,matrix_size),dtype="complex128")op_sparse_idx=_ps_to_sparse_index(pauli_words,wire_order)_,unique_sparse_structures,unique_invs=np.unique(op_sparse_idx,axis=0,return_index=True,return_inverse=True)pw_sparse_structures=unique_sparse_structures[unique_invs]buffer_size=buffer_sizeor2**30# Default to 1GB of memory# Convert bytes to number of matrices:# complex128 (16) for each data entry and int64 (8) for each indices entrybuffer_size=max(1,buffer_size//((16+8)*matrix_size))mat_data=np.empty((matrix_size,buffer_size),dtype=np.complex128)mat_indices=np.empty((matrix_size,buffer_size),dtype=np.int64)n_matrices_in_buffer=0forsparse_structureinunique_sparse_structures:indices,*_=np.nonzero(pw_sparse_structures==sparse_structure)mat=self._sum_same_structure_pws([pauli_words[i]foriinindices],wire_order)mat_data[:,n_matrices_in_buffer]=mat.datamat_indices[:,n_matrices_in_buffer]=mat.indicesn_matrices_in_buffer+=1ifn_matrices_in_buffer==buffer_size:# Add partial results in batches to control the memory usagematrix+=self._sum_different_structure_pws(mat_indices,mat_data)n_matrices_in_buffer=0matrix+=self._sum_different_structure_pws(mat_indices[:,:n_matrices_in_buffer],mat_data[:,:n_matrices_in_buffer])matrix.eliminate_zeros()returnmatrixdef_to_dense_mat(self,wire_order):"""Compute the dense matrix of the Pauli sentence by efficiently adding the Pauli words that it is composed of. See pauli_sparse_matrices.md for the technical details."""pauli_words=list(self)# Ensure consistent orderingtry:op_sparse_idx=_ps_to_sparse_index(pauli_words,wire_order)exceptqml.wires.WireErrorase:raiseValueError("Can't get the matrix for the specified wire order because it "f"does not contain all the Pauli sentence's wires {self.wires}")frome_,unique_sparse_structures,unique_invs=np.unique(op_sparse_idx,axis=0,return_index=True,return_inverse=True)pw_sparse_structures=unique_sparse_structures[unique_invs]full_matrix=Noneforsparse_structureinunique_sparse_structures:indices,*_=np.nonzero(pw_sparse_structures==sparse_structure)mat=self._sum_same_structure_pws_dense([pauli_words[i]foriinindices],wire_order)full_matrix=matiffull_matrixisNoneelseqml.math.add(full_matrix,mat)returnfull_matrix
[docs]defdot(self,vector,wire_order=None):"""Computes the matrix-vector product of the Pauli sentence with a state vector. See pauli_sparse_matrices.md for the technical details."""wire_order=self.wiresifwire_orderisNoneelseWires(wire_order)ifnotwire_order.contains_wires(self.wires):raiseValueError("Can't get the matrix for the specified wire order because it "f"does not contain all the Pauli sentence's wires {self.wires}")pauli_words=list(self)# Ensure consistent orderingop_sparse_idx=_ps_to_sparse_index(pauli_words,wire_order)_,unique_sparse_structures,unique_invs=np.unique(op_sparse_idx,axis=0,return_index=True,return_inverse=True)pw_sparse_structures=unique_sparse_structures[unique_invs]dtype=np.complex64ifvector.dtypein(np.float32,np.complex64)elsenp.complex128ifvector.ndim==1:vector=vector.reshape(1,-1)mv=np.zeros_like(vector,dtype=dtype)forsparse_structureinunique_sparse_structures:indices,*_=np.nonzero(pw_sparse_structures==sparse_structure)entries,data=self._get_same_structure_csr([pauli_words[i]foriinindices],wire_order)mv+=vector[:,entries]*data.reshape(1,-1)returnmv.reshape(vector.shape)
def_get_same_structure_csr(self,pauli_words,wire_order):"""Returns the CSR indices and data for Pauli words with the same sparse structure."""indices=pauli_words[0]._get_csr_indices(wire_order)nwires=len(wire_order)nwords=len(pauli_words)inner=np.empty((nwords,2**(nwires-nwires//2)),dtype=np.complex128)outer=np.empty((nwords,2**(nwires//2)),dtype=np.complex128)fori,wordinenumerate(pauli_words):outer[i,:],inner[i,:]=word._get_csr_data_2(wire_order,coeff=qml.math.to_numpy(self[word]))data=outer.T@innerreturnindices,data.ravel()def_sum_same_structure_pws_dense(self,pauli_words,wire_order):matrix_size=2**(len(wire_order))base_matrix=sparse.csr_matrix((matrix_size,matrix_size),dtype="complex128")data0=pauli_words[0]._get_csr_data(wire_order,1)base_matrix.data=np.ones_like(data0)base_matrix.indices=pauli_words[0]._get_csr_indices(wire_order)base_matrix.indptr=_cached_arange(matrix_size+1)# Non-zero entries by row (starting from 0)base_matrix=base_matrix.toarray()coeff=self[pauli_words[0]]ml_interface=qml.math.get_interface(coeff)ifml_interface=="torch":data0=qml.math.convert_like(data0,coeff)data=coeff*data0ifqml.math.ndim(coeff)==0elseqml.math.outer(coeff,data0)forpwinpauli_words[1:]:coeff=self[pw]csr_data=pw._get_csr_data(wire_order,1)ml_interface=qml.math.get_interface(coeff)ifml_interface=="torch":csr_data=qml.math.convert_like(csr_data,coeff)data+=(coeff*csr_dataifqml.math.ndim(coeff)==0elseqml.math.outer(coeff,csr_data))returnqml.math.einsum("ij,...i->...ij",base_matrix,data)def_sum_same_structure_pws(self,pauli_words,wire_order):"""Sums Pauli words with the same sparse structure."""mat=pauli_words[0].to_mat(wire_order,coeff=qml.math.to_numpy(self[pauli_words[0]]),format="csr")forwordinpauli_words[1:]:mat.data+=word.to_mat(wire_order,coeff=qml.math.to_numpy(self[word]),format="csr").datareturnmat@staticmethoddef_sum_different_structure_pws(indices,data):"""Sums Pauli words with different parse structures."""size=indices.shape[0]idx=np.argsort(indices,axis=1)matrix=sparse.csr_matrix((size,size),dtype="complex128")matrix.indices=np.take_along_axis(indices,idx,axis=1).ravel()matrix.data=np.take_along_axis(data,idx,axis=1).ravel()num_entries_per_row=indices.shape[1]matrix.indptr=_cached_arange(size+1)*num_entries_per_row# remove zeros and things sufficiently close to zeromatrix.data[np.abs(matrix.data)<1e-16]=0# Faster than np.isclose(matrix.data, 0)matrix.eliminate_zeros()returnmatrix
[docs]defoperation(self,wire_order:WiresLike=()):"""Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliSentence."""iflen(self)==0:returnqml.s_prod(0,Identity(wires=wire_order))summands=[]wire_order=wire_orderorself.wiresforpw,coeffinself.items():pw_op=pw.operation(wire_order=list(wire_order))rep=PauliSentence({pw:coeff})summands.append(pw_opifqml.math.all(coeff==1)elseSProd(coeff,pw_op,_pauli_rep=rep))returnsummands[0]iflen(summands)==1elseSum(*summands,_pauli_rep=self)
[docs]defsimplify(self,tol=1e-8):"""Remove any PauliWords in the PauliSentence with coefficients less than the threshold tolerance."""items=list(self.items())forpw,coeffinitems:ifabs(coeff)<=tol:delself[pw]iflen(self)==0:self=PauliSentence({})# pylint: disable=self-cls-assignment
[docs]defmap_wires(self,wire_map:dict)->"PauliSentence":"""Return a new PauliSentence with the wires mapped."""returnself.__class__({pw.map_wires(wire_map):coeffforpw,coeffinself.items()})