import numpy as np
import scipy.stats
from qibo import Circuit, gates
from qibo.config import raise_error
[docs]class IQAE:
"""Model that performs the Iterative Quantum Amplitude Estimation algorithm.
The implemented class in this code utilizes the Iterative Quantum Amplitude
Estimation (IQAE) algorithm, which was proposed in `arxiv:1912.05559
<https://arxiv.org/abs/1912.05559>`_. The algorithm provides an estimated
output that, with a probability ``alpha``, differs from the target value by
``epsilon``. Both ``alpha`` and ``epsilon`` can be specified.
Unlike Brassard's original QAE algorithm `arxiv:quant-ph/0005055
<http://arxiv.org/abs/quant-ph/0005055>`_, this implementation does not rely
on Quantum Phase Estimation but instead is based solely on Grover's
algorithm. The IQAE algorithm employs a series of carefully selected Grover
iterations to determine an estimate for the target amplitude.
Args:
circuit_a (:class:`qibo.models.circuit.Circuit`): quantum circuit that
specifies the QAE problem.
circuit_q (:class:`qibo.models.circuit.Circuit`): quantum circuit of the
Grover/Amplification operator.
alpha (float): confidence level, the target probability is 1 - ``alpha``,
has values between 0 and 1.
epsilon (float): target precision for estimation target `a`, has values
between 0 and 0.5.
method (str): statistical method used to estimate the confidence
intervals in each iteration, can be either `chernoff` (default) for
the Chernoff intervals or `beta` for the Clopper-Pearson intervals.
n_shots (int): number of shots.
Raises:
ValueError: If ``epsilon`` is not in (0, 0.5].
ValueError: If ``alpha`` is not in (0, 1).
ValueError: If ``method`` is not supported.
ValueError: If the number of qubits in ``circuit_a`` is greater than in ``circuit_q``.
Example:
.. testcode::
from qibo import Circuit, gates
from qibo.models.iqae import IQAE
# Defining circuit A to integrate sin(x)^2 from [0,1]
a_circuit = Circuit(2)
a_circuit.add(gates.H(0))
a_circuit.add(gates.RY(q = 1, theta = 1 / 2))
a_circuit.add(gates.CU3(0, 1, 1, 0, 0))
# Defining circuit Q = -A S_0 A^-1 S_X
q_circuit = Circuit(2)
# S_X
q_circuit.add(gates.Z(q = 1))
# A^-1
q_circuit = q_circuit + a_circuit.invert()
# S_0
q_circuit.add(gates.X(0))
q_circuit.add(gates.X(1))
q_circuit.add(gates.CZ(0, 1))
# A
q_circuit = q_circuit + a_circuit
# Executing IQAE and obtaining the result
iae = IQAE(a_circuit, q_circuit)
results = iae.execute()
integral_value = results.estimation
integral_error = results.epsilon_estimated
"""
def __init__(
self,
circuit_a,
circuit_q,
alpha=0.05,
epsilon=0.005,
n_shots=1024,
method="chernoff",
):
self.circuit_a = circuit_a
self.circuit_q = circuit_q
if circuit_a.nqubits > circuit_q.nqubits:
raise_error(
ValueError,
"The number of qubits for Q must be greater or equal than the number"
"of qubits of A.",
)
if not isinstance(n_shots, int):
raise_error(
ValueError,
"The number of shots must be an integer number.",
)
# validate ranges of input arguments
if not 0 < epsilon <= 0.5:
raise_error(ValueError, f"Epsilon must be in (0, 0.5], but is {epsilon}.")
if not 0 < alpha < 1:
raise_error(
ValueError,
f"The confidence level alpha must be in (0, 1), but is {alpha}.",
)
if method not in {"chernoff", "beta"}:
raise_error(
ValueError,
f"The confidence interval method must be chernoff or beta, but is {method}.",
)
self.alpha = alpha
self.epsilon = epsilon
self.n_shots = n_shots
self.method = method
[docs] def construct_qae_circuit(self, k):
"""Generates quantum circuit for QAE.
Args:
k (int): number of times the amplification operator ``circuit_q`` is applied.
Returns:
The quantum circuit of the QAE algorithm.
"""
initialization_circuit_a = self.circuit_a
amplification_circuit_q = self.circuit_q
qc = Circuit(amplification_circuit_q.nqubits)
qc.add(
initialization_circuit_a.on_qubits(
*range(0, initialization_circuit_a.nqubits, 1)
)
)
for i in range(k):
qc = qc + amplification_circuit_q
qc.add(gates.M(initialization_circuit_a.nqubits - 1))
return qc
[docs] def clopper_pearson(self, count, n, alpha):
"""Calculates the confidence interval for the quantity to estimate `a`.
Args:
count (int): number of successes.
n (int): total number of trials.
alpha (float): significance level. Must be in (0, 0.5).
Return:
The confidence interval [a_min, a_max].
"""
beta_prob_function = scipy.stats.beta.ppf
a_min = beta_prob_function(alpha / 2, count, n - count + 1)
a_max = beta_prob_function(1 - alpha / 2, count + 1, n - count)
if np.isnan(a_min):
a_min = 0
if np.isnan(a_max):
a_max = 1
return a_min, a_max
[docs] def h_calc_CP(self, n_successes, n_total_shots, upper_bound_t):
"""Calculates the `h` function.
Args:
n_successes (int): number of successes.
n_total_shots (int): total number of trials.
upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
Returns:
The h function for the given inputs.
"""
a_min, a_max = self.clopper_pearson(
n_successes, n_total_shots, alpha=(self.alpha / upper_bound_t)
)
return np.abs(np.arccos(1 - 2 * a_max) - np.arccos(1 - 2 * a_min)) / 2
[docs] def calc_L_range_CP(self, n_shots, upper_bound_t):
"""Calculate the confidence interval for the Clopper-Pearson method.
Args:
n_shots (int): number of shots.
upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
Returns:
max_L, min_L (float, float): The maximum and minimum possible error which could be returned
on a given iteration.
"""
x = np.linspace(0, np.pi, 10000)
x_domain = [x <= 1.0 + 1 / 10 / n_shots]
y = [
self.h_calc_CP(int(t * n_shots), n_shots, upper_bound_t)
for t in x[tuple(x_domain)]
]
max_L = np.max(y) / (2 * np.pi)
min_L = np.min(y) / (2 * np.pi)
return max_L, min_L
[docs] def calc_L_range_CH(self, n_shots, upper_bound_t):
"""Calculate the confidence interval for the Chernoff method.
Args:
n_shots (int): number of shots.
upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
Returns:
max_L, min_L (float, float): The maximum and minimum possible error which could be returned
on a given iteration.
"""
max_L = (
np.arcsin(
(2 / (n_shots) * np.log(2 * upper_bound_t / self.alpha)) ** (1 / 4)
)
/ 2
/ np.pi
)
min_L = np.arcsin(np.sin(max_L) ** 2)
return max_L, min_L
[docs] def find_next_k(self, uppercase_k_i, up_i, theta_l, theta_u, r=2):
r"""Find the largest integer ``uppercase_k`` such that the interval ``uppercase_k`` * [ ``theta_l`` , ``theta_u`` ]
lies completely in [0, `\pi`] or [`\pi`, 2 `\pi`].
Args:
uppercase_k_i (int): the current ``uppercase_k`` such ``uppercase_k`` = 4 ``k`` + 2,
where ``k`` is the power of the operator ``circuit_q``.
up_i (bool): boolean flag of whether theta_interval lies in the
upper half-circle [0, `\pi`] or in the lower one [`\pi`, 2 `\pi`].
theta_l (float): the current lower limit of the confidence interval for the angle theta.
theta_u (float): the current upper limit of the confidence interval for the angle theta.
r (int): lower bound for ``uppercase_k``.
Returns:
The next power `K_i`, and boolean flag for the extrapolated interval.
"""
uppercase_k_max = int(1 / (2 * (theta_u - theta_l)))
uppercase_k = uppercase_k_max - (uppercase_k_max - 2) % 4
while uppercase_k >= r * uppercase_k_i:
theta_min = uppercase_k * theta_l - int(uppercase_k * theta_l)
theta_max = uppercase_k * theta_u - int(uppercase_k * theta_u)
if int(uppercase_k * theta_u) == int(uppercase_k * theta_l):
if theta_max <= 1 / 2 and theta_min <= 1 / 2:
up = True
return (uppercase_k, up)
elif theta_max >= 1 / 2 and theta_min >= 1 / 2:
up = False
return (uppercase_k, up)
uppercase_k -= 4
return (uppercase_k_i, up_i)
[docs] def execute(self, backend=None):
"""Execute IQAE algorithm.
Args:
backend: the qibo backend.
Returns:
A :class:`qibo.models.iqae.IterativeAmplitudeEstimationResult` results object.
"""
from qibo.backends import _check_backend
backend = _check_backend(backend)
# Initializing all parameters
k = [0]
# uppercase_k=4k+2
uppercase_k = [2]
theta_u = 1 / 4
theta_l = 0
theta_intervals = [theta_l, theta_u]
a_intervals = [
np.sin(2 * np.pi * theta_l) ** 2,
np.sin(2 * np.pi * theta_u) ** 2,
]
a_min = [0]
a_max = [1]
theta_dif = np.abs(theta_u - theta_l)
up = [True]
samples_history = []
n_shots_history = []
eps = self.epsilon / (2 * np.pi)
upper_bound_t = int(np.log2(np.pi / (8 * self.epsilon))) + 1
n_total_shots = self.n_shots
num_oracle_queries = 0
if self.method == "chernoff":
# Chernoff method
max_L, min_L = self.calc_L_range_CH(n_total_shots, upper_bound_t)
else:
# Clopper-Pearson (beta) method
max_L, min_L = self.calc_L_range_CP(n_total_shots, upper_bound_t)
i = 0
while theta_dif > 2 * eps:
i = i + 1
uppercase_k_i, up_i = self.find_next_k(
uppercase_k[-1], up[-1], theta_l, theta_u
)
k_i = int((uppercase_k_i - 2) / 4)
uppercase_k.append(uppercase_k_i)
up.append(up_i)
k.append(k_i)
# Checking the no-overshooting condition
if uppercase_k_i > int(max_L / eps):
# We ensure to not make unnecessary measurement shots at last iterations of the algorithm
n_shots_i = int((max_L / eps) * self.n_shots / uppercase_k_i / 10)
# To avoid having a null number of shots
if n_shots_i == 0:
n_shots_i = 1
else:
n_shots_i = self.n_shots
# Calling and executing the quantum circuit
qc = self.construct_qae_circuit(k_i)
samples = backend.execute_circuit(qc, nshots=n_shots_i).frequencies(
binary=True
)["1"]
samples_history.append(samples)
n_shots_history.append(n_shots_i)
num_oracle_queries += n_shots_i * k_i
m = 1
if i > 1:
while uppercase_k[i - m] == uppercase_k[i] and i >= m + 1:
m += 1
sum_total_samples = sum([samples_history[j] for j in range(i - m, i)])
n_total_shots = sum([n_shots_history[j] for j in range(i - m, i)])
else:
sum_total_samples = samples
n_total_shots = n_shots_i
a = sum_total_samples / n_total_shots
if self.method == "chernoff":
delta_a = np.sqrt(
np.log(2 * upper_bound_t / self.alpha) / 2 / n_total_shots
)
a_min_i = max(0, a - delta_a)
a_max_i = min(1, a + delta_a)
else:
a_min_i, a_max_i = self.clopper_pearson(
sum_total_samples, n_total_shots, alpha=self.alpha / upper_bound_t
)
a_min.append(a_min_i)
a_max.append(a_max_i)
if up_i:
theta_min_i = np.arccos(1 - 2 * a_min_i) / 2 / np.pi
theta_max_i = np.arccos(1 - 2 * a_max_i) / 2 / np.pi
else:
theta_min_i = 1 - np.arccos(1 - 2 * a_max_i) / 2 / np.pi
theta_max_i = 1 - np.arccos(1 - 2 * a_min_i) / 2 / np.pi
theta = min(int(uppercase_k_i * theta_u), int(uppercase_k_i * theta_l))
theta_u = (theta + theta_max_i) / uppercase_k_i
theta_l = (theta + theta_min_i) / uppercase_k_i
theta_dif = np.abs(theta_u - theta_l)
theta_intervals.append([theta_l, theta_u])
a_l = np.sin(2 * np.pi * theta_l) ** 2
a_u = np.sin(2 * np.pi * theta_u) ** 2
a_intervals.append([a_l, a_u])
result = IterativeAmplitudeEstimationResult()
result.alpha = self.alpha
result.epsilon_target = self.epsilon
result.epsilon_estimated = (a_u - a_l) / 2
result.estimate_intervals = a_intervals
result.num_oracle_queries = num_oracle_queries
result.estimation = (a_u + a_l) / 2
result.theta_intervals = theta_intervals
result.k_list = k
result.ratios = samples_history
result.shots = n_shots_history
return result
[docs]class IterativeAmplitudeEstimationResult:
"""The ``IterativeAmplitudeEstimationResult`` result object."""
def __init__(self):
self._alpha = None
self._epsilon_target = None
self._epsilon_estimated = None
self._num_oracle_queries = None
self._estimation = None
self._estimate_intervals = None
self._theta_intervals = None
self._k_list = None
self._ratios = None
self._shots = None