Code
qcr:2606.37510.1

Quantum Phase Estimation with Amazon Braket

Quantum Phase Estimation (QPE) is a cornerstone quantum subroutine that estimates the eigenvalue phase of a unitary operator for a given eigenstate, and it powers algorithms ranging from Shor's factoring to the HHL linear solver and quantum chemistry. This Amazon Braket notebook implements QPE end to end on a simulator. It prepares a register of counting (ancilla) qubits in superposition, applies a sequence of controlled powers of the target unitary so that the unknown phase is kicked back and encoded into the relative phases of the counting register, and then applies an inverse Quantum Fourier Transform to convert those phases into a binary number that can be read out by measurement. The notebook shows how the number of counting qubits sets the precision of the estimate and how to interpret the measured bitstring as an approximation of the eigenvalue phase. By building the controlled-unitary ladder and the inverse QFT explicitly with Braket circuit primitives and running the full estimator through simulation, it gives a clear, hands-on view of how phase estimation extracts spectral information from a quantum operator, the engine behind many of Braket's more advanced algorithmic examples.
Qubit
Circuit-based
Uploaded 2 days ago
9
Views
GitHub582
Citing this entry? Use this QCR ID
Uploaded by
QL
QCR Librarian

Overview

amazon-braket/amazon-braket-examples
582261
In [ ]:
# --- 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 PHASE ESTIMATION

In [23]:
# Use Braket SDK Cost Tracking to estimate the cost to run this example
from braket.tracking import Tracker

t = Tracker().start()

This tutorial provides a detailed implementation of the Quantum Phase Estimation (QPE) algorithm using the Amazon Braket SDK. The QPE algorithm is designed to estimate the eigenvalues of a unitary operator [1, 2]; it is a very important subroutine to many quantum algorithms, most famously Shor's algorithm for factoring and the HHL algorithm (named after the physicists Harrow, Hassidim and Lloyd) for solving linear systems of equations on a quantum computer [1, 2]. Moreover, eigenvalue problems can be found across many disciplines and application areas, including (for example) principal component analysis (PCA) as used in machine learning or the solution of differential equations as relevant across mathematics, physics, engineering and chemistry. We first review the basics of the QPE algorithm. We then implement the QPE algorithm in code using the Amazon Braket SDK, and we illustrate the application thereof with simple examples. This notebook also showcases the Amazon Braket circuit.subroutine functionality, which allows us to use custom-built gates as if they were any other built-in gates. This tutorial is set up to run either on the local simulator or the on-demand simulators; changing between these devices merely requires changing one line of code as demonstrated as follows in cell [4].

TECHNICAL BACKGROUND OF QPE

Introduction: A unitary matrix is a complex, square matrix whose adjoint (or conjugate transpose) is equal to its inverse. Unitary matrices have many nice properties, including the fact that their eigenvalues are always roots of unity (that is, phases). Given a unitary matrix (satisfying 𝟙) and an eigenstate with , the Quantum Phase Estimation (QPE) algorithm provides an estimate for the phase (with since the eigenvalues of a unitary have modulus one). The QPE works with high probability within an additive error using qubits (without counting the qubits used to encode the eigenstate) and controlled- operations [1].

Quantum Phase Estimation Algorithm: The QPE algorithm takes a unitary as input. For the sake of simplicity (we will generalize the discussion below), suppose that the algorithm also takes as input an eigenstate fulfilling

with .

QPE uses two registers of qubits: we refer to the first register as precision qubits (as the number of qubits in the first register sets the achievable precision of our results) and the second register as query qubits (as the second register hosts the eigenstate ). Suppose we have prepared this second register in . We then prepare a uniform superposition of all basis vectors in the first register using a series of Hadamard gates.

Next, we apply a series of controlled-unitaries for different powers of (as illustrated in the circuit diagram that follows). For example, for we get

Note that the second register remains unaffected as it stays in the eigenstate . However, we managed to transfer information about the phase of the eigenvalue of (that is, ) into the first precision register by encoding it as a relative phase in the state of the qubits in the first register.

Similarly, for we obtain

where this time we wrote into the precision register. The process is similar for all .

Introducing the following notation for binary fractions

one can show that the application of a controlled unitary leads to the following transformation

where the first bits of precision in the binary expansion (that is, those bits to the left of the decimal) can be dropped, because for any whole number .

The QPE algorithm implements a series of these transformations for , using qubits in the precision register. In its entirety, this sequence of controlled unitaries leads to the transformation

By inspection, one can see that the state of the register qubits above corresponds to a quantum Fourier transform of the state . Thus, the final step of the QPE algorithm is to run the inverse Quantum Fourier Transform (QFT) algorithm on the precision register to extract the phase information from this state. The resulting state is

Measuring the precision qubits in the computational basis then gives the classical bitstring , from which we can readily infer the phase estimate with the corresponding eigenvalue .

Simple example for illustration: For concreteness, consider a simple example with the unitary given by the Pauli gate, , for which is an eigenstate with eigenvalue , i.e., . This state can be prepared with a Hadamard gate as . We take a precision register consisting of just two qubits ().

Thus, after the first layer of Hadamard gates, the quantum state is

Next, the applications of the controlled- gates (equal to operations, or CNOT gates in this example) leave this state untouched, because is an eigenstate of with eigenvalue . Finally, applying the inverse QFT leads to

from which we deduce and therefore , as expected. Here, in the last step we have used , which makes the effect of the inverse QFT more apparent.

Initial state of query register: So far, we have assumed that the query register is prepared in an eigenstate of . What happens if this is not the case? Let's reconsider the simple example given previously.

Suppose now that the query register is instead prepared in the state . We can always express this state in the eigenbasis of , that is, . By linearity, application of the QPE algorithm then gives (up to normalization)

When we measure the precision qubits in this state, 50% of the time we will observe the eigenphase and 50% of the time we will measure . We illustrate this example numerically as follows.

This example motivates the general case: we can pass a state that is not an eigenstate of to the QPE algorithm, but we may need to repeat our measurements several times in order to obtain an estimate of the desired phase.

CIRCUIT IMPLEMENTATION OF QPE

The QPE circuit can be implemented using Hadamard gates, controlled- unitaries, and the inverse QFT (denoted as ). The details of the calculation can be found in a number of resources (such as, [1]); we omit them here. Following the previous discussion, the circuit that implements the QPE algorithm reads as below, where m is the size of lower query register and n is the size of upper precision register.

IMPORTS and SETUP

In [24]:
# general imports
import matplotlib.pyplot as plt
import numpy as np

# magic word for producing visualizations in notebook
%matplotlib inline
In [25]:
# AWS imports: Import Amazon Braket SDK modules
from braket.circuits import Circuit
from braket.devices import LocalSimulator
In [26]:
# local imports
from utils_qpe import run_qpe

%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
In [27]:
# set up device: local simulator or the on-demand simulator
device = LocalSimulator()
# device = AwsDevice(Devices.Amazon.SV1)

Pauli Matrices:

In some of our examples, we choose the unitary to be given by the Pauli Matrices, which we thus define as follows:

In [28]:
# Define Pauli matrices
Id = np.eye(2)  # Identity matrix
X = np.array([[0.0, 1.0], [1.0, 0.0]])  # Pauli X
Y = np.array([[0.0, -1.0j], [1.0j, 0.0]])  # Pauli Y
Z = np.array([[1.0, 0.0], [0.0, -1.0]])  # Pauli Z

IMPLEMENTATION OF THE QPE CIRCUIT

In utils_qpe.py we provide simple helper functions to implement the quantum circuit for the QPE algorithm. Specifically, we demonstrate that such modular building blocks can be registered as subroutines, using @circuit.subroutine(register=True). Moreover, we provide a helper function (called get_qpe_phases) to perform postprocessing based on the measurement results to extract the phase. The details of utils_qpe.py are shown in the Appendix.

To implement the unitary , one can use the fact that , so that can be constructed by repeatedly applying the core building block . However, the circuit generated using this approach will have a significantly larger depth. In our implementation, we instead define the matrix and create the controlled gate from that.

VISUALIZATION OF THE QFT CIRCUIT

To check our implementation of the QPE circuit, we visualize this circuit for a small number of qubits.

In [29]:
# set total number of qubits
precision_qubits = [0, 1]
query_qubits = [2]

# prepare query register
my_qpe_circ = Circuit().h(query_qubits)

# set unitary
unitary = X

# show small QPE example circuit
my_qpe_circ = my_qpe_circ.qpe(precision_qubits, query_qubits, unitary)
print("QPE CIRCUIT:")
print(my_qpe_circ)
QPE CIRCUIT:
T  : |0|1|2| 3  |4|     5      |6|
                                  
q0 : -H---U-SWAP---PHASE(-1.57)-H-
          | |      |              
q1 : -H-U-|-SWAP-H-C--------------
        | |                       
q2 : -H-U-U-----------------------

T  : |0|1|2| 3  |4|     5      |6|

As shown in the following code, the two registers can be distributed anywhere across the circuit, with arbitrary indices for the precision and the query registers.

In [30]:
# set qubits
precision_qubits = [1, 3]
query_qubits = [5]

# prepare query register
my_qpe_circ = Circuit().i(range(7))
my_qpe_circ.h(query_qubits)

# set unitary
unitary = X

# show small QPE example circuit
my_qpe_circ = my_qpe_circ.qpe(precision_qubits, query_qubits, unitary)
print("QPE CIRCUIT:")
print(my_qpe_circ)
QPE CIRCUIT:
T  : |0|1|2|3| 4  |5|     6      |7|
                                    
q0 : -I-----------------------------
                                    
q1 : -I-H---U-SWAP---PHASE(-1.57)-H-
            | |      |              
q2 : -I-----|-|------|--------------
            | |      |              
q3 : -I-H-U-|-SWAP-H-C--------------
          | |                       
q4 : -I---|-|-----------------------
          | |                       
q5 : -I-H-U-U-----------------------
                                    
q6 : -I-----------------------------

T  : |0|1|2|3| 4  |5|     6      |7|

As follows, we set up the same circuit, this time implementing the unitary , by repeatedly applying the core building block . This operation can be done by setting the parameter control_unitary=False (default is True).

In [31]:
# set qubits
precision_qubits = [1, 3]
query_qubits = [5]

# prepare query register
my_qpe_circ = Circuit().i(range(7))
my_qpe_circ.h(query_qubits)

# set unitary
unitary = X

# show small QPE example circuit
my_qpe_circ = my_qpe_circ.qpe(precision_qubits, query_qubits, unitary, control_unitary=False)
print("QPE CIRCUIT:")
print(my_qpe_circ)
QPE CIRCUIT:
T  : |0|1|2|3|4| 5  |6|     7      |8|
                                      
q0 : -I-------------------------------
                                      
q1 : -I-H---U-U-SWAP---PHASE(-1.57)-H-
            | | |      |              
q2 : -I-----|-|-|------|--------------
            | | |      |              
q3 : -I-H-U-|-|-SWAP-H-C--------------
          | | |                       
q4 : -I---|-|-|-----------------------
          | | |                       
q5 : -I-H-U-U-U-----------------------
                                      
q6 : -I-------------------------------

T  : |0|1|2|3|4| 5  |6|     7      |8|

In the circuit diagram, we can visually infer the exponents for , at the expense of a larger circuit depth.

NUMERICAL TEST EXPERIMENTS

In the following section, we verify that our QFT implementation works as expected with a few test examples:

  1. We run QPE with and prepare the eigenstate with phase and eigenvalue .
  2. We run QPE with and prepare the eigenstate with phase and eigenvalue .
  3. We run QPE with and prepare which is not an eigenstate of . Because , we expect to measure both and associated with the two eigenstates .
  4. We run QPE with unitary , and prepare the query register in the eigenstate . Here, we expect to measure the phase (giving the corresponding eigenvalue ).
  5. We run QPE with a random two qubit unitary, diagonal in the computational basis, and prepare the query register in the eigenstate . In this case, we should be able to read off the eigenvalue and phase from and verify QPE gives the right answer (with high probability) up to a small error (that depends on the number of qubits in the precision register).

HELPER FUNCTIONS FOR NUMERICAL TESTS

Because we will run the same code repeatedly, let's first create a helper function we can use to keep the notebook clean.

In [32]:
def postprocess_qpe_results(out):
    """Function to postprocess dictionary returned by run_qpe

    Args:
        out: dictionary containing results/information associated with QPE run as produced by run_qpe

    """
    # unpack results
    circ = out["circuit"]
    measurement_counts = out["measurement_counts"]
    bitstring_keys = out["bitstring_keys"]
    probs_values = out["probs_values"]
    precision_results_dic = out["precision_results_dic"]
    phases_decimal = out["phases_decimal"]
    eigenvalues = out["eigenvalues"]

    # print the circuit
    print("Printing circuit:")
    print(circ)

    # print measurement results
    print("Measurement counts:", measurement_counts)

    # plot probabalities
    plt.bar(bitstring_keys, probs_values)
    plt.xlabel("bitstrings")
    plt.ylabel("probability")
    plt.xticks(rotation=90)
    # print results
    print("Results in precision register:", precision_results_dic)
    print("QPE phase estimates:", phases_decimal)
    print("QPE eigenvalue estimates:", np.round(eigenvalues, 5))

NUMERICAL TEST EXAMPLE 1

First, apply the QPE algorithm to the simple single-qubit unitary , with eigenstate . Here, we expect to measure the phase (giving the corresponding eigenvalue ). We show that this result stays the same as we increase the number of qubits for the top register.

In [33]:
# Set total number of precision qubits: 2
number_precision_qubits = 2

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits]

# State preparation for eigenstate of U=X
query = Circuit().h(query_qubits)

# Run the test with U=X
out = run_qpe(X, precision_qubits, query_qubits, query, device)

# Postprocess results
postprocess_qpe_results(out)
Printing circuit:
T  : |0|1|2| 3  |4|     5      |6|Result Types|
                                               
q0 : -H---U-SWAP---PHASE(-1.57)-H-Probability--
          | |      |              |            
q1 : -H-U-|-SWAP-H-C--------------Probability--
        | |                       |            
q2 : -H-U-U-----------------------Probability--

T  : |0|1|2| 3  |4|     5      |6|Result Types|
Measurement counts: Counter({'001': 505, '000': 495})
Results in precision register: {'00': 1000}
QPE phase estimates: [0.0]
QPE eigenvalue estimates: [1.+0.j]

Next, check that we get the same result for a larger precision (top) register.

In [34]:
# Set total number of precision qubits: 3
number_precision_qubits = 3

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits]

# State preparation for eigenstate of U=X
query = Circuit().h(query_qubits)

# Run the test with U=X
out = run_qpe(X, precision_qubits, query_qubits, query, device)

# Postprocess results
postprocess_qpe_results(out)
Printing circuit:
T  : |0|1|2|3| 4  |5|     6      |      7       |     8      |9|Result Types|
                                                                             
q0 : -H-----U-SWAP------------------PHASE(-0.79)-PHASE(-1.57)-H-Probability--
            | |                     |            |              |            
q1 : -H---U-|-|------PHASE(-1.57)-H-|------------C--------------Probability--
          | | |      |              |                           |            
q2 : -H-U-|-|-SWAP-H-C--------------C---------------------------Probability--
        | | |                                                   |            
q3 : -H-U-U-U---------------------------------------------------Probability--

T  : |0|1|2|3| 4  |5|     6      |      7       |     8      |9|Result Types|
Measurement counts: Counter({'0000': 511, '0001': 489})
Results in precision register: {'000': 1000}
QPE phase estimates: [0.0]
QPE eigenvalue estimates: [1.+0.j]

NUMERICAL TEST EXAMPLE 2

Next, apply the QPE algorithm to the simple single-qubit unitary , with eigenstate . Here, we expect to measure the phase (giving the corresponding eigenvalue ).

In [35]:
# Set total number of precision qubits: 2
number_precision_qubits = 2

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits]

# State preparation for eigenstate of U=X
query = Circuit().x(query_qubits).h(query_qubits)

# Run the test with U=X
out = run_qpe(X, precision_qubits, query_qubits, query, device)

# Postprocess results
postprocess_qpe_results(out)
Printing circuit:
T  : |0|1|2|3| 4  |5|     6      |7|Result Types|
                                                 
q0 : -H-----U-SWAP---PHASE(-1.57)-H-Probability--
            | |      |              |            
q1 : -H---U-|-SWAP-H-C--------------Probability--
          | |                       |            
q2 : -X-H-U-U-----------------------Probability--

T  : |0|1|2|3| 4  |5|     6      |7|Result Types|
Measurement counts: Counter({'100': 501, '101': 499})
Results in precision register: {'10': 1000}
QPE phase estimates: [0.5]
QPE eigenvalue estimates: [-1.+0.j]

NUMERICAL TEST EXAMPLE 3

Next, apply the QPE algorithm again to the simple single-qubit unitary , but we initialize the query register in the state which is not an eigenstate of . Here, following the previous discussion, we expect to measure the phases (giving the corresponding eigenvalue ). Accordingly, here we set items_to_keep=2.

In [36]:
# Set total number of precision qubits: 2
number_precision_qubits = 2

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits]

# State preparation for |1>, which is not an eigenstate of U=X
query = Circuit().x(query_qubits)

# Run the test with U=X
out = run_qpe(X, precision_qubits, query_qubits, query, device, items_to_keep=2)

# Postprocess results
postprocess_qpe_results(out)
Printing circuit:
T  : |0|1|2| 3  |4|     5      |6|Result Types|
                                               
q0 : -H---U-SWAP---PHASE(-1.57)-H-Probability--
          | |      |              |            
q1 : -H-U-|-SWAP-H-C--------------Probability--
        | |                       |            
q2 : -X-U-U-----------------------Probability--

T  : |0|1|2| 3  |4|     5      |6|Result Types|
Measurement counts: Counter({'000': 254, '001': 252, '101': 249, '100': 245})
Results in precision register: {'10': 494, '00': 506}
QPE phase estimates: [0.0, 0.5]
QPE eigenvalue estimates: [ 1.+0.j -1.+0.j]

NUMERICAL TEST EXAMPLE 4

Next, apply the QPE algorithm to the two-qubit unitary , and prepare the query register in the eigenstate . Here, we expect to measure the phase (giving the corresponding eigenvalue ).

In [37]:
# set unitary matrix U
u1 = np.kron(X, Id)
u2 = np.kron(Id, Z)
unitary = np.dot(u1, u2)
print("Two-qubit unitary (XZ):\n", unitary)

# get example eigensystem
eig_values, eig_vectors = np.linalg.eig(unitary)
print("Eigenvalues:", eig_values)
# print('Eigenvectors:', eig_vectors)
Two-qubit unitary (XZ):
 [[ 0.  0.  1.  0.]
 [ 0.  0.  0. -1.]
 [ 1.  0.  0.  0.]
 [ 0. -1.  0.  0.]]
Eigenvalues: [ 1. -1.  1. -1.]
In [38]:
# Set total number of precision qubits: 2
number_precision_qubits = 2

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits, number_precision_qubits + 1]

# State preparation for eigenstate |+,1> of U=X \otimes Z
query = Circuit().h(query_qubits[0]).x(query_qubits[1])

# Run the test with U=X
out = run_qpe(unitary, precision_qubits, query_qubits, query, device)

# Postprocess results
postprocess_qpe_results(out)
Printing circuit:
T  : |0|1|2| 3  |4|     5      |6|Result Types|
                                               
q0 : -H---U-SWAP---PHASE(-1.57)-H-Probability--
          | |      |              |            
q1 : -H-U-|-SWAP-H-C--------------Probability--
        | |                       |            
q2 : -H-U-U-----------------------Probability--
        | |                       |            
q3 : -X-U-U-----------------------Probability--

T  : |0|1|2| 3  |4|     5      |6|Result Types|
Measurement counts: Counter({'1001': 520, '1011': 480})
Results in precision register: {'10': 1000}
QPE phase estimates: [0.5]
QPE eigenvalue estimates: [-1.+0.j]

NUMERICAL TEST EXAMPLE 5

In this example, we choose the unitary to be a random two-qubit unitary, diagonal in the computational basis. We initialize the query register to be in the eigenstate of , which we can prepare using that . In this case we should be able to read off the eigenvalue and phase from and verify that QPE gives the right answer.

In [39]:
# Generate a random 2 qubit unitary matrix:
from scipy.stats import unitary_group

# Fix random seed for reproducibility
np.random.seed(seed=42)

# Get random two-qubit unitary
random_unitary = unitary_group.rvs(2**2)

# Let's diagonalize this
evals = np.linalg.eig(random_unitary)[0]

# Since we want to be able to read off the eigenvalues of the unitary in question
# let's choose our unitary to be diagonal in this basis
unitary = np.diag(evals)

# Check that this is indeed unitary, and print it out:
print("Two-qubit random unitary:\n", np.round(unitary, 3))
print("Check for unitarity: ", np.allclose(np.eye(len(unitary)), unitary.dot(unitary.T.conj())))

# Print eigenvalues
print("Eigenvalues:", np.round(evals, 3))
Two-qubit random unitary:
 [[-0.078+0.997j  0.   +0.j     0.   +0.j     0.   +0.j   ]
 [ 0.   +0.j    -0.987-0.159j  0.   +0.j     0.   +0.j   ]
 [ 0.   +0.j     0.   +0.j     0.192-0.981j  0.   +0.j   ]
 [ 0.   +0.j     0.   +0.j     0.   +0.j     0.747-0.665j]]
Check for unitarity:  True
Eigenvalues: [-0.078+0.997j -0.987-0.159j  0.192-0.981j  0.747-0.665j]

When we execute the QPE circuit, we expect the following (approximate) result for the eigenvalue estimate:

In [40]:
print("Target eigenvalue:", np.round(evals[-1], 3))
Target eigenvalue: (0.747-0.665j)
In [41]:
# Set total number of precision qubits
number_precision_qubits = 3

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits, number_precision_qubits + 1]

# State preparation for eigenstate |1,1> of diagonal U
query = Circuit().x(query_qubits[0]).x(query_qubits[1])

# Run the test with U=X
out = run_qpe(unitary, precision_qubits, query_qubits, query, device)

# Postprocess results
postprocess_qpe_results(out)

# compare output to exact target values
print("Target eigenvalue:", np.round(evals[-1], 3))
Printing circuit:
T  : |0|1|2|3| 4  |5|     6      |      7       |     8      |9|Result Types|
                                                                             
q0 : -H-----U-SWAP------------------PHASE(-0.79)-PHASE(-1.57)-H-Probability--
            | |                     |            |              |            
q1 : -H---U-|-|------PHASE(-1.57)-H-|------------C--------------Probability--
          | | |      |              |                           |            
q2 : -H-U-|-|-SWAP-H-C--------------C---------------------------Probability--
        | | |                                                   |            
q3 : -X-U-U-U---------------------------------------------------Probability--
        | | |                                                   |            
q4 : -X-U-U-U---------------------------------------------------Probability--

T  : |0|1|2|3| 4  |5|     6      |      7       |     8      |9|Result Types|
Measurement counts: Counter({'11111': 985, '11011': 6, '00011': 3, '10011': 2, '10111': 2, '00111': 1, '01011': 1})
Results in precision register: {'000': 3, '100': 2, '010': 1, '111': 985, '101': 2, '110': 6, '001': 1}
QPE phase estimates: [0.875]
QPE eigenvalue estimates: [0.70711-0.70711j]
Target eigenvalue: (0.747-0.665j)

We can easily improve the precision of our parameter estimate by increasing the number of qubits in the precision register, as shown in the following example.

In [42]:
# Set total number of precision qubits
number_precision_qubits = 10

# Define the set of precision qubits
precision_qubits = range(number_precision_qubits)

# Define the query qubits. We'll have them start after the precision qubits
query_qubits = [number_precision_qubits, number_precision_qubits + 1]

# State preparation for eigenstate |1,1> of diagonal U
query = Circuit().x(query_qubits[0]).x(query_qubits[1])

# Run the test with U=X
out = run_qpe(unitary, precision_qubits, query_qubits, query, device)

# Postprocess results
eigenvalues = out["eigenvalues"]
print("QPE eigenvalue estimates:", np.round(eigenvalues, 5))

# compare output to exact target values
print("Target eigenvalue:", np.round(evals[-1], 5))
QPE eigenvalue estimates: [0.74506-0.667j]
Target eigenvalue: (0.74699-0.66484j)

APPENDIX

Details of the utiles_qpe.py module

Imports, including inverse QFT

# general imports
import numpy as np
import math
from collections import Counter
from datetime import datetime
import pickle

# AWS imports: Import Braket SDK modules
from braket.circuits import Circuit, circuit

# local imports
from utils_qft import inverse_qft

QPE Subroutine

@circuit.subroutine(register=True)
def controlled_unitary(control, target_qubits, unitary):
    """
    Construct a circuit object corresponding to the controlled unitary

    Args:
        control: The qubit on which to control the gate

        target_qubits: List of qubits on which the unitary U acts

        unitary: matrix representation of the unitary we wish to implement in a controlled way
    """

    # Define projectors onto the computational basis
    p0 = np.array([[1., 0.],
                   [0., 0.]])

    p1 = np.array([[0., 0.],
                   [0., 1.]])

    # Instantiate circuit object
    circ = Circuit()

    # Construct numpy matrix
    id_matrix = np.eye(len(unitary))
    controlled_matrix = np.kron(p0, id_matrix) + np.kron(p1, unitary)

    # Set all target qubits
    targets = [control] + target_qubits

    # Add controlled unitary
    circ.unitary(matrix=controlled_matrix, targets=targets)

    return circ


@circuit.subroutine(register=True)
def qpe(precision_qubits, query_qubits, unitary, control_unitary=True):
    """
    Function to implement the QPE algorithm using two registers for precision (read-out) and query.
    Register qubits need not be contiguous.

    Args:
        precision_qubits: list of qubits defining the precision register

        query_qubits: list of qubits defining the query register

        unitary: Matrix representation of the unitary whose eigenvalues we wish to estimate

        control_unitary: Optional boolean flag for controlled unitaries,
                         with C-(U^{2^k}) by default (default is True),
                         or C-U controlled-unitary (2**power) times
    """
    qpe_circ = Circuit()

    # Get number of qubits
    num_precision_qubits = len(precision_qubits)
    num_query_qubits = len(query_qubits)

    # Apply Hadamard across precision register
    qpe_circ.h(precision_qubits)

    # Apply controlled unitaries. Start with the last precision_qubit, and end with the first
    for ii, qubit in enumerate(reversed(precision_qubits)):
        # Set power exponent for unitary
        power = ii

        # Alterantive 1: Implement C-(U^{2^k})
        if control_unitary:
            # Define the matrix U^{2^k}
            Uexp = np.linalg.matrix_power(unitary,2**power)

            # Apply the controlled unitary C-(U^{2^k})
            qpe_circ.controlled_unitary(qubit, query_qubits, Uexp)
        # Alterantive 2: One can instead apply controlled-unitary (2**power) times to get C-U^{2^power}
        else:
            for _ in range(2**power):
                qpe_circ.controlled_unitary(qubit, query_qubits, unitary)

    # Apply inverse qft to the precision_qubits
    qpe_circ.inverse_qft(precision_qubits)

    return qpe_circ

QPE postprocessing helper functions

# helper function to remove query bits from bitstrings
def substring(key, precision_qubits):
    """
    Helper function to get substring from keys for dedicated string positions as given by precision_qubits.
    This function is necessary to allow for arbitrary qubit mappings in the precision and query registers
    (that is, so that the register qubits need not be contiguous.)

    Args:
        key: string from which we want to extract the substring supported only on the precision qubits

        precision_qubits: List of qubits corresponding to precision_qubits.
                          Currently assumed to be a list of integers corresponding to the indices of the qubits
    """
    short_key = ''
    for idx in precision_qubits:
        short_key = short_key + key[idx]

    return short_key


# helper function to convert binary fractional to decimal
# reference: https://www.geeksforgeeks.org/convert-binary-fraction-decimal/
def binaryToDecimal(binary):
    """
    Helper function to convert binary string (example: '01001') to decimal

    Args:
        binary: string which to convert to decimal fraction
    """

    length = len(binary)
    fracDecimal = 0

    # Convert fractional part of binary to decimal equivalent
    twos = 2

    for ii in range(length):
        fracDecimal += ((ord(binary[ii]) - ord('0')) / twos);
        twos *= 2.0

    # return fractional part
    return fracDecimal


# helper function for postprocessing based on measurement shots
def get_qpe_phases(measurement_counts, precision_qubits, items_to_keep=1):
    """
    Get QPE phase estimate from measurement_counts for given number of precision qubits

    Args:
        measurement_counts: measurement results from a device run

        precision_qubits: List of qubits corresponding to precision_qubits.
                          Currently assumed to be a list of integers corresponding to the indices of the qubits

        items_to_keep: number of items to return (topmost measurement counts for precision register)
    """

    # Aggregate the results (that is, ignore the query register qubits):

    # First get bitstrings with corresponding counts for precision qubits only
    bitstrings_precision_register = [substring(key, precision_qubits)  for key in measurement_counts.keys()]
    # Then keep only the unique strings
    bitstrings_precision_register_set = set(bitstrings_precision_register)
    # Cast as a list for later use
    bitstrings_precision_register_list = list(bitstrings_precision_register_set)

    # Now create a new dict to collect measurement results on the precision_qubits.
    # Keys are given by the measurement count substrings on the register qubits. Initialize the counts to zero.
    precision_results_dic = {key: 0 for key in bitstrings_precision_register_list}

    # Loop over all measurement outcomes
    for key in measurement_counts.keys():
        # Save the measurement count for this outcome
        counts = measurement_counts[key]
        # Generate the corresponding shortened key (supported only on the precision_qubits register)
        count_key = substring(key, precision_qubits)
        # Add these measurement counts to the corresponding key in our new dict
        precision_results_dic[count_key] += counts

    # Get topmost values only
    c = Counter(precision_results_dic)
    topmost= c.most_common(items_to_keep)
    # get decimal phases from bitstrings for topmost bitstrings
    phases_decimal = [binaryToDecimal(item[0]) for item in topmost]

    # Get decimal phases from bitstrings for all bitstrings
    # number_precision_qubits = len(precision_qubits)
    # Generate binary decimal expansion
    # phases_decimal = [int(key, 2)/(2**number_precision_qubits) for key in precision_results_dic]
    # phases_decimal = [binaryToDecimal(key) for key in precision_results_dic]

    return phases_decimal, precision_results_dic

Run QPE experiments:

def run_qpe(unitary, precision_qubits, query_qubits, query_circuit,
            device, items_to_keep=1, shots=1000, save_to_pck=False):
    """
    Function to run QPE algorithm end-to-end and return measurement counts.

    Args:
        precision_qubits: list of qubits defining the precision register

        query_qubits: list of qubits defining the query register

        unitary: Matrix representation of the unitary whose eigenvalues we wish to estimate

        query_circuit: query circuit for state preparation of query register

        items_to_keep: (optional) number of items to return (topmost measurement counts for precision register)

        device: Braket device backend

        shots: (optional) number of measurement shots (default is 1000)

        save_to_pck: (optional) save results to pickle file if True (default is False)
    """

    # get size of precision register and total number of qubits
    number_precision_qubits = len(precision_qubits)
    num_qubits = len(precision_qubits) + len(query_qubits)

    # Define the circuit. Start by copying the query_circuit, then add the QPE:
    circ = query_circuit
    circ.qpe(precision_qubits, query_qubits, unitary)

    # Add desired results_types
    circ.probability()

    # Run the circuit with all zeros input.
    # The query_circuit subcircuit generates the desired input from all zeros.
    # The following code executes the correct device.run call, depending on whether the backend is local or on-demand
    task = device.run(circ, shots=shots)

    # get result for this task
    result = task.result()

    # get metadata
    metadata = result.task_metadata

    # get output probabilities (see result_types above)
    probs_values = result.values[0]

    # get measurement results
    measurements = result.measurements
    measured_qubits = result.measured_qubits
    measurement_counts = result.measurement_counts
    measurement_probabilities = result.measurement_probabilities

    # bitstrings
    format_bitstring = '{0:0' + str(num_qubits) + 'b}'
    bitstring_keys = [format_bitstring.format(ii) for ii in range(2**num_qubits)]

    # QPE postprocessing
    phases_decimal, precision_results_dic = get_qpe_phases(measurement_counts, precision_qubits, items_to_keep)
    eigenvalues = [np.exp(2*np.pi*1j*phase) for phase in phases_decimal]

    # aggregate results
    out = {'circuit': circ,
           'task_metadata': metadata,
           'measurements': measurements,
           'measured_qubits': measured_qubits,
           'measurement_counts': measurement_counts,
           'measurement_probabilities': measurement_probabilities,
           'probs_values': probs_values,
           'bitstring_keys': bitstring_keys,
           'precision_results_dic': precision_results_dic,
           'phases_decimal': phases_decimal,
           'eigenvalues': eigenvalues}

    if save_to_pck:
        # store results: dump output to pickle with timestamp in filename
        time_now = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S')
        results_file = 'results-'+time_now+'.pck'
        pickle.dump(out, open(results_file, "wb"))
        # you can load results as follows
        # out = pickle.load(open(results_file, "rb"))

    return out

REFERENCES

[1] Wikipedia: https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm

[2] Nielsen, Michael A., Chuang, Isaac L. (2010). Quantum Computation and Quantum Information (2nd ed.). Cambridge: Cambridge University Press.

In [44]:
print("Quantum Task Summary")
print(t.quantum_tasks_statistics())
print(
    "Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2).",
)
print(
    f"Estimated cost to run this example: {t.qpu_tasks_cost() + t.simulator_tasks_cost():.2f} USD",
)
Quantum Task Summary
{}
Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2).
Estimated cost to run this example: 0.00 USD

Join the Discussion

Comments (0)

No comments yet. Be the first to share your thoughts!

Indexed by QCR Librarian

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

v1 Latest
Jun 17, 2026
qcr:2606.37510.1

Cite all versions? Use the base QCR ID to always reference the latest version of this entry.

Tools used

Amazon Braket SDK

Keywords

phase-estimation
qpe
braket
eigenvalue
inverse-qft

You may also like5