# 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 module contains the qml.matrix function."""fromfunctoolsimportpartial# pylint: disable=protected-access,too-many-branchesfromtypingimportUnionimportpennylaneasqmlfrompennylaneimporttransformfrompennylane.operationimportOperatorfrompennylane.pauliimportPauliSentence,PauliWordfrompennylane.tapeimportQuantumScript,QuantumScriptBatchfrompennylane.transformsimportTransformErrorfrompennylane.typingimportPostprocessingFn,TensorLikedefcatalyst_qjit(qnode):"""A method checking whether a qnode is compiled by catalyst.qjit"""returnqnode.__class__.__name__=="QJIT"andhasattr(qnode,"user_function")
[docs]defmatrix(op:Union[Operator,PauliWord,PauliSentence],wire_order=None)->TensorLike:r"""The matrix representation of an operation or quantum circuit. Args: op (Operator or QNode or QuantumTape or Callable or PauliWord or PauliSentence): A quantum operator or quantum circuit. wire_order (Sequence[Any], optional): Order of the wires in the quantum circuit. The default wire order depends on the type of ``op``: - If ``op`` is a :class:`~.QNode`, then the wire order is determined by the associated device's wires, if provided. - Otherwise, the wire order is determined by the order in which wires appear in the circuit. - See the usage details for more information. Returns: TensorLike or qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: If an operator, :class:`~PauliWord` or :class:`~PauliSentence` is provided as input, the matrix is returned directly in the form of a tensor. Otherwise, the transformed circuit is returned as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide its matrix representation. **Example** Given an instantiated operator, ``qml.matrix`` returns the matrix representation: >>> op = qml.RX(0.54, wires=0) >>> qml.matrix(op) [[0.9637709+0.j 0. -0.26673144j] [0. -0.26673144j 0.9637709+0.j ]] It can also be used in a functional form: >>> x = torch.tensor(0.6, requires_grad=True) >>> matrix_fn = qml.matrix(qml.RX) >>> matrix_fn(x, wires=0) tensor([[0.9553+0.0000j, 0.0000-0.2955j], [0.0000-0.2955j, 0.9553+0.0000j]], grad_fn=<AddBackward0>) In its functional form, it is fully differentiable with respect to gate arguments: >>> loss = torch.real(torch.trace(matrix_fn(x, wires=0))) >>> loss.backward() >>> x.grad tensor(-0.5910) This operator transform can also be applied to QNodes, tapes, and quantum functions that contain multiple operations; see Usage Details below for more details. .. details:: :title: Usage Details ``qml.matrix`` can also be used with :class:`~PauliWord` and :class:`~PauliSentence` instances. Internally, we are using their ``to_mat()`` methods. >>> X0 = PauliWord({0:"X"}) >>> np.allclose(qml.matrix(X0), X0.to_mat()) True ``qml.matrix`` can also be used with QNodes, tapes, or quantum functions that contain multiple operations. Consider the following quantum function: .. code-block:: python3 def circuit(theta): qml.RX(theta, wires=1) qml.Z(0) We can use ``qml.matrix`` to generate a new function that returns the unitary matrix corresponding to the function ``circuit``: >>> matrix_fn = qml.matrix(circuit) >>> theta = np.pi / 4 >>> matrix_fn(theta) array([[ 0.92387953+0.j, 0.+0.j , 0.-0.38268343j, 0.+0.j], [ 0.+0.j, -0.92387953+0.j, 0.+0.j, 0. +0.38268343j], [ 0. -0.38268343j, 0.+0.j, 0.92387953+0.j, 0.+0.j], [ 0.+0.j, 0.+0.38268343j, 0.+0.j, -0.92387953+0.j]]) Note that since ``wire_order`` was not specified, the default order ``[1, 0]`` for ``circuit`` was used, and the unitary matrix corresponds to the operation :math:`R_X(\theta)\otimes Z`. To obtain the matrix for :math:`Z\otimes R_X(\theta)`, specify ``wire_order=[0, 1]`` in the function call: >>> matrix = qml.matrix(circuit, wire_order=[0, 1]) You can also get the unitary matrix for operations on a subspace of a larger Hilbert space. For example, with the same function ``circuit`` and ``wire_order=["a", 0, "b", 1]`` you obtain the :math:`16\times 16` matrix for the operation :math:`I\otimes Z\otimes I\otimes R_X(\theta)`. This unitary matrix can also be used in differentiable calculations. For example, consider the following cost function: .. code-block:: python def circuit(theta): qml.RX(theta, wires=1) qml.Z(0) qml.CNOT(wires=[0, 1]) def cost(theta): matrix = qml.matrix(circuit)(theta) return np.real(np.trace(matrix)) Since this cost function returns a real scalar as a function of ``theta``, we can differentiate it: >>> theta = np.array(0.3, requires_grad=True) >>> cost(theta) 1.9775421558720845 >>> qml.grad(cost)(theta) -0.14943813247359922 .. note:: When using ``qml.matrix`` with a ``QNode``, unless specified, the device wire order will be used. If the device wires are not set, the wire order will be inferred from the quantum function used to create the ``QNode``. Consider the following example: .. code-block:: python def circuit(): qml.Hadamard(wires=1) qml.CZ(wires=[0, 1]) qml.Hadamard(wires=1) return qml.state() dev_with_wires = qml.device("default.qubit", wires=[0, 1]) dev_without_wires = qml.device("default.qubit") qnode_with_wires = qml.QNode(circuit, dev_with_wires) qnode_without_wires = qml.QNode(circuit, dev_without_wires) >>> qml.matrix(qnode_with_wires)().round(2) array([[ 1.+0.j, -0.+0.j, 0.+0.j, 0.+0.j], [-0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [ 0.+0.j, 0.+0.j, -0.+0.j, 1.+0.j], [ 0.+0.j, 0.+0.j, 1.+0.j, -0.+0.j]]) >>> qml.matrix(qnode_without_wires)().round(2) array([[ 1.+0.j, 0.+0.j, -0.+0.j, 0.+0.j], [ 0.+0.j, -0.+0.j, 0.+0.j, 1.+0.j], [-0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], [ 0.+0.j, 1.+0.j, 0.+0.j, -0.+0.j]]) The second matrix above uses wire order ``[1, 0]`` because the device does not have wires specified, and this is the order in which wires appear in ``circuit()``. """ifcatalyst_qjit(op):op=op.user_functionifnotisinstance(op,Operator):ifisinstance(op,(PauliWord,PauliSentence)):ifwire_orderisNoneandlen(op.wires)>1:raiseValueError("wire_order is required by qml.matrix() for PauliWords ""or PauliSentences with more than one wire.")returnop.to_mat(wire_order=wire_order)ifisinstance(op,QuantumScript):ifwire_orderisNone:error_base_str="wire_order is required by qml.matrix() for tapes"iflen(op.wires)>1:raiseValueError(error_base_str+" with more than one wire.")iflen(op.wires)==0:raiseValueError(error_base_str+" without wires.")elifisinstance(op,qml.QNode):ifwire_orderisNoneandop.device.wiresisNone:raiseValueError("wire_order is required by qml.matrix() for QNodes if the device does ""not have wires specified.")elifcallable(op):ifgetattr(op,"num_wires",0)!=1andwire_orderisNone:raiseValueError("wire_order is required by qml.matrix() for quantum functions.")else:raiseTransformError("Input is not an Operator, tape, QNode, or quantum function")return_matrix_transform(op,wire_order=wire_order)try:returnop.matrix(wire_order=wire_order)except:# pylint: disable=bare-exceptreturnmatrix(QuantumScript(op.decomposition()),wire_order=wire_orderorop.wires)
@partial(transform,is_informative=True)def_matrix_transform(tape:QuantumScript,wire_order=None,**kwargs)->tuple[QuantumScriptBatch,PostprocessingFn]:ifwire_orderandnotset(tape.wires).issubset(wire_order):raiseTransformError(f"Wires in circuit {list(tape.wires)} are inconsistent with "f"those in wire_order {list(wire_order)}")wires=kwargs.get("device_wires",None)ortape.wireswire_order=wire_orderorwiresdefprocessing_fn(res):"""Defines how matrix works if applied to a tape containing multiple operations."""params=res[0].get_parameters(trainable_only=False)interface=qml.math.get_interface(*params)# initialize the unitary matrixiflen(res[0].operations)==0:result=qml.math.eye(2**len(wire_order),like=interface)else:result=matrix(res[0].operations[0],wire_order=wire_order)foropinres[0].operations[1:]:U=matrix(op,wire_order=wire_order)# Coerce the matrices U and result and use matrix multiplication. Broadcasted axes# are handled correctly automatically by ``matmul`` (See e.g. NumPy documentation)result=qml.math.matmul(*qml.math.coerce([U,result],like=interface),like=interface)returnresultreturn[tape],processing_fn@_matrix_transform.custom_qnode_transformdef_matrix_transform_qnode(self,qnode,targs,tkwargs):tkwargs.setdefault("device_wires",qnode.device.wires)returnself.default_qnode_transform(qnode,targs,tkwargs)