From 910819d78e32ec40d871eec6bbbc3e8a8679ebac Mon Sep 17 00:00:00 2001 From: John Bolt Date: Sat, 1 Feb 2025 10:54:02 -0500 Subject: [PATCH] Feature: Add Hadamard Test with ancilla qubit parameter --- notebooks/textbook/Bells_Inequality.ipynb | 2 +- notebooks/textbook/Hadamard_Test.ipynb | 375 ++++++++++++++++++ .../algorithms/hadamard_test/hadamard_test.md | 7 + .../algorithms/hadamard_test/hadamard_test.py | 51 +++ .../hadamard_test/test_hadamard_test.py | 71 ++++ 5 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 notebooks/textbook/Hadamard_Test.ipynb create mode 100644 src/braket/experimental/algorithms/hadamard_test/hadamard_test.md create mode 100644 src/braket/experimental/algorithms/hadamard_test/hadamard_test.py create mode 100644 test/unit_tests/braket/experimental/algorithms/hadamard_test/test_hadamard_test.py diff --git a/notebooks/textbook/Bells_Inequality.ipynb b/notebooks/textbook/Bells_Inequality.ipynb index 7e21cd16..bce318a9 100644 --- a/notebooks/textbook/Bells_Inequality.ipynb +++ b/notebooks/textbook/Bells_Inequality.ipynb @@ -170,7 +170,7 @@ "# from braket.aws import AwsDevice\n", "# iqm_garnet = AwsDevice(\"arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet\")\n", "# iqm_tasks = run_bell_inequality([circAB, circAC, circBC], iqm_garnet, shots=1000)\n", - "# results, pAB, pAC, pBC = get_bell_inequality_results(iqm_tasks)\n" + "# results, pAB, pAC, pBC = get_bell_inequality_results(iqm_tasks)" ] }, { diff --git a/notebooks/textbook/Hadamard_Test.ipynb b/notebooks/textbook/Hadamard_Test.ipynb new file mode 100644 index 00000000..f390a03e --- /dev/null +++ b/notebooks/textbook/Hadamard_Test.ipynb @@ -0,0 +1,375 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hadamard Test\n", + "---\n", + "\n", + "\n", + "The Hadamard test is a quantum circuit that allows estimation of the real and imaginary parts of the expected value of a unitary operator. It is a fundamental subroutine used in many quantum algorithms. The test works by applying a controlled-unitary operation between an auxiliary qubit and the system of interest, with the measurement statistics of the auxiliary qubit encoding information about the unitary's expectation value. For the original paper, see [1]. For a more modern treatment, see [2].\n", + "\n", + "In this notebook, we explore the Braket implementation of the Hadamard test for estimating the real and imaginary parts of the expected value of a unitary operator.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] R. Cleve, A. Ekert, C. Macchiavello, and M. Mosca (1998). Quantum algorithms revisited. [arXiv:quant-ph/9708016](https://arxiv.org/abs/quant-ph/9708016).\n", + "\n", + "[2] Nielsen, Michael A., Chuang, Isaac L. (2010). Quantum Computation and Quantum Information (2nd ed.). Cambridge: Cambridge University Press." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Description\n", + "\n", + "Given a unitary operator $U$ and qubits $|0\\rangle|\\psi\\rangle$, the Hadamard test is used to estimate the expected value of $U$ on $|\\psi\\rangle$, i.e. $\\langle \\psi | U |\\psi \\rangle$. We will walk through the algorithm step by step. First, we apply a Hadamard gate to $|0\\rangle$, putting the system into the state,\n", + " \n", + "$$\n", + "\\dfrac{|0\\rangle + |1\\rangle}{\\sqrt{2}}|\\psi\\rangle.\n", + "$$ \n", + " \n", + "We then apply the unitary operator $U$ on $|\\psi\\rangle$, controlled on the first qubit, to obtain the state, \n", + "$$\n", + "\\dfrac{|0\\rangle |\\psi\\rangle + |1\\rangle U |\\psi\\rangle}{\\sqrt{2}} .\n", + "$$ \n", + "Then, a Hadamard gate is applied to the first qubit once more, giving the state,\n", + "$$\n", + "\\dfrac{|0\\rangle(|\\psi\\rangle + U |\\psi\\rangle) + |1\\rangle(|\\psi\\rangle - U |\\psi\\rangle)}{2}.\n", + "$$\n", + "\n", + "And this completes the Hadamard test. Now, we get the probability of measuring the first qubit in the state $|0\\rangle$ as\n", + "$$\n", + "\\begin{align*}\n", + "P_0 &= \\bigg\\lVert\\dfrac{|\\psi\\rangle + U|\\psi\\rangle}{2}\\bigg\\rVert^2 \\\\\n", + " &= \\dfrac{\\langle \\psi | \\psi \\rangle + \\langle \\psi | U |\\psi \\rangle + \\langle \\psi | U^\\dagger |\\psi \\rangle + \\langle \\psi | U^\\dagger U |\\psi \\rangle}{4} \\\\\n", + " &= \\dfrac{1 + 2\\langle \\psi | U |\\psi \\rangle + 1}{4}\\\\\n", + " &= \\dfrac{1 + Re\\langle \\psi | U |\\psi \\rangle}{2},\n", + "\\end{align*}\n", + "$$\n", + "allowing us to estimate the expected value of the real part as $ Re(\\langle \\psi | U |\\psi \\rangle) = 2 P_0 - 1$. The algorithm can be modified to estimate the imaginary part by applying a phase shift to the first qubit after the first Hadamard gate, the proof of which is left as an exercise or inspiration for further reading.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run on a local simulator\n", + "\n", + "Braket provides an implementation of the Hadamard test, `hadamard_test_circuit`, for estimating the expected values of both the real and imaginary parts of a given unitary operator, `U`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from notebook_plotting import plot_bitstrings_formatted\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "\n", + "from braket.aws import AwsDevice\n", + "from braket.circuits import Circuit, Qubit\n", + "from braket.devices import LocalSimulator\n", + "from braket.tracking import Tracker\n", + "\n", + "from braket.experimental.algorithms.hadamard_test.hadamard_test import hadamard_test_circuit\n", + "\n", + "tracker = Tracker().start() # to track Braket costs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For our controlled unitary operator, we will use the Pauli-Z gate, which is represented by the matrix,\n", + "$$\n", + "\\begin{bmatrix}\n", + "1 & 0 \\\\\n", + "0 & -1\n", + "\\end{bmatrix}\n", + "$$\n", + "Though Braket has built-in support for the Pauli-Z gate, we will use the `unitary` method to apply the gate to the qubit. Consider playing around with different matrices to test your understanding of the algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "T : │ 0 │\n", + " ┌───┐ \n", + "q0 : ─┤ U ├─\n", + " └───┘ \n", + "T : │ 0 │\n" + ] + } + ], + "source": [ + "pauli_z = np.array([[1, 0], [0, -1]])\n", + "controlled_unitary = Circuit().unitary([0], pauli_z, \"U\")\n", + "ancilla = Qubit(0)\n", + "print(controlled_unitary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Real Part Estimation" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "T : │ 0 │ 1 │ 2 │\n", + " ┌───┐ ┌───┐ \n", + "q0 : ─┤ H ├───●───┤ H ├─\n", + " └───┘ │ └───┘ \n", + " ┌─┴─┐ \n", + "q1 : ───────┤ U ├───────\n", + " └───┘ \n", + "T : │ 0 │ 1 │ 2 │\n" + ] + } + ], + "source": [ + "ht_circuit = hadamard_test_circuit(ancilla, controlled_unitary)\n", + "print(ht_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, Braket will measure all qubits. Since the Hadamard test only requires measuring the ancilla qubits, we will explicitly add a measurement instruction to the circuit to avoid unnecessary measurements." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "T : │ 0 │ 1 │ 2 │ 3 │\n", + " ┌───┐ ┌───┐ ┌───┐ \n", + "q0 : ─┤ H ├───●───┤ H ├─┤ M ├─\n", + " └───┘ │ └───┘ └───┘ \n", + " ┌─┴─┐ \n", + "q1 : ───────┤ U ├─────────────\n", + " └───┘ \n", + "T : │ 0 │ 1 │ 2 │ 3 │\n" + ] + } + ], + "source": [ + "ht_circuit.measure(0)\n", + "print(ht_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can run the circuit on a local simulator. Take note of the small bit of algebra we need to extract the real part, which can also be seen in the description of the algorithm above." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0\n" + ] + } + ], + "source": [ + "device = LocalSimulator()\n", + "task = device.run(ht_circuit, shots=1000)\n", + "\n", + "probs = task.result().measurement_probabilities\n", + "p_zero = probs.get('0', 0)\n", + "\n", + "real_part = 2 * p_zero - 1\n", + "\n", + "print(real_part)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imaginary Part Estimation\n", + "\n", + "Braket's `hadamard_test_circuit` supports the real part estimation by default, but we can also estimate the imaginary part by setting the `component` argument to `'imaginary'`. We will use the same controlled unitary operator as before." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "T : │ 0 │ 1 │ 2 │ 3 │ 4 │\n", + " ┌───┐ ┌───┐ ┌───┐ ┌───┐ \n", + "q0 : ─┤ H ├─┤ S ├───●───┤ H ├─┤ M ├─\n", + " └───┘ └───┘ │ └───┘ └───┘ \n", + " ┌─┴─┐ \n", + "q1 : ─────────────┤ U ├─────────────\n", + " └───┘ \n", + "T : │ 0 │ 1 │ 2 │ 3 │ 4 │\n" + ] + } + ], + "source": [ + "ht_circuit = hadamard_test_circuit(Qubit(0), controlled_unitary, component='imaginary')\n", + "ht_circuit.measure(0)\n", + "print(ht_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The only difference in the circuit is the addition of a phase shift gate to the first qubit, which allows us to estimate the expected value of the imaginary part." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.01200000000000001\n" + ] + } + ], + "source": [ + "device = LocalSimulator()\n", + "task = device.run(ht_circuit, shots=1000)\n", + "\n", + "probs = task.result().measurement_probabilities\n", + "p_zero = probs.get('0', 0)\n", + "\n", + "imaginary_part = 2 * p_zero - 1\n", + "\n", + "print(imaginary_part)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run on a QPU \n", + "\n", + "To run the Hadamard test on a QPU, we replace the LocalSimulator with an AwsDevice. The cost to run this experiment is $0.3 per task and $0.00145 per shot on the IQM Garnet device, that totals $1.75 USD. Because Garnet does not support arbitrary unitary operators, we will directly apply the Pauli-Z gate in this example." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# controlled_unitary = Circuit().z(0)\n", + "# ht_circuit = hadamard_test_circuit(controlled_unitary)\n", + "# ht_circuit.measure(0)\n", + "# print(ht_circuit)\n", + "\n", + "# device = AwsDevice(\"arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet\")\n", + "# task = device.run(ht_circuit, shots=1000)\n", + "\n", + "# counts = task.result().measurement_counts\n", + "# p_zero = counts.get('0', 0) / 1000\n", + "\n", + "# real_part = 2 * p_zero - 1\n", + "\n", + "# print(real_part)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Task Summary\n", + "{} \n", + "\n", + "Estimated cost to run this example: 0.00 USD\n" + ] + } + ], + "source": [ + "print(\"Task Summary\")\n", + "print(f\"{tracker.quantum_tasks_statistics()} \\n\")\n", + "print(\n", + " f\"Estimated cost to run this example: {tracker.qpu_tasks_cost() + tracker.simulator_tasks_cost():.2f} USD\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/braket/experimental/algorithms/hadamard_test/hadamard_test.md b/src/braket/experimental/algorithms/hadamard_test/hadamard_test.md new file mode 100644 index 00000000..d15375be --- /dev/null +++ b/src/braket/experimental/algorithms/hadamard_test/hadamard_test.md @@ -0,0 +1,7 @@ +The Hadamard test is a quantum circuit that allows estimation of the real and imaginary parts of the expected value of a unitary operator. It is a fundamental subroutine used in many quantum algorithms, including quantum phase estimation where it helps extract eigenvalue information through controlled operations. The test works by applying a controlled-unitary operation between an auxiliary qubit and the system of interest, with the measurement statistics of the auxiliary qubit encoding information about the unitary's expectation value. + + diff --git a/src/braket/experimental/algorithms/hadamard_test/hadamard_test.py b/src/braket/experimental/algorithms/hadamard_test/hadamard_test.py new file mode 100644 index 00000000..b28640ff --- /dev/null +++ b/src/braket/experimental/algorithms/hadamard_test/hadamard_test.py @@ -0,0 +1,51 @@ +# 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. + +from braket.circuits import Circuit, circuit, Instruction, Qubit + + +@circuit.subroutine(register=True) +def hadamard_test_circuit(ancilla_qubit: Qubit, controlled_unitary: Circuit, component: str = 'real') -> Circuit: + """Implements the Hadamard test circuit for estimating real or imaginary parts + of the expected value of a unitary operator. + + Args: + ancilla_qubit (Qubit): The ancilla qubit used as control + controlled_unitary (Circuit): The unitary operation to be controlled + component (str): Either 'real' or 'imaginary' to determine which component to estimate + + Returns: + Circuit: The complete Hadamard test circuit + """ + if component not in ['real', 'imaginary']: + raise ValueError("Component must be either 'real' or 'imaginary'") + + circ = Circuit() + + circ.h(ancilla_qubit) + if component == 'imaginary': + circ.s(ancilla_qubit).adjoint() + + # Add control qubit to the unitary circuit + for inst in controlled_unitary.instructions: + targets = [q + 1 for q in inst.target] + controlled_inst = Instruction( + operator=inst.operator, + target=targets, + control=ancilla_qubit + ) + circ.add_instruction(controlled_inst) + + circ.h(ancilla_qubit) + + return circ diff --git a/test/unit_tests/braket/experimental/algorithms/hadamard_test/test_hadamard_test.py b/test/unit_tests/braket/experimental/algorithms/hadamard_test/test_hadamard_test.py new file mode 100644 index 00000000..e203f66f --- /dev/null +++ b/test/unit_tests/braket/experimental/algorithms/hadamard_test/test_hadamard_test.py @@ -0,0 +1,71 @@ +import numpy as np +import pytest +from braket.circuits import Circuit, ResultType, Qubit +from braket.devices import LocalSimulator + +from braket.experimental.algorithms.hadamard_test import hadamard_test_circuit + + +def test_hadamard_test_real(): + unitary = Circuit().h(0) + ancilla = Qubit(0) + + test_circuit = hadamard_test_circuit(ancilla, unitary, component='real') + test_circuit.measure(ancilla) + + device = LocalSimulator() + task = device.run(test_circuit, shots=1000) + + probs = task.result().measurement_probabilities + p_zero = probs.get('0', 0) + real_part = 2 * p_zero - 1 + + assert np.isclose(real_part, 1/np.sqrt(2), atol=0.1) + + +def test_hadamard_test_imaginary(): + unitary = Circuit().s(0) + ancilla = Qubit(0) + + test_circuit = hadamard_test_circuit(ancilla, unitary, component='imaginary') + test_circuit.measure(ancilla) + + device = LocalSimulator() + task = device.run(test_circuit, shots=10000) + + probs = task.result().measurement_probabilities + p_zero = probs.get('0', 0) + imag_part = 2 * p_zero - 1 + + assert np.isclose(imag_part, 0.0, atol=0.1) + + +def test_hadamard_test_identity(): + unitary = Circuit().i(0) + ancilla = Qubit(0) + + real_circuit = hadamard_test_circuit(ancilla, unitary, component='real') + imag_circuit = hadamard_test_circuit(ancilla, unitary, component='imaginary') + real_circuit.measure(ancilla) + imag_circuit.measure(ancilla) + + device = LocalSimulator() + + real_task = device.run(real_circuit, shots=1000) + real_probs = real_task.result().measurement_probabilities + p_zero_real = real_probs.get('0', 0) + real_part = 2 * p_zero_real - 1 + + imag_task = device.run(imag_circuit, shots=1000) + imag_probs = imag_task.result().measurement_probabilities + p_zero_imag = imag_probs.get('0', 0) + imag_part = 2 * p_zero_imag - 1 + + assert np.isclose(real_part, 1.0, atol=0.1) + assert np.isclose(imag_part, 0.0, atol=0.1) + +@pytest.mark.xfail(raises=ValueError) +def test_hadamard_test_invalid_component(): + unitary = Circuit().h(0) + ancilla = Qubit(0) + hadamard_test_circuit(ancilla, unitary, component='invalid')