# Copyright 2024 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."""Cartan involutions"""fromfunctoolsimportsingledispatch# pylint: disable=missing-function-docstringfromtypingimportOptional,UnionimportnumpyasnpimportpennylaneasqmlfrompennylaneimportY,Z,matrixfrompennylane.operationimportOperatorfrompennylane.pauliimportPauliSentence# Canonical involutions# see https://arxiv.org/abs/2406.04418 appendix Cdefint_log2(x):"""Compute the integer closest to log_2(x)."""returnint(np.round(np.log2(x)))defis_qubit_case(p,q):"""Return whether p and q are the same and a power of 2."""returnp==qand2**int_log2(p)==pdefJ(n,wire=None):"""This is the standard choice for the symplectic transformation operator. For an :math:`N`-qubit system (:math:`n=2^N`), it equals :math:`Y_0`."""N=int_log2(n)if2**N==n:ifwireisNone:wire=0returnY(wire).matrix(wire_order=range(N+1))ifwireisnotNone:raiseValueError("The wire argument is only supported for n=2**N for some integer N.")zeros=np.zeros((n,n))eye=np.eye(n)returnnp.block([[zeros,-1j*eye],[1j*eye,zeros]])defIpq(p,q,wire=None):"""This is the canonical transformation operator for AIII and BDI Cartan decompositions. For an :math:`N`-qubit system (:math:`n=2^N`) and :math:`p=q=n/2`, it equals :math:`Z_0`."""# If p = q and they are a power of two, use Pauli representationifis_qubit_case(p,q):ifwireisNone:wire=0returnZ(wire).matrix(wire_order=range(int_log2(p)+1))ifwireisnotNone:raiseValueError("The wire argument is only supported for p=q=2**N for some integer N.")returnnp.diag(np.concatenate([np.ones(p),-np.ones(q)]))defKpq(p,q,wire=None):"""This is the canonical transformation operator for CII Cartan decompositions. For an :math:`N`-qubit system (:math:`n=2^N`) and :math:`p=q=n/2`, it equals :math:`Z_1`."""# If p = q and they are a power of two, use Pauli representationifis_qubit_case(p,q):ifwireisNone:wire=1returnZ(wire).matrix(wire_order=range(int_log2(p)+1))ifwireisnotNone:raiseValueError("The wire argument is only supported for p=q=2**N for some integer N.")returnnp.diag(np.concatenate([np.ones(p),-np.ones(q),np.ones(p),-np.ones(q)]))
[docs]defA(op:Union[np.ndarray,PauliSentence,Operator],wire:Optional[int]=None)->bool:r"""Canonical Cartan decomposition of type A on :math:`\mathfrak{su}(n)\oplus \mathfrak{su}(n)`, given by :math:`\theta: x\oplus y \mapsto y\oplus x`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. wire (int): The wire on which the Pauli-:math:`Y` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """# Note that even though DIII implements Ad_{Y_0} and we want a simple swap, so Ad_{X_0},# they only differ by applying Ad_{Z_0}, which in turn does not have an effect on the algebrareturnDIII(op,wire)
[docs]defAI(op:Union[np.ndarray,PauliSentence,Operator])->bool:r"""Canonical Cartan decomposition of type AI, given by :math:`\theta: x \mapsto x^\ast`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """return_AI(op)
@singledispatchdef_AI(op):# pylint:disable=unused-argumentr"""Default implementation of the canonical form of the AI involution :math:`\theta: x \mapsto x^\ast`. """raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_AI.register(np.ndarray)def_AI_matrix(op:np.ndarray)->bool:r"""Matrix implementation of the canonical form of the AI involution :math:`\theta: x \mapsto x^\ast`. """op=op*1jreturnnp.allclose(op,op.conj())@_AI.register(PauliSentence)def_AI_ps(op:PauliSentence)->bool:r"""PauliSentence implementation of the canonical form of the AI involution :math:`\theta: x \mapsto x^\ast`. """parity=[]forpwinop:result=sum(el=="Y"forelinpw.values())parity.append(bool(result%2))# only makes sense if parity is the same for all termsassertall(parity)ornotany(parity)returnparity[0]@_AI.register(Operator)def_AI_op(op:Operator)->bool:r"""Operator implementation of the canonical form of the AI involution :math:`\theta: x \mapsto x^\ast`. """return_AI_ps(op.pauli_rep)
[docs]defAII(op:Union[np.ndarray,PauliSentence,Operator],wire:Optional[int]=None)->bool:r"""Canonical Cartan decomposition of type AII, given by :math:`\theta: x \mapsto Y_0 x^\ast Y_0`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. wire (int): The wire on which the Pauli-:math:`Y` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """return_AII(op,wire)
@singledispatchdef_AII(op,wire=None):# pylint:disable=unused-argumentr"""Default implementation of the canonical form of the AII involution :math:`\theta: x \mapsto Y_0 x^\ast Y_0`. """raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_AII.register(np.ndarray)def_AII_matrix(op:np.ndarray,wire:Optional[int]=None)->bool:r"""Matrix implementation of the canonical form of the AII involution :math:`\theta: x \mapsto Y_0 x^\ast Y_0`. """op=op*1jy=J(op.shape[-1]//2,wire=wire)returnnp.allclose(op,y@op.conj()@y)@_AII.register(PauliSentence)def_AII_ps(op:PauliSentence,wire:Optional[int]=None)->bool:r"""PauliSentence implementation of the canonical form of the AII involution :math:`\theta: x \mapsto Y_0 x^\ast Y_0`. """ifwireisNone:wire=0parity=[]forpwinop:result=sum(el=="Y"forelinpw.values())+(pw.get(wire,"I")in"XZ")parity.append(bool(result%2))# only makes sense if parity is the same for all termsassertall(parity)ornotany(parity)returnparity[0]@_AII.register(Operator)def_AII_op(op:Operator,wire:Optional[int]=None)->bool:r"""Operator implementation of the canonical form of the AII involution :math:`\theta: x \mapsto Y_0 x^\ast Y_0`. """return_AII_ps(op.pauli_rep,wire)
[docs]defAIII(op:Union[np.ndarray,PauliSentence,Operator],p:int=None,q:int=None,wire:Optional[int]=None,)->bool:r"""Canonical Cartan decomposition of type AIII, given by :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. The matrix :math:`I_{p,q}` is given by .. math:: I_{p,q}=\text{diag}(\underset{p \text{times}}{\underbrace{1, \dots 1}}, \underset{q \text{times}}{\underbrace{-1, \dots -1}}). For :math:`p=q=2^N` for some integer :math:`N`, we have :math:`I_{p,q}=Z_0`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. p (int): Dimension of first subspace. q (int): Dimension of second subspace. wire (int): The wire on which the Pauli-:math:`Z` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """ifpisNoneorqisNone:raiseValueError("please specify p and q for the involution via functools.partial(AIII, p=p, q=q)")return_AIII(op,p,q,wire)
@singledispatchdef_AIII(op,p=None,q=None,wire=None):# pylint:disable=unused-argumentr"""Default implementation of the canonical form of the AIII involution :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. """raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_AIII.register(np.ndarray)def_AIII_matrix(op:np.ndarray,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""Matrix implementation of the canonical form of the AIII involution :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. """op=op*1jz=Ipq(p,q,wire=wire)returnnp.allclose(op,z@op@z)@_AIII.register(PauliSentence)def_AIII_ps(op:PauliSentence,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""PauliSentence implementation of the canonical form of the AIII involution :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. """ifis_qubit_case(p,q):ifwireisNone:wire=0parity=[pw.get(wire,"I")in"IZ"forpwinop]# only makes sense if parity is the same for all termsassertall(parity)ornotany(parity)returnparity[0]# If it is not a qubit case, use the matrix representationreturn_AIII_matrix(matrix(op,wire_order=sorted(op.wires)),p,q,wire)@_AIII.register(Operator)def_AIII_op(op:Operator,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""Operator implementation of the canonical form of the AIII involution :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. """return_AIII_ps(op.pauli_rep,p,q,wire)
[docs]defBD(op:Union[np.ndarray,PauliSentence,Operator],wire:Optional[int]=None)->bool:r"""Canonical Cartan decomposition of type BD on :math:`\mathfrak{so}(n)\oplus \mathfrak{so}(n)`, given by :math:`\theta: x\oplus y \mapsto y\oplus x`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. wire (int): The wire on which the operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """# Note that even though DIII implements Ad_{Y_0} and we want a simple swap, so Ad_{X_0},# they only differ by applying Ad_{Z_0}, which in turn does not have an effect on the algebrareturnDIII(op,wire)
[docs]defBDI(op:Union[np.ndarray,PauliSentence,Operator],p:int=None,q:int=None,wire:Optional[int]=None,)->bool:r"""Canonical Cartan decomposition of type BDI, given by :math:`\theta: x \mapsto I_{p,q} x I_{p,q}`. The matrix :math:`I_{p,q}` is given by .. math:: I_{p,q}=\text{diag}(\underset{p \text{times}}{\underbrace{1, \dots 1}}, \underset{q \text{times}}{\underbrace{-1, \dots -1}}). For :math:`p=q=2^N` for some integer :math:`N`, we have :math:`I_{p,q}=Z_0`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. p (int): Dimension of first subspace. q (int): Dimension of second subspace. wire (int): The wire on which the Pauli-:math:`Z` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """returnAIII(op,p,q,wire)
[docs]defDIII(op:Union[np.ndarray,PauliSentence,Operator],wire:Optional[int]=None)->bool:r"""Canonical Cartan decomposition of type DIII, given by :math:`\theta: x \mapsto Y_0 x Y_0`. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. wire (int): The wire on which the Pauli-:math:`Y` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """return_DIII(op,wire)
@singledispatchdef_DIII(op,wire=None):# pylint:disable=unused-argumentr"""Default implementation of the canonical form of the DIII involution :math:`\theta: x \mapsto Y_0 x Y_0`. """raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_DIII.register(np.ndarray)def_DIII_matrix(op:np.ndarray,wire:Optional[int]=None)->bool:r"""Matrix implementation of the canonical form of the DIII involution :math:`\theta: x \mapsto Y_0 x Y_0`. """y=J(op.shape[-1]//2,wire=wire)returnnp.allclose(op,y@op@y)@_DIII.register(PauliSentence)def_DIII_ps(op:PauliSentence,wire:Optional[int]=None)->bool:r"""PauliSentence implementation of the canonical form of the DIII involution :math:`\theta: x \mapsto Y_0 x Y_0`. """ifwireisNone:wire=0parity=[pw.get(wire,"I")in"IY"forpwinop]# only makes sense if parity is the same for all termsassertall(parity)ornotany(parity)returnparity[0]@_DIII.register(Operator)def_DIII_op(op:Operator,wire:Optional[int]=None)->bool:r"""Operator implementation of the canonical form of the DIII involution :math:`\theta: x \mapsto Y_0 x Y_0`. """return_DIII_ps(op.pauli_rep,wire)
[docs]defC(op:Union[np.ndarray,PauliSentence,Operator],wire:Optional[int]=None)->bool:r"""Canonical Cartan decomposition of type C on :math:`\mathfrak{sp}(n)\oplus \mathfrak{sp}(n)`, given by :math:`\theta: x\oplus y \mapsto y\oplus x`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. wire (int): The wire on which the Pauli-:math:`Y` operator acts to implement the involution. Will default to ``0`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """# Note that even though DIII implements Ad_{Y_0} and we want a simple swap, so Ad_{X_0},# they only differ by applying Ad_{Z_0}, which in turn does not have an effect on the algebrareturnDIII(op,wire)
[docs]defCI(op:Union[np.ndarray,PauliSentence,Operator])->bool:r"""Canonical Cartan decomposition of type CI, given by :math:`\theta: x \mapsto x^\ast`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """returnAI(op)
[docs]defCII(op:Union[np.ndarray,PauliSentence,Operator],p:int=None,q:int=None,wire:Optional[int]=None,)->bool:r"""Canonical Cartan decomposition of type CII, given by :math:`\theta: x \mapsto K_{p,q} x K_{p,q}`. The matrix :math:`K_{p,q}` is given by .. math:: K_{p,q}=\text{diag}( \underset{p \text{times}}{\underbrace{1, \dots 1}}, \underset{q \text{times}}{\underbrace{-1, \dots -1}}, \underset{p \text{times}}{\underbrace{1, \dots 1}}, \underset{q \text{times}}{\underbrace{-1, \dots -1}}, ). For :math:`p=q=2^N` for some integer :math:`N`, we have :math:`K_{p,q}=Z_1`. .. note:: Note that we work with Hermitian operators internally, so that the input will be multiplied by :math:`i` before evaluating the involution. Args: op (Union[np.ndarray, PauliSentence, Operator]): Operator on which the involution is evaluated and for which the parity under the involution is returned. p (int): Dimension of first subspace. q (int): Dimension of second subspace. wire (int): The wire on which the Pauli-:math:`Z` operator acts to implement the involution. Will default to ``1`` if ``None``. Returns: bool: Whether or not the input operator (times :math:`i`) is in the eigenspace of the involution :math:`\theta` with eigenvalue :math:`+1`. """ifpisNoneorqisNone:raiseValueError("please specify p and q for the involution via functools.partial(CII, p=p, q=q)")return_CII(op,p,q,wire)
@singledispatchdef_CII(op,p=None,q=None,wire=None):# pylint:disable=unused-argumentr"""Default implementation of the canonical form of the CII involution :math:`\theta: x \mapsto K_{p,q} x K_{p,q}`. """raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_CII.register(np.ndarray)def_CII_matrix(op:np.ndarray,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""Matrix implementation of the canonical form of the CII involution :math:`\theta: x \mapsto K_{p,q} x K_{p,q}`. """op=op*1jz=Kpq(p,q,wire=wire)returnnp.allclose(op,z@op@z)@_CII.register(PauliSentence)def_CII_ps(op:PauliSentence,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""PauliSentence implementation of the canonical form of the CII involution :math:`\theta: x \mapsto K_{p,q} x K_{p,q}`. """ifis_qubit_case(p,q):ifwireisNone:wire=1parity=[pw.get(wire,"I")in"IZ"forpwinop]# only makes sense if parity is the same for all termsassertall(parity)ornotany(parity)returnparity[0]# If it is not a qubit case, use the matrix representationreturn_CII(matrix(op,wire_order=sorted(op.wires)),p,q,wire)@_CII.register(Operator)def_CII_op(op:Operator,p:int=None,q:int=None,wire:Optional[int]=None)->bool:r"""Operator implementation of the canonical form of the CII involution :math:`\theta: x \mapsto K_{p,q} x K_{p,q}`. """return_CII_ps(op.pauli_rep,p,q,wire)
[docs]defeven_odd_involution(op:Union[PauliSentence,np.ndarray,Operator])->bool:r"""The Even-Odd involution. This is defined in `quant-ph/0701193 <https://arxiv.org/abs/quant-ph/0701193>`__. For Pauli words and sentences, it comes down to counting non-trivial Paulis in Pauli words. For an even (odd) number of qubits, it is of type AI (AII). Args: op ( Union[PauliSentence, np.ndarray, Operator]): Input operator Returns: bool: Boolean output ``True`` or ``False`` for odd (:math:`\mathfrak{k}`) and even parity subspace (:math:`\mathfrak{m}`), respectively .. seealso:: :func:`~cartan_decomp` **Example** >>> from pennylane import X, Y, Z >>> from pennylane.liealg import even_odd_involution >>> ops = [X(0), X(0) @ Y(1), X(0) @ Y(1) @ Z(2)] >>> [even_odd_involution(op) for op in ops] [True, False, True] Operators with an odd number of non-identity Paulis yield ``1``, whereas even ones yield ``0``. The function also works with dense matrix representations. >>> ops_m = [qml.matrix(op, wire_order=range(3)) for op in ops] >>> [even_odd_involution(op_m) for op_m in ops_m] [True, False, True] """return_even_odd_involution(op)
@singledispatchdef_even_odd_involution(op):# pylint:disable=unused-argument, missing-function-docstringraiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_even_odd_involution.register(PauliSentence)def_even_odd_involution_ps(op:PauliSentence):# Generalization to sums of Paulis: check each term and assert they all have the same parityparity=[]forpwinop.keys():parity.append(len(pw)%2)# only makes sense if parity is the same for all terms, e.g. Heisenberg modelassertall(parity[0]==pforpinparity),f"The Even-Odd involution is not well-defined for operator {op} as individual terms have different parity"returnbool(parity[0])@_even_odd_involution.register(np.ndarray)def_even_odd_involution_matrix(op:np.ndarray):"""see Table CI in https://arxiv.org/abs/2406.04418"""n=int(np.round(np.log2(op.shape[-1])))YYY=qml.prod(*[Y(i)foriinrange(n)])YYY=qml.matrix(YYY,range(n))transformed=YYY@op.conj()@YYYifnp.allclose(transformed,op):returnFalseifnp.allclose(transformed,-op):returnTrueraiseValueError(f"The Even-Odd involution is not well-defined for operator {op}.")@_even_odd_involution.register(Operator)def_even_odd_involution_op(op:Operator):"""use pauli representation"""return_even_odd_involution_ps(op.pauli_rep)# dispatch to different input types
[docs]defconcurrence_involution(op:Union[PauliSentence,np.ndarray,Operator])->bool:r"""The Concurrence Canonical Decomposition :math:`\Theta(g) = -g^T` as a Cartan involution function. It is of type AI. This is defined in `quant-ph/0701193 <https://arxiv.org/abs/quant-ph/0701193>`__. For Pauli words and sentences, it comes down to counting Pauli-Y operators. Args: op ( Union[PauliSentence, np.ndarray, Operator]): Input operator Returns: bool: Boolean output ``True`` or ``False`` for odd (:math:`\mathfrak{k}`) and even parity subspace (:math:`\mathfrak{m}`), respectively .. seealso:: :func:`~cartan_decomp` **Example** >>> from pennylane import X, Y, Z >>> from pennylane.liealg import concurrence_involution >>> ops = [X(0), X(0) @ Y(1), X(0) @ Y(1) @ Z(2), Y(0) @ Y(2)] >>> [concurrence_involution(op) for op in ops] [False, True, True, False] Operators with an odd number of ``Y`` operators yield ``1``, whereas even ones yield ``0``. The function also works with dense matrix representations. >>> ops_m = [qml.matrix(op, wire_order=range(3)) for op in ops] >>> [even_odd_involution(op_m) for op_m in ops_m] [False, True, True, False] """return_concurrence_involution(op)
@singledispatchdef_concurrence_involution(op):raiseNotImplementedError(f"Involution not implemented for operator {op} of type {type(op)}")@_concurrence_involution.register(PauliSentence)def_concurrence_involution_pauli(op:PauliSentence):# Generalization to sums of Paulis: check each term and assert they all have the same parityparity=[]forpwinop.keys():result=sum(1ifel=="Y"else0forelinpw.values())parity.append(result%2)# only makes sense if parity is the same for all terms, e.g. Heisenberg modelassertall(parity[0]==pforpinparity),f"The concurrence canonical decomposition is not well-defined for operator {op} as individual terms have different parity"returnbool(parity[0])@_concurrence_involution.register(Operator)def_concurrence_involution_operator(op:Operator):return_concurrence_involution_matrix(op.matrix())@_concurrence_involution.register(np.ndarray)def_concurrence_involution_matrix(op:np.ndarray):ifnp.allclose(op,-op.T):returnTrueifnp.allclose(op,op.T):returnFalseraiseValueError(f"The concurrence canonical decomposition is not well-defined for operator {op}")