Quantum Amplitude Amplification with Amazon Braket
Overview
# --- Setup cell added by QCR (not part of the original tutorial) ---
# Source: amazon-braket/amazon-braket-examples @ 0c0818f315479aab9deebed7e7ed7533ac581923, Apache License 2.0.
# Installs the example's dependencies. If a later cell still reports a missing
# package, restart the runtime/kernel and run again from the top.
%pip install -q amazon-braket-sdk==1.117.3 matplotlib
QUANTUM AMPLITUDE AMPLIFICATION
In this tutorial, we provide a detailed discussion and implementation of the Quantum Amplitude Amplification (QAA) algorithm using the Amazon Braket SDK. QAA is a routine in quantum computing which generalizes the idea behind Grover's famous search algorithm, with applications across many quantum algorithms. In short, QAA uses an iterative approach to systematically increase the probability of finding one or multiple target states in a given search space. In a quantum computer, QAA can be used to obtain a quadratic speedup over several classical algorithms [1].
TECHNICAL BACKGROUND OF QAA
Formal introduction of QAA: We start off with a brief formal introduction to QAA, following standard references [1-4].
In the lines that follow, we provide a pictorial derivation of QAA, giving an intuitive background to the derivation shown here.
Consider a unitary
where
Here, we have introduced the
Goal of QAA: The goal of the algorithm is then to evolve the initial state
The amplification process that follows boosts the amplitude of the good state
Procedure of QAA: Rather than directly taking measurements on
Here,
In a nutshell, this equation shows that, for small values of the unknown parameter
Because
Intuition for QAA: The previous discussion follows the canonical, formal introduction to QAA.
This section provides a pictorial derivation of the QAA routine, which helps to get an intuitive understanding of the definition and the role of the operator

Going from left to right in the image above, the QAA routine proceeds as follows:
- Original situation: First, the original state prepared by
as resides in the two-dimensional plane spanned by the basis vectors and , respectively, with (typically small) projection onto the axis. - Reflection about
: We then apply a reflection around , transforming to , keeping the projection along unchanged, but adding a minus sign to the component. Reflection about means that all terms apart from pick up a negative sign. Because there are only two terms, only picks up a negative sign, which is accomplished by the operator , as defined previously. - Reflection about
: Finally, we apply a reflection around the original state , giving the final state , with an amplified amplitude of , adding an angle of from the rotation around to the original angle . Reflection about means that all terms except for pick up a minus sign, as can be done by applying the operator . Using the definition and conversely as well as the unitarity condition , this reflection can be rewritten as , where is a reflection about the all-zero state where all terms except for pick up a minus sign.
This sequence completes one cycle of the QAA routine, that is one application of
thereby confirming the formal definition above.
Repeating this sequence, after
Obtaining a quantum speedup:
To see that QAA indeed provides a quantum speedup, suppose we use it to query an unsorted database with
Thus,
We then apply the QAA algorithm to amplify the amplitude of the good states, such that the probability of obtaining a good outcome is amplified.
To ensure that we measure a good outcome with high probability, we apply the Grover iterator
CIRCUIT IMPLEMENTATION OF QAA
Implementation of QAA on a QC: Now that we have an intuitive understanding for QAA (after all, it is just a rotation, as illustrated previously), the final puzzle piece that remains to be solved is the actual implementation of the reflections
which gives a minus sign to
Implementation of
which is a reflection about
Implementation of

We can decompose the

Thus, the final circuit can be implemented using ancilla qubits as

IMPORTS and SETUP
# general imports
import matplotlib.pyplot as plt
import numpy as np
# magic word for producing visualizations in notebook
%matplotlib inline# AWS imports: Import Braket SDK modules
from braket.circuits import Circuit, circuit # noqa: F401
from braket.devices import LocalSimulator# local imports
from utils_circuit import adjoint, get_unitary
from utils_qaa import qaa # noqa: F401
# monkey patch get_unitary() and adjoint() to the Circuit class
Circuit.get_unitary = get_unitary
Circuit.adjoint = adjoint# set up device: Local Schroedinger Simulator
device = LocalSimulator()IMPLEMENTATION OF REFLECTION OPERATORS
In utils_qaa.py we provide a set of simple helper functions to implement the quantum circuit for the QAA algorithm.
Specifically, we demonstrate how such modular building blocks can be registered as subroutines, using @circuit.subroutine(register=True).
Here we first highlight the implementation of the reflections utiles_qaa.py module.
REFLECTION AROUND
We need to apply a minus sign to
# helper function to apply XZX to given qubit
@circuit.subroutine(register=True)
def minus_R_B(qubit):
"""
Function to apply a minus sign to |B>|0>. This goal is achieved by applying XZX to the ancilla qubit.
Args:
qubit: the ancilla qubit on which we apply XZX.
"""
# instantiate circuit object
circ = Circuit()
# Apply sequence XZX to given qubit
circ.x(qubit).z(qubit).x(qubit)
return circ
REFLECTION AROUND
We must implement use_explicit_unitary, one can evolve the system with the the unitary
# Helper function to apply rotation -R0
@circuit.subroutine(register=True)
def minus_R_zero(qubits, use_explicit_unitary=False):
"""
Function to implement transformation: |0,0,...0> -> -|0,0,...0>, all others unchanged.
Args:
qubits: list of qubits on which to apply the gates
use_explicit_unitary (default False): Flag to specify that we could instead implement
the desired gate using a custom gate defined by the unitary diag(-1,1,...,1).
"""
circ = Circuit()
# If the use_explicit_matrix flag is True, we just apply the unitary defined by |0,0,...0> -> -|0,0,...0>
if use_explicit_unitary:
# Create the matrix diag(-1,1,1,...,1)
unitary = np.eye(2**len(qubits))
unitary[0][0]=-1
# Add a gate defined by this matrix
circ.unitary(matrix=unitary, targets=qubits)
# Otherwise implement the unitary using ancilla qubits:
else:
# Flip all qubits. We now must check whether all qubits are |1>, rather than |0>.
circ.x(qubits)
# If we have only 1 qubit, we only must apply XZX to that qubit to pick up a minus sign on |0>
if len(qubits) < 2:
circ.z(qubits)
# For more qubits, we use Toffoli (or CCNOT) gates to verify the qubits are in |1> (after applying X)
else:
# Dynamically add ancilla qubits, starting on the next unused qubit in the circuit
# NOTE: if this subroutine is being applied to a subset of qubits in a circuit, these ancilla
# registers may already be used. We could pass in circ as an argument and add ancillas outside of
# circ.targets instead, if desired.
ancilla_start = max(qubits) + 1
# Check that the first two register qubits are both 1's using a CCNOT on a new ancilla qubit.
circ.ccnot(qubits[0],qubits[1],ancilla_start)
# Now add a CCNOT from each of the next register qubits, comparing with the ancilla we just added.
# Target on a new ancilla. If len(qubits) is 2, this does not execute.
for ii,qubit in enumerate(qubits[2:]):
circ.ccnot(qubit,ancilla_start+ii, ancilla_start+ii+1)
# A Z gate applied to the last ancilla qubit gives a minus sign if all register qubits are |1>
ancilla_end = ancilla_start + len(qubits[2:])
circ.z(ancilla_end)
# Now uncompute to disentangle the ancilla qubits by applying CCNOTs in the reverse order to the previous.
for jj,qubit in enumerate(reversed(qubits[2:])):
circ.ccnot(qubit,ancilla_end-jj-1, ancilla_end-jj)
# Finally undo the last CCNOT on the first two register qubits.
circ.ccnot(qubits[0],qubits[1],ancilla_start)
# Flip all qubits back
circ.x(qubits)
return circ
VISUALIZATION OF THE CIRCUIT FOR THE REFLECTION
To check our implementation of the
Example circuit with four qubits and simple index ordering:
qubits = [0, 1, 2, 3]
circ = Circuit()
circ.minus_R_zero(qubits)
print(circ)T : |0|1|2|3|4|5| 6 | 7 |8|
q0 : -X-C-------------C---X-
| |
q1 : -X-C-------------C---X-
| |
q2 : -X-|-C-------C---|-X---
| | | |
q3 : -X-|-|-C---C-|-X-|-----
| | | | | |
q4 : ---X-C-|---|-C---X-----
| | | |
q5 : -----X-C---C-X---------
| |
q6 : -------X-Z-X-----------
T : |0|1|2|3|4|5| 6 | 7 |8|
Example with four qubits and arbitrary index ordering:
Note that the simulators require contiguous qubit indexing, while our algorithm does not.
qubits = [1, 0, 5, 4]
circ = Circuit()
circ.minus_R_zero(qubits)
print(circ)T : |0|1|2|3|4|5|6| 7 |8|
q0 : -X-C-----------C---X-
| |
q1 : -X-C-----------C---X-
| |
q4 : -X-|---C---C-X-|-----
| | | |
q5 : -X-|-C-|---|-C-|-X---
| | | | | |
q6 : ---X-C-|---|-C-X-----
| | | |
q7 : -----X-C---C-X-------
| |
q8 : -------X-Z-X---------
T : |0|1|2|3|4|5|6| 7 |8|
IMPLEMENTATION OF QAA
This section puts everything together and shows how to implement QAA using the subroutines given previously.
We first build a function grover_iterator(...) that implements the Grover iterator qaa(...) which repeatedly applies the iterator
The full code (imported into this notebook in the imports and setup section) is available in the module utils_qaa.py.
@circuit.subroutine(register=True)
def grover_iterator(A,flag_qubit,qubits=None,use_explicit_unitary=False):
"""
Function to implement the Grover iterator Q=A R_0 A* R_B.
Args:
A: Circuit defining the unitary A
flag_qubit: Specifies which of the qubits A acts on labels the good/bad subspace.
Must be an element of qubits (if passed) or A.qubits.
qubits: list of qubits on which to apply the gates (including the flag_qubit).
If qubits is different from A.qubits, A is applied to qubits instead.
use_explicit_unitary: Flag to specify that we should implement R_0 using using a custom
gate defined by the unitary diag(-1,1,...,1). Default is False.
"""
# If no qubits are passed, apply the gates to the targets of A
if qubits is None:
qubits = A.qubits
else:
# If qubits are passed, make sure it's the right number to remap from A.
if len(qubits)!=len(A.qubits):
raise ValueError('Number of desired target qubits differs from number of targets in A'.format(flag_qubit=repr(flag_qubit)))
# Verify that flag_qubit is one of the qubits on which A acts, or one of the user defined qubits
if flag_qubit not in qubits:
raise ValueError('flag_qubit {flag_qubit} is not in targets of A'.format(flag_qubit=repr(flag_qubit)))
# Instantiate the circuit
circ = Circuit()
# Apply -R_B to the flag qubit
circ.minus_R_B(flag_qubit)
# Apply A^\dagger. Use target mapping if different qubits are specified
circ.add_circuit(A.adjoint(),target=qubits)
# Apply -R_0
circ.minus_R_zero(qubits,use_explicit_unitary)
# Apply A, mapping targets if desired.
circ.add_circuit(A,target=qubits)
return circ
@circuit.subroutine(register=True)
def qaa(A,flag_qubit,num_iterations,qubits=None,use_explicit_unitary=False):
"""
Function to implement the Quantum Amplitude Amplification Q^m, where Q=A R_0 A* R_B, m=num_iterations.
Args:
A: Circuit defining the unitary A
flag_qubit: Specifies which of the qubits A acts on labels the good/bad subspace.
Must be an element of qubits (if passed) or A.qubits.
num_iterations: number of applications of the Grover iterator Q.
qubits: list of qubits on which to apply the gates (including the flag_qubit).
If qubits is different from A.qubits, A is applied to qubits instead.
use_explicit_unitary: Flag to specify that we should implement R_0 using using a custom
gate defined by the unitary diag(-1,1,...,1). Default is False.
"""
# Instantiate the circuit
circ = Circuit()
# Apply the Grover iterator num_iterations times:
for _ in range(num_iterations):
circ.grover_iterator(A,flag_qubit,qubits,use_explicit_unitary)
return circ
NUMERICAL EXAMPLE
This section shows how to use QAA to amplify the amplitude of
where
where
Suppose
Our goal is to amplify the coefficient of the "good" state. Using the previous algorithm, this amplification corresponds to applying Quantum Amplitude Amplification with an input unitary
Let us check the effectiveness of the algorithm by plotting the probability of measuring ResultType we will use requires a non-zero value for shots when running on an on-demand simulator.
# Set up the state A|00>
flag_qubit = 1
epsilon = 0.05
A_circ = Circuit().ry(0, epsilon).cnot(0, 1)
# Add marginal probability for flag qubit as result Type
A_circ.probability(target=[flag_qubit])
# Let's find the probability of measuring |11> for different values of m, the number of applications of QAA:
probabilities = []
stepsize = 2
iterations = range(1, 40, stepsize)
for m in iterations:
# Get circuit object
circ = Circuit()
# Apply QAA using A defined by A_circ
circ.qaa(A_circ, flag_qubit, m, use_explicit_unitary=True)
# Classically simulate the circuit
# Give the correct device.run call depending on whether the device is local or on-demand
if isinstance(device, LocalSimulator):
task = device.run(circ, shots=0)
else:
task = device.run(circ, shots=1000)
# Get result
result = task.result()
# Append the probability of measuring |11> for this value of m.
probabilities.append(result.values[0][1])
# Get analytical result for comparison
probs_theo = [np.sin((2 * mm + 1) * epsilon / 2) ** 2 for mm in iterations]
# Plot the results
plt.figure(figsize=(7, 5))
plt.plot(iterations, probabilities, "o")
plt.plot(iterations, probs_theo)
plt.xlabel("Number of Iterations")
plt.ylabel("Probability of measuring flag qubit in |1>");As expected, we see that the repeated application of the Grover iterator
APPENDIX
# Check SDK version
# alternative: braket.__version__
!pip show amazon-braket-sdk | grep VersionVersion: 1.53.3
APPENDIX: ALTERNATIVE RUN WITH AMPLITUDE RESULT TYPE
Rather than just examining the marginal probability to find the flag qubit in state
Using amplitudes also presents a learning opportunity:
If we use use_explicit_unitary = False), then measurement outcomes are bitstrings of size
Since the ancilla qubits are initialized in
Using a classical simulator backend, we can attach the corresponding amplitude as a ResultType to the circuit, as shown in the following code.
# Set up the state A|00>
flag_qubit = 1
epsilon = 0.05
A_circ = Circuit().ry(0, epsilon).cnot(0, 1)
# set switch to either use explicit unitary diag(-1, 1, ...) [True] or use ancillas [False]
use_explicit_unitary = True
# Let's find the probability of measuring |11> for different values of m, the number of applications of QAA:
probabilities = []
stepsize = 2
iterations = range(1, 40, stepsize)
for m in iterations:
# Get circuit object
circ = Circuit()
# Apply QAA using A defined by A_circ
circ.qaa(A_circ, flag_qubit, m, use_explicit_unitary=use_explicit_unitary)
if use_explicit_unitary:
target_string = "11"
circ.amplitude(state=[target_string])
else:
number_ancillas = A_circ.qubit_count - 1
target_string = "11" + "0" * number_ancillas
circ.amplitude(state=[target_string])
# Classically simulate the circuit
# Execute the correct device.run call depending on whether the device is local or on-demand
if isinstance(device, LocalSimulator):
task = device.run(circ, shots=0)
else:
task = device.run(circ, shots=0)
# Get result
result = task.result()
# Append the probability of measuring |11> for this value of m.
probabilities.append(np.linalg.norm(result.values[0][target_string]) ** 2)
# Get analytical result for comparison
probs_theo = [np.sin((2 * mm + 1) * epsilon / 2) ** 2 for mm in iterations]
# Plot the results
plt.figure(figsize=(7, 5))
plt.plot(iterations, probabilities)
plt.plot(iterations, probs_theo, "o")
plt.xlabel("Number of Iterations")
plt.ylabel("Probability of measuring 11")
# Let's compare the amplitude of |11> in the initial state versus the state with maximum probability:
# Print the initial amplitude of |11>
# Add a Result Type to output the amplitude of |11> for A
A_initial = A_circ.copy()
A_initial.amplitude(state=["11"])
if isinstance(device, LocalSimulator):
initial_result = device.run(A_initial, shots=0).result()
else:
initial_result = device.run(A_initial, shots=0).result()
print("Amplitude <11|Initial State>:\n", initial_result.values[0], "\n")
# Find the number of iterations required to achieve the maximum probability:
max_prob = max(probabilities)
max_iter = iterations[probabilities.index(max_prob)]
# Generate that state:
circ = Circuit()
circ.qaa(A_circ, flag_qubit, max_iter, use_explicit_unitary=use_explicit_unitary)
circ.amplitude(state=[target_string])
# Run the simulator
if isinstance(device, LocalSimulator):
task = device.run(circ, shots=0)
result = task.result()
else:
task = device.run(circ, shots=0)
result = task.result()
# Print the final amplitude of |11>:
info = "Maximum amplified amplitude <110|Final State> after approximately"
print(info + f" {max_iter} Grover iterations:\n {result.values[0]}")Amplitude <11|Initial State>:
{'11': (0.024997395914712332+0j)}
Maximum amplified amplitude <110|Final State> after approximately 31 Grover iterations:
{'11': (0.9997837641893592+0j)}
References
[1] Wikipedia: Amplitude Amplification.
[2] G. Brassard, P. Høyer, "An exact quantum polynomial-time algorithm for Simon's problem", Proceedings of Fifth Israeli Symposium on Theory of Computing and Systems. IEEE Computer Society Press: 12–23, arXiv:quant-ph/9704027 (1997).
[3] G. Brassard, P. Høyer, M. Mosca, A. Tapp, "Quantum Amplitude Amplification and Estimation", arXiv:quant-ph/0005055 (2000).
[4] Y. Suzuki, S. Uno, R. Raymond, T. Tanaka, T. Onodera, N. Yamamoto, "Amplitude Estimation without Phase Estimation", arXiv:1904.10246 (2019).
This entry was created automatically from publicly available records. QCR links to public sources and only stores repository content where the license permits redistribution.
Versions
Cite all versions? Use the base QCR ID to always reference the latest version of this entry.
Join the Discussion
Comments (0)
No comments yet. Be the first to share your thoughts!