Source code for pennylane.templates.subroutines.qmc

# 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.
"""
Contains the QuantumMonteCarlo template and utility functions.
"""
# pylint: disable=too-many-arguments
import copy

import numpy as np

import pennylane as qml
from pennylane.operation import AnyWires, Operation
from pennylane.ops import QubitUnitary
from pennylane.wires import Wires


def probs_to_unitary(probs):
    r"""Calculates the unitary matrix corresponding to an input probability distribution.

    For a given distribution :math:`p(i)`, this function returns the unitary :math:`\mathcal{A}`
    that transforms the :math:`|0\rangle` state as

    .. math::

        \mathcal{A} |0\rangle = \sum_{i} \sqrt{p(i)} |i\rangle,

    so that measuring the resulting state in the computational basis will give the state
    :math:`|i\rangle` with probability :math:`p(i)`. Note that the returned unitary matrix is
    real and hence an orthogonal matrix.

    Args:
        probs (array): input probability distribution as a flat array

    Returns:
        array: unitary

    Raises:
        ValueError: if the input array is not flat or does not correspond to a probability
            distribution

    **Example:**

    >>> p = np.ones(4) / 4
    >>> probs_to_unitary(p)
    array([[ 0.5       ,  0.5       ,  0.5       ,  0.5       ],
           [ 0.5       , -0.83333333,  0.16666667,  0.16666667],
           [ 0.5       ,  0.16666667, -0.83333333,  0.16666667],
           [ 0.5       ,  0.16666667,  0.16666667, -0.83333333]])
    """

    if not qml.math.is_abstract(
        sum(probs)
    ):  # skip check and error if jitting to avoid JAX tracer errors
        if not qml.math.allclose(sum(probs), 1) or min(probs) < 0:
            raise ValueError(
                "A valid probability distribution of non-negative numbers that sum to one "
                "must be input"
            )

    # Using the approach discussed here:
    # https://quantumcomputing.stackexchange.com/questions/10239/how-can-i-fill-a-unitary-knowing-only-its-first-column
    psi = qml.math.sqrt(probs)
    overlap = psi[0]
    denominator = qml.math.sqrt(2 + 2 * overlap)
    psi = qml.math.set_index(psi, 0, psi[0] + 1)  # psi[0] += 1, but JAX-JIT compatible
    psi /= denominator

    dim = len(probs)
    return 2 * qml.math.outer(psi, psi) - np.eye(dim)


def func_to_unitary(func, M):
    r"""Calculates the unitary that encodes a function onto an ancilla qubit register.

    Consider a function defined on the set of integers :math:`X = \{0, 1, \ldots, M - 1\}` whose
    output is bounded in the interval :math:`[0, 1]`, i.e., :math:`f: X \rightarrow [0, 1]`.

    The ``func_to_unitary`` function returns a unitary :math:`\mathcal{R}` that performs the
    transformation:

    .. math::

        \mathcal{R} |i\rangle \otimes |0\rangle = |i\rangle\otimes \left(\sqrt{1 - f(i)}|0\rangle +
        \sqrt{f(i)} |1\rangle\right).

    In other words, for a given input state :math:`|i\rangle \otimes |0\rangle`, this unitary
    encodes the amplitude :math:`\sqrt{f(i)}` onto the :math:`|1\rangle` state of the ancilla qubit.
    Hence, measuring the ancilla qubit will result in the :math:`|1\rangle` state with probability
    :math:`f(i)`.

    Args:
        func (callable): a function defined on the set of integers
            :math:`X = \{0, 1, \ldots, M - 1\}` with output value inside :math:`[0, 1]`
        M (int): the number of integers that the function is defined on

    Returns:
        array: the :math:`\mathcal{R}` unitary

    Raises:
        ValueError: if func is not bounded with :math:`[0, 1]` for all :math:`X`

    **Example:**

    >>> func = lambda i: np.sin(i) ** 2
    >>> M = 16
    >>> func_to_unitary(func, M)
    array([[ 1.        ,  0.        ,  0.        , ...,  0.        ,
             0.        ,  0.        ],
           [ 0.        , -1.        ,  0.        , ...,  0.        ,
             0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.54030231, ...,  0.        ,
             0.        ,  0.        ],
           ...,
           [ 0.        ,  0.        ,  0.        , ..., -0.13673722,
             0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        , ...,  0.        ,
             0.75968791,  0.65028784],
           [ 0.        ,  0.        ,  0.        , ...,  0.        ,
             0.65028784, -0.75968791]])
    """
    unitary = np.zeros((2 * M, 2 * M))

    fs = [func(i) for i in range(M)]
    if not qml.math.is_abstract(
        fs[0]
    ):  # skip check and error if jitting to avoid JAX tracer errors
        if min(fs) < 0 or max(fs) > 1:
            raise ValueError(
                "func must be bounded within the interval [0, 1] for the range of input values"
            )

    for i, f in enumerate(fs):
        # array = set_index(array, idx, val) is a JAX-JIT compatible version of array[idx] = val
        unitary = qml.math.set_index(unitary, (2 * i, 2 * i), qml.math.sqrt(1 - f))
        unitary = qml.math.set_index(unitary, (2 * i + 1, 2 * i), qml.math.sqrt(f))
        unitary = qml.math.set_index(unitary, (2 * i, 2 * i + 1), qml.math.sqrt(f))
        unitary = qml.math.set_index(unitary, (2 * i + 1, 2 * i + 1), -qml.math.sqrt(1 - f))

    return unitary


def _make_V(dim):
    r"""Calculates the :math:`\mathcal{V}` unitary which performs a reflection along the
    :math:`|1\rangle` state of the end ancilla qubit.

    Args:
        dim (int): dimension of :math:`\mathcal{V}`

    Returns:
        array: the :math:`\mathcal{V}` unitary
    """
    assert dim % 2 == 0, "dimension for _make_V() must be even"

    one = np.array([[0, 0], [0, 1]])
    dim_without_qubit = int(dim / 2)

    return 2 * np.kron(np.eye(dim_without_qubit), one) - np.eye(dim)


def _make_Z(dim):
    r"""Calculates the :math:`\mathcal{Z}` unitary which performs a reflection along the all
    :math:`|0\rangle` state.

    Args:
        dim (int): dimension of :math:`\mathcal{Z}`

    Returns:
        array: the :math:`\mathcal{Z}` unitary
    """
    Z = -np.eye(dim)
    Z[0, 0] = 1
    return Z


def make_Q(A, R):
    r"""Calculates the :math:`\mathcal{Q}` matrix that encodes the expectation value according to
    the probability unitary :math:`\mathcal{A}` and the function-encoding unitary
    :math:`\mathcal{R}`.

    Following `this <https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.022321>`__ paper,
    the expectation value is encoded as the phase of an eigenvalue of :math:`\mathcal{Q}`. This
    phase can be estimated using quantum phase estimation using the
    :func:`~.QuantumPhaseEstimation` template. See :func:`~.QuantumMonteCarlo` for more details,
    which loads ``make_Q()`` internally and applies phase estimation.

    Args:
        A (array): The unitary matrix of :math:`\mathcal{A}` which encodes the probability
            distribution
        R (array): The unitary matrix of :math:`\mathcal{R}` which encodes the function

    Returns:
        array: the :math:`\mathcal{Q}` unitary
    """
    A_big = qml.math.kron(A, np.eye(2))
    F = R @ A_big
    F_dagger = F.conj().T

    dim = len(R)
    V = _make_V(dim)
    Z = _make_Z(dim)
    UV = F @ Z @ F_dagger @ V

    return UV @ UV


[docs]class QuantumMonteCarlo(Operation): r"""Performs the `quantum Monte Carlo estimation <https://arxiv.org/abs/1805.00109>`__ algorithm. Given a probability distribution :math:`p(i)` of dimension :math:`M = 2^{m}` for some :math:`m \geq 1` and a function :math:`f: X \rightarrow [0, 1]` defined on the set of integers :math:`X = \{0, 1, \ldots, M - 1\}`, this function implements the algorithm that allows the following expectation value to be estimated: .. math:: \mu = \sum_{i \in X} p(i) f(i). .. figure:: ../../_static/templates/subroutines/qmc.svg :align: center :width: 60% :target: javascript:void(0); Args: probs (array): input probability distribution as a flat array func (callable): input function :math:`f` defined on the set of integers :math:`X = \{0, 1, \ldots, M - 1\}` such that :math:`f(i)\in [0, 1]` for :math:`i \in X` target_wires (Union[Wires, Sequence[int], or int]): the target wires estimation_wires (Union[Wires, Sequence[int], or int]): the estimation wires Raises: ValueError: if ``probs`` is not flat or has a length that is not compatible with ``target_wires`` .. note:: This template is only compatible with simulators because the algorithm is performed using unitary matrices. Additionally, this operation is not differentiable. To implement the quantum Monte Carlo algorithm on hardware requires breaking down the unitary matrices into hardware-compatible gates, check out the :func:`~.quantum_monte_carlo` transformation for more details. .. details:: :title: Usage Details The algorithm proceeds as follows: #. The probability distribution :math:`p(i)` is encoded using a unitary :math:`\mathcal{A}` applied to the first :math:`m` qubits specified by ``target_wires``. #. The function :math:`f(i)` is encoded onto the last qubit of ``target_wires`` using a unitary :math:`\mathcal{R}`. #. The unitary :math:`\mathcal{Q}` is defined with eigenvalues :math:`e^{\pm 2 \pi i \theta}` such that the phase :math:`\theta` encodes the expectation value through the equation :math:`\mu = (1 + \cos (\pi \theta)) / 2`. The circuit in steps 1 and 2 prepares an equal superposition over the two states corresponding to the eigenvalues :math:`e^{\pm 2 \pi i \theta}`. #. The :func:`~.QuantumPhaseEstimation` circuit is applied so that :math:`\pm\theta` can be estimated by finding the probabilities of the :math:`n` estimation wires. This in turn allows for the estimation of :math:`\mu`. Visit `Rebentrost et al. (2018) <https://arxiv.org/abs/1805.00109>`__ for further details. In this algorithm, the number of applications :math:`N` of the :math:`\mathcal{Q}` unitary scales as :math:`2^{n}`. However, due to the use of quantum phase estimation, the error :math:`\epsilon` scales as :math:`\mathcal{O}(2^{-n})`. Hence, .. math:: N = \mathcal{O}\left(\frac{1}{\epsilon}\right). This scaling can be compared to standard Monte Carlo estimation, where :math:`N` samples are generated from the probability distribution and the average over :math:`f` is taken. In that case, .. math:: N = \mathcal{O}\left(\frac{1}{\epsilon^{2}}\right). Hence, the quantum Monte Carlo algorithm has a quadratically improved time complexity with :math:`N`. An example use case is given below. Consider a standard normal distribution :math:`p(x)` and a function :math:`f(x) = \sin ^{2} (x)`. The expectation value of :math:`f(x)` is :math:`\int_{-\infty}^{\infty}f(x)p(x)dx \approx 0.432332`. This number can be approximated by discretizing the problem and using the quantum Monte Carlo algorithm. First, the problem is discretized: .. code-block:: python from scipy.stats import norm m = 5 M = 2 ** m xmax = np.pi # bound to region [-pi, pi] xs = np.linspace(-xmax, xmax, M) probs = np.array([norm().pdf(x) for x in xs]) probs /= np.sum(probs) func = lambda i: np.sin(xs[i]) ** 2 The ``QuantumMonteCarlo`` template can then be used: .. code-block:: n = 10 N = 2 ** n target_wires = range(m + 1) estimation_wires = range(m + 1, n + m + 1) dev = qml.device("default.qubit", wires=(n + m + 1)) @qml.qnode(dev) def circuit(): qml.templates.QuantumMonteCarlo( probs, func, target_wires=target_wires, estimation_wires=estimation_wires, ) return qml.probs(estimation_wires) phase_estimated = np.argmax(circuit()[:int(N / 2)]) / N The estimated value can be retrieved using the formula :math:`\mu = (1-\cos(\pi \theta))/2` >>> (1 - np.cos(np.pi * phase_estimated)) / 2 0.4327096457464369 """ num_wires = AnyWires grad_method = None @classmethod def _primitive_bind_call(cls, *args, **kwargs): return cls._primitive.bind(*args, **kwargs) @classmethod def _unflatten(cls, data, metadata): new_op = cls.__new__(cls) new_op._hyperparameters = dict(metadata[1]) # pylint: disable=protected-access # call operation.__init__ to initialize private properties like _name, _id, _pauli_rep, etc. Operation.__init__(new_op, *data, wires=metadata[0]) return new_op def __init__(self, probs, func, target_wires, estimation_wires, id=None): if isinstance(probs, np.ndarray) and probs.ndim != 1: raise ValueError("The probability distribution must be specified as a flat array") dim_p = len(probs) num_target_wires_ = np.log2(2 * dim_p) num_target_wires = int(num_target_wires_) if not np.allclose(num_target_wires_, num_target_wires): raise ValueError( "The probability distribution must have a length that is a power of two" ) target_wires = tuple(target_wires) estimation_wires = tuple(estimation_wires) wires = target_wires + estimation_wires if num_target_wires != len(target_wires): raise ValueError( f"The probability distribution of dimension {dim_p} requires" f" {num_target_wires} target wires" ) self._hyperparameters = {"estimation_wires": estimation_wires, "target_wires": target_wires} A = probs_to_unitary(probs) R = func_to_unitary(func, dim_p) Q = make_Q(A, R) super().__init__(A, R, Q, wires=wires, id=id)
[docs] def map_wires(self, wire_map: dict): # pylint: disable=protected-access new_op = copy.deepcopy(self) new_op._wires = Wires([wire_map.get(wire, wire) for wire in self.wires]) for key in ["estimation_wires", "target_wires"]: new_op._hyperparameters[key] = tuple( wire_map.get(w, w) for w in self._hyperparameters[key] ) return new_op
@property def num_params(self): return 3
[docs] @staticmethod def compute_decomposition( A, R, Q, wires, estimation_wires, target_wires ): # pylint: disable=arguments-differ,unused-argument r"""Representation of the operator as a product of other operators. .. math:: O = O_1 O_2 \dots O_n. .. seealso:: :meth:`~.QuantumMonteCarlo.decomposition`. Args: A (array): unitary matrix corresponding to an input probability distribution R (array): unitary that encodes the function applied to the ancilla qubit register Q (array): matrix that encodes the expectation value according to the probability unitary and the function-encoding unitary wires (Any or Iterable[Any]): full set of wires that the operator acts on target_wires (Iterable[Any]): the target wires estimation_wires (Iterable[Any]): the estimation wires Returns: list[.Operator]: decomposition of the operator """ op_list = [ QubitUnitary(A, wires=target_wires[:-1]), QubitUnitary(R, wires=target_wires), qml.templates.QuantumPhaseEstimation( Q, target_wires=target_wires, estimation_wires=estimation_wires ), ] return op_list