# Copyright 2018-2021 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."""This submodule contains the discrete-variable quantum operations thataccept a hermitian or an unitary matrix as a parameter."""# pylint:disable=arguments-differimportwarningsfromitertoolsimportproductfromtypingimportOptional,Unionimportnumpyasnpimportscipyasspfromscipy.linalgimportfractional_matrix_powerfromscipy.sparseimportcsr_matriximportpennylaneasqmlfrompennylaneimportnumpyaspnpfrompennylane.mathimport(cast,conj,eye,norm,sqrt,sqrt_matrix,sqrt_matrix_sparse,transpose,zeros,)frompennylane.operationimportAnyWires,DecompositionUndefinedError,FlatPytree,Operationfrompennylane.typingimportTensorLikefrompennylane.wiresimportWires,WiresLike_walsh_hadamard_matrix=np.array([[1,1],[1,-1]])/2def_walsh_hadamard_transform(D:TensorLike,n:Optional[int]=None):r"""Compute the Walsh–Hadamard Transform of a one-dimensional array. Args: D (tensor_like): The array or tensor to be transformed. Must have a length that is a power of two. Returns: tensor_like: The transformed tensor with the same shape as the input ``D``. Due to the execution of the transform as a sequence of tensor multiplications with shapes ``(2, 2), (2, 2,... 2)->(2, 2,... 2)``, the theoretical scaling of this method is the same as the one for the `Fast Walsh-Hadamard transform <https://en.wikipedia.org/wiki/Fast_Walsh-Hadamard_transform>`__: On ``n`` qubits, there are ``n`` calls to ``tensordot``, each multiplying a ``(2, 2)`` matrix to a ``(2,)*n`` vector, with a single axis being contracted. This means that there are ``n`` operations with a FLOP count of ``4 * 2**(n-1)``, where ``4`` is the cost of a single ``(2, 2) @ (2,)`` contraction and ``2**(n-1)`` is the number of copies due to the non-contracted ``n-1`` axes. Due to the large internal speedups of compiled matrix multiplication and compatibility with autodifferentiation frameworks, the approach taken here is favourable over a manual realization of the FWHT unless memory limitations restrict the creation of intermediate arrays. """orig_shape=qml.math.shape(D)n=norint(qml.math.log2(orig_shape[-1]))# Reshape the array so that we may apply the Hadamard transform to each axis individuallyifbroadcasted:=len(orig_shape)>1:new_shape=(orig_shape[0],)+(2,)*nelse:new_shape=(2,)*nD=qml.math.reshape(D,new_shape)# Apply Hadamard transform to each axis, shifted by one for broadcastingforiinrange(broadcasted,n+broadcasted):D=qml.math.tensordot(_walsh_hadamard_matrix,D,axes=[[1],[i]])# The axes are in reverted order after all matrix multiplications, so we need to transpose;# If D was broadcasted, this moves the broadcasting axis to first position as well.# Finally, reshape to original shapereturnqml.math.reshape(qml.math.transpose(D),orig_shape)
[docs]classQubitUnitary(Operation):r"""QubitUnitary(U, wires) Apply an arbitrary unitary matrix with a dimension that is a power of two. .. warning:: The sparse matrix representation of QubitUnitary is still under development. Currently we only support a limited set of interfaces that preserve the sparsity of the matrix, including :func:`~.adjoint`, :func:`~.pow`, :meth:`~.QubitUnitary.compute_sparse_matrix` and :meth:`~.QubitUnitary.compute_decomposition`. Differentiability is not supported for sparse matrices. **Details:** * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 * Number of dimensions per parameter: (2,) * Gradient recipe: None Args: U (array[complex] or csr_matrix): square unitary matrix wires (Sequence[int] or int): the wire(s) the operation acts on id (str): custom label given to an operator instance, can be useful for some applications where the instance has to be identified unitary_check (bool): check for unitarity of the given matrix Raises: ValueError: if the number of wires doesn't fit the dimensions of the matrix **Example** >>> dev = qml.device('default.qubit', wires=1) >>> U = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) >>> @qml.qnode(dev) ... def example_circuit(): ... qml.QubitUnitary(U, wires=0) ... return qml.expval(qml.Z(0)) >>> print(example_circuit()) 0.0 """num_wires=AnyWires"""int: Number of wires that the operator acts on."""num_params=1"""int: Number of trainable parameters that the operator depends on."""ndim_params=(2,)"""tuple[int]: Number of dimensions per trainable parameter that the operator depends on."""resource_keys={"num_wires"}grad_method=None"""Gradient computation method."""def__init__(self,U:Union[TensorLike,csr_matrix],wires:WiresLike,id:Optional[str]=None,unitary_check:bool=False,):# pylint: disable=too-many-argumentswires=Wires(wires)U_shape=qml.math.shape(U)dim=2**len(wires)# For pure QubitUnitary operations (not controlled), check that the number# of wires fits the dimensions of the matrixiflen(U_shape)notin{2,3}orU_shape[-2:]!=(dim,dim):raiseValueError(f"Input unitary must be of shape {(dim,dim)} or (batch_size, {dim}, {dim}) "f"to act on {len(wires)} wires. Got shape {U_shape} instead.")# If the matrix is sparse, we need to convert it to a csr_matrixself._issparse=sp.sparse.issparse(U)ifself._issparse:U=U.tocsr()# Check for unitarity; due to variable precision across the different ML frameworks,# here we issue a warning to check the operation, instead of raising an error outright.ifunitary_checkandnotself._unitary_check(U,dim):warnings.warn(f"Operator {U}\n may not be unitary. ""Verify unitarity of operation, or use a datatype with increased precision.",UserWarning,)super().__init__(U,wires=wires,id=id)@staticmethoddef_unitary_check(U,dim):ifisinstance(U,csr_matrix):U_dagger=U.conjugate().transpose()identity=sp.sparse.eye(dim,format="csr")returnsp.sparse.linalg.norm(U@U_dagger-identity)<1e-10returnqml.math.allclose(qml.math.einsum("...ij,...kj->...ik",U,qml.math.conj(U)),qml.math.eye(dim),atol=1e-6,)@propertydefresource_params(self)->dict:return{"num_wires":len(self.wires)}
[docs]@staticmethoddefcompute_matrix(U:TensorLike):# pylint: disable=arguments-differr"""Representation of the operator as a canonical matrix in the computational basis (static method). The canonical matrix is the textbook matrix representation that does not consider wires. Implicitly, this assumes that the wires of the operator correspond to the global wire order. .. seealso:: :meth:`~.QubitUnitary.matrix` Args: U (tensor_like): unitary matrix Returns: tensor_like: canonical matrix **Example** >>> U = np.array([[0.98877108+0.j, 0.-0.14943813j], [0.-0.14943813j, 0.98877108+0.j]]) >>> qml.QubitUnitary.compute_matrix(U) [[0.98877108+0.j, 0.-0.14943813j], [0.-0.14943813j, 0.98877108+0.j]] """ifsp.sparse.issparse(U):raiseqml.operation.MatrixUndefinedError("U is sparse matrix. Use sparse_matrix method instead.")returnU
[docs]@staticmethoddefcompute_sparse_matrix(U:TensorLike,format="csr"):# pylint: disable=arguments-differr"""Representation of the operator as a sparse matrix. Args: U (tensor_like): unitary matrix Returns: csr_matrix: sparse matrix representation **Example** >>> U = np.array([[0.98877108+0.j, 0.-0.14943813j], [0.-0.14943813j, 0.98877108+0.j]]) >>> qml.QubitUnitary.compute_sparse_matrix(U) <2x2 sparse matrix of type '<class 'numpy.complex128'>' with 2 stored elements in Compressed Sparse Row format> """ifsp.sparse.issparse(U):returnU.asformat(format)raiseqml.operation.SparseMatrixUndefinedError("U is a dense matrix. Use matrix method instead")
[docs]@staticmethoddefcompute_decomposition(U:TensorLike,wires:WiresLike):r"""Representation of the operator as a product of other operators (static method). .. math:: O = O_1 O_2 \dots O_n. A decomposition is only defined for matrices that act on either one or two wires. For more than two wires, this method raises a ``DecompositionUndefined``. See :func:`~.ops.one_qubit_decomposition` and :func:`~.ops.two_qubit_decomposition` for more information on how the decompositions are computed. .. seealso:: :meth:`~.QubitUnitary.decomposition`. Args: U (array[complex]): square unitary matrix wires (Iterable[Any] or Wires): the wire(s) the operation acts on Returns: list[Operator]: decomposition of the operator **Example:** >>> U = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) >>> qml.QubitUnitary.compute_decomposition(U, 0) [Rot(tensor(3.14159265, requires_grad=True), tensor(1.57079633, requires_grad=True), tensor(0., requires_grad=True), wires=[0])] """# Decomposes arbitrary single-qubit unitaries as Rot gates (RZ - RY - RZ format),# or a single RZ for diagonal matrices.shape=qml.math.shape(U)is_batched=len(shape)==3shape_without_batch_dim=shape[1:]ifis_batchedelseshapeifshape_without_batch_dim==(2,2):returnqml.ops.one_qubit_decomposition(U,Wires(wires)[0],return_global_phase=True)ifshape_without_batch_dim==(4,4):# TODO[dwierichs]: Implement decomposition of broadcasted unitaryifis_batched:raiseDecompositionUndefinedError("The decomposition of a two-qubit QubitUnitary does not support broadcasting.")ifsp.sparse.issparse(U):raiseDecompositionUndefinedError("The decomposition of a two-qubit sparse QubitUnitary is undefined.")returnqml.ops.two_qubit_decomposition(U,Wires(wires))returnsuper(QubitUnitary,QubitUnitary).compute_decomposition(U,wires=wires)
[docs]defadjoint(self)->"QubitUnitary":ifself.has_matrix:U=self.matrix()returnQubitUnitary(qml.math.moveaxis(qml.math.conj(U),-2,-1),wires=self.wires)U=self.sparse_matrix()adjoint_sp_mat=U.conjugate().transpose()# Note: it is necessary to explicitly cast back to csr, or it will become csc.returnQubitUnitary(adjoint_sp_mat,wires=self.wires)
[docs]classDiagonalQubitUnitary(Operation):r"""DiagonalQubitUnitary(D, wires) Apply an arbitrary diagonal unitary matrix with a dimension that is a power of two. **Details:** * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 * Number of dimensions per parameter: (1,) * Gradient recipe: None Args: D (array[complex]): diagonal of unitary matrix wires (Sequence[int] or int): the wire(s) the operation acts on """num_wires=AnyWires"""int: Number of wires that the operator acts on."""num_params=1"""int: Number of trainable parameters that the operator depends on."""ndim_params=(1,)"""tuple[int]: Number of dimensions per trainable parameter that the operator depends on."""grad_method=None"""Gradient computation method."""
[docs]@staticmethoddefcompute_matrix(D:TensorLike)->TensorLike:# pylint: disable=arguments-differr"""Representation of the operator as a canonical matrix in the computational basis (static method). The canonical matrix is the textbook matrix representation that does not consider wires. Implicitly, this assumes that the wires of the operator correspond to the global wire order. .. seealso:: :meth:`~.DiagonalQubitUnitary.matrix` Args: D (tensor_like): diagonal of the matrix Returns: tensor_like: canonical matrix **Example** >>> qml.DiagonalQubitUnitary.compute_matrix(torch.tensor([1, -1])) tensor([[ 1, 0], [ 0, -1]]) """D=qml.math.asarray(D)ifnotqml.math.is_abstract(D)andnotqml.math.allclose(D*qml.math.conj(D),qml.math.ones_like(D)):raiseValueError("Operator must be unitary.")# The diagonal is supposed to have one-dimension. If it is broadcasted, it has twoifqml.math.ndim(D)==2:returnqml.math.stack([qml.math.diag(_D)for_DinD])returnqml.math.diag(D)
[docs]@staticmethoddefcompute_eigvals(D:TensorLike)->TensorLike:# pylint: disable=arguments-differr"""Eigenvalues of the operator in the computational basis (static method). If :attr:`diagonalizing_gates` are specified and implement a unitary :math:`U^{\dagger}`, the operator can be reconstructed as .. math:: O = U \Sigma U^{\dagger}, where :math:`\Sigma` is the diagonal matrix containing the eigenvalues. Otherwise, no particular order for the eigenvalues is guaranteed. .. seealso:: :meth:`~.DiagonalQubitUnitary.eigvals` Args: D (tensor_like): diagonal of the matrix Returns: tensor_like: eigenvalues **Example** >>> qml.DiagonalQubitUnitary.compute_eigvals(torch.tensor([1, -1])) tensor([ 1, -1]) """D=qml.math.asarray(D)ifnot(qml.math.is_abstract(D)orqml.math.allclose(D*qml.math.conj(D),qml.math.ones_like(D))):raiseValueError("Operator must be unitary.")returnD
[docs]@staticmethoddefcompute_decomposition(D:TensorLike,wires:WiresLike)->list["qml.operation.Operator"]:r"""Representation of the operator as a product of other operators (static method). .. math:: O = O_1 O_2 \dots O_n. ``DiagonalQubitUnitary`` decomposes into :class:`~.QubitUnitary`, :class:`~.RZ`, :class:`~.IsingZZ`, and/or :class:`~.MultiRZ` depending on the number of wires. .. note:: The parameters of the decomposed operations are cast to the ``complex128`` dtype as real dtypes can lead to ``NaN`` values in the decomposition. .. seealso:: :meth:`~.DiagonalQubitUnitary.decomposition`. Args: D (tensor_like): diagonal of the matrix wires (Iterable[Any] or Wires): the wire(s) the operation acts on Returns: list[Operator]: decomposition into lower level operations **Example:** >>> diag = np.exp(1j * np.array([0.4, 2.1, 0.5, 1.8])) >>> qml.DiagonalQubitUnitary.compute_decomposition(diag, wires=[0, 1]) [QubitUnitary(array([[0.36235775+0.93203909j, 0. +0.j ], [0. +0.j , 0.36235775+0.93203909j]]), wires=[0]), RZ(1.5000000000000002, wires=[1]), RZ(-0.10000000000000003, wires=[0]), IsingZZ(0.2, wires=[0, 1])] """n=len(wires)# Cast the diagonal into a complex dtype so that the logarithm works as expectedD_casted=qml.math.cast(D,"complex128")phases=qml.math.real(qml.math.log(D_casted)*(-1j))coeffs=_walsh_hadamard_transform(phases,n).Tglobal_phase=qml.math.exp(1j*coeffs[0])# For all other gates, there is a prefactor -1/2 to be compensated.coeffs=coeffs*(-2.0)# TODO: Replace the following by a GlobalPhase gate.ops=[QubitUnitary(qml.math.tensordot(global_phase,qml.math.eye(2),axes=0),wires[0])]forwire0inrange(n):# Single PauliZ generators correspond to the coeffs at powers of twoops.append(qml.RZ(coeffs[1<<wire0],wires[n-1-wire0]))# Double PauliZ generators correspond to the coeffs at the sum of two powers of twoops.extend(qml.IsingZZ(coeffs[(1<<wire0)+(1<<wire1)],[wires[n-1-wire0],wires[n-1-wire1]],)forwire1inrange(wire0))# Add all multi RZ gates that are not generated by single or double PauliZ generatorsops.extend(qml.MultiRZ(c,[wires[k]forkinnp.where(term)[0]])forc,terminzip(coeffs,product((0,1),repeat=n))ifsum(term)>2)returnops
[docs]classBlockEncode(Operation):r"""BlockEncode(A, wires) Construct a unitary :math:`U(A)` such that an arbitrary matrix :math:`A` is encoded in the top-left block. .. math:: \begin{align} U(A) &= \begin{bmatrix} A & \sqrt{I-AA^\dagger} \\ \sqrt{I-A^\dagger A} & -A^\dagger \end{bmatrix}. \end{align} **Details:** * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 * Number of dimensions per parameter: (2,) * Gradient recipe: None Args: A (tensor_like): a general :math:`(n \times m)` matrix to be encoded wires (Iterable[int, str], Wires): the wires the operation acts on id (str or None): String representing the operation (optional) Raises: ValueError: if the number of wires doesn't fit the dimensions of the matrix **Example** We can define a matrix and a block-encoding circuit as follows: >>> A = [[0.1,0.2],[0.3,0.4]] >>> dev = qml.device('default.qubit', wires=2) >>> @qml.qnode(dev) ... def example_circuit(): ... qml.BlockEncode(A, wires=range(2)) ... return qml.state() We can see that :math:`A` has been block encoded in the matrix of the circuit: >>> print(qml.matrix(example_circuit)()) [[ 0.1 0.2 0.97283788 -0.05988708] [ 0.3 0.4 -0.05988708 0.86395228] [ 0.94561648 -0.07621992 -0.1 -0.3 ] [-0.07621992 0.89117368 -0.2 -0.4 ]] We can also block-encode a non-square matrix and check the resulting unitary matrix: >>> A = [[0.2, 0, 0.2],[-0.2, 0.2, 0]] >>> op = qml.BlockEncode(A, wires=range(3)) >>> print(np.round(qml.matrix(op), 2)) [[ 0.2 0. 0.2 0.96 0.02 0. 0. 0. ] [-0.2 0.2 0. 0.02 0.96 0. 0. 0. ] [ 0.96 0.02 -0.02 -0.2 0.2 0. 0. 0. ] [ 0.02 0.98 0. -0. -0.2 0. 0. 0. ] [-0.02 0. 0.98 -0.2 -0. 0. 0. 0. ] [ 0. 0. 0. 0. 0. 1. 0. 0. ] [ 0. 0. 0. 0. 0. 0. 1. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 1. ]] .. note:: If the operator norm of :math:`A` is greater than 1, we normalize it to ensure :math:`U(A)` is unitary. The normalization constant can be accessed through :code:`op.hyperparameters["norm"]`. Specifically, the norm is computed as the maximum of :math:`\| AA^\dagger \|` and :math:`\| A^\dagger A \|`. """num_params=1"""int: Number of trainable parameters that the operator depends on."""num_wires=AnyWires"""int: Number of wires that the operator acts on."""ndim_params=(2,)"""tuple[int]: Number of dimensions per trainable parameter that the operator depends on."""grad_method=None"""Gradient computation method."""def__init__(self,A:TensorLike,wires:WiresLike,id:Optional[str]=None):wires=Wires(wires)shape_a=qml.math.shape(A)ifshape_a==()orall(x==1forxinshape_a):A=qml.math.reshape(A,[1,1])normalization=qml.math.abs(A)subspace=(1,1,2**len(wires))else:iflen(shape_a)==1:A=qml.math.reshape(A,[1,len(A)])shape_a=qml.math.shape(A)normalization=qml.math.maximum(norm(A@qml.math.transpose(qml.math.conj(A)),ord=pnp.inf),norm(qml.math.transpose(qml.math.conj(A))@A,ord=pnp.inf),)subspace=(*shape_a,2**len(wires))# Clip the normalization to at least 1 (= normalize(A) if norm > 1 else A).A=qml.math.array(A)/qml.math.maximum(normalization,qml.math.ones_like(normalization))ifsubspace[2]<(subspace[0]+subspace[1]):raiseValueError(f"Block encoding a ({subspace[0]} x {subspace[1]}) matrix "f"requires a Hilbert space of size at least "f"({subspace[0]+subspace[1]} x {subspace[0]+subspace[1]})."f" Cannot be embedded in a {len(wires)} qubit system.")super().__init__(A,wires=wires,id=id)self.hyperparameters["norm"]=normalizationself.hyperparameters["subspace"]=subspacedef_flatten(self)->FlatPytree:returnself.data,(self.wires,())
[docs]@staticmethoddefcompute_matrix(*params,**hyperparams):r"""Representation of the operator as a canonical matrix in the computational basis (static method). The canonical matrix is the textbook matrix representation that does not consider wires. Implicitly, this assumes that the wires of the operator correspond to the global wire order. .. seealso:: :meth:`~.BlockEncode.matrix` Args: *params (list): trainable parameters of the operator, as stored in the ``parameters`` attribute **hyperparams (dict): non-trainable hyperparameters of the operator, as stored in the ``hyperparameters`` attribute Returns: tensor_like: canonical matrix **Example** >>> A = np.array([[0.1,0.2],[0.3,0.4]]) >>> A tensor([[0.1, 0.2], [0.3, 0.4]]) >>> qml.BlockEncode.compute_matrix(A, subspace=[2,2,4]) array([[ 0.1 , 0.2 , 0.97283788, -0.05988708], [ 0.3 , 0.4 , -0.05988708, 0.86395228], [ 0.94561648, -0.07621992, -0.1 , -0.3 ], [-0.07621992, 0.89117368, -0.2 , -0.4 ]]) """A=params[0]n,m,k=hyperparams["subspace"]shape_a=qml.math.shape(A)sqrtm=sqrt_matrix_sparseifsp.sparse.issparse(A)elsesqrt_matrixdef_stack(lst,h=False,like=None):iflike=="tensorflow":axis=1ifhelse0returnqml.math.concat(lst,like=like,axis=axis)returnqml.math.hstack(lst)ifhelseqml.math.vstack(lst)interface=qml.math.get_interface(A)ifqml.math.sum(shape_a)<=2:col1=_stack([A,sqrt(1-A*conj(A))],like=interface)col2=_stack([sqrt(1-A*conj(A)),-conj(A)],like=interface)u=_stack([col1,col2],h=True,like=interface)else:d1,d2=shape_acol1=_stack([A,sqrtm(cast(eye(d2,like=A),A.dtype)-qml.math.transpose(conj(A))@A)],like=interface,)col2=_stack([sqrtm(cast(eye(d1,like=A),A.dtype)-A@transpose(conj(A))),-transpose(conj(A)),],like=interface,)u=_stack([col1,col2],h=True,like=interface)ifn+m<k:r=k-(n+m)col1=_stack([u,zeros((r,n+m),like=A)],like=interface)col2=_stack([zeros((n+m,r),like=A),eye(r,like=A)],like=interface)u=_stack([col1,col2],h=True,like=interface)returnu