Source code for pennylane.templates.state_preparations.mottonen

# 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.
r"""
Contains the MottonenStatePreparation template.
"""
import numpy as np

import pennylane as qml
from pennylane.operation import AnyWires, Operation


# pylint: disable=len-as-condition,arguments-out-of-order,consider-using-enumerate
def gray_code(rank):
    """Generates the Gray code of given rank.

    Args:
        rank (int): rank of the Gray code (i.e. number of bits)
    """

    def gray_code_recurse(g, rank):
        k = len(g)
        if rank <= 0:
            return

        for i in range(k - 1, -1, -1):
            char = "1" + g[i]
            g.append(char)
        for i in range(k - 1, -1, -1):
            g[i] = "0" + g[i]

        gray_code_recurse(g, rank - 1)

    g = ["0", "1"]
    gray_code_recurse(g, rank - 1)

    return g


def _matrix_M_entry(row, col):
    """Returns one entry for the matrix that maps alpha to theta.

    See Eq. (3) in `Möttönen et al. (2004) <https://arxiv.org/abs/quant-ph/0407010>`_.

    Args:
        row (int): one-based row number
        col (int): one-based column number

    Returns:
        (float): transformation matrix entry at given row and column
    """
    # (col >> 1) ^ col is the Gray code of col
    b_and_g = row & ((col >> 1) ^ col)
    sum_of_ones = 0
    while b_and_g > 0:
        if b_and_g & 0b1:
            sum_of_ones += 1

        b_and_g = b_and_g >> 1

    return (-1) ** sum_of_ones


def compute_theta(alpha):
    """Maps the angles alpha of the multi-controlled rotations decomposition of a uniformly controlled rotation
     to the rotation angles used in the Gray code implementation.

    Args:
        alpha (tensor_like): alpha parameters

    Returns:
        (tensor_like): rotation angles theta
    """
    ln = alpha.shape[-1]

    M_trans = np.zeros(shape=(ln, ln))
    for i in range(len(M_trans)):
        for j in range(len(M_trans[0])):
            M_trans[i, j] = _matrix_M_entry(j, i)

    theta = qml.math.transpose(qml.math.dot(M_trans, qml.math.transpose(alpha)))

    return theta / ln


def _apply_uniform_rotation_dagger(gate, alpha, control_wires, target_wire):
    r"""Applies a uniformly-controlled rotation to the target qubit.

    A uniformly-controlled rotation is a sequence of multi-controlled
    rotations, each of which is conditioned on the control qubits being in a different state.
    For example, a uniformly-controlled rotation with two control qubits describes a sequence of
    four multi-controlled rotations, each applying the rotation only if the control qubits
    are in states :math:`|00\rangle`, :math:`|01\rangle`, :math:`|10\rangle`, and :math:`|11\rangle`, respectively.

    To implement a uniformly-controlled rotation using single qubit rotations and CNOT gates,
    a decomposition based on Gray codes is used. For this purpose, the multi-controlled rotation
    angles alpha have to be converted into a set of non-controlled rotation angles theta.

    For more details, see `Möttönen and Vartiainen (2005), Fig 7a<https://arxiv.org/abs/quant-ph/0504100>`_.

    Args:
        gate (.Operation): gate to be applied, needs to have exactly one parameter
        alpha (tensor_like): angles to decompose the uniformly-controlled rotation into multi-controlled rotations
        control_wires (array[int]): wires that act as control
        target_wire (int): wire that acts as target

    Returns:
          list[.Operator]: sequence of operators defined by this function
    """
    op_list = []
    theta = compute_theta(alpha)

    gray_code_rank = len(control_wires)

    if gray_code_rank == 0:
        if (
            qml.math.is_abstract(theta)
            or qml.math.requires_grad(theta)
            or qml.math.all(theta[..., 0] != 0.0)
        ):
            op_list.append(gate(theta[..., 0], wires=[target_wire]))
        return op_list

    code = gray_code(gray_code_rank)
    num_selections = len(code)

    control_indices = [
        int(np.log2(int(code[i], 2) ^ int(code[(i + 1) % num_selections], 2)))
        for i in range(num_selections)
    ]

    for i, control_index in enumerate(control_indices):
        if (
            qml.math.is_abstract(theta)
            or qml.math.requires_grad(theta)
            or qml.math.all(theta[..., i] != 0.0)
        ):
            op_list.append(gate(theta[..., i], wires=[target_wire]))
        op_list.append(qml.CNOT(wires=[control_wires[control_index], target_wire]))
    return op_list


def _get_alpha_z(omega, n, k):
    r"""Computes the rotation angles required to implement the uniformly-controlled Z rotation
    applied to the :math:`k`th qubit.

    The :math:`j`th angle is related to the phases omega of the desired amplitudes via:

    .. math:: \alpha^{z,k}_j = \sum_{l=1}^{2^{k-1}} \frac{\omega_{(2j-1) 2^{k-1}+l} - \omega_{(2j-2) 2^{k-1}+l}}{2^{k-1}}

    Args:
        omega (tensor_like): phases of the state to prepare
        n (int): total number of qubits for the uniformly-controlled rotation
        k (int): index of current qubit

    Returns:
        array representing :math:`\alpha^{z,k}`
    """
    indices1 = [
        [(2 * j - 1) * 2 ** (k - 1) + l - 1 for l in range(1, 2 ** (k - 1) + 1)]
        for j in range(1, 2 ** (n - k) + 1)
    ]
    indices2 = [
        [(2 * j - 2) * 2 ** (k - 1) + l - 1 for l in range(1, 2 ** (k - 1) + 1)]
        for j in range(1, 2 ** (n - k) + 1)
    ]

    term1 = qml.math.take(omega, indices=indices1, axis=-1)
    term2 = qml.math.take(omega, indices=indices2, axis=-1)
    diff = (term1 - term2) / 2 ** (k - 1)

    return qml.math.sum(diff, axis=-1)


def _get_alpha_y(a, n, k):
    r"""Computes the rotation angles required to implement the uniformly controlled Y rotation
    applied to the :math:`k`th qubit.

    The :math:`j`-th angle is related to the absolute values, a, of the desired amplitudes via:

    .. math:: \alpha^{y,k}_j = 2 \arcsin \sqrt{ \frac{ \sum_{l=1}^{2^{k-1}} a_{(2j-1)2^{k-1} +l}^2  }{ \sum_{l=1}^{2^{k}} a_{(j-1)2^{k} +l}^2  } }

    Args:
        a (tensor_like): absolute values of the state to prepare
        n (int): total number of qubits for the uniformly-controlled rotation
        k (int): index of current qubit

    Returns:
        array representing :math:`\alpha^{y,k}`
    """
    indices_numerator = [
        [(2 * (j + 1) - 1) * 2 ** (k - 1) + l for l in range(2 ** (k - 1))]
        for j in range(2 ** (n - k))
    ]
    numerator = qml.math.take(a, indices=indices_numerator, axis=-1)
    numerator = qml.math.sum(qml.math.abs(numerator) ** 2, axis=-1)

    indices_denominator = [[j * 2**k + l for l in range(2**k)] for j in range(2 ** (n - k))]
    denominator = qml.math.take(a, indices=indices_denominator, axis=-1)
    denominator = qml.math.sum(qml.math.abs(denominator) ** 2, axis=-1)

    # Divide only where denominator is zero, else leave initial value of zero.
    # The equation guarantees that the numerator is also zero in the corresponding entries.

    with np.errstate(divide="ignore", invalid="ignore"):
        division = numerator / denominator

    # Cast the numerator and denominator to ensure compatibility with interfaces
    division = qml.math.cast(division, np.float64)
    denominator = qml.math.cast(denominator, np.float64)

    division = qml.math.where(denominator != 0.0, division, 0.0)

    return 2 * qml.math.arcsin(qml.math.sqrt(division))


[docs]class MottonenStatePreparation(Operation): r""" Prepares an arbitrary state on the given wires using a decomposition into gates developed by `Möttönen et al. (2004) <https://arxiv.org/abs/quant-ph/0407010>`_. The state is prepared via a sequence of uniformly controlled rotations. A uniformly controlled rotation on a target qubit is composed from all possible controlled rotations on the qubit and can be used to address individual elements of the state vector. In the work of Möttönen et al., inverse state preparation is executed by first equalizing the phases of the state vector via uniformly controlled Z rotations, and then rotating the now real state vector into the direction of the state :math:`|0\rangle` via uniformly controlled Y rotations. This code is adapted from code written by Carsten Blank for PennyLane-Qiskit. .. warning:: Due to non-trivial classical processing of the state vector, this template is not always fully differentiable. Args: state_vector (tensor_like): Input array of shape ``(2^n,)``, where ``n`` is the number of wires the state preparation acts on. The input array must be normalized. wires (Iterable): wires that the template acts on Example: ``MottonenStatePreparation`` creates any arbitrary state on the given wires depending on the input state vector. .. code-block:: python dev = qml.device('default.qubit', wires=3) @qml.qnode(dev) def circuit(state): qml.MottonenStatePreparation(state_vector=state, wires=range(3)) return qml.state() state = np.array([1, 2j, 3, 4j, 5, 6j, 7, 8j]) state = state / np.linalg.norm(state) print(qml.draw(circuit, level="device", max_length=80)(state)) .. code-block:: 0: ──RY(2.35)─╭●───────────╭●──────────────╭●────────────────────────╭● 1: ──RY(2.09)─╰X──RY(0.21)─╰X─╭●───────────│────────────╭●───────────│─ 2: ──RY(1.88)─────────────────╰X──RY(0.10)─╰X──RY(0.08)─╰X──RY(0.15)─╰X ──╭●────────╭●────╭●────╭●─╭GlobalPhase(-0.79)─┤ ╭State ──╰X────────╰X─╭●─│──╭●─│──├GlobalPhase(-0.79)─┤ ├State ───RZ(1.57)────╰X─╰X─╰X─╰X─╰GlobalPhase(-0.79)─┤ ╰State The state preparation can be checked by running: >>> print(np.allclose(state, circuit(state))) True """ num_wires = AnyWires grad_method = None ndim_params = (1,) def __init__(self, state_vector, wires, id=None): # check if the `state_vector` param is batched batched = len(qml.math.shape(state_vector)) > 1 state_batch = state_vector if batched else [state_vector] # apply checks to each state vector in the batch for i, state in enumerate(state_batch): shape = qml.math.shape(state) if len(shape) != 1: raise ValueError( f"State vectors must be one-dimensional; vector {i} has shape {shape}." ) n_amplitudes = shape[0] if n_amplitudes != 2 ** len(qml.wires.Wires(wires)): raise ValueError( f"State vectors must be of length {2 ** len(wires)} or less; vector {i} has length {n_amplitudes}." ) if not qml.math.is_abstract(state): norm = qml.math.sum(qml.math.abs(state) ** 2) if not qml.math.allclose(norm, 1.0, atol=1e-3): raise ValueError( f"State vectors have to be of norm 1.0, vector {i} has squared norm {norm}" ) super().__init__(state_vector, wires=wires, id=id) @property def num_params(self): return 1
[docs] @staticmethod def compute_decomposition(state_vector, wires): # pylint: disable=arguments-differ r"""Representation of the operator as a product of other operators. .. math:: O = O_1 O_2 \dots O_n. .. seealso:: :meth:`~.MottonenStatePreparation.decomposition`. Args: state_vector (tensor_like): Normalized state vector of shape ``(2^len(wires),)`` wires (Any or Iterable[Any]): wires that the operator acts on Returns: list[.Operator]: decomposition of the operator **Example** >>> state_vector = torch.tensor([0.5, 0.5, 0.5, 0.5]) >>> qml.MottonenStatePreparation.compute_decomposition(state_vector, wires=["a", "b"]) [RY(array(1.57079633), wires=['a']), RY(array(1.57079633), wires=['b']), CNOT(wires=['a', 'b']), CNOT(wires=['a', 'b'])] """ if len(qml.math.shape(state_vector)) > 1: raise ValueError( "Broadcasting with MottonenStatePreparation is not supported. Please use the " "qml.transforms.broadcast_expand transform to use broadcasting with " "MottonenStatePreparation." ) a = qml.math.abs(state_vector) omega = qml.math.angle(state_vector) # change ordering of wires, since original code # was written for IBM machines wires_reverse = wires[::-1] op_list = [] # Apply inverse y rotation cascade to prepare correct absolute values of amplitudes for k in range(len(wires_reverse), 0, -1): alpha_y_k = _get_alpha_y(a, len(wires_reverse), k) control = wires_reverse[k:] target = wires_reverse[k - 1] op_list.extend(_apply_uniform_rotation_dagger(qml.RY, alpha_y_k, control, target)) # If necessary, apply inverse z rotation cascade to prepare correct phases of amplitudes if ( qml.math.is_abstract(omega) or qml.math.requires_grad(omega) or not qml.math.allclose(omega, 0) ): for k in range(len(wires_reverse), 0, -1): alpha_z_k = _get_alpha_z(omega, len(wires_reverse), k) control = wires_reverse[k:] target = wires_reverse[k - 1] if len(alpha_z_k) > 0: op_list.extend( _apply_uniform_rotation_dagger(qml.RZ, alpha_z_k, control, target) ) global_phase = qml.math.sum(-1 * qml.math.angle(state_vector) / len(state_vector)) op_list.extend([qml.GlobalPhase(global_phase, wires=wires)]) return op_list