# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import math
from collections import Counter
from typing import Any, Callable, Dict, List, Tuple
import numpy as np
from braket.circuits import Circuit, circuit
from braket.circuits.qubit_set import QubitSetInput
from braket.devices import Device
from braket.tasks import QuantumTask
[docs]
def quantum_phase_estimation_circuit(
quantum_phase_estimation_circ: Circuit,
precision_qubits: QubitSetInput,
query_qubits: QubitSetInput,
unitary_apply_func: Callable,
) -> Circuit:
"""Adds result type to quantum phase estimation
Args:
quantum_phase_estimation_circ (Circuit): Circuit with query bits prepared
precision_qubits (QubitSetInput): Qubits defining the precision register
query_qubits (QubitSetInput) : Qubits defining the query register
unitary_apply_func (Callable): Function that applies the desired controlled unitary to a
provided circuit using provided control and target qubits
Returns:
Circuit: Implements the Quantum Phase Estimation algorithm with result type
"""
return quantum_phase_estimation_circ.quantum_phase_estimation(
precision_qubits, query_qubits, unitary_apply_func
).probability()
[docs]
@circuit.subroutine(register=True)
def quantum_phase_estimation(
precision_qubits: QubitSetInput,
query_qubits: QubitSetInput,
unitary_apply_func: Callable,
) -> Circuit:
"""Creates the Quantum Phase Estimation circuit using:
1) The first register for precision.
2)The second register for query qubits which hosts the eigenstate and should already be prepared
in its initial state. 3) A function that applies a controlled unitary circuit. This function
accepts a control qubit and the target qubits on which to apply the unitary. Quantum Phase
Estimation will repeatedly apply this function for the target qubits. This is a necessary input
because the controlled unitary needs to be defined in terms of available gates for a given QPU.
Example:
>>> def cnot_apply_func(circ, control_qubit, query_qubits):
... circ.qpe_cnot_unitary(control_qubit, query_qubits)
>>> circ = Circuit().h([2])
>>> circ.quantum_phase_estimation([0, 1], [2], cnot_apply_func)
>>> print(circ)
T : |0|1| 2 |3| 4 |5|
q0 : -H---SWAP---PHASE(-1.57)-H-
| |
q1 : -H-C-SWAP-H-C--------------
|
q2 : -H-X-----------------------
T : |0|1| 2 |3| 4 |5|
Args:
precision_qubits (QubitSetInput): Qubits defining the precision register
query_qubits (QubitSetInput) : Qubits defining the query register
unitary_apply_func (Callable): Function that applies the desired controlled unitary to a
provided circuit using provided control and target qubits
Returns:
Circuit: Circuit object that implements the Quantum Phase Estimation algorithm
"""
quantum_phase_estimation_circ = Circuit()
quantum_phase_estimation_circ.h(precision_qubits)
# Apply controlled unitaries C-U(2^k). Start with the last precision_qubit, end with the first
for ii, qubit in enumerate(reversed(precision_qubits)):
if qubit:
for _ in range(2**ii):
unitary_apply_func(quantum_phase_estimation_circ, qubit, query_qubits)
quantum_phase_estimation_circ.inverse_qft(precision_qubits)
return quantum_phase_estimation_circ
[docs]
def run_quantum_phase_estimation(
circuit: Circuit,
device: Device,
shots: int = 1000,
) -> QuantumTask:
"""Function to run Quantum Phase Estimation algorithm and return measurement counts.
Args:
circuit (Circuit): Quantum Phase Estimation circuit
device (Device): Braket device backend
shots (int) : Number of measurement shots (default is 1000).
Returns:
QuantumTask: Task from running Quantum Phase Estimation
"""
task = device.run(circuit, shots=shots)
return task
[docs]
def get_quantum_phase_estimation_results(
task: QuantumTask,
precision_qubits: QubitSetInput,
query_qubits: QubitSetInput,
verbose: bool = False,
) -> Dict[str, Any]:
"""Function to postprocess results returned by run_quantum_phase_estimation and pretty print
results.
Args:
task (QuantumTask): The task which holds the results for the quantum phase estimation run
precision_qubits (QubitSetInput): Qubits defining the precision register
query_qubits (QubitSetInput) : Qubits defining the query register
verbose (bool) : If True, prints aggregate results (default is False)
Returns:
Dict[str, Any]: aggregate measurement results
"""
result = task.result()
metadata = result.task_metadata
probs_values = result.values[0]
measurements = result.measurements
measured_qubits = result.measured_qubits
measurement_counts = result.measurement_counts
measurement_probabilities = result.measurement_probabilities
num_qubits = len(precision_qubits) + len(query_qubits)
format_bitstring = "{0:0" + str(num_qubits) + "b}"
bitstring_keys = [format_bitstring.format(ii) for ii in range(2**num_qubits)]
if not measurement_counts:
phases_decimal = result.result_types[0].value
precision_results_dict = None
else:
phases_decimal, precision_results_dict = _get_quantum_phase_estimation_phases(
measurement_counts, precision_qubits
)
eigenvalues = [np.exp(2 * np.pi * 1j * phase) for phase in phases_decimal]
eigenvalue_estimates = np.round(eigenvalues, 5)
aggregate_results = {
"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_dict": precision_results_dict,
"phases_decimal": phases_decimal,
"eigenvalues": eigenvalues,
"eigenvalue_estimates": eigenvalue_estimates,
}
if verbose:
print(f"Measurement counts: {measurement_counts}")
print(f"Results in precision register: {precision_results_dict}")
print(f"Quantum phase estimation phase estimates: {phases_decimal}")
print(f"Quantum phase estimation eigenvalue estimates: {eigenvalue_estimates}")
return aggregate_results
def _binary_to_decimal(binary: str) -> float:
"""Helper function to convert binary string (example: '01001') to decimal.
Args:
binary (str): value to convert to decimal fraction
Returns:
float: decimal value
"""
fracDecimal = 0
# Convert fractional part of binary to decimal equivalent
twos = 2
for ii in range(len(binary)):
fracDecimal += (ord(binary[ii]) - ord("0")) / twos
twos *= 2.0
# return fractional part
return fracDecimal
def _get_quantum_phase_estimation_phases(
measurement_counts: Counter, precision_qubits: QubitSetInput
) -> Tuple[List[float], Dict[str, int]]:
"""Get Quantum Phase Estimates phase estimate from measurement_counts for given number of
precision qubits.
Args:
measurement_counts (Counter) : measurement results from a device run
precision_qubits (QubitSetInput): Qubits defining the precision register
Returns:
Tuple[List[float], Dict[str, int]]: decimal phase estimates, precision results
"""
# Aggregate the results (i.e., ignore/trace out the query register qubits):
# First get bitstrings with corresponding counts for precision qubits only
bitstrings_precision_register = [
key[: len(precision_qubits)] for key in measurement_counts.keys()
]
# 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_dict = {key: 0 for key in set(bitstrings_precision_register)}
# 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 = key[: len(precision_qubits)]
# Add these measurement counts to the corresponding key in our new dict
precision_results_dict[count_key] += counts
# get decimal phases from bitstrings
phases_decimal = [_binary_to_decimal(item[0]) for item in precision_results_dict]
return phases_decimal, precision_results_dict
# TODO: Add to qft module once available
# inverse QFT
[docs]
@circuit.subroutine(register=True)
def inverse_qft(qubits: QubitSetInput) -> Circuit:
"""Construct a circuit object corresponding to the inverse Quantum Fourier Transform (QFT)
algorithm, applied to the argument qubits. Does not use recursion to generate the circuit.
Args:
qubits (QubitSetInput): Qubits on which to apply the inverse Quantum Fourier Transform
Returns:
Circuit: Circuit object that implements the inverse Quantum Fourier Transform algorithm
"""
# Instantiate circuit object
qft_circuit = Circuit()
# Fet number of qubits
num_qubits = len(qubits)
# First add SWAP gates to reverse the order of the qubits:
for i in range(math.floor(num_qubits / 2)):
qft_circuit.swap(qubits[i], qubits[-i - 1])
# Start on the last qubit and work to the first.
for k in reversed(range(num_qubits)):
# Apply the controlled rotations, with weights (angles) defined by the distance to the
# control qubit. These angles are the negative of the angle used in the QFT.
# Start on the last qubit and iterate until the qubit after k.
# When num_qubits==1, this loop does not run.
for j in reversed(range(1, num_qubits - k)):
angle = -2 * math.pi / (2 ** (j + 1))
qft_circuit.cphaseshift(qubits[k + j], qubits[k], angle)
# Then add a Hadamard gate
qft_circuit.h(qubits[k])
return qft_circuit