Source code for pennylane.templates.subroutines.fable
# Copyright 2018-2024 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 template for the Fast Approximate BLock Encoding (FABLE) technique.
"""
# pylint: disable=no-value-for-parameter
import warnings
from collections import Counter
import numpy as np
from pennylane import math
from pennylane.control_flow import for_loop
from pennylane.decomposition import add_decomps, register_resources, resource_rep
from pennylane.operation import Operation
from pennylane.ops import CNOT, RY, SWAP, Hadamard, cond
from pennylane.templates.state_preparations.mottonen import compute_theta, gray_code
from pennylane.wires import Wires
[docs]
class FABLE(Operation):
r"""
Construct a unitary with the fast approximate block encoding method.
The FABLE method allows to simplify block encoding circuits without reducing accuracy,
for matrices of specific structure [`arXiv:2205.00081 <https://arxiv.org/abs/2205.00081>`_].
Args:
input_matrix (tensor_like): a :math:`(2^n \times 2^n)` matrix to be encoded,
where :math:`n` is an integer
wires (Iterable[int, str], Wires): the wires the operation acts on. The number of wires can
be computed as :math:`(2 \times n + 1)`.
tol (float): rotation gates that have an angle value smaller than this tolerance are removed
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:
.. code-block:: python
input_matrix = np.array([[0.1, 0.2],[0.3, -0.2]])
dev = qml.device('default.qubit', wires=3)
@qml.qnode(dev)
def example_circuit():
qml.FABLE(input_matrix, wires=range(3), tol=0)
return qml.state()
We can see that the input matrix has been block encoded in the matrix of the circuit:
>>> s = int(np.ceil(np.log2(max(len(input_matrix), len(input_matrix[0])))))
>>> expected = 2**s * qml.matrix(example_circuit)().real[0 : 2**s, 0 : 2**s]
>>> print(f"Block-encoded matrix:\n{expected}")
Block-encoded matrix:
[[0.1 0.2]
[0.3 -0.2]]
.. note::
FABLE can be implemented for matrices of arbitrary shape and size.
When given a :math:`(N \times M)` matrix, the matrix is padded with zeroes
until it is of :math:`(N \times N)` dimension, where :math:`N` is equal to :math:`2^n`,
and :math:`n` is an integer. It is also assumed that the values
of the input matrix are within :math:`[-1, 1]`. Apply a subnormalization factor if needed.
"""
num_params = 1
"""int: Number of trainable parameters that the operator depends on."""
grad_method = None
"""Gradient computation method."""
resource_keys = {"wires", "thetas", "control_wires", "tol"}
def __init__(self, input_matrix, wires, tol=0, id=None):
wires = Wires(wires)
if not math.is_abstract(input_matrix):
if math.any(math.iscomplex(input_matrix)):
raise ValueError("Support for imaginary values has not been implemented.")
alpha = math.linalg.norm(math.ravel(input_matrix), np.inf)
if alpha > 1:
raise ValueError(
"The subnormalization factor should be lower than 1."
+ "Ensure that the values of the input matrix are within [-1, 1]."
)
else:
if tol != 0:
raise ValueError(
"JIT is not supported for tolerance values greater than 0. Set tol = 0 to run."
)
row, col = math.shape(input_matrix)
if row != col:
warnings.warn(
f"The input matrix should be of shape NxN, got {input_matrix.shape}."
+ "Zeroes were padded automatically."
)
dimension = max(row, col)
input_matrix = math.pad(input_matrix, ((0, dimension - row), (0, dimension - col)))
row, col = math.shape(input_matrix)
n = int(math.ceil(math.log2(col)))
if n == 0: ### For edge case where someone puts a 1x1 array.
n = 1
if col < 2**n:
input_matrix = math.pad(input_matrix, ((0, 2**n - col), (0, 2**n - col)))
col = 2**n
warnings.warn(
"The input matrix should be of shape NxN, where N is a power of 2."
+ f"Zeroes were padded automatically. Input is now of shape {input_matrix.shape}."
)
if len(wires) != 2 * n + 1:
raise ValueError(f"Number of wires is incorrect, expected {2*n+1} but got {len(wires)}")
self._hyperparameters = {"tol": tol}
super().__init__(input_matrix, wires=wires, id=id)
@property
def resource_params(self) -> dict:
code = gray_code(int(2 * math.log2(len(self.parameters[0]))))
control_wires = np.log2(code ^ np.roll(code, -1)).astype(int)
alphas = math.arccos(self.parameters).flatten()
thetas = compute_theta(alphas)
return {
"wires": self.wires,
"thetas": thetas,
"control_wires": control_wires,
"tol": self.hyperparameters["tol"],
}
[docs]
@staticmethod
def compute_decomposition(input_matrix, wires, tol=0): # pylint:disable=arguments-differ
r"""Sequence of gates that represents the efficient circuit produced by the FABLE technique
Args:
input_matrix (tensor_like): an :math:`(N \times N)` matrix to be encoded
wires (Any or Iterable[Any]): wires that the operator acts on
tol (float): rotation gates that have an angle value smaller than this tolerance are removed
Returns:
list[.Operator]: list of gates for efficient circuit
"""
alphas = math.arccos(input_matrix).flatten()
thetas = compute_theta(alphas)
auxiliary = [wires[0]]
wires_i = wires[1 : 1 + len(wires) // 2][::-1]
wires_j = wires[1 + len(wires) // 2 : len(wires)][::-1]
code = gray_code(int(2 * math.log2(len(input_matrix))))
control_wires = np.log2(code ^ np.roll(code, -1)).astype(int)
wire_map = dict(enumerate(wires_j + wires_i))
op_list = [Hadamard(w) for w in wires_i]
nots = {}
for theta, control_index in zip(thetas, control_wires):
if math.is_abstract(theta):
for c_wire in nots:
op_list.append(CNOT(wires=[c_wire] + auxiliary))
op_list.append(RY(2 * theta, wires=auxiliary))
nots = {}
nots[wire_map[control_index]] = 1
continue
if math.abs(2 * theta) > tol:
for c_wire in nots:
op_list.append(CNOT(wires=[c_wire] + auxiliary))
op_list.append(RY(2 * theta, wires=auxiliary))
nots = {}
if wire_map[control_index] in nots:
del nots[wire_map[control_index]]
else:
nots[wire_map[control_index]] = 1
for c_wire in nots:
op_list.append(CNOT([c_wire] + auxiliary))
for w_i, w_j in zip(wires_i, wires_j):
op_list.append(SWAP(wires=[w_i, w_j]))
for w in wires_i:
op_list.append(Hadamard(w))
return op_list
def _fable_resources(wires, thetas, control_wires, tol):
resources = Counter({resource_rep(Hadamard): len(wires) // 2 * 2})
wires_i = wires[1 : 1 + len(wires) // 2][::-1]
wires_j = wires[1 + len(wires) // 2 : len(wires)][::-1]
wire_map = dict(enumerate(wires_j + wires_i))
nots = {}
for theta, control_index in zip(thetas, control_wires):
if math.abs(2 * theta) > tol:
for _ in nots:
resources[resource_rep(CNOT)] += 1
resources[resource_rep(RY)] += 1
nots = {}
if wire_map[control_index] in nots:
del nots[wire_map[control_index]]
else:
nots[wire_map[control_index]] = 1
for _ in range(len(nots.keys())):
resources[resource_rep(CNOT)] += 1
resources[resource_rep(SWAP)] = min(len(wires_i), len(wires_j))
return dict(resources)
@register_resources(_fable_resources)
def _fable_decomposition(input_matrix, wires, tol=0):
alphas = math.arccos(input_matrix).flatten()
thetas = compute_theta(alphas)
auxilliary = [wires[0]]
wires_i = wires[1 : 1 + len(wires) // 2][::-1]
wires_j = wires[1 + len(wires) // 2 : len(wires)][::-1]
code = gray_code(int(2 * math.log2(len(input_matrix))))
control_wires = np.log2(code ^ np.roll(code, -1)).astype(int)
wire_map = dict(enumerate(wires_j + wires_i))
@for_loop(len(wires_i))
def wires_i_hadamard_loop(i):
w = wires_i[i]
Hadamard(w)
wires_i_hadamard_loop()
nots = {}
@for_loop(min(len(thetas), len(control_wires)))
def theta_controls_loop(j, nots):
theta = thetas[j]
control_index = control_wires[j]
def in_tol_branch(nots):
@for_loop(len(nots.keys()))
def concrete_nots_loop(l, nots):
c_wire = list(nots.keys())[l]
CNOT(wires=[c_wire] + auxilliary)
return nots
concrete_nots_loop(nots)
RY(2 * theta, wires=auxilliary)
nots = {}
return nots
def trivial_branch(nots):
return nots
nots = cond(math.abs(2 * theta) > tol, in_tol_branch, trivial_branch)(nots)
def del_branch(nots):
del nots[wire_map[control_index]]
return nots
def add_branch(nots):
nots[wire_map[control_index]] = 1
return nots
nots = cond(wire_map[control_index] in nots, del_branch, add_branch)(nots)
return nots
nots = theta_controls_loop(nots)
@for_loop(len(nots.keys()))
def outer_nots_loop(m, nots):
c_wire = list(nots.keys())[m]
CNOT([c_wire] + auxilliary)
outer_nots_loop(nots)
@for_loop(min(len(wires_i), len(wires_j)))
def swap_loop(n):
w_i = wires_i[n]
w_j = wires_j[n]
SWAP(wires=[w_i, w_j])
swap_loop()
wires_i_hadamard_loop()
add_decomps(FABLE, _fable_decomposition)
_modules/pennylane/templates/subroutines/fable
Download Python script
Download Notebook
View on GitHub