herutriana44's picture
First Commit
b7d9967 verified
raw
history blame contribute delete
No virus
7.4 kB
# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2020.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Utils for the Maximum-Likelihood estimation used in ``AmplitudeEstimation``."""
import logging
import numpy as np
logger = logging.getLogger(__name__)
# pylint: disable=invalid-name
def bisect_max(f, a, b, steps=50, minwidth=1e-12, retval=False):
"""Find the maximum of the real-valued function f in the interval [a, b] using bisection.
Args:
f (callable): the function to find the maximum of
a (float): the lower limit of the interval
b (float): the upper limit of the interval
steps (int): the maximum number of steps in the bisection
minwidth (float): if the current interval is smaller than minwidth stop
the search
retval (bool): return value
Returns:
float: The maximum of f in [a,b] according to this algorithm.
"""
it = 0
m = (a + b) / 2
fm = 0
while it < steps and b - a > minwidth:
l, r = (a + m) / 2, (m + b) / 2
fl, fm, fr = f(l), f(m), f(r)
# fl is the maximum
if fl > fm and fl > fr:
b = m
m = l
# fr is the maximum
elif fr > fm and fr > fl:
a = m
m = r
# fm is the maximum
else:
a = l
b = r
it += 1
if it == steps:
logger.warning("-- Warning, bisect_max didn't converge after %s steps", steps)
if retval:
return m, fm
return m
def _circ_dist(x, p):
r"""Circumferential distance function.
For two angles :math:`x` and :math:`p` on the unit circuit this function is defined as
.. math::
d(x, p) = \min_{z \in [-1, 0, 1]} |z + p - x|
Args:
x (float): first angle
p (float): second angle
Returns:
float: d(x, p)
"""
t = p - x
# Since x and p \in [0,1] it suffices to check not all integers
# but only -1, 0 and 1
z = np.array([-1, 0, 1])
if hasattr(t, "__len__"):
d = np.empty_like(t)
for idx, ti in enumerate(t):
d[idx] = np.min(np.abs(z + ti))
return d
return np.min(np.abs(z + t))
def _derivative_circ_dist(x, p):
"""Derivative of circumferential distance function.
Args:
x (float): first angle
p (float): second angle
Returns:
float: The derivative.
"""
# pylint: disable=chained-comparison,misplaced-comparison-constant
t = p - x
if t < -0.5 or (0 < t and t < 0.5):
return -1
if t > 0.5 or (-0.5 < t and t < 0):
return 1
return 0
def _amplitude_to_angle(a):
r"""Transform from the amplitude :math:`a \in [0, 1]` to the generating angle.
In QAE, the amplitude can be written from a generating angle :math:`\omega` as
.. math:
a = \sin^2(\pi \omega)
This returns the :math:`\omega` for a given :math:`a`.
Args:
a (float): A value in :math:`[0,1]`.
Returns:
float: :math:`\sin^{-1}(\sqrt{a}) / \pi`
"""
return np.arcsin(np.sqrt(a)) / np.pi
def _derivative_amplitude_to_angle(a):
"""Compute the derivative of ``amplitude_to_angle``."""
return 1 / (2 * np.pi * np.sqrt((1 - a) * a))
def _alpha(x, p):
"""Helper function for `pdf_a`, alpha = pi * d(omega(x), omega(p)).
Here, omega(x) is `_amplitude_to_angle(x)`.
"""
omega = _amplitude_to_angle
return np.pi * _circ_dist(omega(x), omega(p))
def _derivative_alpha(x, p):
"""Compute the derivative of alpha."""
omega = _amplitude_to_angle
d_omega = _derivative_amplitude_to_angle
return np.pi * _derivative_circ_dist(omega(x), omega(p)) * d_omega(p)
def _beta(x, p):
"""Helper function for `pdf_a`, beta = pi * d(1 - omega(x), omega(p))."""
omega = _amplitude_to_angle
return np.pi * _circ_dist(1 - omega(x), omega(p))
def _derivative_beta(x, p):
"""Compute the derivative of beta."""
omega = _amplitude_to_angle
d_omega = _derivative_amplitude_to_angle
return np.pi * _derivative_circ_dist(1 - omega(x), omega(p)) * d_omega(p)
def _pdf_a_single_angle(x, p, m, pi_delta):
"""Helper function for `pdf_a`."""
M = 2**m
d = pi_delta(x, p)
res = np.sin(M * d) ** 2 / (M * np.sin(d)) ** 2 if d != 0 else 1
return res
def pdf_a(x, p, m):
"""
Return the PDF of a, i.e. the probability of getting the estimate x
(in [0, 1]) if p (in [0, 1]) is the true value, given that we use m qubits.
Args:
x (float): the grid point
p (float): the true value
m (float): the number of evaluation qubits
Returns:
float: PDF(x|p)
"""
# We'll use list comprehension, so the input should be a list
scalar = False
if not hasattr(x, "__len__"):
scalar = True
x = np.asarray([x])
# Compute the probabilities: Add up both angles that produce the given
# value, except for the angles 0 and 0.5, which map to the unique a-values,
# 0 and 1, respectively
pr = np.array(
[
_pdf_a_single_angle(xi, p, m, _alpha) + _pdf_a_single_angle(xi, p, m, _beta)
if (xi not in [0, 1])
else _pdf_a_single_angle(xi, p, m, _alpha)
for xi in x
]
).flatten()
# If is was a scalar return scalar otherwise the array
return pr[0] if scalar else pr
def derivative_log_pdf_a(x, p, m):
"""
Return the derivative of the logarithm of the PDF of a.
Args:
x (float): the grid point
p (float): the true value
m (float): the number of evaluation qubits
Returns:
float: d/dp log(PDF(x|p))
"""
M = 2**m
if x not in [0, 1]:
num_p1 = 0
for A, dA, B, dB in zip(
[_alpha, _beta],
[_derivative_alpha, _derivative_beta],
[_beta, _alpha],
[_derivative_beta, _derivative_alpha],
):
num_p1 += 2 * M * np.sin(M * A(x, p)) * np.cos(M * A(x, p)) * dA(x, p) * np.sin(
B(x, p)
) ** 2 + 2 * np.sin(M * A(x, p)) ** 2 * np.sin(B(x, p)) * np.cos(B(x, p)) * dB(x, p)
den_p1 = (
np.sin(M * _alpha(x, p)) ** 2 * np.sin(_beta(x, p)) ** 2
+ np.sin(M * _beta(x, p)) ** 2 * np.sin(_alpha(x, p)) ** 2
)
num_p2 = 0
for A, dA, B in zip(
[_alpha, _beta], [_derivative_alpha, _derivative_beta], [_beta, _alpha]
):
num_p2 += 2 * np.cos(A(x, p)) * dA(x, p) * np.sin(B(x, p))
den_p2 = np.sin(_alpha(x, p)) * np.sin(_beta(x, p))
return num_p1 / den_p1 - num_p2 / den_p2
return 2 * _derivative_alpha(x, p) * (M / np.tan(M * _alpha(x, p)) - 1 / np.tan(_alpha(x, p)))