Source code for pennylane.qinfo.transforms

# Copyright 2018-2020 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.
"""QNode transforms for the quantum information quantities."""
# pylint: disable=import-outside-toplevel, not-callable
import warnings
from collections.abc import Callable, Sequence
from functools import partial

import pennylane as qml
from pennylane import transform
from pennylane.devices import DefaultMixed, DefaultQubit, DefaultQubitLegacy
from pennylane.gradients import adjoint_metric_tensor, metric_tensor
from pennylane.measurements import DensityMatrixMP, StateMP
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.typing import PostprocessingFn


[docs]@partial(transform, final_transform=True) def reduced_dm(tape: QuantumScript, wires, **kwargs) -> tuple[QuantumScriptBatch, PostprocessingFn]: """Compute the reduced density matrix from a :class:`~.QNode` returning :func:`~pennylane.state`. Args: tape (QuantumTape or QNode or Callable)): A quantum circuit returning :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the reduced density matrix in the form of a tensor. **Example** .. code-block:: python import numpy as np dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0,1]) return qml.state() >>> transformed_circuit = reduced_dm(circuit, wires=[0]) >>> transformed_circuit(np.pi/2) tensor([[0.5+0.j, 0. +0.j], [0. +0.j, 0.5+0.j]], requires_grad=True) This is equivalent to the state of the wire ``0`` after measuring the wire ``1``: .. code-block:: python @qml.qnode(dev) def measured_circuit(x): qml.IsingXX(x, wires=[0,1]) m = qml.measure(1) return qml.density_matrix(wires=[0]), qml.probs(op=m) >>> dm, probs = measured_circuit(np.pi/2) >>> dm tensor([[0.5+0.j, 0. +0.j], [0. +0.j, 0.5+0.j]], requires_grad=True) >>> probs tensor([0.5, 0.5], requires_grad=True) .. seealso:: :func:`pennylane.density_matrix` and :func:`pennylane.math.reduce_dm` """ # device_wires is provided by the custom QNode transform all_wires = kwargs.get("device_wires", tape.wires) wire_map = {w: i for i, w in enumerate(all_wires)} indices = [wire_map[w] for w in wires] measurements = tape.measurements if len(measurements) != 1 or not isinstance(measurements[0], StateMP): raise ValueError("The qfunc measurement needs to be State.") def processing_fn(res): # device is provided by the custom QNode transform device = kwargs.get("device", None) c_dtype = getattr(device, "C_DTYPE", "complex128") # determine the density matrix dm_func = ( qml.math.reduce_dm if isinstance(measurements[0], DensityMatrixMP) or isinstance(device, DefaultMixed) else qml.math.reduce_statevector ) density_matrix = dm_func(res[0], indices=indices, c_dtype=c_dtype) return density_matrix return [tape], processing_fn
@reduced_dm.custom_qnode_transform def _reduced_dm_qnode(self, qnode, targs, tkwargs): if tkwargs.get("device", False): raise ValueError( "Cannot provide a 'device' value directly to the reduced_dm decorator when " "transforming a QNode." ) if tkwargs.get("device_wires", None): raise ValueError( "Cannot provide a 'device_wires' value directly to the reduced_dm decorator when " "transforming a QNode." ) tkwargs.setdefault("device", qnode.device) tkwargs.setdefault("device_wires", qnode.device.wires) return self.default_qnode_transform(qnode, targs, tkwargs)
[docs]@partial(transform, final_transform=True) def purity(tape: QuantumScript, wires, **kwargs) -> tuple[QuantumScriptBatch, PostprocessingFn]: r"""Compute the purity of a :class:`~.QuantumTape` returning :func:`~pennylane.state`. .. math:: \gamma = \text{Tr}(\rho^2) where :math:`\rho` is the density matrix. The purity of a normalized quantum state satisfies :math:`\frac{1}{d} \leq \gamma \leq 1`, where :math:`d` is the dimension of the Hilbert space. A pure state has a purity of 1. It is possible to compute the purity of a sub-system from a given state. To find the purity of the overall state, include all wires in the ``wires`` argument. Args: tape (QNode or QuantumTape or Callable): A quantum circuit object returning a :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the purity in the form of a tensor. **Example** .. code-block:: python dev = qml.device("default.mixed", wires=2) @qml.qnode(dev) def noisy_circuit(p): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) qml.BitFlip(p, wires=0) qml.BitFlip(p, wires=1) return qml.state() @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0, 1]) return qml.state() >>> purity(noisy_circuit, wires=[0, 1])(0.2) 0.5648000000000398 >>> purity(circuit, wires=[0])(np.pi / 2) 0.5 >>> purity(circuit, wires=[0, 1])(np.pi / 2) 1.0 .. seealso:: :func:`pennylane.math.purity` """ # device_wires is provided by the custom QNode transform all_wires = kwargs.get("device_wires", tape.wires) wire_map = {w: i for i, w in enumerate(all_wires)} indices = [wire_map[w] for w in wires] # Check measurement measurements = tape.measurements if len(measurements) != 1 or not isinstance(measurements[0], StateMP): raise ValueError("The qfunc return type needs to be a state.") def processing_fn(res): # device is provided by the custom QNode transform device = kwargs.get("device", None) c_dtype = getattr(device, "C_DTYPE", "complex128") # determine the density matrix density_matrix = ( res[0] if isinstance(measurements[0], DensityMatrixMP) or isinstance(device, DefaultMixed) else qml.math.dm_from_state_vector(res[0], c_dtype=c_dtype) ) return qml.math.purity(density_matrix, indices, c_dtype=c_dtype) return [tape], processing_fn
@purity.custom_qnode_transform def _purity_qnode(self, qnode, targs, tkwargs): if tkwargs.get("device", False): raise ValueError( "Cannot provide a 'device' value directly to the purity decorator when " "transforming a QNode." ) if tkwargs.get("device_wires", None): raise ValueError( "Cannot provide a 'device_wires' value directly to the purity decorator when " "transforming a QNode." ) tkwargs.setdefault("device", qnode.device) tkwargs.setdefault("device_wires", qnode.device.wires) return self.default_qnode_transform(qnode, targs, tkwargs)
[docs]@partial(transform, final_transform=True) def vn_entropy( tape: QuantumScript, wires: Sequence[int], base: float = None, **kwargs ) -> tuple[QuantumScriptBatch, PostprocessingFn]: r"""Compute the Von Neumann entropy from a :class:`.QuantumTape` returning a :func:`~pennylane.state`. .. math:: S( \rho ) = -\text{Tr}( \rho \log ( \rho )) Args: tape (QNode or QuantumTape or Callable): A quantum circuit returning a :func:`~pennylane.state`. wires (Sequence(int)): List of wires in the considered subsystem. base (float): Base for the logarithm, default is None the natural logarithm is used in this case. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the Von Neumann entropy in the form of a tensor. **Example** It is possible to obtain the entropy of a subsystem from a :class:`.QNode` returning a :func:`~pennylane.state`. .. code-block:: python dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0, 1]) return qml.state() >>> vn_entropy(circuit, wires=[0])(np.pi/2) 0.6931471805599453 The function is differentiable with backpropagation for all interfaces, e.g.: >>> param = np.array(np.pi/4, requires_grad=True) >>> qml.grad(vn_entropy(circuit, wires=[0]))(param) tensor(0.62322524, requires_grad=True) .. seealso:: :func:`pennylane.math.vn_entropy` and :func:`pennylane.vn_entropy` """ # device_wires is provided by the custom QNode transform all_wires = kwargs.get("device_wires", tape.wires) wire_map = {w: i for i, w in enumerate(all_wires)} indices = [wire_map[w] for w in wires] measurements = tape.measurements if len(measurements) != 1 or not isinstance(measurements[0], StateMP): raise ValueError("The qfunc return type needs to be a state.") def processing_fn(res): # device is provided by the custom QNode transform device = kwargs.get("device", None) c_dtype = getattr(device, "C_DTYPE", "complex128") # determine if the measurement is a state vector or a density matrix if not isinstance(measurements[0], DensityMatrixMP) and not isinstance( device, DefaultMixed ): # Compute entropy from state vector if len(wires) == len(all_wires): # The subsystem has all wires, so the entropy is 0 return 0.0 density_matrix = qml.math.dm_from_state_vector(res[0], c_dtype=c_dtype) entropy = qml.math.vn_entropy(density_matrix, indices, base, c_dtype=c_dtype) return entropy # Compute entropy from density matrix entropy = qml.math.vn_entropy(res[0], indices, base, c_dtype) return entropy return [tape], processing_fn
@vn_entropy.custom_qnode_transform def _vn_entropy_qnode(self, qnode, targs, tkwargs): if tkwargs.get("device", False): raise ValueError( "Cannot provide a 'device' value directly to the vn_entropy decorator when " "transforming a QNode." ) if tkwargs.get("device_wires", None): raise ValueError( "Cannot provide a 'device_wires' value directly to the vn_entropy decorator when " "transforming a QNode." ) tkwargs.setdefault("device", qnode.device) tkwargs.setdefault("device_wires", qnode.device.wires) return self.default_qnode_transform(qnode, targs, tkwargs) def _bipartite_qinfo_transform( transform_func: Callable, tape: QuantumScript, wires0: Sequence[int], wires1: Sequence[int], base: float = None, **kwargs, ): # device_wires is provided by the custom QNode transform all_wires = kwargs.get("device_wires", tape.wires) wire_map = {w: i for i, w in enumerate(all_wires)} indices0 = [wire_map[w] for w in wires0] indices1 = [wire_map[w] for w in wires1] # Check measurement measurements = tape.measurements if len(measurements) != 1 or not isinstance(measurements[0], StateMP): raise ValueError("The qfunc return type needs to be a state.") def processing_fn(res): # device is provided by the custom QNode transform device = kwargs.get("device", None) c_dtype = getattr(device, "C_DTYPE", "complex128") density_matrix = ( res[0] if isinstance(measurements[0], DensityMatrixMP) or isinstance(device, DefaultMixed) else qml.math.dm_from_state_vector(res[0], c_dtype=c_dtype) ) entropy = transform_func(density_matrix, indices0, indices1, base=base, c_dtype=c_dtype) return entropy return [tape], processing_fn
[docs]@partial(transform, final_transform=True) def mutual_info( tape: QuantumScript, wires0: Sequence[int], wires1: Sequence[int], base: float = None, **kwargs ) -> tuple[QuantumScriptBatch, PostprocessingFn]: r"""Compute the mutual information from a :class:`.QuantumTape` returning a :func:`~pennylane.state`: .. math:: I(A, B) = S(\rho^A) + S(\rho^B) - S(\rho^{AB}) where :math:`S` is the von Neumann entropy. The mutual information is a measure of correlation between two subsystems. More specifically, it quantifies the amount of information obtained about one system by measuring the other system. Args: qnode (QNode or QuantumTape or Callable): A quantum circuit returning a :func:`~pennylane.state`. wires0 (Sequence(int)): List of wires in the first subsystem. wires1 (Sequence(int)): List of wires in the second subsystem. base (float): Base for the logarithm. If None, the natural logarithm is used. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the mutual information in the form of a tensor. **Example** It is possible to obtain the mutual information of two subsystems from a :class:`.QNode` returning a :func:`~pennylane.state`. .. code-block:: python dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0, 1]) return qml.state() >>> mutual_info_circuit = qinfo.mutual_info(circuit, wires0=[0], wires1=[1]) >>> mutual_info_circuit(np.pi/2) 1.3862943611198906 >>> x = np.array(0.4, requires_grad=True) >>> mutual_info_circuit(x) 0.3325090393262875 >>> qml.grad(mutual_info_circuit)(np.array(0.4, requires_grad=True)) tensor(1.24300677, requires_grad=True) .. seealso:: :func:`~.qinfo.vn_entropy`, :func:`pennylane.math.mutual_info` and :func:`pennylane.mutual_info` """ return _bipartite_qinfo_transform(qml.math.mutual_info, tape, wires0, wires1, base, **kwargs)
@mutual_info.custom_qnode_transform def _mutual_info_qnode(self, qnode, targs, tkwargs): if tkwargs.get("device", False): raise ValueError( "Cannot provide a 'device' value directly to the mutual_info decorator when " "transforming a QNode." ) if tkwargs.get("device_wires", None): raise ValueError( "Cannot provide a 'device_wires' value directly to the mutual_info decorator when " "transforming a QNode." ) tkwargs.setdefault("device", qnode.device) tkwargs.setdefault("device_wires", qnode.device.wires) return self.default_qnode_transform(qnode, targs, tkwargs)
[docs]@partial(transform, final_transform=True) def vn_entanglement_entropy( tape, wires0: Sequence[int], wires1: Sequence[int], base: float = None, **kwargs ): r"""Compute the Von Neumann entanglement entropy from a circuit returning a :func:`~pennylane.state`: .. math:: S(\rho_A) = -\text{Tr}[\rho_A \log \rho_A] = -\text{Tr}[\rho_B \log \rho_B] = S(\rho_B) where :math:`S` is the von Neumann entropy; :math:`\rho_A = \text{Tr}_B [\rho_{AB}]` and :math:`\rho_B = \text{Tr}_A [\rho_{AB}]` are the reduced density matrices for each partition. The Von Neumann entanglement entropy is a measure of the degree of quantum entanglement between two subsystems constituting a pure bipartite quantum state. The entropy of entanglement is the Von Neumann entropy of the reduced density matrix for any of the subsystems. If it is non-zero, it indicates the two subsystems are entangled. Args: tape (QNode or QuantumTape or Callable): A quantum circuit returning a :func:`~pennylane.state`. wires0 (Sequence(int)): List of wires in the first subsystem. wires1 (Sequence(int)): List of wires in the second subsystem. base (float): Base for the logarithm. If None, the natural logarithm is used. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the entanglement entropy in the form of a tensor. """ return _bipartite_qinfo_transform( qml.math.vn_entanglement_entropy, tape, wires0, wires1, base, **kwargs )
def classical_fisher(qnode, argnums=0): r"""Returns a function that computes the classical fisher information matrix (CFIM) of a given :class:`.QNode` or quantum tape. Given a parametrized (classical) probability distribution :math:`p(\bm{\theta})`, the classical fisher information matrix quantifies how changes to the parameters :math:`\bm{\theta}` are reflected in the probability distribution. For a parametrized quantum state, we apply the concept of classical fisher information to the computational basis measurement. More explicitly, this function implements eq. (15) in `arxiv:2103.15191 <https://arxiv.org/abs/2103.15191>`_: .. math:: \text{CFIM}_{i, j} = \sum_{\ell=0}^{2^N-1} \frac{1}{p_\ell(\bm{\theta})} \frac{\partial p_\ell(\bm{\theta})}{ \partial \theta_i} \frac{\partial p_\ell(\bm{\theta})}{\partial \theta_j} for :math:`N` qubits. Args: tape (:class:`.QNode` or qml.QuantumTape): A :class:`.QNode` or quantum tape that may have arbitrary return types. argnums (Optional[int or List[int]]): Arguments to be differentiated in case interface ``jax`` is used. Returns: func: The function that computes the classical fisher information matrix. This function accepts the same signature as the :class:`.QNode`. If the signature contains one differentiable variable ``params``, the function returns a matrix of size ``(len(params), len(params))``. For multiple differentiable arguments ``x, y, z``, it returns a list of sizes ``[(len(x), len(x)), (len(y), len(y)), (len(z), len(z))]``. .. warning:: ``pennylane.qinfo.classical_fisher`` is being migrated to a different module and will removed in version 0.39. Instead, use :func:`pennylane.gradients.classical_fisher`. .. seealso:: :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.qinfo.transforms.quantum_fisher` **Example** First, let us define a parametrized quantum state and return its (classical) probability distribution for all computational basis elements: .. code-block:: python import pennylane.numpy as pnp dev = qml.device("default.qubit") @qml.qnode(dev) def circ(params): qml.RX(params[0], wires=0) qml.CNOT([0, 1]) qml.CRY(params[1], wires=[1, 0]) qml.Hadamard(1) return qml.probs(wires=[0, 1]) Executing this circuit yields the ``2**2=4`` elements of :math:`p_\ell(\bm{\theta})` >>> pnp.random.seed(25) >>> params = pnp.random.random(2) >>> circ(params) [0.41850088 0.41850088 0.08149912 0.08149912] We can obtain its ``(2, 2)`` classical fisher information matrix (CFIM) by simply calling the function returned by ``classical_fisher()``: >>> cfim_func = qml.qinfo.classical_fisher(circ) >>> cfim_func(params) [[ 0.901561 -0.125558] [-0.125558 0.017486]] This function has the same signature as the :class:`.QNode`. Here is a small example with multiple arguments: .. code-block:: python @qml.qnode(dev) def circ(x, y): qml.RX(x, wires=0) qml.RY(y, wires=0) return qml.probs(wires=range(n_wires)) >>> x, y = pnp.array([0.5, 0.6], requires_grad=True) >>> circ(x, y) [0.86215007 0. 0.13784993 0. ] >>> qml.qinfo.classical_fisher(circ)(x, y) [array([[0.32934729]]), array([[0.51650396]])] Note how in the case of multiple variables we get a list of matrices with sizes ``[(n_params0, n_params0), (n_params1, n_params1)]``, which in this case is simply two ``(1, 1)`` matrices. A typical setting where the classical fisher information matrix is used is in variational quantum algorithms. Closely related to the `quantum natural gradient <https://arxiv.org/abs/1909.02108>`_, which employs the `quantum` fisher information matrix, we can compute a rescaled gradient using the CFIM. In this scenario, typically a Hamiltonian objective function :math:`\langle H \rangle` is minimized: .. code-block:: python H = qml.Hamiltonian(coeffs=[0.5, 0.5], observables=[qml.Z(0), qml.Z(1)]) @qml.qnode(dev) def circ(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) qml.RX(params[2], wires=1) qml.RY(params[3], wires=1) qml.CNOT(wires=(0,1)) return qml.expval(H) params = pnp.random.random(4) We can compute both the gradient of :math:`\langle H \rangle` and the CFIM with the same :class:`.QNode` ``circ`` in this example since ``classical_fisher()`` ignores the return types and assumes ``qml.probs()`` for all wires. >>> grad = qml.grad(circ)(params) >>> cfim = qml.qinfo.classical_fisher(circ)(params) >>> print(grad.shape, cfim.shape) (4,) (4, 4) Combined together, we can get a rescaled gradient to be employed for optimization schemes like natural gradient descent. >>> rescaled_grad = cfim @ grad >>> print(rescaled_grad) [-0.66772533 -0.16618756 -0.05865127 -0.06696078] The ``classical_fisher`` matrix itself is again differentiable: .. code-block:: python @qml.qnode(dev) def circ(params): qml.RX(qml.math.cos(params[0]), wires=0) qml.RX(qml.math.cos(params[0]), wires=1) qml.RX(qml.math.cos(params[1]), wires=0) qml.RX(qml.math.cos(params[1]), wires=1) return qml.probs(wires=range(2)) params = pnp.random.random(2) >>> qml.qinfo.classical_fisher(circ)(params) [[4.18575068e-06 2.34443943e-03] [2.34443943e-03 1.31312079e+00]] >>> qml.jacobian(qml.qinfo.classical_fisher(circ))(params) array([[[9.98030491e-01, 3.46944695e-18], [1.36541817e-01, 5.15248592e-01]], [[1.36541817e-01, 5.15248592e-01], [2.16840434e-18, 2.81967252e-01]]])) """ warnings.warn( "pennylane.qinfo.classical_fisher is being migrated to a different module and will " "removed in version 0.39. Instead, use pennylane.gradients.classical_fisher.", qml.PennyLaneDeprecationWarning, ) return qml.gradients.classical_fisher(qnode, argnums=argnums) @partial(transform, is_informative=True) def quantum_fisher( tape: QuantumScript, device, *args, **kwargs ) -> tuple[QuantumScriptBatch, PostprocessingFn]: r"""Returns a function that computes the quantum fisher information matrix (QFIM) of a given :class:`.QNode`. Given a parametrized quantum state :math:`|\psi(\bm{\theta})\rangle`, the quantum fisher information matrix (QFIM) quantifies how changes to the parameters :math:`\bm{\theta}` are reflected in the quantum state. The metric used to induce the QFIM is the fidelity :math:`f = |\langle \psi | \psi' \rangle|^2` between two (pure) quantum states. This leads to the following definition of the QFIM (see eq. (27) in `arxiv:2103.15191 <https://arxiv.org/abs/2103.15191>`_): .. math:: \text{QFIM}_{i, j} = 4 \text{Re}\left[ \langle \partial_i \psi(\bm{\theta}) | \partial_j \psi(\bm{\theta}) \rangle - \langle \partial_i \psi(\bm{\theta}) | \psi(\bm{\theta}) \rangle \langle \psi(\bm{\theta}) | \partial_j \psi(\bm{\theta}) \rangle \right] with short notation :math:`| \partial_j \psi(\bm{\theta}) \rangle := \frac{\partial}{\partial \theta_j}| \psi(\bm{\theta}) \rangle`. .. seealso:: :func:`~.pennylane.metric_tensor`, :func:`~.pennylane.adjoint_metric_tensor`, :func:`~.pennylane.qinfo.transforms.classical_fisher` Args: tape (QNode or QuantumTape or Callable): A quantum circuit that may have arbitrary return types. *args: In case finite shots are used, further arguments according to :func:`~.pennylane.metric_tensor` may be passed. Returns: qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will provide the quantum Fisher information in the form of a tensor. .. warning:: ``pennylane.qinfo.quantum_fisher`` is being migrated to a different module and will removed in version 0.39. Instead, use :func:`pennylane.gradients.quantum_fisher`. .. note:: ``quantum_fisher`` coincides with the ``metric_tensor`` with a prefactor of :math:`4`. Internally, :func:`~.pennylane.adjoint_metric_tensor` is used when executing on ``"default.qubit"`` with exact expectations (``shots=None``). In all other cases, e.g. if a device with finite shots is used, the hardware-compatible transform :func:`~.pennylane.metric_tensor` is used, which may require an additional wire on the device. Please refer to the respective documentations for details. **Example** The quantum Fisher information matrix (QIFM) can be used to compute the `natural` gradient for `Quantum Natural Gradient Descent <https://arxiv.org/abs/1909.02108>`_. A typical scenario is optimizing the expectation value of a Hamiltonian: .. code-block:: python n_wires = 2 dev = qml.device("default.qubit", wires=n_wires) H = 1.*qml.X(0) @ qml.X(1) - 0.5 * qml.Z(1) @qml.qnode(dev) def circ(params): qml.RY(params[0], wires=1) qml.CNOT(wires=(1,0)) qml.RY(params[1], wires=1) qml.RZ(params[2], wires=1) return qml.expval(H) params = pnp.array([0.5, 1., 0.2], requires_grad=True) The natural gradient is then simply the QFIM multiplied by the gradient: >>> grad = qml.grad(circ)(params) >>> grad [ 0.59422561 -0.02615095 -0.05146226] >>> qfim = qml.qinfo.quantum_fisher(circ)(params) >>> qfim [[1. 0. 0. ] [0. 1. 0. ] [0. 0. 0.77517241]] >>> qfim @ grad tensor([ 0.59422561, -0.02615095, -0.03989212], requires_grad=True) When using real hardware or finite shots, ``quantum_fisher`` is internally calling :func:`~.pennylane.metric_tensor`. To obtain the full QFIM, we need an auxilary wire to perform the Hadamard test. >>> dev = qml.device("default.qubit", wires=n_wires+1, shots=1000) >>> @qml.qnode(dev) ... def circ(params): ... qml.RY(params[0], wires=1) ... qml.CNOT(wires=(1,0)) ... qml.RY(params[1], wires=1) ... qml.RZ(params[2], wires=1) ... return qml.expval(H) >>> qfim = qml.qinfo.quantum_fisher(circ)(params) Alternatively, we can fall back on the block-diagonal QFIM without the additional wire. >>> qfim = qml.qinfo.quantum_fisher(circ, approx="block-diag")(params) """ warnings.warn( "pennylane.qinfo.quantum_fisher is being migrated to a different module and will " "removed in version 0.39. Instead, use pennylane.gradients.quantum_fisher.", qml.PennyLaneDeprecationWarning, ) if device.shots or not isinstance(device, (DefaultQubitLegacy, DefaultQubit)): tapes, processing_fn = metric_tensor(tape, *args, **kwargs) def processing_fn_multiply(res): res = qml.execute(res, device=device) return 4 * processing_fn(res) return tapes, processing_fn_multiply res = adjoint_metric_tensor(tape, *args, **kwargs) def processing_fn_multiply(r): # pylint: disable=function-redefined r = qml.math.stack(r) return 4 * r return res, processing_fn_multiply
[docs]@quantum_fisher.custom_qnode_transform def qnode_execution_wrapper(self, qnode, targs, tkwargs): """Here, we overwrite the QNode execution wrapper in order to take into account that classical processing may be present inside the QNode.""" tkwargs["device"] = qnode.device return self.default_qnode_transform(qnode, targs, tkwargs)
[docs]def fidelity(qnode0, qnode1, wires0, wires1): r"""Compute the fidelity for two :class:`.QNode` returning a :func:`~pennylane.state` (a state can be a state vector or a density matrix, depending on the device) acting on quantum systems with the same size. The fidelity for two mixed states given by density matrices :math:`\rho` and :math:`\sigma` is defined as .. math:: F( \rho , \sigma ) = \text{Tr}( \sqrt{\sqrt{\rho} \sigma \sqrt{\rho}})^2 If one of the states is pure, say :math:`\rho=\ket{\psi}\bra{\psi}`, then the expression for fidelity simplifies to .. math:: F( \ket{\psi} , \sigma ) = \bra{\psi} \sigma \ket{\psi} Finally, if both states are pure, :math:`\sigma=\ket{\phi}\bra{\phi}`, then the fidelity is simply .. math:: F( \ket{\psi} , \ket{\phi}) = \left|\braket{\psi| \phi}\right|^2 .. note:: The second state is coerced to the type and dtype of the first state. The fidelity is returned in the type of the interface of the first state. Args: state0 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. state1 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. wires0 (Sequence[int]): the wires of the first subsystem wires1 (Sequence[int]): the wires of the second subsystem Returns: func: A function that returns the fidelity between the states outputted by the QNodes. **Example** First, let's consider two QNodes with potentially different signatures: a circuit with two parameters and another circuit with a single parameter. The output of the :func:`~.qinfo.fidelity` transform then requires two tuples to be passed as arguments, each containing the args and kwargs of their respective circuit, e.g. ``all_args0 = (0.1, 0.3)`` and ``all_args1 = (0.2)`` in the following case: .. code-block:: python dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def circuit_rx(x, y): qml.RX(x, wires=0) qml.RZ(y, wires=0) return qml.state() @qml.qnode(dev) def circuit_ry(y): qml.RY(y, wires=0) return qml.state() >>> qml.qinfo.fidelity(circuit_rx, circuit_ry, wires0=[0], wires1=[0])((0.1, 0.3), (0.2)) 0.9905158135644924 It is also possible to use QNodes that do not depend on any parameters. When it is the case for the first QNode, it is required to pass an empty tuple as an argument for the first QNode. .. code-block:: python dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def circuit_rx(): return qml.state() @qml.qnode(dev) def circuit_ry(x): qml.RY(x, wires=0) return qml.state() >>> qml.qinfo.fidelity(circuit_rx, circuit_ry, wires0=[0], wires1=[0])(None, (0.2)) 0.9900332889206207 On the other hand, if the second QNode is the one that does not depend on parameters then a single tuple can also be passed: >>> qml.qinfo.fidelity(circuit_ry, circuit_rx, wires0=[0], wires1=[0])((0.2)) 0.9900332889206207 The :func:`~.qinfo.fidelity` transform is also differentiable and the gradient can be obtained in the different frameworks with backpropagation, the following example uses ``jax`` and ``backprop``. .. code-block:: python dev = qml.device("default.qubit", wires=1) @qml.qnode(dev, interface="jax") def circuit0(x): qml.RX(x, wires=0) return qml.state() @qml.qnode(dev, interface="jax") def circuit1(): qml.Z(0) return qml.state() >>> jax.grad(qml.qinfo.fidelity(circuit0, circuit1, wires0=[0], wires1=[0]))((jax.numpy.array(0.3))) Array(-0.14776011, dtype=float64, weak_type=True) There is also the possibility to pass a single dictionary at the end of the tuple for fixing args, you can follow this example: .. code-block:: python dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def circuit_rx(x, y): qml.RX(x, wires=0) qml.RZ(y, wires=0) return qml.state() @qml.qnode(dev) def circuit_ry(y, use_ry): if use_ry: qml.RY(y, wires=0) return qml.state() >>> fidelity(circuit_rx, circuit_ry, wires0=[0], wires1=[0])((0.1, 0.3), (0.9, {'use_ry': True})) 0.8208074192135424 .. seealso:: :func:`pennylane.math.fidelity` """ if len(wires0) != len(wires1): raise qml.QuantumFunctionError("The two states must have the same number of wires.") state_qnode0 = qml.qinfo.reduced_dm(qnode0, wires=wires0) state_qnode1 = qml.qinfo.reduced_dm(qnode1, wires=wires1) def evaluate_fidelity(all_args0=None, all_args1=None): """Wrapper used for evaluation of the fidelity between two states computed from QNodes. It allows giving the args and kwargs to each :class:`.QNode`. Args: all_args0 (tuple): Tuple containing the arguments (*args, kwargs) of the first :class:`.QNode`. all_args1 (tuple): Tuple containing the arguments (*args, kwargs) of the second :class:`.QNode`. Returns: float: Fidelity between two quantum states """ if not isinstance(all_args0, tuple) and all_args0 is not None: all_args0 = (all_args0,) if not isinstance(all_args1, tuple) and all_args1 is not None: all_args1 = (all_args1,) # If no all_args is given, evaluate the QNode without args if all_args0 is not None: # Handle a dictionary as last argument if isinstance(all_args0[-1], dict): args0 = all_args0[:-1] kwargs0 = all_args0[-1] else: args0 = all_args0 kwargs0 = {} state0 = state_qnode0(*args0, **kwargs0) else: # No args state0 = state_qnode0() # If no all_args is given, evaluate the QNode without args if all_args1 is not None: # Handle a dictionary as last argument if isinstance(all_args1[-1], dict): args1 = all_args1[:-1] kwargs1 = all_args1[-1] else: args1 = all_args1 kwargs1 = {} state1 = state_qnode1(*args1, **kwargs1) else: # No args state1 = state_qnode1() # From the two generated states, compute the fidelity. fid = qml.math.fidelity(state0, state1) return fid return evaluate_fidelity
[docs]def relative_entropy(qnode0, qnode1, wires0, wires1): r""" Compute the relative entropy for two :class:`.QNode` returning a :func:`~pennylane.state` (a state can be a state vector or a density matrix, depending on the device) acting on quantum systems with the same size. .. math:: S(\rho\,\|\,\sigma)=-\text{Tr}(\rho\log\sigma)-S(\rho)=\text{Tr}(\rho\log\rho)-\text{Tr}(\rho\log\sigma) =\text{Tr}(\rho(\log\rho-\log\sigma)) Roughly speaking, quantum relative entropy is a measure of distinguishability between two quantum states. It is the quantum mechanical analog of relative entropy. Args: qnode0 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. qnode1 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. wires0 (Sequence[int]): the subsystem of the first QNode wires1 (Sequence[int]): the subsystem of the second QNode Returns: func: A function that takes as input the joint arguments of the two QNodes, and returns the relative entropy from their output states. **Example** Consider the following QNode: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(param): qml.RY(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.state() The ``qml.qinfo.relative_entropy`` transform can be used to compute the relative entropy between the output states of the QNode: >>> relative_entropy_circuit = qml.qinfo.relative_entropy(circuit, circuit, wires0=[0], wires1=[0]) The returned function takes two tuples as input, the first being the arguments to the first QNode and the second being the arguments to the second QNode: >>> x, y = np.array(0.4), np.array(0.6) >>> relative_entropy_circuit((x,), (y,)) tensor(0.01775001, requires_grad=True) This transform is fully differentiable: .. code-block:: python def wrapper(x, y): return relative_entropy_circuit((x,), (y,)) >>> wrapper(x, y) tensor(0.01775001, requires_grad=True) >>> qml.grad(wrapper)(x, y) (tensor(-0.16458856, requires_grad=True), tensor(0.16953273, requires_grad=True)) """ if len(wires0) != len(wires1): raise qml.QuantumFunctionError("The two states must have the same number of wires.") state_qnode0 = qml.qinfo.reduced_dm(qnode0, wires=wires0) state_qnode1 = qml.qinfo.reduced_dm(qnode1, wires=wires1) def evaluate_relative_entropy(all_args0=None, all_args1=None): """Wrapper used for evaluation of the relative entropy between two states computed from QNodes. It allows giving the args and kwargs to each :class:`.QNode`. Args: all_args0 (tuple): Tuple containing the arguments (*args, kwargs) of the first :class:`.QNode`. all_args1 (tuple): Tuple containing the arguments (*args, kwargs) of the second :class:`.QNode`. Returns: float: Relative entropy between two quantum states """ if not isinstance(all_args0, tuple) and all_args0 is not None: all_args0 = (all_args0,) if not isinstance(all_args1, tuple) and all_args1 is not None: all_args1 = (all_args1,) # If no all_args is given, evaluate the QNode without args if all_args0 is not None: # Handle a dictionary as last argument if isinstance(all_args0[-1], dict): args0 = all_args0[:-1] kwargs0 = all_args0[-1] else: args0 = all_args0 kwargs0 = {} state0 = state_qnode0(*args0, **kwargs0) else: # No args state0 = state_qnode0() # If no all_args is given, evaluate the QNode without args if all_args1 is not None: # Handle a dictionary as last argument if isinstance(all_args1[-1], dict): args1 = all_args1[:-1] kwargs1 = all_args1[-1] else: args1 = all_args1 kwargs1 = {} state1 = state_qnode1(*args1, **kwargs1) else: # No args state1 = state_qnode1() # From the two generated states, compute the relative entropy return qml.math.relative_entropy(state0, state1) return evaluate_relative_entropy
[docs]def trace_distance(qnode0, qnode1, wires0, wires1): r""" Compute the trace distance for two :class:`.QNode` returning a :func:`~pennylane.state` (a state can be a state vector or a density matrix, depending on the device) acting on quantum systems with the same size. .. math:: T(\rho, \sigma)=\frac12\|\rho-\sigma\|_1 =\frac12\text{Tr}\left(\sqrt{(\rho-\sigma)^{\dagger}(\rho-\sigma)}\right) where :math:`\|\cdot\|_1` is the Schatten :math:`1`-norm. The trace distance measures how close two quantum states are. In particular, it upper-bounds the probability of distinguishing two quantum states. Args: qnode0 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. qnode1 (QNode): A :class:`.QNode` returning a :func:`~pennylane.state`. wires0 (Sequence[int]): the subsystem of the first QNode. wires1 (Sequence[int]): the subsystem of the second QNode. Returns: func: A function that takes as input the joint arguments of the two QNodes, and returns the trace distance between their output states. **Example** Consider the following QNode: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(param): qml.RY(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.state() The ``qml.qinfo.trace_distance`` transform can be used to compute the trace distance between the output states of the QNode: >>> trace_distance_circuit = qml.qinfo.trace_distance(circuit, circuit, wires0=[0], wires1=[0]) The returned function takes two tuples as input, the first being the arguments to the first QNode and the second being the arguments to the second QNode: >>> x, y = np.array(0.4), np.array(0.6) >>> trace_distance_circuit((x,), (y,)) 0.047862689546603415 This transform is fully differentiable: .. code-block:: python def wrapper(x, y): return trace_distance_circuit((x,), (y,)) >>> wrapper(x, y) 0.047862689546603415 >>> qml.grad(wrapper)(x, y) (tensor(-0.19470917, requires_grad=True), tensor(0.28232124, requires_grad=True)) """ if len(wires0) != len(wires1): raise qml.QuantumFunctionError("The two states must have the same number of wires.") state_qnode0 = qml.qinfo.reduced_dm(qnode0, wires=wires0) state_qnode1 = qml.qinfo.reduced_dm(qnode1, wires=wires1) def evaluate_trace_distance(all_args0=None, all_args1=None): """Wrapper used for evaluation of the trace distance between two states computed from QNodes. It allows giving the args and kwargs to each :class:`.QNode`. Args: all_args0 (tuple): Tuple containing the arguments (*args, kwargs) of the first :class:`.QNode`. all_args1 (tuple): Tuple containing the arguments (*args, kwargs) of the second :class:`.QNode`. Returns: float: Trace distance between two quantum states """ if not isinstance(all_args0, tuple) and all_args0 is not None: all_args0 = (all_args0,) if not isinstance(all_args1, tuple) and all_args1 is not None: all_args1 = (all_args1,) # If no all_args is given, evaluate the QNode without args if all_args0 is not None: # Handle a dictionary as last argument if isinstance(all_args0[-1], dict): args0 = all_args0[:-1] kwargs0 = all_args0[-1] else: args0 = all_args0 kwargs0 = {} state0 = state_qnode0(*args0, **kwargs0) else: # No args state0 = state_qnode0() # If no all_args is given, evaluate the QNode without args if all_args1 is not None: # Handle a dictionary as last argument if isinstance(all_args1[-1], dict): args1 = all_args1[:-1] kwargs1 = all_args1[-1] else: args1 = all_args1 kwargs1 = {} state1 = state_qnode1(*args1, **kwargs1) else: # No args state1 = state_qnode1() # From the two generated states, compute the trace distance return qml.math.trace_distance(state0, state1) return evaluate_trace_distance