From af0afaa60f8efa98002eaa03d16f1763e8a166c3 Mon Sep 17 00:00:00 2001 From: Becky Dimock Date: Thu, 14 May 2026 11:40:13 -0500 Subject: [PATCH 01/13] Set up Box link --- scripts/config/api-html-artifacts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/config/api-html-artifacts.json b/scripts/config/api-html-artifacts.json index b4ef47fc1e6..9dae83389d6 100644 --- a/scripts/config/api-html-artifacts.json +++ b/scripts/config/api-html-artifacts.json @@ -90,6 +90,7 @@ "0.3": "https://ibm.box.com/shared/static/qiskfdazhq1dpxcl9b627x9yi60gncb4.zip" }, "qiskit-addon-aqc-tensor": { + "0.3": "https://ibm.box.com/shared/static/uzw1mzmypuqf2one830vdrvy27x2brvu.zip", "0.2": "https://ibm.box.com/shared/static/dhuyxtri674xvb3oky5rg019bd1a44hf.zip", "0.1": "https://ibm.box.com/shared/static/jnxlji7b6cguhbvp1txdi2vhym38wcnc.zip" }, From fac2f7880c12c07cc75420ebe38f3c84b73a04c6 Mon Sep 17 00:00:00 2001 From: Becky Dimock Date: Wed, 20 May 2026 10:03:49 -0500 Subject: [PATCH 02/13] Changes in notebook files --- docs/guides/debug-qiskit-runtime-jobs.ipynb | 1371 ++--- ...function-template-chemistry-workflow.ipynb | 3535 +++++------ ...tion-template-hamiltonian-simulation.ipynb | 2885 ++++----- docs/guides/ibm-circuit-function.ipynb | 725 +-- docs/guides/sampler-options.ipynb | 1123 ++-- docs/guides/serverless-first-program.ipynb | 876 +-- docs/guides/serverless-manage-resources.ipynb | 1399 ++--- .../advanced-techniques-for-qaoa.ipynb | 2858 ++++----- .../ai-transpiler-introduction.ipynb | 2343 +++---- ...antum-compilation-for-time-evolution.ipynb | 3772 ++++++------ ...-for-hamiltonian-simulation-circuits.ipynb | 3277 +++++----- docs/tutorials/long-range-entanglement.ipynb | 3129 +++++----- .../nishimori-phase-transition.ipynb | 1435 ++--- .../tutorials/operator-back-propagation.ipynb | 1590 ++--- .../probabilistic-error-amplification.ipynb | 2692 ++++---- .../tutorials/projected-quantum-kernels.ipynb | 3012 ++++----- find_long_comments.py | 139 + fix_long_comments.py | 232 + .../quantum-chem-with-vqe/geometry.ipynb | 1659 ++--- .../krylov.ipynb | 5444 +++++++++-------- .../sqd-implementation.ipynb | 2013 +++--- .../vqe.ipynb | 2182 +++---- .../data-encoding.ipynb | 3307 +++++----- .../introduction.ipynb | 793 +-- .../quantum-kernel-methods.ipynb | 2587 ++++---- .../quantum-machine-learning/qvc-qnn.ipynb | 4067 ++++++------ .../symmetric-key-cryptography.ipynb | 1302 ++-- .../bits-gates-and-circuits.ipynb | 3999 ++++++------ .../error-mitigation.ipynb | 1946 +++--- .../grovers-algorithm.ipynb | 2149 +++---- .../hardware.ipynb | 1189 ++-- .../quantum-circuit-optimization.ipynb | 4315 ++++++------- .../teleportation.ipynb | 3722 +++++------ .../cost-functions.ipynb | 2999 ++++----- .../examples-and-applications.ipynb | 4715 +++++++------- .../computer-science/deutsch-jozsa.ipynb | 2201 +++---- .../modules/computer-science/grovers.ipynb | 2952 ++++----- .../quantum-key-distribution.ipynb | 2730 ++++----- .../quantum-teleportation.ipynb | 2216 +++---- learning/modules/computer-science/vqe.ipynb | 3815 ++++++------ .../bells-inequality-with-qiskit.ipynb | 2732 +++++---- .../exploring-uncertainty-with-qiskit.ipynb | 2832 ++++----- .../get-started-with-qiskit.ipynb | 2523 ++++---- ...ern-gerlach-measurements-with-qiskit.ipynb | 2891 ++++----- .../superposition-with-qiskit.ipynb | 2463 ++++---- long_comments_report.json | 81 + 46 files changed, 56402 insertions(+), 55815 deletions(-) create mode 100644 find_long_comments.py create mode 100644 fix_long_comments.py create mode 100644 long_comments_report.json diff --git a/docs/guides/debug-qiskit-runtime-jobs.ipynb b/docs/guides/debug-qiskit-runtime-jobs.ipynb index 2a9969ccd60..1e1ae2d08e5 100644 --- a/docs/guides/debug-qiskit-runtime-jobs.ipynb +++ b/docs/guides/debug-qiskit-runtime-jobs.ipynb @@ -1,685 +1,686 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c52e7bba-1230-4974-8e86-2dbe8f6f219b", - "metadata": {}, - "source": [ - "---\n", - "title: Debug Qiskit Runtime jobs\n", - "description: Use the Qiskit Runtime Debugging tools module and `Neat` class to debug and analyze jobs.\n", - "---\n", - "\n", - "\n", - "# Debug Qiskit Runtime jobs\n", - "{/* cspell:ignore ZIIIII, IZIIII,IIZIII, IIIZII, IIIIZI, IIIIIZ, rdiff */}" - ] - }, - { - "cell_type": "markdown", - "id": "d0599f3e", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [ - "{/*\n", - " DO NOT EDIT THIS CELL!!!\n", - " This cell's content is generated automatically by a script. Anything you add\n", - " here will be removed next time the notebook is run. To add new content, create\n", - " a new cell before or after this one.\n", - "*/}\n", - "\n", - "\n", - "\n", - "\n", - "The code on this page was developed using the following requirements.\n", - "We recommend using these versions or newer.\n", - "\n", - "```\n", - "qiskit[all]~=2.4.0\n", - "qiskit-ibm-runtime~=0.46.1\n", - "qiskit-aer~=0.17\n", - "```\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "80d0a1da-8d98-49e1-9cb0-0006093bf44c", - "metadata": {}, - "source": [ - "You can use the `Neat` class to analyze the noise impact on an Estimator workload. For syntax verification, use [Local testing mode](/docs/guides/local-testing-mode)." - ] - }, - { - "cell_type": "markdown", - "id": "b08f0589", - "metadata": {}, - "source": [ - "## `Neat` class usage\n", - "\n", - "Before submitting a resource-intensive Qiskit Runtime workload to execute on hardware, you can use the Qiskit Runtime [`Neat` (Noisy Estimator Analyzer Tool)](/docs/api/qiskit-ibm-runtime/debug-tools-neat#neat) class to verify that your Estimator workload is set up correctly, is likely to return accurate results, uses the most appropriate options for the specified problem, and more.\n", - "\n", - "`Neat` Cliffordizes the input circuits for efficient simulation, while retaining its structure and depth. Clifford circuits suffer similar levels of noise and are a good proxy for studying the original circuit of interest." - ] - }, - { - "cell_type": "markdown", - "id": "0dc5bf2a-e536-4141-a77c-0ee407cbd9b2", - "metadata": {}, - "source": [ - "First, import the relevant packages and [authenticate to the Qiskit Runtime service](/docs/guides/cloud-setup)." - ] - }, - { - "cell_type": "markdown", - "id": "d653e186-7ec3-4f1b-b0e9-b322055dd6c8", - "metadata": {}, - "source": [ - "### Prepare the environment" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "2f28c824-3158-43e6-ab3c-fd96c31859f0", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import random\n", - "\n", - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator\n", - "from qiskit_ibm_runtime.debug_tools import Neat\n", - "\n", - "from qiskit_aer.noise import NoiseModel, depolarizing_error" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a45a6d9e-de39-4586-8395-a7f580f0e0dc", - "metadata": {}, - "outputs": [], - "source": [ - "# Choose the least busy backend\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "# Generate a preset pass manager\n", - "# This will be used to convert the abstract circuit to an equivalent Instruction Set Architecture (ISA) circuit.\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=0)\n", - "\n", - "# Set the random seed\n", - "random.seed(10)" - ] - }, - { - "cell_type": "markdown", - "id": "67572a70-da01-40fe-b299-b5599561164a", - "metadata": {}, - "source": [ - "### Initialize a target circuit\n", - "\n", - "Consider a six-qubit circuit that has the following properties:\n", - "\n", - "* Alternates between random `RZ` rotations and layers of `CNOT` gates.\n", - "* Has a mirror structure, that is, it applies a unitary `U` followed by its inverse." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "df19af55-897d-4b1f-baf8-fac2641ae87d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def generate_circuit(n_qubits, n_layers):\n", - " r\"\"\"\n", - " A function to generate a pseudo-random a circuit with ``n_qubits`` qubits and\n", - " ``2*n_layers`` entangling layers of the type used in this notebook.\n", - " \"\"\"\n", - " # An array of random angles\n", - " angles = [\n", - " [random.random() for q in range(n_qubits)] for s in range(n_layers)\n", - " ]\n", - "\n", - " qc = QuantumCircuit(n_qubits)\n", - " qubits = list(range(n_qubits))\n", - "\n", - " # do random circuit\n", - " for layer in range(n_layers):\n", - " # rotations\n", - " for q_idx, qubit in enumerate(qubits):\n", - " qc.rz(angles[layer][q_idx], qubit)\n", - "\n", - " # cx gates\n", - " control_qubits = (\n", - " qubits[::2] if layer % 2 == 0 else qubits[1 : n_qubits - 1 : 2]\n", - " )\n", - " for qubit in control_qubits:\n", - " qc.cx(qubit, qubit + 1)\n", - "\n", - " # undo random circuit\n", - " for layer in range(n_layers)[::-1]:\n", - " # cx gates\n", - " control_qubits = (\n", - " qubits[::2] if layer % 2 == 0 else qubits[1 : n_qubits - 1 : 2]\n", - " )\n", - " for qubit in control_qubits:\n", - " qc.cx(qubit, qubit + 1)\n", - "\n", - " # rotations\n", - " for q_idx, qubit in enumerate(qubits):\n", - " qc.rz(-angles[layer][q_idx], qubit)\n", - "\n", - " return qc\n", - "\n", - "\n", - "# Generate a random circuit\n", - "qc = generate_circuit(6, 3)\n", - "# Convert the abstract circuit to an equivalent ISA circuit.\n", - "isa_qc = pm.run(qc)\n", - "\n", - "qc.draw(\"mpl\", idle_wires=0)" - ] - }, - { - "cell_type": "markdown", - "id": "0167329b-c6a6-4b2c-98fc-bf9aba9b7ee6", - "metadata": {}, - "source": [ - "Choose single-Pauli `Z` operators as observables and use them to initialize the primitive unified blocs (PUBs)." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "830b1dcc-2669-46cc-bff8-01a96a05c6ab", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observables: ['ZIIIII', 'IZIIII', 'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']\n" - ] - } - ], - "source": [ - "# Initialize the observables\n", - "obs = [\"ZIIIII\", \"IZIIII\", \"IIZIII\", \"IIIZII\", \"IIIIZI\", \"IIIIIZ\"]\n", - "print(f\"Observables: {obs}\")\n", - "\n", - "# Map the observables to the backend's layout\n", - "isa_obs = [SparsePauliOp(o).apply_layout(isa_qc.layout) for o in obs]\n", - "\n", - "# Initialize the PUBs, which consist of six-qubit circuits with `n_layers` 1, ..., 6\n", - "all_n_layers = [1, 2, 3, 4, 5, 6]\n", - "\n", - "pubs = [(pm.run(generate_circuit(6, n)), isa_obs) for n in all_n_layers]" - ] - }, - { - "cell_type": "markdown", - "id": "2a49fc84-0c82-4cbb-a557-6e676e57c9fa", - "metadata": {}, - "source": [ - "### Cliffordize the circuits\n", - "\n", - "The previously defined PUB circuits are not Clifford, which makes them difficult to simulate classically. However, you can use the `Neat` [`to_clifford`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#to_clifford) method to map them to Clifford circuits for more efficient simulation. The [`to_clifford`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#to_clifford) method is a wrapper around the [`ConvertISAToClifford`](/docs/api/qiskit-ibm-runtime/transpiler-passes-convert-isa-to-clifford) transpiler pass, which can also be used independently. In particular, it replaces non-Clifford single-qubit gates in the original circuit with Clifford single-qubit gates, but it does not mutate the two-qubit gates, number of qubits, or circuit depth.\n", - "\n", - "See [Efficient simulation of stabilizer circuits with Qiskit Aer primitives](/docs/guides/simulate-stabilizer-circuits) for more information about Clifford circuit simulation." - ] - }, - { - "cell_type": "markdown", - "id": "7a86d99e-4431-4d62-8227-c49d17856369", - "metadata": {}, - "source": [ - "First, initialize `Neat`." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "4b5bbd4c-bd7f-4679-9348-d41da74d26eb", - "metadata": {}, - "outputs": [], - "source": [ - "# You could specify a custom `NoiseModel` here. If `None`, `Neat`\n", - "# pulls the noise model from the given backend\n", - "noise_model = None\n", - "\n", - "# Initialize `Neat`\n", - "analyzer = Neat(backend, noise_model)" - ] - }, - { - "cell_type": "markdown", - "id": "b740dcdf-660e-41e2-b5e6-e8cc288af38b", - "metadata": {}, - "source": [ - "Next, Cliffordize the PUBs." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3ad78f41-a2f8-4381-826a-ae728e081ad6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "clifford_pubs = analyzer.to_clifford(pubs)\n", - "\n", - "clifford_pubs[0].circuit.draw(\"mpl\", idle_wires=0)" - ] - }, - { - "cell_type": "markdown", - "id": "83c3ff81-9f18-43eb-ba6e-57c5ef3d118f", - "metadata": {}, - "source": [ - "## Application 1: Analyze the impact of noise on the circuit outputs\n", - "\n", - "This example shows how to use `Neat` to study the impact of different noise models on PUBs as a function of circuit depth by running simulations in both ideal ([`ideal_sim`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#ideal_sim)) and noisy ([`noisy_sim`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#noisy_sim)) conditions. This can be useful to set up expectations on the quality of the experimental results before running a job on a QPU. To learn more about noise models, see [Exact and noisy simulation with Qiskit Aer primitives](/docs/guides/simulate-with-qiskit-aer#exact-and-noisy-simulation-with-qiskit-aer-primitives).\n", - "\n", - "The simulated results support mathematical operations, and can therefore be compared with each other (or with experimental results) to calculate figures of merit.\n", - "\n", - "\n", - "A QPU can be affected by different kinds of noise. The Qiskit Aer noise model used here only simulates some of them and therefore is likely to be less severe than the noise on a real QPU.\n", - "\n", - "For details on what errors are included when initializing a noise model from a QPU, see the Aer [`NoiseModel`](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.NoiseModel.html#qiskit_aer.noise.NoiseModel.from_backend) API reference.\n", - "\n", - "\n", - "Begin by performing ideal and noisy classical simulations." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "23859a99-2455-460e-98ea-17b36ea59c36", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ideal results:\n", - " NeatResult([NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.]))])\n", - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Noisy results:\n", - " NeatResult([NeatPubResult(vals=array([0.98242188, 0.984375 , 0.98828125, 0.99023438, 0.96484375,\n", - " 0.97265625])), NeatPubResult(vals=array([0.96875 , 0.97070312, 0.98046875, 0.98828125, 0.96484375,\n", - " 0.984375 ])), NeatPubResult(vals=array([0.94140625, 0.94726562, 0.92773438, 0.93164062, 0.93164062,\n", - " 0.95703125])), NeatPubResult(vals=array([0.91015625, 0.90429688, 0.90039062, 0.93164062, 0.94140625,\n", - " 0.953125 ])), NeatPubResult(vals=array([0.875 , 0.88476562, 0.88476562, 0.8984375 , 0.91601562,\n", - " 0.91210938])), NeatPubResult(vals=array([0.88476562, 0.89453125, 0.86523438, 0.91015625, 0.86914062,\n", - " 0.91992188]))])\n", - "\n" - ] - } - ], - "source": [ - "# Perform a noiseless simulation\n", - "ideal_results = analyzer.ideal_sim(clifford_pubs)\n", - "print(f\"Ideal results:\\n {ideal_results}\\n\")\n", - "\n", - "# Perform a noisy simulation with the backend's noise model\n", - "noisy_results = analyzer.noisy_sim(clifford_pubs)\n", - "print(f\"Noisy results:\\n {noisy_results}\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "a000a77a-0285-4b72-a69f-8f144f2c2a80", - "metadata": {}, - "source": [ - "Next, apply mathematical operations to compute the absolute difference. The remainder of the guide uses the absolute difference as a figure of merit to compare ideal results with noisy or experimental results, but similar figures of merit can be set up.\n", - "\n", - "The absolute difference shows that the impact of noise grows with the circuits' sizes." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "cd61e437-bd2f-4349-a667-7edab51c4a6e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mean absolute difference between ideal and noisy results for circuits with 1 layers:\n", - " 1.95%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 2 layers:\n", - " 2.38%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 3 layers:\n", - " 6.06%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 4 layers:\n", - " 7.65%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 5 layers:\n", - " 10.48%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 6 layers:\n", - " 10.94%\n", - "\n" - ] - } - ], - "source": [ - "# Figure of merit: Absolute difference\n", - "def rdiff(res1, re2):\n", - " r\"\"\"The absolute difference between `res1` and re2`.\n", - "\n", - " --> The closer to `0`, the better.\n", - " \"\"\"\n", - " d = abs(res1 - re2)\n", - " return np.round(d.vals * 100, 2)\n", - "\n", - "\n", - "for idx, (ideal_res, noisy_res) in enumerate(\n", - " zip(ideal_results, noisy_results)\n", - "):\n", - " vals = rdiff(ideal_res, noisy_res)\n", - "\n", - " # Print the mean absolute difference for the observables\n", - " mean_vals = np.round(np.mean(vals), 2)\n", - " print(\n", - " f\"Mean absolute difference between ideal and noisy results for circuits with {all_n_layers[idx]} layers:\\n {mean_vals}%\\n\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "7abcd001-9eac-4015-97a3-d6250ea4b667", - "metadata": {}, - "source": [ - "You can follow these rough and simplified guidelines to improve circuits of this type:\n", - "\n", - "- If the mean absolute difference is greater than 90%, mitigation will likely not help.\n", - "- If the mean absolute difference is less than 90%, [Probabilistic Error Amplification (PEA)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-amplification-pea) will likely be able to improve the results.\n", - "- If the mean absolute difference is less than 80%, [ZNE with gate folding](/docs/guides/error-mitigation-and-suppression-techniques#zero-noise-extrapolation-zne) will also likely be able to improve the results.\n", - "\n", - "Because all of the absolute differences above are less than 90%, applying PEA to the original circuit will hopefully improve the quality of its results." - ] - }, - { - "cell_type": "markdown", - "id": "c64c936b-5b8f-4fd2-861d-8b1ded2a0ad4", - "metadata": {}, - "source": [ - "You can specify different noise models in the analyzer. The following example performs the same test but adds a custom noise model." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "0835c562-55c9-4dbe-879e-7271f8bed280", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mean absolute difference between ideal and noisy results for circuits with 1 layers:\n", - " 0.0%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 2 layers:\n", - " 0.0%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 3 layers:\n", - " 0.0%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 4 layers:\n", - " 0.0%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 5 layers:\n", - " 0.0%\n", - "\n", - "Mean absolute difference between ideal and noisy results for circuits with 6 layers:\n", - " 0.0%\n", - "\n" - ] - } - ], - "source": [ - "# Set up a noise model with strength 0.02 on every two-qubit gate\n", - "noise_model = NoiseModel()\n", - "for qubits in backend.coupling_map:\n", - " noise_model.add_quantum_error(\n", - " depolarizing_error(0.02, 2), [\"ecr\", \"cx\"], qubits\n", - " )\n", - "\n", - "# Update the analyzer's noise model\n", - "analyzer.noise_model = noise_model\n", - "\n", - "# Perform a noiseless simulation\n", - "ideal_results = analyzer.ideal_sim(clifford_pubs)\n", - "\n", - "# Perform a noisy simulation with the backend's noise model\n", - "noisy_results = analyzer.noisy_sim(clifford_pubs)\n", - "\n", - "# Compare the results\n", - "for idx, (ideal_res, noisy_res) in enumerate(\n", - " zip(ideal_results, noisy_results)\n", - "):\n", - " values = rdiff(ideal_res, noisy_res)\n", - "\n", - " # Print the mean absolute difference for the observables\n", - " mean_values = np.round(np.mean(values), 2)\n", - " print(\n", - " f\"Mean absolute difference between ideal and noisy results for circuits with {all_n_layers[idx]} layers:\\n {mean_values}%\\n\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "f2408ca9-3e3c-4a2f-a99a-ce413d5d470f", - "metadata": {}, - "source": [ - "As shown, given a noise model, you can try to quantify the impact of noise on the (Cliffordized version of the) PUBs of interest before running them on a QPU." - ] - }, - { - "cell_type": "markdown", - "id": "ddd6da5f-4e84-4bf4-aaeb-0403f21275db", - "metadata": {}, - "source": [ - "## Application 2: Benchmark different strategies\n", - "\n", - "This example uses `Neat` to help identify the best options for your PUBs. To do so, consider running an estimation problem with PEA, which cannot be simulated with `qiskit_aer`. You can use `Neat` to help determine which noise amplification factors will work best, then use those factors when running the original experiment on a QPU." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "358bb82a-4bc9-46c2-98a0-e745ffc6788f", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate a circuit with six qubits and six layers\n", - "isa_qc = pm.run(generate_circuit(6, 3))\n", - "\n", - "# Use the same observables as previously\n", - "pubs = [(isa_qc, isa_obs)]\n", - "clifford_pubs = analyzer.to_clifford(pubs)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "5774cb3f-c999-4242-a83a-7dcc0c57510b", - "metadata": {}, - "outputs": [], - "source": [ - "noise_factors = [\n", - " [1, 1.1],\n", - " [1, 1.1, 1.2],\n", - " [1, 1.5, 2],\n", - " [1, 1.5, 2, 2.5, 3],\n", - " [1, 4],\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "0b9900e6-84fe-4776-9bb5-08c6c729be29", - "metadata": {}, - "outputs": [], - "source": [ - "# Run the PUBs on a QPU\n", - "estimator = Estimator(backend)\n", - "estimator.options.default_shots = 100000\n", - "estimator.options.twirling.enable_gates = True\n", - "estimator.options.twirling.enable_measure = True\n", - "estimator.options.twirling.shots_per_randomization = 100\n", - "estimator.options.resilience.measure_mitigation = True\n", - "estimator.options.resilience.zne_mitigation = True\n", - "estimator.options.resilience.zne.amplifier = \"pea\"\n", - "\n", - "jobs = []\n", - "for factors in noise_factors:\n", - " estimator.options.resilience.zne.noise_factors = factors\n", - " jobs.append(estimator.run(clifford_pubs))\n", - "\n", - "results = [job.result() for job in jobs]" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "16c18377-059a-4751-9ab1-afee0ed5b089", - "metadata": {}, - "outputs": [], - "source": [ - "# Perform a noiseless simulation\n", - "ideal_results = analyzer.ideal_sim(clifford_pubs)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "7db531a1-c417-4d5b-bdc3-7a4ad3385fd4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mean absolute difference for factors [1, 1.1]:\n", - " 2.42%\n", - "\n", - "Mean absolute difference for factors [1, 1.1, 1.2]:\n", - " 11.34%\n", - "\n", - "Mean absolute difference for factors [1, 1.5, 2]:\n", - " 3.68%\n", - "\n", - "Mean absolute difference for factors [1, 1.5, 2, 2.5, 3]:\n", - " 4.77%\n", - "\n", - "Mean absolute difference for factors [1, 4]:\n", - " 3.61%\n", - "\n" - ] - } - ], - "source": [ - "# Look at the mean absolute difference to quickly tell the best choice for your options\n", - "for factors, res in zip(noise_factors, results):\n", - " d = rdiff(ideal_results[0], res[0])\n", - " print(\n", - " f\"Mean absolute difference for factors {factors}:\\n {np.round(np.mean(d), 2)}%\\n\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "0c37ef7b-df56-4f5f-9e11-10f209f105f9", - "metadata": {}, - "source": [ - "The result with the smallest difference suggests which options to choose." - ] - }, - { - "cell_type": "markdown", - "id": "2530a3e9-21a6-4841-9449-fe181c54aca4", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - " - Read an overview of [Qiskit debugging tools](/docs/guides/debugging-tools).\n", - " - Learn about [Exact and noisy simulation with Qiskit Aer primitives](/docs/guides/simulate-with-qiskit-aer).\n", - " - Learn about [available Qiskit Runtime options](/docs/guides/runtime-options-overview).\n", - " - Learn about [Error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques).\n", - " - Visit the [Transpile with pass managers](transpile-with-pass-managers) topic.\n", - " - Learn [how to transpile circuits](/docs/guides/circuit-transpilation-settings#compare-transpiler-settings) as part of Qiskit patterns workflows using Qiskit Runtime.\n", - " - Review the [Debugging tools API documentation](/docs/api/qiskit-ibm-runtime/debug-tools).\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c52e7bba-1230-4974-8e86-2dbe8f6f219b", + "metadata": {}, + "source": [ + "---\n", + "title: Debug Qiskit Runtime jobs\n", + "description: Use the Qiskit Runtime Debugging tools module and `Neat` class to debug and analyze jobs.\n", + "---\n", + "\n", + "\n", + "# Debug Qiskit Runtime jobs\n", + "{/* cspell:ignore ZIIIII, IZIIII,IIZIII, IIIZII, IIIIZI, IIIIIZ, rdiff */}" + ] + }, + { + "cell_type": "markdown", + "id": "d0599f3e", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [ + "{/*\n", + " DO NOT EDIT THIS CELL!!!\n", + " This cell's content is generated automatically by a script. Anything you add\n", + " here will be removed next time the notebook is run. To add new content, create\n", + " a new cell before or after this one.\n", + "*/}\n", + "\n", + "\n", + "\n", + "\n", + "The code on this page was developed using the following requirements.\n", + "We recommend using these versions or newer.\n", + "\n", + "```\n", + "qiskit[all]~=2.4.0\n", + "qiskit-ibm-runtime~=0.46.1\n", + "qiskit-aer~=0.17\n", + "```\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "80d0a1da-8d98-49e1-9cb0-0006093bf44c", + "metadata": {}, + "source": [ + "You can use the `Neat` class to analyze the noise impact on an Estimator workload. For syntax verification, use [Local testing mode](/docs/guides/local-testing-mode)." + ] + }, + { + "cell_type": "markdown", + "id": "b08f0589", + "metadata": {}, + "source": [ + "## `Neat` class usage\n", + "\n", + "Before submitting a resource-intensive Qiskit Runtime workload to execute on hardware, you can use the Qiskit Runtime [`Neat` (Noisy Estimator Analyzer Tool)](/docs/api/qiskit-ibm-runtime/debug-tools-neat#neat) class to verify that your Estimator workload is set up correctly, is likely to return accurate results, uses the most appropriate options for the specified problem, and more.\n", + "\n", + "`Neat` Cliffordizes the input circuits for efficient simulation, while retaining its structure and depth. Clifford circuits suffer similar levels of noise and are a good proxy for studying the original circuit of interest." + ] + }, + { + "cell_type": "markdown", + "id": "0dc5bf2a-e536-4141-a77c-0ee407cbd9b2", + "metadata": {}, + "source": [ + "First, import the relevant packages and [authenticate to the Qiskit Runtime service](/docs/guides/cloud-setup)." + ] + }, + { + "cell_type": "markdown", + "id": "d653e186-7ec3-4f1b-b0e9-b322055dd6c8", + "metadata": {}, + "source": [ + "### Prepare the environment" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2f28c824-3158-43e6-ab3c-fd96c31859f0", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import random\n", + "\n", + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator\n", + "from qiskit_ibm_runtime.debug_tools import Neat\n", + "\n", + "from qiskit_aer.noise import NoiseModel, depolarizing_error" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a45a6d9e-de39-4586-8395-a7f580f0e0dc", + "metadata": {}, + "outputs": [], + "source": [ + "# Choose the least busy backend\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "# Generate a preset pass manager\n", + "# This will be used to convert the abstract circuit to an equivalent Instruction Set Architecture\n", + "# (ISA) circuit.\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=0)\n", + "\n", + "# Set the random seed\n", + "random.seed(10)" + ] + }, + { + "cell_type": "markdown", + "id": "67572a70-da01-40fe-b299-b5599561164a", + "metadata": {}, + "source": [ + "### Initialize a target circuit\n", + "\n", + "Consider a six-qubit circuit that has the following properties:\n", + "\n", + "* Alternates between random `RZ` rotations and layers of `CNOT` gates.\n", + "* Has a mirror structure, that is, it applies a unitary `U` followed by its inverse." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "df19af55-897d-4b1f-baf8-fac2641ae87d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def generate_circuit(n_qubits, n_layers):\n", + " r\"\"\"\n", + " A function to generate a pseudo-random a circuit with ``n_qubits`` qubits and\n", + " ``2*n_layers`` entangling layers of the type used in this notebook.\n", + " \"\"\"\n", + " # An array of random angles\n", + " angles = [\n", + " [random.random() for q in range(n_qubits)] for s in range(n_layers)\n", + " ]\n", + "\n", + " qc = QuantumCircuit(n_qubits)\n", + " qubits = list(range(n_qubits))\n", + "\n", + " # do random circuit\n", + " for layer in range(n_layers):\n", + " # rotations\n", + " for q_idx, qubit in enumerate(qubits):\n", + " qc.rz(angles[layer][q_idx], qubit)\n", + "\n", + " # cx gates\n", + " control_qubits = (\n", + " qubits[::2] if layer % 2 == 0 else qubits[1 : n_qubits - 1 : 2]\n", + " )\n", + " for qubit in control_qubits:\n", + " qc.cx(qubit, qubit + 1)\n", + "\n", + " # undo random circuit\n", + " for layer in range(n_layers)[::-1]:\n", + " # cx gates\n", + " control_qubits = (\n", + " qubits[::2] if layer % 2 == 0 else qubits[1 : n_qubits - 1 : 2]\n", + " )\n", + " for qubit in control_qubits:\n", + " qc.cx(qubit, qubit + 1)\n", + "\n", + " # rotations\n", + " for q_idx, qubit in enumerate(qubits):\n", + " qc.rz(-angles[layer][q_idx], qubit)\n", + "\n", + " return qc\n", + "\n", + "\n", + "# Generate a random circuit\n", + "qc = generate_circuit(6, 3)\n", + "# Convert the abstract circuit to an equivalent ISA circuit.\n", + "isa_qc = pm.run(qc)\n", + "\n", + "qc.draw(\"mpl\", idle_wires=0)" + ] + }, + { + "cell_type": "markdown", + "id": "0167329b-c6a6-4b2c-98fc-bf9aba9b7ee6", + "metadata": {}, + "source": [ + "Choose single-Pauli `Z` operators as observables and use them to initialize the primitive unified blocs (PUBs)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "830b1dcc-2669-46cc-bff8-01a96a05c6ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observables: ['ZIIIII', 'IZIIII', 'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']\n" + ] + } + ], + "source": [ + "# Initialize the observables\n", + "obs = [\"ZIIIII\", \"IZIIII\", \"IIZIII\", \"IIIZII\", \"IIIIZI\", \"IIIIIZ\"]\n", + "print(f\"Observables: {obs}\")\n", + "\n", + "# Map the observables to the backend's layout\n", + "isa_obs = [SparsePauliOp(o).apply_layout(isa_qc.layout) for o in obs]\n", + "\n", + "# Initialize the PUBs, which consist of six-qubit circuits with `n_layers` 1, ..., 6\n", + "all_n_layers = [1, 2, 3, 4, 5, 6]\n", + "\n", + "pubs = [(pm.run(generate_circuit(6, n)), isa_obs) for n in all_n_layers]" + ] + }, + { + "cell_type": "markdown", + "id": "2a49fc84-0c82-4cbb-a557-6e676e57c9fa", + "metadata": {}, + "source": [ + "### Cliffordize the circuits\n", + "\n", + "The previously defined PUB circuits are not Clifford, which makes them difficult to simulate classically. However, you can use the `Neat` [`to_clifford`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#to_clifford) method to map them to Clifford circuits for more efficient simulation. The [`to_clifford`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#to_clifford) method is a wrapper around the [`ConvertISAToClifford`](/docs/api/qiskit-ibm-runtime/transpiler-passes-convert-isa-to-clifford) transpiler pass, which can also be used independently. In particular, it replaces non-Clifford single-qubit gates in the original circuit with Clifford single-qubit gates, but it does not mutate the two-qubit gates, number of qubits, or circuit depth.\n", + "\n", + "See [Efficient simulation of stabilizer circuits with Qiskit Aer primitives](/docs/guides/simulate-stabilizer-circuits) for more information about Clifford circuit simulation." + ] + }, + { + "cell_type": "markdown", + "id": "7a86d99e-4431-4d62-8227-c49d17856369", + "metadata": {}, + "source": [ + "First, initialize `Neat`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4b5bbd4c-bd7f-4679-9348-d41da74d26eb", + "metadata": {}, + "outputs": [], + "source": [ + "# You could specify a custom `NoiseModel` here. If `None`, `Neat`\n", + "# pulls the noise model from the given backend\n", + "noise_model = None\n", + "\n", + "# Initialize `Neat`\n", + "analyzer = Neat(backend, noise_model)" + ] + }, + { + "cell_type": "markdown", + "id": "b740dcdf-660e-41e2-b5e6-e8cc288af38b", + "metadata": {}, + "source": [ + "Next, Cliffordize the PUBs." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3ad78f41-a2f8-4381-826a-ae728e081ad6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "clifford_pubs = analyzer.to_clifford(pubs)\n", + "\n", + "clifford_pubs[0].circuit.draw(\"mpl\", idle_wires=0)" + ] + }, + { + "cell_type": "markdown", + "id": "83c3ff81-9f18-43eb-ba6e-57c5ef3d118f", + "metadata": {}, + "source": [ + "## Application 1: Analyze the impact of noise on the circuit outputs\n", + "\n", + "This example shows how to use `Neat` to study the impact of different noise models on PUBs as a function of circuit depth by running simulations in both ideal ([`ideal_sim`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#ideal_sim)) and noisy ([`noisy_sim`](/docs/api/qiskit-ibm-runtime/debug-tools-neat#noisy_sim)) conditions. This can be useful to set up expectations on the quality of the experimental results before running a job on a QPU. To learn more about noise models, see [Exact and noisy simulation with Qiskit Aer primitives](/docs/guides/simulate-with-qiskit-aer#exact-and-noisy-simulation-with-qiskit-aer-primitives).\n", + "\n", + "The simulated results support mathematical operations, and can therefore be compared with each other (or with experimental results) to calculate figures of merit.\n", + "\n", + "\n", + "A QPU can be affected by different kinds of noise. The Qiskit Aer noise model used here only simulates some of them and therefore is likely to be less severe than the noise on a real QPU.\n", + "\n", + "For details on what errors are included when initializing a noise model from a QPU, see the Aer [`NoiseModel`](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.NoiseModel.html#qiskit_aer.noise.NoiseModel.from_backend) API reference.\n", + "\n", + "\n", + "Begin by performing ideal and noisy classical simulations." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "23859a99-2455-460e-98ea-17b36ea59c36", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ideal results:\n", + " NeatResult([NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.])), NeatPubResult(vals=array([1., 1., 1., 1., 1., 1.]))])\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Noisy results:\n", + " NeatResult([NeatPubResult(vals=array([0.98242188, 0.984375 , 0.98828125, 0.99023438, 0.96484375,\n", + " 0.97265625])), NeatPubResult(vals=array([0.96875 , 0.97070312, 0.98046875, 0.98828125, 0.96484375,\n", + " 0.984375 ])), NeatPubResult(vals=array([0.94140625, 0.94726562, 0.92773438, 0.93164062, 0.93164062,\n", + " 0.95703125])), NeatPubResult(vals=array([0.91015625, 0.90429688, 0.90039062, 0.93164062, 0.94140625,\n", + " 0.953125 ])), NeatPubResult(vals=array([0.875 , 0.88476562, 0.88476562, 0.8984375 , 0.91601562,\n", + " 0.91210938])), NeatPubResult(vals=array([0.88476562, 0.89453125, 0.86523438, 0.91015625, 0.86914062,\n", + " 0.91992188]))])\n", + "\n" + ] + } + ], + "source": [ + "# Perform a noiseless simulation\n", + "ideal_results = analyzer.ideal_sim(clifford_pubs)\n", + "print(f\"Ideal results:\\n {ideal_results}\\n\")\n", + "\n", + "# Perform a noisy simulation with the backend's noise model\n", + "noisy_results = analyzer.noisy_sim(clifford_pubs)\n", + "print(f\"Noisy results:\\n {noisy_results}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "a000a77a-0285-4b72-a69f-8f144f2c2a80", + "metadata": {}, + "source": [ + "Next, apply mathematical operations to compute the absolute difference. The remainder of the guide uses the absolute difference as a figure of merit to compare ideal results with noisy or experimental results, but similar figures of merit can be set up.\n", + "\n", + "The absolute difference shows that the impact of noise grows with the circuits' sizes." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cd61e437-bd2f-4349-a667-7edab51c4a6e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean absolute difference between ideal and noisy results for circuits with 1 layers:\n", + " 1.95%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 2 layers:\n", + " 2.38%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 3 layers:\n", + " 6.06%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 4 layers:\n", + " 7.65%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 5 layers:\n", + " 10.48%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 6 layers:\n", + " 10.94%\n", + "\n" + ] + } + ], + "source": [ + "# Figure of merit: Absolute difference\n", + "def rdiff(res1, re2):\n", + " r\"\"\"The absolute difference between `res1` and re2`.\n", + "\n", + " --> The closer to `0`, the better.\n", + " \"\"\"\n", + " d = abs(res1 - re2)\n", + " return np.round(d.vals * 100, 2)\n", + "\n", + "\n", + "for idx, (ideal_res, noisy_res) in enumerate(\n", + " zip(ideal_results, noisy_results)\n", + "):\n", + " vals = rdiff(ideal_res, noisy_res)\n", + "\n", + " # Print the mean absolute difference for the observables\n", + " mean_vals = np.round(np.mean(vals), 2)\n", + " print(\n", + " f\"Mean absolute difference between ideal and noisy results for circuits with {all_n_layers[idx]} layers:\\n {mean_vals}%\\n\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "7abcd001-9eac-4015-97a3-d6250ea4b667", + "metadata": {}, + "source": [ + "You can follow these rough and simplified guidelines to improve circuits of this type:\n", + "\n", + "- If the mean absolute difference is greater than 90%, mitigation will likely not help.\n", + "- If the mean absolute difference is less than 90%, [Probabilistic Error Amplification (PEA)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-amplification-pea) will likely be able to improve the results.\n", + "- If the mean absolute difference is less than 80%, [ZNE with gate folding](/docs/guides/error-mitigation-and-suppression-techniques#zero-noise-extrapolation-zne) will also likely be able to improve the results.\n", + "\n", + "Because all of the absolute differences above are less than 90%, applying PEA to the original circuit will hopefully improve the quality of its results." + ] + }, + { + "cell_type": "markdown", + "id": "c64c936b-5b8f-4fd2-861d-8b1ded2a0ad4", + "metadata": {}, + "source": [ + "You can specify different noise models in the analyzer. The following example performs the same test but adds a custom noise model." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0835c562-55c9-4dbe-879e-7271f8bed280", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean absolute difference between ideal and noisy results for circuits with 1 layers:\n", + " 0.0%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 2 layers:\n", + " 0.0%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 3 layers:\n", + " 0.0%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 4 layers:\n", + " 0.0%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 5 layers:\n", + " 0.0%\n", + "\n", + "Mean absolute difference between ideal and noisy results for circuits with 6 layers:\n", + " 0.0%\n", + "\n" + ] + } + ], + "source": [ + "# Set up a noise model with strength 0.02 on every two-qubit gate\n", + "noise_model = NoiseModel()\n", + "for qubits in backend.coupling_map:\n", + " noise_model.add_quantum_error(\n", + " depolarizing_error(0.02, 2), [\"ecr\", \"cx\"], qubits\n", + " )\n", + "\n", + "# Update the analyzer's noise model\n", + "analyzer.noise_model = noise_model\n", + "\n", + "# Perform a noiseless simulation\n", + "ideal_results = analyzer.ideal_sim(clifford_pubs)\n", + "\n", + "# Perform a noisy simulation with the backend's noise model\n", + "noisy_results = analyzer.noisy_sim(clifford_pubs)\n", + "\n", + "# Compare the results\n", + "for idx, (ideal_res, noisy_res) in enumerate(\n", + " zip(ideal_results, noisy_results)\n", + "):\n", + " values = rdiff(ideal_res, noisy_res)\n", + "\n", + " # Print the mean absolute difference for the observables\n", + " mean_values = np.round(np.mean(values), 2)\n", + " print(\n", + " f\"Mean absolute difference between ideal and noisy results for circuits with {all_n_layers[idx]} layers:\\n {mean_values}%\\n\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f2408ca9-3e3c-4a2f-a99a-ce413d5d470f", + "metadata": {}, + "source": [ + "As shown, given a noise model, you can try to quantify the impact of noise on the (Cliffordized version of the) PUBs of interest before running them on a QPU." + ] + }, + { + "cell_type": "markdown", + "id": "ddd6da5f-4e84-4bf4-aaeb-0403f21275db", + "metadata": {}, + "source": [ + "## Application 2: Benchmark different strategies\n", + "\n", + "This example uses `Neat` to help identify the best options for your PUBs. To do so, consider running an estimation problem with PEA, which cannot be simulated with `qiskit_aer`. You can use `Neat` to help determine which noise amplification factors will work best, then use those factors when running the original experiment on a QPU." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "358bb82a-4bc9-46c2-98a0-e745ffc6788f", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate a circuit with six qubits and six layers\n", + "isa_qc = pm.run(generate_circuit(6, 3))\n", + "\n", + "# Use the same observables as previously\n", + "pubs = [(isa_qc, isa_obs)]\n", + "clifford_pubs = analyzer.to_clifford(pubs)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5774cb3f-c999-4242-a83a-7dcc0c57510b", + "metadata": {}, + "outputs": [], + "source": [ + "noise_factors = [\n", + " [1, 1.1],\n", + " [1, 1.1, 1.2],\n", + " [1, 1.5, 2],\n", + " [1, 1.5, 2, 2.5, 3],\n", + " [1, 4],\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0b9900e6-84fe-4776-9bb5-08c6c729be29", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the PUBs on a QPU\n", + "estimator = Estimator(backend)\n", + "estimator.options.default_shots = 100000\n", + "estimator.options.twirling.enable_gates = True\n", + "estimator.options.twirling.enable_measure = True\n", + "estimator.options.twirling.shots_per_randomization = 100\n", + "estimator.options.resilience.measure_mitigation = True\n", + "estimator.options.resilience.zne_mitigation = True\n", + "estimator.options.resilience.zne.amplifier = \"pea\"\n", + "\n", + "jobs = []\n", + "for factors in noise_factors:\n", + " estimator.options.resilience.zne.noise_factors = factors\n", + " jobs.append(estimator.run(clifford_pubs))\n", + "\n", + "results = [job.result() for job in jobs]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "16c18377-059a-4751-9ab1-afee0ed5b089", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform a noiseless simulation\n", + "ideal_results = analyzer.ideal_sim(clifford_pubs)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "7db531a1-c417-4d5b-bdc3-7a4ad3385fd4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean absolute difference for factors [1, 1.1]:\n", + " 2.42%\n", + "\n", + "Mean absolute difference for factors [1, 1.1, 1.2]:\n", + " 11.34%\n", + "\n", + "Mean absolute difference for factors [1, 1.5, 2]:\n", + " 3.68%\n", + "\n", + "Mean absolute difference for factors [1, 1.5, 2, 2.5, 3]:\n", + " 4.77%\n", + "\n", + "Mean absolute difference for factors [1, 4]:\n", + " 3.61%\n", + "\n" + ] + } + ], + "source": [ + "# Look at the mean absolute difference to quickly tell the best choice for your options\n", + "for factors, res in zip(noise_factors, results):\n", + " d = rdiff(ideal_results[0], res[0])\n", + " print(\n", + " f\"Mean absolute difference for factors {factors}:\\n {np.round(np.mean(d), 2)}%\\n\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "0c37ef7b-df56-4f5f-9e11-10f209f105f9", + "metadata": {}, + "source": [ + "The result with the smallest difference suggests which options to choose." + ] + }, + { + "cell_type": "markdown", + "id": "2530a3e9-21a6-4841-9449-fe181c54aca4", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + " - Read an overview of [Qiskit debugging tools](/docs/guides/debugging-tools).\n", + " - Learn about [Exact and noisy simulation with Qiskit Aer primitives](/docs/guides/simulate-with-qiskit-aer).\n", + " - Learn about [available Qiskit Runtime options](/docs/guides/runtime-options-overview).\n", + " - Learn about [Error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques).\n", + " - Visit the [Transpile with pass managers](transpile-with-pass-managers) topic.\n", + " - Learn [how to transpile circuits](/docs/guides/circuit-transpilation-settings#compare-transpiler-settings) as part of Qiskit patterns workflows using Qiskit Runtime.\n", + " - Review the [Debugging tools API documentation](/docs/api/qiskit-ibm-runtime/debug-tools).\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/guides/function-template-chemistry-workflow.ipynb b/docs/guides/function-template-chemistry-workflow.ipynb index 7a32734dd1b..c6aa7134d00 100644 --- a/docs/guides/function-template-chemistry-workflow.ipynb +++ b/docs/guides/function-template-chemistry-workflow.ipynb @@ -1,1767 +1,1768 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8cfa587b", - "metadata": {}, - "source": [ - "---\n", - "title: Build a Qiskit Function for chemistry simulation\n", - "description: Learn how to deploy and execute the chemistry workflow template\n", - "---\n", - "\n", - "\n", - "# Build and run a Qiskit Function template for electronic structure simulation with an implicit solvent model\n", - "\n", - "{/* cspell:ignore pvdz, fcisolver, avas, ncas, nelecas, ecore, chkfile, fcivec, hcore, ncore, myci, sqdvec, myeps, mymethod, mysolvmethod, myavas, mcscf, MCSCF, chkfile, prqs */}" - ] - }, - { - "cell_type": "markdown", - "id": "f9bfec87", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [] - }, - { - "cell_type": "markdown", - "id": "f3a337d1", - "metadata": {}, - "source": [ - "This template, developed in collaboration with the Cleveland Clinic, consists of a workflow to calculate the ground state energy and solvation free energy of a molecule in an implicit solvent [[1]](#references). These simulations are based on the sample-based quantum diagonalization (SQD) method [[2-6]](#references) and the integral equation formalism polarizable continuum model (IEF-PCM) of solvent [[7]](#references).\n", - "\n", - "This guide utilizes the template with a methanol molecule as the solute, the electronic structure of which is simulated explicitly, and water as the solvent, approximated as a continuous dielectric medium. To account for the [electron correlation effects](https://onlinelibrary.wiley.com/doi/epdf/10.1002/ijch.202100111) in methanol, while maintaining the balance between the computational cost and accuracy, we only include the $\\sigma$, $\\sigma^{*}$, and lone pair orbitals in the active space simulated with SQD IEF-PCM. This orbital selection is done with [atomic valence active space (AVAS) method](https://github.com/pyscf/pyscf.github.io/blob/master/examples/mcscf/43-avas.py) using the C[2s,2p], O[2s,2p], and H[1s] atomic orbital components, which results in the active space of 14 electrons and 12 orbitals (14e,12o). The reference orbitals are calculated with closed-shell Hartree Fock using the cc-pvdz basis set." - ] - }, - { - "cell_type": "markdown", - "id": "25d18ba5", - "metadata": {}, - "source": [ - "## Workflow introduction\n", - "\n", - "\n", - "This interactive guide shows how to upload this function template to Qiskit Serverless and run an example workload. The template is structured as a Qiskit pattern with four steps:\n", - "\n", - "#### 1. Collect input and map the problem\n", - "\n", - "This step takes the geometry of the molecule, selected active space, solvation model, LUCJ options, and SQD options as an input. It then produces the PySCF Checkpoint file, which contains the Hartree-Fock (HF) IEF-PCM data. This data will be used in the SQD portion of the workflow. For the LUCJ portion of the workflow, the input section also generates the gas-phase HF data, which is stored internally in PySCF FCIDUMP format.\n", - "\n", - "The information from the HF gas-phase simulation and the definition of the active space are taken as input. Importantly, it also uses the user-defined information from the input section concerning the error suppression, number of shots, circuit transpiler optimization level, and the qubit layout.\n", - "\n", - "It generates one-electron and two-electron integrals within the defined active space. The integrals are then used to perform classical CCSD calculations, which return t2 amplitudes that we use to parametrize the LUCJ circuit.\n", - "\n", - "#### 2. Optimize the circuit\n", - "\n", - "The LUCJ circuit is then transpiled into an ISA circuit for the target hardware. A Sampler primitive is then instantiated with a default set of error mitigation options to manage the execution.\n", - "\n", - "#### 3. Execute the circuit\n", - "\n", - "The LUCJ calculations return the bitstrings for each measurement, where these bitstrings correspond to electron configurations of the studied system. The bitstrings are then used as input for post-processing.\n", - "\n", - "#### 4. Post-process by using SQD\n", - "\n", - "This final step takes the PySCF Checkpoint file containing the HF IEF-PCM information, the bitstrings representing the electron configurations predicted by LUCJ, and the user-defined SQD options selected in the input section as input. As output, it produces the SQD IEF-PCM total energy of the lowest energy batch and the corresponding solvation free energy." - ] - }, - { - "cell_type": "markdown", - "id": "4832ef18", - "metadata": {}, - "source": [ - "### Options\n", - "\n", - "For this template you must specify options for generating the LUCJ circuit, and SQD run parameters.\n", - "\n", - "#### LUCJ options\n", - "\n", - "When the LUCJ quantum circuit is executed, a set of samples that represent the computational basis states from the probability distribution of the molecular system are produced. To balance the depth of the LUCJ circuit and its expressibility, the qubits corresponding to the spin orbitals with the opposite spin have the two-qubit gates applied between them when these qubits are neighbors through a single ancilla qubit. To implement this approach on IBM hardware with a heavy-hex topology, qubits that represent the spin orbitals with the same spin are connected through a line topology where each line takes a zig-zag shape due to the heavy-hex connectivity of the target hardware, while the qubits that represent the spin orbitals with the opposite spin only have a connection at every fourth qubit.\n", - "\n", - "\n", - "\n", - "\n", - "The user has to provide the `initial_layout` array corresponding to the qubits that satisfy this [_zig-zag_ pattern](https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k) in the `lucj_options` section of the SQD IEF-PCM function. In case of SQD IEF-PCM (14e,12o)/cc-pvdz simulations of methanol, we chose the initial qubit layout corresponding to the main diagonal of the Eagle R3 QPU. Here, the first 12 elements of the `initial_layout` array `[0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, ...]` correspond to the alpha spin orbitals. The last 12 elements `[... 2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54]` correspond to beta spin orbitals.\n", - "\n", - "Importantly, the user has to determine the `number_of_shots`, which corresponds to the number of measurements in the LUCJ circuit. The number of shots needs to be sufficiently large because the first step of S-CORE procedure relies on the samples in the right particle sector to obtain the initial approximation to the ground-state occupation number distribution.\n", - "\n", - "The number of shots is highly system- and hardware-dependent, but [non-covalent](https://arxiv.org/abs/2410.09209), [fragment-based](https://arxiv.org/abs/2411.09861), and [implicit solvent](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030) SQD studies suggest that one can reach the chemical accuracy by following these guidelines:\n", - "\n", - "- 20,000 - 200,000 shots for systems with fewer than 16 molecular orbitals (32 spin orbitals)\n", - "- 200,000 shots for systems with 16 - 18 molecular orbitals\n", - "- 200,000 - 2,000,000 shots for systems with more than 18 molecular orbitals\n", - "\n", - "The required number of shots is affected by the number of spin orbitals in the studied system and by the size of the Hilbert space corresponding to the selected active space within the studied system. Generally, instances with smaller Hilbert spaces require fewer shots. Other available LUCJ options are [circuit transpiler optimization level](https://docs.quantum.ibm.com/guides/set-optimization) and [error suppression options](https://docs.quantum.ibm.com/guides/error-mitigation-and-suppression-techniques). Note that these options also affect the required number of shots and the resulting accuracy.\n", - "\n", - " \n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "e2b46498", - "metadata": {}, - "source": [ - "#### SQD options\n", - "\n", - "Important options in SQD simulations include the `sqd_iterations`, `number_of_batches`, and `samples_per_batch`. Generally, the lower number of samples per batch can be counteracted with more batches (`number_of_batches`) and more iterations of S-CORE (`sqd_iterations`). With more batches we can sample more variations of the configurational subspaces. Since the lowest-energy batch is taken as the solution for the ground state energy of the system, more batches can improve the results through better statistics. Additional iterations of S-CORE allow more configurations to be recovered from the original LUCJ distribution if the number of samples in the correct particle sector is low. This can allow the number of samples per batch to be reduced.\n", - "\n", - "\n", - "\n", - "\n", - "An alternative strategy is to use more samples per batch, which ensures that most of the initial LUCJ samples in right particle space are used during the S-CORE procedure, and individual subspaces encapsulate a sufficient variety of electron configurations. In turn, this reduces the number of required S-CORE steps, where only two or three iterations of SQD are needed if the number of samples per batch is large enough. However, more samples per batch results in a higher computational cost of each diagonalization step. Hence, the balance between the accuracy and computational cost in SQD simulations can be achieved by choosing `sqd_iterations`, `number_of_batches`, and `samples_per_batch` optimally.\n", - "\n", - "The [SQD IEF-PCM study](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030) shows that when three iterations of S-CORE are used, the chemical accuracy can be reached by following these guidelines:\n", - "\n", - "- 600 samples per batch in methanol SQD IEF-PCM (14e,12o) simulations\n", - "- 1500 samples per batch in methylamine SQD IEF-PCM (14e,13o) simulations\n", - "- 6000 samples per batch in water SQD IEF-PCM (8e,23o) simulations\n", - "- 16000 samples per batch in ethanol SQD IEF-PCM (20e,18o) simulations\n", - "\n", - "Just like the required number of shots in LUCJ, the required number of samples per batch used in S-CORE procedure is highly system- and hardware-dependent. The examples above can be used to estimate the initial point for the benchmark of required number of samples per batch. The tutorial on systematic benchmark of the required number of samples per batch can be found [here](https://qiskit.github.io/qiskit-addon-sqd/how_tos/choose_subspace_dimension.html).\n", - "\n", - " \n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "998a0f25", - "metadata": {}, - "source": [ - "## Deploy and execute the template SQD IEF-PCM function" - ] - }, - { - "cell_type": "markdown", - "id": "6c92ac84", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "### Authentication\n", - "\n", - "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key (token), which can be found on the [IBM Quantum Platform](https://quantum.cloud.ibm.com) dashboard. This allows for the instantiation of the serverless client to upload or run the selected function:\n", - "\n", - "```python\n", - "from qiskit_ibm_catalog import QiskitServerless\n", - "\n", - "serverless = QiskitServerless(\n", - " channel=\"ibm_quantum_platform\",\n", - " instance=\"INSTANCE_CRN\",\n", - " token=\"YOUR_API_KEY\" # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", - ")\n", - "```\n", - "\n", - "Optionally, use `save_account()` to save your credentials in a local environment (see the [Set up your IBM Cloud account](/docs/guides/cloud-setup#cloud-save) guide). Note that this writes your credentials to the same file as [`QiskitRuntimeService.save_account()`](/docs/api/qiskit-ibm-runtime/qiskit-runtime-service#save_account):\n", - "\n", - "```python\n", - "QiskitServerless.save_account(token=\"YOUR_API_KEY\", channel=\"ibm_quantum_platform\", instance=\"INSTANCE_CRN\")\n", - "```\n", - "\n", - "If the [account is saved](/docs/guides/save-credentials), there is no need to provide the token to authenticate:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9276e2d4", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_catalog import QiskitServerless\n", - "\n", - "serverless = QiskitServerless()" - ] - }, - { - "cell_type": "markdown", - "id": "e1f99d80", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "### Upload the template" - ] - }, - { - "cell_type": "markdown", - "id": "3e0e8cc8", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "To upload a custom Qiskit Function, you must first instantiate a `QiskitFunction` object that defines the function source code. The title will allow you to identify the function once it's in the remote cluster. The main entry point is the file that contains `if __name__ == \"__main__\"`. If your workflow requires additional source files, you can define a working directory that will be uploaded together with the entry point." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77b2b9b6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "QiskitFunction(sqd_pcm_template)\n" - ] - } - ], - "source": [ - "from qiskit_ibm_catalog import QiskitFunction\n", - "\n", - "template = QiskitFunction(\n", - " title=\"sqd_pcm_template\",\n", - " entrypoint=\"sqd_pcm_entrypoint.py\",\n", - " working_dir=\"./source_files/\", # all files in this directory will be uploaded\n", - " dependencies=[\n", - " \"ffsim==0.0.54\",\n", - " \"pyscf==2.9.0\",\n", - " \"qiskit_addon_sqd==0.10.0\",\n", - " ],\n", - ")\n", - "print(template)" - ] - }, - { - "cell_type": "markdown", - "id": "72854f5a", - "metadata": {}, - "source": [ - "Once the instance is ready, upload it to serverless:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "59e7fdb5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "QiskitFunction(sqd_pcm_template)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.upload(template)" - ] - }, - { - "cell_type": "markdown", - "id": "ac7d8764", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "To check if the program successfully uploaded, use `serverless.list()`:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "03a91030", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[QiskitFunction(sqd_pcm_template),\n", - " QiskitFunction(hamiltonian_simulation_template)]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.list()" - ] - }, - { - "cell_type": "markdown", - "id": "99408586", - "metadata": {}, - "source": [ - "## Load and run the template remotely" - ] - }, - { - "cell_type": "markdown", - "id": "62b37d7a", - "metadata": {}, - "source": [ - "The function template has been uploaded, so you can run it remotely with Qiskit Serverless. First, load the template by name:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "854d12cf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "QiskitFunction(sqd_pcm_template)\n" - ] - } - ], - "source": [ - "template = serverless.load(\"sqd_pcm_template\")\n", - "print(template)" - ] - }, - { - "cell_type": "markdown", - "id": "fa2dc721", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "Next, run the template with the domain-level inputs for SQD-IEF PCM. This example specifies a methanol-based workload." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a1719ab1", - "metadata": {}, - "outputs": [], - "source": [ - "molecule = {\n", - " \"atom\": \"\"\"\n", - " O -0.04559 -0.75076 -0.00000;\n", - " C -0.04844 0.65398 -0.00000;\n", - " H 0.85330 -1.05128 -0.00000;\n", - " H -1.08779 0.98076 -0.00000;\n", - " H 0.44171 1.06337 0.88811;\n", - " H 0.44171 1.06337 -0.88811\n", - " \"\"\", # Must be specified\n", - " \"basis\": \"cc-pvdz\", # default is \"sto-3g\"\n", - " \"spin\": 0, # default is 0\n", - " \"charge\": 0, # default is 0\n", - " \"verbosity\": 0, # default is 0\n", - " \"number_of_active_orb\": 12, # Must be specified\n", - " \"number_of_active_alpha_elec\": 7, # Must be specified\n", - " \"number_of_active_beta_elec\": 7, # Must be specified\n", - " \"avas_selection\": [\n", - " \"%d O %s\" % (k, x) for k in [0] for x in [\"2s\", \"2px\", \"2py\", \"2pz\"]\n", - " ]\n", - " + [\"%d C %s\" % (k, x) for k in [1] for x in [\"2s\", \"2px\", \"2py\", \"2pz\"]]\n", - " + [\"%d H 1s\" % k for k in [2, 3, 4, 5]], # default is None\n", - "}\n", - "\n", - "solvent_options = {\n", - " \"method\": \"IEF-PCM\", # other available methods are COSMO, C-PCM, SS(V)PE, see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", - " \"eps\": 78.3553, # value for water\n", - "}\n", - "\n", - "lucj_options = {\n", - " \"initial_layout\": [\n", - " 0,\n", - " 14,\n", - " 18,\n", - " 19,\n", - " 20,\n", - " 33,\n", - " 39,\n", - " 40,\n", - " 41,\n", - " 53,\n", - " 60,\n", - " 61,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 15,\n", - " 22,\n", - " 23,\n", - " 24,\n", - " 34,\n", - " 43,\n", - " 44,\n", - " 45,\n", - " 54,\n", - " ],\n", - " \"dynamical_decoupling_choice\": True,\n", - " \"twirling_choice\": True,\n", - " \"number_of_shots\": 200000,\n", - " \"optimization_level\": 2,\n", - "}\n", - "\n", - "sqd_options = {\n", - " \"sqd_iterations\": 3,\n", - " \"number_of_batches\": 10,\n", - " \"samples_per_batch\": 1000,\n", - " \"max_davidson_cycles\": 200,\n", - "}\n", - "\n", - "backend_name = \"ibm_sherbrooke\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "01c0667c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "39f8fb70-79b2-43ca-b723-84e6b6135821\n" - ] - } - ], - "source": [ - "job = template.run(\n", - " backend_name=backend_name,\n", - " molecule=molecule,\n", - " solvent_options=solvent_options,\n", - " lucj_options=lucj_options,\n", - " sqd_options=sqd_options,\n", - ")\n", - "print(job.job_id)" - ] - }, - { - "cell_type": "markdown", - "id": "a9101a94", - "metadata": {}, - "source": [ - "Check the detailed status of the job:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "4385a34f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "time = 2.35, status = DONE\n" - ] - } - ], - "source": [ - "import time\n", - "\n", - "t0 = time.time()\n", - "status = job.status()\n", - "if status == \"QUEUED\":\n", - " print(f\"time = {time.time()-t0:.2f}, status = QUEUED\")\n", - "while True:\n", - " status = job.status()\n", - " if status == \"QUEUED\":\n", - " continue\n", - " print(f\"time = {time.time()-t0:.2f}, status = {status}\")\n", - " if status == \"DONE\" or status == \"ERROR\":\n", - " break" - ] - }, - { - "cell_type": "markdown", - "id": "4adc5293", - "metadata": {}, - "source": [ - "While the job is running, you can fetch logs created from the `logger.info` outputs. These can provide actionable information about the progress of the SQD IEF-PCM workflow. For example, the same spin orbital connections, or the two-qubit depth of the final ISA circuit intended for execution on hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5b1f190", - "metadata": {}, - "outputs": [], - "source": [ - "print(job.logs())" - ] - }, - { - "cell_type": "markdown", - "id": "ba179114", - "metadata": {}, - "source": [ - "Calling for the job result blocks the rest of the program until a result is available. After the job is done, you can retrieve the results. These include the solvation free energy, as well as information about the lowest energy batch, lowest energy value, and other useful information such as the total solver duration." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3500adce", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'total_energy_hist': array([[-115.14768518, -115.1368396 , -114.19181692, -115.13745429,\n", - " -115.1445012 , -114.19673326, -115.1547003 , -114.20563866,\n", - " -115.13748344, -115.14764974],\n", - " [-115.15768392, -115.15850126, -115.15857275, -115.15770916,\n", - " -115.15801684, -115.15822125, -115.15833521, -115.15844051,\n", - " -115.15735538, -115.15862354],\n", - " [-115.15795148, -115.15847925, -115.15856677, -115.15811156,\n", - " -115.15815602, -115.15785171, -115.1583672 , -115.1585533 ,\n", - " -115.15833528, -115.15808791]]),\n", - " 'spin_squared_value_hist': array([[5.37327508e-03, 1.32981759e-02, 1.36214922e-02, 8.84413615e-03,\n", - " 7.26723578e-03, 1.94875195e-02, 3.03153152e-03, 6.07543106e-03,\n", - " 1.04951849e-02, 5.36529204e-03],\n", - " [6.39397528e-04, 1.36814350e-04, 9.09054260e-05, 5.99361358e-04,\n", - " 3.64261739e-04, 2.54905866e-04, 2.32540370e-04, 1.53181990e-04,\n", - " 7.23519739e-04, 6.80737671e-05],\n", - " [4.53776416e-04, 1.63043449e-04, 1.05317263e-04, 3.82912836e-04,\n", - " 3.41047803e-04, 5.18620393e-04, 2.06819142e-04, 1.17086537e-04,\n", - " 2.32357159e-04, 4.26071537e-04]]),\n", - " 'solvation_free_energy_hist': array([[-0.00725018, -0.00743955, -0.01132905, -0.0073377 , -0.00722221,\n", - " -0.01136705, -0.00719279, -0.01072829, -0.00733404, -0.00725961],\n", - " [-0.00719252, -0.00718315, -0.00718074, -0.00719325, -0.00717703,\n", - " -0.00718391, -0.00718354, -0.00717928, -0.00719887, -0.0071801 ],\n", - " [-0.00719351, -0.00718255, -0.00718198, -0.00718429, -0.00718349,\n", - " -0.00718329, -0.0071882 , -0.00718363, -0.00718549, -0.00718814]]),\n", - " 'occupancy_hist': [[array([0.99712298, 0.99278936, 0.99083163, 0.97328469, 0.98959809,\n", - " 0.98922134, 0.720333 , 0.25683194, 0.01939338, 0.02840332,\n", - " 0.00946988, 0.0327204 ]),\n", - " array([0.99712298, 0.99278936, 0.99083163, 0.97328469, 0.98959809,\n", - " 0.98922134, 0.720333 , 0.25683194, 0.01939338, 0.02840332,\n", - " 0.00946988, 0.0327204 ])],\n", - " [array([0.9959042 , 0.9922607 , 0.99018862, 0.99265843, 0.98927447,\n", - " 0.9900833 , 0.99403876, 0.00989025, 0.01120814, 0.01137717,\n", - " 0.01152871, 0.01158725]),\n", - " array([0.9959042 , 0.9922607 , 0.99018862, 0.99265843, 0.98927447,\n", - " 0.9900833 , 0.99403876, 0.00989025, 0.01120814, 0.01137717,\n", - " 0.01152871, 0.01158725])],\n", - " [array([0.99590079, 0.99222193, 0.99016753, 0.99265045, 0.98927264,\n", - " 0.99007179, 0.99407207, 0.00986684, 0.01125181, 0.01141439,\n", - " 0.01150733, 0.01160243]),\n", - " array([0.99590079, 0.99222193, 0.99016753, 0.99265045, 0.98927264,\n", - " 0.99007179, 0.99407207, 0.00986684, 0.01125181, 0.01141439,\n", - " 0.01150733, 0.01160243])]],\n", - " 'lowest_energy_batch': 2,\n", - " 'lowest_energy_value': -115.1585667736213,\n", - " 'solvation_free_energy': -0.007181981952470838,\n", - " 'sci_solver_total_duration': 493.997501373291,\n", - " 'metadata': {'resources_usage': {'RUNNING: MAPPING': {'CPU_TIME': 6.080063343048096},\n", - " 'RUNNING: OPTIMIZING_FOR_HARDWARE': {'CPU_TIME': 1.999896764755249},\n", - " 'RUNNING: WAITING_FOR_QPU': {'CPU_TIME': 6.2850868701934814},\n", - " 'RUNNING: EXECUTING_QPU': {'QPU_TIME': 21.639373540878296},\n", - " 'RUNNING: POST_PROCESSING': {'CPU_TIME': 495.40831995010376}},\n", - " 'num_iterations_executed': 3}}" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result = job.result()\n", - "\n", - "result" - ] - }, - { - "cell_type": "markdown", - "id": "94a2c921", - "metadata": {}, - "source": [ - "Note that the result metadata includes a resource usage summary that lets you better estimate the QPU and CPU time required for each workload (this example ran on a dummy device, so actual resource usage times might differ)." - ] - }, - { - "cell_type": "markdown", - "id": "49d0b26d", - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "source": [ - "After the job completes, the entire logging output will be available." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "ddcba564", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-06-27 08:42:41,358\tINFO job_manager.py:531 -- Runtime env is setting up.\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:45,015: Starting runtime service\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:45,621: Backend: ibm_sherbrooke\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:46,809: Initializing molecule object\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,599: Performing CCSD\n", - "Parsing /tmp/ray/session_2025-06-27_08-42-13_898146_1/runtime_resources/working_dir_files/_ray_pkg_4bc93dcc58c04b91/output_sqd_pcm/2025-06-27_08-42-45.fcidump.txt\n", - "Overwritten attributes get_ovlp get_hcore of \n", - "/usr/local/lib/python3.11/site-packages/pyscf/gto/mole.py:1293: UserWarning: Function mol.dumps drops attribute energy_nuc because it is not JSON-serializable\n", - " warnings.warn(msg)\n", - "/usr/local/lib/python3.11/site-packages/pyscf/gto/mole.py:1293: UserWarning: Function mol.dumps drops attribute intor_symmetric because it is not JSON-serializable\n", - " warnings.warn(msg)\n", - "converged SCF energy = -115.049680672847\n", - "E(CCSD) = -115.1519910037652 E_corr = -0.1023103309180226\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,694: Same spin orbital connections: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11)]\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,694: Opposite spin orbital connections: [(0, 0), (4, 4), (8, 8)]\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,718: Optimization level: 2, ops: OrderedDict([('rz', 2438), ('sx', 1496), ('ecr', 766), ('x', 185), ('measure', 24), ('barrier', 1)]), depth: 391\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,736: Two-qubit gate depth: 94\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,737: Submitting sampler job\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:54,273: Job ID: d1f5j3lqbivc73ebqpj0\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:54,313: Job Status: QUEUED\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,813: Starting configuration recovery iteration 0\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,841: Batch 0 subspace dimension: 531441\n", - "2025-06-27 08:43:24,844\tINFO worker.py:1588 -- Using address 172.17.16.124:6379 set in the environment variable RAY_ADDRESS\n", - "2025-06-27 08:43:24,847\tINFO worker.py:1723 -- Connecting to existing Ray cluster at address: 172.17.16.124:6379...\n", - "2025-06-27 08:43:24,876\tINFO worker.py:1908 -- Connected to Ray cluster. View the dashboard at \u001b[1m\u001b[32mhttp://172.17.16.124:8265 \u001b[39m\u001b[22m\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,945: Batch 1 subspace dimension: 519841\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,950: Batch 2 subspace dimension: 543169\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,955: Batch 3 subspace dimension: 532900\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,960: Batch 4 subspace dimension: 534361\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,964: Batch 5 subspace dimension: 531441\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,969: Batch 6 subspace dimension: 540225\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,974: Batch 7 subspace dimension: 524176\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,979: Batch 8 subspace dimension: 537289\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,983: Batch 9 subspace dimension: 540225\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,006: Lowest energy batch: 6\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Lowest energy value: -115.15470029849135\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Corresponding g_solv value: -0.0071927910374866375\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: -----------------------------------\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Starting configuration recovery iteration 1\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,564: Batch 0 subspace dimension: 413449\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,572: Batch 1 subspace dimension: 399424\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,578: Batch 2 subspace dimension: 438244\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,583: Batch 3 subspace dimension: 422500\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,589: Batch 4 subspace dimension: 409600\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,596: Batch 5 subspace dimension: 404496\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,601: Batch 6 subspace dimension: 410881\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,605: Batch 7 subspace dimension: 442225\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,611: Batch 8 subspace dimension: 409600\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,618: Batch 9 subspace dimension: 405769\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Lowest energy batch: 9\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Lowest energy value: -115.15862353596414\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Corresponding g_solv value: -0.0071800982859467006\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,918: -----------------------------------\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,918: Starting configuration recovery iteration 2\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,501: Batch 0 subspace dimension: 399424\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,508: Batch 1 subspace dimension: 412164\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,514: Batch 2 subspace dimension: 432964\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,519: Batch 3 subspace dimension: 400689\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,524: Batch 4 subspace dimension: 432964\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,529: Batch 5 subspace dimension: 418609\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,533: Batch 6 subspace dimension: 418609\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,538: Batch 7 subspace dimension: 425104\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,543: Batch 8 subspace dimension: 404496\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,548: Batch 9 subspace dimension: 429025\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,900: Lowest energy batch: 2\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,900: Lowest energy value: -115.1585667736213\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: Corresponding g_solv value: -0.007181981952470838\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: -----------------------------------\n", - "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: SCI_solver totally takes: 493.997501373291 seconds\n", - "\n" - ] - } - ], - "source": [ - "print(job.logs())" - ] - }, - { - "cell_type": "markdown", - "id": "d7cc1cb2", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "- Review the guide on building a function template for [Hamiltonian simulation](/docs/guides/function-template-hamiltonian-simulation)\n", - "- Check out the source files for this template on [GitHub](https://github.com/qiskit-community/qiskit-function-templates/tree/main/chemistry/sqd_pcm)\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "aabba015", - "metadata": {}, - "source": [ - "#### References\n", - "\n", - "[1] Danil Kaliakin, Akhil Shajan, Fangchun Liang, and Kenneth M. Merz Jr. [Implicit Solvent Sample-Based Quantum Diagonalization](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030), The Journal of Physical Chemistry B, 2025, DOI: 10.1021/acs.jpcb.5c01030\n", - "\n", - "[2] Javier Robledo-Moreno, et al., [Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer](https://arxiv.org/abs/2405.05068), arXiv:2405.05068 [quant-ph].\n", - "\n", - "[3] Jeffery Yu, et al., [Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization](https://arxiv.org/abs/2501.09702), arXiv:2501.09702 [quant-ph].\n", - "\n", - "[4] Keita Kanno, et al., [Quantum-Selected Configuration Interaction: classical diagonalization of Hamiltonians in subspaces selected by quantum computers](https://arxiv.org/abs/2302.11320), arXiv:2302.11320 [quant-ph].\n", - "\n", - "[5] Kenji Sugisaki, et al., [Hamiltonian simulation-based quantum-selected configuration interaction for large-scale electronic structure calculations with a quantum computer](https://arxiv.org/abs/2412.07218), arXiv:2412.07218 [quant-ph].\n", - "\n", - "[6] Mathias Mikkelsen, Yuya O. Nakagawa, [Quantum-selected configuration interaction with time-evolved state](https://arxiv.org/abs/2412.13839), arXiv:2412.13839 [quant-ph].\n", - "\n", - "[7] Herbert, John M. [Dielectric continuum methods for quantum chemistry. WIREs Computational Molecular Science](https://wires.onlinelibrary.wiley.com/doi/10.1002/wcms.1519), 2021, 11, 1759-0876.\n", - "\n", - "[8] Saki, A. A.; Barison, S.; Fuller, B.; Garrison, J. R.; Glick, J. R.; Johnson, C.; Mezzacapo, A.; Robledo-Moreno, J.; Rossmannek, M.; Schweigert, P. et al. Qiskit addon: sample-based quantum diagonalization, 2024; https://github.com/Qiskit/qiskit-addon-sqd\n", - "\n", - "[9] Asun, Q.; Zhang, X.; Banerjee, S.; Bao, P.; Barbry, M.; Blunt, N. S.; Bogdanov, N. A.; Booth, G. H.; Chen, J.; Cui, Z.-H. PySCF: Python-based Simulations of Chemistry Framework, 2025; https://github.com/pyscf/pyscf\n", - "\n", - "[10] Kevin J. Sung; et al., FFSIM: Faster simulations of fermionic quantum circuits, 2024. https://github.com/qiskit-community/ffsim" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c99b106c", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "%%writefile ./source_files/__init__.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b182c2e4", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "%%writefile ./source_files/solve_solvent.py\n", - "\n", - "# This code is part of a Qiskit project.\n", - "#\n", - "# (C) Copyright IBM and Cleveland Clinic 2025\n", - "#\n", - "# This code is licensed under the Apache License, Version 2.0. You may\n", - "# obtain a copy of this license in the LICENSE.txt file in the root directory\n", - "# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.\n", - "#\n", - "# Any modifications or derivative works of this code must retain this\n", - "# copyright notice, and modified files need to carry a notice indicating\n", - "# that they have been altered from the originals.\n", - "\n", - "\"\"\"Functions for the study of fermionic systems.\"\"\"\n", - "\n", - "from __future__ import annotations\n", - "\n", - "import warnings\n", - "\n", - "import numpy as np\n", - "\n", - "# DSK Add imports needed for CASCI wrapper\n", - "from pyscf import ao2mo, scf, fci\n", - "from pyscf.mcscf import avas, casci\n", - "from pyscf.solvent import pcm\n", - "from pyscf.lib import chkfile, logger\n", - "\n", - "from qiskit_addon_sqd.fermion import (\n", - " SCIState,\n", - " bitstring_matrix_to_ci_strs,\n", - " _check_ci_strs,\n", - ")\n", - "\n", - "# DSK Below is the modified CASCI kernel compatible with SQD.\n", - "# It utilizes the \"fci.selected_ci.kernel_fixed_space\"\n", - "# as well as enables passing the \"batch\" and \"max_davidson\"\n", - "# input arguments from \"solve_solvent\".\n", - "# The \"batch\" contains the CI addresses corresponding to subspaces\n", - "# derived from LUCJ and S-CORE calculations.\n", - "# The \"max_davidson\" controls the maximum number of cycles of Davidson's algorithm.\n", - "\n", - "\n", - "# pylint: disable = unused-argument\n", - "def kernel(casci_object, mo_coeff=None, ci0=None, verbose=logger.NOTE, envs=None):\n", - " \"\"\"CASCI solver compatible with SQD.\n", - "\n", - " Args:\n", - " casci_object: CASCI or CASSCF object.\n", - " In case of SQD, only CASCI instance is currently incorporated.\n", - "\n", - " mo_coeff : ndarray\n", - " orbitals to construct active space Hamiltonian.\n", - " In context of SQD, these are either AVAS mo_coeff\n", - " or all of the MOs (with option to exclude core MOs).\n", - "\n", - " ci0 : ndarray or custom types FCI solver initial guess.\n", - " For SQD the usage of ci0 was not tested.\n", - "\n", - " For external FCI-like solvers, it can be\n", - " overloaded different data type. For example, in the state-average\n", - " FCI solver, ci0 is a list of ndarray. In other solvers such as\n", - " DMRGCI solver, SHCI solver, ci0 are custom types.\n", - "\n", - " kwargs:\n", - " envs: dict\n", - " In case of SQD this option was not explored,\n", - " but in principle this can facilitate the incorporation of the external solvers.\n", - "\n", - " The variable envs is created (for PR 807) to passes MCSCF runtime\n", - " environment variables to SHCI solver. For solvers which do not\n", - " need this parameter, a kwargs should be created in kernel method\n", - " and \"envs\" pop in kernel function.\n", - " \"\"\"\n", - " if mo_coeff is None:\n", - " mo_coeff = casci_object.mo_coeff\n", - " if ci0 is None:\n", - " ci0 = casci_object.ci\n", - "\n", - " log = logger.new_logger(casci_object, verbose)\n", - " t0 = (logger.process_clock(), logger.perf_counter())\n", - " log.debug(\"Start CASCI\")\n", - "\n", - " ncas = casci_object.ncas\n", - " nelecas = casci_object.nelecas\n", - "\n", - " # The start of SQD version of kernel\n", - " # DSK add the read of configurations for batch\n", - " ci_strs_sqd = casci_object.batch\n", - "\n", - " # DSK add the input for the maximum number of cycles of Davidson's algorithm\n", - " max_davidson = casci_object.max_davidson\n", - "\n", - " # DSK add electron up and down count and norb = ncas\n", - " n_up = nelecas[0]\n", - " n_dn = nelecas[1]\n", - " norb = ncas\n", - "\n", - " # DSK Eigenstate solver info\n", - " sqd_verbose = verbose\n", - "\n", - " # DSK ERI read\n", - " eri_cas = ao2mo.restore(1, casci_object.get_h2eff(), casci_object.ncas)\n", - " t1 = log.timer(\"integral transformation to CAS space\", *t0)\n", - "\n", - " # DSK 1e integrals\n", - " h1eff, energy_core = casci_object.get_h1eff()\n", - " log.debug(\"core energy = %.15g\", energy_core)\n", - " t1 = log.timer(\"effective h1e in CAS space\", *t1)\n", - "\n", - " if h1eff.shape[0] != ncas:\n", - " raise RuntimeError(\n", - " \"Active space size error. nmo=%d ncore=%d ncas=%d\" # pylint: disable=consider-using-f-string\n", - " % (mo_coeff.shape[1], casci_object.ncore, ncas)\n", - " )\n", - "\n", - " # DSK fcisolver needs to be defined in accordance with SQD\n", - " # in this software stack it is done in the \"solve_solvent\" portion of the code.\n", - " myci = casci_object.fcisolver\n", - " e_cas, sqdvec = fci.selected_ci.kernel_fixed_space(\n", - " myci,\n", - " h1eff,\n", - " eri_cas,\n", - " norb,\n", - " (n_up, n_dn),\n", - " ci_strs=ci_strs_sqd,\n", - " verbose=sqd_verbose,\n", - " max_cycle=max_davidson,\n", - " )\n", - "\n", - " # DSK fcivec is the general name for CI vector assigned by PySCF.\n", - " # Depending on type of solver it is either FCI or SCI vector.\n", - " # In case of sqd we can call it \"sqdvec\" for clarity.\n", - " # Nonetheless, for further processing PySCF expects\n", - " # this data structure to be called fcivec, regardless of the used solver.\n", - "\n", - " fcivec = sqdvec\n", - "\n", - " t1 = log.timer(\"CI solver\", *t1)\n", - " e_tot = energy_core + e_cas\n", - "\n", - " # Returns either standard CASCI data or SQD data. Return depends on \"sqd_run\" True/False.\n", - " return e_tot, e_cas, fcivec\n", - "\n", - "\n", - "# Replace standard CASCI kernel with the SQD-compatible CASCI kernel defined above\n", - "casci.kernel = kernel\n", - "\n", - "\n", - "def solve_solvent(\n", - " bitstring_matrix: tuple[np.ndarray, np.ndarray] | np.ndarray,\n", - " /,\n", - " myeps: float,\n", - " mysolvmethod: str,\n", - " myavas: list,\n", - " num_orbitals: int,\n", - " *,\n", - " spin_sq: int | None = None,\n", - " max_davidson: int = 100,\n", - " verbose: int | None = 0,\n", - " checkpoint_file: str,\n", - ") -> tuple[float, SCIState, list[np.ndarray], float]:\n", - " \"\"\"Approximate the ground state given molecular integrals and a set of electronic configurations.\n", - "\n", - " Args:\n", - " bitstring_matrix: A set of configurations defining the subspace onto which the Hamiltonian\n", - " will be projected and diagonalized. This is a 2D array of ``bool`` representations of bit\n", - " values such that each row represents a single bitstring. The spin-up configurations\n", - " should be specified by column indices in range ``(N, N/2]``, and the spin-down\n", - " configurations should be specified by column indices in range ``(N/2, 0]``, where ``N``\n", - " is the number of qubits.\n", - "\n", - " (DEPRECATED) The configurations may also be specified by a length-2 tuple of sorted 1D\n", - " arrays containing unsigned integer representations of the determinants. The two lists\n", - " should represent the spin-up and spin-down orbitals, respectively.\n", - "\n", - " To build PCM model PySCF needs the structure of the molecule. Hence, the electron integrals\n", - " (hcore and eri) are not enough to form IEF-PCM simulation. Instead the \"start.chk\" file is used.\n", - " This workflow also requires additional information about solute and solvent,\n", - " which is reflected by additional arguments below\n", - "\n", - " myeps: Dielectric parameter of the solvent.\n", - " mysolvmethod: Solvent model, which can be IEF-PCM, COSMO, C-PCM, SS(V)PE,\n", - " see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", - " At the moment only IEF-PCM was tested.\n", - " In principle two other models from PySCF \"solvent\" module can be used as well,\n", - " namely SMD and polarizable embedding (PE).\n", - " The SMD and PE were not tested yet and their usage requires addition of more\n", - " input arguments for \"solve_solvent\".\n", - " myavas: This argument allows user to select active space in solute with AVAS.\n", - " The corresponding list should include target atomic orbitals.\n", - " If myavas=None, then active space selected based on number of orbitals\n", - " derived from ci_strs.\n", - " It is assumed that if myavas=None, then the target calculation is either\n", - " a) corresponds to full basis case.\n", - " b) close to full basis case and only few core orbitals are excluded.\n", - " num_orbitals: Number of orbitals, which is essential when myavas = None.\n", - " In AVAS case number of orbitals and electrons is derived by AVAS procedure itself.\n", - " spin_sq: Target value for the total spin squared for the ground state.\n", - " If ``None``, no spin will be imposed.\n", - " max_davidson: The maximum number of cycles of Davidson's algorithm\n", - " verbose: A verbosity level between 0 and 10\n", - " checkpoint_file: Name of the checkpoint file\n", - "\n", - " NOTE: For now open shell functionality is not supported in SQD PCM calculations.\n", - " Hence, at the moment solve_solvent does not include open_shell as one of the arguments.\n", - "\n", - " Returns:\n", - " - Minimum energy from SCI calculation\n", - " - The SCI ground state\n", - " - Average occupancy of the alpha and beta orbitals, respectively\n", - " - Expectation value of spin-squared\n", - " - Solvation free energy\n", - "\n", - " \"\"\"\n", - " # Unlike the \"solve_fermion\", the \"solve_solvent\" utilizes the \"checkpoint\" file to\n", - " # get the starting HF information, which means that \"solve_solvent\" does not accept\n", - " # \"hcore\" and \"eri\" as the input arguments.\n", - " # Instead \"hcore\" and \"eri\" are generated inside of the custom SQD-compatible\n", - " # CASCI kernel (defined above).\n", - " # The generation of \"hcore\" and \"eri\" is based on the information from \"checkpoint\" file\n", - " # as well as \"myavas\" and \"num_orbitals\" input arguments.\n", - "\n", - " # DSK this part handles addresses and is identical to \"solve_fermion\"\n", - " if isinstance(bitstring_matrix, tuple):\n", - " warnings.warn(\n", - " \"Passing the input determinants as integers is deprecated. \"\n", - " \"Users should instead pass a bitstring matrix defining the subspace.\",\n", - " DeprecationWarning,\n", - " stacklevel=2,\n", - " )\n", - " ci_strs = bitstring_matrix\n", - " else:\n", - " # This will become the default code path after the deprecation period.\n", - " ci_strs = bitstring_matrix_to_ci_strs(bitstring_matrix, open_shell=False)\n", - " ci_strs = _check_ci_strs(ci_strs)\n", - "\n", - " num_up = format(ci_strs[0][0], \"b\").count(\"1\")\n", - " num_dn = format(ci_strs[1][0], \"b\").count(\"1\")\n", - "\n", - " # DSK assign verbosity\n", - " verbose_ci = verbose\n", - "\n", - " # DSK add information about solute and solvent.\n", - " # Since PCM model needs the information about the structure of the molecule\n", - " # one cannot use only FCIDUMP. Instead converged HF data can be passed from \"checkpoint\" file\n", - " # along with \"mol\" object containing the geometry and other information about the solute.\n", - "\n", - " ############################################\n", - " # This section is specific to \"solve_solvent\" and is not present in \"solve_fermion\".\n", - " # In case of \"solve_fermion\" the \"eri\" and \"hcore\" are passed directly to\n", - " # \"fci.selected_ci.kernel_fixed_space\".\n", - " # In case of \"solve_solvent\" the incorporation of the polarizable continuum model\n", - " # requires utilization of \"CASCI.with_solvent\"\n", - " # data object from PySCF, where underlying CASCI.base_kernel has to be replaced\n", - " # with SQD-compatible version.\n", - " # Due to these differences in the implementation the \"solve_solvent\" recovers\n", - " # the converged mean field results and \"molecule\" object from \"checkpoint\" file\n", - " # (instead of using FCIDUMP),\n", - " # followed by passing of solute, solvent, and active space information to \"CASCI.with_solvent\".\n", - " # This includes the initiation of \"mol\", \"cm\", \"mf\", and \"mc\" data structures.\n", - "\n", - " mol = chkfile.load_mol(checkpoint_file)\n", - "\n", - " # DSK Initiation of the solvent model\n", - " cm = pcm.PCM(mol)\n", - " cm.eps = myeps # solute eps value\n", - " cm.method = mysolvmethod # IEF-PCM, COSMO, C-PCM, SS(V)PE,\n", - " # see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", - "\n", - " # DSK Read-in converged RHF solution\n", - " scf_result_dic = chkfile.load(checkpoint_file, \"scf\")\n", - " mf = scf.RHF(mol).PCM(cm)\n", - " mf.__dict__.update(scf_result_dic)\n", - "\n", - " # Identify the active space based on the user input of AVAS or number of orbitals and electrons\n", - " if myavas is not None:\n", - " orbs = myavas\n", - " avas_obj = avas.AVAS(mf, orbs, with_iao=True)\n", - " avas_obj.kernel()\n", - " ncas, nelecas, _, _, _ = (\n", - " avas_obj.ncas,\n", - " avas_obj.nelecas,\n", - " avas_obj.mo_coeff,\n", - " avas_obj.occ_weights,\n", - " avas_obj.vir_weights,\n", - " )\n", - " else:\n", - " ncas = num_orbitals\n", - " nelecas = (num_up, num_dn)\n", - "\n", - " # Initiate the \"CASCI.with_solvent\" object\n", - " mc = casci.CASCI(mf, ncas=ncas, nelecas=nelecas).PCM(cm)\n", - " # Replace mo_coeff with ones produced by AVAS if AVAS is utilized\n", - " if myavas is not None:\n", - " mc.mo_coeff = avas_obj.mo_coeff\n", - " # Read-in the configuration interaction subspace derived from LUCJ and S-CORE\n", - " mc.batch = ci_strs\n", - " # Assign number of maximum Davidson steps\n", - " mc.max_davidson = max_davidson\n", - "\n", - " ####### The definition of \"fcisolver\" object is identical to \"solve_fermion\" case ########\n", - " myci = fci.selected_ci.SelectedCI()\n", - " if spin_sq is not None:\n", - " myci = fci.addons.fix_spin_(myci, ss=spin_sq)\n", - " mc.fcisolver = myci\n", - " mc.verbose = verbose_ci\n", - " #########################################################################################\n", - "\n", - " # Initiate the \"CASCI.with_solvent\" simulation with SQD-compatible based CASCI kernel.\n", - " mc_result = mc.kernel()\n", - "\n", - " # Get data out of the \"CASCI.with_solvent\" object\n", - " e_sci = mc_result[0]\n", - " sci_vec = mc_result[2]\n", - " # Here we get additional output comparing to \"solve_fermion\",\n", - " # which is the solvation free energy (G_solv)\n", - " g_solv = mc.with_solvent.e\n", - "\n", - " #####################################################\n", - " # The remainder of the code in solve_solvent is nearly identical to solve_fermion code.\n", - "\n", - " # However, there are two exceptions in \"solve_solvent\":\n", - "\n", - " # 1) The dm2 is currently not computed, but can be included if needed\n", - " # 2) e_sci is directly output as the result of CASCI.with_solvent object.\n", - "\n", - " # Hence, the two following lines of code are not present in \"solve_solvent\"\n", - " # comparing to the \"solve_fermion\" code:\n", - "\n", - " # dm2 = myci.make_rdm2(sci_vec, norb, (num_up, num_dn))\n", - " # e_sci = np.einsum(\"pr,pr->\", dm1, hcore) + 0.5 * np.einsum(\"prqs,prqs->\", dm2, eri)\n", - "\n", - " # Calculate the avg occupancy of each orbital\n", - " dm1 = myci.make_rdm1s(sci_vec, ncas, (num_up, num_dn))\n", - " avg_occupancy = [np.diagonal(dm1[0]), np.diagonal(dm1[1])]\n", - "\n", - " # Compute total spin\n", - " spin_squared = myci.spin_square(sci_vec, ncas, (num_up, num_dn))[0]\n", - "\n", - " # Convert the PySCF SCIVector to internal format. We access a private field here,\n", - " # so we assert that we expect the SCIVector output from kernel_fixed_space to\n", - " # have its _strs field populated with alpha and beta strings.\n", - " assert isinstance(sci_vec._strs[0], np.ndarray) and isinstance(sci_vec._strs[1], np.ndarray)\n", - " assert sci_vec.shape == (len(sci_vec._strs[0]), len(sci_vec._strs[1]))\n", - " sci_state = SCIState(\n", - " amplitudes=np.array(sci_vec),\n", - " ci_strs_a=sci_vec._strs[0],\n", - " ci_strs_b=sci_vec._strs[1],\n", - " )\n", - "\n", - " return e_sci, sci_state, avg_occupancy, spin_squared, g_solv" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b478edb", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "%%writefile ./source_files/sqc_pcm_entrypoint.py\n", - "\n", - "# This code is part of a Qiskit project.\n", - "#\n", - "# (C) Copyright IBM and Cleveland Clinic 2025\n", - "#\n", - "# This code is licensed under the Apache License, Version 2.0. You may\n", - "# obtain a copy of this license in the LICENSE.txt file in the root directory\n", - "# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.\n", - "#\n", - "# Any modifications or derivative works of this code must retain this\n", - "# copyright notice, and modified files need to carry a notice indicating\n", - "# that they have been altered from the originals.\n", - "\n", - "\"\"\"\n", - "SQD-PCM Function Template source code.\n", - "\"\"\"\n", - "from pathlib import Path\n", - "from typing import Any\n", - "from datetime import datetime\n", - "import os\n", - "import sys\n", - "import json\n", - "import logging\n", - "import time\n", - "import traceback\n", - "import numpy as np\n", - "\n", - "import ffsim\n", - "\n", - "from pyscf import gto, scf, mcscf, ao2mo, tools, cc\n", - "from pyscf.lib import chkfile\n", - "from pyscf.mcscf import avas\n", - "from pyscf.solvent import pcm\n", - "\n", - "from qiskit import QuantumCircuit, QuantumRegister\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.primitives import BackendSamplerV2\n", - "\n", - "from qiskit_addon_sqd.counts import counts_to_arrays\n", - "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n", - "from qiskit_addon_sqd.fermion import bitstring_matrix_to_ci_strs\n", - "from qiskit_addon_sqd.subsampling import postselect_and_subsample\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2\n", - "from qiskit_serverless import get_arguments, save_result, distribute_task, get, update_status, Job\n", - "\n", - "current_dir = os.path.dirname(os.path.abspath(__file__))\n", - "sys.path.insert(0, current_dir)\n", - "from solve_solvent import solve_solvent # pylint: disable=wrong-import-position\n", - "\n", - "logger = logging.getLogger(__name__)\n", - "\n", - "\n", - "def run_function(\n", - " backend_name: str,\n", - " molecule: dict,\n", - " solvent_options: dict,\n", - " sqd_options: dict,\n", - " lucj_options: dict | None = None,\n", - " **kwargs,\n", - ") -> dict[str, Any]:\n", - " \"\"\"\n", - " Main entry point for the SQD-PCM (Polarizable Continuum Model) workflow.\n", - "\n", - " This function encapsulates the end-to-end execution of the algorithm.\n", - "\n", - " Args:\n", - " backend_name: Identifier for the target backend, required for all\n", - " workflows that access IBM Quantum hardware.\n", - "\n", - " molecule: dictionary with molecule information:\n", - " - \"atom\" (str): required field, follows pyscf specification for atomic geometry.\n", - " For example, for methanol the value would be::\n", - "\n", - " '''\n", - " O -0.04559 -0.75076 -0.00000;\n", - " C -0.04844 0.65398 -0.00000;\n", - " H 0.85330 -1.05128 -0.00000;\n", - " H -1.08779 0.98076 -0.00000;\n", - " H 0.44171 1.06337 0.88811;\n", - " H 0.44171 1.06337 -0.88811;\n", - " '''\n", - "\n", - " - \"number_of_active_orb\" (int): required field\n", - " - \"number_of_active_alpha_elec\" (int): required field\n", - " - \"number_of_active_beta_elec\" (int): required field\n", - " - \"basis\" (str): optional field, default is \"sto-3g\"\n", - " - \"verbosity\" (int): optional field, default is 0\n", - " - \"charge\" (int): optional field, default is 0\n", - " - \"spin\" (int): optional field, default is 0\n", - " - \"avas_selection\" (list[str] | None): optional field, default is None\n", - "\n", - " solvent_options: dictionary with solvent options information:\n", - " - \"method\" (str): required field. Method for computing solvent reaction field\n", - " for the PCM. Accepted values are: \"IEF-PCM\", \"COSMO\",\n", - " \"C-PCM\", \"SS(V)PE\", see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", - " - \"eps\" (float): required field. Dielectric constant of the solvent in the PCM.\n", - "\n", - " sqd_options: dictionary with sqd options information:\n", - " - \"sqd_iterations\" (int): required field.\n", - " - \"number_of_batches\" (int): required field.\n", - " - \"samples_per_batch\" (int): required field.\n", - " - \"max_davidson_cycles\" (int): required field.\n", - "\n", - " lucj_options: optional dictionary with lucj options information:\n", - " - \"optimization_level\" (int): optional field, default is 2\n", - " - \"initial_layout\" (list[int]): optional field, default is None\n", - " - \"dynamical_decoupling\" (bool): optional field, default is True\n", - " - \"twirling\" (bool): optional field, default is True\n", - " - \"number_of_shots\" (int): optional field, default is 10000\n", - "\n", - " **kwargs\n", - " Optional keyword arguments to customize behavior. Existing kwargs include:\n", - " - \"files_name\" (str): optional name for output files (enabled for local testing)\n", - " - \"testing_backend\" (FakeBackendV2): optional fake backend instance to bypass\n", - " qiskit runtime service instantiation (enabled for local testing)\n", - " - \"count_dict_file_name\" (str): path to a count dict file to bypass primitive\n", - " execution and jump directly to SQD section (enabled for local testing)\n", - "\n", - " Returns:\n", - " The function should return the execution results as a dictionary with string keys.\n", - " This is to ensure compatibility with ``qiskit_serverless.save_result``.\n", - " \"\"\"\n", - "\n", - " # Preparation Step: Input validation.\n", - " # Do this at the top of the function definition so it fails early if any required\n", - " # arguments are missing or invalid.\n", - "\n", - " # Molecule parsing\n", - " # Required:\n", - " geo = molecule[\"atom\"]\n", - " num_active_orb = molecule[\"number_of_active_orb\"]\n", - " num_active_alpha = molecule[\"number_of_active_alpha_elec\"]\n", - " num_active_beta = molecule[\"number_of_active_beta_elec\"]\n", - " # Optional:\n", - " input_basis = molecule.get(\"basis\", \"sto-3g\")\n", - " input_verbosity = molecule.get(\"verbosity\", 0)\n", - " input_charge = molecule.get(\"charge\", 0)\n", - " input_spin = molecule.get(\"spin\", 0)\n", - " myavas = molecule.get(\"avas_selection\", None)\n", - "\n", - " # Solvent options parsing\n", - " myeps = solvent_options[\"eps\"]\n", - " mymethod = solvent_options[\"method\"]\n", - "\n", - " # LUCJ options parsing\n", - " if lucj_options is None:\n", - " lucj_options = {}\n", - " opt_level = lucj_options.get(\"optimization_level\", 2)\n", - " initial_layout = lucj_options.get(\"initial_layout\", None)\n", - " use_dd = lucj_options.get(\"dynamical_decoupling\", True)\n", - " use_twirling = lucj_options.get(\"twirling\", True)\n", - " num_shots = lucj_options.get(\"number_of_shots\", True)\n", - "\n", - " # SQD options parsing\n", - " iterations = sqd_options[\"sqd_iterations\"]\n", - " n_batches = sqd_options[\"number_of_batches\"]\n", - " samples_per_batch = sqd_options[\"samples_per_batch\"]\n", - " max_davidson_cycles = sqd_options[\"max_davidson_cycles\"]\n", - "\n", - " # kwarg parsing (local testing)\n", - " testing_backend = kwargs.get(\"testing_backend\", None)\n", - " count_dict_file_name = kwargs.get(\"count_dict_file_name\", None)\n", - "\n", - " files_name = kwargs.get(\"files_name\", datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\"))\n", - " output_path = Path.cwd() / \"output_sqd_pcm\"\n", - " output_path.mkdir(exist_ok=True)\n", - " datafiles_name = str(output_path) + \"/\" + files_name\n", - "\n", - " # --\n", - " # Preparation Step: Qiskit Runtime & primitive configuration for\n", - " # execution on IBM Quantum hardware.\n", - "\n", - " if testing_backend is None:\n", - " # Initialize Qiskit Runtime Service\n", - " logger.info(\"Starting runtime service\")\n", - " service = QiskitRuntimeService(\n", - " channel=os.environ[\"QISKIT_IBM_CHANNEL\"],\n", - " instance=os.environ[\"IBM_CLOUD_INSTANCE\"],\n", - " token=os.environ[\"your-API_KEY\"], # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", - " )\n", - " backend = service.backend(backend_name)\n", - " logger.info(f\"Backend: {backend.name}\")\n", - "\n", - " # Set up sampler and corresponding options\n", - " sampler = SamplerV2(backend)\n", - " sampler.options.dynamical_decoupling.enable = use_dd\n", - " sampler.options.twirling.enable_measure = False\n", - " sampler.options.twirling.enable_gates = use_twirling\n", - " sampler.options.default_shots = num_shots\n", - " else:\n", - " backend = testing_backend\n", - " logger.info(f\"Testing backend: {backend.name}\")\n", - "\n", - " # Set up backend sampler.\n", - " # This doesn't allow running with twirling and dd\n", - " sampler = BackendSamplerV2(backend=testing_backend)\n", - "\n", - " # Once the preparation steps are completed, the algorithm can be structured following a\n", - " # Qiskit Pattern workflow:\n", - " # https://docs.quantum.ibm.com/guides/intro-to-patterns\n", - "\n", - " # --\n", - " # Step 1: Map\n", - " # In this step, input arguments are used to construct relevant quantum circuits and operators\n", - "\n", - " start_mapping = time.time()\n", - " update_status(Job.MAPPING)\n", - "\n", - " # Initialize the molecule object (pyscf)\n", - " logger.info(\"Initializing molecule object\")\n", - " mol = gto.Mole()\n", - " mol.build(\n", - " atom=geo,\n", - " basis=input_basis,\n", - " verbose=input_verbosity,\n", - " charge=input_charge,\n", - " spin=input_spin,\n", - " symmetry=False,\n", - " ) # Not tested for symmetry calculations\n", - "\n", - " cm = pcm.PCM(mol)\n", - " cm.eps = myeps\n", - " cm.method = mymethod\n", - "\n", - " mf = scf.RHF(mol).PCM(cm)\n", - " # Generation of checkpoint file for the solute and solvent\n", - " # which will be used reused in all subsequent sections\n", - " checkpoint_file_name = str(datafiles_name + \".chk\")\n", - " mf.chkfile = checkpoint_file_name\n", - " mf.kernel()\n", - "\n", - " # Read-in the information about the molecule\n", - " mol = chkfile.load_mol(checkpoint_file_name)\n", - "\n", - " # Read-in RHF data\n", - " scf_result_dic = chkfile.load(checkpoint_file_name, \"scf\")\n", - " mf = scf.RHF(mol)\n", - " mf.__dict__.update(scf_result_dic)\n", - "\n", - " # LUCJ uses isolated solute\n", - " mf.kernel()\n", - "\n", - " # Initialize orbital selection based on user input\n", - " if myavas is not None:\n", - " orbs = myavas\n", - " avas_out = avas.AVAS(mf, orbs, with_iao=True)\n", - " avas_out.kernel()\n", - " ncas, nelecas = (avas_out.ncas, avas_out.nelecas)\n", - " else:\n", - " ncas = num_active_orb\n", - " nelecas = (\n", - " num_active_alpha,\n", - " num_active_beta,\n", - " )\n", - "\n", - " # LUCJ Step:\n", - " # Generate active space\n", - " mc = mcscf.CASCI(mf, ncas=ncas, nelecas=nelecas)\n", - " if myavas is not None:\n", - " mc.mo_coeff = avas_out.mo_coeff\n", - " mc.batch = None\n", - " # Reliable and most convenient way to do the CCSD on only the active space\n", - " # is to create the FCIDUMP file and then run the CCSD calculation only on the\n", - " # orbitals stored in the FCIDUMP file.\n", - "\n", - " h1e_cas, ecore = mc.get_h1eff()\n", - " h2e_cas = ao2mo.restore(1, mc.get_h2eff(), mc.ncas)\n", - "\n", - " fcidump_file_name = str(datafiles_name + \".fcidump.txt\")\n", - " tools.fcidump.from_integrals(\n", - " fcidump_file_name,\n", - " h1e_cas,\n", - " h2e_cas,\n", - " ncas,\n", - " nelecas,\n", - " nuc=ecore,\n", - " ms=0,\n", - " orbsym=[1] * ncas,\n", - " )\n", - "\n", - " logger.info(\"Performing CCSD\")\n", - " # Read FCIDUMP and perform CCSD on only active space\n", - " mf_as = tools.fcidump.to_scf(fcidump_file_name)\n", - " mf_as.kernel()\n", - "\n", - " mc_cc = cc.CCSD(mf_as)\n", - " mc_cc.kernel()\n", - " mc_cc.t1 # pylint: disable=pointless-statement\n", - " t2 = mc_cc.t2\n", - "\n", - " n_reps = 2\n", - " norb = ncas\n", - "\n", - " if myavas is not None:\n", - " nelec = (int(nelecas / 2), int(nelecas / 2))\n", - " else:\n", - " nelec = nelecas\n", - "\n", - " alpha_alpha_indices = [(p, p + 1) for p in range(norb - 1)]\n", - " alpha_beta_indices = [(p, p) for p in range(0, norb, 4)]\n", - "\n", - " logger.info(f\"Same spin orbital connections: {alpha_alpha_indices}\")\n", - " logger.info(f\"Opposite spin orbital connections: {alpha_beta_indices}\")\n", - "\n", - " # Construct LUCJ op\n", - " ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n", - " t2, n_reps=n_reps, interaction_pairs=(alpha_alpha_indices, alpha_beta_indices)\n", - " )\n", - " # Construct circuit\n", - " qubits = QuantumRegister(2 * norb, name=\"q\")\n", - " circuit = QuantumCircuit(qubits)\n", - " circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)\n", - " circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n", - " circuit.measure_all()\n", - " end_mapping = time.time()\n", - "\n", - " # --\n", - " # Step 2: Optimize\n", - " # Transpile circuits to match ISA\n", - "\n", - " start_optimizing = time.time()\n", - " update_status(Job.OPTIMIZING_HARDWARE)\n", - "\n", - " pass_manager = generate_preset_pass_manager(\n", - " optimization_level=opt_level,\n", - " backend=backend,\n", - " initial_layout=initial_layout,\n", - " )\n", - "\n", - " pass_manager.pre_init = ffsim.qiskit.PRE_INIT\n", - " transpiled = pass_manager.run(circuit)\n", - "\n", - " end_optimizing = time.time()\n", - " logger.info(\n", - " f\"Optimization level: {opt_level}, ops: {transpiled.count_ops()}, depth: {transpiled.depth()}\"\n", - " )\n", - "\n", - " two_q_depth = transpiled.depth(lambda x: x.operation.num_qubits == 2)\n", - " logger.info(f\"Two-qubit gate depth: {two_q_depth}\")\n", - "\n", - " # --\n", - " # Step 3: Execute on Hardware\n", - " # Submit the underlying Sampler job. Note that this is not the\n", - " # actual function job.\n", - " if count_dict_file_name is None:\n", - " # Submit the LUCJ job\n", - " logger.info(\"Submitting sampler job\")\n", - " job = sampler.run([transpiled])\n", - " logger.info(f\"Job ID: {job.job_id()}\")\n", - " logger.info(f\"Job Status: {job.status()}\")\n", - "\n", - " start_waiting_qpu = time.time()\n", - " while job.status() == \"QUEUED\":\n", - " update_status(Job.WAITING_QPU)\n", - " time.sleep(5)\n", - "\n", - " end_waiting_qpu = time.time()\n", - " update_status(Job.EXECUTING_QPU)\n", - "\n", - " # Wait until job is complete\n", - " result = job.result()\n", - " end_executing_qpu = time.time()\n", - "\n", - " pub_result = result[0]\n", - " counts_dict = pub_result.data.meas.get_counts()\n", - "\n", - " waiting_qpu_time = end_waiting_qpu - start_waiting_qpu\n", - " executing_qpu_time = end_executing_qpu - end_waiting_qpu\n", - " else:\n", - " # read LUCJ samples from count_dict\n", - " logger.info(\"Skipping sampler, loading counts dict from file\")\n", - " with open(count_dict_file_name, \"r\") as file:\n", - " count_dict_string = file.read().replace(\"\\n\", \"\")\n", - " counts_dict = json.loads(count_dict_string.replace(\"'\", '\"'))\n", - " waiting_qpu_time = 0\n", - " executing_qpu_time = 0\n", - "\n", - " # --\n", - " # Step 4: Post-process\n", - "\n", - " start_pp = time.time()\n", - " update_status(Job.POST_PROCESSING)\n", - "\n", - " # SQD-PCM section\n", - " start = time.time()\n", - "\n", - " # Orbitals, electron, and spin initialization\n", - " num_orbitals = ncas\n", - " if myavas is not None:\n", - " num_elec_a = num_elec_b = int(nelecas / 2)\n", - " else:\n", - " num_elec_a, num_elec_b = nelecas\n", - " spin_sq = input_spin\n", - "\n", - " # Convert counts into bitstring and probability arrays\n", - " bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts_dict)\n", - "\n", - " # We set qiskit_serverless to explicitly reserve 1 cpu per thread, as\n", - " # the task is CPU-bound and might degrade in performance when sharing\n", - " # a core at scale (this might not be the case with smaller examples)\n", - " @distribute_task(target={\"cpu\": 1})\n", - " def solve_solvent_parallel(\n", - " batches,\n", - " myeps,\n", - " mysolvmethod,\n", - " myavas,\n", - " num_orbitals,\n", - " spin_sq,\n", - " max_davidson,\n", - " checkpoint_file,\n", - " ):\n", - " return solve_solvent( # sqd for pyscf\n", - " batches,\n", - " myeps,\n", - " mysolvmethod,\n", - " myavas,\n", - " num_orbitals,\n", - " spin_sq=spin_sq,\n", - " max_davidson=max_davidson,\n", - " checkpoint_file=checkpoint_file,\n", - " )\n", - "\n", - " e_hist = np.zeros((iterations, n_batches)) # energy history\n", - " s_hist = np.zeros((iterations, n_batches)) # spin history\n", - " g_solv_hist = np.zeros((iterations, n_batches)) # g_solv history\n", - " occupancy_hist = []\n", - " avg_occupancy = None\n", - "\n", - " num_ran_iter = 0\n", - " for i in range(iterations):\n", - " logger.info(f\"Starting configuration recovery iteration {i}\")\n", - " # On the first iteration, we have no orbital occupancy information from the\n", - " # solver, so we begin with the full set of noisy configurations.\n", - " if avg_occupancy is None:\n", - " bs_mat_tmp = bitstring_matrix_full\n", - " probs_arr_tmp = probs_arr_full\n", - "\n", - " # If we have average orbital occupancy information, we use it to refine the full\n", - " # set of noisy configurations\n", - " else:\n", - " bs_mat_tmp, probs_arr_tmp = recover_configurations(\n", - " bitstring_matrix_full, probs_arr_full, avg_occupancy, num_elec_a, num_elec_b\n", - " )\n", - "\n", - " # Create batches of subsamples. We postselect here to remove configurations\n", - " # with incorrect hamming weight during iteration 0, since no config recovery was performed.\n", - " batches = postselect_and_subsample(\n", - " bs_mat_tmp,\n", - " probs_arr_tmp,\n", - " hamming_right=num_elec_a,\n", - " hamming_left=num_elec_b,\n", - " samples_per_batch=samples_per_batch,\n", - " num_batches=n_batches,\n", - " )\n", - "\n", - " # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n", - " e_tmp = np.zeros(n_batches)\n", - " s_tmp = np.zeros(n_batches)\n", - " g_solvs_tmp = np.zeros(n_batches)\n", - " occs_tmp = []\n", - " coeffs = []\n", - "\n", - " res1 = []\n", - " for j in range(n_batches):\n", - " strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])\n", - " logger.info(f\"Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}\")\n", - "\n", - " res1.append(\n", - " solve_solvent_parallel(\n", - " batches[j],\n", - " myeps,\n", - " mymethod,\n", - " myavas,\n", - " num_orbitals,\n", - " spin_sq=spin_sq,\n", - " max_davidson=max_davidson_cycles,\n", - " checkpoint_file=checkpoint_file_name,\n", - " )\n", - " )\n", - "\n", - " res = get(res1)\n", - "\n", - " for j in range(n_batches):\n", - " energy_sci, coeffs_sci, avg_occs, spin, g_solv = res[j]\n", - " e_tmp[j] = energy_sci\n", - " s_tmp[j] = spin\n", - " g_solvs_tmp[j] = g_solv\n", - " occs_tmp.append(avg_occs)\n", - " coeffs.append(coeffs_sci)\n", - "\n", - " # Combine batch results\n", - " avg_occupancy = tuple(np.mean(occs_tmp, axis=0))\n", - "\n", - " # Track optimization history\n", - " e_hist[i, :] = e_tmp\n", - " s_hist[i, :] = s_tmp\n", - " g_solv_hist[i, :] = g_solvs_tmp\n", - " occupancy_hist.append(avg_occupancy)\n", - "\n", - " lowest_e_batch_index = np.argmin(e_hist[i, :])\n", - "\n", - " logger.info(f\"Lowest energy batch: {lowest_e_batch_index}\")\n", - " logger.info(f\"Lowest energy value: {np.min(e_hist[i, :])}\")\n", - " logger.info(f\"Corresponding g_solv value: {g_solv_hist[i, lowest_e_batch_index]}\")\n", - " logger.info(\"-----------------------------------\")\n", - " num_ran_iter += 1\n", - "\n", - " end_pp = time.time()\n", - " end = time.time()\n", - " duration = end - start\n", - " logger.info(f\"SCI_solver totally takes: {duration} seconds\")\n", - "\n", - " metadata = {\n", - " \"resources_usage\": {\n", - " \"RUNNING: MAPPING\": {\n", - " \"CPU_TIME\": end_mapping - start_mapping,\n", - " },\n", - " \"RUNNING: OPTIMIZING_FOR_HARDWARE\": {\n", - " \"CPU_TIME\": end_optimizing - start_optimizing,\n", - " },\n", - " \"RUNNING: WAITING_FOR_QPU\": {\n", - " \"CPU_TIME\": waiting_qpu_time,\n", - " },\n", - " \"RUNNING: EXECUTING_QPU\": {\n", - " \"QPU_TIME\": executing_qpu_time,\n", - " },\n", - " \"RUNNING: POST_PROCESSING\": {\n", - " \"CPU_TIME\": end_pp - start_pp,\n", - " },\n", - " },\n", - " \"num_iterations_executed\": num_ran_iter,\n", - " }\n", - "\n", - " output = {\n", - " \"total_energy_hist\": e_hist,\n", - " \"spin_squared_value_hist\": s_hist,\n", - " \"solvation_free_energy_hist\": g_solv_hist,\n", - " \"occupancy_hist\": occupancy_hist,\n", - " \"lowest_energy_batch\": lowest_e_batch_index,\n", - " \"lowest_energy_value\": np.min(e_hist[i, :]),\n", - " \"solvation_free_energy\": g_solv_hist[i, lowest_e_batch_index],\n", - " \"sci_solver_total_duration\": duration,\n", - " \"metadata\": metadata,\n", - " }\n", - "\n", - " return output\n", - "\n", - "\n", - "def set_up_logger(my_logger: logging.Logger, level: int = logging.INFO) -> None:\n", - " \"\"\"Logger setup to communicate logs through serverless.\"\"\"\n", - "\n", - " log_fmt = \"%(module)s.%(funcName)s:%(levelname)s:%(asctime)s: %(message)s\"\n", - " formatter = logging.Formatter(log_fmt)\n", - "\n", - " # Set propagate to `False` since handlers are to be attached.\n", - " my_logger.propagate = False\n", - "\n", - " stream_handler = logging.StreamHandler()\n", - " stream_handler.setFormatter(formatter)\n", - " my_logger.addHandler(stream_handler)\n", - " my_logger.setLevel(level)\n", - "\n", - "\n", - "# This is the section where `run_function` is called, it's boilerplate code and can be used\n", - "# without customization.\n", - "if __name__ == \"__main__\":\n", - "\n", - " # Use serverless helper function to extract input arguments,\n", - " input_args = get_arguments()\n", - "\n", - " # Allow to configure logging level\n", - " logging_level = input_args.get(\"logging_level\", logging.INFO)\n", - " set_up_logger(logger, logging_level)\n", - "\n", - " try:\n", - " func_result = run_function(**input_args)\n", - " # Use serverless function to save the results that\n", - " # will be returned in the job.\n", - " save_result(func_result)\n", - " except Exception:\n", - " save_result(traceback.format_exc())\n", - " raise\n", - "\n", - " sys.exit(0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "142fee48", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users. It verifies both source listings are identical then deletes the working folder we created\n", - "import shutil\n", - "\n", - "with open(\"./source_files/sqd_pcm_entrypoint.py\") as f1:\n", - " with open(\"./source_files/sqd_pcm_entrypoint.py\") as f2:\n", - " assert f1.read() == f2.read()\n", - "\n", - "with open(\"./source_files/solve_solvent.py\") as f1:\n", - " with open(\"./source_files/solve_solvent.py\") as f2:\n", - " assert f1.read() == f2.read()\n", - "\n", - "\n", - "with open(\"./source_files/__init__.py\") as f1:\n", - " with open(\"./source_files/__init__.py\") as f2:\n", - " assert f1.read() == f2.read()\n", - "\n", - "\n", - "shutil.rmtree(\"./source_files/\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8cfa587b", + "metadata": {}, + "source": [ + "---\n", + "title: Build a Qiskit Function for chemistry simulation\n", + "description: Learn how to deploy and execute the chemistry workflow template\n", + "---\n", + "\n", + "\n", + "# Build and run a Qiskit Function template for electronic structure simulation with an implicit solvent model\n", + "\n", + "{/* cspell:ignore pvdz, fcisolver, avas, ncas, nelecas, ecore, chkfile, fcivec, hcore, ncore, myci, sqdvec, myeps, mymethod, mysolvmethod, myavas, mcscf, MCSCF, chkfile, prqs */}" + ] + }, + { + "cell_type": "markdown", + "id": "f9bfec87", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [] + }, + { + "cell_type": "markdown", + "id": "f3a337d1", + "metadata": {}, + "source": [ + "This template, developed in collaboration with the Cleveland Clinic, consists of a workflow to calculate the ground state energy and solvation free energy of a molecule in an implicit solvent [[1]](#references). These simulations are based on the sample-based quantum diagonalization (SQD) method [[2-6]](#references) and the integral equation formalism polarizable continuum model (IEF-PCM) of solvent [[7]](#references).\n", + "\n", + "This guide utilizes the template with a methanol molecule as the solute, the electronic structure of which is simulated explicitly, and water as the solvent, approximated as a continuous dielectric medium. To account for the [electron correlation effects](https://onlinelibrary.wiley.com/doi/epdf/10.1002/ijch.202100111) in methanol, while maintaining the balance between the computational cost and accuracy, we only include the $\\sigma$, $\\sigma^{*}$, and lone pair orbitals in the active space simulated with SQD IEF-PCM. This orbital selection is done with [atomic valence active space (AVAS) method](https://github.com/pyscf/pyscf.github.io/blob/master/examples/mcscf/43-avas.py) using the C[2s,2p], O[2s,2p], and H[1s] atomic orbital components, which results in the active space of 14 electrons and 12 orbitals (14e,12o). The reference orbitals are calculated with closed-shell Hartree Fock using the cc-pvdz basis set." + ] + }, + { + "cell_type": "markdown", + "id": "25d18ba5", + "metadata": {}, + "source": [ + "## Workflow introduction\n", + "\n", + "\n", + "This interactive guide shows how to upload this function template to Qiskit Serverless and run an example workload. The template is structured as a Qiskit pattern with four steps:\n", + "\n", + "#### 1. Collect input and map the problem\n", + "\n", + "This step takes the geometry of the molecule, selected active space, solvation model, LUCJ options, and SQD options as an input. It then produces the PySCF Checkpoint file, which contains the Hartree-Fock (HF) IEF-PCM data. This data will be used in the SQD portion of the workflow. For the LUCJ portion of the workflow, the input section also generates the gas-phase HF data, which is stored internally in PySCF FCIDUMP format.\n", + "\n", + "The information from the HF gas-phase simulation and the definition of the active space are taken as input. Importantly, it also uses the user-defined information from the input section concerning the error suppression, number of shots, circuit transpiler optimization level, and the qubit layout.\n", + "\n", + "It generates one-electron and two-electron integrals within the defined active space. The integrals are then used to perform classical CCSD calculations, which return t2 amplitudes that we use to parametrize the LUCJ circuit.\n", + "\n", + "#### 2. Optimize the circuit\n", + "\n", + "The LUCJ circuit is then transpiled into an ISA circuit for the target hardware. A Sampler primitive is then instantiated with a default set of error mitigation options to manage the execution.\n", + "\n", + "#### 3. Execute the circuit\n", + "\n", + "The LUCJ calculations return the bitstrings for each measurement, where these bitstrings correspond to electron configurations of the studied system. The bitstrings are then used as input for post-processing.\n", + "\n", + "#### 4. Post-process by using SQD\n", + "\n", + "This final step takes the PySCF Checkpoint file containing the HF IEF-PCM information, the bitstrings representing the electron configurations predicted by LUCJ, and the user-defined SQD options selected in the input section as input. As output, it produces the SQD IEF-PCM total energy of the lowest energy batch and the corresponding solvation free energy." + ] + }, + { + "cell_type": "markdown", + "id": "4832ef18", + "metadata": {}, + "source": [ + "### Options\n", + "\n", + "For this template you must specify options for generating the LUCJ circuit, and SQD run parameters.\n", + "\n", + "#### LUCJ options\n", + "\n", + "When the LUCJ quantum circuit is executed, a set of samples that represent the computational basis states from the probability distribution of the molecular system are produced. To balance the depth of the LUCJ circuit and its expressibility, the qubits corresponding to the spin orbitals with the opposite spin have the two-qubit gates applied between them when these qubits are neighbors through a single ancilla qubit. To implement this approach on IBM hardware with a heavy-hex topology, qubits that represent the spin orbitals with the same spin are connected through a line topology where each line takes a zig-zag shape due to the heavy-hex connectivity of the target hardware, while the qubits that represent the spin orbitals with the opposite spin only have a connection at every fourth qubit.\n", + "\n", + "\n", + "\n", + "\n", + "The user has to provide the `initial_layout` array corresponding to the qubits that satisfy this [_zig-zag_ pattern](https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k) in the `lucj_options` section of the SQD IEF-PCM function. In case of SQD IEF-PCM (14e,12o)/cc-pvdz simulations of methanol, we chose the initial qubit layout corresponding to the main diagonal of the Eagle R3 QPU. Here, the first 12 elements of the `initial_layout` array `[0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, ...]` correspond to the alpha spin orbitals. The last 12 elements `[... 2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54]` correspond to beta spin orbitals.\n", + "\n", + "Importantly, the user has to determine the `number_of_shots`, which corresponds to the number of measurements in the LUCJ circuit. The number of shots needs to be sufficiently large because the first step of S-CORE procedure relies on the samples in the right particle sector to obtain the initial approximation to the ground-state occupation number distribution.\n", + "\n", + "The number of shots is highly system- and hardware-dependent, but [non-covalent](https://arxiv.org/abs/2410.09209), [fragment-based](https://arxiv.org/abs/2411.09861), and [implicit solvent](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030) SQD studies suggest that one can reach the chemical accuracy by following these guidelines:\n", + "\n", + "- 20,000 - 200,000 shots for systems with fewer than 16 molecular orbitals (32 spin orbitals)\n", + "- 200,000 shots for systems with 16 - 18 molecular orbitals\n", + "- 200,000 - 2,000,000 shots for systems with more than 18 molecular orbitals\n", + "\n", + "The required number of shots is affected by the number of spin orbitals in the studied system and by the size of the Hilbert space corresponding to the selected active space within the studied system. Generally, instances with smaller Hilbert spaces require fewer shots. Other available LUCJ options are [circuit transpiler optimization level](https://docs.quantum.ibm.com/guides/set-optimization) and [error suppression options](https://docs.quantum.ibm.com/guides/error-mitigation-and-suppression-techniques). Note that these options also affect the required number of shots and the resulting accuracy.\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "e2b46498", + "metadata": {}, + "source": [ + "#### SQD options\n", + "\n", + "Important options in SQD simulations include the `sqd_iterations`, `number_of_batches`, and `samples_per_batch`. Generally, the lower number of samples per batch can be counteracted with more batches (`number_of_batches`) and more iterations of S-CORE (`sqd_iterations`). With more batches we can sample more variations of the configurational subspaces. Since the lowest-energy batch is taken as the solution for the ground state energy of the system, more batches can improve the results through better statistics. Additional iterations of S-CORE allow more configurations to be recovered from the original LUCJ distribution if the number of samples in the correct particle sector is low. This can allow the number of samples per batch to be reduced.\n", + "\n", + "\n", + "\n", + "\n", + "An alternative strategy is to use more samples per batch, which ensures that most of the initial LUCJ samples in right particle space are used during the S-CORE procedure, and individual subspaces encapsulate a sufficient variety of electron configurations. In turn, this reduces the number of required S-CORE steps, where only two or three iterations of SQD are needed if the number of samples per batch is large enough. However, more samples per batch results in a higher computational cost of each diagonalization step. Hence, the balance between the accuracy and computational cost in SQD simulations can be achieved by choosing `sqd_iterations`, `number_of_batches`, and `samples_per_batch` optimally.\n", + "\n", + "The [SQD IEF-PCM study](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030) shows that when three iterations of S-CORE are used, the chemical accuracy can be reached by following these guidelines:\n", + "\n", + "- 600 samples per batch in methanol SQD IEF-PCM (14e,12o) simulations\n", + "- 1500 samples per batch in methylamine SQD IEF-PCM (14e,13o) simulations\n", + "- 6000 samples per batch in water SQD IEF-PCM (8e,23o) simulations\n", + "- 16000 samples per batch in ethanol SQD IEF-PCM (20e,18o) simulations\n", + "\n", + "Just like the required number of shots in LUCJ, the required number of samples per batch used in S-CORE procedure is highly system- and hardware-dependent. The examples above can be used to estimate the initial point for the benchmark of required number of samples per batch. The tutorial on systematic benchmark of the required number of samples per batch can be found [here](https://qiskit.github.io/qiskit-addon-sqd/how_tos/choose_subspace_dimension.html).\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "998a0f25", + "metadata": {}, + "source": [ + "## Deploy and execute the template SQD IEF-PCM function" + ] + }, + { + "cell_type": "markdown", + "id": "6c92ac84", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "### Authentication\n", + "\n", + "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key (token), which can be found on the [IBM Quantum Platform](https://quantum.cloud.ibm.com) dashboard. This allows for the instantiation of the serverless client to upload or run the selected function:\n", + "\n", + "```python\n", + "from qiskit_ibm_catalog import QiskitServerless\n", + "\n", + "serverless = QiskitServerless(\n", + " channel=\"ibm_quantum_platform\",\n", + " instance=\"INSTANCE_CRN\",\n", + " token=\"YOUR_API_KEY\" # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", + ")\n", + "```\n", + "\n", + "Optionally, use `save_account()` to save your credentials in a local environment (see the [Set up your IBM Cloud account](/docs/guides/cloud-setup#cloud-save) guide). Note that this writes your credentials to the same file as [`QiskitRuntimeService.save_account()`](/docs/api/qiskit-ibm-runtime/qiskit-runtime-service#save_account):\n", + "\n", + "```python\n", + "QiskitServerless.save_account(token=\"YOUR_API_KEY\", channel=\"ibm_quantum_platform\", instance=\"INSTANCE_CRN\")\n", + "```\n", + "\n", + "If the [account is saved](/docs/guides/save-credentials), there is no need to provide the token to authenticate:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9276e2d4", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitServerless\n", + "\n", + "serverless = QiskitServerless()" + ] + }, + { + "cell_type": "markdown", + "id": "e1f99d80", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "### Upload the template" + ] + }, + { + "cell_type": "markdown", + "id": "3e0e8cc8", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "To upload a custom Qiskit Function, you must first instantiate a `QiskitFunction` object that defines the function source code. The title will allow you to identify the function once it's in the remote cluster. The main entry point is the file that contains `if __name__ == \"__main__\"`. If your workflow requires additional source files, you can define a working directory that will be uploaded together with the entry point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77b2b9b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QiskitFunction(sqd_pcm_template)\n" + ] + } + ], + "source": [ + "from qiskit_ibm_catalog import QiskitFunction\n", + "\n", + "template = QiskitFunction(\n", + " title=\"sqd_pcm_template\",\n", + " entrypoint=\"sqd_pcm_entrypoint.py\",\n", + " working_dir=\"./source_files/\", # all files in this directory will be uploaded\n", + " dependencies=[\n", + " \"ffsim==0.0.54\",\n", + " \"pyscf==2.9.0\",\n", + " \"qiskit_addon_sqd==0.10.0\",\n", + " ],\n", + ")\n", + "print(template)" + ] + }, + { + "cell_type": "markdown", + "id": "72854f5a", + "metadata": {}, + "source": [ + "Once the instance is ready, upload it to serverless:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "59e7fdb5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "QiskitFunction(sqd_pcm_template)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.upload(template)" + ] + }, + { + "cell_type": "markdown", + "id": "ac7d8764", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "To check if the program successfully uploaded, use `serverless.list()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "03a91030", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[QiskitFunction(sqd_pcm_template),\n", + " QiskitFunction(hamiltonian_simulation_template)]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.list()" + ] + }, + { + "cell_type": "markdown", + "id": "99408586", + "metadata": {}, + "source": [ + "## Load and run the template remotely" + ] + }, + { + "cell_type": "markdown", + "id": "62b37d7a", + "metadata": {}, + "source": [ + "The function template has been uploaded, so you can run it remotely with Qiskit Serverless. First, load the template by name:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "854d12cf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QiskitFunction(sqd_pcm_template)\n" + ] + } + ], + "source": [ + "template = serverless.load(\"sqd_pcm_template\")\n", + "print(template)" + ] + }, + { + "cell_type": "markdown", + "id": "fa2dc721", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "Next, run the template with the domain-level inputs for SQD-IEF PCM. This example specifies a methanol-based workload." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a1719ab1", + "metadata": {}, + "outputs": [], + "source": [ + "molecule = {\n", + " \"atom\": \"\"\"\n", + " O -0.04559 -0.75076 -0.00000;\n", + " C -0.04844 0.65398 -0.00000;\n", + " H 0.85330 -1.05128 -0.00000;\n", + " H -1.08779 0.98076 -0.00000;\n", + " H 0.44171 1.06337 0.88811;\n", + " H 0.44171 1.06337 -0.88811\n", + " \"\"\", # Must be specified\n", + " \"basis\": \"cc-pvdz\", # default is \"sto-3g\"\n", + " \"spin\": 0, # default is 0\n", + " \"charge\": 0, # default is 0\n", + " \"verbosity\": 0, # default is 0\n", + " \"number_of_active_orb\": 12, # Must be specified\n", + " \"number_of_active_alpha_elec\": 7, # Must be specified\n", + " \"number_of_active_beta_elec\": 7, # Must be specified\n", + " \"avas_selection\": [\n", + " \"%d O %s\" % (k, x) for k in [0] for x in [\"2s\", \"2px\", \"2py\", \"2pz\"]\n", + " ]\n", + " + [\"%d C %s\" % (k, x) for k in [1] for x in [\"2s\", \"2px\", \"2py\", \"2pz\"]]\n", + " + [\"%d H 1s\" % k for k in [2, 3, 4, 5]], # default is None\n", + "}\n", + "\n", + "solvent_options = {\n", + " \"method\": \"IEF-PCM\", # other available methods are COSMO, C-PCM, SS(V)PE, see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", + " \"eps\": 78.3553, # value for water\n", + "}\n", + "\n", + "lucj_options = {\n", + " \"initial_layout\": [\n", + " 0,\n", + " 14,\n", + " 18,\n", + " 19,\n", + " 20,\n", + " 33,\n", + " 39,\n", + " 40,\n", + " 41,\n", + " 53,\n", + " 60,\n", + " 61,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 15,\n", + " 22,\n", + " 23,\n", + " 24,\n", + " 34,\n", + " 43,\n", + " 44,\n", + " 45,\n", + " 54,\n", + " ],\n", + " \"dynamical_decoupling_choice\": True,\n", + " \"twirling_choice\": True,\n", + " \"number_of_shots\": 200000,\n", + " \"optimization_level\": 2,\n", + "}\n", + "\n", + "sqd_options = {\n", + " \"sqd_iterations\": 3,\n", + " \"number_of_batches\": 10,\n", + " \"samples_per_batch\": 1000,\n", + " \"max_davidson_cycles\": 200,\n", + "}\n", + "\n", + "backend_name = \"ibm_sherbrooke\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "01c0667c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "39f8fb70-79b2-43ca-b723-84e6b6135821\n" + ] + } + ], + "source": [ + "job = template.run(\n", + " backend_name=backend_name,\n", + " molecule=molecule,\n", + " solvent_options=solvent_options,\n", + " lucj_options=lucj_options,\n", + " sqd_options=sqd_options,\n", + ")\n", + "print(job.job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "a9101a94", + "metadata": {}, + "source": [ + "Check the detailed status of the job:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "4385a34f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time = 2.35, status = DONE\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "t0 = time.time()\n", + "status = job.status()\n", + "if status == \"QUEUED\":\n", + " print(f\"time = {time.time()-t0:.2f}, status = QUEUED\")\n", + "while True:\n", + " status = job.status()\n", + " if status == \"QUEUED\":\n", + " continue\n", + " print(f\"time = {time.time()-t0:.2f}, status = {status}\")\n", + " if status == \"DONE\" or status == \"ERROR\":\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "4adc5293", + "metadata": {}, + "source": [ + "While the job is running, you can fetch logs created from the `logger.info` outputs. These can provide actionable information about the progress of the SQD IEF-PCM workflow. For example, the same spin orbital connections, or the two-qubit depth of the final ISA circuit intended for execution on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b1f190", + "metadata": {}, + "outputs": [], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "ba179114", + "metadata": {}, + "source": [ + "Calling for the job result blocks the rest of the program until a result is available. After the job is done, you can retrieve the results. These include the solvation free energy, as well as information about the lowest energy batch, lowest energy value, and other useful information such as the total solver duration." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3500adce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'total_energy_hist': array([[-115.14768518, -115.1368396 , -114.19181692, -115.13745429,\n", + " -115.1445012 , -114.19673326, -115.1547003 , -114.20563866,\n", + " -115.13748344, -115.14764974],\n", + " [-115.15768392, -115.15850126, -115.15857275, -115.15770916,\n", + " -115.15801684, -115.15822125, -115.15833521, -115.15844051,\n", + " -115.15735538, -115.15862354],\n", + " [-115.15795148, -115.15847925, -115.15856677, -115.15811156,\n", + " -115.15815602, -115.15785171, -115.1583672 , -115.1585533 ,\n", + " -115.15833528, -115.15808791]]),\n", + " 'spin_squared_value_hist': array([[5.37327508e-03, 1.32981759e-02, 1.36214922e-02, 8.84413615e-03,\n", + " 7.26723578e-03, 1.94875195e-02, 3.03153152e-03, 6.07543106e-03,\n", + " 1.04951849e-02, 5.36529204e-03],\n", + " [6.39397528e-04, 1.36814350e-04, 9.09054260e-05, 5.99361358e-04,\n", + " 3.64261739e-04, 2.54905866e-04, 2.32540370e-04, 1.53181990e-04,\n", + " 7.23519739e-04, 6.80737671e-05],\n", + " [4.53776416e-04, 1.63043449e-04, 1.05317263e-04, 3.82912836e-04,\n", + " 3.41047803e-04, 5.18620393e-04, 2.06819142e-04, 1.17086537e-04,\n", + " 2.32357159e-04, 4.26071537e-04]]),\n", + " 'solvation_free_energy_hist': array([[-0.00725018, -0.00743955, -0.01132905, -0.0073377 , -0.00722221,\n", + " -0.01136705, -0.00719279, -0.01072829, -0.00733404, -0.00725961],\n", + " [-0.00719252, -0.00718315, -0.00718074, -0.00719325, -0.00717703,\n", + " -0.00718391, -0.00718354, -0.00717928, -0.00719887, -0.0071801 ],\n", + " [-0.00719351, -0.00718255, -0.00718198, -0.00718429, -0.00718349,\n", + " -0.00718329, -0.0071882 , -0.00718363, -0.00718549, -0.00718814]]),\n", + " 'occupancy_hist': [[array([0.99712298, 0.99278936, 0.99083163, 0.97328469, 0.98959809,\n", + " 0.98922134, 0.720333 , 0.25683194, 0.01939338, 0.02840332,\n", + " 0.00946988, 0.0327204 ]),\n", + " array([0.99712298, 0.99278936, 0.99083163, 0.97328469, 0.98959809,\n", + " 0.98922134, 0.720333 , 0.25683194, 0.01939338, 0.02840332,\n", + " 0.00946988, 0.0327204 ])],\n", + " [array([0.9959042 , 0.9922607 , 0.99018862, 0.99265843, 0.98927447,\n", + " 0.9900833 , 0.99403876, 0.00989025, 0.01120814, 0.01137717,\n", + " 0.01152871, 0.01158725]),\n", + " array([0.9959042 , 0.9922607 , 0.99018862, 0.99265843, 0.98927447,\n", + " 0.9900833 , 0.99403876, 0.00989025, 0.01120814, 0.01137717,\n", + " 0.01152871, 0.01158725])],\n", + " [array([0.99590079, 0.99222193, 0.99016753, 0.99265045, 0.98927264,\n", + " 0.99007179, 0.99407207, 0.00986684, 0.01125181, 0.01141439,\n", + " 0.01150733, 0.01160243]),\n", + " array([0.99590079, 0.99222193, 0.99016753, 0.99265045, 0.98927264,\n", + " 0.99007179, 0.99407207, 0.00986684, 0.01125181, 0.01141439,\n", + " 0.01150733, 0.01160243])]],\n", + " 'lowest_energy_batch': 2,\n", + " 'lowest_energy_value': -115.1585667736213,\n", + " 'solvation_free_energy': -0.007181981952470838,\n", + " 'sci_solver_total_duration': 493.997501373291,\n", + " 'metadata': {'resources_usage': {'RUNNING: MAPPING': {'CPU_TIME': 6.080063343048096},\n", + " 'RUNNING: OPTIMIZING_FOR_HARDWARE': {'CPU_TIME': 1.999896764755249},\n", + " 'RUNNING: WAITING_FOR_QPU': {'CPU_TIME': 6.2850868701934814},\n", + " 'RUNNING: EXECUTING_QPU': {'QPU_TIME': 21.639373540878296},\n", + " 'RUNNING: POST_PROCESSING': {'CPU_TIME': 495.40831995010376}},\n", + " 'num_iterations_executed': 3}}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = job.result()\n", + "\n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "94a2c921", + "metadata": {}, + "source": [ + "Note that the result metadata includes a resource usage summary that lets you better estimate the QPU and CPU time required for each workload (this example ran on a dummy device, so actual resource usage times might differ)." + ] + }, + { + "cell_type": "markdown", + "id": "49d0b26d", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "After the job completes, the entire logging output will be available." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ddcba564", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-06-27 08:42:41,358\tINFO job_manager.py:531 -- Runtime env is setting up.\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:45,015: Starting runtime service\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:45,621: Backend: ibm_sherbrooke\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:46,809: Initializing molecule object\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,599: Performing CCSD\n", + "Parsing /tmp/ray/session_2025-06-27_08-42-13_898146_1/runtime_resources/working_dir_files/_ray_pkg_4bc93dcc58c04b91/output_sqd_pcm/2025-06-27_08-42-45.fcidump.txt\n", + "Overwritten attributes get_ovlp get_hcore of \n", + "/usr/local/lib/python3.11/site-packages/pyscf/gto/mole.py:1293: UserWarning: Function mol.dumps drops attribute energy_nuc because it is not JSON-serializable\n", + " warnings.warn(msg)\n", + "/usr/local/lib/python3.11/site-packages/pyscf/gto/mole.py:1293: UserWarning: Function mol.dumps drops attribute intor_symmetric because it is not JSON-serializable\n", + " warnings.warn(msg)\n", + "converged SCF energy = -115.049680672847\n", + "E(CCSD) = -115.1519910037652 E_corr = -0.1023103309180226\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,694: Same spin orbital connections: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11)]\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:51,694: Opposite spin orbital connections: [(0, 0), (4, 4), (8, 8)]\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,718: Optimization level: 2, ops: OrderedDict([('rz', 2438), ('sx', 1496), ('ecr', 766), ('x', 185), ('measure', 24), ('barrier', 1)]), depth: 391\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,736: Two-qubit gate depth: 94\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:53,737: Submitting sampler job\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:54,273: Job ID: d1f5j3lqbivc73ebqpj0\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:42:54,313: Job Status: QUEUED\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,813: Starting configuration recovery iteration 0\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,841: Batch 0 subspace dimension: 531441\n", + "2025-06-27 08:43:24,844\tINFO worker.py:1588 -- Using address 172.17.16.124:6379 set in the environment variable RAY_ADDRESS\n", + "2025-06-27 08:43:24,847\tINFO worker.py:1723 -- Connecting to existing Ray cluster at address: 172.17.16.124:6379...\n", + "2025-06-27 08:43:24,876\tINFO worker.py:1908 -- Connected to Ray cluster. View the dashboard at \u001b[1m\u001b[32mhttp://172.17.16.124:8265 \u001b[39m\u001b[22m\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,945: Batch 1 subspace dimension: 519841\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,950: Batch 2 subspace dimension: 543169\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,955: Batch 3 subspace dimension: 532900\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,960: Batch 4 subspace dimension: 534361\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,964: Batch 5 subspace dimension: 531441\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,969: Batch 6 subspace dimension: 540225\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,974: Batch 7 subspace dimension: 524176\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,979: Batch 8 subspace dimension: 537289\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:43:24,983: Batch 9 subspace dimension: 540225\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,006: Lowest energy batch: 6\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Lowest energy value: -115.15470029849135\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Corresponding g_solv value: -0.0071927910374866375\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: -----------------------------------\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:09,007: Starting configuration recovery iteration 1\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,564: Batch 0 subspace dimension: 413449\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,572: Batch 1 subspace dimension: 399424\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,578: Batch 2 subspace dimension: 438244\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,583: Batch 3 subspace dimension: 422500\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,589: Batch 4 subspace dimension: 409600\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,596: Batch 5 subspace dimension: 404496\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,601: Batch 6 subspace dimension: 410881\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,605: Batch 7 subspace dimension: 442225\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,611: Batch 8 subspace dimension: 409600\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:48:40,618: Batch 9 subspace dimension: 405769\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Lowest energy batch: 9\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Lowest energy value: -115.15862353596414\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,917: Corresponding g_solv value: -0.0071800982859467006\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,918: -----------------------------------\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:49:54,918: Starting configuration recovery iteration 2\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,501: Batch 0 subspace dimension: 399424\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,508: Batch 1 subspace dimension: 412164\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,514: Batch 2 subspace dimension: 432964\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,519: Batch 3 subspace dimension: 400689\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,524: Batch 4 subspace dimension: 432964\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,529: Batch 5 subspace dimension: 418609\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,533: Batch 6 subspace dimension: 418609\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,538: Batch 7 subspace dimension: 425104\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,543: Batch 8 subspace dimension: 404496\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:50:25,548: Batch 9 subspace dimension: 429025\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,900: Lowest energy batch: 2\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,900: Lowest energy value: -115.1585667736213\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: Corresponding g_solv value: -0.007181981952470838\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: -----------------------------------\n", + "sqd_pcm_entrypoint.run_function:INFO:2025-06-27 08:51:37,901: SCI_solver totally takes: 493.997501373291 seconds\n", + "\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "d7cc1cb2", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- Review the guide on building a function template for [Hamiltonian simulation](/docs/guides/function-template-hamiltonian-simulation)\n", + "- Check out the source files for this template on [GitHub](https://github.com/qiskit-community/qiskit-function-templates/tree/main/chemistry/sqd_pcm)\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "aabba015", + "metadata": {}, + "source": [ + "#### References\n", + "\n", + "[1] Danil Kaliakin, Akhil Shajan, Fangchun Liang, and Kenneth M. Merz Jr. [Implicit Solvent Sample-Based Quantum Diagonalization](https://pubs.acs.org/doi/10.1021/acs.jpcb.5c01030), The Journal of Physical Chemistry B, 2025, DOI: 10.1021/acs.jpcb.5c01030\n", + "\n", + "[2] Javier Robledo-Moreno, et al., [Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer](https://arxiv.org/abs/2405.05068), arXiv:2405.05068 [quant-ph].\n", + "\n", + "[3] Jeffery Yu, et al., [Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization](https://arxiv.org/abs/2501.09702), arXiv:2501.09702 [quant-ph].\n", + "\n", + "[4] Keita Kanno, et al., [Quantum-Selected Configuration Interaction: classical diagonalization of Hamiltonians in subspaces selected by quantum computers](https://arxiv.org/abs/2302.11320), arXiv:2302.11320 [quant-ph].\n", + "\n", + "[5] Kenji Sugisaki, et al., [Hamiltonian simulation-based quantum-selected configuration interaction for large-scale electronic structure calculations with a quantum computer](https://arxiv.org/abs/2412.07218), arXiv:2412.07218 [quant-ph].\n", + "\n", + "[6] Mathias Mikkelsen, Yuya O. Nakagawa, [Quantum-selected configuration interaction with time-evolved state](https://arxiv.org/abs/2412.13839), arXiv:2412.13839 [quant-ph].\n", + "\n", + "[7] Herbert, John M. [Dielectric continuum methods for quantum chemistry. WIREs Computational Molecular Science](https://wires.onlinelibrary.wiley.com/doi/10.1002/wcms.1519), 2021, 11, 1759-0876.\n", + "\n", + "[8] Saki, A. A.; Barison, S.; Fuller, B.; Garrison, J. R.; Glick, J. R.; Johnson, C.; Mezzacapo, A.; Robledo-Moreno, J.; Rossmannek, M.; Schweigert, P. et al. Qiskit addon: sample-based quantum diagonalization, 2024; https://github.com/Qiskit/qiskit-addon-sqd\n", + "\n", + "[9] Asun, Q.; Zhang, X.; Banerjee, S.; Bao, P.; Barbry, M.; Blunt, N. S.; Bogdanov, N. A.; Booth, G. H.; Chen, J.; Cui, Z.-H. PySCF: Python-based Simulations of Chemistry Framework, 2025; https://github.com/pyscf/pyscf\n", + "\n", + "[10] Kevin J. Sung; et al., FFSIM: Faster simulations of fermionic quantum circuits, 2024. https://github.com/qiskit-community/ffsim" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c99b106c", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "%%writefile ./source_files/__init__.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b182c2e4", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "%%writefile ./source_files/solve_solvent.py\n", + "\n", + "# This code is part of a Qiskit project.\n", + "#\n", + "# (C) Copyright IBM and Cleveland Clinic 2025\n", + "#\n", + "# This code is licensed under the Apache License, Version 2.0. You may\n", + "# obtain a copy of this license in the LICENSE.txt file in the root directory\n", + "# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.\n", + "#\n", + "# Any modifications or derivative works of this code must retain this\n", + "# copyright notice, and modified files need to carry a notice indicating\n", + "# that they have been altered from the originals.\n", + "\n", + "\"\"\"Functions for the study of fermionic systems.\"\"\"\n", + "\n", + "from __future__ import annotations\n", + "\n", + "import warnings\n", + "\n", + "import numpy as np\n", + "\n", + "# DSK Add imports needed for CASCI wrapper\n", + "from pyscf import ao2mo, scf, fci\n", + "from pyscf.mcscf import avas, casci\n", + "from pyscf.solvent import pcm\n", + "from pyscf.lib import chkfile, logger\n", + "\n", + "from qiskit_addon_sqd.fermion import (\n", + " SCIState,\n", + " bitstring_matrix_to_ci_strs,\n", + " _check_ci_strs,\n", + ")\n", + "\n", + "# DSK Below is the modified CASCI kernel compatible with SQD.\n", + "# It utilizes the \"fci.selected_ci.kernel_fixed_space\"\n", + "# as well as enables passing the \"batch\" and \"max_davidson\"\n", + "# input arguments from \"solve_solvent\".\n", + "# The \"batch\" contains the CI addresses corresponding to subspaces\n", + "# derived from LUCJ and S-CORE calculations.\n", + "# The \"max_davidson\" controls the maximum number of cycles of Davidson's algorithm.\n", + "\n", + "\n", + "# pylint: disable = unused-argument\n", + "def kernel(casci_object, mo_coeff=None, ci0=None, verbose=logger.NOTE, envs=None):\n", + " \"\"\"CASCI solver compatible with SQD.\n", + "\n", + " Args:\n", + " casci_object: CASCI or CASSCF object.\n", + " In case of SQD, only CASCI instance is currently incorporated.\n", + "\n", + " mo_coeff : ndarray\n", + " orbitals to construct active space Hamiltonian.\n", + " In context of SQD, these are either AVAS mo_coeff\n", + " or all of the MOs (with option to exclude core MOs).\n", + "\n", + " ci0 : ndarray or custom types FCI solver initial guess.\n", + " For SQD the usage of ci0 was not tested.\n", + "\n", + " For external FCI-like solvers, it can be\n", + " overloaded different data type. For example, in the state-average\n", + " FCI solver, ci0 is a list of ndarray. In other solvers such as\n", + " DMRGCI solver, SHCI solver, ci0 are custom types.\n", + "\n", + " kwargs:\n", + " envs: dict\n", + " In case of SQD this option was not explored,\n", + " but in principle this can facilitate the incorporation of the external solvers.\n", + "\n", + " The variable envs is created (for PR 807) to passes MCSCF runtime\n", + " environment variables to SHCI solver. For solvers which do not\n", + " need this parameter, a kwargs should be created in kernel method\n", + " and \"envs\" pop in kernel function.\n", + " \"\"\"\n", + " if mo_coeff is None:\n", + " mo_coeff = casci_object.mo_coeff\n", + " if ci0 is None:\n", + " ci0 = casci_object.ci\n", + "\n", + " log = logger.new_logger(casci_object, verbose)\n", + " t0 = (logger.process_clock(), logger.perf_counter())\n", + " log.debug(\"Start CASCI\")\n", + "\n", + " ncas = casci_object.ncas\n", + " nelecas = casci_object.nelecas\n", + "\n", + " # The start of SQD version of kernel\n", + " # DSK add the read of configurations for batch\n", + " ci_strs_sqd = casci_object.batch\n", + "\n", + " # DSK add the input for the maximum number of cycles of Davidson's algorithm\n", + " max_davidson = casci_object.max_davidson\n", + "\n", + " # DSK add electron up and down count and norb = ncas\n", + " n_up = nelecas[0]\n", + " n_dn = nelecas[1]\n", + " norb = ncas\n", + "\n", + " # DSK Eigenstate solver info\n", + " sqd_verbose = verbose\n", + "\n", + " # DSK ERI read\n", + " eri_cas = ao2mo.restore(1, casci_object.get_h2eff(), casci_object.ncas)\n", + " t1 = log.timer(\"integral transformation to CAS space\", *t0)\n", + "\n", + " # DSK 1e integrals\n", + " h1eff, energy_core = casci_object.get_h1eff()\n", + " log.debug(\"core energy = %.15g\", energy_core)\n", + " t1 = log.timer(\"effective h1e in CAS space\", *t1)\n", + "\n", + " if h1eff.shape[0] != ncas:\n", + " raise RuntimeError(\n", + " \"Active space size error. nmo=%d ncore=%d ncas=%d\" # pylint: disable=consider-using-f-string\n", + " % (mo_coeff.shape[1], casci_object.ncore, ncas)\n", + " )\n", + "\n", + " # DSK fcisolver needs to be defined in accordance with SQD\n", + " # in this software stack it is done in the \"solve_solvent\" portion of the code.\n", + " myci = casci_object.fcisolver\n", + " e_cas, sqdvec = fci.selected_ci.kernel_fixed_space(\n", + " myci,\n", + " h1eff,\n", + " eri_cas,\n", + " norb,\n", + " (n_up, n_dn),\n", + " ci_strs=ci_strs_sqd,\n", + " verbose=sqd_verbose,\n", + " max_cycle=max_davidson,\n", + " )\n", + "\n", + " # DSK fcivec is the general name for CI vector assigned by PySCF.\n", + " # Depending on type of solver it is either FCI or SCI vector.\n", + " # In case of sqd we can call it \"sqdvec\" for clarity.\n", + " # Nonetheless, for further processing PySCF expects\n", + " # this data structure to be called fcivec, regardless of the used solver.\n", + "\n", + " fcivec = sqdvec\n", + "\n", + " t1 = log.timer(\"CI solver\", *t1)\n", + " e_tot = energy_core + e_cas\n", + "\n", + " # Returns either standard CASCI data or SQD data. Return depends on \"sqd_run\" True/False.\n", + " return e_tot, e_cas, fcivec\n", + "\n", + "\n", + "# Replace standard CASCI kernel with the SQD-compatible CASCI kernel defined above\n", + "casci.kernel = kernel\n", + "\n", + "\n", + "def solve_solvent(\n", + " bitstring_matrix: tuple[np.ndarray, np.ndarray] | np.ndarray,\n", + " /,\n", + " myeps: float,\n", + " mysolvmethod: str,\n", + " myavas: list,\n", + " num_orbitals: int,\n", + " *,\n", + " spin_sq: int | None = None,\n", + " max_davidson: int = 100,\n", + " verbose: int | None = 0,\n", + " checkpoint_file: str,\n", + ") -> tuple[float, SCIState, list[np.ndarray], float]:\n", + " \"\"\"Approximate the ground state given molecular integrals and a set of electronic configurations.\n", + "\n", + " Args:\n", + " bitstring_matrix: A set of configurations defining the subspace onto which the Hamiltonian\n", + " will be projected and diagonalized. This is a 2D array of ``bool`` representations of bit\n", + " values such that each row represents a single bitstring. The spin-up configurations\n", + " should be specified by column indices in range ``(N, N/2]``, and the spin-down\n", + " configurations should be specified by column indices in range ``(N/2, 0]``, where ``N``\n", + " is the number of qubits.\n", + "\n", + " (DEPRECATED) The configurations may also be specified by a length-2 tuple of sorted 1D\n", + " arrays containing unsigned integer representations of the determinants. The two lists\n", + " should represent the spin-up and spin-down orbitals, respectively.\n", + "\n", + " To build PCM model PySCF needs the structure of the molecule. Hence, the electron integrals\n", + " (hcore and eri) are not enough to form IEF-PCM simulation. Instead the \"start.chk\" file is used.\n", + " This workflow also requires additional information about solute and solvent,\n", + " which is reflected by additional arguments below\n", + "\n", + " myeps: Dielectric parameter of the solvent.\n", + " mysolvmethod: Solvent model, which can be IEF-PCM, COSMO, C-PCM, SS(V)PE,\n", + " see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", + " At the moment only IEF-PCM was tested.\n", + " In principle two other models from PySCF \"solvent\" module can be used as well,\n", + " namely SMD and polarizable embedding (PE).\n", + " The SMD and PE were not tested yet and their usage requires addition of more\n", + " input arguments for \"solve_solvent\".\n", + " myavas: This argument allows user to select active space in solute with AVAS.\n", + " The corresponding list should include target atomic orbitals.\n", + " If myavas=None, then active space selected based on number of orbitals\n", + " derived from ci_strs.\n", + " It is assumed that if myavas=None, then the target calculation is either\n", + " a) corresponds to full basis case.\n", + " b) close to full basis case and only few core orbitals are excluded.\n", + " num_orbitals: Number of orbitals, which is essential when myavas = None.\n", + " In AVAS case number of orbitals and electrons is derived by AVAS procedure itself.\n", + " spin_sq: Target value for the total spin squared for the ground state.\n", + " If ``None``, no spin will be imposed.\n", + " max_davidson: The maximum number of cycles of Davidson's algorithm\n", + " verbose: A verbosity level between 0 and 10\n", + " checkpoint_file: Name of the checkpoint file\n", + "\n", + " NOTE: For now open shell functionality is not supported in SQD PCM calculations.\n", + " Hence, at the moment solve_solvent does not include open_shell as one of the arguments.\n", + "\n", + " Returns:\n", + " - Minimum energy from SCI calculation\n", + " - The SCI ground state\n", + " - Average occupancy of the alpha and beta orbitals, respectively\n", + " - Expectation value of spin-squared\n", + " - Solvation free energy\n", + "\n", + " \"\"\"\n", + " # Unlike the \"solve_fermion\", the \"solve_solvent\" utilizes the \"checkpoint\" file to\n", + " # get the starting HF information, which means that \"solve_solvent\" does not accept\n", + " # \"hcore\" and \"eri\" as the input arguments.\n", + " # Instead \"hcore\" and \"eri\" are generated inside of the custom SQD-compatible\n", + " # CASCI kernel (defined above).\n", + " # The generation of \"hcore\" and \"eri\" is based on the information from \"checkpoint\" file\n", + " # as well as \"myavas\" and \"num_orbitals\" input arguments.\n", + "\n", + " # DSK this part handles addresses and is identical to \"solve_fermion\"\n", + " if isinstance(bitstring_matrix, tuple):\n", + " warnings.warn(\n", + " \"Passing the input determinants as integers is deprecated. \"\n", + " \"Users should instead pass a bitstring matrix defining the subspace.\",\n", + " DeprecationWarning,\n", + " stacklevel=2,\n", + " )\n", + " ci_strs = bitstring_matrix\n", + " else:\n", + " # This will become the default code path after the deprecation period.\n", + " ci_strs = bitstring_matrix_to_ci_strs(bitstring_matrix, open_shell=False)\n", + " ci_strs = _check_ci_strs(ci_strs)\n", + "\n", + " num_up = format(ci_strs[0][0], \"b\").count(\"1\")\n", + " num_dn = format(ci_strs[1][0], \"b\").count(\"1\")\n", + "\n", + " # DSK assign verbosity\n", + " verbose_ci = verbose\n", + "\n", + " # DSK add information about solute and solvent.\n", + " # Since PCM model needs the information about the structure of the molecule\n", + " # one cannot use only FCIDUMP. Instead converged HF data can be passed from \"checkpoint\" file\n", + " # along with \"mol\" object containing the geometry and other information about the solute.\n", + "\n", + " ############################################\n", + " # This section is specific to \"solve_solvent\" and is not present in \"solve_fermion\".\n", + " # In case of \"solve_fermion\" the \"eri\" and \"hcore\" are passed directly to\n", + " # \"fci.selected_ci.kernel_fixed_space\".\n", + " # In case of \"solve_solvent\" the incorporation of the polarizable continuum model\n", + " # requires utilization of \"CASCI.with_solvent\"\n", + " # data object from PySCF, where underlying CASCI.base_kernel has to be replaced\n", + " # with SQD-compatible version.\n", + " # Due to these differences in the implementation the \"solve_solvent\" recovers\n", + " # the converged mean field results and \"molecule\" object from \"checkpoint\" file\n", + " # (instead of using FCIDUMP),\n", + " # followed by passing of solute, solvent, and active space information to \"CASCI.with_solvent\".\n", + " # This includes the initiation of \"mol\", \"cm\", \"mf\", and \"mc\" data structures.\n", + "\n", + " mol = chkfile.load_mol(checkpoint_file)\n", + "\n", + " # DSK Initiation of the solvent model\n", + " cm = pcm.PCM(mol)\n", + " cm.eps = myeps # solute eps value\n", + " cm.method = mysolvmethod # IEF-PCM, COSMO, C-PCM, SS(V)PE,\n", + " # see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", + "\n", + " # DSK Read-in converged RHF solution\n", + " scf_result_dic = chkfile.load(checkpoint_file, \"scf\")\n", + " mf = scf.RHF(mol).PCM(cm)\n", + " mf.__dict__.update(scf_result_dic)\n", + "\n", + " # Identify the active space based on the user input of AVAS or number of orbitals and electrons\n", + " if myavas is not None:\n", + " orbs = myavas\n", + " avas_obj = avas.AVAS(mf, orbs, with_iao=True)\n", + " avas_obj.kernel()\n", + " ncas, nelecas, _, _, _ = (\n", + " avas_obj.ncas,\n", + " avas_obj.nelecas,\n", + " avas_obj.mo_coeff,\n", + " avas_obj.occ_weights,\n", + " avas_obj.vir_weights,\n", + " )\n", + " else:\n", + " ncas = num_orbitals\n", + " nelecas = (num_up, num_dn)\n", + "\n", + " # Initiate the \"CASCI.with_solvent\" object\n", + " mc = casci.CASCI(mf, ncas=ncas, nelecas=nelecas).PCM(cm)\n", + " # Replace mo_coeff with ones produced by AVAS if AVAS is utilized\n", + " if myavas is not None:\n", + " mc.mo_coeff = avas_obj.mo_coeff\n", + " # Read-in the configuration interaction subspace derived from LUCJ and S-CORE\n", + " mc.batch = ci_strs\n", + " # Assign number of maximum Davidson steps\n", + " mc.max_davidson = max_davidson\n", + "\n", + " ####### The definition of \"fcisolver\" object is identical to \"solve_fermion\" case ########\n", + " myci = fci.selected_ci.SelectedCI()\n", + " if spin_sq is not None:\n", + " myci = fci.addons.fix_spin_(myci, ss=spin_sq)\n", + " mc.fcisolver = myci\n", + " mc.verbose = verbose_ci\n", + " #########################################################################################\n", + "\n", + " # Initiate the \"CASCI.with_solvent\" simulation with SQD-compatible based CASCI kernel.\n", + " mc_result = mc.kernel()\n", + "\n", + " # Get data out of the \"CASCI.with_solvent\" object\n", + " e_sci = mc_result[0]\n", + " sci_vec = mc_result[2]\n", + " # Here we get additional output comparing to \"solve_fermion\",\n", + " # which is the solvation free energy (G_solv)\n", + " g_solv = mc.with_solvent.e\n", + "\n", + " #####################################################\n", + " # The remainder of the code in solve_solvent is nearly identical to solve_fermion code.\n", + "\n", + " # However, there are two exceptions in \"solve_solvent\":\n", + "\n", + " # 1) The dm2 is currently not computed, but can be included if needed\n", + " # 2) e_sci is directly output as the result of CASCI.with_solvent object.\n", + "\n", + " # Hence, the two following lines of code are not present in \"solve_solvent\"\n", + " # comparing to the \"solve_fermion\" code:\n", + "\n", + " # dm2 = myci.make_rdm2(sci_vec, norb, (num_up, num_dn))\n", + " # e_sci = np.einsum(\"pr,pr->\", dm1, hcore) + 0.5 * np.einsum(\"prqs,prqs->\", dm2, eri)\n", + "\n", + " # Calculate the avg occupancy of each orbital\n", + " dm1 = myci.make_rdm1s(sci_vec, ncas, (num_up, num_dn))\n", + " avg_occupancy = [np.diagonal(dm1[0]), np.diagonal(dm1[1])]\n", + "\n", + " # Compute total spin\n", + " spin_squared = myci.spin_square(sci_vec, ncas, (num_up, num_dn))[0]\n", + "\n", + " # Convert the PySCF SCIVector to internal format. We access a private field here,\n", + " # so we assert that we expect the SCIVector output from kernel_fixed_space to\n", + " # have its _strs field populated with alpha and beta strings.\n", + " assert isinstance(sci_vec._strs[0], np.ndarray) and isinstance(sci_vec._strs[1], np.ndarray)\n", + " assert sci_vec.shape == (len(sci_vec._strs[0]), len(sci_vec._strs[1]))\n", + " sci_state = SCIState(\n", + " amplitudes=np.array(sci_vec),\n", + " ci_strs_a=sci_vec._strs[0],\n", + " ci_strs_b=sci_vec._strs[1],\n", + " )\n", + "\n", + " return e_sci, sci_state, avg_occupancy, spin_squared, g_solv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b478edb", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "%%writefile ./source_files/sqc_pcm_entrypoint.py\n", + "\n", + "# This code is part of a Qiskit project.\n", + "#\n", + "# (C) Copyright IBM and Cleveland Clinic 2025\n", + "#\n", + "# This code is licensed under the Apache License, Version 2.0. You may\n", + "# obtain a copy of this license in the LICENSE.txt file in the root directory\n", + "# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.\n", + "#\n", + "# Any modifications or derivative works of this code must retain this\n", + "# copyright notice, and modified files need to carry a notice indicating\n", + "# that they have been altered from the originals.\n", + "\n", + "\"\"\"\n", + "SQD-PCM Function Template source code.\n", + "\"\"\"\n", + "from pathlib import Path\n", + "from typing import Any\n", + "from datetime import datetime\n", + "import os\n", + "import sys\n", + "import json\n", + "import logging\n", + "import time\n", + "import traceback\n", + "import numpy as np\n", + "\n", + "import ffsim\n", + "\n", + "from pyscf import gto, scf, mcscf, ao2mo, tools, cc\n", + "from pyscf.lib import chkfile\n", + "from pyscf.mcscf import avas\n", + "from pyscf.solvent import pcm\n", + "\n", + "from qiskit import QuantumCircuit, QuantumRegister\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.primitives import BackendSamplerV2\n", + "\n", + "from qiskit_addon_sqd.counts import counts_to_arrays\n", + "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n", + "from qiskit_addon_sqd.fermion import bitstring_matrix_to_ci_strs\n", + "from qiskit_addon_sqd.subsampling import postselect_and_subsample\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2\n", + "from qiskit_serverless import get_arguments, save_result, distribute_task, get, update_status, Job\n", + "\n", + "current_dir = os.path.dirname(os.path.abspath(__file__))\n", + "sys.path.insert(0, current_dir)\n", + "from solve_solvent import solve_solvent # pylint: disable=wrong-import-position\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "\n", + "\n", + "def run_function(\n", + " backend_name: str,\n", + " molecule: dict,\n", + " solvent_options: dict,\n", + " sqd_options: dict,\n", + " lucj_options: dict | None = None,\n", + " **kwargs,\n", + ") -> dict[str, Any]:\n", + " \"\"\"\n", + " Main entry point for the SQD-PCM (Polarizable Continuum Model) workflow.\n", + "\n", + " This function encapsulates the end-to-end execution of the algorithm.\n", + "\n", + " Args:\n", + " backend_name: Identifier for the target backend, required for all\n", + " workflows that access IBM Quantum hardware.\n", + "\n", + " molecule: dictionary with molecule information:\n", + " - \"atom\" (str): required field, follows pyscf specification for atomic geometry.\n", + " For example, for methanol the value would be::\n", + "\n", + " '''\n", + " O -0.04559 -0.75076 -0.00000;\n", + " C -0.04844 0.65398 -0.00000;\n", + " H 0.85330 -1.05128 -0.00000;\n", + " H -1.08779 0.98076 -0.00000;\n", + " H 0.44171 1.06337 0.88811;\n", + " H 0.44171 1.06337 -0.88811;\n", + " '''\n", + "\n", + " - \"number_of_active_orb\" (int): required field\n", + " - \"number_of_active_alpha_elec\" (int): required field\n", + " - \"number_of_active_beta_elec\" (int): required field\n", + " - \"basis\" (str): optional field, default is \"sto-3g\"\n", + " - \"verbosity\" (int): optional field, default is 0\n", + " - \"charge\" (int): optional field, default is 0\n", + " - \"spin\" (int): optional field, default is 0\n", + " - \"avas_selection\" (list[str] | None): optional field, default is None\n", + "\n", + " solvent_options: dictionary with solvent options information:\n", + " - \"method\" (str): required field. Method for computing solvent reaction field\n", + " for the PCM. Accepted values are: \"IEF-PCM\", \"COSMO\",\n", + " \"C-PCM\", \"SS(V)PE\", see https://manual.q-chem.com/5.4/topic_pcm-em.html\n", + " - \"eps\" (float): required field. Dielectric constant of the solvent in the PCM.\n", + "\n", + " sqd_options: dictionary with sqd options information:\n", + " - \"sqd_iterations\" (int): required field.\n", + " - \"number_of_batches\" (int): required field.\n", + " - \"samples_per_batch\" (int): required field.\n", + " - \"max_davidson_cycles\" (int): required field.\n", + "\n", + " lucj_options: optional dictionary with lucj options information:\n", + " - \"optimization_level\" (int): optional field, default is 2\n", + " - \"initial_layout\" (list[int]): optional field, default is None\n", + " - \"dynamical_decoupling\" (bool): optional field, default is True\n", + " - \"twirling\" (bool): optional field, default is True\n", + " - \"number_of_shots\" (int): optional field, default is 10000\n", + "\n", + " **kwargs\n", + " Optional keyword arguments to customize behavior. Existing kwargs include:\n", + " - \"files_name\" (str): optional name for output files (enabled for local testing)\n", + " - \"testing_backend\" (FakeBackendV2): optional fake backend instance to bypass\n", + " qiskit runtime service instantiation (enabled for local testing)\n", + " - \"count_dict_file_name\" (str): path to a count dict file to bypass primitive\n", + " execution and jump directly to SQD section (enabled for local testing)\n", + "\n", + " Returns:\n", + " The function should return the execution results as a dictionary with string keys.\n", + " This is to ensure compatibility with ``qiskit_serverless.save_result``.\n", + " \"\"\"\n", + "\n", + " # Preparation Step: Input validation.\n", + " # Do this at the top of the function definition so it fails early if any required\n", + " # arguments are missing or invalid.\n", + "\n", + " # Molecule parsing\n", + " # Required:\n", + " geo = molecule[\"atom\"]\n", + " num_active_orb = molecule[\"number_of_active_orb\"]\n", + " num_active_alpha = molecule[\"number_of_active_alpha_elec\"]\n", + " num_active_beta = molecule[\"number_of_active_beta_elec\"]\n", + " # Optional:\n", + " input_basis = molecule.get(\"basis\", \"sto-3g\")\n", + " input_verbosity = molecule.get(\"verbosity\", 0)\n", + " input_charge = molecule.get(\"charge\", 0)\n", + " input_spin = molecule.get(\"spin\", 0)\n", + " myavas = molecule.get(\"avas_selection\", None)\n", + "\n", + " # Solvent options parsing\n", + " myeps = solvent_options[\"eps\"]\n", + " mymethod = solvent_options[\"method\"]\n", + "\n", + " # LUCJ options parsing\n", + " if lucj_options is None:\n", + " lucj_options = {}\n", + " opt_level = lucj_options.get(\"optimization_level\", 2)\n", + " initial_layout = lucj_options.get(\"initial_layout\", None)\n", + " use_dd = lucj_options.get(\"dynamical_decoupling\", True)\n", + " use_twirling = lucj_options.get(\"twirling\", True)\n", + " num_shots = lucj_options.get(\"number_of_shots\", True)\n", + "\n", + " # SQD options parsing\n", + " iterations = sqd_options[\"sqd_iterations\"]\n", + " n_batches = sqd_options[\"number_of_batches\"]\n", + " samples_per_batch = sqd_options[\"samples_per_batch\"]\n", + " max_davidson_cycles = sqd_options[\"max_davidson_cycles\"]\n", + "\n", + " # kwarg parsing (local testing)\n", + " testing_backend = kwargs.get(\"testing_backend\", None)\n", + " count_dict_file_name = kwargs.get(\"count_dict_file_name\", None)\n", + "\n", + " files_name = kwargs.get(\"files_name\", datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\"))\n", + " output_path = Path.cwd() / \"output_sqd_pcm\"\n", + " output_path.mkdir(exist_ok=True)\n", + " datafiles_name = str(output_path) + \"/\" + files_name\n", + "\n", + " # --\n", + " # Preparation Step: Qiskit Runtime & primitive configuration for\n", + " # execution on IBM Quantum hardware.\n", + "\n", + " if testing_backend is None:\n", + " # Initialize Qiskit Runtime Service\n", + " logger.info(\"Starting runtime service\")\n", + " service = QiskitRuntimeService(\n", + " channel=os.environ[\"QISKIT_IBM_CHANNEL\"],\n", + " instance=os.environ[\"IBM_CLOUD_INSTANCE\"],\n", + " token=os.environ[\"your-API_KEY\"], # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", + " )\n", + " backend = service.backend(backend_name)\n", + " logger.info(f\"Backend: {backend.name}\")\n", + "\n", + " # Set up sampler and corresponding options\n", + " sampler = SamplerV2(backend)\n", + " sampler.options.dynamical_decoupling.enable = use_dd\n", + " sampler.options.twirling.enable_measure = False\n", + " sampler.options.twirling.enable_gates = use_twirling\n", + " sampler.options.default_shots = num_shots\n", + " else:\n", + " backend = testing_backend\n", + " logger.info(f\"Testing backend: {backend.name}\")\n", + "\n", + " # Set up backend sampler.\n", + " # This doesn't allow running with twirling and dd\n", + " sampler = BackendSamplerV2(backend=testing_backend)\n", + "\n", + " # Once the preparation steps are completed, the algorithm can be structured following a\n", + " # Qiskit Pattern workflow:\n", + " # https://docs.quantum.ibm.com/guides/intro-to-patterns\n", + "\n", + " # --\n", + " # Step 1: Map\n", + " # In this step, input arguments are used to construct relevant quantum circuits and operators\n", + "\n", + " start_mapping = time.time()\n", + " update_status(Job.MAPPING)\n", + "\n", + " # Initialize the molecule object (pyscf)\n", + " logger.info(\"Initializing molecule object\")\n", + " mol = gto.Mole()\n", + " mol.build(\n", + " atom=geo,\n", + " basis=input_basis,\n", + " verbose=input_verbosity,\n", + " charge=input_charge,\n", + " spin=input_spin,\n", + " symmetry=False,\n", + " ) # Not tested for symmetry calculations\n", + "\n", + " cm = pcm.PCM(mol)\n", + " cm.eps = myeps\n", + " cm.method = mymethod\n", + "\n", + " mf = scf.RHF(mol).PCM(cm)\n", + " # Generation of checkpoint file for the solute and solvent\n", + " # which will be used reused in all subsequent sections\n", + " checkpoint_file_name = str(datafiles_name + \".chk\")\n", + " mf.chkfile = checkpoint_file_name\n", + " mf.kernel()\n", + "\n", + " # Read-in the information about the molecule\n", + " mol = chkfile.load_mol(checkpoint_file_name)\n", + "\n", + " # Read-in RHF data\n", + " scf_result_dic = chkfile.load(checkpoint_file_name, \"scf\")\n", + " mf = scf.RHF(mol)\n", + " mf.__dict__.update(scf_result_dic)\n", + "\n", + " # LUCJ uses isolated solute\n", + " mf.kernel()\n", + "\n", + " # Initialize orbital selection based on user input\n", + " if myavas is not None:\n", + " orbs = myavas\n", + " avas_out = avas.AVAS(mf, orbs, with_iao=True)\n", + " avas_out.kernel()\n", + " ncas, nelecas = (avas_out.ncas, avas_out.nelecas)\n", + " else:\n", + " ncas = num_active_orb\n", + " nelecas = (\n", + " num_active_alpha,\n", + " num_active_beta,\n", + " )\n", + "\n", + " # LUCJ Step:\n", + " # Generate active space\n", + " mc = mcscf.CASCI(mf, ncas=ncas, nelecas=nelecas)\n", + " if myavas is not None:\n", + " mc.mo_coeff = avas_out.mo_coeff\n", + " mc.batch = None\n", + " # Reliable and most convenient way to do the CCSD on only the active space\n", + " # is to create the FCIDUMP file and then run the CCSD calculation only on the\n", + " # orbitals stored in the FCIDUMP file.\n", + "\n", + " h1e_cas, ecore = mc.get_h1eff()\n", + " h2e_cas = ao2mo.restore(1, mc.get_h2eff(), mc.ncas)\n", + "\n", + " fcidump_file_name = str(datafiles_name + \".fcidump.txt\")\n", + " tools.fcidump.from_integrals(\n", + " fcidump_file_name,\n", + " h1e_cas,\n", + " h2e_cas,\n", + " ncas,\n", + " nelecas,\n", + " nuc=ecore,\n", + " ms=0,\n", + " orbsym=[1] * ncas,\n", + " )\n", + "\n", + " logger.info(\"Performing CCSD\")\n", + " # Read FCIDUMP and perform CCSD on only active space\n", + " mf_as = tools.fcidump.to_scf(fcidump_file_name)\n", + " mf_as.kernel()\n", + "\n", + " mc_cc = cc.CCSD(mf_as)\n", + " mc_cc.kernel()\n", + " mc_cc.t1 # pylint: disable=pointless-statement\n", + " t2 = mc_cc.t2\n", + "\n", + " n_reps = 2\n", + " norb = ncas\n", + "\n", + " if myavas is not None:\n", + " nelec = (int(nelecas / 2), int(nelecas / 2))\n", + " else:\n", + " nelec = nelecas\n", + "\n", + " alpha_alpha_indices = [(p, p + 1) for p in range(norb - 1)]\n", + " alpha_beta_indices = [(p, p) for p in range(0, norb, 4)]\n", + "\n", + " logger.info(f\"Same spin orbital connections: {alpha_alpha_indices}\")\n", + " logger.info(f\"Opposite spin orbital connections: {alpha_beta_indices}\")\n", + "\n", + " # Construct LUCJ op\n", + " ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n", + " t2, n_reps=n_reps, interaction_pairs=(alpha_alpha_indices, alpha_beta_indices)\n", + " )\n", + " # Construct circuit\n", + " qubits = QuantumRegister(2 * norb, name=\"q\")\n", + " circuit = QuantumCircuit(qubits)\n", + " circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)\n", + " circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n", + " circuit.measure_all()\n", + " end_mapping = time.time()\n", + "\n", + " # --\n", + " # Step 2: Optimize\n", + " # Transpile circuits to match ISA\n", + "\n", + " start_optimizing = time.time()\n", + " update_status(Job.OPTIMIZING_HARDWARE)\n", + "\n", + " pass_manager = generate_preset_pass_manager(\n", + " optimization_level=opt_level,\n", + " backend=backend,\n", + " initial_layout=initial_layout,\n", + " )\n", + "\n", + " pass_manager.pre_init = ffsim.qiskit.PRE_INIT\n", + " transpiled = pass_manager.run(circuit)\n", + "\n", + " end_optimizing = time.time()\n", + " logger.info(\n", + " f\"Optimization level: {opt_level}, ops: {transpiled.count_ops()}, depth: {transpiled.depth()}\"\n", + " )\n", + "\n", + " two_q_depth = transpiled.depth(lambda x: x.operation.num_qubits == 2)\n", + " logger.info(f\"Two-qubit gate depth: {two_q_depth}\")\n", + "\n", + " # --\n", + " # Step 3: Execute on Hardware\n", + " # Submit the underlying Sampler job. Note that this is not the\n", + " # actual function job.\n", + " if count_dict_file_name is None:\n", + " # Submit the LUCJ job\n", + " logger.info(\"Submitting sampler job\")\n", + " job = sampler.run([transpiled])\n", + " logger.info(f\"Job ID: {job.job_id()}\")\n", + " logger.info(f\"Job Status: {job.status()}\")\n", + "\n", + " start_waiting_qpu = time.time()\n", + " while job.status() == \"QUEUED\":\n", + " update_status(Job.WAITING_QPU)\n", + " time.sleep(5)\n", + "\n", + " end_waiting_qpu = time.time()\n", + " update_status(Job.EXECUTING_QPU)\n", + "\n", + " # Wait until job is complete\n", + " result = job.result()\n", + " end_executing_qpu = time.time()\n", + "\n", + " pub_result = result[0]\n", + " counts_dict = pub_result.data.meas.get_counts()\n", + "\n", + " waiting_qpu_time = end_waiting_qpu - start_waiting_qpu\n", + " executing_qpu_time = end_executing_qpu - end_waiting_qpu\n", + " else:\n", + " # read LUCJ samples from count_dict\n", + " logger.info(\"Skipping sampler, loading counts dict from file\")\n", + " with open(count_dict_file_name, \"r\") as file:\n", + " count_dict_string = file.read().replace(\"\\n\", \"\")\n", + " counts_dict = json.loads(count_dict_string.replace(\"'\", '\"'))\n", + " waiting_qpu_time = 0\n", + " executing_qpu_time = 0\n", + "\n", + " # --\n", + " # Step 4: Post-process\n", + "\n", + " start_pp = time.time()\n", + " update_status(Job.POST_PROCESSING)\n", + "\n", + " # SQD-PCM section\n", + " start = time.time()\n", + "\n", + " # Orbitals, electron, and spin initialization\n", + " num_orbitals = ncas\n", + " if myavas is not None:\n", + " num_elec_a = num_elec_b = int(nelecas / 2)\n", + " else:\n", + " num_elec_a, num_elec_b = nelecas\n", + " spin_sq = input_spin\n", + "\n", + " # Convert counts into bitstring and probability arrays\n", + " bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts_dict)\n", + "\n", + " # We set qiskit_serverless to explicitly reserve 1 cpu per thread, as\n", + " # the task is CPU-bound and might degrade in performance when sharing\n", + " # a core at scale (this might not be the case with smaller examples)\n", + " @distribute_task(target={\"cpu\": 1})\n", + " def solve_solvent_parallel(\n", + " batches,\n", + " myeps,\n", + " mysolvmethod,\n", + " myavas,\n", + " num_orbitals,\n", + " spin_sq,\n", + " max_davidson,\n", + " checkpoint_file,\n", + " ):\n", + " return solve_solvent( # sqd for pyscf\n", + " batches,\n", + " myeps,\n", + " mysolvmethod,\n", + " myavas,\n", + " num_orbitals,\n", + " spin_sq=spin_sq,\n", + " max_davidson=max_davidson,\n", + " checkpoint_file=checkpoint_file,\n", + " )\n", + "\n", + " e_hist = np.zeros((iterations, n_batches)) # energy history\n", + " s_hist = np.zeros((iterations, n_batches)) # spin history\n", + " g_solv_hist = np.zeros((iterations, n_batches)) # g_solv history\n", + " occupancy_hist = []\n", + " avg_occupancy = None\n", + "\n", + " num_ran_iter = 0\n", + " for i in range(iterations):\n", + " logger.info(f\"Starting configuration recovery iteration {i}\")\n", + " # On the first iteration, we have no orbital occupancy information from the\n", + " # solver, so we begin with the full set of noisy configurations.\n", + " if avg_occupancy is None:\n", + " bs_mat_tmp = bitstring_matrix_full\n", + " probs_arr_tmp = probs_arr_full\n", + "\n", + " # If we have average orbital occupancy information, we use it to refine the full\n", + " # set of noisy configurations\n", + " else:\n", + " bs_mat_tmp, probs_arr_tmp = recover_configurations(\n", + " bitstring_matrix_full, probs_arr_full, avg_occupancy, num_elec_a, num_elec_b\n", + " )\n", + "\n", + " # Create batches of subsamples. We postselect here to remove configurations\n", + " # with incorrect hamming weight during iteration 0, since no config recovery was performed.\n", + " batches = postselect_and_subsample(\n", + " bs_mat_tmp,\n", + " probs_arr_tmp,\n", + " hamming_right=num_elec_a,\n", + " hamming_left=num_elec_b,\n", + " samples_per_batch=samples_per_batch,\n", + " num_batches=n_batches,\n", + " )\n", + "\n", + " # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n", + " e_tmp = np.zeros(n_batches)\n", + " s_tmp = np.zeros(n_batches)\n", + " g_solvs_tmp = np.zeros(n_batches)\n", + " occs_tmp = []\n", + " coeffs = []\n", + "\n", + " res1 = []\n", + " for j in range(n_batches):\n", + " strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])\n", + " logger.info(f\"Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}\")\n", + "\n", + " res1.append(\n", + " solve_solvent_parallel(\n", + " batches[j],\n", + " myeps,\n", + " mymethod,\n", + " myavas,\n", + " num_orbitals,\n", + " spin_sq=spin_sq,\n", + " max_davidson=max_davidson_cycles,\n", + " checkpoint_file=checkpoint_file_name,\n", + " )\n", + " )\n", + "\n", + " res = get(res1)\n", + "\n", + " for j in range(n_batches):\n", + " energy_sci, coeffs_sci, avg_occs, spin, g_solv = res[j]\n", + " e_tmp[j] = energy_sci\n", + " s_tmp[j] = spin\n", + " g_solvs_tmp[j] = g_solv\n", + " occs_tmp.append(avg_occs)\n", + " coeffs.append(coeffs_sci)\n", + "\n", + " # Combine batch results\n", + " avg_occupancy = tuple(np.mean(occs_tmp, axis=0))\n", + "\n", + " # Track optimization history\n", + " e_hist[i, :] = e_tmp\n", + " s_hist[i, :] = s_tmp\n", + " g_solv_hist[i, :] = g_solvs_tmp\n", + " occupancy_hist.append(avg_occupancy)\n", + "\n", + " lowest_e_batch_index = np.argmin(e_hist[i, :])\n", + "\n", + " logger.info(f\"Lowest energy batch: {lowest_e_batch_index}\")\n", + " logger.info(f\"Lowest energy value: {np.min(e_hist[i, :])}\")\n", + " logger.info(f\"Corresponding g_solv value: {g_solv_hist[i, lowest_e_batch_index]}\")\n", + " logger.info(\"-----------------------------------\")\n", + " num_ran_iter += 1\n", + "\n", + " end_pp = time.time()\n", + " end = time.time()\n", + " duration = end - start\n", + " logger.info(f\"SCI_solver totally takes: {duration} seconds\")\n", + "\n", + " metadata = {\n", + " \"resources_usage\": {\n", + " \"RUNNING: MAPPING\": {\n", + " \"CPU_TIME\": end_mapping - start_mapping,\n", + " },\n", + " \"RUNNING: OPTIMIZING_FOR_HARDWARE\": {\n", + " \"CPU_TIME\": end_optimizing - start_optimizing,\n", + " },\n", + " \"RUNNING: WAITING_FOR_QPU\": {\n", + " \"CPU_TIME\": waiting_qpu_time,\n", + " },\n", + " \"RUNNING: EXECUTING_QPU\": {\n", + " \"QPU_TIME\": executing_qpu_time,\n", + " },\n", + " \"RUNNING: POST_PROCESSING\": {\n", + " \"CPU_TIME\": end_pp - start_pp,\n", + " },\n", + " },\n", + " \"num_iterations_executed\": num_ran_iter,\n", + " }\n", + "\n", + " output = {\n", + " \"total_energy_hist\": e_hist,\n", + " \"spin_squared_value_hist\": s_hist,\n", + " \"solvation_free_energy_hist\": g_solv_hist,\n", + " \"occupancy_hist\": occupancy_hist,\n", + " \"lowest_energy_batch\": lowest_e_batch_index,\n", + " \"lowest_energy_value\": np.min(e_hist[i, :]),\n", + " \"solvation_free_energy\": g_solv_hist[i, lowest_e_batch_index],\n", + " \"sci_solver_total_duration\": duration,\n", + " \"metadata\": metadata,\n", + " }\n", + "\n", + " return output\n", + "\n", + "\n", + "def set_up_logger(my_logger: logging.Logger, level: int = logging.INFO) -> None:\n", + " \"\"\"Logger setup to communicate logs through serverless.\"\"\"\n", + "\n", + " log_fmt = \"%(module)s.%(funcName)s:%(levelname)s:%(asctime)s: %(message)s\"\n", + " formatter = logging.Formatter(log_fmt)\n", + "\n", + " # Set propagate to `False` since handlers are to be attached.\n", + " my_logger.propagate = False\n", + "\n", + " stream_handler = logging.StreamHandler()\n", + " stream_handler.setFormatter(formatter)\n", + " my_logger.addHandler(stream_handler)\n", + " my_logger.setLevel(level)\n", + "\n", + "\n", + "# This is the section where `run_function` is called, it's boilerplate code and can be used\n", + "# without customization.\n", + "if __name__ == \"__main__\":\n", + "\n", + " # Use serverless helper function to extract input arguments,\n", + " input_args = get_arguments()\n", + "\n", + " # Allow to configure logging level\n", + " logging_level = input_args.get(\"logging_level\", logging.INFO)\n", + " set_up_logger(logger, logging_level)\n", + "\n", + " try:\n", + " func_result = run_function(**input_args)\n", + " # Use serverless function to save the results that\n", + " # will be returned in the job.\n", + " save_result(func_result)\n", + " except Exception:\n", + " save_result(traceback.format_exc())\n", + " raise\n", + "\n", + " sys.exit(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "142fee48", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users. It verifies both source listings are identical then deletes the\n", + "# working folder we created\n", + "import shutil\n", + "\n", + "with open(\"./source_files/sqd_pcm_entrypoint.py\") as f1:\n", + " with open(\"./source_files/sqd_pcm_entrypoint.py\") as f2:\n", + " assert f1.read() == f2.read()\n", + "\n", + "with open(\"./source_files/solve_solvent.py\") as f1:\n", + " with open(\"./source_files/solve_solvent.py\") as f2:\n", + " assert f1.read() == f2.read()\n", + "\n", + "\n", + "with open(\"./source_files/__init__.py\") as f1:\n", + " with open(\"./source_files/__init__.py\") as f2:\n", + " assert f1.read() == f2.read()\n", + "\n", + "\n", + "shutil.rmtree(\"./source_files/\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/guides/function-template-hamiltonian-simulation.ipynb b/docs/guides/function-template-hamiltonian-simulation.ipynb index ef3b33bab66..650a39b17d7 100644 --- a/docs/guides/function-template-hamiltonian-simulation.ipynb +++ b/docs/guides/function-template-hamiltonian-simulation.ipynb @@ -1,1441 +1,1444 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b3e994de-6477-421d-8a9a-6b20d45260ae", - "metadata": {}, - "source": [ - "---\n", - "title: Build a Qiskit Function template for Hamiltonian simulation\n", - "description: How to create a parallel transpilation program and deploy it to IBM Quantum Platform to use as a reusable remote service.\n", - "---\n", - "\n", - "\n", - "# Build a Qiskit Function template for Hamiltonian simulation" - ] - }, - { - "cell_type": "markdown", - "id": "31aee42c-1834-4fae-a05f-f78d8e5db7c0", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [] - }, - { - "cell_type": "markdown", - "id": "b51e81bf-0bbf-4f64-af1e-87fcb443d997", - "metadata": {}, - "source": [ - "This template encapsulates a workflow to simulate the time evolution of an initial state against a user defined spin-based Hamiltonian and returns a set of specified expectation values using the [AQC-Tensor](https://qiskit.github.io/qiskit-addon-aqc-tensor/) Qiskit addon.\n", - "\n", - "This template is structured as a Qiskit pattern with the following steps:\n", - "\n", - "#### 1. Collecting input and mapping the problem\n", - "\n", - "This section takes as an input the Hamiltonian to simulate, an initial state in the form of a `QuantumCircuit`, a set of observables to estimate expectation values, and a specification of options for the AQC addon. This step validates that all required input data is present and that they are in the correct format.\n", - "\n", - "The input arguments are then used to construct the relevant quantum circuits and operators for the workflow. A target circuit is created and a matrix product state representation of this circuit is found using the AQC addon. Following this, an ansatz circuit is generated and optimized using tensor network methods, producing a final circuit which executes the remainder of the time evolution.\n", - "\n", - "#### 2. Prepare the generated circuits for execution\n", - "\n", - "The generated circuits from the AQC addon are then transpiled to execute on a chosen backend. An [`EstimatorV2`](../api/qiskit-ibm-runtime/estimator-v2) instance is created with a default set of error mitigation options to manage the circuit execution.\n", - "\n", - "#### 3. Execution\n", - "\n", - "Finally, the ansatz circuit is transpiled and executed on a QPU and collects estimates for all of the specified expectation values, which are returned in a serializable format for access by the user." - ] - }, - { - "cell_type": "markdown", - "id": "e451f954-1d8f-4687-a7e9-e4b0dfa170f3", - "metadata": {}, - "source": [ - "## Write the function template\n", - "\n", - "First, write a function template for Hamiltonian simulation that uses the [AQC-Tensor Qiskit addon](https://qiskit.github.io/qiskit-addon-aqc-tensor/) to map the problem description to a reduced-depth circuit for execution on hardware.\n", - "\n", - "If you download this page and view it locally in a notebook editor, you will see some of the code cells contain the [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile) `%%writefile`. This magic command saves the code to `./source_files/template_hamiltonian_simulation.py`, which is the function template you can upload to and run remotely with Qiskit Serverless." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "35f5800c-163d-47a6-9fcf-13377be57282", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it just creates a new folder\n", - "from pathlib import Path\n", - "\n", - "Path(\"./source_files\").mkdir(exist_ok=True)" - ] - }, - { - "cell_type": "markdown", - "id": "115c14aa-5028-46f9-ab19-b49d47519636", - "metadata": {}, - "source": [ - "### Collect and validate the inputs\n", - "\n", - "Start by getting the inputs for the template. This example has domain-specific inputs relevant for Hamiltonian simulation (such as the Hamiltonian and observable) and capability-specific options (such as how much you want to compress the initial layers of the Trotter circuit using AQC-Tensor, or advanced options for fine-tuning error suppression and mitigation beyond the defaults that are part of this example)." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5e1e974b-feaa-47ce-abd1-65d442e8176e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Writing ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "from qiskit import QuantumCircuit\n", - "from qiskit_serverless import get_arguments, save_result\n", - "\n", - "\n", - "# Extract parameters from arguments\n", - "#\n", - "# Do this at the top of the program so it fails early if any required arguments are missing or invalid.\n", - "\n", - "arguments = get_arguments()\n", - "\n", - "dry_run = arguments.get(\"dry_run\", False)\n", - "backend_name = arguments[\"backend_name\"]\n", - "\n", - "aqc_evolution_time = arguments[\"aqc_evolution_time\"]\n", - "aqc_ansatz_num_trotter_steps = arguments[\"aqc_ansatz_num_trotter_steps\"]\n", - "aqc_target_num_trotter_steps = arguments[\"aqc_target_num_trotter_steps\"]\n", - "\n", - "remainder_evolution_time = arguments[\"remainder_evolution_time\"]\n", - "remainder_num_trotter_steps = arguments[\"remainder_num_trotter_steps\"]\n", - "\n", - "# Stop if this fidelity is achieved\n", - "aqc_stopping_fidelity = arguments.get(\"aqc_stopping_fidelity\", 1.0)\n", - "# Stop after this number of iterations, even if stopping fidelity is not achieved\n", - "aqc_max_iterations = arguments.get(\"aqc_max_iterations\", 500)\n", - "\n", - "hamiltonian = arguments[\"hamiltonian\"]\n", - "observable = arguments[\"observable\"]\n", - "initial_state = arguments.get(\"initial_state\", QuantumCircuit(hamiltonian.num_qubits))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "3f2629d5-5183-432a-8802-115a3b2f6ff7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "import numpy as np\n", - "import json\n", - "from mergedeep import merge\n", - "\n", - "\n", - "# Configure `EstimatorOptions`, to control the parameters of the hardware experiment\n", - "#\n", - "# Set default options\n", - "estimator_default_options = {\n", - " \"resilience\": {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\n", - " \"amplifier\": \"gate_folding\",\n", - " \"noise_factors\": [1, 2, 3],\n", - " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", - " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", - " },\n", - " \"measure_noise_learning\": {\n", - " \"num_randomizations\": 512,\n", - " \"shots_per_randomization\": 512,\n", - " },\n", - " },\n", - " \"twirling\": {\n", - " \"enable_gates\": True,\n", - " \"enable_measure\": True,\n", - " \"num_randomizations\": 300,\n", - " \"shots_per_randomization\": 100,\n", - " \"strategy\": \"active\",\n", - " },\n", - "}\n", - "# Merge with user-provided options\n", - "estimator_options = merge(\n", - " arguments.get(\"estimator_options\", {}), estimator_default_options\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "ac4e21bf-ccec-459e-984b-dc6a13ea56c8", - "metadata": {}, - "source": [ - "When the function template is running, it is helpful to return information in the logs by using print statements, so that you can better evaluate the workload's progress. Following is a simple example of printing the `estimator_options` so there is a record of the actual Estimator options used. There are many more similar examples throughout the program to report progress during execution, including the value of the objective function during the iterative component of AQC-Tensor, and the two-qubit depth of the final instruction set architecture (ISA) circuit intended for execution on hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "be933b77-fb13-4875-9734-4226067bc8d2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "print(\"estimator_options =\", json.dumps(estimator_options, indent=4))" - ] - }, - { - "cell_type": "markdown", - "id": "fb65718c-8837-4610-87b7-59e6dc7abb80", - "metadata": {}, - "source": [ - "#### Validate the inputs\n", - "\n", - "An important aspect of ensuring that the template can be reused across a range of inputs is input validation. The following code is an example of verifying that the stopping fidelity during AQC-Tensor has been specified appropriately and if not, returning an informative error message for how to fix the error." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "0af1aee2-5771-4ae1-82dc-3ec08943de54", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "# Perform parameter validation\n", - "\n", - "if not 0.0 < aqc_stopping_fidelity <= 1.0:\n", - " raise ValueError(\n", - " f\"Invalid stopping fidelity: {aqc_stopping_fidelity}. It must be a positive float no greater than 1.\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "0e5495ee-82e2-43dc-bca7-f8e81f8b6302", - "metadata": {}, - "source": [ - "#### Prepare the function outputs\n", - "\n", - "First, prepare a dictionary to hold all of the function template outputs. Keys will be added to this dictionary throughout the workflow, and it is returned at the end of the program." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "1677fa7c-3b4a-4a24-b1e4-e03d9b3c49da", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "output = {}" - ] - }, - { - "cell_type": "markdown", - "id": "3acd5522-a880-4de7-9251-2d68efc261ad", - "metadata": {}, - "source": [ - "### Map the problem and pre-process the circuit with AQC\n", - "\n", - "The AQC-Tensor optimization happens in step 1 of a Qiskit pattern. First, a target state is constructed. In this example, it is constructed from a target circuit that evolves the same Hamiltonian for the same time period as the AQC portion. Then, an ansatz is generated from an equivalent circuit but with fewer Trotter steps. In the main portion of the AQC algorithm, that ansatz is iteratively brought closer to the target state. Finally, the result is combined with the remainder of the Trotter steps needed to reach the desired evolution time.\n", - "\n", - "Note the additional examples of logging incorporated in the following code." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "92b59882-2844-4312-a09b-3da02c63f60b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "import os\n", - "os.environ[\"NUMBA_CACHE_DIR\"] = \"/data\"\n", - "\n", - "import datetime\n", - "import quimb.tensor\n", - "from scipy.optimize import OptimizeResult, minimize\n", - "from qiskit.synthesis import SuzukiTrotter\n", - "from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit\n", - "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", - " generate_ansatz_from_circuit,\n", - " AnsatzBlock,\n", - ")\n", - "from qiskit_addon_aqc_tensor.simulation import (\n", - " tensornetwork_from_circuit,\n", - " compute_overlap,\n", - ")\n", - "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", - "from qiskit_addon_aqc_tensor.objective import OneMinusFidelity\n", - "\n", - "print(\"Hamiltonian:\", hamiltonian)\n", - "print(\"Observable:\", observable)\n", - "simulator_settings = QuimbSimulator(quimb.tensor.CircuitMPS, autodiff_backend=\"jax\")\n", - "\n", - "# Construct the AQC target circuit\n", - "aqc_target_circuit = initial_state.copy()\n", - "if aqc_evolution_time:\n", - " aqc_target_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - " )\n", - "\n", - "# Construct matrix-product state representation of the AQC target state\n", - "aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)\n", - "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", - "output[\"target_bond_dimension\"] = aqc_target_mps.psi.max_bond()\n", - "\n", - "# Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", - "aqc_good_circuit = initial_state.copy()\n", - "if aqc_evolution_time:\n", - " aqc_good_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - " )\n", - "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(aqc_good_circuit)\n", - "print(\"Number of AQC parameters:\", len(aqc_initial_parameters))\n", - "output[\"num_aqc_parameters\"] = len(aqc_initial_parameters)\n", - "\n", - "# Calculate the fidelity of ansatz circuit vs. the target state, before optimization\n", - "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", - "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", - "print(\"Starting fidelity of AQC portion:\", starting_fidelity)\n", - "output[\"aqc_starting_fidelity\"] = starting_fidelity\n", - "\n", - "# Optimize the ansatz parameters by using MPS calculations\n", - "def callback(intermediate_result: OptimizeResult):\n", - " fidelity = 1 - intermediate_result.fun\n", - " print(f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\")\n", - " if intermediate_result.fun < stopping_point:\n", - " raise StopIteration\n", - "\n", - "\n", - "objective = OneMinusFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)\n", - "stopping_point = 1.0 - aqc_stopping_fidelity\n", - "\n", - "result = minimize(\n", - " objective,\n", - " aqc_initial_parameters,\n", - " method=\"L-BFGS-B\",\n", - " jac=True,\n", - " options={\"maxiter\": aqc_max_iterations},\n", - " callback=callback,\n", - ")\n", - "if result.status not in (\n", - " 0,\n", - " 1,\n", - " 99,\n", - "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", - " raise RuntimeError(\n", - " f\"Optimization failed: {result.message} (status={result.status})\"\n", - " )\n", - "print(f\"Done after {result.nit} iterations.\")\n", - "output[\"num_iterations\"] = result.nit\n", - "aqc_final_parameters = result.x\n", - "output[\"aqc_final_parameters\"] = list(aqc_final_parameters)\n", - "\n", - "# Construct an optimized circuit for initial portion of time evolution\n", - "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", - "\n", - "# Calculate fidelity after optimization\n", - "aqc_final_mps = tensornetwork_from_circuit(aqc_final_circuit, simulator_settings)\n", - "aqc_fidelity = abs(compute_overlap(aqc_final_mps, aqc_target_mps)) ** 2\n", - "print(\"Fidelity of AQC portion:\", aqc_fidelity)\n", - "output[\"aqc_fidelity\"] = aqc_fidelity\n", - "\n", - "# Construct final circuit, with remainder of time evolution\n", - "final_circuit = aqc_final_circuit.copy()\n", - "if remainder_evolution_time:\n", - " remainder_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=remainder_num_trotter_steps),\n", - " time=remainder_evolution_time,\n", - " )\n", - " final_circuit.compose(remainder_circuit, inplace=True)" - ] - }, - { - "cell_type": "markdown", - "id": "ae8e53a6-2106-4753-ad1a-ebc146cd44ab", - "metadata": {}, - "source": [ - "### Optimize the final circuit for execution\n", - "\n", - "After the AQC portion of the workflow, the `final_circuit` is [transpiled for the hardware](/docs/guides/transpile#instruction-set-architecture) as usual." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "1db2749f-1285-48c6-8ec3-bc9f422686e2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.backend(backend_name)\n", - "\n", - "# Transpile PUBs (circuits and observables) to match ISA\n", - "pass_manager = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", - "isa_circuit = pass_manager.run(final_circuit)\n", - "isa_observable = observable.apply_layout(isa_circuit.layout)\n", - "\n", - "isa_2qubit_depth = isa_circuit.depth(lambda x: x.operation.num_qubits == 2)\n", - "print(\"ISA circuit two-qubit depth:\", isa_2qubit_depth)\n", - "output[\"twoqubit_depth\"] = isa_2qubit_depth" - ] - }, - { - "cell_type": "markdown", - "id": "c41b9c0e-1f00-454c-acff-c194cc16e03e", - "metadata": {}, - "source": [ - "#### Exit early if using dry run mode\n", - "\n", - "If dry run mode has been selected, then the program is stopped before executing on hardware. This can be useful if, for example, you want first to inspect the two-qubit depth of the ISA circuit before deciding to execute on hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "e7c6c770-0453-4ad7-9a3e-20a47208768a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "# Exit now if dry run; don't execute on hardware\n", - "if dry_run:\n", - " import sys\n", - "\n", - " print(\"Exiting before hardware execution since `dry_run` is True.\")\n", - " save_result(output)\n", - " sys.exit(0)" - ] - }, - { - "cell_type": "markdown", - "id": "3a603817-abaf-4403-beea-cca838a59577", - "metadata": {}, - "source": [ - "#### Execute the circuit on hardware" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "b7525014-a473-4d9d-b7cb-9c590f2364ae", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "# ## Step 3: Execute quantum experiments on backend\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "\n", - "estimator = Estimator(backend, options=estimator_options)\n", - "\n", - "# Submit the underlying Estimator job. Note that this is not the\n", - "# actual function job.\n", - "job = estimator.run([(isa_circuit, isa_observable)])\n", - "print(\"Job ID:\", job.job_id())\n", - "output[\"job_id\"] = job.job_id()\n", - "\n", - "# Wait until job is complete\n", - "hw_results = job.result()\n", - "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", - "\n", - "# Save hardware results to serverless output dictionary\n", - "output[\"hw_results\"] = hw_results_dicts\n", - "\n", - "# Reorganize expectation values\n", - "hw_expvals = [pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts]\n", - "\n", - "# Save expectation values to Qiskit Serverless\n", - "print(\"Hardware expectation values\", hw_expvals)\n", - "output[\"hw_expvals\"] = hw_expvals[0]" - ] - }, - { - "cell_type": "markdown", - "id": "12df35c4-4f1c-48cb-a323-d0cf33422fab", - "metadata": {}, - "source": [ - "#### Save the output\n", - "\n", - "This function template returns the relevant domain-level output for this Hamiltonian simulation workflow (expectation values) in addition to important metadata generated along the way." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "bfb8ab87-c42d-4993-92bd-8b38364dc443", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/template_hamiltonian_simulation.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", - "\n", - "save_result(output)" - ] - }, - { - "cell_type": "markdown", - "id": "220f8bc8-4f04-41d3-86f1-15e9c2f45a56", - "metadata": {}, - "source": [ - "## Deploy the function to IBM Quantum Platform\n", - "The previous section created a program to be run remotely. The code in this section uploads that program to Qiskit Serverless.\n", - "\n", - "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key, which you can find on the [IBM Quantum Platform](https://quantum.cloud.ibm.com) dashboard, and upload the program.\n", - "\n", - "You can optionally use `save_account()` to save your credentials (see the [Set up your IBM Cloud account](/docs/guides/cloud-setup#cloud-save) guide). Note that this writes your credentials to the same file as [`QiskitRuntimeService.save_account()`](/docs/api/qiskit-ibm-runtime/qiskit-runtime-service#save_account)." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "313bd03b-bf9b-4e6c-aa05-1fe8def3868d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_catalog import QiskitServerless, QiskitFunction\n", - "\n", - "# Authenticate to the remote cluster and submit the pattern for remote execution\n", - "serverless = QiskitServerless()" - ] - }, - { - "cell_type": "markdown", - "id": "e9c495d4-cdab-4329-b65d-d111029aee64", - "metadata": {}, - "source": [ - "This program has custom `pip` dependencies. Add them to a `dependencies` array when constructing the `QiskitFunction` instance:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "ca386323-d92d-4c41-908b-1670324e1264", - "metadata": {}, - "outputs": [], - "source": [ - "template = QiskitFunction(\n", - " title=\"template_hamiltonian_simulation\",\n", - " entrypoint=\"template_hamiltonian_simulation.py\",\n", - " working_dir=\"./source_files/\",\n", - " dependencies=[\n", - " \"qiskit-addon-utils~=0.1.0\",\n", - " \"qiskit-addon-aqc-tensor[quimb-jax]~=0.1.2\",\n", - " \"mergedeep==1.3.4\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "80a2c6b5-1f1e-4e90-9b1f-75907caf1df3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "QiskitFunction(template_hamiltonian_simulation)" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.upload(template)" - ] - }, - { - "cell_type": "markdown", - "id": "06677105-627a-4948-aac7-071f44327a0b", - "metadata": {}, - "source": [ - "Finally, check if the program successfully uploaded, use `serverless.list()`:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "11f14088-8eca-4a99-a291-3f37a61b0d26", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - " QiskitFunction(template_hamiltonian_simulation),\n" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.list()" - ] - }, - { - "cell_type": "markdown", - "id": "385bda3e-73de-413c-8e45-cf7047303219", - "metadata": {}, - "source": [ - "## Run the function template remotely\n", - "\n", - "The function template has been uploaded, so you can run it remotely with Qiskit Serverless. First, load the template by name:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "d4ff8dea-95ab-4cc9-92b9-e28e277e76f2", - "metadata": {}, - "outputs": [], - "source": [ - "template = serverless.load(\"template_hamiltonian_simulation\")" - ] - }, - { - "cell_type": "markdown", - "id": "57964962-87e9-4c0b-a717-cb620019960e", - "metadata": {}, - "source": [ - "Next, run the template with the domain-level inputs for Hamiltonian simulation. This example specifies a 50-qubit XXZ model with random couplings, and an initial state and observable." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "e5d1555c-9abb-4fef-b8a2-f8d1b723df01", - "metadata": {}, - "outputs": [], - "source": [ - "from itertools import chain\n", - "import numpy as np\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "L = 50\n", - "\n", - "# Generate the edge list for this spin-chain\n", - "edges = [(i, i + 1) for i in range(L - 1)]\n", - "# Generate an edge-coloring so we can make hw-efficient circuits\n", - "edges = edges[::2] + edges[1::2]\n", - "\n", - "# Generate random coefficients for our XXZ Hamiltonian\n", - "np.random.seed(0)\n", - "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", - "\n", - "hamiltonian = SparsePauliOp.from_sparse_list(\n", - " chain.from_iterable(\n", - " [\n", - " [\n", - " (\"XX\", (i, j), Js[i] / 2),\n", - " (\"YY\", (i, j), Js[i] / 2),\n", - " (\"ZZ\", (i, j), Js[i]),\n", - " ]\n", - " for i, j in edges\n", - " ]\n", - " ),\n", - " num_qubits=L,\n", - ")\n", - "observable = SparsePauliOp.from_sparse_list(\n", - " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "7ec5ab02-280c-4c04-8e63-e5354eb8ebb9", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "initial_state = QuantumCircuit(L)\n", - "for i in range(L):\n", - " if i % 2:\n", - " initial_state.x(i)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "ac8d8fec-0290-4d6b-a18c-afd02acfa850", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "853b0edb-d63f-4629-be71-398b6dcf33cb\n" - ] - } - ], - "source": [ - "job = template.run(\n", - " dry_run=True,\n", - " initial_state=initial_state,\n", - " hamiltonian=hamiltonian,\n", - " observable=observable,\n", - " backend_name=\"ibm_fez\",\n", - " estimator_options={},\n", - " aqc_evolution_time=0.2,\n", - " aqc_ansatz_num_trotter_steps=1,\n", - " aqc_target_num_trotter_steps=32,\n", - " remainder_evolution_time=0.2,\n", - " remainder_num_trotter_steps=4,\n", - " aqc_max_iterations=300,\n", - ")\n", - "print(job.job_id)" - ] - }, - { - "cell_type": "markdown", - "id": "2249cabc-6821-4d84-b4d8-e519c2d94a5c", - "metadata": {}, - "source": [ - "Check the status of the job:" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "ed3b744d-cb00-43e0-907c-34dfebe2fa9b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'QUEUED'" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job.status()" - ] - }, - { - "cell_type": "markdown", - "id": "83751f45-ab37-4037-bc77-d1aef91ed46c", - "metadata": {}, - "source": [ - "After the job is running, you can fetch logs created from the `print()` outputs. These can provide actionable information about the progress of the Hamiltonian simulation workflow. For example, the value of the objective function during the iterative component of AQC, or the two-qubit depth of the final ISA circuit intended for execution on hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "3be62464-18b0-4598-b386-0ac0cc9cccb6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "No logs yet.\n" - ] - } - ], - "source": [ - "print(job.logs())" - ] - }, - { - "cell_type": "markdown", - "id": "a8384509-2a9d-44e6-a729-40464ff52bea", - "metadata": {}, - "source": [ - "Block the rest of the program until a result is available. After the job is done, you can retrieve the results. These include the domain-level output of Hamiltonian simulation (expectation value) and useful metadata." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "fc678bcd-539d-4970-86e0-9a69f0a367ef", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'target_bond_dimension': 5,\n", - " 'num_aqc_parameters': 816,\n", - " 'aqc_starting_fidelity': 0.9914382555614002,\n", - " 'num_iterations': 72,\n", - " 'aqc_fidelity': 0.9998108844412502,\n", - " 'twoqubit_depth': 33}" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result = job.result()\n", - "\n", - "del result[\n", - " \"aqc_final_parameters\"\n", - "] # the list is too long to conveniently display here\n", - "result" - ] - }, - { - "cell_type": "markdown", - "id": "12b0ccfe-325a-4939-81e7-58d94557990d", - "metadata": {}, - "source": [ - "After the job completes, the entire logging output will be available." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "cb722373-cbfb-45a7-a2b5-d7a97e18ee6c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2024-12-17 14:50:15,580\tINFO job_manager.py:531 -- Runtime env is setting up.\n", - "estimator_options = {\n", - " \"resilience\": {\n", - " \"measure_mitigation\": true,\n", - " \"zne_mitigation\": true,\n", - " \"zne\": {\n", - " \"amplifier\": \"gate_folding\",\n", - " \"noise_factors\": [\n", - " 1,\n", - " 2,\n", - " 3\n", - " ],\n", - " \"extrapolated_noise_factors\": [\n", - " 0.0,\n", - " 0.1,\n", - " 0.2,\n", - " 0.30000000000000004,\n", - " 0.4,\n", - " 0.5,\n", - " 0.6000000000000001,\n", - " 0.7000000000000001,\n", - " 0.8,\n", - " 0.9,\n", - " 1.0,\n", - " 1.1,\n", - " 1.2000000000000002,\n", - " 1.3,\n", - " 1.4000000000000001,\n", - " 1.5,\n", - " 1.6,\n", - " 1.7000000000000002,\n", - " 1.8,\n", - " 1.9000000000000001,\n", - " 2.0,\n", - " 2.1,\n", - " 2.2,\n", - " 2.3000000000000003,\n", - " 2.4000000000000004,\n", - " 2.5,\n", - " 2.6,\n", - " 2.7,\n", - " 2.8000000000000003,\n", - " 2.9000000000000004,\n", - " 3.0\n", - " ],\n", - " \"extrapolator\": [\n", - " \"exponential\",\n", - " \"linear\",\n", - " \"fallback\"\n", - " ]\n", - " },\n", - " \"measure_noise_learning\": {\n", - " \"num_randomizations\": 512,\n", - " \"shots_per_randomization\": 512\n", - " }\n", - " },\n", - " \"twirling\": {\n", - " \"enable_gates\": true,\n", - " \"enable_measure\": true,\n", - " \"num_randomizations\": 300,\n", - " \"shots_per_randomization\": 100,\n", - " \"strategy\": \"active\"\n", - " }\n", - "}\n", - "Hamiltonian: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXX', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYY', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'XXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'YYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j, 0.55138169+0.j,\n", - " 0.55138169+0.j, 1.10276338+0.j, 0.4618274 +0.j, 0.4618274 +0.j,\n", - " 0.9236548 +0.j, 0.46879361+0.j, 0.46879361+0.j, 0.93758721+0.j,\n", - " 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j, 0.64586252+0.j,\n", - " 0.64586252+0.j, 1.29172504+0.j, 0.53402228+0.j, 0.53402228+0.j,\n", - " 1.06804456+0.j, 0.28551803+0.j, 0.28551803+0.j, 0.57103606+0.j,\n", - " 0.2601092 +0.j, 0.2601092 +0.j, 0.5202184 +0.j, 0.63907838+0.j,\n", - " 0.63907838+0.j, 1.27815675+0.j, 0.73930917+0.j, 0.73930917+0.j,\n", - " 1.47861834+0.j, 0.48073968+0.j, 0.48073968+0.j, 0.96147936+0.j,\n", - " 0.30913721+0.j, 0.30913721+0.j, 0.61827443+0.j, 0.32167664+0.j,\n", - " 0.32167664+0.j, 0.64335329+0.j, 0.51092416+0.j, 0.51092416+0.j,\n", - " 1.02184832+0.j, 0.38227781+0.j, 0.38227781+0.j, 0.76455561+0.j,\n", - " 0.47807517+0.j, 0.47807517+0.j, 0.95615033+0.j, 0.2593949 +0.j,\n", - " 0.2593949 +0.j, 0.5187898 +0.j, 0.55604786+0.j, 0.55604786+0.j,\n", - " 1.11209572+0.j, 0.72187404+0.j, 0.72187404+0.j, 1.44374808+0.j,\n", - " 0.42975395+0.j, 0.42975395+0.j, 0.8595079 +0.j, 0.5988156 +0.j,\n", - " 0.5988156 +0.j, 1.1976312 +0.j, 0.58338336+0.j, 0.58338336+0.j,\n", - " 1.16676672+0.j, 0.35519128+0.j, 0.35519128+0.j, 0.71038256+0.j,\n", - " 0.40771418+0.j, 0.40771418+0.j, 0.81542835+0.j, 0.60759468+0.j,\n", - " 0.60759468+0.j, 1.21518937+0.j, 0.52244159+0.j, 0.52244159+0.j,\n", - " 1.04488318+0.j, 0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j,\n", - " 0.6958865 +0.j, 0.6958865 +0.j, 1.391773 +0.j, 0.44172076+0.j,\n", - " 0.44172076+0.j, 0.88344152+0.j, 0.51444746+0.j, 0.51444746+0.j,\n", - " 1.02889492+0.j, 0.71279832+0.j, 0.71279832+0.j, 1.42559664+0.j,\n", - " 0.29356465+0.j, 0.29356465+0.j, 0.5871293 +0.j, 0.66630992+0.j,\n", - " 0.66630992+0.j, 1.33261985+0.j, 0.68500607+0.j, 0.68500607+0.j,\n", - " 1.37001215+0.j, 0.64957928+0.j, 0.64957928+0.j, 1.29915856+0.j,\n", - " 0.64026459+0.j, 0.64026459+0.j, 1.28052918+0.j, 0.56996051+0.j,\n", - " 0.56996051+0.j, 1.13992102+0.j, 0.72233446+0.j, 0.72233446+0.j,\n", - " 1.44466892+0.j, 0.45733097+0.j, 0.45733097+0.j, 0.91466194+0.j,\n", - " 0.63711684+0.j, 0.63711684+0.j, 1.27423369+0.j, 0.53421697+0.j,\n", - " 0.53421697+0.j, 1.06843395+0.j, 0.55881775+0.j, 0.55881775+0.j,\n", - " 1.1176355 +0.j, 0.558467 +0.j, 0.558467 +0.j, 1.116934 +0.j,\n", - " 0.59091015+0.j, 0.59091015+0.j, 1.1818203 +0.j, 0.46851598+0.j,\n", - " 0.46851598+0.j, 0.93703195+0.j, 0.28011274+0.j, 0.28011274+0.j,\n", - " 0.56022547+0.j, 0.58531893+0.j, 0.58531893+0.j, 1.17063787+0.j,\n", - " 0.31446315+0.j, 0.31446315+0.j, 0.6289263 +0.j])\n", - "Observable: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[1.+0.j])\n", - "Target MPS maximum bond dimension: 5\n", - "Number of AQC parameters: 816\n", - "Starting fidelity of AQC portion: 0.9914382555614002\n", - "2024-12-17 14:52:23.400028 Intermediate result: Fidelity 0.99764093\n", - "2024-12-17 14:52:23.429669 Intermediate result: Fidelity 0.99788003\n", - "2024-12-17 14:52:23.459674 Intermediate result: Fidelity 0.99795970\n", - "2024-12-17 14:52:23.489666 Intermediate result: Fidelity 0.99799067\n", - "2024-12-17 14:52:23.518545 Intermediate result: Fidelity 0.99803401\n", - "2024-12-17 14:52:23.546952 Intermediate result: Fidelity 0.99809821\n", - "2024-12-17 14:52:23.575271 Intermediate result: Fidelity 0.99824660\n", - "2024-12-17 14:52:23.604049 Intermediate result: Fidelity 0.99845326\n", - "2024-12-17 14:52:23.632709 Intermediate result: Fidelity 0.99870497\n", - "2024-12-17 14:52:23.660527 Intermediate result: Fidelity 0.99891442\n", - "2024-12-17 14:52:23.688273 Intermediate result: Fidelity 0.99904488\n", - "2024-12-17 14:52:23.716105 Intermediate result: Fidelity 0.99914438\n", - "2024-12-17 14:52:23.744336 Intermediate result: Fidelity 0.99922827\n", - "2024-12-17 14:52:23.773399 Intermediate result: Fidelity 0.99929071\n", - "2024-12-17 14:52:23.801482 Intermediate result: Fidelity 0.99932432\n", - "2024-12-17 14:52:23.830466 Intermediate result: Fidelity 0.99936460\n", - "2024-12-17 14:52:23.860738 Intermediate result: Fidelity 0.99938891\n", - "2024-12-17 14:52:23.889958 Intermediate result: Fidelity 0.99940607\n", - "2024-12-17 14:52:23.918703 Intermediate result: Fidelity 0.99941965\n", - "2024-12-17 14:52:23.949744 Intermediate result: Fidelity 0.99944337\n", - "2024-12-17 14:52:23.980871 Intermediate result: Fidelity 0.99946875\n", - "2024-12-17 14:52:24.012124 Intermediate result: Fidelity 0.99949009\n", - "2024-12-17 14:52:24.044359 Intermediate result: Fidelity 0.99952191\n", - "2024-12-17 14:52:24.075840 Intermediate result: Fidelity 0.99953669\n", - "2024-12-17 14:52:24.106303 Intermediate result: Fidelity 0.99955242\n", - "2024-12-17 14:52:24.139329 Intermediate result: Fidelity 0.99958412\n", - "2024-12-17 14:52:24.169725 Intermediate result: Fidelity 0.99960176\n", - "2024-12-17 14:52:24.198749 Intermediate result: Fidelity 0.99961606\n", - "2024-12-17 14:52:24.227874 Intermediate result: Fidelity 0.99963811\n", - "2024-12-17 14:52:24.256818 Intermediate result: Fidelity 0.99964383\n", - "2024-12-17 14:52:24.285889 Intermediate result: Fidelity 0.99964717\n", - "2024-12-17 14:52:24.315228 Intermediate result: Fidelity 0.99966064\n", - "2024-12-17 14:52:24.345322 Intermediate result: Fidelity 0.99966517\n", - "2024-12-17 14:52:24.374921 Intermediate result: Fidelity 0.99967089\n", - "2024-12-17 14:52:24.404309 Intermediate result: Fidelity 0.99968305\n", - "2024-12-17 14:52:24.432664 Intermediate result: Fidelity 0.99968889\n", - "2024-12-17 14:52:24.461639 Intermediate result: Fidelity 0.99969997\n", - "2024-12-17 14:52:24.491244 Intermediate result: Fidelity 0.99971666\n", - "2024-12-17 14:52:24.520354 Intermediate result: Fidelity 0.99972441\n", - "2024-12-17 14:52:24.549965 Intermediate result: Fidelity 0.99973561\n", - "2024-12-17 14:52:24.583464 Intermediate result: Fidelity 0.99973811\n", - "2024-12-17 14:52:24.617537 Intermediate result: Fidelity 0.99974074\n", - "2024-12-17 14:52:24.652247 Intermediate result: Fidelity 0.99974467\n", - "2024-12-17 14:52:24.686831 Intermediate result: Fidelity 0.99974991\n", - "2024-12-17 14:52:24.725476 Intermediate result: Fidelity 0.99975230\n", - "2024-12-17 14:52:24.764637 Intermediate result: Fidelity 0.99975373\n", - "2024-12-17 14:52:24.802499 Intermediate result: Fidelity 0.99975552\n", - "2024-12-17 14:52:24.839960 Intermediate result: Fidelity 0.99975885\n", - "2024-12-17 14:52:24.877472 Intermediate result: Fidelity 0.99976469\n", - "2024-12-17 14:52:24.916233 Intermediate result: Fidelity 0.99976517\n", - "2024-12-17 14:52:24.993750 Intermediate result: Fidelity 0.99976875\n", - "2024-12-17 14:52:25.034953 Intermediate result: Fidelity 0.99976887\n", - "2024-12-17 14:52:25.076197 Intermediate result: Fidelity 0.99977244\n", - "2024-12-17 14:52:25.112340 Intermediate result: Fidelity 0.99977638\n", - "2024-12-17 14:52:25.149947 Intermediate result: Fidelity 0.99977828\n", - "2024-12-17 14:52:25.190049 Intermediate result: Fidelity 0.99978174\n", - "2024-12-17 14:52:25.310903 Intermediate result: Fidelity 0.99978222\n", - "2024-12-17 14:52:25.347512 Intermediate result: Fidelity 0.99978508\n", - "2024-12-17 14:52:25.385201 Intermediate result: Fidelity 0.99978543\n", - "2024-12-17 14:52:25.457436 Intermediate result: Fidelity 0.99978770\n", - "2024-12-17 14:52:25.497133 Intermediate result: Fidelity 0.99978818\n", - "2024-12-17 14:52:25.541179 Intermediate result: Fidelity 0.99978913\n", - "2024-12-17 14:52:25.584791 Intermediate result: Fidelity 0.99978937\n", - "2024-12-17 14:52:25.621484 Intermediate result: Fidelity 0.99979068\n", - "2024-12-17 14:52:25.655847 Intermediate result: Fidelity 0.99979211\n", - "2024-12-17 14:52:25.691710 Intermediate result: Fidelity 0.99979700\n", - "2024-12-17 14:52:25.767711 Intermediate result: Fidelity 0.99979759\n", - "2024-12-17 14:52:25.804517 Intermediate result: Fidelity 0.99979807\n", - "2024-12-17 14:52:25.839394 Intermediate result: Fidelity 0.99980236\n", - "2024-12-17 14:52:25.874438 Intermediate result: Fidelity 0.99980296\n", - "2024-12-17 14:52:25.909900 Intermediate result: Fidelity 0.99980320\n", - "2024-12-17 14:52:26.713044 Intermediate result: Fidelity 0.99980320\n", - "Done after 72 iterations.\n", - "Fidelity of AQC portion: 0.9998108844412502\n", - "ISA circuit two-qubit depth: 33\n", - "Exiting before hardware execution since `dry_run` is True.\n", - "\n" - ] - } - ], - "source": [ - "print(job.logs())" - ] - }, - { - "cell_type": "markdown", - "id": "196d6261-e26b-4057-ae55-19f003fdc10a", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "For a deeper dive into the AQC-Tensor Qiskit addon, check out the [Improved Trotterized Time Evolution with Approximate Quantum Compilation](/docs/tutorials/approximate-quantum-compilation-for-time-evolution) tutorial or the [qiskit-addon-aqc-tensor repository](https://github.com/Qiskit/qiskit-addon-aqc-tensor).\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20502fe4-7940-40fa-a978-64cc3ff6c1b1", - "metadata": { - "tags": [ - "id-full-source" - ] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Overwriting ./source_files/template_hamiltonian_simulation_full.py\n" - ] - } - ], - "source": [ - "%%writefile ./source_files/template_hamiltonian_simulation_full.py\n", - "\n", - "from qiskit import QuantumCircuit\n", - "from qiskit_serverless import get_arguments, save_result\n", - "\n", - "\n", - "# Extract parameters from arguments\n", - "#\n", - "# Do this at the top of the program so it fails early if any required arguments are missing or invalid.\n", - "\n", - "arguments = get_arguments()\n", - "\n", - "dry_run = arguments.get(\"dry_run\", False)\n", - "backend_name = arguments[\"backend_name\"]\n", - "\n", - "aqc_evolution_time = arguments[\"aqc_evolution_time\"]\n", - "aqc_ansatz_num_trotter_steps = arguments[\"aqc_ansatz_num_trotter_steps\"]\n", - "aqc_target_num_trotter_steps = arguments[\"aqc_target_num_trotter_steps\"]\n", - "\n", - "remainder_evolution_time = arguments[\"remainder_evolution_time\"]\n", - "remainder_num_trotter_steps = arguments[\"remainder_num_trotter_steps\"]\n", - "\n", - "# Stop if this fidelity is achieved\n", - "aqc_stopping_fidelity = arguments.get(\"aqc_stopping_fidelity\", 1.0)\n", - "# Stop after this number of iterations, even if stopping fidelity is not achieved\n", - "aqc_max_iterations = arguments.get(\"aqc_max_iterations\", 500)\n", - "\n", - "hamiltonian = arguments[\"hamiltonian\"]\n", - "observable = arguments[\"observable\"]\n", - "initial_state = arguments.get(\"initial_state\", QuantumCircuit(hamiltonian.num_qubits))\n", - "\n", - "import numpy as np\n", - "import json\n", - "from mergedeep import merge\n", - "\n", - "\n", - "# Configure `EstimatorOptions`, to control the parameters of the hardware experiment\n", - "#\n", - "# Set default options\n", - "estimator_default_options = {\n", - " \"resilience\": {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\n", - " \"amplifier\": \"gate_folding\",\n", - " \"noise_factors\": [1, 2, 3],\n", - " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", - " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", - " },\n", - " \"measure_noise_learning\": {\n", - " \"num_randomizations\": 512,\n", - " \"shots_per_randomization\": 512,\n", - " },\n", - " },\n", - " \"twirling\": {\n", - " \"enable_gates\": True,\n", - " \"enable_measure\": True,\n", - " \"num_randomizations\": 300,\n", - " \"shots_per_randomization\": 100,\n", - " \"strategy\": \"active\",\n", - " },\n", - "}\n", - "# Merge with user-provided options\n", - "estimator_options = merge(\n", - " arguments.get(\"estimator_options\", {}), estimator_default_options\n", - ")\n", - "\n", - "print(\"estimator_options =\", json.dumps(estimator_options, indent=4))\n", - "\n", - "# Perform parameter validation\n", - "\n", - "if not 0.0 < aqc_stopping_fidelity <= 1.0:\n", - " raise ValueError(\n", - " f\"Invalid stopping fidelity: {aqc_stopping_fidelity}. It must be a positive float no greater than 1.\"\n", - " )\n", - "\n", - "output = {}\n", - "\n", - "import os\n", - "os.environ[\"NUMBA_CACHE_DIR\"] = \"/data\"\n", - "\n", - "import datetime\n", - "import quimb.tensor\n", - "from scipy.optimize import OptimizeResult, minimize\n", - "from qiskit.synthesis import SuzukiTrotter\n", - "from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit\n", - "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", - " generate_ansatz_from_circuit,\n", - " AnsatzBlock,\n", - ")\n", - "from qiskit_addon_aqc_tensor.simulation import (\n", - " tensornetwork_from_circuit,\n", - " compute_overlap,\n", - ")\n", - "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", - "from qiskit_addon_aqc_tensor.objective import OneMinusFidelity\n", - "\n", - "print(\"Hamiltonian:\", hamiltonian)\n", - "print(\"Observable:\", observable)\n", - "simulator_settings = QuimbSimulator(quimb.tensor.CircuitMPS, autodiff_backend=\"jax\")\n", - "\n", - "# Construct the AQC target circuit\n", - "aqc_target_circuit = initial_state.copy()\n", - "if aqc_evolution_time:\n", - " aqc_target_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - " )\n", - "\n", - "# Construct matrix-product state representation of the AQC target state\n", - "aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)\n", - "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", - "output[\"target_bond_dimension\"] = aqc_target_mps.psi.max_bond()\n", - "\n", - "# Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", - "aqc_good_circuit = initial_state.copy()\n", - "if aqc_evolution_time:\n", - " aqc_good_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - " )\n", - "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(aqc_good_circuit)\n", - "print(\"Number of AQC parameters:\", len(aqc_initial_parameters))\n", - "output[\"num_aqc_parameters\"] = len(aqc_initial_parameters)\n", - "\n", - "# Calculate the fidelity of ansatz circuit vs. the target state, before optimization\n", - "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", - "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", - "print(\"Starting fidelity of AQC portion:\", starting_fidelity)\n", - "output[\"aqc_starting_fidelity\"] = starting_fidelity\n", - "\n", - "# Optimize the ansatz parameters by using MPS calculations\n", - "def callback(intermediate_result: OptimizeResult):\n", - " fidelity = 1 - intermediate_result.fun\n", - " print(f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\")\n", - " if intermediate_result.fun < stopping_point:\n", - " raise StopIteration\n", - "\n", - "\n", - "objective = OneMinusFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)\n", - "stopping_point = 1.0 - aqc_stopping_fidelity\n", - "\n", - "result = minimize(\n", - " objective,\n", - " aqc_initial_parameters,\n", - " method=\"L-BFGS-B\",\n", - " jac=True,\n", - " options={\"maxiter\": aqc_max_iterations},\n", - " callback=callback,\n", - ")\n", - "if result.status not in (\n", - " 0,\n", - " 1,\n", - " 99,\n", - "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", - " raise RuntimeError(\n", - " f\"Optimization failed: {result.message} (status={result.status})\"\n", - " )\n", - "print(f\"Done after {result.nit} iterations.\")\n", - "output[\"num_iterations\"] = result.nit\n", - "aqc_final_parameters = result.x\n", - "output[\"aqc_final_parameters\"] = list(aqc_final_parameters)\n", - "\n", - "# Construct an optimized circuit for initial portion of time evolution\n", - "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", - "\n", - "# Calculate fidelity after optimization\n", - "aqc_final_mps = tensornetwork_from_circuit(aqc_final_circuit, simulator_settings)\n", - "aqc_fidelity = abs(compute_overlap(aqc_final_mps, aqc_target_mps)) ** 2\n", - "print(\"Fidelity of AQC portion:\", aqc_fidelity)\n", - "output[\"aqc_fidelity\"] = aqc_fidelity\n", - "\n", - "# Construct final circuit, with remainder of time evolution\n", - "final_circuit = aqc_final_circuit.copy()\n", - "if remainder_evolution_time:\n", - " remainder_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=remainder_num_trotter_steps),\n", - " time=remainder_evolution_time,\n", - " )\n", - " final_circuit.compose(remainder_circuit, inplace=True)\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.backend(backend_name)\n", - "\n", - "# Transpile PUBs (circuits and observables) to match ISA\n", - "pass_manager = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", - "isa_circuit = pass_manager.run(final_circuit)\n", - "isa_observable = observable.apply_layout(isa_circuit.layout)\n", - "\n", - "isa_2qubit_depth = isa_circuit.depth(lambda x: x.operation.num_qubits == 2)\n", - "print(\"ISA circuit two-qubit depth:\", isa_2qubit_depth)\n", - "output[\"twoqubit_depth\"] = isa_2qubit_depth\n", - "\n", - "# Exit now if dry run; don't execute on hardware\n", - "if dry_run:\n", - " import sys\n", - "\n", - " print(\"Exiting before hardware execution since `dry_run` is True.\")\n", - " save_result(output)\n", - " sys.exit(0)\n", - "\n", - "# ## Step 3: Execute quantum experiments on backend\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "\n", - "estimator = Estimator(backend, options=estimator_options)\n", - "\n", - "# Submit the underlying Estimator job. Note that this is not the\n", - "# actual function job.\n", - "job = estimator.run([(isa_circuit, isa_observable)])\n", - "print(\"Job ID:\", job.job_id())\n", - "output[\"job_id\"] = job.job_id()\n", - "\n", - "# Wait until job is complete\n", - "hw_results = job.result()\n", - "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", - "\n", - "# Save hardware results to serverless output dictionary\n", - "output[\"hw_results\"] = hw_results_dicts\n", - "\n", - "# Reorganize expectation values\n", - "hw_expvals = [pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts]\n", - "\n", - "# Save expectation values to Qiskit Serverless\n", - "output[\"hw_expvals\"] = hw_expvals[0]\n", - "\n", - "save_result(output)" - ] - }, - { - "cell_type": "markdown", - "id": "d7f1776a-a8f6-43a3-85b7-33975c4eeec0", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "Here is the entire source of `./source_files/template_hamiltonian_simulation.py` as one code block.\n", - "\n", - "\n", - "\n", - " \n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "0a5ab26b-baea-48d0-979e-485df3118ac6", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users. It verifies both source listings are identical then deletes the working folder we created\n", - "import shutil\n", - "\n", - "with open(\"./source_files/template_hamiltonian_simulation.py\") as f1:\n", - " with open(\"./source_files/template_hamiltonian_simulation_full.py\") as f2:\n", - " assert f1.read() == f2.read()\n", - "\n", - "shutil.rmtree(\"./source_files/\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b3e994de-6477-421d-8a9a-6b20d45260ae", + "metadata": {}, + "source": [ + "---\n", + "title: Build a Qiskit Function template for Hamiltonian simulation\n", + "description: How to create a parallel transpilation program and deploy it to IBM Quantum Platform to use as a reusable remote service.\n", + "---\n", + "\n", + "\n", + "# Build a Qiskit Function template for Hamiltonian simulation" + ] + }, + { + "cell_type": "markdown", + "id": "31aee42c-1834-4fae-a05f-f78d8e5db7c0", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [] + }, + { + "cell_type": "markdown", + "id": "b51e81bf-0bbf-4f64-af1e-87fcb443d997", + "metadata": {}, + "source": [ + "This template encapsulates a workflow to simulate the time evolution of an initial state against a user defined spin-based Hamiltonian and returns a set of specified expectation values using the [AQC-Tensor](https://qiskit.github.io/qiskit-addon-aqc-tensor/) Qiskit addon.\n", + "\n", + "This template is structured as a Qiskit pattern with the following steps:\n", + "\n", + "#### 1. Collecting input and mapping the problem\n", + "\n", + "This section takes as an input the Hamiltonian to simulate, an initial state in the form of a `QuantumCircuit`, a set of observables to estimate expectation values, and a specification of options for the AQC addon. This step validates that all required input data is present and that they are in the correct format.\n", + "\n", + "The input arguments are then used to construct the relevant quantum circuits and operators for the workflow. A target circuit is created and a matrix product state representation of this circuit is found using the AQC addon. Following this, an ansatz circuit is generated and optimized using tensor network methods, producing a final circuit which executes the remainder of the time evolution.\n", + "\n", + "#### 2. Prepare the generated circuits for execution\n", + "\n", + "The generated circuits from the AQC addon are then transpiled to execute on a chosen backend. An [`EstimatorV2`](../api/qiskit-ibm-runtime/estimator-v2) instance is created with a default set of error mitigation options to manage the circuit execution.\n", + "\n", + "#### 3. Execution\n", + "\n", + "Finally, the ansatz circuit is transpiled and executed on a QPU and collects estimates for all of the specified expectation values, which are returned in a serializable format for access by the user." + ] + }, + { + "cell_type": "markdown", + "id": "e451f954-1d8f-4687-a7e9-e4b0dfa170f3", + "metadata": {}, + "source": [ + "## Write the function template\n", + "\n", + "First, write a function template for Hamiltonian simulation that uses the [AQC-Tensor Qiskit addon](https://qiskit.github.io/qiskit-addon-aqc-tensor/) to map the problem description to a reduced-depth circuit for execution on hardware.\n", + "\n", + "If you download this page and view it locally in a notebook editor, you will see some of the code cells contain the [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile) `%%writefile`. This magic command saves the code to `./source_files/template_hamiltonian_simulation.py`, which is the function template you can upload to and run remotely with Qiskit Serverless." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "35f5800c-163d-47a6-9fcf-13377be57282", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users, it just creates a new folder\n", + "from pathlib import Path\n", + "\n", + "Path(\"./source_files\").mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "id": "115c14aa-5028-46f9-ab19-b49d47519636", + "metadata": {}, + "source": [ + "### Collect and validate the inputs\n", + "\n", + "Start by getting the inputs for the template. This example has domain-specific inputs relevant for Hamiltonian simulation (such as the Hamiltonian and observable) and capability-specific options (such as how much you want to compress the initial layers of the Trotter circuit using AQC-Tensor, or advanced options for fine-tuning error suppression and mitigation beyond the defaults that are part of this example)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5e1e974b-feaa-47ce-abd1-65d442e8176e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Writing ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "from qiskit import QuantumCircuit\n", + "from qiskit_serverless import get_arguments, save_result\n", + "\n", + "\n", + "# Extract parameters from arguments\n", + "#\n", + "# Do this at the top of the program so it fails early if any required arguments are missing or\n", + "# invalid.\n", + "\n", + "arguments = get_arguments()\n", + "\n", + "dry_run = arguments.get(\"dry_run\", False)\n", + "backend_name = arguments[\"backend_name\"]\n", + "\n", + "aqc_evolution_time = arguments[\"aqc_evolution_time\"]\n", + "aqc_ansatz_num_trotter_steps = arguments[\"aqc_ansatz_num_trotter_steps\"]\n", + "aqc_target_num_trotter_steps = arguments[\"aqc_target_num_trotter_steps\"]\n", + "\n", + "remainder_evolution_time = arguments[\"remainder_evolution_time\"]\n", + "remainder_num_trotter_steps = arguments[\"remainder_num_trotter_steps\"]\n", + "\n", + "# Stop if this fidelity is achieved\n", + "aqc_stopping_fidelity = arguments.get(\"aqc_stopping_fidelity\", 1.0)\n", + "# Stop after this number of iterations, even if stopping fidelity is not achieved\n", + "aqc_max_iterations = arguments.get(\"aqc_max_iterations\", 500)\n", + "\n", + "hamiltonian = arguments[\"hamiltonian\"]\n", + "observable = arguments[\"observable\"]\n", + "initial_state = arguments.get(\"initial_state\", QuantumCircuit(hamiltonian.num_qubits))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3f2629d5-5183-432a-8802-115a3b2f6ff7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "import numpy as np\n", + "import json\n", + "from mergedeep import merge\n", + "\n", + "\n", + "# Configure `EstimatorOptions`, to control the parameters of the hardware experiment\n", + "#\n", + "# Set default options\n", + "estimator_default_options = {\n", + " \"resilience\": {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\n", + " \"amplifier\": \"gate_folding\",\n", + " \"noise_factors\": [1, 2, 3],\n", + " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", + " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", + " },\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": 512,\n", + " \"shots_per_randomization\": 512,\n", + " },\n", + " },\n", + " \"twirling\": {\n", + " \"enable_gates\": True,\n", + " \"enable_measure\": True,\n", + " \"num_randomizations\": 300,\n", + " \"shots_per_randomization\": 100,\n", + " \"strategy\": \"active\",\n", + " },\n", + "}\n", + "# Merge with user-provided options\n", + "estimator_options = merge(\n", + " arguments.get(\"estimator_options\", {}), estimator_default_options\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ac4e21bf-ccec-459e-984b-dc6a13ea56c8", + "metadata": {}, + "source": [ + "When the function template is running, it is helpful to return information in the logs by using print statements, so that you can better evaluate the workload's progress. Following is a simple example of printing the `estimator_options` so there is a record of the actual Estimator options used. There are many more similar examples throughout the program to report progress during execution, including the value of the objective function during the iterative component of AQC-Tensor, and the two-qubit depth of the final instruction set architecture (ISA) circuit intended for execution on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "be933b77-fb13-4875-9734-4226067bc8d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "print(\"estimator_options =\", json.dumps(estimator_options, indent=4))" + ] + }, + { + "cell_type": "markdown", + "id": "fb65718c-8837-4610-87b7-59e6dc7abb80", + "metadata": {}, + "source": [ + "#### Validate the inputs\n", + "\n", + "An important aspect of ensuring that the template can be reused across a range of inputs is input validation. The following code is an example of verifying that the stopping fidelity during AQC-Tensor has been specified appropriately and if not, returning an informative error message for how to fix the error." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0af1aee2-5771-4ae1-82dc-3ec08943de54", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "# Perform parameter validation\n", + "\n", + "if not 0.0 < aqc_stopping_fidelity <= 1.0:\n", + " raise ValueError(\n", + " f\"Invalid stopping fidelity: {aqc_stopping_fidelity}. It must be a positive float no greater than 1.\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "0e5495ee-82e2-43dc-bca7-f8e81f8b6302", + "metadata": {}, + "source": [ + "#### Prepare the function outputs\n", + "\n", + "First, prepare a dictionary to hold all of the function template outputs. Keys will be added to this dictionary throughout the workflow, and it is returned at the end of the program." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1677fa7c-3b4a-4a24-b1e4-e03d9b3c49da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "output = {}" + ] + }, + { + "cell_type": "markdown", + "id": "3acd5522-a880-4de7-9251-2d68efc261ad", + "metadata": {}, + "source": [ + "### Map the problem and pre-process the circuit with AQC\n", + "\n", + "The AQC-Tensor optimization happens in step 1 of a Qiskit pattern. First, a target state is constructed. In this example, it is constructed from a target circuit that evolves the same Hamiltonian for the same time period as the AQC portion. Then, an ansatz is generated from an equivalent circuit but with fewer Trotter steps. In the main portion of the AQC algorithm, that ansatz is iteratively brought closer to the target state. Finally, the result is combined with the remainder of the Trotter steps needed to reach the desired evolution time.\n", + "\n", + "Note the additional examples of logging incorporated in the following code." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "92b59882-2844-4312-a09b-3da02c63f60b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "import os\n", + "os.environ[\"NUMBA_CACHE_DIR\"] = \"/data\"\n", + "\n", + "import datetime\n", + "import quimb.tensor\n", + "from scipy.optimize import OptimizeResult, minimize\n", + "from qiskit.synthesis import SuzukiTrotter\n", + "from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit\n", + "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", + " generate_ansatz_from_circuit,\n", + " AnsatzBlock,\n", + ")\n", + "from qiskit_addon_aqc_tensor.simulation import (\n", + " tensornetwork_from_circuit,\n", + " compute_overlap,\n", + ")\n", + "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", + "from qiskit_addon_aqc_tensor.objective import OneMinusFidelity\n", + "\n", + "print(\"Hamiltonian:\", hamiltonian)\n", + "print(\"Observable:\", observable)\n", + "simulator_settings = QuimbSimulator(quimb.tensor.CircuitMPS, autodiff_backend=\"jax\")\n", + "\n", + "# Construct the AQC target circuit\n", + "aqc_target_circuit = initial_state.copy()\n", + "if aqc_evolution_time:\n", + " aqc_target_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + " )\n", + "\n", + "# Construct matrix-product state representation of the AQC target state\n", + "aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)\n", + "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", + "output[\"target_bond_dimension\"] = aqc_target_mps.psi.max_bond()\n", + "\n", + "# Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", + "aqc_good_circuit = initial_state.copy()\n", + "if aqc_evolution_time:\n", + " aqc_good_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + " )\n", + "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(aqc_good_circuit)\n", + "print(\"Number of AQC parameters:\", len(aqc_initial_parameters))\n", + "output[\"num_aqc_parameters\"] = len(aqc_initial_parameters)\n", + "\n", + "# Calculate the fidelity of ansatz circuit vs. the target state, before optimization\n", + "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", + "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", + "print(\"Starting fidelity of AQC portion:\", starting_fidelity)\n", + "output[\"aqc_starting_fidelity\"] = starting_fidelity\n", + "\n", + "# Optimize the ansatz parameters by using MPS calculations\n", + "def callback(intermediate_result: OptimizeResult):\n", + " fidelity = 1 - intermediate_result.fun\n", + " print(f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\")\n", + " if intermediate_result.fun < stopping_point:\n", + " raise StopIteration\n", + "\n", + "\n", + "objective = OneMinusFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)\n", + "stopping_point = 1.0 - aqc_stopping_fidelity\n", + "\n", + "result = minimize(\n", + " objective,\n", + " aqc_initial_parameters,\n", + " method=\"L-BFGS-B\",\n", + " jac=True,\n", + " options={\"maxiter\": aqc_max_iterations},\n", + " callback=callback,\n", + ")\n", + "if result.status not in (\n", + " 0,\n", + " 1,\n", + " 99,\n", + "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", + " raise RuntimeError(\n", + " f\"Optimization failed: {result.message} (status={result.status})\"\n", + " )\n", + "print(f\"Done after {result.nit} iterations.\")\n", + "output[\"num_iterations\"] = result.nit\n", + "aqc_final_parameters = result.x\n", + "output[\"aqc_final_parameters\"] = list(aqc_final_parameters)\n", + "\n", + "# Construct an optimized circuit for initial portion of time evolution\n", + "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", + "\n", + "# Calculate fidelity after optimization\n", + "aqc_final_mps = tensornetwork_from_circuit(aqc_final_circuit, simulator_settings)\n", + "aqc_fidelity = abs(compute_overlap(aqc_final_mps, aqc_target_mps)) ** 2\n", + "print(\"Fidelity of AQC portion:\", aqc_fidelity)\n", + "output[\"aqc_fidelity\"] = aqc_fidelity\n", + "\n", + "# Construct final circuit, with remainder of time evolution\n", + "final_circuit = aqc_final_circuit.copy()\n", + "if remainder_evolution_time:\n", + " remainder_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=remainder_num_trotter_steps),\n", + " time=remainder_evolution_time,\n", + " )\n", + " final_circuit.compose(remainder_circuit, inplace=True)" + ] + }, + { + "cell_type": "markdown", + "id": "ae8e53a6-2106-4753-ad1a-ebc146cd44ab", + "metadata": {}, + "source": [ + "### Optimize the final circuit for execution\n", + "\n", + "After the AQC portion of the workflow, the `final_circuit` is [transpiled for the hardware](/docs/guides/transpile#instruction-set-architecture) as usual." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1db2749f-1285-48c6-8ec3-bc9f422686e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.backend(backend_name)\n", + "\n", + "# Transpile PUBs (circuits and observables) to match ISA\n", + "pass_manager = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", + "isa_circuit = pass_manager.run(final_circuit)\n", + "isa_observable = observable.apply_layout(isa_circuit.layout)\n", + "\n", + "isa_2qubit_depth = isa_circuit.depth(lambda x: x.operation.num_qubits == 2)\n", + "print(\"ISA circuit two-qubit depth:\", isa_2qubit_depth)\n", + "output[\"twoqubit_depth\"] = isa_2qubit_depth" + ] + }, + { + "cell_type": "markdown", + "id": "c41b9c0e-1f00-454c-acff-c194cc16e03e", + "metadata": {}, + "source": [ + "#### Exit early if using dry run mode\n", + "\n", + "If dry run mode has been selected, then the program is stopped before executing on hardware. This can be useful if, for example, you want first to inspect the two-qubit depth of the ISA circuit before deciding to execute on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e7c6c770-0453-4ad7-9a3e-20a47208768a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "# Exit now if dry run; don't execute on hardware\n", + "if dry_run:\n", + " import sys\n", + "\n", + " print(\"Exiting before hardware execution since `dry_run` is True.\")\n", + " save_result(output)\n", + " sys.exit(0)" + ] + }, + { + "cell_type": "markdown", + "id": "3a603817-abaf-4403-beea-cca838a59577", + "metadata": {}, + "source": [ + "#### Execute the circuit on hardware" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b7525014-a473-4d9d-b7cb-9c590f2364ae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "# ## Step 3: Execute quantum experiments on backend\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "\n", + "estimator = Estimator(backend, options=estimator_options)\n", + "\n", + "# Submit the underlying Estimator job. Note that this is not the\n", + "# actual function job.\n", + "job = estimator.run([(isa_circuit, isa_observable)])\n", + "print(\"Job ID:\", job.job_id())\n", + "output[\"job_id\"] = job.job_id()\n", + "\n", + "# Wait until job is complete\n", + "hw_results = job.result()\n", + "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", + "\n", + "# Save hardware results to serverless output dictionary\n", + "output[\"hw_results\"] = hw_results_dicts\n", + "\n", + "# Reorganize expectation values\n", + "hw_expvals = [pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts]\n", + "\n", + "# Save expectation values to Qiskit Serverless\n", + "print(\"Hardware expectation values\", hw_expvals)\n", + "output[\"hw_expvals\"] = hw_expvals[0]" + ] + }, + { + "cell_type": "markdown", + "id": "12df35c4-4f1c-48cb-a323-d0cf33422fab", + "metadata": {}, + "source": [ + "#### Save the output\n", + "\n", + "This function template returns the relevant domain-level output for this Hamiltonian simulation workflow (expectation values) in addition to important metadata generated along the way." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bfb8ab87-c42d-4993-92bd-8b38364dc443", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/template_hamiltonian_simulation.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/template_hamiltonian_simulation.py\n", + "\n", + "save_result(output)" + ] + }, + { + "cell_type": "markdown", + "id": "220f8bc8-4f04-41d3-86f1-15e9c2f45a56", + "metadata": {}, + "source": [ + "## Deploy the function to IBM Quantum Platform\n", + "The previous section created a program to be run remotely. The code in this section uploads that program to Qiskit Serverless.\n", + "\n", + "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key, which you can find on the [IBM Quantum Platform](https://quantum.cloud.ibm.com) dashboard, and upload the program.\n", + "\n", + "You can optionally use `save_account()` to save your credentials (see the [Set up your IBM Cloud account](/docs/guides/cloud-setup#cloud-save) guide). Note that this writes your credentials to the same file as [`QiskitRuntimeService.save_account()`](/docs/api/qiskit-ibm-runtime/qiskit-runtime-service#save_account)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "313bd03b-bf9b-4e6c-aa05-1fe8def3868d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitServerless, QiskitFunction\n", + "\n", + "# Authenticate to the remote cluster and submit the pattern for remote execution\n", + "serverless = QiskitServerless()" + ] + }, + { + "cell_type": "markdown", + "id": "e9c495d4-cdab-4329-b65d-d111029aee64", + "metadata": {}, + "source": [ + "This program has custom `pip` dependencies. Add them to a `dependencies` array when constructing the `QiskitFunction` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "ca386323-d92d-4c41-908b-1670324e1264", + "metadata": {}, + "outputs": [], + "source": [ + "template = QiskitFunction(\n", + " title=\"template_hamiltonian_simulation\",\n", + " entrypoint=\"template_hamiltonian_simulation.py\",\n", + " working_dir=\"./source_files/\",\n", + " dependencies=[\n", + " \"qiskit-addon-utils~=0.1.0\",\n", + " \"qiskit-addon-aqc-tensor[quimb-jax]~=0.1.2\",\n", + " \"mergedeep==1.3.4\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "80a2c6b5-1f1e-4e90-9b1f-75907caf1df3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "QiskitFunction(template_hamiltonian_simulation)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.upload(template)" + ] + }, + { + "cell_type": "markdown", + "id": "06677105-627a-4948-aac7-071f44327a0b", + "metadata": {}, + "source": [ + "Finally, check if the program successfully uploaded, use `serverless.list()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "11f14088-8eca-4a99-a291-3f37a61b0d26", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " QiskitFunction(template_hamiltonian_simulation),\n" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.list()" + ] + }, + { + "cell_type": "markdown", + "id": "385bda3e-73de-413c-8e45-cf7047303219", + "metadata": {}, + "source": [ + "## Run the function template remotely\n", + "\n", + "The function template has been uploaded, so you can run it remotely with Qiskit Serverless. First, load the template by name:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d4ff8dea-95ab-4cc9-92b9-e28e277e76f2", + "metadata": {}, + "outputs": [], + "source": [ + "template = serverless.load(\"template_hamiltonian_simulation\")" + ] + }, + { + "cell_type": "markdown", + "id": "57964962-87e9-4c0b-a717-cb620019960e", + "metadata": {}, + "source": [ + "Next, run the template with the domain-level inputs for Hamiltonian simulation. This example specifies a 50-qubit XXZ model with random couplings, and an initial state and observable." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "e5d1555c-9abb-4fef-b8a2-f8d1b723df01", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import chain\n", + "import numpy as np\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "L = 50\n", + "\n", + "# Generate the edge list for this spin-chain\n", + "edges = [(i, i + 1) for i in range(L - 1)]\n", + "# Generate an edge-coloring so we can make hw-efficient circuits\n", + "edges = edges[::2] + edges[1::2]\n", + "\n", + "# Generate random coefficients for our XXZ Hamiltonian\n", + "np.random.seed(0)\n", + "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", + "\n", + "hamiltonian = SparsePauliOp.from_sparse_list(\n", + " chain.from_iterable(\n", + " [\n", + " [\n", + " (\"XX\", (i, j), Js[i] / 2),\n", + " (\"YY\", (i, j), Js[i] / 2),\n", + " (\"ZZ\", (i, j), Js[i]),\n", + " ]\n", + " for i, j in edges\n", + " ]\n", + " ),\n", + " num_qubits=L,\n", + ")\n", + "observable = SparsePauliOp.from_sparse_list(\n", + " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "7ec5ab02-280c-4c04-8e63-e5354eb8ebb9", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "initial_state = QuantumCircuit(L)\n", + "for i in range(L):\n", + " if i % 2:\n", + " initial_state.x(i)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ac8d8fec-0290-4d6b-a18c-afd02acfa850", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "853b0edb-d63f-4629-be71-398b6dcf33cb\n" + ] + } + ], + "source": [ + "job = template.run(\n", + " dry_run=True,\n", + " initial_state=initial_state,\n", + " hamiltonian=hamiltonian,\n", + " observable=observable,\n", + " backend_name=\"ibm_fez\",\n", + " estimator_options={},\n", + " aqc_evolution_time=0.2,\n", + " aqc_ansatz_num_trotter_steps=1,\n", + " aqc_target_num_trotter_steps=32,\n", + " remainder_evolution_time=0.2,\n", + " remainder_num_trotter_steps=4,\n", + " aqc_max_iterations=300,\n", + ")\n", + "print(job.job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "2249cabc-6821-4d84-b4d8-e519c2d94a5c", + "metadata": {}, + "source": [ + "Check the status of the job:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "ed3b744d-cb00-43e0-907c-34dfebe2fa9b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'QUEUED'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "markdown", + "id": "83751f45-ab37-4037-bc77-d1aef91ed46c", + "metadata": {}, + "source": [ + "After the job is running, you can fetch logs created from the `print()` outputs. These can provide actionable information about the progress of the Hamiltonian simulation workflow. For example, the value of the objective function during the iterative component of AQC, or the two-qubit depth of the final ISA circuit intended for execution on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "3be62464-18b0-4598-b386-0ac0cc9cccb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No logs yet.\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "a8384509-2a9d-44e6-a729-40464ff52bea", + "metadata": {}, + "source": [ + "Block the rest of the program until a result is available. After the job is done, you can retrieve the results. These include the domain-level output of Hamiltonian simulation (expectation value) and useful metadata." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "fc678bcd-539d-4970-86e0-9a69f0a367ef", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'target_bond_dimension': 5,\n", + " 'num_aqc_parameters': 816,\n", + " 'aqc_starting_fidelity': 0.9914382555614002,\n", + " 'num_iterations': 72,\n", + " 'aqc_fidelity': 0.9998108844412502,\n", + " 'twoqubit_depth': 33}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = job.result()\n", + "\n", + "del result[\n", + " \"aqc_final_parameters\"\n", + "] # the list is too long to conveniently display here\n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "12b0ccfe-325a-4939-81e7-58d94557990d", + "metadata": {}, + "source": [ + "After the job completes, the entire logging output will be available." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "cb722373-cbfb-45a7-a2b5-d7a97e18ee6c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-12-17 14:50:15,580\tINFO job_manager.py:531 -- Runtime env is setting up.\n", + "estimator_options = {\n", + " \"resilience\": {\n", + " \"measure_mitigation\": true,\n", + " \"zne_mitigation\": true,\n", + " \"zne\": {\n", + " \"amplifier\": \"gate_folding\",\n", + " \"noise_factors\": [\n", + " 1,\n", + " 2,\n", + " 3\n", + " ],\n", + " \"extrapolated_noise_factors\": [\n", + " 0.0,\n", + " 0.1,\n", + " 0.2,\n", + " 0.30000000000000004,\n", + " 0.4,\n", + " 0.5,\n", + " 0.6000000000000001,\n", + " 0.7000000000000001,\n", + " 0.8,\n", + " 0.9,\n", + " 1.0,\n", + " 1.1,\n", + " 1.2000000000000002,\n", + " 1.3,\n", + " 1.4000000000000001,\n", + " 1.5,\n", + " 1.6,\n", + " 1.7000000000000002,\n", + " 1.8,\n", + " 1.9000000000000001,\n", + " 2.0,\n", + " 2.1,\n", + " 2.2,\n", + " 2.3000000000000003,\n", + " 2.4000000000000004,\n", + " 2.5,\n", + " 2.6,\n", + " 2.7,\n", + " 2.8000000000000003,\n", + " 2.9000000000000004,\n", + " 3.0\n", + " ],\n", + " \"extrapolator\": [\n", + " \"exponential\",\n", + " \"linear\",\n", + " \"fallback\"\n", + " ]\n", + " },\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": 512,\n", + " \"shots_per_randomization\": 512\n", + " }\n", + " },\n", + " \"twirling\": {\n", + " \"enable_gates\": true,\n", + " \"enable_measure\": true,\n", + " \"num_randomizations\": 300,\n", + " \"shots_per_randomization\": 100,\n", + " \"strategy\": \"active\"\n", + " }\n", + "}\n", + "Hamiltonian: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXX', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYY', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'XXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'YYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j, 0.55138169+0.j,\n", + " 0.55138169+0.j, 1.10276338+0.j, 0.4618274 +0.j, 0.4618274 +0.j,\n", + " 0.9236548 +0.j, 0.46879361+0.j, 0.46879361+0.j, 0.93758721+0.j,\n", + " 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j, 0.64586252+0.j,\n", + " 0.64586252+0.j, 1.29172504+0.j, 0.53402228+0.j, 0.53402228+0.j,\n", + " 1.06804456+0.j, 0.28551803+0.j, 0.28551803+0.j, 0.57103606+0.j,\n", + " 0.2601092 +0.j, 0.2601092 +0.j, 0.5202184 +0.j, 0.63907838+0.j,\n", + " 0.63907838+0.j, 1.27815675+0.j, 0.73930917+0.j, 0.73930917+0.j,\n", + " 1.47861834+0.j, 0.48073968+0.j, 0.48073968+0.j, 0.96147936+0.j,\n", + " 0.30913721+0.j, 0.30913721+0.j, 0.61827443+0.j, 0.32167664+0.j,\n", + " 0.32167664+0.j, 0.64335329+0.j, 0.51092416+0.j, 0.51092416+0.j,\n", + " 1.02184832+0.j, 0.38227781+0.j, 0.38227781+0.j, 0.76455561+0.j,\n", + " 0.47807517+0.j, 0.47807517+0.j, 0.95615033+0.j, 0.2593949 +0.j,\n", + " 0.2593949 +0.j, 0.5187898 +0.j, 0.55604786+0.j, 0.55604786+0.j,\n", + " 1.11209572+0.j, 0.72187404+0.j, 0.72187404+0.j, 1.44374808+0.j,\n", + " 0.42975395+0.j, 0.42975395+0.j, 0.8595079 +0.j, 0.5988156 +0.j,\n", + " 0.5988156 +0.j, 1.1976312 +0.j, 0.58338336+0.j, 0.58338336+0.j,\n", + " 1.16676672+0.j, 0.35519128+0.j, 0.35519128+0.j, 0.71038256+0.j,\n", + " 0.40771418+0.j, 0.40771418+0.j, 0.81542835+0.j, 0.60759468+0.j,\n", + " 0.60759468+0.j, 1.21518937+0.j, 0.52244159+0.j, 0.52244159+0.j,\n", + " 1.04488318+0.j, 0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j,\n", + " 0.6958865 +0.j, 0.6958865 +0.j, 1.391773 +0.j, 0.44172076+0.j,\n", + " 0.44172076+0.j, 0.88344152+0.j, 0.51444746+0.j, 0.51444746+0.j,\n", + " 1.02889492+0.j, 0.71279832+0.j, 0.71279832+0.j, 1.42559664+0.j,\n", + " 0.29356465+0.j, 0.29356465+0.j, 0.5871293 +0.j, 0.66630992+0.j,\n", + " 0.66630992+0.j, 1.33261985+0.j, 0.68500607+0.j, 0.68500607+0.j,\n", + " 1.37001215+0.j, 0.64957928+0.j, 0.64957928+0.j, 1.29915856+0.j,\n", + " 0.64026459+0.j, 0.64026459+0.j, 1.28052918+0.j, 0.56996051+0.j,\n", + " 0.56996051+0.j, 1.13992102+0.j, 0.72233446+0.j, 0.72233446+0.j,\n", + " 1.44466892+0.j, 0.45733097+0.j, 0.45733097+0.j, 0.91466194+0.j,\n", + " 0.63711684+0.j, 0.63711684+0.j, 1.27423369+0.j, 0.53421697+0.j,\n", + " 0.53421697+0.j, 1.06843395+0.j, 0.55881775+0.j, 0.55881775+0.j,\n", + " 1.1176355 +0.j, 0.558467 +0.j, 0.558467 +0.j, 1.116934 +0.j,\n", + " 0.59091015+0.j, 0.59091015+0.j, 1.1818203 +0.j, 0.46851598+0.j,\n", + " 0.46851598+0.j, 0.93703195+0.j, 0.28011274+0.j, 0.28011274+0.j,\n", + " 0.56022547+0.j, 0.58531893+0.j, 0.58531893+0.j, 1.17063787+0.j,\n", + " 0.31446315+0.j, 0.31446315+0.j, 0.6289263 +0.j])\n", + "Observable: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[1.+0.j])\n", + "Target MPS maximum bond dimension: 5\n", + "Number of AQC parameters: 816\n", + "Starting fidelity of AQC portion: 0.9914382555614002\n", + "2024-12-17 14:52:23.400028 Intermediate result: Fidelity 0.99764093\n", + "2024-12-17 14:52:23.429669 Intermediate result: Fidelity 0.99788003\n", + "2024-12-17 14:52:23.459674 Intermediate result: Fidelity 0.99795970\n", + "2024-12-17 14:52:23.489666 Intermediate result: Fidelity 0.99799067\n", + "2024-12-17 14:52:23.518545 Intermediate result: Fidelity 0.99803401\n", + "2024-12-17 14:52:23.546952 Intermediate result: Fidelity 0.99809821\n", + "2024-12-17 14:52:23.575271 Intermediate result: Fidelity 0.99824660\n", + "2024-12-17 14:52:23.604049 Intermediate result: Fidelity 0.99845326\n", + "2024-12-17 14:52:23.632709 Intermediate result: Fidelity 0.99870497\n", + "2024-12-17 14:52:23.660527 Intermediate result: Fidelity 0.99891442\n", + "2024-12-17 14:52:23.688273 Intermediate result: Fidelity 0.99904488\n", + "2024-12-17 14:52:23.716105 Intermediate result: Fidelity 0.99914438\n", + "2024-12-17 14:52:23.744336 Intermediate result: Fidelity 0.99922827\n", + "2024-12-17 14:52:23.773399 Intermediate result: Fidelity 0.99929071\n", + "2024-12-17 14:52:23.801482 Intermediate result: Fidelity 0.99932432\n", + "2024-12-17 14:52:23.830466 Intermediate result: Fidelity 0.99936460\n", + "2024-12-17 14:52:23.860738 Intermediate result: Fidelity 0.99938891\n", + "2024-12-17 14:52:23.889958 Intermediate result: Fidelity 0.99940607\n", + "2024-12-17 14:52:23.918703 Intermediate result: Fidelity 0.99941965\n", + "2024-12-17 14:52:23.949744 Intermediate result: Fidelity 0.99944337\n", + "2024-12-17 14:52:23.980871 Intermediate result: Fidelity 0.99946875\n", + "2024-12-17 14:52:24.012124 Intermediate result: Fidelity 0.99949009\n", + "2024-12-17 14:52:24.044359 Intermediate result: Fidelity 0.99952191\n", + "2024-12-17 14:52:24.075840 Intermediate result: Fidelity 0.99953669\n", + "2024-12-17 14:52:24.106303 Intermediate result: Fidelity 0.99955242\n", + "2024-12-17 14:52:24.139329 Intermediate result: Fidelity 0.99958412\n", + "2024-12-17 14:52:24.169725 Intermediate result: Fidelity 0.99960176\n", + "2024-12-17 14:52:24.198749 Intermediate result: Fidelity 0.99961606\n", + "2024-12-17 14:52:24.227874 Intermediate result: Fidelity 0.99963811\n", + "2024-12-17 14:52:24.256818 Intermediate result: Fidelity 0.99964383\n", + "2024-12-17 14:52:24.285889 Intermediate result: Fidelity 0.99964717\n", + "2024-12-17 14:52:24.315228 Intermediate result: Fidelity 0.99966064\n", + "2024-12-17 14:52:24.345322 Intermediate result: Fidelity 0.99966517\n", + "2024-12-17 14:52:24.374921 Intermediate result: Fidelity 0.99967089\n", + "2024-12-17 14:52:24.404309 Intermediate result: Fidelity 0.99968305\n", + "2024-12-17 14:52:24.432664 Intermediate result: Fidelity 0.99968889\n", + "2024-12-17 14:52:24.461639 Intermediate result: Fidelity 0.99969997\n", + "2024-12-17 14:52:24.491244 Intermediate result: Fidelity 0.99971666\n", + "2024-12-17 14:52:24.520354 Intermediate result: Fidelity 0.99972441\n", + "2024-12-17 14:52:24.549965 Intermediate result: Fidelity 0.99973561\n", + "2024-12-17 14:52:24.583464 Intermediate result: Fidelity 0.99973811\n", + "2024-12-17 14:52:24.617537 Intermediate result: Fidelity 0.99974074\n", + "2024-12-17 14:52:24.652247 Intermediate result: Fidelity 0.99974467\n", + "2024-12-17 14:52:24.686831 Intermediate result: Fidelity 0.99974991\n", + "2024-12-17 14:52:24.725476 Intermediate result: Fidelity 0.99975230\n", + "2024-12-17 14:52:24.764637 Intermediate result: Fidelity 0.99975373\n", + "2024-12-17 14:52:24.802499 Intermediate result: Fidelity 0.99975552\n", + "2024-12-17 14:52:24.839960 Intermediate result: Fidelity 0.99975885\n", + "2024-12-17 14:52:24.877472 Intermediate result: Fidelity 0.99976469\n", + "2024-12-17 14:52:24.916233 Intermediate result: Fidelity 0.99976517\n", + "2024-12-17 14:52:24.993750 Intermediate result: Fidelity 0.99976875\n", + "2024-12-17 14:52:25.034953 Intermediate result: Fidelity 0.99976887\n", + "2024-12-17 14:52:25.076197 Intermediate result: Fidelity 0.99977244\n", + "2024-12-17 14:52:25.112340 Intermediate result: Fidelity 0.99977638\n", + "2024-12-17 14:52:25.149947 Intermediate result: Fidelity 0.99977828\n", + "2024-12-17 14:52:25.190049 Intermediate result: Fidelity 0.99978174\n", + "2024-12-17 14:52:25.310903 Intermediate result: Fidelity 0.99978222\n", + "2024-12-17 14:52:25.347512 Intermediate result: Fidelity 0.99978508\n", + "2024-12-17 14:52:25.385201 Intermediate result: Fidelity 0.99978543\n", + "2024-12-17 14:52:25.457436 Intermediate result: Fidelity 0.99978770\n", + "2024-12-17 14:52:25.497133 Intermediate result: Fidelity 0.99978818\n", + "2024-12-17 14:52:25.541179 Intermediate result: Fidelity 0.99978913\n", + "2024-12-17 14:52:25.584791 Intermediate result: Fidelity 0.99978937\n", + "2024-12-17 14:52:25.621484 Intermediate result: Fidelity 0.99979068\n", + "2024-12-17 14:52:25.655847 Intermediate result: Fidelity 0.99979211\n", + "2024-12-17 14:52:25.691710 Intermediate result: Fidelity 0.99979700\n", + "2024-12-17 14:52:25.767711 Intermediate result: Fidelity 0.99979759\n", + "2024-12-17 14:52:25.804517 Intermediate result: Fidelity 0.99979807\n", + "2024-12-17 14:52:25.839394 Intermediate result: Fidelity 0.99980236\n", + "2024-12-17 14:52:25.874438 Intermediate result: Fidelity 0.99980296\n", + "2024-12-17 14:52:25.909900 Intermediate result: Fidelity 0.99980320\n", + "2024-12-17 14:52:26.713044 Intermediate result: Fidelity 0.99980320\n", + "Done after 72 iterations.\n", + "Fidelity of AQC portion: 0.9998108844412502\n", + "ISA circuit two-qubit depth: 33\n", + "Exiting before hardware execution since `dry_run` is True.\n", + "\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "196d6261-e26b-4057-ae55-19f003fdc10a", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "For a deeper dive into the AQC-Tensor Qiskit addon, check out the [Improved Trotterized Time Evolution with Approximate Quantum Compilation](/docs/tutorials/approximate-quantum-compilation-for-time-evolution) tutorial or the [qiskit-addon-aqc-tensor repository](https://github.com/Qiskit/qiskit-addon-aqc-tensor).\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20502fe4-7940-40fa-a978-64cc3ff6c1b1", + "metadata": { + "tags": [ + "id-full-source" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting ./source_files/template_hamiltonian_simulation_full.py\n" + ] + } + ], + "source": [ + "%%writefile ./source_files/template_hamiltonian_simulation_full.py\n", + "\n", + "from qiskit import QuantumCircuit\n", + "from qiskit_serverless import get_arguments, save_result\n", + "\n", + "\n", + "# Extract parameters from arguments\n", + "#\n", + "# Do this at the top of the program so it fails early if any required arguments are missing or\n", + "# invalid.\n", + "\n", + "arguments = get_arguments()\n", + "\n", + "dry_run = arguments.get(\"dry_run\", False)\n", + "backend_name = arguments[\"backend_name\"]\n", + "\n", + "aqc_evolution_time = arguments[\"aqc_evolution_time\"]\n", + "aqc_ansatz_num_trotter_steps = arguments[\"aqc_ansatz_num_trotter_steps\"]\n", + "aqc_target_num_trotter_steps = arguments[\"aqc_target_num_trotter_steps\"]\n", + "\n", + "remainder_evolution_time = arguments[\"remainder_evolution_time\"]\n", + "remainder_num_trotter_steps = arguments[\"remainder_num_trotter_steps\"]\n", + "\n", + "# Stop if this fidelity is achieved\n", + "aqc_stopping_fidelity = arguments.get(\"aqc_stopping_fidelity\", 1.0)\n", + "# Stop after this number of iterations, even if stopping fidelity is not achieved\n", + "aqc_max_iterations = arguments.get(\"aqc_max_iterations\", 500)\n", + "\n", + "hamiltonian = arguments[\"hamiltonian\"]\n", + "observable = arguments[\"observable\"]\n", + "initial_state = arguments.get(\"initial_state\", QuantumCircuit(hamiltonian.num_qubits))\n", + "\n", + "import numpy as np\n", + "import json\n", + "from mergedeep import merge\n", + "\n", + "\n", + "# Configure `EstimatorOptions`, to control the parameters of the hardware experiment\n", + "#\n", + "# Set default options\n", + "estimator_default_options = {\n", + " \"resilience\": {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\n", + " \"amplifier\": \"gate_folding\",\n", + " \"noise_factors\": [1, 2, 3],\n", + " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", + " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", + " },\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": 512,\n", + " \"shots_per_randomization\": 512,\n", + " },\n", + " },\n", + " \"twirling\": {\n", + " \"enable_gates\": True,\n", + " \"enable_measure\": True,\n", + " \"num_randomizations\": 300,\n", + " \"shots_per_randomization\": 100,\n", + " \"strategy\": \"active\",\n", + " },\n", + "}\n", + "# Merge with user-provided options\n", + "estimator_options = merge(\n", + " arguments.get(\"estimator_options\", {}), estimator_default_options\n", + ")\n", + "\n", + "print(\"estimator_options =\", json.dumps(estimator_options, indent=4))\n", + "\n", + "# Perform parameter validation\n", + "\n", + "if not 0.0 < aqc_stopping_fidelity <= 1.0:\n", + " raise ValueError(\n", + " f\"Invalid stopping fidelity: {aqc_stopping_fidelity}. It must be a positive float no greater than 1.\"\n", + " )\n", + "\n", + "output = {}\n", + "\n", + "import os\n", + "os.environ[\"NUMBA_CACHE_DIR\"] = \"/data\"\n", + "\n", + "import datetime\n", + "import quimb.tensor\n", + "from scipy.optimize import OptimizeResult, minimize\n", + "from qiskit.synthesis import SuzukiTrotter\n", + "from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit\n", + "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", + " generate_ansatz_from_circuit,\n", + " AnsatzBlock,\n", + ")\n", + "from qiskit_addon_aqc_tensor.simulation import (\n", + " tensornetwork_from_circuit,\n", + " compute_overlap,\n", + ")\n", + "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", + "from qiskit_addon_aqc_tensor.objective import OneMinusFidelity\n", + "\n", + "print(\"Hamiltonian:\", hamiltonian)\n", + "print(\"Observable:\", observable)\n", + "simulator_settings = QuimbSimulator(quimb.tensor.CircuitMPS, autodiff_backend=\"jax\")\n", + "\n", + "# Construct the AQC target circuit\n", + "aqc_target_circuit = initial_state.copy()\n", + "if aqc_evolution_time:\n", + " aqc_target_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + " )\n", + "\n", + "# Construct matrix-product state representation of the AQC target state\n", + "aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)\n", + "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", + "output[\"target_bond_dimension\"] = aqc_target_mps.psi.max_bond()\n", + "\n", + "# Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", + "aqc_good_circuit = initial_state.copy()\n", + "if aqc_evolution_time:\n", + " aqc_good_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + " )\n", + "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(aqc_good_circuit)\n", + "print(\"Number of AQC parameters:\", len(aqc_initial_parameters))\n", + "output[\"num_aqc_parameters\"] = len(aqc_initial_parameters)\n", + "\n", + "# Calculate the fidelity of ansatz circuit vs. the target state, before optimization\n", + "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", + "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", + "print(\"Starting fidelity of AQC portion:\", starting_fidelity)\n", + "output[\"aqc_starting_fidelity\"] = starting_fidelity\n", + "\n", + "# Optimize the ansatz parameters by using MPS calculations\n", + "def callback(intermediate_result: OptimizeResult):\n", + " fidelity = 1 - intermediate_result.fun\n", + " print(f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\")\n", + " if intermediate_result.fun < stopping_point:\n", + " raise StopIteration\n", + "\n", + "\n", + "objective = OneMinusFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)\n", + "stopping_point = 1.0 - aqc_stopping_fidelity\n", + "\n", + "result = minimize(\n", + " objective,\n", + " aqc_initial_parameters,\n", + " method=\"L-BFGS-B\",\n", + " jac=True,\n", + " options={\"maxiter\": aqc_max_iterations},\n", + " callback=callback,\n", + ")\n", + "if result.status not in (\n", + " 0,\n", + " 1,\n", + " 99,\n", + "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", + " raise RuntimeError(\n", + " f\"Optimization failed: {result.message} (status={result.status})\"\n", + " )\n", + "print(f\"Done after {result.nit} iterations.\")\n", + "output[\"num_iterations\"] = result.nit\n", + "aqc_final_parameters = result.x\n", + "output[\"aqc_final_parameters\"] = list(aqc_final_parameters)\n", + "\n", + "# Construct an optimized circuit for initial portion of time evolution\n", + "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", + "\n", + "# Calculate fidelity after optimization\n", + "aqc_final_mps = tensornetwork_from_circuit(aqc_final_circuit, simulator_settings)\n", + "aqc_fidelity = abs(compute_overlap(aqc_final_mps, aqc_target_mps)) ** 2\n", + "print(\"Fidelity of AQC portion:\", aqc_fidelity)\n", + "output[\"aqc_fidelity\"] = aqc_fidelity\n", + "\n", + "# Construct final circuit, with remainder of time evolution\n", + "final_circuit = aqc_final_circuit.copy()\n", + "if remainder_evolution_time:\n", + " remainder_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=remainder_num_trotter_steps),\n", + " time=remainder_evolution_time,\n", + " )\n", + " final_circuit.compose(remainder_circuit, inplace=True)\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.backend(backend_name)\n", + "\n", + "# Transpile PUBs (circuits and observables) to match ISA\n", + "pass_manager = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", + "isa_circuit = pass_manager.run(final_circuit)\n", + "isa_observable = observable.apply_layout(isa_circuit.layout)\n", + "\n", + "isa_2qubit_depth = isa_circuit.depth(lambda x: x.operation.num_qubits == 2)\n", + "print(\"ISA circuit two-qubit depth:\", isa_2qubit_depth)\n", + "output[\"twoqubit_depth\"] = isa_2qubit_depth\n", + "\n", + "# Exit now if dry run; don't execute on hardware\n", + "if dry_run:\n", + " import sys\n", + "\n", + " print(\"Exiting before hardware execution since `dry_run` is True.\")\n", + " save_result(output)\n", + " sys.exit(0)\n", + "\n", + "# ## Step 3: Execute quantum experiments on backend\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "\n", + "estimator = Estimator(backend, options=estimator_options)\n", + "\n", + "# Submit the underlying Estimator job. Note that this is not the\n", + "# actual function job.\n", + "job = estimator.run([(isa_circuit, isa_observable)])\n", + "print(\"Job ID:\", job.job_id())\n", + "output[\"job_id\"] = job.job_id()\n", + "\n", + "# Wait until job is complete\n", + "hw_results = job.result()\n", + "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", + "\n", + "# Save hardware results to serverless output dictionary\n", + "output[\"hw_results\"] = hw_results_dicts\n", + "\n", + "# Reorganize expectation values\n", + "hw_expvals = [pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts]\n", + "\n", + "# Save expectation values to Qiskit Serverless\n", + "output[\"hw_expvals\"] = hw_expvals[0]\n", + "\n", + "save_result(output)" + ] + }, + { + "cell_type": "markdown", + "id": "d7f1776a-a8f6-43a3-85b7-33975c4eeec0", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "Here is the entire source of `./source_files/template_hamiltonian_simulation.py` as one code block.\n", + "\n", + "\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "0a5ab26b-baea-48d0-979e-485df3118ac6", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users. It verifies both source listings are identical then deletes the\n", + "# working folder we created\n", + "import shutil\n", + "\n", + "with open(\"./source_files/template_hamiltonian_simulation.py\") as f1:\n", + " with open(\"./source_files/template_hamiltonian_simulation_full.py\") as f2:\n", + " assert f1.read() == f2.read()\n", + "\n", + "shutil.rmtree(\"./source_files/\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/guides/ibm-circuit-function.ipynb b/docs/guides/ibm-circuit-function.ipynb index e75921903c3..7627db1f319 100644 --- a/docs/guides/ibm-circuit-function.ipynb +++ b/docs/guides/ibm-circuit-function.ipynb @@ -1,362 +1,363 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "dde95705", - "metadata": {}, - "source": [ - "---\n", - "title: IBM Circuit function\n", - "description: Explore how to use the IBM Circuit function, with AI-driven transpilation and advanced error mitigation (TREX, ZNE, PEA)\n", - "---\n", - "\n", - "\n", - "# IBM Circuit function\n", - "\n", - "*See the [API reference](/docs/api/functions/ibm-circuit-function)*" - ] - }, - { - "cell_type": "markdown", - "id": "904dadeb", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [] - }, - { - "cell_type": "markdown", - "id": "64b16815", - "metadata": {}, - "source": [ - "\n", - "* Qiskit Functions are an experimental feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (via IBM Quantum Platform API) Plan users. They are in preview release status and subject to change.\n", - "\n", - "\n", - "## Overview\n", - "\n", - "The IBM® Circuit function takes [abstract PUBs](/docs/guides/primitive-input-output#pubs) as inputs, and returns mitigated expectation values as outputs. This circuit function includes an automated and customized pipeline to enable researchers to focus on algorithm and application discovery." - ] - }, - { - "cell_type": "markdown", - "id": "5f761442", - "metadata": {}, - "source": [ - "## Description\n", - "\n", - "After submitting your PUB, your abstract circuits and observables are automatically transpiled, executed on hardware, and post-processed to return mitigated expectation values. To do so, this combines the following tools:\n", - "\n", - "- [Qiskit Transpiler Service](/docs/guides/qiskit-transpiler-service), including auto-selection of AI-driven and heuristic transpilation passes to translate your abstract circuits to hardware-optimized ISA circuits\n", - "- [Error suppression and mitigation required for utility-scale computation](/docs/guides/error-mitigation-and-suppression-techniques), including measurement and gate twirling, dynamical decoupling, Twirled Readout Error eXtinction (TREX), Zero-Noise Extrapolation (ZNE), and Probabilistic Error Amplification (PEA)\n", - "- [Qiskit Runtime Estimator](/docs/guides/get-started-with-estimator), to execute ISA PUBs on hardware and return mitigated expectation values\n", - "\n", - "![IBM Circuit function](/docs/images/guides/ibm-circuit-function/ibm-circuit-function.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "73390a19", - "metadata": {}, - "source": [ - "## Get started" - ] - }, - { - "cell_type": "markdown", - "id": "f46c531c", - "metadata": {}, - "source": [ - "Authenticate using your [API key](http://quantum.cloud.ibm.com/) and select the Qiskit Function as follows. (This snippet assumes you've already [saved your account](/docs/guides/functions#install-qiskit-functions-catalog-client) to your local environment.)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "95a715d2", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n", - "\n", - "catalog = QiskitFunctionsCatalog(channel=\"ibm_quantum_platform\")\n", - "\n", - "function = catalog.load(\"ibm/circuit-function\")" - ] - }, - { - "cell_type": "markdown", - "id": "e8837f5f", - "metadata": {}, - "source": [ - "## Examples\n", - "\n", - "To get started, try this basic example:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "d56e1440", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.random import random_circuit\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# You can skip this step if you have a target backend, e.g.\n", - "# backend_name = \"ibm_brisbane\"\n", - "# You'll need to specify the credentials when initializing QiskitRuntimeService, if they were not previously saved.\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "circuit = random_circuit(num_qubits=2, depth=2, seed=42)\n", - "observable = \"Z\" * circuit.num_qubits\n", - "pubs = [(circuit, observable)]\n", - "\n", - "job = function.run(\n", - " # Use `backend_name=backend_name` if you didn't initialize a backend object\n", - " backend_name=backend.name,\n", - " pubs=pubs,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "03998691", - "metadata": {}, - "source": [ - "Check your Qiskit Function workload's [status](/docs/guides/functions#check-job-status) or return [results](/docs/guides/functions#retrieve-results) as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "856fe992", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "QUEUED\n" - ] - } - ], - "source": [ - "print(job.status())\n", - "result = job.result()" - ] - }, - { - "cell_type": "markdown", - "id": "eda36356", - "metadata": {}, - "source": [ - "The results have the same format as an [Estimator result](/docs/guides/estimator-input-output):" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "765f3207", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The result of the submitted job had 1 PUB\n", - "\n", - "The associated PubResult of this job has the following DataBins:\n", - " DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray())\n", - "\n", - "And this DataBin has attributes: dict_keys(['evs', 'stds', 'ensemble_standard_error'])\n", - "The expectation values measured from this PUB are: \n", - "1.02116704805492\n" - ] - } - ], - "source": [ - "print(f\"The result of the submitted job had {len(result)} PUB\\n\")\n", - "print(\n", - " f\"The associated PubResult of this job has the following DataBins:\\n {result[0].data}\\n\"\n", - ")\n", - "print(f\"And this DataBin has attributes: {result[0].data.keys()}\")\n", - "print(\n", - " f\"The expectation values measured from this PUB are: \\n{result[0].data.evs}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a1072bd6", - "metadata": {}, - "source": [ - "### Mitigation level examples\n", - "\n", - "The following example demonstrates setting the mitigation level:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "13443be4", - "metadata": {}, - "outputs": [], - "source": [ - "options = {\"mitigation_level\": 2}\n", - "\n", - "job = function.run(backend_name=backend.name, pubs=pubs, options=options)" - ] - }, - { - "cell_type": "markdown", - "id": "c823d7b3", - "metadata": {}, - "source": [ - "In the following example, setting the mitigation level to 1 initially turns off ZNE mitigation, but setting `zne_mitigation` to `True` overrides the relevant setup from `mitigation_level`." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "b2632bd9", - "metadata": {}, - "outputs": [], - "source": [ - "options = {\"mitigation_level\": 1, \"resilience\": {\"zne_mitigation\": True}}" - ] - }, - { - "cell_type": "markdown", - "id": "75d7cbba", - "metadata": {}, - "source": [ - "### Output example\n", - "\n", - "The following code snippet describes the `PrimitiveResult` (and associated `PubResult`) format." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "6e3f86aa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The result of the submitted job had 1 PUB\n", - "The expectation values measured from this PUB are: \n", - "1.02116704805492\n", - "And the associated metadata is: \n", - "{'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32}\n" - ] - } - ], - "source": [ - "print(f\"The result of the submitted job had {len(result)} PUB\")\n", - "print(\n", - " f\"The expectation values measured from this PUB are: \\n{result[0].data.evs}\"\n", - ")\n", - "print(f\"And the associated metadata is: \\n{result[0].metadata}\")" - ] - }, - { - "cell_type": "markdown", - "id": "02295f5d", - "metadata": {}, - "source": [ - "### Fetch error messages\n", - "\n", - "If your workload status is `ERROR`, use `job.result()` to fetch the error message to help debug as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "4070e592", - "metadata": { - "tags": [ - "raises-exception" - ] - }, - "outputs": [ - { - "ename": "QiskitServerlessException", - "evalue": "\"Traceback (most recent call last):\\n File \\\"/runner/runner.py\\\", line 10, in run\\n func = CircuitFunction(**arguments)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/runner/circuit_function/circuit_function.py\\\", line 87, in __init__\\n self._backend = self._service.backend(\\n ^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 754, in backend\\n backends = self.backends(name, instance=instance, use_fractional_gates=use_fractional_gates)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 497, in backends\\n raise QiskitBackendNotFoundError(\\\"No backend matches the criteria.\\\")\\nqiskit.providers.exceptions.QiskitBackendNotFoundError: 'No backend matches the criteria.'\\n\"", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mQiskitServerlessException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[8], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m job \u001b[38;5;241m=\u001b[39m function\u001b[38;5;241m.\u001b[39mrun(\n\u001b[1;32m 2\u001b[0m backend_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbad_backend_name\u001b[39m\u001b[38;5;124m\"\u001b[39m, pubs\u001b[38;5;241m=\u001b[39mpubs, options\u001b[38;5;241m=\u001b[39moptions\n\u001b[1;32m 3\u001b[0m )\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m)\n", - "File \u001b[0;32m~/work/documentation/documentation/.tox/py311/lib/python3.11/site-packages/qiskit_serverless/core/job.py:189\u001b[0m, in \u001b[0;36mJob.result\u001b[0;34m(self, wait, cadence, verbose, maxwait)\u001b[0m\n\u001b[1;32m 186\u001b[0m results \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_job_service\u001b[38;5;241m.\u001b[39mresult(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id)\n\u001b[1;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstatus() \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mERROR\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m QiskitServerlessException(results)\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(results, \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m 192\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", - "\u001b[0;31mQiskitServerlessException\u001b[0m: \"Traceback (most recent call last):\\n File \\\"/runner/runner.py\\\", line 10, in run\\n func = CircuitFunction(**arguments)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/runner/circuit_function/circuit_function.py\\\", line 87, in __init__\\n self._backend = self._service.backend(\\n ^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 754, in backend\\n backends = self.backends(name, instance=instance, use_fractional_gates=use_fractional_gates)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 497, in backends\\n raise QiskitBackendNotFoundError(\\\"No backend matches the criteria.\\\")\\nqiskit.providers.exceptions.QiskitBackendNotFoundError: 'No backend matches the criteria.'\\n\"" - ] - } - ], - "source": [ - "job = function.run(\n", - " backend_name=\"bad_backend_name\", pubs=pubs, options=options\n", - ")\n", - "\n", - "print(job.result())" - ] - }, - { - "cell_type": "markdown", - "id": "32db1269", - "metadata": {}, - "source": [ - "## Get support\n", - "\n", - "Reach out to [IBM Quantum support](/docs/guides/support), and include the following information:\n", - "\n", - "- Qiskit Function Job ID (`qiskit-ibm-catalog`), `job.job_id`\n", - "- A detailed description of the issue\n", - "- Any relevant error messages or codes\n", - "- Steps to reproduce the issue" - ] - }, - { - "cell_type": "markdown", - "id": "0439eef4", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "- Try the [Error mitigation with the IBM Circuit function](/docs/tutorials/error-mitigation-with-qiskit-functions) tutorial.\n", - "- Visit the [API reference](/docs/api/functions/ibm-circuit-function) for this Qiskit Function.\n", - "\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dde95705", + "metadata": {}, + "source": [ + "---\n", + "title: IBM Circuit function\n", + "description: Explore how to use the IBM Circuit function, with AI-driven transpilation and advanced error mitigation (TREX, ZNE, PEA)\n", + "---\n", + "\n", + "\n", + "# IBM Circuit function\n", + "\n", + "*See the [API reference](/docs/api/functions/ibm-circuit-function)*" + ] + }, + { + "cell_type": "markdown", + "id": "904dadeb", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [] + }, + { + "cell_type": "markdown", + "id": "64b16815", + "metadata": {}, + "source": [ + "\n", + "* Qiskit Functions are an experimental feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (via IBM Quantum Platform API) Plan users. They are in preview release status and subject to change.\n", + "\n", + "\n", + "## Overview\n", + "\n", + "The IBM® Circuit function takes [abstract PUBs](/docs/guides/primitive-input-output#pubs) as inputs, and returns mitigated expectation values as outputs. This circuit function includes an automated and customized pipeline to enable researchers to focus on algorithm and application discovery." + ] + }, + { + "cell_type": "markdown", + "id": "5f761442", + "metadata": {}, + "source": [ + "## Description\n", + "\n", + "After submitting your PUB, your abstract circuits and observables are automatically transpiled, executed on hardware, and post-processed to return mitigated expectation values. To do so, this combines the following tools:\n", + "\n", + "- [Qiskit Transpiler Service](/docs/guides/qiskit-transpiler-service), including auto-selection of AI-driven and heuristic transpilation passes to translate your abstract circuits to hardware-optimized ISA circuits\n", + "- [Error suppression and mitigation required for utility-scale computation](/docs/guides/error-mitigation-and-suppression-techniques), including measurement and gate twirling, dynamical decoupling, Twirled Readout Error eXtinction (TREX), Zero-Noise Extrapolation (ZNE), and Probabilistic Error Amplification (PEA)\n", + "- [Qiskit Runtime Estimator](/docs/guides/get-started-with-estimator), to execute ISA PUBs on hardware and return mitigated expectation values\n", + "\n", + "![IBM Circuit function](/docs/images/guides/ibm-circuit-function/ibm-circuit-function.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "73390a19", + "metadata": {}, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "id": "f46c531c", + "metadata": {}, + "source": [ + "Authenticate using your [API key](http://quantum.cloud.ibm.com/) and select the Qiskit Function as follows. (This snippet assumes you've already [saved your account](/docs/guides/functions#install-qiskit-functions-catalog-client) to your local environment.)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "95a715d2", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n", + "\n", + "catalog = QiskitFunctionsCatalog(channel=\"ibm_quantum_platform\")\n", + "\n", + "function = catalog.load(\"ibm/circuit-function\")" + ] + }, + { + "cell_type": "markdown", + "id": "e8837f5f", + "metadata": {}, + "source": [ + "## Examples\n", + "\n", + "To get started, try this basic example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d56e1440", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.random import random_circuit\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# You can skip this step if you have a target backend, e.g.\n", + "# backend_name = \"ibm_brisbane\"\n", + "# You'll need to specify the credentials when initializing QiskitRuntimeService, if they were not\n", + "# previously saved.\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "circuit = random_circuit(num_qubits=2, depth=2, seed=42)\n", + "observable = \"Z\" * circuit.num_qubits\n", + "pubs = [(circuit, observable)]\n", + "\n", + "job = function.run(\n", + " # Use `backend_name=backend_name` if you didn't initialize a backend object\n", + " backend_name=backend.name,\n", + " pubs=pubs,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "03998691", + "metadata": {}, + "source": [ + "Check your Qiskit Function workload's [status](/docs/guides/functions#check-job-status) or return [results](/docs/guides/functions#retrieve-results) as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "856fe992", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QUEUED\n" + ] + } + ], + "source": [ + "print(job.status())\n", + "result = job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "eda36356", + "metadata": {}, + "source": [ + "The results have the same format as an [Estimator result](/docs/guides/estimator-input-output):" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "765f3207", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The result of the submitted job had 1 PUB\n", + "\n", + "The associated PubResult of this job has the following DataBins:\n", + " DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray())\n", + "\n", + "And this DataBin has attributes: dict_keys(['evs', 'stds', 'ensemble_standard_error'])\n", + "The expectation values measured from this PUB are: \n", + "1.02116704805492\n" + ] + } + ], + "source": [ + "print(f\"The result of the submitted job had {len(result)} PUB\\n\")\n", + "print(\n", + " f\"The associated PubResult of this job has the following DataBins:\\n {result[0].data}\\n\"\n", + ")\n", + "print(f\"And this DataBin has attributes: {result[0].data.keys()}\")\n", + "print(\n", + " f\"The expectation values measured from this PUB are: \\n{result[0].data.evs}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a1072bd6", + "metadata": {}, + "source": [ + "### Mitigation level examples\n", + "\n", + "The following example demonstrates setting the mitigation level:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "13443be4", + "metadata": {}, + "outputs": [], + "source": [ + "options = {\"mitigation_level\": 2}\n", + "\n", + "job = function.run(backend_name=backend.name, pubs=pubs, options=options)" + ] + }, + { + "cell_type": "markdown", + "id": "c823d7b3", + "metadata": {}, + "source": [ + "In the following example, setting the mitigation level to 1 initially turns off ZNE mitigation, but setting `zne_mitigation` to `True` overrides the relevant setup from `mitigation_level`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b2632bd9", + "metadata": {}, + "outputs": [], + "source": [ + "options = {\"mitigation_level\": 1, \"resilience\": {\"zne_mitigation\": True}}" + ] + }, + { + "cell_type": "markdown", + "id": "75d7cbba", + "metadata": {}, + "source": [ + "### Output example\n", + "\n", + "The following code snippet describes the `PrimitiveResult` (and associated `PubResult`) format." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6e3f86aa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The result of the submitted job had 1 PUB\n", + "The expectation values measured from this PUB are: \n", + "1.02116704805492\n", + "And the associated metadata is: \n", + "{'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32}\n" + ] + } + ], + "source": [ + "print(f\"The result of the submitted job had {len(result)} PUB\")\n", + "print(\n", + " f\"The expectation values measured from this PUB are: \\n{result[0].data.evs}\"\n", + ")\n", + "print(f\"And the associated metadata is: \\n{result[0].metadata}\")" + ] + }, + { + "cell_type": "markdown", + "id": "02295f5d", + "metadata": {}, + "source": [ + "### Fetch error messages\n", + "\n", + "If your workload status is `ERROR`, use `job.result()` to fetch the error message to help debug as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "4070e592", + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "QiskitServerlessException", + "evalue": "\"Traceback (most recent call last):\\n File \\\"/runner/runner.py\\\", line 10, in run\\n func = CircuitFunction(**arguments)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/runner/circuit_function/circuit_function.py\\\", line 87, in __init__\\n self._backend = self._service.backend(\\n ^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 754, in backend\\n backends = self.backends(name, instance=instance, use_fractional_gates=use_fractional_gates)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 497, in backends\\n raise QiskitBackendNotFoundError(\\\"No backend matches the criteria.\\\")\\nqiskit.providers.exceptions.QiskitBackendNotFoundError: 'No backend matches the criteria.'\\n\"", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mQiskitServerlessException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[8], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m job \u001b[38;5;241m=\u001b[39m function\u001b[38;5;241m.\u001b[39mrun(\n\u001b[1;32m 2\u001b[0m backend_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbad_backend_name\u001b[39m\u001b[38;5;124m\"\u001b[39m, pubs\u001b[38;5;241m=\u001b[39mpubs, options\u001b[38;5;241m=\u001b[39moptions\n\u001b[1;32m 3\u001b[0m )\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mjob\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m)\n", + "File \u001b[0;32m~/work/documentation/documentation/.tox/py311/lib/python3.11/site-packages/qiskit_serverless/core/job.py:189\u001b[0m, in \u001b[0;36mJob.result\u001b[0;34m(self, wait, cadence, verbose, maxwait)\u001b[0m\n\u001b[1;32m 186\u001b[0m results \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_job_service\u001b[38;5;241m.\u001b[39mresult(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mjob_id)\n\u001b[1;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstatus() \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mERROR\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m QiskitServerlessException(results)\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(results, \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m 192\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", + "\u001b[0;31mQiskitServerlessException\u001b[0m: \"Traceback (most recent call last):\\n File \\\"/runner/runner.py\\\", line 10, in run\\n func = CircuitFunction(**arguments)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/runner/circuit_function/circuit_function.py\\\", line 87, in __init__\\n self._backend = self._service.backend(\\n ^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 754, in backend\\n backends = self.backends(name, instance=instance, use_fractional_gates=use_fractional_gates)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n File \\\"/usr/local/lib/python3.11/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py\\\", line 497, in backends\\n raise QiskitBackendNotFoundError(\\\"No backend matches the criteria.\\\")\\nqiskit.providers.exceptions.QiskitBackendNotFoundError: 'No backend matches the criteria.'\\n\"" + ] + } + ], + "source": [ + "job = function.run(\n", + " backend_name=\"bad_backend_name\", pubs=pubs, options=options\n", + ")\n", + "\n", + "print(job.result())" + ] + }, + { + "cell_type": "markdown", + "id": "32db1269", + "metadata": {}, + "source": [ + "## Get support\n", + "\n", + "Reach out to [IBM Quantum support](/docs/guides/support), and include the following information:\n", + "\n", + "- Qiskit Function Job ID (`qiskit-ibm-catalog`), `job.job_id`\n", + "- A detailed description of the issue\n", + "- Any relevant error messages or codes\n", + "- Steps to reproduce the issue" + ] + }, + { + "cell_type": "markdown", + "id": "0439eef4", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- Try the [Error mitigation with the IBM Circuit function](/docs/tutorials/error-mitigation-with-qiskit-functions) tutorial.\n", + "- Visit the [API reference](/docs/api/functions/ibm-circuit-function) for this Qiskit Function.\n", + "\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/guides/sampler-options.ipynb b/docs/guides/sampler-options.ipynb index 8551f196a02..d3b8f3f775f 100644 --- a/docs/guides/sampler-options.ipynb +++ b/docs/guides/sampler-options.ipynb @@ -1,561 +1,562 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "bed19311-f71d-4d8c-9a05-1b68fa72df29", - "metadata": {}, - "source": [ - "---\n", - "title: Specify Sampler options\n", - "description: Specify options when building with the Sampler primitive.\n", - "---\n", - "\n", - "\n", - "# Specify Sampler options" - ] - }, - { - "cell_type": "markdown", - "id": "3adc856a-0fcd-47e4-94d9-22b90bddc24f", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [ - "{/*\n", - " DO NOT EDIT THIS CELL!!!\n", - " This cell's content is generated automatically by a script. Anything you add\n", - " here will be removed next time the notebook is run. To add new content, create\n", - " a new cell before or after this one.\n", - "*/}\n", - "\n", - "\n", - "\n", - "\n", - "The code on this page was developed using the following requirements.\n", - "We recommend using these versions or newer.\n", - "\n", - "```\n", - "qiskit[all]~=2.4.0\n", - "qiskit-ibm-runtime~=0.46.1\n", - "```\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "56db929d-8b84-49b2-a98b-55d33f91ea30", - "metadata": {}, - "source": [ - "You can use options to customize the Sampler primitive. This section focuses on how to specify Qiskit Runtime primitive options. While the interface of the primitives' `run()` method is common across all implementations, their options are not. Consult the corresponding API references for information about the [`qiskit.primitives.BackendSamplerV2`](/docs/api/qiskit/qiskit.primitives.BackendSamplerV2) and [`qiskit_aer.primitives.SamplerV2`](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.primitives.SamplerV2.html) options.\n", - "\n", - "\n", - "## Set Sampler options\n", - "\n", - "You can set options when initializing Sampler, after initializing Sampler, or you can update the options after Sampler has been initialized. For instructions to use these techniques, see the [Introduction to options](/docs/guides/runtime-options-overview#options-precedence) topic.\n", - "\n", - "Additionally, you can set the `shots` value in the `run()` method, as is described in the following section." - ] - }, - { - "cell_type": "markdown", - "id": "3ca44016-acfe-4540-ab0e-659b9577c4eb", - "metadata": {}, - "source": [ - "\n", - "### Run() method\n", - "\n", - "The only values you can pass to `run()` are those defined in the interface. That is, `shots`. This overwrites any value set for `default_shots` for the current run." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "c2423fcc-7f7f-4674-af6f-fc90f4b28b17", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "from qiskit.circuit.library import random_iqp\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "circuit1 = random_iqp(3)\n", - "circuit1.measure_all()\n", - "circuit2 = random_iqp(3)\n", - "circuit2.measure_all()\n", - "\n", - "pass_manager = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend\n", - ")\n", - "\n", - "transpiled1 = pass_manager.run(circuit1)\n", - "transpiled2 = pass_manager.run(circuit2)\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "# Default shots to use if not specified in run()\n", - "sampler.options.default_shots = 500\n", - "# Sample two circuits at 128 shots each.\n", - "sampler.run([transpiled1, transpiled2], shots=128)" - ] - }, - { - "cell_type": "markdown", - "id": "4bf67f60-ce62-4b95-b70c-b15f131be46d", - "metadata": {}, - "source": [ - "### Special cases" - ] - }, - { - "cell_type": "markdown", - "id": "857e40a1-de1d-46f0-9db8-db55a6954f47", - "metadata": {}, - "source": [ - "\n", - "#### Shots\n", - "\n", - "The `SamplerV2.run` method accepts two arguments: a list of PUBs, each of which can specify a PUB-specific value for shots, and a shots keyword argument. These shot values are a part of the Sampler execution interface, and are independent of the Runtime Sampler's options. They take precedence over any values specified as options in order to comply with the Sampler abstraction.\n", - "\n", - "However, if `shots` is not specified by any PUB or in the run keyword argument (or if they are all `None`), then the shots value from the options is used, most notably `default_shots`.\n", - "\n", - "To summarize, this is the order of precedence for specifying shots in the Sampler, for any particular PUB:\n", - "\n", - "1. If the PUB specifies shots, use that value.\n", - "2. If the `shots` keyword argument is specified in `run`, use that value.\n", - "4. If `twirling` is enabled (True by default), then the product of `num_randomizations` and `shots_per_randomization`, as specified as [`twirling` options](/docs/api/qiskit-ibm-runtime/options-twirling-options), is used.\n", - "5. If `sampler.options.default_shots` is specified, use that value.\n", - "\n", - "Thus, if shots are specified in all possible places, the one with highest precedence (shots specified in the PUB) is used.\n", - "\n", - "Example:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "e06376f4-fcba-4d03-8cbc-c53ca02d81af", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "from qiskit.circuit.library import random_iqp\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "circuit1 = random_iqp(3)\n", - "circuit1.measure_all()\n", - "circuit2 = random_iqp(3)\n", - "circuit2.measure_all()\n", - "\n", - "pass_manager = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend\n", - ")\n", - "\n", - "transpiled1 = pass_manager.run(circuit1)\n", - "transpiled2 = pass_manager.run(circuit2)\n", - "\n", - "\n", - "# Setting shots during primitive initialization\n", - "sampler = Sampler(mode=backend, options={\"default_shots\": 4096})\n", - "\n", - "# Setting options after primitive initialization\n", - "# This uses auto-complete.\n", - "sampler.options.default_shots = 2000\n", - "\n", - "# This does bulk update. The value for default_shots is overridden if you specify shots with run() or in the PUB.\n", - "sampler.options.update(\n", - " default_shots=1024, dynamical_decoupling={\"sequence_type\": \"XpXm\"}\n", - ")\n", - "\n", - "# Sample two circuits at 128 shots each.\n", - "sampler.run([transpiled1, transpiled2], shots=128)" - ] - }, - { - "cell_type": "markdown", - "id": "29451d36-d88d-4726-96d2-f3083ac9e4a9", - "metadata": {}, - "source": [ - "\n", - "## Available options\n", - "\n", - "The following table documents options from the latest version of `qiskit-ibm-runtime`. To see older option versions, visit the [`qiskit-ibm-runtime` API reference](/docs/api/qiskit-ibm-runtime) and select a previous version.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "The total number of shots to use per circuit per configuration.\n", - "\n", - "**Choices**: Integer >= 0\n", - "\n", - "**Default**: None\n", - "\n", - "[`default_shots` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#default_shots)\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Control dynamical decoupling error mitigation settings.\n", - "\n", - "[`dynamical_decoupling` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#dynamical_decoupling)\n", - "\n", - "\n", - "\n", - "**Choices**: `True`, `False`\n", - "\n", - "**Default**: `False`\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `middle`, `edges`\n", - "\n", - "**Default**: `middle`\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Choices: `asap`, `alap`\n", - "Default: `alap`\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Choices: `XX`, `XpXm`, `XY4`\n", - "Default: `XX`\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Choices: `True`, `False`\n", - "Default: `False`\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "[`environment` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#environment)\n", - "\n", - "\n", - "\n", - "List of tags.\n", - "\n", - "**Choices**: None\n", - "\n", - "**Default**: None\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: DEBUG, INFO, WARNING, ERROR, CRITICAL\n", - "\n", - "**Default**: WARNING\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `True`, `False`\n", - "\n", - "**Default**: `False`\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "[`execution` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#execution)\n", - "\n", - "\n", - "Whether to reset the qubits to the ground state for each shot.\n", - "\n", - "**Choices**: `True`, `False`\n", - "\n", - "**Default**: `True`\n", - " \n", - "\n", - "\n", - "\n", - "The delay between a measurement and the subsequent quantum circuit.\n", - "\n", - "**Choices**: Value in the range supplied by `backend.rep_delay_range`\n", - "\n", - "**Default**: Given by `backend.default_rep_delay`\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `classified`, `kerneled`, `avg_kerneled`\n", - "\n", - "**Default**: `classified`\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Limits how long a job can run, in seconds. See the [maximum execution time](/docs/guides/max-execution-time) guide for details.\n", - "\n", - "**Choices**: Integer number of seconds in the range [1, 10800]\n", - "\n", - "**Default**: 10800 (3 hours)\n", - "\n", - "[`max_execution_time` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#max_execution_time)\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "Options to pass when simulating a backend\n", - "\n", - "[`simulator` API documentation](/docs/api/qiskit-ibm-runtime/options-simulator-options)\n", - "\n", - "\n", - "\n", - "**Choices**: List of basis gate names to unroll to\n", - "\n", - "**Default**: The set of all basis gates supported by [Qiskit Aer simulator](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.AerSimulator.html)\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: List of directed two-qubit interactions\n", - "\n", - "**Default**: None, which implies no connectivity constraints (full connectivity).\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: [Qiskit Aer NoiseModel](/docs/guides/build-noise-models), or its representation\n", - "\n", - "**Default**: None\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: Integer\n", - "\n", - "**Default**: None\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Twirling options\n", - "\n", - "[`twirling` API documentation](/docs/api/qiskit-ibm-runtime/options-twirling-options)\n", - "\n", - "\n", - "\n", - "**Choices**: True, False\n", - "\n", - "**Default**: False\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: True, False\n", - "\n", - "**Default**: False\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `auto`, Integer >= 1\n", - "\n", - "**Default**: `auto`\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `auto`, Integer >= 1\n", - "\n", - "**Default**: `auto`\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "**Choices**: `active`, `active-circuit`, `active-accum`, `all`\n", - "\n", - "**Default**: `active-accum`\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Experimental options, when available.\n", - "\n", - " \n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "80737dfd-a1c7-4b5f-b1ea-3391dcfa61b7", - "metadata": {}, - "source": [ - "\n", - "## Feature compatibility\n", - "\n", - "Certain runtime features cannot be used together in a single job. Click the appropriate tab for a list of features that are incompatible with the selected feature:\n", - "\n", - "\n", - " \n", - " Incompatible with:\n", - " - Dynamical decoupling\n", - "\n", - " Other notes:\n", - " - Gate twirling can be applied to dynamic circuits, but only to gates not inside conditional blocks. Measurement twirling can only be applied to terminal measurements.\n", - " - Compatible with fractional gates when using `qiskit-ibm-runtime` v0.42.0 or later.\n", - "\n", - " \n", - " \n", - " Incompatible with dynamic circuits.\n", - "\n", - " \n", - "\n", - " \n", - " Incompatible with:\n", - " - Gate twirling\n", - "\n", - " Compatible with dynamic circuits when using `qiskit-ibm-runtime` v0.42.0 or later.\n", - "\n", - " \n", - "\n", - " \n", - " Incompatible with fractional gates or with stretches.\n", - "\n", - " Other notes:\n", - " - Gate twirling can be applied to dynamic circuits, but only to gates not inside conditional blocks. Measurement twirling can only be applied to terminal measurements.\n", - " - Does not work with non-Clifford entanglers.\n", - "\n", - " \n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "b95e9e8e-575c-411e-8504-4ef0a00166d3", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - " - Review the [Introduction to options](/docs/guides/runtime-options-overview) guide.\n", - " - Find more details about the `SamplerV2` methods in the [Sampler API reference](/docs/api/qiskit-ibm-runtime/sampler-v2).\n", - " - Decide what [execution mode](execution-modes) to run your job in.\n", - " - Learn about [noise management with Sampler](/docs/guides/sampler-noise-management).\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bed19311-f71d-4d8c-9a05-1b68fa72df29", + "metadata": {}, + "source": [ + "---\n", + "title: Specify Sampler options\n", + "description: Specify options when building with the Sampler primitive.\n", + "---\n", + "\n", + "\n", + "# Specify Sampler options" + ] + }, + { + "cell_type": "markdown", + "id": "3adc856a-0fcd-47e4-94d9-22b90bddc24f", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [ + "{/*\n", + " DO NOT EDIT THIS CELL!!!\n", + " This cell's content is generated automatically by a script. Anything you add\n", + " here will be removed next time the notebook is run. To add new content, create\n", + " a new cell before or after this one.\n", + "*/}\n", + "\n", + "\n", + "\n", + "\n", + "The code on this page was developed using the following requirements.\n", + "We recommend using these versions or newer.\n", + "\n", + "```\n", + "qiskit[all]~=2.4.0\n", + "qiskit-ibm-runtime~=0.46.1\n", + "```\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "56db929d-8b84-49b2-a98b-55d33f91ea30", + "metadata": {}, + "source": [ + "You can use options to customize the Sampler primitive. This section focuses on how to specify Qiskit Runtime primitive options. While the interface of the primitives' `run()` method is common across all implementations, their options are not. Consult the corresponding API references for information about the [`qiskit.primitives.BackendSamplerV2`](/docs/api/qiskit/qiskit.primitives.BackendSamplerV2) and [`qiskit_aer.primitives.SamplerV2`](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.primitives.SamplerV2.html) options.\n", + "\n", + "\n", + "## Set Sampler options\n", + "\n", + "You can set options when initializing Sampler, after initializing Sampler, or you can update the options after Sampler has been initialized. For instructions to use these techniques, see the [Introduction to options](/docs/guides/runtime-options-overview#options-precedence) topic.\n", + "\n", + "Additionally, you can set the `shots` value in the `run()` method, as is described in the following section." + ] + }, + { + "cell_type": "markdown", + "id": "3ca44016-acfe-4540-ab0e-659b9577c4eb", + "metadata": {}, + "source": [ + "\n", + "### Run() method\n", + "\n", + "The only values you can pass to `run()` are those defined in the interface. That is, `shots`. This overwrites any value set for `default_shots` for the current run." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c2423fcc-7f7f-4674-af6f-fc90f4b28b17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "from qiskit.circuit.library import random_iqp\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "circuit1 = random_iqp(3)\n", + "circuit1.measure_all()\n", + "circuit2 = random_iqp(3)\n", + "circuit2.measure_all()\n", + "\n", + "pass_manager = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend\n", + ")\n", + "\n", + "transpiled1 = pass_manager.run(circuit1)\n", + "transpiled2 = pass_manager.run(circuit2)\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "# Default shots to use if not specified in run()\n", + "sampler.options.default_shots = 500\n", + "# Sample two circuits at 128 shots each.\n", + "sampler.run([transpiled1, transpiled2], shots=128)" + ] + }, + { + "cell_type": "markdown", + "id": "4bf67f60-ce62-4b95-b70c-b15f131be46d", + "metadata": {}, + "source": [ + "### Special cases" + ] + }, + { + "cell_type": "markdown", + "id": "857e40a1-de1d-46f0-9db8-db55a6954f47", + "metadata": {}, + "source": [ + "\n", + "#### Shots\n", + "\n", + "The `SamplerV2.run` method accepts two arguments: a list of PUBs, each of which can specify a PUB-specific value for shots, and a shots keyword argument. These shot values are a part of the Sampler execution interface, and are independent of the Runtime Sampler's options. They take precedence over any values specified as options in order to comply with the Sampler abstraction.\n", + "\n", + "However, if `shots` is not specified by any PUB or in the run keyword argument (or if they are all `None`), then the shots value from the options is used, most notably `default_shots`.\n", + "\n", + "To summarize, this is the order of precedence for specifying shots in the Sampler, for any particular PUB:\n", + "\n", + "1. If the PUB specifies shots, use that value.\n", + "2. If the `shots` keyword argument is specified in `run`, use that value.\n", + "4. If `twirling` is enabled (True by default), then the product of `num_randomizations` and `shots_per_randomization`, as specified as [`twirling` options](/docs/api/qiskit-ibm-runtime/options-twirling-options), is used.\n", + "5. If `sampler.options.default_shots` is specified, use that value.\n", + "\n", + "Thus, if shots are specified in all possible places, the one with highest precedence (shots specified in the PUB) is used.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e06376f4-fcba-4d03-8cbc-c53ca02d81af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "from qiskit.circuit.library import random_iqp\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "circuit1 = random_iqp(3)\n", + "circuit1.measure_all()\n", + "circuit2 = random_iqp(3)\n", + "circuit2.measure_all()\n", + "\n", + "pass_manager = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend\n", + ")\n", + "\n", + "transpiled1 = pass_manager.run(circuit1)\n", + "transpiled2 = pass_manager.run(circuit2)\n", + "\n", + "\n", + "# Setting shots during primitive initialization\n", + "sampler = Sampler(mode=backend, options={\"default_shots\": 4096})\n", + "\n", + "# Setting options after primitive initialization\n", + "# This uses auto-complete.\n", + "sampler.options.default_shots = 2000\n", + "\n", + "# This does bulk update. The value for default_shots is overridden if you specify shots with run()\n", + "# or in the PUB.\n", + "sampler.options.update(\n", + " default_shots=1024, dynamical_decoupling={\"sequence_type\": \"XpXm\"}\n", + ")\n", + "\n", + "# Sample two circuits at 128 shots each.\n", + "sampler.run([transpiled1, transpiled2], shots=128)" + ] + }, + { + "cell_type": "markdown", + "id": "29451d36-d88d-4726-96d2-f3083ac9e4a9", + "metadata": {}, + "source": [ + "\n", + "## Available options\n", + "\n", + "The following table documents options from the latest version of `qiskit-ibm-runtime`. To see older option versions, visit the [`qiskit-ibm-runtime` API reference](/docs/api/qiskit-ibm-runtime) and select a previous version.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "The total number of shots to use per circuit per configuration.\n", + "\n", + "**Choices**: Integer >= 0\n", + "\n", + "**Default**: None\n", + "\n", + "[`default_shots` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#default_shots)\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Control dynamical decoupling error mitigation settings.\n", + "\n", + "[`dynamical_decoupling` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#dynamical_decoupling)\n", + "\n", + "\n", + "\n", + "**Choices**: `True`, `False`\n", + "\n", + "**Default**: `False`\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `middle`, `edges`\n", + "\n", + "**Default**: `middle`\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Choices: `asap`, `alap`\n", + "Default: `alap`\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Choices: `XX`, `XpXm`, `XY4`\n", + "Default: `XX`\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Choices: `True`, `False`\n", + "Default: `False`\n", + " \n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "[`environment` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#environment)\n", + "\n", + "\n", + "\n", + "List of tags.\n", + "\n", + "**Choices**: None\n", + "\n", + "**Default**: None\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: DEBUG, INFO, WARNING, ERROR, CRITICAL\n", + "\n", + "**Default**: WARNING\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `True`, `False`\n", + "\n", + "**Default**: `False`\n", + " \n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "[`execution` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#execution)\n", + "\n", + "\n", + "Whether to reset the qubits to the ground state for each shot.\n", + "\n", + "**Choices**: `True`, `False`\n", + "\n", + "**Default**: `True`\n", + " \n", + "\n", + "\n", + "\n", + "The delay between a measurement and the subsequent quantum circuit.\n", + "\n", + "**Choices**: Value in the range supplied by `backend.rep_delay_range`\n", + "\n", + "**Default**: Given by `backend.default_rep_delay`\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `classified`, `kerneled`, `avg_kerneled`\n", + "\n", + "**Default**: `classified`\n", + " \n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Limits how long a job can run, in seconds. See the [maximum execution time](/docs/guides/max-execution-time) guide for details.\n", + "\n", + "**Choices**: Integer number of seconds in the range [1, 10800]\n", + "\n", + "**Default**: 10800 (3 hours)\n", + "\n", + "[`max_execution_time` API documentation](/docs/api/qiskit-ibm-runtime/options-sampler-options#max_execution_time)\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "Options to pass when simulating a backend\n", + "\n", + "[`simulator` API documentation](/docs/api/qiskit-ibm-runtime/options-simulator-options)\n", + "\n", + "\n", + "\n", + "**Choices**: List of basis gate names to unroll to\n", + "\n", + "**Default**: The set of all basis gates supported by [Qiskit Aer simulator](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.AerSimulator.html)\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: List of directed two-qubit interactions\n", + "\n", + "**Default**: None, which implies no connectivity constraints (full connectivity).\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: [Qiskit Aer NoiseModel](/docs/guides/build-noise-models), or its representation\n", + "\n", + "**Default**: None\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: Integer\n", + "\n", + "**Default**: None\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Twirling options\n", + "\n", + "[`twirling` API documentation](/docs/api/qiskit-ibm-runtime/options-twirling-options)\n", + "\n", + "\n", + "\n", + "**Choices**: True, False\n", + "\n", + "**Default**: False\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: True, False\n", + "\n", + "**Default**: False\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `auto`, Integer >= 1\n", + "\n", + "**Default**: `auto`\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `auto`, Integer >= 1\n", + "\n", + "**Default**: `auto`\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "**Choices**: `active`, `active-circuit`, `active-accum`, `all`\n", + "\n", + "**Default**: `active-accum`\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Experimental options, when available.\n", + "\n", + " \n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "80737dfd-a1c7-4b5f-b1ea-3391dcfa61b7", + "metadata": {}, + "source": [ + "\n", + "## Feature compatibility\n", + "\n", + "Certain runtime features cannot be used together in a single job. Click the appropriate tab for a list of features that are incompatible with the selected feature:\n", + "\n", + "\n", + " \n", + " Incompatible with:\n", + " - Dynamical decoupling\n", + "\n", + " Other notes:\n", + " - Gate twirling can be applied to dynamic circuits, but only to gates not inside conditional blocks. Measurement twirling can only be applied to terminal measurements.\n", + " - Compatible with fractional gates when using `qiskit-ibm-runtime` v0.42.0 or later.\n", + "\n", + " \n", + " \n", + " Incompatible with dynamic circuits.\n", + "\n", + " \n", + "\n", + " \n", + " Incompatible with:\n", + " - Gate twirling\n", + "\n", + " Compatible with dynamic circuits when using `qiskit-ibm-runtime` v0.42.0 or later.\n", + "\n", + " \n", + "\n", + " \n", + " Incompatible with fractional gates or with stretches.\n", + "\n", + " Other notes:\n", + " - Gate twirling can be applied to dynamic circuits, but only to gates not inside conditional blocks. Measurement twirling can only be applied to terminal measurements.\n", + " - Does not work with non-Clifford entanglers.\n", + "\n", + " \n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "b95e9e8e-575c-411e-8504-4ef0a00166d3", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + " - Review the [Introduction to options](/docs/guides/runtime-options-overview) guide.\n", + " - Find more details about the `SamplerV2` methods in the [Sampler API reference](/docs/api/qiskit-ibm-runtime/sampler-v2).\n", + " - Decide what [execution mode](execution-modes) to run your job in.\n", + " - Learn about [noise management with Sampler](/docs/guides/sampler-noise-management).\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/guides/serverless-first-program.ipynb b/docs/guides/serverless-first-program.ipynb index 2a532614db7..2df55644306 100644 --- a/docs/guides/serverless-first-program.ipynb +++ b/docs/guides/serverless-first-program.ipynb @@ -1,436 +1,440 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "cdc8dc1e-0cff-46c1-94d0-97456d278e27", - "metadata": {}, - "source": [ - "---\n", - "title: Write your first Qiskit Serverless program\n", - "description: How to create a parallel transpilation program and deploy it to IBM Quantum Platform to use as a reusable remote service.\n", - "---\n", - "\n", - "{/* cspell:ignore mypath */}\n", - "\n", - "# Write your first Qiskit Serverless program" - ] - }, - { - "cell_type": "markdown", - "id": "b6d632cb-d884-4963-9c8d-8f3a3acd8a7d", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [ - "\n", - "\n", - "\n", - "The code on this page was developed using the following requirements.\n", - "We recommend using these versions or newer.\n", - "\n", - "```\n", - "qiskit[all]~=1.3.1\n", - "qiskit-ibm-runtime~=0.34.0\n", - "qiskit-aer~=0.15.1\n", - "qiskit-serverless~=0.18.1\n", - "qiskit-ibm-catalog~=0.2\n", - "qiskit-addon-sqd~=0.8.1\n", - "qiskit-addon-utils~=0.1.0\n", - "qiskit-addon-mpf~=0.2.0\n", - "qiskit-addon-aqc-tensor~=0.1.2\n", - "qiskit-addon-obp~=0.1.0\n", - "scipy~=1.15.0\n", - "pyscf~=2.8.0\n", - "```\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "1cc46aeb-aff6-4fab-9d78-69f2bfe98d98", - "metadata": {}, - "source": [ - "\n", - " **Qiskit Serverless is getting an upgrade, and its features are changing fast.** During this development phase, find release notes and the most recent documentation at the [Qiskit Serverless GitHub](https://qiskit.github.io/qiskit-serverless/index.html) page.\n", - "\n", - "\n", - "This example demonstrates how to use `qiskit-serverless` tools to create a parallel transpilation program, and then implement `qiskit-ibm-catalog` to upload your program to IBM Quantum Platform to use as a reusable remote service.\n", - "\n", - "## Workflow overview\n", - "\n", - "1. Create a local directory and empty program file (`./source_files/transpile_remote.py`)\n", - "1. Add code to your program that, when uploaded to Qiskit Serverless, will transpile a circuit\n", - "1. Use `qiskit-ibm-catalog` to authenticate to Qiskit Serverless\n", - "1. Upload the program to Qiskit Serverless\n", - "\n", - "After uploading your program , you can run it to transpile the circuit by following the [Run your first Qiskit Serverless workload remotely](/docs/guides/serverless-run-first-workload) guide." - ] - }, - { - "cell_type": "markdown", - "id": "cad16f43-8548-4849-a4b8-09059a8b747c", - "metadata": {}, - "source": [ - "## Example: Remote transpilation with Qiskit Serverless\n", - "\n", - "This example walks you through creating and adding to a program file that, when you upload it to Qiskit Serverless, will transpile a `circuit` against a given `backend` and target `optimization_level`.\n", - "\n", - "\n", - "Qiskit Serverless requires setting up your workload’s `.py` files into a dedicated directory. The following structure is an example of good practice:\n", - "\n", - "```text\n", - "serverless_program\n", - "├── program_uploader.ipynb\n", - "└── source_files\n", - " ├── transpile_remote.py\n", - " └── *.py\n", - "```\n", - "\n", - "\n", - "Serverless uploads the contents of a specific directory (in this example, the `source_files` directory) to run remotely. After these are set up, you can adjust `transpile_remote.py` to fetch inputs and return outputs.\n", - "\n", - "### Create the directory and an empty program file\n", - "\n", - "First, create a directory named `source_files`, then create a program file in the directory, so that its path is `./source_files/transpile_remote.py`. This is the file you will upload to Qiskit Serverless." - ] - }, - { - "cell_type": "markdown", - "id": "a9b3fb7d-abd6-4a3e-b9b1-b21202d8db6d", - "metadata": {}, - "source": [ - "### Add code to your program file\n", - "\n", - "Populate your program file with the following code, then save it.\n", - "\n", - "\n", - "If you're reading the code cells locally in a notebook, you will see the `%%writefile` [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile). Executing cells with this magic command saves them to disk rather than executing them.\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "214eb803-d1fa-4ac0-b955-8b24717086f4", - "metadata": {}, - "outputs": [], - "source": [ - "# This cell is hidden from users, it creates a new folder\n", - "from pathlib import Path\n", - "\n", - "Path(\"./source_files\").mkdir(exist_ok=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "64722041-22ed-42bd-8d83-d8e9f5e5b55b", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "%%writefile ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "def transpile_remote(circuit, optimization_level, backend):\n", - " \"\"\"Transpiles an abstract circuit into an ISA circuit for a given backend.\"\"\"\n", - " pass_manager = generate_preset_pass_manager(\n", - " optimization_level=optimization_level,\n", - "\t\tbackend=backend\n", - " )\n", - " isa_circuit = pass_manager.run(circuit)\n", - " return isa_circuit" - ] - }, - { - "cell_type": "markdown", - "id": "7e80b5bb-0f76-43d1-a68c-97b4b22cc88a", - "metadata": {}, - "source": [ - "### Add code to get program arguments\n", - "\n", - "Now add the following code to your program file, which sets up program arguments.\n", - "\n", - "Your initial `transpile_remote.py` has three inputs: `circuits`, `backend_name`, and `optimization_level`. Serverless is currently limited to only accept serializable inputs and outputs. For this reason, you cannot pass in `backend` directly, so use `backend_name` as a string instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6819379b-d7b2-4fda-9e6b-459eec748a79", - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from qiskit_serverless import get_arguments, save_result, distribute_task, get\n", - "\n", - "# Get program arguments\n", - "arguments = get_arguments()\n", - "circuits = arguments.get(\"circuits\")\n", - "backend_name = arguments.get(\"backend_name\")\n", - "optimization_level = arguments.get(\"optimization_level\")" - ] - }, - { - "cell_type": "markdown", - "id": "5ae9fb12-f06f-4f7d-8f37-1d872285deab", - "metadata": {}, - "source": [ - "### Add code that calls the backend\n", - "\n", - "Add the following code to your program file, which calls your backend with `QiskitRuntimeService`.\n", - "\n", - "The following code assumes that you have already followed the process to save your credentials by using `QiskitRuntimeService.save_account`, and will load your default saved account unless you specify otherwise. See [Save your login credentials](/docs/guides/save-credentials) and [Initialize your Qiskit Runtime service account](/docs/guides/initialize-account) for more information." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b0c7d24-9b87-4e00-bb1d-f82aa967cb1d", - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.backend(backend_name)" - ] - }, - { - "cell_type": "markdown", - "id": "61e7cb3b-6d62-4b68-817a-39c0c1d95a9b", - "metadata": {}, - "source": [ - "### Add code to transpile\n", - "\n", - "Finally, add the following code to your program file. This code runs `transpile_remote()` across all `circuits` passed in, and returns the `transpiled_circuits` as a result:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "041e7f85-e9e8-424d-8694-d54316225cc4", - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "# Each circuit is being transpiled and will populate the array\n", - "results = [\n", - " transpile_remote(circuit, 1, backend)\n", - " for circuit in circuits\n", - "]\n", - "\n", - "save_result({\n", - " \"transpiled_circuits\": results\n", - "})" - ] - }, - { - "cell_type": "markdown", - "id": "dac118ab-e447-4ddd-a5f8-d13e3953db76", - "metadata": {}, - "source": [ - "\n", - "### Authenticate to Qiskit Serverless\n", - "\n", - "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key (you can use your `QiskitRuntimeService` API key, or create a new API key on the [IBM Quantum Platform dashboard](https://quantum.cloud.ibm.com))." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "8d1385c3-09a5-42c6-a5bc-53f686d8410e", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_catalog import QiskitServerless, QiskitFunction\n", - "\n", - "# Authenticate to the remote cluster and submit the pattern for remote execution\n", - "serverless = QiskitServerless()" - ] - }, - { - "cell_type": "markdown", - "id": "a0f64820-5fb1-41b9-9cb4-5095d0b5db43", - "metadata": {}, - "source": [ - "### Run code to upload\n", - "\n", - "Run the following code to upload the program. Qiskit Serverless compresses the contents of `working_dir` (in this case, `source_files`) into a `tar`, which is uploaded and then cleaned up. The `entrypoint` identifies the main program executable for Qiskit Serverless to run." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c1cdd46d-71e9-4711-a09b-6eca569ede6a", - "metadata": {}, - "outputs": [], - "source": [ - "transpile_remote_demo = QiskitFunction(\n", - " title=\"transpile_remote_serverless\",\n", - " entrypoint=\"transpile_remote.py\",\n", - " working_dir=\"./source_files/\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "8b675d04-bde8-4b3d-b9e5-99982ef742e1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "QiskitFunction(transpile_remote_serverless)" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.upload(transpile_remote_demo)" - ] - }, - { - "cell_type": "markdown", - "id": "3a840c18-3010-4d05-91cb-592e2692c1d7", - "metadata": {}, - "source": [ - "### Verify upload\n", - "\n", - "To check if it successfully uploaded, use `serverless.list()`, as in the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "afaa3935-adf7-4b28-a8f0-583a82bbe3f1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "QiskitFunction(transpile_remote_serverless)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get program from serverless.list() that matches the title of the one we uploaded\n", - "next(\n", - " program\n", - " for program in serverless.list()\n", - " if program.title == \"transpile_remote_serverless\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "484713e3-b6ac-4a7a-9425-e9ac34b1bb6e", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it checks the program uploaded correctly\n", - "assert _.title == \"transpile_remote_serverless\" # noqa: F821" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "413e5d94-00f6-4c47-80d0-b5c4441934fc", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it checks the program executes correctly\n", - "from time import sleep\n", - "from qiskit import QuantumCircuit\n", - "\n", - "qc = QuantumCircuit(2)\n", - "transpile_remote_serverless = serverless.load(\"transpile_remote_serverless\")\n", - "job = transpile_remote_serverless.run(\n", - " circuits=[qc],\n", - " backend=\"ibm_sherbrooke\",\n", - " optimization_level=1,\n", - ")\n", - "while True:\n", - " sleep(5)\n", - " status = job.status()\n", - " if status not in [\"QUEUED\", \"INITIALIZING\", \"RUNNING\", \"DONE\"]:\n", - " raise Exception(\n", - " f\"Unexpected job status: '{status}'\\n\"\n", - " + \"Here are the logs:\\n\"\n", - " + job.logs()\n", - " )\n", - " if status == \"DONE\":\n", - " break" - ] - }, - { - "cell_type": "markdown", - "id": "a416b143-6f70-49dc-ab2f-ad66f042cab1", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "- Learn how to pass inputs and run your program remotely in the [Run your first Qiskit Serverless workload remotely](/docs/guides/serverless-run-first-workload) topic.\n", - "\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cdc8dc1e-0cff-46c1-94d0-97456d278e27", + "metadata": {}, + "source": [ + "---\n", + "title: Write your first Qiskit Serverless program\n", + "description: How to create a parallel transpilation program and deploy it to IBM Quantum Platform to use as a reusable remote service.\n", + "---\n", + "\n", + "{/* cspell:ignore mypath */}\n", + "\n", + "# Write your first Qiskit Serverless program" + ] + }, + { + "cell_type": "markdown", + "id": "b6d632cb-d884-4963-9c8d-8f3a3acd8a7d", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [ + "\n", + "\n", + "\n", + "The code on this page was developed using the following requirements.\n", + "We recommend using these versions or newer.\n", + "\n", + "```\n", + "qiskit[all]~=1.3.1\n", + "qiskit-ibm-runtime~=0.34.0\n", + "qiskit-aer~=0.15.1\n", + "qiskit-serverless~=0.18.1\n", + "qiskit-ibm-catalog~=0.2\n", + "qiskit-addon-sqd~=0.8.1\n", + "qiskit-addon-utils~=0.1.0\n", + "qiskit-addon-mpf~=0.2.0\n", + "qiskit-addon-aqc-tensor~=0.1.2\n", + "qiskit-addon-obp~=0.1.0\n", + "scipy~=1.15.0\n", + "pyscf~=2.8.0\n", + "```\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "1cc46aeb-aff6-4fab-9d78-69f2bfe98d98", + "metadata": {}, + "source": [ + "\n", + " **Qiskit Serverless is getting an upgrade, and its features are changing fast.** During this development phase, find release notes and the most recent documentation at the [Qiskit Serverless GitHub](https://qiskit.github.io/qiskit-serverless/index.html) page.\n", + "\n", + "\n", + "This example demonstrates how to use `qiskit-serverless` tools to create a parallel transpilation program, and then implement `qiskit-ibm-catalog` to upload your program to IBM Quantum Platform to use as a reusable remote service.\n", + "\n", + "## Workflow overview\n", + "\n", + "1. Create a local directory and empty program file (`./source_files/transpile_remote.py`)\n", + "1. Add code to your program that, when uploaded to Qiskit Serverless, will transpile a circuit\n", + "1. Use `qiskit-ibm-catalog` to authenticate to Qiskit Serverless\n", + "1. Upload the program to Qiskit Serverless\n", + "\n", + "After uploading your program , you can run it to transpile the circuit by following the [Run your first Qiskit Serverless workload remotely](/docs/guides/serverless-run-first-workload) guide." + ] + }, + { + "cell_type": "markdown", + "id": "cad16f43-8548-4849-a4b8-09059a8b747c", + "metadata": {}, + "source": [ + "## Example: Remote transpilation with Qiskit Serverless\n", + "\n", + "This example walks you through creating and adding to a program file that, when you upload it to Qiskit Serverless, will transpile a `circuit` against a given `backend` and target `optimization_level`.\n", + "\n", + "\n", + "Qiskit Serverless requires setting up your workload’s `.py` files into a dedicated directory. The following structure is an example of good practice:\n", + "\n", + "```text\n", + "serverless_program\n", + "├── program_uploader.ipynb\n", + "└── source_files\n", + " ├── transpile_remote.py\n", + " └── *.py\n", + "```\n", + "\n", + "\n", + "Serverless uploads the contents of a specific directory (in this example, the `source_files` directory) to run remotely. After these are set up, you can adjust `transpile_remote.py` to fetch inputs and return outputs.\n", + "\n", + "### Create the directory and an empty program file\n", + "\n", + "First, create a directory named `source_files`, then create a program file in the directory, so that its path is `./source_files/transpile_remote.py`. This is the file you will upload to Qiskit Serverless." + ] + }, + { + "cell_type": "markdown", + "id": "a9b3fb7d-abd6-4a3e-b9b1-b21202d8db6d", + "metadata": {}, + "source": [ + "### Add code to your program file\n", + "\n", + "Populate your program file with the following code, then save it.\n", + "\n", + "\n", + "If you're reading the code cells locally in a notebook, you will see the `%%writefile` [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile). Executing cells with this magic command saves them to disk rather than executing them.\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "214eb803-d1fa-4ac0-b955-8b24717086f4", + "metadata": {}, + "outputs": [], + "source": [ + "# This cell is hidden from users, it creates a new folder\n", + "from pathlib import Path\n", + "\n", + "Path(\"./source_files\").mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64722041-22ed-42bd-8d83-d8e9f5e5b55b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "%%writefile ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "def transpile_remote(circuit, optimization_level, backend):\n", + " \"\"\"Transpiles an abstract circuit into an ISA circuit for a given backend.\"\"\"\n", + " pass_manager = generate_preset_pass_manager(\n", + " optimization_level=optimization_level,\n", + "\t\tbackend=backend\n", + " )\n", + " isa_circuit = pass_manager.run(circuit)\n", + " return isa_circuit" + ] + }, + { + "cell_type": "markdown", + "id": "7e80b5bb-0f76-43d1-a68c-97b4b22cc88a", + "metadata": {}, + "source": [ + "### Add code to get program arguments\n", + "\n", + "Now add the following code to your program file, which sets up program arguments.\n", + "\n", + "Your initial `transpile_remote.py` has three inputs: `circuits`, `backend_name`, and `optimization_level`. Serverless is currently limited to only accept serializable inputs and outputs. For this reason, you cannot pass in `backend` directly, so use `backend_name` as a string instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6819379b-d7b2-4fda-9e6b-459eec748a79", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from qiskit_serverless import get_arguments, save_result, distribute_task, get\n", + "\n", + "# Get program arguments\n", + "arguments = get_arguments()\n", + "circuits = arguments.get(\"circuits\")\n", + "backend_name = arguments.get(\"backend_name\")\n", + "optimization_level = arguments.get(\"optimization_level\")" + ] + }, + { + "cell_type": "markdown", + "id": "5ae9fb12-f06f-4f7d-8f37-1d872285deab", + "metadata": {}, + "source": [ + "### Add code that calls the backend\n", + "\n", + "Add the following code to your program file, which calls your backend with `QiskitRuntimeService`.\n", + "\n", + "The following code assumes that you have already followed the process to save your credentials by using `QiskitRuntimeService.save_account`, and will load your default saved account unless you specify otherwise. See [Save your login credentials](/docs/guides/save-credentials) and [Initialize your Qiskit Runtime service account](/docs/guides/initialize-account) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b0c7d24-9b87-4e00-bb1d-f82aa967cb1d", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.backend(backend_name)" + ] + }, + { + "cell_type": "markdown", + "id": "61e7cb3b-6d62-4b68-817a-39c0c1d95a9b", + "metadata": {}, + "source": [ + "### Add code to transpile\n", + "\n", + "Finally, add the following code to your program file. This code runs `transpile_remote()` across all `circuits` passed in, and returns the `transpiled_circuits` as a result:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "041e7f85-e9e8-424d-8694-d54316225cc4", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "# Each circuit is being transpiled and will populate the array\n", + "results = [\n", + " transpile_remote(circuit, 1, backend)\n", + " for circuit in circuits\n", + "]\n", + "\n", + "save_result({\n", + " \"transpiled_circuits\": results\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "dac118ab-e447-4ddd-a5f8-d13e3953db76", + "metadata": {}, + "source": [ + "\n", + "### Authenticate to Qiskit Serverless\n", + "\n", + "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key (you can use your `QiskitRuntimeService` API key, or create a new API key on the [IBM Quantum Platform dashboard](https://quantum.cloud.ibm.com))." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8d1385c3-09a5-42c6-a5bc-53f686d8410e", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitServerless, QiskitFunction\n", + "\n", + "# Authenticate to the remote cluster and submit the pattern for remote execution\n", + "serverless = QiskitServerless()" + ] + }, + { + "cell_type": "markdown", + "id": "a0f64820-5fb1-41b9-9cb4-5095d0b5db43", + "metadata": {}, + "source": [ + "### Run code to upload\n", + "\n", + "Run the following code to upload the program. Qiskit Serverless compresses the contents of `working_dir` (in this case, `source_files`) into a `tar`, which is uploaded and then cleaned up. The `entrypoint` identifies the main program executable for Qiskit Serverless to run." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c1cdd46d-71e9-4711-a09b-6eca569ede6a", + "metadata": {}, + "outputs": [], + "source": [ + "transpile_remote_demo = QiskitFunction(\n", + " title=\"transpile_remote_serverless\",\n", + " entrypoint=\"transpile_remote.py\",\n", + " working_dir=\"./source_files/\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8b675d04-bde8-4b3d-b9e5-99982ef742e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "QiskitFunction(transpile_remote_serverless)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.upload(transpile_remote_demo)" + ] + }, + { + "cell_type": "markdown", + "id": "3a840c18-3010-4d05-91cb-592e2692c1d7", + "metadata": {}, + "source": [ + "### Verify upload\n", + "\n", + "To check if it successfully uploaded, use `serverless.list()`, as in the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "afaa3935-adf7-4b28-a8f0-583a82bbe3f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "QiskitFunction(transpile_remote_serverless)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get program from serverless.list() that matches the title of the one we uploaded\n", + "next(\n", + " program\n", + " for program in serverless.list()\n", + " if program.title == \"transpile_remote_serverless\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "484713e3-b6ac-4a7a-9425-e9ac34b1bb6e", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users, it checks the program uploaded correctly\n", + "assert _.title == \"transpile_remote_serverless\" # noqa: F821" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "413e5d94-00f6-4c47-80d0-b5c4441934fc", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users, it checks the program executes correctly\n", + "from time import sleep\n", + "from qiskit import QuantumCircuit\n", + "\n", + "qc = QuantumCircuit(2)\n", + "transpile_remote_serverless = serverless.load(\"transpile_remote_serverless\")\n", + "job = transpile_remote_serverless.run(\n", + " circuits=[qc],\n", + " backend=\"ibm_sherbrooke\",\n", + " optimization_level=1,\n", + ")\n", + "while True:\n", + " sleep(5)\n", + " status = job.status()\n", + " if status not in [\"QUEUED\", \"INITIALIZING\", \"RUNNING\", \"DONE\"]:\n", + " raise Exception(\n", + " f\"Unexpected job status: '{status}'\\n\"\n", + " + \"Here are the logs:\\n\"\n", + " + job.logs()\n", + " )\n", + " if status == \"DONE\":\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "a416b143-6f70-49dc-ab2f-ad66f042cab1", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- Learn how to pass inputs and run your program remotely in the [Run your first Qiskit Serverless workload remotely](/docs/guides/serverless-run-first-workload) topic.\n", + "\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/guides/serverless-manage-resources.ipynb b/docs/guides/serverless-manage-resources.ipynb index a5969825e04..04c7f23d367 100644 --- a/docs/guides/serverless-manage-resources.ipynb +++ b/docs/guides/serverless-manage-resources.ipynb @@ -1,694 +1,705 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "6b7abc7b-b435-43d1-9fd8-c349ee8710f3", - "metadata": {}, - "source": [ - "---\n", - "title: Manage Qiskit Serverless compute and data resources\n", - "description: Manage compute and data across your Qiskit pattern with Qiskit Serverless.\n", - "---\n", - "\n", - "\n", - "# Manage Qiskit Serverless compute and data resources" - ] - }, - { - "cell_type": "markdown", - "id": "3b0771d6-95c9-46dc-955a-f8702f6a2632", - "metadata": { - "tags": [ - "version-info" - ] - }, - "source": [ - "\n", - "\n", - "\n", - "The code on this page was developed using the following requirements.\n", - "We recommend using these versions or newer.\n", - "\n", - "```\n", - "qiskit[all]~=2.0.0\n", - "qiskit-ibm-runtime~=0.37.0\n", - "qiskit-serverless~=0.22.0\n", - "```\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "95b2f280-f685-455f-83e9-b172445d7c6a", - "metadata": {}, - "source": [ - "\n", - " **Qiskit Serverless is getting an upgrade, and its features are changing fast.** During this development phase, find release notes and the most recent documentation at the [Qiskit Serverless GitHub](https://qiskit.github.io/qiskit-serverless/index.html) page.\n", - "\n", - "\n", - "With Qiskit Serverless, you can manage compute and data across your [Qiskit pattern](/docs/guides/intro-to-patterns), including CPUs, QPUs, and other compute accelerators." - ] - }, - { - "cell_type": "markdown", - "id": "380354c0-5cab-464d-b10f-c94055de3605", - "metadata": {}, - "source": [ - "## Set detailed statuses\n", - "\n", - "\n", - "Serverless workloads have several stages across a workflow. By default, the following statuses are viewable with `job.status()`:\n", - "\n", - "- **`QUEUED`**: the workload is queued for classical resources\n", - "- **`INITIALIZING`**: the workload is set up\n", - "- **`RUNNING`**: the workload is currently running on classical resources\n", - "- **`DONE`**: the workload has successfully completed\n", - "\n", - "You can also set custom statuses that further describe the specific workflow stage, as follows.\n", - "\n", - "\n", - "If you're reading the code cells locally in a notebook, you will see the `%%writefile` [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile). Executing cells with this magic command saves them to disk rather than executing them.\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "9e41cd2f-bce6-4c8a-8e44-537c18b3023c", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it just creates a new folder\n", - "from pathlib import Path\n", - "\n", - "Path(\"./source_files\").mkdir(exist_ok=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a69df8bc-5033-45bf-a837-cffa9d29b844", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Writing ./source_files/status_example.py\n" - ] - } - ], - "source": [ - "%%writefile ./source_files/status_example.py\n", - "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from qiskit_serverless import update_status, Job\n", - "\n", - "## If your function has a mapping stage, particularly application functions, you can set the status to \"RUNNING: MAPPING\" as follows:\n", - "update_status(Job.MAPPING)\n", - "\n", - "## While handling transpilation, error suppression, and so forth, you can set the status to \"RUNNING: OPTIMIZING_FOR_HARDWARE\":\n", - "update_status(Job.OPTIMIZING_HARDWARE)\n", - "\n", - "## After you submit jobs to Qiskit Runtime, the underlying quantum job will be queued. You can set status to \"RUNNING: WAITING_FOR_QPU\":\n", - "update_status(Job.WAITING_QPU)\n", - "\n", - "## When the Qiskit Runtime job starts running on the QPU, set the following status \"RUNNING: EXECUTING_QPU\":\n", - "update_status(Job.EXECUTING_QPU)\n", - "\n", - "## Once QPU is completed and post-processing has begun, set the status \"RUNNING: POST_PROCESSING\":\n", - "update_status(Job.POST_PROCESSING)" - ] - }, - { - "cell_type": "markdown", - "id": "a8746eae-6f15-4faf-8771-0f3062efc723", - "metadata": {}, - "source": [ - "After successful completion of this workload (with `save_result()`), this status will be updated to `DONE` automatically." - ] - }, - { - "cell_type": "markdown", - "id": "b2d40a63-3359-46e9-8f1b-4746b449b407", - "metadata": {}, - "source": [ - "## Parallel workflows\n", - "\n", - "For classical tasks that can be parallelized, use the `@distribute_task` decorator to define compute requirements needed to perform a task. Start by recalling the `transpile_remote.py` example from the [Write your first Qiskit Serverless program](/docs/guides/serverless-first-program) topic with the following code.\n", - "\n", - "The following code requires that you have already [saved your credentials](/docs/guides/cloud-setup)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "475d82f0-15cc-4db3-b3b0-54b07822b2a0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Writing ./source_files/transpile_remote.py\n" - ] - } - ], - "source": [ - "%%writefile ./source_files/transpile_remote.py\n", - "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_serverless import distribute_task\n", - "\n", - "service = QiskitRuntimeService()\n", - "\n", - "@distribute_task(target={\"cpu\": 1})\n", - "def transpile_remote(circuit, optimization_level, backend):\n", - " \"\"\"Transpiles an abstract circuit (or list of circuits) into an ISA circuit for a given backend.\"\"\"\n", - " pass_manager = generate_preset_pass_manager(\n", - " optimization_level=optimization_level,\n", - " backend=service.backend(backend)\n", - " )\n", - " isa_circuit = pass_manager.run(circuit)\n", - " return isa_circuit" - ] - }, - { - "cell_type": "markdown", - "id": "a5914f1d-f898-4db4-8d1e-ccc8081883b9", - "metadata": {}, - "source": [ - "In this example, you decorated the `transpile_remote()` function with `@distribute_task(target={\"cpu\": 1})`. When run, this creates an asynchronous parallel worker task with a single CPU core, and returns with a reference to track the worker. To fetch the result, pass the reference to the `get()` function. We can use this to run multiple parallel tasks:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e8fd31e6-9ab9-4d75-9ef9-a2b9ff9ad37a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/transpile_remote.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "from time import time\n", - "from qiskit_serverless import get, get_arguments, save_result, update_status, Job\n", - "\n", - "# Get arguments\n", - "arguments = get_arguments()\n", - "circuit = arguments.get(\"circuit\")\n", - "optimization_level = arguments.get(\"optimization_level\")\n", - "backend = arguments.get(\"backend\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74fdcd4a-01cd-46ca-aa24-2a8a3605346f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/transpile_remote.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "# Start distributed transpilation\n", - "update_status(Job.OPTIMIZING_HARDWARE)\n", - "\n", - "start_time = time()\n", - "transpile_worker_references = [\n", - " transpile_remote(circuit, optimization_level, backend)\n", - " for circuit in arguments.get(\"circuit_list\")\n", - "]\n", - "\n", - "transpiled_circuits = get(transpile_worker_references)\n", - "end_time = time()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81696ede-3aa5-4e8c-9d35-fdd70c1bf4db", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/transpile_remote.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "# Save result, with metadata\n", - "result = {\n", - " \"circuits\": transpiled_circuits,\n", - " \"metadata\": {\n", - " \"resource_usage\": {\n", - " \"RUNNING: OPTIMIZING_FOR_HARDWARE\": {\n", - " \"CPU_TIME\": end_time - start_time,\n", - " \"QPU_TIME\": 0,\n", - " },\n", - " }\n", - " },\n", - "}\n", - "\n", - "save_result(result)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "df28a92c-3585-49f0-a2ea-828a34638684", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job completed successfully\n" - ] - } - ], - "source": [ - "# This cell is hidden from users.\n", - "# It uploads the serverless program and checks it runs.\n", - "\n", - "\n", - "def test_serverless_job(title, entrypoint):\n", - " # Import in function to stop them interfering with user-facing code\n", - " from qiskit.circuit.random import random_circuit\n", - " from qiskit_serverless import IBMServerlessClient, QiskitFunction\n", - " import time\n", - " import uuid\n", - "\n", - " title += \"_\" + uuid.uuid4().hex[:8]\n", - " serverless = IBMServerlessClient()\n", - " transpile_remote_demo = QiskitFunction(\n", - " title=title,\n", - " entrypoint=entrypoint,\n", - " working_dir=\"./source_files/\",\n", - " )\n", - " serverless.upload(transpile_remote_demo)\n", - " job = serverless.get(title).run(\n", - " circuit=random_circuit(3, 3),\n", - " circuit_list=[random_circuit(3, 3) for _ in range(3)],\n", - " backend=\"ibm_torino\",\n", - " optimization_level=1,\n", - " )\n", - " for retry in range(25):\n", - " time.sleep(5)\n", - " status = job.status()\n", - " if status == \"DONE\":\n", - " print(\"Job completed successfully\")\n", - " return\n", - " if status not in [\n", - " \"QUEUED\",\n", - " \"INITIALIZING\",\n", - " \"RUNNING\",\n", - " \"RUNNING: OPTIMIZING_FOR_HARDWARE\",\n", - " \"DONE\",\n", - " ]:\n", - " raise Exception(\n", - " f\"Unexpected job status '{status}'.\\nHere's the logs:\\n\"\n", - " + job.logs()\n", - " )\n", - " print(f\"Waiting for job (status '{status}')\")\n", - " raise Exception(\"Job did not complete in time\")\n", - "\n", - "\n", - "test_serverless_job(\n", - " title=\"transpile_remote_serverless_test\", entrypoint=\"transpile_remote.py\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "611fe030-4494-46b5-9ea1-9678ac513210", - "metadata": {}, - "source": [ - "### Explore different task configurations\n", - "\n", - "You can flexibly allocate CPU, GPU, and memory for your tasks via `@distribute_task()`. For Qiskit Serverless on IBM Quantum® Platform, each program is equipped with 16 CPU cores and 32 GB RAM, which can be allocated dynamically as needed.\n", - "\n", - "CPU cores can be allocated as full CPU cores, or even fractional allocations, as shown in the following.\n", - "\n", - "Memory is allocated in number of bytes. Recall that there are 1024 bytes in a kilobyte, 1024 kilobytes in a megabyte, and 1024 megabytes in a gigabyte. To allocate 2 GB of memory for your worker, you need to allocate `\"mem\": 2 * 1024 * 1024 * 1024`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cea90969-cfbf-4181-9ffa-524f3709dc69", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Appending to ./source_files/transpile_remote.py\n" - ] - } - ], - "source": [ - "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "@distribute_task(target={\n", - " \"cpu\": 16,\n", - " \"mem\": 2 * 1024 * 1024 * 1024\n", - "})\n", - "def transpile_remote(circuit, optimization_level, backend):\n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "55163053-2cd8-4e5d-8470-d08055a6f401", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'QUEUED')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job completed successfully\n" - ] - } - ], - "source": [ - "# This cell is hidden from users.\n", - "# It checks the distributed program works.\n", - "test_serverless_job(\n", - " title=\"transpile_remote_serverless_test\", entrypoint=\"transpile_remote.py\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "6bc45489-56d0-4f46-8659-9df4d1555516", - "metadata": {}, - "source": [ - "## Manage data across your program\n", - "\n", - "Qiskit Serverless allows you to manage files in the `/data` directory across all your programs. This includes several limitations:\n", - "\n", - "- Only `tar` and `h5` files are supported today\n", - "- This is only a flat `/data` storage, and cannot have `/data/folder/` subdirectories\n", - "\n", - "The following shows how to upload files. Be sure you have authenticated to Qiskit Serverless with your IBM Quantum account (see [Upload to Qiskit Serverless](/docs/guides/serverless-first-program#upload-to-qiskit-serverless) for instructions)." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "0183278f-8ce3-4466-9255-097b2d211052", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'{\"message\":\"/usr/src/app/media/5e1f442128cdf60018496a04/transpile_demo.tar\"}'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import tarfile\n", - "from qiskit_serverless import IBMServerlessClient\n", - "\n", - "# Create a tar\n", - "filename = \"transpile_demo.tar\"\n", - "file = tarfile.open(filename, \"w\")\n", - "file.add(\"./source_files/transpile_remote.py\")\n", - "file.close()\n", - "\n", - "# Get a reference to a QiskitFunction\n", - "serverless = IBMServerlessClient()\n", - "transpile_remote_demo = next(\n", - " program\n", - " for program in serverless.list()\n", - " if program.title == \"transpile_remote_serverless\"\n", - ")\n", - "\n", - "# Upload the tar to Serverless data directory\n", - "serverless.file_upload(file=filename, function=transpile_remote_demo)" - ] - }, - { - "cell_type": "markdown", - "id": "4f762470-945f-48d5-a65b-c60d3b2dae3f", - "metadata": {}, - "source": [ - "Next, you can list all the files in your `data` directory. This data is accessible to all programs." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "14241fc4-d0cb-4803-8752-a460e1f48708", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['classifier_name.pkl.tar', 'output.json.tar', 'transpile_demo.tar']" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "serverless.files(function=transpile_remote_demo)" - ] - }, - { - "cell_type": "markdown", - "id": "a97bd83e-8250-43bb-b1c4-d40d822c7ba2", - "metadata": {}, - "source": [ - "This can be done from a program by using `file_download()` to download the file to the program environment, and uncompressing the `tar`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef649b2a-ed95-4dd2-89d9-61438faa7c1e", - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile ./source_files/extract_tarfile.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", - "\n", - "import tarfile\n", - "from qiskit_serverless import IBMServerlessClient\n", - "\n", - "serverless = IBMServerlessClient(token=\"\") # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", - "files = serverless.files()\n", - "demo_file = files[0]\n", - "downloaded_tar = serverless.file_download(demo_file)\n", - "\n", - "\n", - "with tarfile.open(downloaded_tar, 'r') as tar:\n", - " tar.extractall()" - ] - }, - { - "cell_type": "markdown", - "id": "5b93dbdb-2060-468b-8496-ba98142a780b", - "metadata": {}, - "source": [ - "At this point, your program can interact with the files, as you would a local experiment. `file_upload()` , `file_download()`, and `file_delete()` can be called from your local experiment, or your uploaded program, for consistent and flexible data management." - ] - }, - { - "cell_type": "markdown", - "id": "a004dd78-0e0a-4a3b-83cb-333469533ef6", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "- See a full example that [ports existing code to Qiskit Serverless](/docs/guides/serverless-port-code).\n", - "- Read a paper in which researchers used Qiskit Serverless and quantum-centric supercomputing to [explore quantum chemistry](https://arxiv.org/abs/2405.05068v1).\n", - "\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b7abc7b-b435-43d1-9fd8-c349ee8710f3", + "metadata": {}, + "source": [ + "---\n", + "title: Manage Qiskit Serverless compute and data resources\n", + "description: Manage compute and data across your Qiskit pattern with Qiskit Serverless.\n", + "---\n", + "\n", + "\n", + "# Manage Qiskit Serverless compute and data resources" + ] + }, + { + "cell_type": "markdown", + "id": "3b0771d6-95c9-46dc-955a-f8702f6a2632", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [ + "\n", + "\n", + "\n", + "The code on this page was developed using the following requirements.\n", + "We recommend using these versions or newer.\n", + "\n", + "```\n", + "qiskit[all]~=2.0.0\n", + "qiskit-ibm-runtime~=0.37.0\n", + "qiskit-serverless~=0.22.0\n", + "```\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "95b2f280-f685-455f-83e9-b172445d7c6a", + "metadata": {}, + "source": [ + "\n", + " **Qiskit Serverless is getting an upgrade, and its features are changing fast.** During this development phase, find release notes and the most recent documentation at the [Qiskit Serverless GitHub](https://qiskit.github.io/qiskit-serverless/index.html) page.\n", + "\n", + "\n", + "With Qiskit Serverless, you can manage compute and data across your [Qiskit pattern](/docs/guides/intro-to-patterns), including CPUs, QPUs, and other compute accelerators." + ] + }, + { + "cell_type": "markdown", + "id": "380354c0-5cab-464d-b10f-c94055de3605", + "metadata": {}, + "source": [ + "## Set detailed statuses\n", + "\n", + "\n", + "Serverless workloads have several stages across a workflow. By default, the following statuses are viewable with `job.status()`:\n", + "\n", + "- **`QUEUED`**: the workload is queued for classical resources\n", + "- **`INITIALIZING`**: the workload is set up\n", + "- **`RUNNING`**: the workload is currently running on classical resources\n", + "- **`DONE`**: the workload has successfully completed\n", + "\n", + "You can also set custom statuses that further describe the specific workflow stage, as follows.\n", + "\n", + "\n", + "If you're reading the code cells locally in a notebook, you will see the `%%writefile` [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html#cellmagic-writefile). Executing cells with this magic command saves them to disk rather than executing them.\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9e41cd2f-bce6-4c8a-8e44-537c18b3023c", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users, it just creates a new folder\n", + "from pathlib import Path\n", + "\n", + "Path(\"./source_files\").mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a69df8bc-5033-45bf-a837-cffa9d29b844", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Writing ./source_files/status_example.py\n" + ] + } + ], + "source": [ + "%%writefile ./source_files/status_example.py\n", + "\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from qiskit_serverless import update_status, Job\n", + "\n", + "# # If your function has a mapping stage, particularly application functions, you can set the status\n", + "# to \"RUNNING: MAPPING\" as follows:\n", + "update_status(Job.MAPPING)\n", + "\n", + "# # While handling transpilation, error suppression, and so forth, you can set the status to\n", + "# \"RUNNING: OPTIMIZING_FOR_HARDWARE\":\n", + "update_status(Job.OPTIMIZING_HARDWARE)\n", + "\n", + "# # After you submit jobs to Qiskit Runtime, the underlying quantum job will be queued. You can set\n", + "# status to \"RUNNING: WAITING_FOR_QPU\":\n", + "update_status(Job.WAITING_QPU)\n", + "\n", + "# # When the Qiskit Runtime job starts running on the QPU, set the following status \"RUNNING:\n", + "# EXECUTING_QPU\":\n", + "update_status(Job.EXECUTING_QPU)\n", + "\n", + "## Once QPU is completed and post-processing has begun, set the status \"RUNNING: POST_PROCESSING\":\n", + "update_status(Job.POST_PROCESSING)" + ] + }, + { + "cell_type": "markdown", + "id": "a8746eae-6f15-4faf-8771-0f3062efc723", + "metadata": {}, + "source": [ + "After successful completion of this workload (with `save_result()`), this status will be updated to `DONE` automatically." + ] + }, + { + "cell_type": "markdown", + "id": "b2d40a63-3359-46e9-8f1b-4746b449b407", + "metadata": {}, + "source": [ + "## Parallel workflows\n", + "\n", + "For classical tasks that can be parallelized, use the `@distribute_task` decorator to define compute requirements needed to perform a task. Start by recalling the `transpile_remote.py` example from the [Write your first Qiskit Serverless program](/docs/guides/serverless-first-program) topic with the following code.\n", + "\n", + "The following code requires that you have already [saved your credentials](/docs/guides/cloud-setup)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "475d82f0-15cc-4db3-b3b0-54b07822b2a0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Writing ./source_files/transpile_remote.py\n" + ] + } + ], + "source": [ + "%%writefile ./source_files/transpile_remote.py\n", + "\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_serverless import distribute_task\n", + "\n", + "service = QiskitRuntimeService()\n", + "\n", + "@distribute_task(target={\"cpu\": 1})\n", + "def transpile_remote(circuit, optimization_level, backend):\n", + " \"\"\"Transpiles an abstract circuit (or list of circuits) into an ISA circuit for a given backend.\"\"\"\n", + " pass_manager = generate_preset_pass_manager(\n", + " optimization_level=optimization_level,\n", + " backend=service.backend(backend)\n", + " )\n", + " isa_circuit = pass_manager.run(circuit)\n", + " return isa_circuit" + ] + }, + { + "cell_type": "markdown", + "id": "a5914f1d-f898-4db4-8d1e-ccc8081883b9", + "metadata": {}, + "source": [ + "In this example, you decorated the `transpile_remote()` function with `@distribute_task(target={\"cpu\": 1})`. When run, this creates an asynchronous parallel worker task with a single CPU core, and returns with a reference to track the worker. To fetch the result, pass the reference to the `get()` function. We can use this to run multiple parallel tasks:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8fd31e6-9ab9-4d75-9ef9-a2b9ff9ad37a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/transpile_remote.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "from time import time\n", + "from qiskit_serverless import get, get_arguments, save_result, update_status, Job\n", + "\n", + "# Get arguments\n", + "arguments = get_arguments()\n", + "circuit = arguments.get(\"circuit\")\n", + "optimization_level = arguments.get(\"optimization_level\")\n", + "backend = arguments.get(\"backend\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74fdcd4a-01cd-46ca-aa24-2a8a3605346f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/transpile_remote.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "# Start distributed transpilation\n", + "update_status(Job.OPTIMIZING_HARDWARE)\n", + "\n", + "start_time = time()\n", + "transpile_worker_references = [\n", + " transpile_remote(circuit, optimization_level, backend)\n", + " for circuit in arguments.get(\"circuit_list\")\n", + "]\n", + "\n", + "transpiled_circuits = get(transpile_worker_references)\n", + "end_time = time()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81696ede-3aa5-4e8c-9d35-fdd70c1bf4db", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/transpile_remote.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "# Save result, with metadata\n", + "result = {\n", + " \"circuits\": transpiled_circuits,\n", + " \"metadata\": {\n", + " \"resource_usage\": {\n", + " \"RUNNING: OPTIMIZING_FOR_HARDWARE\": {\n", + " \"CPU_TIME\": end_time - start_time,\n", + " \"QPU_TIME\": 0,\n", + " },\n", + " }\n", + " },\n", + "}\n", + "\n", + "save_result(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "df28a92c-3585-49f0-a2ea-828a34638684", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job completed successfully\n" + ] + } + ], + "source": [ + "# This cell is hidden from users.\n", + "# It uploads the serverless program and checks it runs.\n", + "\n", + "\n", + "def test_serverless_job(title, entrypoint):\n", + " # Import in function to stop them interfering with user-facing code\n", + " from qiskit.circuit.random import random_circuit\n", + " from qiskit_serverless import IBMServerlessClient, QiskitFunction\n", + " import time\n", + " import uuid\n", + "\n", + " title += \"_\" + uuid.uuid4().hex[:8]\n", + " serverless = IBMServerlessClient()\n", + " transpile_remote_demo = QiskitFunction(\n", + " title=title,\n", + " entrypoint=entrypoint,\n", + " working_dir=\"./source_files/\",\n", + " )\n", + " serverless.upload(transpile_remote_demo)\n", + " job = serverless.get(title).run(\n", + " circuit=random_circuit(3, 3),\n", + " circuit_list=[random_circuit(3, 3) for _ in range(3)],\n", + " backend=\"ibm_torino\",\n", + " optimization_level=1,\n", + " )\n", + " for retry in range(25):\n", + " time.sleep(5)\n", + " status = job.status()\n", + " if status == \"DONE\":\n", + " print(\"Job completed successfully\")\n", + " return\n", + " if status not in [\n", + " \"QUEUED\",\n", + " \"INITIALIZING\",\n", + " \"RUNNING\",\n", + " \"RUNNING: OPTIMIZING_FOR_HARDWARE\",\n", + " \"DONE\",\n", + " ]:\n", + " raise Exception(\n", + " f\"Unexpected job status '{status}'.\\nHere's the logs:\\n\"\n", + " + job.logs()\n", + " )\n", + " print(f\"Waiting for job (status '{status}')\")\n", + " raise Exception(\"Job did not complete in time\")\n", + "\n", + "\n", + "test_serverless_job(\n", + " title=\"transpile_remote_serverless_test\", entrypoint=\"transpile_remote.py\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "611fe030-4494-46b5-9ea1-9678ac513210", + "metadata": {}, + "source": [ + "### Explore different task configurations\n", + "\n", + "You can flexibly allocate CPU, GPU, and memory for your tasks via `@distribute_task()`. For Qiskit Serverless on IBM Quantum® Platform, each program is equipped with 16 CPU cores and 32 GB RAM, which can be allocated dynamically as needed.\n", + "\n", + "CPU cores can be allocated as full CPU cores, or even fractional allocations, as shown in the following.\n", + "\n", + "Memory is allocated in number of bytes. Recall that there are 1024 bytes in a kilobyte, 1024 kilobytes in a megabyte, and 1024 megabytes in a gigabyte. To allocate 2 GB of memory for your worker, you need to allocate `\"mem\": 2 * 1024 * 1024 * 1024`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cea90969-cfbf-4181-9ffa-524f3709dc69", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Appending to ./source_files/transpile_remote.py\n" + ] + } + ], + "source": [ + "%%writefile --append ./source_files/transpile_remote.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "@distribute_task(target={\n", + " \"cpu\": 16,\n", + " \"mem\": 2 * 1024 * 1024 * 1024\n", + "})\n", + "def transpile_remote(circuit, optimization_level, backend):\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "55163053-2cd8-4e5d-8470-d08055a6f401", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'QUEUED')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for job (status 'RUNNING: OPTIMIZING_FOR_HARDWARE')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job completed successfully\n" + ] + } + ], + "source": [ + "# This cell is hidden from users.\n", + "# It checks the distributed program works.\n", + "test_serverless_job(\n", + " title=\"transpile_remote_serverless_test\", entrypoint=\"transpile_remote.py\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6bc45489-56d0-4f46-8659-9df4d1555516", + "metadata": {}, + "source": [ + "## Manage data across your program\n", + "\n", + "Qiskit Serverless allows you to manage files in the `/data` directory across all your programs. This includes several limitations:\n", + "\n", + "- Only `tar` and `h5` files are supported today\n", + "- This is only a flat `/data` storage, and cannot have `/data/folder/` subdirectories\n", + "\n", + "The following shows how to upload files. Be sure you have authenticated to Qiskit Serverless with your IBM Quantum account (see [Upload to Qiskit Serverless](/docs/guides/serverless-first-program#upload-to-qiskit-serverless) for instructions)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0183278f-8ce3-4466-9255-097b2d211052", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"message\":\"/usr/src/app/media/5e1f442128cdf60018496a04/transpile_demo.tar\"}'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import tarfile\n", + "from qiskit_serverless import IBMServerlessClient\n", + "\n", + "# Create a tar\n", + "filename = \"transpile_demo.tar\"\n", + "file = tarfile.open(filename, \"w\")\n", + "file.add(\"./source_files/transpile_remote.py\")\n", + "file.close()\n", + "\n", + "# Get a reference to a QiskitFunction\n", + "serverless = IBMServerlessClient()\n", + "transpile_remote_demo = next(\n", + " program\n", + " for program in serverless.list()\n", + " if program.title == \"transpile_remote_serverless\"\n", + ")\n", + "\n", + "# Upload the tar to Serverless data directory\n", + "serverless.file_upload(file=filename, function=transpile_remote_demo)" + ] + }, + { + "cell_type": "markdown", + "id": "4f762470-945f-48d5-a65b-c60d3b2dae3f", + "metadata": {}, + "source": [ + "Next, you can list all the files in your `data` directory. This data is accessible to all programs." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "14241fc4-d0cb-4803-8752-a460e1f48708", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['classifier_name.pkl.tar', 'output.json.tar', 'transpile_demo.tar']" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.files(function=transpile_remote_demo)" + ] + }, + { + "cell_type": "markdown", + "id": "a97bd83e-8250-43bb-b1c4-d40d822c7ba2", + "metadata": {}, + "source": [ + "This can be done from a program by using `file_download()` to download the file to the program environment, and uncompressing the `tar`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef649b2a-ed95-4dd2-89d9-61438faa7c1e", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile ./source_files/extract_tarfile.py\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", + "\n", + "import tarfile\n", + "from qiskit_serverless import IBMServerlessClient\n", + "\n", + "serverless = IBMServerlessClient(token=\"\") # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard\n", + "files = serverless.files()\n", + "demo_file = files[0]\n", + "downloaded_tar = serverless.file_download(demo_file)\n", + "\n", + "\n", + "with tarfile.open(downloaded_tar, 'r') as tar:\n", + " tar.extractall()" + ] + }, + { + "cell_type": "markdown", + "id": "5b93dbdb-2060-468b-8496-ba98142a780b", + "metadata": {}, + "source": [ + "At this point, your program can interact with the files, as you would a local experiment. `file_upload()` , `file_download()`, and `file_delete()` can be called from your local experiment, or your uploaded program, for consistent and flexible data management." + ] + }, + { + "cell_type": "markdown", + "id": "a004dd78-0e0a-4a3b-83cb-333469533ef6", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- See a full example that [ports existing code to Qiskit Serverless](/docs/guides/serverless-port-code).\n", + "- Read a paper in which researchers used Qiskit Serverless and quantum-centric supercomputing to [explore quantum chemistry](https://arxiv.org/abs/2405.05068v1).\n", + "\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/advanced-techniques-for-qaoa.ipynb b/docs/tutorials/advanced-techniques-for-qaoa.ipynb index 60deeadbfb3..4fdb4df820a 100644 --- a/docs/tutorials/advanced-techniques-for-qaoa.ipynb +++ b/docs/tutorials/advanced-techniques-for-qaoa.ipynb @@ -1,1428 +1,1430 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "1a869c2d-ae51-47d3-8a41-bfcf5e505f59", - "metadata": {}, - "source": [ - "---\n", - "title: Advanced techniques for QAOA\n", - "description: This notebook introduces advanced techniques to improve the performance of the Quantum Approximate Optimization Algorithm (QAOA) with a large number of qubits.\n", - "---\n", - "{/* cspell:ignore pysat, lbrack, frameon, IEICE */}\n", - "\n", - "# Advanced techniques for QAOA\n", - "\n", - "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime may vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "1a6bbeef-966f-45e1-89aa-e964ef891eeb", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "Users should be familiar with the basics of the Quantum Approximate Optimization Algorithm (QAOA). See the following resources for an introduction to QAOA:\n", - "\n", - "* The [Solve utility-scale quantum optimization problems](/docs/tutorials/quantum-approximate-optimization-algorithm) tutorial\n", - "* The [Utility-scale QAOA](/learning/courses/quantum-computing-in-practice/utility-scale-qaoa#utility-scale-qaoa) lesson (part of the [Quantum computing in practice](/learning/courses/quantum-computing-in-practice) course) in IBM Quantum® Learning\n", - "\n", - "## Learning outcomes\n", - "\n", - "After going through this tutorial, users should be able to do the following:\n", - "* Use advanced techniques that help improve QAOA transpilation and provide a means for improving QAOA performance\n", - "\n", - "For a detailed walkthrough of the contents of this tutorial, see this [Qiskit video](https://youtu.be/rBfK-l-qSNk?si=PC28gFAdu4JYSYdk)." - ] - }, - { - "cell_type": "markdown", - "id": "ea97567d-810f-4cca-8edf-a47d70ea870a", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "This tutorial introduces two advanced techniques to improve the performance of the **Quantum Approximate Optimization Algorithm (QAOA)** at a large number of qubits.\n", - "\n", - "\n", - "The advanced techniques in this notebook include:\n", - "\n", - "* **SWAP strategy with SAT initial mapping**: This is a specifically designed transpiler pass for QAOA that uses a SWAP strategy and a SAT solver together to improve the selection of which physical qubits on the QPU to use. The SWAP strategy exploits the commutativity of the QAOA operators to reorder gates so that layers of SWAP gates can be simultaneously executed, thus reducing the depth of the circuit [\\[1\\]](#references). The SAT solver is used to find an initial mapping that minimizes the number of SWAP operations needed to map the qubits in the circuit to the physical qubits on the device [\\[2\\]](#references).\n", - "* **CVaR cost function**: Typically the expected value of the cost Hamiltonian is used as the cost function for QAOA, but as was shown in [\\[3\\]](#references), focusing on the tail of the distribution, rather than the expected value, can improve the performance of QAOA for combinatorial optimization problems. The CVaR accomplishes this. For a given set of shots with corresponding objective values of the considered optimization problem, the Conditional Value at Risk (CVaR) with confidence level $\\alpha \\in [0, 1]$ is defined as the average of the $\\alpha$ best shots [\\[3\\]](#references).\n", - "Thus, $\\alpha = 1$ corresponds to the standard expected value, while $\\alpha=0$ corresponds to the minimum of the given shots, and $\\alpha \\in (0, 1)$ is a tradeoff between focusing on better shots, while still applying some averaging to smooth out the optimization landscape. Additionally, the CVaR can be used as an error mitigation technique to improve the quality of the objective value estimation [\\[4\\]](#references).\n", - "\n", - "By the end of this tutorial, you should be able to use these techniques to get the best results from running QAOA for your optimization problems." - ] - }, - { - "cell_type": "markdown", - "id": "40fb546e-85e0-450b-a5ea-5d08950d129f", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "\n", - "* Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "* Qiskit Runtime v0.43 or later (`pip install qiskit-ibm-runtime`)\n", - "* Rustworkx graph library (`pip install rustworkx`)\n", - "* Python SAT (`pip install python-sat`)" - ] - }, - { - "cell_type": "markdown", - "id": "50285e5f-1a7b-471c-a223-1ae0af19d9ed", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 91, - "id": "d019ea68-61e0-4341-84c7-e612ca10dde7", - "metadata": {}, - "outputs": [], - "source": [ - "from __future__ import annotations\n", - "\n", - "import numpy as np\n", - "import rustworkx as rx\n", - "from dataclasses import dataclass\n", - "from itertools import combinations\n", - "from threading import Timer\n", - "from collections.abc import Callable, Iterable\n", - "from pysat.formula import CNF, IDPool\n", - "from pysat.solvers import Solver\n", - "from scipy.optimize import minimize\n", - "from rustworkx.visualization import mpl_draw as draw_graph\n", - "\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.circuit.library import QAOAAnsatz\n", - "from qiskit.circuit import QuantumCircuit, ParameterVector\n", - "from qiskit.transpiler import CouplingMap, PassManager\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import (\n", - " SwapStrategy,\n", - " FindCommutingPauliEvolutions,\n", - " Commuting2qGateRouter,\n", - ")\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Session\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler" - ] - }, - { - "cell_type": "markdown", - "id": "8b77b0c9-f5a6-476e-86b8-069ba14f9ab3", - "metadata": {}, - "source": [ - "### Max-cut problem\n", - "\n", - "Let's consider solving the **max-cut** problem on a graph with 100 nodes using QAOA.\n", - "The max-cut problem is a combinatorial optimization problem that is defined on a graph $G = (V, E)$, where $V$ is the set of vertices and $E$ is the set of edges. The goal is to partition the vertices into two sets, $S$ and $V \\setminus S$, such that the number of edges between the two sets is maximized.\n", - "In this example, we use a graph with 100 nodes that is based on a hardware coupling map." - ] - }, - { - "cell_type": "markdown", - "id": "663d13f5-a5f7-4b67-a89b-26deb41224ec", - "metadata": {}, - "source": [ - "## Small-scale simulator example\n", - "\n", - "Since the goal of this tutorial is to show how QAOA performs at scales beyond what a simulator can handle, we will forgo this step.\n", - "\n", - "If you would like to try a simulator-based QAOA workflow, try out the [Quantum approximate optimization algorithm](/docs/tutorials/quantum-approximate-optimization-algorithm) tutorial." - ] - }, - { - "cell_type": "markdown", - "id": "6ca05a7c-5fde-4485-a3ad-c0ba02844e50", - "metadata": {}, - "source": [ - "## Large-scale hardware example" - ] - }, - { - "cell_type": "markdown", - "id": "b825afbf-10fd-4926-bd65-05272044f107", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "#### Graph → Hamiltonian\n", - "\n", - "First, map the problem onto a quantum circuit that is suited for the QAOA. Details on this process can be found in the [introductory QAOA tutorial](/docs/tutorials/quantum-approximate-optimization-algorithm)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8c25db38-5f36-4a39-bdde-4f83616a6dde", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "# Instantiate runtime to access backend\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " min_num_qubits=100, operational=True, simulator=False\n", - ")\n", - "print(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e04ce982-8294-4ebe-8dcc-f74205938800", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 96, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check if the coupling map is symmetric. We will add a conditional below\n", - "# to avoid over-counting edges for symmetric/bi-directional coupling maps.\n", - "\n", - "backend.coupling_map.is_symmetric" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b864d32-9483-45ea-831f-60488e330adb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "n = 100\n", - "graph_100 = rx.PyGraph()\n", - "graph_100.add_nodes_from((np.arange(0, n, 1)))\n", - "w = 1.0\n", - "elist = []\n", - "\n", - "for edge in backend.coupling_map:\n", - " if (edge[0] < n) and (edge[1] < n):\n", - " # Conditional to avoid over-counting edges\n", - " if (\n", - " edge[1],\n", - " edge[0],\n", - " w,\n", - " ) not in elist:\n", - " elist.append((edge[0], edge[1], w))\n", - "\n", - "graph_100.add_edges_from(elist)\n", - "draw_graph(graph_100, with_labels=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 98, - "id": "e39c4e42-ce97-4a04-8879-da4d33a684bc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cost Function Hamiltonian: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j])\n" - ] - } - ], - "source": [ - "# Construct cost hamiltonian\n", - "\n", - "\n", - "def build_max_cut_paulis(graph: rx.PyGraph) -> list[tuple[str, float]]:\n", - " \"\"\"Convert the graph to Pauli list.\n", - "\n", - " This function does the inverse of `build_max_cut_graph`\n", - " \"\"\"\n", - " pauli_list = []\n", - " for edge in list(graph.edge_list()):\n", - " paulis = [\"I\"] * len(graph)\n", - " paulis[edge[0]], paulis[edge[1]] = \"Z\", \"Z\"\n", - "\n", - " weight = graph.get_edge_data(edge[0], edge[1])\n", - "\n", - " pauli_list.append((\"\".join(paulis)[::-1], weight))\n", - "\n", - " return pauli_list\n", - "\n", - "\n", - "max_cut_paulis = build_max_cut_paulis(graph_100)\n", - "\n", - "cost_hamiltonian = SparsePauliOp.from_list(max_cut_paulis)\n", - "print(\"Cost Function Hamiltonian:\", cost_hamiltonian)" - ] - }, - { - "cell_type": "markdown", - "id": "4e576068-53e7-4a06-a83b-87e95de141e9", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "#### SWAP strategy with the SAT initial mapping\n", - "\n", - "We will demonstrate how to build and optimize QAOA circuits using the **SWAP strategy with SAT initial mapping**, a specifically designed transpiler pass for QAOA applied to quadratic problems.\n", - "\n", - "In this example, we choose a SWAP insertion strategy for blocks of commuting two-qubit gates, which applies layers of SWAP gates that are simultaneously executable on the coupling map. This strategy is presented in [\\[1\\]](#references) and is passed into `Commuting2qGateRouter`, which is exposed as a standardized Qiskit transpiler pass (see [`Commuting2qGateRouter`](/docs/api/qiskit/qiskit.transpiler.passes.Commuting2qGateRouter)). We use a line swap strategy in this example." - ] - }, - { - "cell_type": "code", - "execution_count": 99, - "id": "7c54402a-7696-4b0e-826a-6e0ac4c54395", - "metadata": {}, - "outputs": [], - "source": [ - "# Extract longest path with no repeated nodes\n", - "nodes = rx.longest_simple_path(graph_100)\n", - "\n", - "# Collect even edges and odd edges\n", - "even_edges = [\n", - " (nodes[i], nodes[i + 1])\n", - " if nodes[i] < nodes[i + 1]\n", - " else (nodes[i + 1], nodes[i])\n", - " for i in range(0, len(nodes) - 1, 2)\n", - "]\n", - "odd_edges = [\n", - " (nodes[i], nodes[i + 1])\n", - " if nodes[i] < nodes[i + 1]\n", - " else (nodes[i + 1], nodes[i])\n", - " for i in range(1, len(nodes) - 1, 2)\n", - "]\n", - "edge_list = [\n", - " (edge[0], edge[1]) if edge[0] < edge[1] else (edge[1], edge[0])\n", - " for edge in graph_100.edge_list()\n", - "]\n", - "\n", - "swap_strategy = SwapStrategy(CouplingMap(edge_list), (even_edges, odd_edges))" - ] - }, - { - "cell_type": "markdown", - "id": "ba53fd1d-0d68-43e3-9102-4c6e64f04de7", - "metadata": {}, - "source": [ - "#### Remap the graph using a SAT mapper\n", - "\n", - "Even when a circuit consists of commuting gates (this is the case for the QAOA circuit, but also for Trotterized simulations of Ising Hamiltonians), finding a good initial mapping is a challenging task. When we use the SAT-based approach presented in [\\[2\\]](#references), we can discover effective initial mappings for circuits with commuting gates, resulting in a significant reduction in the number of required SWAP layers. This approach has been demonstrated to scale to up to *500 qubits*, as illustrated in the paper.\n", - "\n", - "The following code demonstrates how to use the `SATMapper` from Matsuo et al. to remap the graph. This process allows the problem to be mapped to a more optimal initial state for a specified SWAP strategy, resulting in a significant reduction in the number of SWAP layers required to execute the circuit.\n", - "\n", - "In `SATMapper`, the problem of finding a good initial mapping is formulated as a SAT problem. A SAT solver is used to find such an initial mapping for the QAOA circuit. `python-sat` (`pysat` for short) is a Python library for a SAT solver, and we will use it to solve the SAT problem in this example." - ] - }, - { - "cell_type": "code", - "execution_count": 100, - "id": "df9d861a-64e8-49ee-aed1-c41f437fa743", - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"A class to solve the SWAP gate insertion initial mapping problem\n", - "using the SAT approach from https://arxiv.org/abs/2212.05666.\n", - "\"\"\"\n", - "\n", - "\n", - "@dataclass\n", - "class SATResult:\n", - " \"\"\"A data class to hold the result of a SAT solver.\"\"\"\n", - "\n", - " satisfiable: bool # Satisfiable is True if the SAT model could be solved\n", - " # in a given time.\n", - " solution: dict # The solution to the SAT problem if it is satisfiable.\n", - " mapping: list # The mapping of nodes in the pattern graph to nodes in the\n", - " # target graph.\n", - " elapsed_time: float # The time it took to solve the SAT model.\n", - "\n", - "\n", - "class SATMapper:\n", - " r\"\"\"A class to introduce a SAT-approach to solve\n", - " the initial mapping problem in SWAP gate insertion for commuting gates.\n", - "\n", - " When this pass is run on a DAG it will look for the first instance of\n", - " :class:`.Commuting2qBlock` and use the program graph :math:`P` of this block\n", - " of gates to find a layout for a given swap strategy. This layout is found\n", - " with a binary search over the layers :math:`l` of the swap strategy. At each\n", - " considered layer a subgraph isomorphism problem formulated as a SAT is solved\n", - " by a SAT solver. Each instance is whether it is possible to embed the program\n", - " graph :math:`P` into the effective connectivity graph :math:`C_l` that is\n", - " achieved by applying :math:`l` layers of the swap strategy to the coupling map\n", - " :math:`C_0` of the backend. Since solving SAT problems can be hard, a\n", - " ``time_out`` fixes the maximum time allotted to the SAT solver for each\n", - " instance. If this time is exceeded the considered problem is deemed\n", - " unsatisfiable and the binary search proceeds to the next number of swap\n", - " layers :math:``l``.\n", - " \"\"\"\n", - "\n", - " def __init__(self, timeout: int = 60):\n", - " \"\"\"Initialize the SATMapping.\n", - "\n", - " Args:\n", - " timeout: The allowed time in seconds for each iteration of the SAT\n", - " solver. This variable defaults to 60 seconds.\n", - " \"\"\"\n", - " self.timeout = timeout\n", - "\n", - " def find_initial_mappings(\n", - " self,\n", - " program_graph: rx.Graph,\n", - " swap_strategy: SwapStrategy,\n", - " min_layers: int | None = None,\n", - " max_layers: int | None = None,\n", - " ) -> dict[int, SATResult]:\n", - " r\"\"\"Find an initial mapping for a given swap strategy. Perform a\n", - " binary search over the number of swap layers, and for each number\n", - " of swap layers solve a subgraph isomorphism problem formulated as\n", - " a SAT problem.\n", - "\n", - " Args:\n", - " program_graph (rx.Graph): The program graph with commuting gates, where\n", - " each edge represents a two-qubit gate.\n", - " swap_strategy (SwapStrategy): The swap strategy to use to find the\n", - " initial mapping.\n", - " min_layers (int): The minimum number of swap layers to consider.\n", - " Defaults to the maximum degree of the\n", - " program graph - 2.\n", - " max_layers (int): The maximum number of swap layers to consider.\n", - " Defaults to the number of qubits in the\n", - " swap strategy - 2.\n", - "\n", - " Returns:\n", - " dict[int, SATResult]: A dictionary containing the results of the SAT\n", - " solver for each number of swap layers.\n", - " \"\"\"\n", - " num_nodes_g1 = len(program_graph.nodes())\n", - " num_nodes_g2 = swap_strategy.distance_matrix.shape[0]\n", - " if num_nodes_g1 > num_nodes_g2:\n", - " return SATResult(False, [], [], 0)\n", - " if min_layers is None:\n", - " # use the maximum degree of the program graph - 2\n", - " # as the lower bound.\n", - " min_layers = max((d for _, d in program_graph.degree)) - 2\n", - " if max_layers is None:\n", - " max_layers = num_nodes_g2 - 1\n", - "\n", - " variable_pool = IDPool(start_from=1)\n", - " variables = np.array(\n", - " [\n", - " [variable_pool.id(f\"v_{i}_{j}\") for j in range(num_nodes_g2)]\n", - " for i in range(num_nodes_g1)\n", - " ],\n", - " dtype=int,\n", - " )\n", - " vid2mapping = {v: idx for idx, v in np.ndenumerate(variables)}\n", - " binary_search_results = {}\n", - "\n", - " def interrupt(solver):\n", - " # This function is called to interrupt the solver when the\n", - " # timeout is reached.\n", - " solver.interrupt()\n", - "\n", - " # Make a cnf (conjunctive normal form) for the one-to-one\n", - " # mapping constraint\n", - " cnf1 = []\n", - " for i in range(num_nodes_g1):\n", - " clause = variables[i, :].tolist()\n", - " cnf1.append(clause)\n", - " for k, m in combinations(clause, 2):\n", - " cnf1.append([-1 * k, -1 * m])\n", - " for j in range(num_nodes_g2):\n", - " clause = variables[:, j].tolist()\n", - " for k, m in combinations(clause, 2):\n", - " cnf1.append([-1 * k, -1 * m])\n", - "\n", - " # Perform a binary search over the number of swap layers to find the\n", - " # minimum number of swap layers that satisfies the subgraph isomorphism\n", - " # problem.\n", - " while min_layers < max_layers:\n", - " num_layers = (min_layers + max_layers) // 2\n", - "\n", - " # Create the connectivity matrix. Note that if the swap strategy\n", - " # cannot reach full connectivity then its distance matrix will have\n", - " # entries with -1. These entries must be treated as False.\n", - " d_matrix = swap_strategy.distance_matrix\n", - " connectivity_matrix = (\n", - " (-1 < d_matrix) & (d_matrix <= num_layers)\n", - " ).astype(int)\n", - " # Make a cnf for the adjacency constraint\n", - " cnf2 = []\n", - " for e_0, e_1 in list(program_graph.edge_list()):\n", - " clause_matrix = np.multiply(\n", - " connectivity_matrix, variables[e_1, :]\n", - " )\n", - " clause = np.concatenate(\n", - " (\n", - " [[-variables[e_0, i]] for i in range(num_nodes_g2)],\n", - " clause_matrix,\n", - " ),\n", - " axis=1,\n", - " )\n", - " # Remove 0s from each clause\n", - " cnf2.extend([c[c != 0].tolist() for c in clause])\n", - "\n", - " cnf = CNF(from_clauses=cnf1 + cnf2)\n", - "\n", - " with Solver(bootstrap_with=cnf, use_timer=True) as solver:\n", - " # Solve the SAT problem with a timeout.\n", - " # Timer is used to interrupt the solver when the\n", - " # timeout is reached.\n", - " timer = Timer(self.timeout, interrupt, [solver])\n", - " timer.start()\n", - " status = solver.solve_limited(expect_interrupt=True)\n", - " timer.cancel()\n", - " # Get the solution and the elapsed time.\n", - " sol = solver.get_model()\n", - " e_time = solver.time()\n", - "\n", - " print(\n", - " f\"Layers: {num_layers}, Status: {status}, Time: {e_time}\"\n", - " )\n", - " if status:\n", - " # If the SAT problem is satisfiable, convert the solution\n", - " # to a mapping.\n", - " mapping = [vid2mapping[idx] for idx in sol if idx > 0]\n", - " binary_search_results[num_layers] = SATResult(\n", - " status, sol, mapping, e_time\n", - " )\n", - " max_layers = num_layers\n", - " else:\n", - " # If the SAT problem is unsatisfiable, return the last\n", - " # satisfiable solution.\n", - " binary_search_results[num_layers] = SATResult(\n", - " status, sol, [], e_time\n", - " )\n", - " min_layers = num_layers + 1\n", - "\n", - " return binary_search_results\n", - "\n", - " def remap_graph_with_sat(\n", - " self, graph: rx.Graph, swap_strategy, max_layers\n", - " ):\n", - " \"\"\"Applies the SAT mapping.\n", - "\n", - " Args:\n", - " graph (nx.Graph): The graph to remap.\n", - " swap_strategy (SwapStrategy): The swap strategy to use\n", - " to find the initial mapping.\n", - "\n", - " Returns:\n", - " tuple: A tuple containing the remapped graph, the edge map, and the\n", - " number of layers of the swap strategy that was used to find the\n", - " initial mapping. If no solution is found then the tuple contains\n", - " None for each element. Note the returned edge map `{k: v}` means that\n", - " node `k` in the original graph gets mapped to node `v` in the\n", - " Pauli strings.\n", - " \"\"\"\n", - " num_nodes = len(graph.nodes())\n", - " results = self.find_initial_mappings(\n", - " graph, swap_strategy, 0, max_layers\n", - " )\n", - " solutions = [k for k, v in results.items() if v.satisfiable]\n", - "\n", - " if len(solutions):\n", - " min_k = min(solutions)\n", - " edge_map = dict(results[min_k].mapping)\n", - " # Create the remapped graph\n", - " remapped_graph = rx.PyGraph()\n", - " remapped_graph.add_nodes_from(range(num_nodes))\n", - " mapping = dict(results[min_k].mapping)\n", - " for i, graph_edge in enumerate(list(graph.edge_list())):\n", - " remapped_edge = tuple(mapping[node] for node in graph_edge)\n", - " remapped_graph.add_edge(*remapped_edge, graph.edges()[i])\n", - " return remapped_graph, edge_map, min_k\n", - " else:\n", - " return None, None, None" - ] - }, - { - "cell_type": "code", - "execution_count": 101, - "id": "e689e09e-6ca7-4154-8602-d1d954ebe80b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Layers: 0, Status: True, Time: 0.022812999999999306\n", - "Map from old to new nodes: {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}\n", - "Min SWAP layers: 0\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "sm = SATMapper(timeout=10)\n", - "remapped_graph, edge_map, min_swap_layers = sm.remap_graph_with_sat(\n", - " graph=graph_100, swap_strategy=swap_strategy, max_layers=1\n", - ")\n", - "print(\"Map from old to new nodes: \", edge_map)\n", - "print(\"Min SWAP layers:\", min_swap_layers)\n", - "draw_graph(remapped_graph, node_size=200, with_labels=True, width=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 102, - "id": "ecf6e8c3-65c2-4430-8dd3-d67b8842045d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", - " 1.+0.j, 1.+0.j, 1.+0.j])\n" - ] - } - ], - "source": [ - "remapped_max_cut_paulis = build_max_cut_paulis(remapped_graph)\n", - "# define a qiskit SparsePauliOp from the list of paulis\n", - "remapped_cost_operator = SparsePauliOp.from_list(remapped_max_cut_paulis)\n", - "print(remapped_cost_operator)" - ] - }, - { - "cell_type": "markdown", - "id": "5ae531be-80eb-4acd-b84a-7d466fd872e7", - "metadata": {}, - "source": [ - "#### Build a QAOA circuit with the SWAP strategy and the SAT mapping\n", - "\n", - "We only want to apply the SWAP strategies to the cost operator layer, so we start by creating the isolated block that we will later transform and append to the final QAOA circuit.\n", - "\n", - "For this, we can use the [`QAOAAnsatz`](/docs/api/qiskit/qiskit.circuit.library.QAOAAnsatz) class from Qiskit. We input an empty circuit to the `initial_state` and `mixer_operator` fields to make sure we are building an isolated cost operator layer.\n", - "We also define the `edge_coloring` map so that RZZ gates are positioned next to SWAP gates. This strategic placement allows us to exploit CX cancellations, optimizing the circuit for better performance.\n", - "This process is executed within the `create_qaoa_swap_circuit` function." - ] - }, - { - "cell_type": "code", - "execution_count": 103, - "id": "57d5eb53-9cda-4c38-a00b-26ed4b533bcd", - "metadata": {}, - "outputs": [], - "source": [ - "def make_meas_map(circuit: QuantumCircuit) -> dict:\n", - " \"\"\"Return a mapping from qubit index (the key) to classical bit (the value).\n", - "\n", - " This allows us to account for the swapping order introduced by the SWAP strategy.\n", - " \"\"\"\n", - " creg = circuit.cregs[0]\n", - " qreg = circuit.qregs[0]\n", - "\n", - " meas_map = {}\n", - " for inst in circuit.data:\n", - " if inst.operation.name == \"measure\":\n", - " meas_map[qreg.index(inst.qubits[0])] = creg.index(inst.clbits[0])\n", - "\n", - " return meas_map\n", - "\n", - "\n", - "def apply_swap_strategy(\n", - " circuit: QuantumCircuit,\n", - " swap_strategy: SwapStrategy,\n", - " edge_coloring: dict[tuple[int, int], int] | None = None,\n", - ") -> QuantumCircuit:\n", - " \"\"\"Transpile with a SWAP strategy.\n", - "\n", - " Returns:\n", - " A quantum circuit transpiled with the given swap strategy.\n", - " \"\"\"\n", - "\n", - " pm_pre = PassManager(\n", - " [\n", - " FindCommutingPauliEvolutions(),\n", - " Commuting2qGateRouter(\n", - " swap_strategy,\n", - " edge_coloring,\n", - " ),\n", - " ]\n", - " )\n", - " return pm_pre.run(circuit)\n", - "\n", - "\n", - "def apply_qaoa_layers(\n", - " cost_layer: QuantumCircuit,\n", - " meas_map: dict,\n", - " num_layers: int,\n", - " gamma: list[float] | ParameterVector = None,\n", - " beta: list[float] | ParameterVector = None,\n", - " initial_state: QuantumCircuit = None,\n", - " mixer: QuantumCircuit = None,\n", - "):\n", - " \"\"\"Applies QAOA layers to construct circuit.\n", - "\n", - " First, the initial state is applied. If `initial_state` is None, we begin in the\n", - " initial superposition state. Next, we alternate between layers of the cost operator\n", - " and the mixer. The cost operator is alternatively applied in order and in reverse\n", - " instruction order. This allows us to apply the swap strategy on odd `p` layers\n", - " and undo the swap strategy on even `p` layers.\n", - " \"\"\"\n", - "\n", - " num_qubits = cost_layer.num_qubits\n", - " new_circuit = QuantumCircuit(num_qubits, num_qubits)\n", - "\n", - " if initial_state is not None:\n", - " new_circuit.append(initial_state, range(num_qubits))\n", - " else:\n", - " # all h state by default\n", - " new_circuit.h(range(num_qubits))\n", - "\n", - " if gamma is None or beta is None:\n", - " gamma = ParameterVector(\"γ'\", num_layers)\n", - " if mixer is None or mixer.num_parameters == 0:\n", - " beta = ParameterVector(\"β'\", num_layers)\n", - " else:\n", - " beta = ParameterVector(\"β'\", num_layers * mixer.num_parameters)\n", - "\n", - " if mixer is not None:\n", - " mixer_layer = mixer\n", - " else:\n", - " mixer_layer = QuantumCircuit(num_qubits)\n", - " mixer_layer.rx(-2 * beta[0], range(num_qubits))\n", - "\n", - " for layer in range(num_layers):\n", - " bind_dict = {cost_layer.parameters[0]: gamma[layer]}\n", - " cost_layer_ = cost_layer.assign_parameters(bind_dict)\n", - " bind_dict = {\n", - " mixer_layer.parameters[i]: beta[layer + i]\n", - " for i in range(mixer_layer.num_parameters)\n", - " }\n", - " layer_mixer = mixer_layer.assign_parameters(bind_dict)\n", - "\n", - " if layer % 2 == 0:\n", - " new_circuit.append(cost_layer_, range(num_qubits))\n", - " else:\n", - " new_circuit.append(cost_layer_.reverse_ops(), range(num_qubits))\n", - "\n", - " new_circuit.append(layer_mixer, range(num_qubits))\n", - "\n", - " for qidx, cidx in meas_map.items():\n", - " new_circuit.measure(qidx, cidx)\n", - "\n", - " return new_circuit\n", - "\n", - "\n", - "def create_qaoa_swap_circuit(\n", - " cost_operator: SparsePauliOp,\n", - " swap_strategy: SwapStrategy,\n", - " edge_coloring: dict = None,\n", - " theta: list[float] = None,\n", - " qaoa_layers: int = 1,\n", - " initial_state: QuantumCircuit = None,\n", - " mixer: QuantumCircuit = None,\n", - "):\n", - " \"\"\"Create the circuit for QAOA.\n", - "\n", - " Notes: This circuit construction for QAOA works for quadratic terms in `Z` and will be\n", - " extended to first-order terms in `Z`. Higher-orders are not supported.\n", - "\n", - " Args:\n", - " cost_operator: the cost operator.\n", - " swap_strategy: selected swap strategy\n", - " edge_coloring: A coloring of edges that should correspond to the coupling\n", - " map of the hardware. It defines the order in which we apply the Rzz\n", - " gates. This allows us to choose an ordering such that `Rzz` gates will\n", - " immediately precede SWAP gates to leverage CNOT cancellation.\n", - " theta: The QAOA angles.\n", - " qaoa_layers: The number of layers of the cost operator and the mixer operator.\n", - " initial_state: The initial state on which we apply layers of cost operator\n", - " and mixer.\n", - " mixer: The QAOA mixer. It will be applied as is onto the QAOA circuit. Therefore,\n", - " its output must have the same ordering of qubits as its input.\n", - " \"\"\"\n", - "\n", - " num_qubits = cost_operator.num_qubits\n", - "\n", - " if theta is not None:\n", - " gamma = theta[: len(theta) // 2]\n", - " beta = theta[len(theta) // 2 :]\n", - " qaoa_layers = len(theta) // 2\n", - " else:\n", - " gamma = beta = None\n", - "\n", - " # First, create the ansatz of one layer of QAOA without mixer\n", - " cost_layer = QAOAAnsatz(\n", - " cost_operator,\n", - " reps=1,\n", - " initial_state=QuantumCircuit(num_qubits),\n", - " mixer_operator=QuantumCircuit(num_qubits),\n", - " ).decompose()\n", - "\n", - " # This will allow us to recover the permutation of the measurements that the swaps introduce.\n", - " cost_layer.measure_all()\n", - "\n", - " # Now, apply the swap strategy for commuting gates\n", - " cost_layer = apply_swap_strategy(cost_layer, swap_strategy, edge_coloring)\n", - "\n", - " # Compute the measurement map (qubit to classical bit).\n", - " # We will apply this for odd layers where the swaps were inserted.\n", - " if qaoa_layers % 2 == 1:\n", - " meas_map = make_meas_map(cost_layer)\n", - " else:\n", - " meas_map = {idx: idx for idx in range(num_qubits)}\n", - "\n", - " cost_layer.remove_final_measurements()\n", - "\n", - " # Finally, introduce the mixer circuit and add measurements following measurement map\n", - " circuit = apply_qaoa_layers(\n", - " cost_layer, meas_map, qaoa_layers, gamma, beta, initial_state, mixer\n", - " )\n", - "\n", - " return circuit" - ] - }, - { - "cell_type": "code", - "execution_count": 104, - "id": "7793ef92-ce59-4fd7-b43f-48d4e3427e3a", - "metadata": {}, - "outputs": [], - "source": [ - "# We can define the edge_coloring map so that RZZ gates are positioned right before SWAP gates to exploit CX cancellations\n", - "# We use greedy edge coloring from rustworkx to color the edges of the graph. This coloring is used to order the RZZ gates in the circuit.\n", - "\n", - "edge_coloring_idx = rx.graph_greedy_edge_color(graph_100)\n", - "edge_coloring = {\n", - " edge: edge_coloring_idx[idx]\n", - " for idx, edge in enumerate(list(graph_100.edge_list()))\n", - "}\n", - "edge_coloring = {tuple(sorted(k)): v for k, v in edge_coloring.items()}" - ] - }, - { - "cell_type": "code", - "execution_count": 105, - "id": "82ae28b3-85eb-4487-8100-1e622e93cccf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 105, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qaoa_circ = create_qaoa_swap_circuit(\n", - " remapped_cost_operator,\n", - " swap_strategy,\n", - " edge_coloring=edge_coloring,\n", - " qaoa_layers=1,\n", - ")\n", - "qaoa_circ.draw(output=\"mpl\", fold=False)" - ] - }, - { - "cell_type": "markdown", - "id": "e2afd1a7-0980-433b-a3a8-303d7e7718b1", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit Runtime primitives\n", - "\n", - "Let's now prepare for hardware execution. Our first step will be to define a Conditional Value at Risk (CVaR) cost function, which was introduced in [\\[3\\]](#references) for use within the paradigm of variational quantum optimization algorithms.\n", - "\n", - "The CVaR of a random variable $X$ for a confidence level $α ∈ (0, 1]$ is defined as\n", - "$CVaR_{\\alpha}(X) = \\mathbb{E} \\lbrack X | X \\leq F_X^{-1}(\\alpha) \\rbrack$\n", - "where $F_X^{-1}(p)$ is the inverse cumulative distribution function of $X$. In other words, CVaR is the expected value of the lower $\\alpha$-tail of the distribution of $X$." - ] - }, - { - "cell_type": "code", - "execution_count": 106, - "id": "172023f9-164e-43bd-bbc8-39d87628287e", - "metadata": {}, - "outputs": [], - "source": [ - "pass_manager = generate_preset_pass_manager(\n", - " backend=backend,\n", - " optimization_level=3,\n", - ")\n", - "\n", - "transpiled_qaoa_circ = pass_manager.run(qaoa_circ)" - ] - }, - { - "cell_type": "code", - "execution_count": 107, - "id": "e6794cf3-7fbe-46a5-bdc0-5faad1235365", - "metadata": {}, - "outputs": [], - "source": [ - "# Utility functions for the evaluation of the expectation value of a measured state\n", - "# In this code, for optimization, the measured state is converted into a bit string,\n", - "# and the sign of the value is determined by taking the exclusive OR of the bits\n", - "# corresponding to Pauli Z.\n", - "\n", - "_PARITY = np.array(\n", - " [-1 if bin(i).count(\"1\") % 2 else 1 for i in range(256)],\n", - " dtype=np.complex128,\n", - ")\n", - "\n", - "\n", - "def evaluate_sparse_pauli(state: int, observable: SparsePauliOp) -> complex:\n", - " \"\"\"Utility for the evaluation of the expectation value of a measured state.\n", - "\n", - " Args:\n", - " state (int): The measured state.\n", - " observable (SparsePauliOp): The observable to evaluate the expectation value for.\n", - "\n", - " Returns:\n", - " complex: The expectation value of the measured state.\n", - " \"\"\"\n", - " packed_uint8 = np.packbits(\n", - " observable.paulis.z, axis=1, bitorder=\"little\"\n", - " ) # convert observable to array with 8 bit integer\n", - " state_bytes = np.frombuffer(\n", - " state.to_bytes(packed_uint8.shape[1], \"little\"),\n", - " dtype=np.uint8, # convert bitstring to array with 8 bit integer\n", - " )\n", - " reduced = np.bitwise_xor.reduce(\n", - " packed_uint8 & state_bytes, axis=1\n", - " ) # take bitwise xor of the result of 'and' conditional on the above two, return 0 or 1\n", - " return np.sum(observable.coeffs * _PARITY[reduced])" - ] - }, - { - "cell_type": "code", - "execution_count": 108, - "id": "c5e5f7a4-01f6-4a02-9114-3bb0e24be1a2", - "metadata": {}, - "outputs": [], - "source": [ - "def qaoa_sampler_cost_fun(\n", - " params, ansatz, hamiltonian, sampler, aggregation=None\n", - "):\n", - " \"\"\"Standard sampler-based QAOA cost function to be plugged into optimizer routines.\n", - "\n", - " Args:\n", - " params (np.ndarray): Parameters for the ansatz.\n", - " ansatz (QuantumCircuit): Ansatz circuit.\n", - " hamiltonian (SparsePauliOp): Hamiltonian to be minimized.\n", - " sampler (QAOASampler): Sampler to be used.\n", - " aggregation (Callable | float | None): Aggregation function to be applied to\n", - " the sampled results. If None, the sum of the expectation values is returned.\n", - " If float, the CVaR with the given alpha is used.\n", - " \"\"\"\n", - " # Run the circuit\n", - " job = sampler.run([(ansatz, params)])\n", - " sampler_result = job.result()\n", - " sampled_int_counts = sampler_result[\n", - " 0\n", - " ].data.c.get_int_counts() # bitstrings are stored as integers\n", - " shots = sum(sampled_int_counts.values())\n", - " int_count_distribution = {\n", - " key: val / shots for key, val in sampled_int_counts.items()\n", - " }\n", - "\n", - " # a dictionary containing: {state: (measurement probability, value)}\n", - " evaluated = {\n", - " state: (\n", - " probability,\n", - " np.real(evaluate_sparse_pauli(state, hamiltonian)),\n", - " )\n", - " for state, probability in int_count_distribution.items()\n", - " }\n", - "\n", - " # If aggregation is None, return the sum of the expectation values.\n", - " # If aggregation is a float, return the CVaR with the given alpha.\n", - " # Otherwise, use the aggregation function.\n", - " if aggregation is None:\n", - " result = sum(\n", - " probability * value for probability, value in evaluated.values()\n", - " )\n", - " elif isinstance(aggregation, float):\n", - " cvar_aggregation = _get_cvar_aggregation(aggregation)\n", - " result = cvar_aggregation(evaluated.values())\n", - " else:\n", - " result = aggregation(evaluated.values())\n", - "\n", - " global iter_counts, result_dict\n", - " iter_counts += 1\n", - " temp_dict = {}\n", - " temp_dict[\"params\"] = params.tolist()\n", - " temp_dict[\"cvar_fval\"] = result\n", - " temp_dict[\"fval\"] = sum(\n", - " probability * value for probability, value in evaluated.values()\n", - " )\n", - " temp_dict[\"distribution\"] = sampled_int_counts\n", - " temp_dict[\"evaluated\"] = evaluated\n", - " result_dict[iter_counts] = temp_dict\n", - " print(f\"Iteration {iter_counts}: {result}\")\n", - "\n", - " return result\n", - "\n", - "\n", - "def _get_cvar_aggregation(alpha: float | None) -> Callable:\n", - " \"\"\"Return the CVaR aggregation function with the given alpha.\n", - "\n", - " Args:\n", - " alpha (float | None): Alpha value for the CVaR aggregation. If None, 1 is used\n", - " by default.\n", - " Raises:\n", - " ValueError: If alpha is not in [0, 1].\n", - " \"\"\"\n", - " if alpha is None:\n", - " alpha = 1\n", - " elif not 0 <= alpha <= 1:\n", - " raise ValueError(f\"alpha must be in [0, 1], but {alpha} was given.\")\n", - "\n", - " def cvar_aggregation(\n", - " objective_dict: Iterable[tuple[float, float]],\n", - " ) -> float:\n", - " \"\"\"Return the CVaR of the given measurements.\n", - " Args:\n", - " objective_dict (Iterable[tuple[float, float]]): An iterable of tuples containing\n", - " the measured bit string and the objective value based on the bit string.\n", - "\n", - " \"\"\"\n", - " sorted_measurements = sorted(objective_dict, key=lambda x: x[1])\n", - " # accumulate the probabilities until alpha is reached\n", - " accumulated_percent = 0.0\n", - " cvar = 0.0\n", - " for probability, value in sorted_measurements:\n", - " cvar += value * min(probability, alpha - accumulated_percent)\n", - " accumulated_percent += probability\n", - " if accumulated_percent >= alpha:\n", - " break\n", - " return cvar / alpha\n", - "\n", - " return cvar_aggregation" - ] - }, - { - "cell_type": "markdown", - "id": "63fa2ab4-5354-4022-ab46-e9bbf73870de", - "metadata": {}, - "source": [ - "The CVaR can be used as an error mitigation technique as previously discussed [\\[4\\]](#references). In this example, we determine $\\alpha$ and the number of shots according to the [error per layered gate](/docs/guides/qpu-information#2q-error-layered) (EPLG) associated with the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 109, - "id": "032bf312-4bf4-40f4-81f0-2ae8a719b98b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "layer fidelity 0.5454643821399414\n", - "\n", - "The corresponding CVaR aggregation value is: 0.2568730767702702\n", - "To mitigate the twirled noise, increase shots by a factor of 3.8929731857197782\n" - ] - } - ], - "source": [ - "num_2q_ops = transpiled_qaoa_circ.count_ops()[\n", - " \"cz\"\n", - "] # the two qubit gates on our backend are cz's.\n", - "\n", - "for el in backend.properties().general:\n", - " if el.name[:2] == \"lf\" and el.name[3:] == str(\n", - " n\n", - " ): # pick out lf_100, lf of the best 100q chain\n", - " lf = el.value # layer fidelity\n", - " print(\"layer fidelity\", lf)\n", - " eplg = 1 - lf ** (1 / (n - 1)) # error per layered gate (EPLG)\n", - " fid_cz = 1 - eplg\n", - " gamma_cz = 1 / fid_cz**2\n", - " gamma_circ = gamma_cz**num_2q_ops\n", - "\n", - "cvar_aggregation = 1 / np.sqrt(gamma_circ)\n", - "print(\"\")\n", - "print(\"The corresponding CVaR aggregation value is: \", cvar_aggregation)\n", - "print(\n", - " \"To mitigate the twirled noise, increase shots by a factor of\",\n", - " np.sqrt(gamma_circ),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "382e8acd-d0d0-4302-99aa-b64e5dd31e17", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iteration 1: -13.227556797094595\n", - "Iteration 2: -13.181545294899571\n", - "Iteration 3: -13.149537293372594\n", - "Iteration 4: -3.305576300816324\n", - "Iteration 5: -12.647411769418035\n", - "Iteration 6: -13.443610807401718\n", - "Iteration 7: -12.475368761210511\n", - "Iteration 8: -15.905726329447413\n", - "Iteration 9: -18.011752834505565\n", - "Iteration 10: -14.125781339945583\n", - "Iteration 11: -19.693673319331744\n", - "Iteration 12: -21.175543794613695\n", - "Iteration 13: -21.805701324676196\n", - "Iteration 14: -22.121280244318488\n", - "Iteration 15: -20.02575633517435\n", - "Iteration 16: -22.399349757584158\n", - "Iteration 17: -22.569392265696226\n", - "Iteration 18: -21.877719328111898\n", - "Iteration 19: -22.79144777628963\n", - "Iteration 20: -22.437359259397432\n", - "Iteration 21: -23.021505287264777\n", - "Iteration 22: -22.69742427180412\n", - "Iteration 23: -23.12553129222746\n", - "Iteration 24: -22.893473281156922\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -23.12553129222746\n", - " x: [ 2.766e+00 1.080e+00]\n", - " nfev: 24\n", - " maxcv: 0.0\n" - ] - } - ], - "source": [ - "iter_counts = 0\n", - "result_dict = {}\n", - "init_params = [np.pi, np.pi / 2]\n", - "\n", - "with Session(backend=backend) as session:\n", - " sampler = Sampler(mode=session)\n", - " sampler.options.default_shots = int(1000 / cvar_aggregation)\n", - " sampler.options.dynamical_decoupling.enable = True\n", - " sampler.options.dynamical_decoupling.sequence_type = \"XY4\"\n", - " sampler.options.twirling.enable_gates = True\n", - " sampler.options.twirling.enable_measure = True\n", - " sampler.options.environment.job_tags = [\n", - " \"TUT_AQAOA\"\n", - " ] # add tag for your job execution\n", - "\n", - " result = minimize(\n", - " qaoa_sampler_cost_fun,\n", - " init_params,\n", - " args=(\n", - " transpiled_qaoa_circ,\n", - " remapped_cost_operator,\n", - " sampler,\n", - " cvar_aggregation,\n", - " ),\n", - " method=\"COBYLA\",\n", - " tol=1e-2,\n", - " )\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "1d190fa4-3bbe-412a-b296-6dddd3ad2b12", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "\n", - "Let's now visualize our results and then post-process them to find the value of the cut." - ] - }, - { - "cell_type": "code", - "execution_count": 111, - "id": "761821cb-9a0c-4efb-806b-75513302d34a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 6))\n", - "plt.plot(\n", - " [result_dict[i][\"cvar_fval\"] for i in range(1, iter_counts + 1)],\n", - " label=\"CVaR\",\n", - ")\n", - "plt.plot(\n", - " [result_dict[i][\"fval\"] for i in range(1, iter_counts + 1)],\n", - " label=\"Standard\",\n", - ")\n", - "plt.legend()\n", - "plt.xlabel(\"Iteration\")\n", - "plt.ylabel(\"Cost\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "38aadfcb-aec9-4dbb-a9d3-319239eae196", - "metadata": {}, - "source": [ - "The following retrieves the best solution from the sampled bitstrings:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e8af29e-c99b-41f2-b6dd-2be471e1af21", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "bitstring (int): 283561207335785714592526814041, probability: 0.00025693730729701953, objective value: -43.0\n" - ] - } - ], - "source": [ - "# Sort the result_dict[iter_counts]['evaluated'] by the CVaR value\n", - "sorted_result_dict = [\n", - " (k, v)\n", - " for k, v in sorted(\n", - " result_dict[iter_counts][\"evaluated\"].items(),\n", - " key=lambda item: item[1][1],\n", - " )\n", - "]\n", - "print(\n", - " f\"bitstring (int): {sorted_result_dict[0][0]}, probability: {sorted_result_dict[0][1][0]}, objective value: {sorted_result_dict[0][1][1]}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "9cbeacc1-ff99-4b3d-b38d-b293a19642e2", - "metadata": {}, - "source": [ - "Consider the Hamiltonian $H_C$ for the **max-cut** problem. Let each vertex of the graph be associated with a qubit in state $|0\\rangle$ or $|1\\rangle$, where the value denotes the set the vertex is in. The goal of the problem is to maximize the number of edges $(v_1, v_2)$ for which $v_1 = |0\\rangle$ and $v_2 = |1\\rangle$, or vice versa. If we associate the $Z$ operator with each qubit, where\n", - "\n", - "$$\n", - " Z|0\\rangle = |0\\rangle \\qquad Z|1\\rangle = -|1\\rangle,\n", - "$$\n", - "\n", - "then an edge $(v_1, v_2)$ belongs to the cut if the eigenvalue of $(Z_1|v_1\\rangle) \\cdot (Z_2|v_2\\rangle) = -1$; in other words, the qubits associated with $v_1$ and $v_2$ are different. Similarly, $(v_1, v_2)$ does not belong to the cut if the eigenvalue of $(Z_1|v_1\\rangle) \\cdot (Z_2|v_2\\rangle) = 1$." - ] - }, - { - "cell_type": "code", - "execution_count": 113, - "id": "5ea9e6aa-4297-4687-b484-1695d415bad5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Result bitstring (binary) : [1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0]\n", - "The value of the cut is: 77\n" - ] - } - ], - "source": [ - "from typing import Sequence\n", - "\n", - "\n", - "def to_bitstring(integer, num_bits):\n", - " result = np.binary_repr(integer, width=num_bits)\n", - " return [int(digit) for digit in result]\n", - "\n", - "\n", - "def evaluate_sample(x: Sequence[int], graph: rx.PyGraph) -> float:\n", - " assert len(x) == len(\n", - " list(graph.nodes())\n", - " ), \"The length of x must coincide with the number of nodes in the graph.\"\n", - " return sum(\n", - " x[u] * (1 - x[v])\n", - " + x[v]\n", - " * (\n", - " 1 - x[u]\n", - " ) # x[u] = x[v] if same cut, x[u] \\neq x[v] if different cuts\n", - " for u, v in list(graph.edge_list())\n", - " )\n", - "\n", - "\n", - "bitstring = to_bitstring(\n", - " sorted_result_dict[0][0], len(list(remapped_graph.nodes()))\n", - ")\n", - "bitstring = bitstring[::-1]\n", - "print(f\"Result bitstring (binary) : {bitstring}\")\n", - "\n", - "cut_value = evaluate_sample(bitstring, remapped_graph)\n", - "print(f\"The value of the cut is: {cut_value}\")" - ] - }, - { - "cell_type": "markdown", - "id": "c5879546-35ab-4876-bed9-262b85f130cc", - "metadata": {}, - "source": [ - "Finally, let's draw a graph based on the CVaR result.\n", - "We split the graph nodes into two sets based on the CVaR result.\n", - "The nodes in the first set are colored in gray, and the nodes in the second set are colored in purple.\n", - "The edges between the two sets are the edges that are cut by the partitioning." - ] - }, - { - "cell_type": "code", - "execution_count": 124, - "id": "852dfeed-2871-4ca1-9754-15c95293198e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def plot_result(G, x):\n", - " colors = [\"tab:grey\" if i == 0 else \"tab:purple\" for i in x]\n", - " pos, _default_axes = rx.spring_layout(G), plt.axes(frameon=True)\n", - " rx.visualization.mpl_draw(\n", - " G,\n", - " node_color=colors,\n", - " node_size=150,\n", - " alpha=0.8,\n", - " pos=pos,\n", - " with_labels=True,\n", - " width=1,\n", - " )\n", - "\n", - "\n", - "plot_result(graph_100, to_bitstring(sorted_result_dict[0][0], 100)[::-1])" - ] - }, - { - "cell_type": "markdown", - "id": "82f5c13b-a141-4657-adfd-bb18e88ad9f2", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "\\[1] Weidenfeller, J., Valor, L. C., Gacon, J., Tornow, C., Bello, L., Woerner, S., & Egger, D. J. (2022). Scaling of the quantum approximate optimization algorithm on superconducting qubit based hardware. Quantum, 6, 870.\n", - "\n", - "\\[2] Matsuo, A., Yamashita, S., & Egger, D. J. (2023). A SAT approach to the initial mapping problem in SWAP gate insertion for commuting gates. IEICE Transactions on Fundamentals of Electronics, Communications and Computer Sciences, 106(11), 1424-1431.\n", - "\n", - "\\[3] Barkoutsos, P. K., Nannicini, G., Robert, A., Tavernelli, I., & Woerner, S. (2020). Improving variational quantum optimization using CVaR. Quantum, 4, 256.\n", - "\n", - "\\[4] Barron, S. V., Egger, D. J., Pelofske, E., Bärtschi, A., Eidenbenz, S., Lehmkuehler, M., & Woerner, S. (2023). Provable bounds for noise-free expectation values computed from noisy samples. arXiv preprint arXiv:2312.00733." - ] - }, - { - "cell_type": "markdown", - "id": "a6a5bbfe-a159-4dc1-9333-488737aff503", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "\n", - "\n", - "If you found this work interesting, you might be interested in the following material:\n", - "\n", - "* [The intractable decathlon](https://arxiv.org/pdf/2504.03832): a listing of 10 optimization problems that are difficult for classical optimization algorithms, and which may be good use cases to test the techniques introduced in this tutorial.\n", - "* [A repo of best practices for quantum optimization](https://github.com/qiskit-community/qopt-best-practices) to further improve the results of your QAOA-based workflow.\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1a869c2d-ae51-47d3-8a41-bfcf5e505f59", + "metadata": {}, + "source": [ + "---\n", + "title: Advanced techniques for QAOA\n", + "description: This notebook introduces advanced techniques to improve the performance of the Quantum Approximate Optimization Algorithm (QAOA) with a large number of qubits.\n", + "---\n", + "{/* cspell:ignore pysat, lbrack, frameon, IEICE */}\n", + "\n", + "# Advanced techniques for QAOA\n", + "\n", + "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime may vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "1a6bbeef-966f-45e1-89aa-e964ef891eeb", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Users should be familiar with the basics of the Quantum Approximate Optimization Algorithm (QAOA). See the following resources for an introduction to QAOA:\n", + "\n", + "* The [Solve utility-scale quantum optimization problems](/docs/tutorials/quantum-approximate-optimization-algorithm) tutorial\n", + "* The [Utility-scale QAOA](/learning/courses/quantum-computing-in-practice/utility-scale-qaoa#utility-scale-qaoa) lesson (part of the [Quantum computing in practice](/learning/courses/quantum-computing-in-practice) course) in IBM Quantum® Learning\n", + "\n", + "## Learning outcomes\n", + "\n", + "After going through this tutorial, users should be able to do the following:\n", + "* Use advanced techniques that help improve QAOA transpilation and provide a means for improving QAOA performance\n", + "\n", + "For a detailed walkthrough of the contents of this tutorial, see this [Qiskit video](https://youtu.be/rBfK-l-qSNk?si=PC28gFAdu4JYSYdk)." + ] + }, + { + "cell_type": "markdown", + "id": "ea97567d-810f-4cca-8edf-a47d70ea870a", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "This tutorial introduces two advanced techniques to improve the performance of the **Quantum Approximate Optimization Algorithm (QAOA)** at a large number of qubits.\n", + "\n", + "\n", + "The advanced techniques in this notebook include:\n", + "\n", + "* **SWAP strategy with SAT initial mapping**: This is a specifically designed transpiler pass for QAOA that uses a SWAP strategy and a SAT solver together to improve the selection of which physical qubits on the QPU to use. The SWAP strategy exploits the commutativity of the QAOA operators to reorder gates so that layers of SWAP gates can be simultaneously executed, thus reducing the depth of the circuit [\\[1\\]](#references). The SAT solver is used to find an initial mapping that minimizes the number of SWAP operations needed to map the qubits in the circuit to the physical qubits on the device [\\[2\\]](#references).\n", + "* **CVaR cost function**: Typically the expected value of the cost Hamiltonian is used as the cost function for QAOA, but as was shown in [\\[3\\]](#references), focusing on the tail of the distribution, rather than the expected value, can improve the performance of QAOA for combinatorial optimization problems. The CVaR accomplishes this. For a given set of shots with corresponding objective values of the considered optimization problem, the Conditional Value at Risk (CVaR) with confidence level $\\alpha \\in [0, 1]$ is defined as the average of the $\\alpha$ best shots [\\[3\\]](#references).\n", + "Thus, $\\alpha = 1$ corresponds to the standard expected value, while $\\alpha=0$ corresponds to the minimum of the given shots, and $\\alpha \\in (0, 1)$ is a tradeoff between focusing on better shots, while still applying some averaging to smooth out the optimization landscape. Additionally, the CVaR can be used as an error mitigation technique to improve the quality of the objective value estimation [\\[4\\]](#references).\n", + "\n", + "By the end of this tutorial, you should be able to use these techniques to get the best results from running QAOA for your optimization problems." + ] + }, + { + "cell_type": "markdown", + "id": "40fb546e-85e0-450b-a5ea-5d08950d129f", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "\n", + "* Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "* Qiskit Runtime v0.43 or later (`pip install qiskit-ibm-runtime`)\n", + "* Rustworkx graph library (`pip install rustworkx`)\n", + "* Python SAT (`pip install python-sat`)" + ] + }, + { + "cell_type": "markdown", + "id": "50285e5f-1a7b-471c-a223-1ae0af19d9ed", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "id": "d019ea68-61e0-4341-84c7-e612ca10dde7", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import numpy as np\n", + "import rustworkx as rx\n", + "from dataclasses import dataclass\n", + "from itertools import combinations\n", + "from threading import Timer\n", + "from collections.abc import Callable, Iterable\n", + "from pysat.formula import CNF, IDPool\n", + "from pysat.solvers import Solver\n", + "from scipy.optimize import minimize\n", + "from rustworkx.visualization import mpl_draw as draw_graph\n", + "\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.circuit.library import QAOAAnsatz\n", + "from qiskit.circuit import QuantumCircuit, ParameterVector\n", + "from qiskit.transpiler import CouplingMap, PassManager\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import (\n", + " SwapStrategy,\n", + " FindCommutingPauliEvolutions,\n", + " Commuting2qGateRouter,\n", + ")\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Session\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler" + ] + }, + { + "cell_type": "markdown", + "id": "8b77b0c9-f5a6-476e-86b8-069ba14f9ab3", + "metadata": {}, + "source": [ + "### Max-cut problem\n", + "\n", + "Let's consider solving the **max-cut** problem on a graph with 100 nodes using QAOA.\n", + "The max-cut problem is a combinatorial optimization problem that is defined on a graph $G = (V, E)$, where $V$ is the set of vertices and $E$ is the set of edges. The goal is to partition the vertices into two sets, $S$ and $V \\setminus S$, such that the number of edges between the two sets is maximized.\n", + "In this example, we use a graph with 100 nodes that is based on a hardware coupling map." + ] + }, + { + "cell_type": "markdown", + "id": "663d13f5-a5f7-4b67-a89b-26deb41224ec", + "metadata": {}, + "source": [ + "## Small-scale simulator example\n", + "\n", + "Since the goal of this tutorial is to show how QAOA performs at scales beyond what a simulator can handle, we will forgo this step.\n", + "\n", + "If you would like to try a simulator-based QAOA workflow, try out the [Quantum approximate optimization algorithm](/docs/tutorials/quantum-approximate-optimization-algorithm) tutorial." + ] + }, + { + "cell_type": "markdown", + "id": "6ca05a7c-5fde-4485-a3ad-c0ba02844e50", + "metadata": {}, + "source": [ + "## Large-scale hardware example" + ] + }, + { + "cell_type": "markdown", + "id": "b825afbf-10fd-4926-bd65-05272044f107", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "#### Graph → Hamiltonian\n", + "\n", + "First, map the problem onto a quantum circuit that is suited for the QAOA. Details on this process can be found in the [introductory QAOA tutorial](/docs/tutorials/quantum-approximate-optimization-algorithm)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c25db38-5f36-4a39-bdde-4f83616a6dde", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Instantiate runtime to access backend\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " min_num_qubits=100, operational=True, simulator=False\n", + ")\n", + "print(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e04ce982-8294-4ebe-8dcc-f74205938800", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 96, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check if the coupling map is symmetric. We will add a conditional below\n", + "# to avoid over-counting edges for symmetric/bi-directional coupling maps.\n", + "\n", + "backend.coupling_map.is_symmetric" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b864d32-9483-45ea-831f-60488e330adb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "n = 100\n", + "graph_100 = rx.PyGraph()\n", + "graph_100.add_nodes_from((np.arange(0, n, 1)))\n", + "w = 1.0\n", + "elist = []\n", + "\n", + "for edge in backend.coupling_map:\n", + " if (edge[0] < n) and (edge[1] < n):\n", + " # Conditional to avoid over-counting edges\n", + " if (\n", + " edge[1],\n", + " edge[0],\n", + " w,\n", + " ) not in elist:\n", + " elist.append((edge[0], edge[1], w))\n", + "\n", + "graph_100.add_edges_from(elist)\n", + "draw_graph(graph_100, with_labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "e39c4e42-ce97-4a04-8879-da4d33a684bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cost Function Hamiltonian: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j])\n" + ] + } + ], + "source": [ + "# Construct cost hamiltonian\n", + "\n", + "\n", + "def build_max_cut_paulis(graph: rx.PyGraph) -> list[tuple[str, float]]:\n", + " \"\"\"Convert the graph to Pauli list.\n", + "\n", + " This function does the inverse of `build_max_cut_graph`\n", + " \"\"\"\n", + " pauli_list = []\n", + " for edge in list(graph.edge_list()):\n", + " paulis = [\"I\"] * len(graph)\n", + " paulis[edge[0]], paulis[edge[1]] = \"Z\", \"Z\"\n", + "\n", + " weight = graph.get_edge_data(edge[0], edge[1])\n", + "\n", + " pauli_list.append((\"\".join(paulis)[::-1], weight))\n", + "\n", + " return pauli_list\n", + "\n", + "\n", + "max_cut_paulis = build_max_cut_paulis(graph_100)\n", + "\n", + "cost_hamiltonian = SparsePauliOp.from_list(max_cut_paulis)\n", + "print(\"Cost Function Hamiltonian:\", cost_hamiltonian)" + ] + }, + { + "cell_type": "markdown", + "id": "4e576068-53e7-4a06-a83b-87e95de141e9", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "#### SWAP strategy with the SAT initial mapping\n", + "\n", + "We will demonstrate how to build and optimize QAOA circuits using the **SWAP strategy with SAT initial mapping**, a specifically designed transpiler pass for QAOA applied to quadratic problems.\n", + "\n", + "In this example, we choose a SWAP insertion strategy for blocks of commuting two-qubit gates, which applies layers of SWAP gates that are simultaneously executable on the coupling map. This strategy is presented in [\\[1\\]](#references) and is passed into `Commuting2qGateRouter`, which is exposed as a standardized Qiskit transpiler pass (see [`Commuting2qGateRouter`](/docs/api/qiskit/qiskit.transpiler.passes.Commuting2qGateRouter)). We use a line swap strategy in this example." + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "7c54402a-7696-4b0e-826a-6e0ac4c54395", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract longest path with no repeated nodes\n", + "nodes = rx.longest_simple_path(graph_100)\n", + "\n", + "# Collect even edges and odd edges\n", + "even_edges = [\n", + " (nodes[i], nodes[i + 1])\n", + " if nodes[i] < nodes[i + 1]\n", + " else (nodes[i + 1], nodes[i])\n", + " for i in range(0, len(nodes) - 1, 2)\n", + "]\n", + "odd_edges = [\n", + " (nodes[i], nodes[i + 1])\n", + " if nodes[i] < nodes[i + 1]\n", + " else (nodes[i + 1], nodes[i])\n", + " for i in range(1, len(nodes) - 1, 2)\n", + "]\n", + "edge_list = [\n", + " (edge[0], edge[1]) if edge[0] < edge[1] else (edge[1], edge[0])\n", + " for edge in graph_100.edge_list()\n", + "]\n", + "\n", + "swap_strategy = SwapStrategy(CouplingMap(edge_list), (even_edges, odd_edges))" + ] + }, + { + "cell_type": "markdown", + "id": "ba53fd1d-0d68-43e3-9102-4c6e64f04de7", + "metadata": {}, + "source": [ + "#### Remap the graph using a SAT mapper\n", + "\n", + "Even when a circuit consists of commuting gates (this is the case for the QAOA circuit, but also for Trotterized simulations of Ising Hamiltonians), finding a good initial mapping is a challenging task. When we use the SAT-based approach presented in [\\[2\\]](#references), we can discover effective initial mappings for circuits with commuting gates, resulting in a significant reduction in the number of required SWAP layers. This approach has been demonstrated to scale to up to *500 qubits*, as illustrated in the paper.\n", + "\n", + "The following code demonstrates how to use the `SATMapper` from Matsuo et al. to remap the graph. This process allows the problem to be mapped to a more optimal initial state for a specified SWAP strategy, resulting in a significant reduction in the number of SWAP layers required to execute the circuit.\n", + "\n", + "In `SATMapper`, the problem of finding a good initial mapping is formulated as a SAT problem. A SAT solver is used to find such an initial mapping for the QAOA circuit. `python-sat` (`pysat` for short) is a Python library for a SAT solver, and we will use it to solve the SAT problem in this example." + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "df9d861a-64e8-49ee-aed1-c41f437fa743", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"A class to solve the SWAP gate insertion initial mapping problem\n", + "using the SAT approach from https://arxiv.org/abs/2212.05666.\n", + "\"\"\"\n", + "\n", + "\n", + "@dataclass\n", + "class SATResult:\n", + " \"\"\"A data class to hold the result of a SAT solver.\"\"\"\n", + "\n", + " satisfiable: bool # Satisfiable is True if the SAT model could be solved\n", + " # in a given time.\n", + " solution: dict # The solution to the SAT problem if it is satisfiable.\n", + " mapping: list # The mapping of nodes in the pattern graph to nodes in the\n", + " # target graph.\n", + " elapsed_time: float # The time it took to solve the SAT model.\n", + "\n", + "\n", + "class SATMapper:\n", + " r\"\"\"A class to introduce a SAT-approach to solve\n", + " the initial mapping problem in SWAP gate insertion for commuting gates.\n", + "\n", + " When this pass is run on a DAG it will look for the first instance of\n", + " :class:`.Commuting2qBlock` and use the program graph :math:`P` of this block\n", + " of gates to find a layout for a given swap strategy. This layout is found\n", + " with a binary search over the layers :math:`l` of the swap strategy. At each\n", + " considered layer a subgraph isomorphism problem formulated as a SAT is solved\n", + " by a SAT solver. Each instance is whether it is possible to embed the program\n", + " graph :math:`P` into the effective connectivity graph :math:`C_l` that is\n", + " achieved by applying :math:`l` layers of the swap strategy to the coupling map\n", + " :math:`C_0` of the backend. Since solving SAT problems can be hard, a\n", + " ``time_out`` fixes the maximum time allotted to the SAT solver for each\n", + " instance. If this time is exceeded the considered problem is deemed\n", + " unsatisfiable and the binary search proceeds to the next number of swap\n", + " layers :math:``l``.\n", + " \"\"\"\n", + "\n", + " def __init__(self, timeout: int = 60):\n", + " \"\"\"Initialize the SATMapping.\n", + "\n", + " Args:\n", + " timeout: The allowed time in seconds for each iteration of the SAT\n", + " solver. This variable defaults to 60 seconds.\n", + " \"\"\"\n", + " self.timeout = timeout\n", + "\n", + " def find_initial_mappings(\n", + " self,\n", + " program_graph: rx.Graph,\n", + " swap_strategy: SwapStrategy,\n", + " min_layers: int | None = None,\n", + " max_layers: int | None = None,\n", + " ) -> dict[int, SATResult]:\n", + " r\"\"\"Find an initial mapping for a given swap strategy. Perform a\n", + " binary search over the number of swap layers, and for each number\n", + " of swap layers solve a subgraph isomorphism problem formulated as\n", + " a SAT problem.\n", + "\n", + " Args:\n", + " program_graph (rx.Graph): The program graph with commuting gates, where\n", + " each edge represents a two-qubit gate.\n", + " swap_strategy (SwapStrategy): The swap strategy to use to find the\n", + " initial mapping.\n", + " min_layers (int): The minimum number of swap layers to consider.\n", + " Defaults to the maximum degree of the\n", + " program graph - 2.\n", + " max_layers (int): The maximum number of swap layers to consider.\n", + " Defaults to the number of qubits in the\n", + " swap strategy - 2.\n", + "\n", + " Returns:\n", + " dict[int, SATResult]: A dictionary containing the results of the SAT\n", + " solver for each number of swap layers.\n", + " \"\"\"\n", + " num_nodes_g1 = len(program_graph.nodes())\n", + " num_nodes_g2 = swap_strategy.distance_matrix.shape[0]\n", + " if num_nodes_g1 > num_nodes_g2:\n", + " return SATResult(False, [], [], 0)\n", + " if min_layers is None:\n", + " # use the maximum degree of the program graph - 2\n", + " # as the lower bound.\n", + " min_layers = max((d for _, d in program_graph.degree)) - 2\n", + " if max_layers is None:\n", + " max_layers = num_nodes_g2 - 1\n", + "\n", + " variable_pool = IDPool(start_from=1)\n", + " variables = np.array(\n", + " [\n", + " [variable_pool.id(f\"v_{i}_{j}\") for j in range(num_nodes_g2)]\n", + " for i in range(num_nodes_g1)\n", + " ],\n", + " dtype=int,\n", + " )\n", + " vid2mapping = {v: idx for idx, v in np.ndenumerate(variables)}\n", + " binary_search_results = {}\n", + "\n", + " def interrupt(solver):\n", + " # This function is called to interrupt the solver when the\n", + " # timeout is reached.\n", + " solver.interrupt()\n", + "\n", + " # Make a cnf (conjunctive normal form) for the one-to-one\n", + " # mapping constraint\n", + " cnf1 = []\n", + " for i in range(num_nodes_g1):\n", + " clause = variables[i, :].tolist()\n", + " cnf1.append(clause)\n", + " for k, m in combinations(clause, 2):\n", + " cnf1.append([-1 * k, -1 * m])\n", + " for j in range(num_nodes_g2):\n", + " clause = variables[:, j].tolist()\n", + " for k, m in combinations(clause, 2):\n", + " cnf1.append([-1 * k, -1 * m])\n", + "\n", + " # Perform a binary search over the number of swap layers to find the\n", + " # minimum number of swap layers that satisfies the subgraph isomorphism\n", + " # problem.\n", + " while min_layers < max_layers:\n", + " num_layers = (min_layers + max_layers) // 2\n", + "\n", + " # Create the connectivity matrix. Note that if the swap strategy\n", + " # cannot reach full connectivity then its distance matrix will have\n", + " # entries with -1. These entries must be treated as False.\n", + " d_matrix = swap_strategy.distance_matrix\n", + " connectivity_matrix = (\n", + " (-1 < d_matrix) & (d_matrix <= num_layers)\n", + " ).astype(int)\n", + " # Make a cnf for the adjacency constraint\n", + " cnf2 = []\n", + " for e_0, e_1 in list(program_graph.edge_list()):\n", + " clause_matrix = np.multiply(\n", + " connectivity_matrix, variables[e_1, :]\n", + " )\n", + " clause = np.concatenate(\n", + " (\n", + " [[-variables[e_0, i]] for i in range(num_nodes_g2)],\n", + " clause_matrix,\n", + " ),\n", + " axis=1,\n", + " )\n", + " # Remove 0s from each clause\n", + " cnf2.extend([c[c != 0].tolist() for c in clause])\n", + "\n", + " cnf = CNF(from_clauses=cnf1 + cnf2)\n", + "\n", + " with Solver(bootstrap_with=cnf, use_timer=True) as solver:\n", + " # Solve the SAT problem with a timeout.\n", + " # Timer is used to interrupt the solver when the\n", + " # timeout is reached.\n", + " timer = Timer(self.timeout, interrupt, [solver])\n", + " timer.start()\n", + " status = solver.solve_limited(expect_interrupt=True)\n", + " timer.cancel()\n", + " # Get the solution and the elapsed time.\n", + " sol = solver.get_model()\n", + " e_time = solver.time()\n", + "\n", + " print(\n", + " f\"Layers: {num_layers}, Status: {status}, Time: {e_time}\"\n", + " )\n", + " if status:\n", + " # If the SAT problem is satisfiable, convert the solution\n", + " # to a mapping.\n", + " mapping = [vid2mapping[idx] for idx in sol if idx > 0]\n", + " binary_search_results[num_layers] = SATResult(\n", + " status, sol, mapping, e_time\n", + " )\n", + " max_layers = num_layers\n", + " else:\n", + " # If the SAT problem is unsatisfiable, return the last\n", + " # satisfiable solution.\n", + " binary_search_results[num_layers] = SATResult(\n", + " status, sol, [], e_time\n", + " )\n", + " min_layers = num_layers + 1\n", + "\n", + " return binary_search_results\n", + "\n", + " def remap_graph_with_sat(\n", + " self, graph: rx.Graph, swap_strategy, max_layers\n", + " ):\n", + " \"\"\"Applies the SAT mapping.\n", + "\n", + " Args:\n", + " graph (nx.Graph): The graph to remap.\n", + " swap_strategy (SwapStrategy): The swap strategy to use\n", + " to find the initial mapping.\n", + "\n", + " Returns:\n", + " tuple: A tuple containing the remapped graph, the edge map, and the\n", + " number of layers of the swap strategy that was used to find the\n", + " initial mapping. If no solution is found then the tuple contains\n", + " None for each element. Note the returned edge map `{k: v}` means that\n", + " node `k` in the original graph gets mapped to node `v` in the\n", + " Pauli strings.\n", + " \"\"\"\n", + " num_nodes = len(graph.nodes())\n", + " results = self.find_initial_mappings(\n", + " graph, swap_strategy, 0, max_layers\n", + " )\n", + " solutions = [k for k, v in results.items() if v.satisfiable]\n", + "\n", + " if len(solutions):\n", + " min_k = min(solutions)\n", + " edge_map = dict(results[min_k].mapping)\n", + " # Create the remapped graph\n", + " remapped_graph = rx.PyGraph()\n", + " remapped_graph.add_nodes_from(range(num_nodes))\n", + " mapping = dict(results[min_k].mapping)\n", + " for i, graph_edge in enumerate(list(graph.edge_list())):\n", + " remapped_edge = tuple(mapping[node] for node in graph_edge)\n", + " remapped_graph.add_edge(*remapped_edge, graph.edges()[i])\n", + " return remapped_graph, edge_map, min_k\n", + " else:\n", + " return None, None, None" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "e689e09e-6ca7-4154-8602-d1d954ebe80b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Layers: 0, Status: True, Time: 0.022812999999999306\n", + "Map from old to new nodes: {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}\n", + "Min SWAP layers: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sm = SATMapper(timeout=10)\n", + "remapped_graph, edge_map, min_swap_layers = sm.remap_graph_with_sat(\n", + " graph=graph_100, swap_strategy=swap_strategy, max_layers=1\n", + ")\n", + "print(\"Map from old to new nodes: \", edge_map)\n", + "print(\"Min SWAP layers:\", min_swap_layers)\n", + "draw_graph(remapped_graph, node_size=200, with_labels=True, width=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "ecf6e8c3-65c2-4430-8dd3-d67b8842045d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n", + " 1.+0.j, 1.+0.j, 1.+0.j])\n" + ] + } + ], + "source": [ + "remapped_max_cut_paulis = build_max_cut_paulis(remapped_graph)\n", + "# define a qiskit SparsePauliOp from the list of paulis\n", + "remapped_cost_operator = SparsePauliOp.from_list(remapped_max_cut_paulis)\n", + "print(remapped_cost_operator)" + ] + }, + { + "cell_type": "markdown", + "id": "5ae531be-80eb-4acd-b84a-7d466fd872e7", + "metadata": {}, + "source": [ + "#### Build a QAOA circuit with the SWAP strategy and the SAT mapping\n", + "\n", + "We only want to apply the SWAP strategies to the cost operator layer, so we start by creating the isolated block that we will later transform and append to the final QAOA circuit.\n", + "\n", + "For this, we can use the [`QAOAAnsatz`](/docs/api/qiskit/qiskit.circuit.library.QAOAAnsatz) class from Qiskit. We input an empty circuit to the `initial_state` and `mixer_operator` fields to make sure we are building an isolated cost operator layer.\n", + "We also define the `edge_coloring` map so that RZZ gates are positioned next to SWAP gates. This strategic placement allows us to exploit CX cancellations, optimizing the circuit for better performance.\n", + "This process is executed within the `create_qaoa_swap_circuit` function." + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "id": "57d5eb53-9cda-4c38-a00b-26ed4b533bcd", + "metadata": {}, + "outputs": [], + "source": [ + "def make_meas_map(circuit: QuantumCircuit) -> dict:\n", + " \"\"\"Return a mapping from qubit index (the key) to classical bit (the value).\n", + "\n", + " This allows us to account for the swapping order introduced by the SWAP strategy.\n", + " \"\"\"\n", + " creg = circuit.cregs[0]\n", + " qreg = circuit.qregs[0]\n", + "\n", + " meas_map = {}\n", + " for inst in circuit.data:\n", + " if inst.operation.name == \"measure\":\n", + " meas_map[qreg.index(inst.qubits[0])] = creg.index(inst.clbits[0])\n", + "\n", + " return meas_map\n", + "\n", + "\n", + "def apply_swap_strategy(\n", + " circuit: QuantumCircuit,\n", + " swap_strategy: SwapStrategy,\n", + " edge_coloring: dict[tuple[int, int], int] | None = None,\n", + ") -> QuantumCircuit:\n", + " \"\"\"Transpile with a SWAP strategy.\n", + "\n", + " Returns:\n", + " A quantum circuit transpiled with the given swap strategy.\n", + " \"\"\"\n", + "\n", + " pm_pre = PassManager(\n", + " [\n", + " FindCommutingPauliEvolutions(),\n", + " Commuting2qGateRouter(\n", + " swap_strategy,\n", + " edge_coloring,\n", + " ),\n", + " ]\n", + " )\n", + " return pm_pre.run(circuit)\n", + "\n", + "\n", + "def apply_qaoa_layers(\n", + " cost_layer: QuantumCircuit,\n", + " meas_map: dict,\n", + " num_layers: int,\n", + " gamma: list[float] | ParameterVector = None,\n", + " beta: list[float] | ParameterVector = None,\n", + " initial_state: QuantumCircuit = None,\n", + " mixer: QuantumCircuit = None,\n", + "):\n", + " \"\"\"Applies QAOA layers to construct circuit.\n", + "\n", + " First, the initial state is applied. If `initial_state` is None, we begin in the\n", + " initial superposition state. Next, we alternate between layers of the cost operator\n", + " and the mixer. The cost operator is alternatively applied in order and in reverse\n", + " instruction order. This allows us to apply the swap strategy on odd `p` layers\n", + " and undo the swap strategy on even `p` layers.\n", + " \"\"\"\n", + "\n", + " num_qubits = cost_layer.num_qubits\n", + " new_circuit = QuantumCircuit(num_qubits, num_qubits)\n", + "\n", + " if initial_state is not None:\n", + " new_circuit.append(initial_state, range(num_qubits))\n", + " else:\n", + " # all h state by default\n", + " new_circuit.h(range(num_qubits))\n", + "\n", + " if gamma is None or beta is None:\n", + " gamma = ParameterVector(\"γ'\", num_layers)\n", + " if mixer is None or mixer.num_parameters == 0:\n", + " beta = ParameterVector(\"β'\", num_layers)\n", + " else:\n", + " beta = ParameterVector(\"β'\", num_layers * mixer.num_parameters)\n", + "\n", + " if mixer is not None:\n", + " mixer_layer = mixer\n", + " else:\n", + " mixer_layer = QuantumCircuit(num_qubits)\n", + " mixer_layer.rx(-2 * beta[0], range(num_qubits))\n", + "\n", + " for layer in range(num_layers):\n", + " bind_dict = {cost_layer.parameters[0]: gamma[layer]}\n", + " cost_layer_ = cost_layer.assign_parameters(bind_dict)\n", + " bind_dict = {\n", + " mixer_layer.parameters[i]: beta[layer + i]\n", + " for i in range(mixer_layer.num_parameters)\n", + " }\n", + " layer_mixer = mixer_layer.assign_parameters(bind_dict)\n", + "\n", + " if layer % 2 == 0:\n", + " new_circuit.append(cost_layer_, range(num_qubits))\n", + " else:\n", + " new_circuit.append(cost_layer_.reverse_ops(), range(num_qubits))\n", + "\n", + " new_circuit.append(layer_mixer, range(num_qubits))\n", + "\n", + " for qidx, cidx in meas_map.items():\n", + " new_circuit.measure(qidx, cidx)\n", + "\n", + " return new_circuit\n", + "\n", + "\n", + "def create_qaoa_swap_circuit(\n", + " cost_operator: SparsePauliOp,\n", + " swap_strategy: SwapStrategy,\n", + " edge_coloring: dict = None,\n", + " theta: list[float] = None,\n", + " qaoa_layers: int = 1,\n", + " initial_state: QuantumCircuit = None,\n", + " mixer: QuantumCircuit = None,\n", + "):\n", + " \"\"\"Create the circuit for QAOA.\n", + "\n", + " Notes: This circuit construction for QAOA works for quadratic terms in `Z` and will be\n", + " extended to first-order terms in `Z`. Higher-orders are not supported.\n", + "\n", + " Args:\n", + " cost_operator: the cost operator.\n", + " swap_strategy: selected swap strategy\n", + " edge_coloring: A coloring of edges that should correspond to the coupling\n", + " map of the hardware. It defines the order in which we apply the Rzz\n", + " gates. This allows us to choose an ordering such that `Rzz` gates will\n", + " immediately precede SWAP gates to leverage CNOT cancellation.\n", + " theta: The QAOA angles.\n", + " qaoa_layers: The number of layers of the cost operator and the mixer operator.\n", + " initial_state: The initial state on which we apply layers of cost operator\n", + " and mixer.\n", + " mixer: The QAOA mixer. It will be applied as is onto the QAOA circuit. Therefore,\n", + " its output must have the same ordering of qubits as its input.\n", + " \"\"\"\n", + "\n", + " num_qubits = cost_operator.num_qubits\n", + "\n", + " if theta is not None:\n", + " gamma = theta[: len(theta) // 2]\n", + " beta = theta[len(theta) // 2 :]\n", + " qaoa_layers = len(theta) // 2\n", + " else:\n", + " gamma = beta = None\n", + "\n", + " # First, create the ansatz of one layer of QAOA without mixer\n", + " cost_layer = QAOAAnsatz(\n", + " cost_operator,\n", + " reps=1,\n", + " initial_state=QuantumCircuit(num_qubits),\n", + " mixer_operator=QuantumCircuit(num_qubits),\n", + " ).decompose()\n", + "\n", + " # This will allow us to recover the permutation of the measurements that the swaps introduce.\n", + " cost_layer.measure_all()\n", + "\n", + " # Now, apply the swap strategy for commuting gates\n", + " cost_layer = apply_swap_strategy(cost_layer, swap_strategy, edge_coloring)\n", + "\n", + " # Compute the measurement map (qubit to classical bit).\n", + " # We will apply this for odd layers where the swaps were inserted.\n", + " if qaoa_layers % 2 == 1:\n", + " meas_map = make_meas_map(cost_layer)\n", + " else:\n", + " meas_map = {idx: idx for idx in range(num_qubits)}\n", + "\n", + " cost_layer.remove_final_measurements()\n", + "\n", + " # Finally, introduce the mixer circuit and add measurements following measurement map\n", + " circuit = apply_qaoa_layers(\n", + " cost_layer, meas_map, qaoa_layers, gamma, beta, initial_state, mixer\n", + " )\n", + "\n", + " return circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "id": "7793ef92-ce59-4fd7-b43f-48d4e3427e3a", + "metadata": {}, + "outputs": [], + "source": [ + "# We can define the edge_coloring map so that RZZ gates are positioned right before SWAP gates to\n", + "# exploit CX cancellations\n", + "# We use greedy edge coloring from rustworkx to color the edges of the graph. This coloring is used\n", + "# to order the RZZ gates in the circuit.\n", + "\n", + "edge_coloring_idx = rx.graph_greedy_edge_color(graph_100)\n", + "edge_coloring = {\n", + " edge: edge_coloring_idx[idx]\n", + " for idx, edge in enumerate(list(graph_100.edge_list()))\n", + "}\n", + "edge_coloring = {tuple(sorted(k)): v for k, v in edge_coloring.items()}" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "id": "82ae28b3-85eb-4487-8100-1e622e93cccf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 105, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qaoa_circ = create_qaoa_swap_circuit(\n", + " remapped_cost_operator,\n", + " swap_strategy,\n", + " edge_coloring=edge_coloring,\n", + " qaoa_layers=1,\n", + ")\n", + "qaoa_circ.draw(output=\"mpl\", fold=False)" + ] + }, + { + "cell_type": "markdown", + "id": "e2afd1a7-0980-433b-a3a8-303d7e7718b1", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit Runtime primitives\n", + "\n", + "Let's now prepare for hardware execution. Our first step will be to define a Conditional Value at Risk (CVaR) cost function, which was introduced in [\\[3\\]](#references) for use within the paradigm of variational quantum optimization algorithms.\n", + "\n", + "The CVaR of a random variable $X$ for a confidence level $α ∈ (0, 1]$ is defined as\n", + "$CVaR_{\\alpha}(X) = \\mathbb{E} \\lbrack X | X \\leq F_X^{-1}(\\alpha) \\rbrack$\n", + "where $F_X^{-1}(p)$ is the inverse cumulative distribution function of $X$. In other words, CVaR is the expected value of the lower $\\alpha$-tail of the distribution of $X$." + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "id": "172023f9-164e-43bd-bbc8-39d87628287e", + "metadata": {}, + "outputs": [], + "source": [ + "pass_manager = generate_preset_pass_manager(\n", + " backend=backend,\n", + " optimization_level=3,\n", + ")\n", + "\n", + "transpiled_qaoa_circ = pass_manager.run(qaoa_circ)" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "id": "e6794cf3-7fbe-46a5-bdc0-5faad1235365", + "metadata": {}, + "outputs": [], + "source": [ + "# Utility functions for the evaluation of the expectation value of a measured state\n", + "# In this code, for optimization, the measured state is converted into a bit string,\n", + "# and the sign of the value is determined by taking the exclusive OR of the bits\n", + "# corresponding to Pauli Z.\n", + "\n", + "_PARITY = np.array(\n", + " [-1 if bin(i).count(\"1\") % 2 else 1 for i in range(256)],\n", + " dtype=np.complex128,\n", + ")\n", + "\n", + "\n", + "def evaluate_sparse_pauli(state: int, observable: SparsePauliOp) -> complex:\n", + " \"\"\"Utility for the evaluation of the expectation value of a measured state.\n", + "\n", + " Args:\n", + " state (int): The measured state.\n", + " observable (SparsePauliOp): The observable to evaluate the expectation value for.\n", + "\n", + " Returns:\n", + " complex: The expectation value of the measured state.\n", + " \"\"\"\n", + " packed_uint8 = np.packbits(\n", + " observable.paulis.z, axis=1, bitorder=\"little\"\n", + " ) # convert observable to array with 8 bit integer\n", + " state_bytes = np.frombuffer(\n", + " state.to_bytes(packed_uint8.shape[1], \"little\"),\n", + " dtype=np.uint8, # convert bitstring to array with 8 bit integer\n", + " )\n", + " reduced = np.bitwise_xor.reduce(\n", + " packed_uint8 & state_bytes, axis=1\n", + " ) # take bitwise xor of the result of 'and' conditional on the above two, return 0 or 1\n", + " return np.sum(observable.coeffs * _PARITY[reduced])" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "id": "c5e5f7a4-01f6-4a02-9114-3bb0e24be1a2", + "metadata": {}, + "outputs": [], + "source": [ + "def qaoa_sampler_cost_fun(\n", + " params, ansatz, hamiltonian, sampler, aggregation=None\n", + "):\n", + " \"\"\"Standard sampler-based QAOA cost function to be plugged into optimizer routines.\n", + "\n", + " Args:\n", + " params (np.ndarray): Parameters for the ansatz.\n", + " ansatz (QuantumCircuit): Ansatz circuit.\n", + " hamiltonian (SparsePauliOp): Hamiltonian to be minimized.\n", + " sampler (QAOASampler): Sampler to be used.\n", + " aggregation (Callable | float | None): Aggregation function to be applied to\n", + " the sampled results. If None, the sum of the expectation values is returned.\n", + " If float, the CVaR with the given alpha is used.\n", + " \"\"\"\n", + " # Run the circuit\n", + " job = sampler.run([(ansatz, params)])\n", + " sampler_result = job.result()\n", + " sampled_int_counts = sampler_result[\n", + " 0\n", + " ].data.c.get_int_counts() # bitstrings are stored as integers\n", + " shots = sum(sampled_int_counts.values())\n", + " int_count_distribution = {\n", + " key: val / shots for key, val in sampled_int_counts.items()\n", + " }\n", + "\n", + " # a dictionary containing: {state: (measurement probability, value)}\n", + " evaluated = {\n", + " state: (\n", + " probability,\n", + " np.real(evaluate_sparse_pauli(state, hamiltonian)),\n", + " )\n", + " for state, probability in int_count_distribution.items()\n", + " }\n", + "\n", + " # If aggregation is None, return the sum of the expectation values.\n", + " # If aggregation is a float, return the CVaR with the given alpha.\n", + " # Otherwise, use the aggregation function.\n", + " if aggregation is None:\n", + " result = sum(\n", + " probability * value for probability, value in evaluated.values()\n", + " )\n", + " elif isinstance(aggregation, float):\n", + " cvar_aggregation = _get_cvar_aggregation(aggregation)\n", + " result = cvar_aggregation(evaluated.values())\n", + " else:\n", + " result = aggregation(evaluated.values())\n", + "\n", + " global iter_counts, result_dict\n", + " iter_counts += 1\n", + " temp_dict = {}\n", + " temp_dict[\"params\"] = params.tolist()\n", + " temp_dict[\"cvar_fval\"] = result\n", + " temp_dict[\"fval\"] = sum(\n", + " probability * value for probability, value in evaluated.values()\n", + " )\n", + " temp_dict[\"distribution\"] = sampled_int_counts\n", + " temp_dict[\"evaluated\"] = evaluated\n", + " result_dict[iter_counts] = temp_dict\n", + " print(f\"Iteration {iter_counts}: {result}\")\n", + "\n", + " return result\n", + "\n", + "\n", + "def _get_cvar_aggregation(alpha: float | None) -> Callable:\n", + " \"\"\"Return the CVaR aggregation function with the given alpha.\n", + "\n", + " Args:\n", + " alpha (float | None): Alpha value for the CVaR aggregation. If None, 1 is used\n", + " by default.\n", + " Raises:\n", + " ValueError: If alpha is not in [0, 1].\n", + " \"\"\"\n", + " if alpha is None:\n", + " alpha = 1\n", + " elif not 0 <= alpha <= 1:\n", + " raise ValueError(f\"alpha must be in [0, 1], but {alpha} was given.\")\n", + "\n", + " def cvar_aggregation(\n", + " objective_dict: Iterable[tuple[float, float]],\n", + " ) -> float:\n", + " \"\"\"Return the CVaR of the given measurements.\n", + " Args:\n", + " objective_dict (Iterable[tuple[float, float]]): An iterable of tuples containing\n", + " the measured bit string and the objective value based on the bit string.\n", + "\n", + " \"\"\"\n", + " sorted_measurements = sorted(objective_dict, key=lambda x: x[1])\n", + " # accumulate the probabilities until alpha is reached\n", + " accumulated_percent = 0.0\n", + " cvar = 0.0\n", + " for probability, value in sorted_measurements:\n", + " cvar += value * min(probability, alpha - accumulated_percent)\n", + " accumulated_percent += probability\n", + " if accumulated_percent >= alpha:\n", + " break\n", + " return cvar / alpha\n", + "\n", + " return cvar_aggregation" + ] + }, + { + "cell_type": "markdown", + "id": "63fa2ab4-5354-4022-ab46-e9bbf73870de", + "metadata": {}, + "source": [ + "The CVaR can be used as an error mitigation technique as previously discussed [\\[4\\]](#references). In this example, we determine $\\alpha$ and the number of shots according to the [error per layered gate](/docs/guides/qpu-information#2q-error-layered) (EPLG) associated with the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "id": "032bf312-4bf4-40f4-81f0-2ae8a719b98b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "layer fidelity 0.5454643821399414\n", + "\n", + "The corresponding CVaR aggregation value is: 0.2568730767702702\n", + "To mitigate the twirled noise, increase shots by a factor of 3.8929731857197782\n" + ] + } + ], + "source": [ + "num_2q_ops = transpiled_qaoa_circ.count_ops()[\n", + " \"cz\"\n", + "] # the two qubit gates on our backend are cz's.\n", + "\n", + "for el in backend.properties().general:\n", + " if el.name[:2] == \"lf\" and el.name[3:] == str(\n", + " n\n", + " ): # pick out lf_100, lf of the best 100q chain\n", + " lf = el.value # layer fidelity\n", + " print(\"layer fidelity\", lf)\n", + " eplg = 1 - lf ** (1 / (n - 1)) # error per layered gate (EPLG)\n", + " fid_cz = 1 - eplg\n", + " gamma_cz = 1 / fid_cz**2\n", + " gamma_circ = gamma_cz**num_2q_ops\n", + "\n", + "cvar_aggregation = 1 / np.sqrt(gamma_circ)\n", + "print(\"\")\n", + "print(\"The corresponding CVaR aggregation value is: \", cvar_aggregation)\n", + "print(\n", + " \"To mitigate the twirled noise, increase shots by a factor of\",\n", + " np.sqrt(gamma_circ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "382e8acd-d0d0-4302-99aa-b64e5dd31e17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration 1: -13.227556797094595\n", + "Iteration 2: -13.181545294899571\n", + "Iteration 3: -13.149537293372594\n", + "Iteration 4: -3.305576300816324\n", + "Iteration 5: -12.647411769418035\n", + "Iteration 6: -13.443610807401718\n", + "Iteration 7: -12.475368761210511\n", + "Iteration 8: -15.905726329447413\n", + "Iteration 9: -18.011752834505565\n", + "Iteration 10: -14.125781339945583\n", + "Iteration 11: -19.693673319331744\n", + "Iteration 12: -21.175543794613695\n", + "Iteration 13: -21.805701324676196\n", + "Iteration 14: -22.121280244318488\n", + "Iteration 15: -20.02575633517435\n", + "Iteration 16: -22.399349757584158\n", + "Iteration 17: -22.569392265696226\n", + "Iteration 18: -21.877719328111898\n", + "Iteration 19: -22.79144777628963\n", + "Iteration 20: -22.437359259397432\n", + "Iteration 21: -23.021505287264777\n", + "Iteration 22: -22.69742427180412\n", + "Iteration 23: -23.12553129222746\n", + "Iteration 24: -22.893473281156922\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -23.12553129222746\n", + " x: [ 2.766e+00 1.080e+00]\n", + " nfev: 24\n", + " maxcv: 0.0\n" + ] + } + ], + "source": [ + "iter_counts = 0\n", + "result_dict = {}\n", + "init_params = [np.pi, np.pi / 2]\n", + "\n", + "with Session(backend=backend) as session:\n", + " sampler = Sampler(mode=session)\n", + " sampler.options.default_shots = int(1000 / cvar_aggregation)\n", + " sampler.options.dynamical_decoupling.enable = True\n", + " sampler.options.dynamical_decoupling.sequence_type = \"XY4\"\n", + " sampler.options.twirling.enable_gates = True\n", + " sampler.options.twirling.enable_measure = True\n", + " sampler.options.environment.job_tags = [\n", + " \"TUT_AQAOA\"\n", + " ] # add tag for your job execution\n", + "\n", + " result = minimize(\n", + " qaoa_sampler_cost_fun,\n", + " init_params,\n", + " args=(\n", + " transpiled_qaoa_circ,\n", + " remapped_cost_operator,\n", + " sampler,\n", + " cvar_aggregation,\n", + " ),\n", + " method=\"COBYLA\",\n", + " tol=1e-2,\n", + " )\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "1d190fa4-3bbe-412a-b296-6dddd3ad2b12", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "\n", + "Let's now visualize our results and then post-process them to find the value of the cut." + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "id": "761821cb-9a0c-4efb-806b-75513302d34a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(\n", + " [result_dict[i][\"cvar_fval\"] for i in range(1, iter_counts + 1)],\n", + " label=\"CVaR\",\n", + ")\n", + "plt.plot(\n", + " [result_dict[i][\"fval\"] for i in range(1, iter_counts + 1)],\n", + " label=\"Standard\",\n", + ")\n", + "plt.legend()\n", + "plt.xlabel(\"Iteration\")\n", + "plt.ylabel(\"Cost\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "38aadfcb-aec9-4dbb-a9d3-319239eae196", + "metadata": {}, + "source": [ + "The following retrieves the best solution from the sampled bitstrings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e8af29e-c99b-41f2-b6dd-2be471e1af21", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bitstring (int): 283561207335785714592526814041, probability: 0.00025693730729701953, objective value: -43.0\n" + ] + } + ], + "source": [ + "# Sort the result_dict[iter_counts]['evaluated'] by the CVaR value\n", + "sorted_result_dict = [\n", + " (k, v)\n", + " for k, v in sorted(\n", + " result_dict[iter_counts][\"evaluated\"].items(),\n", + " key=lambda item: item[1][1],\n", + " )\n", + "]\n", + "print(\n", + " f\"bitstring (int): {sorted_result_dict[0][0]}, probability: {sorted_result_dict[0][1][0]}, objective value: {sorted_result_dict[0][1][1]}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9cbeacc1-ff99-4b3d-b38d-b293a19642e2", + "metadata": {}, + "source": [ + "Consider the Hamiltonian $H_C$ for the **max-cut** problem. Let each vertex of the graph be associated with a qubit in state $|0\\rangle$ or $|1\\rangle$, where the value denotes the set the vertex is in. The goal of the problem is to maximize the number of edges $(v_1, v_2)$ for which $v_1 = |0\\rangle$ and $v_2 = |1\\rangle$, or vice versa. If we associate the $Z$ operator with each qubit, where\n", + "\n", + "$$\n", + " Z|0\\rangle = |0\\rangle \\qquad Z|1\\rangle = -|1\\rangle,\n", + "$$\n", + "\n", + "then an edge $(v_1, v_2)$ belongs to the cut if the eigenvalue of $(Z_1|v_1\\rangle) \\cdot (Z_2|v_2\\rangle) = -1$; in other words, the qubits associated with $v_1$ and $v_2$ are different. Similarly, $(v_1, v_2)$ does not belong to the cut if the eigenvalue of $(Z_1|v_1\\rangle) \\cdot (Z_2|v_2\\rangle) = 1$." + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "id": "5ea9e6aa-4297-4687-b484-1695d415bad5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Result bitstring (binary) : [1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0]\n", + "The value of the cut is: 77\n" + ] + } + ], + "source": [ + "from typing import Sequence\n", + "\n", + "\n", + "def to_bitstring(integer, num_bits):\n", + " result = np.binary_repr(integer, width=num_bits)\n", + " return [int(digit) for digit in result]\n", + "\n", + "\n", + "def evaluate_sample(x: Sequence[int], graph: rx.PyGraph) -> float:\n", + " assert len(x) == len(\n", + " list(graph.nodes())\n", + " ), \"The length of x must coincide with the number of nodes in the graph.\"\n", + " return sum(\n", + " x[u] * (1 - x[v])\n", + " + x[v]\n", + " * (\n", + " 1 - x[u]\n", + " ) # x[u] = x[v] if same cut, x[u] \\neq x[v] if different cuts\n", + " for u, v in list(graph.edge_list())\n", + " )\n", + "\n", + "\n", + "bitstring = to_bitstring(\n", + " sorted_result_dict[0][0], len(list(remapped_graph.nodes()))\n", + ")\n", + "bitstring = bitstring[::-1]\n", + "print(f\"Result bitstring (binary) : {bitstring}\")\n", + "\n", + "cut_value = evaluate_sample(bitstring, remapped_graph)\n", + "print(f\"The value of the cut is: {cut_value}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5879546-35ab-4876-bed9-262b85f130cc", + "metadata": {}, + "source": [ + "Finally, let's draw a graph based on the CVaR result.\n", + "We split the graph nodes into two sets based on the CVaR result.\n", + "The nodes in the first set are colored in gray, and the nodes in the second set are colored in purple.\n", + "The edges between the two sets are the edges that are cut by the partitioning." + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "id": "852dfeed-2871-4ca1-9754-15c95293198e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def plot_result(G, x):\n", + " colors = [\"tab:grey\" if i == 0 else \"tab:purple\" for i in x]\n", + " pos, _default_axes = rx.spring_layout(G), plt.axes(frameon=True)\n", + " rx.visualization.mpl_draw(\n", + " G,\n", + " node_color=colors,\n", + " node_size=150,\n", + " alpha=0.8,\n", + " pos=pos,\n", + " with_labels=True,\n", + " width=1,\n", + " )\n", + "\n", + "\n", + "plot_result(graph_100, to_bitstring(sorted_result_dict[0][0], 100)[::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "82f5c13b-a141-4657-adfd-bb18e88ad9f2", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "\\[1] Weidenfeller, J., Valor, L. C., Gacon, J., Tornow, C., Bello, L., Woerner, S., & Egger, D. J. (2022). Scaling of the quantum approximate optimization algorithm on superconducting qubit based hardware. Quantum, 6, 870.\n", + "\n", + "\\[2] Matsuo, A., Yamashita, S., & Egger, D. J. (2023). A SAT approach to the initial mapping problem in SWAP gate insertion for commuting gates. IEICE Transactions on Fundamentals of Electronics, Communications and Computer Sciences, 106(11), 1424-1431.\n", + "\n", + "\\[3] Barkoutsos, P. K., Nannicini, G., Robert, A., Tavernelli, I., & Woerner, S. (2020). Improving variational quantum optimization using CVaR. Quantum, 4, 256.\n", + "\n", + "\\[4] Barron, S. V., Egger, D. J., Pelofske, E., Bärtschi, A., Eidenbenz, S., Lehmkuehler, M., & Woerner, S. (2023). Provable bounds for noise-free expectation values computed from noisy samples. arXiv preprint arXiv:2312.00733." + ] + }, + { + "cell_type": "markdown", + "id": "a6a5bbfe-a159-4dc1-9333-488737aff503", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "If you found this work interesting, you might be interested in the following material:\n", + "\n", + "* [The intractable decathlon](https://arxiv.org/pdf/2504.03832): a listing of 10 optimization problems that are difficult for classical optimization algorithms, and which may be good use cases to test the techniques introduced in this tutorial.\n", + "* [A repo of best practices for quantum optimization](https://github.com/qiskit-community/qopt-best-practices) to further improve the results of your QAOA-based workflow.\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/ai-transpiler-introduction.ipynb b/docs/tutorials/ai-transpiler-introduction.ipynb index 738b6c4fcf4..730022eb87c 100644 --- a/docs/tutorials/ai-transpiler-introduction.ipynb +++ b/docs/tutorials/ai-transpiler-introduction.ipynb @@ -1,1171 +1,1172 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "f59032f0-f29a-4e52-9cab-855ed6f86b00", - "metadata": {}, - "source": [ - "---\n", - "title: Qiskit AI-powered transpiler service introduction\n", - "description: In this notebook, we will explore the key benefits of Qiskit AI-powered transpiler service and how it compares to traditional methods.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore fontsize idxmin */}\n", - "\n", - "# Qiskit AI-powered transpiler service introduction\n", - "*Estimated QPU usage: None (NOTE: This tutorial does not execute jobs because it is focused on transpilation)*\n", - "\n", - "## Background\n", - "\n", - "The **Qiskit AI-powered transpiler service (QTS)** introduces machine learning-based optimizations in both routing and synthesis passes. These AI modes have been designed to tackle the limitations of traditional transpilation, particularly for large-scale circuits and complex hardware topologies.\n", - "\n", - "As of **July 2025**, the **Transpiler Service** has been migrated to the new IBM Quantum® Platform and is no longer available. For the latest updates about the status of the Transpiler Service, please refer to the [transpiler service documentation](/docs/guides/qiskit-transpiler-service). You can still use the AI transpiler locally, similar to standard Qiskit transpilation. Simply replace `generate_preset_pass_manager()` with `generate_ai_pass_manager()`. This function constructs a pass manager that integrates the AI-powered routing and synthesis passes directly into your local transpilation workflow.\n", - "\n", - "### Key features of AI passes\n", - "\n", - "- Routing passes: AI-powered routing can dynamically adjust qubit paths based on the specific circuit and backend, reducing the need for excessive SWAP gates.\n", - " - `AIRouting`: Layout selection and circuit routing\n", - "\n", - "- Synthesis passes: AI techniques optimize the decomposition of multi-qubit gates, minimizing the number of two-qubit gates, which are typically more error-prone.\n", - " - `AICliffordSynthesis`: Clifford gate synthesis\n", - " - `AILinearFunctionSynthesis`: Linear function circuit synthesis\n", - " - `AIPermutationSynthesis`: Permutation circuit synthesis\n", - " - `AIPauliNetworkSynthesis`: Pauli Network circuit synthesis (only available in the Qiskit Transpiler Service, not in local environment)\n", - "\n", - "- Comparison with traditional transpilation: The standard Qiskit transpiler is a robust tool that can handle a broad spectrum of quantum circuits effectively. However, when circuits grow larger in scale or hardware configurations become more complex, AI passes can deliver additional optimization gains. By using learned models for routing and synthesis, QTS further refines circuit layouts and reduces overhead for challenging or large-scale quantum tasks.\n", - "\n", - "\n", - "This tutorial evaluates the AI modes using both routing and synthesis passes, comparing the results to traditional transpilation to highlight where AI offers performance gains.\n", - "\n", - "For more details on the available AI passes, see the [AI passes documentation](/docs/guides/ai-transpiler-passes).\n", - "\n", - "\n", - "### Why use AI for quantum circuit transpilation?\n", - "\n", - "As quantum circuits grow in size and complexity, traditional transpilation methods struggle to optimize layouts and reduce gate counts efficiently. Larger circuits, particularly those involving hundreds of qubits, impose significant challenges on routing and synthesis due to device constraints, limited connectivity, and qubit error rates.\n", - "\n", - "This is where AI-powered transpilation offers a potential solution. By leveraging machine learning techniques, the AI-powered transpiler in Qiskit can make smarter decisions about qubit routing and gate synthesis, leading to better optimization of large-scale quantum circuits.\n", - "\n", - "### Brief benchmarking results\n", - "![Graph showing AI transpiler performance against Qiskit](/docs/images/tutorials/ai-transpiler-introduction/ai-transpiler-benchmarks.avif)\n", - "\n", - "\n", - "In benchmarking tests, the AI transpiler consistently produced shallower, higher-quality circuits compared to the standard Qiskit transpiler. For these tests, we used Qiskit’s default pass manager strategy, configured with [`generate_preset_passmanager`]. While this default strategy is often effective, it can struggle with larger or more complex circuits. By contrast, AI-powered passes achieved an average 24% reduction in two-qubit gate counts and a 36% reduction in circuit depth for large circuits (100+ qubits) when transpiling to the heavy-hex topology of IBM Quantum hardware. For more information on these benchmarks, refer to this [blog](https://www.ibm.com/quantum/blog/qiskit-performance).\n", - "\n", - "This tutorial explores the key benefits of AI passes and how it compares to traditional methods." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "2aa75e36-471f-49aa-8478-134f13e3630b", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users;\n", - "# it just disables a linting rule.\n", - "# ruff: noqa: F811" - ] - }, - { - "cell_type": "markdown", - "id": "4a781d15-0953-4af6-b581-ea6cb3a74228", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, ensure that you have the following installed:\n", - "\n", - "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "* Qiskit Runtime (`pip install qiskit-ibm-runtime`) v0.22 or later\n", - "* Qiskit IBM® Transpiler with AI local mode(`pip install 'qiskit-ibm-transpiler[ai-local-mode]'`)" - ] - }, - { - "cell_type": "markdown", - "id": "c7c26e24-329b-4283-9cc0-67a241807049", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2c462d48-ae45-4528-9b09-cebc869a6812", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit.library import efficient_su2, PermutationGate\n", - "from qiskit.synthesis.qft import synth_qft_full\n", - "from qiskit.circuit.random import random_circuit, random_clifford_circuit\n", - "from qiskit.transpiler import generate_preset_pass_manager, CouplingMap\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", - "from qiskit.synthesis.permutation import (\n", - " synth_permutation_depth_lnn_kms,\n", - " synth_permutation_basic,\n", - ")\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "import numpy as np\n", - "import time\n", - "import logging\n", - "\n", - "seed = 42\n", - "\n", - "\n", - "# Used for generating permutation circuits in part two for comparison\n", - "def generate_permutation_circuit(width, pattern):\n", - " circuit = QuantumCircuit(width)\n", - " circuit.append(\n", - " PermutationGate(pattern=pattern),\n", - " qargs=range(width),\n", - " )\n", - " return circuit\n", - "\n", - "\n", - "# Creates a Bernstein-Vazirani circuit given the number of qubits\n", - "def create_bv_circuit(num_qubits):\n", - " qc = QuantumCircuit(num_qubits, num_qubits - 1)\n", - " qc.x(num_qubits - 1)\n", - " qc.h(qc.qubits)\n", - " for i in range(num_qubits - 1):\n", - " qc.cx(i, num_qubits - 1)\n", - " qc.h(qc.qubits[:-1])\n", - " return qc\n", - "\n", - "\n", - "# Transpile a circuit with a given pass manager and return metrics\n", - "def transpile_with_metrics(pass_manager, circuit):\n", - " start = time.time()\n", - " qc_out = pass_manager.run(circuit)\n", - " elapsed = time.time() - start\n", - "\n", - " depth_2q = qc_out.depth(lambda x: x.operation.num_qubits == 2)\n", - " gate_count = qc_out.size()\n", - "\n", - " return qc_out, {\n", - " \"depth_2q\": depth_2q,\n", - " \"gate_count\": gate_count,\n", - " \"time_s\": elapsed,\n", - " }\n", - "\n", - "\n", - "# Used for collecting metrics for part 3 of synthesis methods\n", - "def synth_transpile_with_metrics(qc, pm, pattern_id, method):\n", - " start = time.time()\n", - " qc = pm.run(qc)\n", - " elapsed = time.time() - start\n", - "\n", - " return {\n", - " \"Pattern\": pattern_id,\n", - " \"Method\": method,\n", - " \"Depth (2Q)\": qc.depth(lambda x: x.operation.num_qubits == 2),\n", - " \"Gates\": qc.size(),\n", - " \"Time (s)\": elapsed,\n", - " }\n", - "\n", - "\n", - "# Ignore logs like \"INFO:qiskit_ibm_transpiler.wrappers.ai_local_synthesis:Running Linear Functions AI synthesis on local mode\"\n", - "\n", - "logging.getLogger(\n", - " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", - ").setLevel(logging.WARNING)" - ] - }, - { - "cell_type": "markdown", - "id": "ba7568f8-50c9-47b4-acc0-33ea34f5fca0", - "metadata": {}, - "source": [ - "# Part I. Qiskit patterns\n", - "\n", - "Let's now see how to use the AI transpiler service with a simple quantum circuit, using Qiskit patterns. The key is creating a `PassManager` with `generate_ai_pass_manager()` instead of the standard `generate_preset_pass_manager()`." - ] - }, - { - "cell_type": "markdown", - "id": "5ba1bb22-272f-4f8f-ae78-7c3d1cdaacc6", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "In this section, we will test the AI transpiler on the `efficient_su2` circuit, a widely used hardware-efficient ansatz. This circuit is particularly relevant for variational quantum algorithms (for example, VQE) and quantum machine-learning tasks, making it an ideal test case for assessing transpilation performance.\n", - "\n", - "The `efficient_su2` circuit consists of alternating layers of single-qubit rotations and entangling gates like CNOTs. These layers enable flexible exploration of the quantum state space while keeping the gate depth manageable. By optimizing this circuit, we aim to reduce gate count, improve fidelity, and minimize noise. This makes it a strong candidate for testing the AI transpiler’s efficiency." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c6e9c2c0-e02c-4276-bae8-d5692e60b6b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# For our transpilation, we will use a large circuit of 101 qubits\n", - "qc = efficient_su2(90, entanglement=\"circular\", reps=1).decompose()\n", - "\n", - "# Draw a smaller version of the circuit to get a visual representation\n", - "qc_small = efficient_su2(5, entanglement=\"circular\", reps=1).decompose()\n", - "qc_small.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "6c7c76f7-c376-47e9-bc9c-dbe32b2c89b7", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "### Choose a backend\n", - "\n", - "For this example, we select the least busy operational IBM Quantum backend that is not a simulator and has at least 100 qubits:\n", - "\n", - "**Note:** Since the least-busy backend can change over time, different devices might be selected for different runs. Device-specific properties, such as coupling maps, can lead to differences in the transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6b6e55e-9b70-4c94-8bbf-5ea47d0510ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using backend: ibm_torino\n" - ] - } - ], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=100\n", - ")\n", - "cm = backend.coupling_map\n", - "print(f\"Using backend: {backend.name}\")" - ] - }, - { - "cell_type": "markdown", - "id": "7b02350f-998e-40cf-a79e-2e6182b5a875", - "metadata": {}, - "source": [ - "### Create AI and traditional pass managers\n", - "To evaluate the effectiveness of the AI transpiler, we will perform two transpilation runs. First, we will transpile the circuit using the AI transpiler. Then, we will run a comparison by transpiling the same circuit without the AI transpiler, using traditional methods. Both transpilation processes will use the same coupling map from the chosen backend and the optimization level set to 3 for a fair comparison.\n", - "\n", - "Both of these methods reflect the standard approach to create `PassManager` instances to transpile circuits in Qiskit." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a1aa25dd-41a9-4416-a959-44f28af613c8", - "metadata": {}, - "outputs": [], - "source": [ - "pm_ai = generate_ai_pass_manager(\n", - " optimization_level=3,\n", - " ai_optimization_level=3,\n", - " coupling_map=cm,\n", - " include_ai_synthesis=True, # used for part 3 when comparing synthesis methods\n", - ")\n", - "\n", - "pm_no_ai = generate_preset_pass_manager(\n", - " optimization_level=3,\n", - " coupling_map=cm,\n", - " seed_transpiler=seed, # note that the AI pass manager does not currently support seeding\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a06d6144-3445-4446-a3e1-18ca78a1173c", - "metadata": {}, - "source": [ - "Transpile the circuits and record the times." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "fb5167bd-35f0-432f-af6d-023c70783d20", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Standard transpilation: Depth (2q) 95, Gate count 458, Time 0.04650712013244629\n", - "AI transpilation : Depth (2q) 90, Gate count 456, Time 0.9342479705810547\n" - ] - } - ], - "source": [ - "# Transpile using standard (non-AI) pass manager\n", - "_, metrics_no_ai = transpile_with_metrics(pm_no_ai, qc)\n", - "print(\n", - " f\"Standard transpilation: Depth (2q) {metrics_no_ai['depth_2q']}, \"\n", - " f\"Gate count {metrics_no_ai['gate_count']}, Time {metrics_no_ai['time_s']}\"\n", - ")\n", - "\n", - "# Transpile using AI pass manager\n", - "_, metrics_ai = transpile_with_metrics(pm_ai, qc)\n", - "print(\n", - " f\"AI transpilation : Depth (2q) {metrics_ai['depth_2q']}, \"\n", - " f\"Gate count {metrics_ai['gate_count']}, Time {metrics_ai['time_s']}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d934ebd2-e594-4076-8b21-822087df01ea", - "metadata": {}, - "source": [ - "In this test, we compare the performance of the AI transpiler and the standard transpilation method on the efficient_su2 circuit. The AI transpiler achieves a noticeably shallower circuit depth while maintaining a similar gate count.\n", - "\n", - "- **Circuit depth:** The AI transpiler produces a circuit with lower two-qubit depth. This is expected, as the AI passes are trained to optimize depth by learning qubit interaction patterns and exploiting hardware connectivity more effectively than rule-based heuristics.\n", - "\n", - "- **Gate count:** The total gate count remains similar between the two methods. This aligns with expectations since the standard SABRE-based transpilation explicitly minimizes swap count, which dominates gate overhead. The AI transpiler instead prioritizes overall depth and may occasionally trade off a few additional gates for a shorter execution path.\n", - "\n", - "- **Transpilation time:** The AI transpiler takes longer to run than the standard method. This is due to the added computational cost of invoking learned models during routing and synthesis. In contrast, the SABRE-based transpiler is now significantly faster after being rewritten and optimized in Rust, providing highly efficient heuristic routing at scale.\n", - "\n", - "It is important to note that these results are based on just one circuit. To obtain a comprehensive understanding of how the AI transpiler compares to traditional methods, it is necessary to test a variety of circuits. The performance of QTS can vary greatly depending on the type of circuit being optimized. For a broader comparison, refer to the benchmarks above or visit the [blog.](https://www.ibm.com/quantum/blog/qiskit-performance)" - ] - }, - { - "cell_type": "markdown", - "id": "c8a55587-abf6-4096-85fd-2702a077ae75", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." - ] - }, - { - "cell_type": "markdown", - "id": "8d0cfca9-be4e-40ab-ab98-d7899bb8b3fa", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "c82277b2-22e9-44fe-886e-e8ceb2178278", - "metadata": {}, - "source": [ - "# Part II. Analyze and benchmark the transpiled circuits\n", - "\n", - "In this section, we will demonstrate how to analyze the transpiled circuit and benchmark it against the original version in more detail. We will focus on metrics such as circuit depth, gate count, and transpilation time to assess the effectiveness of the optimization. Additionally, we will discuss how the results may differ across various circuit types, offering insights into the broader performance of the transpiler across different scenarios." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "ee24725b-64c9-4d6a-aa97-5a3502b0982a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Completed transpilation for Random\n", - "Completed transpilation for Clifford\n", - "Completed transpilation for QFT\n", - "Completed transpilation for BV\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
CircuitDepth 2Q (No AI)Gate Count (No AI)Time (No AI)Depth 2Q (AI)Gate Count (AI)Time (AI)
0Random372210.039347241810.773718
1Clifford362320.036633432671.097431
2QFT1659240.0774581309133.660771
3BV651550.024993701550.345522
\n", - "
" - ], - "text/plain": [ - " Circuit Depth 2Q (No AI) Gate Count (No AI) Time (No AI) \\\n", - "0 Random 37 221 0.039347 \n", - "1 Clifford 36 232 0.036633 \n", - "2 QFT 165 924 0.077458 \n", - "3 BV 65 155 0.024993 \n", - "\n", - " Depth 2Q (AI) Gate Count (AI) Time (AI) \n", - "0 24 181 0.773718 \n", - "1 43 267 1.097431 \n", - "2 130 913 3.660771 \n", - "3 70 155 0.345522 " - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Circuits to benchmark\n", - "seed = 42\n", - "circuits = [\n", - " {\n", - " \"name\": \"Random\",\n", - " \"qc\": random_circuit(num_qubits=30, depth=10, seed=seed),\n", - " },\n", - " {\n", - " \"name\": \"Clifford\",\n", - " \"qc\": random_clifford_circuit(\n", - " num_qubits=40, num_gates=200, seed=seed\n", - " ),\n", - " },\n", - " {\n", - " \"name\": \"QFT\",\n", - " \"qc\": synth_qft_full(num_qubits=20, do_swaps=False).decompose(),\n", - " },\n", - " {\n", - " \"name\": \"BV\",\n", - " \"qc\": create_bv_circuit(40),\n", - " },\n", - "]\n", - "\n", - "results = []\n", - "\n", - "# Run the transpilation for each circuit and store the results\n", - "for circuit in circuits:\n", - " qc_no_ai, metrics_no_ai = transpile_with_metrics(pm_no_ai, circuit[\"qc\"])\n", - " qc_ai, metrics_ai = transpile_with_metrics(pm_ai, circuit[\"qc\"])\n", - "\n", - " print(\"Completed transpilation for\", circuit[\"name\"])\n", - "\n", - " results.append(\n", - " {\n", - " \"Circuit\": circuit[\"name\"],\n", - " \"Depth 2Q (No AI)\": metrics_no_ai[\"depth_2q\"],\n", - " \"Gate Count (No AI)\": metrics_no_ai[\"gate_count\"],\n", - " \"Time (No AI)\": metrics_no_ai[\"time_s\"],\n", - " \"Depth 2Q (AI)\": metrics_ai[\"depth_2q\"],\n", - " \"Gate Count (AI)\": metrics_ai[\"gate_count\"],\n", - " \"Time (AI)\": metrics_ai[\"time_s\"],\n", - " }\n", - " )\n", - "\n", - "df = pd.DataFrame(results)\n", - "df" - ] - }, - { - "cell_type": "markdown", - "id": "061d85cf-3841-4ed3-bd0d-cd950564efb7", - "metadata": {}, - "source": [ - "Average percentage reduction for each metric. Positive are improvements, negative are degradations." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "70cf9c05-62a3-4049-9712-319902107ba6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Average reduction in depth: 11.88%\n", - "Average reduction in gate count: 1.04%\n", - "Average reduction in transpilation time: -3193.95%\n" - ] - } - ], - "source": [ - "# Average reduction from non-AI to AI transpilation as a percentage\n", - "avg_reduction_depth = (\n", - " (df[\"Depth 2Q (No AI)\"] - df[\"Depth 2Q (AI)\"]).mean()\n", - " / df[\"Depth 2Q (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "avg_reduction_gates = (\n", - " (df[\"Gate Count (No AI)\"] - df[\"Gate Count (AI)\"]).mean()\n", - " / df[\"Gate Count (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "avg_reduction_time = (\n", - " (df[\"Time (No AI)\"] - df[\"Time (AI)\"]).mean()\n", - " / df[\"Time (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "\n", - "print(f\"Average reduction in depth: {avg_reduction_depth:.2f}%\")\n", - "print(f\"Average reduction in gate count: {avg_reduction_gates:.2f}%\")\n", - "print(f\"Average reduction in transpilation time: {avg_reduction_time:.2f}%\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "79b8d5d9-0f9d-42ca-9583-8bec17430014", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, axs = plt.subplots(1, 3, figsize=(21, 6))\n", - "df.plot(\n", - " x=\"Circuit\",\n", - " y=[\"Depth 2Q (No AI)\", \"Depth 2Q (AI)\"],\n", - " kind=\"bar\",\n", - " ax=axs[0],\n", - ")\n", - "axs[0].set_title(\"Circuit Depth Comparison\")\n", - "axs[0].set_ylabel(\"Depth\")\n", - "axs[0].set_xlabel(\"Circuit\")\n", - "axs[0].tick_params(axis=\"x\", rotation=45)\n", - "df.plot(\n", - " x=\"Circuit\",\n", - " y=[\"Gate Count (No AI)\", \"Gate Count (AI)\"],\n", - " kind=\"bar\",\n", - " ax=axs[1],\n", - ")\n", - "axs[1].set_title(\"Gate Count Comparison\")\n", - "axs[1].set_ylabel(\"Gate Count\")\n", - "axs[1].set_xlabel(\"Circuit\")\n", - "axs[1].tick_params(axis=\"x\", rotation=45)\n", - "df.plot(x=\"Circuit\", y=[\"Time (No AI)\", \"Time (AI)\"], kind=\"bar\", ax=axs[2])\n", - "axs[2].set_title(\"Time Comparison\")\n", - "axs[2].set_ylabel(\"Time (seconds)\")\n", - "axs[2].set_xlabel(\"Circuit\")\n", - "axs[2].tick_params(axis=\"x\", rotation=45)\n", - "fig.suptitle(\n", - " \"Benchmarking AI transpilation vs Non-AI transpilation for various circuits\"\n", - ")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "345022d3-e302-47e6-9453-9261136923a7", - "metadata": {}, - "source": [ - "The AI transpiler's performance varies significantly based on the type of circuit being optimized. In some cases, it achieves notable reductions in circuit depth and gate count compared to the standard transpiler. However, these improvements often come with a substantial increase in runtime.\n", - "\n", - "For certain types of circuits, the AI transpiler may yield slightly better results in terms of circuit depth but may also lead to an increase in gate count and a significant runtime penalty. These observations suggest that the AI transpiler's benefits are not uniform across all circuit types. Instead, its effectiveness depends on the specific characteristics of the circuit, making it more suitable for some use cases than others." - ] - }, - { - "cell_type": "markdown", - "id": "9e496e7a-64a8-46fd-b240-c494e7825bd2", - "metadata": {}, - "source": [ - "## When should users choose AI-powered transpilation?\n", - "\n", - "The AI-powered transpiler in Qiskit excels in scenarios where traditional transpilation methods struggle, particularly with large-scale and complex quantum circuits. For circuits involving hundreds of qubits or those targeting hardware with intricate coupling maps, the AI transpiler offers superior optimization in terms of circuit depth, gate count, and runtime efficiency. In benchmarking tests, it has consistently outperformed traditional methods, delivering significantly shallower circuits and reducing gate counts, which are critical for enhancing performance and mitigating noise on real quantum hardware.\n", - "\n", - "Users should consider AI-powered transpilation when working with:\n", - "- Large circuits where traditional methods fail to efficiently handle the scale.\n", - "- Complex hardware topologies where device connectivity and routing challenges arise.\n", - "- Performance-sensitive applications where reducing circuit depth and improving fidelity are paramount." - ] - }, - { - "cell_type": "markdown", - "id": "c345cb54-a838-427f-898f-51fb607da493", - "metadata": {}, - "source": [ - "# Part III. Explore AI-powered permutation network synthesis\n", - "\n", - "Permutation networks are foundational in quantum computing, particularly for systems constrained by restricted topologies. These networks facilitate long-range interactions by dynamically swapping qubits to mimic all-to-all connectivity on hardware with limited connectivity. Such transformations are essential for implementing complex quantum algorithms on near-term devices, where interactions often span beyond nearest neighbors.\n", - "\n", - "In this section, we highlight the synthesis of permutation networks as a compelling use case for the AI-powered transpiler in Qiskit. Specifically, the `AIPermutationSynthesis` pass leverages AI-driven optimization to generate efficient circuits for qubit permutation tasks. By contrast, generic synthesis approaches often struggle to balance gate count and circuit depth, especially in scenarios with dense qubit interactions or when attempting to achieve full connectivity.\n", - "\n", - "We will walk through a Qiskit patterns example showcasing the synthesis of a permutation network to achieve all-to-all connectivity for a set of qubits. We will compare the performance of `AIPermutationSynthesis` against the standard synthesis methods in Qiskit. This example will demonstrate how the AI transpiler optimizes for lower circuit depth and gate count, highlighting its advantages in practical quantum workflows. To activate the AI synthesis pass, we will use the `generate_ai_pass_manager()` function with the `include_ai_synthesis` parameter set to `True`." - ] - }, - { - "cell_type": "markdown", - "id": "76de0959-1eca-43d9-b8fe-f9aea9a122d8", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "To represent a classical permutation problem on a quantum computer, we start by defining the structure of the quantum circuits. For this example:\n", - "\n", - "1. Quantum circuit initialization:\n", - " We allocate 27 qubits to match the backend we will use, which has 27 qubits.\n", - "\n", - "2. Apply permutations:\n", - " We generate ten random permutation patterns (`pattern_1` through `pattern_10`) using a fixed seed for reproducibility. Each permutation pattern is applied to a separate quantum circuit (`qc_1` through `qc_10`).\n", - "\n", - "3. Circuit decomposition:\n", - " Each permutation operation is decomposed into native gate sets compatible with the target quantum hardware. We analyze the depth and the number of two-qubit gates (nonlocal gates) for each decomposed circuit.\n", - "\n", - "The results provide insight into the complexity of representing classical permutation problems on a quantum device, demonstrating the resource requirements for different permutation patterns." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "76a3e847-0808-4413-bd0c-c760cd2df3f4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Parameters\n", - "width = 27\n", - "num_circuits = 10\n", - "\n", - "# Set random seed\n", - "np.random.seed(seed)\n", - "\n", - "\n", - "# Generate random patterns and circuits\n", - "patterns = [\n", - " np.random.permutation(width).tolist() for _ in range(num_circuits)\n", - "]\n", - "circuits = {\n", - " f\"qc_{i}\": generate_permutation_circuit(width, pattern)\n", - " for i, pattern in enumerate(patterns, start=1)\n", - "}\n", - "\n", - "# Display one of the circuits\n", - "circuits[\"qc_1\"].decompose(reps=3).draw(output=\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "a8b79798-fa80-44d8-8a52-2d2a50e0c280", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "In this step, we proceed with optimization using the AI synthesis passes.\n", - "\n", - "For the AI synthesis passes, the `PassManager` requires only the coupling map of the backend. However, it is important to note that not all coupling maps are compatible; only those that the `AIPermutationSynthesis` pass has been trained on will work. Currently, the `AIPermutationSynthesis` pass supports blocks of sizes 65, 33, and 27 qubits. For this example we use a 27-qubit QPU.\n", - "\n", - "For comparison, we will evaluate the performance of AI synthesis against generic permutation synthesis methods in Qiskit, including:\n", - "\n", - "- `synth_permutation_depth_lnn_kms`: This method synthesizes a permutation circuit for a linear nearest-neighbor (LNN) architecture using the Kutin, Moulton, and Smithline (KMS) algorithm. It guarantees a circuit with a depth of at most $ n $ and a size of at most $ n(n-1)/2 $, where both depth and size are measured in terms of SWAP gates.\n", - "\n", - "- `synth_permutation_basic`: This is a straightforward implementation that synthesizes permutation circuits without imposing constraints on connectivity or optimization for specific architectures. It serves as a baseline for comparing performance with more advanced methods.\n", - "\n", - "Each of these methods represents a distinct approach to synthesizing permutation networks, providing a comprehensive benchmark against the AI-powered methods.\n", - "\n", - "For more details about synthesis methods in Qiskit, refer to the [Qiskit API documentation](/docs/api/qiskit/synthesis)." - ] - }, - { - "cell_type": "markdown", - "id": "b1733a10-c285-444e-af47-4a32329c5f7a", - "metadata": {}, - "source": [ - "Define the coupling map representing the 27-qubit QPU." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "84dff2c2-a496-4828-bb8e-08d373816a36", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "coupling_map = [\n", - " [1, 0],\n", - " [2, 1],\n", - " [3, 2],\n", - " [3, 5],\n", - " [4, 1],\n", - " [6, 7],\n", - " [7, 4],\n", - " [7, 10],\n", - " [8, 5],\n", - " [8, 9],\n", - " [8, 11],\n", - " [11, 14],\n", - " [12, 10],\n", - " [12, 13],\n", - " [12, 15],\n", - " [13, 14],\n", - " [16, 14],\n", - " [17, 18],\n", - " [18, 15],\n", - " [18, 21],\n", - " [19, 16],\n", - " [19, 22],\n", - " [20, 19],\n", - " [21, 23],\n", - " [23, 24],\n", - " [25, 22],\n", - " [25, 24],\n", - " [26, 25],\n", - "]\n", - "CouplingMap(coupling_map).draw()" - ] - }, - { - "cell_type": "markdown", - "id": "47bdb1f5-1fc6-46c4-8fc9-98d16a4d2529", - "metadata": {}, - "source": [ - "Transpile each of the permutation circuits using the AI synthesis passes and generic synthesis methods." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "128cc285-094a-4b07-a37d-8424a4003b2c", - "metadata": {}, - "outputs": [], - "source": [ - "results = []\n", - "pm_no_ai_synth = generate_preset_pass_manager(\n", - " coupling_map=cm,\n", - " optimization_level=1, # set to 1 since we are using the synthesis methods\n", - ")\n", - "\n", - "# Transpile and analyze all circuits\n", - "for i, (qc_name, qc) in enumerate(circuits.items(), start=1):\n", - " pattern = patterns[i - 1] # Get the corresponding pattern\n", - "\n", - " qc_depth_lnn_kms = synth_permutation_depth_lnn_kms(pattern)\n", - " qc_basic = synth_permutation_basic(pattern)\n", - "\n", - " # AI synthesis\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc.decompose(reps=3),\n", - " pm_ai,\n", - " qc_name,\n", - " \"AI\",\n", - " )\n", - " )\n", - "\n", - " # Depth-LNN-KMS Method\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc_depth_lnn_kms.decompose(reps=3),\n", - " pm_no_ai_synth,\n", - " qc_name,\n", - " \"Depth-LNN-KMS\",\n", - " )\n", - " )\n", - "\n", - " # Basic Method\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc_basic.decompose(reps=3),\n", - " pm_no_ai_synth,\n", - " qc_name,\n", - " \"Basic\",\n", - " )\n", - " )\n", - "\n", - "\n", - "results_df = pd.DataFrame(results)" - ] - }, - { - "cell_type": "markdown", - "id": "42f80e32-60fd-46a8-a6b5-4bcadb15810a", - "metadata": {}, - "source": [ - "Record the metrics (depth, gate count, time) for each circuit after transpilation." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "72ee8474-eea6-421a-9d7d-070587eaff71", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "=== Average Metrics ===\n", - " Depth (2Q) Gates Time (s)\n", - "Method \n", - "AI 23.9 82.8 0.248\n", - "Basic 29.8 91.0 0.012\n", - "Depth-LNN-KMS 70.8 531.6 0.017\n", - "\n", - "Best Non-AI Method (based on least average depth): Basic\n", - "\n", - "=== Comparison of AI vs Best Non-AI Method ===\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
MetricAIBasicImprovement (AI vs Best Non-AI)
0Depth (2Q)23.90029.800-5.900
1Gates82.80091.000-8.200
2Time (s)0.2480.0120.236
\n", - "
" - ], - "text/plain": [ - " Metric AI Basic Improvement (AI vs Best Non-AI)\n", - "0 Depth (2Q) 23.900 29.800 -5.900\n", - "1 Gates 82.800 91.000 -8.200\n", - "2 Time (s) 0.248 0.012 0.236" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Calculate averages for each metric\n", - "average_metrics = results_df.groupby(\"Method\")[\n", - " [\"Depth (2Q)\", \"Gates\", \"Time (s)\"]\n", - "].mean()\n", - "average_metrics = average_metrics.round(3) # Round to two decimal places\n", - "print(\"\\n=== Average Metrics ===\")\n", - "print(average_metrics)\n", - "\n", - "# Identify the best non-AI method based on least average depth\n", - "non_ai_methods = [\n", - " method for method in results_df[\"Method\"].unique() if method != \"AI\"\n", - "]\n", - "best_non_ai_method = average_metrics.loc[non_ai_methods][\n", - " \"Depth (2Q)\"\n", - "].idxmin()\n", - "print(\n", - " f\"\\nBest Non-AI Method (based on least average depth): {best_non_ai_method}\"\n", - ")\n", - "\n", - "# Compare AI to the best non-AI method\n", - "ai_metrics = average_metrics.loc[\"AI\"]\n", - "best_non_ai_metrics = average_metrics.loc[best_non_ai_method]\n", - "\n", - "comparison = {\n", - " \"Metric\": [\"Depth (2Q)\", \"Gates\", \"Time (s)\"],\n", - " \"AI\": [\n", - " ai_metrics[\"Depth (2Q)\"],\n", - " ai_metrics[\"Gates\"],\n", - " ai_metrics[\"Time (s)\"],\n", - " ],\n", - " best_non_ai_method: [\n", - " best_non_ai_metrics[\"Depth (2Q)\"],\n", - " best_non_ai_metrics[\"Gates\"],\n", - " best_non_ai_metrics[\"Time (s)\"],\n", - " ],\n", - " \"Improvement (AI vs Best Non-AI)\": [\n", - " ai_metrics[\"Depth (2Q)\"] - best_non_ai_metrics[\"Depth (2Q)\"],\n", - " ai_metrics[\"Gates\"] - best_non_ai_metrics[\"Gates\"],\n", - " ai_metrics[\"Time (s)\"] - best_non_ai_metrics[\"Time (s)\"],\n", - " ],\n", - "}\n", - "\n", - "comparison_df = pd.DataFrame(comparison)\n", - "print(\"\\n=== Comparison of AI vs Best Non-AI Method ===\")\n", - "comparison_df" - ] - }, - { - "cell_type": "markdown", - "id": "e1ba3767-5ce1-4663-803b-73ccfc22f03b", - "metadata": {}, - "source": [ - "The results demonstrate that the AI transpiler outperforms all other Qiskit synthesis methods for this set of random permutation circuits. Key findings include:\n", - "\n", - "1. Depth: The AI transpiler achieves the lowest average depth, indicating superior optimization of circuit layouts.\n", - "2. Gate count: It significantly reduces the number of gates compared to other methods, improving execution fidelity and efficiency.\n", - "3. Transpilation time: All methods run very quickly at this scale, making them practical for use. However, the AI transpiler does has a notable runtime increase compared to traditional methods due to the complexity of the AI models used.\n", - "\n", - "These results establish the AI transpiler as the most effective approach for this benchmark, particularly for depth and gate count optimization." - ] - }, - { - "cell_type": "markdown", - "id": "dbaab943-5fd7-4720-98bf-8602b2ab4473", - "metadata": {}, - "source": [ - "Plot the results to compare the performance of the AI synthesis passes against the generic synthesis methods." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "a326f268-0115-442c-8563-968676b66670", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "methods = results_df[\"Method\"].unique()\n", - "\n", - "fig, axs = plt.subplots(1, 3, figsize=(18, 5))\n", - "\n", - "# Pivot the DataFrame and reorder columns to ensure AI is first\n", - "pivot_depth = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Depth (2Q)\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "pivot_gates = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Gates\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "pivot_time = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Time (s)\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "\n", - "pivot_depth.plot(kind=\"bar\", ax=axs[0], legend=False)\n", - "axs[0].set_title(\"Circuit Depth Comparison\")\n", - "axs[0].set_ylabel(\"Depth\")\n", - "axs[0].set_xlabel(\"Pattern\")\n", - "axs[0].tick_params(axis=\"x\", rotation=45)\n", - "pivot_gates.plot(kind=\"bar\", ax=axs[1], legend=False)\n", - "axs[1].set_title(\"2Q Gate Count Comparison\")\n", - "axs[1].set_ylabel(\"Number of 2Q Gates\")\n", - "axs[1].set_xlabel(\"Pattern\")\n", - "axs[1].tick_params(axis=\"x\", rotation=45)\n", - "pivot_time.plot(\n", - " kind=\"bar\", ax=axs[2], legend=True, title=\"Legend\"\n", - ") # Show legend on the last plot\n", - "axs[2].set_title(\"Time Comparison\")\n", - "axs[2].set_ylabel(\"Time (seconds)\")\n", - "axs[2].set_xlabel(\"Pattern\")\n", - "axs[2].tick_params(axis=\"x\", rotation=45)\n", - "fig.suptitle(\n", - " \"Benchmarking AI Synthesis Methods vs Non-AI Synthesis Methods For Random Permutations Circuits\",\n", - " fontsize=16,\n", - " y=1,\n", - ")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "03a9af42-42a7-4344-b834-0d2b506d4d78", - "metadata": {}, - "source": [ - "This graph highlights the individual results for each circuit (`qc_1` to `qc_10`) across different synthesis methods:\n", - "\n", - "While these results underscore the AI transpiler’s effectiveness for permutation circuits, it is important to note its limitations. The AI synthesis method is currently only available for certain coupling maps, which may restrict its broader applicability. This constraint should be considered when evaluating its usage in different scenarios.\n", - "\n", - "Overall, the AI transpiler demonstrates promising improvements in depth and gate count optimization for these specific circuits while maintaining comparable transpilation times." - ] - }, - { - "cell_type": "markdown", - "id": "41b1405d-fa90-48b6-9ce2-933f05358778", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." - ] - }, - { - "cell_type": "markdown", - "id": "3d942ee4-e4d7-4e87-8c8a-17c662d5379f", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "3b21bb06-7a2b-4181-af59-734c89435d45", - "metadata": {}, - "source": [ - "## Tutorial survey\n", - "\n", - "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", - "\n", - "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_0igXMtMCQfApgDI)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "f59032f0-f29a-4e52-9cab-855ed6f86b00", + "metadata": {}, + "source": [ + "---\n", + "title: Qiskit AI-powered transpiler service introduction\n", + "description: In this notebook, we will explore the key benefits of Qiskit AI-powered transpiler service and how it compares to traditional methods.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore fontsize idxmin */}\n", + "\n", + "# Qiskit AI-powered transpiler service introduction\n", + "*Estimated QPU usage: None (NOTE: This tutorial does not execute jobs because it is focused on transpilation)*\n", + "\n", + "## Background\n", + "\n", + "The **Qiskit AI-powered transpiler service (QTS)** introduces machine learning-based optimizations in both routing and synthesis passes. These AI modes have been designed to tackle the limitations of traditional transpilation, particularly for large-scale circuits and complex hardware topologies.\n", + "\n", + "As of **July 2025**, the **Transpiler Service** has been migrated to the new IBM Quantum® Platform and is no longer available. For the latest updates about the status of the Transpiler Service, please refer to the [transpiler service documentation](/docs/guides/qiskit-transpiler-service). You can still use the AI transpiler locally, similar to standard Qiskit transpilation. Simply replace `generate_preset_pass_manager()` with `generate_ai_pass_manager()`. This function constructs a pass manager that integrates the AI-powered routing and synthesis passes directly into your local transpilation workflow.\n", + "\n", + "### Key features of AI passes\n", + "\n", + "- Routing passes: AI-powered routing can dynamically adjust qubit paths based on the specific circuit and backend, reducing the need for excessive SWAP gates.\n", + " - `AIRouting`: Layout selection and circuit routing\n", + "\n", + "- Synthesis passes: AI techniques optimize the decomposition of multi-qubit gates, minimizing the number of two-qubit gates, which are typically more error-prone.\n", + " - `AICliffordSynthesis`: Clifford gate synthesis\n", + " - `AILinearFunctionSynthesis`: Linear function circuit synthesis\n", + " - `AIPermutationSynthesis`: Permutation circuit synthesis\n", + " - `AIPauliNetworkSynthesis`: Pauli Network circuit synthesis (only available in the Qiskit Transpiler Service, not in local environment)\n", + "\n", + "- Comparison with traditional transpilation: The standard Qiskit transpiler is a robust tool that can handle a broad spectrum of quantum circuits effectively. However, when circuits grow larger in scale or hardware configurations become more complex, AI passes can deliver additional optimization gains. By using learned models for routing and synthesis, QTS further refines circuit layouts and reduces overhead for challenging or large-scale quantum tasks.\n", + "\n", + "\n", + "This tutorial evaluates the AI modes using both routing and synthesis passes, comparing the results to traditional transpilation to highlight where AI offers performance gains.\n", + "\n", + "For more details on the available AI passes, see the [AI passes documentation](/docs/guides/ai-transpiler-passes).\n", + "\n", + "\n", + "### Why use AI for quantum circuit transpilation?\n", + "\n", + "As quantum circuits grow in size and complexity, traditional transpilation methods struggle to optimize layouts and reduce gate counts efficiently. Larger circuits, particularly those involving hundreds of qubits, impose significant challenges on routing and synthesis due to device constraints, limited connectivity, and qubit error rates.\n", + "\n", + "This is where AI-powered transpilation offers a potential solution. By leveraging machine learning techniques, the AI-powered transpiler in Qiskit can make smarter decisions about qubit routing and gate synthesis, leading to better optimization of large-scale quantum circuits.\n", + "\n", + "### Brief benchmarking results\n", + "![Graph showing AI transpiler performance against Qiskit](/docs/images/tutorials/ai-transpiler-introduction/ai-transpiler-benchmarks.avif)\n", + "\n", + "\n", + "In benchmarking tests, the AI transpiler consistently produced shallower, higher-quality circuits compared to the standard Qiskit transpiler. For these tests, we used Qiskit’s default pass manager strategy, configured with [`generate_preset_passmanager`]. While this default strategy is often effective, it can struggle with larger or more complex circuits. By contrast, AI-powered passes achieved an average 24% reduction in two-qubit gate counts and a 36% reduction in circuit depth for large circuits (100+ qubits) when transpiling to the heavy-hex topology of IBM Quantum hardware. For more information on these benchmarks, refer to this [blog](https://www.ibm.com/quantum/blog/qiskit-performance).\n", + "\n", + "This tutorial explores the key benefits of AI passes and how it compares to traditional methods." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2aa75e36-471f-49aa-8478-134f13e3630b", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users;\n", + "# it just disables a linting rule.\n", + "# ruff: noqa: F811" + ] + }, + { + "cell_type": "markdown", + "id": "4a781d15-0953-4af6-b581-ea6cb3a74228", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "* Qiskit Runtime (`pip install qiskit-ibm-runtime`) v0.22 or later\n", + "* Qiskit IBM® Transpiler with AI local mode(`pip install 'qiskit-ibm-transpiler[ai-local-mode]'`)" + ] + }, + { + "cell_type": "markdown", + "id": "c7c26e24-329b-4283-9cc0-67a241807049", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2c462d48-ae45-4528-9b09-cebc869a6812", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit.library import efficient_su2, PermutationGate\n", + "from qiskit.synthesis.qft import synth_qft_full\n", + "from qiskit.circuit.random import random_circuit, random_clifford_circuit\n", + "from qiskit.transpiler import generate_preset_pass_manager, CouplingMap\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", + "from qiskit.synthesis.permutation import (\n", + " synth_permutation_depth_lnn_kms,\n", + " synth_permutation_basic,\n", + ")\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "import time\n", + "import logging\n", + "\n", + "seed = 42\n", + "\n", + "\n", + "# Used for generating permutation circuits in part two for comparison\n", + "def generate_permutation_circuit(width, pattern):\n", + " circuit = QuantumCircuit(width)\n", + " circuit.append(\n", + " PermutationGate(pattern=pattern),\n", + " qargs=range(width),\n", + " )\n", + " return circuit\n", + "\n", + "\n", + "# Creates a Bernstein-Vazirani circuit given the number of qubits\n", + "def create_bv_circuit(num_qubits):\n", + " qc = QuantumCircuit(num_qubits, num_qubits - 1)\n", + " qc.x(num_qubits - 1)\n", + " qc.h(qc.qubits)\n", + " for i in range(num_qubits - 1):\n", + " qc.cx(i, num_qubits - 1)\n", + " qc.h(qc.qubits[:-1])\n", + " return qc\n", + "\n", + "\n", + "# Transpile a circuit with a given pass manager and return metrics\n", + "def transpile_with_metrics(pass_manager, circuit):\n", + " start = time.time()\n", + " qc_out = pass_manager.run(circuit)\n", + " elapsed = time.time() - start\n", + "\n", + " depth_2q = qc_out.depth(lambda x: x.operation.num_qubits == 2)\n", + " gate_count = qc_out.size()\n", + "\n", + " return qc_out, {\n", + " \"depth_2q\": depth_2q,\n", + " \"gate_count\": gate_count,\n", + " \"time_s\": elapsed,\n", + " }\n", + "\n", + "\n", + "# Used for collecting metrics for part 3 of synthesis methods\n", + "def synth_transpile_with_metrics(qc, pm, pattern_id, method):\n", + " start = time.time()\n", + " qc = pm.run(qc)\n", + " elapsed = time.time() - start\n", + "\n", + " return {\n", + " \"Pattern\": pattern_id,\n", + " \"Method\": method,\n", + " \"Depth (2Q)\": qc.depth(lambda x: x.operation.num_qubits == 2),\n", + " \"Gates\": qc.size(),\n", + " \"Time (s)\": elapsed,\n", + " }\n", + "\n", + "\n", + "# Ignore logs like \"INFO:qiskit_ibm_transpiler.wrappers.ai_local_synthesis:Running Linear Functions\n", + "# AI synthesis on local mode\"\n", + "\n", + "logging.getLogger(\n", + " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", + ").setLevel(logging.WARNING)" + ] + }, + { + "cell_type": "markdown", + "id": "ba7568f8-50c9-47b4-acc0-33ea34f5fca0", + "metadata": {}, + "source": [ + "# Part I. Qiskit patterns\n", + "\n", + "Let's now see how to use the AI transpiler service with a simple quantum circuit, using Qiskit patterns. The key is creating a `PassManager` with `generate_ai_pass_manager()` instead of the standard `generate_preset_pass_manager()`." + ] + }, + { + "cell_type": "markdown", + "id": "5ba1bb22-272f-4f8f-ae78-7c3d1cdaacc6", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "In this section, we will test the AI transpiler on the `efficient_su2` circuit, a widely used hardware-efficient ansatz. This circuit is particularly relevant for variational quantum algorithms (for example, VQE) and quantum machine-learning tasks, making it an ideal test case for assessing transpilation performance.\n", + "\n", + "The `efficient_su2` circuit consists of alternating layers of single-qubit rotations and entangling gates like CNOTs. These layers enable flexible exploration of the quantum state space while keeping the gate depth manageable. By optimizing this circuit, we aim to reduce gate count, improve fidelity, and minimize noise. This makes it a strong candidate for testing the AI transpiler’s efficiency." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c6e9c2c0-e02c-4276-bae8-d5692e60b6b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# For our transpilation, we will use a large circuit of 101 qubits\n", + "qc = efficient_su2(90, entanglement=\"circular\", reps=1).decompose()\n", + "\n", + "# Draw a smaller version of the circuit to get a visual representation\n", + "qc_small = efficient_su2(5, entanglement=\"circular\", reps=1).decompose()\n", + "qc_small.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "6c7c76f7-c376-47e9-bc9c-dbe32b2c89b7", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "### Choose a backend\n", + "\n", + "For this example, we select the least busy operational IBM Quantum backend that is not a simulator and has at least 100 qubits:\n", + "\n", + "**Note:** Since the least-busy backend can change over time, different devices might be selected for different runs. Device-specific properties, such as coupling maps, can lead to differences in the transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6b6e55e-9b70-4c94-8bbf-5ea47d0510ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using backend: ibm_torino\n" + ] + } + ], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=100\n", + ")\n", + "cm = backend.coupling_map\n", + "print(f\"Using backend: {backend.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b02350f-998e-40cf-a79e-2e6182b5a875", + "metadata": {}, + "source": [ + "### Create AI and traditional pass managers\n", + "To evaluate the effectiveness of the AI transpiler, we will perform two transpilation runs. First, we will transpile the circuit using the AI transpiler. Then, we will run a comparison by transpiling the same circuit without the AI transpiler, using traditional methods. Both transpilation processes will use the same coupling map from the chosen backend and the optimization level set to 3 for a fair comparison.\n", + "\n", + "Both of these methods reflect the standard approach to create `PassManager` instances to transpile circuits in Qiskit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a1aa25dd-41a9-4416-a959-44f28af613c8", + "metadata": {}, + "outputs": [], + "source": [ + "pm_ai = generate_ai_pass_manager(\n", + " optimization_level=3,\n", + " ai_optimization_level=3,\n", + " coupling_map=cm,\n", + " include_ai_synthesis=True, # used for part 3 when comparing synthesis methods\n", + ")\n", + "\n", + "pm_no_ai = generate_preset_pass_manager(\n", + " optimization_level=3,\n", + " coupling_map=cm,\n", + " seed_transpiler=seed, # note that the AI pass manager does not currently support seeding\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a06d6144-3445-4446-a3e1-18ca78a1173c", + "metadata": {}, + "source": [ + "Transpile the circuits and record the times." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fb5167bd-35f0-432f-af6d-023c70783d20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Standard transpilation: Depth (2q) 95, Gate count 458, Time 0.04650712013244629\n", + "AI transpilation : Depth (2q) 90, Gate count 456, Time 0.9342479705810547\n" + ] + } + ], + "source": [ + "# Transpile using standard (non-AI) pass manager\n", + "_, metrics_no_ai = transpile_with_metrics(pm_no_ai, qc)\n", + "print(\n", + " f\"Standard transpilation: Depth (2q) {metrics_no_ai['depth_2q']}, \"\n", + " f\"Gate count {metrics_no_ai['gate_count']}, Time {metrics_no_ai['time_s']}\"\n", + ")\n", + "\n", + "# Transpile using AI pass manager\n", + "_, metrics_ai = transpile_with_metrics(pm_ai, qc)\n", + "print(\n", + " f\"AI transpilation : Depth (2q) {metrics_ai['depth_2q']}, \"\n", + " f\"Gate count {metrics_ai['gate_count']}, Time {metrics_ai['time_s']}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d934ebd2-e594-4076-8b21-822087df01ea", + "metadata": {}, + "source": [ + "In this test, we compare the performance of the AI transpiler and the standard transpilation method on the efficient_su2 circuit. The AI transpiler achieves a noticeably shallower circuit depth while maintaining a similar gate count.\n", + "\n", + "- **Circuit depth:** The AI transpiler produces a circuit with lower two-qubit depth. This is expected, as the AI passes are trained to optimize depth by learning qubit interaction patterns and exploiting hardware connectivity more effectively than rule-based heuristics.\n", + "\n", + "- **Gate count:** The total gate count remains similar between the two methods. This aligns with expectations since the standard SABRE-based transpilation explicitly minimizes swap count, which dominates gate overhead. The AI transpiler instead prioritizes overall depth and may occasionally trade off a few additional gates for a shorter execution path.\n", + "\n", + "- **Transpilation time:** The AI transpiler takes longer to run than the standard method. This is due to the added computational cost of invoking learned models during routing and synthesis. In contrast, the SABRE-based transpiler is now significantly faster after being rewritten and optimized in Rust, providing highly efficient heuristic routing at scale.\n", + "\n", + "It is important to note that these results are based on just one circuit. To obtain a comprehensive understanding of how the AI transpiler compares to traditional methods, it is necessary to test a variety of circuits. The performance of QTS can vary greatly depending on the type of circuit being optimized. For a broader comparison, refer to the benchmarks above or visit the [blog.](https://www.ibm.com/quantum/blog/qiskit-performance)" + ] + }, + { + "cell_type": "markdown", + "id": "c8a55587-abf6-4096-85fd-2702a077ae75", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." + ] + }, + { + "cell_type": "markdown", + "id": "8d0cfca9-be4e-40ab-ab98-d7899bb8b3fa", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "c82277b2-22e9-44fe-886e-e8ceb2178278", + "metadata": {}, + "source": [ + "# Part II. Analyze and benchmark the transpiled circuits\n", + "\n", + "In this section, we will demonstrate how to analyze the transpiled circuit and benchmark it against the original version in more detail. We will focus on metrics such as circuit depth, gate count, and transpilation time to assess the effectiveness of the optimization. Additionally, we will discuss how the results may differ across various circuit types, offering insights into the broader performance of the transpiler across different scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ee24725b-64c9-4d6a-aa97-5a3502b0982a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed transpilation for Random\n", + "Completed transpilation for Clifford\n", + "Completed transpilation for QFT\n", + "Completed transpilation for BV\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CircuitDepth 2Q (No AI)Gate Count (No AI)Time (No AI)Depth 2Q (AI)Gate Count (AI)Time (AI)
0Random372210.039347241810.773718
1Clifford362320.036633432671.097431
2QFT1659240.0774581309133.660771
3BV651550.024993701550.345522
\n", + "
" + ], + "text/plain": [ + " Circuit Depth 2Q (No AI) Gate Count (No AI) Time (No AI) \\\n", + "0 Random 37 221 0.039347 \n", + "1 Clifford 36 232 0.036633 \n", + "2 QFT 165 924 0.077458 \n", + "3 BV 65 155 0.024993 \n", + "\n", + " Depth 2Q (AI) Gate Count (AI) Time (AI) \n", + "0 24 181 0.773718 \n", + "1 43 267 1.097431 \n", + "2 130 913 3.660771 \n", + "3 70 155 0.345522 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Circuits to benchmark\n", + "seed = 42\n", + "circuits = [\n", + " {\n", + " \"name\": \"Random\",\n", + " \"qc\": random_circuit(num_qubits=30, depth=10, seed=seed),\n", + " },\n", + " {\n", + " \"name\": \"Clifford\",\n", + " \"qc\": random_clifford_circuit(\n", + " num_qubits=40, num_gates=200, seed=seed\n", + " ),\n", + " },\n", + " {\n", + " \"name\": \"QFT\",\n", + " \"qc\": synth_qft_full(num_qubits=20, do_swaps=False).decompose(),\n", + " },\n", + " {\n", + " \"name\": \"BV\",\n", + " \"qc\": create_bv_circuit(40),\n", + " },\n", + "]\n", + "\n", + "results = []\n", + "\n", + "# Run the transpilation for each circuit and store the results\n", + "for circuit in circuits:\n", + " qc_no_ai, metrics_no_ai = transpile_with_metrics(pm_no_ai, circuit[\"qc\"])\n", + " qc_ai, metrics_ai = transpile_with_metrics(pm_ai, circuit[\"qc\"])\n", + "\n", + " print(\"Completed transpilation for\", circuit[\"name\"])\n", + "\n", + " results.append(\n", + " {\n", + " \"Circuit\": circuit[\"name\"],\n", + " \"Depth 2Q (No AI)\": metrics_no_ai[\"depth_2q\"],\n", + " \"Gate Count (No AI)\": metrics_no_ai[\"gate_count\"],\n", + " \"Time (No AI)\": metrics_no_ai[\"time_s\"],\n", + " \"Depth 2Q (AI)\": metrics_ai[\"depth_2q\"],\n", + " \"Gate Count (AI)\": metrics_ai[\"gate_count\"],\n", + " \"Time (AI)\": metrics_ai[\"time_s\"],\n", + " }\n", + " )\n", + "\n", + "df = pd.DataFrame(results)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "061d85cf-3841-4ed3-bd0d-cd950564efb7", + "metadata": {}, + "source": [ + "Average percentage reduction for each metric. Positive are improvements, negative are degradations." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "70cf9c05-62a3-4049-9712-319902107ba6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average reduction in depth: 11.88%\n", + "Average reduction in gate count: 1.04%\n", + "Average reduction in transpilation time: -3193.95%\n" + ] + } + ], + "source": [ + "# Average reduction from non-AI to AI transpilation as a percentage\n", + "avg_reduction_depth = (\n", + " (df[\"Depth 2Q (No AI)\"] - df[\"Depth 2Q (AI)\"]).mean()\n", + " / df[\"Depth 2Q (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "avg_reduction_gates = (\n", + " (df[\"Gate Count (No AI)\"] - df[\"Gate Count (AI)\"]).mean()\n", + " / df[\"Gate Count (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "avg_reduction_time = (\n", + " (df[\"Time (No AI)\"] - df[\"Time (AI)\"]).mean()\n", + " / df[\"Time (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "\n", + "print(f\"Average reduction in depth: {avg_reduction_depth:.2f}%\")\n", + "print(f\"Average reduction in gate count: {avg_reduction_gates:.2f}%\")\n", + "print(f\"Average reduction in transpilation time: {avg_reduction_time:.2f}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "79b8d5d9-0f9d-42ca-9583-8bec17430014", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 3, figsize=(21, 6))\n", + "df.plot(\n", + " x=\"Circuit\",\n", + " y=[\"Depth 2Q (No AI)\", \"Depth 2Q (AI)\"],\n", + " kind=\"bar\",\n", + " ax=axs[0],\n", + ")\n", + "axs[0].set_title(\"Circuit Depth Comparison\")\n", + "axs[0].set_ylabel(\"Depth\")\n", + "axs[0].set_xlabel(\"Circuit\")\n", + "axs[0].tick_params(axis=\"x\", rotation=45)\n", + "df.plot(\n", + " x=\"Circuit\",\n", + " y=[\"Gate Count (No AI)\", \"Gate Count (AI)\"],\n", + " kind=\"bar\",\n", + " ax=axs[1],\n", + ")\n", + "axs[1].set_title(\"Gate Count Comparison\")\n", + "axs[1].set_ylabel(\"Gate Count\")\n", + "axs[1].set_xlabel(\"Circuit\")\n", + "axs[1].tick_params(axis=\"x\", rotation=45)\n", + "df.plot(x=\"Circuit\", y=[\"Time (No AI)\", \"Time (AI)\"], kind=\"bar\", ax=axs[2])\n", + "axs[2].set_title(\"Time Comparison\")\n", + "axs[2].set_ylabel(\"Time (seconds)\")\n", + "axs[2].set_xlabel(\"Circuit\")\n", + "axs[2].tick_params(axis=\"x\", rotation=45)\n", + "fig.suptitle(\n", + " \"Benchmarking AI transpilation vs Non-AI transpilation for various circuits\"\n", + ")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "345022d3-e302-47e6-9453-9261136923a7", + "metadata": {}, + "source": [ + "The AI transpiler's performance varies significantly based on the type of circuit being optimized. In some cases, it achieves notable reductions in circuit depth and gate count compared to the standard transpiler. However, these improvements often come with a substantial increase in runtime.\n", + "\n", + "For certain types of circuits, the AI transpiler may yield slightly better results in terms of circuit depth but may also lead to an increase in gate count and a significant runtime penalty. These observations suggest that the AI transpiler's benefits are not uniform across all circuit types. Instead, its effectiveness depends on the specific characteristics of the circuit, making it more suitable for some use cases than others." + ] + }, + { + "cell_type": "markdown", + "id": "9e496e7a-64a8-46fd-b240-c494e7825bd2", + "metadata": {}, + "source": [ + "## When should users choose AI-powered transpilation?\n", + "\n", + "The AI-powered transpiler in Qiskit excels in scenarios where traditional transpilation methods struggle, particularly with large-scale and complex quantum circuits. For circuits involving hundreds of qubits or those targeting hardware with intricate coupling maps, the AI transpiler offers superior optimization in terms of circuit depth, gate count, and runtime efficiency. In benchmarking tests, it has consistently outperformed traditional methods, delivering significantly shallower circuits and reducing gate counts, which are critical for enhancing performance and mitigating noise on real quantum hardware.\n", + "\n", + "Users should consider AI-powered transpilation when working with:\n", + "- Large circuits where traditional methods fail to efficiently handle the scale.\n", + "- Complex hardware topologies where device connectivity and routing challenges arise.\n", + "- Performance-sensitive applications where reducing circuit depth and improving fidelity are paramount." + ] + }, + { + "cell_type": "markdown", + "id": "c345cb54-a838-427f-898f-51fb607da493", + "metadata": {}, + "source": [ + "# Part III. Explore AI-powered permutation network synthesis\n", + "\n", + "Permutation networks are foundational in quantum computing, particularly for systems constrained by restricted topologies. These networks facilitate long-range interactions by dynamically swapping qubits to mimic all-to-all connectivity on hardware with limited connectivity. Such transformations are essential for implementing complex quantum algorithms on near-term devices, where interactions often span beyond nearest neighbors.\n", + "\n", + "In this section, we highlight the synthesis of permutation networks as a compelling use case for the AI-powered transpiler in Qiskit. Specifically, the `AIPermutationSynthesis` pass leverages AI-driven optimization to generate efficient circuits for qubit permutation tasks. By contrast, generic synthesis approaches often struggle to balance gate count and circuit depth, especially in scenarios with dense qubit interactions or when attempting to achieve full connectivity.\n", + "\n", + "We will walk through a Qiskit patterns example showcasing the synthesis of a permutation network to achieve all-to-all connectivity for a set of qubits. We will compare the performance of `AIPermutationSynthesis` against the standard synthesis methods in Qiskit. This example will demonstrate how the AI transpiler optimizes for lower circuit depth and gate count, highlighting its advantages in practical quantum workflows. To activate the AI synthesis pass, we will use the `generate_ai_pass_manager()` function with the `include_ai_synthesis` parameter set to `True`." + ] + }, + { + "cell_type": "markdown", + "id": "76de0959-1eca-43d9-b8fe-f9aea9a122d8", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "To represent a classical permutation problem on a quantum computer, we start by defining the structure of the quantum circuits. For this example:\n", + "\n", + "1. Quantum circuit initialization:\n", + " We allocate 27 qubits to match the backend we will use, which has 27 qubits.\n", + "\n", + "2. Apply permutations:\n", + " We generate ten random permutation patterns (`pattern_1` through `pattern_10`) using a fixed seed for reproducibility. Each permutation pattern is applied to a separate quantum circuit (`qc_1` through `qc_10`).\n", + "\n", + "3. Circuit decomposition:\n", + " Each permutation operation is decomposed into native gate sets compatible with the target quantum hardware. We analyze the depth and the number of two-qubit gates (nonlocal gates) for each decomposed circuit.\n", + "\n", + "The results provide insight into the complexity of representing classical permutation problems on a quantum device, demonstrating the resource requirements for different permutation patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "76a3e847-0808-4413-bd0c-c760cd2df3f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Parameters\n", + "width = 27\n", + "num_circuits = 10\n", + "\n", + "# Set random seed\n", + "np.random.seed(seed)\n", + "\n", + "\n", + "# Generate random patterns and circuits\n", + "patterns = [\n", + " np.random.permutation(width).tolist() for _ in range(num_circuits)\n", + "]\n", + "circuits = {\n", + " f\"qc_{i}\": generate_permutation_circuit(width, pattern)\n", + " for i, pattern in enumerate(patterns, start=1)\n", + "}\n", + "\n", + "# Display one of the circuits\n", + "circuits[\"qc_1\"].decompose(reps=3).draw(output=\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "a8b79798-fa80-44d8-8a52-2d2a50e0c280", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "In this step, we proceed with optimization using the AI synthesis passes.\n", + "\n", + "For the AI synthesis passes, the `PassManager` requires only the coupling map of the backend. However, it is important to note that not all coupling maps are compatible; only those that the `AIPermutationSynthesis` pass has been trained on will work. Currently, the `AIPermutationSynthesis` pass supports blocks of sizes 65, 33, and 27 qubits. For this example we use a 27-qubit QPU.\n", + "\n", + "For comparison, we will evaluate the performance of AI synthesis against generic permutation synthesis methods in Qiskit, including:\n", + "\n", + "- `synth_permutation_depth_lnn_kms`: This method synthesizes a permutation circuit for a linear nearest-neighbor (LNN) architecture using the Kutin, Moulton, and Smithline (KMS) algorithm. It guarantees a circuit with a depth of at most $ n $ and a size of at most $ n(n-1)/2 $, where both depth and size are measured in terms of SWAP gates.\n", + "\n", + "- `synth_permutation_basic`: This is a straightforward implementation that synthesizes permutation circuits without imposing constraints on connectivity or optimization for specific architectures. It serves as a baseline for comparing performance with more advanced methods.\n", + "\n", + "Each of these methods represents a distinct approach to synthesizing permutation networks, providing a comprehensive benchmark against the AI-powered methods.\n", + "\n", + "For more details about synthesis methods in Qiskit, refer to the [Qiskit API documentation](/docs/api/qiskit/synthesis)." + ] + }, + { + "cell_type": "markdown", + "id": "b1733a10-c285-444e-af47-4a32329c5f7a", + "metadata": {}, + "source": [ + "Define the coupling map representing the 27-qubit QPU." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "84dff2c2-a496-4828-bb8e-08d373816a36", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coupling_map = [\n", + " [1, 0],\n", + " [2, 1],\n", + " [3, 2],\n", + " [3, 5],\n", + " [4, 1],\n", + " [6, 7],\n", + " [7, 4],\n", + " [7, 10],\n", + " [8, 5],\n", + " [8, 9],\n", + " [8, 11],\n", + " [11, 14],\n", + " [12, 10],\n", + " [12, 13],\n", + " [12, 15],\n", + " [13, 14],\n", + " [16, 14],\n", + " [17, 18],\n", + " [18, 15],\n", + " [18, 21],\n", + " [19, 16],\n", + " [19, 22],\n", + " [20, 19],\n", + " [21, 23],\n", + " [23, 24],\n", + " [25, 22],\n", + " [25, 24],\n", + " [26, 25],\n", + "]\n", + "CouplingMap(coupling_map).draw()" + ] + }, + { + "cell_type": "markdown", + "id": "47bdb1f5-1fc6-46c4-8fc9-98d16a4d2529", + "metadata": {}, + "source": [ + "Transpile each of the permutation circuits using the AI synthesis passes and generic synthesis methods." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "128cc285-094a-4b07-a37d-8424a4003b2c", + "metadata": {}, + "outputs": [], + "source": [ + "results = []\n", + "pm_no_ai_synth = generate_preset_pass_manager(\n", + " coupling_map=cm,\n", + " optimization_level=1, # set to 1 since we are using the synthesis methods\n", + ")\n", + "\n", + "# Transpile and analyze all circuits\n", + "for i, (qc_name, qc) in enumerate(circuits.items(), start=1):\n", + " pattern = patterns[i - 1] # Get the corresponding pattern\n", + "\n", + " qc_depth_lnn_kms = synth_permutation_depth_lnn_kms(pattern)\n", + " qc_basic = synth_permutation_basic(pattern)\n", + "\n", + " # AI synthesis\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc.decompose(reps=3),\n", + " pm_ai,\n", + " qc_name,\n", + " \"AI\",\n", + " )\n", + " )\n", + "\n", + " # Depth-LNN-KMS Method\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc_depth_lnn_kms.decompose(reps=3),\n", + " pm_no_ai_synth,\n", + " qc_name,\n", + " \"Depth-LNN-KMS\",\n", + " )\n", + " )\n", + "\n", + " # Basic Method\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc_basic.decompose(reps=3),\n", + " pm_no_ai_synth,\n", + " qc_name,\n", + " \"Basic\",\n", + " )\n", + " )\n", + "\n", + "\n", + "results_df = pd.DataFrame(results)" + ] + }, + { + "cell_type": "markdown", + "id": "42f80e32-60fd-46a8-a6b5-4bcadb15810a", + "metadata": {}, + "source": [ + "Record the metrics (depth, gate count, time) for each circuit after transpilation." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "72ee8474-eea6-421a-9d7d-070587eaff71", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== Average Metrics ===\n", + " Depth (2Q) Gates Time (s)\n", + "Method \n", + "AI 23.9 82.8 0.248\n", + "Basic 29.8 91.0 0.012\n", + "Depth-LNN-KMS 70.8 531.6 0.017\n", + "\n", + "Best Non-AI Method (based on least average depth): Basic\n", + "\n", + "=== Comparison of AI vs Best Non-AI Method ===\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricAIBasicImprovement (AI vs Best Non-AI)
0Depth (2Q)23.90029.800-5.900
1Gates82.80091.000-8.200
2Time (s)0.2480.0120.236
\n", + "
" + ], + "text/plain": [ + " Metric AI Basic Improvement (AI vs Best Non-AI)\n", + "0 Depth (2Q) 23.900 29.800 -5.900\n", + "1 Gates 82.800 91.000 -8.200\n", + "2 Time (s) 0.248 0.012 0.236" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Calculate averages for each metric\n", + "average_metrics = results_df.groupby(\"Method\")[\n", + " [\"Depth (2Q)\", \"Gates\", \"Time (s)\"]\n", + "].mean()\n", + "average_metrics = average_metrics.round(3) # Round to two decimal places\n", + "print(\"\\n=== Average Metrics ===\")\n", + "print(average_metrics)\n", + "\n", + "# Identify the best non-AI method based on least average depth\n", + "non_ai_methods = [\n", + " method for method in results_df[\"Method\"].unique() if method != \"AI\"\n", + "]\n", + "best_non_ai_method = average_metrics.loc[non_ai_methods][\n", + " \"Depth (2Q)\"\n", + "].idxmin()\n", + "print(\n", + " f\"\\nBest Non-AI Method (based on least average depth): {best_non_ai_method}\"\n", + ")\n", + "\n", + "# Compare AI to the best non-AI method\n", + "ai_metrics = average_metrics.loc[\"AI\"]\n", + "best_non_ai_metrics = average_metrics.loc[best_non_ai_method]\n", + "\n", + "comparison = {\n", + " \"Metric\": [\"Depth (2Q)\", \"Gates\", \"Time (s)\"],\n", + " \"AI\": [\n", + " ai_metrics[\"Depth (2Q)\"],\n", + " ai_metrics[\"Gates\"],\n", + " ai_metrics[\"Time (s)\"],\n", + " ],\n", + " best_non_ai_method: [\n", + " best_non_ai_metrics[\"Depth (2Q)\"],\n", + " best_non_ai_metrics[\"Gates\"],\n", + " best_non_ai_metrics[\"Time (s)\"],\n", + " ],\n", + " \"Improvement (AI vs Best Non-AI)\": [\n", + " ai_metrics[\"Depth (2Q)\"] - best_non_ai_metrics[\"Depth (2Q)\"],\n", + " ai_metrics[\"Gates\"] - best_non_ai_metrics[\"Gates\"],\n", + " ai_metrics[\"Time (s)\"] - best_non_ai_metrics[\"Time (s)\"],\n", + " ],\n", + "}\n", + "\n", + "comparison_df = pd.DataFrame(comparison)\n", + "print(\"\\n=== Comparison of AI vs Best Non-AI Method ===\")\n", + "comparison_df" + ] + }, + { + "cell_type": "markdown", + "id": "e1ba3767-5ce1-4663-803b-73ccfc22f03b", + "metadata": {}, + "source": [ + "The results demonstrate that the AI transpiler outperforms all other Qiskit synthesis methods for this set of random permutation circuits. Key findings include:\n", + "\n", + "1. Depth: The AI transpiler achieves the lowest average depth, indicating superior optimization of circuit layouts.\n", + "2. Gate count: It significantly reduces the number of gates compared to other methods, improving execution fidelity and efficiency.\n", + "3. Transpilation time: All methods run very quickly at this scale, making them practical for use. However, the AI transpiler does has a notable runtime increase compared to traditional methods due to the complexity of the AI models used.\n", + "\n", + "These results establish the AI transpiler as the most effective approach for this benchmark, particularly for depth and gate count optimization." + ] + }, + { + "cell_type": "markdown", + "id": "dbaab943-5fd7-4720-98bf-8602b2ab4473", + "metadata": {}, + "source": [ + "Plot the results to compare the performance of the AI synthesis passes against the generic synthesis methods." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a326f268-0115-442c-8563-968676b66670", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "methods = results_df[\"Method\"].unique()\n", + "\n", + "fig, axs = plt.subplots(1, 3, figsize=(18, 5))\n", + "\n", + "# Pivot the DataFrame and reorder columns to ensure AI is first\n", + "pivot_depth = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Depth (2Q)\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "pivot_gates = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Gates\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "pivot_time = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Time (s)\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "\n", + "pivot_depth.plot(kind=\"bar\", ax=axs[0], legend=False)\n", + "axs[0].set_title(\"Circuit Depth Comparison\")\n", + "axs[0].set_ylabel(\"Depth\")\n", + "axs[0].set_xlabel(\"Pattern\")\n", + "axs[0].tick_params(axis=\"x\", rotation=45)\n", + "pivot_gates.plot(kind=\"bar\", ax=axs[1], legend=False)\n", + "axs[1].set_title(\"2Q Gate Count Comparison\")\n", + "axs[1].set_ylabel(\"Number of 2Q Gates\")\n", + "axs[1].set_xlabel(\"Pattern\")\n", + "axs[1].tick_params(axis=\"x\", rotation=45)\n", + "pivot_time.plot(\n", + " kind=\"bar\", ax=axs[2], legend=True, title=\"Legend\"\n", + ") # Show legend on the last plot\n", + "axs[2].set_title(\"Time Comparison\")\n", + "axs[2].set_ylabel(\"Time (seconds)\")\n", + "axs[2].set_xlabel(\"Pattern\")\n", + "axs[2].tick_params(axis=\"x\", rotation=45)\n", + "fig.suptitle(\n", + " \"Benchmarking AI Synthesis Methods vs Non-AI Synthesis Methods For Random Permutations Circuits\",\n", + " fontsize=16,\n", + " y=1,\n", + ")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "03a9af42-42a7-4344-b834-0d2b506d4d78", + "metadata": {}, + "source": [ + "This graph highlights the individual results for each circuit (`qc_1` to `qc_10`) across different synthesis methods:\n", + "\n", + "While these results underscore the AI transpiler’s effectiveness for permutation circuits, it is important to note its limitations. The AI synthesis method is currently only available for certain coupling maps, which may restrict its broader applicability. This constraint should be considered when evaluating its usage in different scenarios.\n", + "\n", + "Overall, the AI transpiler demonstrates promising improvements in depth and gate count optimization for these specific circuits while maintaining comparable transpilation times." + ] + }, + { + "cell_type": "markdown", + "id": "41b1405d-fa90-48b6-9ce2-933f05358778", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." + ] + }, + { + "cell_type": "markdown", + "id": "3d942ee4-e4d7-4e87-8c8a-17c662d5379f", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "3b21bb06-7a2b-4181-af59-734c89435d45", + "metadata": {}, + "source": [ + "## Tutorial survey\n", + "\n", + "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", + "\n", + "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_0igXMtMCQfApgDI)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb b/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb index 9ea79755449..f0c6aa58083 100644 --- a/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb +++ b/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb @@ -1,1885 +1,1887 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2c991cef-110a-455a-8741-52d5e20d3196", - "metadata": {}, - "source": [ - "---\n", - "title: Approximate quantum compilation for time evolution circuits\n", - "description: This tutorial demonstrates how to implement AQC-Tensor with Qiskit to enhance quantum circuit performance\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore circo */}\n", - "\n", - "# Approximate quantum compilation for time evolution circuits\n", - "*Usage estimate: Five minutes on an Eagle processor (NOTE: This is an estimate only. Your runtime might vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "93895788-7d38-4cc7-a7e9-ba97c34966af", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "This tutorial demonstrates how to implement **Approximate Quantum Compilation** using tensor networks (AQC-Tensor) with Qiskit to enhance quantum circuit performance. We apply AQC-Tensor within the context of a Trotterized time evolution to reduce circuit depth while maintaining simulation accuracy, following the Qiskit framework for state preparation and optimization. You will learn how to create a low-depth ansatz circuit from an initial Trotter circuit, optimize it with tensor networks, and prepare it for quantum hardware execution.\n", - "\n", - "The primary objective is to simulate time evolution for a model Hamiltonian with a reduced circuit depth. This is achieved using the **AQC-Tensor** Qiskit addon, [qiskit-addon-aqc-tensor](https://github.com/Qiskit/qiskit-addon-aqc-tensor), which leverages tensor networks, specifically matrix product states (MPS), to compress and optimize the initial circuit. Through iterative adjustments, the compressed ansatz circuit maintains fidelity to the original circuit while staying feasible for near-term quantum hardware. See the [documentation](https://qiskit.github.io/qiskit-addon-aqc-tensor/) for more information.\n", - "\n", - "Approximate Quantum Compilation is particularly advantageous in quantum simulations that exceed hardware coherence times, as it allows complex simulations to be performed more efficiently. This tutorial guides you through the AQC-Tensor workflow setup in Qiskit, covering initialization of a Hamiltonian, generation of Trotter circuits, and transpilation of the final optimized circuit for a target device." - ] - }, - { - "cell_type": "markdown", - "id": "b1b8f238-0dab-42c5-b658-779f3ff178a0", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, ensure that you have the following installed:\n", - "\n", - "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "* Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)\n", - "* AQC-Tensor Qiskit addon (`pip install 'qiskit-addon-aqc-tensor[aer,quimb-jax]'`)" - ] - }, - { - "cell_type": "markdown", - "id": "f0723f2d-e0ba-4fe9-a742-1891b7b45459", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "ccdcdca2-4e77-4696-b0ff-45d13dbc6ac3", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import quimb.tensor\n", - "import datetime\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from scipy.optimize import OptimizeResult, minimize\n", - "\n", - "from qiskit.quantum_info import SparsePauliOp, Pauli\n", - "from qiskit.transpiler import CouplingMap\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit import QuantumCircuit\n", - "from qiskit.synthesis import SuzukiTrotter\n", - "\n", - "from qiskit_addon_utils.problem_generators import (\n", - " generate_time_evolution_circuit,\n", - ")\n", - "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", - " generate_ansatz_from_circuit,\n", - ")\n", - "from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity\n", - "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", - "from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit\n", - "from qiskit_addon_aqc_tensor.simulation import compute_overlap\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "from rustworkx.visualization import graphviz_draw" - ] - }, - { - "cell_type": "markdown", - "id": "39bc4470-f7f8-4550-83a7-a22167024000", - "metadata": {}, - "source": [ - "## Part I. Small-scale example\n", - "\n", - "The first part of this tutorial uses a small-scale example with 10 sites to illustrate the process of mapping a quantum simulation problem to an executable quantum circuit. Here, we’ll explore the dynamics of a 10-site XXZ model, allowing us to build and optimize a manageable quantum circuit before scaling to larger systems.\n", - "\n", - "The XXZ model is widely studied in physics for examining spin interactions and magnetic properties. We set up the Hamiltonian to have open boundary conditions with site-dependent interactions between neighboring sites along the chain.\n", - "\n", - "### Model Hamiltonian and observable\n", - "\n", - "The Hamiltonian for our 10-site XXZ model is defined as:\n", - "$$\n", - "\\hat{\\mathcal{H}}_{XXZ} = \\sum_{i=1}^{L-1} J_{i,(i+1)}\\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\\cdot Z_i Z_{(i+1)} \\right) \\, ,\n", - "$$\n", - "\n", - "where $J_{i,(i+1)}$ is a random coefficient corresponding to edge $(i, i+1)$, and $L=10$ is the number of sites.\n", - "\n", - "By simulating the evolution of this system with reduced circuit depth, we can gain insights into using AQC-Tensor to compress and optimize circuits." - ] - }, - { - "cell_type": "markdown", - "id": "89f22428-d4d8-49e7-a1c5-39197dc58330", - "metadata": {}, - "source": [ - "#### Set up the Hamiltonian and observable\n", - "\n", - "Before we map our problem, we need to set up the coupling map, Hamiltonian, and observable for the 10-site XXZ model." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1ea0e102-23d5-4e6e-8ef8-e82843452b19", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hamiltonian: SparsePauliOp(['IIIIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII'],\n", - " coeffs=[1. +0.j, 0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j,\n", - " 0.60759468+0.j, 0.60759468+0.j, 1.21518937+0.j, 0.55138169+0.j,\n", - " 0.55138169+0.j, 1.10276338+0.j, 0.52244159+0.j, 0.52244159+0.j,\n", - " 1.04488318+0.j, 0.4618274 +0.j, 0.4618274 +0.j, 0.9236548 +0.j,\n", - " 0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j, 0.46879361+0.j,\n", - " 0.46879361+0.j, 0.93758721+0.j, 0.6958865 +0.j, 0.6958865 +0.j,\n", - " 1.391773 +0.j, 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j])\n", - "Observable: SparsePauliOp(['IIIIZZIIII'],\n", - " coeffs=[1.+0.j])\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# L is the number of sites, also the length of the 1D spin chain\n", - "L = 10\n", - "\n", - "# Generate the coupling map\n", - "edge_list = [(i - 1, i) for i in range(1, L)]\n", - "# Generate an edge-coloring so we can make hw-efficient circuits\n", - "even_edges = edge_list[::2]\n", - "odd_edges = edge_list[1::2]\n", - "coupling_map = CouplingMap(edge_list)\n", - "\n", - "# Generate random coefficients for our XXZ Hamiltonian\n", - "np.random.seed(0)\n", - "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", - "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n", - "for i, edge in enumerate(even_edges + odd_edges):\n", - " hamiltonian += SparsePauliOp.from_sparse_list(\n", - " [\n", - " (\"XX\", (edge), Js[i] / 2),\n", - " (\"YY\", (edge), Js[i] / 2),\n", - " (\"ZZ\", (edge), Js[i]),\n", - " ],\n", - " num_qubits=L,\n", - " )\n", - "\n", - "# Generate a ZZ observable between the two middle qubits\n", - "observable = SparsePauliOp.from_sparse_list(\n", - " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", - ")\n", - "\n", - "print(\"Hamiltonian:\", hamiltonian)\n", - "print(\"Observable:\", observable)\n", - "graphviz_draw(coupling_map.graph, method=\"circo\")" - ] - }, - { - "cell_type": "markdown", - "id": "e6cd54e3-6a9c-4cdc-8493-d612e930dcee", - "metadata": {}, - "source": [ - "With the Hamiltonian defined, we can proceed to construct the initial state." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "71252a74-e7bf-4003-a1fd-8f7659195f9a", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate an initial state\n", - "initial_state = QuantumCircuit(L)\n", - "for i in range(L):\n", - " if i % 2:\n", - " initial_state.x(i)" - ] - }, - { - "cell_type": "markdown", - "id": "5fb34a14-d197-4ac4-a224-5be2cec06a2e", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Now that we have constructed the Hamiltonian, defining the spin-spin interactions and external magnetic fields that characterize the system, we follow three main steps in the AQC-Tensor workflow:\n", - "\n", - "1. **Generate the optimized AQC circuit**: Using Trotterization, we approximate the initial evolution, which is then compressed to reduce circuit depth.\n", - "2. **Create the remaining time evolution circuit**: Capture the evolution for the remaining time beyond the initial segment.\n", - "3. **Combine the circuits**: Merge the optimized AQC circuit with the remaining evolution circuit into a complete time-evolution circuit ready for execution.\n", - "\n", - "This approach creates a low-depth ansatz for the target evolution, supporting efficient simulation within near-term quantum hardware constraints." - ] - }, - { - "cell_type": "markdown", - "id": "7c5f2a2e-179c-4e62-a32f-5bc202f89701", - "metadata": {}, - "source": [ - "#### Determine the portion of time evolution to simulate classically\n", - "\n", - "Our goal is to simulate the time evolution of the model Hamiltonian defined earlier using Trotter evolution. To make this process efficient for quantum hardware, we split the evolution into two segments:\n", - "\n", - "- **Initial Segment**: This initial portion of the evolution, from $ t_i = 0.0 $ to $ t_f = 0.2 $, is simulable with MPS and can be efficiently “compiled” using AQC-Tensor. By using the [AQC-Tensor Qiskit addon](https://github.com/Qiskit/qiskit-addon-aqc-tensor), we generate a compressed circuit for this segment, referred to as the `aqc_target_circuit`. Because this segment will be simulated on a tensor-network simulator, we can afford to use a higher number of Trotter layers without impacting hardware resources significantly. We set `aqc_target_num_trotter_steps = 32` for this segment.\n", - "\n", - "- **Subsequent Segment**: This remaining portion of the evolution, from $ t = 0.2 $ to $ t = 0.4 $, will be executed on quantum hardware, referred to as the `subsequent_circuit`. Given hardware limitations, we aim to use as few Trotter layers as possible to maintain a manageable circuit depth. For this segment, we use `subsequent_num_trotter_steps = 3`.\n", - "\n", - "\n", - "#### Choose the split time\n", - "We choose $t = 0.2$ as the split time to balance classical simulability with hardware feasibility. Early in the evolution, entanglement in the XXZ model remains low enough for classical methods like MPS to approximate accurately.\n", - "\n", - "When choosing a split time, a good guideline is to select a point where entanglement is still manageable classically but captures enough of the evolution to simplify the hardware-executed portion. Trial and error may be needed to find the best balance for different Hamiltonians." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "199d4e7e-02b2-4da8-b567-8195fb9de536", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate the AQC target circuit (initial segment)\n", - "aqc_evolution_time = 0.2\n", - "aqc_target_num_trotter_steps = 32\n", - "\n", - "aqc_target_circuit = initial_state.copy()\n", - "aqc_target_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "83039f82-97cb-4613-86c9-a8faf0839a02", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Generate the subsequent circuit\n", - "subsequent_num_trotter_steps = 3\n", - "subsequent_evolution_time = 0.2\n", - "\n", - "subsequent_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),\n", - " time=subsequent_evolution_time,\n", - ")\n", - "subsequent_circuit.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "064a17af-4af8-4ff5-899c-750147d269df", - "metadata": {}, - "source": [ - "To enable a meaningful comparison, we will generate two additional circuits:\n", - "\n", - "- **AQC comparison circuit**: This circuit evolves up to `aqc_evolution_time` but uses the same Trotter step duration as the `subsequent_circuit`. It serves as a comparison to the `aqc_target_circuit`, showing the evolution we would observe without using an increased number of Trotter steps. We will refer to this circuit as the `aqc_comparison_circuit`.\n", - "\n", - "- **Reference circuit**: This circuit is used as a baseline to obtain the exact result. It simulates the full evolution using tensor networks to calculate the exact outcome, providing a reference for evaluating the effectiveness of AQC-Tensor. We will refer to this circuit as the `reference_circuit`." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "33b2ad93-7ae4-4250-bf38-9adc3bc2970d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of Trotter steps for comparison: 3\n" - ] - } - ], - "source": [ - "# Generate the AQC comparison circuit\n", - "aqc_comparison_num_trotter_steps = int(\n", - " subsequent_num_trotter_steps\n", - " / subsequent_evolution_time\n", - " * aqc_evolution_time\n", - ")\n", - "print(\n", - " \"Number of Trotter steps for comparison:\",\n", - " aqc_comparison_num_trotter_steps,\n", - ")\n", - "\n", - "aqc_comparison_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1bfa67a2-ac51-4158-b539-2c33f4c5ecf3", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate the reference circuit\n", - "evolution_time = 0.4\n", - "reps = 200\n", - "\n", - "reference_circuit = initial_state.copy()\n", - "reference_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=reps),\n", - " time=evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "2051f622-5b71-4be5-aab9-e9eb5d315a6f", - "metadata": {}, - "source": [ - "#### Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", - "\n", - "Now that we have constructed our four circuits, let's proceed with the AQC-Tensor workflow. First, we construct a “good” circuit that has the same evolution time as the target circuit, but with fewer Trotter steps (and thus fewer layers).\n", - "\n", - "Then we pass this “good” circuit to AQC-Tensor’s `generate_ansatz_from_circuit` function. This function analyzes the two-qubit connectivity of the circuit and returns two things:\n", - "\n", - "1. A general, parametrized ansatz circuit with the same two-qubit connectivity as the input circuit.\n", - "2. Parameters that, when plugged into the ansatz, yield the input (good) circuit.\n", - "\n", - "Soon we will take these parameters and iteratively adjust them to bring the ansatz circuit as close as possible to the target MPS." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b9e81c51-dc6f-4237-9aca-e1384f1897bc", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aqc_ansatz_num_trotter_steps = 1\n", - "\n", - "aqc_good_circuit = initial_state.copy()\n", - "aqc_good_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")\n", - "\n", - "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(\n", - " aqc_good_circuit\n", - ")\n", - "aqc_ansatz.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "bd9e9bf2-9ee5-445d-aa4a-c7f412c488f8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AQC Comparison circuit: depth 36\n", - "Target circuit: depth 385\n", - "Ansatz circuit: depth 7, with 156 parameters\n" - ] - } - ], - "source": [ - "print(f\"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}\")\n", - "print(f\"Target circuit: depth {aqc_target_circuit.depth()}\")\n", - "print(\n", - " f\"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "3c8663b9-d513-4a2a-9911-03f3500ad486", - "metadata": {}, - "source": [ - "#### Choose settings for tensor network simulation\n", - "\n", - "Here, we use Quimb's matrix-product state circuit simulator, along with jax to provide the gradient." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e5b9471f-fc7a-45c9-ab11-295cfff620bf", - "metadata": {}, - "outputs": [], - "source": [ - "simulator_settings = QuimbSimulator(\n", - " quimb.tensor.CircuitMPS, autodiff_backend=\"jax\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "5030d11d-54e3-4f29-acb2-6f7dd3ff05cb", - "metadata": {}, - "source": [ - "Next, we build a MPS representation of the target state that will be approximated using AQC-Tensor. This representation enables efficient handling of entanglement, providing a compact description of the quantum state for further optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "1f050059-a281-41f1-a277-d7450e8b3ee3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Target MPS maximum bond dimension: 5\n", - "Reference MPS maximum bond dimension: 7\n" - ] - } - ], - "source": [ - "aqc_target_mps = tensornetwork_from_circuit(\n", - " aqc_target_circuit, simulator_settings\n", - ")\n", - "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", - "\n", - "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``\n", - "reference_mps = tensornetwork_from_circuit(\n", - " reference_circuit, simulator_settings\n", - ")\n", - "reference_expval = reference_mps.local_expectation(\n", - " quimb.pauli(\"Z\") & quimb.pauli(\"Z\"), (L // 2 - 1, L // 2)\n", - ").real.item()\n", - "print(\"Reference MPS maximum bond dimension:\", reference_mps.psi.max_bond())" - ] - }, - { - "cell_type": "markdown", - "id": "5ffbdd20-c9bf-4265-8a8d-8135abb2b8f7", - "metadata": {}, - "source": [ - "Note that, by choosing a larger number of Trotter steps for the target state, we have effectively reduced its Trotter error compared to the initial circuit. We can evaluate the fidelity ($ |\\langle \\psi_1 | \\psi_2 \\rangle|^2 $) between the state prepared by the initial circuit and the target state to quantify this difference." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "51d7d623-3e7c-44e7-80f1-94fd555a2c9e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting fidelity: 0.9982464959067222\n" - ] - } - ], - "source": [ - "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", - "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", - "print(\"Starting fidelity:\", starting_fidelity)" - ] - }, - { - "cell_type": "markdown", - "id": "01234b5b-f4db-4e22-993a-c8673621ab7f", - "metadata": {}, - "source": [ - "#### Optimize the parameters of the ansatz using MPS calculations\n", - "In this step, we optimize the ansatz parameters by minimizing a simple cost function, `MaximizeStateFidelity`, using the L-BFGS optimizer from SciPy. We select a stopping criterion for the fidelity that ensures it surpasses the fidelity of the initial circuit without AQC-Tensor. Once this threshold is reached, the compressed circuit will exhibit both lower Trotter error and reduced depth compared to the original circuit. By using additional CPU time, further optimization can continue to increase fidelity." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "ad2265cb-19a6-4402-8b0d-24239c930c90", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-04-14 11:46:52.174235 Intermediate result: Fidelity 0.99795851\n", - "2025-04-14 11:46:52.218249 Intermediate result: Fidelity 0.99822826\n", - "2025-04-14 11:46:52.280924 Intermediate result: Fidelity 0.99829675\n", - "2025-04-14 11:46:52.356214 Intermediate result: Fidelity 0.99832474\n", - "2025-04-14 11:46:52.411609 Intermediate result: Fidelity 0.99836131\n", - "2025-04-14 11:46:52.453747 Intermediate result: Fidelity 0.99839954\n", - "2025-04-14 11:46:52.496184 Intermediate result: Fidelity 0.99846517\n", - "2025-04-14 11:46:52.542046 Intermediate result: Fidelity 0.99865029\n", - "2025-04-14 11:46:52.583679 Intermediate result: Fidelity 0.99872332\n", - "2025-04-14 11:46:52.628732 Intermediate result: Fidelity 0.99892359\n", - "2025-04-14 11:46:52.690386 Intermediate result: Fidelity 0.99900640\n", - "2025-04-14 11:46:52.759398 Intermediate result: Fidelity 0.99907169\n", - "2025-04-14 11:46:52.819496 Intermediate result: Fidelity 0.99911423\n", - "2025-04-14 11:46:52.884505 Intermediate result: Fidelity 0.99918716\n", - "2025-04-14 11:46:52.947919 Intermediate result: Fidelity 0.99921278\n", - "2025-04-14 11:46:53.012808 Intermediate result: Fidelity 0.99924853\n", - "2025-04-14 11:46:53.083626 Intermediate result: Fidelity 0.99928797\n", - "2025-04-14 11:46:53.153235 Intermediate result: Fidelity 0.99933028\n", - "2025-04-14 11:46:53.221371 Intermediate result: Fidelity 0.99935757\n", - "2025-04-14 11:46:53.286211 Intermediate result: Fidelity 0.99938140\n", - "2025-04-14 11:46:53.352391 Intermediate result: Fidelity 0.99940964\n", - "2025-04-14 11:46:53.420472 Intermediate result: Fidelity 0.99944051\n", - "2025-04-14 11:46:53.486279 Intermediate result: Fidelity 0.99946828\n", - "2025-04-14 11:46:53.552338 Intermediate result: Fidelity 0.99948723\n", - "2025-04-14 11:46:53.618688 Intermediate result: Fidelity 0.99951011\n", - "2025-04-14 11:46:53.690878 Intermediate result: Fidelity 0.99954718\n", - "2025-04-14 11:46:53.762725 Intermediate result: Fidelity 0.99956267\n", - "2025-04-14 11:46:53.829784 Intermediate result: Fidelity 0.99958949\n", - "2025-04-14 11:46:53.897477 Intermediate result: Fidelity 0.99960498\n", - "2025-04-14 11:46:53.954633 Intermediate result: Fidelity 0.99961308\n", - "2025-04-14 11:46:54.010125 Intermediate result: Fidelity 0.99962894\n", - "2025-04-14 11:46:54.064717 Intermediate result: Fidelity 0.99964121\n", - "2025-04-14 11:46:54.118892 Intermediate result: Fidelity 0.99964348\n", - "2025-04-14 11:46:54.183236 Intermediate result: Fidelity 0.99964860\n", - "2025-04-14 11:46:54.245521 Intermediate result: Fidelity 0.99965695\n", - "2025-04-14 11:46:54.305792 Intermediate result: Fidelity 0.99966398\n", - "2025-04-14 11:46:54.355819 Intermediate result: Fidelity 0.99967816\n", - "2025-04-14 11:46:54.409580 Intermediate result: Fidelity 0.99968293\n", - "2025-04-14 11:46:54.457979 Intermediate result: Fidelity 0.99968936\n", - "2025-04-14 11:46:54.505891 Intermediate result: Fidelity 0.99969223\n", - "2025-04-14 11:46:54.551084 Intermediate result: Fidelity 0.99970009\n", - "2025-04-14 11:46:54.601817 Intermediate result: Fidelity 0.99970724\n", - "2025-04-14 11:46:54.650097 Intermediate result: Fidelity 0.99970987\n", - "2025-04-14 11:46:54.714727 Intermediate result: Fidelity 0.99971237\n", - "2025-04-14 11:46:54.780052 Intermediate result: Fidelity 0.99971916\n", - "2025-04-14 11:46:54.871994 Intermediate result: Fidelity 0.99971940\n", - "2025-04-14 11:46:54.958244 Intermediate result: Fidelity 0.99972465\n", - "2025-04-14 11:46:55.011057 Intermediate result: Fidelity 0.99972763\n", - "2025-04-14 11:46:55.175339 Intermediate result: Fidelity 0.99972894\n", - "2025-04-14 11:46:56.688912 Intermediate result: Fidelity 0.99972894\n", - "Done after 50 iterations.\n" - ] - } - ], - "source": [ - "# Setting values for the optimization\n", - "aqc_stopping_fidelity = 1\n", - "aqc_max_iterations = 500\n", - "\n", - "stopping_point = 1.0 - aqc_stopping_fidelity\n", - "objective = MaximizeStateFidelity(\n", - " aqc_target_mps, aqc_ansatz, simulator_settings\n", - ")\n", - "\n", - "\n", - "def callback(intermediate_result: OptimizeResult):\n", - " fidelity = 1 - intermediate_result.fun\n", - " print(\n", - " f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\"\n", - " )\n", - " if intermediate_result.fun < stopping_point:\n", - " # Good enough for now\n", - " raise StopIteration\n", - "\n", - "\n", - "result = minimize(\n", - " objective,\n", - " aqc_initial_parameters,\n", - " method=\"L-BFGS-B\",\n", - " jac=True,\n", - " options={\"maxiter\": aqc_max_iterations},\n", - " callback=callback,\n", - ")\n", - "if (\n", - " result.status\n", - " not in (\n", - " 0,\n", - " 1,\n", - " 99,\n", - " )\n", - "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", - " raise RuntimeError(\n", - " f\"Optimization failed: {result.message} (status={result.status})\"\n", - " )\n", - "\n", - "print(f\"Done after {result.nit} iterations.\")\n", - "aqc_final_parameters = result.x" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "c95ccea0-be99-4db9-838d-2327851e6761", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Final parameters: [-7.853983035039254, 1.5707966468427772, 1.5707962768868613, -1.570798010835122, 1.570794480409574, 1.5707972214146968, -1.570796593027083, 1.5707968206822998, -1.5707959018046258, -1.5707991700969144, 1.5707965852600927, 4.712386891737442, -7.853980840717957, 1.5707967508132654, 1.5707943162503217, -1.5707955382023582, 1.5707958007156742, 1.570796096113293, -1.5707928509846847, 1.5707971042943747, -1.570797909276557, -1.5707941020637393, 1.5707980179540793, 4.712389823219363, -1.5707928752386107, 1.5707996426312891, -1.5707975640471001, -1.570794132802984, 1.5707944361599957, 4.712390747060803, 0.1048818190315936, 0.06686710468840577, -0.0668645844756557, -3.1415923537135466, 1.2374931269696063, 6.323169390432535e-07, 3.53229204771738e-08, 2.1091105688681484, 6.283186439944202, 0.12152258846156239, 0.07961752617254866, -0.07961775088604585, -1.6564278051174865e-06, 2.0771163596472384, 3.141592651630471, -6.283185775192653, 1.7691609006726954, 3.1415922910116216, 0.19837572065074083, 0.11114901449078964, -0.11115124544944892, -3.141591983034976, 0.8570788408766729, 4.201601390404146e-07, -3.141593736550978, 0.34652010942396333, 6.283186232785291, 0.13606356527241956, 0.03891676349289617, -0.03891524189533726, -1.5707965732853424, 1.5707968967088564, -0.3086133992238162, 1.5707957152428194, 1.5707968398959653, -0.32062737993080026, 0.11027416939993417, 0.0726167290795046, -0.07262020423334464, -2.3729431959735024e-06, 1.8204437429254703, 9.299060301196612e-07, -3.141592899563451, 2.103269568939461, 3.1415937539734626, 0.11536891854817125, 0.09099022308254198, -0.09098864958606581, -3.1415913307373127, 2.078429034357281, -1.509777998069368e-06, -3.1415922600663255, 1.5189162645358172, -3.1415878461323583, 0.09999070991480716, 0.04352011445148391, -0.04351849541849812, -1.570797642506462, 1.570795238023824, 0.8903442644396505, 1.5707962698006606, 1.5707946765132268, 0.9098791754570567, 0.10448284343424026, 0.07317037684936827, -0.07316718173961152, -3.141592682240966, 2.1665363080039612, -7.450882112394189e-07, -5.771181304929921e-07, 2.615334999517103, -3.1415914971653898, 0.1890887078648001, 0.13578163074571992, -0.13578078143610256, 7.156734195912883e-07, 1.7915385305413096, -5.188866034727312e-07, 1.2827742939197711e-06, 1.2348316581417487, 6.28318357406372, 0.08061187643781703, 0.03820789039271876, -0.03820731868804904, 1.5707964027727628, 1.570798734462218, 4.387336153720882, -1.570795722044763, 1.570798457375325, 4.450361734163248, 0.092360147257953, 0.06047700345049011, -0.06048592856713045, -3.141591214829027, 2.6593289993286047, -2.366937342261038e-07, 8.112162974032695e-08, 1.8907014631413432, 8.355881261853104e-07, 0.23303641819370874, 0.14331998953606456, -0.1433194488304741, -3.141591621822901, 0.7455776479558791, 3.1415914520163586, -3.1415933560496105, 0.7603938554148255, -1.6230983177616282e-06, 0.07186349688535713, 0.03197144517771341, -0.031971177878588546, -4.712389048748508, 1.5707948403165752, 1.2773619319829186, -1.5707990802172127, 1.5707957676951863, 1.289083769394045, 0.13644999397718796, 0.032761460443590046, -0.032762060585195645, -1.5707977610073176, 1.5707964181578042, -3.4826435600366983, -4.712389691708343, 1.570794277502252, 2.799088046133275]\n" - ] - } - ], - "source": [ - "parameters = [float(param) for param in aqc_final_parameters]\n", - "print(\"Final parameters:\", parameters)" - ] - }, - { - "cell_type": "markdown", - "id": "ca32c1ef-f7d5-4489-83b3-17972bf94700", - "metadata": {}, - "source": [ - "At this point, it is only necessary to find the final parameters to the ansatz circuit. We can then merge the optimized AQC circuit with the remaining evolution circuit to create a complete time-evolution circuit for execution on quantum hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "813c9ced-6a2e-4345-bffc-7dae938e2015", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", - "aqc_final_circuit.compose(subsequent_circuit, inplace=True)\n", - "aqc_final_circuit.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "46a93127-5443-40b7-a81a-dfd70df42ba2", - "metadata": {}, - "source": [ - "We also need to merge our `aqc_comparison_circuit` with the remaining evolution circuit. This circuit will be used to compare the performance of the AQC-Tensor-optimized circuit with the original circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "86ba26ff-0bfa-47d0-b5ee-8944a8ddf274", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)\n", - "aqc_comparison_circuit.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "982c5067-9a74-42a0-bd31-aaff72cf79df", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution" - ] - }, - { - "cell_type": "markdown", - "id": "2121c40c-a9a7-4c36-a20a-04ee9f6637db", - "metadata": {}, - "source": [ - "Select the hardware. Here we will use any of the IBM Quantum® devices available that have at least 127 qubits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88715b59-4516-4b75-b041-c978944dd14a", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(min_num_qubits=127)\n", - "print(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "3f477394-5eac-4c12-b36b-e94c800fa889", - "metadata": {}, - "source": [ - "We transpile PUBs (circuit and observables) to match the backend ISA (Instruction Set Architecture). By setting `optimization_level=3`, the transpiler optimizes the circuit to fit a one-dimensional chain of qubits, reducing the noise that impacts circuit fidelity. Once the circuits are transformed into a format compatible with the backend, we apply a corresponding transformation to the observables to ensure they align with the modified qubit layout." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "087fff8d-98b9-4f9a-8004-01a3b0166e12", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],\n", - " coeffs=[1.+0.j])\n", - "Circuit depth: 111\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pass_manager = generate_preset_pass_manager(\n", - " backend=backend, optimization_level=3\n", - ")\n", - "isa_circuit = pass_manager.run(aqc_final_circuit)\n", - "isa_observable = observable.apply_layout(isa_circuit.layout)\n", - "print(\"Observable info:\", isa_observable)\n", - "print(\"Circuit depth:\", isa_circuit.depth())\n", - "isa_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "58e8e840-94a8-4414-81f7-01bbf1b73810", - "metadata": {}, - "source": [ - "Perform transpilation for the comparison circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "7c2e5fe7-21ce-461d-adaa-776f8d882163", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],\n", - " coeffs=[1.+0.j])\n", - "Circuit depth: 158\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)\n", - "isa_comparison_observable = observable.apply_layout(\n", - " isa_comparison_circuit.layout\n", - ")\n", - "print(\"Observable info:\", isa_comparison_observable)\n", - "print(\"Circuit depth:\", isa_comparison_circuit.depth())\n", - "isa_comparison_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "1f832aaa-c2a9-42b7-95b6-bd920cdc17b0", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "In this step, we execute the transpiled circuit on quantum hardware (or a simulated backend). Using the `EstimatorV2` class from `qiskit_ibm_runtime`, we set up an estimator to run the circuit and measure the specified observable. The job result provides the expected outcome for the observable, giving us insights into the circuit’s performance on the target hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "3aabf36b-4587-43b7-be99-9376fc7f47c1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: czyhqdxd8drg008hx0yg\n" - ] - }, - { - "data": { - "text/plain": [ - "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray()), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "estimator = Estimator(backend)\n", - "job = estimator.run([(isa_circuit, isa_observable)])\n", - "print(\"Job ID:\", job.job_id())\n", - "job.result()" - ] - }, - { - "cell_type": "markdown", - "id": "4ec6669b-2b76-41df-9634-3c073998b1d7", - "metadata": {}, - "source": [ - "Perform the execution for the comparison circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "91f16fff-da28-42b4-a32a-fa2f0945f8fd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job Comparison ID: czyhqdxd8drg008hx0yg\n" - ] - }, - { - "data": { - "text/plain": [ - "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray()), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])\n", - "print(\"Job Comparison ID:\", job.job_id())\n", - "job_comparison.result()" - ] - }, - { - "cell_type": "markdown", - "id": "3c6d16d4-3118-49b6-9594-3d7ddb02701c", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "\n", - "In this case, reconstruction is unnecessary. We can directly examine the result by accessing the expectation value from the execution output." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "7247e56f-0ab8-4aa9-834c-ba95965a0b3f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exact: \t-0.5252\n", - "AQC: \t-0.4903, |∆| = 0.0349\n", - "AQC Comparison:\t0.5424, |∆| = 1.0676\n" - ] - } - ], - "source": [ - "# AQC results\n", - "hw_results = job.result()\n", - "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", - "hw_expvals = [\n", - " pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts\n", - "]\n", - "aqc_expval = hw_expvals[0]\n", - "\n", - "# AQC comparison results\n", - "hw_comparison_results = job_comparison.result()\n", - "hw_comparison_results_dicts = [\n", - " pub_result.data.__dict__ for pub_result in hw_comparison_results\n", - "]\n", - "hw_comparison_expvals = [\n", - " pub_result_data[\"evs\"].tolist()\n", - " for pub_result_data in hw_comparison_results_dicts\n", - "]\n", - "aqc_compare_expval = hw_comparison_expvals[0]\n", - "\n", - "print(f\"Exact: \\t{reference_expval:.4f}\")\n", - "print(\n", - " f\"AQC: \\t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}\"\n", - ")\n", - "print(\n", - " f\"AQC Comparison:\\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "1c8b65e6-df72-45b3-8f31-a6aed44cb867", - "metadata": {}, - "source": [ - "Bar plot to compare the results of the AQC, comparison, and exact circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "5f7b36a6-3666-4223-9c5d-d92bca741ad2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.style.use(\"seaborn-v0_8\")\n", - "\n", - "labels = [\"AQC Result\", \"AQC Comparison Result\"]\n", - "values = [abs(aqc_expval), abs(aqc_compare_expval)]\n", - "\n", - "plt.figure(figsize=(10, 6))\n", - "bars = plt.bar(labels, values, color=[\"tab:blue\", \"tab:purple\"])\n", - "plt.axhline(\n", - " y=abs(reference_expval), color=\"red\", linestyle=\"--\", label=\"Exact Result\"\n", - ")\n", - "plt.xlabel(\"Results\")\n", - "plt.ylabel(\"Absolute Expected Value\")\n", - "plt.title(\"AQC Result vs AQC Comparison Result (Absolute Values)\")\n", - "plt.legend()\n", - "for bar in bars:\n", - " y_val = bar.get_height()\n", - " plt.text(\n", - " bar.get_x() + bar.get_width() / 2.0,\n", - " y_val,\n", - " round(y_val, 2),\n", - " va=\"bottom\",\n", - " )\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "ff81fb19-d175-42b0-8ef3-025712a7630d", - "metadata": {}, - "source": [ - "## Part II: scale it up\n", - "\n", - "\n", - "The second part of this tutorial builds on the previous example by scaling up to a larger system with 50 sites, illustrating how to map more complex quantum simulation problems to executable quantum circuits. Here, we explore the dynamics of a 50-site XXZ model, allowing us to build and optimize a substantial quantum circuit that reflects more realistic system sizes.\n", - "\n", - "The Hamiltonian for our 50-site XXZ model is defined as:\n", - "$$\n", - "\\hat{\\mathcal{H}}_{XXZ} = \\sum_{i=1}^{L-1} J_{i,(i+1)}\\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\\cdot Z_i Z_{(i+1)} \\right) \\, ,\n", - "$$\n", - "\n", - "where $J_{i,(i+1)}$ is a random coefficient corresponding to edge $(i, i+1)$, and $L=50$ is the number of sites." - ] - }, - { - "cell_type": "markdown", - "id": "f6d11e17-b4be-44fa-bc09-465e5e66a6af", - "metadata": {}, - "source": [ - "Define the coupling map and edges for the Hamiltonian." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "d0dec1ee-85fe-4c40-9595-b7764e9cfcca", - "metadata": {}, - "outputs": [], - "source": [ - "L = 50 # L = length of our 1D spin chain\n", - "\n", - "# Generate the edge list for this spin-chain\n", - "edge_list = [(i - 1, i) for i in range(1, L)]\n", - "# Generate an edge-coloring so we can make hw-efficient circuits\n", - "even_edges = edge_list[::2]\n", - "odd_edges = edge_list[1::2]\n", - "\n", - "# Instantiate a CouplingMap object\n", - "coupling_map = CouplingMap(edge_list)\n", - "\n", - "# Generate random coefficients for our XXZ Hamiltonian\n", - "np.random.seed(0)\n", - "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", - "\n", - "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n", - "for i, edge in enumerate(even_edges + odd_edges):\n", - " hamiltonian += SparsePauliOp.from_sparse_list(\n", - " [\n", - " (\"XX\", (edge), Js[i] / 2),\n", - " (\"YY\", (edge), Js[i] / 2),\n", - " (\"ZZ\", (edge), Js[i]),\n", - " ],\n", - " num_qubits=L,\n", - " )\n", - "\n", - "observable = SparsePauliOp.from_sparse_list(\n", - " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", - ")\n", - "\n", - "# Generate an initial state\n", - "L = hamiltonian.num_qubits\n", - "initial_state = QuantumCircuit(L)\n", - "for i in range(L):\n", - " if i % 2:\n", - " initial_state.x(i)" - ] - }, - { - "cell_type": "markdown", - "id": "65009d89-4752-40ac-b3ab-8e88425851b2", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "For this larger problem, we start by constructing the Hamiltonian for the 50-site XXZ model, defining spin-spin interactions and external magnetic fields across all sites. After this, we follow three main steps:\n", - "\n", - "1. **Generate the optimized AQC circuit**: Use Trotterization to approximate the initial evolution, then compress this segment to reduce circuit depth.\n", - "2. **Create the remaining time evolution circuit**: Capture the remaining time evolution beyond the initial segment.\n", - "3. **Combine the circuits**: Merge the optimized AQC circuit with the remaining evolution circuit to create a complete time-evolution circuit ready for execution." - ] - }, - { - "cell_type": "markdown", - "id": "41f5186a-e54e-4913-8a52-1c90611991c7", - "metadata": {}, - "source": [ - "Generate the AQC target circuit (the initial segment)." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "86735d08-49b3-47e4-abcd-631a77f50a02", - "metadata": {}, - "outputs": [], - "source": [ - "aqc_evolution_time = 0.2\n", - "aqc_target_num_trotter_steps = 32\n", - "\n", - "aqc_target_circuit = initial_state.copy()\n", - "aqc_target_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "c5e79369-19ae-4b17-a9fc-9502a8a1254c", - "metadata": {}, - "source": [ - "Generate the subsequent circuit (the remaining segment)." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "35c6469c-3be7-4e89-a065-0e4957305b59", - "metadata": {}, - "outputs": [], - "source": [ - "subsequent_num_trotter_steps = 3\n", - "subsequent_evolution_time = 0.2\n", - "\n", - "subsequent_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),\n", - " time=subsequent_evolution_time,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "0e13fbc5-0c00-4f21-9925-8d1230854dc5", - "metadata": {}, - "source": [ - "Generate the AQC comparison circuit (the initial segment, but with the same number of Trotter steps as the subsequent circuit)." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "922fb467-81b0-40d7-b21b-ef781ad043a1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of Trotter steps for comparison: 3\n" - ] - } - ], - "source": [ - "# Generate the AQC comparison circuit\n", - "aqc_comparison_num_trotter_steps = int(\n", - " subsequent_num_trotter_steps\n", - " / subsequent_evolution_time\n", - " * aqc_evolution_time\n", - ")\n", - "print(\n", - " \"Number of Trotter steps for comparison:\",\n", - " aqc_comparison_num_trotter_steps,\n", - ")\n", - "\n", - "aqc_comparison_circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "21228ca8-3fa6-4efe-9e53-8d3f824b7729", - "metadata": {}, - "source": [ - "Generate the reference circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "3525fe02-25cf-4254-b2c4-717f27bc29d8", - "metadata": {}, - "outputs": [], - "source": [ - "evolution_time = 0.4\n", - "reps = 200\n", - "\n", - "reference_circuit = initial_state.copy()\n", - "reference_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=reps),\n", - " time=evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a15b40ac-0259-4229-b76b-502ff7068499", - "metadata": {}, - "source": [ - "Generate an ansatz and initial parameters from a Trotter circuit with fewer steps." - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "ddbc3549-3171-4a29-b046-2a52b5e7065a", - "metadata": {}, - "outputs": [], - "source": [ - "aqc_ansatz_num_trotter_steps = 1\n", - "\n", - "aqc_good_circuit = initial_state.copy()\n", - "aqc_good_circuit.compose(\n", - " generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", - " time=aqc_evolution_time,\n", - " ),\n", - " inplace=True,\n", - ")\n", - "\n", - "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(\n", - " aqc_good_circuit\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "8e596d77-5954-4756-b765-48855cd38659", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AQC Comparison circuit: depth 36\n", - "Target circuit: depth 385\n", - "Ansatz circuit: depth 7, with 816 parameters\n" - ] - } - ], - "source": [ - "print(f\"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}\")\n", - "print(f\"Target circuit: depth {aqc_target_circuit.depth()}\")\n", - "print(\n", - " f\"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4d07d63a-8572-4116-9fd5-c070f62043a7", - "metadata": {}, - "source": [ - "Set settings for tensor network simulation and then construct a matrix product state representation of the target state for optimization. Then, evaluate the fidelity between the initial circuit and the target state to quantify the difference in Trotter error." - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "6030706e-1451-47d9-9e47-bcccf4cb5d9c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Target MPS maximum bond dimension: 5\n", - "Starting fidelity: 0.9926466919924161\n" - ] - } - ], - "source": [ - "simulator_settings = QuimbSimulator(\n", - " quimb.tensor.CircuitMPS, autodiff_backend=\"jax\"\n", - ")\n", - "\n", - "# Build the matrix-product representation of the state to be approximated by AQC\n", - "aqc_target_mps = tensornetwork_from_circuit(\n", - " aqc_target_circuit, simulator_settings\n", - ")\n", - "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", - "\n", - "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``\n", - "reference_mps = tensornetwork_from_circuit(\n", - " reference_circuit, simulator_settings\n", - ")\n", - "reference_expval = reference_mps.local_expectation(\n", - " quimb.pauli(\"Z\") & quimb.pauli(\"Z\"), (L // 2 - 1, L // 2)\n", - ").real.item()\n", - "\n", - "# Compute the starting fidelity\n", - "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", - "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", - "print(\"Starting fidelity:\", starting_fidelity)" - ] - }, - { - "cell_type": "markdown", - "id": "56dfc4e4-6b28-4ed1-81da-8b0f4bc99425", - "metadata": {}, - "source": [ - "To optimize the ansatz parameters, we minimize the `MaximizeStateFidelity` cost function using the L-BFGS optimizer from SciPy, with a stopping criterion set to surpass the fidelity of the initial circuit without AQC-Tensor. This ensures that the compressed circuit has both lower Trotter error and reduced depth." - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "af10ef7f-e57d-49a3-abb0-3d51e856c4b0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-04-14 11:48:28.705807 Intermediate result: Fidelity 0.99795851\n", - "2025-04-14 11:48:28.743265 Intermediate result: Fidelity 0.99822826\n", - "2025-04-14 11:48:28.776629 Intermediate result: Fidelity 0.99829675\n", - "2025-04-14 11:48:28.816153 Intermediate result: Fidelity 0.99832474\n", - "2025-04-14 11:48:28.856437 Intermediate result: Fidelity 0.99836131\n", - "2025-04-14 11:48:28.896432 Intermediate result: Fidelity 0.99839954\n", - "2025-04-14 11:48:28.936670 Intermediate result: Fidelity 0.99846517\n", - "2025-04-14 11:48:28.982069 Intermediate result: Fidelity 0.99865029\n", - "2025-04-14 11:48:29.026130 Intermediate result: Fidelity 0.99872332\n", - "2025-04-14 11:48:29.067426 Intermediate result: Fidelity 0.99892359\n", - "2025-04-14 11:48:29.110742 Intermediate result: Fidelity 0.99900640\n", - "2025-04-14 11:48:29.161362 Intermediate result: Fidelity 0.99907169\n", - "2025-04-14 11:48:29.207933 Intermediate result: Fidelity 0.99911423\n", - "2025-04-14 11:48:29.266772 Intermediate result: Fidelity 0.99918716\n", - "2025-04-14 11:48:29.331727 Intermediate result: Fidelity 0.99921278\n", - "2025-04-14 11:48:29.401694 Intermediate result: Fidelity 0.99924853\n", - "2025-04-14 11:48:29.467980 Intermediate result: Fidelity 0.99928797\n", - "2025-04-14 11:48:29.533281 Intermediate result: Fidelity 0.99933028\n", - "2025-04-14 11:48:29.600833 Intermediate result: Fidelity 0.99935757\n", - "2025-04-14 11:48:29.670816 Intermediate result: Fidelity 0.99938140\n", - "2025-04-14 11:48:29.736928 Intermediate result: Fidelity 0.99940964\n", - "2025-04-14 11:48:29.802931 Intermediate result: Fidelity 0.99944051\n", - "2025-04-14 11:48:29.869177 Intermediate result: Fidelity 0.99946828\n", - "2025-04-14 11:48:29.940156 Intermediate result: Fidelity 0.99948723\n", - "2025-04-14 11:48:30.005751 Intermediate result: Fidelity 0.99951011\n", - "2025-04-14 11:48:30.070853 Intermediate result: Fidelity 0.99954718\n", - "2025-04-14 11:48:30.139171 Intermediate result: Fidelity 0.99956267\n", - "2025-04-14 11:48:30.210506 Intermediate result: Fidelity 0.99958949\n", - "2025-04-14 11:48:30.279647 Intermediate result: Fidelity 0.99960498\n", - "2025-04-14 11:48:30.348016 Intermediate result: Fidelity 0.99961308\n", - "2025-04-14 11:48:30.414311 Intermediate result: Fidelity 0.99962894\n", - "2025-04-14 11:48:30.488910 Intermediate result: Fidelity 0.99964121\n", - "2025-04-14 11:48:30.561298 Intermediate result: Fidelity 0.99964348\n", - "2025-04-14 11:48:30.632214 Intermediate result: Fidelity 0.99964860\n", - "2025-04-14 11:48:30.705703 Intermediate result: Fidelity 0.99965695\n", - "2025-04-14 11:48:30.775679 Intermediate result: Fidelity 0.99966398\n", - "2025-04-14 11:48:30.842629 Intermediate result: Fidelity 0.99967816\n", - "2025-04-14 11:48:30.912357 Intermediate result: Fidelity 0.99968293\n", - "2025-04-14 11:48:30.979420 Intermediate result: Fidelity 0.99968936\n", - "2025-04-14 11:48:31.049196 Intermediate result: Fidelity 0.99969223\n", - "2025-04-14 11:48:31.125391 Intermediate result: Fidelity 0.99970009\n", - "2025-04-14 11:48:31.201256 Intermediate result: Fidelity 0.99970724\n", - "2025-04-14 11:48:31.272424 Intermediate result: Fidelity 0.99970987\n", - "2025-04-14 11:48:31.338907 Intermediate result: Fidelity 0.99971237\n", - "2025-04-14 11:48:31.404800 Intermediate result: Fidelity 0.99971916\n", - "2025-04-14 11:48:31.475226 Intermediate result: Fidelity 0.99971940\n", - "2025-04-14 11:48:31.547746 Intermediate result: Fidelity 0.99972465\n", - "2025-04-14 11:48:31.622827 Intermediate result: Fidelity 0.99972763\n", - "2025-04-14 11:48:31.819516 Intermediate result: Fidelity 0.99972894\n", - "2025-04-14 11:48:33.444538 Intermediate result: Fidelity 0.99972894\n", - "Done after 50 iterations.\n" - ] - } - ], - "source": [ - "# Setting values for the optimization\n", - "aqc_stopping_fidelity = 1\n", - "aqc_max_iterations = 500\n", - "\n", - "stopping_point = 1.0 - aqc_stopping_fidelity\n", - "objective = MaximizeStateFidelity(\n", - " aqc_target_mps, aqc_ansatz, simulator_settings\n", - ")\n", - "\n", - "\n", - "def callback(intermediate_result: OptimizeResult):\n", - " fidelity = 1 - intermediate_result.fun\n", - " print(\n", - " f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\"\n", - " )\n", - " if intermediate_result.fun < stopping_point:\n", - " # Good enough for now\n", - " raise StopIteration\n", - "\n", - "\n", - "result = minimize(\n", - " objective,\n", - " aqc_initial_parameters,\n", - " method=\"L-BFGS-B\",\n", - " jac=True,\n", - " options={\"maxiter\": aqc_max_iterations},\n", - " callback=callback,\n", - ")\n", - "if (\n", - " result.status\n", - " not in (\n", - " 0,\n", - " 1,\n", - " 99,\n", - " )\n", - "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", - " raise RuntimeError(\n", - " f\"Optimization failed: {result.message} (status={result.status})\"\n", - " )\n", - "\n", - "print(f\"Done after {result.nit} iterations.\")\n", - "aqc_final_parameters = result.x" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "0e711fcc-7767-411e-bb73-fd9c8408a22b", - "metadata": {}, - "outputs": [], - "source": [ - "parameters = [float(param) for param in aqc_final_parameters]" - ] - }, - { - "cell_type": "markdown", - "id": "b1838b8e-81d4-4897-8cbd-dd7daa8bb220", - "metadata": {}, - "source": [ - "Construct the final circuit for transpilation by assembling the optimized ansatz with the remaining time evolution circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "f96b1ab0-f0ed-485f-a763-4ab57d36f410", - "metadata": {}, - "outputs": [], - "source": [ - "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", - "aqc_final_circuit.compose(subsequent_circuit, inplace=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "579cbef1-a3a8-4916-b840-ab3fcc5c4b33", - "metadata": {}, - "outputs": [], - "source": [ - "aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)" - ] - }, - { - "cell_type": "markdown", - "id": "bb111fb1-07e4-4f0c-ad71-933b04492a23", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution" - ] - }, - { - "cell_type": "markdown", - "id": "627dd426-730d-4554-b631-1e9949f46c26", - "metadata": {}, - "source": [ - "Select the backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "771a44a6-b5ba-4b9d-8c64-9a79f6d4ed77", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(min_num_qubits=127)\n", - "print(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "cbc5d688-9acf-4fe2-8d58-fea74edb09e8", - "metadata": {}, - "source": [ - "Transpile the completed circuit on the target hardware, preparing it for execution. The resulting ISA circuit can then be sent for execution on the backend." - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "85b4acc0-7121-416d-9bf5-b6d3135ae805", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[1.+0.j])\n", - "Circuit depth: 122\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pass_manager = generate_preset_pass_manager(\n", - " backend=backend, optimization_level=3\n", - ")\n", - "isa_circuit = pass_manager.run(aqc_final_circuit)\n", - "isa_observable = observable.apply_layout(isa_circuit.layout)\n", - "print(\"Observable info:\", isa_observable)\n", - "print(\"Circuit depth:\", isa_circuit.depth())\n", - "isa_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "b0d295c7-c816-4683-bb2a-0ce9898e5d88", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", - " coeffs=[1.+0.j])\n", - "Circuit depth: 158\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)\n", - "isa_comparison_observable = observable.apply_layout(\n", - " isa_comparison_circuit.layout\n", - ")\n", - "print(\"Observable info:\", isa_comparison_observable)\n", - "print(\"Circuit depth:\", isa_comparison_circuit.depth())\n", - "isa_comparison_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "f9431bec-66d1-44e3-a77d-01ca9e03e523", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "In this step, we run the transpiled circuit on quantum hardware (or a simulated backend) using `EstimatorV2` from `qiskit_ibm_runtime` to measure the specified observable. The job result will provide valuable insights into the circuit’s performance on the target hardware.\n", - "\n", - "For this larger-scale example, we will explore how to utilize `EstimatorOptions` to better manage and control the parameters of our hardware experiment. While these settings are optional, they are useful for tracking experiment parameters and refining execution options for optimal results.\n", - "\n", - "For a complete list of available execution options, refer to the [qiskit-ibm-runtime documentation](/docs/api/qiskit-ibm-runtime/options-estimator-options)." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "cf449353-f392-4248-b6ad-5af9107c1fff", - "metadata": {}, - "outputs": [], - "source": [ - "twirling_options = {\n", - " \"enable_gates\": True,\n", - " \"enable_measure\": True,\n", - " \"num_randomizations\": 300,\n", - " \"shots_per_randomization\": 100,\n", - " \"strategy\": \"active\",\n", - "}\n", - "\n", - "zne_options = {\n", - " \"amplifier\": \"gate_folding\",\n", - " \"noise_factors\": [1, 2, 3],\n", - " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", - " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", - "}\n", - "\n", - "meas_learning_options = {\n", - " \"num_randomizations\": 512,\n", - " \"shots_per_randomization\": 512,\n", - "}\n", - "\n", - "resilience_options = {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": zne_options,\n", - " \"measure_noise_learning\": meas_learning_options,\n", - "}\n", - "\n", - "estimator_options = {\n", - " \"resilience\": resilience_options,\n", - " \"twirling\": twirling_options,\n", - "}\n", - "\n", - "estimator = Estimator(backend, options=estimator_options)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "ea2a1425-a49b-4b31-b019-abbee4fdf690", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: czyjx6crxz8g008f63r0\n" - ] - }, - { - "data": { - "text/plain": [ - "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), evs_noise_factors=np.ndarray(), stds_noise_factors=np.ndarray(), ensemble_stds_noise_factors=np.ndarray(), evs_extrapolated=np.ndarray(), stds_extrapolated=np.ndarray()), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job = estimator.run([(isa_circuit, isa_observable)])\n", - "print(\"Job ID:\", job.job_id())\n", - "job.result()" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "4f7d9e7d-9da1-4cc3-ae09-0e3613c5479a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job Comparison ID: czyjx6crxz8g008f63r0\n" - ] - }, - { - "data": { - "text/plain": [ - "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), evs_noise_factors=np.ndarray(), stds_noise_factors=np.ndarray(), ensemble_stds_noise_factors=np.ndarray(), evs_extrapolated=np.ndarray(), stds_extrapolated=np.ndarray()), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])\n", - "print(\"Job Comparison ID:\", job.job_id())\n", - "job_comparison.result()" - ] - }, - { - "cell_type": "markdown", - "id": "17ced13b-a3d3-4ded-92a6-b978508dd27a", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "Here, no reconstruction is needed, like before; we can directly access the expectation value from the execution output to examine the result." - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "5b2e2d3e-bebb-44a4-bbdb-881ffc2749e5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exact: \t-0.5888\n", - "AQC: \t-0.4809, |∆| = 0.1078\n", - "AQC Comparison:\t1.1764, |∆| = 1.7652\n" - ] - } - ], - "source": [ - "# AQC results\n", - "hw_results = job.result()\n", - "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", - "hw_expvals = [\n", - " pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts\n", - "]\n", - "aqc_expval = hw_expvals[0]\n", - "\n", - "# AQC comparison results\n", - "hw_comparison_results = job_comparison.result()\n", - "hw_comparison_results_dicts = [\n", - " pub_result.data.__dict__ for pub_result in hw_comparison_results\n", - "]\n", - "hw_comparison_expvals = [\n", - " pub_result_data[\"evs\"].tolist()\n", - " for pub_result_data in hw_comparison_results_dicts\n", - "]\n", - "aqc_compare_expval = hw_comparison_expvals[0]\n", - "\n", - "print(f\"Exact: \\t{reference_expval:.4f}\")\n", - "print(\n", - " f\"AQC: \\t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}\"\n", - ")\n", - "print(\n", - " f\"AQC Comparison:\\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f5ba5634-1689-457a-b7c7-adf3ca8e1d41", - "metadata": {}, - "source": [ - "Plot the results of the AQC, comparison, and exact circuits for the 50-site XXZ model." - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "01889c4d-16a4-458a-9211-08be8bcae1e4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "labels = [\"AQC Result\", \"AQC Comparison Result\"]\n", - "values = [abs(aqc_expval), abs(aqc_compare_expval)]\n", - "\n", - "plt.figure(figsize=(10, 6))\n", - "bars = plt.bar(labels, values, color=[\"tab:blue\", \"tab:purple\"])\n", - "plt.axhline(\n", - " y=abs(reference_expval), color=\"red\", linestyle=\"--\", label=\"Exact Result\"\n", - ")\n", - "plt.xlabel(\"Results\")\n", - "plt.ylabel(\"Absolute Expected Value\")\n", - "plt.title(\"AQC Result vs AQC Comparison Result (Absolute Values)\")\n", - "plt.legend()\n", - "for bar in bars:\n", - " y_val = bar.get_height()\n", - " plt.text(\n", - " bar.get_x() + bar.get_width() / 2.0,\n", - " y_val,\n", - " round(y_val, 2),\n", - " va=\"bottom\",\n", - " )\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "45f512f7-9233-4f1f-8f24-cb3524567135", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "This tutorial demonstrated how to use Approximate Quantum Compilation with tensor networks (AQC-Tensor) to compress and optimize circuits for simulating quantum dynamics at scale. Utilizing both small and large Heisenberg models, we applied AQC-Tensor to reduce the circuit depth required for Trotterized time evolution. By generating a parametrized ansatz from a simplified Trotter circuit and optimizing it with matrix product state (MPS) techniques, we achieved a low-depth approximation of the target evolution that is both accurate and efficient.\n", - "\n", - "The workflow here highlights the key advantages of AQC-Tensor for scaling quantum simulations:\n", - "\n", - "- **Significant circuit compression**: AQC-Tensor reduced the circuit depth needed for complex time evolution, enhancing its feasibility on current devices.\n", - "- **Efficient optimization**: The MPS approach provided a robust framework for parameter optimization, balancing fidelity with computational efficiency.\n", - "- **Hardware-ready execution**: Transpiling the final optimized circuit ensured it met the constraints of the target quantum hardware.\n", - "\n", - "As larger quantum devices and more advanced algorithms emerge, techniques like AQC-Tensor will become essential for running complex quantum simulations on near-term hardware, demonstrating promising progress in managing depth and fidelity for scalable quantum applications." - ] - }, - { - "cell_type": "markdown", - "id": "3cc40a5a-4b55-45e8-a4f1-df45b9e37abd", - "metadata": {}, - "source": [ - "## Tutorial survey\n", - "\n", - "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", - "\n", - "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_eF01c2sfeSt6cqq)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2c991cef-110a-455a-8741-52d5e20d3196", + "metadata": {}, + "source": [ + "---\n", + "title: Approximate quantum compilation for time evolution circuits\n", + "description: This tutorial demonstrates how to implement AQC-Tensor with Qiskit to enhance quantum circuit performance\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore circo */}\n", + "\n", + "# Approximate quantum compilation for time evolution circuits\n", + "*Usage estimate: Five minutes on an Eagle processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "93895788-7d38-4cc7-a7e9-ba97c34966af", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "This tutorial demonstrates how to implement **Approximate Quantum Compilation** using tensor networks (AQC-Tensor) with Qiskit to enhance quantum circuit performance. We apply AQC-Tensor within the context of a Trotterized time evolution to reduce circuit depth while maintaining simulation accuracy, following the Qiskit framework for state preparation and optimization. You will learn how to create a low-depth ansatz circuit from an initial Trotter circuit, optimize it with tensor networks, and prepare it for quantum hardware execution.\n", + "\n", + "The primary objective is to simulate time evolution for a model Hamiltonian with a reduced circuit depth. This is achieved using the **AQC-Tensor** Qiskit addon, [qiskit-addon-aqc-tensor](https://github.com/Qiskit/qiskit-addon-aqc-tensor), which leverages tensor networks, specifically matrix product states (MPS), to compress and optimize the initial circuit. Through iterative adjustments, the compressed ansatz circuit maintains fidelity to the original circuit while staying feasible for near-term quantum hardware. See the [documentation](https://qiskit.github.io/qiskit-addon-aqc-tensor/) for more information.\n", + "\n", + "Approximate Quantum Compilation is particularly advantageous in quantum simulations that exceed hardware coherence times, as it allows complex simulations to be performed more efficiently. This tutorial guides you through the AQC-Tensor workflow setup in Qiskit, covering initialization of a Hamiltonian, generation of Trotter circuits, and transpilation of the final optimized circuit for a target device." + ] + }, + { + "cell_type": "markdown", + "id": "b1b8f238-0dab-42c5-b658-779f3ff178a0", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "* Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)\n", + "* AQC-Tensor Qiskit addon (`pip install 'qiskit-addon-aqc-tensor[aer,quimb-jax]'`)" + ] + }, + { + "cell_type": "markdown", + "id": "f0723f2d-e0ba-4fe9-a742-1891b7b45459", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "ccdcdca2-4e77-4696-b0ff-45d13dbc6ac3", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import quimb.tensor\n", + "import datetime\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from scipy.optimize import OptimizeResult, minimize\n", + "\n", + "from qiskit.quantum_info import SparsePauliOp, Pauli\n", + "from qiskit.transpiler import CouplingMap\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.synthesis import SuzukiTrotter\n", + "\n", + "from qiskit_addon_utils.problem_generators import (\n", + " generate_time_evolution_circuit,\n", + ")\n", + "from qiskit_addon_aqc_tensor.ansatz_generation import (\n", + " generate_ansatz_from_circuit,\n", + ")\n", + "from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity\n", + "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", + "from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit\n", + "from qiskit_addon_aqc_tensor.simulation import compute_overlap\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "from rustworkx.visualization import graphviz_draw" + ] + }, + { + "cell_type": "markdown", + "id": "39bc4470-f7f8-4550-83a7-a22167024000", + "metadata": {}, + "source": [ + "## Part I. Small-scale example\n", + "\n", + "The first part of this tutorial uses a small-scale example with 10 sites to illustrate the process of mapping a quantum simulation problem to an executable quantum circuit. Here, we’ll explore the dynamics of a 10-site XXZ model, allowing us to build and optimize a manageable quantum circuit before scaling to larger systems.\n", + "\n", + "The XXZ model is widely studied in physics for examining spin interactions and magnetic properties. We set up the Hamiltonian to have open boundary conditions with site-dependent interactions between neighboring sites along the chain.\n", + "\n", + "### Model Hamiltonian and observable\n", + "\n", + "The Hamiltonian for our 10-site XXZ model is defined as:\n", + "$$\n", + "\\hat{\\mathcal{H}}_{XXZ} = \\sum_{i=1}^{L-1} J_{i,(i+1)}\\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\\cdot Z_i Z_{(i+1)} \\right) \\, ,\n", + "$$\n", + "\n", + "where $J_{i,(i+1)}$ is a random coefficient corresponding to edge $(i, i+1)$, and $L=10$ is the number of sites.\n", + "\n", + "By simulating the evolution of this system with reduced circuit depth, we can gain insights into using AQC-Tensor to compress and optimize circuits." + ] + }, + { + "cell_type": "markdown", + "id": "89f22428-d4d8-49e7-a1c5-39197dc58330", + "metadata": {}, + "source": [ + "#### Set up the Hamiltonian and observable\n", + "\n", + "Before we map our problem, we need to set up the coupling map, Hamiltonian, and observable for the 10-site XXZ model." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ea0e102-23d5-4e6e-8ef8-e82843452b19", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hamiltonian: SparsePauliOp(['IIIIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII'],\n", + " coeffs=[1. +0.j, 0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j,\n", + " 0.60759468+0.j, 0.60759468+0.j, 1.21518937+0.j, 0.55138169+0.j,\n", + " 0.55138169+0.j, 1.10276338+0.j, 0.52244159+0.j, 0.52244159+0.j,\n", + " 1.04488318+0.j, 0.4618274 +0.j, 0.4618274 +0.j, 0.9236548 +0.j,\n", + " 0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j, 0.46879361+0.j,\n", + " 0.46879361+0.j, 0.93758721+0.j, 0.6958865 +0.j, 0.6958865 +0.j,\n", + " 1.391773 +0.j, 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j])\n", + "Observable: SparsePauliOp(['IIIIZZIIII'],\n", + " coeffs=[1.+0.j])\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# L is the number of sites, also the length of the 1D spin chain\n", + "L = 10\n", + "\n", + "# Generate the coupling map\n", + "edge_list = [(i - 1, i) for i in range(1, L)]\n", + "# Generate an edge-coloring so we can make hw-efficient circuits\n", + "even_edges = edge_list[::2]\n", + "odd_edges = edge_list[1::2]\n", + "coupling_map = CouplingMap(edge_list)\n", + "\n", + "# Generate random coefficients for our XXZ Hamiltonian\n", + "np.random.seed(0)\n", + "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", + "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n", + "for i, edge in enumerate(even_edges + odd_edges):\n", + " hamiltonian += SparsePauliOp.from_sparse_list(\n", + " [\n", + " (\"XX\", (edge), Js[i] / 2),\n", + " (\"YY\", (edge), Js[i] / 2),\n", + " (\"ZZ\", (edge), Js[i]),\n", + " ],\n", + " num_qubits=L,\n", + " )\n", + "\n", + "# Generate a ZZ observable between the two middle qubits\n", + "observable = SparsePauliOp.from_sparse_list(\n", + " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", + ")\n", + "\n", + "print(\"Hamiltonian:\", hamiltonian)\n", + "print(\"Observable:\", observable)\n", + "graphviz_draw(coupling_map.graph, method=\"circo\")" + ] + }, + { + "cell_type": "markdown", + "id": "e6cd54e3-6a9c-4cdc-8493-d612e930dcee", + "metadata": {}, + "source": [ + "With the Hamiltonian defined, we can proceed to construct the initial state." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "71252a74-e7bf-4003-a1fd-8f7659195f9a", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate an initial state\n", + "initial_state = QuantumCircuit(L)\n", + "for i in range(L):\n", + " if i % 2:\n", + " initial_state.x(i)" + ] + }, + { + "cell_type": "markdown", + "id": "5fb34a14-d197-4ac4-a224-5be2cec06a2e", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Now that we have constructed the Hamiltonian, defining the spin-spin interactions and external magnetic fields that characterize the system, we follow three main steps in the AQC-Tensor workflow:\n", + "\n", + "1. **Generate the optimized AQC circuit**: Using Trotterization, we approximate the initial evolution, which is then compressed to reduce circuit depth.\n", + "2. **Create the remaining time evolution circuit**: Capture the evolution for the remaining time beyond the initial segment.\n", + "3. **Combine the circuits**: Merge the optimized AQC circuit with the remaining evolution circuit into a complete time-evolution circuit ready for execution.\n", + "\n", + "This approach creates a low-depth ansatz for the target evolution, supporting efficient simulation within near-term quantum hardware constraints." + ] + }, + { + "cell_type": "markdown", + "id": "7c5f2a2e-179c-4e62-a32f-5bc202f89701", + "metadata": {}, + "source": [ + "#### Determine the portion of time evolution to simulate classically\n", + "\n", + "Our goal is to simulate the time evolution of the model Hamiltonian defined earlier using Trotter evolution. To make this process efficient for quantum hardware, we split the evolution into two segments:\n", + "\n", + "- **Initial Segment**: This initial portion of the evolution, from $ t_i = 0.0 $ to $ t_f = 0.2 $, is simulable with MPS and can be efficiently “compiled” using AQC-Tensor. By using the [AQC-Tensor Qiskit addon](https://github.com/Qiskit/qiskit-addon-aqc-tensor), we generate a compressed circuit for this segment, referred to as the `aqc_target_circuit`. Because this segment will be simulated on a tensor-network simulator, we can afford to use a higher number of Trotter layers without impacting hardware resources significantly. We set `aqc_target_num_trotter_steps = 32` for this segment.\n", + "\n", + "- **Subsequent Segment**: This remaining portion of the evolution, from $ t = 0.2 $ to $ t = 0.4 $, will be executed on quantum hardware, referred to as the `subsequent_circuit`. Given hardware limitations, we aim to use as few Trotter layers as possible to maintain a manageable circuit depth. For this segment, we use `subsequent_num_trotter_steps = 3`.\n", + "\n", + "\n", + "#### Choose the split time\n", + "We choose $t = 0.2$ as the split time to balance classical simulability with hardware feasibility. Early in the evolution, entanglement in the XXZ model remains low enough for classical methods like MPS to approximate accurately.\n", + "\n", + "When choosing a split time, a good guideline is to select a point where entanglement is still manageable classically but captures enough of the evolution to simplify the hardware-executed portion. Trial and error may be needed to find the best balance for different Hamiltonians." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "199d4e7e-02b2-4da8-b567-8195fb9de536", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate the AQC target circuit (initial segment)\n", + "aqc_evolution_time = 0.2\n", + "aqc_target_num_trotter_steps = 32\n", + "\n", + "aqc_target_circuit = initial_state.copy()\n", + "aqc_target_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "83039f82-97cb-4613-86c9-a8faf0839a02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Generate the subsequent circuit\n", + "subsequent_num_trotter_steps = 3\n", + "subsequent_evolution_time = 0.2\n", + "\n", + "subsequent_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),\n", + " time=subsequent_evolution_time,\n", + ")\n", + "subsequent_circuit.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "064a17af-4af8-4ff5-899c-750147d269df", + "metadata": {}, + "source": [ + "To enable a meaningful comparison, we will generate two additional circuits:\n", + "\n", + "- **AQC comparison circuit**: This circuit evolves up to `aqc_evolution_time` but uses the same Trotter step duration as the `subsequent_circuit`. It serves as a comparison to the `aqc_target_circuit`, showing the evolution we would observe without using an increased number of Trotter steps. We will refer to this circuit as the `aqc_comparison_circuit`.\n", + "\n", + "- **Reference circuit**: This circuit is used as a baseline to obtain the exact result. It simulates the full evolution using tensor networks to calculate the exact outcome, providing a reference for evaluating the effectiveness of AQC-Tensor. We will refer to this circuit as the `reference_circuit`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "33b2ad93-7ae4-4250-bf38-9adc3bc2970d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of Trotter steps for comparison: 3\n" + ] + } + ], + "source": [ + "# Generate the AQC comparison circuit\n", + "aqc_comparison_num_trotter_steps = int(\n", + " subsequent_num_trotter_steps\n", + " / subsequent_evolution_time\n", + " * aqc_evolution_time\n", + ")\n", + "print(\n", + " \"Number of Trotter steps for comparison:\",\n", + " aqc_comparison_num_trotter_steps,\n", + ")\n", + "\n", + "aqc_comparison_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1bfa67a2-ac51-4158-b539-2c33f4c5ecf3", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate the reference circuit\n", + "evolution_time = 0.4\n", + "reps = 200\n", + "\n", + "reference_circuit = initial_state.copy()\n", + "reference_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=reps),\n", + " time=evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2051f622-5b71-4be5-aab9-e9eb5d315a6f", + "metadata": {}, + "source": [ + "#### Generate an ansatz and initial parameters from a Trotter circuit with fewer steps\n", + "\n", + "Now that we have constructed our four circuits, let's proceed with the AQC-Tensor workflow. First, we construct a “good” circuit that has the same evolution time as the target circuit, but with fewer Trotter steps (and thus fewer layers).\n", + "\n", + "Then we pass this “good” circuit to AQC-Tensor’s `generate_ansatz_from_circuit` function. This function analyzes the two-qubit connectivity of the circuit and returns two things:\n", + "\n", + "1. A general, parametrized ansatz circuit with the same two-qubit connectivity as the input circuit.\n", + "2. Parameters that, when plugged into the ansatz, yield the input (good) circuit.\n", + "\n", + "Soon we will take these parameters and iteratively adjust them to bring the ansatz circuit as close as possible to the target MPS." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b9e81c51-dc6f-4237-9aca-e1384f1897bc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "aqc_ansatz_num_trotter_steps = 1\n", + "\n", + "aqc_good_circuit = initial_state.copy()\n", + "aqc_good_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")\n", + "\n", + "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(\n", + " aqc_good_circuit\n", + ")\n", + "aqc_ansatz.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bd9e9bf2-9ee5-445d-aa4a-c7f412c488f8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AQC Comparison circuit: depth 36\n", + "Target circuit: depth 385\n", + "Ansatz circuit: depth 7, with 156 parameters\n" + ] + } + ], + "source": [ + "print(f\"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}\")\n", + "print(f\"Target circuit: depth {aqc_target_circuit.depth()}\")\n", + "print(\n", + " f\"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3c8663b9-d513-4a2a-9911-03f3500ad486", + "metadata": {}, + "source": [ + "#### Choose settings for tensor network simulation\n", + "\n", + "Here, we use Quimb's matrix-product state circuit simulator, along with jax to provide the gradient." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e5b9471f-fc7a-45c9-ab11-295cfff620bf", + "metadata": {}, + "outputs": [], + "source": [ + "simulator_settings = QuimbSimulator(\n", + " quimb.tensor.CircuitMPS, autodiff_backend=\"jax\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5030d11d-54e3-4f29-acb2-6f7dd3ff05cb", + "metadata": {}, + "source": [ + "Next, we build a MPS representation of the target state that will be approximated using AQC-Tensor. This representation enables efficient handling of entanglement, providing a compact description of the quantum state for further optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "1f050059-a281-41f1-a277-d7450e8b3ee3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target MPS maximum bond dimension: 5\n", + "Reference MPS maximum bond dimension: 7\n" + ] + } + ], + "source": [ + "aqc_target_mps = tensornetwork_from_circuit(\n", + " aqc_target_circuit, simulator_settings\n", + ")\n", + "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", + "\n", + "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the\n", + "# `local_expectation``\n", + "reference_mps = tensornetwork_from_circuit(\n", + " reference_circuit, simulator_settings\n", + ")\n", + "reference_expval = reference_mps.local_expectation(\n", + " quimb.pauli(\"Z\") & quimb.pauli(\"Z\"), (L // 2 - 1, L // 2)\n", + ").real.item()\n", + "print(\"Reference MPS maximum bond dimension:\", reference_mps.psi.max_bond())" + ] + }, + { + "cell_type": "markdown", + "id": "5ffbdd20-c9bf-4265-8a8d-8135abb2b8f7", + "metadata": {}, + "source": [ + "Note that, by choosing a larger number of Trotter steps for the target state, we have effectively reduced its Trotter error compared to the initial circuit. We can evaluate the fidelity ($ |\\langle \\psi_1 | \\psi_2 \\rangle|^2 $) between the state prepared by the initial circuit and the target state to quantify this difference." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "51d7d623-3e7c-44e7-80f1-94fd555a2c9e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting fidelity: 0.9982464959067222\n" + ] + } + ], + "source": [ + "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", + "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", + "print(\"Starting fidelity:\", starting_fidelity)" + ] + }, + { + "cell_type": "markdown", + "id": "01234b5b-f4db-4e22-993a-c8673621ab7f", + "metadata": {}, + "source": [ + "#### Optimize the parameters of the ansatz using MPS calculations\n", + "In this step, we optimize the ansatz parameters by minimizing a simple cost function, `MaximizeStateFidelity`, using the L-BFGS optimizer from SciPy. We select a stopping criterion for the fidelity that ensures it surpasses the fidelity of the initial circuit without AQC-Tensor. Once this threshold is reached, the compressed circuit will exhibit both lower Trotter error and reduced depth compared to the original circuit. By using additional CPU time, further optimization can continue to increase fidelity." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "ad2265cb-19a6-4402-8b0d-24239c930c90", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-04-14 11:46:52.174235 Intermediate result: Fidelity 0.99795851\n", + "2025-04-14 11:46:52.218249 Intermediate result: Fidelity 0.99822826\n", + "2025-04-14 11:46:52.280924 Intermediate result: Fidelity 0.99829675\n", + "2025-04-14 11:46:52.356214 Intermediate result: Fidelity 0.99832474\n", + "2025-04-14 11:46:52.411609 Intermediate result: Fidelity 0.99836131\n", + "2025-04-14 11:46:52.453747 Intermediate result: Fidelity 0.99839954\n", + "2025-04-14 11:46:52.496184 Intermediate result: Fidelity 0.99846517\n", + "2025-04-14 11:46:52.542046 Intermediate result: Fidelity 0.99865029\n", + "2025-04-14 11:46:52.583679 Intermediate result: Fidelity 0.99872332\n", + "2025-04-14 11:46:52.628732 Intermediate result: Fidelity 0.99892359\n", + "2025-04-14 11:46:52.690386 Intermediate result: Fidelity 0.99900640\n", + "2025-04-14 11:46:52.759398 Intermediate result: Fidelity 0.99907169\n", + "2025-04-14 11:46:52.819496 Intermediate result: Fidelity 0.99911423\n", + "2025-04-14 11:46:52.884505 Intermediate result: Fidelity 0.99918716\n", + "2025-04-14 11:46:52.947919 Intermediate result: Fidelity 0.99921278\n", + "2025-04-14 11:46:53.012808 Intermediate result: Fidelity 0.99924853\n", + "2025-04-14 11:46:53.083626 Intermediate result: Fidelity 0.99928797\n", + "2025-04-14 11:46:53.153235 Intermediate result: Fidelity 0.99933028\n", + "2025-04-14 11:46:53.221371 Intermediate result: Fidelity 0.99935757\n", + "2025-04-14 11:46:53.286211 Intermediate result: Fidelity 0.99938140\n", + "2025-04-14 11:46:53.352391 Intermediate result: Fidelity 0.99940964\n", + "2025-04-14 11:46:53.420472 Intermediate result: Fidelity 0.99944051\n", + "2025-04-14 11:46:53.486279 Intermediate result: Fidelity 0.99946828\n", + "2025-04-14 11:46:53.552338 Intermediate result: Fidelity 0.99948723\n", + "2025-04-14 11:46:53.618688 Intermediate result: Fidelity 0.99951011\n", + "2025-04-14 11:46:53.690878 Intermediate result: Fidelity 0.99954718\n", + "2025-04-14 11:46:53.762725 Intermediate result: Fidelity 0.99956267\n", + "2025-04-14 11:46:53.829784 Intermediate result: Fidelity 0.99958949\n", + "2025-04-14 11:46:53.897477 Intermediate result: Fidelity 0.99960498\n", + "2025-04-14 11:46:53.954633 Intermediate result: Fidelity 0.99961308\n", + "2025-04-14 11:46:54.010125 Intermediate result: Fidelity 0.99962894\n", + "2025-04-14 11:46:54.064717 Intermediate result: Fidelity 0.99964121\n", + "2025-04-14 11:46:54.118892 Intermediate result: Fidelity 0.99964348\n", + "2025-04-14 11:46:54.183236 Intermediate result: Fidelity 0.99964860\n", + "2025-04-14 11:46:54.245521 Intermediate result: Fidelity 0.99965695\n", + "2025-04-14 11:46:54.305792 Intermediate result: Fidelity 0.99966398\n", + "2025-04-14 11:46:54.355819 Intermediate result: Fidelity 0.99967816\n", + "2025-04-14 11:46:54.409580 Intermediate result: Fidelity 0.99968293\n", + "2025-04-14 11:46:54.457979 Intermediate result: Fidelity 0.99968936\n", + "2025-04-14 11:46:54.505891 Intermediate result: Fidelity 0.99969223\n", + "2025-04-14 11:46:54.551084 Intermediate result: Fidelity 0.99970009\n", + "2025-04-14 11:46:54.601817 Intermediate result: Fidelity 0.99970724\n", + "2025-04-14 11:46:54.650097 Intermediate result: Fidelity 0.99970987\n", + "2025-04-14 11:46:54.714727 Intermediate result: Fidelity 0.99971237\n", + "2025-04-14 11:46:54.780052 Intermediate result: Fidelity 0.99971916\n", + "2025-04-14 11:46:54.871994 Intermediate result: Fidelity 0.99971940\n", + "2025-04-14 11:46:54.958244 Intermediate result: Fidelity 0.99972465\n", + "2025-04-14 11:46:55.011057 Intermediate result: Fidelity 0.99972763\n", + "2025-04-14 11:46:55.175339 Intermediate result: Fidelity 0.99972894\n", + "2025-04-14 11:46:56.688912 Intermediate result: Fidelity 0.99972894\n", + "Done after 50 iterations.\n" + ] + } + ], + "source": [ + "# Setting values for the optimization\n", + "aqc_stopping_fidelity = 1\n", + "aqc_max_iterations = 500\n", + "\n", + "stopping_point = 1.0 - aqc_stopping_fidelity\n", + "objective = MaximizeStateFidelity(\n", + " aqc_target_mps, aqc_ansatz, simulator_settings\n", + ")\n", + "\n", + "\n", + "def callback(intermediate_result: OptimizeResult):\n", + " fidelity = 1 - intermediate_result.fun\n", + " print(\n", + " f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\"\n", + " )\n", + " if intermediate_result.fun < stopping_point:\n", + " # Good enough for now\n", + " raise StopIteration\n", + "\n", + "\n", + "result = minimize(\n", + " objective,\n", + " aqc_initial_parameters,\n", + " method=\"L-BFGS-B\",\n", + " jac=True,\n", + " options={\"maxiter\": aqc_max_iterations},\n", + " callback=callback,\n", + ")\n", + "if (\n", + " result.status\n", + " not in (\n", + " 0,\n", + " 1,\n", + " 99,\n", + " )\n", + "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", + " raise RuntimeError(\n", + " f\"Optimization failed: {result.message} (status={result.status})\"\n", + " )\n", + "\n", + "print(f\"Done after {result.nit} iterations.\")\n", + "aqc_final_parameters = result.x" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c95ccea0-be99-4db9-838d-2327851e6761", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final parameters: [-7.853983035039254, 1.5707966468427772, 1.5707962768868613, -1.570798010835122, 1.570794480409574, 1.5707972214146968, -1.570796593027083, 1.5707968206822998, -1.5707959018046258, -1.5707991700969144, 1.5707965852600927, 4.712386891737442, -7.853980840717957, 1.5707967508132654, 1.5707943162503217, -1.5707955382023582, 1.5707958007156742, 1.570796096113293, -1.5707928509846847, 1.5707971042943747, -1.570797909276557, -1.5707941020637393, 1.5707980179540793, 4.712389823219363, -1.5707928752386107, 1.5707996426312891, -1.5707975640471001, -1.570794132802984, 1.5707944361599957, 4.712390747060803, 0.1048818190315936, 0.06686710468840577, -0.0668645844756557, -3.1415923537135466, 1.2374931269696063, 6.323169390432535e-07, 3.53229204771738e-08, 2.1091105688681484, 6.283186439944202, 0.12152258846156239, 0.07961752617254866, -0.07961775088604585, -1.6564278051174865e-06, 2.0771163596472384, 3.141592651630471, -6.283185775192653, 1.7691609006726954, 3.1415922910116216, 0.19837572065074083, 0.11114901449078964, -0.11115124544944892, -3.141591983034976, 0.8570788408766729, 4.201601390404146e-07, -3.141593736550978, 0.34652010942396333, 6.283186232785291, 0.13606356527241956, 0.03891676349289617, -0.03891524189533726, -1.5707965732853424, 1.5707968967088564, -0.3086133992238162, 1.5707957152428194, 1.5707968398959653, -0.32062737993080026, 0.11027416939993417, 0.0726167290795046, -0.07262020423334464, -2.3729431959735024e-06, 1.8204437429254703, 9.299060301196612e-07, -3.141592899563451, 2.103269568939461, 3.1415937539734626, 0.11536891854817125, 0.09099022308254198, -0.09098864958606581, -3.1415913307373127, 2.078429034357281, -1.509777998069368e-06, -3.1415922600663255, 1.5189162645358172, -3.1415878461323583, 0.09999070991480716, 0.04352011445148391, -0.04351849541849812, -1.570797642506462, 1.570795238023824, 0.8903442644396505, 1.5707962698006606, 1.5707946765132268, 0.9098791754570567, 0.10448284343424026, 0.07317037684936827, -0.07316718173961152, -3.141592682240966, 2.1665363080039612, -7.450882112394189e-07, -5.771181304929921e-07, 2.615334999517103, -3.1415914971653898, 0.1890887078648001, 0.13578163074571992, -0.13578078143610256, 7.156734195912883e-07, 1.7915385305413096, -5.188866034727312e-07, 1.2827742939197711e-06, 1.2348316581417487, 6.28318357406372, 0.08061187643781703, 0.03820789039271876, -0.03820731868804904, 1.5707964027727628, 1.570798734462218, 4.387336153720882, -1.570795722044763, 1.570798457375325, 4.450361734163248, 0.092360147257953, 0.06047700345049011, -0.06048592856713045, -3.141591214829027, 2.6593289993286047, -2.366937342261038e-07, 8.112162974032695e-08, 1.8907014631413432, 8.355881261853104e-07, 0.23303641819370874, 0.14331998953606456, -0.1433194488304741, -3.141591621822901, 0.7455776479558791, 3.1415914520163586, -3.1415933560496105, 0.7603938554148255, -1.6230983177616282e-06, 0.07186349688535713, 0.03197144517771341, -0.031971177878588546, -4.712389048748508, 1.5707948403165752, 1.2773619319829186, -1.5707990802172127, 1.5707957676951863, 1.289083769394045, 0.13644999397718796, 0.032761460443590046, -0.032762060585195645, -1.5707977610073176, 1.5707964181578042, -3.4826435600366983, -4.712389691708343, 1.570794277502252, 2.799088046133275]\n" + ] + } + ], + "source": [ + "parameters = [float(param) for param in aqc_final_parameters]\n", + "print(\"Final parameters:\", parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "ca32c1ef-f7d5-4489-83b3-17972bf94700", + "metadata": {}, + "source": [ + "At this point, it is only necessary to find the final parameters to the ansatz circuit. We can then merge the optimized AQC circuit with the remaining evolution circuit to create a complete time-evolution circuit for execution on quantum hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "813c9ced-6a2e-4345-bffc-7dae938e2015", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", + "aqc_final_circuit.compose(subsequent_circuit, inplace=True)\n", + "aqc_final_circuit.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "46a93127-5443-40b7-a81a-dfd70df42ba2", + "metadata": {}, + "source": [ + "We also need to merge our `aqc_comparison_circuit` with the remaining evolution circuit. This circuit will be used to compare the performance of the AQC-Tensor-optimized circuit with the original circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "86ba26ff-0bfa-47d0-b5ee-8944a8ddf274", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)\n", + "aqc_comparison_circuit.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "982c5067-9a74-42a0-bd31-aaff72cf79df", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution" + ] + }, + { + "cell_type": "markdown", + "id": "2121c40c-a9a7-4c36-a20a-04ee9f6637db", + "metadata": {}, + "source": [ + "Select the hardware. Here we will use any of the IBM Quantum® devices available that have at least 127 qubits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88715b59-4516-4b75-b041-c978944dd14a", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(min_num_qubits=127)\n", + "print(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "3f477394-5eac-4c12-b36b-e94c800fa889", + "metadata": {}, + "source": [ + "We transpile PUBs (circuit and observables) to match the backend ISA (Instruction Set Architecture). By setting `optimization_level=3`, the transpiler optimizes the circuit to fit a one-dimensional chain of qubits, reducing the noise that impacts circuit fidelity. Once the circuits are transformed into a format compatible with the backend, we apply a corresponding transformation to the observables to ensure they align with the modified qubit layout." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "087fff8d-98b9-4f9a-8004-01a3b0166e12", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],\n", + " coeffs=[1.+0.j])\n", + "Circuit depth: 111\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pass_manager = generate_preset_pass_manager(\n", + " backend=backend, optimization_level=3\n", + ")\n", + "isa_circuit = pass_manager.run(aqc_final_circuit)\n", + "isa_observable = observable.apply_layout(isa_circuit.layout)\n", + "print(\"Observable info:\", isa_observable)\n", + "print(\"Circuit depth:\", isa_circuit.depth())\n", + "isa_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "58e8e840-94a8-4414-81f7-01bbf1b73810", + "metadata": {}, + "source": [ + "Perform transpilation for the comparison circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "7c2e5fe7-21ce-461d-adaa-776f8d882163", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ'],\n", + " coeffs=[1.+0.j])\n", + "Circuit depth: 158\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)\n", + "isa_comparison_observable = observable.apply_layout(\n", + " isa_comparison_circuit.layout\n", + ")\n", + "print(\"Observable info:\", isa_comparison_observable)\n", + "print(\"Circuit depth:\", isa_comparison_circuit.depth())\n", + "isa_comparison_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "1f832aaa-c2a9-42b7-95b6-bd920cdc17b0", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "In this step, we execute the transpiled circuit on quantum hardware (or a simulated backend). Using the `EstimatorV2` class from `qiskit_ibm_runtime`, we set up an estimator to run the circuit and measure the specified observable. The job result provides the expected outcome for the observable, giving us insights into the circuit’s performance on the target hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "3aabf36b-4587-43b7-be99-9376fc7f47c1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: czyhqdxd8drg008hx0yg\n" + ] + }, + { + "data": { + "text/plain": [ + "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray()), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "estimator = Estimator(backend)\n", + "job = estimator.run([(isa_circuit, isa_observable)])\n", + "print(\"Job ID:\", job.job_id())\n", + "job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "4ec6669b-2b76-41df-9634-3c073998b1d7", + "metadata": {}, + "source": [ + "Perform the execution for the comparison circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "91f16fff-da28-42b4-a32a-fa2f0945f8fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job Comparison ID: czyhqdxd8drg008hx0yg\n" + ] + }, + { + "data": { + "text/plain": [ + "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), ensemble_standard_error=np.ndarray()), metadata={'shots': 4096, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 32})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])\n", + "print(\"Job Comparison ID:\", job.job_id())\n", + "job_comparison.result()" + ] + }, + { + "cell_type": "markdown", + "id": "3c6d16d4-3118-49b6-9594-3d7ddb02701c", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "In this case, reconstruction is unnecessary. We can directly examine the result by accessing the expectation value from the execution output." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "7247e56f-0ab8-4aa9-834c-ba95965a0b3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exact: \t-0.5252\n", + "AQC: \t-0.4903, |∆| = 0.0349\n", + "AQC Comparison:\t0.5424, |∆| = 1.0676\n" + ] + } + ], + "source": [ + "# AQC results\n", + "hw_results = job.result()\n", + "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", + "hw_expvals = [\n", + " pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts\n", + "]\n", + "aqc_expval = hw_expvals[0]\n", + "\n", + "# AQC comparison results\n", + "hw_comparison_results = job_comparison.result()\n", + "hw_comparison_results_dicts = [\n", + " pub_result.data.__dict__ for pub_result in hw_comparison_results\n", + "]\n", + "hw_comparison_expvals = [\n", + " pub_result_data[\"evs\"].tolist()\n", + " for pub_result_data in hw_comparison_results_dicts\n", + "]\n", + "aqc_compare_expval = hw_comparison_expvals[0]\n", + "\n", + "print(f\"Exact: \\t{reference_expval:.4f}\")\n", + "print(\n", + " f\"AQC: \\t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}\"\n", + ")\n", + "print(\n", + " f\"AQC Comparison:\\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1c8b65e6-df72-45b3-8f31-a6aed44cb867", + "metadata": {}, + "source": [ + "Bar plot to compare the results of the AQC, comparison, and exact circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "5f7b36a6-3666-4223-9c5d-d92bca741ad2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.style.use(\"seaborn-v0_8\")\n", + "\n", + "labels = [\"AQC Result\", \"AQC Comparison Result\"]\n", + "values = [abs(aqc_expval), abs(aqc_compare_expval)]\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "bars = plt.bar(labels, values, color=[\"tab:blue\", \"tab:purple\"])\n", + "plt.axhline(\n", + " y=abs(reference_expval), color=\"red\", linestyle=\"--\", label=\"Exact Result\"\n", + ")\n", + "plt.xlabel(\"Results\")\n", + "plt.ylabel(\"Absolute Expected Value\")\n", + "plt.title(\"AQC Result vs AQC Comparison Result (Absolute Values)\")\n", + "plt.legend()\n", + "for bar in bars:\n", + " y_val = bar.get_height()\n", + " plt.text(\n", + " bar.get_x() + bar.get_width() / 2.0,\n", + " y_val,\n", + " round(y_val, 2),\n", + " va=\"bottom\",\n", + " )\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ff81fb19-d175-42b0-8ef3-025712a7630d", + "metadata": {}, + "source": [ + "## Part II: scale it up\n", + "\n", + "\n", + "The second part of this tutorial builds on the previous example by scaling up to a larger system with 50 sites, illustrating how to map more complex quantum simulation problems to executable quantum circuits. Here, we explore the dynamics of a 50-site XXZ model, allowing us to build and optimize a substantial quantum circuit that reflects more realistic system sizes.\n", + "\n", + "The Hamiltonian for our 50-site XXZ model is defined as:\n", + "$$\n", + "\\hat{\\mathcal{H}}_{XXZ} = \\sum_{i=1}^{L-1} J_{i,(i+1)}\\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\\cdot Z_i Z_{(i+1)} \\right) \\, ,\n", + "$$\n", + "\n", + "where $J_{i,(i+1)}$ is a random coefficient corresponding to edge $(i, i+1)$, and $L=50$ is the number of sites." + ] + }, + { + "cell_type": "markdown", + "id": "f6d11e17-b4be-44fa-bc09-465e5e66a6af", + "metadata": {}, + "source": [ + "Define the coupling map and edges for the Hamiltonian." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "d0dec1ee-85fe-4c40-9595-b7764e9cfcca", + "metadata": {}, + "outputs": [], + "source": [ + "L = 50 # L = length of our 1D spin chain\n", + "\n", + "# Generate the edge list for this spin-chain\n", + "edge_list = [(i - 1, i) for i in range(1, L)]\n", + "# Generate an edge-coloring so we can make hw-efficient circuits\n", + "even_edges = edge_list[::2]\n", + "odd_edges = edge_list[1::2]\n", + "\n", + "# Instantiate a CouplingMap object\n", + "coupling_map = CouplingMap(edge_list)\n", + "\n", + "# Generate random coefficients for our XXZ Hamiltonian\n", + "np.random.seed(0)\n", + "Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)\n", + "\n", + "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n", + "for i, edge in enumerate(even_edges + odd_edges):\n", + " hamiltonian += SparsePauliOp.from_sparse_list(\n", + " [\n", + " (\"XX\", (edge), Js[i] / 2),\n", + " (\"YY\", (edge), Js[i] / 2),\n", + " (\"ZZ\", (edge), Js[i]),\n", + " ],\n", + " num_qubits=L,\n", + " )\n", + "\n", + "observable = SparsePauliOp.from_sparse_list(\n", + " [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n", + ")\n", + "\n", + "# Generate an initial state\n", + "L = hamiltonian.num_qubits\n", + "initial_state = QuantumCircuit(L)\n", + "for i in range(L):\n", + " if i % 2:\n", + " initial_state.x(i)" + ] + }, + { + "cell_type": "markdown", + "id": "65009d89-4752-40ac-b3ab-8e88425851b2", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "For this larger problem, we start by constructing the Hamiltonian for the 50-site XXZ model, defining spin-spin interactions and external magnetic fields across all sites. After this, we follow three main steps:\n", + "\n", + "1. **Generate the optimized AQC circuit**: Use Trotterization to approximate the initial evolution, then compress this segment to reduce circuit depth.\n", + "2. **Create the remaining time evolution circuit**: Capture the remaining time evolution beyond the initial segment.\n", + "3. **Combine the circuits**: Merge the optimized AQC circuit with the remaining evolution circuit to create a complete time-evolution circuit ready for execution." + ] + }, + { + "cell_type": "markdown", + "id": "41f5186a-e54e-4913-8a52-1c90611991c7", + "metadata": {}, + "source": [ + "Generate the AQC target circuit (the initial segment)." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "86735d08-49b3-47e4-abcd-631a77f50a02", + "metadata": {}, + "outputs": [], + "source": [ + "aqc_evolution_time = 0.2\n", + "aqc_target_num_trotter_steps = 32\n", + "\n", + "aqc_target_circuit = initial_state.copy()\n", + "aqc_target_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c5e79369-19ae-4b17-a9fc-9502a8a1254c", + "metadata": {}, + "source": [ + "Generate the subsequent circuit (the remaining segment)." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "35c6469c-3be7-4e89-a065-0e4957305b59", + "metadata": {}, + "outputs": [], + "source": [ + "subsequent_num_trotter_steps = 3\n", + "subsequent_evolution_time = 0.2\n", + "\n", + "subsequent_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),\n", + " time=subsequent_evolution_time,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0e13fbc5-0c00-4f21-9925-8d1230854dc5", + "metadata": {}, + "source": [ + "Generate the AQC comparison circuit (the initial segment, but with the same number of Trotter steps as the subsequent circuit)." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "922fb467-81b0-40d7-b21b-ef781ad043a1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of Trotter steps for comparison: 3\n" + ] + } + ], + "source": [ + "# Generate the AQC comparison circuit\n", + "aqc_comparison_num_trotter_steps = int(\n", + " subsequent_num_trotter_steps\n", + " / subsequent_evolution_time\n", + " * aqc_evolution_time\n", + ")\n", + "print(\n", + " \"Number of Trotter steps for comparison:\",\n", + " aqc_comparison_num_trotter_steps,\n", + ")\n", + "\n", + "aqc_comparison_circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "21228ca8-3fa6-4efe-9e53-8d3f824b7729", + "metadata": {}, + "source": [ + "Generate the reference circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "3525fe02-25cf-4254-b2c4-717f27bc29d8", + "metadata": {}, + "outputs": [], + "source": [ + "evolution_time = 0.4\n", + "reps = 200\n", + "\n", + "reference_circuit = initial_state.copy()\n", + "reference_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=reps),\n", + " time=evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a15b40ac-0259-4229-b76b-502ff7068499", + "metadata": {}, + "source": [ + "Generate an ansatz and initial parameters from a Trotter circuit with fewer steps." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "ddbc3549-3171-4a29-b046-2a52b5e7065a", + "metadata": {}, + "outputs": [], + "source": [ + "aqc_ansatz_num_trotter_steps = 1\n", + "\n", + "aqc_good_circuit = initial_state.copy()\n", + "aqc_good_circuit.compose(\n", + " generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),\n", + " time=aqc_evolution_time,\n", + " ),\n", + " inplace=True,\n", + ")\n", + "\n", + "aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(\n", + " aqc_good_circuit\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "8e596d77-5954-4756-b765-48855cd38659", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AQC Comparison circuit: depth 36\n", + "Target circuit: depth 385\n", + "Ansatz circuit: depth 7, with 816 parameters\n" + ] + } + ], + "source": [ + "print(f\"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}\")\n", + "print(f\"Target circuit: depth {aqc_target_circuit.depth()}\")\n", + "print(\n", + " f\"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4d07d63a-8572-4116-9fd5-c070f62043a7", + "metadata": {}, + "source": [ + "Set settings for tensor network simulation and then construct a matrix product state representation of the target state for optimization. Then, evaluate the fidelity between the initial circuit and the target state to quantify the difference in Trotter error." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "6030706e-1451-47d9-9e47-bcccf4cb5d9c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target MPS maximum bond dimension: 5\n", + "Starting fidelity: 0.9926466919924161\n" + ] + } + ], + "source": [ + "simulator_settings = QuimbSimulator(\n", + " quimb.tensor.CircuitMPS, autodiff_backend=\"jax\"\n", + ")\n", + "\n", + "# Build the matrix-product representation of the state to be approximated by AQC\n", + "aqc_target_mps = tensornetwork_from_circuit(\n", + " aqc_target_circuit, simulator_settings\n", + ")\n", + "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", + "\n", + "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the\n", + "# `local_expectation``\n", + "reference_mps = tensornetwork_from_circuit(\n", + " reference_circuit, simulator_settings\n", + ")\n", + "reference_expval = reference_mps.local_expectation(\n", + " quimb.pauli(\"Z\") & quimb.pauli(\"Z\"), (L // 2 - 1, L // 2)\n", + ").real.item()\n", + "\n", + "# Compute the starting fidelity\n", + "good_mps = tensornetwork_from_circuit(aqc_good_circuit, simulator_settings)\n", + "starting_fidelity = abs(compute_overlap(good_mps, aqc_target_mps)) ** 2\n", + "print(\"Starting fidelity:\", starting_fidelity)" + ] + }, + { + "cell_type": "markdown", + "id": "56dfc4e4-6b28-4ed1-81da-8b0f4bc99425", + "metadata": {}, + "source": [ + "To optimize the ansatz parameters, we minimize the `MaximizeStateFidelity` cost function using the L-BFGS optimizer from SciPy, with a stopping criterion set to surpass the fidelity of the initial circuit without AQC-Tensor. This ensures that the compressed circuit has both lower Trotter error and reduced depth." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "af10ef7f-e57d-49a3-abb0-3d51e856c4b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-04-14 11:48:28.705807 Intermediate result: Fidelity 0.99795851\n", + "2025-04-14 11:48:28.743265 Intermediate result: Fidelity 0.99822826\n", + "2025-04-14 11:48:28.776629 Intermediate result: Fidelity 0.99829675\n", + "2025-04-14 11:48:28.816153 Intermediate result: Fidelity 0.99832474\n", + "2025-04-14 11:48:28.856437 Intermediate result: Fidelity 0.99836131\n", + "2025-04-14 11:48:28.896432 Intermediate result: Fidelity 0.99839954\n", + "2025-04-14 11:48:28.936670 Intermediate result: Fidelity 0.99846517\n", + "2025-04-14 11:48:28.982069 Intermediate result: Fidelity 0.99865029\n", + "2025-04-14 11:48:29.026130 Intermediate result: Fidelity 0.99872332\n", + "2025-04-14 11:48:29.067426 Intermediate result: Fidelity 0.99892359\n", + "2025-04-14 11:48:29.110742 Intermediate result: Fidelity 0.99900640\n", + "2025-04-14 11:48:29.161362 Intermediate result: Fidelity 0.99907169\n", + "2025-04-14 11:48:29.207933 Intermediate result: Fidelity 0.99911423\n", + "2025-04-14 11:48:29.266772 Intermediate result: Fidelity 0.99918716\n", + "2025-04-14 11:48:29.331727 Intermediate result: Fidelity 0.99921278\n", + "2025-04-14 11:48:29.401694 Intermediate result: Fidelity 0.99924853\n", + "2025-04-14 11:48:29.467980 Intermediate result: Fidelity 0.99928797\n", + "2025-04-14 11:48:29.533281 Intermediate result: Fidelity 0.99933028\n", + "2025-04-14 11:48:29.600833 Intermediate result: Fidelity 0.99935757\n", + "2025-04-14 11:48:29.670816 Intermediate result: Fidelity 0.99938140\n", + "2025-04-14 11:48:29.736928 Intermediate result: Fidelity 0.99940964\n", + "2025-04-14 11:48:29.802931 Intermediate result: Fidelity 0.99944051\n", + "2025-04-14 11:48:29.869177 Intermediate result: Fidelity 0.99946828\n", + "2025-04-14 11:48:29.940156 Intermediate result: Fidelity 0.99948723\n", + "2025-04-14 11:48:30.005751 Intermediate result: Fidelity 0.99951011\n", + "2025-04-14 11:48:30.070853 Intermediate result: Fidelity 0.99954718\n", + "2025-04-14 11:48:30.139171 Intermediate result: Fidelity 0.99956267\n", + "2025-04-14 11:48:30.210506 Intermediate result: Fidelity 0.99958949\n", + "2025-04-14 11:48:30.279647 Intermediate result: Fidelity 0.99960498\n", + "2025-04-14 11:48:30.348016 Intermediate result: Fidelity 0.99961308\n", + "2025-04-14 11:48:30.414311 Intermediate result: Fidelity 0.99962894\n", + "2025-04-14 11:48:30.488910 Intermediate result: Fidelity 0.99964121\n", + "2025-04-14 11:48:30.561298 Intermediate result: Fidelity 0.99964348\n", + "2025-04-14 11:48:30.632214 Intermediate result: Fidelity 0.99964860\n", + "2025-04-14 11:48:30.705703 Intermediate result: Fidelity 0.99965695\n", + "2025-04-14 11:48:30.775679 Intermediate result: Fidelity 0.99966398\n", + "2025-04-14 11:48:30.842629 Intermediate result: Fidelity 0.99967816\n", + "2025-04-14 11:48:30.912357 Intermediate result: Fidelity 0.99968293\n", + "2025-04-14 11:48:30.979420 Intermediate result: Fidelity 0.99968936\n", + "2025-04-14 11:48:31.049196 Intermediate result: Fidelity 0.99969223\n", + "2025-04-14 11:48:31.125391 Intermediate result: Fidelity 0.99970009\n", + "2025-04-14 11:48:31.201256 Intermediate result: Fidelity 0.99970724\n", + "2025-04-14 11:48:31.272424 Intermediate result: Fidelity 0.99970987\n", + "2025-04-14 11:48:31.338907 Intermediate result: Fidelity 0.99971237\n", + "2025-04-14 11:48:31.404800 Intermediate result: Fidelity 0.99971916\n", + "2025-04-14 11:48:31.475226 Intermediate result: Fidelity 0.99971940\n", + "2025-04-14 11:48:31.547746 Intermediate result: Fidelity 0.99972465\n", + "2025-04-14 11:48:31.622827 Intermediate result: Fidelity 0.99972763\n", + "2025-04-14 11:48:31.819516 Intermediate result: Fidelity 0.99972894\n", + "2025-04-14 11:48:33.444538 Intermediate result: Fidelity 0.99972894\n", + "Done after 50 iterations.\n" + ] + } + ], + "source": [ + "# Setting values for the optimization\n", + "aqc_stopping_fidelity = 1\n", + "aqc_max_iterations = 500\n", + "\n", + "stopping_point = 1.0 - aqc_stopping_fidelity\n", + "objective = MaximizeStateFidelity(\n", + " aqc_target_mps, aqc_ansatz, simulator_settings\n", + ")\n", + "\n", + "\n", + "def callback(intermediate_result: OptimizeResult):\n", + " fidelity = 1 - intermediate_result.fun\n", + " print(\n", + " f\"{datetime.datetime.now()} Intermediate result: Fidelity {fidelity:.8f}\"\n", + " )\n", + " if intermediate_result.fun < stopping_point:\n", + " # Good enough for now\n", + " raise StopIteration\n", + "\n", + "\n", + "result = minimize(\n", + " objective,\n", + " aqc_initial_parameters,\n", + " method=\"L-BFGS-B\",\n", + " jac=True,\n", + " options={\"maxiter\": aqc_max_iterations},\n", + " callback=callback,\n", + ")\n", + "if (\n", + " result.status\n", + " not in (\n", + " 0,\n", + " 1,\n", + " 99,\n", + " )\n", + "): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration\n", + " raise RuntimeError(\n", + " f\"Optimization failed: {result.message} (status={result.status})\"\n", + " )\n", + "\n", + "print(f\"Done after {result.nit} iterations.\")\n", + "aqc_final_parameters = result.x" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "0e711fcc-7767-411e-bb73-fd9c8408a22b", + "metadata": {}, + "outputs": [], + "source": [ + "parameters = [float(param) for param in aqc_final_parameters]" + ] + }, + { + "cell_type": "markdown", + "id": "b1838b8e-81d4-4897-8cbd-dd7daa8bb220", + "metadata": {}, + "source": [ + "Construct the final circuit for transpilation by assembling the optimized ansatz with the remaining time evolution circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "f96b1ab0-f0ed-485f-a763-4ab57d36f410", + "metadata": {}, + "outputs": [], + "source": [ + "aqc_final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)\n", + "aqc_final_circuit.compose(subsequent_circuit, inplace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "579cbef1-a3a8-4916-b840-ab3fcc5c4b33", + "metadata": {}, + "outputs": [], + "source": [ + "aqc_comparison_circuit.compose(subsequent_circuit, inplace=True)" + ] + }, + { + "cell_type": "markdown", + "id": "bb111fb1-07e4-4f0c-ad71-933b04492a23", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution" + ] + }, + { + "cell_type": "markdown", + "id": "627dd426-730d-4554-b631-1e9949f46c26", + "metadata": {}, + "source": [ + "Select the backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "771a44a6-b5ba-4b9d-8c64-9a79f6d4ed77", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(min_num_qubits=127)\n", + "print(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "cbc5d688-9acf-4fe2-8d58-fea74edb09e8", + "metadata": {}, + "source": [ + "Transpile the completed circuit on the target hardware, preparing it for execution. The resulting ISA circuit can then be sent for execution on the backend." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "85b4acc0-7121-416d-9bf5-b6d3135ae805", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[1.+0.j])\n", + "Circuit depth: 122\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pass_manager = generate_preset_pass_manager(\n", + " backend=backend, optimization_level=3\n", + ")\n", + "isa_circuit = pass_manager.run(aqc_final_circuit)\n", + "isa_observable = observable.apply_layout(isa_circuit.layout)\n", + "print(\"Observable info:\", isa_observable)\n", + "print(\"Circuit depth:\", isa_circuit.depth())\n", + "isa_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "b0d295c7-c816-4683-bb2a-0ce9898e5d88", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observable info: SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],\n", + " coeffs=[1.+0.j])\n", + "Circuit depth: 158\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isa_comparison_circuit = pass_manager.run(aqc_comparison_circuit)\n", + "isa_comparison_observable = observable.apply_layout(\n", + " isa_comparison_circuit.layout\n", + ")\n", + "print(\"Observable info:\", isa_comparison_observable)\n", + "print(\"Circuit depth:\", isa_comparison_circuit.depth())\n", + "isa_comparison_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "f9431bec-66d1-44e3-a77d-01ca9e03e523", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "In this step, we run the transpiled circuit on quantum hardware (or a simulated backend) using `EstimatorV2` from `qiskit_ibm_runtime` to measure the specified observable. The job result will provide valuable insights into the circuit’s performance on the target hardware.\n", + "\n", + "For this larger-scale example, we will explore how to utilize `EstimatorOptions` to better manage and control the parameters of our hardware experiment. While these settings are optional, they are useful for tracking experiment parameters and refining execution options for optimal results.\n", + "\n", + "For a complete list of available execution options, refer to the [qiskit-ibm-runtime documentation](/docs/api/qiskit-ibm-runtime/options-estimator-options)." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "cf449353-f392-4248-b6ad-5af9107c1fff", + "metadata": {}, + "outputs": [], + "source": [ + "twirling_options = {\n", + " \"enable_gates\": True,\n", + " \"enable_measure\": True,\n", + " \"num_randomizations\": 300,\n", + " \"shots_per_randomization\": 100,\n", + " \"strategy\": \"active\",\n", + "}\n", + "\n", + "zne_options = {\n", + " \"amplifier\": \"gate_folding\",\n", + " \"noise_factors\": [1, 2, 3],\n", + " \"extrapolated_noise_factors\": list(np.linspace(0, 3, 31)),\n", + " \"extrapolator\": [\"exponential\", \"linear\", \"fallback\"],\n", + "}\n", + "\n", + "meas_learning_options = {\n", + " \"num_randomizations\": 512,\n", + " \"shots_per_randomization\": 512,\n", + "}\n", + "\n", + "resilience_options = {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": zne_options,\n", + " \"measure_noise_learning\": meas_learning_options,\n", + "}\n", + "\n", + "estimator_options = {\n", + " \"resilience\": resilience_options,\n", + " \"twirling\": twirling_options,\n", + "}\n", + "\n", + "estimator = Estimator(backend, options=estimator_options)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "ea2a1425-a49b-4b31-b019-abbee4fdf690", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: czyjx6crxz8g008f63r0\n" + ] + }, + { + "data": { + "text/plain": [ + "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), evs_noise_factors=np.ndarray(), stds_noise_factors=np.ndarray(), ensemble_stds_noise_factors=np.ndarray(), evs_extrapolated=np.ndarray(), stds_extrapolated=np.ndarray()), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job = estimator.run([(isa_circuit, isa_observable)])\n", + "print(\"Job ID:\", job.job_id())\n", + "job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "4f7d9e7d-9da1-4cc3-ae09-0e3613c5479a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job Comparison ID: czyjx6crxz8g008f63r0\n" + ] + }, + { + "data": { + "text/plain": [ + "PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(), stds=np.ndarray(), evs_noise_factors=np.ndarray(), stds_noise_factors=np.ndarray(), ensemble_stds_noise_factors=np.ndarray(), evs_extrapolated=np.ndarray(), stds_extrapolated=np.ndarray()), metadata={'shots': 30000, 'target_precision': 0.005773502691896258, 'circuit_metadata': {}, 'resilience': {'zne': {'extrapolator': 'exponential'}}, 'num_randomizations': 300})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': True, 'enable_measure': True, 'num_randomizations': 300, 'shots_per_randomization': 100, 'interleave_randomizations': True, 'strategy': 'active'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': True, 'pec_mitigation': False, 'zne': {'noise_factors': [1, 2, 3], 'extrapolator': ['exponential', 'linear', 'fallback'], 'extrapolated_noise_factors': [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1, 1.1, 1.2000000000000002, 1.3, 1.4000000000000001, 1.5, 1.6, 1.7000000000000002, 1.8, 1.9000000000000001, 2, 2.1, 2.2, 2.3000000000000003, 2.4000000000000004, 2.5, 2.6, 2.7, 2.8000000000000003, 2.9000000000000004, 3]}}, 'version': 2})" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job_comparison = estimator.run([(isa_comparison_circuit, isa_observable)])\n", + "print(\"Job Comparison ID:\", job.job_id())\n", + "job_comparison.result()" + ] + }, + { + "cell_type": "markdown", + "id": "17ced13b-a3d3-4ded-92a6-b978508dd27a", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "Here, no reconstruction is needed, like before; we can directly access the expectation value from the execution output to examine the result." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "5b2e2d3e-bebb-44a4-bbdb-881ffc2749e5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exact: \t-0.5888\n", + "AQC: \t-0.4809, |∆| = 0.1078\n", + "AQC Comparison:\t1.1764, |∆| = 1.7652\n" + ] + } + ], + "source": [ + "# AQC results\n", + "hw_results = job.result()\n", + "hw_results_dicts = [pub_result.data.__dict__ for pub_result in hw_results]\n", + "hw_expvals = [\n", + " pub_result_data[\"evs\"].tolist() for pub_result_data in hw_results_dicts\n", + "]\n", + "aqc_expval = hw_expvals[0]\n", + "\n", + "# AQC comparison results\n", + "hw_comparison_results = job_comparison.result()\n", + "hw_comparison_results_dicts = [\n", + " pub_result.data.__dict__ for pub_result in hw_comparison_results\n", + "]\n", + "hw_comparison_expvals = [\n", + " pub_result_data[\"evs\"].tolist()\n", + " for pub_result_data in hw_comparison_results_dicts\n", + "]\n", + "aqc_compare_expval = hw_comparison_expvals[0]\n", + "\n", + "print(f\"Exact: \\t{reference_expval:.4f}\")\n", + "print(\n", + " f\"AQC: \\t{aqc_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_expval):.4f}\"\n", + ")\n", + "print(\n", + " f\"AQC Comparison:\\t{aqc_compare_expval:.4f}, |∆| = {np.abs(reference_expval- aqc_compare_expval):.4f}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f5ba5634-1689-457a-b7c7-adf3ca8e1d41", + "metadata": {}, + "source": [ + "Plot the results of the AQC, comparison, and exact circuits for the 50-site XXZ model." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "01889c4d-16a4-458a-9211-08be8bcae1e4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = [\"AQC Result\", \"AQC Comparison Result\"]\n", + "values = [abs(aqc_expval), abs(aqc_compare_expval)]\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "bars = plt.bar(labels, values, color=[\"tab:blue\", \"tab:purple\"])\n", + "plt.axhline(\n", + " y=abs(reference_expval), color=\"red\", linestyle=\"--\", label=\"Exact Result\"\n", + ")\n", + "plt.xlabel(\"Results\")\n", + "plt.ylabel(\"Absolute Expected Value\")\n", + "plt.title(\"AQC Result vs AQC Comparison Result (Absolute Values)\")\n", + "plt.legend()\n", + "for bar in bars:\n", + " y_val = bar.get_height()\n", + " plt.text(\n", + " bar.get_x() + bar.get_width() / 2.0,\n", + " y_val,\n", + " round(y_val, 2),\n", + " va=\"bottom\",\n", + " )\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "45f512f7-9233-4f1f-8f24-cb3524567135", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This tutorial demonstrated how to use Approximate Quantum Compilation with tensor networks (AQC-Tensor) to compress and optimize circuits for simulating quantum dynamics at scale. Utilizing both small and large Heisenberg models, we applied AQC-Tensor to reduce the circuit depth required for Trotterized time evolution. By generating a parametrized ansatz from a simplified Trotter circuit and optimizing it with matrix product state (MPS) techniques, we achieved a low-depth approximation of the target evolution that is both accurate and efficient.\n", + "\n", + "The workflow here highlights the key advantages of AQC-Tensor for scaling quantum simulations:\n", + "\n", + "- **Significant circuit compression**: AQC-Tensor reduced the circuit depth needed for complex time evolution, enhancing its feasibility on current devices.\n", + "- **Efficient optimization**: The MPS approach provided a robust framework for parameter optimization, balancing fidelity with computational efficiency.\n", + "- **Hardware-ready execution**: Transpiling the final optimized circuit ensured it met the constraints of the target quantum hardware.\n", + "\n", + "As larger quantum devices and more advanced algorithms emerge, techniques like AQC-Tensor will become essential for running complex quantum simulations on near-term hardware, demonstrating promising progress in managing depth and fidelity for scalable quantum applications." + ] + }, + { + "cell_type": "markdown", + "id": "3cc40a5a-4b55-45e8-a4f1-df45b9e37abd", + "metadata": {}, + "source": [ + "## Tutorial survey\n", + "\n", + "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", + "\n", + "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_eF01c2sfeSt6cqq)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb b/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb index 5805af0929e..031a498b47d 100644 --- a/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb +++ b/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb @@ -1,1638 +1,1639 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f056ca4a-fbe4-4051-bbcd-79c2d7848cd0", - "metadata": {}, - "source": [ - "---\n", - "title: Compilation methods for Hamiltonian simulation circuits\n", - "description: This tutorial provides a comparative overview of three compilation methods in Qiskit for Hamiltonian simulation workloads.\n", - "---\n", - "\n", - "\n", - "# Compilation methods for Hamiltonian simulation circuits\n", - "Estimated QPU usage: no execution was done in this tutorial because it is focused on the transpilation process.\n", - "\n", - "{/* cspell:ignore Rustiq, nshuffles, edgecolors, edgecolor, Hamlib, Benchpress, Brugière, Goubault, Martiel, Dubal, Lishman, Ivrii, fontweight, fontsize, textprops, wedgeprops, startangle, autopct */}" - ] - }, - { - "cell_type": "markdown", - "id": "d960a90b-1487-4310-a222-b95b8a77a080", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "Quantum circuit compilation is a crucial step in the quantum computing workflow. It involves transforming a high-level quantum algorithm into a physical quantum circuit that adheres to the constraints of the target quantum hardware. Effective compilation can significantly impact the performance of quantum algorithms by reducing circuit depth, gate count, and execution time. This tutorial explores three distinct approaches to quantum circuit compilation in Qiskit, showcasing their strengths and applications through practical examples.\n", - "\n", - "The goal of this tutorial is to teach users how to apply and evaluate three compilation methods in Qiskit: the SABRE transpiler, the AI-powered transpiler, and the Rustiq plugin. Users will learn how to use each method effectively and how to benchmark their performance across different quantum circuits. By the end of this tutorial, users will be able to choose and tailor compilation strategies based on specific optimization goals such as reducing circuit depth, minimizing gate count, or improving runtime.\n", - "\n", - "### What you will learn\n", - "- **How to use the Qiskit transpiler with SABRE for layout and routing optimization.**\n", - "- **How to leverage the AI transpiler for advanced, automated circuit optimization.**\n", - "- **How to employ the Rustiq plugin for circuits requiring precise synthesis of operations, particularly in Hamiltonian simulation tasks.**\n", - "\n", - "This tutorial uses three example circuits following the [Qiskit patterns](/docs/guides/intro-to-patterns) workflow to illustrate the performance of each compilation method. By the end of this tutorial, users will be equipped to choose the appropriate compilation strategy based on their specific requirements and constraints.\n", - "\n", - "### Compilation methods overview\n", - "\n", - "#### 1. **Qiskit transpiler with SABRE**\n", - "The Qiskit transpiler uses the SABRE (SWAP-based BidiREctional heuristic search) algorithm to optimize circuit layout and routing. SABRE focuses on minimizing SWAP gates and their impact on circuit depth while adhering to hardware connectivity constraints. This method is highly versatile and suitable for general-purpose circuit optimization, providing a balance between performance and computation time. To take advantage of the latest improvements in SABRE, detailed in [\\[1\\]](https://arxiv.org/abs/2409.08368), you can increase the number of trials (for example, `layout_trials=400, swap_trials=400`). For the purposes of this tutorial, we will use the default values for the number of trials in order to compare to Qiskit's default transpiler. The advantages and parameter exploration of SABRE are covered in a separate [deep-dive tutorial](/docs/tutorials/transpilation-optimizations-with-sabre).\n", - "\n", - "#### 2. **AI transpiler**\n", - "\n", - "The AI-powered transpiler in Qiskit uses machine learning to predict optimal transpilation strategies by analyzing patterns in circuit structure and hardware constraints to select the best sequence of optimizations for a given input. This method is particularly effective for large-scale quantum circuits, offering a high degree of automation and adaptability to diverse problem types. In addition to general circuit optimization, the AI transpiler can be used with the `AIPauliNetworkSynthesis` pass, which targets Pauli network circuits — blocks composed of H, S, SX, CX, RX, RY, and RZ gates — and applies a reinforcement learning-based synthesis approach. For more information on the AI transpiler and its synthesis strategies, see [\\[2\\]](https://arxiv.org/abs/2405.13196) and [\\[3\\]](https://arxiv.org/abs/2503.14448).\n", - "\n", - "\n", - "\n", - "#### 3. **Rustiq plugin**\n", - "The Rustiq plugin introduces advanced synthesis techniques specifically for `PauliEvolutionGate` operations, which represent Pauli rotations commonly used in Trotterized dynamics. This plugin is valuable for circuits implementing Hamiltonian simulation, such as those used in quantum chemistry and physics problems, where accurate Pauli rotations are essential for simulating problem Hamiltonians effectively. Rustiq offers precise, low-depth circuit synthesis for these specialized operations. For more details about the implementation and performance of Rustiq, please refer to [\\[4\\]](https://arxiv.org/abs/2404.03280).\n", - "\n", - "By exploring these compilation methods in depth, this tutorial provides users with the tools to enhance the performance of their quantum circuits, paving the way for more efficient and practical quantum computations." - ] - }, - { - "cell_type": "markdown", - "id": "53c589f4-c63c-47f3-8642-1f189e445307", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "- Qiskit SDK v1.3 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.28 or later (`pip install qiskit-ibm-runtime`)\n", - "- Qiskit IBM Transpiler (`pip install qiskit-ibm-transpiler`)\n", - "- Qiskit AI Transpiler local mode (`pip install qiskit_ibm_ai_local_transpiler`)\n", - "- Networkx graph library (`pip install networkx`)" - ] - }, - { - "cell_type": "markdown", - "id": "4f79e4a8-48bc-4af8-a172-f0057fa851eb", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "1ecd9900-e511-486e-97be-aac5f75b1917", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit.circuit.library import (\n", - " efficient_su2,\n", - " PauliEvolutionGate,\n", - ")\n", - "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig\n", - "from collections import Counter\n", - "from IPython.display import display\n", - "import time\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import json\n", - "import requests\n", - "import logging\n", - "\n", - "# Suppress noisy loggers\n", - "logging.getLogger(\n", - " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", - ").setLevel(logging.ERROR)\n", - "\n", - "seed = 42 # Seed for reproducibility" - ] - }, - { - "cell_type": "markdown", - "id": "c3a31b4d-d679-4657-b372-ba19bcaf8eca", - "metadata": {}, - "source": [ - "## Part 1: Efficient SU2 Circuit\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "In this section, we explore the `efficient_su2` circuit, a hardware-efficient ansatz commonly used in variational quantum algorithms (such as VQE) and quantum machine-learning tasks. The circuit consists of alternating layers of single-qubit rotations and entangling gates arranged in a circular pattern, designed to explore the quantum state space effectively while maintaining manageable depth.\n", - "\n", - "We will begin by constructing one `efficient_su2` circuit to demonstrate how to compare different compilation methods. After Part 1, we will expand our analysis to a larger set of circuits, enabling a comprehensive benchmark for evaluating the performance of various compilation techniques." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f362cdac-94d8-4cc5-85f4-015c3d9eba3a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qubit_size = list(range(10, 101, 10))\n", - "qc_su2_list = [\n", - " efficient_su2(n, entanglement=\"circular\", reps=1)\n", - " .decompose()\n", - " .copy(name=f\"SU2_{n}\")\n", - " for n in qubit_size\n", - "]\n", - "\n", - "# Draw the first circuit\n", - "qc_su2_list[0].draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "d6671456-9b17-42bb-b94a-d42a29e6fad9", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "This step is the main focus of the tutorial. Here, we aim to optimize quantum circuits for efficient execution on real quantum hardware. Our primary objective is to reduce circuit depth and gate count, which are key factors in improving execution fidelity and mitigating hardware noise.\n", - "\n", - "- **SABRE transpiler**: Uses Qiskit’s default transpiler with the SABRE layout and routing algorithm.\n", - "- **AI transpiler (local mode)**: The standard AI-powered transpiler using local inference and the default synthesis strategy.\n", - "- **Rustiq plugin**: A transpiler plugin designed for low-depth compilation tailored to Hamiltonian simulation tasks.\n", - "\n", - "The goal of this step is to compare the results of these methods in terms of the transpiled circuit’s depth and gate count. Another important metric we consider is the transpilation runtime. By analyzing these metrics, we can evaluate the relative strengths of each method and determine which produces the most efficient circuit for execution on the selected hardware.\n", - "\n", - "Note: For the initial SU2 circuit example, we will only compare the SABRE transpiler to the default AI transpiler. However, in the subsequent benchmark using Hamlib circuits, we will compare all three transpilation methods." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c1ce1ad9-d529-49a1-91df-b540603ceb88", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qiskit_runtime_service._get_crn_from_instance_name:WARNING:2025-07-30 21:46:30,843: Multiple instances found. Using all matching instances.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using backend: \n" - ] - } - ], - "source": [ - "# QiskitRuntimeService.save_account(channel=\"ibm_quantum_platform\", token=\"\", overwrite=True, set_as_default=True)\n", - "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", - "backend = service.backend(\"ibm_torino\")\n", - "print(f\"Using backend: {backend}\")" - ] - }, - { - "cell_type": "markdown", - "id": "381cfaca-103b-41bc-b3b3-f76808f44638", - "metadata": {}, - "source": [ - "Qiskit transpiler with SABRE:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "92a8f12d-3f97-400d-b6b6-a8c717f9ff0f", - "metadata": {}, - "outputs": [], - "source": [ - "pm_sabre = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d1ae6e70-f276-41f3-8e40-68a9523eae06", - "metadata": {}, - "source": [ - "AI transpiler:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "6c27960f-e1cd-4cb5-b807-4dace42ea970", - "metadata": {}, - "outputs": [], - "source": [ - "# Standard AI transpiler pass manager, using the local mode\n", - "pm_ai = generate_ai_pass_manager(\n", - " backend=backend, optimization_level=3, ai_optimization_level=3\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "b20dfb96-bf70-473f-9c91-d99fe3ea3e88", - "metadata": {}, - "source": [ - "Rustiq plugin:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "7c0ae7c4-f4cd-4575-bbc9-b92e9c1d588c", - "metadata": {}, - "outputs": [], - "source": [ - "hls_config = HLSConfig(\n", - " PauliEvolution=[\n", - " (\n", - " \"rustiq\",\n", - " {\n", - " \"nshuffles\": 400,\n", - " \"upto_phase\": True,\n", - " \"fix_clifford\": True,\n", - " \"preserve_order\": False,\n", - " \"metric\": \"depth\",\n", - " },\n", - " )\n", - " ]\n", - ")\n", - "pm_rustiq = generate_preset_pass_manager(\n", - " optimization_level=3,\n", - " backend=backend,\n", - " hls_config=hls_config,\n", - " seed_transpiler=seed,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a16c8082-d0f1-462f-ad10-ebd7d87a0d82", - "metadata": {}, - "source": [ - "#### Transpile and capture metrics\n", - "\n", - "To compare the performance of the compilation methods, we define a function that transpiles the input circuit and captures relevant metrics in a consistent manner. This includes the total circuit depth, overall gate count, and transpilation time.\n", - "\n", - "In addition to these standard metrics, we also record the 2-qubit gate depth, which is a particularly important metric for evaluating execution on quantum hardware. Unlike total depth, which includes all gates, the 2-qubit depth more accurately reflects the circuit's*actual execution duration on hardware. This is because 2-qubit gates typically dominate the time and error budget in most quantum devices. As such, minimizing 2-qubit depth is critical for improving fidelity and reducing decoherence effects during execution.\n", - "\n", - "We will use this function to analyze the performance of the different compilation methods across multiple circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "eb236a9d-238e-4236-a195-e7b8df43b7a2", - "metadata": {}, - "outputs": [], - "source": [ - "def capture_transpilation_metrics(\n", - " results, pass_manager, circuits, method_name\n", - "):\n", - " \"\"\"\n", - " Capture transpilation metrics for a list of circuits and stores the results in a DataFrame.\n", - "\n", - " Args:\n", - " results (pd.DataFrame): DataFrame to store the results.\n", - " pass_manager: Pass manager used for transpilation.\n", - " circuits (list): List of quantum circuits to transpile.\n", - " method_name (str): Name of the transpilation method.\n", - "\n", - " Returns:\n", - " list: List of transpiled circuits.\n", - " \"\"\"\n", - " transpiled_circuits = []\n", - "\n", - " for i, qc in enumerate(circuits):\n", - " # Transpile the circuit\n", - " start_time = time.time()\n", - " transpiled_qc = pass_manager.run(qc)\n", - " end_time = time.time()\n", - "\n", - " # Needed for AI transpiler to be consistent with other methods\n", - " transpiled_qc = transpiled_qc.decompose(gates_to_decompose=[\"swap\"])\n", - "\n", - " # Collect metrics\n", - " transpilation_time = end_time - start_time\n", - " circuit_depth = transpiled_qc.depth(\n", - " lambda x: x.operation.num_qubits == 2\n", - " )\n", - " circuit_size = transpiled_qc.size()\n", - "\n", - " # Append results to DataFrame\n", - " results.loc[len(results)] = {\n", - " \"method\": method_name,\n", - " \"qc_name\": qc.name,\n", - " \"qc_index\": i,\n", - " \"num_qubits\": qc.num_qubits,\n", - " \"ops\": transpiled_qc.count_ops(),\n", - " \"depth\": circuit_depth,\n", - " \"size\": circuit_size,\n", - " \"runtime\": transpilation_time,\n", - " }\n", - " transpiled_circuits.append(transpiled_qc)\n", - " print(\n", - " f\"Transpiled circuit index {i} ({qc.name}) in {transpilation_time:.2f} seconds with method {method_name}, \"\n", - " f\"depth {circuit_depth}, and size {circuit_size}.\"\n", - " )\n", - "\n", - " return transpiled_circuits" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "d3107fc8-3151-488f-8eb8-8d8dc5f4d085", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Transpiled circuit index 0 (SU2_10) in 0.06 seconds with method sabre, depth 13, and size 167.\n", - "Transpiled circuit index 1 (SU2_20) in 0.24 seconds with method sabre, depth 20, and size 299.\n", - "Transpiled circuit index 2 (SU2_30) in 10.72 seconds with method sabre, depth 72, and size 627.\n", - "Transpiled circuit index 3 (SU2_40) in 16.16 seconds with method sabre, depth 40, and size 599.\n", - "Transpiled circuit index 4 (SU2_50) in 76.89 seconds with method sabre, depth 77, and size 855.\n", - "Transpiled circuit index 5 (SU2_60) in 86.12 seconds with method sabre, depth 60, and size 899.\n", - "Transpiled circuit index 6 (SU2_70) in 94.46 seconds with method sabre, depth 79, and size 1085.\n", - "Transpiled circuit index 7 (SU2_80) in 69.05 seconds with method sabre, depth 80, and size 1199.\n", - "Transpiled circuit index 8 (SU2_90) in 88.25 seconds with method sabre, depth 105, and size 1420.\n", - "Transpiled circuit index 9 (SU2_100) in 83.80 seconds with method sabre, depth 100, and size 1499.\n", - "Transpiled circuit index 0 (SU2_10) in 0.17 seconds with method ai, depth 10, and size 168.\n", - "Transpiled circuit index 1 (SU2_20) in 0.29 seconds with method ai, depth 20, and size 299.\n", - "Transpiled circuit index 2 (SU2_30) in 13.56 seconds with method ai, depth 36, and size 548.\n", - "Transpiled circuit index 3 (SU2_40) in 15.95 seconds with method ai, depth 40, and size 599.\n", - "Transpiled circuit index 4 (SU2_50) in 80.70 seconds with method ai, depth 54, and size 823.\n", - "Transpiled circuit index 5 (SU2_60) in 75.99 seconds with method ai, depth 60, and size 899.\n", - "Transpiled circuit index 6 (SU2_70) in 64.96 seconds with method ai, depth 74, and size 1087.\n", - "Transpiled circuit index 7 (SU2_80) in 68.25 seconds with method ai, depth 80, and size 1199.\n", - "Transpiled circuit index 8 (SU2_90) in 75.07 seconds with method ai, depth 90, and size 1404.\n", - "Transpiled circuit index 9 (SU2_100) in 63.97 seconds with method ai, depth 100, and size 1499.\n" - ] - } - ], - "source": [ - "results_su2 = pd.DataFrame(\n", - " columns=[\n", - " \"method\",\n", - " \"qc_name\",\n", - " \"qc_index\",\n", - " \"num_qubits\",\n", - " \"ops\",\n", - " \"depth\",\n", - " \"size\",\n", - " \"runtime\",\n", - " ]\n", - ")\n", - "\n", - "tqc_sabre = capture_transpilation_metrics(\n", - " results_su2, pm_sabre, qc_su2_list, \"sabre\"\n", - ")\n", - "tqc_ai = capture_transpilation_metrics(results_su2, pm_ai, qc_su2_list, \"ai\")" - ] - }, - { - "cell_type": "markdown", - "id": "a2d68a18-6656-4858-b439-7f9acbb516bb", - "metadata": {}, - "source": [ - "Display transpiled results of one of the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "37924fc2-8fb6-451a-b8f9-cd79573f2384", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sabre transpilation\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AI transpilation\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "print(\"Sabre transpilation\")\n", - "display(tqc_sabre[0].draw(\"mpl\", fold=-1, idle_wires=False))\n", - "print(\"AI transpilation\")\n", - "display(tqc_ai[0].draw(\"mpl\", fold=-1, idle_wires=False))" - ] - }, - { - "cell_type": "markdown", - "id": "1633a4f4-7b01-4eb5-9764-0959f209a077", - "metadata": {}, - "source": [ - "Results table:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "a5911224-3d1d-490d-b730-9cb90f954498", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " depth size runtime\n", - "method \n", - "ai 56.4 852.5 45.89\n", - "sabre 64.6 864.9 52.57\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreSU2_10010{'rz': 81, 'sx': 70, 'cz': 16}131670.058845
1sabreSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.238217
2sabreSU2_30230{'sx': 295, 'rz': 242, 'cz': 90}7262710.723922
3sabreSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059916.159262
4sabreSU2_50450{'rz': 402, 'sx': 367, 'cz': 86}7785576.886604
5sabreSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089986.118255
6sabreSU2_70670{'rz': 562, 'sx': 441, 'cz': 82}79108594.458287
7sabreSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119969.048184
8sabreSU2_90890{'rz': 721, 'sx': 585, 'cz': 114}105142088.254809
9sabreSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149983.795482
10aiSU2_10010{'rz': 81, 'sx': 71, 'cz': 16}101680.171532
11aiSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.291691
12aiSU2_30230{'sx': 243, 'rz': 242, 'cz': 63}3654813.555931
13aiSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059915.952733
14aiSU2_50450{'rz': 403, 'sx': 346, 'cz': 74}5482380.702141
15aiSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089975.993404
16aiSU2_70670{'rz': 563, 'sx': 442, 'cz': 82}74108764.960162
17aiSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119968.253280
18aiSU2_90890{'rz': 721, 'sx': 575, 'cz': 108}90140475.072412
19aiSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149963.967446
\n", - "
" - ], - "text/plain": [ - " method qc_name qc_index num_qubits ops \\\n", - "0 sabre SU2_10 0 10 {'rz': 81, 'sx': 70, 'cz': 16} \n", - "1 sabre SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", - "2 sabre SU2_30 2 30 {'sx': 295, 'rz': 242, 'cz': 90} \n", - "3 sabre SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", - "4 sabre SU2_50 4 50 {'rz': 402, 'sx': 367, 'cz': 86} \n", - "5 sabre SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", - "6 sabre SU2_70 6 70 {'rz': 562, 'sx': 441, 'cz': 82} \n", - "7 sabre SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", - "8 sabre SU2_90 8 90 {'rz': 721, 'sx': 585, 'cz': 114} \n", - "9 sabre SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", - "10 ai SU2_10 0 10 {'rz': 81, 'sx': 71, 'cz': 16} \n", - "11 ai SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", - "12 ai SU2_30 2 30 {'sx': 243, 'rz': 242, 'cz': 63} \n", - "13 ai SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", - "14 ai SU2_50 4 50 {'rz': 403, 'sx': 346, 'cz': 74} \n", - "15 ai SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", - "16 ai SU2_70 6 70 {'rz': 563, 'sx': 442, 'cz': 82} \n", - "17 ai SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", - "18 ai SU2_90 8 90 {'rz': 721, 'sx': 575, 'cz': 108} \n", - "19 ai SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", - "\n", - " depth size runtime \n", - "0 13 167 0.058845 \n", - "1 20 299 0.238217 \n", - "2 72 627 10.723922 \n", - "3 40 599 16.159262 \n", - "4 77 855 76.886604 \n", - "5 60 899 86.118255 \n", - "6 79 1085 94.458287 \n", - "7 80 1199 69.048184 \n", - "8 105 1420 88.254809 \n", - "9 100 1499 83.795482 \n", - "10 10 168 0.171532 \n", - "11 20 299 0.291691 \n", - "12 36 548 13.555931 \n", - "13 40 599 15.952733 \n", - "14 54 823 80.702141 \n", - "15 60 899 75.993404 \n", - "16 74 1087 64.960162 \n", - "17 80 1199 68.253280 \n", - "18 90 1404 75.072412 \n", - "19 100 1499 63.967446 " - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "summary_su2 = (\n", - " results_su2.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", - " .mean()\n", - " .round(2)\n", - ")\n", - "print(summary_su2)\n", - "\n", - "results_su2" - ] - }, - { - "cell_type": "markdown", - "id": "167834f1-00a7-4190-99e4-d221d1952357", - "metadata": {}, - "source": [ - "#### Results graph\n", - "\n", - "As we define a function to consistently capture metrics, we will also define one to graph the metrics. Here, we will plot the two-qubit depth, gate count, and runtime for each compilation method across the circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d90fb4fa-e031-40f6-90e6-e1208a855bec", - "metadata": {}, - "outputs": [], - "source": [ - "def plot_transpilation_metrics(results, overall_title, x_axis=\"qc_index\"):\n", - " \"\"\"\n", - " Plots transpilation metrics (depth, size, runtime) for different transpilation methods.\n", - "\n", - " Parameters:\n", - " results (DataFrame): Data containing columns ['num_qubits', 'method', 'depth', 'size', 'runtime']\n", - " overall_title (str): The title of the overall figure.\n", - " x_axis (str): The x-axis label, either 'num_qubits' or 'qc_index'.\n", - " \"\"\"\n", - "\n", - " fig, axs = plt.subplots(1, 3, figsize=(24, 6))\n", - " metrics = [\"depth\", \"size\", \"runtime\"]\n", - " titles = [\"Circuit Depth\", \"Circuit Size\", \"Transpilation Runtime\"]\n", - " y_labels = [\"Depth\", \"Size (Gate Count)\", \"Runtime (s)\"]\n", - "\n", - " methods = results[\"method\"].unique()\n", - " colors = plt.colormaps[\"tab10\"]\n", - " markers = [\"o\", \"^\", \"s\", \"D\", \"P\", \"*\", \"X\", \"v\"]\n", - " color_list = [colors(i % colors.N) for i in range(len(methods))]\n", - " color_map = {method: color_list[i] for i, method in enumerate(methods)}\n", - " marker_map = {\n", - " method: markers[i % len(markers)] for i, method in enumerate(methods)\n", - " }\n", - " jitter_factor = 0.1 # Small x-axis jitter for visibility\n", - " handles, labels = [], [] # Unique handles for legend\n", - "\n", - " # Plot each metric\n", - " for i, metric in enumerate(metrics):\n", - " for method in methods:\n", - " method_data = results[results[\"method\"] == method]\n", - "\n", - " # Introduce slight jitter to avoid exact overlap\n", - " jitter = np.random.uniform(\n", - " -jitter_factor, jitter_factor, len(method_data)\n", - " )\n", - "\n", - " scatter = axs[i].scatter(\n", - " method_data[x_axis] + jitter,\n", - " method_data[metric],\n", - " color=color_map[method],\n", - " label=method,\n", - " marker=marker_map[method],\n", - " alpha=0.7,\n", - " edgecolors=\"black\",\n", - " s=80,\n", - " )\n", - "\n", - " if method not in labels:\n", - " handles.append(scatter)\n", - " labels.append(method)\n", - "\n", - " axs[i].set_title(titles[i])\n", - " axs[i].set_xlabel(x_axis)\n", - " axs[i].set_ylabel(y_labels[i])\n", - " axs[i].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - " axs[i].tick_params(axis=\"x\", rotation=45)\n", - " axs[i].set_xticks(sorted(results[x_axis].unique()))\n", - "\n", - " fig.suptitle(overall_title, fontsize=16)\n", - " fig.legend(\n", - " handles=handles,\n", - " labels=labels,\n", - " loc=\"upper right\",\n", - " bbox_to_anchor=(1.05, 1),\n", - " )\n", - "\n", - " plt.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "7f7b502a-8ed6-45fa-a698-02977149e283", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_transpilation_metrics(\n", - " results_su2, \"Transpilation Metrics for SU2 Circuits\", x_axis=\"num_qubits\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f75e8e3a-803f-4386-a60a-b088faa3c81a", - "metadata": {}, - "source": [ - "#### Analysis of SU2 circuit compilation results\n", - "\n", - "In this experiment, we compare two transpilation methods — Qiskit's SABRE transpiler and the AI-powered transpiler — on a set of `efficient_su2` circuits. Since these circuits do not include any `PauliEvolutionGate` operations, the Rustiq plugin is not included in this comparison.\n", - "\n", - "On average, the AI transpiler performs better in terms of circuit depth, with a greater than 10% improvement across the full range of SU2 circuits. For gate count (circuit size) and transpilation runtime, both methods yield similar results overall.\n", - "\n", - "However, inspecting the individual data points reveals a deeper insight:\n", - "- For most qubit sizes, both SABRE and AI produce nearly identical results, suggesting that in many cases, both methods converge to similarly efficient solutions.\n", - "- For certain circuit sizes, specifically at 30, 50, 70, and 90 qubits, the AI transpiler finds significantly shallower circuits than SABRE. This indicates that AI's learning-based approach is able to discover more optimal layouts or routing paths in cases where the SABRE heuristic does not.\n", - "\n", - "This behavior highlights an important takeaway:\n", - "> While SABRE and AI often produce comparable results, the AI transpiler can occasionally discover much better solutions, particularly in terms of depth, which can lead to significantly improved performance on hardware." - ] - }, - { - "cell_type": "markdown", - "id": "4c3b2aa8-8187-488a-8e5b-197cf26085bb", - "metadata": {}, - "source": [ - "## Part 2: Hamiltonian simulation circuit\n", - "\n", - "### Step 1: Investigate circuits with `PauliEvolutionGate`\n", - "\n", - "In this section, we investigate quantum circuits constructed using `PauliEvolutionGate`, which enables efficient simulation of Hamiltonians. We will analyze how different compilation methods optimize these circuits across various Hamiltonians.\n", - "\n", - "#### Hamiltonians used in the benchmark\n", - "\n", - "The Hamiltonians used in this benchmark describe pairwise interactions between qubits, including terms such as $ZZ$, $XX$, and $YY$. These Hamiltonians are commonly used in quantum chemistry, condensed matter physics, and materials science, where they model systems of interacting particles.\n", - "\n", - "For reference, users can explore a broader set of Hamiltonians in this paper: [Efficient Hamiltonian Simulation on Noisy Quantum Devices](https://arxiv.org/pdf/2306.13126).\n", - "\n", - "#### Benchmark source: Hamlib and Benchpress\n", - "\n", - "The circuits used in this benchmark are drawn from the [Hamlib benchmark repository](https://github.com/SRI-International/QC-App-Oriented-Benchmarks/tree/master/hamlib), which contains realistic Hamiltonian simulation workloads.\n", - "\n", - "These same circuits were previously benchmarked using [Benchpress](https://github.com/Qiskit/benchpress), an open-source framework for evaluating quantum transpilation performance. By using this standardized set of circuits, we can directly compare the effectiveness of different compilation strategies on representative simulation problems.\n", - "\n", - "Hamiltonian simulation is a foundational task in quantum computing, with applications in molecular simulations, optimization problems, and quantum many-body physics. Understanding how different compilation methods optimize these circuits can help users improve practical execution of such circuits on near-term quantum devices." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "66347c00-1607-4405-bb76-610690adf6b8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of Hamiltonian circuits: 35\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Obtain the Hamiltonian JSON from the benchpress repository\n", - "url = \"https://raw.githubusercontent.com/Qiskit/benchpress/e7b29ef7be4cc0d70237b8fdc03edbd698908eff/benchpress/hamiltonian/hamlib/100_representative.json\"\n", - "response = requests.get(url)\n", - "response.raise_for_status() # Raise an error if download failed\n", - "ham_records = json.loads(response.text)\n", - "# Remove circuits that are too large for the backend\n", - "ham_records = [\n", - " h for h in ham_records if h[\"ham_qubits\"] <= backend.num_qubits\n", - "]\n", - "# Remove the circuits that are large to save transpilation time\n", - "ham_records = sorted(ham_records, key=lambda x: x[\"ham_terms\"])[:35]\n", - "\n", - "qc_ham_list = []\n", - "for h in ham_records:\n", - " terms = h[\"ham_hamlib_hamiltonian_terms\"]\n", - " coeff = h[\"ham_hamlib_hamiltonian_coefficients\"]\n", - " num_qubits = h[\"ham_qubits\"]\n", - " name = h[\"ham_problem\"]\n", - "\n", - " evo_gate = PauliEvolutionGate(SparsePauliOp(terms, coeff))\n", - "\n", - " qc_ham = QuantumCircuit(num_qubits)\n", - " qc_ham.name = name\n", - "\n", - " qc_ham.append(evo_gate, range(num_qubits))\n", - " qc_ham_list.append(qc_ham)\n", - "print(f\"Number of Hamiltonian circuits: {len(qc_ham_list)}\")\n", - "\n", - "# Draw the first Hamiltonian circuit\n", - "qc_ham_list[0].draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "9690d94d-7d38-45d3-b37d-85eaf268dbd6", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "As in the previous example, we will use the same backend to ensure consistency in our comparisons. Since the pass managers (`pm_sabre`, `pm_ai`, and `pm_rustiq`) have already been initialized, we can directly proceed with transpiling the Hamiltonian circuits using each method.\n", - "\n", - "This step focuses solely on performing the transpilation and recording the resulting circuit metrics, including depth, gate count, and transpilation runtime. By analyzing these results, we aim to determine the efficiency of each transpilation method for this type of circuit." - ] - }, - { - "cell_type": "markdown", - "id": "22fb1fe7-333f-421f-a17d-bc32190a0f86", - "metadata": {}, - "source": [ - "Transpile and capture metrics:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "4138c7d6-5ec8-4c8f-aef4-08ec1b13633b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method sabre, depth 6, and size 58.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.10 seconds with method sabre, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method sabre, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.03 seconds with method sabre, depth 18, and size 115.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method sabre, depth 24, and size 129.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.05 seconds with method sabre, depth 14, and size 134.\n", - "Transpiled circuit index 6 (all-vib-hno) in 8.39 seconds with method sabre, depth 6, and size 174.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.92 seconds with method sabre, depth 22, and size 220.\n", - "Transpiled circuit index 8 (LiH) in 0.03 seconds with method sabre, depth 67, and size 290.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method sabre, depth 50, and size 340.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.62 seconds with method sabre, depth 30, and size 286.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.04 seconds with method sabre, depth 67, and size 339.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.04 seconds with method sabre, depth 87, and size 421.\n", - "Transpiled circuit index 13 (tfim) in 0.05 seconds with method sabre, depth 36, and size 222.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.51 seconds with method sabre, depth 22, and size 345.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.05 seconds with method sabre, depth 128, and size 704.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.83 seconds with method sabre, depth 2, and size 242.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method sabre, depth 106, and size 609.\n", - "Transpiled circuit index 18 (tfim) in 0.29 seconds with method sabre, depth 73, and size 399.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 21.97 seconds with method sabre, depth 30, and size 572.\n", - "Transpiled circuit index 20 (Be2) in 0.09 seconds with method sabre, depth 324, and size 1555.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.12 seconds with method sabre, depth 250, and size 1394.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.07 seconds with method sabre, depth 215, and size 1027.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 41.22 seconds with method sabre, depth 30, and size 1144.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 1.89 seconds with method sabre, depth 175, and size 1933.\n", - "Transpiled circuit index 25 (H2) in 0.32 seconds with method sabre, depth 1237, and size 5502.\n", - "Transpiled circuit index 26 (uuf100-ham) in 0.20 seconds with method sabre, depth 385, and size 4303.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.20 seconds with method sabre, depth 311, and size 3654.\n", - "Transpiled circuit index 28 (tfim) in 0.15 seconds with method sabre, depth 276, and size 3213.\n", - "Transpiled circuit index 29 (uuf100-ham) in 0.21 seconds with method sabre, depth 520, and size 5250.\n", - "Transpiled circuit index 30 (flat100-ham) in 0.15 seconds with method sabre, depth 131, and size 3157.\n", - "Transpiled circuit index 31 (uf100-ham) in 0.24 seconds with method sabre, depth 624, and size 7378.\n", - "Transpiled circuit index 32 (OH) in 0.88 seconds with method sabre, depth 2175, and size 9808.\n", - "Transpiled circuit index 33 (HF) in 0.66 seconds with method sabre, depth 2206, and size 9417.\n", - "Transpiled circuit index 34 (BH) in 0.89 seconds with method sabre, depth 2177, and size 9802.\n", - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method ai, depth 6, and size 58.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method ai, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method ai, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.11 seconds with method ai, depth 18, and size 94.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.11 seconds with method ai, depth 22, and size 129.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.06 seconds with method ai, depth 22, and size 177.\n", - "Transpiled circuit index 6 (all-vib-hno) in 8.62 seconds with method ai, depth 10, and size 198.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.71 seconds with method ai, depth 18, and size 195.\n", - "Transpiled circuit index 8 (LiH) in 0.19 seconds with method ai, depth 62, and size 267.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.22 seconds with method ai, depth 47, and size 321.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.71 seconds with method ai, depth 38, and size 369.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.24 seconds with method ai, depth 65, and size 315.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.24 seconds with method ai, depth 91, and size 430.\n", - "Transpiled circuit index 13 (tfim) in 0.15 seconds with method ai, depth 12, and size 251.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 8.50 seconds with method ai, depth 18, and size 311.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.25 seconds with method ai, depth 117, and size 659.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 16.11 seconds with method ai, depth 2, and size 242.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.39 seconds with method ai, depth 98, and size 564.\n", - "Transpiled circuit index 18 (tfim) in 0.38 seconds with method ai, depth 23, and size 437.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 24.97 seconds with method ai, depth 38, and size 707.\n", - "Transpiled circuit index 20 (Be2) in 1.07 seconds with method ai, depth 293, and size 1392.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.61 seconds with method ai, depth 229, and size 1437.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.57 seconds with method ai, depth 178, and size 964.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 50.89 seconds with method ai, depth 34, and size 1425.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 1.61 seconds with method ai, depth 171, and size 2020.\n", - "Transpiled circuit index 25 (H2) in 6.39 seconds with method ai, depth 1148, and size 5208.\n", - "Transpiled circuit index 26 (uuf100-ham) in 3.97 seconds with method ai, depth 376, and size 5048.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 3.54 seconds with method ai, depth 357, and size 4451.\n", - "Transpiled circuit index 28 (tfim) in 1.72 seconds with method ai, depth 216, and size 3026.\n", - "Transpiled circuit index 29 (uuf100-ham) in 4.45 seconds with method ai, depth 426, and size 5399.\n", - "Transpiled circuit index 30 (flat100-ham) in 7.02 seconds with method ai, depth 86, and size 3108.\n", - "Transpiled circuit index 31 (uf100-ham) in 12.85 seconds with method ai, depth 623, and size 8354.\n", - "Transpiled circuit index 32 (OH) in 15.19 seconds with method ai, depth 2084, and size 9543.\n", - "Transpiled circuit index 33 (HF) in 17.51 seconds with method ai, depth 2063, and size 9446.\n", - "Transpiled circuit index 34 (BH) in 15.33 seconds with method ai, depth 2094, and size 9730.\n", - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method rustiq, depth 13, and size 83.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method rustiq, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method rustiq, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.01 seconds with method rustiq, depth 13, and size 79.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method rustiq, depth 31, and size 131.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.04 seconds with method rustiq, depth 50, and size 306.\n", - "Transpiled circuit index 6 (all-vib-hno) in 14.03 seconds with method rustiq, depth 22, and size 276.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.15 seconds with method rustiq, depth 13, and size 155.\n", - "Transpiled circuit index 8 (LiH) in 0.03 seconds with method rustiq, depth 54, and size 270.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method rustiq, depth 65, and size 398.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.16 seconds with method rustiq, depth 41, and size 516.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.02 seconds with method rustiq, depth 34, and size 189.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.03 seconds with method rustiq, depth 49, and size 240.\n", - "Transpiled circuit index 13 (tfim) in 0.05 seconds with method rustiq, depth 20, and size 366.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.08 seconds with method rustiq, depth 16, and size 277.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.04 seconds with method rustiq, depth 116, and size 612.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.89 seconds with method rustiq, depth 2, and size 257.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method rustiq, depth 133, and size 737.\n", - "Transpiled circuit index 18 (tfim) in 0.11 seconds with method rustiq, depth 25, and size 680.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 27.19 seconds with method rustiq, depth 66, and size 983.\n", - "Transpiled circuit index 20 (Be2) in 0.07 seconds with method rustiq, depth 215, and size 1030.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.14 seconds with method rustiq, depth 328, and size 1918.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.05 seconds with method rustiq, depth 114, and size 692.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 62.25 seconds with method rustiq, depth 74, and size 2348.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 0.20 seconds with method rustiq, depth 436, and size 3605.\n", - "Transpiled circuit index 25 (H2) in 0.21 seconds with method rustiq, depth 643, and size 3476.\n", - "Transpiled circuit index 26 (uuf100-ham) in 0.24 seconds with method rustiq, depth 678, and size 6120.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.22 seconds with method rustiq, depth 588, and size 5241.\n", - "Transpiled circuit index 28 (tfim) in 0.34 seconds with method rustiq, depth 340, and size 5901.\n", - "Transpiled circuit index 29 (uuf100-ham) in 0.33 seconds with method rustiq, depth 881, and size 7667.\n", - "Transpiled circuit index 30 (flat100-ham) in 0.31 seconds with method rustiq, depth 279, and size 4910.\n", - "Transpiled circuit index 31 (uf100-ham) in 0.38 seconds with method rustiq, depth 1138, and size 10607.\n", - "Transpiled circuit index 32 (OH) in 0.38 seconds with method rustiq, depth 1148, and size 6512.\n", - "Transpiled circuit index 33 (HF) in 0.37 seconds with method rustiq, depth 1090, and size 6256.\n", - "Transpiled circuit index 34 (BH) in 0.37 seconds with method rustiq, depth 1148, and size 6501.\n" - ] - } - ], - "source": [ - "results_ham = pd.DataFrame(\n", - " columns=[\n", - " \"method\",\n", - " \"qc_name\",\n", - " \"qc_index\",\n", - " \"num_qubits\",\n", - " \"ops\",\n", - " \"depth\",\n", - " \"size\",\n", - " \"runtime\",\n", - " ]\n", - ")\n", - "\n", - "tqc_sabre = capture_transpilation_metrics(\n", - " results_ham, pm_sabre, qc_ham_list, \"sabre\"\n", - ")\n", - "tqc_ai = capture_transpilation_metrics(results_ham, pm_ai, qc_ham_list, \"ai\")\n", - "tqc_rustiq = capture_transpilation_metrics(\n", - " results_ham, pm_rustiq, qc_ham_list, \"rustiq\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "ef88a127-79b8-4a53-90ef-6caa84c29e06", - "metadata": {}, - "source": [ - "Results table (skipping visualization as the output circuits are very large):" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "bc4a7910-5e8a-48ec-bd7e-2508ae8dfccc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " depth size runtime\n", - "method \n", - "ai 316.86 2181.26 5.97\n", - "rustiq 281.94 2268.80 3.86\n", - "sabre 337.97 2120.14 3.07\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreall-vib-o304{'rz': 28, 'sx': 24, 'cz': 6}6580.016597
1sabreall-vib-c2h14{'rz': 17, 'sx': 16, 'cz': 4, 'x': 2}2391.102089
2sabreall-vib-bh22{'sx': 14, 'rz': 13, 'cz': 3}3300.011042
3sabreall-vib-c2h33{'sx': 46, 'rz': 45, 'cz': 18, 'x': 6}181150.025816
4sabregraph-gnp_k-244{'sx': 49, 'rz': 47, 'cz': 24, 'x': 9}241290.023077
...........................
100rustiqflat100-ham3090{'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5}27949100.309448
101rustiquf100-ham3146{'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4}1138106070.380977
102rustiqOH3210{'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23}114865120.383564
103rustiqHF3310{'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17}109062560.368578
104rustiqBH3410{'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19}114865010.374822
\n", - "

105 rows × 8 columns

\n", - "
" - ], - "text/plain": [ - " method qc_name qc_index num_qubits \\\n", - "0 sabre all-vib-o3 0 4 \n", - "1 sabre all-vib-c2h 1 4 \n", - "2 sabre all-vib-bh 2 2 \n", - "3 sabre all-vib-c2h 3 3 \n", - "4 sabre graph-gnp_k-2 4 4 \n", - ".. ... ... ... ... \n", - "100 rustiq flat100-ham 30 90 \n", - "101 rustiq uf100-ham 31 46 \n", - "102 rustiq OH 32 10 \n", - "103 rustiq HF 33 10 \n", - "104 rustiq BH 34 10 \n", - "\n", - " ops depth size runtime \n", - "0 {'rz': 28, 'sx': 24, 'cz': 6} 6 58 0.016597 \n", - "1 {'rz': 17, 'sx': 16, 'cz': 4, 'x': 2} 2 39 1.102089 \n", - "2 {'sx': 14, 'rz': 13, 'cz': 3} 3 30 0.011042 \n", - "3 {'sx': 46, 'rz': 45, 'cz': 18, 'x': 6} 18 115 0.025816 \n", - "4 {'sx': 49, 'rz': 47, 'cz': 24, 'x': 9} 24 129 0.023077 \n", - ".. ... ... ... ... \n", - "100 {'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5} 279 4910 0.309448 \n", - "101 {'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4} 1138 10607 0.380977 \n", - "102 {'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23} 1148 6512 0.383564 \n", - "103 {'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17} 1090 6256 0.368578 \n", - "104 {'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19} 1148 6501 0.374822 \n", - "\n", - "[105 rows x 8 columns]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "summary_ham = (\n", - " results_ham.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", - " .mean()\n", - " .round(2)\n", - ")\n", - "print(summary_ham)\n", - "\n", - "results_ham" - ] - }, - { - "cell_type": "markdown", - "id": "17e7b228-01b0-4fcc-b659-9f9958f5477e", - "metadata": {}, - "source": [ - "Visualize performance based on circuit index:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "4c6b810d-a4cf-4de3-aae6-c6c1cdd83d8f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_transpilation_metrics(\n", - " results_ham, \"Transpilation Metrics for Hamiltonian Circuits\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "7ad6f4ee-c9d1-4e8b-ae25-cf39672acbec", - "metadata": {}, - "source": [ - "Visualize the percentage of circuits for which each method performed best." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "01b4644e-ac91-483c-944b-924f8b41718d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best-performing methods based on depth:\n", - " ai: 16 circuit(s)\n", - " rustiq: 16 circuit(s)\n", - " sabre: 10 circuit(s)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best-performing methods based on size:\n", - " sabre: 18 circuit(s)\n", - " rustiq: 14 circuit(s)\n", - " ai: 10 circuit(s)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def analyze_and_plot_best_methods(results, metric):\n", - " \"\"\"\n", - " Analyze the best-performing methods for a given metric and plot a pie chart.\n", - "\n", - " Parameters:\n", - " results (DataFrame): The input DataFrame containing method performance data.\n", - " metric (str): The metric to evaluate (\"depth\" or \"size\").\n", - " \"\"\"\n", - " method_counts = Counter()\n", - " for qc_idx, group in results.groupby(\"qc_index\"):\n", - " min_value = group[metric].min()\n", - "\n", - " # Find all methods that achieved this minimum value\n", - " best_methods = group[group[metric] == min_value][\"method\"]\n", - " # Update counts for all best methods (handling ties)\n", - " method_counts.update(best_methods)\n", - " best_method_counts = dict(\n", - " sorted(method_counts.items(), key=lambda x: x[1], reverse=True)\n", - " )\n", - "\n", - " # Print summary\n", - " print(f\"Best-performing methods based on {metric}:\")\n", - " for method, count in best_method_counts.items():\n", - " print(f\" {method}: {count} circuit(s)\")\n", - "\n", - " # Plot pie chart\n", - " num_methods = len(best_method_counts)\n", - " colors = plt.cm.viridis_r(range(0, 256, 256 // num_methods))\n", - " plt.figure(figsize=(5, 5))\n", - " plt.pie(\n", - " best_method_counts.values(),\n", - " labels=best_method_counts.keys(),\n", - " autopct=\"%1.1f%%\",\n", - " startangle=140,\n", - " wedgeprops={\"edgecolor\": \"black\"},\n", - " textprops={\"fontsize\": 10},\n", - " colors=colors,\n", - " )\n", - " plt.title(\n", - " f\"Percentage of Circuits Method Performed Best for {metric.capitalize()}\",\n", - " fontsize=12,\n", - " fontweight=\"bold\",\n", - " )\n", - " plt.show()\n", - "\n", - "\n", - "analyze_and_plot_best_methods(results_ham, \"depth\")\n", - "analyze_and_plot_best_methods(results_ham, \"size\")" - ] - }, - { - "cell_type": "markdown", - "id": "e4f6aa23-d528-4d66-92e0-31d29c522792", - "metadata": {}, - "source": [ - "#### Analysis of Hamiltonian circuit compilation results\n", - "\n", - "In this section, we evaluate the performance of three transpilation methods — SABRE, the AI-powered transpiler, and Rustiq — on quantum circuits constructed with `PauliEvolutionGate`, which are commonly used in Hamiltonian simulation tasks.\n", - "\n", - "Rustiq performed best on average in terms of circuit depth**, achieving approximately 20% lower depth than SABRE. This is expected, as Rustiq is specifically designed to synthesize `PauliEvolutionGate` operations with optimized, low-depth decomposition strategies. Furthermore, the depth plot shows that as the circuits scale in size and complexity, Rustiq scales most effectively, maintaining significantly lower depth than both AI and SABRE on larger circuits.\n", - "\n", - "AI transpiler showed strong and consistent performance for circuit depth, consistently outperforming SABRE across most circuits. However, it incurred the highest runtime, especially on larger circuits, which may limit its practicality in time-sensitive workloads. Its scalability in runtime remains a key limitation, even though it offers solid improvements in depth.\n", - "\n", - "SABRE, while producing the highest average depth, achieved the lowest average gate count, closely followed by the AI transpiler. This aligns with the design of SABRE’s heuristic, which prioritizes minimizing gate count directly. Rustiq, despite its strength in lowering depth, had the highest average gate count, which is a notable trade-off to consider in applications where circuit size matters more than circuit duration.\n", - "\n", - "### Summary\n", - "\n", - "While the AI transpiler generally delivers better results than SABRE, particularly in circuit depth, the takeaway should not simply be \"always use the AI transpiler.\" There are important nuances to consider:\n", - "\n", - "- **AI transpiler** is typically reliable and provides depth-optimized circuits, but it comes with trade-offs in runtime, and also has other limitations, including supported coupling maps and synthesis capabilities. These are detailed in the [Qiskit Transpiler Service documentation](/docs/guides/qiskit-transpiler-service).\n", - "\n", - "- In some cases, particularly with very large or hardware-specific circuits, the AI transpiler may not be as effective. In these cases, the default SABRE transpiler remains extremely reliable and can be further optimized by adjusting its parameters (see the [SABRE optimization tutorial](/docs/tutorials/transpilation-optimizations-with-sabre)).\n", - "\n", - "- It's also important to consider circuit structure when choosing a method. For example, `rustiq` is purpose-built for circuits involving `PauliEvolutionGate` and often yields the best performance for Hamiltonian simulation problems.\n", - "\n", - "**Recommendation:**\n", - "> There is no one-size-fits-all transpilation strategy. Users are encouraged to understand the structure of their circuit and test multiple transpilation methods — including AI, SABRE, and specialized tools like Rustiq — to find the most efficient solution for their specific problem and hardware constraints." - ] - }, - { - "cell_type": "markdown", - "id": "9a747477-1dc1-4706-b844-d29570fb5844", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "markdown", - "id": "d3d6d473-7508-4e28-82ad-eee09c9a1acf", - "metadata": {}, - "source": [ - "As this tutorial focuses on transpilation, no experiments are executed on a quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth and gate count." - ] - }, - { - "cell_type": "markdown", - "id": "bb4c4c67-a7e8-4ca1-a5d6-908a2e1d27e5", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format" - ] - }, - { - "cell_type": "markdown", - "id": "4aa45929-f8d9-4be8-9008-0588615fa45e", - "metadata": {}, - "source": [ - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "552ef44b-e567-4870-9699-d152806b9505", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "[1] \"LightSABRE: A Lightweight and Enhanced SABRE Algorithm\". H. Zou, M. Treinish, K. Hartman, A. Ivrii, J. Lishman et al. https://arxiv.org/abs/2409.08368\n", - "\n", - "[2] \"Practical and efficient quantum circuit synthesis and transpiling with Reinforcement Learning\". D. Kremer, V. Villar, H. Paik, I. Duran, I. Faro, J. Cruz-Benito et al. https://arxiv.org/abs/2405.13196\n", - "\n", - "[3] \"Pauli Network Circuit Synthesis with Reinforcement Learning\". A. Dubal, D. Kremer, S. Martiel, V. Villar, D. Wang, J. Cruz-Benito et al. https://arxiv.org/abs/2503.14448\n", - "\n", - "[4] \"Faster and shorter synthesis of Hamiltonian simulation circuits\". T. Goubault de Brugière, S. Martiel et al. https://arxiv.org/abs/2404.03280" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f056ca4a-fbe4-4051-bbcd-79c2d7848cd0", + "metadata": {}, + "source": [ + "---\n", + "title: Compilation methods for Hamiltonian simulation circuits\n", + "description: This tutorial provides a comparative overview of three compilation methods in Qiskit for Hamiltonian simulation workloads.\n", + "---\n", + "\n", + "\n", + "# Compilation methods for Hamiltonian simulation circuits\n", + "Estimated QPU usage: no execution was done in this tutorial because it is focused on the transpilation process.\n", + "\n", + "{/* cspell:ignore Rustiq, nshuffles, edgecolors, edgecolor, Hamlib, Benchpress, Brugière, Goubault, Martiel, Dubal, Lishman, Ivrii, fontweight, fontsize, textprops, wedgeprops, startangle, autopct */}" + ] + }, + { + "cell_type": "markdown", + "id": "d960a90b-1487-4310-a222-b95b8a77a080", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "Quantum circuit compilation is a crucial step in the quantum computing workflow. It involves transforming a high-level quantum algorithm into a physical quantum circuit that adheres to the constraints of the target quantum hardware. Effective compilation can significantly impact the performance of quantum algorithms by reducing circuit depth, gate count, and execution time. This tutorial explores three distinct approaches to quantum circuit compilation in Qiskit, showcasing their strengths and applications through practical examples.\n", + "\n", + "The goal of this tutorial is to teach users how to apply and evaluate three compilation methods in Qiskit: the SABRE transpiler, the AI-powered transpiler, and the Rustiq plugin. Users will learn how to use each method effectively and how to benchmark their performance across different quantum circuits. By the end of this tutorial, users will be able to choose and tailor compilation strategies based on specific optimization goals such as reducing circuit depth, minimizing gate count, or improving runtime.\n", + "\n", + "### What you will learn\n", + "- **How to use the Qiskit transpiler with SABRE for layout and routing optimization.**\n", + "- **How to leverage the AI transpiler for advanced, automated circuit optimization.**\n", + "- **How to employ the Rustiq plugin for circuits requiring precise synthesis of operations, particularly in Hamiltonian simulation tasks.**\n", + "\n", + "This tutorial uses three example circuits following the [Qiskit patterns](/docs/guides/intro-to-patterns) workflow to illustrate the performance of each compilation method. By the end of this tutorial, users will be equipped to choose the appropriate compilation strategy based on their specific requirements and constraints.\n", + "\n", + "### Compilation methods overview\n", + "\n", + "#### 1. **Qiskit transpiler with SABRE**\n", + "The Qiskit transpiler uses the SABRE (SWAP-based BidiREctional heuristic search) algorithm to optimize circuit layout and routing. SABRE focuses on minimizing SWAP gates and their impact on circuit depth while adhering to hardware connectivity constraints. This method is highly versatile and suitable for general-purpose circuit optimization, providing a balance between performance and computation time. To take advantage of the latest improvements in SABRE, detailed in [\\[1\\]](https://arxiv.org/abs/2409.08368), you can increase the number of trials (for example, `layout_trials=400, swap_trials=400`). For the purposes of this tutorial, we will use the default values for the number of trials in order to compare to Qiskit's default transpiler. The advantages and parameter exploration of SABRE are covered in a separate [deep-dive tutorial](/docs/tutorials/transpilation-optimizations-with-sabre).\n", + "\n", + "#### 2. **AI transpiler**\n", + "\n", + "The AI-powered transpiler in Qiskit uses machine learning to predict optimal transpilation strategies by analyzing patterns in circuit structure and hardware constraints to select the best sequence of optimizations for a given input. This method is particularly effective for large-scale quantum circuits, offering a high degree of automation and adaptability to diverse problem types. In addition to general circuit optimization, the AI transpiler can be used with the `AIPauliNetworkSynthesis` pass, which targets Pauli network circuits — blocks composed of H, S, SX, CX, RX, RY, and RZ gates — and applies a reinforcement learning-based synthesis approach. For more information on the AI transpiler and its synthesis strategies, see [\\[2\\]](https://arxiv.org/abs/2405.13196) and [\\[3\\]](https://arxiv.org/abs/2503.14448).\n", + "\n", + "\n", + "\n", + "#### 3. **Rustiq plugin**\n", + "The Rustiq plugin introduces advanced synthesis techniques specifically for `PauliEvolutionGate` operations, which represent Pauli rotations commonly used in Trotterized dynamics. This plugin is valuable for circuits implementing Hamiltonian simulation, such as those used in quantum chemistry and physics problems, where accurate Pauli rotations are essential for simulating problem Hamiltonians effectively. Rustiq offers precise, low-depth circuit synthesis for these specialized operations. For more details about the implementation and performance of Rustiq, please refer to [\\[4\\]](https://arxiv.org/abs/2404.03280).\n", + "\n", + "By exploring these compilation methods in depth, this tutorial provides users with the tools to enhance the performance of their quantum circuits, paving the way for more efficient and practical quantum computations." + ] + }, + { + "cell_type": "markdown", + "id": "53c589f4-c63c-47f3-8642-1f189e445307", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "- Qiskit SDK v1.3 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.28 or later (`pip install qiskit-ibm-runtime`)\n", + "- Qiskit IBM Transpiler (`pip install qiskit-ibm-transpiler`)\n", + "- Qiskit AI Transpiler local mode (`pip install qiskit_ibm_ai_local_transpiler`)\n", + "- Networkx graph library (`pip install networkx`)" + ] + }, + { + "cell_type": "markdown", + "id": "4f79e4a8-48bc-4af8-a172-f0057fa851eb", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1ecd9900-e511-486e-97be-aac5f75b1917", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit.circuit.library import (\n", + " efficient_su2,\n", + " PauliEvolutionGate,\n", + ")\n", + "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig\n", + "from collections import Counter\n", + "from IPython.display import display\n", + "import time\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import json\n", + "import requests\n", + "import logging\n", + "\n", + "# Suppress noisy loggers\n", + "logging.getLogger(\n", + " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", + ").setLevel(logging.ERROR)\n", + "\n", + "seed = 42 # Seed for reproducibility" + ] + }, + { + "cell_type": "markdown", + "id": "c3a31b4d-d679-4657-b372-ba19bcaf8eca", + "metadata": {}, + "source": [ + "## Part 1: Efficient SU2 Circuit\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "In this section, we explore the `efficient_su2` circuit, a hardware-efficient ansatz commonly used in variational quantum algorithms (such as VQE) and quantum machine-learning tasks. The circuit consists of alternating layers of single-qubit rotations and entangling gates arranged in a circular pattern, designed to explore the quantum state space effectively while maintaining manageable depth.\n", + "\n", + "We will begin by constructing one `efficient_su2` circuit to demonstrate how to compare different compilation methods. After Part 1, we will expand our analysis to a larger set of circuits, enabling a comprehensive benchmark for evaluating the performance of various compilation techniques." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f362cdac-94d8-4cc5-85f4-015c3d9eba3a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qubit_size = list(range(10, 101, 10))\n", + "qc_su2_list = [\n", + " efficient_su2(n, entanglement=\"circular\", reps=1)\n", + " .decompose()\n", + " .copy(name=f\"SU2_{n}\")\n", + " for n in qubit_size\n", + "]\n", + "\n", + "# Draw the first circuit\n", + "qc_su2_list[0].draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "d6671456-9b17-42bb-b94a-d42a29e6fad9", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "This step is the main focus of the tutorial. Here, we aim to optimize quantum circuits for efficient execution on real quantum hardware. Our primary objective is to reduce circuit depth and gate count, which are key factors in improving execution fidelity and mitigating hardware noise.\n", + "\n", + "- **SABRE transpiler**: Uses Qiskit’s default transpiler with the SABRE layout and routing algorithm.\n", + "- **AI transpiler (local mode)**: The standard AI-powered transpiler using local inference and the default synthesis strategy.\n", + "- **Rustiq plugin**: A transpiler plugin designed for low-depth compilation tailored to Hamiltonian simulation tasks.\n", + "\n", + "The goal of this step is to compare the results of these methods in terms of the transpiled circuit’s depth and gate count. Another important metric we consider is the transpilation runtime. By analyzing these metrics, we can evaluate the relative strengths of each method and determine which produces the most efficient circuit for execution on the selected hardware.\n", + "\n", + "Note: For the initial SU2 circuit example, we will only compare the SABRE transpiler to the default AI transpiler. However, in the subsequent benchmark using Hamlib circuits, we will compare all three transpilation methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1ce1ad9-d529-49a1-91df-b540603ceb88", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "qiskit_runtime_service._get_crn_from_instance_name:WARNING:2025-07-30 21:46:30,843: Multiple instances found. Using all matching instances.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using backend: \n" + ] + } + ], + "source": [ + "# QiskitRuntimeService.save_account(channel=\"ibm_quantum_platform\", token=\"\",\n", + "# overwrite=True, set_as_default=True)\n", + "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", + "backend = service.backend(\"ibm_torino\")\n", + "print(f\"Using backend: {backend}\")" + ] + }, + { + "cell_type": "markdown", + "id": "381cfaca-103b-41bc-b3b3-f76808f44638", + "metadata": {}, + "source": [ + "Qiskit transpiler with SABRE:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "92a8f12d-3f97-400d-b6b6-a8c717f9ff0f", + "metadata": {}, + "outputs": [], + "source": [ + "pm_sabre = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=seed\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d1ae6e70-f276-41f3-8e40-68a9523eae06", + "metadata": {}, + "source": [ + "AI transpiler:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6c27960f-e1cd-4cb5-b807-4dace42ea970", + "metadata": {}, + "outputs": [], + "source": [ + "# Standard AI transpiler pass manager, using the local mode\n", + "pm_ai = generate_ai_pass_manager(\n", + " backend=backend, optimization_level=3, ai_optimization_level=3\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b20dfb96-bf70-473f-9c91-d99fe3ea3e88", + "metadata": {}, + "source": [ + "Rustiq plugin:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7c0ae7c4-f4cd-4575-bbc9-b92e9c1d588c", + "metadata": {}, + "outputs": [], + "source": [ + "hls_config = HLSConfig(\n", + " PauliEvolution=[\n", + " (\n", + " \"rustiq\",\n", + " {\n", + " \"nshuffles\": 400,\n", + " \"upto_phase\": True,\n", + " \"fix_clifford\": True,\n", + " \"preserve_order\": False,\n", + " \"metric\": \"depth\",\n", + " },\n", + " )\n", + " ]\n", + ")\n", + "pm_rustiq = generate_preset_pass_manager(\n", + " optimization_level=3,\n", + " backend=backend,\n", + " hls_config=hls_config,\n", + " seed_transpiler=seed,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a16c8082-d0f1-462f-ad10-ebd7d87a0d82", + "metadata": {}, + "source": [ + "#### Transpile and capture metrics\n", + "\n", + "To compare the performance of the compilation methods, we define a function that transpiles the input circuit and captures relevant metrics in a consistent manner. This includes the total circuit depth, overall gate count, and transpilation time.\n", + "\n", + "In addition to these standard metrics, we also record the 2-qubit gate depth, which is a particularly important metric for evaluating execution on quantum hardware. Unlike total depth, which includes all gates, the 2-qubit depth more accurately reflects the circuit's*actual execution duration on hardware. This is because 2-qubit gates typically dominate the time and error budget in most quantum devices. As such, minimizing 2-qubit depth is critical for improving fidelity and reducing decoherence effects during execution.\n", + "\n", + "We will use this function to analyze the performance of the different compilation methods across multiple circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "eb236a9d-238e-4236-a195-e7b8df43b7a2", + "metadata": {}, + "outputs": [], + "source": [ + "def capture_transpilation_metrics(\n", + " results, pass_manager, circuits, method_name\n", + "):\n", + " \"\"\"\n", + " Capture transpilation metrics for a list of circuits and stores the results in a DataFrame.\n", + "\n", + " Args:\n", + " results (pd.DataFrame): DataFrame to store the results.\n", + " pass_manager: Pass manager used for transpilation.\n", + " circuits (list): List of quantum circuits to transpile.\n", + " method_name (str): Name of the transpilation method.\n", + "\n", + " Returns:\n", + " list: List of transpiled circuits.\n", + " \"\"\"\n", + " transpiled_circuits = []\n", + "\n", + " for i, qc in enumerate(circuits):\n", + " # Transpile the circuit\n", + " start_time = time.time()\n", + " transpiled_qc = pass_manager.run(qc)\n", + " end_time = time.time()\n", + "\n", + " # Needed for AI transpiler to be consistent with other methods\n", + " transpiled_qc = transpiled_qc.decompose(gates_to_decompose=[\"swap\"])\n", + "\n", + " # Collect metrics\n", + " transpilation_time = end_time - start_time\n", + " circuit_depth = transpiled_qc.depth(\n", + " lambda x: x.operation.num_qubits == 2\n", + " )\n", + " circuit_size = transpiled_qc.size()\n", + "\n", + " # Append results to DataFrame\n", + " results.loc[len(results)] = {\n", + " \"method\": method_name,\n", + " \"qc_name\": qc.name,\n", + " \"qc_index\": i,\n", + " \"num_qubits\": qc.num_qubits,\n", + " \"ops\": transpiled_qc.count_ops(),\n", + " \"depth\": circuit_depth,\n", + " \"size\": circuit_size,\n", + " \"runtime\": transpilation_time,\n", + " }\n", + " transpiled_circuits.append(transpiled_qc)\n", + " print(\n", + " f\"Transpiled circuit index {i} ({qc.name}) in {transpilation_time:.2f} seconds with method {method_name}, \"\n", + " f\"depth {circuit_depth}, and size {circuit_size}.\"\n", + " )\n", + "\n", + " return transpiled_circuits" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d3107fc8-3151-488f-8eb8-8d8dc5f4d085", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transpiled circuit index 0 (SU2_10) in 0.06 seconds with method sabre, depth 13, and size 167.\n", + "Transpiled circuit index 1 (SU2_20) in 0.24 seconds with method sabre, depth 20, and size 299.\n", + "Transpiled circuit index 2 (SU2_30) in 10.72 seconds with method sabre, depth 72, and size 627.\n", + "Transpiled circuit index 3 (SU2_40) in 16.16 seconds with method sabre, depth 40, and size 599.\n", + "Transpiled circuit index 4 (SU2_50) in 76.89 seconds with method sabre, depth 77, and size 855.\n", + "Transpiled circuit index 5 (SU2_60) in 86.12 seconds with method sabre, depth 60, and size 899.\n", + "Transpiled circuit index 6 (SU2_70) in 94.46 seconds with method sabre, depth 79, and size 1085.\n", + "Transpiled circuit index 7 (SU2_80) in 69.05 seconds with method sabre, depth 80, and size 1199.\n", + "Transpiled circuit index 8 (SU2_90) in 88.25 seconds with method sabre, depth 105, and size 1420.\n", + "Transpiled circuit index 9 (SU2_100) in 83.80 seconds with method sabre, depth 100, and size 1499.\n", + "Transpiled circuit index 0 (SU2_10) in 0.17 seconds with method ai, depth 10, and size 168.\n", + "Transpiled circuit index 1 (SU2_20) in 0.29 seconds with method ai, depth 20, and size 299.\n", + "Transpiled circuit index 2 (SU2_30) in 13.56 seconds with method ai, depth 36, and size 548.\n", + "Transpiled circuit index 3 (SU2_40) in 15.95 seconds with method ai, depth 40, and size 599.\n", + "Transpiled circuit index 4 (SU2_50) in 80.70 seconds with method ai, depth 54, and size 823.\n", + "Transpiled circuit index 5 (SU2_60) in 75.99 seconds with method ai, depth 60, and size 899.\n", + "Transpiled circuit index 6 (SU2_70) in 64.96 seconds with method ai, depth 74, and size 1087.\n", + "Transpiled circuit index 7 (SU2_80) in 68.25 seconds with method ai, depth 80, and size 1199.\n", + "Transpiled circuit index 8 (SU2_90) in 75.07 seconds with method ai, depth 90, and size 1404.\n", + "Transpiled circuit index 9 (SU2_100) in 63.97 seconds with method ai, depth 100, and size 1499.\n" + ] + } + ], + "source": [ + "results_su2 = pd.DataFrame(\n", + " columns=[\n", + " \"method\",\n", + " \"qc_name\",\n", + " \"qc_index\",\n", + " \"num_qubits\",\n", + " \"ops\",\n", + " \"depth\",\n", + " \"size\",\n", + " \"runtime\",\n", + " ]\n", + ")\n", + "\n", + "tqc_sabre = capture_transpilation_metrics(\n", + " results_su2, pm_sabre, qc_su2_list, \"sabre\"\n", + ")\n", + "tqc_ai = capture_transpilation_metrics(results_su2, pm_ai, qc_su2_list, \"ai\")" + ] + }, + { + "cell_type": "markdown", + "id": "a2d68a18-6656-4858-b439-7f9acbb516bb", + "metadata": {}, + "source": [ + "Display transpiled results of one of the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "37924fc2-8fb6-451a-b8f9-cd79573f2384", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sabre transpilation\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AI transpilation\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(\"Sabre transpilation\")\n", + "display(tqc_sabre[0].draw(\"mpl\", fold=-1, idle_wires=False))\n", + "print(\"AI transpilation\")\n", + "display(tqc_ai[0].draw(\"mpl\", fold=-1, idle_wires=False))" + ] + }, + { + "cell_type": "markdown", + "id": "1633a4f4-7b01-4eb5-9764-0959f209a077", + "metadata": {}, + "source": [ + "Results table:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a5911224-3d1d-490d-b730-9cb90f954498", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " depth size runtime\n", + "method \n", + "ai 56.4 852.5 45.89\n", + "sabre 64.6 864.9 52.57\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreSU2_10010{'rz': 81, 'sx': 70, 'cz': 16}131670.058845
1sabreSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.238217
2sabreSU2_30230{'sx': 295, 'rz': 242, 'cz': 90}7262710.723922
3sabreSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059916.159262
4sabreSU2_50450{'rz': 402, 'sx': 367, 'cz': 86}7785576.886604
5sabreSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089986.118255
6sabreSU2_70670{'rz': 562, 'sx': 441, 'cz': 82}79108594.458287
7sabreSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119969.048184
8sabreSU2_90890{'rz': 721, 'sx': 585, 'cz': 114}105142088.254809
9sabreSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149983.795482
10aiSU2_10010{'rz': 81, 'sx': 71, 'cz': 16}101680.171532
11aiSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.291691
12aiSU2_30230{'sx': 243, 'rz': 242, 'cz': 63}3654813.555931
13aiSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059915.952733
14aiSU2_50450{'rz': 403, 'sx': 346, 'cz': 74}5482380.702141
15aiSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089975.993404
16aiSU2_70670{'rz': 563, 'sx': 442, 'cz': 82}74108764.960162
17aiSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119968.253280
18aiSU2_90890{'rz': 721, 'sx': 575, 'cz': 108}90140475.072412
19aiSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149963.967446
\n", + "
" + ], + "text/plain": [ + " method qc_name qc_index num_qubits ops \\\n", + "0 sabre SU2_10 0 10 {'rz': 81, 'sx': 70, 'cz': 16} \n", + "1 sabre SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", + "2 sabre SU2_30 2 30 {'sx': 295, 'rz': 242, 'cz': 90} \n", + "3 sabre SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", + "4 sabre SU2_50 4 50 {'rz': 402, 'sx': 367, 'cz': 86} \n", + "5 sabre SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", + "6 sabre SU2_70 6 70 {'rz': 562, 'sx': 441, 'cz': 82} \n", + "7 sabre SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", + "8 sabre SU2_90 8 90 {'rz': 721, 'sx': 585, 'cz': 114} \n", + "9 sabre SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", + "10 ai SU2_10 0 10 {'rz': 81, 'sx': 71, 'cz': 16} \n", + "11 ai SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", + "12 ai SU2_30 2 30 {'sx': 243, 'rz': 242, 'cz': 63} \n", + "13 ai SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", + "14 ai SU2_50 4 50 {'rz': 403, 'sx': 346, 'cz': 74} \n", + "15 ai SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", + "16 ai SU2_70 6 70 {'rz': 563, 'sx': 442, 'cz': 82} \n", + "17 ai SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", + "18 ai SU2_90 8 90 {'rz': 721, 'sx': 575, 'cz': 108} \n", + "19 ai SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", + "\n", + " depth size runtime \n", + "0 13 167 0.058845 \n", + "1 20 299 0.238217 \n", + "2 72 627 10.723922 \n", + "3 40 599 16.159262 \n", + "4 77 855 76.886604 \n", + "5 60 899 86.118255 \n", + "6 79 1085 94.458287 \n", + "7 80 1199 69.048184 \n", + "8 105 1420 88.254809 \n", + "9 100 1499 83.795482 \n", + "10 10 168 0.171532 \n", + "11 20 299 0.291691 \n", + "12 36 548 13.555931 \n", + "13 40 599 15.952733 \n", + "14 54 823 80.702141 \n", + "15 60 899 75.993404 \n", + "16 74 1087 64.960162 \n", + "17 80 1199 68.253280 \n", + "18 90 1404 75.072412 \n", + "19 100 1499 63.967446 " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_su2 = (\n", + " results_su2.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", + " .mean()\n", + " .round(2)\n", + ")\n", + "print(summary_su2)\n", + "\n", + "results_su2" + ] + }, + { + "cell_type": "markdown", + "id": "167834f1-00a7-4190-99e4-d221d1952357", + "metadata": {}, + "source": [ + "#### Results graph\n", + "\n", + "As we define a function to consistently capture metrics, we will also define one to graph the metrics. Here, we will plot the two-qubit depth, gate count, and runtime for each compilation method across the circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d90fb4fa-e031-40f6-90e6-e1208a855bec", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_transpilation_metrics(results, overall_title, x_axis=\"qc_index\"):\n", + " \"\"\"\n", + " Plots transpilation metrics (depth, size, runtime) for different transpilation methods.\n", + "\n", + " Parameters:\n", + " results (DataFrame): Data containing columns ['num_qubits', 'method', 'depth', 'size', 'runtime']\n", + " overall_title (str): The title of the overall figure.\n", + " x_axis (str): The x-axis label, either 'num_qubits' or 'qc_index'.\n", + " \"\"\"\n", + "\n", + " fig, axs = plt.subplots(1, 3, figsize=(24, 6))\n", + " metrics = [\"depth\", \"size\", \"runtime\"]\n", + " titles = [\"Circuit Depth\", \"Circuit Size\", \"Transpilation Runtime\"]\n", + " y_labels = [\"Depth\", \"Size (Gate Count)\", \"Runtime (s)\"]\n", + "\n", + " methods = results[\"method\"].unique()\n", + " colors = plt.colormaps[\"tab10\"]\n", + " markers = [\"o\", \"^\", \"s\", \"D\", \"P\", \"*\", \"X\", \"v\"]\n", + " color_list = [colors(i % colors.N) for i in range(len(methods))]\n", + " color_map = {method: color_list[i] for i, method in enumerate(methods)}\n", + " marker_map = {\n", + " method: markers[i % len(markers)] for i, method in enumerate(methods)\n", + " }\n", + " jitter_factor = 0.1 # Small x-axis jitter for visibility\n", + " handles, labels = [], [] # Unique handles for legend\n", + "\n", + " # Plot each metric\n", + " for i, metric in enumerate(metrics):\n", + " for method in methods:\n", + " method_data = results[results[\"method\"] == method]\n", + "\n", + " # Introduce slight jitter to avoid exact overlap\n", + " jitter = np.random.uniform(\n", + " -jitter_factor, jitter_factor, len(method_data)\n", + " )\n", + "\n", + " scatter = axs[i].scatter(\n", + " method_data[x_axis] + jitter,\n", + " method_data[metric],\n", + " color=color_map[method],\n", + " label=method,\n", + " marker=marker_map[method],\n", + " alpha=0.7,\n", + " edgecolors=\"black\",\n", + " s=80,\n", + " )\n", + "\n", + " if method not in labels:\n", + " handles.append(scatter)\n", + " labels.append(method)\n", + "\n", + " axs[i].set_title(titles[i])\n", + " axs[i].set_xlabel(x_axis)\n", + " axs[i].set_ylabel(y_labels[i])\n", + " axs[i].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", + " axs[i].tick_params(axis=\"x\", rotation=45)\n", + " axs[i].set_xticks(sorted(results[x_axis].unique()))\n", + "\n", + " fig.suptitle(overall_title, fontsize=16)\n", + " fig.legend(\n", + " handles=handles,\n", + " labels=labels,\n", + " loc=\"upper right\",\n", + " bbox_to_anchor=(1.05, 1),\n", + " )\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7f7b502a-8ed6-45fa-a698-02977149e283", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_transpilation_metrics(\n", + " results_su2, \"Transpilation Metrics for SU2 Circuits\", x_axis=\"num_qubits\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f75e8e3a-803f-4386-a60a-b088faa3c81a", + "metadata": {}, + "source": [ + "#### Analysis of SU2 circuit compilation results\n", + "\n", + "In this experiment, we compare two transpilation methods — Qiskit's SABRE transpiler and the AI-powered transpiler — on a set of `efficient_su2` circuits. Since these circuits do not include any `PauliEvolutionGate` operations, the Rustiq plugin is not included in this comparison.\n", + "\n", + "On average, the AI transpiler performs better in terms of circuit depth, with a greater than 10% improvement across the full range of SU2 circuits. For gate count (circuit size) and transpilation runtime, both methods yield similar results overall.\n", + "\n", + "However, inspecting the individual data points reveals a deeper insight:\n", + "- For most qubit sizes, both SABRE and AI produce nearly identical results, suggesting that in many cases, both methods converge to similarly efficient solutions.\n", + "- For certain circuit sizes, specifically at 30, 50, 70, and 90 qubits, the AI transpiler finds significantly shallower circuits than SABRE. This indicates that AI's learning-based approach is able to discover more optimal layouts or routing paths in cases where the SABRE heuristic does not.\n", + "\n", + "This behavior highlights an important takeaway:\n", + "> While SABRE and AI often produce comparable results, the AI transpiler can occasionally discover much better solutions, particularly in terms of depth, which can lead to significantly improved performance on hardware." + ] + }, + { + "cell_type": "markdown", + "id": "4c3b2aa8-8187-488a-8e5b-197cf26085bb", + "metadata": {}, + "source": [ + "## Part 2: Hamiltonian simulation circuit\n", + "\n", + "### Step 1: Investigate circuits with `PauliEvolutionGate`\n", + "\n", + "In this section, we investigate quantum circuits constructed using `PauliEvolutionGate`, which enables efficient simulation of Hamiltonians. We will analyze how different compilation methods optimize these circuits across various Hamiltonians.\n", + "\n", + "#### Hamiltonians used in the benchmark\n", + "\n", + "The Hamiltonians used in this benchmark describe pairwise interactions between qubits, including terms such as $ZZ$, $XX$, and $YY$. These Hamiltonians are commonly used in quantum chemistry, condensed matter physics, and materials science, where they model systems of interacting particles.\n", + "\n", + "For reference, users can explore a broader set of Hamiltonians in this paper: [Efficient Hamiltonian Simulation on Noisy Quantum Devices](https://arxiv.org/pdf/2306.13126).\n", + "\n", + "#### Benchmark source: Hamlib and Benchpress\n", + "\n", + "The circuits used in this benchmark are drawn from the [Hamlib benchmark repository](https://github.com/SRI-International/QC-App-Oriented-Benchmarks/tree/master/hamlib), which contains realistic Hamiltonian simulation workloads.\n", + "\n", + "These same circuits were previously benchmarked using [Benchpress](https://github.com/Qiskit/benchpress), an open-source framework for evaluating quantum transpilation performance. By using this standardized set of circuits, we can directly compare the effectiveness of different compilation strategies on representative simulation problems.\n", + "\n", + "Hamiltonian simulation is a foundational task in quantum computing, with applications in molecular simulations, optimization problems, and quantum many-body physics. Understanding how different compilation methods optimize these circuits can help users improve practical execution of such circuits on near-term quantum devices." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "66347c00-1607-4405-bb76-610690adf6b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of Hamiltonian circuits: 35\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Obtain the Hamiltonian JSON from the benchpress repository\n", + "url = \"https://raw.githubusercontent.com/Qiskit/benchpress/e7b29ef7be4cc0d70237b8fdc03edbd698908eff/benchpress/hamiltonian/hamlib/100_representative.json\"\n", + "response = requests.get(url)\n", + "response.raise_for_status() # Raise an error if download failed\n", + "ham_records = json.loads(response.text)\n", + "# Remove circuits that are too large for the backend\n", + "ham_records = [\n", + " h for h in ham_records if h[\"ham_qubits\"] <= backend.num_qubits\n", + "]\n", + "# Remove the circuits that are large to save transpilation time\n", + "ham_records = sorted(ham_records, key=lambda x: x[\"ham_terms\"])[:35]\n", + "\n", + "qc_ham_list = []\n", + "for h in ham_records:\n", + " terms = h[\"ham_hamlib_hamiltonian_terms\"]\n", + " coeff = h[\"ham_hamlib_hamiltonian_coefficients\"]\n", + " num_qubits = h[\"ham_qubits\"]\n", + " name = h[\"ham_problem\"]\n", + "\n", + " evo_gate = PauliEvolutionGate(SparsePauliOp(terms, coeff))\n", + "\n", + " qc_ham = QuantumCircuit(num_qubits)\n", + " qc_ham.name = name\n", + "\n", + " qc_ham.append(evo_gate, range(num_qubits))\n", + " qc_ham_list.append(qc_ham)\n", + "print(f\"Number of Hamiltonian circuits: {len(qc_ham_list)}\")\n", + "\n", + "# Draw the first Hamiltonian circuit\n", + "qc_ham_list[0].draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "9690d94d-7d38-45d3-b37d-85eaf268dbd6", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "As in the previous example, we will use the same backend to ensure consistency in our comparisons. Since the pass managers (`pm_sabre`, `pm_ai`, and `pm_rustiq`) have already been initialized, we can directly proceed with transpiling the Hamiltonian circuits using each method.\n", + "\n", + "This step focuses solely on performing the transpilation and recording the resulting circuit metrics, including depth, gate count, and transpilation runtime. By analyzing these results, we aim to determine the efficiency of each transpilation method for this type of circuit." + ] + }, + { + "cell_type": "markdown", + "id": "22fb1fe7-333f-421f-a17d-bc32190a0f86", + "metadata": {}, + "source": [ + "Transpile and capture metrics:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "4138c7d6-5ec8-4c8f-aef4-08ec1b13633b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method sabre, depth 6, and size 58.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.10 seconds with method sabre, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method sabre, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.03 seconds with method sabre, depth 18, and size 115.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method sabre, depth 24, and size 129.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.05 seconds with method sabre, depth 14, and size 134.\n", + "Transpiled circuit index 6 (all-vib-hno) in 8.39 seconds with method sabre, depth 6, and size 174.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.92 seconds with method sabre, depth 22, and size 220.\n", + "Transpiled circuit index 8 (LiH) in 0.03 seconds with method sabre, depth 67, and size 290.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method sabre, depth 50, and size 340.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.62 seconds with method sabre, depth 30, and size 286.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.04 seconds with method sabre, depth 67, and size 339.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.04 seconds with method sabre, depth 87, and size 421.\n", + "Transpiled circuit index 13 (tfim) in 0.05 seconds with method sabre, depth 36, and size 222.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.51 seconds with method sabre, depth 22, and size 345.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.05 seconds with method sabre, depth 128, and size 704.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.83 seconds with method sabre, depth 2, and size 242.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method sabre, depth 106, and size 609.\n", + "Transpiled circuit index 18 (tfim) in 0.29 seconds with method sabre, depth 73, and size 399.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 21.97 seconds with method sabre, depth 30, and size 572.\n", + "Transpiled circuit index 20 (Be2) in 0.09 seconds with method sabre, depth 324, and size 1555.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.12 seconds with method sabre, depth 250, and size 1394.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.07 seconds with method sabre, depth 215, and size 1027.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 41.22 seconds with method sabre, depth 30, and size 1144.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 1.89 seconds with method sabre, depth 175, and size 1933.\n", + "Transpiled circuit index 25 (H2) in 0.32 seconds with method sabre, depth 1237, and size 5502.\n", + "Transpiled circuit index 26 (uuf100-ham) in 0.20 seconds with method sabre, depth 385, and size 4303.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.20 seconds with method sabre, depth 311, and size 3654.\n", + "Transpiled circuit index 28 (tfim) in 0.15 seconds with method sabre, depth 276, and size 3213.\n", + "Transpiled circuit index 29 (uuf100-ham) in 0.21 seconds with method sabre, depth 520, and size 5250.\n", + "Transpiled circuit index 30 (flat100-ham) in 0.15 seconds with method sabre, depth 131, and size 3157.\n", + "Transpiled circuit index 31 (uf100-ham) in 0.24 seconds with method sabre, depth 624, and size 7378.\n", + "Transpiled circuit index 32 (OH) in 0.88 seconds with method sabre, depth 2175, and size 9808.\n", + "Transpiled circuit index 33 (HF) in 0.66 seconds with method sabre, depth 2206, and size 9417.\n", + "Transpiled circuit index 34 (BH) in 0.89 seconds with method sabre, depth 2177, and size 9802.\n", + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method ai, depth 6, and size 58.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method ai, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method ai, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.11 seconds with method ai, depth 18, and size 94.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.11 seconds with method ai, depth 22, and size 129.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.06 seconds with method ai, depth 22, and size 177.\n", + "Transpiled circuit index 6 (all-vib-hno) in 8.62 seconds with method ai, depth 10, and size 198.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.71 seconds with method ai, depth 18, and size 195.\n", + "Transpiled circuit index 8 (LiH) in 0.19 seconds with method ai, depth 62, and size 267.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.22 seconds with method ai, depth 47, and size 321.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.71 seconds with method ai, depth 38, and size 369.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.24 seconds with method ai, depth 65, and size 315.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.24 seconds with method ai, depth 91, and size 430.\n", + "Transpiled circuit index 13 (tfim) in 0.15 seconds with method ai, depth 12, and size 251.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 8.50 seconds with method ai, depth 18, and size 311.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.25 seconds with method ai, depth 117, and size 659.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 16.11 seconds with method ai, depth 2, and size 242.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.39 seconds with method ai, depth 98, and size 564.\n", + "Transpiled circuit index 18 (tfim) in 0.38 seconds with method ai, depth 23, and size 437.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 24.97 seconds with method ai, depth 38, and size 707.\n", + "Transpiled circuit index 20 (Be2) in 1.07 seconds with method ai, depth 293, and size 1392.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.61 seconds with method ai, depth 229, and size 1437.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.57 seconds with method ai, depth 178, and size 964.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 50.89 seconds with method ai, depth 34, and size 1425.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 1.61 seconds with method ai, depth 171, and size 2020.\n", + "Transpiled circuit index 25 (H2) in 6.39 seconds with method ai, depth 1148, and size 5208.\n", + "Transpiled circuit index 26 (uuf100-ham) in 3.97 seconds with method ai, depth 376, and size 5048.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 3.54 seconds with method ai, depth 357, and size 4451.\n", + "Transpiled circuit index 28 (tfim) in 1.72 seconds with method ai, depth 216, and size 3026.\n", + "Transpiled circuit index 29 (uuf100-ham) in 4.45 seconds with method ai, depth 426, and size 5399.\n", + "Transpiled circuit index 30 (flat100-ham) in 7.02 seconds with method ai, depth 86, and size 3108.\n", + "Transpiled circuit index 31 (uf100-ham) in 12.85 seconds with method ai, depth 623, and size 8354.\n", + "Transpiled circuit index 32 (OH) in 15.19 seconds with method ai, depth 2084, and size 9543.\n", + "Transpiled circuit index 33 (HF) in 17.51 seconds with method ai, depth 2063, and size 9446.\n", + "Transpiled circuit index 34 (BH) in 15.33 seconds with method ai, depth 2094, and size 9730.\n", + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method rustiq, depth 13, and size 83.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method rustiq, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method rustiq, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.01 seconds with method rustiq, depth 13, and size 79.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method rustiq, depth 31, and size 131.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.04 seconds with method rustiq, depth 50, and size 306.\n", + "Transpiled circuit index 6 (all-vib-hno) in 14.03 seconds with method rustiq, depth 22, and size 276.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.15 seconds with method rustiq, depth 13, and size 155.\n", + "Transpiled circuit index 8 (LiH) in 0.03 seconds with method rustiq, depth 54, and size 270.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method rustiq, depth 65, and size 398.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.16 seconds with method rustiq, depth 41, and size 516.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.02 seconds with method rustiq, depth 34, and size 189.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.03 seconds with method rustiq, depth 49, and size 240.\n", + "Transpiled circuit index 13 (tfim) in 0.05 seconds with method rustiq, depth 20, and size 366.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.08 seconds with method rustiq, depth 16, and size 277.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.04 seconds with method rustiq, depth 116, and size 612.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.89 seconds with method rustiq, depth 2, and size 257.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method rustiq, depth 133, and size 737.\n", + "Transpiled circuit index 18 (tfim) in 0.11 seconds with method rustiq, depth 25, and size 680.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 27.19 seconds with method rustiq, depth 66, and size 983.\n", + "Transpiled circuit index 20 (Be2) in 0.07 seconds with method rustiq, depth 215, and size 1030.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.14 seconds with method rustiq, depth 328, and size 1918.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.05 seconds with method rustiq, depth 114, and size 692.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 62.25 seconds with method rustiq, depth 74, and size 2348.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 0.20 seconds with method rustiq, depth 436, and size 3605.\n", + "Transpiled circuit index 25 (H2) in 0.21 seconds with method rustiq, depth 643, and size 3476.\n", + "Transpiled circuit index 26 (uuf100-ham) in 0.24 seconds with method rustiq, depth 678, and size 6120.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.22 seconds with method rustiq, depth 588, and size 5241.\n", + "Transpiled circuit index 28 (tfim) in 0.34 seconds with method rustiq, depth 340, and size 5901.\n", + "Transpiled circuit index 29 (uuf100-ham) in 0.33 seconds with method rustiq, depth 881, and size 7667.\n", + "Transpiled circuit index 30 (flat100-ham) in 0.31 seconds with method rustiq, depth 279, and size 4910.\n", + "Transpiled circuit index 31 (uf100-ham) in 0.38 seconds with method rustiq, depth 1138, and size 10607.\n", + "Transpiled circuit index 32 (OH) in 0.38 seconds with method rustiq, depth 1148, and size 6512.\n", + "Transpiled circuit index 33 (HF) in 0.37 seconds with method rustiq, depth 1090, and size 6256.\n", + "Transpiled circuit index 34 (BH) in 0.37 seconds with method rustiq, depth 1148, and size 6501.\n" + ] + } + ], + "source": [ + "results_ham = pd.DataFrame(\n", + " columns=[\n", + " \"method\",\n", + " \"qc_name\",\n", + " \"qc_index\",\n", + " \"num_qubits\",\n", + " \"ops\",\n", + " \"depth\",\n", + " \"size\",\n", + " \"runtime\",\n", + " ]\n", + ")\n", + "\n", + "tqc_sabre = capture_transpilation_metrics(\n", + " results_ham, pm_sabre, qc_ham_list, \"sabre\"\n", + ")\n", + "tqc_ai = capture_transpilation_metrics(results_ham, pm_ai, qc_ham_list, \"ai\")\n", + "tqc_rustiq = capture_transpilation_metrics(\n", + " results_ham, pm_rustiq, qc_ham_list, \"rustiq\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ef88a127-79b8-4a53-90ef-6caa84c29e06", + "metadata": {}, + "source": [ + "Results table (skipping visualization as the output circuits are very large):" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "bc4a7910-5e8a-48ec-bd7e-2508ae8dfccc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " depth size runtime\n", + "method \n", + "ai 316.86 2181.26 5.97\n", + "rustiq 281.94 2268.80 3.86\n", + "sabre 337.97 2120.14 3.07\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreall-vib-o304{'rz': 28, 'sx': 24, 'cz': 6}6580.016597
1sabreall-vib-c2h14{'rz': 17, 'sx': 16, 'cz': 4, 'x': 2}2391.102089
2sabreall-vib-bh22{'sx': 14, 'rz': 13, 'cz': 3}3300.011042
3sabreall-vib-c2h33{'sx': 46, 'rz': 45, 'cz': 18, 'x': 6}181150.025816
4sabregraph-gnp_k-244{'sx': 49, 'rz': 47, 'cz': 24, 'x': 9}241290.023077
...........................
100rustiqflat100-ham3090{'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5}27949100.309448
101rustiquf100-ham3146{'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4}1138106070.380977
102rustiqOH3210{'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23}114865120.383564
103rustiqHF3310{'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17}109062560.368578
104rustiqBH3410{'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19}114865010.374822
\n", + "

105 rows × 8 columns

\n", + "
" + ], + "text/plain": [ + " method qc_name qc_index num_qubits \\\n", + "0 sabre all-vib-o3 0 4 \n", + "1 sabre all-vib-c2h 1 4 \n", + "2 sabre all-vib-bh 2 2 \n", + "3 sabre all-vib-c2h 3 3 \n", + "4 sabre graph-gnp_k-2 4 4 \n", + ".. ... ... ... ... \n", + "100 rustiq flat100-ham 30 90 \n", + "101 rustiq uf100-ham 31 46 \n", + "102 rustiq OH 32 10 \n", + "103 rustiq HF 33 10 \n", + "104 rustiq BH 34 10 \n", + "\n", + " ops depth size runtime \n", + "0 {'rz': 28, 'sx': 24, 'cz': 6} 6 58 0.016597 \n", + "1 {'rz': 17, 'sx': 16, 'cz': 4, 'x': 2} 2 39 1.102089 \n", + "2 {'sx': 14, 'rz': 13, 'cz': 3} 3 30 0.011042 \n", + "3 {'sx': 46, 'rz': 45, 'cz': 18, 'x': 6} 18 115 0.025816 \n", + "4 {'sx': 49, 'rz': 47, 'cz': 24, 'x': 9} 24 129 0.023077 \n", + ".. ... ... ... ... \n", + "100 {'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5} 279 4910 0.309448 \n", + "101 {'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4} 1138 10607 0.380977 \n", + "102 {'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23} 1148 6512 0.383564 \n", + "103 {'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17} 1090 6256 0.368578 \n", + "104 {'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19} 1148 6501 0.374822 \n", + "\n", + "[105 rows x 8 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_ham = (\n", + " results_ham.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", + " .mean()\n", + " .round(2)\n", + ")\n", + "print(summary_ham)\n", + "\n", + "results_ham" + ] + }, + { + "cell_type": "markdown", + "id": "17e7b228-01b0-4fcc-b659-9f9958f5477e", + "metadata": {}, + "source": [ + "Visualize performance based on circuit index:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4c6b810d-a4cf-4de3-aae6-c6c1cdd83d8f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_transpilation_metrics(\n", + " results_ham, \"Transpilation Metrics for Hamiltonian Circuits\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7ad6f4ee-c9d1-4e8b-ae25-cf39672acbec", + "metadata": {}, + "source": [ + "Visualize the percentage of circuits for which each method performed best." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "01b4644e-ac91-483c-944b-924f8b41718d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best-performing methods based on depth:\n", + " ai: 16 circuit(s)\n", + " rustiq: 16 circuit(s)\n", + " sabre: 10 circuit(s)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best-performing methods based on size:\n", + " sabre: 18 circuit(s)\n", + " rustiq: 14 circuit(s)\n", + " ai: 10 circuit(s)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def analyze_and_plot_best_methods(results, metric):\n", + " \"\"\"\n", + " Analyze the best-performing methods for a given metric and plot a pie chart.\n", + "\n", + " Parameters:\n", + " results (DataFrame): The input DataFrame containing method performance data.\n", + " metric (str): The metric to evaluate (\"depth\" or \"size\").\n", + " \"\"\"\n", + " method_counts = Counter()\n", + " for qc_idx, group in results.groupby(\"qc_index\"):\n", + " min_value = group[metric].min()\n", + "\n", + " # Find all methods that achieved this minimum value\n", + " best_methods = group[group[metric] == min_value][\"method\"]\n", + " # Update counts for all best methods (handling ties)\n", + " method_counts.update(best_methods)\n", + " best_method_counts = dict(\n", + " sorted(method_counts.items(), key=lambda x: x[1], reverse=True)\n", + " )\n", + "\n", + " # Print summary\n", + " print(f\"Best-performing methods based on {metric}:\")\n", + " for method, count in best_method_counts.items():\n", + " print(f\" {method}: {count} circuit(s)\")\n", + "\n", + " # Plot pie chart\n", + " num_methods = len(best_method_counts)\n", + " colors = plt.cm.viridis_r(range(0, 256, 256 // num_methods))\n", + " plt.figure(figsize=(5, 5))\n", + " plt.pie(\n", + " best_method_counts.values(),\n", + " labels=best_method_counts.keys(),\n", + " autopct=\"%1.1f%%\",\n", + " startangle=140,\n", + " wedgeprops={\"edgecolor\": \"black\"},\n", + " textprops={\"fontsize\": 10},\n", + " colors=colors,\n", + " )\n", + " plt.title(\n", + " f\"Percentage of Circuits Method Performed Best for {metric.capitalize()}\",\n", + " fontsize=12,\n", + " fontweight=\"bold\",\n", + " )\n", + " plt.show()\n", + "\n", + "\n", + "analyze_and_plot_best_methods(results_ham, \"depth\")\n", + "analyze_and_plot_best_methods(results_ham, \"size\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4f6aa23-d528-4d66-92e0-31d29c522792", + "metadata": {}, + "source": [ + "#### Analysis of Hamiltonian circuit compilation results\n", + "\n", + "In this section, we evaluate the performance of three transpilation methods — SABRE, the AI-powered transpiler, and Rustiq — on quantum circuits constructed with `PauliEvolutionGate`, which are commonly used in Hamiltonian simulation tasks.\n", + "\n", + "Rustiq performed best on average in terms of circuit depth**, achieving approximately 20% lower depth than SABRE. This is expected, as Rustiq is specifically designed to synthesize `PauliEvolutionGate` operations with optimized, low-depth decomposition strategies. Furthermore, the depth plot shows that as the circuits scale in size and complexity, Rustiq scales most effectively, maintaining significantly lower depth than both AI and SABRE on larger circuits.\n", + "\n", + "AI transpiler showed strong and consistent performance for circuit depth, consistently outperforming SABRE across most circuits. However, it incurred the highest runtime, especially on larger circuits, which may limit its practicality in time-sensitive workloads. Its scalability in runtime remains a key limitation, even though it offers solid improvements in depth.\n", + "\n", + "SABRE, while producing the highest average depth, achieved the lowest average gate count, closely followed by the AI transpiler. This aligns with the design of SABRE’s heuristic, which prioritizes minimizing gate count directly. Rustiq, despite its strength in lowering depth, had the highest average gate count, which is a notable trade-off to consider in applications where circuit size matters more than circuit duration.\n", + "\n", + "### Summary\n", + "\n", + "While the AI transpiler generally delivers better results than SABRE, particularly in circuit depth, the takeaway should not simply be \"always use the AI transpiler.\" There are important nuances to consider:\n", + "\n", + "- **AI transpiler** is typically reliable and provides depth-optimized circuits, but it comes with trade-offs in runtime, and also has other limitations, including supported coupling maps and synthesis capabilities. These are detailed in the [Qiskit Transpiler Service documentation](/docs/guides/qiskit-transpiler-service).\n", + "\n", + "- In some cases, particularly with very large or hardware-specific circuits, the AI transpiler may not be as effective. In these cases, the default SABRE transpiler remains extremely reliable and can be further optimized by adjusting its parameters (see the [SABRE optimization tutorial](/docs/tutorials/transpilation-optimizations-with-sabre)).\n", + "\n", + "- It's also important to consider circuit structure when choosing a method. For example, `rustiq` is purpose-built for circuits involving `PauliEvolutionGate` and often yields the best performance for Hamiltonian simulation problems.\n", + "\n", + "**Recommendation:**\n", + "> There is no one-size-fits-all transpilation strategy. Users are encouraged to understand the structure of their circuit and test multiple transpilation methods — including AI, SABRE, and specialized tools like Rustiq — to find the most efficient solution for their specific problem and hardware constraints." + ] + }, + { + "cell_type": "markdown", + "id": "9a747477-1dc1-4706-b844-d29570fb5844", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives" + ] + }, + { + "cell_type": "markdown", + "id": "d3d6d473-7508-4e28-82ad-eee09c9a1acf", + "metadata": {}, + "source": [ + "As this tutorial focuses on transpilation, no experiments are executed on a quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth and gate count." + ] + }, + { + "cell_type": "markdown", + "id": "bb4c4c67-a7e8-4ca1-a5d6-908a2e1d27e5", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format" + ] + }, + { + "cell_type": "markdown", + "id": "4aa45929-f8d9-4be8-9008-0588615fa45e", + "metadata": {}, + "source": [ + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "552ef44b-e567-4870-9699-d152806b9505", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] \"LightSABRE: A Lightweight and Enhanced SABRE Algorithm\". H. Zou, M. Treinish, K. Hartman, A. Ivrii, J. Lishman et al. https://arxiv.org/abs/2409.08368\n", + "\n", + "[2] \"Practical and efficient quantum circuit synthesis and transpiling with Reinforcement Learning\". D. Kremer, V. Villar, H. Paik, I. Duran, I. Faro, J. Cruz-Benito et al. https://arxiv.org/abs/2405.13196\n", + "\n", + "[3] \"Pauli Network Circuit Synthesis with Reinforcement Learning\". A. Dubal, D. Kremer, S. Martiel, V. Villar, D. Wang, J. Cruz-Benito et al. https://arxiv.org/abs/2503.14448\n", + "\n", + "[4] \"Faster and shorter synthesis of Hamiltonian simulation circuits\". T. Goubault de Brugière, S. Martiel et al. https://arxiv.org/abs/2404.03280" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/long-range-entanglement.ipynb b/docs/tutorials/long-range-entanglement.ipynb index 55d5c305a0b..4a91e5bbeff 100644 --- a/docs/tutorials/long-range-entanglement.ipynb +++ b/docs/tutorials/long-range-entanglement.ipynb @@ -1,1563 +1,1566 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "44d97249", - "metadata": {}, - "source": [ - "---\n", - "title: Long-range entanglement with dynamic circuits\n", - "description: This tutorial implements a long-range CNOT using dynamic circuits with Bell pairs, measurement, and feedforward, and compares it to a direct unitary approach.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore fontsize hcords, ycords, xcords, ecolor, elinewidth, allcr, braket, frameon */}\n", - "\n", - "# Long-range entanglement with dynamic circuits\n", - "*Usage estimate: 4 minutes on a Heron r2 processor. (NOTE: This is an estimate only. Your runtime may vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "57f65bca", - "metadata": {}, - "source": [ - "## Learning outcomes\n", - "After completing this tutorial, you will have learned the following:\n", - "* How to implement a long-range CNOT gate using dynamic circuits with mid-circuit measurements (MCMs) and classical feedforward;\n", - "* How to implement the equivalent gate using a unitary SWAP-based approach;\n", - "* How to compare both approaches by measuring gate fidelity as a function of qubit distance." - ] - }, - { - "cell_type": "markdown", - "id": "748eee3b", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "We suggest that users are familiar with the following topics before going through this tutorial:\n", - "* [Basic quantum computing concepts](/learning/courses/basics-of-quantum-information), including Bell states, entanglement, and quantum gates;\n", - "* Familiarity with [dynamic circuits](/docs/guides/classical-feedforward-and-control-flow) (mid-circuit measurements and classical feedforward);\n", - "* Basic knowledge of [Qiskit SDK](/docs/guides) and [Qiskit Runtime](/docs/guides/compute-services#qiskit-runtime), and access to an [IBM Quantum® account](/docs/guides/cloud-setup)." - ] - }, - { - "cell_type": "markdown", - "id": "b05b669f", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "Long-range entanglement between distant qubits is challenging on devices with limited connectivity. This tutorial shows how dynamic circuits can generate such entanglement by implementing a long-range controlled-X (LRCX) gate using a measurement-based protocol.\n", - "\n", - "Following the approach by Elisa Bäumer et al. in [1](#ref-1), the method uses mid-circuit measurement and feedforward to achieve constant-depth gates regardless of qubit separation. It creates intermediate Bell pairs, measures one qubit from each pair, and applies classically conditioned gates to propagate entanglement across the device. This avoids long SWAP chains, reducing both circuit depth and exposure to two-qubit gate errors.\n", - "\n", - "In this notebook, we adapt the protocol for IBM Quantum hardware and benchmark its performance as a function of the control–target separation, comparing it against a unitary SWAP-based baseline." - ] - }, - { - "cell_type": "markdown", - "id": "c6cd3174", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, ensure that you have the following installed:\n", - "\n", - "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.37 or later (`pip install qiskit-ibm-runtime`)\n", - "- Qiskit Aer v0.17 or later (`pip install qiskit-aer`)" - ] - }, - { - "cell_type": "markdown", - "id": "39e4440a", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5b0aa550", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n", - "from qiskit.circuit.classical import expr\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "from qiskit.visualization import plot_circuit_layout\n", - "from qiskit_ibm_runtime import (\n", - " QiskitRuntimeService,\n", - " Batch,\n", - " SamplerV2 as Sampler,\n", - ")\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np" - ] - }, - { - "cell_type": "markdown", - "id": "6733ceeb", - "metadata": {}, - "source": [ - "## Small-scale simulator example\n", - "\n", - "Before running on the real QPU, we verify that both the dynamic and unitary circuits produce an ideal Bell state on a noiseless simulator. We use the Qiskit Runtime `Sampler` with `AerSimulator` as the backend mode, at a small distance of 6." - ] - }, - { - "cell_type": "markdown", - "id": "28145093", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "We now implement a long-range CNOT gate between two distant qubits, following the dynamic-circuit construction shown below (adapted from Fig. 1a in Ref. [1](#ref-1)). The key idea is to use a “bus” of ancilla qubits, initialized to $|0\\rangle$, to mediate long-range gate teleportation.\n", - "\n", - "![Long-range CNOT circuit](/docs/images/tutorials/long-range-entanglement/dynamic_vs_unitary_long_range_illustration.avif)\n", - "\n", - "As illustrated in the figure, the process works as follows:\n", - "1. Prepare a chain of Bell pairs connecting the control and target qubits via intermediate ancillas.\n", - "2. Perform Bell measurements between non-entangled neighboring qubits, swapping entanglement step-by-step until the control and target share a Bell pair.\n", - "3. Use this Bell pair for gate teleportation, turning a local CNOT into a deterministic long-range CNOT in constant depth.\n", - "\n", - "This approach replaces long SWAP chains with a constant-depth protocol, reducing exposure to two-qubit gate errors and making the operation scalable with device size.\n", - "\n", - "In what follows, we will first walk through the dynamic-circuit implementation of the LRCX circuit. At the end, we will also provide a unitary-based implementation for comparison, to highlight the advantages of dynamic circuits in this setting." - ] - }, - { - "cell_type": "markdown", - "id": "89597fcf", - "metadata": {}, - "source": [ - "#### Initialize circuit\n", - "\n", - "We begin with a simple quantum problem that will serve as the basis for comparison. Specifically, we initialize a circuit with a control qubit at index 0 and apply a Hadamard gate to it. This produces a superposition state that, when followed by a controlled-X operation, generates a Bell state $(|00\\rangle + |11\\rangle)/\\sqrt{2}$ between the control and target qubits.\n", - "\n", - "At this stage, we are not yet constructing the long-range controlled-X (LRCX) itself. Instead, our goal is to define a clear and minimal initial circuit that highlights the role of the LRCX. In Step 2, we will show how the LRCX can be implemented as an optimization using dynamic circuits, and compare its performance against a unitary equivalent. Importantly, the LRCX protocol can be applied to any initial circuit. Here we use this simple Hadamard setup for clarity of demonstration." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "0446b8e8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "distance = 6 # The distance of the CNOT gate, with the convention that a distance of zero is a nearest-neighbor CNOT.\n", - "\n", - "\n", - "def initialize_circuit(distance):\n", - " assert distance >= 0\n", - " control = 0 # control qubit\n", - " n = distance # number of qubits between target and control\n", - "\n", - " qr = QuantumRegister(\n", - " n + 2, name=\"q\"\n", - " ) # Circuit with n qubits between control and target\n", - " cr = ClassicalRegister(\n", - " 2, name=\"cr\"\n", - " ) # Classical register for measuring control and target qubits\n", - "\n", - " k = int(n / 2) # Number of Bell States to be used\n", - "\n", - " allcr = [cr]\n", - " if (\n", - " distance > 1\n", - " ): # This classical register will be used to store ZZ measurements. It is only used for long-range CX gates with distance > 1\n", - " c1 = ClassicalRegister(\n", - " k, name=\"c1\"\n", - " ) # Classical register needed for post processing\n", - " allcr.append(c1)\n", - " if (\n", - " distance > 0\n", - " ): # This classical register will be used to store XX measurements. It is only used if distance > 0\n", - " c2 = ClassicalRegister(\n", - " n - k, name=\"c2\"\n", - " ) # Classical register needed for post processing\n", - " allcr.append(c2)\n", - "\n", - " qc = QuantumCircuit(qr, *allcr, name=\"CNOT\")\n", - "\n", - " # Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a Bell state (|00> + |11>)/sqrt(2)\n", - " qc.h(control)\n", - "\n", - " return qc\n", - "\n", - "\n", - "qc = initialize_circuit(distance)\n", - "qc.draw(fold=-1, output=\"mpl\", scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "e761cfc1", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "In this step, we show how to construct the LRCX circuit using dynamic circuits. The goal is to optimize the circuit for execution on hardware by reducing depth compared to a purely unitary implementation. To illustrate the benefits, we will display both the dynamic LRCX construction and its unitary equivalent, and later compare their performance after transpilation. Importantly, while here we apply the LRCX to a simple Hadamard-initialized problem, the protocol can be applied to any circuit where a long-range CNOT is required.\n", - "\n", - "#### Prepare Bell pairs\n", - "We begin by creating a chain of Bell pairs along the path between the control and target qubits. If the distance is odd, we first apply a CNOT from the control to its neighbor, which is the CNOT that will be teleported. For an even distance, this CNOT will be applied after the Bell pair preparation step. The Bell pair chain then entangles successive pairs of qubits, establishing the resource needed to carry the control information across the device." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4df8ebba", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Determine where to start the Bell pair chain and add an extra CNOT when n is odd\n", - "def check_even(n: int) -> int:\n", - " \"\"\"Return 1 if n is even, else 2.\"\"\"\n", - " return 1 if n % 2 == 0 else 2\n", - "\n", - "\n", - "def prepare_bell_pairs(qc, add_barriers=True):\n", - " n = qc.num_qubits - 2 # number of qubits between target and control\n", - " k = int(n / 2)\n", - "\n", - " if add_barriers:\n", - " qc.barrier()\n", - "\n", - " x0 = check_even(n)\n", - " if n % 2 != 0:\n", - " qc.cx(0, 1)\n", - "\n", - " # Create k Bell pairs\n", - " for i in range(k):\n", - " qc.h(x0 + 2 * i)\n", - " qc.cx(x0 + 2 * i, x0 + 2 * i + 1)\n", - " return qc\n", - "\n", - "\n", - "qc = prepare_bell_pairs(qc)\n", - "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "b80ea657", - "metadata": {}, - "source": [ - "#### Measure neighboring qubit pairs in the Bell basis\n", - "Next, we measure *unentangled* neighboring qubits in the Bell basis (two-qubit measurements of $XX$ and $ZZ$). This creates a long-range Bell pair between the target qubit, and the qubit adjacent to the control (up to Pauli corrections, which will be implemented via feedforward in the next step). In parallel, we implement the entangling measurement that teleports the CNOT gate to act on the intended target qubit." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8eed9e57", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def measure_bell_basis(qc, add_barriers=True):\n", - " n = qc.num_qubits - 2 # number of qubits between target and control\n", - " k = int(n / 2)\n", - "\n", - " if n > 1:\n", - " _, c1, c2 = qc.cregs\n", - " elif n > 0:\n", - " _, c2 = qc.cregs\n", - "\n", - " # Determine where to start the Bell pair chain and add an extra CNOT when n is odd\n", - " x0 = 1 if n % 2 == 0 else 2\n", - "\n", - " # Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be teleported, if n is even)\n", - " for i in range(k + 1):\n", - " qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)\n", - "\n", - " for i in range(1, k + x0):\n", - " if i == 1:\n", - " qc.h(2 * i + 1 - x0)\n", - " else:\n", - " qc.h(2 * i + 1 - x0)\n", - "\n", - " if add_barriers:\n", - " qc.barrier()\n", - "\n", - " # Map the ZZ measurements onto classical register c1\n", - " for i in range(k):\n", - " if i == 0:\n", - " qc.measure(2 * i + x0, c1[i])\n", - " else:\n", - " qc.measure(2 * i + x0, c1[i])\n", - "\n", - " # Map the XX measurements onto classical register c2\n", - " for i in range(1, k + x0):\n", - " if i == 1:\n", - " qc.measure(2 * i + 1 - x0, c2[i - 1])\n", - " else:\n", - " qc.measure(2 * i + 1 - x0, c2[i - 1])\n", - " return qc\n", - "\n", - "\n", - "qc = measure_bell_basis(qc)\n", - "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "f0d7059b", - "metadata": {}, - "source": [ - "#### Apply feedforward corrections to correct Pauli byproduct operators\n", - "\n", - "The Bell-basis measurements introduce Pauli byproducts that must be corrected using the recorded outcomes. This is done in two steps. First, we need to compute the parity of all $ZZ$ measurements, which is then used to conditionally apply an $X$ gate to the target qubit. Likewise, the parity of the $XX$ measurements is computed and used to conditionally apply a $Z$ gate to the control qubit.\n", - "\n", - "With the new classical expression framework in Qiskit, these parities can be computed directly in the classical processing layer of the circuit. Instead of applying a sequence of individual conditional gates for each measurement bit, we can build a single classical expression that represents the XOR (parity) of all relevant measurement outcomes. This expression is then used as the condition in a single `if_test` block, allowing the correction gates to be applied in constant depth. This approach both simplifies the circuit and ensures that the feedforward corrections do not introduce unnecessary additional latency." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4915791a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def apply_ffwd_corrections(qc):\n", - " control = 0 # control qubit\n", - " target = qc.num_qubits - 1 # target qubit\n", - " n = qc.num_qubits - 2 # number of qubits between target and control\n", - "\n", - " k = int(n / 2)\n", - " x0 = check_even(n)\n", - "\n", - " if n > 1:\n", - " _, c1, c2 = qc.cregs\n", - " elif n > 0:\n", - " _, c2 = qc.cregs\n", - "\n", - " # First, let's compute the parity of all ZZ measurements\n", - " for i in range(k):\n", - " if i == 0:\n", - " parity_ZZ = expr.lift(\n", - " c1[i]\n", - " ) # Store the value of the first ZZ measurement in parity_ZZ\n", - " else:\n", - " parity_ZZ = expr.bit_xor(\n", - " c1[i], parity_ZZ\n", - " ) # Successively compute the parity via XOR operations\n", - "\n", - " for i in range(1, k + x0):\n", - " if i == 1:\n", - " parity_XX = expr.lift(\n", - " c2[i - 1]\n", - " ) # Store the value of the first XX measurement in parity_XX\n", - " else:\n", - " parity_XX = expr.bit_xor(\n", - " c2[i - 1], parity_XX\n", - " ) # Successively compute the parity via XOR operations\n", - "\n", - " if n > 0:\n", - " with qc.if_test(parity_XX):\n", - " qc.z(control)\n", - "\n", - " if n > 1:\n", - " with qc.if_test(parity_ZZ):\n", - " qc.x(target)\n", - " return qc\n", - "\n", - "\n", - "qc = apply_ffwd_corrections(qc)\n", - "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "6d22740b", - "metadata": {}, - "source": [ - "#### Measure control and target qubits\n", - "We define a helper function that enables measurement of the control and target qubits in the $XX$, $YY$, or $ZZ$ bases. For verifying the Bell state $(|00\\rangle + |11\\rangle)/\\sqrt{2}$, the expectation values of $XX$ and $ZZ$ should both be $+1$, since they are stabilizers of the state. The $YY$ measurement is also supported here and will be used below when computing the fidelity." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d087d7c1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def measure_in_basis(qc, basis=\"XX\", add_barrier=True):\n", - " control = 0 # control qubit\n", - " target = qc.num_qubits - 1 # target qubit\n", - "\n", - " assert basis in [\"XX\", \"YY\", \"ZZ\"]\n", - "\n", - " qc = (\n", - " qc.copy()\n", - " ) # We copy the circuit because we want to measure in different bases\n", - " cr = qc.cregs[0]\n", - "\n", - " if add_barrier:\n", - " qc.barrier()\n", - "\n", - " if basis == \"XX\":\n", - " qc.h(control)\n", - " qc.h(target)\n", - " elif basis == \"YY\":\n", - " qc.sdg(control)\n", - " qc.sdg(target)\n", - " qc.h(control)\n", - " qc.h(target)\n", - "\n", - " qc.measure(control, cr[0])\n", - " qc.measure(target, cr[1])\n", - " return qc\n", - "\n", - "\n", - "qc_YY = measure_in_basis(qc.copy(), basis=\"YY\")\n", - "qc_YY.draw(\n", - " output=\"mpl\", fold=-1, scale=0.5\n", - ") # Circuit for measuring in the YY basis" - ] - }, - { - "cell_type": "markdown", - "id": "072f8605", - "metadata": {}, - "source": [ - "#### Put it all together\n", - "We combine the various steps defined above to create a long-range CX gate on two ends of a one-dimensional (1D) line. The steps include the following:\n", - "- Initializing the control qubit in $|+\\rangle$\n", - "- Preparing Bell pairs\n", - "- Measuring neighboring qubit pairs\n", - "- Applying feedforward corrections dependent on the MCMs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11fc8adc", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def lrcx(distance, prep_barrier=True, pre_measure_barrier=True):\n", - " qc = initialize_circuit(distance)\n", - " qc = prepare_bell_pairs(qc, prep_barrier)\n", - " qc = measure_bell_basis(qc, pre_measure_barrier)\n", - " qc = apply_ffwd_corrections(qc)\n", - " return qc\n", - "\n", - "\n", - "qc = lrcx(distance)\n", - "# Apply the measurement in the XX, YY, and ZZ bases\n", - "qc_XX, qc_YY, qc_ZZ = [\n", - " measure_in_basis(qc, basis=basis) for basis in [\"XX\", \"YY\", \"ZZ\"]\n", - "]\n", - "\n", - "qc_YY.draw(\n", - " output=\"mpl\", fold=-1, scale=0.5\n", - ") # Circuit for measuring in the YY basis" - ] - }, - { - "cell_type": "markdown", - "id": "2b1f3f70", - "metadata": {}, - "source": [ - "#### Unitary-based implementation swapping the qubits to the middle\n", - "\n", - "For comparison, we first examine the case where a long-range CNOT gate is implemented using nearest-neighbor connections and unitary gates. In the following figure, on the left is a circuit for a long-range CNOT gate spanning a 1D chain of n-qubits subject to nearest-neighbor connections only. In the middle is an equivalent unitary decomposition implementable with local CNOT gates, circuit depth $O(n)$.\n", - "\n", - "![Long-range CNOT circuit](/docs/images/tutorials/long-range-entanglement/dynamic_vs_unitary_long_range_illustration.avif)\n", - "\n", - "The circuit in the middle can be implemented as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "3f816591", - "metadata": {}, - "outputs": [], - "source": [ - "def cnot_unitary(distance):\n", - " \"\"\"Generate a long range CNOT gate using local CNOTs on a 1D chain of qubits subject to n\n", - " nearest-neighbor connections only.\n", - "\n", - "\n", - " Args:\n", - " distance (int) : The distance of the CNOT gate, with the convention that a distance of 0 is a nearest-neighbor CNOT.\n", - "\n", - " Returns:\n", - " QuantumCircuit: A Quantum Circuit implementing a long-range CNOT gate between qubit 0 and qubit distance+1\n", - " \"\"\"\n", - " assert distance >= 0\n", - " n = distance # number of qubits between target and control\n", - "\n", - " qr = QuantumRegister(\n", - " n + 2, name=\"q\"\n", - " ) # Circuit with n qubits between control and target\n", - " cr = ClassicalRegister(\n", - " 2, name=\"cr\"\n", - " ) # Classical register for measuring control and target qubits\n", - "\n", - " qc = QuantumCircuit(qr, cr, name=\"CNOT_unitary\")\n", - "\n", - " control_qubit = 0\n", - "\n", - " qc.h(control_qubit) # Prepare the control qubit in the |+> state\n", - "\n", - " k = int(n / 2)\n", - " qc.barrier()\n", - " for i in range(control_qubit, control_qubit + k):\n", - " qc.cx(i, i + 1)\n", - " qc.cx(i + 1, i)\n", - " qc.cx(-i - 1, -i - 2)\n", - " qc.cx(-i - 2, -i - 1)\n", - " if n % 2 == 1:\n", - " qc.cx(k + 2, k + 1)\n", - " qc.cx(k + 1, k + 2)\n", - " qc.barrier()\n", - " qc.cx(k, k + 1)\n", - " for i in range(control_qubit, control_qubit + k):\n", - " qc.cx(k - i, k - 1 - i)\n", - " qc.cx(k - 1 - i, k - i)\n", - " qc.cx(k + i + 1, k + i + 2)\n", - " qc.cx(k + i + 2, k + i + 1)\n", - " if n % 2 == 1:\n", - " qc.cx(-2, -1)\n", - " qc.cx(-1, -2)\n", - "\n", - " return qc\n", - "\n", - "\n", - "qc_uni = cnot_unitary(distance)" - ] - }, - { - "cell_type": "markdown", - "id": "c0c99c20", - "metadata": {}, - "source": [ - "Now build the circuits that measure in the $XX$, $YY$, and $ZZ$ bases, just like we did for the dynamic circuits above." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b899e143", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Apply the measurement in the XX, YY, and ZZ bases\n", - "qc_uni_XX, qc_uni_YY, qc_uni_ZZ = [\n", - " measure_in_basis(qc_uni, basis=basis) for basis in [\"XX\", \"YY\", \"ZZ\"]\n", - "]\n", - "\n", - "qc_uni_YY.draw(\n", - " output=\"mpl\", fold=-1, scale=0.5\n", - ") # Circuit for measuring in the YY basis" - ] - }, - { - "cell_type": "markdown", - "id": "81e55064", - "metadata": {}, - "source": [ - "Now that we have built both the dynamic circuits and the unitary circuits for a small-scale example with `distance=6`, we then transpile them to run first on a noiseless simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "091e9537", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer import AerSimulator\n", - "\n", - "aer_backend = AerSimulator()\n", - "pm_sim = generate_preset_pass_manager(\n", - " optimization_level=0, backend=aer_backend\n", - ")\n", - "\n", - "# Dynamic circuits\n", - "isa_sim_dyn = pm_sim.run([qc_XX, qc_YY, qc_ZZ])\n", - "\n", - "# Unitary circuits\n", - "isa_sim_uni = pm_sim.run([qc_uni_XX, qc_uni_YY, qc_uni_ZZ])" - ] - }, - { - "cell_type": "markdown", - "id": "fe2fce0e", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "We can now run the experiment on the noiseless simulator backend. We use the Qiskit Runtime Sampler with AerSimulator as the backend mode to execute the circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "689c9a46", - "metadata": {}, - "outputs": [], - "source": [ - "sampler_sim = Sampler(mode=aer_backend)\n", - "sim_job = sampler_sim.run(isa_sim_dyn + isa_sim_uni)\n", - "sim_results = sim_job.result()" - ] - }, - { - "cell_type": "markdown", - "id": "5287f612", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "After the experiments have successfully executed, we now post-process the measurement counts to extract meaningful metrics.\n", - "In this step, we do the following:\n", - "\n", - "- Define quality metrics for evaluating the performance of the long-range CX.\n", - "- Compute expectation values of Pauli operators from raw measurement outcomes.\n", - "- Use these to calculate the fidelity of the generated Bell state.\n", - "\n", - "In a noiseless simulation, we will verify that the fidelity metric is $1$ for the circuits constructed. In the experiments on the real QPUs, this analysis will provide a clear picture of how well the dynamic circuits perform relative to the unitary baseline implementation." - ] - }, - { - "cell_type": "markdown", - "id": "4ba303da", - "metadata": {}, - "source": [ - "#### Quality metrics\n", - "\n", - "To evaluate the success of the long-range CX protocol, we measure how close the output state is to the ideal Bell state. A convenient way to quantify this is by computing the state fidelity using expectation values of Pauli operators. We can compute fidelity for a Bell state on the control and target state after knowing the $\\braket{XX}$, $\\braket{YY}$, and $\\braket{ZZ}$. In particular,\n", - "\n", - "$$ F = \\frac{1}{4} (1 + \\braket{XX} - \\braket{YY} + \\braket{ZZ})$$\n", - "\n", - "To compute these expectation values from raw measurement data, we define a set of helper functions:\n", - "\n", - "- **`compute_ZZ_expectation`**: Given measurement counts, computes the expectation value of a two-qubit Pauli operator in the $Z$ basis.\n", - "- **`compute_fidelity`**: Combines the expectation values of $XX$, $YY$, and $ZZ$ into the fidelity expression above.\n", - "- **`get_counts_from_bitarray`**: Utility to extract counts from backend result objects." - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "5645a9be", - "metadata": {}, - "outputs": [], - "source": [ - "def compute_ZZ_expectation(counts):\n", - " total = sum(counts.values())\n", - " expectation = 0\n", - " for bitstring, count in counts.items():\n", - " # Ensure bitstring is 2 bits\n", - " z1 = (-1) ** (int(bitstring[-1]))\n", - " z2 = (-1) ** (int(bitstring[-2]))\n", - " expectation += z1 * z2 * count\n", - " return expectation / total\n", - "\n", - "\n", - "def compute_fidelity(counts_xx, counts_yy, counts_zz):\n", - " xx, yy, zz = [\n", - " compute_ZZ_expectation(c) for c in [counts_xx, counts_yy, counts_zz]\n", - " ]\n", - " return 1 / 4 * (1 + xx - yy + zz)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "ade0c4e6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dynamic fidelity (distance=6): 1.0000\n", - "Unitary fidelity (distance=6): 1.0000\n" - ] - } - ], - "source": [ - "# Dynamic fidelity\n", - "counts_xx = sim_results[0].data.cr.get_counts()\n", - "counts_yy = sim_results[1].data.cr.get_counts()\n", - "counts_zz = sim_results[2].data.cr.get_counts()\n", - "fidelity_dyn = compute_fidelity(counts_xx, counts_yy, counts_zz)\n", - "\n", - "# Unitary fidelity\n", - "counts_xx = sim_results[3].data.cr.get_counts()\n", - "counts_yy = sim_results[4].data.cr.get_counts()\n", - "counts_zz = sim_results[5].data.cr.get_counts()\n", - "fidelity_uni = compute_fidelity(counts_xx, counts_yy, counts_zz)\n", - "\n", - "print(f\"Dynamic fidelity (distance={distance}): {fidelity_dyn:.4f}\")\n", - "print(f\"Unitary fidelity (distance={distance}): {fidelity_uni:.4f}\")" - ] - }, - { - "cell_type": "markdown", - "id": "aaa66ebb", - "metadata": {}, - "source": [ - "As expected in a noiseless simulation, the fidelities in both dynamic circuits and unitary circuits are $1$." - ] - }, - { - "cell_type": "markdown", - "id": "9ac23eb2", - "metadata": {}, - "source": [ - "## Large-scale hardware example\n", - "\n", - "Here we now put all of these details together into a single workflow at a larger scale, which is then run on real quantum hardware." - ] - }, - { - "cell_type": "markdown", - "id": "ad4224f2", - "metadata": {}, - "source": [ - "### Generate circuits for different distances\n", - "\n", - "We now generate long-range CX circuits for a range of qubit separations up to 60 qubits apart. For each distance, we build circuits that measure in the $XX$, $YY$, and $ZZ$ bases, which will later be used to compute fidelities.\n", - "\n", - "The list of distances includes both short- and long-range separations, with `distance = 0` corresponding to a nearest-neighbor CX. These same distances will also be used to generate the corresponding unitary circuits later for comparison." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72c70b11", - "metadata": {}, - "outputs": [], - "source": [ - "# -------------------------Step 1-------------------------\n", - "distances = [\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 6,\n", - " 11,\n", - " 16,\n", - " 21,\n", - " 28,\n", - " 35,\n", - " 44,\n", - " 55,\n", - " 60,\n", - "] # Distances for long range CX. distance of 0 is a nearest-neighbor CX\n", - "distances.sort()\n", - "assert min(distances) >= 0\n", - "basis_list = [\"XX\", \"YY\", \"ZZ\"]\n", - "\n", - "# Dynamic circuits\n", - "circuits_dyn = []\n", - "for distance in distances:\n", - " for basis in basis_list:\n", - " circuits_dyn.append(\n", - " measure_in_basis(lrcx(distance, prep_barrier=False), basis=basis)\n", - " )\n", - "print(f\"Number of circuits: {len(circuits_dyn)}\")\n", - "\n", - "# Unitary circuits\n", - "circuits_uni = []\n", - "for distance in distances:\n", - " for basis in basis_list:\n", - " circuits_uni.append(\n", - " measure_in_basis(cnot_unitary(distance), basis=basis)\n", - " )\n", - "\n", - "print(f\"Number of circuits: {len(circuits_uni)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "698132a5", - "metadata": {}, - "source": [ - "Now that we have both dynamic and unitary circuits for a range of distances, we are ready for transpilation. We first need to select a backend device." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "e0476bef", - "metadata": {}, - "outputs": [], - "source": [ - "# -------------------------Step 2-------------------------\n", - "# Set up access to IBM Quantum devices\n", - "from qiskit.circuit import IfElseOp\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=156\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "dad36db5", - "metadata": {}, - "source": [ - "The following step ensures that the backend supports the `if_else` instruction, which is required for the newer version of dynamic circuits. Since this feature is still in early access, we explicitly add the `IfElseOp` to the backend target if it is not already available." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "66805cb4", - "metadata": {}, - "outputs": [], - "source": [ - "if \"if_else\" not in backend.target.operation_names:\n", - " backend.target.add_instruction(IfElseOp, name=\"if_else\")" - ] - }, - { - "cell_type": "markdown", - "id": "f15cf5e8", - "metadata": {}, - "source": [ - "#### Use Layer Fidelity string for selecting 1D chain\n", - "Since we want to compare the performance of dynamic and unitary circuits on a 1D chain, we use the Layer Fidelity string to select a linear topology of the best chain of qubits from the device. This ensures that both types of circuits are transpiled under the same connectivity constraints, allowing for a fair comparison of their performance." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "258e3fa1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 76, 81, 82, 83, 84, 85, 86, 87, 97, 107, 108, 109, 110, 111, 98, 91, 92, 93, 94, 95, 99, 115, 114, 113, 119, 133, 132, 131, 138, 151, 150, 149, 148]\n" - ] - } - ], - "source": [ - "# This selects best qubits for longest distance and uses the same control for all lengths\n", - "lf_qubits = backend.properties().to_dict()[\n", - " \"general_qlists\"\n", - "] # best linear chain qubits\n", - "chosen_layouts = {\n", - " distance: [\n", - " val[\"qubits\"]\n", - " for val in lf_qubits\n", - " if val[\"name\"] == f\"lf_{distances[-1] + 2}\"\n", - " ][0][: distance + 2]\n", - " for distance in distances\n", - "}\n", - "print(chosen_layouts[max(distances)]) # best qubits at each distance" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "5886f5c9", - "metadata": {}, - "outputs": [], - "source": [ - "isa_circuits_dyn = []\n", - "isa_circuits_uni = []\n", - "\n", - "# Using the same initial layouts for both circuits for better apples to apples comparison\n", - "for qc in circuits_dyn:\n", - " pm = generate_preset_pass_manager(\n", - " optimization_level=1,\n", - " backend=backend,\n", - " initial_layout=chosen_layouts[qc.num_qubits - 2],\n", - " )\n", - " isa_circuits_dyn.append(pm.run(qc))\n", - "\n", - "for qc in circuits_uni:\n", - " pm = generate_preset_pass_manager(\n", - " optimization_level=1,\n", - " backend=backend,\n", - " initial_layout=chosen_layouts[qc.num_qubits - 2],\n", - " )\n", - " isa_circuits_uni.append(pm.run(qc))" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "c77c3fd3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2Q depth: 2\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(\n", - " f\"2Q depth: {isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}\"\n", - ")\n", - "isa_circuits_dyn[14].draw(\"mpl\", fold=-1, idle_wires=0)" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "7e5fc240", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2Q depth: 13\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(\n", - " f\"2Q depth: {isa_circuits_uni[14].depth(lambda x: x.operation.num_qubits == 2)}\"\n", - ")\n", - "isa_circuits_uni[14].draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "b6995ce7", - "metadata": {}, - "source": [ - "### Visualize qubits used for the LRCX circuit\n", - "\n", - "In this section, we examine how the LRCX circuit is mapped onto hardware. We start by visualizing the physical qubits used in the circuit and then study how the control–target distance in the layout impacts the number of operations." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "2d090f8a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Note: the qubit coordinates must be hard-coded.\n", - "# The backend API does not currently provide this information directly.\n", - "# If using a different backend, you will need to adjust the coordinates accordingly,\n", - "# or set the qubit_coordinates = None to use the default layout coordinates.\n", - "\n", - "\n", - "def _heron_coords_r2():\n", - " \"\"\"Generate coordinates for the Heron layout in R2. Note\"\"\"\n", - " cord_map = np.array(\n", - " [\n", - " [\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 3,\n", - " 7,\n", - " 11,\n", - " 15,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 1,\n", - " 5,\n", - " 9,\n", - " 13,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 3,\n", - " 7,\n", - " 11,\n", - " 15,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 1,\n", - " 5,\n", - " 9,\n", - " 13,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 3,\n", - " 7,\n", - " 11,\n", - " 15,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 1,\n", - " 5,\n", - " 9,\n", - " 13,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " 3,\n", - " 7,\n", - " 11,\n", - " 15,\n", - " 0,\n", - " 1,\n", - " 2,\n", - " 3,\n", - " 4,\n", - " 5,\n", - " 6,\n", - " 7,\n", - " 8,\n", - " 9,\n", - " 10,\n", - " 11,\n", - " 12,\n", - " 13,\n", - " 14,\n", - " 15,\n", - " ],\n", - " -1\n", - " * np.array([j for i in range(15) for j in [i] * [16, 4][i % 2]]),\n", - " ],\n", - " dtype=int,\n", - " )\n", - "\n", - " hcords = []\n", - " ycords = cord_map[0]\n", - " xcords = cord_map[1]\n", - " for i in range(156):\n", - " hcords.append([xcords[i] + 1, np.abs(ycords[i]) + 1])\n", - "\n", - " return hcords\n", - "\n", - "\n", - "# Visualize the active qubits in the circuit layout\n", - "plot_circuit_layout(\n", - " circuit=isa_circuits_uni[-1],\n", - " backend=backend,\n", - " view=\"physical\",\n", - " qubit_coordinates=_heron_coords_r2(),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4d75038a", - "metadata": {}, - "source": [ - "Next, we execute the experiment on the real backend. We also make use of batching to efficiently run the experiment across multiple trials. Running repeated trials allows us to compute averages for a more accurate comparison between the unitary and dynamic methods, as well as to quantify their variability by comparing the deviations across runs." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "e6f7811d", - "metadata": {}, - "outputs": [], - "source": [ - "# -------------------------Step 3-------------------------\n", - "num_trials = 10\n", - "jobs_uni = []\n", - "jobs_dyn = []\n", - "with Batch(backend=backend) as batch:\n", - " sampler = Sampler(mode=batch)\n", - " sampler.options.environment.job_tags = [\"TUT_LRE\"]\n", - " for _ in range(num_trials):\n", - " jobs_uni.append(sampler.run(isa_circuits_uni, shots=1024))\n", - " jobs_dyn.append(sampler.run(isa_circuits_dyn, shots=1024))" - ] - }, - { - "cell_type": "markdown", - "id": "388b6a87", - "metadata": {}, - "source": [ - "We compute the fidelity for the dynamic long-range CX circuits. For each distance, we extract measurement outcomes in the $\\braket{XX}$, $\\braket{YY}$, and $\\braket{ZZ}$ bases. These results are combined using the previously defined helper functions to calculate the fidelity according to $F = \\tfrac{1}{4} \\big( 1 + \\langle XX \\rangle - \\langle YY \\rangle + \\langle ZZ \\rangle \\big)$. This provides the observed fidelity of the dynamically executed protocol at each distance." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "10245513", - "metadata": {}, - "outputs": [], - "source": [ - "# -------------------------Step 4-------------------------\n", - "fidelities_dyn = []\n", - "\n", - "# loop over trials\n", - "for job in jobs_dyn:\n", - " result_dyn = job.result()\n", - " trial_fidelities = []\n", - " # loop over all distances\n", - " for ind, dist in enumerate(distances):\n", - " counts_xx = result_dyn[ind * 3].data.cr.get_counts()\n", - " counts_yy = result_dyn[ind * 3 + 1].data.cr.get_counts()\n", - " counts_zz = result_dyn[ind * 3 + 2].data.cr.get_counts()\n", - " trial_fidelities.append(\n", - " compute_fidelity(counts_xx, counts_yy, counts_zz)\n", - " )\n", - " fidelities_dyn.append(trial_fidelities)\n", - "# average over trials for each distance\n", - "avg_fidelities_dyn = np.mean(fidelities_dyn, axis=0)\n", - "std_fidelities_dyn = np.std(fidelities_dyn, axis=0)" - ] - }, - { - "cell_type": "markdown", - "id": "dabcde16", - "metadata": {}, - "source": [ - "Now we compute the fidelity for the unitary long-range CX circuits, and we do it the same way as we did for the dynamic circuits above." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "decaf83a", - "metadata": {}, - "outputs": [], - "source": [ - "fidelities_uni = []\n", - "\n", - "# loop over trials\n", - "for job in jobs_uni:\n", - " result_uni = job.result()\n", - " trial_fidelities = []\n", - " # loop over all distances\n", - " for ind, dist in enumerate(distances):\n", - " counts_xx = result_uni[ind * 3].data.cr.get_counts()\n", - " counts_yy = result_uni[ind * 3 + 1].data.cr.get_counts()\n", - " counts_zz = result_uni[ind * 3 + 2].data.cr.get_counts()\n", - " trial_fidelities.append(\n", - " compute_fidelity(counts_xx, counts_yy, counts_zz)\n", - " )\n", - " fidelities_uni.append(trial_fidelities)\n", - "# average over trials for each distance\n", - "avg_fidelities_uni = np.mean(fidelities_uni, axis=0)\n", - "std_fidelities_uni = np.std(fidelities_uni, axis=0)" - ] - }, - { - "cell_type": "markdown", - "id": "a9dadc4e", - "metadata": {}, - "source": [ - "### Plot the results\n", - "To appreciate the results visually, the cell below plots the estimated gate fidelities measured at varying distance between entangled qubits for each method." - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "724da22d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "\n", - "# Unitary with error bars\n", - "ax.errorbar(\n", - " distances,\n", - " avg_fidelities_uni,\n", - " yerr=std_fidelities_uni,\n", - " fmt=\"o-.\",\n", - " color=\"c\",\n", - " ecolor=\"c\",\n", - " elinewidth=1,\n", - " capsize=4,\n", - " label=\"Unitary\",\n", - ")\n", - "# Dynamic with error bars\n", - "ax.errorbar(\n", - " distances,\n", - " avg_fidelities_dyn,\n", - " yerr=std_fidelities_dyn,\n", - " fmt=\"o-.\",\n", - " color=\"m\",\n", - " ecolor=\"m\",\n", - " elinewidth=1,\n", - " capsize=4,\n", - " label=\"Dynamic\",\n", - ")\n", - "# Random gate baseline\n", - "ax.axhline(y=1 / 4, linestyle=\"--\", color=\"gray\", label=\"Random gate\")\n", - "\n", - "legend = ax.legend(frameon=True)\n", - "for text in legend.get_texts():\n", - " text.set_color(\"black\")\n", - "legend.get_frame().set_facecolor(\"white\")\n", - "legend.get_frame().set_edgecolor(\"black\")\n", - "ax.set_title(\n", - " \"Bell State Fidelity vs Control–Target Separation\", color=\"black\"\n", - ")\n", - "ax.set_xlabel(\"Distance\", color=\"black\")\n", - "ax.set_ylabel(\"Bell state fidelity\", color=\"black\")\n", - "ax.grid(linestyle=\":\", linewidth=0.6, alpha=0.4, color=\"gray\")\n", - "ax.set_ylim((0.2, 1))\n", - "ax.set_facecolor(\"white\")\n", - "fig.patch.set_facecolor(\"white\")\n", - "for spine in ax.spines.values():\n", - " spine.set_visible(True)\n", - " spine.set_color(\"black\")\n", - "ax.tick_params(axis=\"x\", colors=\"black\")\n", - "ax.tick_params(axis=\"y\", colors=\"black\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "bb2545e7", - "metadata": {}, - "source": [ - "From the fidelity plot above, the LRCX did not consistently outperform the direct unitary implementation. In fact, for short control–target separations, the unitary circuit achieved higher fidelity. However, at larger separations, the dynamic circuit begins to achieve better fidelity than the unitary implementation. This behavior is not unexpected on current hardware: while dynamic circuits reduce circuit depth by avoiding long SWAP chains, they introduce additional circuit time from mid-circuit measurements, classical feedforward, and control-path delays. The added latency increases decoherence and readout errors, which can outweigh the depth savings at short distances.\n", - "\n", - "Nevertheless, we observe a crossover point where the dynamic approach surpasses the unitary one. This is a direct result of the different scaling: the depth of the unitary circuit grows linearly with the distance between qubits, while the depth of the dynamic circuit remains constant.\n", - "\n", - "**Key points:**\n", - "- **Immediate benefit of dynamic circuits:** The main present-day motivation is reduced *two-qubit depth*, not necessarily improved fidelity.\n", - "- **Why fidelity can be worse today:** Increased circuit time from measurement and classical operations often dominates, especially when the control–target separation is small.\n", - "- **Looking forward:** As hardware improves, specifically faster readout, shorter classical control latency, and reduced mid-circuit overhead, we should expect these depth and duration reductions to translate into measurable fidelity gains." - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "3dcff343", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Compute metrics for each distance, skipping the basis circuits since they are identical for each distance\n", - "depths_2q_dyn = [\n", - " c.depth(lambda x: x.operation.num_qubits == 2)\n", - " for c in isa_circuits_dyn[::3]\n", - "]\n", - "meas_dyn = [\n", - " sum(1 for instr in c.data if instr.operation.name == \"measure\")\n", - " for c in isa_circuits_dyn[::3]\n", - "]\n", - "\n", - "depths_2q_uni = [\n", - " c.depth(lambda x: x.operation.num_qubits == 2)\n", - " for c in isa_circuits_uni[::3]\n", - "]\n", - "meas_uni = [\n", - " sum(1 for instr in c.data if instr.operation.name == \"measure\")\n", - " for c in isa_circuits_uni[::3]\n", - "]\n", - "\n", - "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n", - "\n", - "axes[0].plot(\n", - " distances, depths_2q_uni, \"o-.\", color=\"c\", label=\"Unitary (2Q depth)\"\n", - ")\n", - "axes[0].plot(\n", - " distances, depths_2q_dyn, \"o-.\", color=\"m\", label=\"Dynamic (2Q depth)\"\n", - ")\n", - "axes[0].set_xlabel(\"Number of qubits between control and target\")\n", - "axes[0].set_ylabel(\"Two-qubit depth\")\n", - "axes[0].grid(True, linestyle=\":\", linewidth=0.6, alpha=0.4)\n", - "axes[0].legend()\n", - "\n", - "axes[1].plot(\n", - " distances, meas_uni, \"o-.\", color=\"c\", label=\"Unitary (# measurements)\"\n", - ")\n", - "axes[1].plot(\n", - " distances, meas_dyn, \"o-.\", color=\"m\", label=\"Dynamic (# measurements)\"\n", - ")\n", - "axes[1].set_xlabel(\"Number of qubits between control and target\")\n", - "axes[1].set_ylabel(\"Number of measurements\")\n", - "axes[1].grid(True, linestyle=\":\", linewidth=0.6, alpha=0.4)\n", - "axes[1].legend()\n", - "\n", - "fig.suptitle(\"Scaling of Unitary vs Dynamic LRCX with Distance\", fontsize=12)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "55326bf8", - "metadata": {}, - "source": [ - "This two-qubit depth plot highlights the primary advantage of the LRCX implemented with dynamic circuits: performance remains essentially constant as the separation between control and target qubits increases. In contrast, the unitary implementation grows linearly with distance due to the required SWAP chains. Depth captures the logical scaling of two-qubit operations, while the measurement count reflects the additional overhead for dynamic circuits. These measurements are efficient, since they are performed in parallel, but they still introduce a fixed cost on today’s hardware.\n", - "\n", - "Why fidelity can be worse today: Increased circuit time from measurement and classical operations often dominates, especially when the control-target separation is small. For example, the average readout length on a Heron r2 processor is 2,280 ns, whereas its 2Q gate length is only 68 ns.\n", - "\n", - "As measurement and classical latencies improve, we expect the constant-depth and constant-measurement scaling of dynamic circuits to yield clear fidelity and runtime advantages on larger circuits." - ] - }, - { - "cell_type": "markdown", - "id": "dae8ad4a", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "If you found this work interesting, you might be interested in the following materials:\n", - "\n", - "- [Benchmark dynamic circuits with cut Bell pairs](/docs/tutorials/edc-cut-bell-pair-benchmarking)\n", - "- [Simulation of kicked Ising Hamiltonian with dynamic circuits](/docs/tutorials/dc-hex-ising)\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "cc5af2f9", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "\n", - "[1] Efficient Long-Range Entanglement using Dynamic Circuits, by\n", - "*Elisa Bäumer, Vinay Tripathi, Derek S. Wang, Patrick Rall, Edward H. Chen, Swarnadeep Majumder, Alireza Seif, Zlatko K. Minev*. IBM Quantum, (2023).\n", - "https://arxiv.org/abs/2308.13065" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44d97249", + "metadata": {}, + "source": [ + "---\n", + "title: Long-range entanglement with dynamic circuits\n", + "description: This tutorial implements a long-range CNOT using dynamic circuits with Bell pairs, measurement, and feedforward, and compares it to a direct unitary approach.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore fontsize hcords, ycords, xcords, ecolor, elinewidth, allcr, braket, frameon */}\n", + "\n", + "# Long-range entanglement with dynamic circuits\n", + "*Usage estimate: 4 minutes on a Heron r2 processor. (NOTE: This is an estimate only. Your runtime may vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "57f65bca", + "metadata": {}, + "source": [ + "## Learning outcomes\n", + "After completing this tutorial, you will have learned the following:\n", + "* How to implement a long-range CNOT gate using dynamic circuits with mid-circuit measurements (MCMs) and classical feedforward;\n", + "* How to implement the equivalent gate using a unitary SWAP-based approach;\n", + "* How to compare both approaches by measuring gate fidelity as a function of qubit distance." + ] + }, + { + "cell_type": "markdown", + "id": "748eee3b", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "We suggest that users are familiar with the following topics before going through this tutorial:\n", + "* [Basic quantum computing concepts](/learning/courses/basics-of-quantum-information), including Bell states, entanglement, and quantum gates;\n", + "* Familiarity with [dynamic circuits](/docs/guides/classical-feedforward-and-control-flow) (mid-circuit measurements and classical feedforward);\n", + "* Basic knowledge of [Qiskit SDK](/docs/guides) and [Qiskit Runtime](/docs/guides/compute-services#qiskit-runtime), and access to an [IBM Quantum® account](/docs/guides/cloud-setup)." + ] + }, + { + "cell_type": "markdown", + "id": "b05b669f", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "Long-range entanglement between distant qubits is challenging on devices with limited connectivity. This tutorial shows how dynamic circuits can generate such entanglement by implementing a long-range controlled-X (LRCX) gate using a measurement-based protocol.\n", + "\n", + "Following the approach by Elisa Bäumer et al. in [1](#ref-1), the method uses mid-circuit measurement and feedforward to achieve constant-depth gates regardless of qubit separation. It creates intermediate Bell pairs, measures one qubit from each pair, and applies classically conditioned gates to propagate entanglement across the device. This avoids long SWAP chains, reducing both circuit depth and exposure to two-qubit gate errors.\n", + "\n", + "In this notebook, we adapt the protocol for IBM Quantum hardware and benchmark its performance as a function of the control–target separation, comparing it against a unitary SWAP-based baseline." + ] + }, + { + "cell_type": "markdown", + "id": "c6cd3174", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.37 or later (`pip install qiskit-ibm-runtime`)\n", + "- Qiskit Aer v0.17 or later (`pip install qiskit-aer`)" + ] + }, + { + "cell_type": "markdown", + "id": "39e4440a", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5b0aa550", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n", + "from qiskit.circuit.classical import expr\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit.visualization import plot_circuit_layout\n", + "from qiskit_ibm_runtime import (\n", + " QiskitRuntimeService,\n", + " Batch,\n", + " SamplerV2 as Sampler,\n", + ")\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "6733ceeb", + "metadata": {}, + "source": [ + "## Small-scale simulator example\n", + "\n", + "Before running on the real QPU, we verify that both the dynamic and unitary circuits produce an ideal Bell state on a noiseless simulator. We use the Qiskit Runtime `Sampler` with `AerSimulator` as the backend mode, at a small distance of 6." + ] + }, + { + "cell_type": "markdown", + "id": "28145093", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "We now implement a long-range CNOT gate between two distant qubits, following the dynamic-circuit construction shown below (adapted from Fig. 1a in Ref. [1](#ref-1)). The key idea is to use a “bus” of ancilla qubits, initialized to $|0\\rangle$, to mediate long-range gate teleportation.\n", + "\n", + "![Long-range CNOT circuit](/docs/images/tutorials/long-range-entanglement/dynamic_vs_unitary_long_range_illustration.avif)\n", + "\n", + "As illustrated in the figure, the process works as follows:\n", + "1. Prepare a chain of Bell pairs connecting the control and target qubits via intermediate ancillas.\n", + "2. Perform Bell measurements between non-entangled neighboring qubits, swapping entanglement step-by-step until the control and target share a Bell pair.\n", + "3. Use this Bell pair for gate teleportation, turning a local CNOT into a deterministic long-range CNOT in constant depth.\n", + "\n", + "This approach replaces long SWAP chains with a constant-depth protocol, reducing exposure to two-qubit gate errors and making the operation scalable with device size.\n", + "\n", + "In what follows, we will first walk through the dynamic-circuit implementation of the LRCX circuit. At the end, we will also provide a unitary-based implementation for comparison, to highlight the advantages of dynamic circuits in this setting." + ] + }, + { + "cell_type": "markdown", + "id": "89597fcf", + "metadata": {}, + "source": [ + "#### Initialize circuit\n", + "\n", + "We begin with a simple quantum problem that will serve as the basis for comparison. Specifically, we initialize a circuit with a control qubit at index 0 and apply a Hadamard gate to it. This produces a superposition state that, when followed by a controlled-X operation, generates a Bell state $(|00\\rangle + |11\\rangle)/\\sqrt{2}$ between the control and target qubits.\n", + "\n", + "At this stage, we are not yet constructing the long-range controlled-X (LRCX) itself. Instead, our goal is to define a clear and minimal initial circuit that highlights the role of the LRCX. In Step 2, we will show how the LRCX can be implemented as an optimization using dynamic circuits, and compare its performance against a unitary equivalent. Importantly, the LRCX protocol can be applied to any initial circuit. Here we use this simple Hadamard setup for clarity of demonstration." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "0446b8e8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "distance = 6 # The distance of the CNOT gate, with the convention that a distance of zero is a nearest-neighbor CNOT.\n", + "\n", + "\n", + "def initialize_circuit(distance):\n", + " assert distance >= 0\n", + " control = 0 # control qubit\n", + " n = distance # number of qubits between target and control\n", + "\n", + " qr = QuantumRegister(\n", + " n + 2, name=\"q\"\n", + " ) # Circuit with n qubits between control and target\n", + " cr = ClassicalRegister(\n", + " 2, name=\"cr\"\n", + " ) # Classical register for measuring control and target qubits\n", + "\n", + " k = int(n / 2) # Number of Bell States to be used\n", + "\n", + " allcr = [cr]\n", + " if (\n", + " distance > 1\n", + " ): # This classical register will be used to store ZZ measurements. It is only used for long-range CX gates with distance > 1\n", + " c1 = ClassicalRegister(\n", + " k, name=\"c1\"\n", + " ) # Classical register needed for post processing\n", + " allcr.append(c1)\n", + " if (\n", + " distance > 0\n", + " ): # This classical register will be used to store XX measurements. It is only used if distance > 0\n", + " c2 = ClassicalRegister(\n", + " n - k, name=\"c2\"\n", + " ) # Classical register needed for post processing\n", + " allcr.append(c2)\n", + "\n", + " qc = QuantumCircuit(qr, *allcr, name=\"CNOT\")\n", + "\n", + " # Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a\n", + " # Bell state (|00> + |11>)/sqrt(2)\n", + " qc.h(control)\n", + "\n", + " return qc\n", + "\n", + "\n", + "qc = initialize_circuit(distance)\n", + "qc.draw(fold=-1, output=\"mpl\", scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "e761cfc1", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "In this step, we show how to construct the LRCX circuit using dynamic circuits. The goal is to optimize the circuit for execution on hardware by reducing depth compared to a purely unitary implementation. To illustrate the benefits, we will display both the dynamic LRCX construction and its unitary equivalent, and later compare their performance after transpilation. Importantly, while here we apply the LRCX to a simple Hadamard-initialized problem, the protocol can be applied to any circuit where a long-range CNOT is required.\n", + "\n", + "#### Prepare Bell pairs\n", + "We begin by creating a chain of Bell pairs along the path between the control and target qubits. If the distance is odd, we first apply a CNOT from the control to its neighbor, which is the CNOT that will be teleported. For an even distance, this CNOT will be applied after the Bell pair preparation step. The Bell pair chain then entangles successive pairs of qubits, establishing the resource needed to carry the control information across the device." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4df8ebba", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Determine where to start the Bell pair chain and add an extra CNOT when n is odd\n", + "def check_even(n: int) -> int:\n", + " \"\"\"Return 1 if n is even, else 2.\"\"\"\n", + " return 1 if n % 2 == 0 else 2\n", + "\n", + "\n", + "def prepare_bell_pairs(qc, add_barriers=True):\n", + " n = qc.num_qubits - 2 # number of qubits between target and control\n", + " k = int(n / 2)\n", + "\n", + " if add_barriers:\n", + " qc.barrier()\n", + "\n", + " x0 = check_even(n)\n", + " if n % 2 != 0:\n", + " qc.cx(0, 1)\n", + "\n", + " # Create k Bell pairs\n", + " for i in range(k):\n", + " qc.h(x0 + 2 * i)\n", + " qc.cx(x0 + 2 * i, x0 + 2 * i + 1)\n", + " return qc\n", + "\n", + "\n", + "qc = prepare_bell_pairs(qc)\n", + "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "b80ea657", + "metadata": {}, + "source": [ + "#### Measure neighboring qubit pairs in the Bell basis\n", + "Next, we measure *unentangled* neighboring qubits in the Bell basis (two-qubit measurements of $XX$ and $ZZ$). This creates a long-range Bell pair between the target qubit, and the qubit adjacent to the control (up to Pauli corrections, which will be implemented via feedforward in the next step). In parallel, we implement the entangling measurement that teleports the CNOT gate to act on the intended target qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8eed9e57", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def measure_bell_basis(qc, add_barriers=True):\n", + " n = qc.num_qubits - 2 # number of qubits between target and control\n", + " k = int(n / 2)\n", + "\n", + " if n > 1:\n", + " _, c1, c2 = qc.cregs\n", + " elif n > 0:\n", + " _, c2 = qc.cregs\n", + "\n", + " # Determine where to start the Bell pair chain and add an extra CNOT when n is odd\n", + " x0 = 1 if n % 2 == 0 else 2\n", + "\n", + " # Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be\n", + " # teleported, if n is even)\n", + " for i in range(k + 1):\n", + " qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)\n", + "\n", + " for i in range(1, k + x0):\n", + " if i == 1:\n", + " qc.h(2 * i + 1 - x0)\n", + " else:\n", + " qc.h(2 * i + 1 - x0)\n", + "\n", + " if add_barriers:\n", + " qc.barrier()\n", + "\n", + " # Map the ZZ measurements onto classical register c1\n", + " for i in range(k):\n", + " if i == 0:\n", + " qc.measure(2 * i + x0, c1[i])\n", + " else:\n", + " qc.measure(2 * i + x0, c1[i])\n", + "\n", + " # Map the XX measurements onto classical register c2\n", + " for i in range(1, k + x0):\n", + " if i == 1:\n", + " qc.measure(2 * i + 1 - x0, c2[i - 1])\n", + " else:\n", + " qc.measure(2 * i + 1 - x0, c2[i - 1])\n", + " return qc\n", + "\n", + "\n", + "qc = measure_bell_basis(qc)\n", + "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "f0d7059b", + "metadata": {}, + "source": [ + "#### Apply feedforward corrections to correct Pauli byproduct operators\n", + "\n", + "The Bell-basis measurements introduce Pauli byproducts that must be corrected using the recorded outcomes. This is done in two steps. First, we need to compute the parity of all $ZZ$ measurements, which is then used to conditionally apply an $X$ gate to the target qubit. Likewise, the parity of the $XX$ measurements is computed and used to conditionally apply a $Z$ gate to the control qubit.\n", + "\n", + "With the new classical expression framework in Qiskit, these parities can be computed directly in the classical processing layer of the circuit. Instead of applying a sequence of individual conditional gates for each measurement bit, we can build a single classical expression that represents the XOR (parity) of all relevant measurement outcomes. This expression is then used as the condition in a single `if_test` block, allowing the correction gates to be applied in constant depth. This approach both simplifies the circuit and ensures that the feedforward corrections do not introduce unnecessary additional latency." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4915791a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def apply_ffwd_corrections(qc):\n", + " control = 0 # control qubit\n", + " target = qc.num_qubits - 1 # target qubit\n", + " n = qc.num_qubits - 2 # number of qubits between target and control\n", + "\n", + " k = int(n / 2)\n", + " x0 = check_even(n)\n", + "\n", + " if n > 1:\n", + " _, c1, c2 = qc.cregs\n", + " elif n > 0:\n", + " _, c2 = qc.cregs\n", + "\n", + " # First, let's compute the parity of all ZZ measurements\n", + " for i in range(k):\n", + " if i == 0:\n", + " parity_ZZ = expr.lift(\n", + " c1[i]\n", + " ) # Store the value of the first ZZ measurement in parity_ZZ\n", + " else:\n", + " parity_ZZ = expr.bit_xor(\n", + " c1[i], parity_ZZ\n", + " ) # Successively compute the parity via XOR operations\n", + "\n", + " for i in range(1, k + x0):\n", + " if i == 1:\n", + " parity_XX = expr.lift(\n", + " c2[i - 1]\n", + " ) # Store the value of the first XX measurement in parity_XX\n", + " else:\n", + " parity_XX = expr.bit_xor(\n", + " c2[i - 1], parity_XX\n", + " ) # Successively compute the parity via XOR operations\n", + "\n", + " if n > 0:\n", + " with qc.if_test(parity_XX):\n", + " qc.z(control)\n", + "\n", + " if n > 1:\n", + " with qc.if_test(parity_ZZ):\n", + " qc.x(target)\n", + " return qc\n", + "\n", + "\n", + "qc = apply_ffwd_corrections(qc)\n", + "qc.draw(output=\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "6d22740b", + "metadata": {}, + "source": [ + "#### Measure control and target qubits\n", + "We define a helper function that enables measurement of the control and target qubits in the $XX$, $YY$, or $ZZ$ bases. For verifying the Bell state $(|00\\rangle + |11\\rangle)/\\sqrt{2}$, the expectation values of $XX$ and $ZZ$ should both be $+1$, since they are stabilizers of the state. The $YY$ measurement is also supported here and will be used below when computing the fidelity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d087d7c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def measure_in_basis(qc, basis=\"XX\", add_barrier=True):\n", + " control = 0 # control qubit\n", + " target = qc.num_qubits - 1 # target qubit\n", + "\n", + " assert basis in [\"XX\", \"YY\", \"ZZ\"]\n", + "\n", + " qc = (\n", + " qc.copy()\n", + " ) # We copy the circuit because we want to measure in different bases\n", + " cr = qc.cregs[0]\n", + "\n", + " if add_barrier:\n", + " qc.barrier()\n", + "\n", + " if basis == \"XX\":\n", + " qc.h(control)\n", + " qc.h(target)\n", + " elif basis == \"YY\":\n", + " qc.sdg(control)\n", + " qc.sdg(target)\n", + " qc.h(control)\n", + " qc.h(target)\n", + "\n", + " qc.measure(control, cr[0])\n", + " qc.measure(target, cr[1])\n", + " return qc\n", + "\n", + "\n", + "qc_YY = measure_in_basis(qc.copy(), basis=\"YY\")\n", + "qc_YY.draw(\n", + " output=\"mpl\", fold=-1, scale=0.5\n", + ") # Circuit for measuring in the YY basis" + ] + }, + { + "cell_type": "markdown", + "id": "072f8605", + "metadata": {}, + "source": [ + "#### Put it all together\n", + "We combine the various steps defined above to create a long-range CX gate on two ends of a one-dimensional (1D) line. The steps include the following:\n", + "- Initializing the control qubit in $|+\\rangle$\n", + "- Preparing Bell pairs\n", + "- Measuring neighboring qubit pairs\n", + "- Applying feedforward corrections dependent on the MCMs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11fc8adc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def lrcx(distance, prep_barrier=True, pre_measure_barrier=True):\n", + " qc = initialize_circuit(distance)\n", + " qc = prepare_bell_pairs(qc, prep_barrier)\n", + " qc = measure_bell_basis(qc, pre_measure_barrier)\n", + " qc = apply_ffwd_corrections(qc)\n", + " return qc\n", + "\n", + "\n", + "qc = lrcx(distance)\n", + "# Apply the measurement in the XX, YY, and ZZ bases\n", + "qc_XX, qc_YY, qc_ZZ = [\n", + " measure_in_basis(qc, basis=basis) for basis in [\"XX\", \"YY\", \"ZZ\"]\n", + "]\n", + "\n", + "qc_YY.draw(\n", + " output=\"mpl\", fold=-1, scale=0.5\n", + ") # Circuit for measuring in the YY basis" + ] + }, + { + "cell_type": "markdown", + "id": "2b1f3f70", + "metadata": {}, + "source": [ + "#### Unitary-based implementation swapping the qubits to the middle\n", + "\n", + "For comparison, we first examine the case where a long-range CNOT gate is implemented using nearest-neighbor connections and unitary gates. In the following figure, on the left is a circuit for a long-range CNOT gate spanning a 1D chain of n-qubits subject to nearest-neighbor connections only. In the middle is an equivalent unitary decomposition implementable with local CNOT gates, circuit depth $O(n)$.\n", + "\n", + "![Long-range CNOT circuit](/docs/images/tutorials/long-range-entanglement/dynamic_vs_unitary_long_range_illustration.avif)\n", + "\n", + "The circuit in the middle can be implemented as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "3f816591", + "metadata": {}, + "outputs": [], + "source": [ + "def cnot_unitary(distance):\n", + " \"\"\"Generate a long range CNOT gate using local CNOTs on a 1D chain of qubits subject to n\n", + " nearest-neighbor connections only.\n", + "\n", + "\n", + " Args:\n", + " distance (int) : The distance of the CNOT gate, with the convention that a distance of 0 is a nearest-neighbor CNOT.\n", + "\n", + " Returns:\n", + " QuantumCircuit: A Quantum Circuit implementing a long-range CNOT gate between qubit 0 and qubit distance+1\n", + " \"\"\"\n", + " assert distance >= 0\n", + " n = distance # number of qubits between target and control\n", + "\n", + " qr = QuantumRegister(\n", + " n + 2, name=\"q\"\n", + " ) # Circuit with n qubits between control and target\n", + " cr = ClassicalRegister(\n", + " 2, name=\"cr\"\n", + " ) # Classical register for measuring control and target qubits\n", + "\n", + " qc = QuantumCircuit(qr, cr, name=\"CNOT_unitary\")\n", + "\n", + " control_qubit = 0\n", + "\n", + " qc.h(control_qubit) # Prepare the control qubit in the |+> state\n", + "\n", + " k = int(n / 2)\n", + " qc.barrier()\n", + " for i in range(control_qubit, control_qubit + k):\n", + " qc.cx(i, i + 1)\n", + " qc.cx(i + 1, i)\n", + " qc.cx(-i - 1, -i - 2)\n", + " qc.cx(-i - 2, -i - 1)\n", + " if n % 2 == 1:\n", + " qc.cx(k + 2, k + 1)\n", + " qc.cx(k + 1, k + 2)\n", + " qc.barrier()\n", + " qc.cx(k, k + 1)\n", + " for i in range(control_qubit, control_qubit + k):\n", + " qc.cx(k - i, k - 1 - i)\n", + " qc.cx(k - 1 - i, k - i)\n", + " qc.cx(k + i + 1, k + i + 2)\n", + " qc.cx(k + i + 2, k + i + 1)\n", + " if n % 2 == 1:\n", + " qc.cx(-2, -1)\n", + " qc.cx(-1, -2)\n", + "\n", + " return qc\n", + "\n", + "\n", + "qc_uni = cnot_unitary(distance)" + ] + }, + { + "cell_type": "markdown", + "id": "c0c99c20", + "metadata": {}, + "source": [ + "Now build the circuits that measure in the $XX$, $YY$, and $ZZ$ bases, just like we did for the dynamic circuits above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b899e143", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Apply the measurement in the XX, YY, and ZZ bases\n", + "qc_uni_XX, qc_uni_YY, qc_uni_ZZ = [\n", + " measure_in_basis(qc_uni, basis=basis) for basis in [\"XX\", \"YY\", \"ZZ\"]\n", + "]\n", + "\n", + "qc_uni_YY.draw(\n", + " output=\"mpl\", fold=-1, scale=0.5\n", + ") # Circuit for measuring in the YY basis" + ] + }, + { + "cell_type": "markdown", + "id": "81e55064", + "metadata": {}, + "source": [ + "Now that we have built both the dynamic circuits and the unitary circuits for a small-scale example with `distance=6`, we then transpile them to run first on a noiseless simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "091e9537", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "\n", + "aer_backend = AerSimulator()\n", + "pm_sim = generate_preset_pass_manager(\n", + " optimization_level=0, backend=aer_backend\n", + ")\n", + "\n", + "# Dynamic circuits\n", + "isa_sim_dyn = pm_sim.run([qc_XX, qc_YY, qc_ZZ])\n", + "\n", + "# Unitary circuits\n", + "isa_sim_uni = pm_sim.run([qc_uni_XX, qc_uni_YY, qc_uni_ZZ])" + ] + }, + { + "cell_type": "markdown", + "id": "fe2fce0e", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "We can now run the experiment on the noiseless simulator backend. We use the Qiskit Runtime Sampler with AerSimulator as the backend mode to execute the circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "689c9a46", + "metadata": {}, + "outputs": [], + "source": [ + "sampler_sim = Sampler(mode=aer_backend)\n", + "sim_job = sampler_sim.run(isa_sim_dyn + isa_sim_uni)\n", + "sim_results = sim_job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "5287f612", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "After the experiments have successfully executed, we now post-process the measurement counts to extract meaningful metrics.\n", + "In this step, we do the following:\n", + "\n", + "- Define quality metrics for evaluating the performance of the long-range CX.\n", + "- Compute expectation values of Pauli operators from raw measurement outcomes.\n", + "- Use these to calculate the fidelity of the generated Bell state.\n", + "\n", + "In a noiseless simulation, we will verify that the fidelity metric is $1$ for the circuits constructed. In the experiments on the real QPUs, this analysis will provide a clear picture of how well the dynamic circuits perform relative to the unitary baseline implementation." + ] + }, + { + "cell_type": "markdown", + "id": "4ba303da", + "metadata": {}, + "source": [ + "#### Quality metrics\n", + "\n", + "To evaluate the success of the long-range CX protocol, we measure how close the output state is to the ideal Bell state. A convenient way to quantify this is by computing the state fidelity using expectation values of Pauli operators. We can compute fidelity for a Bell state on the control and target state after knowing the $\\braket{XX}$, $\\braket{YY}$, and $\\braket{ZZ}$. In particular,\n", + "\n", + "$$ F = \\frac{1}{4} (1 + \\braket{XX} - \\braket{YY} + \\braket{ZZ})$$\n", + "\n", + "To compute these expectation values from raw measurement data, we define a set of helper functions:\n", + "\n", + "- **`compute_ZZ_expectation`**: Given measurement counts, computes the expectation value of a two-qubit Pauli operator in the $Z$ basis.\n", + "- **`compute_fidelity`**: Combines the expectation values of $XX$, $YY$, and $ZZ$ into the fidelity expression above.\n", + "- **`get_counts_from_bitarray`**: Utility to extract counts from backend result objects." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "5645a9be", + "metadata": {}, + "outputs": [], + "source": [ + "def compute_ZZ_expectation(counts):\n", + " total = sum(counts.values())\n", + " expectation = 0\n", + " for bitstring, count in counts.items():\n", + " # Ensure bitstring is 2 bits\n", + " z1 = (-1) ** (int(bitstring[-1]))\n", + " z2 = (-1) ** (int(bitstring[-2]))\n", + " expectation += z1 * z2 * count\n", + " return expectation / total\n", + "\n", + "\n", + "def compute_fidelity(counts_xx, counts_yy, counts_zz):\n", + " xx, yy, zz = [\n", + " compute_ZZ_expectation(c) for c in [counts_xx, counts_yy, counts_zz]\n", + " ]\n", + " return 1 / 4 * (1 + xx - yy + zz)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "ade0c4e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dynamic fidelity (distance=6): 1.0000\n", + "Unitary fidelity (distance=6): 1.0000\n" + ] + } + ], + "source": [ + "# Dynamic fidelity\n", + "counts_xx = sim_results[0].data.cr.get_counts()\n", + "counts_yy = sim_results[1].data.cr.get_counts()\n", + "counts_zz = sim_results[2].data.cr.get_counts()\n", + "fidelity_dyn = compute_fidelity(counts_xx, counts_yy, counts_zz)\n", + "\n", + "# Unitary fidelity\n", + "counts_xx = sim_results[3].data.cr.get_counts()\n", + "counts_yy = sim_results[4].data.cr.get_counts()\n", + "counts_zz = sim_results[5].data.cr.get_counts()\n", + "fidelity_uni = compute_fidelity(counts_xx, counts_yy, counts_zz)\n", + "\n", + "print(f\"Dynamic fidelity (distance={distance}): {fidelity_dyn:.4f}\")\n", + "print(f\"Unitary fidelity (distance={distance}): {fidelity_uni:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "aaa66ebb", + "metadata": {}, + "source": [ + "As expected in a noiseless simulation, the fidelities in both dynamic circuits and unitary circuits are $1$." + ] + }, + { + "cell_type": "markdown", + "id": "9ac23eb2", + "metadata": {}, + "source": [ + "## Large-scale hardware example\n", + "\n", + "Here we now put all of these details together into a single workflow at a larger scale, which is then run on real quantum hardware." + ] + }, + { + "cell_type": "markdown", + "id": "ad4224f2", + "metadata": {}, + "source": [ + "### Generate circuits for different distances\n", + "\n", + "We now generate long-range CX circuits for a range of qubit separations up to 60 qubits apart. For each distance, we build circuits that measure in the $XX$, $YY$, and $ZZ$ bases, which will later be used to compute fidelities.\n", + "\n", + "The list of distances includes both short- and long-range separations, with `distance = 0` corresponding to a nearest-neighbor CX. These same distances will also be used to generate the corresponding unitary circuits later for comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72c70b11", + "metadata": {}, + "outputs": [], + "source": [ + "# -------------------------Step 1-------------------------\n", + "distances = [\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 6,\n", + " 11,\n", + " 16,\n", + " 21,\n", + " 28,\n", + " 35,\n", + " 44,\n", + " 55,\n", + " 60,\n", + "] # Distances for long range CX. distance of 0 is a nearest-neighbor CX\n", + "distances.sort()\n", + "assert min(distances) >= 0\n", + "basis_list = [\"XX\", \"YY\", \"ZZ\"]\n", + "\n", + "# Dynamic circuits\n", + "circuits_dyn = []\n", + "for distance in distances:\n", + " for basis in basis_list:\n", + " circuits_dyn.append(\n", + " measure_in_basis(lrcx(distance, prep_barrier=False), basis=basis)\n", + " )\n", + "print(f\"Number of circuits: {len(circuits_dyn)}\")\n", + "\n", + "# Unitary circuits\n", + "circuits_uni = []\n", + "for distance in distances:\n", + " for basis in basis_list:\n", + " circuits_uni.append(\n", + " measure_in_basis(cnot_unitary(distance), basis=basis)\n", + " )\n", + "\n", + "print(f\"Number of circuits: {len(circuits_uni)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "698132a5", + "metadata": {}, + "source": [ + "Now that we have both dynamic and unitary circuits for a range of distances, we are ready for transpilation. We first need to select a backend device." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "e0476bef", + "metadata": {}, + "outputs": [], + "source": [ + "# -------------------------Step 2-------------------------\n", + "# Set up access to IBM Quantum devices\n", + "from qiskit.circuit import IfElseOp\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=156\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "dad36db5", + "metadata": {}, + "source": [ + "The following step ensures that the backend supports the `if_else` instruction, which is required for the newer version of dynamic circuits. Since this feature is still in early access, we explicitly add the `IfElseOp` to the backend target if it is not already available." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "66805cb4", + "metadata": {}, + "outputs": [], + "source": [ + "if \"if_else\" not in backend.target.operation_names:\n", + " backend.target.add_instruction(IfElseOp, name=\"if_else\")" + ] + }, + { + "cell_type": "markdown", + "id": "f15cf5e8", + "metadata": {}, + "source": [ + "#### Use Layer Fidelity string for selecting 1D chain\n", + "Since we want to compare the performance of dynamic and unitary circuits on a 1D chain, we use the Layer Fidelity string to select a linear topology of the best chain of qubits from the device. This ensures that both types of circuits are transpiled under the same connectivity constraints, allowing for a fair comparison of their performance." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "258e3fa1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 76, 81, 82, 83, 84, 85, 86, 87, 97, 107, 108, 109, 110, 111, 98, 91, 92, 93, 94, 95, 99, 115, 114, 113, 119, 133, 132, 131, 138, 151, 150, 149, 148]\n" + ] + } + ], + "source": [ + "# This selects best qubits for longest distance and uses the same control for all lengths\n", + "lf_qubits = backend.properties().to_dict()[\n", + " \"general_qlists\"\n", + "] # best linear chain qubits\n", + "chosen_layouts = {\n", + " distance: [\n", + " val[\"qubits\"]\n", + " for val in lf_qubits\n", + " if val[\"name\"] == f\"lf_{distances[-1] + 2}\"\n", + " ][0][: distance + 2]\n", + " for distance in distances\n", + "}\n", + "print(chosen_layouts[max(distances)]) # best qubits at each distance" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "5886f5c9", + "metadata": {}, + "outputs": [], + "source": [ + "isa_circuits_dyn = []\n", + "isa_circuits_uni = []\n", + "\n", + "# Using the same initial layouts for both circuits for better apples to apples comparison\n", + "for qc in circuits_dyn:\n", + " pm = generate_preset_pass_manager(\n", + " optimization_level=1,\n", + " backend=backend,\n", + " initial_layout=chosen_layouts[qc.num_qubits - 2],\n", + " )\n", + " isa_circuits_dyn.append(pm.run(qc))\n", + "\n", + "for qc in circuits_uni:\n", + " pm = generate_preset_pass_manager(\n", + " optimization_level=1,\n", + " backend=backend,\n", + " initial_layout=chosen_layouts[qc.num_qubits - 2],\n", + " )\n", + " isa_circuits_uni.append(pm.run(qc))" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "c77c3fd3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2Q depth: 2\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(\n", + " f\"2Q depth: {isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}\"\n", + ")\n", + "isa_circuits_dyn[14].draw(\"mpl\", fold=-1, idle_wires=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "7e5fc240", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2Q depth: 13\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(\n", + " f\"2Q depth: {isa_circuits_uni[14].depth(lambda x: x.operation.num_qubits == 2)}\"\n", + ")\n", + "isa_circuits_uni[14].draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "b6995ce7", + "metadata": {}, + "source": [ + "### Visualize qubits used for the LRCX circuit\n", + "\n", + "In this section, we examine how the LRCX circuit is mapped onto hardware. We start by visualizing the physical qubits used in the circuit and then study how the control–target distance in the layout impacts the number of operations." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "2d090f8a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Note: the qubit coordinates must be hard-coded.\n", + "# The backend API does not currently provide this information directly.\n", + "# If using a different backend, you will need to adjust the coordinates accordingly,\n", + "# or set the qubit_coordinates = None to use the default layout coordinates.\n", + "\n", + "\n", + "def _heron_coords_r2():\n", + " \"\"\"Generate coordinates for the Heron layout in R2. Note\"\"\"\n", + " cord_map = np.array(\n", + " [\n", + " [\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 3,\n", + " 7,\n", + " 11,\n", + " 15,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 1,\n", + " 5,\n", + " 9,\n", + " 13,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 3,\n", + " 7,\n", + " 11,\n", + " 15,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 1,\n", + " 5,\n", + " 9,\n", + " 13,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 3,\n", + " 7,\n", + " 11,\n", + " 15,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 1,\n", + " 5,\n", + " 9,\n", + " 13,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " 3,\n", + " 7,\n", + " 11,\n", + " 15,\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 3,\n", + " 4,\n", + " 5,\n", + " 6,\n", + " 7,\n", + " 8,\n", + " 9,\n", + " 10,\n", + " 11,\n", + " 12,\n", + " 13,\n", + " 14,\n", + " 15,\n", + " ],\n", + " -1\n", + " * np.array([j for i in range(15) for j in [i] * [16, 4][i % 2]]),\n", + " ],\n", + " dtype=int,\n", + " )\n", + "\n", + " hcords = []\n", + " ycords = cord_map[0]\n", + " xcords = cord_map[1]\n", + " for i in range(156):\n", + " hcords.append([xcords[i] + 1, np.abs(ycords[i]) + 1])\n", + "\n", + " return hcords\n", + "\n", + "\n", + "# Visualize the active qubits in the circuit layout\n", + "plot_circuit_layout(\n", + " circuit=isa_circuits_uni[-1],\n", + " backend=backend,\n", + " view=\"physical\",\n", + " qubit_coordinates=_heron_coords_r2(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4d75038a", + "metadata": {}, + "source": [ + "Next, we execute the experiment on the real backend. We also make use of batching to efficiently run the experiment across multiple trials. Running repeated trials allows us to compute averages for a more accurate comparison between the unitary and dynamic methods, as well as to quantify their variability by comparing the deviations across runs." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "e6f7811d", + "metadata": {}, + "outputs": [], + "source": [ + "# -------------------------Step 3-------------------------\n", + "num_trials = 10\n", + "jobs_uni = []\n", + "jobs_dyn = []\n", + "with Batch(backend=backend) as batch:\n", + " sampler = Sampler(mode=batch)\n", + " sampler.options.environment.job_tags = [\"TUT_LRE\"]\n", + " for _ in range(num_trials):\n", + " jobs_uni.append(sampler.run(isa_circuits_uni, shots=1024))\n", + " jobs_dyn.append(sampler.run(isa_circuits_dyn, shots=1024))" + ] + }, + { + "cell_type": "markdown", + "id": "388b6a87", + "metadata": {}, + "source": [ + "We compute the fidelity for the dynamic long-range CX circuits. For each distance, we extract measurement outcomes in the $\\braket{XX}$, $\\braket{YY}$, and $\\braket{ZZ}$ bases. These results are combined using the previously defined helper functions to calculate the fidelity according to $F = \\tfrac{1}{4} \\big( 1 + \\langle XX \\rangle - \\langle YY \\rangle + \\langle ZZ \\rangle \\big)$. This provides the observed fidelity of the dynamically executed protocol at each distance." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "10245513", + "metadata": {}, + "outputs": [], + "source": [ + "# -------------------------Step 4-------------------------\n", + "fidelities_dyn = []\n", + "\n", + "# loop over trials\n", + "for job in jobs_dyn:\n", + " result_dyn = job.result()\n", + " trial_fidelities = []\n", + " # loop over all distances\n", + " for ind, dist in enumerate(distances):\n", + " counts_xx = result_dyn[ind * 3].data.cr.get_counts()\n", + " counts_yy = result_dyn[ind * 3 + 1].data.cr.get_counts()\n", + " counts_zz = result_dyn[ind * 3 + 2].data.cr.get_counts()\n", + " trial_fidelities.append(\n", + " compute_fidelity(counts_xx, counts_yy, counts_zz)\n", + " )\n", + " fidelities_dyn.append(trial_fidelities)\n", + "# average over trials for each distance\n", + "avg_fidelities_dyn = np.mean(fidelities_dyn, axis=0)\n", + "std_fidelities_dyn = np.std(fidelities_dyn, axis=0)" + ] + }, + { + "cell_type": "markdown", + "id": "dabcde16", + "metadata": {}, + "source": [ + "Now we compute the fidelity for the unitary long-range CX circuits, and we do it the same way as we did for the dynamic circuits above." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "decaf83a", + "metadata": {}, + "outputs": [], + "source": [ + "fidelities_uni = []\n", + "\n", + "# loop over trials\n", + "for job in jobs_uni:\n", + " result_uni = job.result()\n", + " trial_fidelities = []\n", + " # loop over all distances\n", + " for ind, dist in enumerate(distances):\n", + " counts_xx = result_uni[ind * 3].data.cr.get_counts()\n", + " counts_yy = result_uni[ind * 3 + 1].data.cr.get_counts()\n", + " counts_zz = result_uni[ind * 3 + 2].data.cr.get_counts()\n", + " trial_fidelities.append(\n", + " compute_fidelity(counts_xx, counts_yy, counts_zz)\n", + " )\n", + " fidelities_uni.append(trial_fidelities)\n", + "# average over trials for each distance\n", + "avg_fidelities_uni = np.mean(fidelities_uni, axis=0)\n", + "std_fidelities_uni = np.std(fidelities_uni, axis=0)" + ] + }, + { + "cell_type": "markdown", + "id": "a9dadc4e", + "metadata": {}, + "source": [ + "### Plot the results\n", + "To appreciate the results visually, the cell below plots the estimated gate fidelities measured at varying distance between entangled qubits for each method." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "724da22d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "\n", + "# Unitary with error bars\n", + "ax.errorbar(\n", + " distances,\n", + " avg_fidelities_uni,\n", + " yerr=std_fidelities_uni,\n", + " fmt=\"o-.\",\n", + " color=\"c\",\n", + " ecolor=\"c\",\n", + " elinewidth=1,\n", + " capsize=4,\n", + " label=\"Unitary\",\n", + ")\n", + "# Dynamic with error bars\n", + "ax.errorbar(\n", + " distances,\n", + " avg_fidelities_dyn,\n", + " yerr=std_fidelities_dyn,\n", + " fmt=\"o-.\",\n", + " color=\"m\",\n", + " ecolor=\"m\",\n", + " elinewidth=1,\n", + " capsize=4,\n", + " label=\"Dynamic\",\n", + ")\n", + "# Random gate baseline\n", + "ax.axhline(y=1 / 4, linestyle=\"--\", color=\"gray\", label=\"Random gate\")\n", + "\n", + "legend = ax.legend(frameon=True)\n", + "for text in legend.get_texts():\n", + " text.set_color(\"black\")\n", + "legend.get_frame().set_facecolor(\"white\")\n", + "legend.get_frame().set_edgecolor(\"black\")\n", + "ax.set_title(\n", + " \"Bell State Fidelity vs Control–Target Separation\", color=\"black\"\n", + ")\n", + "ax.set_xlabel(\"Distance\", color=\"black\")\n", + "ax.set_ylabel(\"Bell state fidelity\", color=\"black\")\n", + "ax.grid(linestyle=\":\", linewidth=0.6, alpha=0.4, color=\"gray\")\n", + "ax.set_ylim((0.2, 1))\n", + "ax.set_facecolor(\"white\")\n", + "fig.patch.set_facecolor(\"white\")\n", + "for spine in ax.spines.values():\n", + " spine.set_visible(True)\n", + " spine.set_color(\"black\")\n", + "ax.tick_params(axis=\"x\", colors=\"black\")\n", + "ax.tick_params(axis=\"y\", colors=\"black\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bb2545e7", + "metadata": {}, + "source": [ + "From the fidelity plot above, the LRCX did not consistently outperform the direct unitary implementation. In fact, for short control–target separations, the unitary circuit achieved higher fidelity. However, at larger separations, the dynamic circuit begins to achieve better fidelity than the unitary implementation. This behavior is not unexpected on current hardware: while dynamic circuits reduce circuit depth by avoiding long SWAP chains, they introduce additional circuit time from mid-circuit measurements, classical feedforward, and control-path delays. The added latency increases decoherence and readout errors, which can outweigh the depth savings at short distances.\n", + "\n", + "Nevertheless, we observe a crossover point where the dynamic approach surpasses the unitary one. This is a direct result of the different scaling: the depth of the unitary circuit grows linearly with the distance between qubits, while the depth of the dynamic circuit remains constant.\n", + "\n", + "**Key points:**\n", + "- **Immediate benefit of dynamic circuits:** The main present-day motivation is reduced *two-qubit depth*, not necessarily improved fidelity.\n", + "- **Why fidelity can be worse today:** Increased circuit time from measurement and classical operations often dominates, especially when the control–target separation is small.\n", + "- **Looking forward:** As hardware improves, specifically faster readout, shorter classical control latency, and reduced mid-circuit overhead, we should expect these depth and duration reductions to translate into measurable fidelity gains." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "3dcff343", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Compute metrics for each distance, skipping the basis circuits since they are identical for each\n", + "# distance\n", + "depths_2q_dyn = [\n", + " c.depth(lambda x: x.operation.num_qubits == 2)\n", + " for c in isa_circuits_dyn[::3]\n", + "]\n", + "meas_dyn = [\n", + " sum(1 for instr in c.data if instr.operation.name == \"measure\")\n", + " for c in isa_circuits_dyn[::3]\n", + "]\n", + "\n", + "depths_2q_uni = [\n", + " c.depth(lambda x: x.operation.num_qubits == 2)\n", + " for c in isa_circuits_uni[::3]\n", + "]\n", + "meas_uni = [\n", + " sum(1 for instr in c.data if instr.operation.name == \"measure\")\n", + " for c in isa_circuits_uni[::3]\n", + "]\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n", + "\n", + "axes[0].plot(\n", + " distances, depths_2q_uni, \"o-.\", color=\"c\", label=\"Unitary (2Q depth)\"\n", + ")\n", + "axes[0].plot(\n", + " distances, depths_2q_dyn, \"o-.\", color=\"m\", label=\"Dynamic (2Q depth)\"\n", + ")\n", + "axes[0].set_xlabel(\"Number of qubits between control and target\")\n", + "axes[0].set_ylabel(\"Two-qubit depth\")\n", + "axes[0].grid(True, linestyle=\":\", linewidth=0.6, alpha=0.4)\n", + "axes[0].legend()\n", + "\n", + "axes[1].plot(\n", + " distances, meas_uni, \"o-.\", color=\"c\", label=\"Unitary (# measurements)\"\n", + ")\n", + "axes[1].plot(\n", + " distances, meas_dyn, \"o-.\", color=\"m\", label=\"Dynamic (# measurements)\"\n", + ")\n", + "axes[1].set_xlabel(\"Number of qubits between control and target\")\n", + "axes[1].set_ylabel(\"Number of measurements\")\n", + "axes[1].grid(True, linestyle=\":\", linewidth=0.6, alpha=0.4)\n", + "axes[1].legend()\n", + "\n", + "fig.suptitle(\"Scaling of Unitary vs Dynamic LRCX with Distance\", fontsize=12)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "55326bf8", + "metadata": {}, + "source": [ + "This two-qubit depth plot highlights the primary advantage of the LRCX implemented with dynamic circuits: performance remains essentially constant as the separation between control and target qubits increases. In contrast, the unitary implementation grows linearly with distance due to the required SWAP chains. Depth captures the logical scaling of two-qubit operations, while the measurement count reflects the additional overhead for dynamic circuits. These measurements are efficient, since they are performed in parallel, but they still introduce a fixed cost on today’s hardware.\n", + "\n", + "Why fidelity can be worse today: Increased circuit time from measurement and classical operations often dominates, especially when the control-target separation is small. For example, the average readout length on a Heron r2 processor is 2,280 ns, whereas its 2Q gate length is only 68 ns.\n", + "\n", + "As measurement and classical latencies improve, we expect the constant-depth and constant-measurement scaling of dynamic circuits to yield clear fidelity and runtime advantages on larger circuits." + ] + }, + { + "cell_type": "markdown", + "id": "dae8ad4a", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "If you found this work interesting, you might be interested in the following materials:\n", + "\n", + "- [Benchmark dynamic circuits with cut Bell pairs](/docs/tutorials/edc-cut-bell-pair-benchmarking)\n", + "- [Simulation of kicked Ising Hamiltonian with dynamic circuits](/docs/tutorials/dc-hex-ising)\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "cc5af2f9", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "\n", + "[1] Efficient Long-Range Entanglement using Dynamic Circuits, by\n", + "*Elisa Bäumer, Vinay Tripathi, Derek S. Wang, Patrick Rall, Edward H. Chen, Swarnadeep Majumder, Alireza Seif, Zlatko K. Minev*. IBM Quantum, (2023).\n", + "https://arxiv.org/abs/2308.13065" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/nishimori-phase-transition.ipynb b/docs/tutorials/nishimori-phase-transition.ipynb index a5b3ac16fcf..a30a3836c83 100644 --- a/docs/tutorials/nishimori-phase-transition.ipynb +++ b/docs/tutorials/nishimori-phase-transition.ipynb @@ -1,717 +1,718 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "a10c2a41-ab48-4da5-bbe1-a16fd47906fe", - "metadata": {}, - "source": [ - "---\n", - "title: Nishimori phase transition\n", - "description: This tutorial demonstrates how to realize a Nishimori phase transition on an IBM quantum processor.\n", - "---\n", - "\n", - "\n", - "# Nishimori phase transition\n", - "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "838ac5c7-0819-4482-8875-c57db88ba82a", - "metadata": {}, - "source": [ - "## Background\n", - "This tutorial demonstrates how to realize a Nishimori phase transition on an IBM® quantum processor. This experiment was originally described in [*Realizing the Nishimori transition across the error threshold for constant-depth quantum circuits*](https://arxiv.org/abs/2309.02863).\n", - "\n", - "The Nishimori phase transition refers to the transition between short- and long-range ordered phases in the random-bond Ising model. On a quantum computer, the long-range ordered phase manifests as a state in which qubits are entangled across the entire device. This highly entangled state is prepared using the *generation of entanglement by measurement* (GEM) protocol. By utilizing mid-circuit measurements, the GEM protocol is able to entangle qubits across the entire device using circuits of only constant depth. This tutorial uses the implementation of the GEM protocol from the [GEM Suite](https://github.com/qiskit-community/gem-suite) software package." - ] - }, - { - "cell_type": "markdown", - "id": "4091005d-ecd5-40c9-b90f-263e49a4cc16", - "metadata": {}, - "source": [ - "## Requirements\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "\n", - "- Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.22 or later ( `pip install qiskit-ibm-runtime` )\n", - "- GEM Suite ( `pip install gem-suite` )" - ] - }, - { - "cell_type": "markdown", - "id": "210b8730-ca3e-46d9-b8b0-64c36f13df8e", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4ad70734-54f8-400a-8399-188ee8a0f3ac", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from collections import defaultdict\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "from gem_suite import PlaquetteLattice\n", - "from gem_suite.experiments import GemExperiment" - ] - }, - { - "cell_type": "markdown", - "id": "39f59f3a-eb2a-4160-9da2-2bb83c58771c", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "The GEM protocol works on a quantum processor with qubit connectivity described by a lattice. Today's IBM quantum processors use the [heavy hex lattice](https://www.ibm.com/quantum/blog/heavy-hex-lattice). The qubits of the processor are grouped into *plaquettes* based on which unit cell of the lattice they occupy. Because a qubit might occur in more than one unit cell, the plaquettes are not disjoint. On the heavy hex lattice, a plaquette contains 12 qubits. The plaquettes themselves also form lattice, where two plaquettes are connected if they share any qubits. On the heavy hex lattice, neighboring plaquettes share 3 qubits.\n", - "\n", - "In the GEM Suite software package, the fundamental class for implementing the GEM protocol is `PlaquetteLattice`, which represents the lattice of plaquettes (which is distinct from the heavy hex lattice). A `PlaquetteLattice` can be initialized from a qubit coupling map. Currently, only heavy hex coupling maps are supported.\n", - "\n", - "The following code cell initializes a plaquette lattice from the coupling map of a IBM quantum processor. The plaquette lattice does not always encompass the entire hardware. For example, `ibm_torino` has 133 total qubits, but the largest plaquette lattice that fits on the device uses only 125 of them, and comprises a total of 18 plaquettes. Similar can be observed for IBM Quantum® devices with different qubit counts as well." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "553dbafe-1778-4971-83c3-0408605b701d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of qubits in backend: 133\n", - "Number of qubits in plaquette lattice: 125\n", - "Number of plaquettes: 18\n" - ] - } - ], - "source": [ - "# QiskitRuntimeService.save_account(channel=\"ibm_quantum\", token=\"\", overwrite=True, set_as_default=True)\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ")\n", - "plaquette_lattice = PlaquetteLattice.from_coupling_map(backend.coupling_map)\n", - "\n", - "print(f\"Number of qubits in backend: {backend.num_qubits}\")\n", - "print(\n", - " f\"Number of qubits in plaquette lattice: {len(list(plaquette_lattice.qubits()))}\"\n", - ")\n", - "print(f\"Number of plaquettes: {len(list(plaquette_lattice.plaquettes()))}\")" - ] - }, - { - "cell_type": "markdown", - "id": "0bf43539-d782-44a9-ba4e-a1b6c7510803", - "metadata": {}, - "source": [ - "You can visualize the plaquette lattice by generating a diagram of its graph representation. In the diagram, the plaquettes are represented by labeled hexagons, and two plaquettes are connected by an edge if they share qubits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "625882a4-faeb-4d96-b441-c989f43c4dea", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plaquette_lattice.draw_plaquettes()" - ] - }, - { - "cell_type": "markdown", - "id": "2f0a6b73-d1d2-4117-80cf-6fe3adbb82c5", - "metadata": {}, - "source": [ - "You can retrieve information about individual plaquettes, such as the qubits they contain, using the `plaquettes` method." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e412a612-c7d5-4689-840a-2383dd538f06", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PyPlaquette(index=0, qubits=[0, 1, 2, 3, 4, 15, 16, 19, 20, 21, 22, 23], neighbors=[3, 1])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get a list of the plaquettes\n", - "plaquettes = list(plaquette_lattice.plaquettes())\n", - "# Display information about plaquette 0\n", - "plaquettes[0]" - ] - }, - { - "cell_type": "markdown", - "id": "75edf1b4-f95f-4ff7-9e3c-2e810711636f", - "metadata": {}, - "source": [ - "You can also produce a diagram of the underlying qubits that form the plaquette lattice." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a19d63ce-3572-4081-a008-c1332fbbe303", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plaquette_lattice.draw_qubits()" - ] - }, - { - "cell_type": "markdown", - "id": "e7b0f54b-5bc2-42bb-b4d4-1346f0a902e3", - "metadata": {}, - "source": [ - "In addition to the qubit labels and the edges indicating which qubits are connected, the diagram contains three additional pieces of information that are relevant to the GEM protocol:\n", - "- Each qubit is either shaded (gray) or unshaded. The shaded qubits are \"site\" qubits that represent the sites of the Ising model, and the unshaded qubits are \"bond\" qubits used to mediate interactions between the site qubits.\n", - "- Each site qubit is labeled either (A) or (B), indicating one of two roles a site qubit can play in the GEM protocol (the roles are explained later).\n", - "- Each edge is colored using one of six colors, thus partitioning the edges into six groups. This partitioning determines how two-qubit gates can be parallelized, as well as different scheduling patterns that are likely to incur different amounts of error on a noisy quantum processor. Because edges in a group are disjoint, a layer of two-qubit gates can be applied on those edges simultaneously. In fact, it is possible to partition the six colors into three groups of two colors such that the union of each group of two colors is still disjoint. Therefore, only three layers of two-qubit gates are needed to activate every edge. There are 12 ways to so partition the six colors, and each such partition yields a different 3-layer gate schedule.\n", - "\n", - "Now that you have created a plaquette lattice, the next step is to initialize a `GemExperiment` object, passing both the plaquette lattice and the backend that you intend to run the experiment on. The `GemExperiment` class manages the actual implementation of the GEM protocol, including generating circuits, submitting jobs, and analyzing the data. The following code cell initializes the experiment class while restricting the plaquette lattice to only two of the plaquettes (21 qubits), reducing the size of the experiment to ensure that the noise in the hardware doesn't overwhelm the signal." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "02357c6e-5c83-4ac0-811d-22602d9f33d5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(plaquette_lattice.filter([9, 12]), backend=backend)\n", - "\n", - "# visualize the plaquette lattice after filtering\n", - "plaquette_lattice.filter([9, 12]).draw_qubits()" - ] - }, - { - "cell_type": "markdown", - "id": "3234e018-a1a8-47b7-b10c-57d6f7f77ff1", - "metadata": {}, - "source": [ - "A GEM protocol circuit is built using the following steps:\n", - "1. Prepare the all-$|+\\rangle$ state by applying a Hadamard gate to every qubit.\n", - "2. Apply an $R_{ZZ}$ gate between every pair of connected qubits. This can be achieved using 3 layers of gates. Each $R_{ZZ}$ gate acts on a site qubit and a bond qubit. If the site qubit is labeled (B), then the angle is fixed to $\\frac{\\pi}{2}$. If the site qubit is labeled (A), then the angle is allowed to vary, producing different circuits. By default, the range of angles is set to 21 equally spaced points between $0$ and $\\frac{\\pi}{2}$, inclusive.\n", - "3. Measure each bond qubit in the Pauli $X$ basis. Since qubits are measured in the Pauli $Z$ basis, this can be accomplished by applying a Hadamard gate before measuring the qubit.\n", - "\n", - "Note that the paper cited in the introduction to this tutorial uses a different convention for the $R_{ZZ}$ angle, which differs from the convention used in this tutorial by a factor of 2.\n", - "\n", - "In step 3, only the bond qubits are measured. To understand what state the site qubits remain in, it is instructive to consider the case that the $R_{ZZ}$ angle applied to site qubits (A) in step 2 is equal to $\\frac{\\pi}{2}$. In this case, the site qubits are left in a highly entangled state similar to the GHZ state,\n", - "\n", - "$$\n", - "\\lvert \\text{GHZ} \\rangle = \\lvert 00 \\cdots 00 \\rangle + \\lvert 11 \\cdots 11 \\rangle.\n", - "$$\n", - "\n", - "Due to the randomness in the measurement outcomes, the actual state of the site qubits might be a different state with long-range order, for example, $\\lvert 00110 \\rangle + \\lvert 11001 \\rangle$. However, the GHZ state can be recovered by applying a decoding operation based on the measurement outcomes. When the $R_{ZZ}$ angle is tuned down from $\\frac{\\pi}{2}$, the long-range order can still be recovered up until a critical angle, which in the absence of noise, is approximately $0.3 \\pi$. Below this angle, the resulting state no longer exhibits long-range entanglement. This transition between the presence and absence of long-range order is the Nishimori phase transition.\n", - "\n", - "In the description above, the site qubits were left unmeasured, and the decoding operation can be performed by applying quantum gates. In the experiment as implemented in the GEM suite, which this tutorial follows, the site qubits are in fact measured, and the decoding operation is applied in a classical post-processing step.\n", - "\n", - "In the description above, the decoding operation can be performed by applying quantum gates to the site qubits to recover the quantum state. However, if the goal is to immediately measure the state, for example, for characterization purposes, then the site qubits are measured together with the bond qubits, and the decoding operation can be applied in a classical post-processing step. This is how the experiment is implemented in the GEM suite, which this tutorial follows.\n", - "\n", - "In addition to depending on the $R_{ZZ}$ angle in step 2, which by default sweeps across 21 values, the GEM protocol circuit also depends on the scheduling pattern used to implement the 3 layers of $R_{ZZ}$ gates. As discussed previously, there are 12 such scheduling patterns. Therefore, the total number of circuits in the experiment is $21 \\times 12 = 252$.\n", - "\n", - "The circuits of the experiment can be generated using the `circuits` method of the `GemExperiment` class." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "8e2ade62-9a57-42c3-9a85-3fe2dec3c426", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total number of circuits: 252\n" - ] - } - ], - "source": [ - "circuits = gem_exp.circuits()\n", - "print(f\"Total number of circuits: {len(circuits)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "56c3c656-dcfa-4ca0-9df9-2d97c187c685", - "metadata": {}, - "source": [ - "For the purposes of this tutorial, it is enough to consider just a single scheduling pattern. The following code cell restricts the experiment to the first scheduling pattern. As a result, the experiment only has 21 circuits, one for each $R_{ZZ}$ angle swept over." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4f8a2c73-752d-47b9-95d5-83439933fc08", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total number of circuits: 21\n", - "RZZ angles:\n", - "[0. 0.07853982 0.15707963 0.23561945 0.31415927 0.39269908\n", - " 0.4712389 0.54977871 0.62831853 0.70685835 0.78539816 0.86393798\n", - " 0.9424778 1.02101761 1.09955743 1.17809725 1.25663706 1.33517688\n", - " 1.41371669 1.49225651 1.57079633]\n" - ] - } - ], - "source": [ - "# Restrict experiment to the first scheduling pattern\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "\n", - "# There are less circuits now\n", - "circuits = gem_exp.circuits()\n", - "print(f\"Total number of circuits: {len(circuits)}\")\n", - "\n", - "# Print the RZZ angles swept over\n", - "print(f\"RZZ angles:\\n{gem_exp.parameters()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "7011dc79-8561-42ef-909b-00fd8be9ef34", - "metadata": {}, - "source": [ - "The following code cell draws a diagram of the circuit at index 5. To reduce the size of the diagram, the measurement gates at the end of the circuit are removed." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "fd57d483-c70b-4ad5-b309-15750ad38bac", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get the circuit at index 5\n", - "circuit = circuits[5]\n", - "# Remove the final measurements to ease visualization\n", - "circuit.remove_final_measurements()\n", - "# Draw the circuit\n", - "circuit.draw(\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "a3aa063b-44cf-49f3-9e12-23b2f6a1c85b", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "Transpiling quantum circuits for execution on hardware typically involves a [number of stages](/docs/guides/transpiler-stages). Typically, the stages that incur the most computational overhead are choosing the qubit layout, routing the two-qubit gates to conform to the qubit connectivity of the hardware, and optimizing the circuit to minimize its gate count and depth. In the GEM protocol, the layout and routing stages are unnecessary because the hardware connectivity is already incorporated into the design of the protocol. The circuits already have a qubit layout, and the two-qubit gates are already mapped onto native connections. Furthermore, in order to preserve the structure of the circuit as the $R_{ZZ}$ angle is varied, only very basic circuit optimization should be performed.\n", - "\n", - "The `GemExperiment` class transparently transpiles circuits when executing the experiment. The layout and routing stages are already overridden by default to do nothing, and circuit optimization is performed at a level that only optimizes single-qubit gates. However, you can override or pass additional options using the `set_transpile_options` method. For the sake of visualization, the following code cell manually transpiles the circuit displayed previously, and draws the transpiled circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "e9b99d48-8d33-46b5-bff5-480ab1c1c1f2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Demonstrate setting transpile options\n", - "gem_exp.set_transpile_options(\n", - " optimization_level=1 # This is the default optimization level\n", - ")\n", - "pass_manager = generate_preset_pass_manager(\n", - " backend=backend,\n", - " initial_layout=list(gem_exp.physical_qubits),\n", - " **dict(gem_exp.transpile_options),\n", - ")\n", - "transpiled = pass_manager.run(circuit)\n", - "transpiled.draw(\"mpl\", idle_wires=False, fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "8d0dcd59-54ef-4af8-9213-0784ef94b838", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "\n", - "To execute the GEM protocol circuits on the hardware, call the `run` method of the `GemExperiment` object. You can specify the number of shots you want to sample from each circuit. The `run` method returns an [ExperimentData](https://qiskit-community.github.io/qiskit-experiments/stubs/qiskit_experiments.framework.ExperimentData.html) object which you should save to a variable. Note that the `run` method only submits jobs without waiting for them to finish, so it is a non-blocking call." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "6171a383-dd58-4e3f-88aa-bbec7b5870df", - "metadata": {}, - "outputs": [], - "source": [ - "exp_data = gem_exp.run(shots=10_000)" - ] - }, - { - "cell_type": "markdown", - "id": "71e81552-0d33-4950-8d45-e6c0a8a056c9", - "metadata": {}, - "source": [ - "To wait for the results, call the `block_for_results` method of the `ExperimentData` object. This call will cause the interpreter to hang until the jobs are finished." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ed14a067-35ba-4ffc-8534-4ae5ec6bc4c9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ExperimentData(GemExperiment, d0d5880a-34c1-4aab-a7b6-c4f58516bc03, job_ids=['cwg12ptmptp00082khhg'], metadata=<5 items>, figure_names=['two_point_correlation.svg', 'normalized_variance.svg', 'plaquette_ops.svg', 'bond_ops.svg'])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.block_for_results()" - ] - }, - { - "cell_type": "markdown", - "id": "36e0570b-f091-45f2-bb83-143edbc3b433", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "\n", - "At an $R_{ZZ}$ angle of $\\frac{\\pi}{2}$, the decoded state would be the GHZ state in the absence of noise. The long-range order of the GHZ state can be visualized by plotting the magnetization of the measured bitstrings. The magnetization $M$ is defined as the sum of the single-qubit Pauli $Z$ operators,\n", - "$$\n", - "M = \\sum_{j=1}^N Z_j,\n", - "$$\n", - "where $N$ is the number of site qubits. Its value for a bitstring is equal to the difference between the number of zeros and the number of ones. Measuring the GHZ state yields the all zeros state or the all ones state with equal probability, so the magnetization would be $+N$ half of the time and $-N$ the other half of the time. In the presence of errors due to noise, other values would also appear, but if the noise is not too great, the distribution would still be peaked near $+N$ and $-N$.\n", - "\n", - "For the raw bitstrings before decoding, the distribution of the magnetization would be equivalent to that of uniformly random bitstrings, in the absence of noise.\n", - "\n", - "The following code cell plots the magnetization of the raw bitstrings and the decoded bitstrings at the $R_{ZZ}$ angle of $\\frac{\\pi}{2}$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8ead3582-16df-4616-836c-bdce867ad6b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0.5, 1.0, 'Magnetization distribution with and without decoding')" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def magnetization_distribution(\n", - " counts_dict: dict[str, int],\n", - ") -> dict[str, float]:\n", - " \"\"\"Compute magnetization distribution from counts dictionary.\"\"\"\n", - " # Construct dictionary from magnetization to count\n", - " mag_dist = defaultdict(float)\n", - " for bitstring, count in counts_dict.items():\n", - " mag = bitstring.count(\"0\") - bitstring.count(\"1\")\n", - " mag_dist[mag] += count\n", - " # Normalize\n", - " shots = sum(counts_dict.values())\n", - " for mag in mag_dist:\n", - " mag_dist[mag] /= shots\n", - " return mag_dist\n", - "\n", - "\n", - "# Get counts dictionaries with and without decoding\n", - "data = exp_data.data()\n", - "# Get the last data point, which is at the angle for the GHZ state\n", - "raw_counts = data[-1][\"counts\"]\n", - "# Without decoding\n", - "site_indices = [\n", - " i for i, q in enumerate(gem_exp.plaquettes.qubits()) if q.role == \"Site\"\n", - "]\n", - "site_raw_counts = defaultdict(int)\n", - "for key, val in raw_counts.items():\n", - " site_str = \"\".join(key[-1 - i] for i in site_indices)\n", - " site_raw_counts[site_str] += val\n", - "# With decoding\n", - "_, site_decoded_counts = gem_exp.plaquettes.decode_outcomes(\n", - " raw_counts, return_counts=True\n", - ")\n", - "\n", - "# Compute magnetization distribution\n", - "raw_magnetization = magnetization_distribution(site_raw_counts)\n", - "decoded_magnetization = magnetization_distribution(site_decoded_counts)\n", - "\n", - "# Plot\n", - "plt.bar(*zip(*raw_magnetization.items()), label=\"raw\")\n", - "plt.bar(*zip(*decoded_magnetization.items()), label=\"decoded\", width=0.3)\n", - "plt.legend()\n", - "plt.xlabel(\"Magnetization\")\n", - "plt.ylabel(\"Frequency\")\n", - "plt.title(\"Magnetization distribution with and without decoding\")" - ] - }, - { - "cell_type": "markdown", - "id": "90a7ae7a-5175-421f-bda9-bc6b986bdf5f", - "metadata": {}, - "source": [ - "To more rigorously characterize the long-range order, you can examine the average two-point correlation $f$, defined as\n", - "$$\n", - "f = \\frac{1}{N^2} \\left(\\langle M^2 \\rangle - \\langle M \\rangle ^2\\right).\n", - "$$\n", - "A higher value indicates a greater degree of entanglement. The `GemExperiment` class automatically computes this value for the decoded bitstrings as part of processing the experimental data. It stores a figure that is accessible via the `figure` method of the experiment data class. In this case, the name of the figure is `two_point_correlation`." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4ecb25c8-e572-49af-a879-9943039db131", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.figure(\"two_point_correlation\")" - ] - }, - { - "cell_type": "markdown", - "id": "f2f3e7e2-2a8b-4790-8ba7-b190c4ed1049", - "metadata": {}, - "source": [ - "To determine the critical point of the Nishimori phase transition, you can look at the normalized variance of $M^2 / N$, defined as\n", - "$$\n", - "g = \\frac{1}{N^3} \\left(\\langle M^4 \\rangle - \\langle M^2 \\rangle^2\\right),\n", - "$$\n", - "which quantifies the amount of fluctuation in the squared magnetization. This value is maximized at the critical point of the Nishimori phase transition. In the absence of noise, the critical point occurs at approximately $0.3 \\pi$. In the presence of noise, the critical point is shifted higher, but the phase transition is still observed as long as the critical point occurs below $0.5 \\pi$." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "2b351d68-3924-445a-94ef-047b16214e8a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "markdown", - "id": "a94e0cbe-8429-487c-b203-50a8b2eacee3", - "metadata": {}, - "source": [ - "## Scale up the experiment\n", - "\n", - "The following code cells run the experiment for six plaquettes (49 qubits) and the full 12 plaquettes (125 qubits) and plot the normalized variance. As the experiment is scaled to larger sizes, the greater amount of noise shifts the critical point rightwards." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "08581c09-a6a5-4a56-9fc4-abf22b063c6a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(\n", - " plaquette_lattice.filter(range(3, 9)), backend=backend\n", - ")\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "exp_data = gem_exp.run(shots=10_000)\n", - "exp_data.block_for_results()\n", - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "37e9a4cd-6efb-4ade-ad09-8139db9d58e9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(plaquette_lattice, backend=backend)\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "exp_data = gem_exp.run(shots=10_000)\n", - "exp_data.block_for_results()\n", - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "markdown", - "id": "6abd9701-58e4-43a8-a1d7-279506570de4", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "In this tutorial, you realized a Nishimori phase transition on a quantum processor using the GEM protocol. The metrics that you examined during post-processing, in particular the two-point correlation and the normalized variance, serve as benchmarks of the device's ability to generate long-range entangled states. These benchmarks extend the utility of the GEM protocol beyond probing interesting physics. As part of the protocol, you entangled qubits across the entire device using circuits of only constant depth. This feat is only possible due to the protocol's use of mid-circuit measurements. In this experiment, the entangled state was immediately measured, but an interesting avenue to explore would be to continue using the state in additional quantum processing!" - ] - }, - { - "cell_type": "markdown", - "id": "b9562a76", - "metadata": {}, - "source": [ - "## Tutorial survey\n", - "\n", - "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", - "\n", - "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_bsCKQkgzuQUQ7ky)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a10c2a41-ab48-4da5-bbe1-a16fd47906fe", + "metadata": {}, + "source": [ + "---\n", + "title: Nishimori phase transition\n", + "description: This tutorial demonstrates how to realize a Nishimori phase transition on an IBM quantum processor.\n", + "---\n", + "\n", + "\n", + "# Nishimori phase transition\n", + "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "838ac5c7-0819-4482-8875-c57db88ba82a", + "metadata": {}, + "source": [ + "## Background\n", + "This tutorial demonstrates how to realize a Nishimori phase transition on an IBM® quantum processor. This experiment was originally described in [*Realizing the Nishimori transition across the error threshold for constant-depth quantum circuits*](https://arxiv.org/abs/2309.02863).\n", + "\n", + "The Nishimori phase transition refers to the transition between short- and long-range ordered phases in the random-bond Ising model. On a quantum computer, the long-range ordered phase manifests as a state in which qubits are entangled across the entire device. This highly entangled state is prepared using the *generation of entanglement by measurement* (GEM) protocol. By utilizing mid-circuit measurements, the GEM protocol is able to entangle qubits across the entire device using circuits of only constant depth. This tutorial uses the implementation of the GEM protocol from the [GEM Suite](https://github.com/qiskit-community/gem-suite) software package." + ] + }, + { + "cell_type": "markdown", + "id": "4091005d-ecd5-40c9-b90f-263e49a4cc16", + "metadata": {}, + "source": [ + "## Requirements\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "\n", + "- Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.22 or later ( `pip install qiskit-ibm-runtime` )\n", + "- GEM Suite ( `pip install gem-suite` )" + ] + }, + { + "cell_type": "markdown", + "id": "210b8730-ca3e-46d9-b8b0-64c36f13df8e", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4ad70734-54f8-400a-8399-188ee8a0f3ac", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from collections import defaultdict\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "from gem_suite import PlaquetteLattice\n", + "from gem_suite.experiments import GemExperiment" + ] + }, + { + "cell_type": "markdown", + "id": "39f59f3a-eb2a-4160-9da2-2bb83c58771c", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "The GEM protocol works on a quantum processor with qubit connectivity described by a lattice. Today's IBM quantum processors use the [heavy hex lattice](https://www.ibm.com/quantum/blog/heavy-hex-lattice). The qubits of the processor are grouped into *plaquettes* based on which unit cell of the lattice they occupy. Because a qubit might occur in more than one unit cell, the plaquettes are not disjoint. On the heavy hex lattice, a plaquette contains 12 qubits. The plaquettes themselves also form lattice, where two plaquettes are connected if they share any qubits. On the heavy hex lattice, neighboring plaquettes share 3 qubits.\n", + "\n", + "In the GEM Suite software package, the fundamental class for implementing the GEM protocol is `PlaquetteLattice`, which represents the lattice of plaquettes (which is distinct from the heavy hex lattice). A `PlaquetteLattice` can be initialized from a qubit coupling map. Currently, only heavy hex coupling maps are supported.\n", + "\n", + "The following code cell initializes a plaquette lattice from the coupling map of a IBM quantum processor. The plaquette lattice does not always encompass the entire hardware. For example, `ibm_torino` has 133 total qubits, but the largest plaquette lattice that fits on the device uses only 125 of them, and comprises a total of 18 plaquettes. Similar can be observed for IBM Quantum® devices with different qubit counts as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "553dbafe-1778-4971-83c3-0408605b701d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of qubits in backend: 133\n", + "Number of qubits in plaquette lattice: 125\n", + "Number of plaquettes: 18\n" + ] + } + ], + "source": [ + "# QiskitRuntimeService.save_account(channel=\"ibm_quantum\", token=\"\", overwrite=True,\n", + "# set_as_default=True)\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ")\n", + "plaquette_lattice = PlaquetteLattice.from_coupling_map(backend.coupling_map)\n", + "\n", + "print(f\"Number of qubits in backend: {backend.num_qubits}\")\n", + "print(\n", + " f\"Number of qubits in plaquette lattice: {len(list(plaquette_lattice.qubits()))}\"\n", + ")\n", + "print(f\"Number of plaquettes: {len(list(plaquette_lattice.plaquettes()))}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0bf43539-d782-44a9-ba4e-a1b6c7510803", + "metadata": {}, + "source": [ + "You can visualize the plaquette lattice by generating a diagram of its graph representation. In the diagram, the plaquettes are represented by labeled hexagons, and two plaquettes are connected by an edge if they share qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "625882a4-faeb-4d96-b441-c989f43c4dea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plaquette_lattice.draw_plaquettes()" + ] + }, + { + "cell_type": "markdown", + "id": "2f0a6b73-d1d2-4117-80cf-6fe3adbb82c5", + "metadata": {}, + "source": [ + "You can retrieve information about individual plaquettes, such as the qubits they contain, using the `plaquettes` method." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e412a612-c7d5-4689-840a-2383dd538f06", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PyPlaquette(index=0, qubits=[0, 1, 2, 3, 4, 15, 16, 19, 20, 21, 22, 23], neighbors=[3, 1])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get a list of the plaquettes\n", + "plaquettes = list(plaquette_lattice.plaquettes())\n", + "# Display information about plaquette 0\n", + "plaquettes[0]" + ] + }, + { + "cell_type": "markdown", + "id": "75edf1b4-f95f-4ff7-9e3c-2e810711636f", + "metadata": {}, + "source": [ + "You can also produce a diagram of the underlying qubits that form the plaquette lattice." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a19d63ce-3572-4081-a008-c1332fbbe303", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plaquette_lattice.draw_qubits()" + ] + }, + { + "cell_type": "markdown", + "id": "e7b0f54b-5bc2-42bb-b4d4-1346f0a902e3", + "metadata": {}, + "source": [ + "In addition to the qubit labels and the edges indicating which qubits are connected, the diagram contains three additional pieces of information that are relevant to the GEM protocol:\n", + "- Each qubit is either shaded (gray) or unshaded. The shaded qubits are \"site\" qubits that represent the sites of the Ising model, and the unshaded qubits are \"bond\" qubits used to mediate interactions between the site qubits.\n", + "- Each site qubit is labeled either (A) or (B), indicating one of two roles a site qubit can play in the GEM protocol (the roles are explained later).\n", + "- Each edge is colored using one of six colors, thus partitioning the edges into six groups. This partitioning determines how two-qubit gates can be parallelized, as well as different scheduling patterns that are likely to incur different amounts of error on a noisy quantum processor. Because edges in a group are disjoint, a layer of two-qubit gates can be applied on those edges simultaneously. In fact, it is possible to partition the six colors into three groups of two colors such that the union of each group of two colors is still disjoint. Therefore, only three layers of two-qubit gates are needed to activate every edge. There are 12 ways to so partition the six colors, and each such partition yields a different 3-layer gate schedule.\n", + "\n", + "Now that you have created a plaquette lattice, the next step is to initialize a `GemExperiment` object, passing both the plaquette lattice and the backend that you intend to run the experiment on. The `GemExperiment` class manages the actual implementation of the GEM protocol, including generating circuits, submitting jobs, and analyzing the data. The following code cell initializes the experiment class while restricting the plaquette lattice to only two of the plaquettes (21 qubits), reducing the size of the experiment to ensure that the noise in the hardware doesn't overwhelm the signal." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "02357c6e-5c83-4ac0-811d-22602d9f33d5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(plaquette_lattice.filter([9, 12]), backend=backend)\n", + "\n", + "# visualize the plaquette lattice after filtering\n", + "plaquette_lattice.filter([9, 12]).draw_qubits()" + ] + }, + { + "cell_type": "markdown", + "id": "3234e018-a1a8-47b7-b10c-57d6f7f77ff1", + "metadata": {}, + "source": [ + "A GEM protocol circuit is built using the following steps:\n", + "1. Prepare the all-$|+\\rangle$ state by applying a Hadamard gate to every qubit.\n", + "2. Apply an $R_{ZZ}$ gate between every pair of connected qubits. This can be achieved using 3 layers of gates. Each $R_{ZZ}$ gate acts on a site qubit and a bond qubit. If the site qubit is labeled (B), then the angle is fixed to $\\frac{\\pi}{2}$. If the site qubit is labeled (A), then the angle is allowed to vary, producing different circuits. By default, the range of angles is set to 21 equally spaced points between $0$ and $\\frac{\\pi}{2}$, inclusive.\n", + "3. Measure each bond qubit in the Pauli $X$ basis. Since qubits are measured in the Pauli $Z$ basis, this can be accomplished by applying a Hadamard gate before measuring the qubit.\n", + "\n", + "Note that the paper cited in the introduction to this tutorial uses a different convention for the $R_{ZZ}$ angle, which differs from the convention used in this tutorial by a factor of 2.\n", + "\n", + "In step 3, only the bond qubits are measured. To understand what state the site qubits remain in, it is instructive to consider the case that the $R_{ZZ}$ angle applied to site qubits (A) in step 2 is equal to $\\frac{\\pi}{2}$. In this case, the site qubits are left in a highly entangled state similar to the GHZ state,\n", + "\n", + "$$\n", + "\\lvert \\text{GHZ} \\rangle = \\lvert 00 \\cdots 00 \\rangle + \\lvert 11 \\cdots 11 \\rangle.\n", + "$$\n", + "\n", + "Due to the randomness in the measurement outcomes, the actual state of the site qubits might be a different state with long-range order, for example, $\\lvert 00110 \\rangle + \\lvert 11001 \\rangle$. However, the GHZ state can be recovered by applying a decoding operation based on the measurement outcomes. When the $R_{ZZ}$ angle is tuned down from $\\frac{\\pi}{2}$, the long-range order can still be recovered up until a critical angle, which in the absence of noise, is approximately $0.3 \\pi$. Below this angle, the resulting state no longer exhibits long-range entanglement. This transition between the presence and absence of long-range order is the Nishimori phase transition.\n", + "\n", + "In the description above, the site qubits were left unmeasured, and the decoding operation can be performed by applying quantum gates. In the experiment as implemented in the GEM suite, which this tutorial follows, the site qubits are in fact measured, and the decoding operation is applied in a classical post-processing step.\n", + "\n", + "In the description above, the decoding operation can be performed by applying quantum gates to the site qubits to recover the quantum state. However, if the goal is to immediately measure the state, for example, for characterization purposes, then the site qubits are measured together with the bond qubits, and the decoding operation can be applied in a classical post-processing step. This is how the experiment is implemented in the GEM suite, which this tutorial follows.\n", + "\n", + "In addition to depending on the $R_{ZZ}$ angle in step 2, which by default sweeps across 21 values, the GEM protocol circuit also depends on the scheduling pattern used to implement the 3 layers of $R_{ZZ}$ gates. As discussed previously, there are 12 such scheduling patterns. Therefore, the total number of circuits in the experiment is $21 \\times 12 = 252$.\n", + "\n", + "The circuits of the experiment can be generated using the `circuits` method of the `GemExperiment` class." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8e2ade62-9a57-42c3-9a85-3fe2dec3c426", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total number of circuits: 252\n" + ] + } + ], + "source": [ + "circuits = gem_exp.circuits()\n", + "print(f\"Total number of circuits: {len(circuits)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "56c3c656-dcfa-4ca0-9df9-2d97c187c685", + "metadata": {}, + "source": [ + "For the purposes of this tutorial, it is enough to consider just a single scheduling pattern. The following code cell restricts the experiment to the first scheduling pattern. As a result, the experiment only has 21 circuits, one for each $R_{ZZ}$ angle swept over." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4f8a2c73-752d-47b9-95d5-83439933fc08", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total number of circuits: 21\n", + "RZZ angles:\n", + "[0. 0.07853982 0.15707963 0.23561945 0.31415927 0.39269908\n", + " 0.4712389 0.54977871 0.62831853 0.70685835 0.78539816 0.86393798\n", + " 0.9424778 1.02101761 1.09955743 1.17809725 1.25663706 1.33517688\n", + " 1.41371669 1.49225651 1.57079633]\n" + ] + } + ], + "source": [ + "# Restrict experiment to the first scheduling pattern\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "\n", + "# There are less circuits now\n", + "circuits = gem_exp.circuits()\n", + "print(f\"Total number of circuits: {len(circuits)}\")\n", + "\n", + "# Print the RZZ angles swept over\n", + "print(f\"RZZ angles:\\n{gem_exp.parameters()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7011dc79-8561-42ef-909b-00fd8be9ef34", + "metadata": {}, + "source": [ + "The following code cell draws a diagram of the circuit at index 5. To reduce the size of the diagram, the measurement gates at the end of the circuit are removed." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fd57d483-c70b-4ad5-b309-15750ad38bac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get the circuit at index 5\n", + "circuit = circuits[5]\n", + "# Remove the final measurements to ease visualization\n", + "circuit.remove_final_measurements()\n", + "# Draw the circuit\n", + "circuit.draw(\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "a3aa063b-44cf-49f3-9e12-23b2f6a1c85b", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "Transpiling quantum circuits for execution on hardware typically involves a [number of stages](/docs/guides/transpiler-stages). Typically, the stages that incur the most computational overhead are choosing the qubit layout, routing the two-qubit gates to conform to the qubit connectivity of the hardware, and optimizing the circuit to minimize its gate count and depth. In the GEM protocol, the layout and routing stages are unnecessary because the hardware connectivity is already incorporated into the design of the protocol. The circuits already have a qubit layout, and the two-qubit gates are already mapped onto native connections. Furthermore, in order to preserve the structure of the circuit as the $R_{ZZ}$ angle is varied, only very basic circuit optimization should be performed.\n", + "\n", + "The `GemExperiment` class transparently transpiles circuits when executing the experiment. The layout and routing stages are already overridden by default to do nothing, and circuit optimization is performed at a level that only optimizes single-qubit gates. However, you can override or pass additional options using the `set_transpile_options` method. For the sake of visualization, the following code cell manually transpiles the circuit displayed previously, and draws the transpiled circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e9b99d48-8d33-46b5-bff5-480ab1c1c1f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Demonstrate setting transpile options\n", + "gem_exp.set_transpile_options(\n", + " optimization_level=1 # This is the default optimization level\n", + ")\n", + "pass_manager = generate_preset_pass_manager(\n", + " backend=backend,\n", + " initial_layout=list(gem_exp.physical_qubits),\n", + " **dict(gem_exp.transpile_options),\n", + ")\n", + "transpiled = pass_manager.run(circuit)\n", + "transpiled.draw(\"mpl\", idle_wires=False, fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "8d0dcd59-54ef-4af8-9213-0784ef94b838", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "\n", + "To execute the GEM protocol circuits on the hardware, call the `run` method of the `GemExperiment` object. You can specify the number of shots you want to sample from each circuit. The `run` method returns an [ExperimentData](https://qiskit-community.github.io/qiskit-experiments/stubs/qiskit_experiments.framework.ExperimentData.html) object which you should save to a variable. Note that the `run` method only submits jobs without waiting for them to finish, so it is a non-blocking call." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6171a383-dd58-4e3f-88aa-bbec7b5870df", + "metadata": {}, + "outputs": [], + "source": [ + "exp_data = gem_exp.run(shots=10_000)" + ] + }, + { + "cell_type": "markdown", + "id": "71e81552-0d33-4950-8d45-e6c0a8a056c9", + "metadata": {}, + "source": [ + "To wait for the results, call the `block_for_results` method of the `ExperimentData` object. This call will cause the interpreter to hang until the jobs are finished." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ed14a067-35ba-4ffc-8534-4ae5ec6bc4c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ExperimentData(GemExperiment, d0d5880a-34c1-4aab-a7b6-c4f58516bc03, job_ids=['cwg12ptmptp00082khhg'], metadata=<5 items>, figure_names=['two_point_correlation.svg', 'normalized_variance.svg', 'plaquette_ops.svg', 'bond_ops.svg'])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.block_for_results()" + ] + }, + { + "cell_type": "markdown", + "id": "36e0570b-f091-45f2-bb83-143edbc3b433", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "\n", + "At an $R_{ZZ}$ angle of $\\frac{\\pi}{2}$, the decoded state would be the GHZ state in the absence of noise. The long-range order of the GHZ state can be visualized by plotting the magnetization of the measured bitstrings. The magnetization $M$ is defined as the sum of the single-qubit Pauli $Z$ operators,\n", + "$$\n", + "M = \\sum_{j=1}^N Z_j,\n", + "$$\n", + "where $N$ is the number of site qubits. Its value for a bitstring is equal to the difference between the number of zeros and the number of ones. Measuring the GHZ state yields the all zeros state or the all ones state with equal probability, so the magnetization would be $+N$ half of the time and $-N$ the other half of the time. In the presence of errors due to noise, other values would also appear, but if the noise is not too great, the distribution would still be peaked near $+N$ and $-N$.\n", + "\n", + "For the raw bitstrings before decoding, the distribution of the magnetization would be equivalent to that of uniformly random bitstrings, in the absence of noise.\n", + "\n", + "The following code cell plots the magnetization of the raw bitstrings and the decoded bitstrings at the $R_{ZZ}$ angle of $\\frac{\\pi}{2}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ead3582-16df-4616-836c-bdce867ad6b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Magnetization distribution with and without decoding')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def magnetization_distribution(\n", + " counts_dict: dict[str, int],\n", + ") -> dict[str, float]:\n", + " \"\"\"Compute magnetization distribution from counts dictionary.\"\"\"\n", + " # Construct dictionary from magnetization to count\n", + " mag_dist = defaultdict(float)\n", + " for bitstring, count in counts_dict.items():\n", + " mag = bitstring.count(\"0\") - bitstring.count(\"1\")\n", + " mag_dist[mag] += count\n", + " # Normalize\n", + " shots = sum(counts_dict.values())\n", + " for mag in mag_dist:\n", + " mag_dist[mag] /= shots\n", + " return mag_dist\n", + "\n", + "\n", + "# Get counts dictionaries with and without decoding\n", + "data = exp_data.data()\n", + "# Get the last data point, which is at the angle for the GHZ state\n", + "raw_counts = data[-1][\"counts\"]\n", + "# Without decoding\n", + "site_indices = [\n", + " i for i, q in enumerate(gem_exp.plaquettes.qubits()) if q.role == \"Site\"\n", + "]\n", + "site_raw_counts = defaultdict(int)\n", + "for key, val in raw_counts.items():\n", + " site_str = \"\".join(key[-1 - i] for i in site_indices)\n", + " site_raw_counts[site_str] += val\n", + "# With decoding\n", + "_, site_decoded_counts = gem_exp.plaquettes.decode_outcomes(\n", + " raw_counts, return_counts=True\n", + ")\n", + "\n", + "# Compute magnetization distribution\n", + "raw_magnetization = magnetization_distribution(site_raw_counts)\n", + "decoded_magnetization = magnetization_distribution(site_decoded_counts)\n", + "\n", + "# Plot\n", + "plt.bar(*zip(*raw_magnetization.items()), label=\"raw\")\n", + "plt.bar(*zip(*decoded_magnetization.items()), label=\"decoded\", width=0.3)\n", + "plt.legend()\n", + "plt.xlabel(\"Magnetization\")\n", + "plt.ylabel(\"Frequency\")\n", + "plt.title(\"Magnetization distribution with and without decoding\")" + ] + }, + { + "cell_type": "markdown", + "id": "90a7ae7a-5175-421f-bda9-bc6b986bdf5f", + "metadata": {}, + "source": [ + "To more rigorously characterize the long-range order, you can examine the average two-point correlation $f$, defined as\n", + "$$\n", + "f = \\frac{1}{N^2} \\left(\\langle M^2 \\rangle - \\langle M \\rangle ^2\\right).\n", + "$$\n", + "A higher value indicates a greater degree of entanglement. The `GemExperiment` class automatically computes this value for the decoded bitstrings as part of processing the experimental data. It stores a figure that is accessible via the `figure` method of the experiment data class. In this case, the name of the figure is `two_point_correlation`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4ecb25c8-e572-49af-a879-9943039db131", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.figure(\"two_point_correlation\")" + ] + }, + { + "cell_type": "markdown", + "id": "f2f3e7e2-2a8b-4790-8ba7-b190c4ed1049", + "metadata": {}, + "source": [ + "To determine the critical point of the Nishimori phase transition, you can look at the normalized variance of $M^2 / N$, defined as\n", + "$$\n", + "g = \\frac{1}{N^3} \\left(\\langle M^4 \\rangle - \\langle M^2 \\rangle^2\\right),\n", + "$$\n", + "which quantifies the amount of fluctuation in the squared magnetization. This value is maximized at the critical point of the Nishimori phase transition. In the absence of noise, the critical point occurs at approximately $0.3 \\pi$. In the presence of noise, the critical point is shifted higher, but the phase transition is still observed as long as the critical point occurs below $0.5 \\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2b351d68-3924-445a-94ef-047b16214e8a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "markdown", + "id": "a94e0cbe-8429-487c-b203-50a8b2eacee3", + "metadata": {}, + "source": [ + "## Scale up the experiment\n", + "\n", + "The following code cells run the experiment for six plaquettes (49 qubits) and the full 12 plaquettes (125 qubits) and plot the normalized variance. As the experiment is scaled to larger sizes, the greater amount of noise shifts the critical point rightwards." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "08581c09-a6a5-4a56-9fc4-abf22b063c6a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(\n", + " plaquette_lattice.filter(range(3, 9)), backend=backend\n", + ")\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "exp_data = gem_exp.run(shots=10_000)\n", + "exp_data.block_for_results()\n", + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "37e9a4cd-6efb-4ade-ad09-8139db9d58e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(plaquette_lattice, backend=backend)\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "exp_data = gem_exp.run(shots=10_000)\n", + "exp_data.block_for_results()\n", + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "markdown", + "id": "6abd9701-58e4-43a8-a1d7-279506570de4", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this tutorial, you realized a Nishimori phase transition on a quantum processor using the GEM protocol. The metrics that you examined during post-processing, in particular the two-point correlation and the normalized variance, serve as benchmarks of the device's ability to generate long-range entangled states. These benchmarks extend the utility of the GEM protocol beyond probing interesting physics. As part of the protocol, you entangled qubits across the entire device using circuits of only constant depth. This feat is only possible due to the protocol's use of mid-circuit measurements. In this experiment, the entangled state was immediately measured, but an interesting avenue to explore would be to continue using the state in additional quantum processing!" + ] + }, + { + "cell_type": "markdown", + "id": "b9562a76", + "metadata": {}, + "source": [ + "## Tutorial survey\n", + "\n", + "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", + "\n", + "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_bsCKQkgzuQUQ7ky)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/operator-back-propagation.ipynb b/docs/tutorials/operator-back-propagation.ipynb index 43f9e4913e0..9a6d804c6d0 100644 --- a/docs/tutorials/operator-back-propagation.ipynb +++ b/docs/tutorials/operator-back-propagation.ipynb @@ -1,793 +1,797 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "d2c31ae8", - "metadata": {}, - "source": [ - "---\n", - "title: Operator backpropagation (OBP) for estimation of expectation values\n", - "description: This tutorial implements a Qiskit pattern for simulating the quantum dynamics of a Heisenberg spin chain using qiskit-addon-obp\n", - "---\n", - "\n", - "{/* cspell:ignore fontsize edgecolor circo simeq */}\n", - "\n", - "# Operator backpropagation (OBP) for estimation of expectation values\n", - "\n", - "*Usage estimate: 4 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "8bf80006", - "metadata": {}, - "source": [ - "## Learning outcomes\n", - "After going through this tutorial, users should understand:\n", - "- How to use [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) to reduce the depth of the quantum circuit at the cost of an increased number of circuit executions\n", - "- How to use [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) to construct XYZ Hamiltonians and their time-evolution circuits\n", - "\n", - "## Prerequisites\n", - "We suggest that users are familiar with the following topics before going through this tutorial:\n", - "- Using the [Estimator](/docs/api/qiskit-ibm-runtime/estimator-v2) primitive to calculate expectation values of an observable\n", - "\n", - "\n", - "## Background\n", - "Operator backpropagation is a technique that involves absorbing operations from the end of a quantum circuit into the measured observable, generally reducing the depth of the circuit at the cost of additional terms in the observable. The goal is to backpropagate as much of the circuit as possible without allowing the observable to grow too large. A Qiskit-based implementation is available in the OBP Qiskit addon. Read the corresponding [documentation](https://qiskit.github.io/qiskit-addon-obp/) for more information.\n", - "\n", - "Consider an example circuit for which an observable $O = \\sum_P c_P P$ is to be measured, where $P$ are Paulis and $c_P$ are coefficients. Let us denote the circuit as a single unitary $U$, which can be logically partitioned into $U = U_C U_Q$ as shown in the figure below.\n", - "\n", - "![Circuit diagram showing Uq followed by Uc](/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n", - "\n", - "Operator backpropagation absorbs the unitary $U_C$ into the observable by evolving it as $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. In other words, part of the computation is performed classically through the evolution of the observable from $O$ to $O'$. The original problem can now be reformulated as measuring the observable $O'$ for the new lower-depth circuit whose unitary is $U_Q$.\n", - "\n", - "The unitary $U_C$ is represented as a number of slices $U_C = U_S U_{S-1}...U_2U_1$. There are multiple ways to define a slice. For instance, in the above example circuit, each layer of $R_{zz}$ and each layer of $R_x$ gates can be considered as an individual slice. Backpropagation involves calculation of $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$ classically. Each slice $U_s$ can be represented as $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, where $P_s$ is a $n$-qubit Pauli and $\\theta_s$ is a scalar. It is easy to verify that\n", - "\n", - "$$\n", - "U_s^{\\dagger} P U_s = P \\qquad \\text{if} ~[P,P_s] = 0,\n", - "$$\n", - "\n", - "$$\n", - "U_s^{\\dagger} P U_s = \\qquad cos(\\theta_s)P + i sin(\\theta_s)P_sP \\qquad \\text{if} ~\\{P,P_s\\} = 0\n", - "$$\n", - "\n", - "In the above example, if $\\{P,P_s\\} = 0$, then we need to execute two quantum circuits, instead of one, to calculate the expectation value. Therefore, backpropagation might increase the number of terms in the observable, leading to a higher number of circuit executions. One way to allow for deeper backpropagation into the circuit, while preventing the operator from growing too large, is to truncate terms with small coefficients, rather than adding them to the operator. For instance, in the above example, one could choose to truncate the term involving $P_sP$ provided that $\\theta_s$ is sufficiently small. Truncating terms can result in fewer quantum circuits to execute, but doing so results in some error in the final expectation value calculation proportional to the magnitude of the truncated terms' coefficients." - ] - }, - { - "cell_type": "markdown", - "id": "55b94021", - "metadata": {}, - "source": [ - "## Requirements\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "\n", - "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)\n", - "- OBP Qiskit addon 0.3 or later (`pip install qiskit-addon-obp`)\n", - "- Qiskit addon utils 0.3 or later (`pip install qiskit-addon-utils`)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7db2e559", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bc380c46", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from qiskit.primitives import StatevectorEstimator\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.transpiler import CouplingMap\n", - "from qiskit.synthesis import LieTrotter\n", - "\n", - "from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian\n", - "from qiskit_addon_utils.problem_generators import (\n", - " generate_time_evolution_circuit,\n", - ")\n", - "from qiskit_addon_utils.slicing import slice_by_depth, combine_slices\n", - "from qiskit_addon_obp.utils.simplify import OperatorBudget\n", - "from qiskit_addon_obp import backpropagate\n", - "from qiskit_addon_obp.utils.truncating import setup_budget\n", - "\n", - "from rustworkx.visualization import graphviz_draw\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions" - ] - }, - { - "cell_type": "markdown", - "id": "431a5bd2-e6ed-471b-ad9e-c4edd27784a8", - "metadata": {}, - "source": [ - "## Small-scale simulator example\n", - "This tutorial implements a [Qiskit pattern](/docs/guides/intro-to-patterns) for simulating the quantum dynamics of a Heisenberg spin chain using the [OBP Qiskit addon](https://github.com/Qiskit/qiskit-addon-obp). Note that in a noiseless simulator, the expectation value obtained with and without backpropagation will be same." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "988ee237", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "#### Map the time-evolution of a quantum Heisenberg model to a quantum experiment\n", - "\n", - "First, we shall use the [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) function from `qiskit-addon-utils` to generate a Heisenberg-like Hamiltonian on a given connectivity graph. This graph can be either a [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) or a [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap). In the following, we shall use a linear chain `CouplingMap` of 10 qubits." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a3debf65-06df-4277-933e-14b6f6170756", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "num_qubits = 10\n", - "layout = [(i - 1, i) for i in range(1, num_qubits)]\n", - "\n", - "# Instantiate a CouplingMap object\n", - "coupling_map = CouplingMap(layout)\n", - "graphviz_draw(coupling_map.graph, method=\"circo\")" - ] - }, - { - "cell_type": "markdown", - "id": "ef13f367", - "metadata": {}, - "source": [ - "Next, we generate a Pauli operator modeling a Heisenberg XYZ Hamiltonian:\n", - "\n", - "$$\n", - "{\\hat{\\mathcal{H}}_{XYZ} = \\sum_{(j,k)\\in E} (J_{x} \\sigma_j^{x} \\sigma_{k}^{x} + J_{y} \\sigma_j^{y} \\sigma_{k}^{y} + J_{z} \\sigma_j^{z} \\sigma_{k}^{z}) + \\sum_{j\\in V} (h_{x} \\sigma_j^{x} + h_{y} \\sigma_j^{y} + h_{z} \\sigma_j^{z}),}\n", - "$$\n", - "\n", - "where $G(V,E)$ is the graph of the coupling map. For this tutorial, we have used $J_x, J_y, J_z$ to be $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$, respectively, and $h_x, h_y, h_z$ to be $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$, respectively." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "b0f1e5fd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],\n", - " coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n", - " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n", - " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n", - " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n", - " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n", - " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n", - " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,\n", - " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", - " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n", - " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n", - " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", - " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n", - " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n", - " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", - " 0.34906585+0.j])\n" - ] - } - ], - "source": [ - "# Get a qubit operator describing the Heisenberg XYZ model\n", - "hamiltonian = generate_xyz_hamiltonian(\n", - " coupling_map,\n", - " coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n", - " ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n", - ")\n", - "print(hamiltonian)" - ] - }, - { - "cell_type": "markdown", - "id": "a81f0346", - "metadata": {}, - "source": [ - "From the qubit operator, we can generate a quantum circuit which models its time evolution. We have used [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) with Lie Trotter decomposition to construct the time evolution circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "5208e0a8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " time=0.2,\n", - " synthesis=LieTrotter(reps=2),\n", - ")\n", - "circuit.draw(\"mpl\", style=\"iqp\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "ac6f36e3", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "#### Create circuit slices to backpropagate\n", - "\n", - "The `backpropagate` function backpropagates entire circuit slices at a time. Therefore, the choice of slicing can have an impact on how well backpropagation performs for a given problem. Here, we will group gates of the same type into slices using the [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) function.\n", - "\n", - "For a more detailed discussion on circuit slicing, check out this [how-to guide](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) of the [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) package." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "1834cb22", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Separated the circuit into 18 slices.\n" - ] - } - ], - "source": [ - "slices = slice_by_depth(circuit, max_slice_depth=1)\n", - "print(f\"Separated the circuit into {len(slices)} slices.\")" - ] - }, - { - "cell_type": "markdown", - "id": "1df16e6b", - "metadata": {}, - "source": [ - "#### Constrain how large the operator can grow during backpropagation\n", - "\n", - "During backpropagation, the number of terms in the operator will generally approach $2^L$ quickly, where $L$ is the number of slices. When two terms in the operator do not commute qubit-wise, we need separate circuits to obtain the expectation values corresponding to them. For example, if we have a two-qubit observable $O = 0.1 XX + 0.3 IZ - 0.5 IX$, then since $[XX,IX] = 0$, measurement in a single basis is sufficient to calculate the expectation values for these two terms. However, $IZ$ anti-commutes with the other two terms, so we need a separate basis measurement to calculate the expectation value of $IZ$. In other words, we need two circuits instead of one to calculate $\\langle O \\rangle$. As the number of terms in the operator increases, there is a possibility that the required number of circuit executions also increases.\n", - "\n", - "The size of the operator can be bounded by specifying the `operator_budget` kwarg of the `backpropagate` function, which accepts an [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instance.\n", - "\n", - "To control the amount of extra resources (number of circuit execution, and hence the QPU time required) allocated, we restrict the maximum number of qubit-wise commuting Pauli groups that the backpropagated observable is allowed to have. Here we specify that backpropagation should stop when the number of qubit-wise commuting Pauli groups in the operator grows past eight." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3074b173", - "metadata": {}, - "outputs": [], - "source": [ - "op_budget = OperatorBudget(max_qwc_groups=8)" - ] - }, - { - "cell_type": "markdown", - "id": "8bd6ac5f", - "metadata": {}, - "source": [ - "#### Backpropagate slices from the circuit\n", - "\n", - "First we specify the observable to be $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, $N$ being the number of qubits. We will backpropagate slices from the time-evolution circuit until the terms in the observable can no longer be combined into eight or fewer qubit-wise commuting Pauli groups." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a1300365", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],\n", - " coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,\n", - " 0.1+0.j, 0.1+0.j])" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "observable = SparsePauliOp.from_sparse_list(\n", - " [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n", - " num_qubits=num_qubits,\n", - ")\n", - "observable" - ] - }, - { - "cell_type": "markdown", - "id": "820236bf", - "metadata": {}, - "source": [ - "Below you will see that we backpropagated six slices, and the terms were combined into six and not eight groups. This implies that backpropagating one more slice would cause the number of Pauli groups to exceed eight. We can verify that this is the case by inspecting the returned metadata. Also note that in this portion the circuit transformation is exact. That is, no terms of the new observable $O’$ were truncated. The backpropagated circuit and the backpropagated operator give the exact outcome as the original circuit and operator." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ee8fd385", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Backpropagated 6 slices.\n", - "New observable has 60 terms, which can be combined into 6 groups.\n", - "Note that backpropagating one more slice would result in 114 terms across 12 groups.\n", - "The remaining circuit after backpropagation looks as follows:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Backpropagate slices onto the observable\n", - "bp_obs, remaining_slices, metadata = backpropagate(\n", - " observable, slices, operator_budget=op_budget\n", - ")\n", - "# Recombine the slices remaining after backpropagation\n", - "bp_circuit = combine_slices(remaining_slices)\n", - "\n", - "print(f\"Backpropagated {metadata.num_backpropagated_slices} slices.\")\n", - "print(\n", - " f\"New observable has {len(bp_obs.paulis)} terms, which can be combined into {len(bp_obs.group_commuting(qubit_wise=True))} groups.\"\n", - ")\n", - "print(\n", - " f\"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms \"\n", - " f\"across {metadata.backpropagation_history[-1].num_qwc_groups} groups.\"\n", - ")\n", - "print(\"The remaining circuit after backpropagation looks as follows:\")\n", - "bp_circuit.draw(\"mpl\", fold=-1, scale=0.6)" - ] - }, - { - "cell_type": "markdown", - "id": "833d9c36", - "metadata": {}, - "source": [ - "For the small-scale example on a simulator, we will not use truncation. This is because in the absence of noise, the circuit with and without backpropagation leads to the same result, and truncation makes the result worse due to the added approximation." - ] - }, - { - "cell_type": "markdown", - "id": "43f58cfb", - "metadata": {}, - "source": [ - "#### Transpile the circuits into the basis gate set\n", - "\n", - "Now we transpile both the original and the backpropagated circuits into the basis gate of the backend. We don't need to transpile on the actual backend since we are going to run on a simulator for the small instance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29d71cd3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=133\n", - ")\n", - "print(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "18707b33", - "metadata": {}, - "outputs": [], - "source": [ - "pm_basis = generate_preset_pass_manager(\n", - " optimization_level=3, basis_gates=backend.configuration().basis_gates\n", - ")\n", - "isa_circuit = pm_basis.run(circuit)\n", - "isa_bp_circuit = pm_basis.run(bp_circuit)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b4d480b3", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "First, we create two [Primitive Unified Blocs](/docs/api/qiskit/primitives) (PUBs) corresponding to the original circuit, and the backpropagated circuit. Then we execute the pubs on an ideal Estimator to obtain the expectation values." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "b22a1b00", - "metadata": {}, - "outputs": [], - "source": [ - "pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "eb174b15", - "metadata": {}, - "outputs": [], - "source": [ - "rng = np.random.default_rng()\n", - "estimator = StatevectorEstimator(seed=rng)\n", - "job = estimator.run(pubs)" - ] - }, - { - "cell_type": "markdown", - "id": "50b94af2", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "\n", - "Now we obtain the expectation values of the original and backpropagated circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "31dc35ea-6554-4ca7-9c3b-0b5394c46e4e", - "metadata": {}, - "outputs": [], - "source": [ - "primitive_result = job.result()\n", - "circuit_expval = primitive_result[0].data.evs.item()\n", - "bp_circuit_expval = primitive_result[1].data.evs.item()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "fb5f955a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0, 0.5, '$M_Z$')" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "methods = [\n", - " \"No backpropagation\",\n", - " \"Backpropagation\",\n", - "]\n", - "values = [circuit_expval, bp_circuit_expval]\n", - "\n", - "ax = plt.gca()\n", - "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n", - "ax.set_ylim([0.6, 0.92])\n", - "ax.set_ylabel(r\"$M_Z$\", fontsize=12)" - ] - }, - { - "cell_type": "markdown", - "id": "c0be738f", - "metadata": {}, - "source": [ - "As expected, the two expectation values agree. Because we are running on a noiseless statevector simulator, backpropagation is an exact transformation of the circuit-observable pair, so the original and backpropagated workflows must produce the same value of $M_Z$. The benefit of backpropagation only becomes apparent on noisy hardware, where the shorter backpropagated circuit accumulates less error, as illustrated in the large-scale hardware example below." - ] - }, - { - "cell_type": "markdown", - "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6", - "metadata": {}, - "source": [ - "## Large-scale hardware example" - ] - }, - { - "cell_type": "markdown", - "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25", - "metadata": {}, - "source": [ - "When developing an experiment, it's useful to start with a small circuit to make visualizations and simulations easier. Now we look into operator backpropagation for a 50-qubit Heisenberg Hamiltonian with the same set of values for the $J$ and $h$ parameters and the same observable $M_Z$, but for four Trotter steps. The ideal expectation value at this scale cannot be calculated in a brute force method, so we use a tensor network and obtain the ideal expectation value to be $\\simeq 0.89$.\n", - "\n", - "Along with backpropagation, in this large-scale example, we also introduce backpropagation with truncation. Ideally we want to backpropagate as much as possible to reduce the depth of the effective circuit. However, it often leads to a large number of non-commuting terms in the updated observable, increasing the quantum overhead. Therefore, we can eliminate observable terms with small coefficients using a practice called truncation. While truncation allows more propagation by reducing the number of terms in the updated observable, it also introduces some approximation. Therefore, it is necessary to restrict the truncation within some limits so that the approximation error does not overwhelm the reduction in noise obtained from deeper backpropagation.\n", - "\n", - "To restrict the amount of truncation, we allot an error budget for each slice as well as the total error budget over the entire backpropagated circuit using the [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) function. This ensures that the truncation is controlled for each slice as well as for the entire circuit. See also this [guide](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) for other ways of allocating the budget." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "28ac4dbf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2-qubit depth without backpropagation: 24\n", - "2-qubit depth with backpropagation: 20\n", - "2-qubit depth with backpropagation and truncation: 18\n" - ] - } - ], - "source": [ - "num_qubits = 50\n", - "layout = [(i - 1, i) for i in range(1, num_qubits)]\n", - "\n", - "# Instantiate a CouplingMap object\n", - "coupling_map = CouplingMap(layout)\n", - "\n", - "hamiltonian = generate_xyz_hamiltonian(\n", - " coupling_map,\n", - " coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n", - " ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n", - ")\n", - "\n", - "# Generate a time evolution circuit for the Hamiltonian\n", - "circuit = generate_time_evolution_circuit(\n", - " hamiltonian,\n", - " time=0.2,\n", - " synthesis=LieTrotter(reps=4),\n", - ")\n", - "\n", - "# Define the observable to measure\n", - "observable = SparsePauliOp.from_sparse_list(\n", - " [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n", - " num_qubits,\n", - ")\n", - "\n", - "slices = slice_by_depth(circuit, max_slice_depth=1)\n", - "\n", - "# Define the maximum number of qwc groups allowed in the backpropagated observable, and the truncation error budget\n", - "op_budget = OperatorBudget(max_qwc_groups=15)\n", - "truncation_error_budget = setup_budget(\n", - " max_error_total=0.03, max_error_per_slice=0.005\n", - ")\n", - "\n", - "# First backpropagation without truncation\n", - "bp_obs, remaining_slices, metadata = backpropagate(\n", - " observable, slices, operator_budget=op_budget\n", - ")\n", - "bp_circuit = combine_slices(remaining_slices)\n", - "\n", - "# Now backpropagate with truncation, using the same operator budget and the defined truncation error budget\n", - "bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(\n", - " observable,\n", - " slices,\n", - " operator_budget=op_budget,\n", - " truncation_error_budget=truncation_error_budget,\n", - ")\n", - "bp_circuit_trunc = combine_slices(\n", - " remaining_slices_trunc, include_barriers=False\n", - ")\n", - "\n", - "# Now we transpile the original circuit and the two backpropagated circuits, and apply the layout to the corresponding observables\n", - "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", - "\n", - "isa_circuit = pm.run(circuit)\n", - "isa_bp_circuit = pm.run(bp_circuit)\n", - "isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)\n", - "\n", - "isa_observable = observable.apply_layout(isa_circuit.layout)\n", - "isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)\n", - "isa_bp_observable_trunc = bp_obs_trunc.apply_layout(\n", - " isa_bp_circuit_trunc.layout\n", - ")\n", - "\n", - "# Compare the 2-qubit depth of each transpiled circuit to see how much depth backpropagation saved\n", - "print(\n", - " f\"2-qubit depth without backpropagation: {isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n", - ")\n", - "print(\n", - " f\"2-qubit depth with backpropagation: {isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n", - ")\n", - "print(\n", - " f\"2-qubit depth with backpropagation and truncation: {isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}\"\n", - ")\n", - "\n", - "pubs = [\n", - " (isa_circuit, isa_observable),\n", - " (isa_bp_circuit, isa_bp_observable),\n", - " (isa_bp_circuit_trunc, isa_bp_observable_trunc),\n", - "]\n", - "\n", - "# Now we instantiate the Estimator primitive for the hardware with ZNE and measurement error mitigation\n", - "# and compute the three circuits and observables\n", - "options = EstimatorOptions()\n", - "options.default_precision = 0.01\n", - "options.resilience_level = 2\n", - "options.resilience.zne.noise_factors = [1, 1.2, 1.4]\n", - "options.resilience.zne.extrapolator = [\"linear\"]\n", - "estimator = EstimatorV2(mode=backend, options=options)\n", - "\n", - "estimator.options.environment.job_tags = [\"TUT_OBP\"]\n", - "job = estimator.run(pubs)\n", - "\n", - "# Retrieve the results and the standard deviations\n", - "result_no_bp = job.result()[0].data.evs.item()\n", - "result_bp = job.result()[1].data.evs.item()\n", - "result_bp_trunc = job.result()[2].data.evs.item()\n", - "\n", - "std_no_bp = job.result()[0].data.stds.item()\n", - "std_bp = job.result()[1].data.stds.item()\n", - "std_bp_trunc = job.result()[2].data.stds.item()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "4a0155bf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Expectation value without backpropagation: 0.9543907942381811\n", - "Backpropagated expectation value: 0.9445337385406468\n", - "Backpropagated expectation value with truncation: 0.934050286970965\n" - ] - } - ], - "source": [ - "print(f\"Expectation value without backpropagation: {result_no_bp}\")\n", - "print(f\"Backpropagated expectation value: {result_bp}\")\n", - "print(f\"Backpropagated expectation value with truncation: {result_bp_trunc}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "37834c72", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0, 0.5, '$M_Z$')" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Plot the results\n", - "methods = [\n", - " \"No backpropagation\",\n", - " \"Backpropagation\",\n", - " \"Backpropagation w/ truncation\",\n", - "]\n", - "values = [result_no_bp, result_bp, result_bp_trunc]\n", - "error_bars = [std_no_bp, std_bp, std_bp_trunc]\n", - "\n", - "ax = plt.gca()\n", - "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n", - "plt.errorbar(methods, values, yerr=error_bars, fmt=\"o\", color=\"r\", capsize=5)\n", - "plt.axhline(0.89)\n", - "ax.set_ylim([0.8, 0.98])\n", - "plt.text(0.25, 0.895, \"Exact result\")\n", - "ax.set_ylabel(r\"$M_Z$\", fontsize=12)" - ] - }, - { - "cell_type": "markdown", - "id": "75f48e6a-c7e4-46f3-9d39-a7a877427a04", - "metadata": {}, - "source": [ - "## Next steps\n", - "If you found this work interesting, you might be interested in the following material:\n", - "\n", - "- [Approximate quantum compilation for time evolution circuits](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n", - "- [Multi-product formulas to reduce Trotter error](/docs/tutorials/multi-product-formula)\n", - "- [`pauli-prop`](https://github.com/Qiskit/pauli-prop), a Rust-accelerated package for Pauli propagation, with [tutorials](https://github.com/Qiskit/pauli-prop/tree/main/docs/tutorials) covering OBP, classical expectation-value estimation, and noisy simulation\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d2c31ae8", + "metadata": {}, + "source": [ + "---\n", + "title: Operator backpropagation (OBP) for estimation of expectation values\n", + "description: This tutorial implements a Qiskit pattern for simulating the quantum dynamics of a Heisenberg spin chain using qiskit-addon-obp\n", + "---\n", + "\n", + "{/* cspell:ignore fontsize edgecolor circo simeq */}\n", + "\n", + "# Operator backpropagation (OBP) for estimation of expectation values\n", + "\n", + "*Usage estimate: 4 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "8bf80006", + "metadata": {}, + "source": [ + "## Learning outcomes\n", + "After going through this tutorial, users should understand:\n", + "- How to use [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) to reduce the depth of the quantum circuit at the cost of an increased number of circuit executions\n", + "- How to use [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) to construct XYZ Hamiltonians and their time-evolution circuits\n", + "\n", + "## Prerequisites\n", + "We suggest that users are familiar with the following topics before going through this tutorial:\n", + "- Using the [Estimator](/docs/api/qiskit-ibm-runtime/estimator-v2) primitive to calculate expectation values of an observable\n", + "\n", + "\n", + "## Background\n", + "Operator backpropagation is a technique that involves absorbing operations from the end of a quantum circuit into the measured observable, generally reducing the depth of the circuit at the cost of additional terms in the observable. The goal is to backpropagate as much of the circuit as possible without allowing the observable to grow too large. A Qiskit-based implementation is available in the OBP Qiskit addon. Read the corresponding [documentation](https://qiskit.github.io/qiskit-addon-obp/) for more information.\n", + "\n", + "Consider an example circuit for which an observable $O = \\sum_P c_P P$ is to be measured, where $P$ are Paulis and $c_P$ are coefficients. Let us denote the circuit as a single unitary $U$, which can be logically partitioned into $U = U_C U_Q$ as shown in the figure below.\n", + "\n", + "![Circuit diagram showing Uq followed by Uc](/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n", + "\n", + "Operator backpropagation absorbs the unitary $U_C$ into the observable by evolving it as $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. In other words, part of the computation is performed classically through the evolution of the observable from $O$ to $O'$. The original problem can now be reformulated as measuring the observable $O'$ for the new lower-depth circuit whose unitary is $U_Q$.\n", + "\n", + "The unitary $U_C$ is represented as a number of slices $U_C = U_S U_{S-1}...U_2U_1$. There are multiple ways to define a slice. For instance, in the above example circuit, each layer of $R_{zz}$ and each layer of $R_x$ gates can be considered as an individual slice. Backpropagation involves calculation of $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$ classically. Each slice $U_s$ can be represented as $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, where $P_s$ is a $n$-qubit Pauli and $\\theta_s$ is a scalar. It is easy to verify that\n", + "\n", + "$$\n", + "U_s^{\\dagger} P U_s = P \\qquad \\text{if} ~[P,P_s] = 0,\n", + "$$\n", + "\n", + "$$\n", + "U_s^{\\dagger} P U_s = \\qquad cos(\\theta_s)P + i sin(\\theta_s)P_sP \\qquad \\text{if} ~\\{P,P_s\\} = 0\n", + "$$\n", + "\n", + "In the above example, if $\\{P,P_s\\} = 0$, then we need to execute two quantum circuits, instead of one, to calculate the expectation value. Therefore, backpropagation might increase the number of terms in the observable, leading to a higher number of circuit executions. One way to allow for deeper backpropagation into the circuit, while preventing the operator from growing too large, is to truncate terms with small coefficients, rather than adding them to the operator. For instance, in the above example, one could choose to truncate the term involving $P_sP$ provided that $\\theta_s$ is sufficiently small. Truncating terms can result in fewer quantum circuits to execute, but doing so results in some error in the final expectation value calculation proportional to the magnitude of the truncated terms' coefficients." + ] + }, + { + "cell_type": "markdown", + "id": "55b94021", + "metadata": {}, + "source": [ + "## Requirements\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "\n", + "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)\n", + "- OBP Qiskit addon 0.3 or later (`pip install qiskit-addon-obp`)\n", + "- Qiskit addon utils 0.3 or later (`pip install qiskit-addon-utils`)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7db2e559", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bc380c46", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from qiskit.primitives import StatevectorEstimator\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.transpiler import CouplingMap\n", + "from qiskit.synthesis import LieTrotter\n", + "\n", + "from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian\n", + "from qiskit_addon_utils.problem_generators import (\n", + " generate_time_evolution_circuit,\n", + ")\n", + "from qiskit_addon_utils.slicing import slice_by_depth, combine_slices\n", + "from qiskit_addon_obp.utils.simplify import OperatorBudget\n", + "from qiskit_addon_obp import backpropagate\n", + "from qiskit_addon_obp.utils.truncating import setup_budget\n", + "\n", + "from rustworkx.visualization import graphviz_draw\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions" + ] + }, + { + "cell_type": "markdown", + "id": "431a5bd2-e6ed-471b-ad9e-c4edd27784a8", + "metadata": {}, + "source": [ + "## Small-scale simulator example\n", + "This tutorial implements a [Qiskit pattern](/docs/guides/intro-to-patterns) for simulating the quantum dynamics of a Heisenberg spin chain using the [OBP Qiskit addon](https://github.com/Qiskit/qiskit-addon-obp). Note that in a noiseless simulator, the expectation value obtained with and without backpropagation will be same." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "988ee237", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "#### Map the time-evolution of a quantum Heisenberg model to a quantum experiment\n", + "\n", + "First, we shall use the [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) function from `qiskit-addon-utils` to generate a Heisenberg-like Hamiltonian on a given connectivity graph. This graph can be either a [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) or a [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap). In the following, we shall use a linear chain `CouplingMap` of 10 qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a3debf65-06df-4277-933e-14b6f6170756", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "num_qubits = 10\n", + "layout = [(i - 1, i) for i in range(1, num_qubits)]\n", + "\n", + "# Instantiate a CouplingMap object\n", + "coupling_map = CouplingMap(layout)\n", + "graphviz_draw(coupling_map.graph, method=\"circo\")" + ] + }, + { + "cell_type": "markdown", + "id": "ef13f367", + "metadata": {}, + "source": [ + "Next, we generate a Pauli operator modeling a Heisenberg XYZ Hamiltonian:\n", + "\n", + "$$\n", + "{\\hat{\\mathcal{H}}_{XYZ} = \\sum_{(j,k)\\in E} (J_{x} \\sigma_j^{x} \\sigma_{k}^{x} + J_{y} \\sigma_j^{y} \\sigma_{k}^{y} + J_{z} \\sigma_j^{z} \\sigma_{k}^{z}) + \\sum_{j\\in V} (h_{x} \\sigma_j^{x} + h_{y} \\sigma_j^{y} + h_{z} \\sigma_j^{z}),}\n", + "$$\n", + "\n", + "where $G(V,E)$ is the graph of the coupling map. For this tutorial, we have used $J_x, J_y, J_z$ to be $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$, respectively, and $h_x, h_y, h_z$ to be $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b0f1e5fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],\n", + " coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n", + " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n", + " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n", + " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n", + " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n", + " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n", + " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,\n", + " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", + " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n", + " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n", + " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", + " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n", + " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n", + " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n", + " 0.34906585+0.j])\n" + ] + } + ], + "source": [ + "# Get a qubit operator describing the Heisenberg XYZ model\n", + "hamiltonian = generate_xyz_hamiltonian(\n", + " coupling_map,\n", + " coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n", + " ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n", + ")\n", + "print(hamiltonian)" + ] + }, + { + "cell_type": "markdown", + "id": "a81f0346", + "metadata": {}, + "source": [ + "From the qubit operator, we can generate a quantum circuit which models its time evolution. We have used [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) with Lie Trotter decomposition to construct the time evolution circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5208e0a8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " time=0.2,\n", + " synthesis=LieTrotter(reps=2),\n", + ")\n", + "circuit.draw(\"mpl\", style=\"iqp\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "ac6f36e3", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "#### Create circuit slices to backpropagate\n", + "\n", + "The `backpropagate` function backpropagates entire circuit slices at a time. Therefore, the choice of slicing can have an impact on how well backpropagation performs for a given problem. Here, we will group gates of the same type into slices using the [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) function.\n", + "\n", + "For a more detailed discussion on circuit slicing, check out this [how-to guide](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) of the [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) package." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1834cb22", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Separated the circuit into 18 slices.\n" + ] + } + ], + "source": [ + "slices = slice_by_depth(circuit, max_slice_depth=1)\n", + "print(f\"Separated the circuit into {len(slices)} slices.\")" + ] + }, + { + "cell_type": "markdown", + "id": "1df16e6b", + "metadata": {}, + "source": [ + "#### Constrain how large the operator can grow during backpropagation\n", + "\n", + "During backpropagation, the number of terms in the operator will generally approach $2^L$ quickly, where $L$ is the number of slices. When two terms in the operator do not commute qubit-wise, we need separate circuits to obtain the expectation values corresponding to them. For example, if we have a two-qubit observable $O = 0.1 XX + 0.3 IZ - 0.5 IX$, then since $[XX,IX] = 0$, measurement in a single basis is sufficient to calculate the expectation values for these two terms. However, $IZ$ anti-commutes with the other two terms, so we need a separate basis measurement to calculate the expectation value of $IZ$. In other words, we need two circuits instead of one to calculate $\\langle O \\rangle$. As the number of terms in the operator increases, there is a possibility that the required number of circuit executions also increases.\n", + "\n", + "The size of the operator can be bounded by specifying the `operator_budget` kwarg of the `backpropagate` function, which accepts an [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instance.\n", + "\n", + "To control the amount of extra resources (number of circuit execution, and hence the QPU time required) allocated, we restrict the maximum number of qubit-wise commuting Pauli groups that the backpropagated observable is allowed to have. Here we specify that backpropagation should stop when the number of qubit-wise commuting Pauli groups in the operator grows past eight." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3074b173", + "metadata": {}, + "outputs": [], + "source": [ + "op_budget = OperatorBudget(max_qwc_groups=8)" + ] + }, + { + "cell_type": "markdown", + "id": "8bd6ac5f", + "metadata": {}, + "source": [ + "#### Backpropagate slices from the circuit\n", + "\n", + "First we specify the observable to be $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, $N$ being the number of qubits. We will backpropagate slices from the time-evolution circuit until the terms in the observable can no longer be combined into eight or fewer qubit-wise commuting Pauli groups." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a1300365", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],\n", + " coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,\n", + " 0.1+0.j, 0.1+0.j])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "observable = SparsePauliOp.from_sparse_list(\n", + " [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n", + " num_qubits=num_qubits,\n", + ")\n", + "observable" + ] + }, + { + "cell_type": "markdown", + "id": "820236bf", + "metadata": {}, + "source": [ + "Below you will see that we backpropagated six slices, and the terms were combined into six and not eight groups. This implies that backpropagating one more slice would cause the number of Pauli groups to exceed eight. We can verify that this is the case by inspecting the returned metadata. Also note that in this portion the circuit transformation is exact. That is, no terms of the new observable $O’$ were truncated. The backpropagated circuit and the backpropagated operator give the exact outcome as the original circuit and operator." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ee8fd385", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Backpropagated 6 slices.\n", + "New observable has 60 terms, which can be combined into 6 groups.\n", + "Note that backpropagating one more slice would result in 114 terms across 12 groups.\n", + "The remaining circuit after backpropagation looks as follows:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Backpropagate slices onto the observable\n", + "bp_obs, remaining_slices, metadata = backpropagate(\n", + " observable, slices, operator_budget=op_budget\n", + ")\n", + "# Recombine the slices remaining after backpropagation\n", + "bp_circuit = combine_slices(remaining_slices)\n", + "\n", + "print(f\"Backpropagated {metadata.num_backpropagated_slices} slices.\")\n", + "print(\n", + " f\"New observable has {len(bp_obs.paulis)} terms, which can be combined into {len(bp_obs.group_commuting(qubit_wise=True))} groups.\"\n", + ")\n", + "print(\n", + " f\"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms \"\n", + " f\"across {metadata.backpropagation_history[-1].num_qwc_groups} groups.\"\n", + ")\n", + "print(\"The remaining circuit after backpropagation looks as follows:\")\n", + "bp_circuit.draw(\"mpl\", fold=-1, scale=0.6)" + ] + }, + { + "cell_type": "markdown", + "id": "833d9c36", + "metadata": {}, + "source": [ + "For the small-scale example on a simulator, we will not use truncation. This is because in the absence of noise, the circuit with and without backpropagation leads to the same result, and truncation makes the result worse due to the added approximation." + ] + }, + { + "cell_type": "markdown", + "id": "43f58cfb", + "metadata": {}, + "source": [ + "#### Transpile the circuits into the basis gate set\n", + "\n", + "Now we transpile both the original and the backpropagated circuits into the basis gate of the backend. We don't need to transpile on the actual backend since we are going to run on a simulator for the small instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29d71cd3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=133\n", + ")\n", + "print(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "18707b33", + "metadata": {}, + "outputs": [], + "source": [ + "pm_basis = generate_preset_pass_manager(\n", + " optimization_level=3, basis_gates=backend.configuration().basis_gates\n", + ")\n", + "isa_circuit = pm_basis.run(circuit)\n", + "isa_bp_circuit = pm_basis.run(bp_circuit)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b4d480b3", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "First, we create two [Primitive Unified Blocs](/docs/api/qiskit/primitives) (PUBs) corresponding to the original circuit, and the backpropagated circuit. Then we execute the pubs on an ideal Estimator to obtain the expectation values." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b22a1b00", + "metadata": {}, + "outputs": [], + "source": [ + "pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "eb174b15", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng()\n", + "estimator = StatevectorEstimator(seed=rng)\n", + "job = estimator.run(pubs)" + ] + }, + { + "cell_type": "markdown", + "id": "50b94af2", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Now we obtain the expectation values of the original and backpropagated circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "31dc35ea-6554-4ca7-9c3b-0b5394c46e4e", + "metadata": {}, + "outputs": [], + "source": [ + "primitive_result = job.result()\n", + "circuit_expval = primitive_result[0].data.evs.item()\n", + "bp_circuit_expval = primitive_result[1].data.evs.item()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fb5f955a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, '$M_Z$')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "methods = [\n", + " \"No backpropagation\",\n", + " \"Backpropagation\",\n", + "]\n", + "values = [circuit_expval, bp_circuit_expval]\n", + "\n", + "ax = plt.gca()\n", + "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n", + "ax.set_ylim([0.6, 0.92])\n", + "ax.set_ylabel(r\"$M_Z$\", fontsize=12)" + ] + }, + { + "cell_type": "markdown", + "id": "c0be738f", + "metadata": {}, + "source": [ + "As expected, the two expectation values agree. Because we are running on a noiseless statevector simulator, backpropagation is an exact transformation of the circuit-observable pair, so the original and backpropagated workflows must produce the same value of $M_Z$. The benefit of backpropagation only becomes apparent on noisy hardware, where the shorter backpropagated circuit accumulates less error, as illustrated in the large-scale hardware example below." + ] + }, + { + "cell_type": "markdown", + "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6", + "metadata": {}, + "source": [ + "## Large-scale hardware example" + ] + }, + { + "cell_type": "markdown", + "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25", + "metadata": {}, + "source": [ + "When developing an experiment, it's useful to start with a small circuit to make visualizations and simulations easier. Now we look into operator backpropagation for a 50-qubit Heisenberg Hamiltonian with the same set of values for the $J$ and $h$ parameters and the same observable $M_Z$, but for four Trotter steps. The ideal expectation value at this scale cannot be calculated in a brute force method, so we use a tensor network and obtain the ideal expectation value to be $\\simeq 0.89$.\n", + "\n", + "Along with backpropagation, in this large-scale example, we also introduce backpropagation with truncation. Ideally we want to backpropagate as much as possible to reduce the depth of the effective circuit. However, it often leads to a large number of non-commuting terms in the updated observable, increasing the quantum overhead. Therefore, we can eliminate observable terms with small coefficients using a practice called truncation. While truncation allows more propagation by reducing the number of terms in the updated observable, it also introduces some approximation. Therefore, it is necessary to restrict the truncation within some limits so that the approximation error does not overwhelm the reduction in noise obtained from deeper backpropagation.\n", + "\n", + "To restrict the amount of truncation, we allot an error budget for each slice as well as the total error budget over the entire backpropagated circuit using the [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) function. This ensures that the truncation is controlled for each slice as well as for the entire circuit. See also this [guide](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) for other ways of allocating the budget." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "28ac4dbf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2-qubit depth without backpropagation: 24\n", + "2-qubit depth with backpropagation: 20\n", + "2-qubit depth with backpropagation and truncation: 18\n" + ] + } + ], + "source": [ + "num_qubits = 50\n", + "layout = [(i - 1, i) for i in range(1, num_qubits)]\n", + "\n", + "# Instantiate a CouplingMap object\n", + "coupling_map = CouplingMap(layout)\n", + "\n", + "hamiltonian = generate_xyz_hamiltonian(\n", + " coupling_map,\n", + " coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n", + " ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n", + ")\n", + "\n", + "# Generate a time evolution circuit for the Hamiltonian\n", + "circuit = generate_time_evolution_circuit(\n", + " hamiltonian,\n", + " time=0.2,\n", + " synthesis=LieTrotter(reps=4),\n", + ")\n", + "\n", + "# Define the observable to measure\n", + "observable = SparsePauliOp.from_sparse_list(\n", + " [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n", + " num_qubits,\n", + ")\n", + "\n", + "slices = slice_by_depth(circuit, max_slice_depth=1)\n", + "\n", + "# Define the maximum number of qwc groups allowed in the backpropagated observable, and the\n", + "# truncation error budget\n", + "op_budget = OperatorBudget(max_qwc_groups=15)\n", + "truncation_error_budget = setup_budget(\n", + " max_error_total=0.03, max_error_per_slice=0.005\n", + ")\n", + "\n", + "# First backpropagation without truncation\n", + "bp_obs, remaining_slices, metadata = backpropagate(\n", + " observable, slices, operator_budget=op_budget\n", + ")\n", + "bp_circuit = combine_slices(remaining_slices)\n", + "\n", + "# Now backpropagate with truncation, using the same operator budget and the defined truncation error\n", + "# budget\n", + "bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(\n", + " observable,\n", + " slices,\n", + " operator_budget=op_budget,\n", + " truncation_error_budget=truncation_error_budget,\n", + ")\n", + "bp_circuit_trunc = combine_slices(\n", + " remaining_slices_trunc, include_barriers=False\n", + ")\n", + "\n", + "# Now we transpile the original circuit and the two backpropagated circuits, and apply the layout to\n", + "# the corresponding observables\n", + "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", + "\n", + "isa_circuit = pm.run(circuit)\n", + "isa_bp_circuit = pm.run(bp_circuit)\n", + "isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)\n", + "\n", + "isa_observable = observable.apply_layout(isa_circuit.layout)\n", + "isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)\n", + "isa_bp_observable_trunc = bp_obs_trunc.apply_layout(\n", + " isa_bp_circuit_trunc.layout\n", + ")\n", + "\n", + "# Compare the 2-qubit depth of each transpiled circuit to see how much depth backpropagation saved\n", + "print(\n", + " f\"2-qubit depth without backpropagation: {isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n", + ")\n", + "print(\n", + " f\"2-qubit depth with backpropagation: {isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n", + ")\n", + "print(\n", + " f\"2-qubit depth with backpropagation and truncation: {isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}\"\n", + ")\n", + "\n", + "pubs = [\n", + " (isa_circuit, isa_observable),\n", + " (isa_bp_circuit, isa_bp_observable),\n", + " (isa_bp_circuit_trunc, isa_bp_observable_trunc),\n", + "]\n", + "\n", + "# Now we instantiate the Estimator primitive for the hardware with ZNE and measurement error\n", + "# mitigation\n", + "# and compute the three circuits and observables\n", + "options = EstimatorOptions()\n", + "options.default_precision = 0.01\n", + "options.resilience_level = 2\n", + "options.resilience.zne.noise_factors = [1, 1.2, 1.4]\n", + "options.resilience.zne.extrapolator = [\"linear\"]\n", + "estimator = EstimatorV2(mode=backend, options=options)\n", + "\n", + "estimator.options.environment.job_tags = [\"TUT_OBP\"]\n", + "job = estimator.run(pubs)\n", + "\n", + "# Retrieve the results and the standard deviations\n", + "result_no_bp = job.result()[0].data.evs.item()\n", + "result_bp = job.result()[1].data.evs.item()\n", + "result_bp_trunc = job.result()[2].data.evs.item()\n", + "\n", + "std_no_bp = job.result()[0].data.stds.item()\n", + "std_bp = job.result()[1].data.stds.item()\n", + "std_bp_trunc = job.result()[2].data.stds.item()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4a0155bf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expectation value without backpropagation: 0.9543907942381811\n", + "Backpropagated expectation value: 0.9445337385406468\n", + "Backpropagated expectation value with truncation: 0.934050286970965\n" + ] + } + ], + "source": [ + "print(f\"Expectation value without backpropagation: {result_no_bp}\")\n", + "print(f\"Backpropagated expectation value: {result_bp}\")\n", + "print(f\"Backpropagated expectation value with truncation: {result_bp_trunc}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "37834c72", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, '$M_Z$')" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the results\n", + "methods = [\n", + " \"No backpropagation\",\n", + " \"Backpropagation\",\n", + " \"Backpropagation w/ truncation\",\n", + "]\n", + "values = [result_no_bp, result_bp, result_bp_trunc]\n", + "error_bars = [std_no_bp, std_bp, std_bp_trunc]\n", + "\n", + "ax = plt.gca()\n", + "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n", + "plt.errorbar(methods, values, yerr=error_bars, fmt=\"o\", color=\"r\", capsize=5)\n", + "plt.axhline(0.89)\n", + "ax.set_ylim([0.8, 0.98])\n", + "plt.text(0.25, 0.895, \"Exact result\")\n", + "ax.set_ylabel(r\"$M_Z$\", fontsize=12)" + ] + }, + { + "cell_type": "markdown", + "id": "75f48e6a-c7e4-46f3-9d39-a7a877427a04", + "metadata": {}, + "source": [ + "## Next steps\n", + "If you found this work interesting, you might be interested in the following material:\n", + "\n", + "- [Approximate quantum compilation for time evolution circuits](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n", + "- [Multi-product formulas to reduce Trotter error](/docs/tutorials/multi-product-formula)\n", + "- [`pauli-prop`](https://github.com/Qiskit/pauli-prop), a Rust-accelerated package for Pauli propagation, with [tutorials](https://github.com/Qiskit/pauli-prop/tree/main/docs/tutorials) covering OBP, classical expectation-value estimation, and noisy simulation\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/probabilistic-error-amplification.ipynb b/docs/tutorials/probabilistic-error-amplification.ipynb index d72b1c7f6ce..2372a22ba35 100644 --- a/docs/tutorials/probabilistic-error-amplification.ipynb +++ b/docs/tutorials/probabilistic-error-amplification.ipynb @@ -1,1346 +1,1346 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "d2c31ae8", - "metadata": {}, - "source": [ - "---\n", - "title: Utility-scale error mitigation with probabilistic error amplification\n", - "description: Run a utility-scale error mitigation experiment with zero noise extrapolation and probabilistic error amplification.\n", - "---\n", - "\n", - "{/* cspell:ignore mapsto multigraph inds extrap sharex sharey pidx */}\n", - "\n", - "# Utility-scale error mitigation with probabilistic error amplification\n", - "*Usage estimate: 14 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "8bf80006", - "metadata": {}, - "source": [ - "## Learning outcomes\n", - "After going through this tutorial, users should understand:\n", - "- The theory behind *zero-noise extrapolation* (ZNE), the different methods to amplify noise, and why *probabilistic error amplification* (PEA) is preferred for utility-scale experiments.\n", - "- How to implement ZNE with PEA in practice using Qiskit.\n", - "\n", - "## Prerequisites\n", - "We suggest that users are familiar with the following topics before going through this tutorial:\n", - "- The [Error mitigation lesson](/learning/courses/utility-scale-quantum-computing/error-mitigation) of the *Utility-scale quantum computing* course for basic knowledge of using error mitigation in Qiskit.\n", - "- The [Utility-I lesson](/learning/courses/utility-scale-quantum-computing/utility-i) of the *Utility-scale quantum computing* course for more background on the utility-scale experiment used as an example in this tutorial." - ] - }, - { - "cell_type": "markdown", - "id": "a929ccce", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "This tutorial demonstrates how to run a utility-scale error mitigation experiment with Qiskit Runtime using an experimental version of *zero-noise extrapolation* (ZNE) with *probabilistic error amplification* (PEA).\n", - "\n", - "![kim\\_nature\\_fig.png](https://quantum.cloud.ibm.com/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/e1e67c34-9d4d-4a88-9340-f0b2f3676770.avif)\n", - "**Reference**: Y. Kim et al. *Evidence for the utility of quantum computing before fault tolerance.* [Nature 618.7965 (2023)](https://www.nature.com/articles/s41586-023-06096-3)" - ] - }, - { - "cell_type": "markdown", - "id": "a89ed8dd", - "metadata": {}, - "source": [ - "### Zero-noise extrapolation (ZNE)\n", - "Zero-noise extrapolation (ZNE) is an error mitigation technique that removes the effects of an *unknown* noise during circuit execution that can be scaled in a *known* way.\n", - "\n", - "It assumes expectation values scale with noise by a known function\n", - "\n", - "$$\n", - "\\langle A(\\lambda) \\rangle = \\langle A(0) \\rangle + \\sum_{k=0}^{m} a_k \\lambda^k + R\n", - "$$\n", - "\n", - "where $\\lambda$ parameterizes the noise strength and can be amplified.\n", - "\n", - "We can implement ZNE with the following steps:\n", - "\n", - "1. Amplify circuit noise for several noise factors $\\lambda_1, \\lambda_2, ... $\n", - "2. Run every noise-amplified circuit to measure $\\langle A(\\lambda_1)\\rangle, ...$\n", - "3. Extrapolate back to the zero-noise limit $\\langle A(0)\\rangle$\n", - "\n", - "![zne\\_stages.png](https://quantum.cloud.ibm.com/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/5e63d706-82d8-4212-b802-c9191ce53341.avif)" - ] - }, - { - "cell_type": "markdown", - "id": "5db985b9", - "metadata": {}, - "source": [ - "#### Amplify noise for ZNE\n", - "\n", - "The main challenge in successfully implementing ZNE is to have an accurate model for noise in the expectation value and to amplify the noise in a known way.\n", - "\n", - "There are three common ways error amplification is implemented for ZNE.\n", - "\n", - "| **Pulse stretching** | **Gate folding** | **Probabilistic error amplification** |\n", - "| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n", - "| Scale pulse duration via calibration | Repeat gates in identity cycles $U\\mapsto U(U^{-1}U)^{\\lambda-1}/2$ | Add noise via sampling Pauli channels |\n", - "| ![zne\\_pulse\\_stretching.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/83188b57-e88f-43a1-a7bd-29327f46ecf5.avif) | ![zne\\_gate\\_folding.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/e1358d08-2632-4fd2-bf0f-f9384a2d3340.avif) | ![zne\\_pea.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/3d69d5bd-70e5-4eeb-aa02-fc0a62043010.avif) |\n", - "| Kandala et al. Nature (2019) | Shultz et al. PRA (2022) | Li & Benjamin PRX (2017) |" - ] - }, - { - "cell_type": "markdown", - "id": "c23e43ee", - "metadata": {}, - "source": [ - "For utility-scale experiments, *probabilistic error amplification* (PEA) is the most attractive.\n", - "\n", - "* Pulse stretching assumes gate noise is proportional to duration, which is typically not true. Calibration is also costly.\n", - "* Gate folding requires large stretch factors that greatly limit the depth of circuits that can be run.\n", - "* PEA can be applied to any circuit that can be run with native noise factor ($\\lambda=1$) but requires learning the noise model.\n", - "\n", - "### Learn the noise model for PEA\n", - "PEA assumes the same layer-based noise model as *probabilistic error cancellation* (PEC); however, it avoids the sampling overhead that scales exponentially with the circuit noise.\n", - "\n", - "| **Step 1** | **Step 2** | **Step 3** |\n", - "| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n", - "| Pauli twirl layers of two-qubit gates | Repeat identity pairs of layers and learn the noise | Derive a fidelity (error for each noise channel) |\n", - "| ![pec\\_pauli\\_twirling.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/2eab5ff4-40fa-4a41-9f2c-74f5e22c4643.avif) | ![pec\\_learn\\_layer.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/8d0d64c3-65ad-4419-8ac9-4ec9633d39a0.avif) | ![pec\\_curve\\_fitting.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/c51bd42d-2463-4c78-807b-d284ca79296f.avif) |\n", - "\n", - "**Reference**: E. van den Berg, Z. Minev, A. Kandala, and K. Temme, *Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors* [arXiv:2201.09866](https://arxiv.org/abs/2201.09866)" - ] - }, - { - "cell_type": "markdown", - "id": "55b94021", - "metadata": {}, - "source": [ - "## Requirements\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "\n", - "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7db2e559", - "metadata": {}, - "source": [ - "## Setup\n", - "In the cell below, we import relevant packages and create some helper functions to construct the circuits for the Trotterized time evolution of a two-dimensional transverse-field Ising model that adheres to the topology of the backend." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "779bbc51", - "metadata": {}, - "outputs": [], - "source": [ - "from __future__ import annotations\n", - "from collections.abc import Sequence\n", - "from collections import defaultdict\n", - "import numpy as np\n", - "import rustworkx\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from qiskit.circuit import QuantumCircuit, Parameter\n", - "from qiskit.circuit.library import CXGate, CZGate, ECRGate\n", - "from qiskit.providers import Backend\n", - "from qiskit.visualization import plot_error_map\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.primitives import PubResult\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "\n", - "\"\"\"Trotter circuit generation\"\"\"\n", - "\n", - "\n", - "def remove_qubit_couplings(\n", - " couplings: Sequence[tuple[int, int]], qubits: Sequence[int] | None = None\n", - ") -> list[tuple[int, int]]:\n", - " \"\"\"Remove qubits from a coupling list.\n", - "\n", - " Args:\n", - " couplings: A sequence of qubit couplings.\n", - " qubits: Optional, the qubits to remove.\n", - "\n", - " Returns:\n", - " The input couplings with the specified qubits removed.\n", - " \"\"\"\n", - " if qubits is None:\n", - " return couplings\n", - " qubits = set(qubits)\n", - " return [edge for edge in couplings if not qubits.intersection(edge)]\n", - "\n", - "\n", - "def coupling_qubits(\n", - " *couplings: Sequence[tuple[int, int]],\n", - " allowed_qubits: Sequence[int] | None = None,\n", - ") -> list[int]:\n", - " \"\"\"Return a sorted list of all qubits involved in one or more couplings lists.\n", - "\n", - " Args:\n", - " couplings: one or more coupling lists.\n", - " allowed_qubits: Optional, the allowed qubits to include. If None all\n", - " qubits are allowed.\n", - "\n", - " Returns:\n", - " The intersection of all qubits in the couplings and the allowed qubits.\n", - " \"\"\"\n", - " qubits = set()\n", - " for edges in couplings:\n", - " for edge in edges:\n", - " qubits.update(edge)\n", - " if allowed_qubits is not None:\n", - " qubits = qubits.intersection(allowed_qubits)\n", - " return list(qubits)\n", - "\n", - "\n", - "def construct_layer_couplings(\n", - " backend: Backend,\n", - ") -> list[list[tuple[int, int]]]:\n", - " \"\"\"Separate a coupling map into disjoint 2-qubit gate layers.\n", - "\n", - " Args:\n", - " backend: A backend to construct layer couplings for.\n", - "\n", - " Returns:\n", - " A list of disjoint layers of directed couplings for the input coupling map.\n", - " \"\"\"\n", - " coupling_graph = backend.coupling_map.graph.to_undirected(\n", - " multigraph=False\n", - " )\n", - " edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)\n", - "\n", - " layers = defaultdict(list)\n", - " for edge_idx, color in edge_coloring.items():\n", - " layers[color].append(\n", - " coupling_graph.get_edge_endpoints_by_index(edge_idx)\n", - " )\n", - " layers = [sorted(layers[i]) for i in sorted(layers.keys())]\n", - "\n", - " return layers\n", - "\n", - "\n", - "def entangling_layer(\n", - " gate_2q: str,\n", - " couplings: Sequence[tuple[int, int]],\n", - " qubits: Sequence[int] | None = None,\n", - ") -> QuantumCircuit:\n", - " \"\"\"Generating a entangling layer for the specified couplings.\n", - "\n", - " This corresponds to a Trotter layer for a ZZ Ising term with angle Pi/2.\n", - "\n", - " Args:\n", - " gate_2q: The 2-qubit basis gate for the layer, should be \"cx\", \"cz\", or \"ecr\".\n", - " couplings: A sequence of qubit couplings to add CX gates to.\n", - " qubits: Optional, the physical qubits for the layer. Any couplings involving\n", - " qubits not in this list will be removed. If None the range up to the largest\n", - " qubit in the couplings will be used.\n", - "\n", - " Returns:\n", - " The QuantumCircuit for the entangling layer.\n", - " \"\"\"\n", - " # Get qubits and convert to set to order\n", - " if qubits is None:\n", - " qubits = range(1 + max(coupling_qubits(couplings)))\n", - " qubits = set(qubits)\n", - "\n", - " # Mapping of physical qubit to virtual qubit\n", - " qubit_mapping = {q: i for i, q in enumerate(qubits)}\n", - "\n", - " # Convert couplings to indices for virtual qubits\n", - " indices = [\n", - " [qubit_mapping[i] for i in edge]\n", - " for edge in couplings\n", - " if qubits.issuperset(edge)\n", - " ]\n", - "\n", - " # Layer circuit on virtual qubits\n", - " circuit = QuantumCircuit(len(qubits))\n", - "\n", - " # Get 2-qubit basis gate and pre and post rotation circuits\n", - " gate2q = None\n", - " pre = QuantumCircuit(2)\n", - " post = QuantumCircuit(2)\n", - "\n", - " if gate_2q == \"cx\":\n", - " gate2q = CXGate()\n", - " # Pre-rotation\n", - " pre.sdg(0)\n", - " pre.z(1)\n", - " pre.sx(1)\n", - " pre.s(1)\n", - " # Post-rotation\n", - " post.sdg(1)\n", - " post.sxdg(1)\n", - " post.s(1)\n", - " elif gate_2q == \"ecr\":\n", - " gate2q = ECRGate()\n", - " # Pre-rotation\n", - " pre.z(0)\n", - " pre.s(1)\n", - " pre.sx(1)\n", - " pre.s(1)\n", - " # Post-rotation\n", - " post.x(0)\n", - " post.sdg(1)\n", - " post.sxdg(1)\n", - " post.s(1)\n", - " elif gate_2q == \"cz\":\n", - " gate2q = CZGate()\n", - " # Identity pre-rotation\n", - " # Post-rotation\n", - " post.sdg([0, 1])\n", - " else:\n", - " raise ValueError(\n", - " f\"Invalid 2-qubit basis gate {gate_2q}, should be 'cx', 'cz', or 'ecr'\"\n", - " )\n", - "\n", - " # Add 1Q pre-rotations\n", - " for inds in indices:\n", - " circuit.compose(pre, qubits=inds, inplace=True)\n", - "\n", - " # Use barriers around 2-qubit basis gate to specify a layer for PEA noise learning\n", - " circuit.barrier()\n", - " for inds in indices:\n", - " circuit.append(gate2q, (inds[0], inds[1]))\n", - " circuit.barrier()\n", - "\n", - " # Add 1Q post-rotations after barrier\n", - " for inds in indices:\n", - " circuit.compose(post, qubits=inds, inplace=True)\n", - "\n", - " # Add physical qubits as metadata\n", - " circuit.metadata[\"physical_qubits\"] = tuple(qubits)\n", - "\n", - " return circuit\n", - "\n", - "\n", - "def trotter_circuit(\n", - " theta: Parameter | float,\n", - " layer_couplings: Sequence[Sequence[tuple[int, int]]],\n", - " num_steps: int,\n", - " gate_2q: str | None = \"cx\",\n", - " backend: Backend | None = None,\n", - " qubits: Sequence[int] | None = None,\n", - ") -> QuantumCircuit:\n", - " \"\"\"Generate a Trotter circuit for the 2D Ising\n", - "\n", - " Args:\n", - " theta: The angle parameter for X.\n", - " layer_couplings: A list of couplings for each entangling layer.\n", - " num_steps: the number of Trotter steps.\n", - " gate_2q: The 2-qubit basis gate to use in entangling layers.\n", - " Can be \"cx\", \"cz\", \"ecr\", or None if a backend is provided.\n", - " backend: A backend to get the 2-qubit basis gate from, if provided\n", - " will override the basis_gate field.\n", - " qubits: Optional, the allowed physical qubits to truncate the\n", - " couplings to. If None the range up to the largest\n", - " qubit in the couplings will be used.\n", - "\n", - " Returns:\n", - " The Trotter circuit.\n", - " \"\"\"\n", - " if backend is not None:\n", - " try:\n", - " basis_gates = backend.configuration().basis_gates\n", - " except AttributeError:\n", - " basis_gates = backend.basis_gates\n", - " for gate in [\"cx\", \"cz\", \"ecr\"]:\n", - " if gate in basis_gates:\n", - " gate_2q = gate\n", - " break\n", - "\n", - " # If no qubits, get the largest qubit from all layers and\n", - " # specify the range so the same one is used for all layers.\n", - " if qubits is None:\n", - " qubits = range(1 + max(coupling_qubits(layer_couplings)))\n", - "\n", - " # Generate the entangling layers\n", - " layers = [\n", - " entangling_layer(gate_2q, couplings, qubits=qubits)\n", - " for couplings in layer_couplings\n", - " ]\n", - "\n", - " # Construct the circuit for a single Trotter step\n", - " num_qubits = len(qubits)\n", - " trotter_step = QuantumCircuit(num_qubits)\n", - " trotter_step.rx(theta, range(num_qubits))\n", - " for layer in layers:\n", - " trotter_step.compose(layer, range(num_qubits), inplace=True)\n", - "\n", - " # Construct the circuit for the specified number of Trotter steps\n", - " circuit = QuantumCircuit(num_qubits)\n", - " for _ in range(num_steps):\n", - " circuit.rx(theta, range(num_qubits))\n", - " for layer in layers:\n", - " circuit.compose(layer, range(num_qubits), inplace=True)\n", - "\n", - " circuit.metadata[\"physical_qubits\"] = tuple(qubits)\n", - " return circuit\n", - "\n", - "\n", - "\"\"\"Result visualization functions\"\"\"\n", - "\n", - "\n", - "def plot_trotter_results(\n", - " pub_result: PubResult,\n", - " angles: Sequence[float],\n", - " plot_noise_factors: Sequence[float] | None = None,\n", - " plot_extrapolator: Sequence[str] | None = None,\n", - " exact: np.ndarray = None,\n", - " close: bool = True,\n", - "):\n", - " \"\"\"Plot average magnetization from ZNE result data.\n", - " Args:\n", - " pub_result: The Estimator PubResult for the PEA experiment.\n", - " angles: The Rx angle values for the experiment.\n", - " plot_raw: If provided plot the unextrapolated data for the noise factors.\n", - " plot_extrapolator: If provided plot all extrapolators, if False only plot\n", - " the Automatic method.\n", - " exact: Optional, the exact values to include in the plot. Should be a 1D\n", - " array-like where the values represent exact magnetization.\n", - " close: Close the Matplotlib figure before returning.\n", - " Returns:\n", - " The figure.\n", - " \"\"\"\n", - " data = pub_result.data\n", - "\n", - " evs = data.evs\n", - " num_qubits = evs.shape[0]\n", - " num_params = evs.shape[1]\n", - " angles = np.asarray(angles).ravel()\n", - " if angles.shape != (num_params,):\n", - " raise ValueError(\n", - " f\"Incorrect number of angles for input data {angles.size} != {num_params}\"\n", - " )\n", - "\n", - " # Take average magnetization of qubits and its standard error\n", - " x_vals = angles / np.pi\n", - " y_vals = np.mean(evs, axis=0)\n", - " y_errs = np.std(evs, axis=0) / np.sqrt(num_qubits)\n", - "\n", - " fig, _ = plt.subplots(1, 1)\n", - "\n", - " # Plot auto method\n", - " plt.errorbar(x_vals, y_vals, y_errs, fmt=\"o-\", label=\"ZNE (automatic)\")\n", - "\n", - " # Plot individual extrapolator results\n", - " if plot_extrapolator:\n", - " y_vals_extrap = np.mean(data.evs_extrapolated, axis=0)\n", - " y_errs_extrap = np.std(data.evs_extrapolated, axis=0) / np.sqrt(\n", - " num_qubits\n", - " )\n", - " for i, extrap in enumerate(plot_extrapolator):\n", - " plt.errorbar(\n", - " x_vals,\n", - " y_vals_extrap[:, i, 0],\n", - " y_errs_extrap[:, i, 0],\n", - " fmt=\"s-.\",\n", - " alpha=0.5,\n", - " label=f\"ZNE ({extrap})\",\n", - " )\n", - "\n", - " # Plot raw results\n", - " if plot_noise_factors:\n", - " y_vals_raw = np.mean(data.evs_noise_factors, axis=0)\n", - " y_errs_raw = np.std(data.evs_noise_factors, axis=0) / np.sqrt(\n", - " num_qubits\n", - " )\n", - " for i, nf in enumerate(plot_noise_factors):\n", - " plt.errorbar(\n", - " x_vals,\n", - " y_vals_raw[:, i],\n", - " y_errs_raw[:, i],\n", - " fmt=\"d:\",\n", - " alpha=0.5,\n", - " label=f\"Raw (nf={nf:.1f})\",\n", - " )\n", - "\n", - " # Plot exact data\n", - " if exact is not None:\n", - " plt.plot(x_vals, exact, \"--\", color=\"black\", alpha=0.5, label=\"Exact\")\n", - "\n", - " plt.ylim(-0.1, 1.2)\n", - " plt.xlabel(\"θ/π\")\n", - " plt.ylabel(r\"$\\overline{\\langle Z \\rangle}$\")\n", - " plt.legend()\n", - " plt.title(\n", - " f\"Error Mitigated Average Magnetization for Rx(θ) [{num_qubits}-qubit]\"\n", - " )\n", - " if close:\n", - " plt.close(fig)\n", - " return fig\n", - "\n", - "\n", - "def plot_qubit_zne_data(\n", - " pub_result: PubResult,\n", - " angles: Sequence[float],\n", - " qubit: int,\n", - " noise_factors: Sequence[float],\n", - " extrapolator: Sequence[str] | None = None,\n", - " extrapolated_noise_factors: Sequence[float] | None = None,\n", - " num_cols: int | None = None,\n", - " close: bool = True,\n", - "):\n", - " \"\"\"Plot ZNE extrapolation data for specific virtual qubit\n", - " Args:\n", - " pub_result: The Estimator PubResult for the PEA experiment.\n", - " angles: The Rx theta angles used for the experiment.\n", - " qubit: The virtual qubit index to plot.\n", - " noise_factors: the raw noise factors.\n", - " extrapolator: The extrapolator metadata for multiple extrapolators.\n", - " extrapolated_noise_factors: The noise factors used for extrapolation.\n", - " num_cols: The number of columns for the generated subplots.\n", - " close: Close the Matplotlib figure before returning.\n", - " Returns:\n", - " The Matplotlib figure.\n", - " \"\"\"\n", - " data = pub_result.data\n", - "\n", - " evs_auto = data.evs[qubit]\n", - " stds_auto = data.stds[qubit]\n", - " evs_extrap = data.evs_extrapolated[qubit]\n", - " stds_extrap = data.stds_extrapolated[qubit]\n", - " evs_raw = data.evs_noise_factors[qubit]\n", - " stds_raw = data.stds_noise_factors[qubit]\n", - "\n", - " num_params = evs_auto.shape[0]\n", - " angles = np.asarray(angles).ravel()\n", - " if angles.shape != (num_params,):\n", - " raise ValueError(\n", - " f\"Incorrect number of angles for input data {angles.size} != {num_params}\"\n", - " )\n", - "\n", - " # Make a square subplot\n", - " num_cols = num_cols or int(np.ceil(np.sqrt(num_params)))\n", - " num_rows = int(np.ceil(num_params / num_cols))\n", - " fig, axes = plt.subplots(\n", - " num_rows, num_cols, sharex=True, sharey=True, figsize=(12, 5)\n", - " )\n", - " fig.suptitle(f\"ZNE data for virtual qubit {qubit}\")\n", - "\n", - " for pidx, ax in zip(range(num_params), axes.flat):\n", - " # Plot auto extrapolated\n", - " ax.errorbar(\n", - " 0,\n", - " evs_auto[pidx],\n", - " stds_auto[pidx],\n", - " fmt=\"o\",\n", - " label=\"PEA (automatic)\",\n", - " )\n", - "\n", - " # Plot extrapolators\n", - " if (\n", - " extrapolator is not None\n", - " and extrapolated_noise_factors is not None\n", - " ):\n", - " for i, method in enumerate(extrapolator):\n", - " ax.errorbar(\n", - " extrapolated_noise_factors,\n", - " evs_extrap[pidx, i],\n", - " stds_extrap[pidx, i],\n", - " fmt=\"-\",\n", - " alpha=0.5,\n", - " label=f\"PEA ({method})\",\n", - " )\n", - "\n", - " # Plot raw\n", - " ax.errorbar(\n", - " noise_factors, evs_raw[pidx], stds_raw[pidx], fmt=\"d\", label=\"Raw\"\n", - " )\n", - "\n", - " ax.set_yticks([0, 0.5, 1, 1.5, 2])\n", - " ax.set_ylim(0, max(1, 1.1 * max(evs_auto)))\n", - "\n", - " ax.set_xticks([0, *noise_factors])\n", - " ax.set_title(f\"θ/π = {angles[pidx]/np.pi:.2f}\")\n", - " if pidx == 0:\n", - " ax.set_ylabel(r\"$\\langle Z_{\" + str(qubit) + r\"} \\rangle$\")\n", - " if pidx == num_params - 1:\n", - " ax.set_xlabel(\"Noise Factor\")\n", - " ax.legend()\n", - " plt.tight_layout()\n", - " if close:\n", - " plt.close(fig)\n", - " return fig" - ] - }, - { - "cell_type": "markdown", - "id": "431a5bd2-e6ed-471b-ad9e-c4edd27784a8", - "metadata": {}, - "source": [ - "## Small-scale simulator example\n", - "We will forgo this step since runtime error mitigation is not supported on simulators.\n", - "\n", - "## Large-scale hardware example" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "988ee237", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "#### Create a parameterized Ising model circuit\n", - "##### Establish a backend\n", - "First, choose a backend to run on. This demonstration runs on a 127-qubit backend, but you can modify this to any backend available to you." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a3debf65-06df-4277-933e-14b6f6170756", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ")\n", - "backend" - ] - }, - { - "cell_type": "markdown", - "id": "c13564d0", - "metadata": {}, - "source": [ - "##### Define entangling layer couplings\n", - "To implement the Trotterized Ising simulation, define three layers of two-qubit gate couplings for the device, to be repeated at each of the Trotter steps. These define the three twirled layers you need to learn the noise for to implement mitigation." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "0211a3f8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Layer 0:\n", - "[(2, 3), (4, 5), (6, 7), (8, 9), (10, 11), (12, 13), (14, 15), (16, 23), (18, 31), (19, 35), (20, 21), (25, 37), (26, 27), (28, 29), (33, 39), (36, 41), (38, 49), (42, 43), (45, 46), (47, 57), (51, 52), (53, 54), (56, 63), (58, 71), (59, 75), (61, 62), (64, 65), (66, 67), (68, 69), (72, 73), (76, 81), (79, 93), (82, 83), (84, 85), (86, 87), (88, 89), (91, 98), (94, 95), (97, 107), (99, 115), (100, 101), (102, 103), (105, 117), (108, 109), (110, 111), (113, 114), (116, 121), (118, 129), (123, 136), (124, 125), (126, 127), (130, 131), (132, 133), (135, 139), (138, 151), (142, 143), (144, 145), (146, 147), (152, 153), (154, 155)]\n", - "\n", - "Layer 1:\n", - "[(0, 1), (3, 16), (5, 6), (7, 8), (11, 18), (13, 14), (17, 27), (21, 22), (23, 24), (25, 26), (29, 38), (30, 31), (32, 33), (34, 35), (39, 53), (41, 42), (43, 56), (44, 45), (47, 48), (49, 50), (51, 58), (54, 55), (57, 67), (60, 61), (62, 63), (65, 66), (69, 78), (70, 71), (73, 79), (74, 75), (77, 85), (80, 81), (83, 84), (87, 97), (89, 90), (91, 92), (93, 94), (96, 103), (101, 116), (104, 105), (106, 107), (109, 118), (111, 112), (113, 119), (114, 115), (117, 125), (121, 122), (123, 124), (127, 137), (128, 129), (131, 138), (133, 134), (136, 143), (139, 155), (140, 141), (145, 146), (147, 148), (149, 150), (151, 152)]\n", - "\n", - "Layer 2:\n", - "[(1, 2), (3, 4), (7, 17), (9, 10), (11, 12), (15, 19), (21, 36), (22, 23), (24, 25), (27, 28), (29, 30), (31, 32), (33, 34), (37, 45), (40, 41), (43, 44), (46, 47), (48, 49), (50, 51), (52, 53), (55, 59), (61, 76), (63, 64), (65, 77), (67, 68), (69, 70), (71, 72), (73, 74), (78, 89), (81, 82), (83, 96), (85, 86), (87, 88), (90, 91), (92, 93), (95, 99), (98, 111), (101, 102), (103, 104), (105, 106), (107, 108), (109, 110), (112, 113), (119, 133), (120, 121), (122, 123), (125, 126), (127, 128), (129, 130), (131, 132), (134, 135), (137, 147), (141, 142), (143, 144), (148, 149), (150, 151), (153, 154)]\n", - "\n" - ] - } - ], - "source": [ - "layer_couplings = construct_layer_couplings(backend)\n", - "for i, layer in enumerate(layer_couplings):\n", - " print(f\"Layer {i}:\\n{layer}\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "d320e933", - "metadata": {}, - "source": [ - "##### Remove bad qubits\n", - "Look at the coupling map for the backend and see if any qubits connect to couplings with high error. Remove these \"bad\" qubits from your experiment." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "fccef708", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Plot gate error map\n", - "# NOTE: These can change over time, so your results may look different\n", - "plot_error_map(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5973c90b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Physical qubits:\n", - " [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155]\n" - ] - } - ], - "source": [ - "bad_qubits = {\n", - " 32,\n", - " 33,\n", - " 71,\n", - " 72,\n", - " 73,\n", - " 102,\n", - " 103,\n", - "} # qubits removed based on high coupling error (1.00)\n", - "good_qubits = list(set(range(backend.num_qubits)).difference(bad_qubits))\n", - "print(\"Physical qubits:\\n\", good_qubits)" - ] - }, - { - "cell_type": "markdown", - "id": "180c4cb5", - "metadata": {}, - "source": [ - "##### Main Trotter circuit generation" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f814ca82", - "metadata": {}, - "outputs": [], - "source": [ - "num_steps = 6\n", - "theta = Parameter(\"theta\")\n", - "circuit = trotter_circuit(\n", - " theta, layer_couplings, num_steps, qubits=good_qubits, backend=backend\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "7b86b867", - "metadata": {}, - "source": [ - "#### Create a list of parameter values to be assigned later" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5da6e991", - "metadata": {}, - "outputs": [], - "source": [ - "num_params = 12\n", - "\n", - "# 12 parameter values for Rx between [0, pi/2].\n", - "# Reshape to outer product broadcast with observables\n", - "parameter_values = np.linspace(0, np.pi / 2, num_params).reshape(\n", - " (num_params, 1)\n", - ")\n", - "num_params = parameter_values.size" - ] - }, - { - "cell_type": "markdown", - "id": "ac6f36e3", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "#### ISA circuit\n", - "Before running the circuit on hardware, optimize it for hardware execution. This process involves a few steps:\n", - "\n", - "* Pick a qubit layout that maps the virtual qubits of your circuit to physical qubits on the hardware.\n", - "* Insert swap gates as needed to route interactions between qubits that are not connected.\n", - "* Translate the gates in our circuit to [Instruction Set Architecture (ISA)](/docs/guides/transpile#instruction-set-architecture) instructions that can directly be executed on the hardware.\n", - "* Perform circuit optimizations to minimize the circuit depth and gate count.\n", - "\n", - "Although the transpiler built into Qiskit can perform all of these steps, this tutorial demonstrates building the utility-scale Trotter circuit in a ground-up fashion. Select the good physical qubits and define entangling layers on connected qubit pairs from those selected qubits. Nonetheless, you still need to translate non-ISA gates in the circuit and avail any circuit optimization offered by the transpiler.\n", - "\n", - "Transpile your circuit for the chosen backend by creating a pass manager and then running the pass manager on the circuit. Also, fix the initial layout of the circuit to the already selected `good_qubits`. An easy way to create a pass manager is to use the [`generate_preset_pass_manager`](/docs/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) function. See [Transpile with pass managers](/docs/guides/transpile-with-pass-managers) for a more detailed explanation of transpiling with pass managers." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "1834cb22", - "metadata": {}, - "outputs": [], - "source": [ - "pm = generate_preset_pass_manager(\n", - " backend=backend,\n", - " initial_layout=good_qubits,\n", - " layout_method=\"trivial\",\n", - " optimization_level=1,\n", - ")\n", - "\n", - "isa_circuit = pm.run(circuit)" - ] - }, - { - "cell_type": "markdown", - "id": "d395c8cf", - "metadata": {}, - "source": [ - "#### ISA observables\n", - "Next, create all weight-1 $\\langle Z \\rangle$ observables for each virtual qubit by padding the necessary number of $\\langle I \\rangle$ terms." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "cc5ab1ed", - "metadata": {}, - "outputs": [], - "source": [ - "observables = []\n", - "num_qubits = len(good_qubits)\n", - "for q in range(num_qubits):\n", - " observables.append(\n", - " SparsePauliOp(\"I\" * (num_qubits - q - 1) + \"Z\" + \"I\" * q)\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "030db4ed", - "metadata": {}, - "source": [ - "The transpilation process has mapped the virtual qubits of your circuit to physical qubits on the hardware. The information about the qubit layout is stored in the `layout` attribute of the transpiled circuit. Your observable is also defined in terms of the virtual qubits, so you need to apply this layout to the observable. This is done using the `apply_layout` method of `SparsePauliOp`.\n", - "\n", - "Notice that each observable is wrapped in a list in the following code block. It is done to *broadcast* with parameter values so that each qubit observable is measured for each theta value. Find the broadcasting rules for primitives in the [primitives documentation](/docs/guides/primitives)." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "95fd2908", - "metadata": {}, - "outputs": [], - "source": [ - "isa_observables = [\n", - " [obs.apply_layout(layout=isa_circuit.layout)] for obs in observables\n", - "]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b4d480b3", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "b22a1b00", - "metadata": {}, - "outputs": [], - "source": [ - "pub = (isa_circuit, isa_observables, parameter_values)" - ] - }, - { - "cell_type": "markdown", - "id": "4ace7773", - "metadata": {}, - "source": [ - "#### Configure Estimator options\n", - "Next configure the `Estimator` options needed to run the mitigation experiment. This includes options for the noise learning of the entangling layers, and for ZNE extrapolation.\n", - "\n", - "We use the following configuration:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "ad4a4f1c", - "metadata": {}, - "outputs": [], - "source": [ - "# Experiment options\n", - "num_randomizations = 700\n", - "num_randomizations_learning = 40\n", - "max_batch_circuits = 3 * num_params\n", - "shots_per_randomization = 64\n", - "learning_pair_depths = [0, 1, 2, 4, 6, 12, 24]\n", - "noise_factors = [1, 1.3, 1.6]\n", - "extrapolated_noise_factors = np.linspace(0, max(noise_factors), 20)\n", - "\n", - "# Base option formatting\n", - "options = {\n", - " # Builtin resilience settings for ZNE\n", - " \"resilience\": {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " # TREX noise learning configuration\n", - " \"measure_noise_learning\": {\n", - " \"num_randomizations\": num_randomizations_learning,\n", - " \"shots_per_randomization\": 1024,\n", - " },\n", - " # PEA noise model configuration\n", - " \"layer_noise_learning\": {\n", - " \"max_layers_to_learn\": 3,\n", - " \"layer_pair_depths\": learning_pair_depths,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"num_randomizations\": num_randomizations_learning,\n", - " },\n", - " \"zne\": {\n", - " \"amplifier\": \"pea\",\n", - " \"noise_factors\": noise_factors,\n", - " \"extrapolator\": (\"exponential\", \"linear\"),\n", - " \"extrapolated_noise_factors\": extrapolated_noise_factors.tolist(),\n", - " },\n", - " },\n", - " # Randomization configuration\n", - " \"twirling\": {\n", - " \"num_randomizations\": num_randomizations,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"strategy\": \"active-circuit\",\n", - " },\n", - " # Optional Dynamical Decoupling (DD)\n", - " \"dynamical_decoupling\": {\"enable\": True, \"sequence_type\": \"XY4\"},\n", - " # Job tag\n", - " \"environment\": {\"job_tags\": [\"TUT_PEA\"]},\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "3f9fd4c4", - "metadata": {}, - "source": [ - "##### Explanation of ZNE options\n", - "The following gives details on the additional options in the experimental branch. Note that these options and names are not finalized, and everything here is subject to change before an official release.\n", - "\n", - "* **amplifier**: The method to use when amplifying noise to the intended noise factors.\n", - " Allowed values are `\"gate_folding\"`, which amplifies by repeating two-qubit basis gates,\n", - " and `\"pea\"`, which amplifies by probabilistic sampling after learning the Pauli-twirled\n", - " noise model for layers of twirled two-qubit basis gates. Additional options are `\"gate_folding_front\"` and `\"gate_folding_back\"`, which are explained in the [API documentation](/docs/api/qiskit-ibm-runtime/options-zne-options#amplifier).\n", - "* **extrapolated\\_noise\\_factors**: Specify one or more noise factor values at which to evaluate the\n", - " extrapolated models. If a sequence of values, the returned results will be array-valued with specified noise factor evaluated for the extrapolation model. A value\n", - " of 0 corresponds to zero-noise extrapolation.\n", - "\n", - "#### Run the experiment" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "3cf72c8c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID d7fa8oe2cugc739qbb10\n" - ] - } - ], - "source": [ - "estimator = Estimator(mode=backend, options=options)\n", - "job = estimator.run([pub])\n", - "print(f\"Job ID {job.job_id()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "1eea9c17", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job.status()" - ] - }, - { - "cell_type": "markdown", - "id": "50b94af2", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "\n", - "Once the experiment is finished, you can view your results. You fetch the raw and mitigated expectation values and compare them with exact results. Then, plot the expectation values, both mitigated (extrapolated) and raw, averaged over all qubits for each parameter. Finally, plot expectation values for your choice of individual qubits." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "31dc35ea-6554-4ca7-9c3b-0b5394c46e4e", - "metadata": {}, - "outputs": [], - "source": [ - "primitive_result = job.result()" - ] - }, - { - "cell_type": "markdown", - "id": "fbf7ec8d", - "metadata": {}, - "source": [ - "#### General result shapes and metadata\n", - "The `PrimitiveResult` object contains a list-like structure named `PubResult`. As we submit only one PUB to the estimator, the `PrimitiveResult` contains a single `PubResult` object.\n", - "\n", - "The PUB (primitive unified bloc) result expectation values and standard errors are array-valued. For estimator jobs with ZNE, there are several data fields of expectation values and standard errors available in the `PubResult`'s `DataBin` container. We will briefly discuss the data fields for expectation values here (similar data fields are available for standard errors (`stds`) as well).\n", - "\n", - "1. `pub_result.data.evs`: Expectation values corresponding to the zero noise (based on heuristically best extrapolation).\n", - " * The first axis is the virtual qubit index for observable $\\langle Z_i\\rangle$ ($124$ virtual-qubits/observables)\n", - " * The second axis indexes the parameter value for $\\theta$ ($12$ parameter values)\n", - "2. `pub_result.data.evs_extrapolated`: Expectation values for extrapolated noise factors for every extrapolator. This array has two additional axes.\n", - " * The third axis indexes the extrapolation methods ($2$ extrapolators, `exponential` and `linear`)\n", - " * The last axis indexes the `extrapolated_noise_factors` ($20$ extrapolation points specified in the option)\n", - "3. `pub_result.data.evs_noise_factors`: Raw expectation values for each noise factor.\n", - " * The third axis indexes the raw `noise_factors` ($3$ factors)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "e3aa4fc9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pub_result.data.evs.shape=(149, 12)\n", - "pub_result.data.evs_extrapolated.shape=(149, 12, 2, 20)\n", - "pub_result.data.evs_noise_factors.shape=(149, 12, 3)\n", - "\n" - ] - } - ], - "source": [ - "pub_result = primitive_result[0]\n", - "\n", - "print(\n", - " f\"{pub_result.data.evs.shape=}\\n\"\n", - " f\"{pub_result.data.evs_extrapolated.shape=}\\n\"\n", - " f\"{pub_result.data.evs_noise_factors.shape=}\\n\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4c5cc6ee", - "metadata": {}, - "source": [ - "Several metadata fields are also available in the `PrimitiveResult`. The metadata includes\n", - "\n", - "* `resilience/zne/noise_factors`: The raw noise factors\n", - "* `resilience/zne/extrapolator`: The extrapolators used for each result" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "1c77d83a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'dynamical_decoupling': {'enable': True,\n", - " 'sequence_type': 'XY4',\n", - " 'extra_slack_distribution': 'middle',\n", - " 'scheduling_method': 'alap'},\n", - " 'twirling': {'enable_gates': True,\n", - " 'enable_measure': True,\n", - " 'num_randomizations': 700,\n", - " 'shots_per_randomization': 64,\n", - " 'interleave_randomizations': True,\n", - " 'strategy': 'active-circuit'},\n", - " 'resilience': {'measure_mitigation': True,\n", - " 'zne_mitigation': True,\n", - " 'pec_mitigation': False,\n", - " 'zne': {'noise_factors': [1.0, 1.3, 1.6],\n", - " 'extrapolator': ['exponential', 'linear'],\n", - " 'extrapolated_noise_factors': [0.0,\n", - " 0.08421052631578947,\n", - " 0.16842105263157894,\n", - " 0.25263157894736843,\n", - " 0.3368421052631579,\n", - " 0.42105263157894735,\n", - " 0.5052631578947369,\n", - " 0.5894736842105263,\n", - " 0.6736842105263158,\n", - " 0.7578947368421053,\n", - " 0.8421052631578947,\n", - " 0.9263157894736842,\n", - " 1.0105263157894737,\n", - " 1.0947368421052632,\n", - " 1.1789473684210525,\n", - " 1.263157894736842,\n", - " 1.3473684210526315,\n", - " 1.431578947368421,\n", - " 1.5157894736842106,\n", - " 1.6]},\n", - " 'layer_noise_model': [LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00155, 0.00144, 0.00637, 0.00023, 0.0, 0.0, 0.00018, 0.00035, 0.0, 0.00014, 5e-05, 0.00041, 0.0, 0.0, 0.0, 0.0001, 0.0001, 0.0, 9e-05, 6e-05, 0.0, 7e-05, 0.0001, 0.00013, 0.00018, 1e-05, 5e-05, 7e-05, 6e-05, 6e-05, 0.00029, 0.00016, 6e-05, 6e-05, 0.00046, 0.00073, 0.00031, 0.00025, 0.00018, 0.00022, 0.0, 8e-05, 0.00012, 0.00015, 0.00012, 0.0, 0.0, 0.00023, 5e-05, 5e-05, 7e-05, 0.00064, 4e-05, 2e-05, 0.00072, 0.00037, 2e-05, 4e-05, 0.00077, 0.0003, 0.00042, 0.00027, 0.00016, 0.0, 8e-05, 5e-05, 0.00019, 0.0, 0.0, 0.00021, 0.00014, 0.00061, 0.0, 0.00016, 3e-05, 0.00053, 0.00013, 0.0, 0.00068, 0.00011, 0.0, 0.00013, 0.00078, 0.01885, 0.00032, 0.00034, 0.00035, 0.00052, 3e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00123, 0.0, 0.0, 0.0, 0.00034, 0.00011, 0.0001, 0.00076, 0.00041, 0.0001, 0.00011, 0.00082, 0.0, 0.00066, 0.0, 0.00055, 7e-05, 0.00018, 0.00011, 0.00024, 3e-05, 0.00015, 0.00014, 0.0, 0.00076, 9e-05, 0.00016, 8e-05, 0.00132, 0.0, 0.00019, 0.00215, 0.00109, 0.00019, 0.0, 0.00201, 0.00021, 0.0006, 0.00032, 0.00046, 0.00027, 0.0, 8e-05, 0.0001, 0.00027, 0.0, 0.00015, 0.00018, 0.0, 0.00026, 0.00024, 5e-05, 0.00031, 0.0, 0.00034, 0.00039, 9e-05, 0.00034, 0.0, 0.00078, 0.00794, 0.00045, 0.00061, 0.00066, 0.0, 0.0, 0.00032, 6e-05, 5e-05, 7e-05, 0.0, 0.0001, 0.00036, 0.0, 0.00037, 0.00013, 0.00016, 3e-05, 8e-05, 0.00067, 0.00024, 8e-05, 3e-05, 0.00074, 0.00224, 0.00029, 0.00026, 0.00031, 0.00076, 5e-05, 0.0, 2e-05, 0.00072, 0.0, 1e-05, 0.00011, 0.00027, 0.0, 0.00017, 0.0, 0.0, 0.00012, 0.0, 0.0, 0.0, 0.0, 0.00067, 0.00063, 0.0, 0.0, 0.0, 0.00102, 0.0, 0.00011, 0.00026, 4e-05, 1e-05, 0.0002, 0.0, 0.00011, 0.0, 0.00021, 0.00015, 0.0005, 0.00011, 0.00013, 0.0, 0.0002, 0.00016, 0.00015, 8e-05, 2e-05, 7e-05, 0.00023, 0.00042, 0.0, 0.00049, 0.00056, 0.00372, 0.00017, 0.00012, 0.0, 0.00026, 0.00021, 0.0, 0.00012, 0.00046, 0.00305, 0.0005, 0.00057, 9e-05, 0.0009, 0.0, 7e-05, 0.00011, 0.00084, 0.0, 0.0, 0.0001, 0.00067, 0.0, 0.0, 0.0, 4e-05, 0.0, 1e-05, 0.00053, 0.0, 9e-05, 0.00021, 0.0, 1e-05, 0.0, 8e-05, 0.0, 0.0, 0.0, 9e-05, 0.00083, 0.00084, 0.00038, 9e-05, 3e-05, 0.00039, 0.02059, 0.0, 0.0, 0.0, 0.01787, 0.00012, 0.00024, 0.0, 0.00401, 0.0, 0.0, 0.0, 4e-05, 0.0, 0.0, 0.00018, 0.0, 0.00031, 0.00018, 0.0, 0.0, 0.0, 0.0, 0.00013, 0.0, 0.00027, 1e-05, 0.0, 0.00021, 0.0, 0.0, 0.00029, 0.00159, 0.0, 0.0, 0.00052, 0.0079, 0.0, 0.0002, 0.00147, 0.00048, 4e-05, 0.00976, 0.00957, 0.0011, 0.0, 0.0, 4e-05, 0.00048, 0.01068, 0.00487, 0.00225, 0.0, 0.0, 0.00026, 0.00052, 0.00033, 0.0, 0.00019, 0.0, 0.0, 0.00038, 0.0, 0.0, 0.0, 0.00154, 0.0, 0.0, 0.0, 0.00046, 0.0, 9e-05, 0.00077, 0.0002, 9e-05, 0.0, 0.00077, 0.00061, 6e-05, 0.00045, 0.00081, 0.00016, 0.0, 0.0, 0.0001, 0.00064, 4e-05, 0.0002, 0.0, 0.00056, 7e-05, 0.0, 0.0, 0.00066, 5e-05, 0.00025, 0.00077, 0.00011, 0.0, 0.0, 0.00065, 0.00025, 5e-05, 0.00082, 6e-05, 0.0, 0.00011, 0.00354, 0.00027, 0.00039, 0.00046, 0.00014, 0.0, 0.00013, 0.00067, 0.00064, 0.0006, 0.00053, 2e-05, 0.00016, 0.00067, 0.0, 0.00013, 0.0, 0.00047, 0.00016, 2e-05, 0.00067, 4e-05, 0.0, 0.00015, 0.00028, 0.00044, 0.00041, 0.00014, 0.00011, 0.0, 0.0, 5e-05, 0.0, 0.00017, 0.00022, 9e-05, 6e-05, 0.0, 0.00021, 0.0007, 3e-05, 0.0, 0.0, 0.0002, 0.00012, 3e-05, 0.0002, 0.0001, 3e-05, 0.00012, 0.00026, 0.00033, 0.00053, 0.00037, 0.00039, 9e-05, 6e-05, 7e-05, 0.00012, 0.00012, 0.0, 0.00022, 0.0, 0.00034, 0.00014, 8e-05, 0.0001, 0.00179, 0.00186, 0.00096, 0.00028, 0.00051, 0.00033, 0.0, 0.0, 0.00015, 0.0004, 0.0, 8e-05, 0.00015, 2e-05, 0.00015, 0.0, 0.00045, 0.0002, 0.0, 0.0, 0.00063, 0.00044, 0.00036, 0.00064, 0.0003, 2e-05, 0.0, 0.00124, 0.0, 0.0, 0.0, 0.00169, 0.00032, 0.00018, 0.0, 0.00147, 0.0, 0.0, 0.00037, 0.00095, 0.0, 0.00051, 0.00182, 0.00088, 0.00051, 0.0, 0.00116, 0.00093, 0.00124, 0.00219, 0.00052, 0.00072, 4e-05, 0.0, 0.0, 4e-05, 0.0, 0.00025, 0.00013, 0.0001, 0.00031, 0.0, 0.00027, 0.00022, 0.0, 0.00016, 0.0, 1e-05, 0.0001, 0.0, 3e-05, 0.0, 0.0, 2e-05, 6e-05, 0.0, 0.00021, 0.00251, 0.0, 0.0, 7e-05, 0.0, 0.0, 0.0, 0.00047, 5e-05, 2e-05, 0.00062, 0.00038, 2e-05, 5e-05, 0.00055, 0.00125, 0.00049, 0.00033, 0.00031, 0.00015, 0.0, 0.00015, 7e-05, 0.00047, 0.0, 1e-05, 3e-05, 1e-05, 0.00014, 0.0, 0.00026, 0.00092, 0.0, 0.0, 0.0, 0.00048, 0.00011, 4e-05, 0.0, 0.00077, 0.00013, 0.00014, 0.00031, 0.00048, 0.0, 0.0001, 0.00066, 6e-05, 2e-05, 0.0, 0.00029, 0.0001, 0.0, 0.00065, 0.0, 0.00013, 3e-05, 0.0, 0.00033, 0.00034, 0.00019, 2e-05, 0.0, 0.00015, 0.00046, 0.0, 2e-05, 1e-05, 0.00046, 8e-05, 6e-05, 0.0, 0.00035, 1e-05, 0.0001, 0.0, 1e-05, 0.0, 0.00012, 8e-05, 7e-05, 5e-05, 0.0, 0.00013, 0.0, 0.0, 0.0, 0.0, 0.00022, 0.0, 0.00013, 0.00028, 0.00014, 0.00013, 0.0, 0.00042, 0.00055, 0.00054, 0.00036, 5e-05, 0.0002, 0.0, 0.0, 0.00014, 1e-05, 0.00019, 2e-05, 6e-05, 0.00026, 0.0001, 0.0, 5e-05, 8e-05, 0.0, 0.00073, 7e-05, 0.0, 0.0, 1e-05, 0.0, 0.0, 6e-05, 4e-05, 0.00018, 0.00046, 0.00016, 0.00018, 4e-05, 0.00053, 0.0002, 0.00057, 0.00055, 0.00042, 0.00077, 6e-05, 0.00025, 5e-05, 0.00062, 0.00026, 0.00012, 4e-05, 0.00033, 8e-05, 0.0, 0.0004, 0.00036, 0.00016, 0.0, 0.0, 4e-05, 0.0, 4e-05, 0.0002, 4e-05, 0.00036, 0.0, 4e-05, 0.00024, 0.0, 0.0002, 0.00044, 0.00017, 0.0002, 0.0, 0.00051, 0.00059, 0.00061, 0.00069, 0.00064, 0.0006, 0.0, 7e-05, 4e-05, 0.00085, 0.0, 4e-05, 0.0, 0.00031, 0.00033, 0.0, 0.0001, 0.00037, 3e-05, 0.0, 0.0, 0.00018, 0.0, 0.00015, 4e-05, 0.00044, 9e-05, 2e-05, 2e-05, 0.00067, 0.00048, 6e-05, 0.0, 0.0, 0.0, 0.00028, 0.0, 1e-05, 0.0, 0.0, 0.00112, 0.0, 0.0, 0.00018, 0.00016, 0.0, 0.00018, 0.00055, 9e-05, 0.00018, 0.0, 0.00028, 0.00254, 0.00064, 0.00025, 0.00045, 0.00072, 7e-05, 6e-05, 0.00114, 0.00026, 0.00013, 0.0, 0.00081, 6e-05, 7e-05, 0.00139, 0.00014, 0.0, 0.00026, 0.00097, 0.00053, 0.00029, 0.00044, 0.0, 6e-05, 0.0, 0.00011, 3e-05, 0.0, 0.0002, 0.00024, 0.0, 5e-05, 5e-05, 5e-05, 0.0, 0.00014, 0.00025, 0.00032, 0.00011, 5e-05, 0.00067, 4e-05, 5e-05, 0.00011, 0.00061, 0.00015, 0.00035, 0.00035, 0.0003, 0.0006, 0.0, 0.00017, 0.0001, 0.0003, 0.00012, 8e-05, 0.00015, 7e-05, 0.0001, 5e-05, 0.00057, 0.0003, 9e-05, 0.00023, 0.0, 0.0001, 0.00015, 0.00073, 0.0, 0.0, 0.00012, 0.00041, 0.00015, 0.0001, 0.00079, 0.0003, 0.00011, 0.0, 0.00042, 0.00088, 0.00066, 0.00062, 0.00051, 0.0, 0.0, 0.00013, 0.00028, 8e-05, 0.00022, 0.0, 0.00044, 0.0, 0.00013, 0.0, 0.0, 0.0002, 0.00014, 0.00062, 0.00022, 0.00014, 0.0002, 0.0005, 4e-05, 0.00064, 0.00058, 0.00046, 0.00055, 0.0, 8e-05, 0.00012, 0.00067, 0.0, 0.0, 0.00014, 0.00095, 0.00025, 0.0, 0.00016, 0.00058, 0.00041, 0.00052, 0.00022, 6e-05, 0.0, 0.00034, 0.00011, 0.0, 0.0, 0.00015, 0.0, 6e-05, 0.00034, 0.0, 0.00016, 4e-05, 0.00126, 0.00041, 0.00037, 0.00015, 0.0, 0.0, 0.0, 0.00011, 0.0, 0.00024, 5e-05, 0.00029, 1e-05, 2e-05, 0.0, 0.00033, 0.00036, 4e-05, 0.00024, 0.001, 0.0, 0.0, 0.0, 0.00046, 0.0, 0.00028, 2e-05, 0.0009, 0.00012, 0.0, 0.00032, 0.00428, 0.00026, 9e-05, 0.0, 0.00372, 0.0, 9e-05, 0.0, 0.00107, 0.00018, 0.0, 0.00047, 0.00025, 0.00031, 0.00024, 0.00068, 0.00063, 0.00052, 4e-05, 0.00011, 0.00011, 0.00044, 7e-05, 4e-05, 4e-05, 5e-05, 0.00011, 0.00011, 0.00034, 0.0, 0.00017, 0.0, 0.00051, 0.00041, 0.00032, 0.00022, 0.0, 0.0, 9e-05, 6e-05, 7e-05, 0.00011, 2e-05, 0.00052, 0.0, 0.0, 0.0, 0.00731, 0.00017, 0.0, 0.0, 0.00026, 0.0, 0.00031, 0.0005, 0.0, 0.00031, 0.0, 0.00063, 0.0, 0.00026, 0.00052, 0.0, 0.0, 4e-05, 0.0, 0.00024, 7e-05, 9e-05, 6e-05, 3e-05, 0.0, 0.0, 0.00025, 0.00029, 0.00025, 0.00012, 4e-05, 5e-05, 0.00014, 4e-05, 0.00091, 9e-05, 0.0, 7e-05, 0.00019, 4e-05, 0.00014, 0.00085, 0.00037, 6e-05, 4e-05, 0.0001, 0.00025, 0.00026, 0.00013, 0.00026, 0.00014, 0.0, 2e-05, 0.00023, 0.0, 0.00021, 0.0, 0.0, 0.00031, 0.00031, 0.0001, 0.00013, 6e-05, 0.00013, 0.00071, 0.00048, 0.00013, 6e-05, 0.00076, 0.00018, 0.00042, 0.00044, 0.00018, 0.00014, 0.0, 0.00013, 9e-05, 0.0003, 0.0, 0.0, 1e-05, 0.0, 0.00019, 0.0, 7e-05, 1e-05, 9e-05, 0.0, 0.00011, 0.0, 7e-05, 0.00041, 0.0, 0.0, 0.00032, 0.0, 7e-05, 0.0, 0.00034, 0.0014, 0.0, 0.0002, 6e-05, 0.00036, 0.00031, 0.00039, 0.00042, 7e-05, 0.0, 0.0, 0.00014, 0.00011, 0.0, 2e-05, 0.00024, 0.0, 9e-05, 0.00036, 0.00023, 0.00012, 0.00011, 0.0, 0.00052, 5e-05, 0.0, 4e-05, 0.00033, 1e-05, 0.0, 9e-05, 0.00064, 0.0, 7e-05, 0.0, 0.00044, 0.00016, 0.0, 0.0, 0.00029, 0.0, 0.0, 0.00012, 0.00021, 0.0, 0.00017, 0.00068, 7e-05, 0.0, 0.00014, 0.00027, 0.00017, 0.0, 0.0006, 9e-05, 1e-05, 0.0, 0.00064, 0.00025, 0.00031, 0.00019, 0.0, 0.0, 0.00013, 0.00056, 0.0, 0.00017, 0.0, 0.00053, 7e-05, 0.0, 6e-05, 0.00029, 0.00018, 6e-05, 3e-05, 0.00027, 0.0, 6e-05, 0.00058, 0.00044, 6e-05, 0.0, 0.00052, 0.0004, 0.00073, 0.00066, 3e-05, 0.0004, 9e-05, 0.0, 0.00021, 0.00048, 0.0, 0.00016, 0.0, 0.00257, 0.0, 0.00021, 0.00024, 0.00012, 0.0, 0.00015, 8e-05, 0.00025, 0.00012, 0.0, 0.0, 0.00025, 0.00028, 0.0, 0.00014, 0.0, 7e-05, 0.00017, 0.00029, 0.0, 0.00017, 7e-05, 0.00024, 0.0, 0.00061, 0.00068, 0.0, 0.00018, 0.0, 7e-05, 1e-05, 0.0, 0.00017, 0.0, 0.0, 0.0003, 0.00013, 1e-05, 0.00024, 0.00098, 0.00071, 0.00142, 9e-05, 0.00011, 0.0, 0.00056, 0.00042, 0.0, 0.00011, 0.00064, 0.00085, 0.00098, 0.00071, 0.00018, 0.00085, 0.00081, 0.00016, 0.0, 0.0, 0.0, 7e-05, 0.0, 0.0, 0.0, 0.0, 0.00036, 0.00012, 0.0, 0.0, 0.00048, 0.00021, 0.00031, 6e-05, 0.00059, 0.00041, 0.00028, 7e-05, 0.00026, 0.0004, 0.00036, 0.00016, 0.00014, 9e-05, 6e-05, 0.00043, 0.0, 8e-05, 7e-05, 0.00036, 6e-05, 9e-05, 0.00055, 6e-05, 0.0, 3e-05, 0.00032, 0.00036, 0.00036, 0.00017, 0.0, 0.0, 1e-05, 0.00038, 0.0, 8e-05, 5e-05, 0.00026, 0.00014, 3e-05, 5e-05, 0.0, 0.0, 0.0, 0.00017, 0.00027, 0.0, 0.00019, 0.00063, 4e-05, 0.00019, 0.0, 0.00077, 0.00116, 0.00051, 0.00048, 0.00036, 8e-05, 0.0, 0.00011, 0.0001, 0.00013, 7e-05, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00026, 0.00014, 0.0003, 0.00011, 5e-05, 6e-05, 0.00017, 0.0007, 0.0, 0.0, 0.00011, 0.00063, 0.00017, 6e-05, 0.00079, 0.0, 0.0, 5e-05, 9e-05, 0.00029, 0.00021, 0.00048, 0.00072, 0.0, 0.0, 0.0, 0.00034, 9e-05, 4e-05, 0.0, 0.00013, 0.0, 5e-05, 0.00037, 0.0, 0.00011, 0.0, 0.00034, 0.0, 0.0, 7e-05, 0.0, 0.00605, 0.0, 0.00011, 0.00012, 0.00012, 0.00023, 0.0, 0.00026, 0.00016, 0.0, 0.00023, 0.00031, 0.00078, 0.0006, 0.00026, 0.00055, 0.00043, 0.00012, 0.0001, 0.00052, 8e-05, 0.0, 0.0, 0.00033, 0.0001, 0.00012, 0.00051, 5e-05, 0.00012, 0.0, 0.00105, 0.00028, 0.00018, 0.00023, 0.0, 2e-05, 0.0, 0.0, 0.00019, 0.0, 0.00015, 0.00013, 0.00018, 2e-05, 0.0, 7e-05, 0.0001, 0.0002, 0.00014, 0.00029, 0.0, 8e-05, 0.0005, 0.0002, 8e-05, 0.0, 0.00046, 0.0017, 0.00108, 0.00089, 0.00035, 0.0, 0.00016, 1e-05, 9e-05, 0.00024, 0.0, 1e-05, 8e-05, 0.00024, 0.00013, 0.00032, 8e-05, 0.00127, 4e-05, 0.0, 0.0, 0.00095, 0.0, 0.00017, 0.0, 0.00052, 0.00017, 2e-05, 0.00029, 0.00036, 0.00049, 0.00056, 2e-05, 0.00026, 3e-05, 0.00048, 0.0, 3e-05, 0.00014, 0.00024, 3e-05, 0.00026, 0.0006, 2e-05, 0.00015, 5e-05, 0.0, 0.00025, 0.00038, 0.00034, 4e-05, 0.0, 0.00029, 0.00044, 0.00024, 0.0, 0.0, 0.00046, 5e-05, 0.0001, 0.0, 0.00048, 0.0, 4e-05, 0.00028, 0.0, 0.00026, 0.0, 3e-05, 1e-05, 0.0, 0.0, 0.00027, 0.00034, 0.0, 0.00016, 9e-05, 0.00013, 0.00019, 0.0, 0.0, 0.00014, 0.0, 0.0001, 3e-05, 0.00031, 5e-05, 0.00026, 0.00022, 0.0001, 0.00022, 0.0, 5e-05, 0.00012, 0.0, 0.00056, 0.0, 0.0, 0.00023, 0.0, 0.0, 0.00012, 0.00064, 0.00059, 0.0, 2e-05, 0.0, 0.00033, 0.00028, 0.00017, 0.00025, 3e-05, 1e-05, 6e-05, 0.00011, 0.0, 8e-05, 6e-05, 3e-05, 0.00016, 0.00034, 0.0, 0.00011, 0.00015, 0.0, 0.00044, 0.00028, 0.0, 0.00015, 0.00062, 0.00203, 0.00035, 0.00025, 0.00049, 0.00037, 0.0001, 2e-05, 0.0, 0.0003, 7e-05, 8e-05, 0.0, 0.00074, 9e-05, 0.0, 9e-05, 0.00016, 3e-05, 0.00013, 0.00079, 6e-05, 6e-05, 1e-05, 0.0, 0.00013, 3e-05, 0.00076, 0.0, 0.00017, 5e-05, 0.00031, 0.00025, 0.00035, 0.00023, 0.0, 2e-05, 0.0002, 0.00015, 9e-05, 1e-05, 0.00017, 0.0001, 0.00011, 6e-05, 1e-05, 0.00041, 0.0003, 0.00048, 0.0, 0.00017, 4e-05, 0.00025, 0.00063, 0.00018, 0.00025, 4e-05, 0.00065, 0.0019, 0.00043, 0.00028, 0.00033, 0.0, 1e-05, 0.00012, 0.0001, 0.00019, 3e-05, 0.0, 5e-05, 0.00038, 0.00012, 0.0, 0.0, 0.00025, 6e-05, 9e-05, 0.0, 0.00017, 1e-05, 0.0006, 0.00019, 0.0001, 0.00013, 0.0, 1e-05, 0.00017, 0.00068, 0.0, 3e-05, 0.0, 0.00021, 0.00019, 0.00029, 0.00041, 0.00073, 0.00011, 0.0, 0.0, 0.00064, 0.0, 0.00026, 5e-05, 0.00044, 0.0001, 0.0, 0.0002, 0.00037, 6e-05, 0.0, 8e-05, 0.00026, 0.0, 0.00019, 8e-05, 0.00017, 0.0, 0.0, 0.00021, 0.00023, 0.00016, 1e-05, 0.00037, 0.00041, 1e-05, 0.00016, 0.00044, 0.00046, 0.00054, 0.00065, 0.00033, 0.00033, 8e-05, 0.0, 8e-05, 0.00046, 0.0, 0.0001, 0.0, 0.00023, 0.0, 0.00015, 3e-05, 2e-05, 2e-05, 0.00031, 0.00012, 0.00028, 1e-05, 4e-05, 4e-05, 0.00038, 0.00027, 0.0, 0.0, 0.00073, 0.0002, 7e-05, 0.00076, 0.00063, 7e-05, 0.0002, 0.00086, 4e-05, 0.00052, 0.00053, 0.00012, 0.00068, 0.00068, 0.00019, 0.00063, 0.0, 1e-05, 5e-05, 0.00058, 0.0, 0.0, 0.0001, 0.00059, 0.00011, 0.0, 0.0, 0.00024, 0.00012, 0.0, 0.0, 0.00036, 0.0, 2e-05, 1e-05, 0.00021, 0.0, 0.00012, 0.0, 0.00031, 9e-05, 0.0, 0.0, 0.0, 8e-05, 0.00054, 6e-05, 0.0, 0.0, 0.00026, 8e-05, 0.0, 0.00056, 0.00078, 5e-05, 2e-05, 4e-05, 0.00036, 0.0004, 0.00015, 8e-05, 5e-05, 0.00012, 6e-05, 0.00017, 5e-05, 1e-05, 0.0, 0.0, 5e-05, 0.00011, 7e-05, 0.00033, 5e-05, 7e-05, 0.00042, 0.00042, 7e-05, 5e-05, 0.00042, 0.00015, 0.00031, 0.00023, 1e-05, 0.00012, 0.0, 0.0, 0.00013, 0.00022, 2e-05, 0.0, 0.0, 0.00062, 7e-05, 0.0, 0.0, 0.00024, 0.0001, 0.0, 0.0, 1e-05, 6e-05, 0.00046, 0.0, 0.0, 3e-05, 0.00018, 6e-05, 1e-05, 0.00042, 0.00019, 5e-05, 3e-05, 0.0, 0.00026, 0.00024, 0.00016, 0.00029, 5e-05, 0.0, 9e-05, 0.00082, 0.0, 8e-05, 5e-05, 0.00037, 5e-05, 0.00016, 0.0, 0.00147, 0.00017, 5e-05, 0.0, 0.00051, 0.0, 0.0, 4e-05, 0.00646, 0.00045, 0.0, 0.0, 0.00097, 0.0001, 0.00017, 0.00029, 0.00072, 0.00015, 0.00018, 6e-05, 0.0038, 0.00059, 0.00069, 0.00314, 0.00027, 1e-05, 6e-05, 0.0006, 2e-05, 0.0, 0.0, 0.0, 6e-05, 1e-05, 0.00043, 0.0, 0.00027, 8e-05, 0.00024, 0.00048, 0.00037, 0.00034, 0.0, 0.0, 0.00021, 0.00046, 0.0, 0.0, 0.0, 0.00019, 5e-05, 0.00012, 0.0, 0.00017, 0.00025, 0.0, 0.0002, 0.00013, 9e-05, 6e-05, 0.00046, 0.00043, 6e-05, 9e-05, 0.00048, 0.00046, 0.00046, 0.00036, 7e-05, 0.00028, 1e-05, 5e-05, 0.0, 0.00025, 0.0, 0.0, 0.0001, 6e-05, 0.00032, 0.0, 0.0, 0.00036, 4e-05, 7e-05, 7e-05, 1e-05, 0.00012, 0.00053, 0.00044, 0.0, 0.00015, 0.00022, 0.00012, 1e-05, 0.00081, 0.00177, 0.0, 0.0, 0.00021, 0.00035, 0.00034, 0.00039]))),\n", - " LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00087, 0.00084, 0.00784, 0.0, 0.0, 0.00028, 0.00012, 0.0001, 0.00028, 0.0, 0.00029, 0.0096, 0.00087, 0.00084, 0.0, 0.00054, 0.0, 0.0, 0.0, 0.0, 0.00021, 0.0, 5e-05, 0.00034, 0.0, 0.00019, 0.0, 0.0, 0.00016, 0.0, 9e-05, 0.0, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0, 6e-05, 0.00017, 0.00011, 0.0, 0.0, 0.00012, 0.0, 0.00014, 0.0, 0.00062, 0.00011, 6e-05, 3e-05, 0.00167, 0.00017, 0.0, 0.0, 0.00174, 0.0, 0.00014, 0.0, 0.00211, 0.0, 0.0, 0.0, 0.00028, 0.00024, 0.00016, 0.0003, 0.0, 0.00016, 0.00024, 0.0001, 3e-05, 0.00184, 0.00188, 0.00039, 0.0, 0.0, 0.0, 0.0004, 0.00065, 0.0, 0.00011, 0.0, 0.005, 0.0, 5e-05, 9e-05, 0.00029, 0.00024, 0.0, 0.00044, 0.00022, 0.0, 0.00024, 0.00043, 0.00068, 0.00102, 0.00088, 0.0005, 0.00055, 0.00015, 0.0, 0.00013, 0.00062, 0.0, 0.0, 7e-05, 0.00038, 0.0, 0.0002, 1e-05, 0.00025, 0.0, 6e-05, 5e-05, 0.00062, 0.0, 0.0, 0.0, 0.00034, 6e-05, 0.0, 3e-05, 0.0, 0.0, 0.00012, 0.00042, 0.00072, 0.00012, 0.0, 3e-05, 0.0005, 7e-05, 0.0, 0.00012, 0.00038, 0.0, 1e-05, 0.0003, 0.00053, 0.00016, 0.0, 0.0, 0.00027, 0.00034, 0.0, 0.0, 0.00011, 0.00012, 7e-05, 7e-05, 0.00021, 0.0, 0.00014, 1e-05, 0.00141, 4e-05, 0.0, 0.00035, 5e-05, 0.00012, 1e-05, 0.00026, 0.0001, 1e-05, 0.00012, 0.00026, 0.00011, 0.00037, 0.00035, 0.00045, 0.00036, 0.0, 5e-05, 5e-05, 0.0005, 4e-05, 7e-05, 5e-05, 0.00014, 0.00017, 4e-05, 0.0001, 0.00014, 0.00015, 1e-05, 0.00027, 0.00023, 1e-05, 0.00015, 0.00035, 0.00086, 0.0005, 0.00032, 0.00036, 0.00082, 0.0, 0.00011, 0.0, 0.0, 0.00064, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4e-05, 0.00015, 0.00036, 1e-05, 0.00015, 4e-05, 0.00034, 0.00067, 0.001, 0.00089, 0.0009, 0.00042, 0.0, 1e-05, 8e-05, 0.00042, 7e-05, 0.0, 0.0, 0.00025, 9e-05, 0.0, 0.0, 0.0005, 0.00106, 0.00168, 0.00024, 0.0, 0.0, 0.0, 0.0, 5e-05, 7e-05, 0.00015, 0.00053, 0.0001, 0.0, 0.00012, 0.00035, 0.0, 0.0, 0.00061, 0.00064, 0.0, 0.0, 0.00071, 0.00061, 0.00049, 0.00049, 0.00091, 0.0, 0.0, 0.00012, 0.0, 7e-05, 7e-05, 1e-05, 0.00053, 0.0, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.0057, 0.00013, 0.0, 0.0, 0.00019, 0.0, 0.0, 0.00818, 0.0, 4e-05, 0.00844, 0.00635, 4e-05, 0.0, 0.00647, 0.00203, 0.00024, 0.00068, 0.00159, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00015, 0.0, 0.0, 0.0, 0.00011, 0.00012, 0.0, 0.00051, 0.00033, 0.00025, 0.00051, 5e-05, 0.00025, 0.00033, 0.00038, 0.0001, 0.00032, 0.0004, 0.0, 0.00967, 0.00039, 3e-05, 0.00967, 0.0, 0.0, 0.0, 0.01187, 3e-05, 0.00039, 0.01275, 0.0, 0.0, 0.00042, 0.00994, 0.0012, 0.0002, 0.00248, 0.0, 0.00033, 0.0, 0.00086, 0.0, 0.0, 0.0, 0.00087, 0.0, 0.0, 0.0, 0.00093, 0.0, 0.00045, 0.0, 0.0, 2e-05, 0.00031, 0.00021, 0.0, 0.00021, 9e-05, 0.00014, 0.0, 6e-05, 8e-05, 0.00038, 0.00023, 0.0, 0.0, 0.0, 0.00019, 5e-05, 0.0, 0.0, 0.00021, 0.0, 0.00012, 0.00015, 0.00028, 0.00038, 0.0, 0.00017, 0.00024, 1e-05, 0.00083, 0.00072, 1e-05, 0.00024, 0.0, 1e-05, 0.00024, 0.00098, 0.00278, 0.0, 7e-05, 7e-05, 0.00023, 0.00025, 0.00042, 0.00039, 0.00028, 0.00038, 0.00015, 5e-05, 4e-05, 0.00012, 4e-05, 7e-05, 0.00036, 0.00025, 0.0, 3e-05, 9e-05, 7e-05, 4e-05, 0.00037, 0.00025, 0.00019, 2e-05, 0.0, 0.00039, 0.00028, 6e-05, 0.00035, 7e-05, 0.0, 0.00014, 0.00055, 0.00016, 7e-05, 0.0, 0.0, 0.0, 0.00018, 0.00045, 0.00027, 0.0, 7e-05, 0.0, 0.00014, 0.00018, 7e-05, 0.0, 0.00014, 0.0001, 8e-05, 0.0, 0.00016, 4e-05, 7e-05, 0.00042, 9e-05, 7e-05, 4e-05, 0.00021, 0.0, 0.00053, 0.00053, 5e-05, 0.00074, 0.00073, 0.00078, 0.00033, 0.00048, 0.0002, 0.0, 7e-05, 0.00013, 6e-05, 1e-05, 0.0, 0.00015, 0.00016, 7e-05, 3e-05, 2e-05, 4e-05, 5e-05, 0.0, 0.00071, 0.00014, 0.0, 0.00022, 0.00016, 0.0, 0.00024, 0.0002, 0.0001, 0.0, 0.00066, 0.00088, 0.0, 0.0001, 0.00096, 0.00215, 0.0004, 0.00036, 0.00041, 0.00125, 8e-05, 8e-05, 4e-05, 0.00165, 0.00038, 0.0, 0.0, 0.00243, 0.0, 0.0, 0.00011, 0.00023, 0.0, 0.00016, 0.00029, 0.00013, 0.00031, 0.0, 0.0, 0.00072, 0.00016, 0.0001, 0.0, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0002, 0.0004, 0.00013, 3e-05, 0.0, 0.00016, 0.0002, 0.0, 0.00059, 0.00123, 2e-05, 0.0, 0.0, 0.00068, 0.00044, 0.00014, 0.0007, 7e-05, 5e-05, 0.0, 0.00069, 0.00018, 0.0, 0.0, 0.0014, 0.0, 0.00021, 0.0, 0.0, 0.0001, 0.00016, 8e-05, 0.0, 0.0, 6e-05, 0.00023, 0.0, 0.0, 0.0, 2e-05, 0.00016, 0.0, 0.00011, 0.00033, 3e-05, 0.00011, 0.0, 0.00033, 0.00049, 0.00062, 0.00072, 0.00067, 0.00086, 1e-05, 6e-05, 0.0, 2e-05, 7e-05, 0.0, 0.00032, 0.0, 7e-05, 0.00043, 3e-05, 0.0, 0.00017, 0.0, 0.00026, 0.0, 0.0, 3e-05, 0.00014, 0.00029, 0.0, 0.00018, 0.00016, 0.00044, 0.00018, 0.00016, 0.00018, 0.00034, 0.0, 0.00101, 0.00102, 0.00052, 0.00022, 0.00011, 0.0, 9e-05, 0.00014, 0.0001, 0.0001, 0.00013, 0.00012, 0.00027, 2e-05, 0.00023, 0.0003, 0.0, 0.00016, 0.0, 0.00036, 0.00022, 0.0, 5e-05, 0.00059, 6e-05, 0.00015, 0.0, 0.0, 2e-05, 0.00016, 0.00108, 0.0, 0.0002, 0.00031, 0.0, 0.00016, 2e-05, 0.00047, 0.00015, 0.0, 0.0, 0.00809, 0.00074, 0.00073, 0.00068, 8e-05, 0.0, 0.0, 8e-05, 0.00022, 0.00019, 2e-05, 0.00012, 0.0001, 9e-05, 0.00023, 5e-05, 0.00028, 6e-05, 0.0, 0.0006, 6e-05, 0.00017, 0.00064, 0.00027, 0.00017, 6e-05, 0.00061, 0.00039, 0.00051, 0.00053, 0.00025, 0.0, 0.0, 0.00029, 0.00032, 0.00019, 0.00029, 0.0, 0.0004, 0.00019, 0.00192, 0.00229, 0.00056, 0.00034, 0.0, 2e-05, 8e-05, 0.00019, 0.00025, 0.00013, 0.00012, 0.00246, 4e-05, 0.0003, 0.00062, 0.00037, 0.0, 0.00012, 0.00037, 0.00032, 0.00012, 0.0, 0.00032, 0.00095, 0.00071, 0.00078, 0.00025, 0.00085, 4e-05, 0.0, 0.0, 0.00045, 0.0, 1e-05, 0.00013, 0.00012, 0.0, 0.00033, 6e-05, 0.00023, 0.0004, 0.00042, 2e-05, 0.0, 0.0, 0.0003, 0.0, 0.0, 0.0, 0.00022, 0.00055, 0.00023, 0.0004, 0.00044, 0.00011, 0.00017, 0.0, 0.0, 0.00028, 0.0, 0.0, 1e-05, 0.0057, 0.0, 0.00032, 0.0, 0.00088, 2e-05, 0.00021, 0.00022, 9e-05, 0.0, 0.00135, 0.00142, 4e-05, 0.0, 0.0, 0.0, 9e-05, 0.00161, 0.00155, 0.00026, 0.0, 9e-05, 0.00028, 0.00029, 0.00021, 0.00054, 0.0, 0.0, 0.00029, 0.00024, 3e-05, 1e-05, 0.0, 0.00018, 0.0, 0.00014, 0.00013, 0.00028, 0.0001, 0.0, 0.0, 0.0, 0.0, 0.00046, 1e-05, 0.00141, 0.0, 0.0, 0.00026, 0.00076, 0.00014, 0.0, 0.00096, 0.0, 0.0, 0.00014, 0.00052, 0.00061, 0.00068, 0.00077, 0.00079, 0.0, 0.00049, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00049, 0.0013, 0.0, 0.00073, 0.0, 0.02919, 0.00044, 0.00069, 0.00012, 0.0, 0.00014, 0.00025, 0.00141, 0.00072, 0.0, 0.0, 0.0008, 0.0, 0.00061, 0.00012, 0.0012, 1e-05, 0.0, 0.0, 0.0, 0.00011, 0.0, 0.00028, 0.0, 0.00043, 0.0, 0.0, 0.00108, 0.00033, 0.0, 0.00014, 0.0006, 0.0, 0.00011, 1e-05, 0.0007, 0.0, 0.0, 0.0, 0.00103, 0.00016, 0.0, 0.0, 0.00032, 0.00031, 0.00036, 0.00034, 5e-05, 0.0, 7e-05, 0.00014, 0.0, 0.00046, 0.00026, 2e-05, 6e-05, 1e-05, 0.0, 0.00014, 0.00035, 0.00093, 0.0, 2e-05, 0.0, 0.00032, 0.00031, 6e-05, 0.00042, 0.0, 0.0, 0.00029, 0.00011, 2e-05, 0.0, 0.00017, 0.00041, 9e-05, 5e-05, 0.0002, 2e-05, 0.00018, 0.0, 0.00025, 0.0, 0.0, 0.00035, 0.0001, 0.00087, 9e-05, 2e-05, 0.00026, 0.0016, 0.0, 0.0001, 0.00173, 0.0013, 0.0001, 0.0, 0.00142, 0.00111, 0.00057, 0.00044, 0.00047, 0.00051, 0.00041, 0.00034, 0.00034, 0.00038, 0.00035, 0.0, 0.0, 0.0, 0.00013, 0.00016, 0.00016, 0.00031, 0.0, 9e-05, 0.00016, 0.0, 0.00016, 0.00016, 0.00035, 0.0, 0.0, 9e-05, 1e-05, 0.00034, 0.00038, 0.00027, 0.0, 0.0, 0.0, 3e-05, 0.00098, 0.00031, 0.00011, 0.0, 0.00973, 0.0, 0.0, 0.00017, 0.0, 0.00024, 0.0, 0.00012, 0.00017, 0.00022, 0.0, 0.0, 0.00021, 5e-05, 4e-05, 4e-05, 0.00013, 7e-05, 0.00018, 0.00029, 0.00018, 0.00018, 7e-05, 0.00026, 0.00033, 0.00023, 0.00095, 0.00018, 0.0002, 9e-05, 2e-05, 0.00045, 1e-05, 0.0, 0.00011, 0.00012, 2e-05, 9e-05, 0.00042, 0.0, 8e-05, 4e-05, 0.00228, 0.00051, 0.00039, 0.00025, 0.00016, 0.0, 0.00015, 0.00021, 0.0001, 0.0, 0.0001, 0.00053, 0.0, 0.0001, 0.0, 0.0006, 0.0, 4e-05, 0.0, 9e-05, 0.0, 0.0001, 0.00011, 0.0, 0.00018, 0.0, 8e-05, 0.00063, 4e-05, 0.0, 0.0, 0.00032, 0.0, 0.00015, 0.0, 0.00043, 7e-05, 2e-05, 0.0, 3e-05, 0.00011, 0.0, 0.0001, 0.00026, 0.0001, 0.0, 3e-05, 0.0, 0.0, 5e-05, 0.00033, 3e-05, 0.00012, 0.0, 1e-05, 0.0, 0.0, 0.00064, 0.0, 0.0, 0.0, 0.0, 0.00012, 0.0001, 0.0001, 0.0, 5e-05, 0.00035, 0.00011, 5e-05, 0.0, 0.00032, 0.00017, 0.00044, 0.00048, 0.00017, 0.0001, 0.00018, 0.0, 0.00012, 0.00021, 0.0, 0.00015, 0.0001, 8e-05, 6e-05, 4e-05, 0.0, 0.00011, 0.00013, 2e-05, 0.00042, 4e-05, 2e-05, 0.00013, 0.00018, 0.00038, 0.00066, 0.00062, 0.00022, 0.00024, 0.0, 0.0, 0.0, 0.0, 0.00014, 0.00021, 0.0001, 0.00014, 0.00018, 0.0, 0.00018, 0.0, 0.0, 0.00155, 0.0, 0.0, 0.0001, 0.00013, 0.0, 0.00012, 0.00036, 0.00011, 0.00013, 0.0005, 0.00034, 0.00013, 0.00011, 0.00046, 0.00041, 0.00059, 0.00061, 0.00026, 0.00065, 1e-05, 1e-05, 8e-05, 0.00045, 0.0, 2e-05, 0.00013, 0.0004, 0.00013, 0.0001, 7e-05, 0.00027, 0.0, 1e-05, 5e-05, 0.00069, 0.0, 0.00015, 0.0, 0.00115, 0.0, 0.00033, 0.0, 0.00021, 0.0, 0.00013, 0.0003, 0.00019, 0.00013, 0.0, 0.0003, 9e-05, 0.00048, 0.00041, 5e-05, 0.00019, 0.0, 3e-05, 0.00012, 0.0004, 0.00014, 8e-05, 0.0, 0.00063, 0.00012, 4e-05, 0.00022, 0.00023, 0.0, 0.00013, 0.0, 0.00024, 4e-05, 0.0, 0.0, 0.00052, 6e-05, 0.0, 1e-05, 0.002, 0.00128, 0.00096, 0.0004, 0.0, 0.0, 5e-05, 0.00034, 0.0, 3e-05, 0.00013, 0.00066, 0.0, 4e-05, 0.0, 0.0005, 0.00037, 0.00029, 0.00018, 2e-05, 3e-05, 0.00055, 0.00034, 3e-05, 2e-05, 0.00068, 0.00077, 0.0005, 0.00037, 0.00018, 0.00033, 0.0, 0.0, 0.00013, 0.0003, 7e-05, 5e-05, 0.0, 0.00021, 9e-05, 8e-05, 0.0, 0.0002, 0.0, 0.00012, 2e-05, 0.0, 3e-05, 0.00038, 0.00021, 6e-05, 0.0, 2e-05, 3e-05, 0.0, 0.00042, 0.00076, 3e-05, 0.0, 5e-05, 0.00046, 0.00042, 0.0002, 0.00054, 0.0, 1e-05, 0.0, 0.00071, 4e-05, 5e-05, 0.0, 0.00032, 0.0, 7e-05, 2e-05, 0.00034, 4e-05, 0.0, 4e-05, 0.00019, 5e-05, 7e-05, 0.0, 0.00125, 3e-05, 0.0, 8e-05, 0.00026, 0.0, 0.00014, 0.0, 0.00048, 0.0, 0.0, 3e-05, 0.00026, 6e-05, 0.0, 0.00021, 5e-05, 0.00016, 0.0, 0.00024, 5e-05, 0.0, 6e-05, 0.00023, 1e-05, 7e-05, 0.0, 0.00011, 0.0, 0.0, 0.0004, 6e-05, 0.0, 0.00023, 8e-05, 0.0, 0.00021, 0.00011, 0.0, 0.00013, 0.00025, 0.00022, 0.00013, 0.0, 0.00029, 0.0007, 0.00056, 0.00042, 0.00045, 0.00021, 8e-05, 0.0, 0.0, 0.0001, 3e-05, 7e-05, 0.0001, 0.00176, 3e-05, 0.0, 0.0, 0.0, 0.0, 3e-05, 0.00029, 0.00023, 0.0001, 0.0, 0.0, 0.00036, 0.00018, 9e-05, 0.00011, 0.00038, 4e-05, 4e-05, 0.0, 8e-05, 9e-05, 0.00045, 0.00046, 0.00012, 2e-05, 0.0, 9e-05, 8e-05, 0.0006, 0.00023, 0.0, 0.0, 0.00018, 0.00029, 0.00034, 0.00038, 0.0, 6e-05, 4e-05, 0.00035, 4e-05, 4e-05, 6e-05, 0.00029, 0.0, 0.00045, 0.00051, 0.00014, 0.00017, 3e-05, 0.00011, 3e-05, 0.00033, 0.0, 0.0001, 2e-05, 0.00137, 0.00017, 0.0, 0.00037, 0.00031, 8e-05, 0.0, 0.00037, 0.0, 0.0, 8e-05, 0.0003, 0.0, 0.00048, 0.00045, 0.00034, 0.0003, 0.00013, 7e-05, 0.00052, 0.00049, 7e-05, 0.00013, 0.00054, 0.00061, 0.00058, 0.00042, 0.00012, 0.0005, 0.00029, 0.00037, 0.0, 0.00012, 0.00012, 0.00012, 0.0, 0.00021, 3e-05, 9e-05, 6e-05, 0.0001, 0.00014, 4e-05, 0.0, 0.00016, 0.00122, 0.00018, 3e-05, 0.00016, 4e-05, 5e-05, 0.00019, 5e-05, 7e-05, 0.00013, 0.00047, 0.00031, 0.00013, 7e-05, 0.00034, 0.00044, 0.0006, 0.0006, 0.00055, 0.00034, 8e-05, 2e-05, 5e-05, 6e-05, 0.00019, 0.0, 0.00027, 0.00031, 0.00015, 1e-05, 0.0003, 0.00016, 0.00014, 3e-05, 0.00037, 0.00035, 3e-05, 0.00014, 0.00041, 0.0, 0.00071, 0.00077, 0.00011, 0.00036, 5e-05, 9e-05, 0.00067, 0.00018, 0.0, 0.0, 0.00016, 9e-05, 5e-05, 0.00072, 0.0, 6e-05, 0.00023, 0.00597, 0.00035, 0.00044, 0.00102, 3e-05, 0.0, 0.00052, 0.00043, 4e-05, 7e-05, 0.0, 0.00044, 9e-05, 0.0, 0.0, 0.0, 0.0, 0.0002, 0.00035, 0.0, 0.00017, 5e-05, 0.0, 0.0, 0.0, 1e-05, 0.00025, 0.00048, 0.0, 5e-05, 0.00012, 0.00035, 0.0001, 0.0, 0.0, 4e-05, 0.00012, 9e-05, 5e-05, 6e-05, 3e-05, 0.00022, 0.00017, 0.00013, 0.0, 8e-05, 0.00013, 5e-05, 3e-05, 0.00051, 0.0002, 2e-05, 0.0002, 0.0002, 3e-05, 5e-05, 0.00064, 0.0, 1e-05, 9e-05, 0.00018, 0.00046, 0.00031, 0.00025, 0.00063, 0.0, 0.0, 0.0, 0.0006, 6e-05, 2e-05, 3e-05, 0.00051, 0.00011, 0.0, 0.00016, 0.0, 0.0, 0.0, 0.00031, 0.00028, 0.00011, 0.0, 0.0, 0.0006, 5e-05, 1e-05, 0.0, 0.00022, 0.0, 0.00013, 9e-05, 0.00063, 0.0, 0.0, 2e-05, 0.0, 0.00026, 0.0, 0.0, 0.00028, 0.0, 2e-05, 7e-05, 0.0, 0.0, 0.00017, 0.00022, 5e-05, 4e-05, 4e-05, 0.0, 0.0, 0.00015, 9e-05, 0.00017, 0.0, 0.00012, 0.0001, 1e-05, 0.00013, 0.00035, 0.0, 8e-05, 0.00045, 0.00014, 8e-05, 0.0, 0.0004, 1e-05, 0.00054, 0.00049, 0.00031, 0.00078, 0.0, 6e-05, 0.00015, 0.00054, 0.0, 0.0002, 0.00019, 0.0, 0.0001, 0.0, 0.00022, 0.00016, 6e-05, 0.0, 0.00018, 7e-05, 0.00013, 0.00012, 0.0, 0.0003, 3e-05, 0.00013, 0.00019, 0.00016, 9e-05, 0.0, 0.00037, 0.00018, 0.0, 9e-05, 0.00025, 0.00054, 0.00047, 0.00052, 0.00025, 0.00026, 0.0, 4e-05, 0.00055, 0.00017, 4e-05, 0.0, 0.00049, 0.0001, 0.00048, 0.00055, 3e-05, 0.00039, 3e-05, 0.00027, 0.0, 0.00041, 0.0, 0.00015, 0.0, 0.00042, 0.00018, 0.0, 0.00024, 0.00036, 0.00031, 0.00026, 0.00039, 5e-05, 0.0, 0.00053, 0.00038, 0.0, 5e-05, 0.0005, 0.00051, 0.00036, 0.00031, 4e-05, 0.00058, 0.0, 0.0, 1e-05, 0.00024, 0.0, 9e-05, 0.0, 0.00027, 0.00013, 3e-05, 4e-05, 0.00023, 0.00018, 0.0, 0.00044, 1e-05, 5e-05, 4e-05, 0.00026, 0.0, 0.00018, 0.0005, 0.0, 5e-05, 0.0, 0.00049, 0.0004, 0.00033, 0.00018, 2e-05, 1e-05, 0.0, 0.00051, 9e-05, 4e-05, 0.0, 0.00016, 2e-05, 6e-05, 6e-05, 0.00029, 0.0, 9e-05, 0.00011, 0.00027, 2e-05, 6e-05, 0.0, 0.00028, 4e-05, 0.0, 9e-05, 0.00013, 0.0, 0.0, 0.00015, 8e-05, 1e-05, 6e-05, 0.00022, 8e-05, 6e-05, 1e-05, 0.00021, 0.00047, 0.00034, 0.00041, 0.00019, 0.00029, 6e-05, 5e-05, 0.0001, 7e-05, 0.0, 0.0, 0.00024, 3e-05, 3e-05, 8e-05, 0.0, 2e-05, 0.00013, 0.00032, 0.00013, 0.0, 0.0, 6e-05, 0.00011, 0.0, 0.00033, 0.0002, 7e-05, 0.00071, 0.00044, 7e-05, 0.0002, 0.00066, 0.00058, 0.00056, 0.00053, 0.00019, 0.00117, 0.0, 0.00022, 0.00042, 0.00183, 0.00029, 0.0, 0.00029, 0.00916, 8e-05, 0.0, 0.0, 0.00012, 0.00026, 0.00038, 0.00064, 0.0003, 0.00038, 0.00026, 0.00097, 0.00262, 0.00181, 0.00241, 0.00299, 0.0, 2e-05, 0.00022, 0.00054, 0.00028, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00038, 0.0, 0.00042, 2e-05, 0.0, 0.00018, 0.0001, 0.00018, 0.00023, 0.00025, 0.0, 0.00025, 5e-05, 0.00016, 0.00042, 9e-05, 0.00016, 5e-05, 0.00034, 0.00049, 0.00102, 0.00086, 0.00073, 0.0005, 0.0, 0.00024, 0.0, 0.0004, 6e-05, 0.0, 0.0001, 0.00049, 0.00011, 0.0, 0.0002, 0.00049, 3e-05, 0.0, 0.0, 0.00037, 5e-05, 0.0001, 0.0, 0.00037, 0.0, 0.0, 0.00015, 0.00036, 0.0, 0.00017, 0.00048, 0.0, 0.00011, 0.0, 0.0004, 0.00017, 0.0, 0.00049, 6e-05, 0.0, 3e-05, 0.00124, 0.00069, 0.00056, 0.00014, 1e-05, 0.0, 0.0]))),\n", - " LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", - " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00135, 0.001, 0.00567, 0.0004, 0.0, 7e-05, 0.0, 9e-05, 0.0, 7e-05, 0.00013, 0.00241, 5e-05, 0.0, 0.0, 0.00014, 0.00013, 3e-05, 0.00036, 2e-05, 3e-05, 0.00013, 0.00029, 0.0, 0.00051, 0.00034, 0.0001, 0.00019, 6e-05, 0.00018, 0.0, 0.00018, 9e-05, 9e-05, 8e-05, 0.00214, 7e-05, 0.0, 0.00027, 0.0, 0.0, 7e-05, 0.0002, 0.0, 7e-05, 0.0, 0.00017, 0.0, 0.00043, 0.00044, 0.00016, 0.0011, 0.00014, 0.00012, 0.00012, 0.00111, 7e-05, 0.00014, 0.00018, 0.00109, 0.00013, 0.0, 0.00027, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00054, 0.0, 0.0, 0.0, 0.0005, 0.0, 0.0, 0.0, 0.00089, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00028, 7e-05, 0.0, 0.00028, 0.00028, 0.00016, 0.0, 0.00054, 0.0005, 0.00042, 0.00096, 0.0, 5e-05, 6e-05, 0.00077, 0.0002, 0.0, 0.0, 0.00072, 0.0, 0.00014, 0.0, 0.0003, 0.00014, 0.0, 0.00048, 0.00023, 0.0, 0.00014, 0.00044, 0.00054, 0.00135, 0.00142, 0.00023, 0.00031, 1e-05, 7e-05, 0.00011, 0.00047, 0.00018, 0.0, 0.0, 0.00011, 0.0, 0.00014, 3e-05, 0.00029, 0.0, 4e-05, 0.0, 0.00014, 6e-05, 8e-05, 9e-05, 0.00014, 0.00011, 0.00016, 2e-05, 0.00029, 0.0, 0.0, 0.00017, 0.00024, 9e-05, 3e-05, 0.0, 0.00036, 5e-05, 1e-05, 0.0, 0.00025, 0.0, 0.0, 0.0, 0.0002, 0.0, 0.0, 0.0, 0.00058, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.37843, 0.0, 0.0, 0.53164, 0.5365, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00028, 9e-05, 9e-05, 4e-05, 7e-05, 0.0, 0.0, 0.00025, 0.00011, 0.0, 0.00012, 7e-05, 4e-05, 0.00035, 0.00015, 4e-05, 7e-05, 0.00029, 0.0, 0.00047, 0.00036, 9e-05, 0.00164, 0.00232, 0.0028, 0.00131, 0.0, 0.0, 0.0, 0.00148, 0.0, 0.0, 0.0, 0.00084, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00521, 0.00527, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.40338, 0.0, 0.0, 0.0, 0.30521, 0.09093, 0.09126, 0.14967, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25536, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.70904, 0.0, 0.0, 0.0, 0.0, 0.44482, 0.05059, 1.98941, 2.66137, 1.82174, 1.98941, 0.0, 1.82174, 2.66137, 0.0, 0.0, 0.10991, 0.02851, 1.35927, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00581, 0.0, 0.0, 0.0, 0.00042, 0.00025, 0.00021, 0.00026, 0.0, 0.0, 0.00084, 0.00058, 0.00021, 0.00019, 0.00022, 0.0, 0.0, 0.00072, 0.0, 9e-05, 0.00016, 0.00029, 0.0, 0.0, 0.0005, 0.00067, 0.00059, 0.00051, 0.00058, 0.00013, 0.0, 0.00015, 2e-05, 0.0, 1e-05, 0.00032, 3e-05, 0.00015, 0.0002, 0.0, 0.00011, 0.0, 0.00022, 7e-05, 0.0, 0.00015, 0.0, 0.0, 7e-05, 0.00035, 0.0, 0.0, 0.00077, 0.00017, 0.0, 0.0, 0.00066, 0.00234, 0.00131, 0.00148, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00214, 0.0, 0.0, 0.0, 0.00178, 0.0, 0.0, 0.0, 0.00307, 0.0, 0.0, 0.0, 0.00178, 0.00165, 0.00056, 0.00035, 0.00033, 0.00061, 0.0, 0.00028, 4e-05, 9e-05, 0.0, 0.0, 0.00052, 0.0, 8e-05, 0.00017, 0.0002, 0.0, 0.0, 0.00038, 0.00022, 6e-05, 0.00029, 0.0, 0.00035, 0.00033, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e-05, 0.0, 0.00043, 0.00147, 0.00019, 0.0, 0.0001, 0.01096, 0.0, 0.00027, 4e-05, 0.01189, 0.0, 0.00048, 0.0, 0.0, 0.00088, 0.0, 0.0, 0.00084, 0.00106, 0.00067, 0.00119, 0.00069, 0.00067, 0.00106, 0.00117, 0.0048, 0.0117, 0.0124, 0.00417, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00051, 0.0, 0.0, 0.0, 0.00035, 0.00048, 0.00099, 5e-05, 0.0, 0.0, 0.00043, 0.0, 0.0, 0.0, 0.00067, 0.00087, 0.0, 0.0, 0.00021, 0.00031, 0.00016, 0.00031, 0.00044, 0.00017, 0.00031, 0.00016, 0.00047, 1e-05, 0.00073, 0.00086, 0.00056, 0.00019, 0.0, 0.0, 0.0, 0.00016, 0.0, 0.0, 0.0, 0.00108, 4e-05, 0.00021, 0.00028, 0.0, 0.00023, 0.00044, 0.00039, 0.0, 0.0, 0.00012, 0.0, 0.00042, 0.00034, 0.00032, 0.0, 0.0, 0.00017, 0.0, 0.00069, 0.00049, 0.00032, 0.00019, 0.00016, 0.0, 0.00032, 0.0, 0.0, 0.0, 0.00035, 0.0, 0.0, 0.0, 0.00013, 0.0, 0.0, 0.0, 0.00013, 0.0, 5e-05, 0.00056, 0.00032, 5e-05, 0.0, 0.00059, 0.00053, 0.00032, 0.00035, 9e-05, 0.00029, 0.0, 7e-05, 0.0001, 0.00019, 1e-05, 0.0, 0.00015, 0.0002, 0.0003, 0.0, 0.0, 0.0, 8e-05, 0.0, 0.00023, 0.0, 8e-05, 0.00067, 0.00015, 0.0, 9e-05, 1e-05, 8e-05, 0.0, 0.00048, 0.00075, 0.0, 1e-05, 0.0, 0.00045, 0.00035, 0.00013, 0.00063, 2e-05, 9e-05, 3e-05, 0.00059, 0.0, 8e-05, 0.00012, 0.00045, 0.00035, 5e-05, 0.0, 0.00013, 5e-05, 0.0, 0.00028, 0.00025, 3e-05, 0.00018, 0.0, 0.00042, 0.0, 1e-05, 9e-05, 0.0, 0.0, 2e-05, 0.001, 0.0, 0.00043, 1e-05, 0.0, 0.0, 0.0, 0.0, 7e-05, 0.00027, 6e-05, 0.0, 0.0, 0.00098, 1e-05, 8e-05, 0.0, 0.00539, 2e-05, 0.0, 0.0, 0.00051, 0.0, 0.00015, 0.0, 0.00053, 0.0, 0.0, 9e-05, 0.00072, 0.00012, 5e-05, 0.0, 6e-05, 0.0001, 9e-05, 0.00036, 0.00021, 9e-05, 0.0001, 0.00035, 0.00017, 0.00047, 0.00047, 0.00014, 0.00044, 0.00029, 0.00075, 0.0, 0.0, 1e-05, 0.0, 0.0, 8e-05, 0.0, 0.00013, 0.0007, 9e-05, 0.0, 6e-05, 0.00074, 0.00022, 9e-05, 0.0003, 0.0, 0.0, 0.0, 0.0, 0.00177, 0.00024, 0.00027, 0.0002, 0.00866, 0.0, 0.0002, 0.0, 8e-05, 1e-05, 0.0, 0.00901, 0.0, 0.00042, 0.00042, 0.0, 0.00057, 0.0, 0.00748, 0.0, 0.0, 0.00116, 6e-05, 0.0, 0.00055, 0.0, 0.00082, 0.00104, 0.00061, 0.00124, 0.00104, 0.00082, 0.00129, 0.00317, 0.00896, 0.01041, 0.00599, 0.00052, 0.0, 0.0, 0.0, 0.00039, 5e-05, 0.0, 8e-05, 0.00468, 0.00064, 0.0, 0.0, 0.0009, 0.0, 0.00013, 0.00028, 0.00097, 0.00033, 5e-05, 0.0, 0.0004, 0.00021, 0.00017, 0.00014, 0.0, 0.0, 0.0, 0.00036, 7e-05, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.00026, 0.0, 6e-05, 3e-05, 0.00043, 0.0, 0.0, 0.00038, 0.00027, 0.00016, 5e-05, 0.00031, 7e-05, 0.0, 0.00045, 0.00028, 0.0, 7e-05, 0.0004, 0.00059, 0.00054, 0.0003, 0.00045, 0.00064, 0.0, 0.0, 0.0, 0.00026, 4e-05, 0.0, 0.00034, 0.0007, 0.00011, 0.00012, 0.0, 0.00056, 0.0, 0.0002, 0.00057, 0.00065, 0.0002, 0.0, 0.00066, 0.00067, 0.00121, 0.00123, 0.00025, 0.00043, 0.00044, 0.0005, 0.00075, 0.0, 0.00014, 0.00022, 0.0, 6e-05, 7e-05, 0.00083, 0.00028, 0.0, 0.0, 0.00013, 0.0, 0.0, 0.00099, 2e-05, 0.0, 0.0, 2e-05, 0.0, 2e-05, 0.00024, 0.0001, 4e-05, 0.00038, 0.00026, 4e-05, 0.0001, 0.00031, 0.00026, 0.00045, 0.00054, 0.0004, 0.00023, 0.00026, 0.0002, 0.00047, 0.0, 0.0002, 0.00026, 0.0003, 0.00087, 0.00106, 0.00088, 0.00097, 0.00151, 0.0, 6e-05, 0.00023, 0.00137, 0.00015, 0.0, 0.0, 0.00016, 0.00042, 0.00053, 0.00013, 0.00075, 0.00043, 0.00018, 0.00075, 0.00062, 0.00051, 0.0, 0.00015, 0.0, 0.0, 3e-05, 0.00012, 0.0, 0.0, 0.0, 5e-05, 0.00015, 0.0, 6e-05, 0.00021, 0.0, 0.0, 0.0, 8e-05, 2e-05, 0.0, 0.00015, 0.00011, 7e-05, 9e-05, 0.00039, 0.00028, 9e-05, 7e-05, 0.00057, 0.00552, 0.00028, 0.00045, 0.00041, 0.0, 0.0, 0.00029, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00036, 0.00033, 0.0, 0.00011, 0.0, 0.00015, 0.00013, 0.0, 0.0, 0.00036, 0.0, 0.00012, 5e-05, 0.00022, 0.0, 0.00018, 7e-05, 5e-05, 7e-05, 0.0, 0.00032, 8e-05, 5e-05, 0.0, 4e-05, 1e-05, 8e-05, 0.00027, 0.00016, 7e-05, 0.00018, 0.0, 3e-05, 0.00027, 0.00034, 3e-05, 7e-05, 0.00048, 0.00045, 7e-05, 3e-05, 0.00053, 0.00018, 0.00058, 0.00057, 8e-05, 0.0, 8e-05, 0.0, 0.00016, 0.0, 0.0, 7e-05, 0.00014, 0.0, 6e-05, 1e-05, 0.0, 0.00022, 0.00011, 0.0, 0.00022, 0.00026, 0.0, 0.00011, 0.00035, 0.00033, 0.00045, 0.00032, 0.00016, 0.0005, 0.00027, 3e-05, 0.0008, 0.0, 0.0, 0.0, 0.0003, 3e-05, 0.00027, 0.00083, 0.0, 0.0, 0.0, 2e-05, 0.00181, 0.00152, 0.00038, 0.0, 9e-05, 0.0, 0.0, 0.00012, 0.00011, 7e-05, 7e-05, 4e-05, 0.00014, 0.00012, 0.0, 0.00014, 0.00029, 0.00012, 0.00039, 0.0, 0.00032, 0.00066, 0.00032, 0.00032, 0.0, 0.0009, 0.00201, 0.00021, 0.00041, 0.00014, 6e-05, 3e-05, 0.00021, 0.0, 0.0002, 0.0, 0.0, 0.00011, 0.00028, 2e-05, 0.0, 0.0, 9e-05, 4e-05, 9e-05, 9e-05, 0.0, 0.0001, 0.0005, 0.0002, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00039, 0.00028, 0.0, 0.0, 6e-05, 0.0003, 0.00031, 0.0001, 0.00092, 0.0, 0.00012, 4e-05, 0.00098, 4e-05, 7e-05, 4e-05, 0.00062, 0.00015, 0.0, 0.0, 0.00049, 5e-05, 0.0, 0.0, 0.00029, 6e-05, 0.0, 8e-05, 0.00102, 0.0, 0.0, 0.0001, 0.00037, 1e-05, 0.00021, 0.00038, 6e-05, 0.00021, 1e-05, 7e-05, 0.00062, 0.00026, 0.00036, 0.0003, 0.00045, 1e-05, 0.0, 0.0, 0.00047, 0.0, 3e-05, 9e-05, 0.00057, 0.00022, 8e-05, 6e-05, 0.0, 2e-05, 0.00036, 0.0, 0.00021, 0.0, 0.0, 0.0001, 0.0005, 0.00019, 1e-05, 0.0, 4e-05, 8e-05, 3e-05, 0.00028, 0.00013, 3e-05, 8e-05, 0.00021, 0.0, 0.00054, 0.00044, 0.0002, 0.00148, 0.00101, 0.00116, 0.00033, 0.00012, 0.0, 0.0, 0.00034, 0.0, 7e-05, 0.0001, 0.00066, 2e-05, 0.0, 0.0, 0.00079, 0.00061, 9e-05, 0.00011, 0.0, 0.0, 0.00012, 0.0001, 3e-05, 0.0, 7e-05, 0.00019, 3e-05, 3e-05, 0.0, 0.00027, 0.0001, 0.0, 0.00037, 0.00012, 0.0, 0.0001, 0.0003, 0.0002, 0.00043, 0.00033, 0.00018, 0.00033, 0.0, 3e-05, 0.0001, 0.0, 0.0, 4e-05, 0.00025, 0.0, 0.0, 0.0, 0.0, 4e-05, 0.0, 0.00028, 6e-05, 5e-05, 0.0, 0.0001, 0.00014, 0.0, 0.00033, 0.0, 3e-05, 0.00042, 0.00025, 3e-05, 0.0, 0.0003, 0.00054, 0.00049, 0.0003, 0.00081, 0.00041, 0.0, 0.0, 0.0, 0.00022, 0.0, 0.0, 0.0, 0.00184, 9e-05, 5e-05, 0.0, 3e-05, 6e-05, 0.0001, 0.00032, 7e-05, 0.0001, 6e-05, 0.0002, 0.00062, 0.00045, 0.00037, 0.00015, 0.00043, 0.0, 0.0001, 0.00073, 0.0, 0.0, 7e-05, 0.00029, 0.0001, 0.0, 0.00076, 0.00015, 0.0, 0.00015, 0.0, 0.00028, 0.00036, 0.00014, 0.00014, 0.00013, 0.0, 7e-05, 0.0, 1e-05, 9e-05, 4e-05, 2e-05, 0.0001, 0.0002, 0.0002, 0.00021, 4e-05, 2e-05, 0.00041, 0.0001, 0.00016, 0.00083, 0.00013, 0.00016, 0.0001, 0.00051, 0.00138, 0.00017, 0.00036, 0.00048, 0.0005, 0.0, 7e-05, 0.0, 0.00032, 0.0, 0.0, 0.00015, 0.00028, 6e-05, 8e-05, 0.00035, 0.00059, 5e-05, 0.0, 0.0002, 0.0, 0.0, 0.0, 0.00051, 0.0, 8e-05, 8e-05, 1e-05, 0.0, 0.00011, 0.00027, 0.00019, 1e-05, 6e-05, 6e-05, 0.0001, 0.0, 0.0003, 7e-05, 0.0, 0.00011, 0.00023, 0.00016, 2e-05, 0.0, 0.00016, 0.0, 0.00012, 7e-05, 0.00038, 0.0, 0.0, 7e-05, 0.00058, 7e-05, 0.0, 0.0, 0.00119, 0.00013, 0.00013, 0.0, 0.00019, 3e-05, 1e-05, 3e-05, 0.00045, 0.0, 5e-05, 0.00012, 0.00067, 1e-05, 7e-05, 0.0, 0.00038, 0.00019, 0.0, 0.0, 0.00026, 0.00015, 1e-05, 0.0, 0.00041, 0.0, 0.00021, 0.00053, 0.00021, 0.00027, 0.00033, 7e-05, 0.00014, 0.0, 0.00013, 1e-05, 5e-05, 0.00061, 0.0, 0.0, 2e-05, 0.00016, 5e-05, 1e-05, 0.00039, 0.0, 0.0, 5e-05, 5e-05, 0.00021, 0.00027, 9e-05, 0.0003, 0.0001, 4e-05, 0.0, 0.00027, 2e-05, 0.0, 8e-05, 0.00019, 0.00014, 0.00026, 0.00019, 0.00023, 6e-05, 1e-05, 0.00068, 0.00018, 1e-05, 6e-05, 0.00057, 0.00117, 0.00044, 0.00037, 0.00034, 0.00046, 0.0, 2e-05, 0.0, 0.00028, 6e-05, 0.0, 0.00011, 0.00014, 9e-05, 0.00017, 9e-05, 0.00021, 3e-05, 4e-05, 8e-05, 9e-05, 0.0, 0.0, 0.00037, 0.0, 0.0, 1e-05, 0.0, 4e-05, 6e-05, 0.00054, 0.00021, 0.0, 0.0, 3e-05, 8e-05, 3e-05, 0.00012, 0.00015, 1e-05, 0.00033, 8e-05, 1e-05, 0.00015, 0.00027, 0.0, 0.00046, 0.00049, 0.00027, 0.0, 0.00012, 0.0, 0.00023, 3e-05, 9e-05, 0.00012, 0.0, 0.0, 0.0, 8e-05, 4e-05, 0.00019, 0.00015, 0.00011, 0.00025, 9e-05, 0.00011, 0.00015, 0.00037, 0.00042, 0.00061, 0.00043, 0.00033, 0.0, 0.0, 0.0, 0.00023, 0.0, 0.00015, 0.00014, 0.0, 5e-05, 0.00012, 3e-05, 3e-05, 0.00013, 0.0, 0.0, 4e-05, 0.0, 0.00034, 4e-05, 0.0, 0.00011, 0.00028, 4e-05, 0.00011, 0.00039, 0.00032, 0.00011, 4e-05, 0.00032, 0.00012, 0.00044, 0.00038, 0.0003, 0.0, 0.0, 0.0, 0.00026, 0.00032, 0.00011, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0002, 6e-05, 1e-05, 7e-05, 0.00024, 6e-05, 7e-05, 1e-05, 0.00039, 0.00021, 0.00032, 0.00049, 0.0003, 0.00028, 0.00017, 7e-05, 0.00064, 0.00018, 0.0, 0.0, 0.0, 7e-05, 0.00017, 0.00078, 0.0, 0.00021, 0.00021, 0.00043, 0.00059, 0.00045, 0.00036, 0.0, 0.00011, 2e-05, 0.0002, 0.00017, 0.0, 0.0, 8e-05, 0.0, 7e-05, 0.00024, 0.00041, 0.0, 0.00032, 5e-05, 0.00042, 4e-05, 6e-05, 0.00049, 0.0, 6e-05, 4e-05, 0.0002, 0.001, 0.00037, 0.0004, 0.00021, 0.0005, 0.0, 0.0, 0.0, 0.00027, 9e-05, 1e-05, 0.00011, 0.00032, 0.00021, 0.00019, 7e-05, 0.0, 8e-05, 0.00013, 0.00012, 0.00019, 2e-05, 0.0, 7e-05, 0.0, 0.00015, 7e-05, 0.00014, 0.00051, 0.00016, 3e-05, 0.0, 0.00078, 0.0, 0.0, 7e-05, 0.00041, 0.0, 0.0, 0.00014, 0.00253, 4e-05, 0.0001, 0.0, 0.00224, 0.0, 0.0, 4e-05, 8e-05, 0.0, 0.00019, 0.00018, 0.00057, 0.00048, 0.0003, 0.00032, 8e-05, 1e-05, 3e-05, 0.00036, 0.0, 2e-05, 0.0, 0.00048, 0.0, 0.0, 9e-05, 0.00035, 3e-05, 3e-05, 0.00042, 0.00031, 3e-05, 3e-05, 0.00032, 0.00024, 0.00044, 0.00039, 0.00018, 0.00032, 1e-05, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.00043, 5e-05, 0.0, 0.0, 4e-05, 1e-05, 0.0, 0.00069, 0.0, 6e-05, 8e-05, 3e-05, 0.0, 4e-05, 0.00019, 6e-05, 2e-05, 0.00028, 0.00013, 2e-05, 6e-05, 0.00021, 0.0, 0.00047, 0.00053, 6e-05, 0.00036, 5e-05, 0.0, 0.00024, 0.00063, 0.0, 5e-05, 6e-05, 0.00032, 4e-05, 3e-05, 0.0, 0.00022, 0.0, 0.0, 0.0, 0.00033, 0.0, 0.0, 3e-05, 0.00033, 0.00011, 6e-05, 7e-05, 0.00046, 8e-05, 7e-05, 0.00067, 0.0, 0.0, 7e-05, 0.00036, 7e-05, 8e-05, 0.00066, 0.0, 3e-05, 4e-05, 0.0, 0.00032, 0.00028, 5e-05, 6e-05, 0.0, 0.0, 0.00033, 0.0, 0.0, 0.00012, 0.00039, 0.0, 5e-05, 5e-05, 0.00099, 0.00013, 0.00017, 9e-05, 0.00052, 0.0, 0.00024, 0.00095, 0.00046, 0.00024, 0.0, 0.00118, 0.01194, 0.00045, 0.0005, 0.0, 0.0, 0.0, 1e-05, 0.00057, 0.00038, 0.0, 0.00022, 0.0, 0.00398, 0.00042, 0.00049, 0.0016, 0.0, 6e-05, 0.0, 0.0001, 4e-05, 0.0, 0.00031, 0.00013, 0.0, 0.0001, 0.0, 0.0, 4e-05, 0.00044, 0.0, 0.0, 6e-05, 8e-05, 0.0003, 0.00077, 0.00031, 0.00038, 2e-05, 0.0, 0.0, 0.00029, 0.0, 7e-05, 0.0, 0.00034, 0.00018, 0.00012, 2e-05, 0.00028, 0.00011, 0.0, 0.0, 0.0003, 5e-05, 0.0, 4e-05, 0.00045, 0.0, 4e-05, 0.0, 0.00031, 0.00012, 0.00011, 0.00031, 7e-05, 0.00011, 0.00012, 0.00028, 0.0003, 0.00039, 0.00039, 0.00017, 0.00096, 0.0, 1e-05, 0.0, 0.0, 2e-05, 9e-05, 0.00075, 0.0, 5e-05, 0.0001, 0.0, 5e-05, 2e-05, 0.00012, 9e-05, 9e-05, 9e-05, 0.0, 0.00015, 0.0])))]},\n", - " 'version': 2}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "primitive_result.metadata" - ] - }, - { - "cell_type": "markdown", - "id": "69f5426e", - "metadata": {}, - "source": [ - "The `PubResult` object has additional resilience metadata about the learned noise models used in mitigation." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "52482e42", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "noise_overhead: 9.2584227461744e+229\n", - "total_mitigated_layers: 18\n", - "unique_mitigated_layers: 3\n", - "unique_mitigated_layers_noise_overhead: [2.0713004613510885e+36, 10.600275591731494, 9.687147432958504]\n" - ] - } - ], - "source": [ - "# Print learned layer noise metadata\n", - "for field, value in pub_result.metadata[\"resilience\"][\"layer_noise\"].items():\n", - " print(f\"{field}: {value}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "2b96bdd2", - "metadata": {}, - "outputs": [], - "source": [ - "# Exact data computed using the methods described in the original reference\n", - "# Y. Kim et al. \"Evidence for the utility of quantum computing before fault tolerance\" (Nature 618, 500–505 (2023))\n", - "# Directly used here for brevity\n", - "exact_data = np.array(\n", - " [\n", - " 1,\n", - " 0.9899,\n", - " 0.9531,\n", - " 0.8809,\n", - " 0.7536,\n", - " 0.5677,\n", - " 0.3545,\n", - " 0.1607,\n", - " 0.0539,\n", - " 0.0103,\n", - " 0.0012,\n", - " 0.0,\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f6dfbb9a", - "metadata": {}, - "source": [ - "### Plot Trotter simulation results\n", - "\n", - "The following code creates a plot to compare the raw and mitigated experiment results against the exact solution." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "e466736a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "zne_metadata = primitive_result.metadata[\"resilience\"][\"zne\"]\n", - "# Plot Trotter simulation results\n", - "fig = plot_trotter_results(\n", - " pub_result,\n", - " parameter_values,\n", - " plot_extrapolator=zne_metadata[\"extrapolator\"],\n", - " plot_noise_factors=zne_metadata[\"noise_factors\"],\n", - " exact=exact_data,\n", - ")\n", - "display(fig)" - ] - }, - { - "cell_type": "markdown", - "id": "1cd46c88", - "metadata": {}, - "source": [ - "While the noisy (noise factor `nf=1.0`) values show high deviation from exact values, the mitigated values are close to exact values, demonstrating the utility of the PEA-based mitigation technique.\n", - "\n", - "### Plot extrapolation results for individual qubits\n", - "\n", - "Finally, the following code creates a plot to show the extrapolation curves for different values of theta on a specific qubit." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "bea9695a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "virtual_qubit = 1\n", - "plot_qubit_zne_data(\n", - " pub_result=pub_result,\n", - " angles=parameter_values,\n", - " qubit=virtual_qubit,\n", - " noise_factors=zne_metadata[\"noise_factors\"],\n", - " extrapolator=zne_metadata[\"extrapolator\"],\n", - " extrapolated_noise_factors=zne_metadata[\"extrapolated_noise_factors\"],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "75f48e6a-c7e4-46f3-9d39-a7a877427a04", - "metadata": {}, - "source": [ - "## Next steps\n", - - "\n", - "If you found this work interesting, you might be interested in the following material:\n", - "- A [tutorial](/docs/tutorials/combine-error-mitigation-techniques) focused on combining error mitigation techniques.\n", - "- Detailed [documentation](/docs/guides/error-mitigation-and-suppression-techniques) on the error mitigation techniques available in Qiskit.\n", - "- Additional lessons covering utility-scale experiments: [Utility II](/learning/courses/utility-scale-quantum-computing/utility-ii) and [Utility III](/learning/courses/utility-scale-quantum-computing/utility-iii).\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d2c31ae8", + "metadata": {}, + "source": [ + "---\n", + "title: Utility-scale error mitigation with probabilistic error amplification\n", + "description: Run a utility-scale error mitigation experiment with zero noise extrapolation and probabilistic error amplification.\n", + "---\n", + "\n", + "{/* cspell:ignore mapsto multigraph inds extrap sharex sharey pidx */}\n", + "\n", + "# Utility-scale error mitigation with probabilistic error amplification\n", + "*Usage estimate: 14 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "8bf80006", + "metadata": {}, + "source": [ + "## Learning outcomes\n", + "After going through this tutorial, users should understand:\n", + "- The theory behind *zero-noise extrapolation* (ZNE), the different methods to amplify noise, and why *probabilistic error amplification* (PEA) is preferred for utility-scale experiments.\n", + "- How to implement ZNE with PEA in practice using Qiskit.\n", + "\n", + "## Prerequisites\n", + "We suggest that users are familiar with the following topics before going through this tutorial:\n", + "- The [Error mitigation lesson](/learning/courses/utility-scale-quantum-computing/error-mitigation) of the *Utility-scale quantum computing* course for basic knowledge of using error mitigation in Qiskit.\n", + "- The [Utility-I lesson](/learning/courses/utility-scale-quantum-computing/utility-i) of the *Utility-scale quantum computing* course for more background on the utility-scale experiment used as an example in this tutorial." + ] + }, + { + "cell_type": "markdown", + "id": "a929ccce", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "This tutorial demonstrates how to run a utility-scale error mitigation experiment with Qiskit Runtime using an experimental version of *zero-noise extrapolation* (ZNE) with *probabilistic error amplification* (PEA).\n", + "\n", + "![kim\\_nature\\_fig.png](https://quantum.cloud.ibm.com/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/e1e67c34-9d4d-4a88-9340-f0b2f3676770.avif)\n", + "**Reference**: Y. Kim et al. *Evidence for the utility of quantum computing before fault tolerance.* [Nature 618.7965 (2023)](https://www.nature.com/articles/s41586-023-06096-3)" + ] + }, + { + "cell_type": "markdown", + "id": "a89ed8dd", + "metadata": {}, + "source": [ + "### Zero-noise extrapolation (ZNE)\n", + "Zero-noise extrapolation (ZNE) is an error mitigation technique that removes the effects of an *unknown* noise during circuit execution that can be scaled in a *known* way.\n", + "\n", + "It assumes expectation values scale with noise by a known function\n", + "\n", + "$$\n", + "\\langle A(\\lambda) \\rangle = \\langle A(0) \\rangle + \\sum_{k=0}^{m} a_k \\lambda^k + R\n", + "$$\n", + "\n", + "where $\\lambda$ parameterizes the noise strength and can be amplified.\n", + "\n", + "We can implement ZNE with the following steps:\n", + "\n", + "1. Amplify circuit noise for several noise factors $\\lambda_1, \\lambda_2, ... $\n", + "2. Run every noise-amplified circuit to measure $\\langle A(\\lambda_1)\\rangle, ...$\n", + "3. Extrapolate back to the zero-noise limit $\\langle A(0)\\rangle$\n", + "\n", + "![zne\\_stages.png](https://quantum.cloud.ibm.com/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/5e63d706-82d8-4212-b802-c9191ce53341.avif)" + ] + }, + { + "cell_type": "markdown", + "id": "5db985b9", + "metadata": {}, + "source": [ + "#### Amplify noise for ZNE\n", + "\n", + "The main challenge in successfully implementing ZNE is to have an accurate model for noise in the expectation value and to amplify the noise in a known way.\n", + "\n", + "There are three common ways error amplification is implemented for ZNE.\n", + "\n", + "| **Pulse stretching** | **Gate folding** | **Probabilistic error amplification** |\n", + "| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n", + "| Scale pulse duration via calibration | Repeat gates in identity cycles $U\\mapsto U(U^{-1}U)^{\\lambda-1}/2$ | Add noise via sampling Pauli channels |\n", + "| ![zne\\_pulse\\_stretching.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/83188b57-e88f-43a1-a7bd-29327f46ecf5.avif) | ![zne\\_gate\\_folding.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/e1358d08-2632-4fd2-bf0f-f9384a2d3340.avif) | ![zne\\_pea.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/3d69d5bd-70e5-4eeb-aa02-fc0a62043010.avif) |\n", + "| Kandala et al. Nature (2019) | Shultz et al. PRA (2022) | Li & Benjamin PRX (2017) |" + ] + }, + { + "cell_type": "markdown", + "id": "c23e43ee", + "metadata": {}, + "source": [ + "For utility-scale experiments, *probabilistic error amplification* (PEA) is the most attractive.\n", + "\n", + "* Pulse stretching assumes gate noise is proportional to duration, which is typically not true. Calibration is also costly.\n", + "* Gate folding requires large stretch factors that greatly limit the depth of circuits that can be run.\n", + "* PEA can be applied to any circuit that can be run with native noise factor ($\\lambda=1$) but requires learning the noise model.\n", + "\n", + "### Learn the noise model for PEA\n", + "PEA assumes the same layer-based noise model as *probabilistic error cancellation* (PEC); however, it avoids the sampling overhead that scales exponentially with the circuit noise.\n", + "\n", + "| **Step 1** | **Step 2** | **Step 3** |\n", + "| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n", + "| Pauli twirl layers of two-qubit gates | Repeat identity pairs of layers and learn the noise | Derive a fidelity (error for each noise channel) |\n", + "| ![pec\\_pauli\\_twirling.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/2eab5ff4-40fa-4a41-9f2c-74f5e22c4643.avif) | ![pec\\_learn\\_layer.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/8d0d64c3-65ad-4419-8ac9-4ec9633d39a0.avif) | ![pec\\_curve\\_fitting.png](/docs/images/tutorials/utility-scale-error-mitigation-with-probabilistic-error-amplification/c51bd42d-2463-4c78-807b-d284ca79296f.avif) |\n", + "\n", + "**Reference**: E. van den Berg, Z. Minev, A. Kandala, and K. Temme, *Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors* [arXiv:2201.09866](https://arxiv.org/abs/2201.09866)" + ] + }, + { + "cell_type": "markdown", + "id": "55b94021", + "metadata": {}, + "source": [ + "## Requirements\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "\n", + "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7db2e559", + "metadata": {}, + "source": [ + "## Setup\n", + "In the cell below, we import relevant packages and create some helper functions to construct the circuits for the Trotterized time evolution of a two-dimensional transverse-field Ising model that adheres to the topology of the backend." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "779bbc51", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "from collections.abc import Sequence\n", + "from collections import defaultdict\n", + "import numpy as np\n", + "import rustworkx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from qiskit.circuit import QuantumCircuit, Parameter\n", + "from qiskit.circuit.library import CXGate, CZGate, ECRGate\n", + "from qiskit.providers import Backend\n", + "from qiskit.visualization import plot_error_map\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.primitives import PubResult\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "\n", + "\"\"\"Trotter circuit generation\"\"\"\n", + "\n", + "\n", + "def remove_qubit_couplings(\n", + " couplings: Sequence[tuple[int, int]], qubits: Sequence[int] | None = None\n", + ") -> list[tuple[int, int]]:\n", + " \"\"\"Remove qubits from a coupling list.\n", + "\n", + " Args:\n", + " couplings: A sequence of qubit couplings.\n", + " qubits: Optional, the qubits to remove.\n", + "\n", + " Returns:\n", + " The input couplings with the specified qubits removed.\n", + " \"\"\"\n", + " if qubits is None:\n", + " return couplings\n", + " qubits = set(qubits)\n", + " return [edge for edge in couplings if not qubits.intersection(edge)]\n", + "\n", + "\n", + "def coupling_qubits(\n", + " *couplings: Sequence[tuple[int, int]],\n", + " allowed_qubits: Sequence[int] | None = None,\n", + ") -> list[int]:\n", + " \"\"\"Return a sorted list of all qubits involved in one or more couplings lists.\n", + "\n", + " Args:\n", + " couplings: one or more coupling lists.\n", + " allowed_qubits: Optional, the allowed qubits to include. If None all\n", + " qubits are allowed.\n", + "\n", + " Returns:\n", + " The intersection of all qubits in the couplings and the allowed qubits.\n", + " \"\"\"\n", + " qubits = set()\n", + " for edges in couplings:\n", + " for edge in edges:\n", + " qubits.update(edge)\n", + " if allowed_qubits is not None:\n", + " qubits = qubits.intersection(allowed_qubits)\n", + " return list(qubits)\n", + "\n", + "\n", + "def construct_layer_couplings(\n", + " backend: Backend,\n", + ") -> list[list[tuple[int, int]]]:\n", + " \"\"\"Separate a coupling map into disjoint 2-qubit gate layers.\n", + "\n", + " Args:\n", + " backend: A backend to construct layer couplings for.\n", + "\n", + " Returns:\n", + " A list of disjoint layers of directed couplings for the input coupling map.\n", + " \"\"\"\n", + " coupling_graph = backend.coupling_map.graph.to_undirected(\n", + " multigraph=False\n", + " )\n", + " edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)\n", + "\n", + " layers = defaultdict(list)\n", + " for edge_idx, color in edge_coloring.items():\n", + " layers[color].append(\n", + " coupling_graph.get_edge_endpoints_by_index(edge_idx)\n", + " )\n", + " layers = [sorted(layers[i]) for i in sorted(layers.keys())]\n", + "\n", + " return layers\n", + "\n", + "\n", + "def entangling_layer(\n", + " gate_2q: str,\n", + " couplings: Sequence[tuple[int, int]],\n", + " qubits: Sequence[int] | None = None,\n", + ") -> QuantumCircuit:\n", + " \"\"\"Generating a entangling layer for the specified couplings.\n", + "\n", + " This corresponds to a Trotter layer for a ZZ Ising term with angle Pi/2.\n", + "\n", + " Args:\n", + " gate_2q: The 2-qubit basis gate for the layer, should be \"cx\", \"cz\", or \"ecr\".\n", + " couplings: A sequence of qubit couplings to add CX gates to.\n", + " qubits: Optional, the physical qubits for the layer. Any couplings involving\n", + " qubits not in this list will be removed. If None the range up to the largest\n", + " qubit in the couplings will be used.\n", + "\n", + " Returns:\n", + " The QuantumCircuit for the entangling layer.\n", + " \"\"\"\n", + " # Get qubits and convert to set to order\n", + " if qubits is None:\n", + " qubits = range(1 + max(coupling_qubits(couplings)))\n", + " qubits = set(qubits)\n", + "\n", + " # Mapping of physical qubit to virtual qubit\n", + " qubit_mapping = {q: i for i, q in enumerate(qubits)}\n", + "\n", + " # Convert couplings to indices for virtual qubits\n", + " indices = [\n", + " [qubit_mapping[i] for i in edge]\n", + " for edge in couplings\n", + " if qubits.issuperset(edge)\n", + " ]\n", + "\n", + " # Layer circuit on virtual qubits\n", + " circuit = QuantumCircuit(len(qubits))\n", + "\n", + " # Get 2-qubit basis gate and pre and post rotation circuits\n", + " gate2q = None\n", + " pre = QuantumCircuit(2)\n", + " post = QuantumCircuit(2)\n", + "\n", + " if gate_2q == \"cx\":\n", + " gate2q = CXGate()\n", + " # Pre-rotation\n", + " pre.sdg(0)\n", + " pre.z(1)\n", + " pre.sx(1)\n", + " pre.s(1)\n", + " # Post-rotation\n", + " post.sdg(1)\n", + " post.sxdg(1)\n", + " post.s(1)\n", + " elif gate_2q == \"ecr\":\n", + " gate2q = ECRGate()\n", + " # Pre-rotation\n", + " pre.z(0)\n", + " pre.s(1)\n", + " pre.sx(1)\n", + " pre.s(1)\n", + " # Post-rotation\n", + " post.x(0)\n", + " post.sdg(1)\n", + " post.sxdg(1)\n", + " post.s(1)\n", + " elif gate_2q == \"cz\":\n", + " gate2q = CZGate()\n", + " # Identity pre-rotation\n", + " # Post-rotation\n", + " post.sdg([0, 1])\n", + " else:\n", + " raise ValueError(\n", + " f\"Invalid 2-qubit basis gate {gate_2q}, should be 'cx', 'cz', or 'ecr'\"\n", + " )\n", + "\n", + " # Add 1Q pre-rotations\n", + " for inds in indices:\n", + " circuit.compose(pre, qubits=inds, inplace=True)\n", + "\n", + " # Use barriers around 2-qubit basis gate to specify a layer for PEA noise learning\n", + " circuit.barrier()\n", + " for inds in indices:\n", + " circuit.append(gate2q, (inds[0], inds[1]))\n", + " circuit.barrier()\n", + "\n", + " # Add 1Q post-rotations after barrier\n", + " for inds in indices:\n", + " circuit.compose(post, qubits=inds, inplace=True)\n", + "\n", + " # Add physical qubits as metadata\n", + " circuit.metadata[\"physical_qubits\"] = tuple(qubits)\n", + "\n", + " return circuit\n", + "\n", + "\n", + "def trotter_circuit(\n", + " theta: Parameter | float,\n", + " layer_couplings: Sequence[Sequence[tuple[int, int]]],\n", + " num_steps: int,\n", + " gate_2q: str | None = \"cx\",\n", + " backend: Backend | None = None,\n", + " qubits: Sequence[int] | None = None,\n", + ") -> QuantumCircuit:\n", + " \"\"\"Generate a Trotter circuit for the 2D Ising\n", + "\n", + " Args:\n", + " theta: The angle parameter for X.\n", + " layer_couplings: A list of couplings for each entangling layer.\n", + " num_steps: the number of Trotter steps.\n", + " gate_2q: The 2-qubit basis gate to use in entangling layers.\n", + " Can be \"cx\", \"cz\", \"ecr\", or None if a backend is provided.\n", + " backend: A backend to get the 2-qubit basis gate from, if provided\n", + " will override the basis_gate field.\n", + " qubits: Optional, the allowed physical qubits to truncate the\n", + " couplings to. If None the range up to the largest\n", + " qubit in the couplings will be used.\n", + "\n", + " Returns:\n", + " The Trotter circuit.\n", + " \"\"\"\n", + " if backend is not None:\n", + " try:\n", + " basis_gates = backend.configuration().basis_gates\n", + " except AttributeError:\n", + " basis_gates = backend.basis_gates\n", + " for gate in [\"cx\", \"cz\", \"ecr\"]:\n", + " if gate in basis_gates:\n", + " gate_2q = gate\n", + " break\n", + "\n", + " # If no qubits, get the largest qubit from all layers and\n", + " # specify the range so the same one is used for all layers.\n", + " if qubits is None:\n", + " qubits = range(1 + max(coupling_qubits(layer_couplings)))\n", + "\n", + " # Generate the entangling layers\n", + " layers = [\n", + " entangling_layer(gate_2q, couplings, qubits=qubits)\n", + " for couplings in layer_couplings\n", + " ]\n", + "\n", + " # Construct the circuit for a single Trotter step\n", + " num_qubits = len(qubits)\n", + " trotter_step = QuantumCircuit(num_qubits)\n", + " trotter_step.rx(theta, range(num_qubits))\n", + " for layer in layers:\n", + " trotter_step.compose(layer, range(num_qubits), inplace=True)\n", + "\n", + " # Construct the circuit for the specified number of Trotter steps\n", + " circuit = QuantumCircuit(num_qubits)\n", + " for _ in range(num_steps):\n", + " circuit.rx(theta, range(num_qubits))\n", + " for layer in layers:\n", + " circuit.compose(layer, range(num_qubits), inplace=True)\n", + "\n", + " circuit.metadata[\"physical_qubits\"] = tuple(qubits)\n", + " return circuit\n", + "\n", + "\n", + "\"\"\"Result visualization functions\"\"\"\n", + "\n", + "\n", + "def plot_trotter_results(\n", + " pub_result: PubResult,\n", + " angles: Sequence[float],\n", + " plot_noise_factors: Sequence[float] | None = None,\n", + " plot_extrapolator: Sequence[str] | None = None,\n", + " exact: np.ndarray = None,\n", + " close: bool = True,\n", + "):\n", + " \"\"\"Plot average magnetization from ZNE result data.\n", + " Args:\n", + " pub_result: The Estimator PubResult for the PEA experiment.\n", + " angles: The Rx angle values for the experiment.\n", + " plot_raw: If provided plot the unextrapolated data for the noise factors.\n", + " plot_extrapolator: If provided plot all extrapolators, if False only plot\n", + " the Automatic method.\n", + " exact: Optional, the exact values to include in the plot. Should be a 1D\n", + " array-like where the values represent exact magnetization.\n", + " close: Close the Matplotlib figure before returning.\n", + " Returns:\n", + " The figure.\n", + " \"\"\"\n", + " data = pub_result.data\n", + "\n", + " evs = data.evs\n", + " num_qubits = evs.shape[0]\n", + " num_params = evs.shape[1]\n", + " angles = np.asarray(angles).ravel()\n", + " if angles.shape != (num_params,):\n", + " raise ValueError(\n", + " f\"Incorrect number of angles for input data {angles.size} != {num_params}\"\n", + " )\n", + "\n", + " # Take average magnetization of qubits and its standard error\n", + " x_vals = angles / np.pi\n", + " y_vals = np.mean(evs, axis=0)\n", + " y_errs = np.std(evs, axis=0) / np.sqrt(num_qubits)\n", + "\n", + " fig, _ = plt.subplots(1, 1)\n", + "\n", + " # Plot auto method\n", + " plt.errorbar(x_vals, y_vals, y_errs, fmt=\"o-\", label=\"ZNE (automatic)\")\n", + "\n", + " # Plot individual extrapolator results\n", + " if plot_extrapolator:\n", + " y_vals_extrap = np.mean(data.evs_extrapolated, axis=0)\n", + " y_errs_extrap = np.std(data.evs_extrapolated, axis=0) / np.sqrt(\n", + " num_qubits\n", + " )\n", + " for i, extrap in enumerate(plot_extrapolator):\n", + " plt.errorbar(\n", + " x_vals,\n", + " y_vals_extrap[:, i, 0],\n", + " y_errs_extrap[:, i, 0],\n", + " fmt=\"s-.\",\n", + " alpha=0.5,\n", + " label=f\"ZNE ({extrap})\",\n", + " )\n", + "\n", + " # Plot raw results\n", + " if plot_noise_factors:\n", + " y_vals_raw = np.mean(data.evs_noise_factors, axis=0)\n", + " y_errs_raw = np.std(data.evs_noise_factors, axis=0) / np.sqrt(\n", + " num_qubits\n", + " )\n", + " for i, nf in enumerate(plot_noise_factors):\n", + " plt.errorbar(\n", + " x_vals,\n", + " y_vals_raw[:, i],\n", + " y_errs_raw[:, i],\n", + " fmt=\"d:\",\n", + " alpha=0.5,\n", + " label=f\"Raw (nf={nf:.1f})\",\n", + " )\n", + "\n", + " # Plot exact data\n", + " if exact is not None:\n", + " plt.plot(x_vals, exact, \"--\", color=\"black\", alpha=0.5, label=\"Exact\")\n", + "\n", + " plt.ylim(-0.1, 1.2)\n", + " plt.xlabel(\"θ/π\")\n", + " plt.ylabel(r\"$\\overline{\\langle Z \\rangle}$\")\n", + " plt.legend()\n", + " plt.title(\n", + " f\"Error Mitigated Average Magnetization for Rx(θ) [{num_qubits}-qubit]\"\n", + " )\n", + " if close:\n", + " plt.close(fig)\n", + " return fig\n", + "\n", + "\n", + "def plot_qubit_zne_data(\n", + " pub_result: PubResult,\n", + " angles: Sequence[float],\n", + " qubit: int,\n", + " noise_factors: Sequence[float],\n", + " extrapolator: Sequence[str] | None = None,\n", + " extrapolated_noise_factors: Sequence[float] | None = None,\n", + " num_cols: int | None = None,\n", + " close: bool = True,\n", + "):\n", + " \"\"\"Plot ZNE extrapolation data for specific virtual qubit\n", + " Args:\n", + " pub_result: The Estimator PubResult for the PEA experiment.\n", + " angles: The Rx theta angles used for the experiment.\n", + " qubit: The virtual qubit index to plot.\n", + " noise_factors: the raw noise factors.\n", + " extrapolator: The extrapolator metadata for multiple extrapolators.\n", + " extrapolated_noise_factors: The noise factors used for extrapolation.\n", + " num_cols: The number of columns for the generated subplots.\n", + " close: Close the Matplotlib figure before returning.\n", + " Returns:\n", + " The Matplotlib figure.\n", + " \"\"\"\n", + " data = pub_result.data\n", + "\n", + " evs_auto = data.evs[qubit]\n", + " stds_auto = data.stds[qubit]\n", + " evs_extrap = data.evs_extrapolated[qubit]\n", + " stds_extrap = data.stds_extrapolated[qubit]\n", + " evs_raw = data.evs_noise_factors[qubit]\n", + " stds_raw = data.stds_noise_factors[qubit]\n", + "\n", + " num_params = evs_auto.shape[0]\n", + " angles = np.asarray(angles).ravel()\n", + " if angles.shape != (num_params,):\n", + " raise ValueError(\n", + " f\"Incorrect number of angles for input data {angles.size} != {num_params}\"\n", + " )\n", + "\n", + " # Make a square subplot\n", + " num_cols = num_cols or int(np.ceil(np.sqrt(num_params)))\n", + " num_rows = int(np.ceil(num_params / num_cols))\n", + " fig, axes = plt.subplots(\n", + " num_rows, num_cols, sharex=True, sharey=True, figsize=(12, 5)\n", + " )\n", + " fig.suptitle(f\"ZNE data for virtual qubit {qubit}\")\n", + "\n", + " for pidx, ax in zip(range(num_params), axes.flat):\n", + " # Plot auto extrapolated\n", + " ax.errorbar(\n", + " 0,\n", + " evs_auto[pidx],\n", + " stds_auto[pidx],\n", + " fmt=\"o\",\n", + " label=\"PEA (automatic)\",\n", + " )\n", + "\n", + " # Plot extrapolators\n", + " if (\n", + " extrapolator is not None\n", + " and extrapolated_noise_factors is not None\n", + " ):\n", + " for i, method in enumerate(extrapolator):\n", + " ax.errorbar(\n", + " extrapolated_noise_factors,\n", + " evs_extrap[pidx, i],\n", + " stds_extrap[pidx, i],\n", + " fmt=\"-\",\n", + " alpha=0.5,\n", + " label=f\"PEA ({method})\",\n", + " )\n", + "\n", + " # Plot raw\n", + " ax.errorbar(\n", + " noise_factors, evs_raw[pidx], stds_raw[pidx], fmt=\"d\", label=\"Raw\"\n", + " )\n", + "\n", + " ax.set_yticks([0, 0.5, 1, 1.5, 2])\n", + " ax.set_ylim(0, max(1, 1.1 * max(evs_auto)))\n", + "\n", + " ax.set_xticks([0, *noise_factors])\n", + " ax.set_title(f\"θ/π = {angles[pidx]/np.pi:.2f}\")\n", + " if pidx == 0:\n", + " ax.set_ylabel(r\"$\\langle Z_{\" + str(qubit) + r\"} \\rangle$\")\n", + " if pidx == num_params - 1:\n", + " ax.set_xlabel(\"Noise Factor\")\n", + " ax.legend()\n", + " plt.tight_layout()\n", + " if close:\n", + " plt.close(fig)\n", + " return fig" + ] + }, + { + "cell_type": "markdown", + "id": "431a5bd2-e6ed-471b-ad9e-c4edd27784a8", + "metadata": {}, + "source": [ + "## Small-scale simulator example\n", + "We will forgo this step since runtime error mitigation is not supported on simulators.\n", + "\n", + "## Large-scale hardware example" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "988ee237", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "#### Create a parameterized Ising model circuit\n", + "##### Establish a backend\n", + "First, choose a backend to run on. This demonstration runs on a 127-qubit backend, but you can modify this to any backend available to you." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a3debf65-06df-4277-933e-14b6f6170756", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ")\n", + "backend" + ] + }, + { + "cell_type": "markdown", + "id": "c13564d0", + "metadata": {}, + "source": [ + "##### Define entangling layer couplings\n", + "To implement the Trotterized Ising simulation, define three layers of two-qubit gate couplings for the device, to be repeated at each of the Trotter steps. These define the three twirled layers you need to learn the noise for to implement mitigation." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0211a3f8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Layer 0:\n", + "[(2, 3), (4, 5), (6, 7), (8, 9), (10, 11), (12, 13), (14, 15), (16, 23), (18, 31), (19, 35), (20, 21), (25, 37), (26, 27), (28, 29), (33, 39), (36, 41), (38, 49), (42, 43), (45, 46), (47, 57), (51, 52), (53, 54), (56, 63), (58, 71), (59, 75), (61, 62), (64, 65), (66, 67), (68, 69), (72, 73), (76, 81), (79, 93), (82, 83), (84, 85), (86, 87), (88, 89), (91, 98), (94, 95), (97, 107), (99, 115), (100, 101), (102, 103), (105, 117), (108, 109), (110, 111), (113, 114), (116, 121), (118, 129), (123, 136), (124, 125), (126, 127), (130, 131), (132, 133), (135, 139), (138, 151), (142, 143), (144, 145), (146, 147), (152, 153), (154, 155)]\n", + "\n", + "Layer 1:\n", + "[(0, 1), (3, 16), (5, 6), (7, 8), (11, 18), (13, 14), (17, 27), (21, 22), (23, 24), (25, 26), (29, 38), (30, 31), (32, 33), (34, 35), (39, 53), (41, 42), (43, 56), (44, 45), (47, 48), (49, 50), (51, 58), (54, 55), (57, 67), (60, 61), (62, 63), (65, 66), (69, 78), (70, 71), (73, 79), (74, 75), (77, 85), (80, 81), (83, 84), (87, 97), (89, 90), (91, 92), (93, 94), (96, 103), (101, 116), (104, 105), (106, 107), (109, 118), (111, 112), (113, 119), (114, 115), (117, 125), (121, 122), (123, 124), (127, 137), (128, 129), (131, 138), (133, 134), (136, 143), (139, 155), (140, 141), (145, 146), (147, 148), (149, 150), (151, 152)]\n", + "\n", + "Layer 2:\n", + "[(1, 2), (3, 4), (7, 17), (9, 10), (11, 12), (15, 19), (21, 36), (22, 23), (24, 25), (27, 28), (29, 30), (31, 32), (33, 34), (37, 45), (40, 41), (43, 44), (46, 47), (48, 49), (50, 51), (52, 53), (55, 59), (61, 76), (63, 64), (65, 77), (67, 68), (69, 70), (71, 72), (73, 74), (78, 89), (81, 82), (83, 96), (85, 86), (87, 88), (90, 91), (92, 93), (95, 99), (98, 111), (101, 102), (103, 104), (105, 106), (107, 108), (109, 110), (112, 113), (119, 133), (120, 121), (122, 123), (125, 126), (127, 128), (129, 130), (131, 132), (134, 135), (137, 147), (141, 142), (143, 144), (148, 149), (150, 151), (153, 154)]\n", + "\n" + ] + } + ], + "source": [ + "layer_couplings = construct_layer_couplings(backend)\n", + "for i, layer in enumerate(layer_couplings):\n", + " print(f\"Layer {i}:\\n{layer}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "d320e933", + "metadata": {}, + "source": [ + "##### Remove bad qubits\n", + "Look at the coupling map for the backend and see if any qubits connect to couplings with high error. Remove these \"bad\" qubits from your experiment." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "fccef708", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Plot gate error map\n", + "# NOTE: These can change over time, so your results may look different\n", + "plot_error_map(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5973c90b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Physical qubits:\n", + " [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155]\n" + ] + } + ], + "source": [ + "bad_qubits = {\n", + " 32,\n", + " 33,\n", + " 71,\n", + " 72,\n", + " 73,\n", + " 102,\n", + " 103,\n", + "} # qubits removed based on high coupling error (1.00)\n", + "good_qubits = list(set(range(backend.num_qubits)).difference(bad_qubits))\n", + "print(\"Physical qubits:\\n\", good_qubits)" + ] + }, + { + "cell_type": "markdown", + "id": "180c4cb5", + "metadata": {}, + "source": [ + "##### Main Trotter circuit generation" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f814ca82", + "metadata": {}, + "outputs": [], + "source": [ + "num_steps = 6\n", + "theta = Parameter(\"theta\")\n", + "circuit = trotter_circuit(\n", + " theta, layer_couplings, num_steps, qubits=good_qubits, backend=backend\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7b86b867", + "metadata": {}, + "source": [ + "#### Create a list of parameter values to be assigned later" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5da6e991", + "metadata": {}, + "outputs": [], + "source": [ + "num_params = 12\n", + "\n", + "# 12 parameter values for Rx between [0, pi/2].\n", + "# Reshape to outer product broadcast with observables\n", + "parameter_values = np.linspace(0, np.pi / 2, num_params).reshape(\n", + " (num_params, 1)\n", + ")\n", + "num_params = parameter_values.size" + ] + }, + { + "cell_type": "markdown", + "id": "ac6f36e3", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "#### ISA circuit\n", + "Before running the circuit on hardware, optimize it for hardware execution. This process involves a few steps:\n", + "\n", + "* Pick a qubit layout that maps the virtual qubits of your circuit to physical qubits on the hardware.\n", + "* Insert swap gates as needed to route interactions between qubits that are not connected.\n", + "* Translate the gates in our circuit to [Instruction Set Architecture (ISA)](/docs/guides/transpile#instruction-set-architecture) instructions that can directly be executed on the hardware.\n", + "* Perform circuit optimizations to minimize the circuit depth and gate count.\n", + "\n", + "Although the transpiler built into Qiskit can perform all of these steps, this tutorial demonstrates building the utility-scale Trotter circuit in a ground-up fashion. Select the good physical qubits and define entangling layers on connected qubit pairs from those selected qubits. Nonetheless, you still need to translate non-ISA gates in the circuit and avail any circuit optimization offered by the transpiler.\n", + "\n", + "Transpile your circuit for the chosen backend by creating a pass manager and then running the pass manager on the circuit. Also, fix the initial layout of the circuit to the already selected `good_qubits`. An easy way to create a pass manager is to use the [`generate_preset_pass_manager`](/docs/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) function. See [Transpile with pass managers](/docs/guides/transpile-with-pass-managers) for a more detailed explanation of transpiling with pass managers." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1834cb22", + "metadata": {}, + "outputs": [], + "source": [ + "pm = generate_preset_pass_manager(\n", + " backend=backend,\n", + " initial_layout=good_qubits,\n", + " layout_method=\"trivial\",\n", + " optimization_level=1,\n", + ")\n", + "\n", + "isa_circuit = pm.run(circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "d395c8cf", + "metadata": {}, + "source": [ + "#### ISA observables\n", + "Next, create all weight-1 $\\langle Z \\rangle$ observables for each virtual qubit by padding the necessary number of $\\langle I \\rangle$ terms." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cc5ab1ed", + "metadata": {}, + "outputs": [], + "source": [ + "observables = []\n", + "num_qubits = len(good_qubits)\n", + "for q in range(num_qubits):\n", + " observables.append(\n", + " SparsePauliOp(\"I\" * (num_qubits - q - 1) + \"Z\" + \"I\" * q)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "030db4ed", + "metadata": {}, + "source": [ + "The transpilation process has mapped the virtual qubits of your circuit to physical qubits on the hardware. The information about the qubit layout is stored in the `layout` attribute of the transpiled circuit. Your observable is also defined in terms of the virtual qubits, so you need to apply this layout to the observable. This is done using the `apply_layout` method of `SparsePauliOp`.\n", + "\n", + "Notice that each observable is wrapped in a list in the following code block. It is done to *broadcast* with parameter values so that each qubit observable is measured for each theta value. Find the broadcasting rules for primitives in the [primitives documentation](/docs/guides/primitives)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "95fd2908", + "metadata": {}, + "outputs": [], + "source": [ + "isa_observables = [\n", + " [obs.apply_layout(layout=isa_circuit.layout)] for obs in observables\n", + "]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b4d480b3", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b22a1b00", + "metadata": {}, + "outputs": [], + "source": [ + "pub = (isa_circuit, isa_observables, parameter_values)" + ] + }, + { + "cell_type": "markdown", + "id": "4ace7773", + "metadata": {}, + "source": [ + "#### Configure Estimator options\n", + "Next configure the `Estimator` options needed to run the mitigation experiment. This includes options for the noise learning of the entangling layers, and for ZNE extrapolation.\n", + "\n", + "We use the following configuration:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ad4a4f1c", + "metadata": {}, + "outputs": [], + "source": [ + "# Experiment options\n", + "num_randomizations = 700\n", + "num_randomizations_learning = 40\n", + "max_batch_circuits = 3 * num_params\n", + "shots_per_randomization = 64\n", + "learning_pair_depths = [0, 1, 2, 4, 6, 12, 24]\n", + "noise_factors = [1, 1.3, 1.6]\n", + "extrapolated_noise_factors = np.linspace(0, max(noise_factors), 20)\n", + "\n", + "# Base option formatting\n", + "options = {\n", + " # Builtin resilience settings for ZNE\n", + " \"resilience\": {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " # TREX noise learning configuration\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": num_randomizations_learning,\n", + " \"shots_per_randomization\": 1024,\n", + " },\n", + " # PEA noise model configuration\n", + " \"layer_noise_learning\": {\n", + " \"max_layers_to_learn\": 3,\n", + " \"layer_pair_depths\": learning_pair_depths,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"num_randomizations\": num_randomizations_learning,\n", + " },\n", + " \"zne\": {\n", + " \"amplifier\": \"pea\",\n", + " \"noise_factors\": noise_factors,\n", + " \"extrapolator\": (\"exponential\", \"linear\"),\n", + " \"extrapolated_noise_factors\": extrapolated_noise_factors.tolist(),\n", + " },\n", + " },\n", + " # Randomization configuration\n", + " \"twirling\": {\n", + " \"num_randomizations\": num_randomizations,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"strategy\": \"active-circuit\",\n", + " },\n", + " # Optional Dynamical Decoupling (DD)\n", + " \"dynamical_decoupling\": {\"enable\": True, \"sequence_type\": \"XY4\"},\n", + " # Job tag\n", + " \"environment\": {\"job_tags\": [\"TUT_PEA\"]},\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "3f9fd4c4", + "metadata": {}, + "source": [ + "##### Explanation of ZNE options\n", + "The following gives details on the additional options in the experimental branch. Note that these options and names are not finalized, and everything here is subject to change before an official release.\n", + "\n", + "* **amplifier**: The method to use when amplifying noise to the intended noise factors.\n", + " Allowed values are `\"gate_folding\"`, which amplifies by repeating two-qubit basis gates,\n", + " and `\"pea\"`, which amplifies by probabilistic sampling after learning the Pauli-twirled\n", + " noise model for layers of twirled two-qubit basis gates. Additional options are `\"gate_folding_front\"` and `\"gate_folding_back\"`, which are explained in the [API documentation](/docs/api/qiskit-ibm-runtime/options-zne-options#amplifier).\n", + "* **extrapolated\\_noise\\_factors**: Specify one or more noise factor values at which to evaluate the\n", + " extrapolated models. If a sequence of values, the returned results will be array-valued with specified noise factor evaluated for the extrapolation model. A value\n", + " of 0 corresponds to zero-noise extrapolation.\n", + "\n", + "#### Run the experiment" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3cf72c8c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID d7fa8oe2cugc739qbb10\n" + ] + } + ], + "source": [ + "estimator = Estimator(mode=backend, options=options)\n", + "job = estimator.run([pub])\n", + "print(f\"Job ID {job.job_id()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1eea9c17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "markdown", + "id": "50b94af2", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Once the experiment is finished, you can view your results. You fetch the raw and mitigated expectation values and compare them with exact results. Then, plot the expectation values, both mitigated (extrapolated) and raw, averaged over all qubits for each parameter. Finally, plot expectation values for your choice of individual qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "31dc35ea-6554-4ca7-9c3b-0b5394c46e4e", + "metadata": {}, + "outputs": [], + "source": [ + "primitive_result = job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "fbf7ec8d", + "metadata": {}, + "source": [ + "#### General result shapes and metadata\n", + "The `PrimitiveResult` object contains a list-like structure named `PubResult`. As we submit only one PUB to the estimator, the `PrimitiveResult` contains a single `PubResult` object.\n", + "\n", + "The PUB (primitive unified bloc) result expectation values and standard errors are array-valued. For estimator jobs with ZNE, there are several data fields of expectation values and standard errors available in the `PubResult`'s `DataBin` container. We will briefly discuss the data fields for expectation values here (similar data fields are available for standard errors (`stds`) as well).\n", + "\n", + "1. `pub_result.data.evs`: Expectation values corresponding to the zero noise (based on heuristically best extrapolation).\n", + " * The first axis is the virtual qubit index for observable $\\langle Z_i\\rangle$ ($124$ virtual-qubits/observables)\n", + " * The second axis indexes the parameter value for $\\theta$ ($12$ parameter values)\n", + "2. `pub_result.data.evs_extrapolated`: Expectation values for extrapolated noise factors for every extrapolator. This array has two additional axes.\n", + " * The third axis indexes the extrapolation methods ($2$ extrapolators, `exponential` and `linear`)\n", + " * The last axis indexes the `extrapolated_noise_factors` ($20$ extrapolation points specified in the option)\n", + "3. `pub_result.data.evs_noise_factors`: Raw expectation values for each noise factor.\n", + " * The third axis indexes the raw `noise_factors` ($3$ factors)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "e3aa4fc9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pub_result.data.evs.shape=(149, 12)\n", + "pub_result.data.evs_extrapolated.shape=(149, 12, 2, 20)\n", + "pub_result.data.evs_noise_factors.shape=(149, 12, 3)\n", + "\n" + ] + } + ], + "source": [ + "pub_result = primitive_result[0]\n", + "\n", + "print(\n", + " f\"{pub_result.data.evs.shape=}\\n\"\n", + " f\"{pub_result.data.evs_extrapolated.shape=}\\n\"\n", + " f\"{pub_result.data.evs_noise_factors.shape=}\\n\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4c5cc6ee", + "metadata": {}, + "source": [ + "Several metadata fields are also available in the `PrimitiveResult`. The metadata includes\n", + "\n", + "* `resilience/zne/noise_factors`: The raw noise factors\n", + "* `resilience/zne/extrapolator`: The extrapolators used for each result" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1c77d83a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'dynamical_decoupling': {'enable': True,\n", + " 'sequence_type': 'XY4',\n", + " 'extra_slack_distribution': 'middle',\n", + " 'scheduling_method': 'alap'},\n", + " 'twirling': {'enable_gates': True,\n", + " 'enable_measure': True,\n", + " 'num_randomizations': 700,\n", + " 'shots_per_randomization': 64,\n", + " 'interleave_randomizations': True,\n", + " 'strategy': 'active-circuit'},\n", + " 'resilience': {'measure_mitigation': True,\n", + " 'zne_mitigation': True,\n", + " 'pec_mitigation': False,\n", + " 'zne': {'noise_factors': [1.0, 1.3, 1.6],\n", + " 'extrapolator': ['exponential', 'linear'],\n", + " 'extrapolated_noise_factors': [0.0,\n", + " 0.08421052631578947,\n", + " 0.16842105263157894,\n", + " 0.25263157894736843,\n", + " 0.3368421052631579,\n", + " 0.42105263157894735,\n", + " 0.5052631578947369,\n", + " 0.5894736842105263,\n", + " 0.6736842105263158,\n", + " 0.7578947368421053,\n", + " 0.8421052631578947,\n", + " 0.9263157894736842,\n", + " 1.0105263157894737,\n", + " 1.0947368421052632,\n", + " 1.1789473684210525,\n", + " 1.263157894736842,\n", + " 1.3473684210526315,\n", + " 1.431578947368421,\n", + " 1.5157894736842106,\n", + " 1.6]},\n", + " 'layer_noise_model': [LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00155, 0.00144, 0.00637, 0.00023, 0.0, 0.0, 0.00018, 0.00035, 0.0, 0.00014, 5e-05, 0.00041, 0.0, 0.0, 0.0, 0.0001, 0.0001, 0.0, 9e-05, 6e-05, 0.0, 7e-05, 0.0001, 0.00013, 0.00018, 1e-05, 5e-05, 7e-05, 6e-05, 6e-05, 0.00029, 0.00016, 6e-05, 6e-05, 0.00046, 0.00073, 0.00031, 0.00025, 0.00018, 0.00022, 0.0, 8e-05, 0.00012, 0.00015, 0.00012, 0.0, 0.0, 0.00023, 5e-05, 5e-05, 7e-05, 0.00064, 4e-05, 2e-05, 0.00072, 0.00037, 2e-05, 4e-05, 0.00077, 0.0003, 0.00042, 0.00027, 0.00016, 0.0, 8e-05, 5e-05, 0.00019, 0.0, 0.0, 0.00021, 0.00014, 0.00061, 0.0, 0.00016, 3e-05, 0.00053, 0.00013, 0.0, 0.00068, 0.00011, 0.0, 0.00013, 0.00078, 0.01885, 0.00032, 0.00034, 0.00035, 0.00052, 3e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00123, 0.0, 0.0, 0.0, 0.00034, 0.00011, 0.0001, 0.00076, 0.00041, 0.0001, 0.00011, 0.00082, 0.0, 0.00066, 0.0, 0.00055, 7e-05, 0.00018, 0.00011, 0.00024, 3e-05, 0.00015, 0.00014, 0.0, 0.00076, 9e-05, 0.00016, 8e-05, 0.00132, 0.0, 0.00019, 0.00215, 0.00109, 0.00019, 0.0, 0.00201, 0.00021, 0.0006, 0.00032, 0.00046, 0.00027, 0.0, 8e-05, 0.0001, 0.00027, 0.0, 0.00015, 0.00018, 0.0, 0.00026, 0.00024, 5e-05, 0.00031, 0.0, 0.00034, 0.00039, 9e-05, 0.00034, 0.0, 0.00078, 0.00794, 0.00045, 0.00061, 0.00066, 0.0, 0.0, 0.00032, 6e-05, 5e-05, 7e-05, 0.0, 0.0001, 0.00036, 0.0, 0.00037, 0.00013, 0.00016, 3e-05, 8e-05, 0.00067, 0.00024, 8e-05, 3e-05, 0.00074, 0.00224, 0.00029, 0.00026, 0.00031, 0.00076, 5e-05, 0.0, 2e-05, 0.00072, 0.0, 1e-05, 0.00011, 0.00027, 0.0, 0.00017, 0.0, 0.0, 0.00012, 0.0, 0.0, 0.0, 0.0, 0.00067, 0.00063, 0.0, 0.0, 0.0, 0.00102, 0.0, 0.00011, 0.00026, 4e-05, 1e-05, 0.0002, 0.0, 0.00011, 0.0, 0.00021, 0.00015, 0.0005, 0.00011, 0.00013, 0.0, 0.0002, 0.00016, 0.00015, 8e-05, 2e-05, 7e-05, 0.00023, 0.00042, 0.0, 0.00049, 0.00056, 0.00372, 0.00017, 0.00012, 0.0, 0.00026, 0.00021, 0.0, 0.00012, 0.00046, 0.00305, 0.0005, 0.00057, 9e-05, 0.0009, 0.0, 7e-05, 0.00011, 0.00084, 0.0, 0.0, 0.0001, 0.00067, 0.0, 0.0, 0.0, 4e-05, 0.0, 1e-05, 0.00053, 0.0, 9e-05, 0.00021, 0.0, 1e-05, 0.0, 8e-05, 0.0, 0.0, 0.0, 9e-05, 0.00083, 0.00084, 0.00038, 9e-05, 3e-05, 0.00039, 0.02059, 0.0, 0.0, 0.0, 0.01787, 0.00012, 0.00024, 0.0, 0.00401, 0.0, 0.0, 0.0, 4e-05, 0.0, 0.0, 0.00018, 0.0, 0.00031, 0.00018, 0.0, 0.0, 0.0, 0.0, 0.00013, 0.0, 0.00027, 1e-05, 0.0, 0.00021, 0.0, 0.0, 0.00029, 0.00159, 0.0, 0.0, 0.00052, 0.0079, 0.0, 0.0002, 0.00147, 0.00048, 4e-05, 0.00976, 0.00957, 0.0011, 0.0, 0.0, 4e-05, 0.00048, 0.01068, 0.00487, 0.00225, 0.0, 0.0, 0.00026, 0.00052, 0.00033, 0.0, 0.00019, 0.0, 0.0, 0.00038, 0.0, 0.0, 0.0, 0.00154, 0.0, 0.0, 0.0, 0.00046, 0.0, 9e-05, 0.00077, 0.0002, 9e-05, 0.0, 0.00077, 0.00061, 6e-05, 0.00045, 0.00081, 0.00016, 0.0, 0.0, 0.0001, 0.00064, 4e-05, 0.0002, 0.0, 0.00056, 7e-05, 0.0, 0.0, 0.00066, 5e-05, 0.00025, 0.00077, 0.00011, 0.0, 0.0, 0.00065, 0.00025, 5e-05, 0.00082, 6e-05, 0.0, 0.00011, 0.00354, 0.00027, 0.00039, 0.00046, 0.00014, 0.0, 0.00013, 0.00067, 0.00064, 0.0006, 0.00053, 2e-05, 0.00016, 0.00067, 0.0, 0.00013, 0.0, 0.00047, 0.00016, 2e-05, 0.00067, 4e-05, 0.0, 0.00015, 0.00028, 0.00044, 0.00041, 0.00014, 0.00011, 0.0, 0.0, 5e-05, 0.0, 0.00017, 0.00022, 9e-05, 6e-05, 0.0, 0.00021, 0.0007, 3e-05, 0.0, 0.0, 0.0002, 0.00012, 3e-05, 0.0002, 0.0001, 3e-05, 0.00012, 0.00026, 0.00033, 0.00053, 0.00037, 0.00039, 9e-05, 6e-05, 7e-05, 0.00012, 0.00012, 0.0, 0.00022, 0.0, 0.00034, 0.00014, 8e-05, 0.0001, 0.00179, 0.00186, 0.00096, 0.00028, 0.00051, 0.00033, 0.0, 0.0, 0.00015, 0.0004, 0.0, 8e-05, 0.00015, 2e-05, 0.00015, 0.0, 0.00045, 0.0002, 0.0, 0.0, 0.00063, 0.00044, 0.00036, 0.00064, 0.0003, 2e-05, 0.0, 0.00124, 0.0, 0.0, 0.0, 0.00169, 0.00032, 0.00018, 0.0, 0.00147, 0.0, 0.0, 0.00037, 0.00095, 0.0, 0.00051, 0.00182, 0.00088, 0.00051, 0.0, 0.00116, 0.00093, 0.00124, 0.00219, 0.00052, 0.00072, 4e-05, 0.0, 0.0, 4e-05, 0.0, 0.00025, 0.00013, 0.0001, 0.00031, 0.0, 0.00027, 0.00022, 0.0, 0.00016, 0.0, 1e-05, 0.0001, 0.0, 3e-05, 0.0, 0.0, 2e-05, 6e-05, 0.0, 0.00021, 0.00251, 0.0, 0.0, 7e-05, 0.0, 0.0, 0.0, 0.00047, 5e-05, 2e-05, 0.00062, 0.00038, 2e-05, 5e-05, 0.00055, 0.00125, 0.00049, 0.00033, 0.00031, 0.00015, 0.0, 0.00015, 7e-05, 0.00047, 0.0, 1e-05, 3e-05, 1e-05, 0.00014, 0.0, 0.00026, 0.00092, 0.0, 0.0, 0.0, 0.00048, 0.00011, 4e-05, 0.0, 0.00077, 0.00013, 0.00014, 0.00031, 0.00048, 0.0, 0.0001, 0.00066, 6e-05, 2e-05, 0.0, 0.00029, 0.0001, 0.0, 0.00065, 0.0, 0.00013, 3e-05, 0.0, 0.00033, 0.00034, 0.00019, 2e-05, 0.0, 0.00015, 0.00046, 0.0, 2e-05, 1e-05, 0.00046, 8e-05, 6e-05, 0.0, 0.00035, 1e-05, 0.0001, 0.0, 1e-05, 0.0, 0.00012, 8e-05, 7e-05, 5e-05, 0.0, 0.00013, 0.0, 0.0, 0.0, 0.0, 0.00022, 0.0, 0.00013, 0.00028, 0.00014, 0.00013, 0.0, 0.00042, 0.00055, 0.00054, 0.00036, 5e-05, 0.0002, 0.0, 0.0, 0.00014, 1e-05, 0.00019, 2e-05, 6e-05, 0.00026, 0.0001, 0.0, 5e-05, 8e-05, 0.0, 0.00073, 7e-05, 0.0, 0.0, 1e-05, 0.0, 0.0, 6e-05, 4e-05, 0.00018, 0.00046, 0.00016, 0.00018, 4e-05, 0.00053, 0.0002, 0.00057, 0.00055, 0.00042, 0.00077, 6e-05, 0.00025, 5e-05, 0.00062, 0.00026, 0.00012, 4e-05, 0.00033, 8e-05, 0.0, 0.0004, 0.00036, 0.00016, 0.0, 0.0, 4e-05, 0.0, 4e-05, 0.0002, 4e-05, 0.00036, 0.0, 4e-05, 0.00024, 0.0, 0.0002, 0.00044, 0.00017, 0.0002, 0.0, 0.00051, 0.00059, 0.00061, 0.00069, 0.00064, 0.0006, 0.0, 7e-05, 4e-05, 0.00085, 0.0, 4e-05, 0.0, 0.00031, 0.00033, 0.0, 0.0001, 0.00037, 3e-05, 0.0, 0.0, 0.00018, 0.0, 0.00015, 4e-05, 0.00044, 9e-05, 2e-05, 2e-05, 0.00067, 0.00048, 6e-05, 0.0, 0.0, 0.0, 0.00028, 0.0, 1e-05, 0.0, 0.0, 0.00112, 0.0, 0.0, 0.00018, 0.00016, 0.0, 0.00018, 0.00055, 9e-05, 0.00018, 0.0, 0.00028, 0.00254, 0.00064, 0.00025, 0.00045, 0.00072, 7e-05, 6e-05, 0.00114, 0.00026, 0.00013, 0.0, 0.00081, 6e-05, 7e-05, 0.00139, 0.00014, 0.0, 0.00026, 0.00097, 0.00053, 0.00029, 0.00044, 0.0, 6e-05, 0.0, 0.00011, 3e-05, 0.0, 0.0002, 0.00024, 0.0, 5e-05, 5e-05, 5e-05, 0.0, 0.00014, 0.00025, 0.00032, 0.00011, 5e-05, 0.00067, 4e-05, 5e-05, 0.00011, 0.00061, 0.00015, 0.00035, 0.00035, 0.0003, 0.0006, 0.0, 0.00017, 0.0001, 0.0003, 0.00012, 8e-05, 0.00015, 7e-05, 0.0001, 5e-05, 0.00057, 0.0003, 9e-05, 0.00023, 0.0, 0.0001, 0.00015, 0.00073, 0.0, 0.0, 0.00012, 0.00041, 0.00015, 0.0001, 0.00079, 0.0003, 0.00011, 0.0, 0.00042, 0.00088, 0.00066, 0.00062, 0.00051, 0.0, 0.0, 0.00013, 0.00028, 8e-05, 0.00022, 0.0, 0.00044, 0.0, 0.00013, 0.0, 0.0, 0.0002, 0.00014, 0.00062, 0.00022, 0.00014, 0.0002, 0.0005, 4e-05, 0.00064, 0.00058, 0.00046, 0.00055, 0.0, 8e-05, 0.00012, 0.00067, 0.0, 0.0, 0.00014, 0.00095, 0.00025, 0.0, 0.00016, 0.00058, 0.00041, 0.00052, 0.00022, 6e-05, 0.0, 0.00034, 0.00011, 0.0, 0.0, 0.00015, 0.0, 6e-05, 0.00034, 0.0, 0.00016, 4e-05, 0.00126, 0.00041, 0.00037, 0.00015, 0.0, 0.0, 0.0, 0.00011, 0.0, 0.00024, 5e-05, 0.00029, 1e-05, 2e-05, 0.0, 0.00033, 0.00036, 4e-05, 0.00024, 0.001, 0.0, 0.0, 0.0, 0.00046, 0.0, 0.00028, 2e-05, 0.0009, 0.00012, 0.0, 0.00032, 0.00428, 0.00026, 9e-05, 0.0, 0.00372, 0.0, 9e-05, 0.0, 0.00107, 0.00018, 0.0, 0.00047, 0.00025, 0.00031, 0.00024, 0.00068, 0.00063, 0.00052, 4e-05, 0.00011, 0.00011, 0.00044, 7e-05, 4e-05, 4e-05, 5e-05, 0.00011, 0.00011, 0.00034, 0.0, 0.00017, 0.0, 0.00051, 0.00041, 0.00032, 0.00022, 0.0, 0.0, 9e-05, 6e-05, 7e-05, 0.00011, 2e-05, 0.00052, 0.0, 0.0, 0.0, 0.00731, 0.00017, 0.0, 0.0, 0.00026, 0.0, 0.00031, 0.0005, 0.0, 0.00031, 0.0, 0.00063, 0.0, 0.00026, 0.00052, 0.0, 0.0, 4e-05, 0.0, 0.00024, 7e-05, 9e-05, 6e-05, 3e-05, 0.0, 0.0, 0.00025, 0.00029, 0.00025, 0.00012, 4e-05, 5e-05, 0.00014, 4e-05, 0.00091, 9e-05, 0.0, 7e-05, 0.00019, 4e-05, 0.00014, 0.00085, 0.00037, 6e-05, 4e-05, 0.0001, 0.00025, 0.00026, 0.00013, 0.00026, 0.00014, 0.0, 2e-05, 0.00023, 0.0, 0.00021, 0.0, 0.0, 0.00031, 0.00031, 0.0001, 0.00013, 6e-05, 0.00013, 0.00071, 0.00048, 0.00013, 6e-05, 0.00076, 0.00018, 0.00042, 0.00044, 0.00018, 0.00014, 0.0, 0.00013, 9e-05, 0.0003, 0.0, 0.0, 1e-05, 0.0, 0.00019, 0.0, 7e-05, 1e-05, 9e-05, 0.0, 0.00011, 0.0, 7e-05, 0.00041, 0.0, 0.0, 0.00032, 0.0, 7e-05, 0.0, 0.00034, 0.0014, 0.0, 0.0002, 6e-05, 0.00036, 0.00031, 0.00039, 0.00042, 7e-05, 0.0, 0.0, 0.00014, 0.00011, 0.0, 2e-05, 0.00024, 0.0, 9e-05, 0.00036, 0.00023, 0.00012, 0.00011, 0.0, 0.00052, 5e-05, 0.0, 4e-05, 0.00033, 1e-05, 0.0, 9e-05, 0.00064, 0.0, 7e-05, 0.0, 0.00044, 0.00016, 0.0, 0.0, 0.00029, 0.0, 0.0, 0.00012, 0.00021, 0.0, 0.00017, 0.00068, 7e-05, 0.0, 0.00014, 0.00027, 0.00017, 0.0, 0.0006, 9e-05, 1e-05, 0.0, 0.00064, 0.00025, 0.00031, 0.00019, 0.0, 0.0, 0.00013, 0.00056, 0.0, 0.00017, 0.0, 0.00053, 7e-05, 0.0, 6e-05, 0.00029, 0.00018, 6e-05, 3e-05, 0.00027, 0.0, 6e-05, 0.00058, 0.00044, 6e-05, 0.0, 0.00052, 0.0004, 0.00073, 0.00066, 3e-05, 0.0004, 9e-05, 0.0, 0.00021, 0.00048, 0.0, 0.00016, 0.0, 0.00257, 0.0, 0.00021, 0.00024, 0.00012, 0.0, 0.00015, 8e-05, 0.00025, 0.00012, 0.0, 0.0, 0.00025, 0.00028, 0.0, 0.00014, 0.0, 7e-05, 0.00017, 0.00029, 0.0, 0.00017, 7e-05, 0.00024, 0.0, 0.00061, 0.00068, 0.0, 0.00018, 0.0, 7e-05, 1e-05, 0.0, 0.00017, 0.0, 0.0, 0.0003, 0.00013, 1e-05, 0.00024, 0.00098, 0.00071, 0.00142, 9e-05, 0.00011, 0.0, 0.00056, 0.00042, 0.0, 0.00011, 0.00064, 0.00085, 0.00098, 0.00071, 0.00018, 0.00085, 0.00081, 0.00016, 0.0, 0.0, 0.0, 7e-05, 0.0, 0.0, 0.0, 0.0, 0.00036, 0.00012, 0.0, 0.0, 0.00048, 0.00021, 0.00031, 6e-05, 0.00059, 0.00041, 0.00028, 7e-05, 0.00026, 0.0004, 0.00036, 0.00016, 0.00014, 9e-05, 6e-05, 0.00043, 0.0, 8e-05, 7e-05, 0.00036, 6e-05, 9e-05, 0.00055, 6e-05, 0.0, 3e-05, 0.00032, 0.00036, 0.00036, 0.00017, 0.0, 0.0, 1e-05, 0.00038, 0.0, 8e-05, 5e-05, 0.00026, 0.00014, 3e-05, 5e-05, 0.0, 0.0, 0.0, 0.00017, 0.00027, 0.0, 0.00019, 0.00063, 4e-05, 0.00019, 0.0, 0.00077, 0.00116, 0.00051, 0.00048, 0.00036, 8e-05, 0.0, 0.00011, 0.0001, 0.00013, 7e-05, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00026, 0.00014, 0.0003, 0.00011, 5e-05, 6e-05, 0.00017, 0.0007, 0.0, 0.0, 0.00011, 0.00063, 0.00017, 6e-05, 0.00079, 0.0, 0.0, 5e-05, 9e-05, 0.00029, 0.00021, 0.00048, 0.00072, 0.0, 0.0, 0.0, 0.00034, 9e-05, 4e-05, 0.0, 0.00013, 0.0, 5e-05, 0.00037, 0.0, 0.00011, 0.0, 0.00034, 0.0, 0.0, 7e-05, 0.0, 0.00605, 0.0, 0.00011, 0.00012, 0.00012, 0.00023, 0.0, 0.00026, 0.00016, 0.0, 0.00023, 0.00031, 0.00078, 0.0006, 0.00026, 0.00055, 0.00043, 0.00012, 0.0001, 0.00052, 8e-05, 0.0, 0.0, 0.00033, 0.0001, 0.00012, 0.00051, 5e-05, 0.00012, 0.0, 0.00105, 0.00028, 0.00018, 0.00023, 0.0, 2e-05, 0.0, 0.0, 0.00019, 0.0, 0.00015, 0.00013, 0.00018, 2e-05, 0.0, 7e-05, 0.0001, 0.0002, 0.00014, 0.00029, 0.0, 8e-05, 0.0005, 0.0002, 8e-05, 0.0, 0.00046, 0.0017, 0.00108, 0.00089, 0.00035, 0.0, 0.00016, 1e-05, 9e-05, 0.00024, 0.0, 1e-05, 8e-05, 0.00024, 0.00013, 0.00032, 8e-05, 0.00127, 4e-05, 0.0, 0.0, 0.00095, 0.0, 0.00017, 0.0, 0.00052, 0.00017, 2e-05, 0.00029, 0.00036, 0.00049, 0.00056, 2e-05, 0.00026, 3e-05, 0.00048, 0.0, 3e-05, 0.00014, 0.00024, 3e-05, 0.00026, 0.0006, 2e-05, 0.00015, 5e-05, 0.0, 0.00025, 0.00038, 0.00034, 4e-05, 0.0, 0.00029, 0.00044, 0.00024, 0.0, 0.0, 0.00046, 5e-05, 0.0001, 0.0, 0.00048, 0.0, 4e-05, 0.00028, 0.0, 0.00026, 0.0, 3e-05, 1e-05, 0.0, 0.0, 0.00027, 0.00034, 0.0, 0.00016, 9e-05, 0.00013, 0.00019, 0.0, 0.0, 0.00014, 0.0, 0.0001, 3e-05, 0.00031, 5e-05, 0.00026, 0.00022, 0.0001, 0.00022, 0.0, 5e-05, 0.00012, 0.0, 0.00056, 0.0, 0.0, 0.00023, 0.0, 0.0, 0.00012, 0.00064, 0.00059, 0.0, 2e-05, 0.0, 0.00033, 0.00028, 0.00017, 0.00025, 3e-05, 1e-05, 6e-05, 0.00011, 0.0, 8e-05, 6e-05, 3e-05, 0.00016, 0.00034, 0.0, 0.00011, 0.00015, 0.0, 0.00044, 0.00028, 0.0, 0.00015, 0.00062, 0.00203, 0.00035, 0.00025, 0.00049, 0.00037, 0.0001, 2e-05, 0.0, 0.0003, 7e-05, 8e-05, 0.0, 0.00074, 9e-05, 0.0, 9e-05, 0.00016, 3e-05, 0.00013, 0.00079, 6e-05, 6e-05, 1e-05, 0.0, 0.00013, 3e-05, 0.00076, 0.0, 0.00017, 5e-05, 0.00031, 0.00025, 0.00035, 0.00023, 0.0, 2e-05, 0.0002, 0.00015, 9e-05, 1e-05, 0.00017, 0.0001, 0.00011, 6e-05, 1e-05, 0.00041, 0.0003, 0.00048, 0.0, 0.00017, 4e-05, 0.00025, 0.00063, 0.00018, 0.00025, 4e-05, 0.00065, 0.0019, 0.00043, 0.00028, 0.00033, 0.0, 1e-05, 0.00012, 0.0001, 0.00019, 3e-05, 0.0, 5e-05, 0.00038, 0.00012, 0.0, 0.0, 0.00025, 6e-05, 9e-05, 0.0, 0.00017, 1e-05, 0.0006, 0.00019, 0.0001, 0.00013, 0.0, 1e-05, 0.00017, 0.00068, 0.0, 3e-05, 0.0, 0.00021, 0.00019, 0.00029, 0.00041, 0.00073, 0.00011, 0.0, 0.0, 0.00064, 0.0, 0.00026, 5e-05, 0.00044, 0.0001, 0.0, 0.0002, 0.00037, 6e-05, 0.0, 8e-05, 0.00026, 0.0, 0.00019, 8e-05, 0.00017, 0.0, 0.0, 0.00021, 0.00023, 0.00016, 1e-05, 0.00037, 0.00041, 1e-05, 0.00016, 0.00044, 0.00046, 0.00054, 0.00065, 0.00033, 0.00033, 8e-05, 0.0, 8e-05, 0.00046, 0.0, 0.0001, 0.0, 0.00023, 0.0, 0.00015, 3e-05, 2e-05, 2e-05, 0.00031, 0.00012, 0.00028, 1e-05, 4e-05, 4e-05, 0.00038, 0.00027, 0.0, 0.0, 0.00073, 0.0002, 7e-05, 0.00076, 0.00063, 7e-05, 0.0002, 0.00086, 4e-05, 0.00052, 0.00053, 0.00012, 0.00068, 0.00068, 0.00019, 0.00063, 0.0, 1e-05, 5e-05, 0.00058, 0.0, 0.0, 0.0001, 0.00059, 0.00011, 0.0, 0.0, 0.00024, 0.00012, 0.0, 0.0, 0.00036, 0.0, 2e-05, 1e-05, 0.00021, 0.0, 0.00012, 0.0, 0.00031, 9e-05, 0.0, 0.0, 0.0, 8e-05, 0.00054, 6e-05, 0.0, 0.0, 0.00026, 8e-05, 0.0, 0.00056, 0.00078, 5e-05, 2e-05, 4e-05, 0.00036, 0.0004, 0.00015, 8e-05, 5e-05, 0.00012, 6e-05, 0.00017, 5e-05, 1e-05, 0.0, 0.0, 5e-05, 0.00011, 7e-05, 0.00033, 5e-05, 7e-05, 0.00042, 0.00042, 7e-05, 5e-05, 0.00042, 0.00015, 0.00031, 0.00023, 1e-05, 0.00012, 0.0, 0.0, 0.00013, 0.00022, 2e-05, 0.0, 0.0, 0.00062, 7e-05, 0.0, 0.0, 0.00024, 0.0001, 0.0, 0.0, 1e-05, 6e-05, 0.00046, 0.0, 0.0, 3e-05, 0.00018, 6e-05, 1e-05, 0.00042, 0.00019, 5e-05, 3e-05, 0.0, 0.00026, 0.00024, 0.00016, 0.00029, 5e-05, 0.0, 9e-05, 0.00082, 0.0, 8e-05, 5e-05, 0.00037, 5e-05, 0.00016, 0.0, 0.00147, 0.00017, 5e-05, 0.0, 0.00051, 0.0, 0.0, 4e-05, 0.00646, 0.00045, 0.0, 0.0, 0.00097, 0.0001, 0.00017, 0.00029, 0.00072, 0.00015, 0.00018, 6e-05, 0.0038, 0.00059, 0.00069, 0.00314, 0.00027, 1e-05, 6e-05, 0.0006, 2e-05, 0.0, 0.0, 0.0, 6e-05, 1e-05, 0.00043, 0.0, 0.00027, 8e-05, 0.00024, 0.00048, 0.00037, 0.00034, 0.0, 0.0, 0.00021, 0.00046, 0.0, 0.0, 0.0, 0.00019, 5e-05, 0.00012, 0.0, 0.00017, 0.00025, 0.0, 0.0002, 0.00013, 9e-05, 6e-05, 0.00046, 0.00043, 6e-05, 9e-05, 0.00048, 0.00046, 0.00046, 0.00036, 7e-05, 0.00028, 1e-05, 5e-05, 0.0, 0.00025, 0.0, 0.0, 0.0001, 6e-05, 0.00032, 0.0, 0.0, 0.00036, 4e-05, 7e-05, 7e-05, 1e-05, 0.00012, 0.00053, 0.00044, 0.0, 0.00015, 0.00022, 0.00012, 1e-05, 0.00081, 0.00177, 0.0, 0.0, 0.00021, 0.00035, 0.00034, 0.00039]))),\n", + " LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00087, 0.00084, 0.00784, 0.0, 0.0, 0.00028, 0.00012, 0.0001, 0.00028, 0.0, 0.00029, 0.0096, 0.00087, 0.00084, 0.0, 0.00054, 0.0, 0.0, 0.0, 0.0, 0.00021, 0.0, 5e-05, 0.00034, 0.0, 0.00019, 0.0, 0.0, 0.00016, 0.0, 9e-05, 0.0, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0, 6e-05, 0.00017, 0.00011, 0.0, 0.0, 0.00012, 0.0, 0.00014, 0.0, 0.00062, 0.00011, 6e-05, 3e-05, 0.00167, 0.00017, 0.0, 0.0, 0.00174, 0.0, 0.00014, 0.0, 0.00211, 0.0, 0.0, 0.0, 0.00028, 0.00024, 0.00016, 0.0003, 0.0, 0.00016, 0.00024, 0.0001, 3e-05, 0.00184, 0.00188, 0.00039, 0.0, 0.0, 0.0, 0.0004, 0.00065, 0.0, 0.00011, 0.0, 0.005, 0.0, 5e-05, 9e-05, 0.00029, 0.00024, 0.0, 0.00044, 0.00022, 0.0, 0.00024, 0.00043, 0.00068, 0.00102, 0.00088, 0.0005, 0.00055, 0.00015, 0.0, 0.00013, 0.00062, 0.0, 0.0, 7e-05, 0.00038, 0.0, 0.0002, 1e-05, 0.00025, 0.0, 6e-05, 5e-05, 0.00062, 0.0, 0.0, 0.0, 0.00034, 6e-05, 0.0, 3e-05, 0.0, 0.0, 0.00012, 0.00042, 0.00072, 0.00012, 0.0, 3e-05, 0.0005, 7e-05, 0.0, 0.00012, 0.00038, 0.0, 1e-05, 0.0003, 0.00053, 0.00016, 0.0, 0.0, 0.00027, 0.00034, 0.0, 0.0, 0.00011, 0.00012, 7e-05, 7e-05, 0.00021, 0.0, 0.00014, 1e-05, 0.00141, 4e-05, 0.0, 0.00035, 5e-05, 0.00012, 1e-05, 0.00026, 0.0001, 1e-05, 0.00012, 0.00026, 0.00011, 0.00037, 0.00035, 0.00045, 0.00036, 0.0, 5e-05, 5e-05, 0.0005, 4e-05, 7e-05, 5e-05, 0.00014, 0.00017, 4e-05, 0.0001, 0.00014, 0.00015, 1e-05, 0.00027, 0.00023, 1e-05, 0.00015, 0.00035, 0.00086, 0.0005, 0.00032, 0.00036, 0.00082, 0.0, 0.00011, 0.0, 0.0, 0.00064, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4e-05, 0.00015, 0.00036, 1e-05, 0.00015, 4e-05, 0.00034, 0.00067, 0.001, 0.00089, 0.0009, 0.00042, 0.0, 1e-05, 8e-05, 0.00042, 7e-05, 0.0, 0.0, 0.00025, 9e-05, 0.0, 0.0, 0.0005, 0.00106, 0.00168, 0.00024, 0.0, 0.0, 0.0, 0.0, 5e-05, 7e-05, 0.00015, 0.00053, 0.0001, 0.0, 0.00012, 0.00035, 0.0, 0.0, 0.00061, 0.00064, 0.0, 0.0, 0.00071, 0.00061, 0.00049, 0.00049, 0.00091, 0.0, 0.0, 0.00012, 0.0, 7e-05, 7e-05, 1e-05, 0.00053, 0.0, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.0057, 0.00013, 0.0, 0.0, 0.00019, 0.0, 0.0, 0.00818, 0.0, 4e-05, 0.00844, 0.00635, 4e-05, 0.0, 0.00647, 0.00203, 0.00024, 0.00068, 0.00159, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00015, 0.0, 0.0, 0.0, 0.00011, 0.00012, 0.0, 0.00051, 0.00033, 0.00025, 0.00051, 5e-05, 0.00025, 0.00033, 0.00038, 0.0001, 0.00032, 0.0004, 0.0, 0.00967, 0.00039, 3e-05, 0.00967, 0.0, 0.0, 0.0, 0.01187, 3e-05, 0.00039, 0.01275, 0.0, 0.0, 0.00042, 0.00994, 0.0012, 0.0002, 0.00248, 0.0, 0.00033, 0.0, 0.00086, 0.0, 0.0, 0.0, 0.00087, 0.0, 0.0, 0.0, 0.00093, 0.0, 0.00045, 0.0, 0.0, 2e-05, 0.00031, 0.00021, 0.0, 0.00021, 9e-05, 0.00014, 0.0, 6e-05, 8e-05, 0.00038, 0.00023, 0.0, 0.0, 0.0, 0.00019, 5e-05, 0.0, 0.0, 0.00021, 0.0, 0.00012, 0.00015, 0.00028, 0.00038, 0.0, 0.00017, 0.00024, 1e-05, 0.00083, 0.00072, 1e-05, 0.00024, 0.0, 1e-05, 0.00024, 0.00098, 0.00278, 0.0, 7e-05, 7e-05, 0.00023, 0.00025, 0.00042, 0.00039, 0.00028, 0.00038, 0.00015, 5e-05, 4e-05, 0.00012, 4e-05, 7e-05, 0.00036, 0.00025, 0.0, 3e-05, 9e-05, 7e-05, 4e-05, 0.00037, 0.00025, 0.00019, 2e-05, 0.0, 0.00039, 0.00028, 6e-05, 0.00035, 7e-05, 0.0, 0.00014, 0.00055, 0.00016, 7e-05, 0.0, 0.0, 0.0, 0.00018, 0.00045, 0.00027, 0.0, 7e-05, 0.0, 0.00014, 0.00018, 7e-05, 0.0, 0.00014, 0.0001, 8e-05, 0.0, 0.00016, 4e-05, 7e-05, 0.00042, 9e-05, 7e-05, 4e-05, 0.00021, 0.0, 0.00053, 0.00053, 5e-05, 0.00074, 0.00073, 0.00078, 0.00033, 0.00048, 0.0002, 0.0, 7e-05, 0.00013, 6e-05, 1e-05, 0.0, 0.00015, 0.00016, 7e-05, 3e-05, 2e-05, 4e-05, 5e-05, 0.0, 0.00071, 0.00014, 0.0, 0.00022, 0.00016, 0.0, 0.00024, 0.0002, 0.0001, 0.0, 0.00066, 0.00088, 0.0, 0.0001, 0.00096, 0.00215, 0.0004, 0.00036, 0.00041, 0.00125, 8e-05, 8e-05, 4e-05, 0.00165, 0.00038, 0.0, 0.0, 0.00243, 0.0, 0.0, 0.00011, 0.00023, 0.0, 0.00016, 0.00029, 0.00013, 0.00031, 0.0, 0.0, 0.00072, 0.00016, 0.0001, 0.0, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0002, 0.0004, 0.00013, 3e-05, 0.0, 0.00016, 0.0002, 0.0, 0.00059, 0.00123, 2e-05, 0.0, 0.0, 0.00068, 0.00044, 0.00014, 0.0007, 7e-05, 5e-05, 0.0, 0.00069, 0.00018, 0.0, 0.0, 0.0014, 0.0, 0.00021, 0.0, 0.0, 0.0001, 0.00016, 8e-05, 0.0, 0.0, 6e-05, 0.00023, 0.0, 0.0, 0.0, 2e-05, 0.00016, 0.0, 0.00011, 0.00033, 3e-05, 0.00011, 0.0, 0.00033, 0.00049, 0.00062, 0.00072, 0.00067, 0.00086, 1e-05, 6e-05, 0.0, 2e-05, 7e-05, 0.0, 0.00032, 0.0, 7e-05, 0.00043, 3e-05, 0.0, 0.00017, 0.0, 0.00026, 0.0, 0.0, 3e-05, 0.00014, 0.00029, 0.0, 0.00018, 0.00016, 0.00044, 0.00018, 0.00016, 0.00018, 0.00034, 0.0, 0.00101, 0.00102, 0.00052, 0.00022, 0.00011, 0.0, 9e-05, 0.00014, 0.0001, 0.0001, 0.00013, 0.00012, 0.00027, 2e-05, 0.00023, 0.0003, 0.0, 0.00016, 0.0, 0.00036, 0.00022, 0.0, 5e-05, 0.00059, 6e-05, 0.00015, 0.0, 0.0, 2e-05, 0.00016, 0.00108, 0.0, 0.0002, 0.00031, 0.0, 0.00016, 2e-05, 0.00047, 0.00015, 0.0, 0.0, 0.00809, 0.00074, 0.00073, 0.00068, 8e-05, 0.0, 0.0, 8e-05, 0.00022, 0.00019, 2e-05, 0.00012, 0.0001, 9e-05, 0.00023, 5e-05, 0.00028, 6e-05, 0.0, 0.0006, 6e-05, 0.00017, 0.00064, 0.00027, 0.00017, 6e-05, 0.00061, 0.00039, 0.00051, 0.00053, 0.00025, 0.0, 0.0, 0.00029, 0.00032, 0.00019, 0.00029, 0.0, 0.0004, 0.00019, 0.00192, 0.00229, 0.00056, 0.00034, 0.0, 2e-05, 8e-05, 0.00019, 0.00025, 0.00013, 0.00012, 0.00246, 4e-05, 0.0003, 0.00062, 0.00037, 0.0, 0.00012, 0.00037, 0.00032, 0.00012, 0.0, 0.00032, 0.00095, 0.00071, 0.00078, 0.00025, 0.00085, 4e-05, 0.0, 0.0, 0.00045, 0.0, 1e-05, 0.00013, 0.00012, 0.0, 0.00033, 6e-05, 0.00023, 0.0004, 0.00042, 2e-05, 0.0, 0.0, 0.0003, 0.0, 0.0, 0.0, 0.00022, 0.00055, 0.00023, 0.0004, 0.00044, 0.00011, 0.00017, 0.0, 0.0, 0.00028, 0.0, 0.0, 1e-05, 0.0057, 0.0, 0.00032, 0.0, 0.00088, 2e-05, 0.00021, 0.00022, 9e-05, 0.0, 0.00135, 0.00142, 4e-05, 0.0, 0.0, 0.0, 9e-05, 0.00161, 0.00155, 0.00026, 0.0, 9e-05, 0.00028, 0.00029, 0.00021, 0.00054, 0.0, 0.0, 0.00029, 0.00024, 3e-05, 1e-05, 0.0, 0.00018, 0.0, 0.00014, 0.00013, 0.00028, 0.0001, 0.0, 0.0, 0.0, 0.0, 0.00046, 1e-05, 0.00141, 0.0, 0.0, 0.00026, 0.00076, 0.00014, 0.0, 0.00096, 0.0, 0.0, 0.00014, 0.00052, 0.00061, 0.00068, 0.00077, 0.00079, 0.0, 0.00049, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00049, 0.0013, 0.0, 0.00073, 0.0, 0.02919, 0.00044, 0.00069, 0.00012, 0.0, 0.00014, 0.00025, 0.00141, 0.00072, 0.0, 0.0, 0.0008, 0.0, 0.00061, 0.00012, 0.0012, 1e-05, 0.0, 0.0, 0.0, 0.00011, 0.0, 0.00028, 0.0, 0.00043, 0.0, 0.0, 0.00108, 0.00033, 0.0, 0.00014, 0.0006, 0.0, 0.00011, 1e-05, 0.0007, 0.0, 0.0, 0.0, 0.00103, 0.00016, 0.0, 0.0, 0.00032, 0.00031, 0.00036, 0.00034, 5e-05, 0.0, 7e-05, 0.00014, 0.0, 0.00046, 0.00026, 2e-05, 6e-05, 1e-05, 0.0, 0.00014, 0.00035, 0.00093, 0.0, 2e-05, 0.0, 0.00032, 0.00031, 6e-05, 0.00042, 0.0, 0.0, 0.00029, 0.00011, 2e-05, 0.0, 0.00017, 0.00041, 9e-05, 5e-05, 0.0002, 2e-05, 0.00018, 0.0, 0.00025, 0.0, 0.0, 0.00035, 0.0001, 0.00087, 9e-05, 2e-05, 0.00026, 0.0016, 0.0, 0.0001, 0.00173, 0.0013, 0.0001, 0.0, 0.00142, 0.00111, 0.00057, 0.00044, 0.00047, 0.00051, 0.00041, 0.00034, 0.00034, 0.00038, 0.00035, 0.0, 0.0, 0.0, 0.00013, 0.00016, 0.00016, 0.00031, 0.0, 9e-05, 0.00016, 0.0, 0.00016, 0.00016, 0.00035, 0.0, 0.0, 9e-05, 1e-05, 0.00034, 0.00038, 0.00027, 0.0, 0.0, 0.0, 3e-05, 0.00098, 0.00031, 0.00011, 0.0, 0.00973, 0.0, 0.0, 0.00017, 0.0, 0.00024, 0.0, 0.00012, 0.00017, 0.00022, 0.0, 0.0, 0.00021, 5e-05, 4e-05, 4e-05, 0.00013, 7e-05, 0.00018, 0.00029, 0.00018, 0.00018, 7e-05, 0.00026, 0.00033, 0.00023, 0.00095, 0.00018, 0.0002, 9e-05, 2e-05, 0.00045, 1e-05, 0.0, 0.00011, 0.00012, 2e-05, 9e-05, 0.00042, 0.0, 8e-05, 4e-05, 0.00228, 0.00051, 0.00039, 0.00025, 0.00016, 0.0, 0.00015, 0.00021, 0.0001, 0.0, 0.0001, 0.00053, 0.0, 0.0001, 0.0, 0.0006, 0.0, 4e-05, 0.0, 9e-05, 0.0, 0.0001, 0.00011, 0.0, 0.00018, 0.0, 8e-05, 0.00063, 4e-05, 0.0, 0.0, 0.00032, 0.0, 0.00015, 0.0, 0.00043, 7e-05, 2e-05, 0.0, 3e-05, 0.00011, 0.0, 0.0001, 0.00026, 0.0001, 0.0, 3e-05, 0.0, 0.0, 5e-05, 0.00033, 3e-05, 0.00012, 0.0, 1e-05, 0.0, 0.0, 0.00064, 0.0, 0.0, 0.0, 0.0, 0.00012, 0.0001, 0.0001, 0.0, 5e-05, 0.00035, 0.00011, 5e-05, 0.0, 0.00032, 0.00017, 0.00044, 0.00048, 0.00017, 0.0001, 0.00018, 0.0, 0.00012, 0.00021, 0.0, 0.00015, 0.0001, 8e-05, 6e-05, 4e-05, 0.0, 0.00011, 0.00013, 2e-05, 0.00042, 4e-05, 2e-05, 0.00013, 0.00018, 0.00038, 0.00066, 0.00062, 0.00022, 0.00024, 0.0, 0.0, 0.0, 0.0, 0.00014, 0.00021, 0.0001, 0.00014, 0.00018, 0.0, 0.00018, 0.0, 0.0, 0.00155, 0.0, 0.0, 0.0001, 0.00013, 0.0, 0.00012, 0.00036, 0.00011, 0.00013, 0.0005, 0.00034, 0.00013, 0.00011, 0.00046, 0.00041, 0.00059, 0.00061, 0.00026, 0.00065, 1e-05, 1e-05, 8e-05, 0.00045, 0.0, 2e-05, 0.00013, 0.0004, 0.00013, 0.0001, 7e-05, 0.00027, 0.0, 1e-05, 5e-05, 0.00069, 0.0, 0.00015, 0.0, 0.00115, 0.0, 0.00033, 0.0, 0.00021, 0.0, 0.00013, 0.0003, 0.00019, 0.00013, 0.0, 0.0003, 9e-05, 0.00048, 0.00041, 5e-05, 0.00019, 0.0, 3e-05, 0.00012, 0.0004, 0.00014, 8e-05, 0.0, 0.00063, 0.00012, 4e-05, 0.00022, 0.00023, 0.0, 0.00013, 0.0, 0.00024, 4e-05, 0.0, 0.0, 0.00052, 6e-05, 0.0, 1e-05, 0.002, 0.00128, 0.00096, 0.0004, 0.0, 0.0, 5e-05, 0.00034, 0.0, 3e-05, 0.00013, 0.00066, 0.0, 4e-05, 0.0, 0.0005, 0.00037, 0.00029, 0.00018, 2e-05, 3e-05, 0.00055, 0.00034, 3e-05, 2e-05, 0.00068, 0.00077, 0.0005, 0.00037, 0.00018, 0.00033, 0.0, 0.0, 0.00013, 0.0003, 7e-05, 5e-05, 0.0, 0.00021, 9e-05, 8e-05, 0.0, 0.0002, 0.0, 0.00012, 2e-05, 0.0, 3e-05, 0.00038, 0.00021, 6e-05, 0.0, 2e-05, 3e-05, 0.0, 0.00042, 0.00076, 3e-05, 0.0, 5e-05, 0.00046, 0.00042, 0.0002, 0.00054, 0.0, 1e-05, 0.0, 0.00071, 4e-05, 5e-05, 0.0, 0.00032, 0.0, 7e-05, 2e-05, 0.00034, 4e-05, 0.0, 4e-05, 0.00019, 5e-05, 7e-05, 0.0, 0.00125, 3e-05, 0.0, 8e-05, 0.00026, 0.0, 0.00014, 0.0, 0.00048, 0.0, 0.0, 3e-05, 0.00026, 6e-05, 0.0, 0.00021, 5e-05, 0.00016, 0.0, 0.00024, 5e-05, 0.0, 6e-05, 0.00023, 1e-05, 7e-05, 0.0, 0.00011, 0.0, 0.0, 0.0004, 6e-05, 0.0, 0.00023, 8e-05, 0.0, 0.00021, 0.00011, 0.0, 0.00013, 0.00025, 0.00022, 0.00013, 0.0, 0.00029, 0.0007, 0.00056, 0.00042, 0.00045, 0.00021, 8e-05, 0.0, 0.0, 0.0001, 3e-05, 7e-05, 0.0001, 0.00176, 3e-05, 0.0, 0.0, 0.0, 0.0, 3e-05, 0.00029, 0.00023, 0.0001, 0.0, 0.0, 0.00036, 0.00018, 9e-05, 0.00011, 0.00038, 4e-05, 4e-05, 0.0, 8e-05, 9e-05, 0.00045, 0.00046, 0.00012, 2e-05, 0.0, 9e-05, 8e-05, 0.0006, 0.00023, 0.0, 0.0, 0.00018, 0.00029, 0.00034, 0.00038, 0.0, 6e-05, 4e-05, 0.00035, 4e-05, 4e-05, 6e-05, 0.00029, 0.0, 0.00045, 0.00051, 0.00014, 0.00017, 3e-05, 0.00011, 3e-05, 0.00033, 0.0, 0.0001, 2e-05, 0.00137, 0.00017, 0.0, 0.00037, 0.00031, 8e-05, 0.0, 0.00037, 0.0, 0.0, 8e-05, 0.0003, 0.0, 0.00048, 0.00045, 0.00034, 0.0003, 0.00013, 7e-05, 0.00052, 0.00049, 7e-05, 0.00013, 0.00054, 0.00061, 0.00058, 0.00042, 0.00012, 0.0005, 0.00029, 0.00037, 0.0, 0.00012, 0.00012, 0.00012, 0.0, 0.00021, 3e-05, 9e-05, 6e-05, 0.0001, 0.00014, 4e-05, 0.0, 0.00016, 0.00122, 0.00018, 3e-05, 0.00016, 4e-05, 5e-05, 0.00019, 5e-05, 7e-05, 0.00013, 0.00047, 0.00031, 0.00013, 7e-05, 0.00034, 0.00044, 0.0006, 0.0006, 0.00055, 0.00034, 8e-05, 2e-05, 5e-05, 6e-05, 0.00019, 0.0, 0.00027, 0.00031, 0.00015, 1e-05, 0.0003, 0.00016, 0.00014, 3e-05, 0.00037, 0.00035, 3e-05, 0.00014, 0.00041, 0.0, 0.00071, 0.00077, 0.00011, 0.00036, 5e-05, 9e-05, 0.00067, 0.00018, 0.0, 0.0, 0.00016, 9e-05, 5e-05, 0.00072, 0.0, 6e-05, 0.00023, 0.00597, 0.00035, 0.00044, 0.00102, 3e-05, 0.0, 0.00052, 0.00043, 4e-05, 7e-05, 0.0, 0.00044, 9e-05, 0.0, 0.0, 0.0, 0.0, 0.0002, 0.00035, 0.0, 0.00017, 5e-05, 0.0, 0.0, 0.0, 1e-05, 0.00025, 0.00048, 0.0, 5e-05, 0.00012, 0.00035, 0.0001, 0.0, 0.0, 4e-05, 0.00012, 9e-05, 5e-05, 6e-05, 3e-05, 0.00022, 0.00017, 0.00013, 0.0, 8e-05, 0.00013, 5e-05, 3e-05, 0.00051, 0.0002, 2e-05, 0.0002, 0.0002, 3e-05, 5e-05, 0.00064, 0.0, 1e-05, 9e-05, 0.00018, 0.00046, 0.00031, 0.00025, 0.00063, 0.0, 0.0, 0.0, 0.0006, 6e-05, 2e-05, 3e-05, 0.00051, 0.00011, 0.0, 0.00016, 0.0, 0.0, 0.0, 0.00031, 0.00028, 0.00011, 0.0, 0.0, 0.0006, 5e-05, 1e-05, 0.0, 0.00022, 0.0, 0.00013, 9e-05, 0.00063, 0.0, 0.0, 2e-05, 0.0, 0.00026, 0.0, 0.0, 0.00028, 0.0, 2e-05, 7e-05, 0.0, 0.0, 0.00017, 0.00022, 5e-05, 4e-05, 4e-05, 0.0, 0.0, 0.00015, 9e-05, 0.00017, 0.0, 0.00012, 0.0001, 1e-05, 0.00013, 0.00035, 0.0, 8e-05, 0.00045, 0.00014, 8e-05, 0.0, 0.0004, 1e-05, 0.00054, 0.00049, 0.00031, 0.00078, 0.0, 6e-05, 0.00015, 0.00054, 0.0, 0.0002, 0.00019, 0.0, 0.0001, 0.0, 0.00022, 0.00016, 6e-05, 0.0, 0.00018, 7e-05, 0.00013, 0.00012, 0.0, 0.0003, 3e-05, 0.00013, 0.00019, 0.00016, 9e-05, 0.0, 0.00037, 0.00018, 0.0, 9e-05, 0.00025, 0.00054, 0.00047, 0.00052, 0.00025, 0.00026, 0.0, 4e-05, 0.00055, 0.00017, 4e-05, 0.0, 0.00049, 0.0001, 0.00048, 0.00055, 3e-05, 0.00039, 3e-05, 0.00027, 0.0, 0.00041, 0.0, 0.00015, 0.0, 0.00042, 0.00018, 0.0, 0.00024, 0.00036, 0.00031, 0.00026, 0.00039, 5e-05, 0.0, 0.00053, 0.00038, 0.0, 5e-05, 0.0005, 0.00051, 0.00036, 0.00031, 4e-05, 0.00058, 0.0, 0.0, 1e-05, 0.00024, 0.0, 9e-05, 0.0, 0.00027, 0.00013, 3e-05, 4e-05, 0.00023, 0.00018, 0.0, 0.00044, 1e-05, 5e-05, 4e-05, 0.00026, 0.0, 0.00018, 0.0005, 0.0, 5e-05, 0.0, 0.00049, 0.0004, 0.00033, 0.00018, 2e-05, 1e-05, 0.0, 0.00051, 9e-05, 4e-05, 0.0, 0.00016, 2e-05, 6e-05, 6e-05, 0.00029, 0.0, 9e-05, 0.00011, 0.00027, 2e-05, 6e-05, 0.0, 0.00028, 4e-05, 0.0, 9e-05, 0.00013, 0.0, 0.0, 0.00015, 8e-05, 1e-05, 6e-05, 0.00022, 8e-05, 6e-05, 1e-05, 0.00021, 0.00047, 0.00034, 0.00041, 0.00019, 0.00029, 6e-05, 5e-05, 0.0001, 7e-05, 0.0, 0.0, 0.00024, 3e-05, 3e-05, 8e-05, 0.0, 2e-05, 0.00013, 0.00032, 0.00013, 0.0, 0.0, 6e-05, 0.00011, 0.0, 0.00033, 0.0002, 7e-05, 0.00071, 0.00044, 7e-05, 0.0002, 0.00066, 0.00058, 0.00056, 0.00053, 0.00019, 0.00117, 0.0, 0.00022, 0.00042, 0.00183, 0.00029, 0.0, 0.00029, 0.00916, 8e-05, 0.0, 0.0, 0.00012, 0.00026, 0.00038, 0.00064, 0.0003, 0.00038, 0.00026, 0.00097, 0.00262, 0.00181, 0.00241, 0.00299, 0.0, 2e-05, 0.00022, 0.00054, 0.00028, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00038, 0.0, 0.00042, 2e-05, 0.0, 0.00018, 0.0001, 0.00018, 0.00023, 0.00025, 0.0, 0.00025, 5e-05, 0.00016, 0.00042, 9e-05, 0.00016, 5e-05, 0.00034, 0.00049, 0.00102, 0.00086, 0.00073, 0.0005, 0.0, 0.00024, 0.0, 0.0004, 6e-05, 0.0, 0.0001, 0.00049, 0.00011, 0.0, 0.0002, 0.00049, 3e-05, 0.0, 0.0, 0.00037, 5e-05, 0.0001, 0.0, 0.00037, 0.0, 0.0, 0.00015, 0.00036, 0.0, 0.00017, 0.00048, 0.0, 0.00011, 0.0, 0.0004, 0.00017, 0.0, 0.00049, 6e-05, 0.0, 3e-05, 0.00124, 0.00069, 0.00056, 0.00014, 1e-05, 0.0, 0.0]))),\n", + " LayerError(circuit=, qubits=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155], error=PauliLindbladError(generators=['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...',\n", + " 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII...', ...], rates=[0.00135, 0.001, 0.00567, 0.0004, 0.0, 7e-05, 0.0, 9e-05, 0.0, 7e-05, 0.00013, 0.00241, 5e-05, 0.0, 0.0, 0.00014, 0.00013, 3e-05, 0.00036, 2e-05, 3e-05, 0.00013, 0.00029, 0.0, 0.00051, 0.00034, 0.0001, 0.00019, 6e-05, 0.00018, 0.0, 0.00018, 9e-05, 9e-05, 8e-05, 0.00214, 7e-05, 0.0, 0.00027, 0.0, 0.0, 7e-05, 0.0002, 0.0, 7e-05, 0.0, 0.00017, 0.0, 0.00043, 0.00044, 0.00016, 0.0011, 0.00014, 0.00012, 0.00012, 0.00111, 7e-05, 0.00014, 0.00018, 0.00109, 0.00013, 0.0, 0.00027, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00054, 0.0, 0.0, 0.0, 0.0005, 0.0, 0.0, 0.0, 0.00089, 0.0, 0.0, 0.0, 0.0, 0.00028, 0.00028, 7e-05, 0.0, 0.00028, 0.00028, 0.00016, 0.0, 0.00054, 0.0005, 0.00042, 0.00096, 0.0, 5e-05, 6e-05, 0.00077, 0.0002, 0.0, 0.0, 0.00072, 0.0, 0.00014, 0.0, 0.0003, 0.00014, 0.0, 0.00048, 0.00023, 0.0, 0.00014, 0.00044, 0.00054, 0.00135, 0.00142, 0.00023, 0.00031, 1e-05, 7e-05, 0.00011, 0.00047, 0.00018, 0.0, 0.0, 0.00011, 0.0, 0.00014, 3e-05, 0.00029, 0.0, 4e-05, 0.0, 0.00014, 6e-05, 8e-05, 9e-05, 0.00014, 0.00011, 0.00016, 2e-05, 0.00029, 0.0, 0.0, 0.00017, 0.00024, 9e-05, 3e-05, 0.0, 0.00036, 5e-05, 1e-05, 0.0, 0.00025, 0.0, 0.0, 0.0, 0.0002, 0.0, 0.0, 0.0, 0.00058, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.37843, 0.0, 0.0, 0.53164, 0.5365, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00028, 9e-05, 9e-05, 4e-05, 7e-05, 0.0, 0.0, 0.00025, 0.00011, 0.0, 0.00012, 7e-05, 4e-05, 0.00035, 0.00015, 4e-05, 7e-05, 0.00029, 0.0, 0.00047, 0.00036, 9e-05, 0.00164, 0.00232, 0.0028, 0.00131, 0.0, 0.0, 0.0, 0.00148, 0.0, 0.0, 0.0, 0.00084, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00521, 0.00527, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.40338, 0.0, 0.0, 0.0, 0.30521, 0.09093, 0.09126, 0.14967, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25536, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.70904, 0.0, 0.0, 0.0, 0.0, 0.44482, 0.05059, 1.98941, 2.66137, 1.82174, 1.98941, 0.0, 1.82174, 2.66137, 0.0, 0.0, 0.10991, 0.02851, 1.35927, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00581, 0.0, 0.0, 0.0, 0.00042, 0.00025, 0.00021, 0.00026, 0.0, 0.0, 0.00084, 0.00058, 0.00021, 0.00019, 0.00022, 0.0, 0.0, 0.00072, 0.0, 9e-05, 0.00016, 0.00029, 0.0, 0.0, 0.0005, 0.00067, 0.00059, 0.00051, 0.00058, 0.00013, 0.0, 0.00015, 2e-05, 0.0, 1e-05, 0.00032, 3e-05, 0.00015, 0.0002, 0.0, 0.00011, 0.0, 0.00022, 7e-05, 0.0, 0.00015, 0.0, 0.0, 7e-05, 0.00035, 0.0, 0.0, 0.00077, 0.00017, 0.0, 0.0, 0.00066, 0.00234, 0.00131, 0.00148, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00214, 0.0, 0.0, 0.0, 0.00178, 0.0, 0.0, 0.0, 0.00307, 0.0, 0.0, 0.0, 0.00178, 0.00165, 0.00056, 0.00035, 0.00033, 0.00061, 0.0, 0.00028, 4e-05, 9e-05, 0.0, 0.0, 0.00052, 0.0, 8e-05, 0.00017, 0.0002, 0.0, 0.0, 0.00038, 0.00022, 6e-05, 0.00029, 0.0, 0.00035, 0.00033, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e-05, 0.0, 0.00043, 0.00147, 0.00019, 0.0, 0.0001, 0.01096, 0.0, 0.00027, 4e-05, 0.01189, 0.0, 0.00048, 0.0, 0.0, 0.00088, 0.0, 0.0, 0.00084, 0.00106, 0.00067, 0.00119, 0.00069, 0.00067, 0.00106, 0.00117, 0.0048, 0.0117, 0.0124, 0.00417, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00051, 0.0, 0.0, 0.0, 0.00035, 0.00048, 0.00099, 5e-05, 0.0, 0.0, 0.00043, 0.0, 0.0, 0.0, 0.00067, 0.00087, 0.0, 0.0, 0.00021, 0.00031, 0.00016, 0.00031, 0.00044, 0.00017, 0.00031, 0.00016, 0.00047, 1e-05, 0.00073, 0.00086, 0.00056, 0.00019, 0.0, 0.0, 0.0, 0.00016, 0.0, 0.0, 0.0, 0.00108, 4e-05, 0.00021, 0.00028, 0.0, 0.00023, 0.00044, 0.00039, 0.0, 0.0, 0.00012, 0.0, 0.00042, 0.00034, 0.00032, 0.0, 0.0, 0.00017, 0.0, 0.00069, 0.00049, 0.00032, 0.00019, 0.00016, 0.0, 0.00032, 0.0, 0.0, 0.0, 0.00035, 0.0, 0.0, 0.0, 0.00013, 0.0, 0.0, 0.0, 0.00013, 0.0, 5e-05, 0.00056, 0.00032, 5e-05, 0.0, 0.00059, 0.00053, 0.00032, 0.00035, 9e-05, 0.00029, 0.0, 7e-05, 0.0001, 0.00019, 1e-05, 0.0, 0.00015, 0.0002, 0.0003, 0.0, 0.0, 0.0, 8e-05, 0.0, 0.00023, 0.0, 8e-05, 0.00067, 0.00015, 0.0, 9e-05, 1e-05, 8e-05, 0.0, 0.00048, 0.00075, 0.0, 1e-05, 0.0, 0.00045, 0.00035, 0.00013, 0.00063, 2e-05, 9e-05, 3e-05, 0.00059, 0.0, 8e-05, 0.00012, 0.00045, 0.00035, 5e-05, 0.0, 0.00013, 5e-05, 0.0, 0.00028, 0.00025, 3e-05, 0.00018, 0.0, 0.00042, 0.0, 1e-05, 9e-05, 0.0, 0.0, 2e-05, 0.001, 0.0, 0.00043, 1e-05, 0.0, 0.0, 0.0, 0.0, 7e-05, 0.00027, 6e-05, 0.0, 0.0, 0.00098, 1e-05, 8e-05, 0.0, 0.00539, 2e-05, 0.0, 0.0, 0.00051, 0.0, 0.00015, 0.0, 0.00053, 0.0, 0.0, 9e-05, 0.00072, 0.00012, 5e-05, 0.0, 6e-05, 0.0001, 9e-05, 0.00036, 0.00021, 9e-05, 0.0001, 0.00035, 0.00017, 0.00047, 0.00047, 0.00014, 0.00044, 0.00029, 0.00075, 0.0, 0.0, 1e-05, 0.0, 0.0, 8e-05, 0.0, 0.00013, 0.0007, 9e-05, 0.0, 6e-05, 0.00074, 0.00022, 9e-05, 0.0003, 0.0, 0.0, 0.0, 0.0, 0.00177, 0.00024, 0.00027, 0.0002, 0.00866, 0.0, 0.0002, 0.0, 8e-05, 1e-05, 0.0, 0.00901, 0.0, 0.00042, 0.00042, 0.0, 0.00057, 0.0, 0.00748, 0.0, 0.0, 0.00116, 6e-05, 0.0, 0.00055, 0.0, 0.00082, 0.00104, 0.00061, 0.00124, 0.00104, 0.00082, 0.00129, 0.00317, 0.00896, 0.01041, 0.00599, 0.00052, 0.0, 0.0, 0.0, 0.00039, 5e-05, 0.0, 8e-05, 0.00468, 0.00064, 0.0, 0.0, 0.0009, 0.0, 0.00013, 0.00028, 0.00097, 0.00033, 5e-05, 0.0, 0.0004, 0.00021, 0.00017, 0.00014, 0.0, 0.0, 0.0, 0.00036, 7e-05, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.00026, 0.0, 6e-05, 3e-05, 0.00043, 0.0, 0.0, 0.00038, 0.00027, 0.00016, 5e-05, 0.00031, 7e-05, 0.0, 0.00045, 0.00028, 0.0, 7e-05, 0.0004, 0.00059, 0.00054, 0.0003, 0.00045, 0.00064, 0.0, 0.0, 0.0, 0.00026, 4e-05, 0.0, 0.00034, 0.0007, 0.00011, 0.00012, 0.0, 0.00056, 0.0, 0.0002, 0.00057, 0.00065, 0.0002, 0.0, 0.00066, 0.00067, 0.00121, 0.00123, 0.00025, 0.00043, 0.00044, 0.0005, 0.00075, 0.0, 0.00014, 0.00022, 0.0, 6e-05, 7e-05, 0.00083, 0.00028, 0.0, 0.0, 0.00013, 0.0, 0.0, 0.00099, 2e-05, 0.0, 0.0, 2e-05, 0.0, 2e-05, 0.00024, 0.0001, 4e-05, 0.00038, 0.00026, 4e-05, 0.0001, 0.00031, 0.00026, 0.00045, 0.00054, 0.0004, 0.00023, 0.00026, 0.0002, 0.00047, 0.0, 0.0002, 0.00026, 0.0003, 0.00087, 0.00106, 0.00088, 0.00097, 0.00151, 0.0, 6e-05, 0.00023, 0.00137, 0.00015, 0.0, 0.0, 0.00016, 0.00042, 0.00053, 0.00013, 0.00075, 0.00043, 0.00018, 0.00075, 0.00062, 0.00051, 0.0, 0.00015, 0.0, 0.0, 3e-05, 0.00012, 0.0, 0.0, 0.0, 5e-05, 0.00015, 0.0, 6e-05, 0.00021, 0.0, 0.0, 0.0, 8e-05, 2e-05, 0.0, 0.00015, 0.00011, 7e-05, 9e-05, 0.00039, 0.00028, 9e-05, 7e-05, 0.00057, 0.00552, 0.00028, 0.00045, 0.00041, 0.0, 0.0, 0.00029, 0.0, 0.0, 0.00018, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00036, 0.00033, 0.0, 0.00011, 0.0, 0.00015, 0.00013, 0.0, 0.0, 0.00036, 0.0, 0.00012, 5e-05, 0.00022, 0.0, 0.00018, 7e-05, 5e-05, 7e-05, 0.0, 0.00032, 8e-05, 5e-05, 0.0, 4e-05, 1e-05, 8e-05, 0.00027, 0.00016, 7e-05, 0.00018, 0.0, 3e-05, 0.00027, 0.00034, 3e-05, 7e-05, 0.00048, 0.00045, 7e-05, 3e-05, 0.00053, 0.00018, 0.00058, 0.00057, 8e-05, 0.0, 8e-05, 0.0, 0.00016, 0.0, 0.0, 7e-05, 0.00014, 0.0, 6e-05, 1e-05, 0.0, 0.00022, 0.00011, 0.0, 0.00022, 0.00026, 0.0, 0.00011, 0.00035, 0.00033, 0.00045, 0.00032, 0.00016, 0.0005, 0.00027, 3e-05, 0.0008, 0.0, 0.0, 0.0, 0.0003, 3e-05, 0.00027, 0.00083, 0.0, 0.0, 0.0, 2e-05, 0.00181, 0.00152, 0.00038, 0.0, 9e-05, 0.0, 0.0, 0.00012, 0.00011, 7e-05, 7e-05, 4e-05, 0.00014, 0.00012, 0.0, 0.00014, 0.00029, 0.00012, 0.00039, 0.0, 0.00032, 0.00066, 0.00032, 0.00032, 0.0, 0.0009, 0.00201, 0.00021, 0.00041, 0.00014, 6e-05, 3e-05, 0.00021, 0.0, 0.0002, 0.0, 0.0, 0.00011, 0.00028, 2e-05, 0.0, 0.0, 9e-05, 4e-05, 9e-05, 9e-05, 0.0, 0.0001, 0.0005, 0.0002, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.00039, 0.00028, 0.0, 0.0, 6e-05, 0.0003, 0.00031, 0.0001, 0.00092, 0.0, 0.00012, 4e-05, 0.00098, 4e-05, 7e-05, 4e-05, 0.00062, 0.00015, 0.0, 0.0, 0.00049, 5e-05, 0.0, 0.0, 0.00029, 6e-05, 0.0, 8e-05, 0.00102, 0.0, 0.0, 0.0001, 0.00037, 1e-05, 0.00021, 0.00038, 6e-05, 0.00021, 1e-05, 7e-05, 0.00062, 0.00026, 0.00036, 0.0003, 0.00045, 1e-05, 0.0, 0.0, 0.00047, 0.0, 3e-05, 9e-05, 0.00057, 0.00022, 8e-05, 6e-05, 0.0, 2e-05, 0.00036, 0.0, 0.00021, 0.0, 0.0, 0.0001, 0.0005, 0.00019, 1e-05, 0.0, 4e-05, 8e-05, 3e-05, 0.00028, 0.00013, 3e-05, 8e-05, 0.00021, 0.0, 0.00054, 0.00044, 0.0002, 0.00148, 0.00101, 0.00116, 0.00033, 0.00012, 0.0, 0.0, 0.00034, 0.0, 7e-05, 0.0001, 0.00066, 2e-05, 0.0, 0.0, 0.00079, 0.00061, 9e-05, 0.00011, 0.0, 0.0, 0.00012, 0.0001, 3e-05, 0.0, 7e-05, 0.00019, 3e-05, 3e-05, 0.0, 0.00027, 0.0001, 0.0, 0.00037, 0.00012, 0.0, 0.0001, 0.0003, 0.0002, 0.00043, 0.00033, 0.00018, 0.00033, 0.0, 3e-05, 0.0001, 0.0, 0.0, 4e-05, 0.00025, 0.0, 0.0, 0.0, 0.0, 4e-05, 0.0, 0.00028, 6e-05, 5e-05, 0.0, 0.0001, 0.00014, 0.0, 0.00033, 0.0, 3e-05, 0.00042, 0.00025, 3e-05, 0.0, 0.0003, 0.00054, 0.00049, 0.0003, 0.00081, 0.00041, 0.0, 0.0, 0.0, 0.00022, 0.0, 0.0, 0.0, 0.00184, 9e-05, 5e-05, 0.0, 3e-05, 6e-05, 0.0001, 0.00032, 7e-05, 0.0001, 6e-05, 0.0002, 0.00062, 0.00045, 0.00037, 0.00015, 0.00043, 0.0, 0.0001, 0.00073, 0.0, 0.0, 7e-05, 0.00029, 0.0001, 0.0, 0.00076, 0.00015, 0.0, 0.00015, 0.0, 0.00028, 0.00036, 0.00014, 0.00014, 0.00013, 0.0, 7e-05, 0.0, 1e-05, 9e-05, 4e-05, 2e-05, 0.0001, 0.0002, 0.0002, 0.00021, 4e-05, 2e-05, 0.00041, 0.0001, 0.00016, 0.00083, 0.00013, 0.00016, 0.0001, 0.00051, 0.00138, 0.00017, 0.00036, 0.00048, 0.0005, 0.0, 7e-05, 0.0, 0.00032, 0.0, 0.0, 0.00015, 0.00028, 6e-05, 8e-05, 0.00035, 0.00059, 5e-05, 0.0, 0.0002, 0.0, 0.0, 0.0, 0.00051, 0.0, 8e-05, 8e-05, 1e-05, 0.0, 0.00011, 0.00027, 0.00019, 1e-05, 6e-05, 6e-05, 0.0001, 0.0, 0.0003, 7e-05, 0.0, 0.00011, 0.00023, 0.00016, 2e-05, 0.0, 0.00016, 0.0, 0.00012, 7e-05, 0.00038, 0.0, 0.0, 7e-05, 0.00058, 7e-05, 0.0, 0.0, 0.00119, 0.00013, 0.00013, 0.0, 0.00019, 3e-05, 1e-05, 3e-05, 0.00045, 0.0, 5e-05, 0.00012, 0.00067, 1e-05, 7e-05, 0.0, 0.00038, 0.00019, 0.0, 0.0, 0.00026, 0.00015, 1e-05, 0.0, 0.00041, 0.0, 0.00021, 0.00053, 0.00021, 0.00027, 0.00033, 7e-05, 0.00014, 0.0, 0.00013, 1e-05, 5e-05, 0.00061, 0.0, 0.0, 2e-05, 0.00016, 5e-05, 1e-05, 0.00039, 0.0, 0.0, 5e-05, 5e-05, 0.00021, 0.00027, 9e-05, 0.0003, 0.0001, 4e-05, 0.0, 0.00027, 2e-05, 0.0, 8e-05, 0.00019, 0.00014, 0.00026, 0.00019, 0.00023, 6e-05, 1e-05, 0.00068, 0.00018, 1e-05, 6e-05, 0.00057, 0.00117, 0.00044, 0.00037, 0.00034, 0.00046, 0.0, 2e-05, 0.0, 0.00028, 6e-05, 0.0, 0.00011, 0.00014, 9e-05, 0.00017, 9e-05, 0.00021, 3e-05, 4e-05, 8e-05, 9e-05, 0.0, 0.0, 0.00037, 0.0, 0.0, 1e-05, 0.0, 4e-05, 6e-05, 0.00054, 0.00021, 0.0, 0.0, 3e-05, 8e-05, 3e-05, 0.00012, 0.00015, 1e-05, 0.00033, 8e-05, 1e-05, 0.00015, 0.00027, 0.0, 0.00046, 0.00049, 0.00027, 0.0, 0.00012, 0.0, 0.00023, 3e-05, 9e-05, 0.00012, 0.0, 0.0, 0.0, 8e-05, 4e-05, 0.00019, 0.00015, 0.00011, 0.00025, 9e-05, 0.00011, 0.00015, 0.00037, 0.00042, 0.00061, 0.00043, 0.00033, 0.0, 0.0, 0.0, 0.00023, 0.0, 0.00015, 0.00014, 0.0, 5e-05, 0.00012, 3e-05, 3e-05, 0.00013, 0.0, 0.0, 4e-05, 0.0, 0.00034, 4e-05, 0.0, 0.00011, 0.00028, 4e-05, 0.00011, 0.00039, 0.00032, 0.00011, 4e-05, 0.00032, 0.00012, 0.00044, 0.00038, 0.0003, 0.0, 0.0, 0.0, 0.00026, 0.00032, 0.00011, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0002, 6e-05, 1e-05, 7e-05, 0.00024, 6e-05, 7e-05, 1e-05, 0.00039, 0.00021, 0.00032, 0.00049, 0.0003, 0.00028, 0.00017, 7e-05, 0.00064, 0.00018, 0.0, 0.0, 0.0, 7e-05, 0.00017, 0.00078, 0.0, 0.00021, 0.00021, 0.00043, 0.00059, 0.00045, 0.00036, 0.0, 0.00011, 2e-05, 0.0002, 0.00017, 0.0, 0.0, 8e-05, 0.0, 7e-05, 0.00024, 0.00041, 0.0, 0.00032, 5e-05, 0.00042, 4e-05, 6e-05, 0.00049, 0.0, 6e-05, 4e-05, 0.0002, 0.001, 0.00037, 0.0004, 0.00021, 0.0005, 0.0, 0.0, 0.0, 0.00027, 9e-05, 1e-05, 0.00011, 0.00032, 0.00021, 0.00019, 7e-05, 0.0, 8e-05, 0.00013, 0.00012, 0.00019, 2e-05, 0.0, 7e-05, 0.0, 0.00015, 7e-05, 0.00014, 0.00051, 0.00016, 3e-05, 0.0, 0.00078, 0.0, 0.0, 7e-05, 0.00041, 0.0, 0.0, 0.00014, 0.00253, 4e-05, 0.0001, 0.0, 0.00224, 0.0, 0.0, 4e-05, 8e-05, 0.0, 0.00019, 0.00018, 0.00057, 0.00048, 0.0003, 0.00032, 8e-05, 1e-05, 3e-05, 0.00036, 0.0, 2e-05, 0.0, 0.00048, 0.0, 0.0, 9e-05, 0.00035, 3e-05, 3e-05, 0.00042, 0.00031, 3e-05, 3e-05, 0.00032, 0.00024, 0.00044, 0.00039, 0.00018, 0.00032, 1e-05, 0.0, 0.00014, 0.0, 0.0, 0.0, 0.00043, 5e-05, 0.0, 0.0, 4e-05, 1e-05, 0.0, 0.00069, 0.0, 6e-05, 8e-05, 3e-05, 0.0, 4e-05, 0.00019, 6e-05, 2e-05, 0.00028, 0.00013, 2e-05, 6e-05, 0.00021, 0.0, 0.00047, 0.00053, 6e-05, 0.00036, 5e-05, 0.0, 0.00024, 0.00063, 0.0, 5e-05, 6e-05, 0.00032, 4e-05, 3e-05, 0.0, 0.00022, 0.0, 0.0, 0.0, 0.00033, 0.0, 0.0, 3e-05, 0.00033, 0.00011, 6e-05, 7e-05, 0.00046, 8e-05, 7e-05, 0.00067, 0.0, 0.0, 7e-05, 0.00036, 7e-05, 8e-05, 0.00066, 0.0, 3e-05, 4e-05, 0.0, 0.00032, 0.00028, 5e-05, 6e-05, 0.0, 0.0, 0.00033, 0.0, 0.0, 0.00012, 0.00039, 0.0, 5e-05, 5e-05, 0.00099, 0.00013, 0.00017, 9e-05, 0.00052, 0.0, 0.00024, 0.00095, 0.00046, 0.00024, 0.0, 0.00118, 0.01194, 0.00045, 0.0005, 0.0, 0.0, 0.0, 1e-05, 0.00057, 0.00038, 0.0, 0.00022, 0.0, 0.00398, 0.00042, 0.00049, 0.0016, 0.0, 6e-05, 0.0, 0.0001, 4e-05, 0.0, 0.00031, 0.00013, 0.0, 0.0001, 0.0, 0.0, 4e-05, 0.00044, 0.0, 0.0, 6e-05, 8e-05, 0.0003, 0.00077, 0.00031, 0.00038, 2e-05, 0.0, 0.0, 0.00029, 0.0, 7e-05, 0.0, 0.00034, 0.00018, 0.00012, 2e-05, 0.00028, 0.00011, 0.0, 0.0, 0.0003, 5e-05, 0.0, 4e-05, 0.00045, 0.0, 4e-05, 0.0, 0.00031, 0.00012, 0.00011, 0.00031, 7e-05, 0.00011, 0.00012, 0.00028, 0.0003, 0.00039, 0.00039, 0.00017, 0.00096, 0.0, 1e-05, 0.0, 0.0, 2e-05, 9e-05, 0.00075, 0.0, 5e-05, 0.0001, 0.0, 5e-05, 2e-05, 0.00012, 9e-05, 9e-05, 9e-05, 0.0, 0.00015, 0.0])))]},\n", + " 'version': 2}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "primitive_result.metadata" + ] + }, + { + "cell_type": "markdown", + "id": "69f5426e", + "metadata": {}, + "source": [ + "The `PubResult` object has additional resilience metadata about the learned noise models used in mitigation." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "52482e42", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "noise_overhead: 9.2584227461744e+229\n", + "total_mitigated_layers: 18\n", + "unique_mitigated_layers: 3\n", + "unique_mitigated_layers_noise_overhead: [2.0713004613510885e+36, 10.600275591731494, 9.687147432958504]\n" + ] + } + ], + "source": [ + "# Print learned layer noise metadata\n", + "for field, value in pub_result.metadata[\"resilience\"][\"layer_noise\"].items():\n", + " print(f\"{field}: {value}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "2b96bdd2", + "metadata": {}, + "outputs": [], + "source": [ + "# Exact data computed using the methods described in the original reference\n", + "# Y. Kim et al. \"Evidence for the utility of quantum computing before fault tolerance\" (Nature 618,\n", + "# 500–505 (2023))\n", + "# Directly used here for brevity\n", + "exact_data = np.array(\n", + " [\n", + " 1,\n", + " 0.9899,\n", + " 0.9531,\n", + " 0.8809,\n", + " 0.7536,\n", + " 0.5677,\n", + " 0.3545,\n", + " 0.1607,\n", + " 0.0539,\n", + " 0.0103,\n", + " 0.0012,\n", + " 0.0,\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f6dfbb9a", + "metadata": {}, + "source": [ + "### Plot Trotter simulation results\n", + "\n", + "The following code creates a plot to compare the raw and mitigated experiment results against the exact solution." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e466736a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "zne_metadata = primitive_result.metadata[\"resilience\"][\"zne\"]\n", + "# Plot Trotter simulation results\n", + "fig = plot_trotter_results(\n", + " pub_result,\n", + " parameter_values,\n", + " plot_extrapolator=zne_metadata[\"extrapolator\"],\n", + " plot_noise_factors=zne_metadata[\"noise_factors\"],\n", + " exact=exact_data,\n", + ")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "1cd46c88", + "metadata": {}, + "source": [ + "While the noisy (noise factor `nf=1.0`) values show high deviation from exact values, the mitigated values are close to exact values, demonstrating the utility of the PEA-based mitigation technique.\n", + "\n", + "### Plot extrapolation results for individual qubits\n", + "\n", + "Finally, the following code creates a plot to show the extrapolation curves for different values of theta on a specific qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "bea9695a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "virtual_qubit = 1\n", + "plot_qubit_zne_data(\n", + " pub_result=pub_result,\n", + " angles=parameter_values,\n", + " qubit=virtual_qubit,\n", + " noise_factors=zne_metadata[\"noise_factors\"],\n", + " extrapolator=zne_metadata[\"extrapolator\"],\n", + " extrapolated_noise_factors=zne_metadata[\"extrapolated_noise_factors\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "75f48e6a-c7e4-46f3-9d39-a7a877427a04", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "If you found this work interesting, you might be interested in the following material:\n", + "- A [tutorial](/docs/tutorials/combine-error-mitigation-techniques) focused on combining error mitigation techniques.\n", + "- Detailed [documentation](/docs/guides/error-mitigation-and-suppression-techniques) on the error mitigation techniques available in Qiskit.\n", + "- Additional lessons covering utility-scale experiments: [Utility II](/learning/courses/utility-scale-quantum-computing/utility-ii) and [Utility III](/learning/courses/utility-scale-quantum-computing/utility-iii).\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/projected-quantum-kernels.ipynb b/docs/tutorials/projected-quantum-kernels.ipynb index 9a8ca77a0b7..25c4cfeb3eb 100644 --- a/docs/tutorials/projected-quantum-kernels.ipynb +++ b/docs/tutorials/projected-quantum-kernels.ipynb @@ -1,1505 +1,1507 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c77f1777-ecb8-4cb0-9bf2-49d7c989b12a", - "metadata": {}, - "source": [ - "---\n", - "title: Enhance feature classification using projected quantum kernels\n", - "description: Tutorial on enhancing feature classification using projected quantum kernels\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore braket sqrtm cytotoxicity Nalm Cytotoxicity binarize Utro Filippo Hsin */}" - ] - }, - { - "cell_type": "markdown", - "id": "bee84331-1f3b-4a23-b691-4d3f7f51e76b", - "metadata": {}, - "source": [ - "# Enhance feature classification using projected quantum kernels\n", - "\n", - "*Usage estimate: 80 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*\n", - "\n", - "In this tutorial, we demonstrate how to run a [projected quantum kernel](https://www.nature.com/articles/s41467-021-22539-9) (PQK) with Qiskit on a real-world biological dataset, based on the paper [Enhanced Prediction of CAR T-Cell Cytotoxicity with Quantum-Kernel Methods](https://arxiv.org/abs/2507.22710) [[1]](#references).\n", - "\n", - "PQK is a method used in quantum machine learning (QML) to encode classical data into a quantum feature space and project them back into the classical domain, by using quantum computers to enhance feature selection. It involves encoding classical data into quantum states using a quantum circuit, typically through a process called feature mapping, where the data is transformed into a high-dimensional Hilbert space. The \"projected\" aspect refers to extracting classical information from the quantum states, by measuring specific observables, to construct a kernel matrix that can be used in classical kernel-based algorithms like support vector machines. This approach leverages the computational advantages of quantum systems to potentially achieve better performance on certain tasks compared to classical methods.\n", - "\n", - "This tutorial also assumes general familiarity with QML methods. For further exploration of QML, refer to the [Quantum machine learning](/learning/courses/quantum-machine-learning) course in IBM Quantum Learning." - ] - }, - { - "cell_type": "markdown", - "id": "542b8075-3b8c-476c-9513-c03de0f162b1", - "metadata": {}, - "source": [ - "### Requirements\n", - "Before starting this tutorial, ensure that you have the following installed:\n", - "\n", - "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.40 or later (`pip install qiskit-ibm-runtime`)\n", - "- Category encoders 2.8.1 (`pip install category-encoders`)\n", - "- NumPy 2.3.2 (`pip install numpy`)\n", - "- Pandas 2.3.2 (`pip install pandas`)\n", - "- Scikit-learn 1.7.1 (`pip install scikit-learn`)\n", - "- Tqdm 4.67.1 (`pip install tqdm`)" - ] - }, - { - "cell_type": "markdown", - "id": "2c676996-1361-4b3a-9c94-4784376097b0", - "metadata": {}, - "source": [ - "### Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fe8a02d3-994a-45fe-823d-5e68ded0d717", - "metadata": {}, - "outputs": [], - "source": [ - "import warnings\n", - "\n", - "# Standard libraries\n", - "import os\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "# Machine learning and data processing\n", - "import category_encoders as ce\n", - "from scipy.linalg import inv, sqrtm\n", - "from sklearn.metrics.pairwise import rbf_kernel\n", - "from sklearn.model_selection import GridSearchCV, StratifiedKFold\n", - "from sklearn.svm import SVC\n", - "\n", - "# Qiskit and Qiskit Runtime\n", - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit import ParameterVector\n", - "from qiskit.circuit.library import UnitaryGate, ZZFeatureMap\n", - "from qiskit.quantum_info import SparsePauliOp, random_unitary\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import (\n", - " Batch,\n", - " EstimatorOptions,\n", - " EstimatorV2 as Estimator,\n", - " QiskitRuntimeService,\n", - ")\n", - "\n", - "# Progress bar\n", - "import tqdm\n", - "\n", - "warnings.filterwarnings(\"ignore\")" - ] - }, - { - "cell_type": "markdown", - "id": "2f8775c8-81b5-4732-8325-3f12dc96b45d", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem" - ] - }, - { - "cell_type": "markdown", - "id": "cb3db2e7-8a3d-4ab1-9e3b-ffc5587dee6d", - "metadata": {}, - "source": [ - "### Dataset preparation\n", - "\n", - "In this tutorial we use a real-world biological dataset for a binary classification task, which is generated by Daniels et al. (2022) and can be downloaded from the [supplementary material](https://www.science.org/doi/full/10.1126/science.abq0225#supplementary-materials) included with the paper. The data consists of CAR T-cells, which are genetically engineered T-cells used in immunotherapy to treat certain cancers. T-cells, a type of immune cell, are modified in a lab to express chimeric antigen receptors (CARs) that target specific proteins on cancer cells. These modified T-cells can recognize and destroy cancer cells more effectively. The data features are the CAR T-cell motifs, which refer to the specific structural or functional component of the CAR engineered into T-cells. Based on these motifs, our task is to predict the cytotoxicity of a given CAR T-cell, labeling it as either toxic or non-toxic." - ] - }, - { - "cell_type": "markdown", - "id": "a2ab0503-1cc9-4512-8f06-12223219cc3e", - "metadata": {}, - "source": [ - "The following shows the helper functions to preprocess this dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f1ce0222-e7c7-451c-a11c-e61425a6bb8e", - "metadata": {}, - "outputs": [], - "source": [ - "def preprocess_data(dir_root, args):\n", - " \"\"\"\n", - " Preprocess the training and test data.\n", - " \"\"\"\n", - " # Read from the csv files\n", - " train_data = pd.read_csv(\n", - " os.path.join(dir_root, args[\"file_train_data\"]),\n", - " encoding=\"unicode_escape\",\n", - " sep=\",\",\n", - " )\n", - " test_data = pd.read_csv(\n", - " os.path.join(dir_root, args[\"file_test_data\"]),\n", - " encoding=\"unicode_escape\",\n", - " sep=\",\",\n", - " )\n", - "\n", - " # Fix the last motif ID\n", - " train_data[train_data == 17] = 14\n", - " train_data.columns = [\n", - " \"Cell Number\",\n", - " \"motif\",\n", - " \"motif.1\",\n", - " \"motif.2\",\n", - " \"motif.3\",\n", - " \"motif.4\",\n", - " \"Nalm 6 Cytotoxicity\",\n", - " ]\n", - " test_data[test_data == 17] = 14\n", - " test_data.columns = [\n", - " \"Cell Number\",\n", - " \"motif\",\n", - " \"motif.1\",\n", - " \"motif.2\",\n", - " \"motif.3\",\n", - " \"motif.4\",\n", - " \"Nalm 6 Cytotoxicity\",\n", - " ]\n", - "\n", - " # Adjust motif at the third position\n", - " if args[\"filter_for_spacer_motif_third_position\"]:\n", - " train_data = train_data[\n", - " (train_data[\"motif.2\"] == 14) | (train_data[\"motif.2\"] == 0)\n", - " ]\n", - " test_data = test_data[\n", - " (test_data[\"motif.2\"] == 14) | (test_data[\"motif.2\"] == 0)\n", - " ]\n", - "\n", - " train_data = train_data[\n", - " args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n", - " ]\n", - " test_data = test_data[\n", - " args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n", - " ]\n", - "\n", - " # Adjust motif at the last position\n", - " if not args[\"allow_spacer_motif_last_position\"]:\n", - " last_motif = args[\"motifs_to_use\"][len(args[\"motifs_to_use\"]) - 1]\n", - " train_data = train_data[\n", - " (train_data[last_motif] != 14) & (train_data[last_motif] != 0)\n", - " ]\n", - " test_data = test_data[\n", - " (test_data[last_motif] != 14) & (test_data[last_motif] != 0)\n", - " ]\n", - "\n", - " # Get the labels\n", - " train_labels = np.array(train_data[args[\"label_name\"]])\n", - " test_labels = np.array(test_data[args[\"label_name\"]])\n", - "\n", - " # For the classification task use the threshold to binarize labels\n", - " train_labels[train_labels > args[\"label_binarization_threshold\"]] = 1\n", - " train_labels[train_labels < 1] = args[\"min_label_value\"]\n", - " test_labels[test_labels > args[\"label_binarization_threshold\"]] = 1\n", - " test_labels[test_labels < 1] = args[\"min_label_value\"]\n", - "\n", - " # Reduce data to just the motifs of interest\n", - " train_data = train_data[args[\"motifs_to_use\"]]\n", - " test_data = test_data[args[\"motifs_to_use\"]]\n", - "\n", - " # Get the class and motif counts\n", - " min_class = np.min(np.unique(np.concatenate([train_data, test_data])))\n", - " max_class = np.max(np.unique(np.concatenate([train_data, test_data])))\n", - "\n", - " num_class = max_class - min_class + 1\n", - " num_motifs = len(args[\"motifs_to_use\"])\n", - " print(str(max_class) + \":\" + str(min_class) + \":\" + str(num_class))\n", - "\n", - " train_data = train_data - min_class\n", - " test_data = test_data - min_class\n", - "\n", - " return (\n", - " train_data,\n", - " test_data,\n", - " train_labels,\n", - " test_labels,\n", - " num_class,\n", - " num_motifs,\n", - " )\n", - "\n", - "\n", - "def data_encoder(args, train_data, test_data, num_class, num_motifs):\n", - " \"\"\"\n", - " Use one-hot or binary encoding for classical data representation.\n", - " \"\"\"\n", - " if args[\"encoder\"] == \"one-hot\":\n", - " # Transform to one-hot encoding\n", - " train_data = np.eye(num_class)[train_data]\n", - " test_data = np.eye(num_class)[test_data]\n", - "\n", - " train_data = train_data.reshape(\n", - " train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n", - " )\n", - " test_data = test_data.reshape(\n", - " test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n", - " )\n", - "\n", - " elif args[\"encoder\"] == \"binary\":\n", - " # Transform to binary encoding\n", - " encoder = ce.BinaryEncoder()\n", - "\n", - " base_array = np.unique(np.concatenate([train_data, test_data]))\n", - " base = pd.DataFrame(base_array).astype(\"category\")\n", - " base.columns = [\"motif\"]\n", - " for motif_name in args[\"motifs_to_use\"][1:]:\n", - " base[motif_name] = base.loc[:, \"motif\"]\n", - " encoder.fit(base)\n", - "\n", - " train_data = encoder.transform(train_data.astype(\"category\"))\n", - " test_data = encoder.transform(test_data.astype(\"category\"))\n", - "\n", - " train_data = np.reshape(\n", - " train_data.values, (train_data.shape[0], num_motifs, -1)\n", - " )\n", - " test_data = np.reshape(\n", - " test_data.values, (test_data.shape[0], num_motifs, -1)\n", - " )\n", - "\n", - " train_data = train_data.reshape(\n", - " train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n", - " )\n", - " test_data = test_data.reshape(\n", - " test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n", - " )\n", - "\n", - " else:\n", - " raise ValueError(\"Invalid encoding type.\")\n", - "\n", - " return train_data, test_data" - ] - }, - { - "cell_type": "markdown", - "id": "d72e1c86-4855-4a5a-865b-14ba6b15afb7", - "metadata": {}, - "source": [ - "You can run this tutorial by running the following cell, which automatically creates the required folder structure and downloads both the training and test files directly into your environment. If you already have these files locally, this step will safely overwrite them to ensure version consistency." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "84495ee1-880a-48cb-a904-d83396e8b29e", - "metadata": {}, - "outputs": [], - "source": [ - "## Download dataset\n", - "\n", - "# Create data directory if it doesn't exist\n", - "!mkdir -p data_tutorial/pqk\n", - "\n", - "# Download the training and test sets from the official Qiskit documentation repo\n", - "!wget -q --show-progress -O data_tutorial/pqk/train_data.csv \\\n", - " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/train_data.csv\n", - "\n", - "!wget -q --show-progress -O data_tutorial/pqk/test_data.csv \\\n", - " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/test_data.csv\n", - "\n", - "!wget -q --show-progress -O data_tutorial/pqk/projections_train.csv \\\n", - " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_train.csv\n", - "\n", - "!wget -q --show-progress -O data_tutorial/pqk/projections_test.csv \\\n", - " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_test.csv\n", - "\n", - "# Check the files have been downloaded\n", - "!echo \"Dataset files downloaded:\"\n", - "!ls -lh data_tutorial/pqk/*.csv" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "35013bc5-6b5e-44c8-8a8c-3af313b00a82", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "14:0:15\n" - ] - } - ], - "source": [ - "args = {\n", - " \"file_train_data\": \"train_data.csv\",\n", - " \"file_test_data\": \"test_data.csv\",\n", - " \"motifs_to_use\": [\"motif\", \"motif.1\", \"motif.2\", \"motif.3\"],\n", - " \"label_name\": \"Nalm 6 Cytotoxicity\",\n", - " \"label_binarization_threshold\": 0.62,\n", - " \"filter_for_spacer_motif_third_position\": False,\n", - " \"allow_spacer_motif_last_position\": True,\n", - " \"min_label_value\": -1,\n", - " \"encoder\": \"one-hot\",\n", - "}\n", - "dir_root = \"./\"\n", - "\n", - "# Preprocess data\n", - "train_data, test_data, train_labels, test_labels, num_class, num_motifs = (\n", - " preprocess_data(dir_root=dir_root, args=args)\n", - ")\n", - "\n", - "# Encode the data\n", - "train_data, test_data = data_encoder(\n", - " args, train_data, test_data, num_class, num_motifs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "050515c2-64fe-43ce-8559-b58db58b76c3", - "metadata": {}, - "source": [ - "We also transform the dataset such that $1$ is represented as $\\pi/2$ for scaling purposes." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d40d9a0f-67d0-4704-8a94-cc0a466ffc92", - "metadata": {}, - "outputs": [], - "source": [ - "# Change 1 to pi/2\n", - "angle = np.pi / 2\n", - "\n", - "tmp = pd.DataFrame(train_data).astype(\"float64\")\n", - "tmp[tmp == 1] = angle\n", - "train_data = tmp.values\n", - "\n", - "tmp = pd.DataFrame(test_data).astype(\"float64\")\n", - "tmp[tmp == 1] = angle\n", - "test_data = tmp.values" - ] - }, - { - "cell_type": "markdown", - "id": "726fbdfc-677b-41b7-86c8-dc11adb1946c", - "metadata": {}, - "source": [ - "We verify sizes and shapes of the training and test datasets." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b98495f3-aeaa-4df1-a9fe-433e26aa7d4e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(172, 60) (172,)\n", - "(74, 60) (74,)\n" - ] - } - ], - "source": [ - "print(train_data.shape, train_labels.shape)\n", - "print(test_data.shape, test_labels.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "0c828dc0-9bd1-44bc-b299-303766ae3d37", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution" - ] - }, - { - "cell_type": "markdown", - "id": "55afd26f-7a53-43bc-920d-88160a61688e", - "metadata": {}, - "source": [ - "### Quantum circuit\n", - "\n", - "We now construct the feature map that embeds our classical dataset into a higher-dimensional feature space. For this embedding, we use the [``ZZFeatureMap``](/docs/api/qiskit/qiskit.circuit.library.ZZFeatureMap) from Qiskit." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "45956df4-5472-4394-a3e1-5514c456791d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "feature_dimension = train_data.shape[1]\n", - "reps = 24\n", - "insert_barriers = True\n", - "entanglement = \"pairwise\"\n", - "\n", - "# ZZFeatureMap with linear entanglement and a repetition of 2\n", - "embed = ZZFeatureMap(\n", - " feature_dimension=feature_dimension,\n", - " reps=reps,\n", - " entanglement=entanglement,\n", - " insert_barriers=insert_barriers,\n", - " name=\"ZZFeatureMap\",\n", - ")\n", - "embed.decompose().draw(output=\"mpl\", style=\"iqp\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "bae2e554-2ca2-4e71-a313-b6aec0b25e6a", - "metadata": {}, - "source": [ - "Another quantum embedding option is the 1D-Heisenberg Hamiltonian evolution ansatz. You can skip running this section if you would like to continue with the `ZZFeatureMap`." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "659dbf23-fd3f-4e01-94b4-33e6d672172c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "feature_dimension = train_data.shape[1]\n", - "num_qubits = feature_dimension + 1\n", - "embed2 = QuantumCircuit(num_qubits)\n", - "num_trotter_steps = 6\n", - "pv_length = feature_dimension * num_trotter_steps\n", - "pv = ParameterVector(\"theta\", pv_length)\n", - "\n", - "# Add Haar random single qubit unitary to each qubit as initial state\n", - "np.random.seed(42)\n", - "seeds_unitary = np.random.randint(0, 100, num_qubits)\n", - "for i in range(num_qubits):\n", - " rand_gate = UnitaryGate(random_unitary(2, seed=seeds_unitary[i]))\n", - " embed2.append(rand_gate, [i])\n", - "\n", - "\n", - "def trotter_circ(feature_dimension, num_trotter_steps):\n", - " num_qubits = feature_dimension + 1\n", - " circ = QuantumCircuit(num_qubits)\n", - " # Even\n", - " for i in range(0, feature_dimension, 2):\n", - " circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " for i in range(0, feature_dimension, 2):\n", - " circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " for i in range(0, feature_dimension, 2):\n", - " circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " # Odd\n", - " for i in range(1, feature_dimension, 2):\n", - " circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " for i in range(1, feature_dimension, 2):\n", - " circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " for i in range(1, feature_dimension, 2):\n", - " circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n", - " return circ\n", - "\n", - "\n", - "# Hamiltonian evolution ansatz\n", - "for step in range(num_trotter_steps):\n", - " circ = trotter_circ(feature_dimension, num_trotter_steps)\n", - " if step % 2 == 0:\n", - " embed2 = embed2.compose(circ)\n", - " else:\n", - " reverse_circ = circ.reverse_ops()\n", - " embed2 = embed2.compose(reverse_circ)\n", - "\n", - "\n", - "embed2.draw(output=\"mpl\", style=\"iqp\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "8b27a3f8-5ee9-41b5-a430-25247545cbb9", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "markdown", - "id": "f202d995-8fc0-4af5-b11e-2ed72ac48c84", - "metadata": {}, - "source": [ - "### Measure 1-RDMs\n", - "\n", - "The main building blocks of projected quantum kernels are the reduced density matrices (RDMs), which are obtained though projective measurements of the quantum feature map. In this step, we obtain all single-qubit reduced density matrices (1-RDMs), which will later be provided into the classical exponential kernel function." - ] - }, - { - "cell_type": "markdown", - "id": "ea823c34-d7d7-42f8-989c-dfe78cdd489a", - "metadata": {}, - "source": [ - "Let's look at how to compute the 1-RDM given a single data point from the dataset before we run over all data. The 1-RDMs are a collection of single-qubit measurements of Pauli ``X``, ``Y`` and ``Z`` operators on all qubits. This is because a single-qubit RDM can be fully expressed as: $$\\rho = \\frac{1}{2} \\big( I + \\braket \\sigma_x \\sigma_x + \\braket \\sigma_y \\sigma_y + \\braket \\sigma_z \\sigma_z \\big)$$" - ] - }, - { - "cell_type": "markdown", - "id": "daa7608c-f482-4df5-a2be-6ca9625bb7e0", - "metadata": {}, - "source": [ - "First we select the backend to use." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e1ab9cea-42ef-478c-bb9d-02ed4cf23ea6", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=133\n", - ")\n", - "target = backend.target" - ] - }, - { - "cell_type": "markdown", - "id": "2ce0917f-3826-477b-b503-57e3b5e7e290", - "metadata": {}, - "source": [ - "Then we run the quantum circuit and measure the projections. Note that we turn on error mitigation, including Zero Noise Extrapolation (ZNE)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53b20cec-ef8a-4fdb-aeed-46546a32ea96", - "metadata": {}, - "outputs": [], - "source": [ - "# Let's select the ZZFeatureMap embedding for this example\n", - "qc = embed\n", - "num_qubits = feature_dimension\n", - "\n", - "# Identity operator on all qubits\n", - "id = \"I\" * num_qubits\n", - "\n", - "# Let's select the first training datapoint as an example\n", - "parameters = train_data[0]\n", - "\n", - "# Bind parameter to the circuit and simplify it\n", - "qc_bound = qc.assign_parameters(parameters)\n", - "transpiler = generate_preset_pass_manager(\n", - " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", - ")\n", - "transpiled_circuit = transpiler.run(qc_bound)\n", - "\n", - "# Transpile for hardware\n", - "transpiler = generate_preset_pass_manager(optimization_level=3, target=target)\n", - "transpiled_circuit = transpiler.run(transpiled_circuit)\n", - "\n", - "# We group all commuting observables\n", - "# These groups are the Pauli X, Y and Z operators on individual qubits\n", - "observables_x = [\n", - " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - "]\n", - "observables_y = [\n", - " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - "]\n", - "observables_z = [\n", - " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - "]\n", - "\n", - "# We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", - "# set of observables and the circuit parameters\n", - "pub_x = (transpiled_circuit, observables_x)\n", - "pub_y = (transpiled_circuit, observables_y)\n", - "pub_z = (transpiled_circuit, observables_z)\n", - "\n", - "# Experiment options for error mitigation\n", - "num_randomizations = 300\n", - "shots_per_randomization = 100\n", - "noise_factors = [1, 3, 5]\n", - "\n", - "experimental_opts = {}\n", - "experimental_opts[\"resilience\"] = {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\n", - " \"noise_factors\": noise_factors,\n", - " \"amplifier\": \"gate_folding\",\n", - " \"extrapolated_noise_factors\": [0] + noise_factors,\n", - " },\n", - "}\n", - "experimental_opts[\"twirling\"] = {\n", - " \"num_randomizations\": num_randomizations,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"strategy\": \"active-accum\",\n", - "}\n", - "\n", - "# We define and run the estimator to obtain , and on all qubits\n", - "estimator = Estimator(mode=backend, options=experimental_opts)\n", - "\n", - "job = estimator.run([pub_x, pub_y, pub_z])" - ] - }, - { - "cell_type": "markdown", - "id": "b84baa9d-22a7-410e-a424-2b34e57ff96e", - "metadata": {}, - "source": [ - "Next we retrieve the results." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7a56eda8-3fb2-43e9-8a82-ec64be05b699", - "metadata": {}, - "outputs": [], - "source": [ - "job_result_x = job.result()[0].data.evs\n", - "job_result_y = job.result()[1].data.evs\n", - "job_result_z = job.result()[2].data.evs" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "1bf21466-ac70-4172-841b-08cedf835645", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ 3.67865951e-03 1.01158571e-02 -3.95790878e-02 6.33984326e-03\n", - " 1.86035759e-02 -2.91533268e-02 -1.06374793e-01 4.48873518e-18\n", - " 4.70201764e-02 3.53997968e-02 2.53130819e-02 3.23903401e-02\n", - " 6.06327843e-03 1.16313667e-02 -1.12387504e-02 -3.18457725e-02\n", - " -4.16445718e-04 -1.45609602e-03 -4.21737114e-01 2.83705669e-02\n", - " 6.91332890e-03 -7.45363001e-02 -1.20139326e-02 -8.85566135e-02\n", - " -3.22648394e-02 -3.24228074e-02 6.20431299e-04 3.04225434e-03\n", - " 5.72795792e-03 1.11288428e-02 1.50395861e-01 9.18380197e-02\n", - " 1.02553163e-01 2.98312847e-02 -3.30298912e-01 -1.13979648e-01\n", - " 4.49159340e-03 8.63861493e-02 3.05666566e-02 2.21463145e-04\n", - " 1.45946735e-02 8.54537275e-03 -8.09805979e-02 -2.92608104e-02\n", - " -3.91243644e-02 -3.96632760e-02 -1.41187613e-01 -1.07363243e-01\n", - " 1.81089440e-02 2.70778895e-02 1.45139414e-02 2.99480458e-02\n", - " 4.99137134e-02 7.08789852e-02 4.30565759e-02 8.71287156e-02\n", - " 1.04334798e-01 7.72191962e-02 7.10059720e-02 1.04650403e-01]\n", - "[-7.31765102e-05 7.42669174e-03 9.82277344e-03 5.92638249e-02\n", - " 4.24120486e-02 -9.06473416e-03 4.55057675e-03 8.43494094e-03\n", - " 6.92097339e-02 -6.82234424e-02 6.13509008e-02 3.94200491e-02\n", - " -1.24037979e-02 1.01976642e-01 7.90538600e-03 -7.19726160e-02\n", - " -1.19501703e-16 -1.03796614e-02 7.37382463e-02 1.97238568e-01\n", - " -3.59250635e-02 -2.67554009e-02 3.55010633e-02 7.68877990e-02\n", - " 6.50677589e-05 -6.59298767e-03 -1.23719487e-02 -6.41938151e-02\n", - " 1.95603072e-02 -2.48448551e-02 5.17784810e-02 -5.93767100e-02\n", - " 3.11897681e-02 -3.91959720e-18 -4.47769148e-03 1.39202197e-01\n", - " -6.56387523e-02 -5.85665483e-02 9.52905894e-03 -8.61460731e-02\n", - " 3.91790656e-02 -1.27544375e-01 1.63712244e-01 3.36816934e-04\n", - " 2.26230028e-02 -2.45023393e-05 4.95635588e-03 1.44779564e-01\n", - " 3.71625177e-02 3.65675948e-03 2.83694017e-02 -7.10500602e-02\n", - " -1.15467702e-01 6.21712129e-03 -4.80958959e-02 2.21021066e-02\n", - " 7.99062499e-02 -1.87164076e-02 -3.67100369e-02 -2.38923731e-02]\n", - "[ 6.85871605e-01 5.07725024e-01 8.71024642e-03 3.34823455e-02\n", - " 4.58684961e-02 9.44384189e-17 -4.46829296e-02 -2.91296778e-02\n", - " 4.15466461e-02 2.89628330e-02 1.88624017e-03 5.37110446e-02\n", - " 2.59579053e-03 1.39327071e-02 -2.90781778e-02 5.07209866e-03\n", - " 5.83403000e-02 2.60764440e-02 4.45999706e-17 -6.66701417e-03\n", - " 3.03215873e-01 2.26172533e-02 2.43105960e-02 4.98861041e-18\n", - " -2.45530791e-02 6.26940708e-02 1.21058073e-02 2.76675948e-04\n", - " 2.63980996e-02 2.58302364e-02 7.47856723e-02 8.42728943e-02\n", - " 5.70989097e-02 6.92955086e-02 -5.68313712e-03 1.32199452e-01\n", - " 8.90511238e-02 -3.45204621e-02 -1.05445836e-01 6.03864150e-03\n", - " 2.16291384e-02 8.22303162e-03 1.00856715e-02 6.28973151e-02\n", - " 6.26727169e-02 6.15399206e-02 9.67320897e-02 1.03045269e-16\n", - " 1.79688783e-01 -1.59960520e-02 -1.15422952e-02 9.60200470e-03\n", - " 6.58396672e-02 7.78329830e-03 6.53226955e-02 2.45778685e-03\n", - " 4.36694753e-03 5.75098762e-03 -2.48896201e-02 8.33740755e-05]\n" - ] - } - ], - "source": [ - "print(job_result_x)\n", - "print(job_result_y)\n", - "print(job_result_z)" - ] - }, - { - "cell_type": "markdown", - "id": "d2772c67-e30b-4e4c-8884-d03c88fe078f", - "metadata": {}, - "source": [ - "We print out the circuit size and the two-qubit gate depth." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4f573436-ec5c-451b-976c-ad718b3c201d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "qubits: 60\n", - "2q-depth: 64\n", - "2q-size: 1888\n", - "Operator counts: OrderedDict({'rz': 6016, 'sx': 4576, 'cz': 1888, 'x': 896, 'barrier': 31})\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(f\"qubits: {qc.num_qubits}\")\n", - "print(\n", - " f\"2q-depth: {transpiled_circuit.depth(lambda x: x.operation.num_qubits==2)}\"\n", - ")\n", - "print(\n", - " f\"2q-size: {transpiled_circuit.size(lambda x: x.operation.num_qubits==2)}\"\n", - ")\n", - "print(f\"Operator counts: {transpiled_circuit.count_ops()}\")\n", - "transpiled_circuit.draw(\"mpl\", fold=-1, style=\"clifford\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "b03a7f5a-4dae-4773-b372-fe04570ad2cd", - "metadata": {}, - "source": [ - "We can now loop over the entire training dataset to obtain all 1-RDMs." - ] - }, - { - "cell_type": "markdown", - "id": "eabb625e-edc1-445f-ad14-5e472d8a2879", - "metadata": {}, - "source": [ - "We also provide the results from an experiment that we ran on quantum hardware. You can either run the training yourself by setting the flag below to `True`, or use the projection results that we provide." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81835ec2-210f-4176-ba79-c8046cc57d92", - "metadata": {}, - "outputs": [], - "source": [ - "# Set this to True if you want to run the training on hardware\n", - "run_experiment = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "932201c0-178b-4a98-b2dc-5b4c81953d49", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Training data progress: 100%|██████████| 172/172 [13:03<00:00, 4.55s/it]\n" - ] - } - ], - "source": [ - "# Identity operator on all qubits\n", - "id = \"I\" * num_qubits\n", - "\n", - "# projections_train[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2: Z)\n", - "# of datapoint i on qubit k\n", - "projections_train = []\n", - "jobs_train = []\n", - "\n", - "# Experiment options for error mitigation\n", - "num_randomizations = 300\n", - "shots_per_randomization = 100\n", - "noise_factors = [1, 3, 5]\n", - "\n", - "experimental_opts = {}\n", - "experimental_opts[\"resilience\"] = {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\n", - " \"noise_factors\": noise_factors,\n", - " \"amplifier\": \"gate_folding\",\n", - " \"return_all_extrapolated\": True,\n", - " \"return_unextrapolated\": True,\n", - " \"extrapolated_noise_factors\": [0] + noise_factors,\n", - " },\n", - "}\n", - "experimental_opts[\"twirling\"] = {\n", - " \"num_randomizations\": num_randomizations,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"strategy\": \"active-accum\",\n", - "}\n", - "options = EstimatorOptions(experimental=experimental_opts)\n", - "\n", - "if run_experiment:\n", - " with Batch(backend=backend):\n", - " for i in tqdm.tqdm(\n", - " range(len(train_data)), desc=\"Training data progress\"\n", - " ):\n", - " # Get training sample\n", - " parameters = train_data[i]\n", - "\n", - " # Bind parameter to the circuit and simplify it\n", - " qc_bound = qc.assign_parameters(parameters)\n", - " transpiler = generate_preset_pass_manager(\n", - " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", - " )\n", - " transpiled_circuit = transpiler.run(qc_bound)\n", - "\n", - " # Transpile for hardware\n", - " transpiler = generate_preset_pass_manager(\n", - " optimization_level=3, target=target\n", - " )\n", - " transpiled_circuit = transpiler.run(transpiled_circuit)\n", - "\n", - " # We group all commuting observables\n", - " # These groups are the Pauli X, Y and Z operators on individual qubits\n", - " observables_x = [\n", - " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - " observables_y = [\n", - " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - " observables_z = [\n", - " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - "\n", - " # We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", - " # set of observables and the circuit parameters\n", - " pub_x = (transpiled_circuit, observables_x)\n", - " pub_y = (transpiled_circuit, observables_y)\n", - " pub_z = (transpiled_circuit, observables_z)\n", - "\n", - " # We define and run the estimator to obtain , and on all qubits\n", - " estimator = Estimator(options=options)\n", - "\n", - " job = estimator.run([pub_x, pub_y, pub_z])\n", - " jobs_train.append(job)" - ] - }, - { - "cell_type": "markdown", - "id": "b401f9a9-c2a2-4454-9708-c53e0cbf122b", - "metadata": {}, - "source": [ - "Once the jobs are complete, we can retrieve the results." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ccbd7603-1dd3-4aab-8ee8-8b0a98068b61", - "metadata": {}, - "outputs": [], - "source": [ - "if run_experiment:\n", - " for i in tqdm.tqdm(\n", - " range(len(train_data)), desc=\"Retrieving training data results\"\n", - " ):\n", - " # Completed job\n", - " job = jobs_train[i]\n", - "\n", - " # Job results\n", - " job_result_x = job.result()[0].data.evs\n", - " job_result_y = job.result()[1].data.evs\n", - " job_result_z = job.result()[2].data.evs\n", - "\n", - " # Record , and on all qubits for the current datapoint\n", - " projections_train.append([job_result_x, job_result_y, job_result_z])" - ] - }, - { - "cell_type": "markdown", - "id": "b03a29c5-c504-4ba9-9eda-4c8bc8817492", - "metadata": {}, - "source": [ - "We repeat this for the test set." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f9e77a9c-d295-4893-aebe-74cc59168e1f", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Test data progress: 100%|██████████| 74/74 [00:13<00:00, 5.56it/s]\n" - ] - } - ], - "source": [ - "# Identity operator on all qubits\n", - "id = \"I\" * num_qubits\n", - "\n", - "# projections_test[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2: Z)\n", - "# of datapoint i on qubit k\n", - "projections_test = []\n", - "jobs_test = []\n", - "\n", - "# Experiment options for error mitigation\n", - "num_randomizations = 300\n", - "shots_per_randomization = 100\n", - "noise_factors = [1, 3, 5]\n", - "\n", - "experimental_opts = {}\n", - "experimental_opts[\"resilience\"] = {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\n", - " \"noise_factors\": noise_factors,\n", - " \"amplifier\": \"gate_folding\",\n", - " \"return_all_extrapolated\": True,\n", - " \"return_unextrapolated\": True,\n", - " \"extrapolated_noise_factors\": [0] + noise_factors,\n", - " },\n", - "}\n", - "experimental_opts[\"twirling\"] = {\n", - " \"num_randomizations\": num_randomizations,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"strategy\": \"active-accum\",\n", - "}\n", - "options = EstimatorOptions(experimental=experimental_opts)\n", - "\n", - "if run_experiment:\n", - " with Batch(backend=backend):\n", - " for i in tqdm.tqdm(range(len(test_data)), desc=\"Test data progress\"):\n", - " # Get test sample\n", - " parameters = test_data[i]\n", - "\n", - " # Bind parameter to the circuit and simplify it\n", - " qc_bound = qc.assign_parameters(parameters)\n", - " transpiler = generate_preset_pass_manager(\n", - " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", - " )\n", - " transpiled_circuit = transpiler.run(qc_bound)\n", - "\n", - " # Transpile for hardware\n", - " transpiler = generate_preset_pass_manager(\n", - " optimization_level=3, target=target\n", - " )\n", - " transpiled_circuit = transpiler.run(transpiled_circuit)\n", - "\n", - " # We group all commuting observables\n", - " # These groups are the Pauli X, Y and Z operators on individual qubits\n", - " observables_x = [\n", - " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - " observables_y = [\n", - " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - " observables_z = [\n", - " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", - " transpiled_circuit.layout\n", - " )\n", - " for i in range(num_qubits)\n", - " ]\n", - "\n", - " # We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", - " # set of observables and the circuit parameters\n", - " pub_x = (transpiled_circuit, observables_x)\n", - " pub_y = (transpiled_circuit, observables_y)\n", - " pub_z = (transpiled_circuit, observables_z)\n", - "\n", - " # We define and run the estimator to obtain , and on all qubits\n", - " estimator = Estimator(options=options)\n", - "\n", - " job = estimator.run([pub_x, pub_y, pub_z])\n", - " jobs_test.append(job)" - ] - }, - { - "cell_type": "markdown", - "id": "7190af84-d419-4dad-b566-0d66c96e320d", - "metadata": {}, - "source": [ - "We can retrieve the results as before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "721ed991-fee2-4f66-9bb6-a750ee935033", - "metadata": {}, - "outputs": [], - "source": [ - "if run_experiment:\n", - " for i in tqdm.tqdm(\n", - " range(len(test_data)), desc=\"Retrieving test data results\"\n", - " ):\n", - " # Completed job\n", - " job = jobs_test[i]\n", - "\n", - " # Job results\n", - " job_result_x = job.result()[0].data.evs\n", - " job_result_y = job.result()[1].data.evs\n", - " job_result_z = job.result()[2].data.evs\n", - "\n", - " # Record , and on all qubits for the current datapoint\n", - " projections_test.append([job_result_x, job_result_y, job_result_z])" - ] - }, - { - "cell_type": "markdown", - "id": "b87be787-8751-4093-9547-57315fa13c88", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format" - ] - }, - { - "cell_type": "markdown", - "id": "7e10f152-d20c-4c70-9530-e9e71c309b59", - "metadata": {}, - "source": [ - "### Define the projected quantum kernel" - ] - }, - { - "cell_type": "markdown", - "id": "34150c16-e6ca-43a3-989d-444104dc60e5", - "metadata": {}, - "source": [ - "The projected quantum kernel is defined with the following kernel function: $$k^{\\textrm{PQ}}(x_i, x_j) = \\textrm{exp} \\Big(-\\gamma \\sum_k \\sum_{P \\in \\{ X,Y,Z \\}} (\\textrm{Tr}[P \\rho_k(x_i)] - \\textrm{Tr}[P \\rho_k(x_j)])^2 \\Big) $$\n", - "In the above equation, $\\gamma>0$ is a tunable hyperparameter. The $K^{\\textrm{PQ}}_{ij} = k^{\\textrm{PQ}}(x_i, x_j)$ are the entries of the kernel matrix $K^{\\textrm{PQ}}$." - ] - }, - { - "cell_type": "markdown", - "id": "42cea21f-3178-4ac9-8826-db7ca7cdfe20", - "metadata": {}, - "source": [ - "Using the definition of 1-RDMs, we can see that the individual terms within the kernel function can be evaluated as $\\textrm{Tr}[P \\rho_k (x_i)] = \\braket P$, where $P \\in \\{ X,Y,Z \\}$. These expectation values are precisely what we measured above." - ] - }, - { - "cell_type": "markdown", - "id": "22e41ed2-93be-4b30-a898-8d2832d1e0ab", - "metadata": {}, - "source": [ - "By using ``scikit-learn``, we can in fact compute the kernel even more easily. This is due to the readily available radial basis function (``'rbf'``) kernel: $ \\textrm{exp} (-\\gamma \\lVert x - x' \\rVert^2)$. First, we simply need to reshape the new projected training and test datasets into two-dimensional arrays." - ] - }, - { - "cell_type": "markdown", - "id": "e01abd8a-2194-41b9-adaf-7ec80f14e3b1", - "metadata": {}, - "source": [ - "Note that going over the entire dataset can take about 80 minutes on the QPU. To make sure that the rest of the tutorial is easily executable, we additionally provide projections from a previously run experiment (which are included in the files you downloaded in the `Download dataset` code block). If you performed the training yourself, you can continue the tutorial with your own results." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a61e6c67-056b-4659-87a0-284bda432cfd", - "metadata": {}, - "outputs": [], - "source": [ - "if run_experiment:\n", - " projections_train = np.array(projections_train).reshape(\n", - " len(projections_train), -1\n", - " )\n", - " projections_test = np.array(projections_test).reshape(\n", - " len(projections_test), -1\n", - " )\n", - "else:\n", - " projections_train = np.loadtxt(\"projections_train.txt\")\n", - " projections_test = np.loadtxt(\"projections_test.txt\")" - ] - }, - { - "cell_type": "markdown", - "id": "2c90c625-27c1-46a0-80a0-5ead9d3c64b7", - "metadata": {}, - "source": [ - "### Support Vector Machine (SVM)" - ] - }, - { - "cell_type": "markdown", - "id": "b2ea7dde-63ec-4e3d-b6dc-dbeab6a07fda", - "metadata": {}, - "source": [ - "We can now run a classical SVM on this precomputed kernel, and use the kernel between test and training sets for prediction." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "d2eef986-22a5-4528-8fa0-c7dbfd586071", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n", - "The best parameters are {'C': 8.5, 'gamma': 0.01} with a score of 0.6980\n", - "Test accuracy with best model: 0.8108\n" - ] - } - ], - "source": [ - "# Range of 'C' and 'gamma' values as SVC hyperparameters\n", - "C_range = [0.001, 0.005, 0.007]\n", - "C_range.extend([x * 0.01 for x in range(1, 11)])\n", - "C_range.extend([x * 0.25 for x in range(1, 60)])\n", - "C_range.extend(\n", - " [\n", - " 20,\n", - " 50,\n", - " 100,\n", - " 200,\n", - " 500,\n", - " 700,\n", - " 1000,\n", - " 1100,\n", - " 1200,\n", - " 1300,\n", - " 1400,\n", - " 1500,\n", - " 1700,\n", - " 2000,\n", - " ]\n", - ")\n", - "\n", - "gamma_range = [\"auto\", \"scale\", 0.001, 0.005, 0.007]\n", - "gamma_range.extend([x * 0.01 for x in range(1, 11)])\n", - "gamma_range.extend([x * 0.25 for x in range(1, 60)])\n", - "gamma_range.extend([20, 50, 100])\n", - "\n", - "param_grid = dict(C=C_range, gamma=gamma_range)\n", - "\n", - "# Support vector classifier\n", - "svc = SVC(kernel=\"rbf\")\n", - "\n", - "# Define the cross validation\n", - "cv = StratifiedKFold(n_splits=10)\n", - "\n", - "# Grid search for hyperparameter tuning (q: quantum)\n", - "grid_search_q = GridSearchCV(\n", - " svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n", - ")\n", - "grid_search_q.fit(projections_train, train_labels)\n", - "\n", - "# Best model with best parameters\n", - "best_svc_q = grid_search_q.best_estimator_\n", - "print(\n", - " f\"The best parameters are {grid_search_q.best_params_} with a score of {grid_search_q.best_score_:.4f}\"\n", - ")\n", - "\n", - "# Test accuracy\n", - "accuracy_q = best_svc_q.score(projections_test, test_labels)\n", - "print(f\"Test accuracy with best model: {accuracy_q:.4f}\")" - ] - }, - { - "cell_type": "markdown", - "id": "d54cb69a-8f53-43f1-870a-af273f91e47c", - "metadata": {}, - "source": [ - "### Classical benchmarking\n", - "We can run a classical SVM with the radial basis function as the kernel without doing a quantum projection. This result is our classical benchmark." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "41867b5a-9091-4aa4-adab-a05cf6238966", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n", - "The best parameters are {'C': 10.75, 'gamma': 0.04} with a score of 0.7830\n", - "Test accuracy with best model: 0.7432\n" - ] - } - ], - "source": [ - "# Support vector classifier\n", - "svc = SVC(kernel=\"rbf\")\n", - "\n", - "# Grid search for hyperparameter tuning (c: classical)\n", - "grid_search_c = GridSearchCV(\n", - " svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n", - ")\n", - "grid_search_c.fit(train_data, train_labels)\n", - "\n", - "# Best model with best parameters\n", - "best_svc_c = grid_search_c.best_estimator_\n", - "print(\n", - " f\"The best parameters are {grid_search_c.best_params_} with a score of {grid_search_c.best_score_:.4f}\"\n", - ")\n", - "\n", - "# Test accuracy\n", - "accuracy_c = best_svc_c.score(test_data, test_labels)\n", - "print(f\"Test accuracy with best model: {accuracy_c:.4f}\")" - ] - }, - { - "cell_type": "markdown", - "id": "5b0d367f-4a15-4269-8012-9baef30422fa", - "metadata": {}, - "source": [ - "## Appendix: Verify the dataset's potential quantum advantage in learning tasks\n", - "\n", - "Not all datasets offer potential advantage from the use of PQKs. There are some theoretical bounds that one can use as a preliminary test to see if a particular dataset can benefit from PQKs. To quantify this, authors of [Power of data in quantum machine learning](https://www.nature.com/articles/s41467-021-22539-9) [2] define quantities referred to as classical and quantum model complexities and geometric separation of the classical and quantum models. To expect a potential quantum advantage from PQKs, the geometric separation between the classical and quantum-projected kernels should be approximately on the order of $\\sqrt{N}$, where $N$ is the number of training samples. If this condition is satisfied, we move on to checking the model complexities. If the classical model complexity is on the order of $N$ while the quantum-projected model complexity is substantially smaller than $N$, we can expect potential advantage from the PQK." - ] - }, - { - "cell_type": "markdown", - "id": "d8fae838-a34b-4cf5-ba98-579ec8527fde", - "metadata": {}, - "source": [ - "Geometric separation is defined as follows (F19 in [[2]](#references)):\n", - "$$g_{cq} = g(K^c \\Vert K^q) = \\sqrt{\\Vert \\sqrt{K^q} \\sqrt{K^c} (K^c + \\lambda I)^{-2} \\sqrt{K^c} \\sqrt{K^q}\\Vert_{\\infty}}$$" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "bc67f5c0-5d79-4633-807e-02b8cc9d39f5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Geometric separation between classical and quantum kernels is 1.5440\n", - "13.114877048604\n" - ] - } - ], - "source": [ - "# Gamma values used in best models above\n", - "gamma_c = grid_search_c.best_params_[\"gamma\"]\n", - "gamma_q = grid_search_q.best_params_[\"gamma\"]\n", - "\n", - "# Regularization parameter used in the best classical model above\n", - "C_c = grid_search_c.best_params_[\"C\"]\n", - "l_c = 1 / C_c\n", - "\n", - "# Classical and quantum kernels used above\n", - "K_c = rbf_kernel(train_data, train_data, gamma=gamma_c)\n", - "K_q = rbf_kernel(projections_train, projections_train, gamma=gamma_q)\n", - "\n", - "# Intermediate matrices in the equation\n", - "K_c_sqrt = sqrtm(K_c)\n", - "K_q_sqrt = sqrtm(K_q)\n", - "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n", - "K_multiplication = (\n", - " K_q_sqrt @ K_c_sqrt @ K_c_inv @ K_c_inv @ K_c_sqrt @ K_q_sqrt\n", - ")\n", - "\n", - "# Geometric separation\n", - "norm = np.linalg.norm(K_multiplication, ord=np.inf)\n", - "g_cq = np.sqrt(norm)\n", - "print(\n", - " f\"Geometric separation between classical and quantum kernels is {g_cq:.4f}\"\n", - ")\n", - "\n", - "print(np.sqrt(len(train_data)))" - ] - }, - { - "cell_type": "markdown", - "id": "68ff51a9-d52e-4103-982b-c81bae17d6a9", - "metadata": {}, - "source": [ - "Model complexity is defined as follows (M1 in [[2]](#references)):\n", - "$$ s_{K, \\lambda}(N) = \\sqrt{\\frac{\\lambda^2 \\sum_{i=1}^N \\sum_{j=1}^N (K+\\lambda I)^{-2}_{ij} y_i y_j}{N}} + \\sqrt{\\frac{\\sum_{i=1}^N \\sum_{j=1}^N ((K+\\lambda I)^{-1}K(K+\\lambda I)^{-1})_{ij} y_i y_j}{N}}$$" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "877cf344-7324-4154-b0d9-7bcfa03b6ac0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Classical model complexity is 1.3578\n" - ] - } - ], - "source": [ - "# Model complexity of the classical kernel\n", - "\n", - "# Number of training data\n", - "N = len(train_data)\n", - "\n", - "# Predicted labels\n", - "pred_labels = best_svc_c.predict(train_data)\n", - "pred_matrix = np.outer(pred_labels, pred_labels)\n", - "\n", - "# Intermediate terms\n", - "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n", - "\n", - "# First term\n", - "first_sum = np.sum((K_c_inv @ K_c_inv) * pred_matrix)\n", - "first_term = l_c * np.sqrt(first_sum / N)\n", - "\n", - "# Second term\n", - "second_sum = np.sum((K_c_inv @ K_c @ K_c_inv) * pred_matrix)\n", - "second_term = np.sqrt(second_sum / N)\n", - "\n", - "# Model complexity\n", - "s_c = first_term + second_term\n", - "print(f\"Classical model complexity is {s_c:.4f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "90e9de08-cafc-4493-9358-581c148f3447", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Quantum model complexity is 1.5806\n" - ] - } - ], - "source": [ - "# Model complexity of the projected quantum kernel\n", - "\n", - "# Number of training data\n", - "N = len(projections_train)\n", - "\n", - "# Predicted labels\n", - "pred_labels = best_svc_q.predict(projections_train)\n", - "pred_matrix = np.outer(pred_labels, pred_labels)\n", - "\n", - "# Regularization parameter used in the best classical model above\n", - "C_q = grid_search_q.best_params_[\"C\"]\n", - "l_q = 1 / C_q\n", - "\n", - "# Intermediate terms\n", - "K_q_inv = inv(K_q + l_q * np.eye(K_q.shape[0]))\n", - "\n", - "# First term\n", - "first_sum = np.sum((K_q_inv @ K_q_inv) * pred_matrix)\n", - "first_term = l_q * np.sqrt(first_sum / N)\n", - "\n", - "# Second term\n", - "second_sum = np.sum((K_q_inv @ K_q @ K_q_inv) * pred_matrix)\n", - "second_term = np.sqrt(second_sum / N)\n", - "\n", - "# Model complexity\n", - "s_q = first_term + second_term\n", - "print(f\"Quantum model complexity is {s_q:.4f}\")" - ] - }, - { - "cell_type": "markdown", - "id": "f082899c-b763-4df0-a81c-5efb3ca43451", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "1. Utro, Filippo, et al. \"[Enhanced Prediction of CAR T-Cell Cytotoxicity with Quantum-Kernel Methods](https://arxiv.org/abs/2507.22710).\" arXiv preprint arXiv:2507.22710 (2025).\n", - "2. Huang, Hsin-Yuan, et al. \"[Power of data in quantum machine learning](https://www.nature.com/articles/s41467-021-22539-9).\" Nature communications 12.1 (2021): 2631.\n", - "3. Daniels, Kyle G., et al. \"[Decoding CAR T cell phenotype using combinatorial signaling motif libraries and machine learning](https://www.science.org/doi/full/10.1126/science.abq0225).\" Science 378.6625 (2022): 1194-1200." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c77f1777-ecb8-4cb0-9bf2-49d7c989b12a", + "metadata": {}, + "source": [ + "---\n", + "title: Enhance feature classification using projected quantum kernels\n", + "description: Tutorial on enhancing feature classification using projected quantum kernels\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore braket sqrtm cytotoxicity Nalm Cytotoxicity binarize Utro Filippo Hsin */}" + ] + }, + { + "cell_type": "markdown", + "id": "bee84331-1f3b-4a23-b691-4d3f7f51e76b", + "metadata": {}, + "source": [ + "# Enhance feature classification using projected quantum kernels\n", + "\n", + "*Usage estimate: 80 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*\n", + "\n", + "In this tutorial, we demonstrate how to run a [projected quantum kernel](https://www.nature.com/articles/s41467-021-22539-9) (PQK) with Qiskit on a real-world biological dataset, based on the paper [Enhanced Prediction of CAR T-Cell Cytotoxicity with Quantum-Kernel Methods](https://arxiv.org/abs/2507.22710) [[1]](#references).\n", + "\n", + "PQK is a method used in quantum machine learning (QML) to encode classical data into a quantum feature space and project them back into the classical domain, by using quantum computers to enhance feature selection. It involves encoding classical data into quantum states using a quantum circuit, typically through a process called feature mapping, where the data is transformed into a high-dimensional Hilbert space. The \"projected\" aspect refers to extracting classical information from the quantum states, by measuring specific observables, to construct a kernel matrix that can be used in classical kernel-based algorithms like support vector machines. This approach leverages the computational advantages of quantum systems to potentially achieve better performance on certain tasks compared to classical methods.\n", + "\n", + "This tutorial also assumes general familiarity with QML methods. For further exploration of QML, refer to the [Quantum machine learning](/learning/courses/quantum-machine-learning) course in IBM Quantum Learning." + ] + }, + { + "cell_type": "markdown", + "id": "542b8075-3b8c-476c-9513-c03de0f162b1", + "metadata": {}, + "source": [ + "### Requirements\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.40 or later (`pip install qiskit-ibm-runtime`)\n", + "- Category encoders 2.8.1 (`pip install category-encoders`)\n", + "- NumPy 2.3.2 (`pip install numpy`)\n", + "- Pandas 2.3.2 (`pip install pandas`)\n", + "- Scikit-learn 1.7.1 (`pip install scikit-learn`)\n", + "- Tqdm 4.67.1 (`pip install tqdm`)" + ] + }, + { + "cell_type": "markdown", + "id": "2c676996-1361-4b3a-9c94-4784376097b0", + "metadata": {}, + "source": [ + "### Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe8a02d3-994a-45fe-823d-5e68ded0d717", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "# Standard libraries\n", + "import os\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "# Machine learning and data processing\n", + "import category_encoders as ce\n", + "from scipy.linalg import inv, sqrtm\n", + "from sklearn.metrics.pairwise import rbf_kernel\n", + "from sklearn.model_selection import GridSearchCV, StratifiedKFold\n", + "from sklearn.svm import SVC\n", + "\n", + "# Qiskit and Qiskit Runtime\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import ParameterVector\n", + "from qiskit.circuit.library import UnitaryGate, ZZFeatureMap\n", + "from qiskit.quantum_info import SparsePauliOp, random_unitary\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import (\n", + " Batch,\n", + " EstimatorOptions,\n", + " EstimatorV2 as Estimator,\n", + " QiskitRuntimeService,\n", + ")\n", + "\n", + "# Progress bar\n", + "import tqdm\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "id": "2f8775c8-81b5-4732-8325-3f12dc96b45d", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem" + ] + }, + { + "cell_type": "markdown", + "id": "cb3db2e7-8a3d-4ab1-9e3b-ffc5587dee6d", + "metadata": {}, + "source": [ + "### Dataset preparation\n", + "\n", + "In this tutorial we use a real-world biological dataset for a binary classification task, which is generated by Daniels et al. (2022) and can be downloaded from the [supplementary material](https://www.science.org/doi/full/10.1126/science.abq0225#supplementary-materials) included with the paper. The data consists of CAR T-cells, which are genetically engineered T-cells used in immunotherapy to treat certain cancers. T-cells, a type of immune cell, are modified in a lab to express chimeric antigen receptors (CARs) that target specific proteins on cancer cells. These modified T-cells can recognize and destroy cancer cells more effectively. The data features are the CAR T-cell motifs, which refer to the specific structural or functional component of the CAR engineered into T-cells. Based on these motifs, our task is to predict the cytotoxicity of a given CAR T-cell, labeling it as either toxic or non-toxic." + ] + }, + { + "cell_type": "markdown", + "id": "a2ab0503-1cc9-4512-8f06-12223219cc3e", + "metadata": {}, + "source": [ + "The following shows the helper functions to preprocess this dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f1ce0222-e7c7-451c-a11c-e61425a6bb8e", + "metadata": {}, + "outputs": [], + "source": [ + "def preprocess_data(dir_root, args):\n", + " \"\"\"\n", + " Preprocess the training and test data.\n", + " \"\"\"\n", + " # Read from the csv files\n", + " train_data = pd.read_csv(\n", + " os.path.join(dir_root, args[\"file_train_data\"]),\n", + " encoding=\"unicode_escape\",\n", + " sep=\",\",\n", + " )\n", + " test_data = pd.read_csv(\n", + " os.path.join(dir_root, args[\"file_test_data\"]),\n", + " encoding=\"unicode_escape\",\n", + " sep=\",\",\n", + " )\n", + "\n", + " # Fix the last motif ID\n", + " train_data[train_data == 17] = 14\n", + " train_data.columns = [\n", + " \"Cell Number\",\n", + " \"motif\",\n", + " \"motif.1\",\n", + " \"motif.2\",\n", + " \"motif.3\",\n", + " \"motif.4\",\n", + " \"Nalm 6 Cytotoxicity\",\n", + " ]\n", + " test_data[test_data == 17] = 14\n", + " test_data.columns = [\n", + " \"Cell Number\",\n", + " \"motif\",\n", + " \"motif.1\",\n", + " \"motif.2\",\n", + " \"motif.3\",\n", + " \"motif.4\",\n", + " \"Nalm 6 Cytotoxicity\",\n", + " ]\n", + "\n", + " # Adjust motif at the third position\n", + " if args[\"filter_for_spacer_motif_third_position\"]:\n", + " train_data = train_data[\n", + " (train_data[\"motif.2\"] == 14) | (train_data[\"motif.2\"] == 0)\n", + " ]\n", + " test_data = test_data[\n", + " (test_data[\"motif.2\"] == 14) | (test_data[\"motif.2\"] == 0)\n", + " ]\n", + "\n", + " train_data = train_data[\n", + " args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n", + " ]\n", + " test_data = test_data[\n", + " args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n", + " ]\n", + "\n", + " # Adjust motif at the last position\n", + " if not args[\"allow_spacer_motif_last_position\"]:\n", + " last_motif = args[\"motifs_to_use\"][len(args[\"motifs_to_use\"]) - 1]\n", + " train_data = train_data[\n", + " (train_data[last_motif] != 14) & (train_data[last_motif] != 0)\n", + " ]\n", + " test_data = test_data[\n", + " (test_data[last_motif] != 14) & (test_data[last_motif] != 0)\n", + " ]\n", + "\n", + " # Get the labels\n", + " train_labels = np.array(train_data[args[\"label_name\"]])\n", + " test_labels = np.array(test_data[args[\"label_name\"]])\n", + "\n", + " # For the classification task use the threshold to binarize labels\n", + " train_labels[train_labels > args[\"label_binarization_threshold\"]] = 1\n", + " train_labels[train_labels < 1] = args[\"min_label_value\"]\n", + " test_labels[test_labels > args[\"label_binarization_threshold\"]] = 1\n", + " test_labels[test_labels < 1] = args[\"min_label_value\"]\n", + "\n", + " # Reduce data to just the motifs of interest\n", + " train_data = train_data[args[\"motifs_to_use\"]]\n", + " test_data = test_data[args[\"motifs_to_use\"]]\n", + "\n", + " # Get the class and motif counts\n", + " min_class = np.min(np.unique(np.concatenate([train_data, test_data])))\n", + " max_class = np.max(np.unique(np.concatenate([train_data, test_data])))\n", + "\n", + " num_class = max_class - min_class + 1\n", + " num_motifs = len(args[\"motifs_to_use\"])\n", + " print(str(max_class) + \":\" + str(min_class) + \":\" + str(num_class))\n", + "\n", + " train_data = train_data - min_class\n", + " test_data = test_data - min_class\n", + "\n", + " return (\n", + " train_data,\n", + " test_data,\n", + " train_labels,\n", + " test_labels,\n", + " num_class,\n", + " num_motifs,\n", + " )\n", + "\n", + "\n", + "def data_encoder(args, train_data, test_data, num_class, num_motifs):\n", + " \"\"\"\n", + " Use one-hot or binary encoding for classical data representation.\n", + " \"\"\"\n", + " if args[\"encoder\"] == \"one-hot\":\n", + " # Transform to one-hot encoding\n", + " train_data = np.eye(num_class)[train_data]\n", + " test_data = np.eye(num_class)[test_data]\n", + "\n", + " train_data = train_data.reshape(\n", + " train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n", + " )\n", + " test_data = test_data.reshape(\n", + " test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n", + " )\n", + "\n", + " elif args[\"encoder\"] == \"binary\":\n", + " # Transform to binary encoding\n", + " encoder = ce.BinaryEncoder()\n", + "\n", + " base_array = np.unique(np.concatenate([train_data, test_data]))\n", + " base = pd.DataFrame(base_array).astype(\"category\")\n", + " base.columns = [\"motif\"]\n", + " for motif_name in args[\"motifs_to_use\"][1:]:\n", + " base[motif_name] = base.loc[:, \"motif\"]\n", + " encoder.fit(base)\n", + "\n", + " train_data = encoder.transform(train_data.astype(\"category\"))\n", + " test_data = encoder.transform(test_data.astype(\"category\"))\n", + "\n", + " train_data = np.reshape(\n", + " train_data.values, (train_data.shape[0], num_motifs, -1)\n", + " )\n", + " test_data = np.reshape(\n", + " test_data.values, (test_data.shape[0], num_motifs, -1)\n", + " )\n", + "\n", + " train_data = train_data.reshape(\n", + " train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n", + " )\n", + " test_data = test_data.reshape(\n", + " test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n", + " )\n", + "\n", + " else:\n", + " raise ValueError(\"Invalid encoding type.\")\n", + "\n", + " return train_data, test_data" + ] + }, + { + "cell_type": "markdown", + "id": "d72e1c86-4855-4a5a-865b-14ba6b15afb7", + "metadata": {}, + "source": [ + "You can run this tutorial by running the following cell, which automatically creates the required folder structure and downloads both the training and test files directly into your environment. If you already have these files locally, this step will safely overwrite them to ensure version consistency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84495ee1-880a-48cb-a904-d83396e8b29e", + "metadata": {}, + "outputs": [], + "source": [ + "## Download dataset\n", + "\n", + "# Create data directory if it doesn't exist\n", + "!mkdir -p data_tutorial/pqk\n", + "\n", + "# Download the training and test sets from the official Qiskit documentation repo\n", + "!wget -q --show-progress -O data_tutorial/pqk/train_data.csv \\\n", + " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/train_data.csv\n", + "\n", + "!wget -q --show-progress -O data_tutorial/pqk/test_data.csv \\\n", + " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/test_data.csv\n", + "\n", + "!wget -q --show-progress -O data_tutorial/pqk/projections_train.csv \\\n", + " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_train.csv\n", + "\n", + "!wget -q --show-progress -O data_tutorial/pqk/projections_test.csv \\\n", + " https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_test.csv\n", + "\n", + "# Check the files have been downloaded\n", + "!echo \"Dataset files downloaded:\"\n", + "!ls -lh data_tutorial/pqk/*.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35013bc5-6b5e-44c8-8a8c-3af313b00a82", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14:0:15\n" + ] + } + ], + "source": [ + "args = {\n", + " \"file_train_data\": \"train_data.csv\",\n", + " \"file_test_data\": \"test_data.csv\",\n", + " \"motifs_to_use\": [\"motif\", \"motif.1\", \"motif.2\", \"motif.3\"],\n", + " \"label_name\": \"Nalm 6 Cytotoxicity\",\n", + " \"label_binarization_threshold\": 0.62,\n", + " \"filter_for_spacer_motif_third_position\": False,\n", + " \"allow_spacer_motif_last_position\": True,\n", + " \"min_label_value\": -1,\n", + " \"encoder\": \"one-hot\",\n", + "}\n", + "dir_root = \"./\"\n", + "\n", + "# Preprocess data\n", + "train_data, test_data, train_labels, test_labels, num_class, num_motifs = (\n", + " preprocess_data(dir_root=dir_root, args=args)\n", + ")\n", + "\n", + "# Encode the data\n", + "train_data, test_data = data_encoder(\n", + " args, train_data, test_data, num_class, num_motifs\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "050515c2-64fe-43ce-8559-b58db58b76c3", + "metadata": {}, + "source": [ + "We also transform the dataset such that $1$ is represented as $\\pi/2$ for scaling purposes." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d40d9a0f-67d0-4704-8a94-cc0a466ffc92", + "metadata": {}, + "outputs": [], + "source": [ + "# Change 1 to pi/2\n", + "angle = np.pi / 2\n", + "\n", + "tmp = pd.DataFrame(train_data).astype(\"float64\")\n", + "tmp[tmp == 1] = angle\n", + "train_data = tmp.values\n", + "\n", + "tmp = pd.DataFrame(test_data).astype(\"float64\")\n", + "tmp[tmp == 1] = angle\n", + "test_data = tmp.values" + ] + }, + { + "cell_type": "markdown", + "id": "726fbdfc-677b-41b7-86c8-dc11adb1946c", + "metadata": {}, + "source": [ + "We verify sizes and shapes of the training and test datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b98495f3-aeaa-4df1-a9fe-433e26aa7d4e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(172, 60) (172,)\n", + "(74, 60) (74,)\n" + ] + } + ], + "source": [ + "print(train_data.shape, train_labels.shape)\n", + "print(test_data.shape, test_labels.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "0c828dc0-9bd1-44bc-b299-303766ae3d37", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution" + ] + }, + { + "cell_type": "markdown", + "id": "55afd26f-7a53-43bc-920d-88160a61688e", + "metadata": {}, + "source": [ + "### Quantum circuit\n", + "\n", + "We now construct the feature map that embeds our classical dataset into a higher-dimensional feature space. For this embedding, we use the [``ZZFeatureMap``](/docs/api/qiskit/qiskit.circuit.library.ZZFeatureMap) from Qiskit." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "45956df4-5472-4394-a3e1-5514c456791d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "feature_dimension = train_data.shape[1]\n", + "reps = 24\n", + "insert_barriers = True\n", + "entanglement = \"pairwise\"\n", + "\n", + "# ZZFeatureMap with linear entanglement and a repetition of 2\n", + "embed = ZZFeatureMap(\n", + " feature_dimension=feature_dimension,\n", + " reps=reps,\n", + " entanglement=entanglement,\n", + " insert_barriers=insert_barriers,\n", + " name=\"ZZFeatureMap\",\n", + ")\n", + "embed.decompose().draw(output=\"mpl\", style=\"iqp\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "bae2e554-2ca2-4e71-a313-b6aec0b25e6a", + "metadata": {}, + "source": [ + "Another quantum embedding option is the 1D-Heisenberg Hamiltonian evolution ansatz. You can skip running this section if you would like to continue with the `ZZFeatureMap`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "659dbf23-fd3f-4e01-94b4-33e6d672172c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "feature_dimension = train_data.shape[1]\n", + "num_qubits = feature_dimension + 1\n", + "embed2 = QuantumCircuit(num_qubits)\n", + "num_trotter_steps = 6\n", + "pv_length = feature_dimension * num_trotter_steps\n", + "pv = ParameterVector(\"theta\", pv_length)\n", + "\n", + "# Add Haar random single qubit unitary to each qubit as initial state\n", + "np.random.seed(42)\n", + "seeds_unitary = np.random.randint(0, 100, num_qubits)\n", + "for i in range(num_qubits):\n", + " rand_gate = UnitaryGate(random_unitary(2, seed=seeds_unitary[i]))\n", + " embed2.append(rand_gate, [i])\n", + "\n", + "\n", + "def trotter_circ(feature_dimension, num_trotter_steps):\n", + " num_qubits = feature_dimension + 1\n", + " circ = QuantumCircuit(num_qubits)\n", + " # Even\n", + " for i in range(0, feature_dimension, 2):\n", + " circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " for i in range(0, feature_dimension, 2):\n", + " circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " for i in range(0, feature_dimension, 2):\n", + " circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " # Odd\n", + " for i in range(1, feature_dimension, 2):\n", + " circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " for i in range(1, feature_dimension, 2):\n", + " circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " for i in range(1, feature_dimension, 2):\n", + " circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n", + " return circ\n", + "\n", + "\n", + "# Hamiltonian evolution ansatz\n", + "for step in range(num_trotter_steps):\n", + " circ = trotter_circ(feature_dimension, num_trotter_steps)\n", + " if step % 2 == 0:\n", + " embed2 = embed2.compose(circ)\n", + " else:\n", + " reverse_circ = circ.reverse_ops()\n", + " embed2 = embed2.compose(reverse_circ)\n", + "\n", + "\n", + "embed2.draw(output=\"mpl\", style=\"iqp\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "8b27a3f8-5ee9-41b5-a430-25247545cbb9", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives" + ] + }, + { + "cell_type": "markdown", + "id": "f202d995-8fc0-4af5-b11e-2ed72ac48c84", + "metadata": {}, + "source": [ + "### Measure 1-RDMs\n", + "\n", + "The main building blocks of projected quantum kernels are the reduced density matrices (RDMs), which are obtained though projective measurements of the quantum feature map. In this step, we obtain all single-qubit reduced density matrices (1-RDMs), which will later be provided into the classical exponential kernel function." + ] + }, + { + "cell_type": "markdown", + "id": "ea823c34-d7d7-42f8-989c-dfe78cdd489a", + "metadata": {}, + "source": [ + "Let's look at how to compute the 1-RDM given a single data point from the dataset before we run over all data. The 1-RDMs are a collection of single-qubit measurements of Pauli ``X``, ``Y`` and ``Z`` operators on all qubits. This is because a single-qubit RDM can be fully expressed as: $$\\rho = \\frac{1}{2} \\big( I + \\braket \\sigma_x \\sigma_x + \\braket \\sigma_y \\sigma_y + \\braket \\sigma_z \\sigma_z \\big)$$" + ] + }, + { + "cell_type": "markdown", + "id": "daa7608c-f482-4df5-a2be-6ca9625bb7e0", + "metadata": {}, + "source": [ + "First we select the backend to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1ab9cea-42ef-478c-bb9d-02ed4cf23ea6", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=133\n", + ")\n", + "target = backend.target" + ] + }, + { + "cell_type": "markdown", + "id": "2ce0917f-3826-477b-b503-57e3b5e7e290", + "metadata": {}, + "source": [ + "Then we run the quantum circuit and measure the projections. Note that we turn on error mitigation, including Zero Noise Extrapolation (ZNE)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53b20cec-ef8a-4fdb-aeed-46546a32ea96", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's select the ZZFeatureMap embedding for this example\n", + "qc = embed\n", + "num_qubits = feature_dimension\n", + "\n", + "# Identity operator on all qubits\n", + "id = \"I\" * num_qubits\n", + "\n", + "# Let's select the first training datapoint as an example\n", + "parameters = train_data[0]\n", + "\n", + "# Bind parameter to the circuit and simplify it\n", + "qc_bound = qc.assign_parameters(parameters)\n", + "transpiler = generate_preset_pass_manager(\n", + " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", + ")\n", + "transpiled_circuit = transpiler.run(qc_bound)\n", + "\n", + "# Transpile for hardware\n", + "transpiler = generate_preset_pass_manager(optimization_level=3, target=target)\n", + "transpiled_circuit = transpiler.run(transpiled_circuit)\n", + "\n", + "# We group all commuting observables\n", + "# These groups are the Pauli X, Y and Z operators on individual qubits\n", + "observables_x = [\n", + " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + "]\n", + "observables_y = [\n", + " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + "]\n", + "observables_z = [\n", + " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + "]\n", + "\n", + "# We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", + "# set of observables and the circuit parameters\n", + "pub_x = (transpiled_circuit, observables_x)\n", + "pub_y = (transpiled_circuit, observables_y)\n", + "pub_z = (transpiled_circuit, observables_z)\n", + "\n", + "# Experiment options for error mitigation\n", + "num_randomizations = 300\n", + "shots_per_randomization = 100\n", + "noise_factors = [1, 3, 5]\n", + "\n", + "experimental_opts = {}\n", + "experimental_opts[\"resilience\"] = {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\n", + " \"noise_factors\": noise_factors,\n", + " \"amplifier\": \"gate_folding\",\n", + " \"extrapolated_noise_factors\": [0] + noise_factors,\n", + " },\n", + "}\n", + "experimental_opts[\"twirling\"] = {\n", + " \"num_randomizations\": num_randomizations,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"strategy\": \"active-accum\",\n", + "}\n", + "\n", + "# We define and run the estimator to obtain , and on all qubits\n", + "estimator = Estimator(mode=backend, options=experimental_opts)\n", + "\n", + "job = estimator.run([pub_x, pub_y, pub_z])" + ] + }, + { + "cell_type": "markdown", + "id": "b84baa9d-22a7-410e-a424-2b34e57ff96e", + "metadata": {}, + "source": [ + "Next we retrieve the results." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7a56eda8-3fb2-43e9-8a82-ec64be05b699", + "metadata": {}, + "outputs": [], + "source": [ + "job_result_x = job.result()[0].data.evs\n", + "job_result_y = job.result()[1].data.evs\n", + "job_result_z = job.result()[2].data.evs" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1bf21466-ac70-4172-841b-08cedf835645", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 3.67865951e-03 1.01158571e-02 -3.95790878e-02 6.33984326e-03\n", + " 1.86035759e-02 -2.91533268e-02 -1.06374793e-01 4.48873518e-18\n", + " 4.70201764e-02 3.53997968e-02 2.53130819e-02 3.23903401e-02\n", + " 6.06327843e-03 1.16313667e-02 -1.12387504e-02 -3.18457725e-02\n", + " -4.16445718e-04 -1.45609602e-03 -4.21737114e-01 2.83705669e-02\n", + " 6.91332890e-03 -7.45363001e-02 -1.20139326e-02 -8.85566135e-02\n", + " -3.22648394e-02 -3.24228074e-02 6.20431299e-04 3.04225434e-03\n", + " 5.72795792e-03 1.11288428e-02 1.50395861e-01 9.18380197e-02\n", + " 1.02553163e-01 2.98312847e-02 -3.30298912e-01 -1.13979648e-01\n", + " 4.49159340e-03 8.63861493e-02 3.05666566e-02 2.21463145e-04\n", + " 1.45946735e-02 8.54537275e-03 -8.09805979e-02 -2.92608104e-02\n", + " -3.91243644e-02 -3.96632760e-02 -1.41187613e-01 -1.07363243e-01\n", + " 1.81089440e-02 2.70778895e-02 1.45139414e-02 2.99480458e-02\n", + " 4.99137134e-02 7.08789852e-02 4.30565759e-02 8.71287156e-02\n", + " 1.04334798e-01 7.72191962e-02 7.10059720e-02 1.04650403e-01]\n", + "[-7.31765102e-05 7.42669174e-03 9.82277344e-03 5.92638249e-02\n", + " 4.24120486e-02 -9.06473416e-03 4.55057675e-03 8.43494094e-03\n", + " 6.92097339e-02 -6.82234424e-02 6.13509008e-02 3.94200491e-02\n", + " -1.24037979e-02 1.01976642e-01 7.90538600e-03 -7.19726160e-02\n", + " -1.19501703e-16 -1.03796614e-02 7.37382463e-02 1.97238568e-01\n", + " -3.59250635e-02 -2.67554009e-02 3.55010633e-02 7.68877990e-02\n", + " 6.50677589e-05 -6.59298767e-03 -1.23719487e-02 -6.41938151e-02\n", + " 1.95603072e-02 -2.48448551e-02 5.17784810e-02 -5.93767100e-02\n", + " 3.11897681e-02 -3.91959720e-18 -4.47769148e-03 1.39202197e-01\n", + " -6.56387523e-02 -5.85665483e-02 9.52905894e-03 -8.61460731e-02\n", + " 3.91790656e-02 -1.27544375e-01 1.63712244e-01 3.36816934e-04\n", + " 2.26230028e-02 -2.45023393e-05 4.95635588e-03 1.44779564e-01\n", + " 3.71625177e-02 3.65675948e-03 2.83694017e-02 -7.10500602e-02\n", + " -1.15467702e-01 6.21712129e-03 -4.80958959e-02 2.21021066e-02\n", + " 7.99062499e-02 -1.87164076e-02 -3.67100369e-02 -2.38923731e-02]\n", + "[ 6.85871605e-01 5.07725024e-01 8.71024642e-03 3.34823455e-02\n", + " 4.58684961e-02 9.44384189e-17 -4.46829296e-02 -2.91296778e-02\n", + " 4.15466461e-02 2.89628330e-02 1.88624017e-03 5.37110446e-02\n", + " 2.59579053e-03 1.39327071e-02 -2.90781778e-02 5.07209866e-03\n", + " 5.83403000e-02 2.60764440e-02 4.45999706e-17 -6.66701417e-03\n", + " 3.03215873e-01 2.26172533e-02 2.43105960e-02 4.98861041e-18\n", + " -2.45530791e-02 6.26940708e-02 1.21058073e-02 2.76675948e-04\n", + " 2.63980996e-02 2.58302364e-02 7.47856723e-02 8.42728943e-02\n", + " 5.70989097e-02 6.92955086e-02 -5.68313712e-03 1.32199452e-01\n", + " 8.90511238e-02 -3.45204621e-02 -1.05445836e-01 6.03864150e-03\n", + " 2.16291384e-02 8.22303162e-03 1.00856715e-02 6.28973151e-02\n", + " 6.26727169e-02 6.15399206e-02 9.67320897e-02 1.03045269e-16\n", + " 1.79688783e-01 -1.59960520e-02 -1.15422952e-02 9.60200470e-03\n", + " 6.58396672e-02 7.78329830e-03 6.53226955e-02 2.45778685e-03\n", + " 4.36694753e-03 5.75098762e-03 -2.48896201e-02 8.33740755e-05]\n" + ] + } + ], + "source": [ + "print(job_result_x)\n", + "print(job_result_y)\n", + "print(job_result_z)" + ] + }, + { + "cell_type": "markdown", + "id": "d2772c67-e30b-4e4c-8884-d03c88fe078f", + "metadata": {}, + "source": [ + "We print out the circuit size and the two-qubit gate depth." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4f573436-ec5c-451b-976c-ad718b3c201d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qubits: 60\n", + "2q-depth: 64\n", + "2q-size: 1888\n", + "Operator counts: OrderedDict({'rz': 6016, 'sx': 4576, 'cz': 1888, 'x': 896, 'barrier': 31})\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(f\"qubits: {qc.num_qubits}\")\n", + "print(\n", + " f\"2q-depth: {transpiled_circuit.depth(lambda x: x.operation.num_qubits==2)}\"\n", + ")\n", + "print(\n", + " f\"2q-size: {transpiled_circuit.size(lambda x: x.operation.num_qubits==2)}\"\n", + ")\n", + "print(f\"Operator counts: {transpiled_circuit.count_ops()}\")\n", + "transpiled_circuit.draw(\"mpl\", fold=-1, style=\"clifford\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "b03a7f5a-4dae-4773-b372-fe04570ad2cd", + "metadata": {}, + "source": [ + "We can now loop over the entire training dataset to obtain all 1-RDMs." + ] + }, + { + "cell_type": "markdown", + "id": "eabb625e-edc1-445f-ad14-5e472d8a2879", + "metadata": {}, + "source": [ + "We also provide the results from an experiment that we ran on quantum hardware. You can either run the training yourself by setting the flag below to `True`, or use the projection results that we provide." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81835ec2-210f-4176-ba79-c8046cc57d92", + "metadata": {}, + "outputs": [], + "source": [ + "# Set this to True if you want to run the training on hardware\n", + "run_experiment = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "932201c0-178b-4a98-b2dc-5b4c81953d49", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Training data progress: 100%|██████████| 172/172 [13:03<00:00, 4.55s/it]\n" + ] + } + ], + "source": [ + "# Identity operator on all qubits\n", + "id = \"I\" * num_qubits\n", + "\n", + "# projections_train[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y,\n", + "# 2: Z)\n", + "# of datapoint i on qubit k\n", + "projections_train = []\n", + "jobs_train = []\n", + "\n", + "# Experiment options for error mitigation\n", + "num_randomizations = 300\n", + "shots_per_randomization = 100\n", + "noise_factors = [1, 3, 5]\n", + "\n", + "experimental_opts = {}\n", + "experimental_opts[\"resilience\"] = {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\n", + " \"noise_factors\": noise_factors,\n", + " \"amplifier\": \"gate_folding\",\n", + " \"return_all_extrapolated\": True,\n", + " \"return_unextrapolated\": True,\n", + " \"extrapolated_noise_factors\": [0] + noise_factors,\n", + " },\n", + "}\n", + "experimental_opts[\"twirling\"] = {\n", + " \"num_randomizations\": num_randomizations,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"strategy\": \"active-accum\",\n", + "}\n", + "options = EstimatorOptions(experimental=experimental_opts)\n", + "\n", + "if run_experiment:\n", + " with Batch(backend=backend):\n", + " for i in tqdm.tqdm(\n", + " range(len(train_data)), desc=\"Training data progress\"\n", + " ):\n", + " # Get training sample\n", + " parameters = train_data[i]\n", + "\n", + " # Bind parameter to the circuit and simplify it\n", + " qc_bound = qc.assign_parameters(parameters)\n", + " transpiler = generate_preset_pass_manager(\n", + " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", + " )\n", + " transpiled_circuit = transpiler.run(qc_bound)\n", + "\n", + " # Transpile for hardware\n", + " transpiler = generate_preset_pass_manager(\n", + " optimization_level=3, target=target\n", + " )\n", + " transpiled_circuit = transpiler.run(transpiled_circuit)\n", + "\n", + " # We group all commuting observables\n", + " # These groups are the Pauli X, Y and Z operators on individual qubits\n", + " observables_x = [\n", + " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + " observables_y = [\n", + " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + " observables_z = [\n", + " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + "\n", + " # We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", + " # set of observables and the circuit parameters\n", + " pub_x = (transpiled_circuit, observables_x)\n", + " pub_y = (transpiled_circuit, observables_y)\n", + " pub_z = (transpiled_circuit, observables_z)\n", + "\n", + " # We define and run the estimator to obtain , and on all qubits\n", + " estimator = Estimator(options=options)\n", + "\n", + " job = estimator.run([pub_x, pub_y, pub_z])\n", + " jobs_train.append(job)" + ] + }, + { + "cell_type": "markdown", + "id": "b401f9a9-c2a2-4454-9708-c53e0cbf122b", + "metadata": {}, + "source": [ + "Once the jobs are complete, we can retrieve the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccbd7603-1dd3-4aab-8ee8-8b0a98068b61", + "metadata": {}, + "outputs": [], + "source": [ + "if run_experiment:\n", + " for i in tqdm.tqdm(\n", + " range(len(train_data)), desc=\"Retrieving training data results\"\n", + " ):\n", + " # Completed job\n", + " job = jobs_train[i]\n", + "\n", + " # Job results\n", + " job_result_x = job.result()[0].data.evs\n", + " job_result_y = job.result()[1].data.evs\n", + " job_result_z = job.result()[2].data.evs\n", + "\n", + " # Record , and on all qubits for the current datapoint\n", + " projections_train.append([job_result_x, job_result_y, job_result_z])" + ] + }, + { + "cell_type": "markdown", + "id": "b03a29c5-c504-4ba9-9eda-4c8bc8817492", + "metadata": {}, + "source": [ + "We repeat this for the test set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9e77a9c-d295-4893-aebe-74cc59168e1f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Test data progress: 100%|██████████| 74/74 [00:13<00:00, 5.56it/s]\n" + ] + } + ], + "source": [ + "# Identity operator on all qubits\n", + "id = \"I\" * num_qubits\n", + "\n", + "# projections_test[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2:\n", + "# Z)\n", + "# of datapoint i on qubit k\n", + "projections_test = []\n", + "jobs_test = []\n", + "\n", + "# Experiment options for error mitigation\n", + "num_randomizations = 300\n", + "shots_per_randomization = 100\n", + "noise_factors = [1, 3, 5]\n", + "\n", + "experimental_opts = {}\n", + "experimental_opts[\"resilience\"] = {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\n", + " \"noise_factors\": noise_factors,\n", + " \"amplifier\": \"gate_folding\",\n", + " \"return_all_extrapolated\": True,\n", + " \"return_unextrapolated\": True,\n", + " \"extrapolated_noise_factors\": [0] + noise_factors,\n", + " },\n", + "}\n", + "experimental_opts[\"twirling\"] = {\n", + " \"num_randomizations\": num_randomizations,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"strategy\": \"active-accum\",\n", + "}\n", + "options = EstimatorOptions(experimental=experimental_opts)\n", + "\n", + "if run_experiment:\n", + " with Batch(backend=backend):\n", + " for i in tqdm.tqdm(range(len(test_data)), desc=\"Test data progress\"):\n", + " # Get test sample\n", + " parameters = test_data[i]\n", + "\n", + " # Bind parameter to the circuit and simplify it\n", + " qc_bound = qc.assign_parameters(parameters)\n", + " transpiler = generate_preset_pass_manager(\n", + " optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n", + " )\n", + " transpiled_circuit = transpiler.run(qc_bound)\n", + "\n", + " # Transpile for hardware\n", + " transpiler = generate_preset_pass_manager(\n", + " optimization_level=3, target=target\n", + " )\n", + " transpiled_circuit = transpiler.run(transpiled_circuit)\n", + "\n", + " # We group all commuting observables\n", + " # These groups are the Pauli X, Y and Z operators on individual qubits\n", + " observables_x = [\n", + " SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + " observables_y = [\n", + " SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + " observables_z = [\n", + " SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n", + " transpiled_circuit.layout\n", + " )\n", + " for i in range(num_qubits)\n", + " ]\n", + "\n", + " # We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n", + " # set of observables and the circuit parameters\n", + " pub_x = (transpiled_circuit, observables_x)\n", + " pub_y = (transpiled_circuit, observables_y)\n", + " pub_z = (transpiled_circuit, observables_z)\n", + "\n", + " # We define and run the estimator to obtain , and on all qubits\n", + " estimator = Estimator(options=options)\n", + "\n", + " job = estimator.run([pub_x, pub_y, pub_z])\n", + " jobs_test.append(job)" + ] + }, + { + "cell_type": "markdown", + "id": "7190af84-d419-4dad-b566-0d66c96e320d", + "metadata": {}, + "source": [ + "We can retrieve the results as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "721ed991-fee2-4f66-9bb6-a750ee935033", + "metadata": {}, + "outputs": [], + "source": [ + "if run_experiment:\n", + " for i in tqdm.tqdm(\n", + " range(len(test_data)), desc=\"Retrieving test data results\"\n", + " ):\n", + " # Completed job\n", + " job = jobs_test[i]\n", + "\n", + " # Job results\n", + " job_result_x = job.result()[0].data.evs\n", + " job_result_y = job.result()[1].data.evs\n", + " job_result_z = job.result()[2].data.evs\n", + "\n", + " # Record , and on all qubits for the current datapoint\n", + " projections_test.append([job_result_x, job_result_y, job_result_z])" + ] + }, + { + "cell_type": "markdown", + "id": "b87be787-8751-4093-9547-57315fa13c88", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format" + ] + }, + { + "cell_type": "markdown", + "id": "7e10f152-d20c-4c70-9530-e9e71c309b59", + "metadata": {}, + "source": [ + "### Define the projected quantum kernel" + ] + }, + { + "cell_type": "markdown", + "id": "34150c16-e6ca-43a3-989d-444104dc60e5", + "metadata": {}, + "source": [ + "The projected quantum kernel is defined with the following kernel function: $$k^{\\textrm{PQ}}(x_i, x_j) = \\textrm{exp} \\Big(-\\gamma \\sum_k \\sum_{P \\in \\{ X,Y,Z \\}} (\\textrm{Tr}[P \\rho_k(x_i)] - \\textrm{Tr}[P \\rho_k(x_j)])^2 \\Big) $$\n", + "In the above equation, $\\gamma>0$ is a tunable hyperparameter. The $K^{\\textrm{PQ}}_{ij} = k^{\\textrm{PQ}}(x_i, x_j)$ are the entries of the kernel matrix $K^{\\textrm{PQ}}$." + ] + }, + { + "cell_type": "markdown", + "id": "42cea21f-3178-4ac9-8826-db7ca7cdfe20", + "metadata": {}, + "source": [ + "Using the definition of 1-RDMs, we can see that the individual terms within the kernel function can be evaluated as $\\textrm{Tr}[P \\rho_k (x_i)] = \\braket P$, where $P \\in \\{ X,Y,Z \\}$. These expectation values are precisely what we measured above." + ] + }, + { + "cell_type": "markdown", + "id": "22e41ed2-93be-4b30-a898-8d2832d1e0ab", + "metadata": {}, + "source": [ + "By using ``scikit-learn``, we can in fact compute the kernel even more easily. This is due to the readily available radial basis function (``'rbf'``) kernel: $ \\textrm{exp} (-\\gamma \\lVert x - x' \\rVert^2)$. First, we simply need to reshape the new projected training and test datasets into two-dimensional arrays." + ] + }, + { + "cell_type": "markdown", + "id": "e01abd8a-2194-41b9-adaf-7ec80f14e3b1", + "metadata": {}, + "source": [ + "Note that going over the entire dataset can take about 80 minutes on the QPU. To make sure that the rest of the tutorial is easily executable, we additionally provide projections from a previously run experiment (which are included in the files you downloaded in the `Download dataset` code block). If you performed the training yourself, you can continue the tutorial with your own results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a61e6c67-056b-4659-87a0-284bda432cfd", + "metadata": {}, + "outputs": [], + "source": [ + "if run_experiment:\n", + " projections_train = np.array(projections_train).reshape(\n", + " len(projections_train), -1\n", + " )\n", + " projections_test = np.array(projections_test).reshape(\n", + " len(projections_test), -1\n", + " )\n", + "else:\n", + " projections_train = np.loadtxt(\"projections_train.txt\")\n", + " projections_test = np.loadtxt(\"projections_test.txt\")" + ] + }, + { + "cell_type": "markdown", + "id": "2c90c625-27c1-46a0-80a0-5ead9d3c64b7", + "metadata": {}, + "source": [ + "### Support Vector Machine (SVM)" + ] + }, + { + "cell_type": "markdown", + "id": "b2ea7dde-63ec-4e3d-b6dc-dbeab6a07fda", + "metadata": {}, + "source": [ + "We can now run a classical SVM on this precomputed kernel, and use the kernel between test and training sets for prediction." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d2eef986-22a5-4528-8fa0-c7dbfd586071", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n", + "The best parameters are {'C': 8.5, 'gamma': 0.01} with a score of 0.6980\n", + "Test accuracy with best model: 0.8108\n" + ] + } + ], + "source": [ + "# Range of 'C' and 'gamma' values as SVC hyperparameters\n", + "C_range = [0.001, 0.005, 0.007]\n", + "C_range.extend([x * 0.01 for x in range(1, 11)])\n", + "C_range.extend([x * 0.25 for x in range(1, 60)])\n", + "C_range.extend(\n", + " [\n", + " 20,\n", + " 50,\n", + " 100,\n", + " 200,\n", + " 500,\n", + " 700,\n", + " 1000,\n", + " 1100,\n", + " 1200,\n", + " 1300,\n", + " 1400,\n", + " 1500,\n", + " 1700,\n", + " 2000,\n", + " ]\n", + ")\n", + "\n", + "gamma_range = [\"auto\", \"scale\", 0.001, 0.005, 0.007]\n", + "gamma_range.extend([x * 0.01 for x in range(1, 11)])\n", + "gamma_range.extend([x * 0.25 for x in range(1, 60)])\n", + "gamma_range.extend([20, 50, 100])\n", + "\n", + "param_grid = dict(C=C_range, gamma=gamma_range)\n", + "\n", + "# Support vector classifier\n", + "svc = SVC(kernel=\"rbf\")\n", + "\n", + "# Define the cross validation\n", + "cv = StratifiedKFold(n_splits=10)\n", + "\n", + "# Grid search for hyperparameter tuning (q: quantum)\n", + "grid_search_q = GridSearchCV(\n", + " svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n", + ")\n", + "grid_search_q.fit(projections_train, train_labels)\n", + "\n", + "# Best model with best parameters\n", + "best_svc_q = grid_search_q.best_estimator_\n", + "print(\n", + " f\"The best parameters are {grid_search_q.best_params_} with a score of {grid_search_q.best_score_:.4f}\"\n", + ")\n", + "\n", + "# Test accuracy\n", + "accuracy_q = best_svc_q.score(projections_test, test_labels)\n", + "print(f\"Test accuracy with best model: {accuracy_q:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d54cb69a-8f53-43f1-870a-af273f91e47c", + "metadata": {}, + "source": [ + "### Classical benchmarking\n", + "We can run a classical SVM with the radial basis function as the kernel without doing a quantum projection. This result is our classical benchmark." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "41867b5a-9091-4aa4-adab-a05cf6238966", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n", + "The best parameters are {'C': 10.75, 'gamma': 0.04} with a score of 0.7830\n", + "Test accuracy with best model: 0.7432\n" + ] + } + ], + "source": [ + "# Support vector classifier\n", + "svc = SVC(kernel=\"rbf\")\n", + "\n", + "# Grid search for hyperparameter tuning (c: classical)\n", + "grid_search_c = GridSearchCV(\n", + " svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n", + ")\n", + "grid_search_c.fit(train_data, train_labels)\n", + "\n", + "# Best model with best parameters\n", + "best_svc_c = grid_search_c.best_estimator_\n", + "print(\n", + " f\"The best parameters are {grid_search_c.best_params_} with a score of {grid_search_c.best_score_:.4f}\"\n", + ")\n", + "\n", + "# Test accuracy\n", + "accuracy_c = best_svc_c.score(test_data, test_labels)\n", + "print(f\"Test accuracy with best model: {accuracy_c:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5b0d367f-4a15-4269-8012-9baef30422fa", + "metadata": {}, + "source": [ + "## Appendix: Verify the dataset's potential quantum advantage in learning tasks\n", + "\n", + "Not all datasets offer potential advantage from the use of PQKs. There are some theoretical bounds that one can use as a preliminary test to see if a particular dataset can benefit from PQKs. To quantify this, authors of [Power of data in quantum machine learning](https://www.nature.com/articles/s41467-021-22539-9) [2] define quantities referred to as classical and quantum model complexities and geometric separation of the classical and quantum models. To expect a potential quantum advantage from PQKs, the geometric separation between the classical and quantum-projected kernels should be approximately on the order of $\\sqrt{N}$, where $N$ is the number of training samples. If this condition is satisfied, we move on to checking the model complexities. If the classical model complexity is on the order of $N$ while the quantum-projected model complexity is substantially smaller than $N$, we can expect potential advantage from the PQK." + ] + }, + { + "cell_type": "markdown", + "id": "d8fae838-a34b-4cf5-ba98-579ec8527fde", + "metadata": {}, + "source": [ + "Geometric separation is defined as follows (F19 in [[2]](#references)):\n", + "$$g_{cq} = g(K^c \\Vert K^q) = \\sqrt{\\Vert \\sqrt{K^q} \\sqrt{K^c} (K^c + \\lambda I)^{-2} \\sqrt{K^c} \\sqrt{K^q}\\Vert_{\\infty}}$$" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "bc67f5c0-5d79-4633-807e-02b8cc9d39f5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Geometric separation between classical and quantum kernels is 1.5440\n", + "13.114877048604\n" + ] + } + ], + "source": [ + "# Gamma values used in best models above\n", + "gamma_c = grid_search_c.best_params_[\"gamma\"]\n", + "gamma_q = grid_search_q.best_params_[\"gamma\"]\n", + "\n", + "# Regularization parameter used in the best classical model above\n", + "C_c = grid_search_c.best_params_[\"C\"]\n", + "l_c = 1 / C_c\n", + "\n", + "# Classical and quantum kernels used above\n", + "K_c = rbf_kernel(train_data, train_data, gamma=gamma_c)\n", + "K_q = rbf_kernel(projections_train, projections_train, gamma=gamma_q)\n", + "\n", + "# Intermediate matrices in the equation\n", + "K_c_sqrt = sqrtm(K_c)\n", + "K_q_sqrt = sqrtm(K_q)\n", + "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n", + "K_multiplication = (\n", + " K_q_sqrt @ K_c_sqrt @ K_c_inv @ K_c_inv @ K_c_sqrt @ K_q_sqrt\n", + ")\n", + "\n", + "# Geometric separation\n", + "norm = np.linalg.norm(K_multiplication, ord=np.inf)\n", + "g_cq = np.sqrt(norm)\n", + "print(\n", + " f\"Geometric separation between classical and quantum kernels is {g_cq:.4f}\"\n", + ")\n", + "\n", + "print(np.sqrt(len(train_data)))" + ] + }, + { + "cell_type": "markdown", + "id": "68ff51a9-d52e-4103-982b-c81bae17d6a9", + "metadata": {}, + "source": [ + "Model complexity is defined as follows (M1 in [[2]](#references)):\n", + "$$ s_{K, \\lambda}(N) = \\sqrt{\\frac{\\lambda^2 \\sum_{i=1}^N \\sum_{j=1}^N (K+\\lambda I)^{-2}_{ij} y_i y_j}{N}} + \\sqrt{\\frac{\\sum_{i=1}^N \\sum_{j=1}^N ((K+\\lambda I)^{-1}K(K+\\lambda I)^{-1})_{ij} y_i y_j}{N}}$$" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "877cf344-7324-4154-b0d9-7bcfa03b6ac0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Classical model complexity is 1.3578\n" + ] + } + ], + "source": [ + "# Model complexity of the classical kernel\n", + "\n", + "# Number of training data\n", + "N = len(train_data)\n", + "\n", + "# Predicted labels\n", + "pred_labels = best_svc_c.predict(train_data)\n", + "pred_matrix = np.outer(pred_labels, pred_labels)\n", + "\n", + "# Intermediate terms\n", + "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n", + "\n", + "# First term\n", + "first_sum = np.sum((K_c_inv @ K_c_inv) * pred_matrix)\n", + "first_term = l_c * np.sqrt(first_sum / N)\n", + "\n", + "# Second term\n", + "second_sum = np.sum((K_c_inv @ K_c @ K_c_inv) * pred_matrix)\n", + "second_term = np.sqrt(second_sum / N)\n", + "\n", + "# Model complexity\n", + "s_c = first_term + second_term\n", + "print(f\"Classical model complexity is {s_c:.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "90e9de08-cafc-4493-9358-581c148f3447", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quantum model complexity is 1.5806\n" + ] + } + ], + "source": [ + "# Model complexity of the projected quantum kernel\n", + "\n", + "# Number of training data\n", + "N = len(projections_train)\n", + "\n", + "# Predicted labels\n", + "pred_labels = best_svc_q.predict(projections_train)\n", + "pred_matrix = np.outer(pred_labels, pred_labels)\n", + "\n", + "# Regularization parameter used in the best classical model above\n", + "C_q = grid_search_q.best_params_[\"C\"]\n", + "l_q = 1 / C_q\n", + "\n", + "# Intermediate terms\n", + "K_q_inv = inv(K_q + l_q * np.eye(K_q.shape[0]))\n", + "\n", + "# First term\n", + "first_sum = np.sum((K_q_inv @ K_q_inv) * pred_matrix)\n", + "first_term = l_q * np.sqrt(first_sum / N)\n", + "\n", + "# Second term\n", + "second_sum = np.sum((K_q_inv @ K_q @ K_q_inv) * pred_matrix)\n", + "second_term = np.sqrt(second_sum / N)\n", + "\n", + "# Model complexity\n", + "s_q = first_term + second_term\n", + "print(f\"Quantum model complexity is {s_q:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f082899c-b763-4df0-a81c-5efb3ca43451", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1. Utro, Filippo, et al. \"[Enhanced Prediction of CAR T-Cell Cytotoxicity with Quantum-Kernel Methods](https://arxiv.org/abs/2507.22710).\" arXiv preprint arXiv:2507.22710 (2025).\n", + "2. Huang, Hsin-Yuan, et al. \"[Power of data in quantum machine learning](https://www.nature.com/articles/s41467-021-22539-9).\" Nature communications 12.1 (2021): 2631.\n", + "3. Daniels, Kyle G., et al. \"[Decoding CAR T cell phenotype using combinatorial signaling motif libraries and machine learning](https://www.science.org/doi/full/10.1126/science.abq0225).\" Science 378.6625 (2022): 1194-1200." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/find_long_comments.py b/find_long_comments.py new file mode 100644 index 00000000000..8f91202cee8 --- /dev/null +++ b/find_long_comments.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Script to find comments longer than 100 characters in Jupyter notebook (.ipynb) files. +""" + +import json +import os +import re +from pathlib import Path +from typing import List, Tuple, Dict + +def find_long_comments_in_code(code: str, max_length: int = 100) -> List[Tuple[int, str, int]]: + """ + Find comments longer than max_length in Python code. + + Returns: + List of tuples: (line_number, comment_text, actual_length) + """ + long_comments = [] + lines = code.split('\n') + + for line_num, line in enumerate(lines, start=1): + # Strip leading whitespace to check comment + stripped = line.lstrip() + + # Check for single-line comments + if stripped.startswith('#'): + # Get the full line length (including indentation) + line_length = len(line) + if line_length > max_length: + long_comments.append((line_num, line, line_length)) + + return long_comments + +def scan_notebook(notebook_path: Path, max_length: int = 100) -> Dict: + """ + Scan a Jupyter notebook for long comments. + + Returns: + Dictionary with notebook info and found long comments + """ + try: + with open(notebook_path, 'r', encoding='utf-8') as f: + notebook = json.load(f) + + results = { + 'path': str(notebook_path), + 'long_comments': [] + } + + # Iterate through cells + for cell_idx, cell in enumerate(notebook.get('cells', [])): + if cell.get('cell_type') == 'code': + # Get source code + source = cell.get('source', []) + if isinstance(source, list): + code = ''.join(source) + else: + code = source + + # Find long comments + long_comments = find_long_comments_in_code(code, max_length) + + for line_num, comment, length in long_comments: + results['long_comments'].append({ + 'cell_index': cell_idx, + 'line_in_cell': line_num, + 'comment': comment, + 'length': length + }) + + return results + + except Exception as e: + return { + 'path': str(notebook_path), + 'error': str(e), + 'long_comments': [] + } + +def find_all_notebooks(root_dir: Path) -> List[Path]: + """Find all .ipynb files in the directory tree.""" + return list(root_dir.rglob('*.ipynb')) + +def main(): + """Main function to scan all notebooks and report findings.""" + # Get the current directory + root_dir = Path('.') + + print("Scanning for .ipynb files with comments longer than 100 characters...") + print("=" * 80) + + # Find all notebooks + notebooks = find_all_notebooks(root_dir) + print(f"\nFound {len(notebooks)} notebook files to scan.\n") + + # Scan each notebook + all_results = [] + total_long_comments = 0 + + for notebook_path in notebooks: + results = scan_notebook(notebook_path) + if results['long_comments']: + all_results.append(results) + total_long_comments += len(results['long_comments']) + + # Print results + if all_results: + print(f"Found {total_long_comments} comments exceeding 100 characters in {len(all_results)} files:\n") + + for result in all_results: + print(f"\n{'=' * 80}") + print(f"File: {result['path']}") + print(f"{'=' * 80}") + + for comment_info in result['long_comments']: + print(f"\n Cell {comment_info['cell_index']}, Line {comment_info['line_in_cell']}:") + print(f" Length: {comment_info['length']} characters") + print(f" Comment: {comment_info['comment']}") + print(f" {'-' * 76}") + + # Save to file + output_file = 'long_comments_report.json' + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(all_results, f, indent=2) + + print(f"\n{'=' * 80}") + print(f"Full report saved to: {output_file}") + print(f"Total files with long comments: {len(all_results)}") + print(f"Total long comments found: {total_long_comments}") + else: + print("✓ No comments exceeding 100 characters found!") + + print("\n" + "=" * 80) + +if __name__ == '__main__': + main() + +# Made with Bob diff --git a/fix_long_comments.py b/fix_long_comments.py new file mode 100644 index 00000000000..af39ad01b53 --- /dev/null +++ b/fix_long_comments.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Script to automatically fix comments longer than 100 characters in Jupyter notebook (.ipynb) files. +""" + +import json +import os +import re +from pathlib import Path +from typing import List, Tuple, Dict +import textwrap + +def wrap_comment(comment: str, max_length: int = 100) -> List[str]: + """ + Wrap a long comment into multiple lines, preserving indentation. + + Args: + comment: The comment line to wrap + max_length: Maximum length per line + + Returns: + List of wrapped comment lines + """ + # Get the indentation + indent = len(comment) - len(comment.lstrip()) + indent_str = comment[:indent] + + # Get the comment content (without # and leading spaces) + stripped = comment.lstrip() + if not stripped.startswith('#'): + return [comment] # Not a comment, return as-is + + # Remove the # and any spaces after it + content = stripped[1:].lstrip() + + # Calculate available width (accounting for indent, #, and a space) + available_width = max_length - indent - 2 # 2 for "# " + + if available_width < 20: # If too narrow, use a minimum width + available_width = 78 + + # Wrap the text + wrapped_lines = textwrap.wrap( + content, + width=available_width, + break_long_words=False, + break_on_hyphens=False + ) + + # Add back the indentation and comment marker + result = [] + for line in wrapped_lines: + result.append(f"{indent_str}# {line}") + + return result + +def fix_long_comments_in_code(code: str, max_length: int = 100) -> Tuple[str, int]: + """ + Fix comments longer than max_length in Python code. + + Returns: + Tuple of (fixed_code, number_of_fixes) + """ + lines = code.split('\n') + fixed_lines = [] + fixes_count = 0 + + for line in lines: + # Check if this is a comment line + stripped = line.lstrip() + if stripped.startswith('#') and len(line) > max_length: + # Wrap the comment + wrapped = wrap_comment(line, max_length) + fixed_lines.extend(wrapped) + fixes_count += 1 + else: + fixed_lines.append(line) + + return '\n'.join(fixed_lines), fixes_count + +def fix_notebook(notebook_path: Path, max_length: int = 100, dry_run: bool = False) -> Dict: + """ + Fix long comments in a Jupyter notebook. + + Args: + notebook_path: Path to the notebook + max_length: Maximum comment length + dry_run: If True, don't save changes + + Returns: + Dictionary with fix results + """ + try: + with open(notebook_path, 'r', encoding='utf-8') as f: + notebook = json.load(f) + + results = { + 'path': str(notebook_path), + 'fixes': 0, + 'cells_modified': 0 + } + + # Iterate through cells + for cell_idx, cell in enumerate(notebook.get('cells', [])): + if cell.get('cell_type') == 'code': + # Get source code + source = cell.get('source', []) + if isinstance(source, list): + code = ''.join(source) + else: + code = source + + # Fix long comments + fixed_code, fixes = fix_long_comments_in_code(code, max_length) + + if fixes > 0: + results['fixes'] += fixes + results['cells_modified'] += 1 + + # Update the cell source + # Convert back to list format (preserving newlines) + if isinstance(source, list): + # Split by newlines but keep them + fixed_lines = fixed_code.split('\n') + cell['source'] = [line + '\n' for line in fixed_lines[:-1]] + if fixed_lines[-1]: # Add last line without newline if not empty + cell['source'].append(fixed_lines[-1]) + else: + cell['source'] = fixed_code + + # Save the notebook if not dry run and fixes were made + if not dry_run and results['fixes'] > 0: + with open(notebook_path, 'w', encoding='utf-8') as f: + json.dump(notebook, f, indent=1, ensure_ascii=False) + f.write('\n') # Add trailing newline + + return results + + except Exception as e: + return { + 'path': str(notebook_path), + 'error': str(e), + 'fixes': 0, + 'cells_modified': 0 + } + +def main(): + """Main function to fix all notebooks.""" + import argparse + + parser = argparse.ArgumentParser( + description='Fix comments longer than 100 characters in Jupyter notebooks' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be fixed without making changes' + ) + parser.add_argument( + '--file', + type=str, + help='Fix a specific file instead of all notebooks' + ) + parser.add_argument( + '--exclude', + type=str, + nargs='+', + default=['.tox', '.ipynb_checkpoints', 'node_modules'], + help='Directories to exclude from scanning' + ) + + args = parser.parse_args() + + print("=" * 80) + if args.dry_run: + print("DRY RUN MODE - No changes will be made") + else: + print("FIXING long comments in .ipynb files") + print("=" * 80) + + # Get files to process + if args.file: + notebooks = [Path(args.file)] + else: + root_dir = Path('.') + all_notebooks = list(root_dir.rglob('*.ipynb')) + # Filter out excluded directories + notebooks = [ + nb for nb in all_notebooks + if not any(excluded in nb.parts for excluded in args.exclude) + ] + + print(f"\nProcessing {len(notebooks)} notebook files...\n") + + # Fix each notebook + total_fixes = 0 + total_cells = 0 + files_modified = 0 + + for notebook_path in notebooks: + results = fix_notebook(notebook_path, dry_run=args.dry_run) + + if 'error' in results: + print(f"ERROR in {results['path']}: {results['error']}") + elif results['fixes'] > 0: + files_modified += 1 + total_fixes += results['fixes'] + total_cells += results['cells_modified'] + status = "[DRY RUN]" if args.dry_run else "[FIXED]" + print(f"{status} {results['path']}") + print(f" - Fixed {results['fixes']} comments in {results['cells_modified']} cells") + + # Print summary + print("\n" + "=" * 80) + print("SUMMARY") + print("=" * 80) + print(f"Files processed: {len(notebooks)}") + print(f"Files modified: {files_modified}") + print(f"Total comments fixed: {total_fixes}") + print(f"Total cells modified: {total_cells}") + + if args.dry_run: + print("\nThis was a dry run. Run without --dry-run to apply fixes.") + else: + print("\nAll fixes have been applied!") + + print("=" * 80) + +if __name__ == '__main__': + main() + +# Made with Bob diff --git a/learning/courses/quantum-chem-with-vqe/geometry.ipynb b/learning/courses/quantum-chem-with-vqe/geometry.ipynb index 0fdf43e9651..9f27020f4b1 100644 --- a/learning/courses/quantum-chem-with-vqe/geometry.ipynb +++ b/learning/courses/quantum-chem-with-vqe/geometry.ipynb @@ -1,829 +1,830 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "de193554-b271-4295-95e4-8904f0f6ee8a", - "metadata": {}, - "source": [ - "---\n", - "title: Molecular geometry\n", - "description: In this lesson we vary the geometry of a simple molecule, minimizing the energy at each step.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore pxxr prqs nelecas mcscf chmax Dmax vmax ecore ncas Excp disp */}\n", - "\n", - "# Determining a molecular geometry\n", - "\n", - "In the previous section, we implemented VQE to determine the ground state energy of a molecule. That is a valid use of quantum computing, but even more useful would be to determine the structure of a molecule.\n", - "\n", - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Remaining with our basic example of diatomic hydrogen, the only geometric parameter to vary is the bond length. To accomplish this, we proceed as before, but using a variable in our initial molecule construction (a bond length, *x*, in the argument). This is a fairly simple change, but it does require that the variable be included in functions throughout the process, since it starts in the fermionic Hamiltonian construction and propagates through the mapping and finally to the cost function.\n", - "\n", - "First, we load some of the packages we used before and define the Cholesky function." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "0a5d39bd-8c26-404f-8057-c29e3af70df4", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "#!pip install pyscf==2.4.0\n", - "from pyscf import ao2mo, gto, mcscf, scf\n", - "\n", - "\n", - "def cholesky(V, eps):\n", - " # see https://arxiv.org/pdf/1711.02242.pdf section B2\n", - " # see https://arxiv.org/abs/1808.02625\n", - " # see https://arxiv.org/abs/2104.08957\n", - " no = V.shape[0]\n", - " chmax, ng = 20 * no, 0\n", - " W = V.reshape(no**2, no**2)\n", - " L = np.zeros((no**2, chmax))\n", - " Dmax = np.diagonal(W).copy()\n", - " nu_max = np.argmax(Dmax)\n", - " vmax = Dmax[nu_max]\n", - " while vmax > eps:\n", - " L[:, ng] = W[:, nu_max]\n", - " if ng > 0:\n", - " L[:, ng] -= np.dot(L[:, 0:ng], (L.T)[0:ng, nu_max])\n", - " L[:, ng] /= np.sqrt(vmax)\n", - " Dmax[: no**2] -= L[: no**2, ng] ** 2\n", - " ng += 1\n", - " nu_max = np.argmax(Dmax)\n", - " vmax = Dmax[nu_max]\n", - " L = L[:, :ng].reshape((no, no, ng))\n", - " print(\n", - " \"accuracy of Cholesky decomposition \",\n", - " np.abs(np.einsum(\"prg,qsg->prqs\", L, L) - V).max(),\n", - " )\n", - " return L, ng\n", - "\n", - "\n", - "def identity(n):\n", - " return SparsePauliOp.from_list([(\"I\" * n, 1)])\n", - "\n", - "\n", - "def creators_destructors(n, mapping=\"jordan_wigner\"):\n", - " c_list = []\n", - " if mapping == \"jordan_wigner\":\n", - " for p in range(n):\n", - " if p == 0:\n", - " ell, r = \"I\" * (n - 1), \"\"\n", - " elif p == n - 1:\n", - " ell, r = \"\", \"Z\" * (n - 1)\n", - " else:\n", - " ell, r = \"I\" * (n - p - 1), \"Z\" * p\n", - " cp = SparsePauliOp.from_list([(ell + \"X\" + r, 0.5), (ell + \"Y\" + r, -0.5j)])\n", - " c_list.append(cp)\n", - " else:\n", - " raise ValueError(\"Unsupported mapping.\")\n", - " d_list = [cp.adjoint() for cp in c_list]\n", - " return c_list, d_list" - ] - }, - { - "cell_type": "markdown", - "id": "40e25b54-7927-4dbb-a26f-1c6b33f7f349", - "metadata": {}, - "source": [ - "Now to define our Hamiltonian, we will use PySCF exactly as in the previous example, but now we will include a variable, `x`, to play the role of our interatomic distance. This will return the core energy, single-electron energy, and two-electron energies as before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dbd10d0c-feb1-4a86-9bd9-b61101a08b95", - "metadata": {}, - "outputs": [], - "source": [ - "def ham_terms(x: float):\n", - " distance = x\n", - " a = distance / 2\n", - " mol = gto.Mole()\n", - " mol.build(\n", - " verbose=0,\n", - " atom=[\n", - " [\"H\", (0, 0, -a)],\n", - " [\"H\", (0, 0, a)],\n", - " ],\n", - " basis=\"sto-6g\",\n", - " spin=0,\n", - " charge=0,\n", - " symmetry=\"Dooh\",\n", - " )\n", - "\n", - " # mf = scf.RHF(mol)\n", - " # mx = mcscf.CASCI(mf, ncas=2, nelecas=(1, 1))\n", - " # mx.kernel()\n", - "\n", - " mf = scf.RHF(mol)\n", - " mf.kernel()\n", - " if not mf.converged:\n", - " raise RuntimeError(f\"SCF did not converge for distance {x}\")\n", - "\n", - " mx = mcscf.CASCI(mf, ncas=2, nelecas=(1, 1))\n", - " casci_energy = mx.kernel()\n", - " if casci_energy is None:\n", - " raise RuntimeError(f\"CASCI failed for distance {x}\")\n", - "\n", - " # Other variables that might come in handy:\n", - " # active_space = range(mol.nelectron // 2 - 1, mol.nelectron // 2 + 1)\n", - " # E1 = mf.kernel()\n", - " # mo = mx.sort_mo(active_space, base=0)\n", - " # E2 = mx.kernel(mo)[:2]\n", - "\n", - " h1e, ecore = mx.get_h1eff()\n", - " h2e = ao2mo.restore(1, mx.get_h2eff(), mx.ncas)\n", - " return ecore, h1e, h2e" - ] - }, - { - "cell_type": "markdown", - "id": "db49a702-e60e-4c9b-8ff2-94aa2ada022c", - "metadata": {}, - "source": [ - "Recall that the construction above is making a fermionic Hamiltonian based on the atomic species, geometry, and electronic orbitals. Below, we map this fermionic Hamiltonian onto Pauli operators. This `build_hamiltonian` function will also include a geometric variable as an argument." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "84e6a56b-eaea-4c0a-8502-567cfc5140a2", - "metadata": {}, - "outputs": [], - "source": [ - "def build_hamiltonian(distx: float) -> SparsePauliOp:\n", - " ecore = ham_terms(distx)[0]\n", - " h1e = ham_terms(distx)[1]\n", - " h2e = ham_terms(distx)[2]\n", - "\n", - " ncas, _ = h1e.shape\n", - "\n", - " C, D = creators_destructors(2 * ncas, mapping=\"jordan_wigner\")\n", - " Exc = []\n", - " for p in range(ncas):\n", - " Excp = [C[p] @ D[p] + C[ncas + p] @ D[ncas + p]]\n", - " for r in range(p + 1, ncas):\n", - " Excp.append(\n", - " C[p] @ D[r]\n", - " + C[ncas + p] @ D[ncas + r]\n", - " + C[r] @ D[p]\n", - " + C[ncas + r] @ D[ncas + p]\n", - " )\n", - " Exc.append(Excp)\n", - "\n", - " # low-rank decomposition of the Hamiltonian\n", - " Lop, ng = cholesky(h2e, 1e-6)\n", - " t1e = h1e - 0.5 * np.einsum(\"pxxr->pr\", h2e)\n", - "\n", - " H = ecore * identity(2 * ncas)\n", - " # one-body term\n", - " for p in range(ncas):\n", - " for r in range(p, ncas):\n", - " H += t1e[p, r] * Exc[p][r - p]\n", - " # two-body term\n", - " for g in range(ng):\n", - " Lg = 0 * identity(2 * ncas)\n", - " for p in range(ncas):\n", - " for r in range(p, ncas):\n", - " Lg += Lop[p, r, g] * Exc[p][r - p]\n", - " H += 0.5 * Lg @ Lg\n", - "\n", - " return H.chop().simplify()" - ] - }, - { - "cell_type": "markdown", - "id": "ffcb2569-f832-4fa9-a8bb-d7626c2a233d", - "metadata": {}, - "source": [ - "We will load the remaining packages for running VQE itself, such as the efficient_su2 ansatz, and SciPy minimizers:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "5f8dd8e0-6dfb-4be7-af15-1591f569201c", - "metadata": {}, - "outputs": [], - "source": [ - "# General imports\n", - "\n", - "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", - "from qiskit.circuit.library import efficient_su2\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "# SciPy minimizer routine\n", - "from scipy.optimize import minimize\n", - "\n", - "# Plotting functions\n", - "\n", - "# Qiskit Runtime tools\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()" - ] - }, - { - "cell_type": "markdown", - "id": "f94c1680-9c2a-4585-a63e-a49da4eb02f3", - "metadata": {}, - "source": [ - "We will again define the cost function, but this always took a fully-built and mapped Hamiltonian as an argument, so nothing changes about this function." - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "0a80ad8c-a9cb-4cab-835d-f65131b99c87", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func(params, ansatz, H, estimator):\n", - " pub = (ansatz, [H], [params])\n", - " result = estimator.run(pubs=[pub]).result()\n", - " energy = result[0].data.evs[0]\n", - " return energy\n", - "\n", - "\n", - "# def cost_func_sim(params, ansatz, H, estimator):\n", - "# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]\n", - "# return energy" - ] - }, - { - "cell_type": "markdown", - "id": "a068e645-8dfb-4e9c-b1b1-7dd936808188", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum execution\n", - "\n", - "Because the Hamiltonian will change with each new geometry, the transpiling of the operator will change at each step. We can nevertheless define a general pass manager to be applied at each step, specific to the hardware we want to use.\n", - "\n", - "Here we will use the least busy backend available. We will use that backend as a model for our AerSimulator, allowing our simulator to mimic, for example, the noise behavior of the real backend. These noise models are not perfect, but they may help you know what to expect from real hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa07518f-a2c0-4f7a-b344-8d9576427478", - "metadata": {}, - "outputs": [], - "source": [ - "# Here, we select the least busy backend available:\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "print(backend)\n", - "# Or to select a specific real backend use the line below, and substitute 'ibm_strasbourg' for your chosen device.\n", - "# backend = service.get_backend('ibm_strasbourg')" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "e67fc84a-431b-4efd-937f-49c8b7ac3abb", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on a simulator:\n", - "# -----------\n", - "from qiskit_aer import AerSimulator\n", - "\n", - "backend_sim = AerSimulator.from_backend(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "0636c55f-f46f-46ca-acaf-72bcc2f5f663", - "metadata": {}, - "source": [ - "We import the pass manager and related packages to help us optimize our circuit. This step, and the one above it, are independent of the Hamiltonian, and so are unchanged from the previous lesson." - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "8202332e-69ca-4049-af27-e98a77f15a5d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.transpiler import PassManager\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.transpiler.passes import (\n", - " ALAPScheduleAnalysis,\n", - " PadDynamicalDecoupling,\n", - " ConstrainedReschedule,\n", - ")\n", - "from qiskit.circuit.library import XGate\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "pm.scheduling = PassManager(\n", - " [\n", - " ALAPScheduleAnalysis(target=target),\n", - " ConstrainedReschedule(\n", - " acquire_alignment=target.acquire_alignment,\n", - " pulse_alignment=target.pulse_alignment,\n", - " target=target,\n", - " ),\n", - " PadDynamicalDecoupling(\n", - " target=target,\n", - " dd_sequence=[XGate(), XGate()],\n", - " pulse_alignment=target.pulse_alignment,\n", - " ),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4932ef5e-3c81-46a1-92cb-3082399734a0", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives.\n", - "\n", - "In the code block below, we set up an array to store our outputs from each step in our interatomic distance $x$. We have chosen the range of $x$ based on our knowledge of the experimental value for the equilibrium bond length: 0.74 Angstrom. We will run this first on a simulator, and will thus be importing our Estimator (BackendEstimator) from `qiskit.primitives`. For each geometry step, we build the Hamiltonian and allow a certain number of optimization steps (here 500) using the optimizer \"cobyla\". At each geometry step, we store both the total energy and the electronic energy. Because of the high number of optimizer steps, this may take an hour or more. You may wish to modify the inputs below to reduce the required time." - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "4c41a221-8a02-4932-882b-5afcc98d1d8d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "accuracy of Cholesky decomposition 1.1102230246251565e-15\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/porter284/.pyenv/versions/3.11.12/lib/python3.11/site-packages/scipy/_lib/pyprima/common/preproc.py:68: UserWarning: COBYLA: Invalid MAXFUN; it should be at least num_vars + 2; it is set to 34\n", - " warn(f'{solver}: Invalid MAXFUN; it should be at least {min_maxfun_str}; it is set to {maxfun}')\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = 1.316011435623847\n", - "The corresponding X is:\n", - "[2.32948769 5.39918229 3.03787975 4.11789904 4.97130735 2.68662232\n", - " 1.76573151 2.48982571 5.40431972 3.65780829 1.33792786 5.48472494\n", - " 6.18738702 1.78741883 0.78195251 2.96658955 1.35827677 5.599321\n", - " 4.54850148 1.0939048 4.26158726 0.52100721 0.82318 4.76796961\n", - " 3.75795507 3.8526447 5.51100375 5.91023075 2.61494836 1.79908918\n", - " 2.65937756 5.53964148]\n", - "\n", - "-0.44791260077615314\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: 1.316011435623847\n", - " x: [ 2.329e+00 5.399e+00 ... 2.659e+00 5.540e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 5.551115123125783e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = 0.7235003672327549\n", - "The corresponding X is:\n", - "[2.56282915 5.63369524 5.58059887 4.049643 4.2021266 3.06866011\n", - " 6.01619635 1.52520776 4.35403161 0.33673958 0.32623161 1.2179545\n", - " 2.84001371 3.98956684 4.89632562 1.38303588 1.96194695 2.13182089\n", - " 0.29739166 1.77895165 3.29151585 3.54355374 4.49626674 0.95756626\n", - " 0.87103927 4.53068385 1.31051302 0.37103108 1.02961355 3.13342311\n", - " 5.65815319 2.24770604]\n", - "\n", - "-0.5994426600672451\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: 0.7235003672327549\n", - " x: [ 2.563e+00 5.634e+00 ... 5.658e+00 2.248e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 5.551115123125783e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = 0.34960914928810116\n", - "The corresponding X is:\n", - "[5.44143165 6.75955835 1.56836472 3.09522093 4.67873235 1.67071481\n", - " 0.3056494 0.65998337 1.02197668 5.21162959 0.43690354 3.56522934\n", - " 4.56033119 1.90736037 0.40863891 2.87007312 3.2516952 5.90360196\n", - " 1.99057799 5.20726456 0.74710237 6.03179202 3.80685028 0.03844391\n", - " 5.88580196 3.62233258 3.98723567 2.50591888 5.44020267 2.2792993\n", - " 5.57102303 4.46548617]\n", - "\n", - "-0.7087452725518989\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: 0.34960914928810116\n", - " x: [ 5.441e+00 6.760e+00 ... 5.571e+00 4.465e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 2.220446049250313e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = 0.10594558882184543\n", - "The corresponding X is:\n", - "[5.35675483 2.26629567 1.45430546 5.56758296 5.76309509 0.73239338\n", - " 5.1216998 3.03258872 4.33624828 1.93197674 0.5292902 3.32274987\n", - " 3.43247633 0.81490741 0.48060245 1.9944799 5.67519646 5.12534057\n", - " 0.06510627 2.52989834 6.1699519 0.94828957 5.91634548 1.5994961\n", - " 4.27902164 2.3129213 1.82353095 2.10634209 1.43740426 4.06988733\n", - " 0.59624074 4.93925418]\n", - "\n", - "-0.7760164293781545\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: 0.10594558882184543\n", - " x: [ 5.357e+00 2.266e+00 ... 5.962e-01 4.939e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.06473600797229297\n", - "The corresponding X is:\n", - "[6.07735568 0.18019501 0.20743128 4.15445985 3.59388894 5.10047555\n", - " 6.09938474 6.54707528 3.36251167 2.05475223 3.67078456 5.96010605\n", - " 2.58589996 5.2723619 3.26352977 2.47432334 3.50289983 2.06620525\n", - " 6.0946056 1.22751903 0.97320057 2.19564095 5.73174941 2.05127682\n", - " 5.73805165 3.84046105 1.84816963 2.1247504 3.11106736 2.44136052\n", - " 3.39002685 0.81596991]\n", - "\n", - "-0.8207034521437214\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.06473600797229297\n", - " x: [ 6.077e+00 1.802e-01 ... 3.390e+00 8.160e-01]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 5.551115123125783e-17\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.19562982094782935\n", - "The corresponding X is:\n", - "[-0.02184462 3.67041038 7.25918653 5.89799546 0.63583624 1.84214506\n", - " 2.84059837 5.31485182 1.6053784 0.04556618 0.32018993 -0.03884066\n", - " 0.69131496 0.24203727 1.97397262 3.59723495 0.43355775 2.30131056\n", - " 4.63482292 3.9857415 4.32320753 4.55388437 2.18753433 5.99034987\n", - " 2.50489913 0.90650534 4.82518088 2.32954849 2.29901832 5.33658863\n", - " 5.91246716 3.2405013 ]\n", - "\n", - "-0.8571013345978292\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.19562982094782935\n", - " x: [-2.184e-02 3.670e+00 ... 5.912e+00 3.241e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.2833766309947055\n", - "The corresponding X is:\n", - "[ 3.1700088 5.05055456 1.2545611 4.28751811 0.6255103 1.67526577\n", - " 5.48201473 4.83820497 7.34880059 5.99705431 4.2502643 0.32066274\n", - " 0.41001404 0.27271241 4.15682546 4.22393693 4.35148115 0.64538137\n", - " 5.26288622 5.03810489 4.62426621 4.74997689 1.09603919 0.34752466\n", - " 1.8116275 0.7474807 5.31754143 4.11181763 1.58797998 5.6299796\n", - " 3.0109383 -0.19062772]\n", - "\n", - "-0.8713513097947054\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.2833766309947055\n", - " x: [ 3.170e+00 5.051e+00 ... 3.011e+00 -1.906e-01]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.3527503628484244\n", - "The corresponding X is:\n", - "[3.90513622 4.61398739 5.92552705 1.99953405 4.82157369 1.35702441\n", - " 2.77701782 5.73612247 4.22710527 1.83463189 0.45796297 4.62509318\n", - " 0.98998668 0.11666217 3.0234641 4.54298546 0.14034033 4.15635797\n", - " 1.41257357 4.48719602 2.39365535 0.19672041 5.0763044 1.86357581\n", - " 3.657757 4.60298344 2.49769577 1.88086199 3.00108725 1.84475841\n", - " 5.24047385 4.91142914]\n", - "\n", - "-0.8819275737684243\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.3527503628484244\n", - " x: [ 3.905e+00 4.614e+00 ... 5.240e+00 4.911e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 2.7755575615628914e-17\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.4022181851996095\n", - "The corresponding X is:\n", - "[6.09453981 3.5109422 3.37216019 4.94732621 1.25662002 5.89645164\n", - " 5.06403334 2.68073141 4.40385083 1.13638366 1.73347762 6.82932871\n", - " 1.15265014 2.07145964 4.36520459 1.14960341 1.62288871 4.32315915\n", - " 5.45622821 0.93554005 3.17418483 0.47230243 1.31535502 5.77698726\n", - " 2.04927925 2.50663538 5.9706002 5.4984681 2.9421232 1.56636313\n", - " 1.09394523 4.62582 ]\n", - "\n", - "-0.8832883769450639\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.4022181851996095\n", - " x: [ 6.095e+00 3.511e+00 ... 1.094e+00 4.626e+00]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", - "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - "Number of function values = 34 Least value of F = -0.44423031870708934\n", - "The corresponding X is:\n", - "[4.05765050e+00 3.99144950e+00 3.13287593e+00 3.28855137e+00\n", - " 4.32613515e+00 4.91104512e+00 1.86521867e+00 2.18822879e+00\n", - " 6.01336171e+00 1.82501276e+00 2.64830637e+00 5.53045823e+00\n", - " 2.36110093e+00 3.98821703e+00 4.69013438e-01 4.38996815e+00\n", - " 7.78103801e-04 1.72994378e+00 2.24970934e+00 1.11978200e+00\n", - " 2.24846445e+00 4.90745512e+00 5.38474921e+00 5.03587994e+00\n", - " 3.54297277e+00 4.78147533e+00 1.25990218e+00 1.99168068e+00\n", - " 5.89203503e+00 1.77673987e+00 5.37848357e+00 5.60245198e-01]\n", - "\n", - "-0.8852113278070892\n", - " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", - " success: False\n", - " status: 3\n", - " fun: -0.44423031870708934\n", - " x: [ 4.058e+00 3.991e+00 ... 5.378e+00 5.602e-01]\n", - " nfev: 34\n", - " maxcv: 0.0\n", - "All energies have been calculated\n" - ] - } - ], - "source": [ - "from qiskit.primitives import BackendEstimatorV2\n", - "\n", - "estimator = BackendEstimatorV2(backend=backend_sim)\n", - "\n", - "distances_sim = np.arange(0.3, 1.3, 0.1)\n", - "vqe_energies_sim = []\n", - "vqe_elec_energies_sim = []\n", - "\n", - "for dist in distances_sim:\n", - " xx = dist\n", - "\n", - " # Random initial state and efficient_su2 ansatz\n", - " H = build_hamiltonian(xx)\n", - " ansatz = efficient_su2(H.num_qubits)\n", - " ansatz_isa = pm.run(ansatz)\n", - " x0 = 2 * np.pi * np.random.random(ansatz_isa.num_parameters)\n", - " H_isa = H.apply_layout(ansatz_isa.layout)\n", - " nuclear_repulsion = ham_terms(xx)[0]\n", - "\n", - " res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa, H_isa, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 20, \"disp\": True},\n", - " )\n", - "\n", - " # Note this returns the total energy, and we are often interested in the electronic energy\n", - " tot_energy = getattr(res, \"fun\")\n", - " electron_energy = getattr(res, \"fun\") - nuclear_repulsion\n", - " print(electron_energy)\n", - " vqe_energies_sim.append(tot_energy)\n", - " vqe_elec_energies_sim.append(electron_energy)\n", - "\n", - " # Print all results\n", - " print(res)\n", - "\n", - "print(\"All energies have been calculated\")" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "bc6164ca-6909-4780-8009-6dc274c66268", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(1.2000000000000004)" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "xx" - ] - }, - { - "cell_type": "markdown", - "id": "633a2a89-950f-4d13-a56e-6adc079245ea", - "metadata": {}, - "source": [ - "The results of this output are discussed below in the post-processing section; for now, simply note that the simulation was successful. Now you are ready to run on real hardware. We will set the resilience to `1`, indicating that TREX error mitigation will be used. Now that we are working with real hardware, we will use Qiskit Runtime, and Runtime primitives. Note that both the for loop related to geometry and also the multiple variational trials are inside the session.\n", - "\n", - "Because there are costs and time limits associated with real hardware runs, we have reduced the number of geometry steps and optimizer steps below. Be sure to tailor these steps according to your precision goals and time limits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b09744f5-87a9-4744-b495-cf993e5ffcb3", - "metadata": {}, - "outputs": [], - "source": [ - "# To continue running on real hardware use\n", - "from qiskit_ibm_runtime import Session\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "from qiskit_ibm_runtime import EstimatorOptions\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=2000)\n", - "\n", - "distances = np.arange(0.5, 0.9, 0.1)\n", - "vqe_energies = []\n", - "vqe_elec_energies = []\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - "\n", - " for dist in distances:\n", - " xx = dist\n", - "\n", - " # Random initial state and efficient_su2 ansatz\n", - "\n", - " H = build_hamiltonian(xx)\n", - " ansatz = efficient_su2(H.num_qubits)\n", - " ansatz_isa = pm.run(ansatz)\n", - " H_isa = H.apply_layout(ansatz_isa.layout)\n", - " nuclear_repulsion = ham_terms(xx)[0]\n", - " x0 = 2 * np.pi * np.random.random(ansatz_isa.num_parameters)\n", - "\n", - " res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa, H_isa, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 50, \"disp\": True},\n", - " )\n", - "\n", - " # Note this returns the total energy, and we are often interested in the electronic energy\n", - " tot_energy = getattr(res, \"fun\")\n", - " electron_energy = getattr(res, \"fun\") - nuclear_repulsion\n", - " print(electron_energy)\n", - " vqe_energies.append(tot_energy)\n", - " vqe_elec_energies.append(electron_energy)\n", - "\n", - " # Print all results\n", - " print(res)\n", - "\n", - "print(\"All energies have been calculated\")" - ] - }, - { - "cell_type": "markdown", - "id": "5b6f1491-88bd-4fb1-a1fe-2e29ffa33f17", - "metadata": {}, - "source": [ - "## Step 4: Post-processing\n", - "\n", - "For both the simulator and real hardware, we can plot the ground state energies calculated for each inter-atomic distance and see where the lowest energy is achieved. That should be the inter-atomic distance found in nature, and indeed it is close. A smoother curve might be obtained by trying other ansaetze, optimizers, and running the calculation multiple times at each geometry step and averaging over several random initial conditions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e81f3ead-27ac-415e-a9f2-64a51d4b7aa3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Here we can plot the results from this simulation.\n", - "plt.plot(distances_sim, vqe_energies_sim, label=\"VQE Energy\")\n", - "plt.xlabel(\"Atomic distance (Angstrom)\")\n", - "plt.ylabel(\"Energy\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "1efe0d76-3c6c-4369-9ace-fc890084e676", - "metadata": {}, - "source": [ - "Note that simply increasing the number of optimization steps is not likely to improve the results from the simulator, since all optimizations actually converged to the required tolerance in fewer than the maximum number of iterations.\n", - "\n", - "The results from the real hardware are comparable, aside from a slightly different range of values sampled." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "de8f53cf-9547-4578-b6cb-20d2b5602ee0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(distances, vqe_energies, label=\"VQE Energy\")\n", - "plt.xlabel(\"Atomic distance (Angstrom)\")\n", - "plt.ylabel(\"Energy\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "7e732366-425a-40e4-b6f2-d26f11f7d86b", - "metadata": {}, - "source": [ - "In addition to expecting an H2 bond length of 0.74 Angstrom, the total energy should be -1.17 Hartrees. We see that the real hardware results came closer to these values than the simulator. This is likely because noise was present (or simulated) in both cases, but only in the case of real hardware was error mitigation employed.\n", - "\n", - "### Closing\n", - "\n", - "This concludes our course on VQE for quantum chemistry. If you are interested in understanding some of the underlying information theory used in quantum computing, check out John Watrous's course on the [Basics of Quantum Information](/learning/courses/basics-of-quantum-information). For an additional short-form example of a VQE workflow, see our [Ground-state energy estimation of the Heisenberg chain with VQE tutorial](/docs/tutorials/spin-chain-vqe). Or browse the [tutorials](/docs/tutorials) and [courses](/learning) to find more educational materials about the latest technology in quantum computing.\n", - "\n", - "Don't forget to take this course's exam. A score of 80% or higher will earn you a Credly badge, which will automatically be emailed to you. Thank you for being a part of the IBM Quantum® Network!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0ac6d249-d210-479d-a792-c8b4e94b8b88", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.3.2\n", - "0.35.0\n" - ] - } - ], - "source": [ - "import qiskit\n", - "import qiskit_ibm_runtime\n", - "\n", - "print(qiskit.version.get_version_info())\n", - "print(qiskit_ibm_runtime.version.get_version_info())" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "de193554-b271-4295-95e4-8904f0f6ee8a", + "metadata": {}, + "source": [ + "---\n", + "title: Molecular geometry\n", + "description: In this lesson we vary the geometry of a simple molecule, minimizing the energy at each step.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore pxxr prqs nelecas mcscf chmax Dmax vmax ecore ncas Excp disp */}\n", + "\n", + "# Determining a molecular geometry\n", + "\n", + "In the previous section, we implemented VQE to determine the ground state energy of a molecule. That is a valid use of quantum computing, but even more useful would be to determine the structure of a molecule.\n", + "\n", + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Remaining with our basic example of diatomic hydrogen, the only geometric parameter to vary is the bond length. To accomplish this, we proceed as before, but using a variable in our initial molecule construction (a bond length, *x*, in the argument). This is a fairly simple change, but it does require that the variable be included in functions throughout the process, since it starts in the fermionic Hamiltonian construction and propagates through the mapping and finally to the cost function.\n", + "\n", + "First, we load some of the packages we used before and define the Cholesky function." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "0a5d39bd-8c26-404f-8057-c29e3af70df4", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "#!pip install pyscf==2.4.0\n", + "from pyscf import ao2mo, gto, mcscf, scf\n", + "\n", + "\n", + "def cholesky(V, eps):\n", + " # see https://arxiv.org/pdf/1711.02242.pdf section B2\n", + " # see https://arxiv.org/abs/1808.02625\n", + " # see https://arxiv.org/abs/2104.08957\n", + " no = V.shape[0]\n", + " chmax, ng = 20 * no, 0\n", + " W = V.reshape(no**2, no**2)\n", + " L = np.zeros((no**2, chmax))\n", + " Dmax = np.diagonal(W).copy()\n", + " nu_max = np.argmax(Dmax)\n", + " vmax = Dmax[nu_max]\n", + " while vmax > eps:\n", + " L[:, ng] = W[:, nu_max]\n", + " if ng > 0:\n", + " L[:, ng] -= np.dot(L[:, 0:ng], (L.T)[0:ng, nu_max])\n", + " L[:, ng] /= np.sqrt(vmax)\n", + " Dmax[: no**2] -= L[: no**2, ng] ** 2\n", + " ng += 1\n", + " nu_max = np.argmax(Dmax)\n", + " vmax = Dmax[nu_max]\n", + " L = L[:, :ng].reshape((no, no, ng))\n", + " print(\n", + " \"accuracy of Cholesky decomposition \",\n", + " np.abs(np.einsum(\"prg,qsg->prqs\", L, L) - V).max(),\n", + " )\n", + " return L, ng\n", + "\n", + "\n", + "def identity(n):\n", + " return SparsePauliOp.from_list([(\"I\" * n, 1)])\n", + "\n", + "\n", + "def creators_destructors(n, mapping=\"jordan_wigner\"):\n", + " c_list = []\n", + " if mapping == \"jordan_wigner\":\n", + " for p in range(n):\n", + " if p == 0:\n", + " ell, r = \"I\" * (n - 1), \"\"\n", + " elif p == n - 1:\n", + " ell, r = \"\", \"Z\" * (n - 1)\n", + " else:\n", + " ell, r = \"I\" * (n - p - 1), \"Z\" * p\n", + " cp = SparsePauliOp.from_list([(ell + \"X\" + r, 0.5), (ell + \"Y\" + r, -0.5j)])\n", + " c_list.append(cp)\n", + " else:\n", + " raise ValueError(\"Unsupported mapping.\")\n", + " d_list = [cp.adjoint() for cp in c_list]\n", + " return c_list, d_list" + ] + }, + { + "cell_type": "markdown", + "id": "40e25b54-7927-4dbb-a26f-1c6b33f7f349", + "metadata": {}, + "source": [ + "Now to define our Hamiltonian, we will use PySCF exactly as in the previous example, but now we will include a variable, `x`, to play the role of our interatomic distance. This will return the core energy, single-electron energy, and two-electron energies as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbd10d0c-feb1-4a86-9bd9-b61101a08b95", + "metadata": {}, + "outputs": [], + "source": [ + "def ham_terms(x: float):\n", + " distance = x\n", + " a = distance / 2\n", + " mol = gto.Mole()\n", + " mol.build(\n", + " verbose=0,\n", + " atom=[\n", + " [\"H\", (0, 0, -a)],\n", + " [\"H\", (0, 0, a)],\n", + " ],\n", + " basis=\"sto-6g\",\n", + " spin=0,\n", + " charge=0,\n", + " symmetry=\"Dooh\",\n", + " )\n", + "\n", + " # mf = scf.RHF(mol)\n", + " # mx = mcscf.CASCI(mf, ncas=2, nelecas=(1, 1))\n", + " # mx.kernel()\n", + "\n", + " mf = scf.RHF(mol)\n", + " mf.kernel()\n", + " if not mf.converged:\n", + " raise RuntimeError(f\"SCF did not converge for distance {x}\")\n", + "\n", + " mx = mcscf.CASCI(mf, ncas=2, nelecas=(1, 1))\n", + " casci_energy = mx.kernel()\n", + " if casci_energy is None:\n", + " raise RuntimeError(f\"CASCI failed for distance {x}\")\n", + "\n", + " # Other variables that might come in handy:\n", + " # active_space = range(mol.nelectron // 2 - 1, mol.nelectron // 2 + 1)\n", + " # E1 = mf.kernel()\n", + " # mo = mx.sort_mo(active_space, base=0)\n", + " # E2 = mx.kernel(mo)[:2]\n", + "\n", + " h1e, ecore = mx.get_h1eff()\n", + " h2e = ao2mo.restore(1, mx.get_h2eff(), mx.ncas)\n", + " return ecore, h1e, h2e" + ] + }, + { + "cell_type": "markdown", + "id": "db49a702-e60e-4c9b-8ff2-94aa2ada022c", + "metadata": {}, + "source": [ + "Recall that the construction above is making a fermionic Hamiltonian based on the atomic species, geometry, and electronic orbitals. Below, we map this fermionic Hamiltonian onto Pauli operators. This `build_hamiltonian` function will also include a geometric variable as an argument." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "84e6a56b-eaea-4c0a-8502-567cfc5140a2", + "metadata": {}, + "outputs": [], + "source": [ + "def build_hamiltonian(distx: float) -> SparsePauliOp:\n", + " ecore = ham_terms(distx)[0]\n", + " h1e = ham_terms(distx)[1]\n", + " h2e = ham_terms(distx)[2]\n", + "\n", + " ncas, _ = h1e.shape\n", + "\n", + " C, D = creators_destructors(2 * ncas, mapping=\"jordan_wigner\")\n", + " Exc = []\n", + " for p in range(ncas):\n", + " Excp = [C[p] @ D[p] + C[ncas + p] @ D[ncas + p]]\n", + " for r in range(p + 1, ncas):\n", + " Excp.append(\n", + " C[p] @ D[r]\n", + " + C[ncas + p] @ D[ncas + r]\n", + " + C[r] @ D[p]\n", + " + C[ncas + r] @ D[ncas + p]\n", + " )\n", + " Exc.append(Excp)\n", + "\n", + " # low-rank decomposition of the Hamiltonian\n", + " Lop, ng = cholesky(h2e, 1e-6)\n", + " t1e = h1e - 0.5 * np.einsum(\"pxxr->pr\", h2e)\n", + "\n", + " H = ecore * identity(2 * ncas)\n", + " # one-body term\n", + " for p in range(ncas):\n", + " for r in range(p, ncas):\n", + " H += t1e[p, r] * Exc[p][r - p]\n", + " # two-body term\n", + " for g in range(ng):\n", + " Lg = 0 * identity(2 * ncas)\n", + " for p in range(ncas):\n", + " for r in range(p, ncas):\n", + " Lg += Lop[p, r, g] * Exc[p][r - p]\n", + " H += 0.5 * Lg @ Lg\n", + "\n", + " return H.chop().simplify()" + ] + }, + { + "cell_type": "markdown", + "id": "ffcb2569-f832-4fa9-a8bb-d7626c2a233d", + "metadata": {}, + "source": [ + "We will load the remaining packages for running VQE itself, such as the efficient_su2 ansatz, and SciPy minimizers:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5f8dd8e0-6dfb-4be7-af15-1591f569201c", + "metadata": {}, + "outputs": [], + "source": [ + "# General imports\n", + "\n", + "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", + "from qiskit.circuit.library import efficient_su2\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "# SciPy minimizer routine\n", + "from scipy.optimize import minimize\n", + "\n", + "# Plotting functions\n", + "\n", + "# Qiskit Runtime tools\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()" + ] + }, + { + "cell_type": "markdown", + "id": "f94c1680-9c2a-4585-a63e-a49da4eb02f3", + "metadata": {}, + "source": [ + "We will again define the cost function, but this always took a fully-built and mapped Hamiltonian as an argument, so nothing changes about this function." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "0a80ad8c-a9cb-4cab-835d-f65131b99c87", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func(params, ansatz, H, estimator):\n", + " pub = (ansatz, [H], [params])\n", + " result = estimator.run(pubs=[pub]).result()\n", + " energy = result[0].data.evs[0]\n", + " return energy\n", + "\n", + "\n", + "# def cost_func_sim(params, ansatz, H, estimator):\n", + "# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]\n", + "# return energy" + ] + }, + { + "cell_type": "markdown", + "id": "a068e645-8dfb-4e9c-b1b1-7dd936808188", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum execution\n", + "\n", + "Because the Hamiltonian will change with each new geometry, the transpiling of the operator will change at each step. We can nevertheless define a general pass manager to be applied at each step, specific to the hardware we want to use.\n", + "\n", + "Here we will use the least busy backend available. We will use that backend as a model for our AerSimulator, allowing our simulator to mimic, for example, the noise behavior of the real backend. These noise models are not perfect, but they may help you know what to expect from real hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa07518f-a2c0-4f7a-b344-8d9576427478", + "metadata": {}, + "outputs": [], + "source": [ + "# Here, we select the least busy backend available:\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "print(backend)\n", + "# Or to select a specific real backend use the line below, and substitute 'ibm_strasbourg' for your\n", + "# chosen device.\n", + "# backend = service.get_backend('ibm_strasbourg')" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "e67fc84a-431b-4efd-937f-49c8b7ac3abb", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on a simulator:\n", + "# -----------\n", + "from qiskit_aer import AerSimulator\n", + "\n", + "backend_sim = AerSimulator.from_backend(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "0636c55f-f46f-46ca-acaf-72bcc2f5f663", + "metadata": {}, + "source": [ + "We import the pass manager and related packages to help us optimize our circuit. This step, and the one above it, are independent of the Hamiltonian, and so are unchanged from the previous lesson." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "8202332e-69ca-4049-af27-e98a77f15a5d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler import PassManager\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.transpiler.passes import (\n", + " ALAPScheduleAnalysis,\n", + " PadDynamicalDecoupling,\n", + " ConstrainedReschedule,\n", + ")\n", + "from qiskit.circuit.library import XGate\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "pm.scheduling = PassManager(\n", + " [\n", + " ALAPScheduleAnalysis(target=target),\n", + " ConstrainedReschedule(\n", + " acquire_alignment=target.acquire_alignment,\n", + " pulse_alignment=target.pulse_alignment,\n", + " target=target,\n", + " ),\n", + " PadDynamicalDecoupling(\n", + " target=target,\n", + " dd_sequence=[XGate(), XGate()],\n", + " pulse_alignment=target.pulse_alignment,\n", + " ),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4932ef5e-3c81-46a1-92cb-3082399734a0", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives.\n", + "\n", + "In the code block below, we set up an array to store our outputs from each step in our interatomic distance $x$. We have chosen the range of $x$ based on our knowledge of the experimental value for the equilibrium bond length: 0.74 Angstrom. We will run this first on a simulator, and will thus be importing our Estimator (BackendEstimator) from `qiskit.primitives`. For each geometry step, we build the Hamiltonian and allow a certain number of optimization steps (here 500) using the optimizer \"cobyla\". At each geometry step, we store both the total energy and the electronic energy. Because of the high number of optimizer steps, this may take an hour or more. You may wish to modify the inputs below to reduce the required time." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "4c41a221-8a02-4932-882b-5afcc98d1d8d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "accuracy of Cholesky decomposition 1.1102230246251565e-15\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/porter284/.pyenv/versions/3.11.12/lib/python3.11/site-packages/scipy/_lib/pyprima/common/preproc.py:68: UserWarning: COBYLA: Invalid MAXFUN; it should be at least num_vars + 2; it is set to 34\n", + " warn(f'{solver}: Invalid MAXFUN; it should be at least {min_maxfun_str}; it is set to {maxfun}')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = 1.316011435623847\n", + "The corresponding X is:\n", + "[2.32948769 5.39918229 3.03787975 4.11789904 4.97130735 2.68662232\n", + " 1.76573151 2.48982571 5.40431972 3.65780829 1.33792786 5.48472494\n", + " 6.18738702 1.78741883 0.78195251 2.96658955 1.35827677 5.599321\n", + " 4.54850148 1.0939048 4.26158726 0.52100721 0.82318 4.76796961\n", + " 3.75795507 3.8526447 5.51100375 5.91023075 2.61494836 1.79908918\n", + " 2.65937756 5.53964148]\n", + "\n", + "-0.44791260077615314\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: 1.316011435623847\n", + " x: [ 2.329e+00 5.399e+00 ... 2.659e+00 5.540e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 5.551115123125783e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = 0.7235003672327549\n", + "The corresponding X is:\n", + "[2.56282915 5.63369524 5.58059887 4.049643 4.2021266 3.06866011\n", + " 6.01619635 1.52520776 4.35403161 0.33673958 0.32623161 1.2179545\n", + " 2.84001371 3.98956684 4.89632562 1.38303588 1.96194695 2.13182089\n", + " 0.29739166 1.77895165 3.29151585 3.54355374 4.49626674 0.95756626\n", + " 0.87103927 4.53068385 1.31051302 0.37103108 1.02961355 3.13342311\n", + " 5.65815319 2.24770604]\n", + "\n", + "-0.5994426600672451\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: 0.7235003672327549\n", + " x: [ 2.563e+00 5.634e+00 ... 5.658e+00 2.248e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 5.551115123125783e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = 0.34960914928810116\n", + "The corresponding X is:\n", + "[5.44143165 6.75955835 1.56836472 3.09522093 4.67873235 1.67071481\n", + " 0.3056494 0.65998337 1.02197668 5.21162959 0.43690354 3.56522934\n", + " 4.56033119 1.90736037 0.40863891 2.87007312 3.2516952 5.90360196\n", + " 1.99057799 5.20726456 0.74710237 6.03179202 3.80685028 0.03844391\n", + " 5.88580196 3.62233258 3.98723567 2.50591888 5.44020267 2.2792993\n", + " 5.57102303 4.46548617]\n", + "\n", + "-0.7087452725518989\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: 0.34960914928810116\n", + " x: [ 5.441e+00 6.760e+00 ... 5.571e+00 4.465e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 2.220446049250313e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = 0.10594558882184543\n", + "The corresponding X is:\n", + "[5.35675483 2.26629567 1.45430546 5.56758296 5.76309509 0.73239338\n", + " 5.1216998 3.03258872 4.33624828 1.93197674 0.5292902 3.32274987\n", + " 3.43247633 0.81490741 0.48060245 1.9944799 5.67519646 5.12534057\n", + " 0.06510627 2.52989834 6.1699519 0.94828957 5.91634548 1.5994961\n", + " 4.27902164 2.3129213 1.82353095 2.10634209 1.43740426 4.06988733\n", + " 0.59624074 4.93925418]\n", + "\n", + "-0.7760164293781545\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: 0.10594558882184543\n", + " x: [ 5.357e+00 2.266e+00 ... 5.962e-01 4.939e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.06473600797229297\n", + "The corresponding X is:\n", + "[6.07735568 0.18019501 0.20743128 4.15445985 3.59388894 5.10047555\n", + " 6.09938474 6.54707528 3.36251167 2.05475223 3.67078456 5.96010605\n", + " 2.58589996 5.2723619 3.26352977 2.47432334 3.50289983 2.06620525\n", + " 6.0946056 1.22751903 0.97320057 2.19564095 5.73174941 2.05127682\n", + " 5.73805165 3.84046105 1.84816963 2.1247504 3.11106736 2.44136052\n", + " 3.39002685 0.81596991]\n", + "\n", + "-0.8207034521437214\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.06473600797229297\n", + " x: [ 6.077e+00 1.802e-01 ... 3.390e+00 8.160e-01]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 5.551115123125783e-17\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.19562982094782935\n", + "The corresponding X is:\n", + "[-0.02184462 3.67041038 7.25918653 5.89799546 0.63583624 1.84214506\n", + " 2.84059837 5.31485182 1.6053784 0.04556618 0.32018993 -0.03884066\n", + " 0.69131496 0.24203727 1.97397262 3.59723495 0.43355775 2.30131056\n", + " 4.63482292 3.9857415 4.32320753 4.55388437 2.18753433 5.99034987\n", + " 2.50489913 0.90650534 4.82518088 2.32954849 2.29901832 5.33658863\n", + " 5.91246716 3.2405013 ]\n", + "\n", + "-0.8571013345978292\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.19562982094782935\n", + " x: [-2.184e-02 3.670e+00 ... 5.912e+00 3.241e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.2833766309947055\n", + "The corresponding X is:\n", + "[ 3.1700088 5.05055456 1.2545611 4.28751811 0.6255103 1.67526577\n", + " 5.48201473 4.83820497 7.34880059 5.99705431 4.2502643 0.32066274\n", + " 0.41001404 0.27271241 4.15682546 4.22393693 4.35148115 0.64538137\n", + " 5.26288622 5.03810489 4.62426621 4.74997689 1.09603919 0.34752466\n", + " 1.8116275 0.7474807 5.31754143 4.11181763 1.58797998 5.6299796\n", + " 3.0109383 -0.19062772]\n", + "\n", + "-0.8713513097947054\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.2833766309947055\n", + " x: [ 3.170e+00 5.051e+00 ... 3.011e+00 -1.906e-01]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.3527503628484244\n", + "The corresponding X is:\n", + "[3.90513622 4.61398739 5.92552705 1.99953405 4.82157369 1.35702441\n", + " 2.77701782 5.73612247 4.22710527 1.83463189 0.45796297 4.62509318\n", + " 0.98998668 0.11666217 3.0234641 4.54298546 0.14034033 4.15635797\n", + " 1.41257357 4.48719602 2.39365535 0.19672041 5.0763044 1.86357581\n", + " 3.657757 4.60298344 2.49769577 1.88086199 3.00108725 1.84475841\n", + " 5.24047385 4.91142914]\n", + "\n", + "-0.8819275737684243\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.3527503628484244\n", + " x: [ 3.905e+00 4.614e+00 ... 5.240e+00 4.911e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 2.7755575615628914e-17\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.4022181851996095\n", + "The corresponding X is:\n", + "[6.09453981 3.5109422 3.37216019 4.94732621 1.25662002 5.89645164\n", + " 5.06403334 2.68073141 4.40385083 1.13638366 1.73347762 6.82932871\n", + " 1.15265014 2.07145964 4.36520459 1.14960341 1.62288871 4.32315915\n", + " 5.45622821 0.93554005 3.17418483 0.47230243 1.31535502 5.77698726\n", + " 2.04927925 2.50663538 5.9706002 5.4984681 2.9421232 1.56636313\n", + " 1.09394523 4.62582 ]\n", + "\n", + "-0.8832883769450639\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.4022181851996095\n", + " x: [ 6.095e+00 3.511e+00 ... 1.094e+00 4.626e+00]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "accuracy of Cholesky decomposition 1.1102230246251565e-16\n", + "Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + "Number of function values = 34 Least value of F = -0.44423031870708934\n", + "The corresponding X is:\n", + "[4.05765050e+00 3.99144950e+00 3.13287593e+00 3.28855137e+00\n", + " 4.32613515e+00 4.91104512e+00 1.86521867e+00 2.18822879e+00\n", + " 6.01336171e+00 1.82501276e+00 2.64830637e+00 5.53045823e+00\n", + " 2.36110093e+00 3.98821703e+00 4.69013438e-01 4.38996815e+00\n", + " 7.78103801e-04 1.72994378e+00 2.24970934e+00 1.11978200e+00\n", + " 2.24846445e+00 4.90745512e+00 5.38474921e+00 5.03587994e+00\n", + " 3.54297277e+00 4.78147533e+00 1.25990218e+00 1.99168068e+00\n", + " 5.89203503e+00 1.77673987e+00 5.37848357e+00 5.60245198e-01]\n", + "\n", + "-0.8852113278070892\n", + " message: Return from COBYLA because the objective function has been evaluated MAXFUN times.\n", + " success: False\n", + " status: 3\n", + " fun: -0.44423031870708934\n", + " x: [ 4.058e+00 3.991e+00 ... 5.378e+00 5.602e-01]\n", + " nfev: 34\n", + " maxcv: 0.0\n", + "All energies have been calculated\n" + ] + } + ], + "source": [ + "from qiskit.primitives import BackendEstimatorV2\n", + "\n", + "estimator = BackendEstimatorV2(backend=backend_sim)\n", + "\n", + "distances_sim = np.arange(0.3, 1.3, 0.1)\n", + "vqe_energies_sim = []\n", + "vqe_elec_energies_sim = []\n", + "\n", + "for dist in distances_sim:\n", + " xx = dist\n", + "\n", + " # Random initial state and efficient_su2 ansatz\n", + " H = build_hamiltonian(xx)\n", + " ansatz = efficient_su2(H.num_qubits)\n", + " ansatz_isa = pm.run(ansatz)\n", + " x0 = 2 * np.pi * np.random.random(ansatz_isa.num_parameters)\n", + " H_isa = H.apply_layout(ansatz_isa.layout)\n", + " nuclear_repulsion = ham_terms(xx)[0]\n", + "\n", + " res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa, H_isa, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 20, \"disp\": True},\n", + " )\n", + "\n", + " # Note this returns the total energy, and we are often interested in the electronic energy\n", + " tot_energy = getattr(res, \"fun\")\n", + " electron_energy = getattr(res, \"fun\") - nuclear_repulsion\n", + " print(electron_energy)\n", + " vqe_energies_sim.append(tot_energy)\n", + " vqe_elec_energies_sim.append(electron_energy)\n", + "\n", + " # Print all results\n", + " print(res)\n", + "\n", + "print(\"All energies have been calculated\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "bc6164ca-6909-4780-8009-6dc274c66268", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(1.2000000000000004)" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "xx" + ] + }, + { + "cell_type": "markdown", + "id": "633a2a89-950f-4d13-a56e-6adc079245ea", + "metadata": {}, + "source": [ + "The results of this output are discussed below in the post-processing section; for now, simply note that the simulation was successful. Now you are ready to run on real hardware. We will set the resilience to `1`, indicating that TREX error mitigation will be used. Now that we are working with real hardware, we will use Qiskit Runtime, and Runtime primitives. Note that both the for loop related to geometry and also the multiple variational trials are inside the session.\n", + "\n", + "Because there are costs and time limits associated with real hardware runs, we have reduced the number of geometry steps and optimizer steps below. Be sure to tailor these steps according to your precision goals and time limits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b09744f5-87a9-4744-b495-cf993e5ffcb3", + "metadata": {}, + "outputs": [], + "source": [ + "# To continue running on real hardware use\n", + "from qiskit_ibm_runtime import Session\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "from qiskit_ibm_runtime import EstimatorOptions\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=2000)\n", + "\n", + "distances = np.arange(0.5, 0.9, 0.1)\n", + "vqe_energies = []\n", + "vqe_elec_energies = []\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + "\n", + " for dist in distances:\n", + " xx = dist\n", + "\n", + " # Random initial state and efficient_su2 ansatz\n", + "\n", + " H = build_hamiltonian(xx)\n", + " ansatz = efficient_su2(H.num_qubits)\n", + " ansatz_isa = pm.run(ansatz)\n", + " H_isa = H.apply_layout(ansatz_isa.layout)\n", + " nuclear_repulsion = ham_terms(xx)[0]\n", + " x0 = 2 * np.pi * np.random.random(ansatz_isa.num_parameters)\n", + "\n", + " res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa, H_isa, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 50, \"disp\": True},\n", + " )\n", + "\n", + " # Note this returns the total energy, and we are often interested in the electronic energy\n", + " tot_energy = getattr(res, \"fun\")\n", + " electron_energy = getattr(res, \"fun\") - nuclear_repulsion\n", + " print(electron_energy)\n", + " vqe_energies.append(tot_energy)\n", + " vqe_elec_energies.append(electron_energy)\n", + "\n", + " # Print all results\n", + " print(res)\n", + "\n", + "print(\"All energies have been calculated\")" + ] + }, + { + "cell_type": "markdown", + "id": "5b6f1491-88bd-4fb1-a1fe-2e29ffa33f17", + "metadata": {}, + "source": [ + "## Step 4: Post-processing\n", + "\n", + "For both the simulator and real hardware, we can plot the ground state energies calculated for each inter-atomic distance and see where the lowest energy is achieved. That should be the inter-atomic distance found in nature, and indeed it is close. A smoother curve might be obtained by trying other ansaetze, optimizers, and running the calculation multiple times at each geometry step and averaging over several random initial conditions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e81f3ead-27ac-415e-a9f2-64a51d4b7aa3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Here we can plot the results from this simulation.\n", + "plt.plot(distances_sim, vqe_energies_sim, label=\"VQE Energy\")\n", + "plt.xlabel(\"Atomic distance (Angstrom)\")\n", + "plt.ylabel(\"Energy\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1efe0d76-3c6c-4369-9ace-fc890084e676", + "metadata": {}, + "source": [ + "Note that simply increasing the number of optimization steps is not likely to improve the results from the simulator, since all optimizations actually converged to the required tolerance in fewer than the maximum number of iterations.\n", + "\n", + "The results from the real hardware are comparable, aside from a slightly different range of values sampled." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de8f53cf-9547-4578-b6cb-20d2b5602ee0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(distances, vqe_energies, label=\"VQE Energy\")\n", + "plt.xlabel(\"Atomic distance (Angstrom)\")\n", + "plt.ylabel(\"Energy\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7e732366-425a-40e4-b6f2-d26f11f7d86b", + "metadata": {}, + "source": [ + "In addition to expecting an H2 bond length of 0.74 Angstrom, the total energy should be -1.17 Hartrees. We see that the real hardware results came closer to these values than the simulator. This is likely because noise was present (or simulated) in both cases, but only in the case of real hardware was error mitigation employed.\n", + "\n", + "### Closing\n", + "\n", + "This concludes our course on VQE for quantum chemistry. If you are interested in understanding some of the underlying information theory used in quantum computing, check out John Watrous's course on the [Basics of Quantum Information](/learning/courses/basics-of-quantum-information). For an additional short-form example of a VQE workflow, see our [Ground-state energy estimation of the Heisenberg chain with VQE tutorial](/docs/tutorials/spin-chain-vqe). Or browse the [tutorials](/docs/tutorials) and [courses](/learning) to find more educational materials about the latest technology in quantum computing.\n", + "\n", + "Don't forget to take this course's exam. A score of 80% or higher will earn you a Credly badge, which will automatically be emailed to you. Thank you for being a part of the IBM Quantum® Network!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ac6d249-d210-479d-a792-c8b4e94b8b88", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.3.2\n", + "0.35.0\n" + ] + } + ], + "source": [ + "import qiskit\n", + "import qiskit_ibm_runtime\n", + "\n", + "print(qiskit.version.get_version_info())\n", + "print(qiskit_ibm_runtime.version.get_version_info())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb b/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb index 958c1a51091..60b18a1e9ee 100644 --- a/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb @@ -1,2717 +1,2727 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2b1776f4-c259-494e-a33d-1ebb0459426c", - "metadata": {}, - "source": [ - "---\n", - "title: Krylov quantum diagonalization\n", - "description: Krylov quantum diagonalization (KQD) is described, starting with classical Krylov methods. Convergence and resource intensiveness are discussed.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore Hndt longrightarrow Bigg Jörg Liesen Zdenek Strakos Yousef Saad MINRES vstar nabla */}\n", - "\n", - "# Krylov quantum diagonalization\n", - "\n", - "In this lesson on Krylov quantum diagonalization (KQD) we will answer the following:\n", - "\n", - "* What is the Krylov method, generally?\n", - "* Why does the Krylov method work and under what conditions?\n", - "* How does quantum computing play a role?\n", - "\n", - "The quantum part of the calculations are based largely on work in Ref [\\[1\\]](#references).\n", - "\n", - "The video below gives an overview of Krylov methods in classical computing, motivates their use, and explains how quantum computing can play a role in that workstream. The subsequent text offers more detail and implements a Krylov method both classically, and using a quantum computer.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "a9ba88fb-734e-44d8-ada4-c588e2654921", - "metadata": {}, - "source": [ - "## 1. Introduction to Krylov methods\n", - "\n", - "A __Krylov subspace method__ can refer to any of several methods built around what is called the __Krylov subspace__. A complete review of these is beyond the scope of this lesson, but Ref [\\[2-4\\]](#references) can all give substantially more background. Here, we will focus on what a Krylov subspace is, how and why it is useful in solving eigenvalue problems, and finally how it can be implemented on a quantum computer." - ] - }, - { - "cell_type": "markdown", - "id": "9b47a985-ec1d-41e2-8516-6cfcd7713e5f", - "metadata": {}, - "source": [ - "__Definition:__ Given a symmetric, positive semi-definite $N\\times N$ matrix $A$, the Krylov space $\\mathcal{K}^r$ of order $r$ is the space spanned by vectors obtained by multiplying higher powers of a matrix $A$, up to $r-1\\leq N$, with a reference vector $\\vert v \\rangle$.\n", - "\n", - "$$\n", - "\\mathcal{K}^r = \\text{span}\\left\\{ \\vert v \\rangle, A \\vert v \\rangle, A^2 \\vert v \\rangle, ..., A^{r-1} \\vert v \\rangle \\right\\}\n", - "$$\n", - "\n", - "Although the vectors above span what we call a Krylov subspace, there is no reason to think that they will be orthogonal. One often uses an iterative orthonormalizing process similar to __Gram-Schmidt orthogonalization__. Here the process is slightly different since each new vector is made orthogonal to the others as it is generated. In this context this is called __Arnoldi iteration__. Starting with the initial vector $|v\\rangle$, one generates the next vector $A|v\\rangle$, and then ensures that this second vector is orthogonal to the first by subtracting off its projection on $|v\\rangle$. That is\n", - "\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "|v_0\\rangle &=\\frac{|v\\rangle}{\\left|\\left| |v\\rangle \\right|\\right|}\\\\\n", - "\n", - "|v_1\\rangle &=\\frac{A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle}{\\left|\\left|A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle \\right|\\right|}\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "It is now easy to see that $|v_0\\rangle \\perp |v_1\\rangle,$ since\n", - "$$\n", - "\\langle v_0 | v_1\\rangle=\\frac{\\langle v_0 | A|v\\rangle-\\langle v_0 |A|v\\rangle\\langle v_0|v_0\\rangle}{\\left|\\left| A|v\\rangle-\\langle A|v\\rangle|v_0\\rangle |v_0\\rangle \\right|\\right|}=0\n", - "$$\n", - "\n", - "We do the same for the next vector, ensuring it is orthogonal to both the previous two:\n", - "$$\n", - "|v_2\\rangle=\\frac{A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle}{\\left|\\left| A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle\\right|\\right|}\n", - "$$\n", - "If we repeat this process for all $r$ vectors, we have a complete orthonormal basis for a Krylov space. Note that the orthogonalization process here will yield zero once $r>m$, since $m$ orthogonal vector necessarily span the full space. The process will also yield zero if any vector is an eigenvector of $A$ since all subsequent vectors will be multiples of that vector." - ] - }, - { - "cell_type": "markdown", - "id": "5351576c-bc3c-4e21-8839-9c14eb10747f", - "metadata": {}, - "source": [ - "### 1.1 A simple example: Krylov by hand\n", - "\n", - "Let us step through a generation of a Krylov subspace generation on a trivially small matrix, so that we can see the process. We start with an initial matrix $A$ of interest to us:\n", - "$$\n", - "A=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\n", - "$$\n", - "For this small example, we can determine the eigenvectors and eigenvalues easily even by hand. We show the numerical solution here." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "49017c87-a971-4d2d-b6dc-b8ec9b64184a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The eigenvalues are [2.58578644 4. 5.41421356]\n", - "The eigenvectors are [[ 5.00000000e-01 -7.07106781e-01 5.00000000e-01]\n", - " [ 7.07106781e-01 1.37464400e-16 -7.07106781e-01]\n", - " [ 5.00000000e-01 7.07106781e-01 5.00000000e-01]]\n" - ] - } - ], - "source": [ - "# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use linalg.eig in this lesson.\n", - "\n", - "import numpy as np\n", - "\n", - "A = np.array([[4, -1, 0], [-1, 4, -1], [0, -1, 4]])\n", - "eigenvalues, eigenvectors = np.linalg.eig(A)\n", - "print(\"The eigenvalues are \", eigenvalues)\n", - "print(\"The eigenvectors are \", eigenvectors)" - ] - }, - { - "cell_type": "markdown", - "id": "a0f96bc7-fef9-48c7-858a-5b44772d710f", - "metadata": {}, - "source": [ - "We record them here for later comparison:\n", - "$$\n", - "\\begin{aligned}\n", - "a_0&=2.59,&|0\\rangle&=&\\begin{pmatrix}1/2\\\\-\\sqrt{2}/2\\\\1/2\\end{pmatrix}\\\\\n", - "\\\\\n", - "a_1&=4,&|1\\rangle&=&\\begin{pmatrix}\\sqrt{2}/2\\\\0\\\\-\\sqrt{2}/2\\end{pmatrix}\\\\\n", - "\\\\\n", - "a_2&=5.41,&|2\\rangle&=&\\begin{pmatrix}1/2\\\\\\sqrt{2}/2\\\\1/2\\end{pmatrix}\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "7f237ba1-d380-4553-b5bc-94422b4f39c1", - "metadata": {}, - "source": [ - "We would like to study how this process works (or fails) as we increase the dimension of our Krylov subspace, $r$. To this end, we will apply this process:\n", - "\n", - "* Generate a subspace of the full vector space starting with a randomly-chosen vector $|v\\rangle$ (call it $|v_0\\rangle$ if it is already normalized, as above).\n", - "* Project the full matrix $A$ onto that subspace, and find the eigenvalues of that projected matrix $\\tilde{A}$.\n", - "* Increase the size of the subspace by generating more vectors, ensuring that they are orthonormal, using a process similar to Gram-Schmidt orthogonalization.\n", - "* Project $A$ onto the larger subspace and find the eigenvalues of the resulting matrix, $\\tilde{A}$.\n", - "* Repeat this until the eigenvalues converge (or in this toy case, until you have generated vectors spanning the full vector space of the original matrix $A$).\n", - "\n", - "A normal implementation of the Krylov method would not need to solve the eigenvalue problem for the matrix projected on every Krylov subspace as it is built. You could construct the subspace of the desired dimension, project the matrix onto that subspace, and diagonalize the projected matrix. Projecting and diagonalizing at each subspace dimension is only done for checking convergence.\n", - "\n", - "#### Dimension $r=1$:\n", - "\n", - "We choose a random vector, say\n", - "$$\n", - "|v_0\\rangle=\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}\n", - "$$\n", - "\n", - "If it is not already normalized, normalize it.\n", - "\n", - "We now project our matrix $A$ onto the subspace of this one vector:\n", - "$$\n", - "\\tilde{A}_0=\\langle v_0| A|v_0\\rangle=\\begin{pmatrix}1&0&0\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=(4)\n", - "$$\n", - "This is our projection of the matrix onto our Krylov subspace when it contains just a single vector, $|v_0\\rangle$. The eigenvalue of this matrix is trivially 4. We can think of this as our zeroth-order estimate of the eigenvalues (in this case just one) of $A$. Although it is a poor estimate, it is the correct order of magnitude." - ] - }, - { - "cell_type": "markdown", - "id": "a0eeb9b1-86a5-4bc8-8651-66588aea7686", - "metadata": {}, - "source": [ - "#### Dimension $r=2$:\n", - "\n", - "We now generate the next vector in our subspace through operation with $A$ on the previous vector:\n", - "\n", - "$$\n", - "A|v_0\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\n", - "$$\n", - "\n", - "Now we subtract off the projection of this vector onto our previous vector to ensure orthogonality.\n", - "$$\n", - "|v_1\\rangle=A|v_0\\rangle-\\langle v_0 |A|v_0\\rangle|v_0\\rangle\n", - "$$\n", - "$$\n", - "|v_1\\rangle=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}-\\begin{pmatrix}1& 0& 0\\end{pmatrix}\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n", - "$$\n", - "\n", - "If it is not already normalized, normalize it. In this case, the vector was already normalized, so\n", - "\n", - "$$\n", - "|v_1\\rangle=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n", - "$$\n", - "\n", - "We now project our matrix A onto the subspace of these two vectors:\n", - "$$\n", - "\\tilde{A}_1= \\begin{pmatrix} 1&0&0\\\\0&-1&0 \\end{pmatrix} \\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0\\\\0&-1\\\\0&0\\end{pmatrix}=\\begin{pmatrix}1&0&0\\\\0&-1&0\\end{pmatrix}\\begin{pmatrix}4&1\\\\-1&-4\\\\0&1\\end{pmatrix}=\\begin{pmatrix}4&1\\\\1&4\\end{pmatrix}\n", - "$$\n", - "\n", - "We are still left with the problem of determining the eigenvalues of this matrix. But this matrix is slightly smaller than the full matrix. In problems involving very large matrices, working with this smaller subspace may be highly advantageous.\n", - "\n", - "$$\n", - "\\det(\\tilde{A_1}-\\lambda I)=0\n", - "$$\n", - "$$\n", - "\\begin{vmatrix} 4-\\lambda&1\\\\1&4-\\lambda\\end{vmatrix} =(4-\\lambda)^2-1=0\n", - "$$\n", - "$$\n", - "4-\\lambda=±1→\\lambda=3,5\n", - "$$\n", - "Although this is still not a good estimate, it is better than the zeroth order estimate. We will carry this out for one more iteration, to ensure the process is clear. However, this undercuts the point of the method, since we will end up diagonalizing a 3x3 matrix in the next iteration, meaning we have not saved time or computational power." - ] - }, - { - "cell_type": "markdown", - "id": "43ef6c6d-ba18-4e8f-94df-83c404d0015a", - "metadata": {}, - "source": [ - "#### Dimension $r=3$:\n", - "\n", - "We now generate the next vector in our subspace through operation with A on the previous vector:\n", - "$$\n", - "A|v_1\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\n", - "$$\n", - "Now we subtract off the projection of this vector onto both our previous vectors to ensure orthogonality.\n", - "$$\n", - "\\begin{aligned}\n", - "|v_2\\rangle&=A|v_1\\rangle-\\langle v_0 |A|v_1\\rangle|v_0\\rangle-\\langle v_1 |A|v_1\\rangle|v_1\\rangle\\\\\n", - "|v_2\\rangle&=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}-\\begin{pmatrix}1& 0& 0 \\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}-\\begin{pmatrix}0&-1& 0\\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "If it is not already normalized, normalize it. In this case, the vector was already normalized, so\n", - "$$\n", - "|v_2 \\rangle=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n", - "$$\n", - "We now project our matrix $A$ onto the subspace of these vectors:\n", - "\n", - "$$\n", - "\\tilde{A}_2=\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&-1&0\\\\1&-4&1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&1&0\\\\1&4&1\\\\0&1&4\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "5f19b90b-93fa-4ccb-b8a2-3bbd55e4dab0", - "metadata": {}, - "source": [ - "We now determine the eigenvalues:\n", - "$$\n", - "\\det(\\tilde{A}_2-\\lambda I)=0\n", - "$$\n", - "$$\n", - "\\begin{vmatrix}4-\\lambda&1&0\\\\1&4-\\lambda&1\\\\0&1&4-\\lambda\\end{vmatrix} = (4-\\lambda)((4-\\lambda)^2-1)-(4-\\lambda)=0\\\\\n", - "$$\n", - "$$\n", - "4-\\lambda=0,4-\\lambda=±2^{1/2}→\\lambda=4-2^{1/2},4,4+2^{1/2}≈2.59,4,5.41\n", - "$$\n", - "These eigenvalues are exactly the eigenvalues of the original matrix $A$. This must be the case, since we have expanded our Krylov subspace to span the entire vector space of the original matrix $A$.\n", - "\n", - "In this example, the Krylov method may not appear particularly easier than direct diagonalization. Indeed, as we will see in later sections, the Krylov method is only advantageous above a certain matrix dimension; this is intended to help us solve eigenvalue/eigenvector problems of extremely large matrices.\n", - "\n", - "![An image showing a very large matrix being projected onto a Krylov subspace, that is, rows of Krylov vectors making a matrix on the left, a Hamiltonian, then columns of Krylov vectors on the right.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig1.avif)\n", - "\n", - "This is the only example we will show worked “by hand”, but section 2 below shows computational examples.\n", - "\n", - "#### Clarification of terms\n", - "\n", - "A common misconception is that there is just a single Krylov subspace for a given problem. But of course, since there are many initial vectors to which our matrix could be applied, there are many possible Krylov subspaces. We will only use the phrase \"__the__ Krylov subspace\" to refer to a specific Krylov subspace already defined for a specific example. For general problem-solving approaches we will refer to \"__a__ Krylov subspace\".\n", - "A final clarification is that it is valid to refer to a \"Krylov __space__\". One often sees it called a \"Krylov __subspace__\" because of its use in the context of projecting matrices from an initial space into a subspace. In keeping with that context, we will mostly refer to it as a subspace here." - ] - }, - { - "cell_type": "markdown", - "id": "f3117328-61e0-45a2-b8f2-05356d210fae", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "Explain why it is not (a) useful, and (b) possible to extend the dimension of the Krylov subspace $r$ beyond the dimension $N$ of the matrix of interest.\n", - "\n", - "\n", - "\n", - "\n", - "(a) Since we are orthonormalizing the vectors as we produce them, a set of $N$ such vectors will form a complete basis, meaning a linear combination of them can be used to create any vector in the space.\n", - "\n", - "(b) The orthogonalization process consists of subtracting off the projection of a new vector onto all previous vectors. If all previous vectors span the full vector space, then subtracting off projections onto the full subspace will always leave us with a zero vector.\n", - "\n", - "\n", - "\n", - "\n", - "Suppose a fellow researcher is demonstrating the Krylov method applied to a small toy matrix. Is there something wrong with their choice of matrix $A$ and initial vector $|\\psi\\rangle$?\n", - "\n", - "$$\n", - "A=\\begin{pmatrix}2&1&3\\\\1&2&3\\\\3&3&5\\end{pmatrix}\n", - "$$\n", - "and\n", - "$$\n", - "|\\psi\\rangle=\\frac{1}{\\sqrt{2}}\\begin{pmatrix}1\\\\-1\\\\0\\end{pmatrix}.\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "Your colleague has accidentally chosen an eigenvector for his/her initial vector. Acting with the matrix on the initial vector will simply return the same vector back, scaled by the eigenvalue. This will not generate a subspace of increasing dimension. Advise your colleague to select a different initial vector, making sure it is not an eigenvector.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Apply the Krylov method to the given matrix, selecting an appropriate new initial vector. Write down the estimates of the minimum eigenvalue at the 0th and 1st order of your Krylov subspace.\n", - "\n", - "$$\n", - "A=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "There are many possible answers depending on the choice of initial vector. We will choose:\n", - "$$\n", - "|v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}.\n", - "$$\n", - "To get $|v_1\\rangle$ we apply $A$ once to $|v_0\\rangle$, and then make $|v_1\\rangle$ orthogonal to $|v_0\\rangle.$\n", - "$$\n", - "A|v_0\\rangle=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\n", - "$$\n", - "$$\n", - "A|v_0\\rangle - \\langle v_0|A|v_0\\rangle |v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix} - \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}-\\frac{7}{3}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}=\\sqrt{\\frac{3}{2}}\\begin{pmatrix}-1/3\\\\2/3\\\\-1/3\\end{pmatrix}\n", - "$$\n", - "At 0th order, the projection onto our Krylov subspace is\n", - "\n", - "$$\n", - "\\langle v_0|A|v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{7}{3}\n", - "$$\n", - "At 1st order, the projection onto this Krylov subspace is\n", - "\n", - "$$\n", - "\\langle V^1|A|V^1\\rangle=\\begin{pmatrix}\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}\\\\-\\sqrt{\\frac{1}{6}}&\\sqrt{\\frac{2}{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\begin{pmatrix}\\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\\\\\frac{1}{\\sqrt{3}}& \\sqrt{\\frac{2}{3}} \\\\ \\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix}\n", - "$$\n", - "This can be done by hand, but is most easily done using numpy:\n", - "\n", - "```python\n", - "import numpy as np\n", - "vstar = np.array([[1/np.sqrt(3),1/np.sqrt(3),1/np.sqrt(3)],[-1/np.sqrt(6),np.sqrt(2/3),-1/np.sqrt(6)]]\n", - ")\n", - "A = np.array([[1, 1, 0],\n", - " [1, 1, 1],\n", - " [0, 1, 1]])\n", - "v = np.array([[1/np.sqrt(3),-1/np.sqrt(6)],[1/np.sqrt(3),np.sqrt(2/3)],[1/np.sqrt(3),-1/np.sqrt(6)]])\n", - "proj = vstar@A@v\n", - "print(proj)\n", - "eigenvalues, eigenvectors = np.linalg.eig(proj)\n", - "print(\"The eigenvalues are \", eigenvalues)\n", - "print(\"The eigenvectors are \", eigenvectors)\n", - "```\n", - "outputs:\n", - "```python\n", - "[[ 2.33333333 0.47140452]\n", - " [ 0.47140452 -0.33333333]]\n", - "The eigenvalues are [ 2.41421356 -0.41421356]\n", - "The eigenvectors are [[ 0.98559856 -0.16910198]\n", - " [ 0.16910198 0.98559856]]\n", - "```\n", - "The minimum eigenvalue estimate is -0.414.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "5a4f969e-d346-485a-9e4c-a04dbc9492e9", - "metadata": {}, - "source": [ - "### 1.2 Types of Krylov methods\n", - "\n", - "\"Krylov subspace methods\" can refer to any of several iterative techniques used to solve large linear systems and eigenvalue problems. What they all have in common is that they construct an approximate solution from a Krylov subspace\n", - "\n", - "$$\\mathcal{K}^r(A,|v\\rangle ) = \\text{span}\\{|v\\rangle, A|v\\rangle, A^2|v\\rangle, ..., A^{r-1}|v\\rangle\\},$$\n", - "\n", - "where $|v\\rangle$ is the initial guess (see Ref [\\[5\\]](#references)). They differ in how they choose the best approximation from this subspace, balancing factors such as convergence rate, memory usage, and overall computational cost. The focus of this lesson is to leverage quantum computing in the context of Krylov subspace methods; an exhaustive discussion of these methods is beyond its scope. The brief definitions below are for context only and include some references for investigating these methods further.\n", - "\n", - "__The conjugate gradient (CG) method__: This method is used for solving symmetric, positive definite linear systems[\\[6\\]](#references). It minimizes the A-norm of the error at each iteration, making it particularly effective for systems arising from discretized elliptic PDEs[\\[7\\]](#references). We will use this approach in the next section to motivate why a Krylov subspace would be an effective subspace in which to probe for improved solutions to linear systems.\n", - "\n", - "__The generalized minimal residual (GMRES) method__: This is designed for solving general nonsymmetric linear systems. It minimizes the residual norm over a Krylov space at each iteration, making it robust but potentially memory-intensive for large systems[\\[7\\]](#references).\n", - "\n", - "__The minimal residual (MINRES) method__: This method is used for solving symmetric indefinite linear systems. It's similar to GMRES but takes advantage of the matrix symmetry to reduce computational cost[\\[8\\]](#references).\n", - "\n", - "Other approaches of note include the __full orthogonalization method (FOM)__, which is closely related to Arnoldi's method for eigenvalue problems, the __bi-conjugate gradient (BiCG) method__, and the __induced dimension reduction (IDR) method__." - ] - }, - { - "cell_type": "markdown", - "id": "774fa41b-9fbb-46cc-9c64-37f7a008e161", - "metadata": {}, - "source": [ - "### 1.3 Why the Krylov subspace method works\n", - "\n", - "Here we will motivate that the Krylov subspace method should be an efficient way to approximate matrix eigenvalues via iterative refinement of eigenvector approximations, through the lens of steepest descent. We will argue that given an initial guess of a ground state, the space of successive corrections to that initial guess that yields the fastest convergence is a Krylov subspace. We stop short of a rigorous proof of convergence behavior.\n", - "\n", - "Assume our matrix of interest $A$ is symmetric and positive definite. This makes our argument most relevant to the CG method above. We make no assumptions about sparsity here; nor are we claiming that $A$ must be a Hermitian (which it needs to be if it is a Hamiltonian).\n", - "\n", - "We typically wish to solve a problem of the form\n", - "$$\n", - "A|x\\rangle=|b\\rangle.\n", - "$$\n", - "One might imagine that $|b\\rangle=c|x\\rangle$ where $c$ is some constant, as in an eigenvalue problem. But our problem statement remains more general for now.\n", - "\n", - "We start with a vector $|x_0\\rangle$ that is an approximate solution. Although there are parallels between this guess $|x_0\\rangle$ and $|v_0\\rangle$ in Section 1.1, we are not leveraging these here. Our guess $|x_0\\rangle$ has error, which we call $|e_0\\rangle:$\n", - "$$\n", - "|e_0\\rangle:=|x\\rangle−|x_0\\rangle.\n", - "$$\n", - "We also define the residual $R_0:$\n", - "$$\n", - "|R_0\\rangle=|b\\rangle−A|x_0\\rangle.\n", - "$$\n", - "Here we use capital $R$ to distinguish the residual from the dimension of our Krylov subspace $r$.\n", - "\n", - "![A true eigenvector labeled x, a guess labeled x 0 and a graphical representation of hte error between those two.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig2.svg)\n", - "\n", - "We now want to make a correction step of the form\n", - "$$\n", - "|x_1\\rangle=|x_0\\rangle+|p_0\\rangle,\n", - "$$\n", - "which we hope improves our approximation. Here $|p_0\\rangle$ is some vector yet to be determined. Let $|e_1\\rangle$ be the error after the correction is made. Then\n", - "$$\n", - "|e_1\\rangle=|x\\rangle−|x_1\\rangle=|x\\rangle−(|x_0\\rangle+|p_0\\rangle)=|e_0\\rangle−|p_0\\rangle.\n", - "$$\n", - "\n", - "![A true eigenvector and an update to the initial guess. The updated guess is closer to the true eigenvector.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig3.svg)\n", - "\n", - "We are interested in how our error behaves when transformed by our matrix. So let us calculate the $A$-norm of the error. That is\n", - "$$\n", - "\\begin{aligned}\n", - "∥|e_0\\rangle−|p_0\\rangle∥_A^2&=\\left(\\langle e_0|A−\\langle p_0|A\\right)\\left(|e_0\\rangle−|p_0\\rangle\\right)\\\\\n", - " & = \\langle e_0|A|e_0 \\rangle − \\langle e_0|A|p_0\\rangle − \\langle p_0|A|e_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n", - " & = \\langle e_0|A|e_0\\rangle−2\\langle e_0|A|p_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n", - " & = d−2\\langle R_0|p_0\\rangle +\\langle p_0|A|p_0\\rangle,\n", - " \\end{aligned}\n", - "$$\n", - "where we have used the symmetry of $A$ and also that $A |e_0\\rangle = |R_0\\rangle.$ Here $d$ is some constant independent of $|p_0\\rangle$. As mentioned in Section 1.2, the $A$-norm of the error is not the only quantity we might choose to minimize, but it is a good one. We want to see how this quantity varies with our choice of correction vectors $|p_0\\rangle.$ So we define the function $f$ by setting\n", - "$$\n", - "f(|p_0\\rangle)=\\langle p_0|A|p_0\\rangle−2\\langle R_0|p_0\\rangle+d.\n", - "$$\n", - "$f$ is just the error $|e_1\\rangle$ as a function of the correction $|p_0\\rangle$ measured in the $A$-norm. Hence, we want to choose $|p_0\\rangle$ such that $f(|p_0\\rangle)$ is as small as possible. For this purpose, we compute the gradient of $f$. Using the symmetry of $A$ we have\n", - "$$\n", - "\\nabla f(|p_0\\rangle) = 2(A|p_0\\rangle−|R_0\\rangle).\n", - "$$\n", - "The gradient points in the direction of steepest ascent, meaning its opposite gives us the direction in which the function decreases the most: the direction of __steepest descent__. At our initial guess $|x_0\\rangle$, where $|p_0\\rangle=0$, we have that\n", - "$\\nabla f(0) = -2|R_0\\rangle.$\n", - "Thus, the function $f$ decreases the most in the direction of the residual $|R_0\\rangle.$ So our initial choice would benefit most by the addition of the vector $|p_0\\rangle=\\alpha_0 |R_0\\rangle$ for some scalar $\\alpha_0$.\n", - "\n", - "In the next step, we choose, again, a vector $|p_1\\rangle$ and add its value to the current approximation. Using the same argument as before we choose $|p_1\\rangle = \\alpha_1 |R_1\\rangle$ for some scalar $\\alpha_1$. We continue in this manner, such that the $k^\\text{th}$ iteration of our vector is\n", - "$$\n", - "|x_{k+1}\\rangle=|x_0\\rangle+\\alpha_0 |R_0\\rangle+\\alpha_1 |R_1\\rangle+⋯+\\alpha_k |R_k\\rangle.\n", - "$$\n", - "Equivalently, we want to build up the space from which we choose our improved estimates by adding $|R_0\\rangle$, $|R_1\\rangle$, and so on, in order. The $k^\\text{th}$ estimated vector lies in\n", - "$$\n", - "|x_{k+1}\\rangle\\in |x_0\\rangle+\\text{span}\\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}.\n", - "$$\n", - "Now, using the relation that\n", - "$$\n", - "|R_{k+1}\\rangle=|b\\rangle−A |x_{k+1}\\rangle=|b\\rangle−A(|x_k\\rangle+\\alpha_k |R_k\\rangle)=|R_k\\rangle−\\alpha_k A |R_k\\rangle,\n", - "$$\n", - "we see that\n", - "$$\n", - "\\text{span} \\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}=\\text{span} \\{|R_0\\rangle,A|R_0\\rangle,…,A^{k}|R_0\\rangle \\}.\n", - "$$\n", - "That is, the space we build up that most efficiently approximates the correct solution $|x\\rangle$ is exactly the space built up by successive operation of the matrix $A$ on $|R_0\\rangle.$ A Krylov subspace _is_ the space spanned by the vectors of successive directions of steepest descent.\n", - "\n", - "Finally we reiterate that we have made no numerical claims about the scaling of this approach, nor have we discussed the comparative benefit for sparse matrices. This is only meant to motivate the use of Krylov subspace methods, and add some intuitive sense for them. We will now explore the behavior of these methods numerically." - ] - }, - { - "cell_type": "markdown", - "id": "2df5e11f-62fe-4237-bcaf-36dcc88f11a7", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "In the workflow above, we proposed minimizing the $A$-norm of the error. What other quantities might one consider minimizing in seeking the ground state and its eigenvalue?\n", - "\n", - "\n", - "\n", - "\n", - "One could imagine using the residual vector instead of the $A$-norm of the error. There might be cases in which considering the error vector itself is useful.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "56bcf069-b3b5-4c51-b91f-7e9bb62c5c94", - "metadata": {}, - "source": [ - "## 2. Krylov methods in classical computation\n", - "\n", - "In this section we implement Arnoldi iterations computationally so that we may leverage a Krylov subspace in solving eigenvalue problems. We will apply this first to a small-scale example, then examine how computation time scales as the size of the matrix of interest increases. A key idea here will be that the generation of the vectors spanning the Krylov space will be a large contributor to total computing time required. The memory required will vary between specific Krylov methods. But memory constraints can limit the use of traditional Krylov methods." - ] - }, - { - "cell_type": "markdown", - "id": "952459b6-75dd-4044-8cec-7004fbc48ddf", - "metadata": {}, - "source": [ - "### 2.1 Simple small-scale example\n", - "\n", - "In the process of creating a Krylov subspace, we will need to orthonormalize the vectors in our subspace. Let us define a function that takes an established vector from our subspace `vknown` (not assumed to be normalized) and a candidate vector to add to our subspace `vnext` and make `vnext` orthogonal to `vknown` and normalized. Let us further define a function that steps through this process for all established vectors in our Krylov subspace to ensure a fully orthonormal set." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "fb4f96ea-906c-4819-a9cb-8e9409cac051", - "metadata": {}, - "outputs": [], - "source": [ - "# vknown is some established vector in our subspace. vnext is one we wish to add, which must be orthogonal to vknown.\n", - "\n", - "\n", - "def orthog_pair(vknown, vnext):\n", - " vknown = vknown / np.sqrt(vknown.T @ vknown)\n", - " diffvec = vknown.T @ vnext * vknown\n", - " vnext = vnext - diffvec\n", - " return vnext\n", - "\n", - "\n", - "# v is the candidate vector to be added to our subspace. s is the existing subspace.\n", - "\n", - "\n", - "def orthoset(v, s):\n", - " v = v / np.sqrt(v.T @ v)\n", - " temp = v\n", - " for i in range(len(s)):\n", - " temp = orthog_pair(s[i], temp)\n", - " v = temp / np.sqrt(temp.T @ temp)\n", - " return v" - ] - }, - { - "cell_type": "markdown", - "id": "61a2704e-3985-4bd4-a9a9-253cc96c708b", - "metadata": {}, - "source": [ - "Now let us define a function that builds an iteratively larger and larger Krylov subspace, until the space of Krylov vectors spans the full space of the original matrix. This will enable us to see how well the eigenvalues obtained using our Krylov subspace method match the exact values, as a function of Krylov subspace dimension. Importantly, our function `krylov_full_build` returns the Krylov vectors, the projected Hamiltonians, the eigenvalues, and the time required." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "818cceee-bd88-472a-8af9-0a6c0dec5646", - "metadata": {}, - "outputs": [], - "source": [ - "# Necessary imports and definitions to track time in microseconds\n", - "import time\n", - "\n", - "\n", - "def time_mus():\n", - " return int(time.time() * 1000000)\n", - "\n", - "\n", - "# This function constructs a Krylov subspace that spans the whole space of the original matrix.\n", - "# Input:\n", - "# v0 : initial vector\n", - "# matrix : original matrix to be diagonalized\n", - "# Output:\n", - "# ks : Krylov vectors\n", - "# Hs : projected Hamiltonians\n", - "# eigs : eigenvalues\n", - "# k_tot_times : time required for the operation\n", - "\n", - "\n", - "def krylov_full_build(v0, matrix):\n", - " t0 = time_mus()\n", - " b = v0 / np.sqrt(v0 @ v0.T)\n", - " A = matrix\n", - " ks = []\n", - " ks.append(b)\n", - " Hs = []\n", - " eigs = []\n", - " Hs.append(b.T @ A @ b)\n", - " eigs.append(np.array([b.T @ A @ b]))\n", - " k_tot_times = []\n", - "\n", - " for j in range(len(A) - 1):\n", - " vec = A @ ks[j].T\n", - " ortho = orthoset(vec, ks)\n", - " ks.append(ortho)\n", - " ksarray = np.array(ks)\n", - " Hs.append(ksarray @ A @ ksarray.T)\n", - " eigs.append(np.linalg.eig(Hs[j + 1]).eigenvalues)\n", - " k_tot_times.append(time_mus() - t0)\n", - "\n", - " # Return the Krylov vectors, the projected Hamiltonians, the eigenvalues, and the total time required.\n", - " return (ks, Hs, eigs, k_tot_times)" - ] - }, - { - "cell_type": "markdown", - "id": "4cd7bdcc-65fd-4199-b9fd-d0a9f62cba17", - "metadata": {}, - "source": [ - "We will test this on a matrix that is still quite small, but larger than we might want to do by hand." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "3e2aa939-b568-4982-b64a-9f2e4b0da2b5", - "metadata": {}, - "outputs": [], - "source": [ - "# Define our small test matrix\n", - "test_matrix = np.array(\n", - " [\n", - " [4, -1, 0, 1, 0],\n", - " [-1, 4, -1, 2, 1],\n", - " [0, -1, 4, 3, 3],\n", - " [1, 2, 3, 4, 0],\n", - " [0, 1, 3, 0, 4],\n", - " ]\n", - ")\n", - "\n", - "# Give the test matrix and an initial guess as arguments in the function defined above. Calculate outputs.\n", - "test_ks, test_Hs, test_eigs, text_k_tot_times = krylov_full_build(\n", - " np.array([0.5, 0.5, 0, 0.5, 0.5]), test_matrix\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "2f74fcf0-8959-4862-b33d-c8b1eddda2a8", - "metadata": {}, - "source": [ - "We can check our functions by ensuring that in the last step (when the Krylov space is the full vector space of the original matrix) the eigenvalues from the Krylov method exactly match those of the exact numerical diagonalization:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "704db2f6-5081-404d-9ae9-7264a62ea432", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-1.36956923 8.43756009 2.9040308 5.34436028 4.68361806]\n", - "[-1.36956923 8.43756009 2.9040308 4.68361806 5.34436028]\n" - ] - } - ], - "source": [ - "print(np.linalg.eig(test_matrix).eigenvalues)\n", - "print(test_eigs[len(test_matrix) - 1])" - ] - }, - { - "cell_type": "markdown", - "id": "f0ddf041-d7e8-4131-bb4c-3310d8351c2c", - "metadata": {}, - "source": [ - "That was successful. Of course, what really matters is how good our approximation is as a function of the dimension of our Krylov subspace dimension. Because we are often concerned with finding ground states and other minimum eigenvalues (and for other more algebraic reasons explained below), let's look at our estimate of the lowest eigenvalue as a function of Krylov subspace dimension. That is" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "48b23ca9-a5ba-4a9c-bbc2-7fba75b36124", - "metadata": {}, - "outputs": [], - "source": [ - "def errors(matrix, krylov_eigs):\n", - " targ_min = min(np.linalg.eig(matrix).eigenvalues)\n", - " err = []\n", - " for i in range(len(matrix)):\n", - " err.append(min(krylov_eigs[i]) - targ_min)\n", - " return err" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "0912e12b-63bd-4026-ac8e-56bc5db8736e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "krylov_error = errors(test_matrix, test_eigs)\n", - "\n", - "plt.plot(krylov_error)\n", - "plt.axhline(y=0, color=\"red\", linestyle=\"--\") # Add dashed red line at y=0\n", - "plt.xlabel(\"Order of Krylov subspace\") # Add x-axis label\n", - "plt.ylabel(\"Error in minimum eigenvalue\") # Add y-axis label\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "7af1b947-205b-46f8-bb23-d439ae6f076a", - "metadata": {}, - "source": [ - "We see that the minimum eigenvalue is reached fairly accurately once the Krylov subspace has grown to $\\mathcal{K}^2,$ and is perfect by $\\mathcal{K}^3.$" - ] - }, - { - "cell_type": "markdown", - "id": "7ee80bf2-e3e8-4124-9afc-ef2ef247f831", - "metadata": {}, - "source": [ - "### 2.2 Time scaling with matrix dimension\n", - "\n", - "Let us convince ourselves that the Krylov method can be advantageous over exact numerical eigensolvers in the following way:\n", - "* Construct random matrices (not sparse, not the ideal application for KQD)\n", - "* Determine eigenvalues using two methods: directly using NumPy and using a Krylov subspace.\n", - "* We choose a cutoff for how precise our eigenvalues must be, before we accept the Krylov estimates.\n", - "* Compare the wall time required to solve in these two ways.\n", - "\n", - "__Caveats:__ As we will discuss in detail below, Krylov quantum diagonalization is best applied to operators whose matrix representations are sparse and/or can be written using a small number of groups of commuting Pauli operators. The random matrices we are using here do not fit that description. These are only useful in probing the scale at which classical Krylov methods might be useful.\n", - "Secondly, in using the Krylov method we will calculate eigenvalues using many different-sized Krylov subspaces. We will report the time required for the minimum-dimension Krylov subspace that achieves our required accuracy for the ground state eigenvalue. Again, this is a bit different from solving a problem that is intractable for exact eigensolvers, since we are using the exact solution to assess the dimension needed.\n", - "\n", - "We begin by generating our set of random matrices." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a5f8f1f6-b334-43e4-ad40-fd689c07a3e1", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# Set the random seed\n", - "np.random.seed(42)\n", - "\n", - "# how many random matrices will we make\n", - "num_matrix = 200\n", - "\n", - "matrices = []\n", - "for m in range(1, num_matrix):\n", - " matrices.append(np.random.rand(m, m))" - ] - }, - { - "cell_type": "markdown", - "id": "d1c31d97-7e76-4577-ab03-ce8f0aebf134", - "metadata": {}, - "source": [ - "We now diagonalize each matrix directly, using numpy. We calculate the time required for diagonalization for later comparison." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "d9cb6e36-9ad3-403f-823f-0072e9cf53fe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "matrix_numpy_times = []\n", - "matrix_numpy_eigs = []\n", - "for mm in range(num_matrix - 1):\n", - " t0 = time_mus()\n", - " matrix_numpy_eigs.append(min(np.linalg.eig(matrices[mm]).eigenvalues))\n", - " matrix_numpy_times.append(time_mus() - t0)\n", - "\n", - "plt.plot(matrix_numpy_times)\n", - "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", - "plt.ylabel(\"Time to diagonalize (microsec)\") # Add y-axis label\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "4e5d4af5-78da-4254-a726-4b0a72f98fe6", - "metadata": {}, - "source": [ - "Note that in the image above, the anomalously high time around a dimension of 125 may be due to the random nature of the matrices or due to implementation on the classical processor used, but it is not reproducible. Re-running the code will yield a different profile with different anomalous peaks.\n", - "\n", - "Now for each matrix we will build up a Krylov subspace and calculate eigenvalues in steps. At each step, we will check to see if the lowest eigenvalue has been obtained to within our specified absolute error. The subspace that first gives us eigenvalues within our specified error is the subspace for which we will record computation times. Executing this cell may take several minutes, depending on processor speed. Feel free to skip evaluation or reduce the maximum dimension of matrices diagonalized. Looking at the pre-calculated results is sufficient." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7fbe57ec-507a-412e-b7fe-09aaea28e103", - "metadata": {}, - "outputs": [], - "source": [ - "# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size at which that error is achieved.\n", - "abserr = 0.05\n", - "accept_subspace_size = []\n", - "\n", - "# Lists to store total time spent on the Krylov method, and the subset of that time spent on diagonalizing the projected matrix.\n", - "matrix_krylov_tot_times = []\n", - "matrix_krylov_dim = []\n", - "\n", - "# Step through all our random matrices\n", - "for mm in range(0, num_matrix - 1):\n", - " test_ks, test_Hs, test_eigs, test_k_tot_times = krylov_full_build(\n", - " np.ones(len(matrices[mm])), matrices[mm]\n", - " )\n", - " # We have not yet found a Krylov subspace that produces our minimum eigenvalue to within the required error.\n", - " found = 0\n", - " for j in range(0, len(matrices[mm]) - 1):\n", - " # If we still haven't found the desired subspace...\n", - " if found == 0:\n", - " # ...but if this one satisfies the requirement, then record everything\n", - " if (\n", - " abs((min(test_eigs[j]) - matrix_numpy_eigs[mm]) / matrix_numpy_eigs[mm])\n", - " < abserr\n", - " ):\n", - " accept_subspace_size.append(j)\n", - " matrix_krylov_tot_times.append(test_k_tot_times[j])\n", - " matrix_krylov_dim.append(mm)\n", - " found = 1" - ] - }, - { - "cell_type": "markdown", - "id": "5046bfdb-9ff6-4142-9dc6-260ce26fd3f5", - "metadata": {}, - "source": [ - "Let us plot the times we have obtained for these two methods for comparison:" - ] - }, - { - "cell_type": "code", - "execution_count": 94, - "id": "ed3302d5-a0a8-479b-9731-755fc5a1b684", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(matrix_numpy_times, color=\"blue\")\n", - "plt.plot(matrix_krylov_dim, matrix_krylov_tot_times, color=\"green\")\n", - "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", - "plt.ylabel(\"Time to diagonalize (microsec)\") # Add y-axis label\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "e3c5ff65-964f-4b7c-8ca7-89958b3d9465", - "metadata": {}, - "source": [ - "These are the actual times required, but for the purposes of discussion, let us smooth these curves by averaging over a few adjacent points / matrix dimensions. This is done below:" - ] - }, - { - "cell_type": "code", - "execution_count": 95, - "id": "41ac6108-526d-476b-8255-db9bb52a30ca", - "metadata": {}, - "outputs": [], - "source": [ - "smooth_numpy_times = []\n", - "smooth_krylov_times = []\n", - "\n", - "# Choose the number of adjacent points over which to average forward; the same will be used backward.\n", - "smooth_steps = 10\n", - "\n", - "# We will do this smoothing for all points/matrix dimensions\n", - "for i in range(len(matrix_krylov_tot_times)):\n", - " # Ensure we don't exceed the boundaries of our lists\n", - " start = max(0, i - smooth_steps)\n", - " end = min(len(matrix_krylov_tot_times) - 1, i + smooth_steps)\n", - "\n", - " # Dummy variables for accumulating an average over adjacent points. This is done for both Krylov and the NumPy calculations.\n", - " smooth_count = 0\n", - " smooth_numpy_sum = 0\n", - " smooth_krylov_sum = 0\n", - "\n", - " for j in range(start, end):\n", - " smooth_numpy_sum = smooth_numpy_sum + matrix_numpy_times[j]\n", - " smooth_krylov_sum = smooth_krylov_sum + matrix_krylov_tot_times[j]\n", - " smooth_count = smooth_count + 1\n", - "\n", - " # Appending the averaged adjacent values to our new smooth lists\n", - " smooth_numpy_times.append(smooth_numpy_sum / smooth_count)\n", - " smooth_krylov_times.append(smooth_krylov_sum / smooth_count)" - ] - }, - { - "cell_type": "code", - "execution_count": 96, - "id": "6963ee9b-55a8-42ab-bc4f-d470bb81e543", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(smooth_numpy_times, color=\"blue\")\n", - "plt.plot(smooth_krylov_times, color=\"green\")\n", - "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", - "plt.ylabel(\"Time to diagonalize (smoothed, microsec)\") # Add y-axis label\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "7e1bbb91-5da7-43ea-8b99-d04ea1196e76", - "metadata": {}, - "source": [ - "Note that the time required for the building of a Krylov subspace initially exceeds the time required for numpy's full diagonalization. But as the size of the matrix grows, the Krylov method becomes advantageous. This is true even if we lower our acceptable error, but the advantage sets in at a larger matrix size. This is worth picking apart.\n", - "\n", - "The time complexity of numerical diagonalization is $O(n^3)$ (with some variation among algorithms). The time complexity of generating an orthonormal basis of $n$ vectors is also $O(n^3)$. So the advantage of the Krylov method is __not__ related to the use of $\\it{some}$ orthonormal basis, but to the use of a particular orthonormal basis that effectively picks out the eigenvalues of interest. We have already seen this from the sketch of a proof in the first section of this lesson, and this is critical for the convergence guarantees in Krylov methods." - ] - }, - { - "cell_type": "markdown", - "id": "da603cad-2e8e-4494-a0fd-f082953c35cc", - "metadata": {}, - "source": [ - "Let us review our progress so far:\n", - "* For very large matrices, the Krylov subspace method may yield approximate eigenvalues within required tolerances faster than traditional diagonalization algorithms.\n", - "* For such very large matrices, the generation of a Krylov subspace is the most time-consuming part of the Krylov subspace method.\n", - "* Thus an efficient way of generating a Krylov subspace would be highly valuable.\n", - "This is finally where quantum computer comes into the picture." - ] - }, - { - "cell_type": "markdown", - "id": "7782cac1-a86b-4a70-971f-3db5922838d7", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "Refer to the smoothed plot of diagonalization times versus matrix dimension above.\n", - "\n", - "(a) At approximately what matrix dimension did the Krylov method become faster, according to this plot?\n", - "\n", - "(b) What aspects of the calculation could change that dimension at which the Krylov method becomes faster?\n", - "\n", - "\n", - "\n", - "\n", - "(a) Answers may vary if you re-run the calculation, but the Krylov method becomes faster at approximately dimension 80-85.\n", - "\n", - "(b) There are many possible answers. Some important factors are the precision we require and the sparsity of the matrices being diagonalized.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "03b62a85-4737-41ec-b264-55d22fe814bc", - "metadata": {}, - "source": [ - "## 3. Krylov via time evolution\n", - "\n", - "Everything that we have described so far can be done classically. So how and when would we use a quantum computer? For very large matrices, the Krylov method can require long computing times and large amounts of memory. The time required for matrix operation of $H$ on $|v\\rangle$ scales like $O(N^2)$ in the worst case. Even multiplying sparse matrices on a vector (the typical case for classical Krylov-type solvers) has a time complexity scaling like $O(N)$. This is done for every vector we want in our subspace. The subspace dimension $r$ is usually not a significant fraction of $N$, and often scales like $\\log(N)$. So generating all vectors scales like $O(N^2 \\log(N))$ in the worst case. Although there are other steps, like orthogonalization, this is the dominant scaling to keep in mind.\n", - "\n", - "Quantum computing allows us to change what attributes of the problem determine the scaling of the time and resources required. Instead of dependence on matrix size $N$ across the board, we will see things like number of shots and number of non-commuting Pauli terms that make up the Hamiltonian. Let’s explore how this works." - ] - }, - { - "cell_type": "markdown", - "id": "1636f90b-6bdf-4b11-aee3-9be3f2e82d79", - "metadata": {}, - "source": [ - "### 3.1 Time evolution\n", - "\n", - "Recall that the operator that time-evolves a quantum state is $e^{-iHt/\\hbar}$ (and it is very common, especially in quantum computing to drop the $\\hbar$ from the notation). One way of understanding and even realizing such an exponential function of an operator is to look at its Taylor series expansion. Note that this operation acting on some initial vector $|v\\rangle$ yields a sum of terms with increasing powers of $H$ applied to the initial state. It looks like we can just make our Krylov subspace by time-evolving our initial guess state!\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "e^{-iHt/\\hbar}→e^{-iHt}&≈1-iHt-\\frac{(H^2 t^2)}{2}+⋯\\\\\n", - "e^{-iHt} |v\\rangle &≈ |v\\rangle-iHt|v\\rangle-\\frac{(H^2 t^2)}{2}|v\\rangle+⋯\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "The caveat is in realizing the time evolution on a real quantum computer. Many of the terms in the Hamiltonian will not commute with each other. So while some simple exponential operators like $e^{-iZ}$ correspond to simple circuits, general Hamiltonians do not. And since they contain non-commuting terms, we can’t simply decompose the exponential into a product of simple ones, the way we can with numbers.\n", - "\n", - "$$\n", - "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n)t}\\neq e^{-iH_1 t} e^{-iH_2 t}... e^{-iH_n t}\n", - "$$\n", - "\n", - "So this is not trivial, but this is a well-studied process in quantum computing. We carry out time-evolution on quantum computers using a process called trotterization, which in itself is a rich subject[\\[10\\]](#references). But at a very high level, by breaking the time evolution into very small steps, say $m$ steps of size $dt$, we limit the effects of the non-commutativity of terms.\n", - "\n", - "$$\n", - "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n )t} = (e^{-i(H_1+H_2+⋯+H_n )t/m} )^m\n", - "≈(e^{-iH_1 dt} e^{-iH_2 dt} …e^{-iH_n dt} )^m\n", - "$$\n", - "where $dt = t/m$.\n", - "\n", - "Let us call a Krylov subspace of order r that we generated in the classical context using powers of H directly a “power Krylov subspace”.\n", - "\n", - "$$\n", - "\\mathcal{K}_P^r (H,|v\\rangle)=\\text{span}\\{|v\\rangle,H|v\\rangle,H^2 |v\\rangle… H^{r-1} |v\\rangle\\}\n", - "$$\n", - "\n", - "Now we generate a similar space using the unitary time-evolution operator $U \\equiv e^{-iHt}$; we'll refer to this as the “unitary Krylov space” $\\mathcal{K}_U^r$. The power Krylov subspace $\\mathcal{K}_P^r$ that we use classically cannot be generated directly on a quantum computer as $H$ is not a unitary operator. Using the unitary Krylov subspace can be shown to give similar convergence guarantees as the power Krylov subspace, namely, the ground state error converges efficiently as long as the initial state $|v\\rangle$ has overlap with the true ground state that is not exponentially vanishing, and as long as there is a sufficient gap between eigenvalues. See Ref [\\[1\\]](#references) for a more precise discussion of convergence.\n", - "\n", - "Here, powers of $U$ become different time steps (the $k^\\text{th}$ power of $U$ is stepping forward by a time $k \\times dt$). We can label the element of the subspace that is time-evolved for total time $k dt$ as $|\\psi_k\\rangle$." - ] - }, - { - "cell_type": "markdown", - "id": "619c81f7-be3c-4ffa-87f0-2ab6420381ca", - "metadata": {}, - "source": [ - "$$\n", - "\\begin{aligned}\n", - "U&=e^{-iHdt}\\\\\n", - "U^k&=e^{-iH(kdt)}\\\\\n", - "\\mathcal{K}_U^r&=\\text{span}\\{|\\psi\\rangle,U|\\psi\\rangle,U^2 |\\psi\\rangle… U^{r-1} |\\psi\\rangle\\}\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "ca6352b4-88c2-4dab-9f82-1750223ec8af", - "metadata": {}, - "source": [ - "We can project our Hamiltonian H on to the unitary Krylov subspace, $\\mathcal{K}_U^r$. In other words, we calculate each matrix element of $H$ in the $\\mathcal{K}_U^r$ basis. We'll refer to this projected matrix as $\\tilde{H}$.\n", - "\n", - "### 3.2 How to implement on a quantum computer\n", - "\n", - "The matrix elements of $\\tilde{H}$ are given by the expectation values $\\langle \\psi_m |H| \\psi_n\\rangle$, which can be estimated using the quantum computer. Keep in mind that $H$ can be written as a sum of Pauli operators on different qubits, and that not all the Pauli operators can be measured simultaneously. We can sort the Pauli terms into groups of commuting terms, and measure all those at once. But we may need many such groups to cover all the terms. So the number of distinct commuting groups into which the terms can be partitioned, $N_\\text{GCP}$ becomes important.\n", - "\n", - "$$\n", - "H=\\sum_{\\alpha=1}^{N_\\text{GCP}} c_\\alpha P_\\alpha\n", - "$$\n", - "Here $P_\\alpha$ is a Pauli string of the form $P_\\alpha \\sim IZIXII...YZXIX$ or a set of such Pauli strings that commute with one another. Given that we can write $H$ as such a sum of measureable operators, the following expressions for matrix elements of $\\tilde{H}$ can be realized using the Qiskit Runtime primitive Estimator.\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\tilde{H}_{mn}&=\\langle \\psi_m |H| \\psi_n\\rangle\\\\\n", - "&=\\langle \\psi e^{iHt_m} |H| \\psi e^{-iHt_n}\\rangle\\\\\n", - "&=\\langle \\psi e^{iHmdt} |H|\\psi e^{-iHndt}\\rangle\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Where $\\vert \\psi_n \\rangle = e^{-i H t_n} \\vert \\psi \\rangle$ are the vectors of the unitary Krylov space and $t_n = n dt$ are the multiples of the time step $dt$ chosen. On a quantum computer, the calculation of each matrix elements can be done with any algorithm which allows us to obtain overlap between quantum states. In this lesson we'll focus on the Hadamard test. Given that the $\\mathcal{K}_U$ has dimension $r$, the Hamiltonian projected into the subspace will have dimensions $r \\times r$. With $r$ small enough (generally $r<<100$ is sufficient to obtain convergence of estimates of eigenvalues) we can then easily diagonalize the projected Hamiltonian $\\tilde{H},$ classically. However, we cannot directly diagonalize $\\tilde{H}$ because of the non-orthogonality of the Krylov space vectors. We'll have to measure their overlaps and construct a matrix $\\tilde{S}$\n", - "\n", - "$$\n", - "\\tilde{S}_{mn} = \\langle \\psi_m \\vert \\psi_n \\rangle\n", - "$$\n", - "\n", - "This allows us to solve the eigenvalue problem in a non-orthogonal space (also called generalized eigenvalue problem)\n", - "\n", - "$$\n", - "\\tilde{H} \\ \\vec{c} = E \\ \\tilde{S} \\ \\vec{c}\n", - "$$\n", - "\n", - "One can then obtain estimates of the eigenvalues and eigenstates of $H$ by looking at the solutions of this generalized eigenvalue problem. For example, the estimate of the ground state energy is obtained by taking the smallest eigenvalue $E$ and the ground state from the corresponding eigenvector $\\vec{c}$. The coefficients in $\\vec{c}$ determines the contribution of the different vectors that span $\\mathcal{K}_U$." - ] - }, - { - "cell_type": "markdown", - "id": "7d29b607-3f2c-445c-9630-3cde4edf3299", - "metadata": {}, - "source": [ - "#### Generalized eigenvalue problem\n", - "\n", - "Why can we not simply diagonalize $\\tilde{H}$? Since $\\tilde{S}$ contains the information about the geometry of the Krylov basis (which is nonorthogonal in all but very special cases), $\\tilde{H}$ on its own does not describe a projection of the full Hamiltonian, so its eigenvalues have no particular relation to those of the full Hamiltonian -- they could be any random values. Solving the generalized eigenvalue problem is required to obtain the approximate eigenvalues and eigenvectors corresponding to the projection of the full Hamiltonian into the Krylov space.." - ] - }, - { - "cell_type": "markdown", - "id": "2f3ba07d-0c16-47fb-aeed-b4ef0a1f25e3", - "metadata": {}, - "source": [ - "![A circuit diagram with many layers indicating that the circuit must be used many times with different states to perform the modified Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig4.avif)\n", - "\n", - "The Figure shows a circuit representation of the modified Hadamard test, a method that is used to compute the overlap between different quantum states. For each matrix element $\\tilde{H}_{i,j}$, a Hadamard test between the state $\\vert \\psi_i \\rangle$, $\\vert \\psi_j \\rangle$ is carried out. This is highlighted in the figure by the color scheme for the matrix elements and the corresponding $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ operations. Thus, a set of Hadamard tests for all the possible combinations of Krylov space vectors is required to compute all the matrix elements of the projected Hamiltonian $\\tilde{H}$. The top wire in the Hadamard test circuit is an ancilla qubit which is measured either in the X or Y basis, its expectation value determines the value of the overlap between the states. The bottom wire represents all the qubits of the system Hamiltonian. The $\\text{Prep} \\; \\psi_i$ operation prepares the system qubit in the state $\\vert \\psi_i \\rangle$ controlled by the state of the ancilla qubit (similarly for $\\text{Prep} \\; \\psi_j$) and the operation $P$ represents Pauli decomposition of the system Hamiltonian $H = \\sum_i P_i$. The implementation of this is on a quantum computer is shown in greater detail below." - ] - }, - { - "cell_type": "markdown", - "id": "c12c2855-f276-48a2-8901-13baf0384778", - "metadata": {}, - "source": [ - "## 4. Krylov quantum diagonalization on a quantum computer\n", - "\n", - "We will now implement Krylov quantum diagonalization on a real quantum computer. Let's start by importing some useful packages." - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "afa233e1-1e80-4843-a958-0f84cec707ea", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import scipy as sp\n", - "import matplotlib.pylab as plt\n", - "from typing import Union, List\n", - "import warnings\n", - "\n", - "from qiskit.quantum_info import SparsePauliOp, Pauli\n", - "from qiskit.circuit import Parameter\n", - "from qiskit import QuantumCircuit, QuantumRegister\n", - "from qiskit.circuit.library import PauliEvolutionGate\n", - "from qiskit.synthesis import LieTrotter\n", - "\n", - "# from qiskit.providers.fake_provider import Fake20QV1\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator, Batch\n", - "\n", - "import itertools as it\n", - "\n", - "warnings.filterwarnings(\"ignore\")" - ] - }, - { - "cell_type": "markdown", - "id": "e4a3fba7-051f-4df4-b78a-7bfeb18caf1e", - "metadata": {}, - "source": [ - "We define the function below to solve the generalized eigenvalue problem we just explained above." - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "5446eb74-126f-4db1-b018-d5f4613f79e7", - "metadata": {}, - "outputs": [], - "source": [ - "def solve_regularized_gen_eig(\n", - " h: np.ndarray,\n", - " s: np.ndarray,\n", - " threshold: float,\n", - " k: int = 1,\n", - " return_dimn: bool = False,\n", - ") -> Union[float, List[float]]:\n", - " \"\"\"\n", - " Method for solving the generalized eigenvalue problem with regularization\n", - "\n", - " Args:\n", - " h (numpy.ndarray):\n", - " The effective representation of the matrix in our Krylov subspace\n", - " s (numpy.ndarray):\n", - " The matrix of overlaps between vectors of our Krylov subspace\n", - " threshold (float):\n", - " Cut-off value for the eigenvalue of s\n", - " k (int):\n", - " Number of eigenvalues to return\n", - " return_dimn (bool):\n", - " Whether to return the size of the regularized subspace\n", - "\n", - " Returns:\n", - " lowest k-eigenvalue(s) that are the solution of the regularized generalized eigenvalue problem\n", - "\n", - "\n", - " \"\"\"\n", - " s_vals, s_vecs = sp.linalg.eigh(s)\n", - " s_vecs = s_vecs.T\n", - " good_vecs = np.array([vec for val, vec in zip(s_vals, s_vecs) if val > threshold])\n", - " h_reg = good_vecs.conj() @ h @ good_vecs.T\n", - " s_reg = good_vecs.conj() @ s @ good_vecs.T\n", - " if k == 1:\n", - " if return_dimn:\n", - " return sp.linalg.eigh(h_reg, s_reg)[0][0], len(good_vecs)\n", - " else:\n", - " return sp.linalg.eigh(h_reg, s_reg)[0][0]\n", - " else:\n", - " if return_dimn:\n", - " return sp.linalg.eigh(h_reg, s_reg)[0][:k], len(good_vecs)\n", - " else:\n", - " return sp.linalg.eigh(h_reg, s_reg)[0][:k]" - ] - }, - { - "cell_type": "markdown", - "id": "7dce32ec-33eb-474d-88bf-e07f6563d6a2", - "metadata": {}, - "source": [ - "At least in initial benchmarking, it is useful to know an exact classical solution to check convergence behavior. The function below calculates the ground state energy of a Hamiltonian, using the Hamiltonian and the number of qubits as arguments." - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "id": "f2d43bcc-5210-448d-b3d9-996796f782f6", - "metadata": {}, - "outputs": [], - "source": [ - "def single_particle_gs(H_op, n_qubits):\n", - " \"\"\"\n", - " Find the ground state of the single particle(excitation) sector\n", - " \"\"\"\n", - " H_x = []\n", - " for p, coeff in H_op.to_list():\n", - " H_x.append(set([i for i, v in enumerate(Pauli(p).x) if v]))\n", - "\n", - " H_z = []\n", - " for p, coeff in H_op.to_list():\n", - " H_z.append(set([i for i, v in enumerate(Pauli(p).z) if v]))\n", - "\n", - " H_c = H_op.coeffs\n", - "\n", - " print(\"n_sys_qubits\", n_qubits)\n", - "\n", - " n_exc = 1\n", - " sub_dimn = int(sp.special.comb(n_qubits + 1, n_exc))\n", - " print(\"n_exc\", n_exc, \", subspace dimension\", sub_dimn)\n", - "\n", - " few_particle_H = np.zeros((sub_dimn, sub_dimn), dtype=complex)\n", - "\n", - " sparse_vecs = [\n", - " set(vec) for vec in it.combinations(range(n_qubits + 1), r=n_exc)\n", - " ] # list all of the possible sets of n_exc indices of 1s in n_exc-particle states\n", - "\n", - " m = 0\n", - " for i, i_set in enumerate(sparse_vecs):\n", - " for j, j_set in enumerate(sparse_vecs):\n", - " m += 1\n", - "\n", - " if len(i_set.symmetric_difference(j_set)) <= 2:\n", - " for p_x, p_z, coeff in zip(H_x, H_z, H_c):\n", - " if i_set.symmetric_difference(j_set) == p_x:\n", - " sgn = ((-1j) ** len(p_x.intersection(p_z))) * (\n", - " (-1) ** len(i_set.intersection(p_z))\n", - " )\n", - " else:\n", - " sgn = 0\n", - "\n", - " few_particle_H[i, j] += sgn * coeff\n", - "\n", - " gs_en = min(np.linalg.eigvalsh(few_particle_H))\n", - " print(\"single particle ground state energy: \", gs_en)\n", - " return gs_en" - ] - }, - { - "cell_type": "markdown", - "id": "c70998c4-e65c-456e-b3b4-61a289c8dd84", - "metadata": {}, - "source": [ - "### 4.1 Step 1: Map problem to quantum circuits and operators\n", - "\n", - "Now we will define a Hamiltonian. This is distinct from the function above in that the function above takes a Hamiltonian as an argument and returns only the ground state, and it does so classically. This Hamiltonian we define here determines the energy levels of all energy eigenstates, and this Hamiltonian can be constructed using Pauli operators and implemented on a quantum computer.\n", - "\n", - "We choose a Hamiltonian corresponding to a chain of spins which can have any orientation in space, called a \"Heisenberg chain\". We assume that the $i^\\text{th}$ spin can be influenced by its nearest neighbors (the $(i-1)^\\text{th}$ and $(i+1)^\\text{th}$ spins) but not by more distant neighbors. We also allow for the possibility that the interaction between spins is different when the spins point along different axes. This asymmetry sometimes arises, for example, due to the structure of the crystal lattice into which spins are embedded." - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "id": "8b547f8a-df47-4e56-921b-3955eb7c19a9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[('ZZIIIIIIII', 2), ('IZZIIIIIII', 2), ('IIZZIIIIII', 2), ('IIIZZIIIII', 2), ('IIIIZZIIII', 2), ('IIIIIZZIII', 2), ('IIIIIIZZII', 2), ('IIIIIIIZZI', 2), ('IIIIIIIIZZ', 2), ('XXIIIIIIII', 1), ('IXXIIIIIII', 1), ('IIXXIIIIII', 1), ('IIIXXIIIII', 1), ('IIIIXXIIII', 1), ('IIIIIXXIII', 1), ('IIIIIIXXII', 1), ('IIIIIIIXXI', 1), ('IIIIIIIIXX', 1), ('YYIIIIIIII', 3), ('IYYIIIIIII', 3), ('IIYYIIIIII', 3), ('IIIYYIIIII', 3), ('IIIIYYIIII', 3), ('IIIIIYYIII', 3), ('IIIIIIYYII', 3), ('IIIIIIIYYI', 3), ('IIIIIIIIYY', 3)]\n" - ] - } - ], - "source": [ - "# Define problem Hamiltonian.\n", - "n_qubits = 10\n", - "# coupling strength for XX, YY, and ZZ interactions\n", - "JX = 1\n", - "JY = 3\n", - "JZ = 2\n", - "\n", - "# Define the Hamiltonian:\n", - "H_int = [[\"I\"] * n_qubits for _ in range(3 * (n_qubits - 1))]\n", - "for i in range(n_qubits - 1):\n", - " H_int[i][i] = \"Z\"\n", - " H_int[i][i + 1] = \"Z\"\n", - "for i in range(n_qubits - 1):\n", - " H_int[n_qubits - 1 + i][i] = \"X\"\n", - " H_int[n_qubits - 1 + i][i + 1] = \"X\"\n", - "for i in range(n_qubits - 1):\n", - " H_int[2 * (n_qubits - 1) + i][i] = \"Y\"\n", - " H_int[2 * (n_qubits - 1) + i][i + 1] = \"Y\"\n", - "H_int = [\"\".join(term) for term in H_int]\n", - "H_tot = [\n", - " (term, JZ)\n", - " if term.count(\"Z\") == 2\n", - " else (term, JY)\n", - " if term.count(\"Y\") == 2\n", - " else (term, JX)\n", - " for term in H_int\n", - "]\n", - "\n", - "# Get operator\n", - "H_op = SparsePauliOp.from_list(H_tot)\n", - "print(H_tot)" - ] - }, - { - "cell_type": "markdown", - "id": "759b8b55-894b-4d3b-93de-5df4f63f9609", - "metadata": {}, - "source": [ - "The code below restricts the Hamiltonian to single particle states, and uses the spectral norm to set a good size for our time step $dt$. We heuristically choose a value for the time-step `dt` (based on upper bounds on the Hamiltonian norm). Ref [\\[9\\]](#references) showed that a sufficiently small timestep is $\\pi/\\vert \\vert H \\vert \\vert$, and that it is preferable up to a point to underestimate this value rather than overestimate, since overestimating can allow contributions from high-energy states to corrupt even the optimal state in the Krylov space. On the other hand, choosing $dt$ to be too small leads to worse conditioning of the Krylov subspace, since the Krylov basis vectors differ less from timestep to timestep." - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "3dd96fbe-47bb-444b-b403-c67c2dcc9d07", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "np.float64(0.17453292519943295)" - ] - }, - "execution_count": 68, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get Hamiltonian restricted to single-particle states\n", - "single_particle_H = np.zeros((n_qubits, n_qubits))\n", - "for i in range(n_qubits):\n", - " for j in range(i + 1):\n", - " for p, coeff in H_op.to_list():\n", - " p_x = Pauli(p).x\n", - " p_z = Pauli(p).z\n", - " if all(p_x[k] == ((i == k) + (j == k)) % 2 for k in range(n_qubits)):\n", - " sgn = ((-1j) ** sum(p_z[k] and p_x[k] for k in range(n_qubits))) * (\n", - " (-1) ** p_z[i]\n", - " )\n", - " else:\n", - " sgn = 0\n", - " single_particle_H[i, j] += sgn * coeff\n", - "for i in range(n_qubits):\n", - " for j in range(i + 1, n_qubits):\n", - " single_particle_H[i, j] = np.conj(single_particle_H[j, i])\n", - "\n", - "# Set dt according to spectral norm\n", - "dt = np.pi / np.linalg.norm(single_particle_H, ord=2)\n", - "dt" - ] - }, - { - "cell_type": "markdown", - "id": "cdb8c08b-caca-4bd5-ae93-065c42485e13", - "metadata": {}, - "source": [ - "We specify the number of Trotter steps to use in the time evolution. We also specify a maximum Krylov dimension of 4. This Krylov dimension is not large enough for realistic applications. But it is sufficient for this example. Furthermore, we will check for convergence at even smaller dimensions. We will explore methods in later lessons that allow us to scale and project our Hamiltonians onto larger subspaces." - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "id": "d95c6f17-7275-474a-b1e5-a41c4539ed83", - "metadata": {}, - "outputs": [], - "source": [ - "# Set parameters for quantum Krylov algorithm\n", - "krylov_dim = 4 # size of krylov subspace\n", - "num_trotter_steps = 4\n", - "dt_circ = dt / num_trotter_steps" - ] - }, - { - "cell_type": "markdown", - "id": "29291306-28a3-4c12-94ef-b3ffd5521c76", - "metadata": {}, - "source": [ - "#### State preparation\n", - "\n", - "Pick a reference state $\\vert \\psi \\rangle$ that has some overlap with the ground state. For this Hamiltonian, We use the a state with an excitation in the middle qubit $\\vert 00..010...00 \\rangle$ as our reference state." - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "410192fb-8197-4860-8c3a-2e874e2f9c56", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc_state_prep = QuantumCircuit(n_qubits)\n", - "qc_state_prep.x(int(n_qubits / 2) + 1)\n", - "qc_state_prep.draw(\"mpl\", scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "dee3c383-fc2e-4420-8d04-861f5f4f4576", - "metadata": {}, - "source": [ - "#### Time evolution\n", - "\n", - "We can realize the time-evolution operator generated by a given Hamiltonian: $U=e^{-iHt}$ via the [Lie-Trotter approximation](/docs/api/qiskit/qiskit.synthesis.LieTrotter). For simplicity we use the built-in ```PauliEvolutionGate``` in the time-evolution circuit. The general syntax for this is this." - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "4b3dddd0-391a-412a-9663-9e15a153125a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 71, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "t = Parameter(\"t\")\n", - "\n", - "## Create the time-evo op circuit\n", - "evol_gate = PauliEvolutionGate(\n", - " H_op, time=t, synthesis=LieTrotter(reps=num_trotter_steps)\n", - ")\n", - "\n", - "qr = QuantumRegister(n_qubits)\n", - "qc_evol = QuantumCircuit(qr)\n", - "qc_evol.append(evol_gate, qargs=qr)" - ] - }, - { - "cell_type": "markdown", - "id": "295bf203-2bc0-4c02-912b-423e5ca392bb", - "metadata": {}, - "source": [ - "We will use a version of this below in the Hadamard test, but stepping forward for times $dt$." - ] - }, - { - "cell_type": "markdown", - "id": "03f71981-3a22-4ba6-8281-7b20895dbda3", - "metadata": {}, - "source": [ - "#### Hadamard test\n", - "\n", - "Recall that we wish to calculate the matrix elements of both $\\tilde{H}$ and the Gram matrix $\\tilde{S}$ using the Hadamard test. Let's review how this works in this context, focusing first on the construction of $\\tilde{H}.$ The overall process is depicted graphically below. The layers of colored state preparation blocks $\\text{Prep}|\\psi_i\\rangle$ serve as a reminder that this process is carried out for all combinations of $|\\psi_i\\rangle$ and $|\\psi_j\\rangle$ in our subspace.\n", - "\n", - "![An image of a quantum circuit diagram with many layers indicating that the circuit must be evaluated for many different states in order to perform the Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig5.avif)\n", - "\n", - "The states of the system at the steps indicated are:\n", - "$$\n", - "\\begin{aligned}\n", - " \\text{Step 0:}\\qquad|\\Psi\\rangle & = |0\\rangle|0\\rangle^N \\\\\n", - " \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle + |1\\rangle \\Big)|0\\rangle^N \\\\\n", - " \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle|0\\rangle^N+|1\\rangle |\\psi_i\\rangle\\Big)\\\\\n", - " \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |0\\rangle^N+|1\\rangle P |\\psi_i\\rangle\\Big) \\\\\n", - " \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |\\psi_j\\rangle+|1\\rangle P|\\psi_i\\rangle\\Big)\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Here $P$ is a Pauli term in the decomposition of the Hamiltonian (note that it cannot be a linear combination of multiple commuting Pauli terms since that would not be unitary -- grouping is possible using a different construction we will show later) $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ are controlled operations that prepare $|\\psi_i\\rangle$, $|\\psi_j\\rangle$ vectors of the unitary Krylov space, with $|\\psi_k\\rangle = e^{-i H k dt } \\vert \\psi \\rangle = e^{-i H k dt } U_{\\psi} \\vert 0 \\rangle^N$. Applying measurements of $X$ and $Y$ to this circuit calculates the real and imaginary parts, respectively, of the matrix elements we require.\n", - "\n", - "Starting from Step 4 above, apply the Hadamard gate $H$ to the zeroth qubit.\n", - "\n", - "$$\n", - "\\begin{equation*}\n", - " |\\Psi\\rangle \\longrightarrow\\quad\\frac{1}{2}|0\\rangle\\Big( |\\psi_j\\rangle + P|\\psi_i\\rangle\\Big) + \\frac{1}{2}|1\\rangle\\Big(|\\psi_j\\rangle - P|\\psi_i\\rangle\\Big)\n", - "\\end{equation*}\n", - "$$\n", - "\n", - "Then measure either $X$ or $Y$.\n", - "\n", - "$$\n", - "\\begin{equation*}\n", - "\\begin{split}\n", - " \\Rightarrow\\quad\\langle X\\rangle &= \\frac{1}{4}\\Bigg(\\Big\\|| \\psi_j\\rangle + P|\\psi_i\\rangle \\Big\\|^2-\\Big\\||\\psi_j\\rangle - P|\\psi_i\\rangle\\Big\\|^2\\Bigg) \\\\\n", - " &= \\text{Re}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n", - "\\end{split}\n", - "\\end{equation*}\n", - "$$\n", - "\n", - "From the identity $|a + b\\|^2 = \\langle a + b | a + b \\rangle = \\|a\\|^2 + \\|b\\|^2 + 2\\text{Re}\\langle a | b \\rangle$. Similarly, measuring $Y$ yields\n", - "\n", - "$$\n", - "\\begin{equation*}\n", - " \\langle Y\\rangle = \\text{Im}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n", - "\\end{equation*}\n", - "$$\n", - "\n", - "Adding these steps to the time-evolution we set up previously we write the following." - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "ac536079-96f2-435b-a15f-72c207fa24d1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Circuit for calculating the real part of the overlap in S via Hadamard test\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 72, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "## Create the time-evo op circuit\n", - "evol_gate = PauliEvolutionGate(\n", - " H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n", - ")\n", - "\n", - "## Create the time-evo op dagger circuit\n", - "evol_gate_d = PauliEvolutionGate(\n", - " H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n", - ")\n", - "evol_gate_d = evol_gate_d.inverse()\n", - "\n", - "# Put pieces together\n", - "qc_reg = QuantumRegister(n_qubits)\n", - "qc_temp = QuantumCircuit(qc_reg)\n", - "qc_temp.compose(qc_state_prep, inplace=True)\n", - "for _ in range(num_trotter_steps):\n", - " qc_temp.append(evol_gate, qargs=qc_reg)\n", - "for _ in range(num_trotter_steps):\n", - " qc_temp.append(evol_gate_d, qargs=qc_reg)\n", - "qc_temp.compose(qc_state_prep.inverse(), inplace=True)\n", - "\n", - "# Create controlled version of the circuit\n", - "controlled_U = qc_temp.to_gate().control(1)\n", - "\n", - "# Create hadamard test circuit for real part\n", - "qr = QuantumRegister(n_qubits + 1)\n", - "qc_real = QuantumCircuit(qr)\n", - "qc_real.h(0)\n", - "qc_real.append(controlled_U, list(range(n_qubits + 1)))\n", - "qc_real.h(0)\n", - "\n", - "print(\"Circuit for calculating the real part of the overlap in S via Hadamard test\")\n", - "qc_real.draw(\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "b88673a4-859a-44f7-ac0c-38ecf17faf36", - "metadata": {}, - "source": [ - "We warned already about the depth involved in Trotter circuits. Performing the Hadamard test in these conditions can yield an even deeper circuit, especially once we decompose to native gates. This will increase even more if we account for the topology of the device. So before using any time on the quantum computer, it is a good idea to check the 2-qubit depth of our circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "f40d206a-9bd5-4355-8007-cecff21f7fbe", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of layers of 2Q operations 14401\n" - ] - } - ], - "source": [ - "print(\n", - " \"Number of layers of 2Q operations\",\n", - " qc_real.decompose(reps=2).depth(lambda x: x[0].num_qubits == 2),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "bbd4f53b-8946-4713-89c2-e6bbbfa80609", - "metadata": {}, - "source": [ - "A circuit of this depth cannot return usable results on modern quantum computers. If we are to construct $\\tilde{H}$ and $\\tilde{S},$ we need a better way. This is the reason for the efficient Hadamard test introduced below." - ] - }, - { - "cell_type": "markdown", - "id": "6cda7f15-7c76-40aa-bf2e-bfea141c8474", - "metadata": {}, - "source": [ - "### 4. 2 Step 2. Optimize circuits and operators for target hardware\n", - "\n", - "#### Efficient Hadamard test\n", - "\n", - "We can optimize the deep circuits for the Hadamard test that we have obtained by introducing some approximations and relying on some assumption about the model Hamiltonian. For example, consider the following circuit for the Hadamard test:\n", - "\n", - "![An image of a quantum circuit diagram with many layers indicating that the circuit must be evaluated for many different unitary operators in order to perform the modified, efficient Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig6.avif)\n", - "\n", - "Assume we can classically calculate $E_0$, the eigenvalue of $|0\\rangle^N$ under the Hamiltonian $H$.\n", - "This is satisfied when the Hamiltonian preserves the U(1) symmetry. Although this may seem like a strong assumption, there are many cases where it is safe to assume that there is a vacuum state (in this case it maps to the $|0\\rangle^N$ state) which is unaffected by the action of the Hamiltonian. This is true for example for chemistry Hamiltonians that describe stable molecule (where the number of electrons is conserved).\n", - "Given that the gate $\\text{Prep} \\; \\psi_0$, prepares the desired reference state $\\ket{\\psi_0} = \\text{Prep} \\; \\psi_0 \\ket{0} = e^{-i H 0 dt} U_{\\psi_0} \\ket{0}$, for example, to prepare the HF state for chemistry $\\text{Prep} \\; \\psi_0$ would be a product of single-qubit NOTs, so controlled-$\\text{Prep} \\; \\psi_0$ is just a product of CNOTs.\n", - "Then the circuit above implements the following state prior to measurement:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - " \\text{Step 0:}\\qquad|\\Psi\\rangle & = \\ket{0} \\ket{0}^{N}\\\\\n", - " \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(\\ket{0}\\ket{0}^N+ \\ket{1} \\ket{0}^N\\right)\\\\\n", - " \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle|0\\rangle^N+|1\\rangle|\\psi_0\\rangle\\right)\\\\\n", - " \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0}\\ket{0}^N+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n", - " \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0} \\ket{\\psi_0}+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n", - " & = \\frac{1}{2}\\left(\\ket{+}\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)+\\ket{-}\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\\right)\\\\\n", - " & = \\frac{1}{2}\\left(\\ket{+i}\\left(e^{i\\phi}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)+\\ket{-i}\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\\right)\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "where we have used the classical simulable phase shift $ U\\ket{0}^N = e^{i\\phi}\\ket{0}^N$ from step 2 to 3. Therefore the expectation values are\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - " \\langle X\\otimes P\\rangle&=\\frac{1}{4}\n", - " \\Big(\n", - " \\left(e^{-i\\phi}\\bra{\\psi_0}+\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)\n", - " \\\\\n", - " &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\n", - " \\Big)\\\\\n", - " &=\\text{Re}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right],\n", - "\\end{aligned}\n", - "\n", - "$$\n", - "\n", - "$$\n", - "\n", - "\\begin{aligned}\n", - " \\langle Y\\otimes P\\rangle&=\\frac{1}{4}\n", - " \\Big(\n", - " \\left(e^{-i\\phi}\\bra{\\psi_0}+i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi_0}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)\n", - " \\\\\n", - " &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\n", - " \\Big)\\\\\n", - " &=\\text{Im}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right].\n", - "\\end{aligned}\n", - "\n", - "$$\n", - "\n", - "Using these assumptions we were able to write the expectation values of operators of interest with fewer controlled operations. In fact, we only need to implement the controlled state preparation $\\text{Prep} \\; \\psi_0$ and not controlled time evolutions. Reframing our calculation as above will allow us to greatly reduce the depth of the resulting circuits.\n", - "\n", - "Note that as a bonus, since the Pauli operator now appears as a measurement at the end of the circuit rather than as a controlled gate in the middle, it can be measured alongside other commuting Pauli operators as in the decomposition $$H=\\sum_{\\alpha = 1}^{N_\\text{GCP}}c_\\alpha P_\\alpha $$ given above." - ] - }, - { - "cell_type": "markdown", - "id": "cd24795e-3ef0-4629-87bf-8e04c471d665", - "metadata": {}, - "source": [ - "### Decompose time-evolution operator with Trotter decomposition\n", - "\n", - "Instead of implementing the time-evolution operator exactly we can use the Trotter decomposition to implement an approximation of it. Repeating several times a certain order Trotter decomposition gives us further reduction of the error introduced from the approximation. In the following, we directly build the Trotter implementation in the most efficient way for the interaction graph of the Hamiltonian we are considering (nearest neighbor interactions only). In practice we insert Pauli rotations $R_{xx}$, $R_{yy}$, $R_{zz}$ with coupling strengths $J_x,$ $J_y,$ and $J_z$ and a parametrized angle $t$, which correspond to the approximate implementation of $e^{-i (J_x XX + J_y YY + J_z ZZ) t}$. Given the difference in definition of the Pauli rotations and the time-evolution that we are trying to implement, we'll have to use the parameter $2*dt$ to achieve a time-evolution of $dt$. Furthermore, we reverse the order of the operations for odd number of repetitions of the Trotter steps, which is functionally equivalent but allows for synthesizing adjacent operations in a single $SU(2)$ unitary. This gives a much shallower circuit than what is obtained using the generic `PauliEvolutionGate()` functionality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8d35e6cd-c0f6-4871-83da-86472d66be2c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 74, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "t = Parameter(\"t\")\n", - "\n", - "# Create instruction for rotation about XX+YY-ZZ:\n", - "Rxyz_circ = QuantumCircuit(2)\n", - "Rxyz_circ.rxx(2 * JX * t, 0, 1)\n", - "Rxyz_circ.ryy(2 * JY * t, 0, 1)\n", - "Rxyz_circ.rzz(2 * JZ * t, 0, 1)\n", - "Rxyz_instr = Rxyz_circ.to_instruction(label=\"R J_x XX + J_y YY + J_z ZZ\")\n", - "\n", - "interaction_list = [\n", - " [[i, i + 1] for i in range(0, n_qubits - 1, 2)],\n", - " [[i, i + 1] for i in range(1, n_qubits - 1, 2)],\n", - "] # linear chain\n", - "\n", - "qr = QuantumRegister(n_qubits)\n", - "trotter_step_circ = QuantumCircuit(qr)\n", - "for i, color in enumerate(interaction_list):\n", - " for interaction in color:\n", - " trotter_step_circ.append(Rxyz_instr, interaction)\n", - " if i < len(interaction_list) - 1:\n", - " trotter_step_circ.barrier()\n", - "reverse_trotter_step_circ = trotter_step_circ.reverse_ops()\n", - "\n", - "qc_evol = QuantumCircuit(qr)\n", - "for step in range(num_trotter_steps):\n", - " if step % 2 == 0:\n", - " qc_evol = qc_evol.compose(trotter_step_circ)\n", - " else:\n", - " qc_evol = qc_evol.compose(reverse_trotter_step_circ)\n", - "\n", - "qc_evol.decompose().draw(\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "2a3d8d4a-fc0f-4702-ac69-67f63d0c0e30", - "metadata": {}, - "source": [ - "We prepare an initial state again for this efficient Hadamard test." - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "d1d0b9de-65d4-4a46-975d-6cfaaac05f9a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 75, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "control = 0\n", - "excitation = int(n_qubits / 2) + 1\n", - "controlled_state_prep = QuantumCircuit(n_qubits + 1)\n", - "controlled_state_prep.cx(control, excitation)\n", - "controlled_state_prep.draw(\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "415e94f8-6594-42ab-b55b-80a34852b943", - "metadata": {}, - "source": [ - "#### Template circuits for calculating matrix elements of $\\tilde{S}$ and $\\tilde{H}$ via Hadamard test\n", - "\n", - "The only difference between the circuits used in the Hadamard test will be the phase in the time-evolution operator and the observables measured. Therefore we can prepare a template circuit which represent the generic circuit for the Hadamard test, with placeholders for the gates that depend on the time-evolution operator." - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "27a54efa-affb-41bc-8523-822f8d92d34f", - "metadata": {}, - "outputs": [], - "source": [ - "# Parameters for the template circuits\n", - "parameters = []\n", - "for idx in range(1, krylov_dim):\n", - " parameters.append(dt_circ * (idx))" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "id": "37382668-1999-4475-b50f-2887449d5c93", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 77, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create modified hadamard test circuit\n", - "qr = QuantumRegister(n_qubits + 1)\n", - "qc = QuantumCircuit(qr)\n", - "qc.h(0)\n", - "qc.compose(controlled_state_prep, list(range(n_qubits + 1)), inplace=True)\n", - "qc.barrier()\n", - "qc.compose(qc_evol, list(range(1, n_qubits + 1)), inplace=True)\n", - "qc.barrier()\n", - "qc.x(0)\n", - "qc.compose(controlled_state_prep.inverse(), list(range(n_qubits + 1)), inplace=True)\n", - "qc.x(0)\n", - "\n", - "qc.decompose().draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "4ad71f04-ec89-473b-9b7a-db4d3936c85e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The optimized circuit has 2Q gates depth: 50\n" - ] - } - ], - "source": [ - "print(\n", - " \"The optimized circuit has 2Q gates depth: \",\n", - " qc.decompose().decompose().depth(lambda x: x[0].num_qubits == 2),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "6f60d587-8ae1-4daa-ab76-a032c2221ab7", - "metadata": {}, - "source": [ - "This depth is substantially reduced compared to the original Hadamard test. This depth is manageable on modern quantum computers, though it is still quite high. We will need to use state-of-the-art error mitigation to obtain useful results.\n", - "\n", - "Select a backend on which to run our quantum Krylov calculation, so that we can transpile our circuit for running on that quantum computer." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5683752b-d520-4cad-9fe9-7ebb7f495ba0", - "metadata": {}, - "outputs": [], - "source": [ - "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "# Or you may choose a specify backend and channel if necessary for your workflow.\n", - "# service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", - "# backend = service.backend(\"ibm_fez\")" - ] - }, - { - "cell_type": "markdown", - "id": "0337f370-edff-4dcb-9570-26818ee51d3a", - "metadata": {}, - "source": [ - "We now transpile our circuits and operators." - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "id": "8a7c9ea1-5bc3-40b7-aa55-040cbb5f8c28", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "basis_gates = list(target.operation_names)\n", - "pm = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, basis_gates=basis_gates\n", - ")\n", - "\n", - "qc_trans = pm.run(qc)" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "id": "054bcfba-fde3-4213-bbd7-d7a6dd6c03c8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "36\n", - "OrderedDict([('rz', 410), ('sx', 361), ('cz', 156), ('x', 18), ('barrier', 6)])\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(qc_trans.depth(lambda x: x[0].num_qubits == 2))\n", - "print(qc_trans.count_ops())\n", - "qc_trans.draw(\"mpl\", fold=-1, idle_wires=False, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "5395cc4a-ce59-4f8f-a126-1f8ca5507f83", - "metadata": {}, - "source": [ - "After optimization, our transpiled two-qubit depth is further reduced.\n", - "\n", - "### 4.3 Step 3. Execute using a Qiskit Runtime primitive\n", - "\n", - "We now create PUBs for execution with Estimator." - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "id": "5d949e77-d7af-47aa-91e1-40fcf5940996", - "metadata": {}, - "outputs": [], - "source": [ - "# Define observables to measure for S\n", - "observable_S_real = \"I\" * (n_qubits) + \"X\"\n", - "observable_S_imag = \"I\" * (n_qubits) + \"Y\"\n", - "\n", - "observable_op_real = SparsePauliOp(\n", - " observable_S_real\n", - ") # define a sparse pauli operator for the observable\n", - "observable_op_imag = SparsePauliOp(observable_S_imag)\n", - "\n", - "layout = qc_trans.layout # get layout of transpiled circuit\n", - "observable_op_real = observable_op_real.apply_layout(\n", - " layout\n", - ") # apply physical layout to the observable\n", - "observable_op_imag = observable_op_imag.apply_layout(layout)\n", - "observable_S_real = (\n", - " observable_op_real.paulis.to_labels()\n", - ") # get the label of the physical observable\n", - "observable_S_imag = observable_op_imag.paulis.to_labels()\n", - "\n", - "observables_S = [[observable_S_real], [observable_S_imag]]\n", - "\n", - "\n", - "# Define observables to measure for H\n", - "# Hamiltonian terms to measure\n", - "observable_list = []\n", - "for pauli, coeff in zip(H_op.paulis, H_op.coeffs):\n", - " # print(pauli)\n", - " observable_H_real = pauli[::-1].to_label() + \"X\"\n", - " observable_H_imag = pauli[::-1].to_label() + \"Y\"\n", - " observable_list.append([observable_H_real])\n", - " observable_list.append([observable_H_imag])\n", - "\n", - "layout = qc_trans.layout\n", - "\n", - "observable_trans_list = []\n", - "for observable in observable_list:\n", - " observable_op = SparsePauliOp(observable)\n", - " observable_op = observable_op.apply_layout(layout)\n", - " observable_trans_list.append([observable_op.paulis.to_labels()])\n", - "\n", - "observables_H = observable_trans_list\n", - "\n", - "\n", - "# Define a sweep over parameter values\n", - "params = np.vstack(parameters).T\n", - "\n", - "\n", - "# Estimate the expectation value for all combinations of\n", - "# observables and parameter values, where the pub result will have\n", - "# shape (# observables, # parameter values).\n", - "pub = (qc_trans, observables_S + observables_H, params)" - ] - }, - { - "cell_type": "markdown", - "id": "e0f30365-57d2-4ece-ba62-4e534a810de6", - "metadata": {}, - "source": [ - "Circuits for $t=0$ are classically calculable. We carry this out before moving on to the $t\\neq 0$ case using a quantum computer." - ] - }, - { - "cell_type": "code", - "execution_count": 83, - "id": "298c43a4-e54c-4ac3-95bd-12f535d44790", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(10+0j)\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import StabilizerState, Pauli\n", - "\n", - "\n", - "qc_cliff = qc.assign_parameters({t: 0})\n", - "\n", - "\n", - "# Get expectation values from experiment\n", - "S_expval_real = StabilizerState(qc_cliff).expectation_value(\n", - " Pauli(\"I\" * (n_qubits) + \"X\")\n", - ")\n", - "S_expval_imag = StabilizerState(qc_cliff).expectation_value(\n", - " Pauli(\"I\" * (n_qubits) + \"Y\")\n", - ")\n", - "\n", - "# Get expectation values\n", - "S_expval = S_expval_real + 1j * S_expval_imag\n", - "\n", - "H_expval = 0\n", - "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n", - " # Get expectation values from experiment\n", - " expval_real = StabilizerState(qc_cliff).expectation_value(\n", - " Pauli(pauli[::-1].to_label() + \"X\")\n", - " )\n", - " expval_imag = StabilizerState(qc_cliff).expectation_value(\n", - " Pauli(pauli[::-1].to_label() + \"Y\")\n", - " )\n", - " expval = expval_real + 1j * expval_imag\n", - "\n", - " # Fill-in matrix elements\n", - " H_expval += coeff * expval\n", - "\n", - "\n", - "print(H_expval)" - ] - }, - { - "cell_type": "markdown", - "id": "9b86af1c-7e37-427d-b4f0-fbca38d45a21", - "metadata": {}, - "source": [ - "Although we were able to reduce our gate depth by orders of magnitude using the efficient Hadamard test, the depth is still sufficient to require state-of-the-art error mitigation. Below, we specify attributes of the mitigation being used. All of the methods used are important, but it is worth called out [probabilistic error amplification (PEA)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-amplification-pea) specifically. This powerful technique comes with a great deal of quantum overhead. The calculation done here can take 20 minutes or more to run on a real quantum computer. You may wish to play with the parameters below to increase or decrease precision and consequentially overhead. The default settings below yield high-fidelity results." - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "id": "2dff41e1-d417-4af7-aa6c-537a9b0e0c7c", - "metadata": {}, - "outputs": [], - "source": [ - "# Experiment options\n", - "num_randomizations = 300\n", - "num_randomizations_learning = 20\n", - "max_batch_circuits = 20\n", - "shots_per_randomization = 100\n", - "learning_pair_depths = [0, 4, 24]\n", - "noise_factors = [1, 1.3, 1.6]\n", - "\n", - "# Base option formatting\n", - "options = {\n", - " # Builtin resilience settings for ZNE\n", - " \"resilience\": {\n", - " \"measure_mitigation\": True,\n", - " \"zne_mitigation\": True,\n", - " \"zne\": {\"noise_factors\": noise_factors},\n", - " # TREX noise learning configuration\n", - " \"measure_noise_learning\": {\n", - " \"num_randomizations\": num_randomizations_learning,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " },\n", - " # PEA noise model configuration\n", - " \"layer_noise_learning\": {\n", - " \"max_layers_to_learn\": 10,\n", - " \"layer_pair_depths\": learning_pair_depths,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"num_randomizations\": num_randomizations_learning,\n", - " },\n", - " },\n", - " # Randomization configuration\n", - " \"twirling\": {\n", - " \"num_randomizations\": num_randomizations,\n", - " \"shots_per_randomization\": shots_per_randomization,\n", - " \"strategy\": \"all\",\n", - " },\n", - " # Experimental settings for PEA method\n", - " \"experimental\": {\n", - " # # Just in case, disable any further qiskit transpilation not related to twirling / DD\n", - " # \"skip_transpilation\": True,\n", - " # Execution configuration\n", - " \"execution\": {\n", - " \"max_pubs_per_batch_job\": max_batch_circuits,\n", - " \"fast_parametric_update\": True,\n", - " },\n", - " # Error Mitigation configuration\n", - " \"resilience\": {\n", - " # ZNE Configuration\n", - " \"zne\": {\n", - " \"amplifier\": \"pea\",\n", - " \"return_all_extrapolated\": True,\n", - " \"return_unextrapolated\": True,\n", - " \"extrapolated_noise_factors\": [0] + noise_factors,\n", - " }\n", - " },\n", - " },\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "5932dc29-7905-4f54-9c97-2bb0fc9c1e39", - "metadata": {}, - "source": [ - "Finally, we execute the circuits for $\\tilde{S}$ and $\\tilde{H}$ with Estimator." - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "id": "01a41068-453f-4ff6-9664-c73be1965dc7", - "metadata": {}, - "outputs": [], - "source": [ - "# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate. Your execution time may vary.\n", - "\n", - "with Batch(backend=backend) as batch:\n", - " # Estimator\n", - " estimator = Estimator(mode=batch, options=options)\n", - "\n", - " job = estimator.run([pub], precision=1)" - ] - }, - { - "cell_type": "markdown", - "id": "936865a2-828d-4a45-987e-b62cce0535da", - "metadata": {}, - "source": [ - "### 4.4 Step 4. Post-process and analyze results\n", - "\n", - "What we have obtained from the quantum computer are the individual matrix elements of $\\tilde{S}$ and the commuting Pauli groups that make up the matrix elements of $\\tilde{H}$. These terms must be combined to recover our matrices, so that we can solve the generalized eigenvalue problem." - ] - }, - { - "cell_type": "code", - "execution_count": 86, - "id": "28ed9319-dd36-4104-aba0-f8798cbbd2b6", - "metadata": {}, - "outputs": [], - "source": [ - "# Store the outputs as 'results'.\n", - "results = job.result()[0]" - ] - }, - { - "cell_type": "markdown", - "id": "1d3c0af8-fb06-415c-b591-f8e8faedc070", - "metadata": {}, - "source": [ - "#### Calculate Effective Hamiltonian and Overlap matrices\n", - "\n", - "First calculate the phase accumulated by the $\\vert 0 \\rangle$ state during the uncontrolled time evolution" - ] - }, - { - "cell_type": "code", - "execution_count": 87, - "id": "d36e8b32-621d-44da-808d-297c0b60754a", - "metadata": {}, - "outputs": [], - "source": [ - "prefactors = [\n", - " np.exp(-1j * sum([c for p, c in H_op.to_list() if \"Z\" in p]) * i * dt)\n", - " for i in range(1, krylov_dim)\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "f20402f5-1264-4eeb-9d41-8dd6e53e82d3", - "metadata": {}, - "source": [ - "Once we have the results of the circuit executions we can post-process the data to calculate the matrix elements of $S$" - ] - }, - { - "cell_type": "code", - "execution_count": 88, - "id": "d1ff8971-2f80-4130-b7f2-e95c3a1b476d", - "metadata": {}, - "outputs": [], - "source": [ - "# Assemble S, the overlap matrix of dimension D:\n", - "S_first_row = np.zeros(krylov_dim, dtype=complex)\n", - "S_first_row[0] = 1 + 0j\n", - "\n", - "# Add in ancilla-only measurements:\n", - "for i in range(krylov_dim - 1):\n", - " # Get expectation values from experiment\n", - " expval_real = results.data.evs[0][0][i] # automatic extrapolated evs if ZNE is used\n", - " expval_imag = results.data.evs[1][0][i] # automatic extrapolated evs if ZNE is used\n", - "\n", - " # Get expectation values\n", - " expval = expval_real + 1j * expval_imag\n", - " S_first_row[i + 1] += prefactors[i] * expval\n", - "\n", - "S_first_row_list = S_first_row.tolist() # for saving purposes\n", - "\n", - "\n", - "S_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n", - "\n", - "# Distribute entries from first row across matrix:\n", - "for i, j in it.product(range(krylov_dim), repeat=2):\n", - " if i >= j:\n", - " S_circ[j, i] = S_first_row[i - j]\n", - " else:\n", - " S_circ[j, i] = np.conj(S_first_row[j - i])" - ] - }, - { - "cell_type": "code", - "execution_count": 89, - "id": "bfe2a427-7ec6-48de-9321-fa6bf2dc7c94", - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$$\n", - "\\displaystyle \\left[\\begin{matrix}1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i & 0.0940509850777074 - 0.094154537369141 i\\\\0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i\\\\0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i\\\\0.0940509850777074 + 0.094154537369141 i & 0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0\\end{matrix}\\right]\n", - "$$" - ], - "text/plain": [ - "Matrix([\n", - "[ 1.0, 0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I, 0.0940509850777074 - 0.094154537369141*I],\n", - "[ 0.149322296177984 + 0.283023058106896*I, 1.0, 0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I],\n", - "[0.185815978760175 + 0.0910521940394691*I, 0.149322296177984 + 0.283023058106896*I, 1.0, 0.149322296177984 - 0.283023058106896*I],\n", - "[0.0940509850777074 + 0.094154537369141*I, 0.185815978760175 + 0.0910521940394691*I, 0.149322296177984 + 0.283023058106896*I, 1.0]])" - ] - }, - "execution_count": 89, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sympy import Matrix\n", - "\n", - "Matrix(S_circ)" - ] - }, - { - "cell_type": "markdown", - "id": "2a863b6f-183f-4d7a-bac8-399da3e3578e", - "metadata": {}, - "source": [ - "And the matrix elements of $\\tilde{H}$" - ] - }, - { - "cell_type": "code", - "execution_count": 90, - "id": "c551fdd0-91ef-4531-83d9-ed399b423bb9", - "metadata": {}, - "outputs": [], - "source": [ - "import itertools\n", - "\n", - "# Assemble S, the overlap matrix of dimension D:\n", - "H_first_row = np.zeros(krylov_dim, dtype=complex)\n", - "H_first_row[0] = H_expval\n", - "\n", - "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n", - " # Add in ancilla-only measurements:\n", - " for i in range(krylov_dim - 1):\n", - " # Get expectation values from experiment\n", - " expval_real = results.data.evs[2 + 2 * obs_idx][0][\n", - " i\n", - " ] # automatic extrapolated evs if ZNE is used\n", - " expval_imag = results.data.evs[2 + 2 * obs_idx + 1][0][\n", - " i\n", - " ] # automatic extrapolated evs if ZNE is used\n", - "\n", - " # Get expectation values\n", - " expval = expval_real + 1j * expval_imag\n", - " H_first_row[i + 1] += prefactors[i] * coeff * expval\n", - "\n", - "H_first_row_list = H_first_row.tolist()\n", - "\n", - "H_eff_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n", - "\n", - "# Distribute entries from first row across matrix:\n", - "for i, j in itertools.product(range(krylov_dim), repeat=2):\n", - " if i >= j:\n", - " H_eff_circ[j, i] = H_first_row[i - j]\n", - " else:\n", - " H_eff_circ[j, i] = np.conj(H_first_row[j - i])" - ] - }, - { - "cell_type": "code", - "execution_count": 91, - "id": "d1950927-74d6-4012-81f2-3d0b7f476de8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$$\n", - "\\displaystyle \\left[\\begin{matrix}10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i & 1.0770511571923 + 0.104340737159455 i\\\\-3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i\\\\0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i\\\\1.0770511571923 - 0.104340737159455 i & 0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0\\end{matrix}\\right]\n", - "$$" - ], - "text/plain": [ - "Matrix([\n", - "[ 10.0, -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I, 1.0770511571923 + 0.104340737159455*I],\n", - "[ -3.02044405310714 + 2.80721615865252*I, 10.0, -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I],\n", - "[0.496487054782717 - 0.188101957039621*I, -3.02044405310714 + 2.80721615865252*I, 10.0, -3.02044405310714 - 2.80721615865252*I],\n", - "[ 1.0770511571923 - 0.104340737159455*I, 0.496487054782717 - 0.188101957039621*I, -3.02044405310714 + 2.80721615865252*I, 10.0]])" - ] - }, - "execution_count": 91, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sympy import Matrix\n", - "\n", - "Matrix(H_eff_circ)" - ] - }, - { - "cell_type": "markdown", - "id": "7ec60832-f7be-447f-841e-e801241fdeae", - "metadata": {}, - "source": [ - "Finally, we can solve the generalized eigenvalue problem for $\\tilde{H}$:\n", - "\n", - "$$\\tilde{H} \\vec{c} = c S \\vec{c}$$\n", - "\n", - "and get an estimate of the ground state energy $c_{min}$" - ] - }, - { - "cell_type": "code", - "execution_count": 92, - "id": "d6635506-399f-47a3-a837-c5f2558f22b4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The estimated ground state energy is: 10.0\n", - "The estimated ground state energy is: 5.933953916292923\n", - "The estimated ground state energy is: 4.4101773995740645\n", - "The estimated ground state energy is: 3.921288588521255\n" - ] - } - ], - "source": [ - "gnd_en_circ_est_list = []\n", - "for d in range(1, krylov_dim + 1):\n", - " # Solve generalized eigenvalue problem\n", - " gnd_en_circ_est = solve_regularized_gen_eig(\n", - " H_eff_circ[:d, :d], S_circ[:d, :d], threshold=1e-1\n", - " )\n", - " gnd_en_circ_est_list.append(gnd_en_circ_est)\n", - " print(\"The estimated ground state energy is: \", gnd_en_circ_est)" - ] - }, - { - "cell_type": "markdown", - "id": "15865587-85f4-41bc-9bf5-47464cf17298", - "metadata": {}, - "source": [ - "For a single-particle sector, we can efficiently calculate the ground state of this sector of the Hamiltonian classically" - ] - }, - { - "cell_type": "code", - "execution_count": 93, - "id": "3303ae29-c288-417a-9cf2-c82d9770de69", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "n_sys_qubits 10\n", - "n_exc 1 , subspace dimension 11\n", - "single particle ground state energy: 2.391547869638771\n" - ] - } - ], - "source": [ - "gs_en = single_particle_gs(H_op, n_qubits)" - ] - }, - { - "cell_type": "code", - "execution_count": 94, - "id": "2f904ea3-38bc-4841-81ce-cdb69f09a0b7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "27" - ] - }, - "execution_count": 94, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(H_op)" - ] - }, - { - "cell_type": "code", - "execution_count": 95, - "id": "a5d4a983-1a30-4cea-b695-3e6a67338633", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(\n", - " range(1, krylov_dim + 1),\n", - " gnd_en_circ_est_list,\n", - " color=\"blue\",\n", - " linestyle=\"-.\",\n", - " label=\"KQD estimate\",\n", - ")\n", - "plt.plot(\n", - " range(1, krylov_dim + 1),\n", - " [gs_en] * krylov_dim,\n", - " color=\"red\",\n", - " linestyle=\"-\",\n", - " label=\"exact\",\n", - ")\n", - "plt.xticks(range(1, krylov_dim + 1), range(1, krylov_dim + 1))\n", - "plt.legend()\n", - "plt.xlabel(\"Krylov space dimension\")\n", - "plt.ylabel(\"Energy\")\n", - "plt.title(\"Estimating Ground state energy with Krylov Quantum Diagonalization\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "543a2d98-1f72-4df1-ae22-f65d60c0d9c5", - "metadata": {}, - "source": [ - "## 5. Discussion and extension\n", - "\n", - "To recap, we start with a reference state, then evolve it for different periods of time to generate the unitary Krylov subspace. We project our Hamiltonian onto that subspace. We also estimate the overlaps of the subspace vectors. Finally we solve the lower-dimensional, generalized eigenvalue problem classically.\n", - "\n", - "![A flow-chart overview of QKD: start with a reference state, evolve the state to approximate Krylov vectors, project into the Krylov subspace, diagonalize the projected subspace classically, and determine ground state properties.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig7.avif)\n", - "\n", - "Let’s compare what determines computational costs of using the Krylov technique classically and quantum mechanically. There are not perfect analogs between classical and quantum approaches for all steps. This table collects some scaling of different steps for consideration.\n", - "\n", - "![A table describing scaling of different processes classically and in the quantum approach to Krylov methods. Some quantum steps have no analog. The scalings are the same as those stated in text.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig8.avif)\n", - "\n", - "Recall that Hamiltonians generally have terms that cannot be simultaneously measured (because they do not commute with on another). We sort terms in the Hamiltonian into groups of commuting Pauli operators that can all be measured simultaneously, and we may require many such groups to account for all the terms that do not commute with one another. To build up $\\tilde{H}$ on a quantum computer requires separate measurements for each group of commuting Pauli strings in the Hamiltonian, and each of those requires many shots. We must do this for $r^2$ different matrix elements, corresponding to $r^2$ combinations of different time evolution factors. There are sometimes ways to reduce this, but in this rough treatment, the time required for this scales like $N_\\text{shots}\\times N_\\text{GCP} \\times r^2.$ The elements of $S$ must be estimated, which scales like $O(N_\\text{shots}\\times r^2)$. Finally, solving the generalized eigenvalue problem in the projected space, classically, takes $O(r^3).$\n", - "\n", - "We see that quantum Krylov diagonalization may be useful in cases where the number of commuting Pauli groups in the Hamiltonian is relatively small. These scaling dependencies suggest some applications where the Krylov method can be useful, and others where it likely will not be.\n", - "Some Hamiltonians have high complexity when mapped to qubits, involving many non-commuting Pauli strings that cannot easily be partitioned into a few commuting groups. This is often true of quantum chemistry problems, for example. This complexity presents two primary challenges for near-term quantum computers:\n", - "\n", - "* The estimation of each element of $\\tilde{H}$ becomes computationally expensive due to the large number of terms.\n", - "* The required Trotter circuits become prohibitively deep.\n", - "\n", - "Both of the above points will be less problematic when quantum computers reach fault-tolerance, but they must be considered in the near term. Even systems with “simpler” mappings than those in quantum chemistry may experience the same impediments, if the Hamiltonians have too many non-commuting terms.\n", - "The Krylov method is most useful where the Hamiltonian can be partitioned into relatively few commuting Pauli groups, and where $H$ is easy to implement in trotter circuits. Both of these conditions are satisfied, for example, for many lattice models of interest in physics. KQD is especially useful if very little is known about the ground state. This stems from its inherent convergence guarantees and its applicability in scenarios where alternative methods are untenable due to insufficient ground state knowledge.\n", - "\n", - "While KQD is a powerful tool, the protocol's time-consuming aspects, particularly the estimation of each element of the projected Hamiltonian and the overlap of Krylov states, represent opportunities for improvement. An alternative approach involves leveraging Krylov methods in conjunction with sampling-based methods, which are the subject of the next lesson." - ] - }, - { - "cell_type": "markdown", - "id": "7fa7609f-9e96-4b0c-b716-ae9242bb40ca", - "metadata": {}, - "source": [ - "## 6. Appendices\n", - "\n", - "### Appendix I: Krylov subspace from real time-evolutions\n", - "\n", - "The unitary Krylov space is defined as\n", - "\n", - "$$\n", - "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ |\\psi\\rangle, e^{-iH\\,dt} |\\psi\\rangle, \\dots, e^{-irH\\,dt} |\\psi\\rangle \\right\\}\n", - "$$\n", - "\n", - "for some timestep $dt$ that we will determine later. Temporarily assume $r$ is even: then define $d=r/2$. Notice that when we project the Hamiltonian into the Krylov space above, it is indistinguishable from the Krylov space\n", - "\n", - "$$\n", - "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ e^{i\\,d\\,H\\,dt}|\\psi\\rangle, e^{i(d-1)H\\,dt} |\\psi\\rangle, \\dots, e^{-i(d-1)H\\,dt} |\\psi\\rangle, e^{-i\\,d\\,H\\,dt} |\\psi\\rangle \\right\\},\n", - "$$\n", - "\n", - "that is, where all the time-evolutions are shifted backward by $d$ timesteps.\n", - "The reason it is indistinguishable is because the matrix elements\n", - "\n", - "$$\n", - "\\tilde{H}_{j,k} = \\langle\\psi|e^{i\\,j\\,H\\,dt}He^{-i\\,k\\,H\\,dt}|\\psi\\rangle=\\langle\\psi|He^{i(j-k)H\\,dt}|\\psi\\rangle\n", - "$$\n", - "\n", - "are invariant under overall shifts of the evolution time, since the time-evolutions commute with the Hamiltonian. For odd $r$, we can use the analysis for $r-1$.\n", - "\n", - "We want to show that somewhere in this Krylov space, there is guaranteed to be a low-energy state. We do so by way of the following result, which is derived from Theorem 3.1 in [\\[3\\]](#references):\n", - "\n", - "**Claim 1:** there exists a function $f$ such that for energies $E$ in the spectral range of the Hamiltonian (that is, between the ground state energy and the maximum energy)...\n", - "\n", - "1. $f(E_0)=1$\n", - "2. $|f(E)|\\le2\\left(1 + \\delta\\right)^{-d}$ for all values of $E$ that lie $\\ge\\delta$ away from $E_0$, that is, it is exponentially suppressed\n", - "3. $f(E)$ is a linear combination of $e^{ijE\\,dt}$ for $j=-d,-d+1,...,d-1,d$\n", - "\n", - "We give a proof below, but that can be safely skipped unless one wants to understand the full, rigorous argument. For now we focus on the implications of the above claim. By property 3 above, we can see that the shifted Krylov space above contains the state $f(H)|\\psi\\rangle$. This is our low-energy state. To see why, write $|\\psi\\rangle$ in the energy eigenbasis:\n", - "\n", - "$$\n", - "|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_k|E_k\\rangle,\n", - "$$\n", - "\n", - "where $|E_k\\rangle$ is the kth energy eigenstate and $\\gamma_k$ is its amplitude in the initial state $|\\psi\\rangle$. Expressed in terms of this, $f(H)|\\psi\\rangle$ is given by\n", - "\n", - "$$\n", - "f(H)|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_kf(E_k)|E_k\\rangle,\n", - "$$\n", - "\n", - "using the fact that we can replace $H$ by $E_k$ when it acts on the eigenstate $|E_k\\rangle$. The energy error of this state is therefore\n", - "\n", - "$$\n", - "\\text{energy error} = \\frac{\\langle\\psi|f(H)(H-E_0)f(H)|\\psi\\rangle}{\\langle\\psi|f(H)^2|\\psi\\rangle}\n", - "$$\n", - "\n", - "$$\n", - "= \\frac{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n", - "$$\n", - "\n", - "To turn this into an upper bound that is easier to understand, we first separate the sum in the numerator into terms with $E_k-E_0\\le\\delta$ and terms with $E_k-E_0>\\delta$:\n", - "\n", - "$$\n", - "\\text{energy error} = \\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} + \\frac{\\sum_{E_k> E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n", - "$$\n", - "\n", - "We can upper bound the first term by $\\delta$,\n", - "\n", - "$$\n", - "\\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} < \\frac{\\delta\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} \\le \\delta,\n", - "$$\n", - "\n", - "where the first step follows because $E_k-E_0\\le\\delta$ for every $E_k$ in the sum, and the second step follows because the sum in the numerator is a subset of the sum in the denominator. For the second term, first we lower bound the denominator by $|\\gamma_0|^2$, since $f(E_0)^2=1$: adding everything back together, this gives\n", - "\n", - "$$\n", - "\\text{energy error} \\le \\delta + \\frac{1}{|\\gamma_0|^2}\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0).\n", - "$$\n", - "\n", - "To simplify what is left, notice that for all these $E_k$, by the definition of $f$ we know that $f(E_k)^2 \\le 4\\left(1 + \\delta\\right)^{-2d}$. Additionally upper bounding $E_k-E_0<2\\|H\\|$ and upper bounding $\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2<1$ gives\n", - "\n", - "$$\n", - "\\text{energy error} \\le \\delta + \\frac{8}{|\\gamma_0|^2}\\|H\\|\\left(1 + \\delta\\right)^{-2d}.\n", - "$$\n", - "\n", - "This holds for any $\\delta>0$, so if we set $\\delta$ equal to our goal error, then the error bound above converges towards that exponentially with the Krylov dimension $2d=r$. Also note that if $\\delta \\delta$ we have\n", - "\n", - "$$\n", - "|f(E)| \\le \\beta(a, b, d) = T_d^{-1}\\left(1 + 2\\frac{1-\\cos\\big(\\delta\\,dt\\big)}{1 + \\cos\\big(\\delta\\,dt\\big)}\\right)\n", - "$$\n", - "\n", - "$$\n", - "\\leq 2\\left(1 + \\delta\\right)^{-d} = 2\\left(1 + \\delta\\right)^{-\\lfloor k/2\\rfloor}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "97d292db-83bb-431c-97e3-91212e168ab8", - "metadata": {}, - "source": [ - "## References:\n", - "\n", - "[1] https://arxiv.org/abs/2407.14431\n", - "\n", - "[2] https://arxiv.org/abs/1811.09025\n", - "\n", - "[3] https://people.math.ethz.ch/~mhg/pub/biksm.pdf\n", - "\n", - "[4] https://academic.oup.com/book/36426\n", - "\n", - "[5] https://en.wikipedia.org/wiki/Krylov_subspace\n", - "\n", - "[6] Krylov Subspace Methods: Principles and Analysis, Jörg Liesen, Zdenek Strakos https://academic.oup.com/book/36426\n", - "\n", - "[7] Iterative Methods for Sparse Linear Systems\" by Yousef Saad\n", - "\n", - "[8] \"MINRES-QLP: A Krylov Subspace Method for Indefinite or Singular Symmetric Systems\" by Sou-Cheng Choi, Christopher Paige, and\n", - "Michael Saunders (https://epubs.siam.org/doi/10.1137/100787921)\n", - "\n", - "[9] Ethan N. Epperly, Lin Lin, and Yuji Nakatsukasa. \"A theory of quantum subspace diagonalization\". SIAM Journal on Matrix Analysis and Applications 43, 1263–1290 (2022).\n", - "\n", - "[10] https://link.aps.org/doi/10.1103/PRXQuantum.4.030319" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2b1776f4-c259-494e-a33d-1ebb0459426c", + "metadata": {}, + "source": [ + "---\n", + "title: Krylov quantum diagonalization\n", + "description: Krylov quantum diagonalization (KQD) is described, starting with classical Krylov methods. Convergence and resource intensiveness are discussed.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore Hndt longrightarrow Bigg Jörg Liesen Zdenek Strakos Yousef Saad MINRES vstar nabla */}\n", + "\n", + "# Krylov quantum diagonalization\n", + "\n", + "In this lesson on Krylov quantum diagonalization (KQD) we will answer the following:\n", + "\n", + "* What is the Krylov method, generally?\n", + "* Why does the Krylov method work and under what conditions?\n", + "* How does quantum computing play a role?\n", + "\n", + "The quantum part of the calculations are based largely on work in Ref [\\[1\\]](#references).\n", + "\n", + "The video below gives an overview of Krylov methods in classical computing, motivates their use, and explains how quantum computing can play a role in that workstream. The subsequent text offers more detail and implements a Krylov method both classically, and using a quantum computer.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "a9ba88fb-734e-44d8-ada4-c588e2654921", + "metadata": {}, + "source": [ + "## 1. Introduction to Krylov methods\n", + "\n", + "A __Krylov subspace method__ can refer to any of several methods built around what is called the __Krylov subspace__. A complete review of these is beyond the scope of this lesson, but Ref [\\[2-4\\]](#references) can all give substantially more background. Here, we will focus on what a Krylov subspace is, how and why it is useful in solving eigenvalue problems, and finally how it can be implemented on a quantum computer." + ] + }, + { + "cell_type": "markdown", + "id": "9b47a985-ec1d-41e2-8516-6cfcd7713e5f", + "metadata": {}, + "source": [ + "__Definition:__ Given a symmetric, positive semi-definite $N\\times N$ matrix $A$, the Krylov space $\\mathcal{K}^r$ of order $r$ is the space spanned by vectors obtained by multiplying higher powers of a matrix $A$, up to $r-1\\leq N$, with a reference vector $\\vert v \\rangle$.\n", + "\n", + "$$\n", + "\\mathcal{K}^r = \\text{span}\\left\\{ \\vert v \\rangle, A \\vert v \\rangle, A^2 \\vert v \\rangle, ..., A^{r-1} \\vert v \\rangle \\right\\}\n", + "$$\n", + "\n", + "Although the vectors above span what we call a Krylov subspace, there is no reason to think that they will be orthogonal. One often uses an iterative orthonormalizing process similar to __Gram-Schmidt orthogonalization__. Here the process is slightly different since each new vector is made orthogonal to the others as it is generated. In this context this is called __Arnoldi iteration__. Starting with the initial vector $|v\\rangle$, one generates the next vector $A|v\\rangle$, and then ensures that this second vector is orthogonal to the first by subtracting off its projection on $|v\\rangle$. That is\n", + "\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "|v_0\\rangle &=\\frac{|v\\rangle}{\\left|\\left| |v\\rangle \\right|\\right|}\\\\\n", + "\n", + "|v_1\\rangle &=\\frac{A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle}{\\left|\\left|A|v\\rangle-\\langle v_0|A|v\\rangle |v_0\\rangle \\right|\\right|}\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "It is now easy to see that $|v_0\\rangle \\perp |v_1\\rangle,$ since\n", + "$$\n", + "\\langle v_0 | v_1\\rangle=\\frac{\\langle v_0 | A|v\\rangle-\\langle v_0 |A|v\\rangle\\langle v_0|v_0\\rangle}{\\left|\\left| A|v\\rangle-\\langle A|v\\rangle|v_0\\rangle |v_0\\rangle \\right|\\right|}=0\n", + "$$\n", + "\n", + "We do the same for the next vector, ensuring it is orthogonal to both the previous two:\n", + "$$\n", + "|v_2\\rangle=\\frac{A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle}{\\left|\\left| A |v_1\\rangle-\\langle v_0|A |v_1\\rangle |v_0\\rangle-\\langle v_1|A |v_1\\rangle |v_1\\rangle\\right|\\right|}\n", + "$$\n", + "If we repeat this process for all $r$ vectors, we have a complete orthonormal basis for a Krylov space. Note that the orthogonalization process here will yield zero once $r>m$, since $m$ orthogonal vector necessarily span the full space. The process will also yield zero if any vector is an eigenvector of $A$ since all subsequent vectors will be multiples of that vector." + ] + }, + { + "cell_type": "markdown", + "id": "5351576c-bc3c-4e21-8839-9c14eb10747f", + "metadata": {}, + "source": [ + "### 1.1 A simple example: Krylov by hand\n", + "\n", + "Let us step through a generation of a Krylov subspace generation on a trivially small matrix, so that we can see the process. We start with an initial matrix $A$ of interest to us:\n", + "$$\n", + "A=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\n", + "$$\n", + "For this small example, we can determine the eigenvectors and eigenvalues easily even by hand. We show the numerical solution here." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "49017c87-a971-4d2d-b6dc-b8ec9b64184a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The eigenvalues are [2.58578644 4. 5.41421356]\n", + "The eigenvectors are [[ 5.00000000e-01 -7.07106781e-01 5.00000000e-01]\n", + " [ 7.07106781e-01 1.37464400e-16 -7.07106781e-01]\n", + " [ 5.00000000e-01 7.07106781e-01 5.00000000e-01]]\n" + ] + } + ], + "source": [ + "# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use linalg.eig in\n", + "# this lesson.\n", + "\n", + "import numpy as np\n", + "\n", + "A = np.array([[4, -1, 0], [-1, 4, -1], [0, -1, 4]])\n", + "eigenvalues, eigenvectors = np.linalg.eig(A)\n", + "print(\"The eigenvalues are \", eigenvalues)\n", + "print(\"The eigenvectors are \", eigenvectors)" + ] + }, + { + "cell_type": "markdown", + "id": "a0f96bc7-fef9-48c7-858a-5b44772d710f", + "metadata": {}, + "source": [ + "We record them here for later comparison:\n", + "$$\n", + "\\begin{aligned}\n", + "a_0&=2.59,&|0\\rangle&=&\\begin{pmatrix}1/2\\\\-\\sqrt{2}/2\\\\1/2\\end{pmatrix}\\\\\n", + "\\\\\n", + "a_1&=4,&|1\\rangle&=&\\begin{pmatrix}\\sqrt{2}/2\\\\0\\\\-\\sqrt{2}/2\\end{pmatrix}\\\\\n", + "\\\\\n", + "a_2&=5.41,&|2\\rangle&=&\\begin{pmatrix}1/2\\\\\\sqrt{2}/2\\\\1/2\\end{pmatrix}\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "7f237ba1-d380-4553-b5bc-94422b4f39c1", + "metadata": {}, + "source": [ + "We would like to study how this process works (or fails) as we increase the dimension of our Krylov subspace, $r$. To this end, we will apply this process:\n", + "\n", + "* Generate a subspace of the full vector space starting with a randomly-chosen vector $|v\\rangle$ (call it $|v_0\\rangle$ if it is already normalized, as above).\n", + "* Project the full matrix $A$ onto that subspace, and find the eigenvalues of that projected matrix $\\tilde{A}$.\n", + "* Increase the size of the subspace by generating more vectors, ensuring that they are orthonormal, using a process similar to Gram-Schmidt orthogonalization.\n", + "* Project $A$ onto the larger subspace and find the eigenvalues of the resulting matrix, $\\tilde{A}$.\n", + "* Repeat this until the eigenvalues converge (or in this toy case, until you have generated vectors spanning the full vector space of the original matrix $A$).\n", + "\n", + "A normal implementation of the Krylov method would not need to solve the eigenvalue problem for the matrix projected on every Krylov subspace as it is built. You could construct the subspace of the desired dimension, project the matrix onto that subspace, and diagonalize the projected matrix. Projecting and diagonalizing at each subspace dimension is only done for checking convergence.\n", + "\n", + "#### Dimension $r=1$:\n", + "\n", + "We choose a random vector, say\n", + "$$\n", + "|v_0\\rangle=\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}\n", + "$$\n", + "\n", + "If it is not already normalized, normalize it.\n", + "\n", + "We now project our matrix $A$ onto the subspace of this one vector:\n", + "$$\n", + "\\tilde{A}_0=\\langle v_0| A|v_0\\rangle=\\begin{pmatrix}1&0&0\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=(4)\n", + "$$\n", + "This is our projection of the matrix onto our Krylov subspace when it contains just a single vector, $|v_0\\rangle$. The eigenvalue of this matrix is trivially 4. We can think of this as our zeroth-order estimate of the eigenvalues (in this case just one) of $A$. Although it is a poor estimate, it is the correct order of magnitude." + ] + }, + { + "cell_type": "markdown", + "id": "a0eeb9b1-86a5-4bc8-8651-66588aea7686", + "metadata": {}, + "source": [ + "#### Dimension $r=2$:\n", + "\n", + "We now generate the next vector in our subspace through operation with $A$ on the previous vector:\n", + "\n", + "$$\n", + "A|v_0\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\n", + "$$\n", + "\n", + "Now we subtract off the projection of this vector onto our previous vector to ensure orthogonality.\n", + "$$\n", + "|v_1\\rangle=A|v_0\\rangle-\\langle v_0 |A|v_0\\rangle|v_0\\rangle\n", + "$$\n", + "$$\n", + "|v_1\\rangle=\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}-\\begin{pmatrix}1& 0& 0\\end{pmatrix}\\begin{pmatrix}4\\\\-1\\\\0\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n", + "$$\n", + "\n", + "If it is not already normalized, normalize it. In this case, the vector was already normalized, so\n", + "\n", + "$$\n", + "|v_1\\rangle=\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}\n", + "$$\n", + "\n", + "We now project our matrix A onto the subspace of these two vectors:\n", + "$$\n", + "\\tilde{A}_1= \\begin{pmatrix} 1&0&0\\\\0&-1&0 \\end{pmatrix} \\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0\\\\0&-1\\\\0&0\\end{pmatrix}=\\begin{pmatrix}1&0&0\\\\0&-1&0\\end{pmatrix}\\begin{pmatrix}4&1\\\\-1&-4\\\\0&1\\end{pmatrix}=\\begin{pmatrix}4&1\\\\1&4\\end{pmatrix}\n", + "$$\n", + "\n", + "We are still left with the problem of determining the eigenvalues of this matrix. But this matrix is slightly smaller than the full matrix. In problems involving very large matrices, working with this smaller subspace may be highly advantageous.\n", + "\n", + "$$\n", + "\\det(\\tilde{A_1}-\\lambda I)=0\n", + "$$\n", + "$$\n", + "\\begin{vmatrix} 4-\\lambda&1\\\\1&4-\\lambda\\end{vmatrix} =(4-\\lambda)^2-1=0\n", + "$$\n", + "$$\n", + "4-\\lambda=±1→\\lambda=3,5\n", + "$$\n", + "Although this is still not a good estimate, it is better than the zeroth order estimate. We will carry this out for one more iteration, to ensure the process is clear. However, this undercuts the point of the method, since we will end up diagonalizing a 3x3 matrix in the next iteration, meaning we have not saved time or computational power." + ] + }, + { + "cell_type": "markdown", + "id": "43ef6c6d-ba18-4e8f-94df-83c404d0015a", + "metadata": {}, + "source": [ + "#### Dimension $r=3$:\n", + "\n", + "We now generate the next vector in our subspace through operation with A on the previous vector:\n", + "$$\n", + "A|v_1\\rangle=\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\n", + "$$\n", + "Now we subtract off the projection of this vector onto both our previous vectors to ensure orthogonality.\n", + "$$\n", + "\\begin{aligned}\n", + "|v_2\\rangle&=A|v_1\\rangle-\\langle v_0 |A|v_1\\rangle|v_0\\rangle-\\langle v_1 |A|v_1\\rangle|v_1\\rangle\\\\\n", + "|v_2\\rangle&=\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}-\\begin{pmatrix}1& 0& 0 \\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}1\\\\0\\\\0\\end{pmatrix}-\\begin{pmatrix}0&-1& 0\\end{pmatrix}\\begin{pmatrix}1\\\\-4\\\\1\\end{pmatrix}\\begin{pmatrix}0\\\\-1\\\\0\\end{pmatrix}=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "If it is not already normalized, normalize it. In this case, the vector was already normalized, so\n", + "$$\n", + "|v_2 \\rangle=\\begin{pmatrix}0\\\\0\\\\1\\end{pmatrix}\n", + "$$\n", + "We now project our matrix $A$ onto the subspace of these vectors:\n", + "\n", + "$$\n", + "\\tilde{A}_2=\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}\\begin{pmatrix}4&-1&0\\\\-1&4&-1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&-1&0\\\\1&-4&1\\\\0&-1&4\\end{pmatrix}\\begin{pmatrix}1&0&0\\\\0&-1&0\\\\0&0&1\\end{pmatrix}=\\begin{pmatrix}4&1&0\\\\1&4&1\\\\0&1&4\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "5f19b90b-93fa-4ccb-b8a2-3bbd55e4dab0", + "metadata": {}, + "source": [ + "We now determine the eigenvalues:\n", + "$$\n", + "\\det(\\tilde{A}_2-\\lambda I)=0\n", + "$$\n", + "$$\n", + "\\begin{vmatrix}4-\\lambda&1&0\\\\1&4-\\lambda&1\\\\0&1&4-\\lambda\\end{vmatrix} = (4-\\lambda)((4-\\lambda)^2-1)-(4-\\lambda)=0\\\\\n", + "$$\n", + "$$\n", + "4-\\lambda=0,4-\\lambda=±2^{1/2}→\\lambda=4-2^{1/2},4,4+2^{1/2}≈2.59,4,5.41\n", + "$$\n", + "These eigenvalues are exactly the eigenvalues of the original matrix $A$. This must be the case, since we have expanded our Krylov subspace to span the entire vector space of the original matrix $A$.\n", + "\n", + "In this example, the Krylov method may not appear particularly easier than direct diagonalization. Indeed, as we will see in later sections, the Krylov method is only advantageous above a certain matrix dimension; this is intended to help us solve eigenvalue/eigenvector problems of extremely large matrices.\n", + "\n", + "![An image showing a very large matrix being projected onto a Krylov subspace, that is, rows of Krylov vectors making a matrix on the left, a Hamiltonian, then columns of Krylov vectors on the right.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig1.avif)\n", + "\n", + "This is the only example we will show worked “by hand”, but section 2 below shows computational examples.\n", + "\n", + "#### Clarification of terms\n", + "\n", + "A common misconception is that there is just a single Krylov subspace for a given problem. But of course, since there are many initial vectors to which our matrix could be applied, there are many possible Krylov subspaces. We will only use the phrase \"__the__ Krylov subspace\" to refer to a specific Krylov subspace already defined for a specific example. For general problem-solving approaches we will refer to \"__a__ Krylov subspace\".\n", + "A final clarification is that it is valid to refer to a \"Krylov __space__\". One often sees it called a \"Krylov __subspace__\" because of its use in the context of projecting matrices from an initial space into a subspace. In keeping with that context, we will mostly refer to it as a subspace here." + ] + }, + { + "cell_type": "markdown", + "id": "f3117328-61e0-45a2-b8f2-05356d210fae", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "Explain why it is not (a) useful, and (b) possible to extend the dimension of the Krylov subspace $r$ beyond the dimension $N$ of the matrix of interest.\n", + "\n", + "\n", + "\n", + "\n", + "(a) Since we are orthonormalizing the vectors as we produce them, a set of $N$ such vectors will form a complete basis, meaning a linear combination of them can be used to create any vector in the space.\n", + "\n", + "(b) The orthogonalization process consists of subtracting off the projection of a new vector onto all previous vectors. If all previous vectors span the full vector space, then subtracting off projections onto the full subspace will always leave us with a zero vector.\n", + "\n", + "\n", + "\n", + "\n", + "Suppose a fellow researcher is demonstrating the Krylov method applied to a small toy matrix. Is there something wrong with their choice of matrix $A$ and initial vector $|\\psi\\rangle$?\n", + "\n", + "$$\n", + "A=\\begin{pmatrix}2&1&3\\\\1&2&3\\\\3&3&5\\end{pmatrix}\n", + "$$\n", + "and\n", + "$$\n", + "|\\psi\\rangle=\\frac{1}{\\sqrt{2}}\\begin{pmatrix}1\\\\-1\\\\0\\end{pmatrix}.\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "Your colleague has accidentally chosen an eigenvector for his/her initial vector. Acting with the matrix on the initial vector will simply return the same vector back, scaled by the eigenvalue. This will not generate a subspace of increasing dimension. Advise your colleague to select a different initial vector, making sure it is not an eigenvector.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Apply the Krylov method to the given matrix, selecting an appropriate new initial vector. Write down the estimates of the minimum eigenvalue at the 0th and 1st order of your Krylov subspace.\n", + "\n", + "$$\n", + "A=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "There are many possible answers depending on the choice of initial vector. We will choose:\n", + "$$\n", + "|v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}.\n", + "$$\n", + "To get $|v_1\\rangle$ we apply $A$ once to $|v_0\\rangle$, and then make $|v_1\\rangle$ orthogonal to $|v_0\\rangle.$\n", + "$$\n", + "A|v_0\\rangle=\\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\n", + "$$\n", + "$$\n", + "A|v_0\\rangle - \\langle v_0|A|v_0\\rangle |v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix} - \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{1}{\\sqrt{3}}\\begin{pmatrix}2\\\\3\\\\2\\end{pmatrix}-\\frac{7}{3}\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix}=\\sqrt{\\frac{3}{2}}\\begin{pmatrix}-1/3\\\\2/3\\\\-1/3\\end{pmatrix}\n", + "$$\n", + "At 0th order, the projection onto our Krylov subspace is\n", + "\n", + "$$\n", + "\\langle v_0|A|v_0\\rangle=\\frac{1}{\\sqrt{3}}\\begin{pmatrix}1&1&1\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\frac{1}{\\sqrt{3}}\\begin{pmatrix}1\\\\1\\\\1\\end{pmatrix} = \\frac{7}{3}\n", + "$$\n", + "At 1st order, the projection onto this Krylov subspace is\n", + "\n", + "$$\n", + "\\langle V^1|A|V^1\\rangle=\\begin{pmatrix}\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}&\\frac{1}{\\sqrt{3}}\\\\-\\sqrt{\\frac{1}{6}}&\\sqrt{\\frac{2}{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix} \\begin{pmatrix}1&1&0\\\\1&1&1\\\\0&1&1\\end{pmatrix} \\begin{pmatrix}\\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\\\\\frac{1}{\\sqrt{3}}& \\sqrt{\\frac{2}{3}} \\\\ \\frac{1}{\\sqrt{3}}&-\\sqrt{\\frac{1}{6}}\\end{pmatrix}\n", + "$$\n", + "This can be done by hand, but is most easily done using numpy:\n", + "\n", + "```python\n", + "import numpy as np\n", + "vstar = np.array([[1/np.sqrt(3),1/np.sqrt(3),1/np.sqrt(3)],[-1/np.sqrt(6),np.sqrt(2/3),-1/np.sqrt(6)]]\n", + ")\n", + "A = np.array([[1, 1, 0],\n", + " [1, 1, 1],\n", + " [0, 1, 1]])\n", + "v = np.array([[1/np.sqrt(3),-1/np.sqrt(6)],[1/np.sqrt(3),np.sqrt(2/3)],[1/np.sqrt(3),-1/np.sqrt(6)]])\n", + "proj = vstar@A@v\n", + "print(proj)\n", + "eigenvalues, eigenvectors = np.linalg.eig(proj)\n", + "print(\"The eigenvalues are \", eigenvalues)\n", + "print(\"The eigenvectors are \", eigenvectors)\n", + "```\n", + "outputs:\n", + "```python\n", + "[[ 2.33333333 0.47140452]\n", + " [ 0.47140452 -0.33333333]]\n", + "The eigenvalues are [ 2.41421356 -0.41421356]\n", + "The eigenvectors are [[ 0.98559856 -0.16910198]\n", + " [ 0.16910198 0.98559856]]\n", + "```\n", + "The minimum eigenvalue estimate is -0.414.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "5a4f969e-d346-485a-9e4c-a04dbc9492e9", + "metadata": {}, + "source": [ + "### 1.2 Types of Krylov methods\n", + "\n", + "\"Krylov subspace methods\" can refer to any of several iterative techniques used to solve large linear systems and eigenvalue problems. What they all have in common is that they construct an approximate solution from a Krylov subspace\n", + "\n", + "$$\\mathcal{K}^r(A,|v\\rangle ) = \\text{span}\\{|v\\rangle, A|v\\rangle, A^2|v\\rangle, ..., A^{r-1}|v\\rangle\\},$$\n", + "\n", + "where $|v\\rangle$ is the initial guess (see Ref [\\[5\\]](#references)). They differ in how they choose the best approximation from this subspace, balancing factors such as convergence rate, memory usage, and overall computational cost. The focus of this lesson is to leverage quantum computing in the context of Krylov subspace methods; an exhaustive discussion of these methods is beyond its scope. The brief definitions below are for context only and include some references for investigating these methods further.\n", + "\n", + "__The conjugate gradient (CG) method__: This method is used for solving symmetric, positive definite linear systems[\\[6\\]](#references). It minimizes the A-norm of the error at each iteration, making it particularly effective for systems arising from discretized elliptic PDEs[\\[7\\]](#references). We will use this approach in the next section to motivate why a Krylov subspace would be an effective subspace in which to probe for improved solutions to linear systems.\n", + "\n", + "__The generalized minimal residual (GMRES) method__: This is designed for solving general nonsymmetric linear systems. It minimizes the residual norm over a Krylov space at each iteration, making it robust but potentially memory-intensive for large systems[\\[7\\]](#references).\n", + "\n", + "__The minimal residual (MINRES) method__: This method is used for solving symmetric indefinite linear systems. It's similar to GMRES but takes advantage of the matrix symmetry to reduce computational cost[\\[8\\]](#references).\n", + "\n", + "Other approaches of note include the __full orthogonalization method (FOM)__, which is closely related to Arnoldi's method for eigenvalue problems, the __bi-conjugate gradient (BiCG) method__, and the __induced dimension reduction (IDR) method__." + ] + }, + { + "cell_type": "markdown", + "id": "774fa41b-9fbb-46cc-9c64-37f7a008e161", + "metadata": {}, + "source": [ + "### 1.3 Why the Krylov subspace method works\n", + "\n", + "Here we will motivate that the Krylov subspace method should be an efficient way to approximate matrix eigenvalues via iterative refinement of eigenvector approximations, through the lens of steepest descent. We will argue that given an initial guess of a ground state, the space of successive corrections to that initial guess that yields the fastest convergence is a Krylov subspace. We stop short of a rigorous proof of convergence behavior.\n", + "\n", + "Assume our matrix of interest $A$ is symmetric and positive definite. This makes our argument most relevant to the CG method above. We make no assumptions about sparsity here; nor are we claiming that $A$ must be a Hermitian (which it needs to be if it is a Hamiltonian).\n", + "\n", + "We typically wish to solve a problem of the form\n", + "$$\n", + "A|x\\rangle=|b\\rangle.\n", + "$$\n", + "One might imagine that $|b\\rangle=c|x\\rangle$ where $c$ is some constant, as in an eigenvalue problem. But our problem statement remains more general for now.\n", + "\n", + "We start with a vector $|x_0\\rangle$ that is an approximate solution. Although there are parallels between this guess $|x_0\\rangle$ and $|v_0\\rangle$ in Section 1.1, we are not leveraging these here. Our guess $|x_0\\rangle$ has error, which we call $|e_0\\rangle:$\n", + "$$\n", + "|e_0\\rangle:=|x\\rangle−|x_0\\rangle.\n", + "$$\n", + "We also define the residual $R_0:$\n", + "$$\n", + "|R_0\\rangle=|b\\rangle−A|x_0\\rangle.\n", + "$$\n", + "Here we use capital $R$ to distinguish the residual from the dimension of our Krylov subspace $r$.\n", + "\n", + "![A true eigenvector labeled x, a guess labeled x 0 and a graphical representation of hte error between those two.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig2.svg)\n", + "\n", + "We now want to make a correction step of the form\n", + "$$\n", + "|x_1\\rangle=|x_0\\rangle+|p_0\\rangle,\n", + "$$\n", + "which we hope improves our approximation. Here $|p_0\\rangle$ is some vector yet to be determined. Let $|e_1\\rangle$ be the error after the correction is made. Then\n", + "$$\n", + "|e_1\\rangle=|x\\rangle−|x_1\\rangle=|x\\rangle−(|x_0\\rangle+|p_0\\rangle)=|e_0\\rangle−|p_0\\rangle.\n", + "$$\n", + "\n", + "![A true eigenvector and an update to the initial guess. The updated guess is closer to the true eigenvector.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig3.svg)\n", + "\n", + "We are interested in how our error behaves when transformed by our matrix. So let us calculate the $A$-norm of the error. That is\n", + "$$\n", + "\\begin{aligned}\n", + "∥|e_0\\rangle−|p_0\\rangle∥_A^2&=\\left(\\langle e_0|A−\\langle p_0|A\\right)\\left(|e_0\\rangle−|p_0\\rangle\\right)\\\\\n", + " & = \\langle e_0|A|e_0 \\rangle − \\langle e_0|A|p_0\\rangle − \\langle p_0|A|e_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n", + " & = \\langle e_0|A|e_0\\rangle−2\\langle e_0|A|p_0\\rangle+\\langle p_0|A|p_0\\rangle\\\\\n", + " & = d−2\\langle R_0|p_0\\rangle +\\langle p_0|A|p_0\\rangle,\n", + " \\end{aligned}\n", + "$$\n", + "where we have used the symmetry of $A$ and also that $A |e_0\\rangle = |R_0\\rangle.$ Here $d$ is some constant independent of $|p_0\\rangle$. As mentioned in Section 1.2, the $A$-norm of the error is not the only quantity we might choose to minimize, but it is a good one. We want to see how this quantity varies with our choice of correction vectors $|p_0\\rangle.$ So we define the function $f$ by setting\n", + "$$\n", + "f(|p_0\\rangle)=\\langle p_0|A|p_0\\rangle−2\\langle R_0|p_0\\rangle+d.\n", + "$$\n", + "$f$ is just the error $|e_1\\rangle$ as a function of the correction $|p_0\\rangle$ measured in the $A$-norm. Hence, we want to choose $|p_0\\rangle$ such that $f(|p_0\\rangle)$ is as small as possible. For this purpose, we compute the gradient of $f$. Using the symmetry of $A$ we have\n", + "$$\n", + "\\nabla f(|p_0\\rangle) = 2(A|p_0\\rangle−|R_0\\rangle).\n", + "$$\n", + "The gradient points in the direction of steepest ascent, meaning its opposite gives us the direction in which the function decreases the most: the direction of __steepest descent__. At our initial guess $|x_0\\rangle$, where $|p_0\\rangle=0$, we have that\n", + "$\\nabla f(0) = -2|R_0\\rangle.$\n", + "Thus, the function $f$ decreases the most in the direction of the residual $|R_0\\rangle.$ So our initial choice would benefit most by the addition of the vector $|p_0\\rangle=\\alpha_0 |R_0\\rangle$ for some scalar $\\alpha_0$.\n", + "\n", + "In the next step, we choose, again, a vector $|p_1\\rangle$ and add its value to the current approximation. Using the same argument as before we choose $|p_1\\rangle = \\alpha_1 |R_1\\rangle$ for some scalar $\\alpha_1$. We continue in this manner, such that the $k^\\text{th}$ iteration of our vector is\n", + "$$\n", + "|x_{k+1}\\rangle=|x_0\\rangle+\\alpha_0 |R_0\\rangle+\\alpha_1 |R_1\\rangle+⋯+\\alpha_k |R_k\\rangle.\n", + "$$\n", + "Equivalently, we want to build up the space from which we choose our improved estimates by adding $|R_0\\rangle$, $|R_1\\rangle$, and so on, in order. The $k^\\text{th}$ estimated vector lies in\n", + "$$\n", + "|x_{k+1}\\rangle\\in |x_0\\rangle+\\text{span}\\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}.\n", + "$$\n", + "Now, using the relation that\n", + "$$\n", + "|R_{k+1}\\rangle=|b\\rangle−A |x_{k+1}\\rangle=|b\\rangle−A(|x_k\\rangle+\\alpha_k |R_k\\rangle)=|R_k\\rangle−\\alpha_k A |R_k\\rangle,\n", + "$$\n", + "we see that\n", + "$$\n", + "\\text{span} \\{|R_0\\rangle,|R_1\\rangle,…,|R_k\\rangle \\}=\\text{span} \\{|R_0\\rangle,A|R_0\\rangle,…,A^{k}|R_0\\rangle \\}.\n", + "$$\n", + "That is, the space we build up that most efficiently approximates the correct solution $|x\\rangle$ is exactly the space built up by successive operation of the matrix $A$ on $|R_0\\rangle.$ A Krylov subspace _is_ the space spanned by the vectors of successive directions of steepest descent.\n", + "\n", + "Finally we reiterate that we have made no numerical claims about the scaling of this approach, nor have we discussed the comparative benefit for sparse matrices. This is only meant to motivate the use of Krylov subspace methods, and add some intuitive sense for them. We will now explore the behavior of these methods numerically." + ] + }, + { + "cell_type": "markdown", + "id": "2df5e11f-62fe-4237-bcaf-36dcc88f11a7", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "In the workflow above, we proposed minimizing the $A$-norm of the error. What other quantities might one consider minimizing in seeking the ground state and its eigenvalue?\n", + "\n", + "\n", + "\n", + "\n", + "One could imagine using the residual vector instead of the $A$-norm of the error. There might be cases in which considering the error vector itself is useful.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "56bcf069-b3b5-4c51-b91f-7e9bb62c5c94", + "metadata": {}, + "source": [ + "## 2. Krylov methods in classical computation\n", + "\n", + "In this section we implement Arnoldi iterations computationally so that we may leverage a Krylov subspace in solving eigenvalue problems. We will apply this first to a small-scale example, then examine how computation time scales as the size of the matrix of interest increases. A key idea here will be that the generation of the vectors spanning the Krylov space will be a large contributor to total computing time required. The memory required will vary between specific Krylov methods. But memory constraints can limit the use of traditional Krylov methods." + ] + }, + { + "cell_type": "markdown", + "id": "952459b6-75dd-4044-8cec-7004fbc48ddf", + "metadata": {}, + "source": [ + "### 2.1 Simple small-scale example\n", + "\n", + "In the process of creating a Krylov subspace, we will need to orthonormalize the vectors in our subspace. Let us define a function that takes an established vector from our subspace `vknown` (not assumed to be normalized) and a candidate vector to add to our subspace `vnext` and make `vnext` orthogonal to `vknown` and normalized. Let us further define a function that steps through this process for all established vectors in our Krylov subspace to ensure a fully orthonormal set." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fb4f96ea-906c-4819-a9cb-8e9409cac051", + "metadata": {}, + "outputs": [], + "source": [ + "# vknown is some established vector in our subspace. vnext is one we wish to add, which must be\n", + "# orthogonal to vknown.\n", + "\n", + "\n", + "def orthog_pair(vknown, vnext):\n", + " vknown = vknown / np.sqrt(vknown.T @ vknown)\n", + " diffvec = vknown.T @ vnext * vknown\n", + " vnext = vnext - diffvec\n", + " return vnext\n", + "\n", + "\n", + "# v is the candidate vector to be added to our subspace. s is the existing subspace.\n", + "\n", + "\n", + "def orthoset(v, s):\n", + " v = v / np.sqrt(v.T @ v)\n", + " temp = v\n", + " for i in range(len(s)):\n", + " temp = orthog_pair(s[i], temp)\n", + " v = temp / np.sqrt(temp.T @ temp)\n", + " return v" + ] + }, + { + "cell_type": "markdown", + "id": "61a2704e-3985-4bd4-a9a9-253cc96c708b", + "metadata": {}, + "source": [ + "Now let us define a function that builds an iteratively larger and larger Krylov subspace, until the space of Krylov vectors spans the full space of the original matrix. This will enable us to see how well the eigenvalues obtained using our Krylov subspace method match the exact values, as a function of Krylov subspace dimension. Importantly, our function `krylov_full_build` returns the Krylov vectors, the projected Hamiltonians, the eigenvalues, and the time required." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "818cceee-bd88-472a-8af9-0a6c0dec5646", + "metadata": {}, + "outputs": [], + "source": [ + "# Necessary imports and definitions to track time in microseconds\n", + "import time\n", + "\n", + "\n", + "def time_mus():\n", + " return int(time.time() * 1000000)\n", + "\n", + "\n", + "# This function constructs a Krylov subspace that spans the whole space of the original matrix.\n", + "# Input:\n", + "# v0 : initial vector\n", + "# matrix : original matrix to be diagonalized\n", + "# Output:\n", + "# ks : Krylov vectors\n", + "# Hs : projected Hamiltonians\n", + "# eigs : eigenvalues\n", + "# k_tot_times : time required for the operation\n", + "\n", + "\n", + "def krylov_full_build(v0, matrix):\n", + " t0 = time_mus()\n", + " b = v0 / np.sqrt(v0 @ v0.T)\n", + " A = matrix\n", + " ks = []\n", + " ks.append(b)\n", + " Hs = []\n", + " eigs = []\n", + " Hs.append(b.T @ A @ b)\n", + " eigs.append(np.array([b.T @ A @ b]))\n", + " k_tot_times = []\n", + "\n", + " for j in range(len(A) - 1):\n", + " vec = A @ ks[j].T\n", + " ortho = orthoset(vec, ks)\n", + " ks.append(ortho)\n", + " ksarray = np.array(ks)\n", + " Hs.append(ksarray @ A @ ksarray.T)\n", + " eigs.append(np.linalg.eig(Hs[j + 1]).eigenvalues)\n", + " k_tot_times.append(time_mus() - t0)\n", + "\n", + " # Return the Krylov vectors, the projected Hamiltonians, the eigenvalues, and the total time\n", + " # required.\n", + " return (ks, Hs, eigs, k_tot_times)" + ] + }, + { + "cell_type": "markdown", + "id": "4cd7bdcc-65fd-4199-b9fd-d0a9f62cba17", + "metadata": {}, + "source": [ + "We will test this on a matrix that is still quite small, but larger than we might want to do by hand." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3e2aa939-b568-4982-b64a-9f2e4b0da2b5", + "metadata": {}, + "outputs": [], + "source": [ + "# Define our small test matrix\n", + "test_matrix = np.array(\n", + " [\n", + " [4, -1, 0, 1, 0],\n", + " [-1, 4, -1, 2, 1],\n", + " [0, -1, 4, 3, 3],\n", + " [1, 2, 3, 4, 0],\n", + " [0, 1, 3, 0, 4],\n", + " ]\n", + ")\n", + "\n", + "# Give the test matrix and an initial guess as arguments in the function defined above. Calculate\n", + "# outputs.\n", + "test_ks, test_Hs, test_eigs, text_k_tot_times = krylov_full_build(\n", + " np.array([0.5, 0.5, 0, 0.5, 0.5]), test_matrix\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2f74fcf0-8959-4862-b33d-c8b1eddda2a8", + "metadata": {}, + "source": [ + "We can check our functions by ensuring that in the last step (when the Krylov space is the full vector space of the original matrix) the eigenvalues from the Krylov method exactly match those of the exact numerical diagonalization:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "704db2f6-5081-404d-9ae9-7264a62ea432", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-1.36956923 8.43756009 2.9040308 5.34436028 4.68361806]\n", + "[-1.36956923 8.43756009 2.9040308 4.68361806 5.34436028]\n" + ] + } + ], + "source": [ + "print(np.linalg.eig(test_matrix).eigenvalues)\n", + "print(test_eigs[len(test_matrix) - 1])" + ] + }, + { + "cell_type": "markdown", + "id": "f0ddf041-d7e8-4131-bb4c-3310d8351c2c", + "metadata": {}, + "source": [ + "That was successful. Of course, what really matters is how good our approximation is as a function of the dimension of our Krylov subspace dimension. Because we are often concerned with finding ground states and other minimum eigenvalues (and for other more algebraic reasons explained below), let's look at our estimate of the lowest eigenvalue as a function of Krylov subspace dimension. That is" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "48b23ca9-a5ba-4a9c-bbc2-7fba75b36124", + "metadata": {}, + "outputs": [], + "source": [ + "def errors(matrix, krylov_eigs):\n", + " targ_min = min(np.linalg.eig(matrix).eigenvalues)\n", + " err = []\n", + " for i in range(len(matrix)):\n", + " err.append(min(krylov_eigs[i]) - targ_min)\n", + " return err" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0912e12b-63bd-4026-ac8e-56bc5db8736e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "krylov_error = errors(test_matrix, test_eigs)\n", + "\n", + "plt.plot(krylov_error)\n", + "plt.axhline(y=0, color=\"red\", linestyle=\"--\") # Add dashed red line at y=0\n", + "plt.xlabel(\"Order of Krylov subspace\") # Add x-axis label\n", + "plt.ylabel(\"Error in minimum eigenvalue\") # Add y-axis label\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7af1b947-205b-46f8-bb23-d439ae6f076a", + "metadata": {}, + "source": [ + "We see that the minimum eigenvalue is reached fairly accurately once the Krylov subspace has grown to $\\mathcal{K}^2,$ and is perfect by $\\mathcal{K}^3.$" + ] + }, + { + "cell_type": "markdown", + "id": "7ee80bf2-e3e8-4124-9afc-ef2ef247f831", + "metadata": {}, + "source": [ + "### 2.2 Time scaling with matrix dimension\n", + "\n", + "Let us convince ourselves that the Krylov method can be advantageous over exact numerical eigensolvers in the following way:\n", + "* Construct random matrices (not sparse, not the ideal application for KQD)\n", + "* Determine eigenvalues using two methods: directly using NumPy and using a Krylov subspace.\n", + "* We choose a cutoff for how precise our eigenvalues must be, before we accept the Krylov estimates.\n", + "* Compare the wall time required to solve in these two ways.\n", + "\n", + "__Caveats:__ As we will discuss in detail below, Krylov quantum diagonalization is best applied to operators whose matrix representations are sparse and/or can be written using a small number of groups of commuting Pauli operators. The random matrices we are using here do not fit that description. These are only useful in probing the scale at which classical Krylov methods might be useful.\n", + "Secondly, in using the Krylov method we will calculate eigenvalues using many different-sized Krylov subspaces. We will report the time required for the minimum-dimension Krylov subspace that achieves our required accuracy for the ground state eigenvalue. Again, this is a bit different from solving a problem that is intractable for exact eigensolvers, since we are using the exact solution to assess the dimension needed.\n", + "\n", + "We begin by generating our set of random matrices." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a5f8f1f6-b334-43e4-ad40-fd689c07a3e1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# Set the random seed\n", + "np.random.seed(42)\n", + "\n", + "# how many random matrices will we make\n", + "num_matrix = 200\n", + "\n", + "matrices = []\n", + "for m in range(1, num_matrix):\n", + " matrices.append(np.random.rand(m, m))" + ] + }, + { + "cell_type": "markdown", + "id": "d1c31d97-7e76-4577-ab03-ce8f0aebf134", + "metadata": {}, + "source": [ + "We now diagonalize each matrix directly, using numpy. We calculate the time required for diagonalization for later comparison." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d9cb6e36-9ad3-403f-823f-0072e9cf53fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "matrix_numpy_times = []\n", + "matrix_numpy_eigs = []\n", + "for mm in range(num_matrix - 1):\n", + " t0 = time_mus()\n", + " matrix_numpy_eigs.append(min(np.linalg.eig(matrices[mm]).eigenvalues))\n", + " matrix_numpy_times.append(time_mus() - t0)\n", + "\n", + "plt.plot(matrix_numpy_times)\n", + "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", + "plt.ylabel(\"Time to diagonalize (microsec)\") # Add y-axis label\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4e5d4af5-78da-4254-a726-4b0a72f98fe6", + "metadata": {}, + "source": [ + "Note that in the image above, the anomalously high time around a dimension of 125 may be due to the random nature of the matrices or due to implementation on the classical processor used, but it is not reproducible. Re-running the code will yield a different profile with different anomalous peaks.\n", + "\n", + "Now for each matrix we will build up a Krylov subspace and calculate eigenvalues in steps. At each step, we will check to see if the lowest eigenvalue has been obtained to within our specified absolute error. The subspace that first gives us eigenvalues within our specified error is the subspace for which we will record computation times. Executing this cell may take several minutes, depending on processor speed. Feel free to skip evaluation or reduce the maximum dimension of matrices diagonalized. Looking at the pre-calculated results is sufficient." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fbe57ec-507a-412e-b7fe-09aaea28e103", + "metadata": {}, + "outputs": [], + "source": [ + "# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size\n", + "# at which that error is achieved.\n", + "abserr = 0.05\n", + "accept_subspace_size = []\n", + "\n", + "# Lists to store total time spent on the Krylov method, and the subset of that time spent on\n", + "# diagonalizing the projected matrix.\n", + "matrix_krylov_tot_times = []\n", + "matrix_krylov_dim = []\n", + "\n", + "# Step through all our random matrices\n", + "for mm in range(0, num_matrix - 1):\n", + " test_ks, test_Hs, test_eigs, test_k_tot_times = krylov_full_build(\n", + " np.ones(len(matrices[mm])), matrices[mm]\n", + " )\n", + " # We have not yet found a Krylov subspace that produces our minimum eigenvalue to within the\n", + " # required error.\n", + " found = 0\n", + " for j in range(0, len(matrices[mm]) - 1):\n", + " # If we still haven't found the desired subspace...\n", + " if found == 0:\n", + " # ...but if this one satisfies the requirement, then record everything\n", + " if (\n", + " abs((min(test_eigs[j]) - matrix_numpy_eigs[mm]) / matrix_numpy_eigs[mm])\n", + " < abserr\n", + " ):\n", + " accept_subspace_size.append(j)\n", + " matrix_krylov_tot_times.append(test_k_tot_times[j])\n", + " matrix_krylov_dim.append(mm)\n", + " found = 1" + ] + }, + { + "cell_type": "markdown", + "id": "5046bfdb-9ff6-4142-9dc6-260ce26fd3f5", + "metadata": {}, + "source": [ + "Let us plot the times we have obtained for these two methods for comparison:" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "ed3302d5-a0a8-479b-9731-755fc5a1b684", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(matrix_numpy_times, color=\"blue\")\n", + "plt.plot(matrix_krylov_dim, matrix_krylov_tot_times, color=\"green\")\n", + "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", + "plt.ylabel(\"Time to diagonalize (microsec)\") # Add y-axis label\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e3c5ff65-964f-4b7c-8ca7-89958b3d9465", + "metadata": {}, + "source": [ + "These are the actual times required, but for the purposes of discussion, let us smooth these curves by averaging over a few adjacent points / matrix dimensions. This is done below:" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "41ac6108-526d-476b-8255-db9bb52a30ca", + "metadata": {}, + "outputs": [], + "source": [ + "smooth_numpy_times = []\n", + "smooth_krylov_times = []\n", + "\n", + "# Choose the number of adjacent points over which to average forward; the same will be used\n", + "# backward.\n", + "smooth_steps = 10\n", + "\n", + "# We will do this smoothing for all points/matrix dimensions\n", + "for i in range(len(matrix_krylov_tot_times)):\n", + " # Ensure we don't exceed the boundaries of our lists\n", + " start = max(0, i - smooth_steps)\n", + " end = min(len(matrix_krylov_tot_times) - 1, i + smooth_steps)\n", + "\n", + " # Dummy variables for accumulating an average over adjacent points. This is done for both Krylov\n", + " # and the NumPy calculations.\n", + " smooth_count = 0\n", + " smooth_numpy_sum = 0\n", + " smooth_krylov_sum = 0\n", + "\n", + " for j in range(start, end):\n", + " smooth_numpy_sum = smooth_numpy_sum + matrix_numpy_times[j]\n", + " smooth_krylov_sum = smooth_krylov_sum + matrix_krylov_tot_times[j]\n", + " smooth_count = smooth_count + 1\n", + "\n", + " # Appending the averaged adjacent values to our new smooth lists\n", + " smooth_numpy_times.append(smooth_numpy_sum / smooth_count)\n", + " smooth_krylov_times.append(smooth_krylov_sum / smooth_count)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "6963ee9b-55a8-42ab-bc4f-d470bb81e543", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(smooth_numpy_times, color=\"blue\")\n", + "plt.plot(smooth_krylov_times, color=\"green\")\n", + "plt.xlabel(\"Dimension of matrix\") # Add x-axis label\n", + "plt.ylabel(\"Time to diagonalize (smoothed, microsec)\") # Add y-axis label\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7e1bbb91-5da7-43ea-8b99-d04ea1196e76", + "metadata": {}, + "source": [ + "Note that the time required for the building of a Krylov subspace initially exceeds the time required for numpy's full diagonalization. But as the size of the matrix grows, the Krylov method becomes advantageous. This is true even if we lower our acceptable error, but the advantage sets in at a larger matrix size. This is worth picking apart.\n", + "\n", + "The time complexity of numerical diagonalization is $O(n^3)$ (with some variation among algorithms). The time complexity of generating an orthonormal basis of $n$ vectors is also $O(n^3)$. So the advantage of the Krylov method is __not__ related to the use of $\\it{some}$ orthonormal basis, but to the use of a particular orthonormal basis that effectively picks out the eigenvalues of interest. We have already seen this from the sketch of a proof in the first section of this lesson, and this is critical for the convergence guarantees in Krylov methods." + ] + }, + { + "cell_type": "markdown", + "id": "da603cad-2e8e-4494-a0fd-f082953c35cc", + "metadata": {}, + "source": [ + "Let us review our progress so far:\n", + "* For very large matrices, the Krylov subspace method may yield approximate eigenvalues within required tolerances faster than traditional diagonalization algorithms.\n", + "* For such very large matrices, the generation of a Krylov subspace is the most time-consuming part of the Krylov subspace method.\n", + "* Thus an efficient way of generating a Krylov subspace would be highly valuable.\n", + "This is finally where quantum computer comes into the picture." + ] + }, + { + "cell_type": "markdown", + "id": "7782cac1-a86b-4a70-971f-3db5922838d7", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "Refer to the smoothed plot of diagonalization times versus matrix dimension above.\n", + "\n", + "(a) At approximately what matrix dimension did the Krylov method become faster, according to this plot?\n", + "\n", + "(b) What aspects of the calculation could change that dimension at which the Krylov method becomes faster?\n", + "\n", + "\n", + "\n", + "\n", + "(a) Answers may vary if you re-run the calculation, but the Krylov method becomes faster at approximately dimension 80-85.\n", + "\n", + "(b) There are many possible answers. Some important factors are the precision we require and the sparsity of the matrices being diagonalized.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "03b62a85-4737-41ec-b264-55d22fe814bc", + "metadata": {}, + "source": [ + "## 3. Krylov via time evolution\n", + "\n", + "Everything that we have described so far can be done classically. So how and when would we use a quantum computer? For very large matrices, the Krylov method can require long computing times and large amounts of memory. The time required for matrix operation of $H$ on $|v\\rangle$ scales like $O(N^2)$ in the worst case. Even multiplying sparse matrices on a vector (the typical case for classical Krylov-type solvers) has a time complexity scaling like $O(N)$. This is done for every vector we want in our subspace. The subspace dimension $r$ is usually not a significant fraction of $N$, and often scales like $\\log(N)$. So generating all vectors scales like $O(N^2 \\log(N))$ in the worst case. Although there are other steps, like orthogonalization, this is the dominant scaling to keep in mind.\n", + "\n", + "Quantum computing allows us to change what attributes of the problem determine the scaling of the time and resources required. Instead of dependence on matrix size $N$ across the board, we will see things like number of shots and number of non-commuting Pauli terms that make up the Hamiltonian. Let’s explore how this works." + ] + }, + { + "cell_type": "markdown", + "id": "1636f90b-6bdf-4b11-aee3-9be3f2e82d79", + "metadata": {}, + "source": [ + "### 3.1 Time evolution\n", + "\n", + "Recall that the operator that time-evolves a quantum state is $e^{-iHt/\\hbar}$ (and it is very common, especially in quantum computing to drop the $\\hbar$ from the notation). One way of understanding and even realizing such an exponential function of an operator is to look at its Taylor series expansion. Note that this operation acting on some initial vector $|v\\rangle$ yields a sum of terms with increasing powers of $H$ applied to the initial state. It looks like we can just make our Krylov subspace by time-evolving our initial guess state!\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "e^{-iHt/\\hbar}→e^{-iHt}&≈1-iHt-\\frac{(H^2 t^2)}{2}+⋯\\\\\n", + "e^{-iHt} |v\\rangle &≈ |v\\rangle-iHt|v\\rangle-\\frac{(H^2 t^2)}{2}|v\\rangle+⋯\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "The caveat is in realizing the time evolution on a real quantum computer. Many of the terms in the Hamiltonian will not commute with each other. So while some simple exponential operators like $e^{-iZ}$ correspond to simple circuits, general Hamiltonians do not. And since they contain non-commuting terms, we can’t simply decompose the exponential into a product of simple ones, the way we can with numbers.\n", + "\n", + "$$\n", + "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n)t}\\neq e^{-iH_1 t} e^{-iH_2 t}... e^{-iH_n t}\n", + "$$\n", + "\n", + "So this is not trivial, but this is a well-studied process in quantum computing. We carry out time-evolution on quantum computers using a process called trotterization, which in itself is a rich subject[\\[10\\]](#references). But at a very high level, by breaking the time evolution into very small steps, say $m$ steps of size $dt$, we limit the effects of the non-commutativity of terms.\n", + "\n", + "$$\n", + "e^{-iHt}=e^{-i(H_1+H_2+⋯+H_n )t} = (e^{-i(H_1+H_2+⋯+H_n )t/m} )^m\n", + "≈(e^{-iH_1 dt} e^{-iH_2 dt} …e^{-iH_n dt} )^m\n", + "$$\n", + "where $dt = t/m$.\n", + "\n", + "Let us call a Krylov subspace of order r that we generated in the classical context using powers of H directly a “power Krylov subspace”.\n", + "\n", + "$$\n", + "\\mathcal{K}_P^r (H,|v\\rangle)=\\text{span}\\{|v\\rangle,H|v\\rangle,H^2 |v\\rangle… H^{r-1} |v\\rangle\\}\n", + "$$\n", + "\n", + "Now we generate a similar space using the unitary time-evolution operator $U \\equiv e^{-iHt}$; we'll refer to this as the “unitary Krylov space” $\\mathcal{K}_U^r$. The power Krylov subspace $\\mathcal{K}_P^r$ that we use classically cannot be generated directly on a quantum computer as $H$ is not a unitary operator. Using the unitary Krylov subspace can be shown to give similar convergence guarantees as the power Krylov subspace, namely, the ground state error converges efficiently as long as the initial state $|v\\rangle$ has overlap with the true ground state that is not exponentially vanishing, and as long as there is a sufficient gap between eigenvalues. See Ref [\\[1\\]](#references) for a more precise discussion of convergence.\n", + "\n", + "Here, powers of $U$ become different time steps (the $k^\\text{th}$ power of $U$ is stepping forward by a time $k \\times dt$). We can label the element of the subspace that is time-evolved for total time $k dt$ as $|\\psi_k\\rangle$." + ] + }, + { + "cell_type": "markdown", + "id": "619c81f7-be3c-4ffa-87f0-2ab6420381ca", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{aligned}\n", + "U&=e^{-iHdt}\\\\\n", + "U^k&=e^{-iH(kdt)}\\\\\n", + "\\mathcal{K}_U^r&=\\text{span}\\{|\\psi\\rangle,U|\\psi\\rangle,U^2 |\\psi\\rangle… U^{r-1} |\\psi\\rangle\\}\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "ca6352b4-88c2-4dab-9f82-1750223ec8af", + "metadata": {}, + "source": [ + "We can project our Hamiltonian H on to the unitary Krylov subspace, $\\mathcal{K}_U^r$. In other words, we calculate each matrix element of $H$ in the $\\mathcal{K}_U^r$ basis. We'll refer to this projected matrix as $\\tilde{H}$.\n", + "\n", + "### 3.2 How to implement on a quantum computer\n", + "\n", + "The matrix elements of $\\tilde{H}$ are given by the expectation values $\\langle \\psi_m |H| \\psi_n\\rangle$, which can be estimated using the quantum computer. Keep in mind that $H$ can be written as a sum of Pauli operators on different qubits, and that not all the Pauli operators can be measured simultaneously. We can sort the Pauli terms into groups of commuting terms, and measure all those at once. But we may need many such groups to cover all the terms. So the number of distinct commuting groups into which the terms can be partitioned, $N_\\text{GCP}$ becomes important.\n", + "\n", + "$$\n", + "H=\\sum_{\\alpha=1}^{N_\\text{GCP}} c_\\alpha P_\\alpha\n", + "$$\n", + "Here $P_\\alpha$ is a Pauli string of the form $P_\\alpha \\sim IZIXII...YZXIX$ or a set of such Pauli strings that commute with one another. Given that we can write $H$ as such a sum of measureable operators, the following expressions for matrix elements of $\\tilde{H}$ can be realized using the Qiskit Runtime primitive Estimator.\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\tilde{H}_{mn}&=\\langle \\psi_m |H| \\psi_n\\rangle\\\\\n", + "&=\\langle \\psi e^{iHt_m} |H| \\psi e^{-iHt_n}\\rangle\\\\\n", + "&=\\langle \\psi e^{iHmdt} |H|\\psi e^{-iHndt}\\rangle\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Where $\\vert \\psi_n \\rangle = e^{-i H t_n} \\vert \\psi \\rangle$ are the vectors of the unitary Krylov space and $t_n = n dt$ are the multiples of the time step $dt$ chosen. On a quantum computer, the calculation of each matrix elements can be done with any algorithm which allows us to obtain overlap between quantum states. In this lesson we'll focus on the Hadamard test. Given that the $\\mathcal{K}_U$ has dimension $r$, the Hamiltonian projected into the subspace will have dimensions $r \\times r$. With $r$ small enough (generally $r<<100$ is sufficient to obtain convergence of estimates of eigenvalues) we can then easily diagonalize the projected Hamiltonian $\\tilde{H},$ classically. However, we cannot directly diagonalize $\\tilde{H}$ because of the non-orthogonality of the Krylov space vectors. We'll have to measure their overlaps and construct a matrix $\\tilde{S}$\n", + "\n", + "$$\n", + "\\tilde{S}_{mn} = \\langle \\psi_m \\vert \\psi_n \\rangle\n", + "$$\n", + "\n", + "This allows us to solve the eigenvalue problem in a non-orthogonal space (also called generalized eigenvalue problem)\n", + "\n", + "$$\n", + "\\tilde{H} \\ \\vec{c} = E \\ \\tilde{S} \\ \\vec{c}\n", + "$$\n", + "\n", + "One can then obtain estimates of the eigenvalues and eigenstates of $H$ by looking at the solutions of this generalized eigenvalue problem. For example, the estimate of the ground state energy is obtained by taking the smallest eigenvalue $E$ and the ground state from the corresponding eigenvector $\\vec{c}$. The coefficients in $\\vec{c}$ determines the contribution of the different vectors that span $\\mathcal{K}_U$." + ] + }, + { + "cell_type": "markdown", + "id": "7d29b607-3f2c-445c-9630-3cde4edf3299", + "metadata": {}, + "source": [ + "#### Generalized eigenvalue problem\n", + "\n", + "Why can we not simply diagonalize $\\tilde{H}$? Since $\\tilde{S}$ contains the information about the geometry of the Krylov basis (which is nonorthogonal in all but very special cases), $\\tilde{H}$ on its own does not describe a projection of the full Hamiltonian, so its eigenvalues have no particular relation to those of the full Hamiltonian -- they could be any random values. Solving the generalized eigenvalue problem is required to obtain the approximate eigenvalues and eigenvectors corresponding to the projection of the full Hamiltonian into the Krylov space.." + ] + }, + { + "cell_type": "markdown", + "id": "2f3ba07d-0c16-47fb-aeed-b4ef0a1f25e3", + "metadata": {}, + "source": [ + "![A circuit diagram with many layers indicating that the circuit must be used many times with different states to perform the modified Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig4.avif)\n", + "\n", + "The Figure shows a circuit representation of the modified Hadamard test, a method that is used to compute the overlap between different quantum states. For each matrix element $\\tilde{H}_{i,j}$, a Hadamard test between the state $\\vert \\psi_i \\rangle$, $\\vert \\psi_j \\rangle$ is carried out. This is highlighted in the figure by the color scheme for the matrix elements and the corresponding $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ operations. Thus, a set of Hadamard tests for all the possible combinations of Krylov space vectors is required to compute all the matrix elements of the projected Hamiltonian $\\tilde{H}$. The top wire in the Hadamard test circuit is an ancilla qubit which is measured either in the X or Y basis, its expectation value determines the value of the overlap between the states. The bottom wire represents all the qubits of the system Hamiltonian. The $\\text{Prep} \\; \\psi_i$ operation prepares the system qubit in the state $\\vert \\psi_i \\rangle$ controlled by the state of the ancilla qubit (similarly for $\\text{Prep} \\; \\psi_j$) and the operation $P$ represents Pauli decomposition of the system Hamiltonian $H = \\sum_i P_i$. The implementation of this is on a quantum computer is shown in greater detail below." + ] + }, + { + "cell_type": "markdown", + "id": "c12c2855-f276-48a2-8901-13baf0384778", + "metadata": {}, + "source": [ + "## 4. Krylov quantum diagonalization on a quantum computer\n", + "\n", + "We will now implement Krylov quantum diagonalization on a real quantum computer. Let's start by importing some useful packages." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "afa233e1-1e80-4843-a958-0f84cec707ea", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import scipy as sp\n", + "import matplotlib.pylab as plt\n", + "from typing import Union, List\n", + "import warnings\n", + "\n", + "from qiskit.quantum_info import SparsePauliOp, Pauli\n", + "from qiskit.circuit import Parameter\n", + "from qiskit import QuantumCircuit, QuantumRegister\n", + "from qiskit.circuit.library import PauliEvolutionGate\n", + "from qiskit.synthesis import LieTrotter\n", + "\n", + "# from qiskit.providers.fake_provider import Fake20QV1\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator, Batch\n", + "\n", + "import itertools as it\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4a3fba7-051f-4df4-b78a-7bfeb18caf1e", + "metadata": {}, + "source": [ + "We define the function below to solve the generalized eigenvalue problem we just explained above." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "5446eb74-126f-4db1-b018-d5f4613f79e7", + "metadata": {}, + "outputs": [], + "source": [ + "def solve_regularized_gen_eig(\n", + " h: np.ndarray,\n", + " s: np.ndarray,\n", + " threshold: float,\n", + " k: int = 1,\n", + " return_dimn: bool = False,\n", + ") -> Union[float, List[float]]:\n", + " \"\"\"\n", + " Method for solving the generalized eigenvalue problem with regularization\n", + "\n", + " Args:\n", + " h (numpy.ndarray):\n", + " The effective representation of the matrix in our Krylov subspace\n", + " s (numpy.ndarray):\n", + " The matrix of overlaps between vectors of our Krylov subspace\n", + " threshold (float):\n", + " Cut-off value for the eigenvalue of s\n", + " k (int):\n", + " Number of eigenvalues to return\n", + " return_dimn (bool):\n", + " Whether to return the size of the regularized subspace\n", + "\n", + " Returns:\n", + " lowest k-eigenvalue(s) that are the solution of the regularized generalized eigenvalue problem\n", + "\n", + "\n", + " \"\"\"\n", + " s_vals, s_vecs = sp.linalg.eigh(s)\n", + " s_vecs = s_vecs.T\n", + " good_vecs = np.array([vec for val, vec in zip(s_vals, s_vecs) if val > threshold])\n", + " h_reg = good_vecs.conj() @ h @ good_vecs.T\n", + " s_reg = good_vecs.conj() @ s @ good_vecs.T\n", + " if k == 1:\n", + " if return_dimn:\n", + " return sp.linalg.eigh(h_reg, s_reg)[0][0], len(good_vecs)\n", + " else:\n", + " return sp.linalg.eigh(h_reg, s_reg)[0][0]\n", + " else:\n", + " if return_dimn:\n", + " return sp.linalg.eigh(h_reg, s_reg)[0][:k], len(good_vecs)\n", + " else:\n", + " return sp.linalg.eigh(h_reg, s_reg)[0][:k]" + ] + }, + { + "cell_type": "markdown", + "id": "7dce32ec-33eb-474d-88bf-e07f6563d6a2", + "metadata": {}, + "source": [ + "At least in initial benchmarking, it is useful to know an exact classical solution to check convergence behavior. The function below calculates the ground state energy of a Hamiltonian, using the Hamiltonian and the number of qubits as arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "f2d43bcc-5210-448d-b3d9-996796f782f6", + "metadata": {}, + "outputs": [], + "source": [ + "def single_particle_gs(H_op, n_qubits):\n", + " \"\"\"\n", + " Find the ground state of the single particle(excitation) sector\n", + " \"\"\"\n", + " H_x = []\n", + " for p, coeff in H_op.to_list():\n", + " H_x.append(set([i for i, v in enumerate(Pauli(p).x) if v]))\n", + "\n", + " H_z = []\n", + " for p, coeff in H_op.to_list():\n", + " H_z.append(set([i for i, v in enumerate(Pauli(p).z) if v]))\n", + "\n", + " H_c = H_op.coeffs\n", + "\n", + " print(\"n_sys_qubits\", n_qubits)\n", + "\n", + " n_exc = 1\n", + " sub_dimn = int(sp.special.comb(n_qubits + 1, n_exc))\n", + " print(\"n_exc\", n_exc, \", subspace dimension\", sub_dimn)\n", + "\n", + " few_particle_H = np.zeros((sub_dimn, sub_dimn), dtype=complex)\n", + "\n", + " sparse_vecs = [\n", + " set(vec) for vec in it.combinations(range(n_qubits + 1), r=n_exc)\n", + " ] # list all of the possible sets of n_exc indices of 1s in n_exc-particle states\n", + "\n", + " m = 0\n", + " for i, i_set in enumerate(sparse_vecs):\n", + " for j, j_set in enumerate(sparse_vecs):\n", + " m += 1\n", + "\n", + " if len(i_set.symmetric_difference(j_set)) <= 2:\n", + " for p_x, p_z, coeff in zip(H_x, H_z, H_c):\n", + " if i_set.symmetric_difference(j_set) == p_x:\n", + " sgn = ((-1j) ** len(p_x.intersection(p_z))) * (\n", + " (-1) ** len(i_set.intersection(p_z))\n", + " )\n", + " else:\n", + " sgn = 0\n", + "\n", + " few_particle_H[i, j] += sgn * coeff\n", + "\n", + " gs_en = min(np.linalg.eigvalsh(few_particle_H))\n", + " print(\"single particle ground state energy: \", gs_en)\n", + " return gs_en" + ] + }, + { + "cell_type": "markdown", + "id": "c70998c4-e65c-456e-b3b4-61a289c8dd84", + "metadata": {}, + "source": [ + "### 4.1 Step 1: Map problem to quantum circuits and operators\n", + "\n", + "Now we will define a Hamiltonian. This is distinct from the function above in that the function above takes a Hamiltonian as an argument and returns only the ground state, and it does so classically. This Hamiltonian we define here determines the energy levels of all energy eigenstates, and this Hamiltonian can be constructed using Pauli operators and implemented on a quantum computer.\n", + "\n", + "We choose a Hamiltonian corresponding to a chain of spins which can have any orientation in space, called a \"Heisenberg chain\". We assume that the $i^\\text{th}$ spin can be influenced by its nearest neighbors (the $(i-1)^\\text{th}$ and $(i+1)^\\text{th}$ spins) but not by more distant neighbors. We also allow for the possibility that the interaction between spins is different when the spins point along different axes. This asymmetry sometimes arises, for example, due to the structure of the crystal lattice into which spins are embedded." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "8b547f8a-df47-4e56-921b-3955eb7c19a9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('ZZIIIIIIII', 2), ('IZZIIIIIII', 2), ('IIZZIIIIII', 2), ('IIIZZIIIII', 2), ('IIIIZZIIII', 2), ('IIIIIZZIII', 2), ('IIIIIIZZII', 2), ('IIIIIIIZZI', 2), ('IIIIIIIIZZ', 2), ('XXIIIIIIII', 1), ('IXXIIIIIII', 1), ('IIXXIIIIII', 1), ('IIIXXIIIII', 1), ('IIIIXXIIII', 1), ('IIIIIXXIII', 1), ('IIIIIIXXII', 1), ('IIIIIIIXXI', 1), ('IIIIIIIIXX', 1), ('YYIIIIIIII', 3), ('IYYIIIIIII', 3), ('IIYYIIIIII', 3), ('IIIYYIIIII', 3), ('IIIIYYIIII', 3), ('IIIIIYYIII', 3), ('IIIIIIYYII', 3), ('IIIIIIIYYI', 3), ('IIIIIIIIYY', 3)]\n" + ] + } + ], + "source": [ + "# Define problem Hamiltonian.\n", + "n_qubits = 10\n", + "# coupling strength for XX, YY, and ZZ interactions\n", + "JX = 1\n", + "JY = 3\n", + "JZ = 2\n", + "\n", + "# Define the Hamiltonian:\n", + "H_int = [[\"I\"] * n_qubits for _ in range(3 * (n_qubits - 1))]\n", + "for i in range(n_qubits - 1):\n", + " H_int[i][i] = \"Z\"\n", + " H_int[i][i + 1] = \"Z\"\n", + "for i in range(n_qubits - 1):\n", + " H_int[n_qubits - 1 + i][i] = \"X\"\n", + " H_int[n_qubits - 1 + i][i + 1] = \"X\"\n", + "for i in range(n_qubits - 1):\n", + " H_int[2 * (n_qubits - 1) + i][i] = \"Y\"\n", + " H_int[2 * (n_qubits - 1) + i][i + 1] = \"Y\"\n", + "H_int = [\"\".join(term) for term in H_int]\n", + "H_tot = [\n", + " (term, JZ)\n", + " if term.count(\"Z\") == 2\n", + " else (term, JY)\n", + " if term.count(\"Y\") == 2\n", + " else (term, JX)\n", + " for term in H_int\n", + "]\n", + "\n", + "# Get operator\n", + "H_op = SparsePauliOp.from_list(H_tot)\n", + "print(H_tot)" + ] + }, + { + "cell_type": "markdown", + "id": "759b8b55-894b-4d3b-93de-5df4f63f9609", + "metadata": {}, + "source": [ + "The code below restricts the Hamiltonian to single particle states, and uses the spectral norm to set a good size for our time step $dt$. We heuristically choose a value for the time-step `dt` (based on upper bounds on the Hamiltonian norm). Ref [\\[9\\]](#references) showed that a sufficiently small timestep is $\\pi/\\vert \\vert H \\vert \\vert$, and that it is preferable up to a point to underestimate this value rather than overestimate, since overestimating can allow contributions from high-energy states to corrupt even the optimal state in the Krylov space. On the other hand, choosing $dt$ to be too small leads to worse conditioning of the Krylov subspace, since the Krylov basis vectors differ less from timestep to timestep." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "3dd96fbe-47bb-444b-b403-c67c2dcc9d07", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.17453292519943295)" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get Hamiltonian restricted to single-particle states\n", + "single_particle_H = np.zeros((n_qubits, n_qubits))\n", + "for i in range(n_qubits):\n", + " for j in range(i + 1):\n", + " for p, coeff in H_op.to_list():\n", + " p_x = Pauli(p).x\n", + " p_z = Pauli(p).z\n", + " if all(p_x[k] == ((i == k) + (j == k)) % 2 for k in range(n_qubits)):\n", + " sgn = ((-1j) ** sum(p_z[k] and p_x[k] for k in range(n_qubits))) * (\n", + " (-1) ** p_z[i]\n", + " )\n", + " else:\n", + " sgn = 0\n", + " single_particle_H[i, j] += sgn * coeff\n", + "for i in range(n_qubits):\n", + " for j in range(i + 1, n_qubits):\n", + " single_particle_H[i, j] = np.conj(single_particle_H[j, i])\n", + "\n", + "# Set dt according to spectral norm\n", + "dt = np.pi / np.linalg.norm(single_particle_H, ord=2)\n", + "dt" + ] + }, + { + "cell_type": "markdown", + "id": "cdb8c08b-caca-4bd5-ae93-065c42485e13", + "metadata": {}, + "source": [ + "We specify the number of Trotter steps to use in the time evolution. We also specify a maximum Krylov dimension of 4. This Krylov dimension is not large enough for realistic applications. But it is sufficient for this example. Furthermore, we will check for convergence at even smaller dimensions. We will explore methods in later lessons that allow us to scale and project our Hamiltonians onto larger subspaces." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "d95c6f17-7275-474a-b1e5-a41c4539ed83", + "metadata": {}, + "outputs": [], + "source": [ + "# Set parameters for quantum Krylov algorithm\n", + "krylov_dim = 4 # size of krylov subspace\n", + "num_trotter_steps = 4\n", + "dt_circ = dt / num_trotter_steps" + ] + }, + { + "cell_type": "markdown", + "id": "29291306-28a3-4c12-94ef-b3ffd5521c76", + "metadata": {}, + "source": [ + "#### State preparation\n", + "\n", + "Pick a reference state $\\vert \\psi \\rangle$ that has some overlap with the ground state. For this Hamiltonian, We use the a state with an excitation in the middle qubit $\\vert 00..010...00 \\rangle$ as our reference state." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "410192fb-8197-4860-8c3a-2e874e2f9c56", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc_state_prep = QuantumCircuit(n_qubits)\n", + "qc_state_prep.x(int(n_qubits / 2) + 1)\n", + "qc_state_prep.draw(\"mpl\", scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "dee3c383-fc2e-4420-8d04-861f5f4f4576", + "metadata": {}, + "source": [ + "#### Time evolution\n", + "\n", + "We can realize the time-evolution operator generated by a given Hamiltonian: $U=e^{-iHt}$ via the [Lie-Trotter approximation](/docs/api/qiskit/qiskit.synthesis.LieTrotter). For simplicity we use the built-in ```PauliEvolutionGate``` in the time-evolution circuit. The general syntax for this is this." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "4b3dddd0-391a-412a-9663-9e15a153125a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t = Parameter(\"t\")\n", + "\n", + "## Create the time-evo op circuit\n", + "evol_gate = PauliEvolutionGate(\n", + " H_op, time=t, synthesis=LieTrotter(reps=num_trotter_steps)\n", + ")\n", + "\n", + "qr = QuantumRegister(n_qubits)\n", + "qc_evol = QuantumCircuit(qr)\n", + "qc_evol.append(evol_gate, qargs=qr)" + ] + }, + { + "cell_type": "markdown", + "id": "295bf203-2bc0-4c02-912b-423e5ca392bb", + "metadata": {}, + "source": [ + "We will use a version of this below in the Hadamard test, but stepping forward for times $dt$." + ] + }, + { + "cell_type": "markdown", + "id": "03f71981-3a22-4ba6-8281-7b20895dbda3", + "metadata": {}, + "source": [ + "#### Hadamard test\n", + "\n", + "Recall that we wish to calculate the matrix elements of both $\\tilde{H}$ and the Gram matrix $\\tilde{S}$ using the Hadamard test. Let's review how this works in this context, focusing first on the construction of $\\tilde{H}.$ The overall process is depicted graphically below. The layers of colored state preparation blocks $\\text{Prep}|\\psi_i\\rangle$ serve as a reminder that this process is carried out for all combinations of $|\\psi_i\\rangle$ and $|\\psi_j\\rangle$ in our subspace.\n", + "\n", + "![An image of a quantum circuit diagram with many layers indicating that the circuit must be evaluated for many different states in order to perform the Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig5.avif)\n", + "\n", + "The states of the system at the steps indicated are:\n", + "$$\n", + "\\begin{aligned}\n", + " \\text{Step 0:}\\qquad|\\Psi\\rangle & = |0\\rangle|0\\rangle^N \\\\\n", + " \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle + |1\\rangle \\Big)|0\\rangle^N \\\\\n", + " \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle|0\\rangle^N+|1\\rangle |\\psi_i\\rangle\\Big)\\\\\n", + " \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |0\\rangle^N+|1\\rangle P |\\psi_i\\rangle\\Big) \\\\\n", + " \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\Big(|0\\rangle |\\psi_j\\rangle+|1\\rangle P|\\psi_i\\rangle\\Big)\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Here $P$ is a Pauli term in the decomposition of the Hamiltonian (note that it cannot be a linear combination of multiple commuting Pauli terms since that would not be unitary -- grouping is possible using a different construction we will show later) $\\text{Prep} \\; \\psi_i$, $\\text{Prep} \\; \\psi_j$ are controlled operations that prepare $|\\psi_i\\rangle$, $|\\psi_j\\rangle$ vectors of the unitary Krylov space, with $|\\psi_k\\rangle = e^{-i H k dt } \\vert \\psi \\rangle = e^{-i H k dt } U_{\\psi} \\vert 0 \\rangle^N$. Applying measurements of $X$ and $Y$ to this circuit calculates the real and imaginary parts, respectively, of the matrix elements we require.\n", + "\n", + "Starting from Step 4 above, apply the Hadamard gate $H$ to the zeroth qubit.\n", + "\n", + "$$\n", + "\\begin{equation*}\n", + " |\\Psi\\rangle \\longrightarrow\\quad\\frac{1}{2}|0\\rangle\\Big( |\\psi_j\\rangle + P|\\psi_i\\rangle\\Big) + \\frac{1}{2}|1\\rangle\\Big(|\\psi_j\\rangle - P|\\psi_i\\rangle\\Big)\n", + "\\end{equation*}\n", + "$$\n", + "\n", + "Then measure either $X$ or $Y$.\n", + "\n", + "$$\n", + "\\begin{equation*}\n", + "\\begin{split}\n", + " \\Rightarrow\\quad\\langle X\\rangle &= \\frac{1}{4}\\Bigg(\\Big\\|| \\psi_j\\rangle + P|\\psi_i\\rangle \\Big\\|^2-\\Big\\||\\psi_j\\rangle - P|\\psi_i\\rangle\\Big\\|^2\\Bigg) \\\\\n", + " &= \\text{Re}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n", + "\\end{split}\n", + "\\end{equation*}\n", + "$$\n", + "\n", + "From the identity $|a + b\\|^2 = \\langle a + b | a + b \\rangle = \\|a\\|^2 + \\|b\\|^2 + 2\\text{Re}\\langle a | b \\rangle$. Similarly, measuring $Y$ yields\n", + "\n", + "$$\n", + "\\begin{equation*}\n", + " \\langle Y\\rangle = \\text{Im}\\Big[\\langle\\psi_j| P|\\psi_i\\rangle\\Big].\n", + "\\end{equation*}\n", + "$$\n", + "\n", + "Adding these steps to the time-evolution we set up previously we write the following." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "ac536079-96f2-435b-a15f-72c207fa24d1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Circuit for calculating the real part of the overlap in S via Hadamard test\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Create the time-evo op circuit\n", + "evol_gate = PauliEvolutionGate(\n", + " H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n", + ")\n", + "\n", + "## Create the time-evo op dagger circuit\n", + "evol_gate_d = PauliEvolutionGate(\n", + " H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)\n", + ")\n", + "evol_gate_d = evol_gate_d.inverse()\n", + "\n", + "# Put pieces together\n", + "qc_reg = QuantumRegister(n_qubits)\n", + "qc_temp = QuantumCircuit(qc_reg)\n", + "qc_temp.compose(qc_state_prep, inplace=True)\n", + "for _ in range(num_trotter_steps):\n", + " qc_temp.append(evol_gate, qargs=qc_reg)\n", + "for _ in range(num_trotter_steps):\n", + " qc_temp.append(evol_gate_d, qargs=qc_reg)\n", + "qc_temp.compose(qc_state_prep.inverse(), inplace=True)\n", + "\n", + "# Create controlled version of the circuit\n", + "controlled_U = qc_temp.to_gate().control(1)\n", + "\n", + "# Create hadamard test circuit for real part\n", + "qr = QuantumRegister(n_qubits + 1)\n", + "qc_real = QuantumCircuit(qr)\n", + "qc_real.h(0)\n", + "qc_real.append(controlled_U, list(range(n_qubits + 1)))\n", + "qc_real.h(0)\n", + "\n", + "print(\"Circuit for calculating the real part of the overlap in S via Hadamard test\")\n", + "qc_real.draw(\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "b88673a4-859a-44f7-ac0c-38ecf17faf36", + "metadata": {}, + "source": [ + "We warned already about the depth involved in Trotter circuits. Performing the Hadamard test in these conditions can yield an even deeper circuit, especially once we decompose to native gates. This will increase even more if we account for the topology of the device. So before using any time on the quantum computer, it is a good idea to check the 2-qubit depth of our circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "f40d206a-9bd5-4355-8007-cecff21f7fbe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of layers of 2Q operations 14401\n" + ] + } + ], + "source": [ + "print(\n", + " \"Number of layers of 2Q operations\",\n", + " qc_real.decompose(reps=2).depth(lambda x: x[0].num_qubits == 2),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "bbd4f53b-8946-4713-89c2-e6bbbfa80609", + "metadata": {}, + "source": [ + "A circuit of this depth cannot return usable results on modern quantum computers. If we are to construct $\\tilde{H}$ and $\\tilde{S},$ we need a better way. This is the reason for the efficient Hadamard test introduced below." + ] + }, + { + "cell_type": "markdown", + "id": "6cda7f15-7c76-40aa-bf2e-bfea141c8474", + "metadata": {}, + "source": [ + "### 4. 2 Step 2. Optimize circuits and operators for target hardware\n", + "\n", + "#### Efficient Hadamard test\n", + "\n", + "We can optimize the deep circuits for the Hadamard test that we have obtained by introducing some approximations and relying on some assumption about the model Hamiltonian. For example, consider the following circuit for the Hadamard test:\n", + "\n", + "![An image of a quantum circuit diagram with many layers indicating that the circuit must be evaluated for many different unitary operators in order to perform the modified, efficient Hadamard test.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig6.avif)\n", + "\n", + "Assume we can classically calculate $E_0$, the eigenvalue of $|0\\rangle^N$ under the Hamiltonian $H$.\n", + "This is satisfied when the Hamiltonian preserves the U(1) symmetry. Although this may seem like a strong assumption, there are many cases where it is safe to assume that there is a vacuum state (in this case it maps to the $|0\\rangle^N$ state) which is unaffected by the action of the Hamiltonian. This is true for example for chemistry Hamiltonians that describe stable molecule (where the number of electrons is conserved).\n", + "Given that the gate $\\text{Prep} \\; \\psi_0$, prepares the desired reference state $\\ket{\\psi_0} = \\text{Prep} \\; \\psi_0 \\ket{0} = e^{-i H 0 dt} U_{\\psi_0} \\ket{0}$, for example, to prepare the HF state for chemistry $\\text{Prep} \\; \\psi_0$ would be a product of single-qubit NOTs, so controlled-$\\text{Prep} \\; \\psi_0$ is just a product of CNOTs.\n", + "Then the circuit above implements the following state prior to measurement:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + " \\text{Step 0:}\\qquad|\\Psi\\rangle & = \\ket{0} \\ket{0}^{N}\\\\\n", + " \\text{Step 1:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(\\ket{0}\\ket{0}^N+ \\ket{1} \\ket{0}^N\\right)\\\\\n", + " \\text{Step 2:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle|0\\rangle^N+|1\\rangle|\\psi_0\\rangle\\right)\\\\\n", + " \\text{Step 3:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0}\\ket{0}^N+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n", + " \\text{Step 4:}\\qquad|\\Psi\\rangle & = \\frac{1}{\\sqrt{2}}\\left(e^{i\\phi}\\ket{0} \\ket{\\psi_0}+\\ket{1} U\\ket{\\psi_0}\\right)\\\\\n", + " & = \\frac{1}{2}\\left(\\ket{+}\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)+\\ket{-}\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\\right)\\\\\n", + " & = \\frac{1}{2}\\left(\\ket{+i}\\left(e^{i\\phi}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)+\\ket{-i}\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\\right)\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "where we have used the classical simulable phase shift $ U\\ket{0}^N = e^{i\\phi}\\ket{0}^N$ from step 2 to 3. Therefore the expectation values are\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + " \\langle X\\otimes P\\rangle&=\\frac{1}{4}\n", + " \\Big(\n", + " \\left(e^{-i\\phi}\\bra{\\psi_0}+\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+U\\ket{\\psi_0}\\right)\n", + " \\\\\n", + " &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}-U\\ket{\\psi_0}\\right)\n", + " \\Big)\\\\\n", + " &=\\text{Re}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right],\n", + "\\end{aligned}\n", + "\n", + "$$\n", + "\n", + "$$\n", + "\n", + "\\begin{aligned}\n", + " \\langle Y\\otimes P\\rangle&=\\frac{1}{4}\n", + " \\Big(\n", + " \\left(e^{-i\\phi}\\bra{\\psi_0}+i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi_0}\\ket{\\psi_0}-iU\\ket{\\psi_0}\\right)\n", + " \\\\\n", + " &\\qquad-\\left(e^{-i\\phi}\\bra{\\psi_0}-i\\bra{\\psi_0}U^\\dagger\\right)P\\left(e^{i\\phi}\\ket{\\psi_0}+iU\\ket{\\psi_0}\\right)\n", + " \\Big)\\\\\n", + " &=\\text{Im}\\left[e^{-i\\phi}\\bra{\\psi_0}PU\\ket{\\psi_0}\\right].\n", + "\\end{aligned}\n", + "\n", + "$$\n", + "\n", + "Using these assumptions we were able to write the expectation values of operators of interest with fewer controlled operations. In fact, we only need to implement the controlled state preparation $\\text{Prep} \\; \\psi_0$ and not controlled time evolutions. Reframing our calculation as above will allow us to greatly reduce the depth of the resulting circuits.\n", + "\n", + "Note that as a bonus, since the Pauli operator now appears as a measurement at the end of the circuit rather than as a controlled gate in the middle, it can be measured alongside other commuting Pauli operators as in the decomposition $$H=\\sum_{\\alpha = 1}^{N_\\text{GCP}}c_\\alpha P_\\alpha $$ given above." + ] + }, + { + "cell_type": "markdown", + "id": "cd24795e-3ef0-4629-87bf-8e04c471d665", + "metadata": {}, + "source": [ + "### Decompose time-evolution operator with Trotter decomposition\n", + "\n", + "Instead of implementing the time-evolution operator exactly we can use the Trotter decomposition to implement an approximation of it. Repeating several times a certain order Trotter decomposition gives us further reduction of the error introduced from the approximation. In the following, we directly build the Trotter implementation in the most efficient way for the interaction graph of the Hamiltonian we are considering (nearest neighbor interactions only). In practice we insert Pauli rotations $R_{xx}$, $R_{yy}$, $R_{zz}$ with coupling strengths $J_x,$ $J_y,$ and $J_z$ and a parametrized angle $t$, which correspond to the approximate implementation of $e^{-i (J_x XX + J_y YY + J_z ZZ) t}$. Given the difference in definition of the Pauli rotations and the time-evolution that we are trying to implement, we'll have to use the parameter $2*dt$ to achieve a time-evolution of $dt$. Furthermore, we reverse the order of the operations for odd number of repetitions of the Trotter steps, which is functionally equivalent but allows for synthesizing adjacent operations in a single $SU(2)$ unitary. This gives a much shallower circuit than what is obtained using the generic `PauliEvolutionGate()` functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d35e6cd-c0f6-4871-83da-86472d66be2c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t = Parameter(\"t\")\n", + "\n", + "# Create instruction for rotation about XX+YY-ZZ:\n", + "Rxyz_circ = QuantumCircuit(2)\n", + "Rxyz_circ.rxx(2 * JX * t, 0, 1)\n", + "Rxyz_circ.ryy(2 * JY * t, 0, 1)\n", + "Rxyz_circ.rzz(2 * JZ * t, 0, 1)\n", + "Rxyz_instr = Rxyz_circ.to_instruction(label=\"R J_x XX + J_y YY + J_z ZZ\")\n", + "\n", + "interaction_list = [\n", + " [[i, i + 1] for i in range(0, n_qubits - 1, 2)],\n", + " [[i, i + 1] for i in range(1, n_qubits - 1, 2)],\n", + "] # linear chain\n", + "\n", + "qr = QuantumRegister(n_qubits)\n", + "trotter_step_circ = QuantumCircuit(qr)\n", + "for i, color in enumerate(interaction_list):\n", + " for interaction in color:\n", + " trotter_step_circ.append(Rxyz_instr, interaction)\n", + " if i < len(interaction_list) - 1:\n", + " trotter_step_circ.barrier()\n", + "reverse_trotter_step_circ = trotter_step_circ.reverse_ops()\n", + "\n", + "qc_evol = QuantumCircuit(qr)\n", + "for step in range(num_trotter_steps):\n", + " if step % 2 == 0:\n", + " qc_evol = qc_evol.compose(trotter_step_circ)\n", + " else:\n", + " qc_evol = qc_evol.compose(reverse_trotter_step_circ)\n", + "\n", + "qc_evol.decompose().draw(\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "2a3d8d4a-fc0f-4702-ac69-67f63d0c0e30", + "metadata": {}, + "source": [ + "We prepare an initial state again for this efficient Hadamard test." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "d1d0b9de-65d4-4a46-975d-6cfaaac05f9a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "control = 0\n", + "excitation = int(n_qubits / 2) + 1\n", + "controlled_state_prep = QuantumCircuit(n_qubits + 1)\n", + "controlled_state_prep.cx(control, excitation)\n", + "controlled_state_prep.draw(\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "415e94f8-6594-42ab-b55b-80a34852b943", + "metadata": {}, + "source": [ + "#### Template circuits for calculating matrix elements of $\\tilde{S}$ and $\\tilde{H}$ via Hadamard test\n", + "\n", + "The only difference between the circuits used in the Hadamard test will be the phase in the time-evolution operator and the observables measured. Therefore we can prepare a template circuit which represent the generic circuit for the Hadamard test, with placeholders for the gates that depend on the time-evolution operator." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "27a54efa-affb-41bc-8523-822f8d92d34f", + "metadata": {}, + "outputs": [], + "source": [ + "# Parameters for the template circuits\n", + "parameters = []\n", + "for idx in range(1, krylov_dim):\n", + " parameters.append(dt_circ * (idx))" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "37382668-1999-4475-b50f-2887449d5c93", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create modified hadamard test circuit\n", + "qr = QuantumRegister(n_qubits + 1)\n", + "qc = QuantumCircuit(qr)\n", + "qc.h(0)\n", + "qc.compose(controlled_state_prep, list(range(n_qubits + 1)), inplace=True)\n", + "qc.barrier()\n", + "qc.compose(qc_evol, list(range(1, n_qubits + 1)), inplace=True)\n", + "qc.barrier()\n", + "qc.x(0)\n", + "qc.compose(controlled_state_prep.inverse(), list(range(n_qubits + 1)), inplace=True)\n", + "qc.x(0)\n", + "\n", + "qc.decompose().draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "4ad71f04-ec89-473b-9b7a-db4d3936c85e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The optimized circuit has 2Q gates depth: 50\n" + ] + } + ], + "source": [ + "print(\n", + " \"The optimized circuit has 2Q gates depth: \",\n", + " qc.decompose().decompose().depth(lambda x: x[0].num_qubits == 2),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6f60d587-8ae1-4daa-ab76-a032c2221ab7", + "metadata": {}, + "source": [ + "This depth is substantially reduced compared to the original Hadamard test. This depth is manageable on modern quantum computers, though it is still quite high. We will need to use state-of-the-art error mitigation to obtain useful results.\n", + "\n", + "Select a backend on which to run our quantum Krylov calculation, so that we can transpile our circuit for running on that quantum computer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5683752b-d520-4cad-9fe9-7ebb7f495ba0", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "# Or you may choose a specify backend and channel if necessary for your workflow.\n", + "# service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", + "# backend = service.backend(\"ibm_fez\")" + ] + }, + { + "cell_type": "markdown", + "id": "0337f370-edff-4dcb-9570-26818ee51d3a", + "metadata": {}, + "source": [ + "We now transpile our circuits and operators." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "8a7c9ea1-5bc3-40b7-aa55-040cbb5f8c28", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "basis_gates = list(target.operation_names)\n", + "pm = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, basis_gates=basis_gates\n", + ")\n", + "\n", + "qc_trans = pm.run(qc)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "054bcfba-fde3-4213-bbd7-d7a6dd6c03c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "36\n", + "OrderedDict([('rz', 410), ('sx', 361), ('cz', 156), ('x', 18), ('barrier', 6)])\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(qc_trans.depth(lambda x: x[0].num_qubits == 2))\n", + "print(qc_trans.count_ops())\n", + "qc_trans.draw(\"mpl\", fold=-1, idle_wires=False, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "5395cc4a-ce59-4f8f-a126-1f8ca5507f83", + "metadata": {}, + "source": [ + "After optimization, our transpiled two-qubit depth is further reduced.\n", + "\n", + "### 4.3 Step 3. Execute using a Qiskit Runtime primitive\n", + "\n", + "We now create PUBs for execution with Estimator." + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "5d949e77-d7af-47aa-91e1-40fcf5940996", + "metadata": {}, + "outputs": [], + "source": [ + "# Define observables to measure for S\n", + "observable_S_real = \"I\" * (n_qubits) + \"X\"\n", + "observable_S_imag = \"I\" * (n_qubits) + \"Y\"\n", + "\n", + "observable_op_real = SparsePauliOp(\n", + " observable_S_real\n", + ") # define a sparse pauli operator for the observable\n", + "observable_op_imag = SparsePauliOp(observable_S_imag)\n", + "\n", + "layout = qc_trans.layout # get layout of transpiled circuit\n", + "observable_op_real = observable_op_real.apply_layout(\n", + " layout\n", + ") # apply physical layout to the observable\n", + "observable_op_imag = observable_op_imag.apply_layout(layout)\n", + "observable_S_real = (\n", + " observable_op_real.paulis.to_labels()\n", + ") # get the label of the physical observable\n", + "observable_S_imag = observable_op_imag.paulis.to_labels()\n", + "\n", + "observables_S = [[observable_S_real], [observable_S_imag]]\n", + "\n", + "\n", + "# Define observables to measure for H\n", + "# Hamiltonian terms to measure\n", + "observable_list = []\n", + "for pauli, coeff in zip(H_op.paulis, H_op.coeffs):\n", + " # print(pauli)\n", + " observable_H_real = pauli[::-1].to_label() + \"X\"\n", + " observable_H_imag = pauli[::-1].to_label() + \"Y\"\n", + " observable_list.append([observable_H_real])\n", + " observable_list.append([observable_H_imag])\n", + "\n", + "layout = qc_trans.layout\n", + "\n", + "observable_trans_list = []\n", + "for observable in observable_list:\n", + " observable_op = SparsePauliOp(observable)\n", + " observable_op = observable_op.apply_layout(layout)\n", + " observable_trans_list.append([observable_op.paulis.to_labels()])\n", + "\n", + "observables_H = observable_trans_list\n", + "\n", + "\n", + "# Define a sweep over parameter values\n", + "params = np.vstack(parameters).T\n", + "\n", + "\n", + "# Estimate the expectation value for all combinations of\n", + "# observables and parameter values, where the pub result will have\n", + "# shape (# observables, # parameter values).\n", + "pub = (qc_trans, observables_S + observables_H, params)" + ] + }, + { + "cell_type": "markdown", + "id": "e0f30365-57d2-4ece-ba62-4e534a810de6", + "metadata": {}, + "source": [ + "Circuits for $t=0$ are classically calculable. We carry this out before moving on to the $t\\neq 0$ case using a quantum computer." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "298c43a4-e54c-4ac3-95bd-12f535d44790", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(10+0j)\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import StabilizerState, Pauli\n", + "\n", + "\n", + "qc_cliff = qc.assign_parameters({t: 0})\n", + "\n", + "\n", + "# Get expectation values from experiment\n", + "S_expval_real = StabilizerState(qc_cliff).expectation_value(\n", + " Pauli(\"I\" * (n_qubits) + \"X\")\n", + ")\n", + "S_expval_imag = StabilizerState(qc_cliff).expectation_value(\n", + " Pauli(\"I\" * (n_qubits) + \"Y\")\n", + ")\n", + "\n", + "# Get expectation values\n", + "S_expval = S_expval_real + 1j * S_expval_imag\n", + "\n", + "H_expval = 0\n", + "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n", + " # Get expectation values from experiment\n", + " expval_real = StabilizerState(qc_cliff).expectation_value(\n", + " Pauli(pauli[::-1].to_label() + \"X\")\n", + " )\n", + " expval_imag = StabilizerState(qc_cliff).expectation_value(\n", + " Pauli(pauli[::-1].to_label() + \"Y\")\n", + " )\n", + " expval = expval_real + 1j * expval_imag\n", + "\n", + " # Fill-in matrix elements\n", + " H_expval += coeff * expval\n", + "\n", + "\n", + "print(H_expval)" + ] + }, + { + "cell_type": "markdown", + "id": "9b86af1c-7e37-427d-b4f0-fbca38d45a21", + "metadata": {}, + "source": [ + "Although we were able to reduce our gate depth by orders of magnitude using the efficient Hadamard test, the depth is still sufficient to require state-of-the-art error mitigation. Below, we specify attributes of the mitigation being used. All of the methods used are important, but it is worth called out [probabilistic error amplification (PEA)](/docs/guides/error-mitigation-and-suppression-techniques#probabilistic-error-amplification-pea) specifically. This powerful technique comes with a great deal of quantum overhead. The calculation done here can take 20 minutes or more to run on a real quantum computer. You may wish to play with the parameters below to increase or decrease precision and consequentially overhead. The default settings below yield high-fidelity results." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "id": "2dff41e1-d417-4af7-aa6c-537a9b0e0c7c", + "metadata": {}, + "outputs": [], + "source": [ + "# Experiment options\n", + "num_randomizations = 300\n", + "num_randomizations_learning = 20\n", + "max_batch_circuits = 20\n", + "shots_per_randomization = 100\n", + "learning_pair_depths = [0, 4, 24]\n", + "noise_factors = [1, 1.3, 1.6]\n", + "\n", + "# Base option formatting\n", + "options = {\n", + " # Builtin resilience settings for ZNE\n", + " \"resilience\": {\n", + " \"measure_mitigation\": True,\n", + " \"zne_mitigation\": True,\n", + " \"zne\": {\"noise_factors\": noise_factors},\n", + " # TREX noise learning configuration\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": num_randomizations_learning,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " },\n", + " # PEA noise model configuration\n", + " \"layer_noise_learning\": {\n", + " \"max_layers_to_learn\": 10,\n", + " \"layer_pair_depths\": learning_pair_depths,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"num_randomizations\": num_randomizations_learning,\n", + " },\n", + " },\n", + " # Randomization configuration\n", + " \"twirling\": {\n", + " \"num_randomizations\": num_randomizations,\n", + " \"shots_per_randomization\": shots_per_randomization,\n", + " \"strategy\": \"all\",\n", + " },\n", + " # Experimental settings for PEA method\n", + " \"experimental\": {\n", + " # # Just in case, disable any further qiskit transpilation not related to twirling / DD\n", + " # \"skip_transpilation\": True,\n", + " # Execution configuration\n", + " \"execution\": {\n", + " \"max_pubs_per_batch_job\": max_batch_circuits,\n", + " \"fast_parametric_update\": True,\n", + " },\n", + " # Error Mitigation configuration\n", + " \"resilience\": {\n", + " # ZNE Configuration\n", + " \"zne\": {\n", + " \"amplifier\": \"pea\",\n", + " \"return_all_extrapolated\": True,\n", + " \"return_unextrapolated\": True,\n", + " \"extrapolated_noise_factors\": [0] + noise_factors,\n", + " }\n", + " },\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "5932dc29-7905-4f54-9c97-2bb0fc9c1e39", + "metadata": {}, + "source": [ + "Finally, we execute the circuits for $\\tilde{S}$ and $\\tilde{H}$ with Estimator." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "01a41068-453f-4ff6-9664-c73be1965dc7", + "metadata": {}, + "outputs": [], + "source": [ + "# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate.\n", + "# Your execution time may vary.\n", + "\n", + "with Batch(backend=backend) as batch:\n", + " # Estimator\n", + " estimator = Estimator(mode=batch, options=options)\n", + "\n", + " job = estimator.run([pub], precision=1)" + ] + }, + { + "cell_type": "markdown", + "id": "936865a2-828d-4a45-987e-b62cce0535da", + "metadata": {}, + "source": [ + "### 4.4 Step 4. Post-process and analyze results\n", + "\n", + "What we have obtained from the quantum computer are the individual matrix elements of $\\tilde{S}$ and the commuting Pauli groups that make up the matrix elements of $\\tilde{H}$. These terms must be combined to recover our matrices, so that we can solve the generalized eigenvalue problem." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "28ed9319-dd36-4104-aba0-f8798cbbd2b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Store the outputs as 'results'.\n", + "results = job.result()[0]" + ] + }, + { + "cell_type": "markdown", + "id": "1d3c0af8-fb06-415c-b591-f8e8faedc070", + "metadata": {}, + "source": [ + "#### Calculate Effective Hamiltonian and Overlap matrices\n", + "\n", + "First calculate the phase accumulated by the $\\vert 0 \\rangle$ state during the uncontrolled time evolution" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "d36e8b32-621d-44da-808d-297c0b60754a", + "metadata": {}, + "outputs": [], + "source": [ + "prefactors = [\n", + " np.exp(-1j * sum([c for p, c in H_op.to_list() if \"Z\" in p]) * i * dt)\n", + " for i in range(1, krylov_dim)\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "f20402f5-1264-4eeb-9d41-8dd6e53e82d3", + "metadata": {}, + "source": [ + "Once we have the results of the circuit executions we can post-process the data to calculate the matrix elements of $S$" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "d1ff8971-2f80-4130-b7f2-e95c3a1b476d", + "metadata": {}, + "outputs": [], + "source": [ + "# Assemble S, the overlap matrix of dimension D:\n", + "S_first_row = np.zeros(krylov_dim, dtype=complex)\n", + "S_first_row[0] = 1 + 0j\n", + "\n", + "# Add in ancilla-only measurements:\n", + "for i in range(krylov_dim - 1):\n", + " # Get expectation values from experiment\n", + " expval_real = results.data.evs[0][0][i] # automatic extrapolated evs if ZNE is used\n", + " expval_imag = results.data.evs[1][0][i] # automatic extrapolated evs if ZNE is used\n", + "\n", + " # Get expectation values\n", + " expval = expval_real + 1j * expval_imag\n", + " S_first_row[i + 1] += prefactors[i] * expval\n", + "\n", + "S_first_row_list = S_first_row.tolist() # for saving purposes\n", + "\n", + "\n", + "S_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n", + "\n", + "# Distribute entries from first row across matrix:\n", + "for i, j in it.product(range(krylov_dim), repeat=2):\n", + " if i >= j:\n", + " S_circ[j, i] = S_first_row[i - j]\n", + " else:\n", + " S_circ[j, i] = np.conj(S_first_row[j - i])" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "id": "bfe2a427-7ec6-48de-9321-fa6bf2dc7c94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\n", + "\\displaystyle \\left[\\begin{matrix}1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i & 0.0940509850777074 - 0.094154537369141 i\\\\0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i & 0.185815978760175 - 0.0910521940394691 i\\\\0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0 & 0.149322296177984 - 0.283023058106896 i\\\\0.0940509850777074 + 0.094154537369141 i & 0.185815978760175 + 0.0910521940394691 i & 0.149322296177984 + 0.283023058106896 i & 1.0\\end{matrix}\\right]\n", + "$$" + ], + "text/plain": [ + "Matrix([\n", + "[ 1.0, 0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I, 0.0940509850777074 - 0.094154537369141*I],\n", + "[ 0.149322296177984 + 0.283023058106896*I, 1.0, 0.149322296177984 - 0.283023058106896*I, 0.185815978760175 - 0.0910521940394691*I],\n", + "[0.185815978760175 + 0.0910521940394691*I, 0.149322296177984 + 0.283023058106896*I, 1.0, 0.149322296177984 - 0.283023058106896*I],\n", + "[0.0940509850777074 + 0.094154537369141*I, 0.185815978760175 + 0.0910521940394691*I, 0.149322296177984 + 0.283023058106896*I, 1.0]])" + ] + }, + "execution_count": 89, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sympy import Matrix\n", + "\n", + "Matrix(S_circ)" + ] + }, + { + "cell_type": "markdown", + "id": "2a863b6f-183f-4d7a-bac8-399da3e3578e", + "metadata": {}, + "source": [ + "And the matrix elements of $\\tilde{H}$" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "c551fdd0-91ef-4531-83d9-ed399b423bb9", + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "\n", + "# Assemble S, the overlap matrix of dimension D:\n", + "H_first_row = np.zeros(krylov_dim, dtype=complex)\n", + "H_first_row[0] = H_expval\n", + "\n", + "for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):\n", + " # Add in ancilla-only measurements:\n", + " for i in range(krylov_dim - 1):\n", + " # Get expectation values from experiment\n", + " expval_real = results.data.evs[2 + 2 * obs_idx][0][\n", + " i\n", + " ] # automatic extrapolated evs if ZNE is used\n", + " expval_imag = results.data.evs[2 + 2 * obs_idx + 1][0][\n", + " i\n", + " ] # automatic extrapolated evs if ZNE is used\n", + "\n", + " # Get expectation values\n", + " expval = expval_real + 1j * expval_imag\n", + " H_first_row[i + 1] += prefactors[i] * coeff * expval\n", + "\n", + "H_first_row_list = H_first_row.tolist()\n", + "\n", + "H_eff_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)\n", + "\n", + "# Distribute entries from first row across matrix:\n", + "for i, j in itertools.product(range(krylov_dim), repeat=2):\n", + " if i >= j:\n", + " H_eff_circ[j, i] = H_first_row[i - j]\n", + " else:\n", + " H_eff_circ[j, i] = np.conj(H_first_row[j - i])" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "id": "d1950927-74d6-4012-81f2-3d0b7f476de8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\n", + "\\displaystyle \\left[\\begin{matrix}10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i & 1.0770511571923 + 0.104340737159455 i\\\\-3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i & 0.496487054782717 + 0.188101957039621 i\\\\0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0 & -3.02044405310714 - 2.80721615865252 i\\\\1.0770511571923 - 0.104340737159455 i & 0.496487054782717 - 0.188101957039621 i & -3.02044405310714 + 2.80721615865252 i & 10.0\\end{matrix}\\right]\n", + "$$" + ], + "text/plain": [ + "Matrix([\n", + "[ 10.0, -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I, 1.0770511571923 + 0.104340737159455*I],\n", + "[ -3.02044405310714 + 2.80721615865252*I, 10.0, -3.02044405310714 - 2.80721615865252*I, 0.496487054782717 + 0.188101957039621*I],\n", + "[0.496487054782717 - 0.188101957039621*I, -3.02044405310714 + 2.80721615865252*I, 10.0, -3.02044405310714 - 2.80721615865252*I],\n", + "[ 1.0770511571923 - 0.104340737159455*I, 0.496487054782717 - 0.188101957039621*I, -3.02044405310714 + 2.80721615865252*I, 10.0]])" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sympy import Matrix\n", + "\n", + "Matrix(H_eff_circ)" + ] + }, + { + "cell_type": "markdown", + "id": "7ec60832-f7be-447f-841e-e801241fdeae", + "metadata": {}, + "source": [ + "Finally, we can solve the generalized eigenvalue problem for $\\tilde{H}$:\n", + "\n", + "$$\\tilde{H} \\vec{c} = c S \\vec{c}$$\n", + "\n", + "and get an estimate of the ground state energy $c_{min}$" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "id": "d6635506-399f-47a3-a837-c5f2558f22b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The estimated ground state energy is: 10.0\n", + "The estimated ground state energy is: 5.933953916292923\n", + "The estimated ground state energy is: 4.4101773995740645\n", + "The estimated ground state energy is: 3.921288588521255\n" + ] + } + ], + "source": [ + "gnd_en_circ_est_list = []\n", + "for d in range(1, krylov_dim + 1):\n", + " # Solve generalized eigenvalue problem\n", + " gnd_en_circ_est = solve_regularized_gen_eig(\n", + " H_eff_circ[:d, :d], S_circ[:d, :d], threshold=1e-1\n", + " )\n", + " gnd_en_circ_est_list.append(gnd_en_circ_est)\n", + " print(\"The estimated ground state energy is: \", gnd_en_circ_est)" + ] + }, + { + "cell_type": "markdown", + "id": "15865587-85f4-41bc-9bf5-47464cf17298", + "metadata": {}, + "source": [ + "For a single-particle sector, we can efficiently calculate the ground state of this sector of the Hamiltonian classically" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "id": "3303ae29-c288-417a-9cf2-c82d9770de69", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n_sys_qubits 10\n", + "n_exc 1 , subspace dimension 11\n", + "single particle ground state energy: 2.391547869638771\n" + ] + } + ], + "source": [ + "gs_en = single_particle_gs(H_op, n_qubits)" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "2f904ea3-38bc-4841-81ce-cdb69f09a0b7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "27" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(H_op)" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "a5d4a983-1a30-4cea-b695-3e6a67338633", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(\n", + " range(1, krylov_dim + 1),\n", + " gnd_en_circ_est_list,\n", + " color=\"blue\",\n", + " linestyle=\"-.\",\n", + " label=\"KQD estimate\",\n", + ")\n", + "plt.plot(\n", + " range(1, krylov_dim + 1),\n", + " [gs_en] * krylov_dim,\n", + " color=\"red\",\n", + " linestyle=\"-\",\n", + " label=\"exact\",\n", + ")\n", + "plt.xticks(range(1, krylov_dim + 1), range(1, krylov_dim + 1))\n", + "plt.legend()\n", + "plt.xlabel(\"Krylov space dimension\")\n", + "plt.ylabel(\"Energy\")\n", + "plt.title(\"Estimating Ground state energy with Krylov Quantum Diagonalization\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "543a2d98-1f72-4df1-ae22-f65d60c0d9c5", + "metadata": {}, + "source": [ + "## 5. Discussion and extension\n", + "\n", + "To recap, we start with a reference state, then evolve it for different periods of time to generate the unitary Krylov subspace. We project our Hamiltonian onto that subspace. We also estimate the overlaps of the subspace vectors. Finally we solve the lower-dimensional, generalized eigenvalue problem classically.\n", + "\n", + "![A flow-chart overview of QKD: start with a reference state, evolve the state to approximate Krylov vectors, project into the Krylov subspace, diagonalize the projected subspace classically, and determine ground state properties.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig7.avif)\n", + "\n", + "Let’s compare what determines computational costs of using the Krylov technique classically and quantum mechanically. There are not perfect analogs between classical and quantum approaches for all steps. This table collects some scaling of different steps for consideration.\n", + "\n", + "![A table describing scaling of different processes classically and in the quantum approach to Krylov methods. Some quantum steps have no analog. The scalings are the same as those stated in text.](/learning/images/courses/quantum-diagonalization-algorithms/krylov/kqd-fig8.avif)\n", + "\n", + "Recall that Hamiltonians generally have terms that cannot be simultaneously measured (because they do not commute with on another). We sort terms in the Hamiltonian into groups of commuting Pauli operators that can all be measured simultaneously, and we may require many such groups to account for all the terms that do not commute with one another. To build up $\\tilde{H}$ on a quantum computer requires separate measurements for each group of commuting Pauli strings in the Hamiltonian, and each of those requires many shots. We must do this for $r^2$ different matrix elements, corresponding to $r^2$ combinations of different time evolution factors. There are sometimes ways to reduce this, but in this rough treatment, the time required for this scales like $N_\\text{shots}\\times N_\\text{GCP} \\times r^2.$ The elements of $S$ must be estimated, which scales like $O(N_\\text{shots}\\times r^2)$. Finally, solving the generalized eigenvalue problem in the projected space, classically, takes $O(r^3).$\n", + "\n", + "We see that quantum Krylov diagonalization may be useful in cases where the number of commuting Pauli groups in the Hamiltonian is relatively small. These scaling dependencies suggest some applications where the Krylov method can be useful, and others where it likely will not be.\n", + "Some Hamiltonians have high complexity when mapped to qubits, involving many non-commuting Pauli strings that cannot easily be partitioned into a few commuting groups. This is often true of quantum chemistry problems, for example. This complexity presents two primary challenges for near-term quantum computers:\n", + "\n", + "* The estimation of each element of $\\tilde{H}$ becomes computationally expensive due to the large number of terms.\n", + "* The required Trotter circuits become prohibitively deep.\n", + "\n", + "Both of the above points will be less problematic when quantum computers reach fault-tolerance, but they must be considered in the near term. Even systems with “simpler” mappings than those in quantum chemistry may experience the same impediments, if the Hamiltonians have too many non-commuting terms.\n", + "The Krylov method is most useful where the Hamiltonian can be partitioned into relatively few commuting Pauli groups, and where $H$ is easy to implement in trotter circuits. Both of these conditions are satisfied, for example, for many lattice models of interest in physics. KQD is especially useful if very little is known about the ground state. This stems from its inherent convergence guarantees and its applicability in scenarios where alternative methods are untenable due to insufficient ground state knowledge.\n", + "\n", + "While KQD is a powerful tool, the protocol's time-consuming aspects, particularly the estimation of each element of the projected Hamiltonian and the overlap of Krylov states, represent opportunities for improvement. An alternative approach involves leveraging Krylov methods in conjunction with sampling-based methods, which are the subject of the next lesson." + ] + }, + { + "cell_type": "markdown", + "id": "7fa7609f-9e96-4b0c-b716-ae9242bb40ca", + "metadata": {}, + "source": [ + "## 6. Appendices\n", + "\n", + "### Appendix I: Krylov subspace from real time-evolutions\n", + "\n", + "The unitary Krylov space is defined as\n", + "\n", + "$$\n", + "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ |\\psi\\rangle, e^{-iH\\,dt} |\\psi\\rangle, \\dots, e^{-irH\\,dt} |\\psi\\rangle \\right\\}\n", + "$$\n", + "\n", + "for some timestep $dt$ that we will determine later. Temporarily assume $r$ is even: then define $d=r/2$. Notice that when we project the Hamiltonian into the Krylov space above, it is indistinguishable from the Krylov space\n", + "\n", + "$$\n", + "\\mathcal{K}_U(H, |\\psi\\rangle) = \\text{span}\\left\\{ e^{i\\,d\\,H\\,dt}|\\psi\\rangle, e^{i(d-1)H\\,dt} |\\psi\\rangle, \\dots, e^{-i(d-1)H\\,dt} |\\psi\\rangle, e^{-i\\,d\\,H\\,dt} |\\psi\\rangle \\right\\},\n", + "$$\n", + "\n", + "that is, where all the time-evolutions are shifted backward by $d$ timesteps.\n", + "The reason it is indistinguishable is because the matrix elements\n", + "\n", + "$$\n", + "\\tilde{H}_{j,k} = \\langle\\psi|e^{i\\,j\\,H\\,dt}He^{-i\\,k\\,H\\,dt}|\\psi\\rangle=\\langle\\psi|He^{i(j-k)H\\,dt}|\\psi\\rangle\n", + "$$\n", + "\n", + "are invariant under overall shifts of the evolution time, since the time-evolutions commute with the Hamiltonian. For odd $r$, we can use the analysis for $r-1$.\n", + "\n", + "We want to show that somewhere in this Krylov space, there is guaranteed to be a low-energy state. We do so by way of the following result, which is derived from Theorem 3.1 in [\\[3\\]](#references):\n", + "\n", + "**Claim 1:** there exists a function $f$ such that for energies $E$ in the spectral range of the Hamiltonian (that is, between the ground state energy and the maximum energy)...\n", + "\n", + "1. $f(E_0)=1$\n", + "2. $|f(E)|\\le2\\left(1 + \\delta\\right)^{-d}$ for all values of $E$ that lie $\\ge\\delta$ away from $E_0$, that is, it is exponentially suppressed\n", + "3. $f(E)$ is a linear combination of $e^{ijE\\,dt}$ for $j=-d,-d+1,...,d-1,d$\n", + "\n", + "We give a proof below, but that can be safely skipped unless one wants to understand the full, rigorous argument. For now we focus on the implications of the above claim. By property 3 above, we can see that the shifted Krylov space above contains the state $f(H)|\\psi\\rangle$. This is our low-energy state. To see why, write $|\\psi\\rangle$ in the energy eigenbasis:\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_k|E_k\\rangle,\n", + "$$\n", + "\n", + "where $|E_k\\rangle$ is the kth energy eigenstate and $\\gamma_k$ is its amplitude in the initial state $|\\psi\\rangle$. Expressed in terms of this, $f(H)|\\psi\\rangle$ is given by\n", + "\n", + "$$\n", + "f(H)|\\psi\\rangle = \\sum_{k=0}^{N}\\gamma_kf(E_k)|E_k\\rangle,\n", + "$$\n", + "\n", + "using the fact that we can replace $H$ by $E_k$ when it acts on the eigenstate $|E_k\\rangle$. The energy error of this state is therefore\n", + "\n", + "$$\n", + "\\text{energy error} = \\frac{\\langle\\psi|f(H)(H-E_0)f(H)|\\psi\\rangle}{\\langle\\psi|f(H)^2|\\psi\\rangle}\n", + "$$\n", + "\n", + "$$\n", + "= \\frac{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n", + "$$\n", + "\n", + "To turn this into an upper bound that is easier to understand, we first separate the sum in the numerator into terms with $E_k-E_0\\le\\delta$ and terms with $E_k-E_0>\\delta$:\n", + "\n", + "$$\n", + "\\text{energy error} = \\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} + \\frac{\\sum_{E_k> E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2}.\n", + "$$\n", + "\n", + "We can upper bound the first term by $\\delta$,\n", + "\n", + "$$\n", + "\\frac{\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0)}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} < \\frac{\\delta\\sum_{E_k\\le E_0+\\delta}|\\gamma_k|^2f(E_k)^2}{\\sum_{k=0}^{N}|\\gamma_k|^2f(E_k)^2} \\le \\delta,\n", + "$$\n", + "\n", + "where the first step follows because $E_k-E_0\\le\\delta$ for every $E_k$ in the sum, and the second step follows because the sum in the numerator is a subset of the sum in the denominator. For the second term, first we lower bound the denominator by $|\\gamma_0|^2$, since $f(E_0)^2=1$: adding everything back together, this gives\n", + "\n", + "$$\n", + "\\text{energy error} \\le \\delta + \\frac{1}{|\\gamma_0|^2}\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2f(E_k)^2(E_k-E_0).\n", + "$$\n", + "\n", + "To simplify what is left, notice that for all these $E_k$, by the definition of $f$ we know that $f(E_k)^2 \\le 4\\left(1 + \\delta\\right)^{-2d}$. Additionally upper bounding $E_k-E_0<2\\|H\\|$ and upper bounding $\\sum_{E_k>E_0+\\delta}|\\gamma_k|^2<1$ gives\n", + "\n", + "$$\n", + "\\text{energy error} \\le \\delta + \\frac{8}{|\\gamma_0|^2}\\|H\\|\\left(1 + \\delta\\right)^{-2d}.\n", + "$$\n", + "\n", + "This holds for any $\\delta>0$, so if we set $\\delta$ equal to our goal error, then the error bound above converges towards that exponentially with the Krylov dimension $2d=r$. Also note that if $\\delta \\delta$ we have\n", + "\n", + "$$\n", + "|f(E)| \\le \\beta(a, b, d) = T_d^{-1}\\left(1 + 2\\frac{1-\\cos\\big(\\delta\\,dt\\big)}{1 + \\cos\\big(\\delta\\,dt\\big)}\\right)\n", + "$$\n", + "\n", + "$$\n", + "\\leq 2\\left(1 + \\delta\\right)^{-d} = 2\\left(1 + \\delta\\right)^{-\\lfloor k/2\\rfloor}.\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "97d292db-83bb-431c-97e3-91212e168ab8", + "metadata": {}, + "source": [ + "## References:\n", + "\n", + "[1] https://arxiv.org/abs/2407.14431\n", + "\n", + "[2] https://arxiv.org/abs/1811.09025\n", + "\n", + "[3] https://people.math.ethz.ch/~mhg/pub/biksm.pdf\n", + "\n", + "[4] https://academic.oup.com/book/36426\n", + "\n", + "[5] https://en.wikipedia.org/wiki/Krylov_subspace\n", + "\n", + "[6] Krylov Subspace Methods: Principles and Analysis, Jörg Liesen, Zdenek Strakos https://academic.oup.com/book/36426\n", + "\n", + "[7] Iterative Methods for Sparse Linear Systems\" by Yousef Saad\n", + "\n", + "[8] \"MINRES-QLP: A Krylov Subspace Method for Indefinite or Singular Symmetric Systems\" by Sou-Cheng Choi, Christopher Paige, and\n", + "Michael Saunders (https://epubs.siam.org/doi/10.1137/100787921)\n", + "\n", + "[9] Ethan N. Epperly, Lin Lin, and Yuji Nakatsukasa. \"A theory of quantum subspace diagonalization\". SIAM Journal on Matrix Analysis and Applications 43, 1263–1290 (2022).\n", + "\n", + "[10] https://link.aps.org/doi/10.1103/PRXQuantum.4.030319" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb b/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb index 520df6f1767..7b61b9fbc0c 100644 --- a/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb @@ -1,1006 +1,1007 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "090b6884", - "metadata": {}, - "source": [ - "---\n", - "title: SQD Implementation\n", - "description: Sample-based quantum diagonalization (SQD) is implemented in the context of solving the ground state of a nitrogen molecule.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore fontdict fontsize milli hcore ccsd Motta */}\n", - "\n", - "# SQD for energy estimation of a chemistry Hamiltonian" - ] - }, - { - "cell_type": "markdown", - "id": "ee7de1bc", - "metadata": {}, - "source": [ - "In this lesson, we will apply SQD to estimate the ground state energy of a molecule.\n", - "\n", - "In particular, we will discuss the following topics using the $4$-step Qiskit pattern approach:\n", - "\n", - "1. Step 1: Map problem to quantum circuits and operators\n", - " - Setup the molecular Hamiltonian for $N_2$.\n", - " - Explain the chemistry-inspired and hardware-friendly local unitary cluster Jastrow (LUCJ) [\\[1\\]](#references)\n", - "2. Step 2: Optimize for target hardware\n", - " - Optimize gate counts and layout of the ansatz for hardware execution\n", - "3. Step 3: Execute on target hardware\n", - " - Run the optimized circuit on a real QPU to generate samples of the subspace.\n", - "4. Step 4: Post-process results\n", - " - Introduce the self-consistent configuration recovery loop [\\[2\\]](#references)\n", - " - Post-process the full set of bitstring samples, using prior knowledge of particle number and the average orbital occupancy calculated on the most recent iteration.\n", - " - Probabilistically create batches of subsamples from recovered bitstrings.\n", - " - Project and diagonalize the molecular Hamiltonian over each sampled subspace.\n", - " - Save the minimum ground state energy found across all batches and update the avg orbital occupancy.\n", - "\n", - "We will use several software packages throughout the lesson.\n", - "- `PySCF` to define the molecule and setup the Hamiltonian.\n", - "- `ffsim` package to construct the LUCJ ansatz.\n", - "- `Qiskit` for transpiling the ansatz for hardware execution.\n", - "- `Qiskit IBM Runtime` to execute the circuit on a QPU and collect samples.\n", - "- `Qiskit addon SQD` configuration recovery and ground state energy estimation using subspace projection and matrix diagonalization." - ] - }, - { - "cell_type": "markdown", - "id": "719a9c0e-8c00-4fab-ba23-f2c8a7ebb573", - "metadata": {}, - "source": [ - "## 1. Map problem to quantum circuits and operators" - ] - }, - { - "cell_type": "markdown", - "id": "d02f97af", - "metadata": {}, - "source": [ - "### Molecular Hamiltonian" - ] - }, - { - "cell_type": "markdown", - "id": "5dac27ce-56b1-4f7f-ab83-ac153c004e82", - "metadata": {}, - "source": [ - "A molecular Hamiltonian takes the generic form:\n", - "\n", - "$$\n", - "\\hat{H} = \\sum_{ \\substack{pr\\\\\\sigma} } h_{pr} \\, \\hat{a}^\\dagger_{p\\sigma} \\hat{a}_{r\\sigma}\n", - "+\n", - "\\sum_{ \\substack{prqs\\\\\\sigma\\tau} }\n", - "\\frac{(pr|qs)}{2} \\,\n", - "\\hat{a}^\\dagger_{p\\sigma}\n", - "\\hat{a}^\\dagger_{q\\tau}\n", - "\\hat{a}_{s\\tau}\n", - "\\hat{a}_{r\\sigma}\n", - "$$\n", - "\n", - "$\\hat{a}^\\dagger_{p\\sigma}$/$\\hat{a}_{p\\sigma}$ are the fermionic creation/annihilation operators associated to the $p$-th basis set element and the spin $\\sigma$. $h_{pr}$ and $(pr|qs)$ are the one- and two-body electronic integrals. Using pySCF, we will define the molecule and compute the one- and two-body integrals of the Hamiltonian for basis set `6-31g`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc987c08-7261-4c4c-a06b-609d7003efe9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "converged SCF energy = -108.835236570774\n", - "CASCI E = -109.046671778080 E(CI) = -32.8155692383188 S^2 = 0.0000000\n" - ] - } - ], - "source": [ - "import warnings\n", - "import pyscf\n", - "import pyscf.cc\n", - "import pyscf.mcscf\n", - "\n", - "warnings.filterwarnings(\"ignore\")\n", - "\n", - "# Specify molecule properties\n", - "open_shell = False\n", - "spin_sq = 0\n", - "\n", - "# Build N2 molecule\n", - "mol = pyscf.gto.Mole()\n", - "mol.build(\n", - " atom=[[\"N\", (0, 0, 0)], [\"N\", (1.0, 0, 0)]], # Two N atoms 1 angstrom apart\n", - " basis=\"6-31g\",\n", - " symmetry=\"Dooh\",\n", - ")\n", - "\n", - "# Define active space\n", - "n_frozen = 2\n", - "active_space = range(n_frozen, mol.nao_nr())\n", - "\n", - "# Get molecular integrals\n", - "scf = pyscf.scf.RHF(mol).run()\n", - "num_orbitals = len(active_space)\n", - "n_electrons = int(sum(scf.mo_occ[active_space]))\n", - "num_elec_a = (n_electrons + mol.spin) // 2\n", - "num_elec_b = (n_electrons - mol.spin) // 2\n", - "cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))\n", - "mo = cas.sort_mo(active_space, base=0)\n", - "hcore, nuclear_repulsion_energy = cas.get_h1cas(mo) # hcore: one-body integrals\n", - "eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals) # eri: two-body integrals\n", - "\n", - "# Compute exact energy for comparison\n", - "exact_energy = cas.run().e_tot" - ] - }, - { - "cell_type": "markdown", - "id": "3aa4e0f0", - "metadata": {}, - "source": [ - "In this lesson, we will use Jordan-Wigner (JW) transformation to map a fermionic wavefunction to a qubit wavefunction so that it can be prepared using a quantum circuit. The JW transformation maps the Fock space of fermions in M spatial orbitals onto the Hilbert space of 2M qubits, that is, a spatial orbital is split into two _spin orbitals_, one associated with a spin up ($\\alpha$) electron and another with spin down ($\\beta$). A spin orbital can be occupied or unoccupied. Usually, when we refer to number of orbitals, we will be using number of _spatial_ orbitals. The number of spin orbitals will be double. In quantum circuits, we will represent each spin orbital with one qubit. Thus, a set of qubits will represent spin-up or $\\alpha$-orbitals, and another set will represent spin-down or $\\beta$-orbitals. For example, $N_2$ molecule for `6-31g` basis set has $16$ spatial orbitals (that is, $16$ $\\alpha$ + $16$ $\\beta$ = $32$ spin orbitals). Thus, we will need a $32$-qubit quantum circuit (we may need extra ancilla qubits as discussed later). The qubits are measured in computational basis to generate bitstrings, which represent electronic configurations or (Slater) determinants. Throughout this lesson, we will use the terms bitstrings, configurations, and determinants interchangeably. The bitstrings tell us electron occupancy in spin orbitals: a $1$ in a bit position means the corresponding spin orbital is occupied, while a $0$ means the spin orbital is empty. As electronic structure problems are particle preserving, only a fixed number of spin orbitals must be occupied. The $N_2$ molecule has $5$ spin-up ($\\alpha$) and $5$ spin-down ($\\beta$) electrons. Thus, any bitstring representing the $\\alpha$ and $\\beta$ orbitals must have five $1\\text{s}$ each for $N_2$ molecule." - ] - }, - { - "cell_type": "markdown", - "id": "f6a89c89-85e1-4c0d-abeb-8ab8b154a7ba", - "metadata": {}, - "source": [ - "### 1.1 Quantum circuit for sample generation: The LUCJ ansatz" - ] - }, - { - "cell_type": "markdown", - "id": "a464c865-1528-45c2-8ede-325583b15976", - "metadata": {}, - "source": [ - "In this lesson, we will use the local unitary coupled cluster Jastrow (LUCJ) [\\\\[1\\\\]](#references) ansatz for quantum state preparation and subsequent sampling. First, we will explain different building blocks of the full UCJ ansatz and the approximations made in the local version of it. Next, by using ffsim package, we will construct the LUCJ ansatz and optimize it using Qiskit transpiler for hardware execution.\n", - "\n", - "The UCJ ansatz has the following form (for a product of $L$ layers or repetitions of the UCJ operator.)\n", - "\n", - "$$\n", - "|\\psi\\rangle = \\prod_{\\mu=1}^{L}{(e^{K^{\\mu}} \\times {e^{iJ^{\\mu}}} \\times {e^{-K^{\\mu}}})} |\\Phi_{0}\\rangle\n", - "$$\n", - "\n", - "where, $\\vert \\Phi_{0} \\rangle$ is a reference state, typically taken as the Hartree-Fock (HF) state. As the Hartree-Fock state is defined as having the lowest numbered orbitals occupied, the HF state preparation will involve applying X gates to set qubits corresponding to occupied orbitals to one. For example, the HF state preparation block for 4 spatial orbitals and 2 up- and 2 down-spin may look like the following:" - ] - }, - { - "cell_type": "markdown", - "id": "0f0bb614", - "metadata": {}, - "source": [ - "![A circuit diagram showing 8 qubits, 4 called alpha orbitals and 4 called beta orbitals. The top two alpha and the top two beta have a \"not\" gate.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig1.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "4a32d88b", - "metadata": {}, - "source": [ - "A single repetition of the UCJ operator ${(e^{K^{(\\mu)}} \\times {e^{iJ^{(\\mu)}}} \\times {e^{-K^{(\\mu)}}})}$ consists of a diagonal Coulomb evolution ($e^{iJ^{(\\mu)}}$) sandwiched by orbital rotations ($e^{K^{(\\mu)}}$ and $e^{-K^{(\\mu)}}$)." - ] - }, - { - "cell_type": "markdown", - "id": "963e8386-39b6-40b7-9740-fffcf1573fe6", - "metadata": {}, - "source": [ - "![A circuit diagram showing that the UCJ circuit can be broken down into rotation layers and a diagonal Coulomb evolution layer.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig2.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "271b1825-bc78-4957-bbf8-21a714e45ed3", - "metadata": {}, - "source": [ - "Orbital rotation blocks work on a single spin species ($\\alpha$ (up-spin)/$\\beta$ (down-spin)). For each electron species, orbital rotation consists of a layer of single-qubit $R_{z}$ gates followed by a sequence of 2-qubit Given's rotation gates ($XX + YY$ gates).\n", - "\n", - "The 2-qubit gates act on adjacent spin-orbitals (nearest neighbor qubits), and therefore, are implementable on IBM® QPUs without the need for SWAP gates." - ] - }, - { - "cell_type": "markdown", - "id": "2e8ac1d2-8f04-4591-921b-8ba0174e4ad0", - "metadata": {}, - "source": [ - "![A circuit diagram showing 4 alpha orbital qubits and 4 beta orbital qubits. The circuits start with R-Z gates, and then have a series of Given's rotation gates.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig3.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "ebc9b48c-e77d-4af5-b8cf-460daeeadcdb", - "metadata": {}, - "source": [ - "The $e^{iJ^{(\\mu)}}$, also known as the diagonal Coulomb operator, consists of three blocks. Two of them work on same spin sectors ($e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ and $e^{iJ_{\\beta \\beta}^{(\\mu)}}$), and one works between two spin sectors ($e^{iJ_{\\alpha \\beta}^{(\\mu)}}$).\n", - "\n", - "All the blocks in $e^{iJ^{(\\mu)}}$ consists of number-number gates $U_{nn}(\\phi)$ [\\[1\\]](#references). A $U_{nn}(\\phi)$ gate can be further broken down into a $R_{ZZ}(\\frac{\\phi}{2})$ gate followed by two single-qubit $Rz(-\\frac{\\phi}{2})$ gates acting on two separate qubits.\n", - "\n", - "Same-spin components ($J_{\\alpha \\alpha}$ and $J_{\\beta \\beta}$) have $U_{nn}$ gates between all possible pairs of qubits. However, as superconducting QPUs have restrictive connectivity, qubits must be swapped to realize gates between non-adjacent qubits.\n", - "\n", - "For example, consider the following $e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ (or $e^{iJ_{\\beta \\beta}^{(\\mu)}}$) block for $N = 4$ spatial orbitals. For a linear qubit connectivity, the last three gates are not directly implementable as they work between non-adjacent qubits (for example, Q0 and Q2 are not directly connected). Therefore, we need SWAP gates to make them adjacent (following figure shows an example with $3$ SWAP gates)." - ] - }, - { - "cell_type": "markdown", - "id": "d3e24a20-1c86-4aea-8300-13bd2ead4e8f", - "metadata": {}, - "source": [ - "![A circuit diagram showing linearly-coupled qubits and corresponding alpha/beta circuits.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig4.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "ef3f1d36-e8ab-46f0-bddb-fa8fb884e531", - "metadata": {}, - "source": [ - "Next, the $J_{\\alpha \\beta}$ implements gates between same indexed orbitals from different spin sectors (for example, between $0\\alpha$ and $0\\beta$). Similarly, if the qubits are not physically adjacent on a QPU, these gates will also require SWAPs." - ] - }, - { - "cell_type": "markdown", - "id": "9afe0036-318c-43b9-9b2c-39a808bda82c", - "metadata": {}, - "source": [ - "![A circuit diagram showing 4 alpha qubits connected to the 4 beta qubits.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig5.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "6219aa8e-8a63-4bfc-b52b-64507292471d", - "metadata": {}, - "source": [ - "From the above discussion, the UCJ ansatz faces some hurdles for HW execution as it needs SWAP gates due to non-adjacent qubit interactions. The local variant of the UCJ ansatz, LUCJ, addresses this challenge by removing some $U_{nn}$ from the diagonal Coulomb operator.\n", - "\n", - "In the same electron species blocks, $J_{\\alpha \\alpha}$ and $J_{\\beta \\beta}$), we only keep the $U_{nn}$ gates compatible with nearest-neighbor connectivity and remove gates between non-adjacent qubits in the LUCJ version. Following figure shows the LUCJ block after removal of non-adjacent gates." - ] - }, - { - "cell_type": "markdown", - "id": "24f54a55-4a09-4508-997a-96306835c7e6", - "metadata": {}, - "source": [ - "![A circuit diagram showing 4 alpha qubits and 4 beta qubits each with R-Z gates, followed by two-qubit gates.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig6.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "165d0a19-0366-4af8-8ea0-20df351f49f5", - "metadata": {}, - "source": [ - "Next, the LUCJ version of the $J_{\\alpha \\beta}$ block that works between different electron species can take different shape based on the device topology.\n", - "\n", - "Here, also, the LUCJ version gets rid of non-compatible gates. The figure below shows variants of the $J_{\\alpha \\beta}$ block for different qubit topology including grid, hexagonal, heavy-hex, and linear.\n", - "\n", - "- **Square**: we can have $U_{nn}$ gates between all $\\alpha$ and $\\beta$ orbitals without any SWAPs, and therefore, do not need to remove any $U_{nn}$ gates.\n", - "- **Heavy-hex**: The $\\alpha$-$\\beta$ interactions are kept between every $4$-th indexed (such as the 0th, 4th, and 8th) spin orbitals and are _ancilla_ mediated, that is, we need ancilla qubits between the linear chains representing $\\alpha$ and $\\beta$ orbitals. This arrangement needs a limited number of SWAPs.\n", - "- **Hexagonal**: Every other orbital, such as 0th, 2nd, and 4th indexed orbitals, becomes nearest neighbors when $\\alpha$ and $\\beta$ are laid out in two adjacent linear chains.\n", - "- **Linear**: Only one $\\alpha$ and one $\\beta$ orbital are connected, which means the $J_{\\alpha \\beta}$ block will have only one gate." - ] - }, - { - "cell_type": "markdown", - "id": "8ec1e433-bc00-42bc-bdbb-288c56c32f9d", - "metadata": {}, - "source": [ - "![Connectivity diagrams for different qubit layouts. They show qubits arranged on a square grid, a hexagonal lattice, a heavy-hex lattice (hexagonal lattice with one extra qubit along each side of the hexagon), and a linear chain.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig7.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "3e070615-2b4a-4866-8c6c-d2d038447f45", - "metadata": {}, - "source": [ - "While removing gates from the UCJ ansatz to construct the LUCJ version makes it more HW compatible, the ansatz loses some expressivity. Therefore, more repetitions ($L$) of the modified UCJ operator may be needed when using the LUCJ ansatz." - ] - }, - { - "cell_type": "markdown", - "id": "04367dac", - "metadata": {}, - "source": [ - "### 1.2 LUCJ ansatz initialization" - ] - }, - { - "cell_type": "markdown", - "id": "f5eae55f", - "metadata": {}, - "source": [ - "The LUCJ is a parameterized ansatz, and we need to initialize the parameters before hardware execution. One way to initialize ansatz is by using `t1` and `t2` amplitudes from classical coupled cluster singles and doubles (CCSD) method, where `t1` amplitudes are the coefficient of single excitation operators and `t2` amplitudes are for double excitation operators.\n", - "\n", - "Note that while initializing the LUCJ ansatz with `t1` and `t2` amplitudes generate decent results, the ansatz parameters may need further optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1835e74e-3354-425f-8596-c574f03e7a6e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "E(CCSD) = -109.0398256929733 E_corr = -0.20458912219883\n" - ] - } - ], - "source": [ - "# Get CCSD t2 amplitudes for initializing the ansatz\n", - "ccsd = pyscf.cc.CCSD(\n", - " scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]\n", - ")\n", - "ccsd.run()\n", - "\n", - "t1 = ccsd.t1\n", - "t2 = ccsd.t2" - ] - }, - { - "cell_type": "markdown", - "id": "9f0842fa", - "metadata": {}, - "source": [ - "### 1.3 Constructing the LUCJ ansatz using `ffsim`" - ] - }, - { - "cell_type": "markdown", - "id": "e5f7f7e8-30e3-40e8-9b85-bf057b35b766", - "metadata": {}, - "source": [ - "We will use the [ffsim](https://github.com/qiskit-community/ffsim/tree/main) package to create and initialize the ansatz with `t1` and `t2` amplitudes computed above. Since our molecule has a closed-shell Hartree-Fock state, we will use the spin-balanced variant of the UCJ ansatz, [UCJOpSpinBalanced](https://qiskit-community.github.io/ffsim/api/ffsim.html#ffsim.UCJOpSpinBalanced).\n", - "\n", - "As IBM hardware has a heavy-hex topology, we will adopt the _zig-zag_ pattern used in [\\[1\\]](#references) and explained above for qubit interactions. In this pattern, orbitals (qubits) with the same spin are connected with a line topology (red and blue circles). Due to the heavy-hex topology, orbitals for different spins have connections between every 4th orbital, that is, the 0th, 4th, 8th, and so on (purple circles).\n", - "\n", - "![A zig-zag pattern traced out along a heavy-hex lattice.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig8.svg)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "6799421a-4425-404a-9994-47215957054d", - "metadata": {}, - "outputs": [], - "source": [ - "import ffsim\n", - "from qiskit import QuantumCircuit, QuantumRegister\n", - "\n", - "n_reps = 2\n", - "alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)]\n", - "alpha_beta_indices = [(p, p) for p in range(0, num_orbitals, 4)]\n", - "\n", - "ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n", - " t2=t2,\n", - " t1=t1,\n", - " n_reps=n_reps,\n", - " interaction_pairs=(alpha_alpha_indices, alpha_beta_indices),\n", - ")\n", - "\n", - "nelec = (num_elec_a, num_elec_b)\n", - "\n", - "# create an empty quantum circuit\n", - "qubits = QuantumRegister(2 * num_orbitals, name=\"q\")\n", - "circuit = QuantumCircuit(qubits)\n", - "\n", - "# prepare Hartree-Fock state as the reference state and append it to the quantum circuit\n", - "circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits)\n", - "\n", - "# apply the UCJ operator to the reference state\n", - "circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n", - "circuit.measure_all()\n", - "# circuit.decompose().draw(\"mpl\", scale=0.5, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "3b2de316", - "metadata": {}, - "source": [ - "The LUCJ ansatz with repeated layers can be optimized by merging some adjacent blocks. Consider a case for `n_reps=2`. The two orbital rotation blocks in the middle can be merged into a single orbital rotation block. The `ffsim` package has a pass manager named `ffsim.qiskit.PRE_INIT` to optimize the circuit by merging such adjacent blocks." - ] - }, - { - "cell_type": "markdown", - "id": "7cb99cd9", - "metadata": {}, - "source": [ - "![A diagram showing layers of the LUCJ ansatz.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig9.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "e9ad460d", - "metadata": {}, - "source": [ - "## 2. Optimize for target hardware" - ] - }, - { - "cell_type": "markdown", - "id": "cecf2994", - "metadata": {}, - "source": [ - "First, we fetch a backend of our choice. We will optimize our circuit for the backend, and then execute the optimized circuit on the same backend to generate samples for the subspace." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c8dbbef-378e-4ae3-9290-bde58b72024d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "# backend = service.backend(\"ibm_brisbane\")" - ] - }, - { - "cell_type": "markdown", - "id": "1b8ba388-6904-4725-ad97-2a31c0adaa07", - "metadata": {}, - "source": [ - "Next, we recommend the following steps to optimize the ansatz and make it hardware-compatible.\n", - "\n", - "- Select physical qubits (`initial_layout`) from the target hardware that adheres to the zig-zag pattern (two linear chains with ancilla qubit in-between them) described above. Laying out qubits in this pattern leads to an efficient hardware-compatible circuit with less gates.\n", - "- Generate a staged pass manager using the [`generate_preset_pass_manager`](/docs/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) function from Qiskit with your choice of `backend` and `initial_layout`.\n", - "- Set the `pre_init` stage of your staged pass manager to `ffsim.qiskit.PRE_INIT`. `ffsim.qiskit.PRE_INIT` includes Qiskit transpiler passes that decompose gates into orbital rotations and then merges the orbital rotations, resulting in fewer gates in the final circuit.\n", - "- Run the pass manager on your circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ecd2675c-a302-43fb-80f4-d658d56360d5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Gate counts (w/o pre-init passes): OrderedDict({'rz': 7579, 'sx': 6106, 'ecr': 2316, 'x': 336, 'measure': 32, 'barrier': 1})\n", - "Gate counts (w/ pre-init passes): OrderedDict({'rz': 4088, 'sx': 3125, 'ecr': 1262, 'x': 201, 'measure': 32, 'barrier': 1})\n" - ] - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "spin_a_layout = [0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, 62, 72, 81, 82]\n", - "spin_b_layout = [2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54, 64, 65, 66, 73]\n", - "\n", - "initial_layout = spin_a_layout + spin_b_layout\n", - "\n", - "pass_manager = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, initial_layout=initial_layout\n", - ")\n", - "\n", - "# without PRE_INIT passes\n", - "isa_circuit = pass_manager.run(circuit)\n", - "print(f\"Gate counts (w/o pre-init passes): {isa_circuit.count_ops()}\")\n", - "\n", - "# with PRE_INIT passes\n", - "# We will use the circuit generated by this pass manager for hardware execution\n", - "pass_manager.pre_init = ffsim.qiskit.PRE_INIT\n", - "isa_circuit = pass_manager.run(circuit)\n", - "print(f\"Gate counts (w/ pre-init passes): {isa_circuit.count_ops()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "72a780f9", - "metadata": {}, - "source": [ - "## 3. Execute on target hardware" - ] - }, - { - "cell_type": "markdown", - "id": "4cd9164c-697a-4aa6-b71f-86720e3d5b66", - "metadata": {}, - "source": [ - "After optimizing the circuit for hardware execution, we are ready to run it on the target hardware and collect samples for ground state energy estimation. As we only have one circuit, we will use Qiskit Runtime's [Job execution mode](/docs/guides/execution-modes) and execute our circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9f542448-5767-4e12-85c8-dd8584545dbb", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "sampler.options.dynamical_decoupling.enable = True\n", - "\n", - "job = sampler.run([isa_circuit], shots=10_000) # Takes approximately 5sec of QPU time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8c5eb3e9-2a5a-423b-ac18-7bba7f69f18f", - "metadata": {}, - "outputs": [], - "source": [ - "# Run cell after IQX job completion\n", - "primitive_result = job.result()\n", - "pub_result = primitive_result[0]\n", - "counts = pub_result.data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "e3ebf77a-8efd-43d6-bd19-e26423921c6f", - "metadata": {}, - "source": [ - "## 4. Post-process results" - ] - }, - { - "cell_type": "markdown", - "id": "1e7f0ecd", - "metadata": {}, - "source": [ - "The post-processing part of the SQD workflow can be summarized using the following diagram.\n", - "\n", - "![A flow chart showing how sampled states are used to determine ground state eigenvalues and eigenvectors.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig10.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "8c51a527", - "metadata": {}, - "source": [ - "Sampling the LUCJ ansatz in the computational basis generates a pool of noisy configurations $\\tilde{\\mathcal{\\chi}}$, which are used in the post-processing routine. It involves a method called (details discussed later) _configuration recovery_ to probabilistically correct configurations with incorrect electron numbers. Configurations only with correct electron numbers $\\tilde{\\mathcal{\\chi}}_{R}$ are then subsampled and distributed into multiple batches based on the frequency of appearance of each unique configuration. Each batch of samples defines a subspace ($\\mathcal{S^{(k)}}$). Next, the molecular Hamiltonian, $H$, is projected onto subspaces:\n", - "$$\n", - "H_{\\mathcal{S}^{(k)}} = P_{\\mathcal{S}^{(k)}} H _{\\mathcal{S}^{(k)}} \\text{ with } P_{\\mathcal{S}^{(k)}} = \\sum_{x \\in \\mathcal{S}^{(k)}} \\vert x \\rangle \\langle x \\vert\n", - "$$\n", - "\n", - "Each projected Hamiltonian $H_{\\mathcal{S}^{(k)}}$ is then fed into an Eigensolver, where it is diagonalized to compute eigenvalues and eigenvectors to reconstruct an eigenstate. In this lesson, we project and diagonalize the Hamiltonian using the `qiskit-addon-sqd` package which uses the Davidson's method from PySCF for diagonalization.\n", - "\n", - "$$\n", - "H_{\\mathcal{S}^{(k)}} \\vert \\psi^{(k)} \\rangle = E^{(k)} \\vert \\psi^{(k)} \\rangle\n", - "$$\n", - "\n", - "We then collect the lowest eigenvalue (energy) from the batches, and also compute average orbital occupancy, $\\text{n}$. The average occupancy information is used in the configuration recovery step to probabilistically correct noise configurations.\n", - "\n", - "Next, we explain the self-consistent configuration recovery loop in detail and show concrete code examples to implement the above-mentioned steps to estimate the ground state energy of $N_2$ Hamiltonian." - ] - }, - { - "cell_type": "markdown", - "id": "39997b5d-8bd3-4bc8-9ade-11fa5cfb34f9", - "metadata": {}, - "source": [ - "### 4.1 Configuration recovery: overview" - ] - }, - { - "cell_type": "markdown", - "id": "b05881ac-80ce-47a6-ac28-c1237f85bc0a", - "metadata": {}, - "source": [ - "Each bit in a bitstring (Slater determinant) represents a spin orbital. The right half of a bitstring represents spin-up orbitals, and the left half represents spin-down orbitals. A `1` means the orbital is occupied by an electron, and a `0` means the orbital is empty. We know the correct number of particles (both up-spin electron and down-spin electron) a priori. Suppose we have a determinant $x$ with $N_x$ electrons (that is, there are $N_x$ numbers of $1$s in the bitstring) in it. The correct number of particles is $N$. If $N_x \\neq N$, then we know that the bitstring is corrupted by noise. The self-consistent configuration routine attempts to correct the bitstring by probabilistically flipping $|N_x - N|$ bits by leveraging average orbital occupancy information. The average orbital occupancy ($n$) tells us how likely an orbital be occupied by an electron. If $N_x < N$, we have fewer electrons and need to flip some $0$s to $1$s and vice versa.\n", - "\n", - "The probability of flipping can be $|x[i] - avg\\_occupancy[i]|$ for `i`-th spin orbital. In [\\[2\\]](#references), the authors used a weighted probability of flipping using modified ReLU function.\n", - "\n", - "$$\n", - "\\begin{align}\n", - " w(y) = \\begin{cases}\n", - "\n", - " \\delta \\frac{y}{h} & \\text{if } y \\leq h\\\\ \\nonumber\n", - "\n", - " \\delta + (1 - \\delta) \\frac{y - h}{1 - h} & \\text{if } y > h\n", - "\n", - "\\end{cases}\n", - "\\end{align}\n", - "$$\n", - "\n", - "Here $h$ defines the location of \"corner\" of the ReLU function, and the parameter $\\delta$ defines the value of the ReLU function at the corner. For $\\delta = 0$, $w$ becomes true ReLU function, and for $\\delta >0$, it becomes _modified_ ReLU. In the paper, the authors used $\\delta = 0.01$ and $h =$ number of alpha (or beta) particles/number of alpha (or beta) spin orbitals $= N/M$ (filling factor).\n", - "\n", - "The average orbital occupancy ($n$) is not known a priori. The first iteration of the ground state estimation starts with configurations with only correct particle numbers in both spin species. After the first iteration, we have an estimate of the ground state, and using the estimate, we can construct the first guess of $n$. This guess of $n$ is used to recover configurations, run the next iteration of ground state estimation, and self-consistently refine the guess of $n$. The process repeats until the a stopping criterion is met.\n", - "\n", - "Consider the following example for $N = 2$ and $x = |1000\\rangle$ ($N_x = 1$). We need to flip one of the 0s to 1 to correct it for particle numbers, and the choices are `1100`, `1010`, and `1001`. Based on the probability of flipping, one of the choices will be selected as _recovered configuration_ (or the bitstring with correct number of particles).\n", - "\n", - "Suppose in the first iteration we run two batches, and the estimated ground states from them are:\n", - "\n", - "$$\n", - "\\begin{align}\\nonumber\n", - " \\text{Batch0: } \\vert \\psi \\rangle &= 0.8 \\times \\vert 1001 \\rangle + 0.6 \\times \\vert 0110 \\rangle \\\\ \\nonumber\n", - " \\text{Batch1: } \\vert \\psi \\rangle &= \\frac{1}{\\sqrt{3}} \\left( \\vert 1001 \\rangle + \\vert 0101 \\rangle + \\vert 0110 \\rangle \\right) \\nonumber\n", - "\\end{align}\n", - "$$\n", - "\n", - "Using the computational basis states and their amplitudes, we can compute probability of electron occupancies (in short _occupancies_) per spin-orbital (qubit) (note that probability = |amplitude|$^2$). Below we tabulate qubit-wise occupancies for each bitstring appearing in the estimated ground state and compute total orbital occupancy for a batch. Note that, as per Qiskit ordering convention, the right most bit represents qubit-0 (Q0), and the left most bit represents Q3.\n", - "\n", - "Occupancy (Batch0):\n", - "| | Q3 | Q2 | Q1 | Q0 |\n", - "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", - "| 1001 | 0.64 | 0.0 | 0.0 | 0.64 |\n", - "| 0110 | 0.0 | 0.36 | 0.36 | 0.0 |\n", - "| **n** _(Batch0)_ | **0.64** | **0.36** | **0.36** | **0.64** |\n", - "\n", - "Occupancy (Batch1)\n", - "| | Q3 | Q2 | Q1 | Q0 |\n", - "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", - "| 1001 | 0.33 | 0.00 | 0.00 | 0.33 |\n", - "| 0101 | 0.0 | 0.33 | 0.00 | 0.33 |\n", - "| 0110 | 0.0 | 0.33 | 0.33 | 0.00 |\n", - "| **n** _(Batch1)_ | **0.33** | **0.66** | **0.33** | **0.66** |\n", - "\n", - "Occupancy (average across batches)\n", - "| | Q3 | Q2 | Q1 | Q0 |\n", - "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", - "| **n** _(Batch0)_ | 0.64 | 0.36 | 0.36 | 0.64 |\n", - "| **n** _(Batch1)_ | 0.33 | 0.66 | 0.33 | 0.66 |\n", - "| **n** _(average)_ | **0.49** | **0.51** | **0.35** | **0.65** |\n", - "\n", - "Using the average orbital occupancy computed above, we can find the probabilities of flip for different orbitals in the configuration $x = \\vert 1000 \\rangle$. As the orbital represented by Q3 is already occupied and need not to be flipped, we set its p(flip) to $0$. For the remaining orbitals, which are unoccupied, the probability of flip is $\\vert x[i] - \\text{n}[i] \\vert$ each. Along with p(flip), we also compute the probability weight associated with flipping using the modified ReLU function described above.\n", - "\n", - "Probability of flip ($x = \\vert 1000 \\rangle$, $\\delta = 0.01$, $h = N/M = 2/4 = 0.50$)\n", - "| | Q3 | Q2 | Q1 | Q0 |\n", - "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", - "| p(flip) ($\\vert x[i] - \\text{n}[i] \\vert$) | 0 | 0.51 | 0.35 | 0.65 |\n", - "| w(p(flip)) | 0 | 0.03 | 0.007 | 0.31 |\n", - "\n", - "Finally, using weighted probabilities above, we can flip one of the unoccupied Q2, Q1, and Q0 orbitals. Based on the values above, Q0 will be flipped most likely, and a possible recovered configuration can be $\\vert \\text{1001} \\rangle$." - ] - }, - { - "cell_type": "markdown", - "id": "c1940235", - "metadata": {}, - "source": [ - "![A diagram of configuration recovery.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig11.svg)" - ] - }, - { - "cell_type": "markdown", - "id": "5c614290", - "metadata": {}, - "source": [ - "The complete self-consistent configuration recovery process can be summarized as follows:\n", - "\n", - "**First iteration:** Suppose the bitstrings (configurations or Slater determinants) generated by the quantum computer form a set $\\widetilde{\\chi}$, which includes both configurations with correct ($\\widetilde{\\chi}_{correct}$) and incorrect ($\\widetilde{\\chi}_{incorrect}$) number of particles in each spin sector.\n", - "1. Configurations from ($\\widetilde{\\chi}_{correct}$) are randomly sampled to create batches $(\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)})$ of vectors for subspace projection. The number of batches and samples in each batch are user defined parameters. The larger the number of samples in each batch, the larger the subspace dimension and more computationally demanding the diagonalization becomes. On the other hand, too small number of samples may miss the ground state support vectors and lead to incorrect estimation.\n", - "2. Run the eigenstate solver (that is, projection onto subspace and diagonalization) on the batches and obtain approximate eigenstates. $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n", - "3. From the approximate eigenstates construct the first guess for $n$.\n", - "\n", - "**Subsequent iterations:**\n", - "1. Using $n$ correct the configurations with wrong particle number in $\\widetilde{\\chi}_{incorrect}$. Suppose we name them $\\widetilde{\\chi}_{correct\\_new}$. Then, $\\widetilde{\\chi}_{recovered} (\\widetilde{\\chi}_{R}) = \\widetilde{\\chi}_{correct} \\cup \\widetilde{\\chi}_{correct\\_new}$ forms the new set of configurations with correct particle numbers.\n", - "2. $\\widetilde{\\chi}_{R}$ is sampled to create batches $\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)}$.\n", - "3. Eigenstate solver runs with new batches and generates new estimates of ground states $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n", - "4. From the approximate eigenstates construct refined guess for $n$.\n", - "5. If the stopping criterion is not met, go back to step `2.1`." - ] - }, - { - "cell_type": "markdown", - "id": "5eea548a", - "metadata": {}, - "source": [ - "### 4.2 Ground state estimation" - ] - }, - { - "cell_type": "markdown", - "id": "31dfe00e", - "metadata": {}, - "source": [ - "First, we will transform the counts into a bitstring matrix and probability array for post-processing.\n", - "\n", - "Each row in the matrix represents one unique bitstring. Since qubits are indexed from the right of a bitstring in Qiskit, column ``0`` represents qubit ``N-1``, and column ``N-1`` represents qubit ``0``, where ``N`` is the number of qubits.\n", - "\n", - "The alpha orbitals are represented in the column index range ``(N, N/2]`` (right half), and the beta orbitals are represented in the column range ``(N/2, 0]`` (left half)." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "71550274", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_addon_sqd.counts import counts_to_arrays\n", - "\n", - "# Convert counts into bitstring and probability arrays\n", - "bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "acbdbdc5", - "metadata": {}, - "source": [ - "There are a few user-controlled options which are important for this technique:\n", - "\n", - "- ``iterations``: Number of self-consistent configuration recovery iterations\n", - "- ``n_batches``: Number of batches of configurations used by the different calls to the eigenstate solver\n", - "- ``samples_per_batch``: Number of unique configurations to include in each batch\n", - "- ``max_davidson_cycles``: Maximum number of Davidson cycles run by each eigensolver" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a2d22e0b-8f51-42a0-858f-ad0297cb0bae", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting configuration recovery iteration 0\n", - " Batch 0 subspace dimension: 21609\n", - " Batch 1 subspace dimension: 21609\n", - " Batch 2 subspace dimension: 21609\n", - " Batch 3 subspace dimension: 21609\n", - " Batch 4 subspace dimension: 21609\n", - "Starting configuration recovery iteration 1\n", - " Batch 0 subspace dimension: 609961\n", - " Batch 1 subspace dimension: 616225\n", - " Batch 2 subspace dimension: 627264\n", - " Batch 3 subspace dimension: 633616\n", - " Batch 4 subspace dimension: 624100\n", - "Starting configuration recovery iteration 2\n", - " Batch 0 subspace dimension: 564001\n", - " Batch 1 subspace dimension: 605284\n", - " Batch 2 subspace dimension: 582169\n", - " Batch 3 subspace dimension: 559504\n", - " Batch 4 subspace dimension: 591361\n", - "Starting configuration recovery iteration 3\n", - " Batch 0 subspace dimension: 550564\n", - " Batch 1 subspace dimension: 549081\n", - " Batch 2 subspace dimension: 531441\n", - " Batch 3 subspace dimension: 527076\n", - " Batch 4 subspace dimension: 531441\n", - "Starting configuration recovery iteration 4\n", - " Batch 0 subspace dimension: 544644\n", - " Batch 1 subspace dimension: 580644\n", - " Batch 2 subspace dimension: 527076\n", - " Batch 3 subspace dimension: 531441\n", - " Batch 4 subspace dimension: 537289\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n", - "from qiskit_addon_sqd.fermion import (\n", - " bitstring_matrix_to_ci_strs,\n", - " solve_fermion,\n", - ")\n", - "from qiskit_addon_sqd.subsampling import postselect_and_subsample\n", - "\n", - "rng = np.random.default_rng(24)\n", - "# SQD options\n", - "iterations = 5\n", - "\n", - "# Eigenstate solver options\n", - "n_batches = 5\n", - "samples_per_batch = 500\n", - "max_davidson_cycles = 300\n", - "\n", - "# Self-consistent configuration recovery loop\n", - "e_hist = np.zeros((iterations, n_batches)) # energy history\n", - "s_hist = np.zeros((iterations, n_batches)) # spin history\n", - "occupancy_hist = []\n", - "avg_occupancy = None\n", - "for i in range(iterations):\n", - " print(f\"Starting configuration recovery iteration {i}\")\n", - " # On the first iteration, we have no orbital occupancy information from the\n", - " # solver, so we begin with the full set of noisy configurations.\n", - " if avg_occupancy is None:\n", - " bs_mat_tmp = bitstring_matrix_full\n", - " probs_arr_tmp = probs_arr_full\n", - "\n", - " # If we have average orbital occupancy information, we use it to refine the full set of noisy configurations\n", - " else:\n", - " bs_mat_tmp, probs_arr_tmp = recover_configurations(\n", - " bitstring_matrix_full,\n", - " probs_arr_full,\n", - " avg_occupancy,\n", - " num_elec_a,\n", - " num_elec_b,\n", - " rand_seed=rng,\n", - " )\n", - "\n", - " # Create batches of subsamples. We postselect here to remove configurations\n", - " # with incorrect hamming weight during iteration 0, since no config recovery was performed.\n", - " batches = postselect_and_subsample(\n", - " bs_mat_tmp,\n", - " probs_arr_tmp,\n", - " hamming_right=num_elec_a,\n", - " hamming_left=num_elec_b,\n", - " samples_per_batch=samples_per_batch,\n", - " num_batches=n_batches,\n", - " rand_seed=rng,\n", - " )\n", - "\n", - " # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n", - " e_tmp = np.zeros(n_batches)\n", - " s_tmp = np.zeros(n_batches)\n", - " occs_tmp = []\n", - " coeffs = []\n", - " for j in range(n_batches):\n", - " strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])\n", - " print(f\" Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}\")\n", - " energy_sci, coeffs_sci, avg_occs, spin = solve_fermion(\n", - " batches[j],\n", - " hcore,\n", - " eri,\n", - " open_shell=open_shell,\n", - " spin_sq=spin_sq,\n", - " max_davidson=max_davidson_cycles,\n", - " )\n", - " energy_sci += nuclear_repulsion_energy\n", - " e_tmp[j] = energy_sci\n", - " s_tmp[j] = spin\n", - " occs_tmp.append(avg_occs)\n", - " coeffs.append(coeffs_sci)\n", - "\n", - " # Combine batch results\n", - " avg_occupancy = tuple(np.mean(occs_tmp, axis=0))\n", - "\n", - " # Track optimization history\n", - " e_hist[i, :] = e_tmp\n", - " s_hist[i, :] = s_tmp\n", - " occupancy_hist.append(avg_occupancy)" - ] - }, - { - "cell_type": "markdown", - "id": "e4f6ec2c-032d-4e18-8ea4-cf867cba5054", - "metadata": {}, - "source": [ - "### 4.3 Discussion of results" - ] - }, - { - "cell_type": "markdown", - "id": "592eabb7-18f3-4622-8710-bfc43ad6cdec", - "metadata": {}, - "source": [ - "The first plot shows that after a few iterations we estimate the ground state energy within ~24 mH (chemical accuracy is typically accepted to be 1 kcal/mol $\\approx$ 1.6 mH). The second plot shows the average occupancy of each spatial orbital after the final iteration. We can see that both the spin-up and spin-down electrons occupy the first five orbitals with high probability in our solutions.\n", - "\n", - "Although the estimated ground state energy is decent, it is not within the chemical accuracy limit ($\\pm \\approx 1.6$ mH). This gap can be attributed to the small subspace dimension we used above for projection and diagonalization. As we used `samples_per_batch=500`, the subspace is spanned by max $500$ vectors, which is missing vectors from ground state support. Increasing the `samples_per_batch` parameter should improve the accuracy at the expense of more classical compute resources and runtime." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "61959e69-a182-4636-abcb-a32349fc9076", - "metadata": {}, - "outputs": [], - "source": [ - "# Data for energies plot\n", - "x1 = range(iterations)\n", - "min_e = [np.min(e) for e in e_hist]\n", - "e_diff = [abs(e - exact_energy) for e in min_e]\n", - "yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5]\n", - "\n", - "# Chemical accuracy (+/- 1 milli-Hartree)\n", - "chem_accuracy = 0.001\n", - "\n", - "# Data for avg spatial orbital occupancy\n", - "y2 = occupancy_hist[-1][0] + occupancy_hist[-1][1]\n", - "x2 = range(len(y2))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "8cd90034-6ef3-41bd-a847-c115cade82f7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exact energy: -109.04667 Ha\n", - "SQD energy: -109.02234 Ha\n", - "Absolute error: 0.02434 Ha\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "fig, axs = plt.subplots(1, 2, figsize=(12, 6))\n", - "\n", - "# Plot energies\n", - "axs[0].plot(x1, e_diff, label=\"energy error\", marker=\"o\")\n", - "axs[0].set_xticks(x1)\n", - "axs[0].set_xticklabels(x1)\n", - "axs[0].set_yticks(yt1)\n", - "axs[0].set_yticklabels(yt1)\n", - "axs[0].set_yscale(\"log\")\n", - "axs[0].set_ylim(1e-6)\n", - "axs[0].axhline(\n", - " y=chem_accuracy, color=\"#BF5700\", linestyle=\"--\", label=\"chemical accuracy\"\n", - ")\n", - "axs[0].set_title(\"Approximated Ground State Energy Error vs SQD Iterations\")\n", - "axs[0].set_xlabel(\"Iteration Index\", fontdict={\"fontsize\": 12})\n", - "axs[0].set_ylabel(\"Energy Error (Ha)\", fontdict={\"fontsize\": 12})\n", - "axs[0].legend()\n", - "\n", - "# Plot orbital occupancy\n", - "axs[1].bar(x2, y2, width=0.8)\n", - "axs[1].set_xticks(x2)\n", - "axs[1].set_xticklabels(x2)\n", - "axs[1].set_title(\"Avg Occupancy per Spatial Orbital\")\n", - "axs[1].set_xlabel(\"Orbital Index\", fontdict={\"fontsize\": 12})\n", - "axs[1].set_ylabel(\"Avg Occupancy\", fontdict={\"fontsize\": 12})\n", - "\n", - "print(f\"Exact energy: {exact_energy:.5f} Ha\")\n", - "print(f\"SQD energy: {min_e[-1]:.5f} Ha\")\n", - "print(f\"Absolute error: {e_diff[-1]:.5f} Ha\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "e1530472", - "metadata": {}, - "source": [ - "#### Exercise for the reader\n", - "Progressively increase the `samples_per_batch` parameter (for example, from $1000$ to $10000$ at a step of $1000$; permitted my your computer's memory) and compare the estimated ground state energies." - ] - }, - { - "cell_type": "markdown", - "id": "d2b2241f", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "\\[1] M. Motta et al., “Bridging physical intuition and hardware efficiency for correlated electronic states: the local unitary cluster Jastrow ansatz for electronic structure” (2023). [Chem. Sci., 2023, 14, 11213](https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k).\n", - "\n", - "\\[2] J. Robledo-Moreno et al., \"Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer\" (2024). [arXiv:quant-ph/2405.05068](https://arxiv.org/abs/2405.05068)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "090b6884", + "metadata": {}, + "source": [ + "---\n", + "title: SQD Implementation\n", + "description: Sample-based quantum diagonalization (SQD) is implemented in the context of solving the ground state of a nitrogen molecule.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore fontdict fontsize milli hcore ccsd Motta */}\n", + "\n", + "# SQD for energy estimation of a chemistry Hamiltonian" + ] + }, + { + "cell_type": "markdown", + "id": "ee7de1bc", + "metadata": {}, + "source": [ + "In this lesson, we will apply SQD to estimate the ground state energy of a molecule.\n", + "\n", + "In particular, we will discuss the following topics using the $4$-step Qiskit pattern approach:\n", + "\n", + "1. Step 1: Map problem to quantum circuits and operators\n", + " - Setup the molecular Hamiltonian for $N_2$.\n", + " - Explain the chemistry-inspired and hardware-friendly local unitary cluster Jastrow (LUCJ) [\\[1\\]](#references)\n", + "2. Step 2: Optimize for target hardware\n", + " - Optimize gate counts and layout of the ansatz for hardware execution\n", + "3. Step 3: Execute on target hardware\n", + " - Run the optimized circuit on a real QPU to generate samples of the subspace.\n", + "4. Step 4: Post-process results\n", + " - Introduce the self-consistent configuration recovery loop [\\[2\\]](#references)\n", + " - Post-process the full set of bitstring samples, using prior knowledge of particle number and the average orbital occupancy calculated on the most recent iteration.\n", + " - Probabilistically create batches of subsamples from recovered bitstrings.\n", + " - Project and diagonalize the molecular Hamiltonian over each sampled subspace.\n", + " - Save the minimum ground state energy found across all batches and update the avg orbital occupancy.\n", + "\n", + "We will use several software packages throughout the lesson.\n", + "- `PySCF` to define the molecule and setup the Hamiltonian.\n", + "- `ffsim` package to construct the LUCJ ansatz.\n", + "- `Qiskit` for transpiling the ansatz for hardware execution.\n", + "- `Qiskit IBM Runtime` to execute the circuit on a QPU and collect samples.\n", + "- `Qiskit addon SQD` configuration recovery and ground state energy estimation using subspace projection and matrix diagonalization." + ] + }, + { + "cell_type": "markdown", + "id": "719a9c0e-8c00-4fab-ba23-f2c8a7ebb573", + "metadata": {}, + "source": [ + "## 1. Map problem to quantum circuits and operators" + ] + }, + { + "cell_type": "markdown", + "id": "d02f97af", + "metadata": {}, + "source": [ + "### Molecular Hamiltonian" + ] + }, + { + "cell_type": "markdown", + "id": "5dac27ce-56b1-4f7f-ab83-ac153c004e82", + "metadata": {}, + "source": [ + "A molecular Hamiltonian takes the generic form:\n", + "\n", + "$$\n", + "\\hat{H} = \\sum_{ \\substack{pr\\\\\\sigma} } h_{pr} \\, \\hat{a}^\\dagger_{p\\sigma} \\hat{a}_{r\\sigma}\n", + "+\n", + "\\sum_{ \\substack{prqs\\\\\\sigma\\tau} }\n", + "\\frac{(pr|qs)}{2} \\,\n", + "\\hat{a}^\\dagger_{p\\sigma}\n", + "\\hat{a}^\\dagger_{q\\tau}\n", + "\\hat{a}_{s\\tau}\n", + "\\hat{a}_{r\\sigma}\n", + "$$\n", + "\n", + "$\\hat{a}^\\dagger_{p\\sigma}$/$\\hat{a}_{p\\sigma}$ are the fermionic creation/annihilation operators associated to the $p$-th basis set element and the spin $\\sigma$. $h_{pr}$ and $(pr|qs)$ are the one- and two-body electronic integrals. Using pySCF, we will define the molecule and compute the one- and two-body integrals of the Hamiltonian for basis set `6-31g`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc987c08-7261-4c4c-a06b-609d7003efe9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "converged SCF energy = -108.835236570774\n", + "CASCI E = -109.046671778080 E(CI) = -32.8155692383188 S^2 = 0.0000000\n" + ] + } + ], + "source": [ + "import warnings\n", + "import pyscf\n", + "import pyscf.cc\n", + "import pyscf.mcscf\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "# Specify molecule properties\n", + "open_shell = False\n", + "spin_sq = 0\n", + "\n", + "# Build N2 molecule\n", + "mol = pyscf.gto.Mole()\n", + "mol.build(\n", + " atom=[[\"N\", (0, 0, 0)], [\"N\", (1.0, 0, 0)]], # Two N atoms 1 angstrom apart\n", + " basis=\"6-31g\",\n", + " symmetry=\"Dooh\",\n", + ")\n", + "\n", + "# Define active space\n", + "n_frozen = 2\n", + "active_space = range(n_frozen, mol.nao_nr())\n", + "\n", + "# Get molecular integrals\n", + "scf = pyscf.scf.RHF(mol).run()\n", + "num_orbitals = len(active_space)\n", + "n_electrons = int(sum(scf.mo_occ[active_space]))\n", + "num_elec_a = (n_electrons + mol.spin) // 2\n", + "num_elec_b = (n_electrons - mol.spin) // 2\n", + "cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))\n", + "mo = cas.sort_mo(active_space, base=0)\n", + "hcore, nuclear_repulsion_energy = cas.get_h1cas(mo) # hcore: one-body integrals\n", + "eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals) # eri: two-body integrals\n", + "\n", + "# Compute exact energy for comparison\n", + "exact_energy = cas.run().e_tot" + ] + }, + { + "cell_type": "markdown", + "id": "3aa4e0f0", + "metadata": {}, + "source": [ + "In this lesson, we will use Jordan-Wigner (JW) transformation to map a fermionic wavefunction to a qubit wavefunction so that it can be prepared using a quantum circuit. The JW transformation maps the Fock space of fermions in M spatial orbitals onto the Hilbert space of 2M qubits, that is, a spatial orbital is split into two _spin orbitals_, one associated with a spin up ($\\alpha$) electron and another with spin down ($\\beta$). A spin orbital can be occupied or unoccupied. Usually, when we refer to number of orbitals, we will be using number of _spatial_ orbitals. The number of spin orbitals will be double. In quantum circuits, we will represent each spin orbital with one qubit. Thus, a set of qubits will represent spin-up or $\\alpha$-orbitals, and another set will represent spin-down or $\\beta$-orbitals. For example, $N_2$ molecule for `6-31g` basis set has $16$ spatial orbitals (that is, $16$ $\\alpha$ + $16$ $\\beta$ = $32$ spin orbitals). Thus, we will need a $32$-qubit quantum circuit (we may need extra ancilla qubits as discussed later). The qubits are measured in computational basis to generate bitstrings, which represent electronic configurations or (Slater) determinants. Throughout this lesson, we will use the terms bitstrings, configurations, and determinants interchangeably. The bitstrings tell us electron occupancy in spin orbitals: a $1$ in a bit position means the corresponding spin orbital is occupied, while a $0$ means the spin orbital is empty. As electronic structure problems are particle preserving, only a fixed number of spin orbitals must be occupied. The $N_2$ molecule has $5$ spin-up ($\\alpha$) and $5$ spin-down ($\\beta$) electrons. Thus, any bitstring representing the $\\alpha$ and $\\beta$ orbitals must have five $1\\text{s}$ each for $N_2$ molecule." + ] + }, + { + "cell_type": "markdown", + "id": "f6a89c89-85e1-4c0d-abeb-8ab8b154a7ba", + "metadata": {}, + "source": [ + "### 1.1 Quantum circuit for sample generation: The LUCJ ansatz" + ] + }, + { + "cell_type": "markdown", + "id": "a464c865-1528-45c2-8ede-325583b15976", + "metadata": {}, + "source": [ + "In this lesson, we will use the local unitary coupled cluster Jastrow (LUCJ) [\\\\[1\\\\]](#references) ansatz for quantum state preparation and subsequent sampling. First, we will explain different building blocks of the full UCJ ansatz and the approximations made in the local version of it. Next, by using ffsim package, we will construct the LUCJ ansatz and optimize it using Qiskit transpiler for hardware execution.\n", + "\n", + "The UCJ ansatz has the following form (for a product of $L$ layers or repetitions of the UCJ operator.)\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\prod_{\\mu=1}^{L}{(e^{K^{\\mu}} \\times {e^{iJ^{\\mu}}} \\times {e^{-K^{\\mu}}})} |\\Phi_{0}\\rangle\n", + "$$\n", + "\n", + "where, $\\vert \\Phi_{0} \\rangle$ is a reference state, typically taken as the Hartree-Fock (HF) state. As the Hartree-Fock state is defined as having the lowest numbered orbitals occupied, the HF state preparation will involve applying X gates to set qubits corresponding to occupied orbitals to one. For example, the HF state preparation block for 4 spatial orbitals and 2 up- and 2 down-spin may look like the following:" + ] + }, + { + "cell_type": "markdown", + "id": "0f0bb614", + "metadata": {}, + "source": [ + "![A circuit diagram showing 8 qubits, 4 called alpha orbitals and 4 called beta orbitals. The top two alpha and the top two beta have a \"not\" gate.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig1.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "4a32d88b", + "metadata": {}, + "source": [ + "A single repetition of the UCJ operator ${(e^{K^{(\\mu)}} \\times {e^{iJ^{(\\mu)}}} \\times {e^{-K^{(\\mu)}}})}$ consists of a diagonal Coulomb evolution ($e^{iJ^{(\\mu)}}$) sandwiched by orbital rotations ($e^{K^{(\\mu)}}$ and $e^{-K^{(\\mu)}}$)." + ] + }, + { + "cell_type": "markdown", + "id": "963e8386-39b6-40b7-9740-fffcf1573fe6", + "metadata": {}, + "source": [ + "![A circuit diagram showing that the UCJ circuit can be broken down into rotation layers and a diagonal Coulomb evolution layer.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig2.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "271b1825-bc78-4957-bbf8-21a714e45ed3", + "metadata": {}, + "source": [ + "Orbital rotation blocks work on a single spin species ($\\alpha$ (up-spin)/$\\beta$ (down-spin)). For each electron species, orbital rotation consists of a layer of single-qubit $R_{z}$ gates followed by a sequence of 2-qubit Given's rotation gates ($XX + YY$ gates).\n", + "\n", + "The 2-qubit gates act on adjacent spin-orbitals (nearest neighbor qubits), and therefore, are implementable on IBM® QPUs without the need for SWAP gates." + ] + }, + { + "cell_type": "markdown", + "id": "2e8ac1d2-8f04-4591-921b-8ba0174e4ad0", + "metadata": {}, + "source": [ + "![A circuit diagram showing 4 alpha orbital qubits and 4 beta orbital qubits. The circuits start with R-Z gates, and then have a series of Given's rotation gates.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig3.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "ebc9b48c-e77d-4af5-b8cf-460daeeadcdb", + "metadata": {}, + "source": [ + "The $e^{iJ^{(\\mu)}}$, also known as the diagonal Coulomb operator, consists of three blocks. Two of them work on same spin sectors ($e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ and $e^{iJ_{\\beta \\beta}^{(\\mu)}}$), and one works between two spin sectors ($e^{iJ_{\\alpha \\beta}^{(\\mu)}}$).\n", + "\n", + "All the blocks in $e^{iJ^{(\\mu)}}$ consists of number-number gates $U_{nn}(\\phi)$ [\\[1\\]](#references). A $U_{nn}(\\phi)$ gate can be further broken down into a $R_{ZZ}(\\frac{\\phi}{2})$ gate followed by two single-qubit $Rz(-\\frac{\\phi}{2})$ gates acting on two separate qubits.\n", + "\n", + "Same-spin components ($J_{\\alpha \\alpha}$ and $J_{\\beta \\beta}$) have $U_{nn}$ gates between all possible pairs of qubits. However, as superconducting QPUs have restrictive connectivity, qubits must be swapped to realize gates between non-adjacent qubits.\n", + "\n", + "For example, consider the following $e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ (or $e^{iJ_{\\beta \\beta}^{(\\mu)}}$) block for $N = 4$ spatial orbitals. For a linear qubit connectivity, the last three gates are not directly implementable as they work between non-adjacent qubits (for example, Q0 and Q2 are not directly connected). Therefore, we need SWAP gates to make them adjacent (following figure shows an example with $3$ SWAP gates)." + ] + }, + { + "cell_type": "markdown", + "id": "d3e24a20-1c86-4aea-8300-13bd2ead4e8f", + "metadata": {}, + "source": [ + "![A circuit diagram showing linearly-coupled qubits and corresponding alpha/beta circuits.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig4.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "ef3f1d36-e8ab-46f0-bddb-fa8fb884e531", + "metadata": {}, + "source": [ + "Next, the $J_{\\alpha \\beta}$ implements gates between same indexed orbitals from different spin sectors (for example, between $0\\alpha$ and $0\\beta$). Similarly, if the qubits are not physically adjacent on a QPU, these gates will also require SWAPs." + ] + }, + { + "cell_type": "markdown", + "id": "9afe0036-318c-43b9-9b2c-39a808bda82c", + "metadata": {}, + "source": [ + "![A circuit diagram showing 4 alpha qubits connected to the 4 beta qubits.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig5.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "6219aa8e-8a63-4bfc-b52b-64507292471d", + "metadata": {}, + "source": [ + "From the above discussion, the UCJ ansatz faces some hurdles for HW execution as it needs SWAP gates due to non-adjacent qubit interactions. The local variant of the UCJ ansatz, LUCJ, addresses this challenge by removing some $U_{nn}$ from the diagonal Coulomb operator.\n", + "\n", + "In the same electron species blocks, $J_{\\alpha \\alpha}$ and $J_{\\beta \\beta}$), we only keep the $U_{nn}$ gates compatible with nearest-neighbor connectivity and remove gates between non-adjacent qubits in the LUCJ version. Following figure shows the LUCJ block after removal of non-adjacent gates." + ] + }, + { + "cell_type": "markdown", + "id": "24f54a55-4a09-4508-997a-96306835c7e6", + "metadata": {}, + "source": [ + "![A circuit diagram showing 4 alpha qubits and 4 beta qubits each with R-Z gates, followed by two-qubit gates.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig6.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "165d0a19-0366-4af8-8ea0-20df351f49f5", + "metadata": {}, + "source": [ + "Next, the LUCJ version of the $J_{\\alpha \\beta}$ block that works between different electron species can take different shape based on the device topology.\n", + "\n", + "Here, also, the LUCJ version gets rid of non-compatible gates. The figure below shows variants of the $J_{\\alpha \\beta}$ block for different qubit topology including grid, hexagonal, heavy-hex, and linear.\n", + "\n", + "- **Square**: we can have $U_{nn}$ gates between all $\\alpha$ and $\\beta$ orbitals without any SWAPs, and therefore, do not need to remove any $U_{nn}$ gates.\n", + "- **Heavy-hex**: The $\\alpha$-$\\beta$ interactions are kept between every $4$-th indexed (such as the 0th, 4th, and 8th) spin orbitals and are _ancilla_ mediated, that is, we need ancilla qubits between the linear chains representing $\\alpha$ and $\\beta$ orbitals. This arrangement needs a limited number of SWAPs.\n", + "- **Hexagonal**: Every other orbital, such as 0th, 2nd, and 4th indexed orbitals, becomes nearest neighbors when $\\alpha$ and $\\beta$ are laid out in two adjacent linear chains.\n", + "- **Linear**: Only one $\\alpha$ and one $\\beta$ orbital are connected, which means the $J_{\\alpha \\beta}$ block will have only one gate." + ] + }, + { + "cell_type": "markdown", + "id": "8ec1e433-bc00-42bc-bdbb-288c56c32f9d", + "metadata": {}, + "source": [ + "![Connectivity diagrams for different qubit layouts. They show qubits arranged on a square grid, a hexagonal lattice, a heavy-hex lattice (hexagonal lattice with one extra qubit along each side of the hexagon), and a linear chain.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig7.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "3e070615-2b4a-4866-8c6c-d2d038447f45", + "metadata": {}, + "source": [ + "While removing gates from the UCJ ansatz to construct the LUCJ version makes it more HW compatible, the ansatz loses some expressivity. Therefore, more repetitions ($L$) of the modified UCJ operator may be needed when using the LUCJ ansatz." + ] + }, + { + "cell_type": "markdown", + "id": "04367dac", + "metadata": {}, + "source": [ + "### 1.2 LUCJ ansatz initialization" + ] + }, + { + "cell_type": "markdown", + "id": "f5eae55f", + "metadata": {}, + "source": [ + "The LUCJ is a parameterized ansatz, and we need to initialize the parameters before hardware execution. One way to initialize ansatz is by using `t1` and `t2` amplitudes from classical coupled cluster singles and doubles (CCSD) method, where `t1` amplitudes are the coefficient of single excitation operators and `t2` amplitudes are for double excitation operators.\n", + "\n", + "Note that while initializing the LUCJ ansatz with `t1` and `t2` amplitudes generate decent results, the ansatz parameters may need further optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1835e74e-3354-425f-8596-c574f03e7a6e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "E(CCSD) = -109.0398256929733 E_corr = -0.20458912219883\n" + ] + } + ], + "source": [ + "# Get CCSD t2 amplitudes for initializing the ansatz\n", + "ccsd = pyscf.cc.CCSD(\n", + " scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]\n", + ")\n", + "ccsd.run()\n", + "\n", + "t1 = ccsd.t1\n", + "t2 = ccsd.t2" + ] + }, + { + "cell_type": "markdown", + "id": "9f0842fa", + "metadata": {}, + "source": [ + "### 1.3 Constructing the LUCJ ansatz using `ffsim`" + ] + }, + { + "cell_type": "markdown", + "id": "e5f7f7e8-30e3-40e8-9b85-bf057b35b766", + "metadata": {}, + "source": [ + "We will use the [ffsim](https://github.com/qiskit-community/ffsim/tree/main) package to create and initialize the ansatz with `t1` and `t2` amplitudes computed above. Since our molecule has a closed-shell Hartree-Fock state, we will use the spin-balanced variant of the UCJ ansatz, [UCJOpSpinBalanced](https://qiskit-community.github.io/ffsim/api/ffsim.html#ffsim.UCJOpSpinBalanced).\n", + "\n", + "As IBM hardware has a heavy-hex topology, we will adopt the _zig-zag_ pattern used in [\\[1\\]](#references) and explained above for qubit interactions. In this pattern, orbitals (qubits) with the same spin are connected with a line topology (red and blue circles). Due to the heavy-hex topology, orbitals for different spins have connections between every 4th orbital, that is, the 0th, 4th, 8th, and so on (purple circles).\n", + "\n", + "![A zig-zag pattern traced out along a heavy-hex lattice.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig8.svg)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6799421a-4425-404a-9994-47215957054d", + "metadata": {}, + "outputs": [], + "source": [ + "import ffsim\n", + "from qiskit import QuantumCircuit, QuantumRegister\n", + "\n", + "n_reps = 2\n", + "alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)]\n", + "alpha_beta_indices = [(p, p) for p in range(0, num_orbitals, 4)]\n", + "\n", + "ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n", + " t2=t2,\n", + " t1=t1,\n", + " n_reps=n_reps,\n", + " interaction_pairs=(alpha_alpha_indices, alpha_beta_indices),\n", + ")\n", + "\n", + "nelec = (num_elec_a, num_elec_b)\n", + "\n", + "# create an empty quantum circuit\n", + "qubits = QuantumRegister(2 * num_orbitals, name=\"q\")\n", + "circuit = QuantumCircuit(qubits)\n", + "\n", + "# prepare Hartree-Fock state as the reference state and append it to the quantum circuit\n", + "circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits)\n", + "\n", + "# apply the UCJ operator to the reference state\n", + "circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n", + "circuit.measure_all()\n", + "# circuit.decompose().draw(\"mpl\", scale=0.5, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "3b2de316", + "metadata": {}, + "source": [ + "The LUCJ ansatz with repeated layers can be optimized by merging some adjacent blocks. Consider a case for `n_reps=2`. The two orbital rotation blocks in the middle can be merged into a single orbital rotation block. The `ffsim` package has a pass manager named `ffsim.qiskit.PRE_INIT` to optimize the circuit by merging such adjacent blocks." + ] + }, + { + "cell_type": "markdown", + "id": "7cb99cd9", + "metadata": {}, + "source": [ + "![A diagram showing layers of the LUCJ ansatz.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig9.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "e9ad460d", + "metadata": {}, + "source": [ + "## 2. Optimize for target hardware" + ] + }, + { + "cell_type": "markdown", + "id": "cecf2994", + "metadata": {}, + "source": [ + "First, we fetch a backend of our choice. We will optimize our circuit for the backend, and then execute the optimized circuit on the same backend to generate samples for the subspace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c8dbbef-378e-4ae3-9290-bde58b72024d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "# backend = service.backend(\"ibm_brisbane\")" + ] + }, + { + "cell_type": "markdown", + "id": "1b8ba388-6904-4725-ad97-2a31c0adaa07", + "metadata": {}, + "source": [ + "Next, we recommend the following steps to optimize the ansatz and make it hardware-compatible.\n", + "\n", + "- Select physical qubits (`initial_layout`) from the target hardware that adheres to the zig-zag pattern (two linear chains with ancilla qubit in-between them) described above. Laying out qubits in this pattern leads to an efficient hardware-compatible circuit with less gates.\n", + "- Generate a staged pass manager using the [`generate_preset_pass_manager`](/docs/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) function from Qiskit with your choice of `backend` and `initial_layout`.\n", + "- Set the `pre_init` stage of your staged pass manager to `ffsim.qiskit.PRE_INIT`. `ffsim.qiskit.PRE_INIT` includes Qiskit transpiler passes that decompose gates into orbital rotations and then merges the orbital rotations, resulting in fewer gates in the final circuit.\n", + "- Run the pass manager on your circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ecd2675c-a302-43fb-80f4-d658d56360d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gate counts (w/o pre-init passes): OrderedDict({'rz': 7579, 'sx': 6106, 'ecr': 2316, 'x': 336, 'measure': 32, 'barrier': 1})\n", + "Gate counts (w/ pre-init passes): OrderedDict({'rz': 4088, 'sx': 3125, 'ecr': 1262, 'x': 201, 'measure': 32, 'barrier': 1})\n" + ] + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "spin_a_layout = [0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, 62, 72, 81, 82]\n", + "spin_b_layout = [2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54, 64, 65, 66, 73]\n", + "\n", + "initial_layout = spin_a_layout + spin_b_layout\n", + "\n", + "pass_manager = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, initial_layout=initial_layout\n", + ")\n", + "\n", + "# without PRE_INIT passes\n", + "isa_circuit = pass_manager.run(circuit)\n", + "print(f\"Gate counts (w/o pre-init passes): {isa_circuit.count_ops()}\")\n", + "\n", + "# with PRE_INIT passes\n", + "# We will use the circuit generated by this pass manager for hardware execution\n", + "pass_manager.pre_init = ffsim.qiskit.PRE_INIT\n", + "isa_circuit = pass_manager.run(circuit)\n", + "print(f\"Gate counts (w/ pre-init passes): {isa_circuit.count_ops()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "72a780f9", + "metadata": {}, + "source": [ + "## 3. Execute on target hardware" + ] + }, + { + "cell_type": "markdown", + "id": "4cd9164c-697a-4aa6-b71f-86720e3d5b66", + "metadata": {}, + "source": [ + "After optimizing the circuit for hardware execution, we are ready to run it on the target hardware and collect samples for ground state energy estimation. As we only have one circuit, we will use Qiskit Runtime's [Job execution mode](/docs/guides/execution-modes) and execute our circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f542448-5767-4e12-85c8-dd8584545dbb", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "sampler.options.dynamical_decoupling.enable = True\n", + "\n", + "job = sampler.run([isa_circuit], shots=10_000) # Takes approximately 5sec of QPU time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c5eb3e9-2a5a-423b-ac18-7bba7f69f18f", + "metadata": {}, + "outputs": [], + "source": [ + "# Run cell after IQX job completion\n", + "primitive_result = job.result()\n", + "pub_result = primitive_result[0]\n", + "counts = pub_result.data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "e3ebf77a-8efd-43d6-bd19-e26423921c6f", + "metadata": {}, + "source": [ + "## 4. Post-process results" + ] + }, + { + "cell_type": "markdown", + "id": "1e7f0ecd", + "metadata": {}, + "source": [ + "The post-processing part of the SQD workflow can be summarized using the following diagram.\n", + "\n", + "![A flow chart showing how sampled states are used to determine ground state eigenvalues and eigenvectors.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig10.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "8c51a527", + "metadata": {}, + "source": [ + "Sampling the LUCJ ansatz in the computational basis generates a pool of noisy configurations $\\tilde{\\mathcal{\\chi}}$, which are used in the post-processing routine. It involves a method called (details discussed later) _configuration recovery_ to probabilistically correct configurations with incorrect electron numbers. Configurations only with correct electron numbers $\\tilde{\\mathcal{\\chi}}_{R}$ are then subsampled and distributed into multiple batches based on the frequency of appearance of each unique configuration. Each batch of samples defines a subspace ($\\mathcal{S^{(k)}}$). Next, the molecular Hamiltonian, $H$, is projected onto subspaces:\n", + "$$\n", + "H_{\\mathcal{S}^{(k)}} = P_{\\mathcal{S}^{(k)}} H _{\\mathcal{S}^{(k)}} \\text{ with } P_{\\mathcal{S}^{(k)}} = \\sum_{x \\in \\mathcal{S}^{(k)}} \\vert x \\rangle \\langle x \\vert\n", + "$$\n", + "\n", + "Each projected Hamiltonian $H_{\\mathcal{S}^{(k)}}$ is then fed into an Eigensolver, where it is diagonalized to compute eigenvalues and eigenvectors to reconstruct an eigenstate. In this lesson, we project and diagonalize the Hamiltonian using the `qiskit-addon-sqd` package which uses the Davidson's method from PySCF for diagonalization.\n", + "\n", + "$$\n", + "H_{\\mathcal{S}^{(k)}} \\vert \\psi^{(k)} \\rangle = E^{(k)} \\vert \\psi^{(k)} \\rangle\n", + "$$\n", + "\n", + "We then collect the lowest eigenvalue (energy) from the batches, and also compute average orbital occupancy, $\\text{n}$. The average occupancy information is used in the configuration recovery step to probabilistically correct noise configurations.\n", + "\n", + "Next, we explain the self-consistent configuration recovery loop in detail and show concrete code examples to implement the above-mentioned steps to estimate the ground state energy of $N_2$ Hamiltonian." + ] + }, + { + "cell_type": "markdown", + "id": "39997b5d-8bd3-4bc8-9ade-11fa5cfb34f9", + "metadata": {}, + "source": [ + "### 4.1 Configuration recovery: overview" + ] + }, + { + "cell_type": "markdown", + "id": "b05881ac-80ce-47a6-ac28-c1237f85bc0a", + "metadata": {}, + "source": [ + "Each bit in a bitstring (Slater determinant) represents a spin orbital. The right half of a bitstring represents spin-up orbitals, and the left half represents spin-down orbitals. A `1` means the orbital is occupied by an electron, and a `0` means the orbital is empty. We know the correct number of particles (both up-spin electron and down-spin electron) a priori. Suppose we have a determinant $x$ with $N_x$ electrons (that is, there are $N_x$ numbers of $1$s in the bitstring) in it. The correct number of particles is $N$. If $N_x \\neq N$, then we know that the bitstring is corrupted by noise. The self-consistent configuration routine attempts to correct the bitstring by probabilistically flipping $|N_x - N|$ bits by leveraging average orbital occupancy information. The average orbital occupancy ($n$) tells us how likely an orbital be occupied by an electron. If $N_x < N$, we have fewer electrons and need to flip some $0$s to $1$s and vice versa.\n", + "\n", + "The probability of flipping can be $|x[i] - avg\\_occupancy[i]|$ for `i`-th spin orbital. In [\\[2\\]](#references), the authors used a weighted probability of flipping using modified ReLU function.\n", + "\n", + "$$\n", + "\\begin{align}\n", + " w(y) = \\begin{cases}\n", + "\n", + " \\delta \\frac{y}{h} & \\text{if } y \\leq h\\\\ \\nonumber\n", + "\n", + " \\delta + (1 - \\delta) \\frac{y - h}{1 - h} & \\text{if } y > h\n", + "\n", + "\\end{cases}\n", + "\\end{align}\n", + "$$\n", + "\n", + "Here $h$ defines the location of \"corner\" of the ReLU function, and the parameter $\\delta$ defines the value of the ReLU function at the corner. For $\\delta = 0$, $w$ becomes true ReLU function, and for $\\delta >0$, it becomes _modified_ ReLU. In the paper, the authors used $\\delta = 0.01$ and $h =$ number of alpha (or beta) particles/number of alpha (or beta) spin orbitals $= N/M$ (filling factor).\n", + "\n", + "The average orbital occupancy ($n$) is not known a priori. The first iteration of the ground state estimation starts with configurations with only correct particle numbers in both spin species. After the first iteration, we have an estimate of the ground state, and using the estimate, we can construct the first guess of $n$. This guess of $n$ is used to recover configurations, run the next iteration of ground state estimation, and self-consistently refine the guess of $n$. The process repeats until the a stopping criterion is met.\n", + "\n", + "Consider the following example for $N = 2$ and $x = |1000\\rangle$ ($N_x = 1$). We need to flip one of the 0s to 1 to correct it for particle numbers, and the choices are `1100`, `1010`, and `1001`. Based on the probability of flipping, one of the choices will be selected as _recovered configuration_ (or the bitstring with correct number of particles).\n", + "\n", + "Suppose in the first iteration we run two batches, and the estimated ground states from them are:\n", + "\n", + "$$\n", + "\\begin{align}\\nonumber\n", + " \\text{Batch0: } \\vert \\psi \\rangle &= 0.8 \\times \\vert 1001 \\rangle + 0.6 \\times \\vert 0110 \\rangle \\\\ \\nonumber\n", + " \\text{Batch1: } \\vert \\psi \\rangle &= \\frac{1}{\\sqrt{3}} \\left( \\vert 1001 \\rangle + \\vert 0101 \\rangle + \\vert 0110 \\rangle \\right) \\nonumber\n", + "\\end{align}\n", + "$$\n", + "\n", + "Using the computational basis states and their amplitudes, we can compute probability of electron occupancies (in short _occupancies_) per spin-orbital (qubit) (note that probability = |amplitude|$^2$). Below we tabulate qubit-wise occupancies for each bitstring appearing in the estimated ground state and compute total orbital occupancy for a batch. Note that, as per Qiskit ordering convention, the right most bit represents qubit-0 (Q0), and the left most bit represents Q3.\n", + "\n", + "Occupancy (Batch0):\n", + "| | Q3 | Q2 | Q1 | Q0 |\n", + "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", + "| 1001 | 0.64 | 0.0 | 0.0 | 0.64 |\n", + "| 0110 | 0.0 | 0.36 | 0.36 | 0.0 |\n", + "| **n** _(Batch0)_ | **0.64** | **0.36** | **0.36** | **0.64** |\n", + "\n", + "Occupancy (Batch1)\n", + "| | Q3 | Q2 | Q1 | Q0 |\n", + "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", + "| 1001 | 0.33 | 0.00 | 0.00 | 0.33 |\n", + "| 0101 | 0.0 | 0.33 | 0.00 | 0.33 |\n", + "| 0110 | 0.0 | 0.33 | 0.33 | 0.00 |\n", + "| **n** _(Batch1)_ | **0.33** | **0.66** | **0.33** | **0.66** |\n", + "\n", + "Occupancy (average across batches)\n", + "| | Q3 | Q2 | Q1 | Q0 |\n", + "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", + "| **n** _(Batch0)_ | 0.64 | 0.36 | 0.36 | 0.64 |\n", + "| **n** _(Batch1)_ | 0.33 | 0.66 | 0.33 | 0.66 |\n", + "| **n** _(average)_ | **0.49** | **0.51** | **0.35** | **0.65** |\n", + "\n", + "Using the average orbital occupancy computed above, we can find the probabilities of flip for different orbitals in the configuration $x = \\vert 1000 \\rangle$. As the orbital represented by Q3 is already occupied and need not to be flipped, we set its p(flip) to $0$. For the remaining orbitals, which are unoccupied, the probability of flip is $\\vert x[i] - \\text{n}[i] \\vert$ each. Along with p(flip), we also compute the probability weight associated with flipping using the modified ReLU function described above.\n", + "\n", + "Probability of flip ($x = \\vert 1000 \\rangle$, $\\delta = 0.01$, $h = N/M = 2/4 = 0.50$)\n", + "| | Q3 | Q2 | Q1 | Q0 |\n", + "|:---------:|:--------:|:--------:|:--------:|:--------:|\n", + "| p(flip) ($\\vert x[i] - \\text{n}[i] \\vert$) | 0 | 0.51 | 0.35 | 0.65 |\n", + "| w(p(flip)) | 0 | 0.03 | 0.007 | 0.31 |\n", + "\n", + "Finally, using weighted probabilities above, we can flip one of the unoccupied Q2, Q1, and Q0 orbitals. Based on the values above, Q0 will be flipped most likely, and a possible recovered configuration can be $\\vert \\text{1001} \\rangle$." + ] + }, + { + "cell_type": "markdown", + "id": "c1940235", + "metadata": {}, + "source": [ + "![A diagram of configuration recovery.](/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig11.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "5c614290", + "metadata": {}, + "source": [ + "The complete self-consistent configuration recovery process can be summarized as follows:\n", + "\n", + "**First iteration:** Suppose the bitstrings (configurations or Slater determinants) generated by the quantum computer form a set $\\widetilde{\\chi}$, which includes both configurations with correct ($\\widetilde{\\chi}_{correct}$) and incorrect ($\\widetilde{\\chi}_{incorrect}$) number of particles in each spin sector.\n", + "1. Configurations from ($\\widetilde{\\chi}_{correct}$) are randomly sampled to create batches $(\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)})$ of vectors for subspace projection. The number of batches and samples in each batch are user defined parameters. The larger the number of samples in each batch, the larger the subspace dimension and more computationally demanding the diagonalization becomes. On the other hand, too small number of samples may miss the ground state support vectors and lead to incorrect estimation.\n", + "2. Run the eigenstate solver (that is, projection onto subspace and diagonalization) on the batches and obtain approximate eigenstates. $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n", + "3. From the approximate eigenstates construct the first guess for $n$.\n", + "\n", + "**Subsequent iterations:**\n", + "1. Using $n$ correct the configurations with wrong particle number in $\\widetilde{\\chi}_{incorrect}$. Suppose we name them $\\widetilde{\\chi}_{correct\\_new}$. Then, $\\widetilde{\\chi}_{recovered} (\\widetilde{\\chi}_{R}) = \\widetilde{\\chi}_{correct} \\cup \\widetilde{\\chi}_{correct\\_new}$ forms the new set of configurations with correct particle numbers.\n", + "2. $\\widetilde{\\chi}_{R}$ is sampled to create batches $\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)}$.\n", + "3. Eigenstate solver runs with new batches and generates new estimates of ground states $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n", + "4. From the approximate eigenstates construct refined guess for $n$.\n", + "5. If the stopping criterion is not met, go back to step `2.1`." + ] + }, + { + "cell_type": "markdown", + "id": "5eea548a", + "metadata": {}, + "source": [ + "### 4.2 Ground state estimation" + ] + }, + { + "cell_type": "markdown", + "id": "31dfe00e", + "metadata": {}, + "source": [ + "First, we will transform the counts into a bitstring matrix and probability array for post-processing.\n", + "\n", + "Each row in the matrix represents one unique bitstring. Since qubits are indexed from the right of a bitstring in Qiskit, column ``0`` represents qubit ``N-1``, and column ``N-1`` represents qubit ``0``, where ``N`` is the number of qubits.\n", + "\n", + "The alpha orbitals are represented in the column index range ``(N, N/2]`` (right half), and the beta orbitals are represented in the column range ``(N/2, 0]`` (left half)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "71550274", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_addon_sqd.counts import counts_to_arrays\n", + "\n", + "# Convert counts into bitstring and probability arrays\n", + "bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "acbdbdc5", + "metadata": {}, + "source": [ + "There are a few user-controlled options which are important for this technique:\n", + "\n", + "- ``iterations``: Number of self-consistent configuration recovery iterations\n", + "- ``n_batches``: Number of batches of configurations used by the different calls to the eigenstate solver\n", + "- ``samples_per_batch``: Number of unique configurations to include in each batch\n", + "- ``max_davidson_cycles``: Maximum number of Davidson cycles run by each eigensolver" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a2d22e0b-8f51-42a0-858f-ad0297cb0bae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting configuration recovery iteration 0\n", + " Batch 0 subspace dimension: 21609\n", + " Batch 1 subspace dimension: 21609\n", + " Batch 2 subspace dimension: 21609\n", + " Batch 3 subspace dimension: 21609\n", + " Batch 4 subspace dimension: 21609\n", + "Starting configuration recovery iteration 1\n", + " Batch 0 subspace dimension: 609961\n", + " Batch 1 subspace dimension: 616225\n", + " Batch 2 subspace dimension: 627264\n", + " Batch 3 subspace dimension: 633616\n", + " Batch 4 subspace dimension: 624100\n", + "Starting configuration recovery iteration 2\n", + " Batch 0 subspace dimension: 564001\n", + " Batch 1 subspace dimension: 605284\n", + " Batch 2 subspace dimension: 582169\n", + " Batch 3 subspace dimension: 559504\n", + " Batch 4 subspace dimension: 591361\n", + "Starting configuration recovery iteration 3\n", + " Batch 0 subspace dimension: 550564\n", + " Batch 1 subspace dimension: 549081\n", + " Batch 2 subspace dimension: 531441\n", + " Batch 3 subspace dimension: 527076\n", + " Batch 4 subspace dimension: 531441\n", + "Starting configuration recovery iteration 4\n", + " Batch 0 subspace dimension: 544644\n", + " Batch 1 subspace dimension: 580644\n", + " Batch 2 subspace dimension: 527076\n", + " Batch 3 subspace dimension: 531441\n", + " Batch 4 subspace dimension: 537289\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n", + "from qiskit_addon_sqd.fermion import (\n", + " bitstring_matrix_to_ci_strs,\n", + " solve_fermion,\n", + ")\n", + "from qiskit_addon_sqd.subsampling import postselect_and_subsample\n", + "\n", + "rng = np.random.default_rng(24)\n", + "# SQD options\n", + "iterations = 5\n", + "\n", + "# Eigenstate solver options\n", + "n_batches = 5\n", + "samples_per_batch = 500\n", + "max_davidson_cycles = 300\n", + "\n", + "# Self-consistent configuration recovery loop\n", + "e_hist = np.zeros((iterations, n_batches)) # energy history\n", + "s_hist = np.zeros((iterations, n_batches)) # spin history\n", + "occupancy_hist = []\n", + "avg_occupancy = None\n", + "for i in range(iterations):\n", + " print(f\"Starting configuration recovery iteration {i}\")\n", + " # On the first iteration, we have no orbital occupancy information from the\n", + " # solver, so we begin with the full set of noisy configurations.\n", + " if avg_occupancy is None:\n", + " bs_mat_tmp = bitstring_matrix_full\n", + " probs_arr_tmp = probs_arr_full\n", + "\n", + " # If we have average orbital occupancy information, we use it to refine the full set of noisy\n", + " # configurations\n", + " else:\n", + " bs_mat_tmp, probs_arr_tmp = recover_configurations(\n", + " bitstring_matrix_full,\n", + " probs_arr_full,\n", + " avg_occupancy,\n", + " num_elec_a,\n", + " num_elec_b,\n", + " rand_seed=rng,\n", + " )\n", + "\n", + " # Create batches of subsamples. We postselect here to remove configurations\n", + " # with incorrect hamming weight during iteration 0, since no config recovery was performed.\n", + " batches = postselect_and_subsample(\n", + " bs_mat_tmp,\n", + " probs_arr_tmp,\n", + " hamming_right=num_elec_a,\n", + " hamming_left=num_elec_b,\n", + " samples_per_batch=samples_per_batch,\n", + " num_batches=n_batches,\n", + " rand_seed=rng,\n", + " )\n", + "\n", + " # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n", + " e_tmp = np.zeros(n_batches)\n", + " s_tmp = np.zeros(n_batches)\n", + " occs_tmp = []\n", + " coeffs = []\n", + " for j in range(n_batches):\n", + " strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])\n", + " print(f\" Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}\")\n", + " energy_sci, coeffs_sci, avg_occs, spin = solve_fermion(\n", + " batches[j],\n", + " hcore,\n", + " eri,\n", + " open_shell=open_shell,\n", + " spin_sq=spin_sq,\n", + " max_davidson=max_davidson_cycles,\n", + " )\n", + " energy_sci += nuclear_repulsion_energy\n", + " e_tmp[j] = energy_sci\n", + " s_tmp[j] = spin\n", + " occs_tmp.append(avg_occs)\n", + " coeffs.append(coeffs_sci)\n", + "\n", + " # Combine batch results\n", + " avg_occupancy = tuple(np.mean(occs_tmp, axis=0))\n", + "\n", + " # Track optimization history\n", + " e_hist[i, :] = e_tmp\n", + " s_hist[i, :] = s_tmp\n", + " occupancy_hist.append(avg_occupancy)" + ] + }, + { + "cell_type": "markdown", + "id": "e4f6ec2c-032d-4e18-8ea4-cf867cba5054", + "metadata": {}, + "source": [ + "### 4.3 Discussion of results" + ] + }, + { + "cell_type": "markdown", + "id": "592eabb7-18f3-4622-8710-bfc43ad6cdec", + "metadata": {}, + "source": [ + "The first plot shows that after a few iterations we estimate the ground state energy within ~24 mH (chemical accuracy is typically accepted to be 1 kcal/mol $\\approx$ 1.6 mH). The second plot shows the average occupancy of each spatial orbital after the final iteration. We can see that both the spin-up and spin-down electrons occupy the first five orbitals with high probability in our solutions.\n", + "\n", + "Although the estimated ground state energy is decent, it is not within the chemical accuracy limit ($\\pm \\approx 1.6$ mH). This gap can be attributed to the small subspace dimension we used above for projection and diagonalization. As we used `samples_per_batch=500`, the subspace is spanned by max $500$ vectors, which is missing vectors from ground state support. Increasing the `samples_per_batch` parameter should improve the accuracy at the expense of more classical compute resources and runtime." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "61959e69-a182-4636-abcb-a32349fc9076", + "metadata": {}, + "outputs": [], + "source": [ + "# Data for energies plot\n", + "x1 = range(iterations)\n", + "min_e = [np.min(e) for e in e_hist]\n", + "e_diff = [abs(e - exact_energy) for e in min_e]\n", + "yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5]\n", + "\n", + "# Chemical accuracy (+/- 1 milli-Hartree)\n", + "chem_accuracy = 0.001\n", + "\n", + "# Data for avg spatial orbital occupancy\n", + "y2 = occupancy_hist[-1][0] + occupancy_hist[-1][1]\n", + "x2 = range(len(y2))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "8cd90034-6ef3-41bd-a847-c115cade82f7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exact energy: -109.04667 Ha\n", + "SQD energy: -109.02234 Ha\n", + "Absolute error: 0.02434 Ha\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axs = plt.subplots(1, 2, figsize=(12, 6))\n", + "\n", + "# Plot energies\n", + "axs[0].plot(x1, e_diff, label=\"energy error\", marker=\"o\")\n", + "axs[0].set_xticks(x1)\n", + "axs[0].set_xticklabels(x1)\n", + "axs[0].set_yticks(yt1)\n", + "axs[0].set_yticklabels(yt1)\n", + "axs[0].set_yscale(\"log\")\n", + "axs[0].set_ylim(1e-6)\n", + "axs[0].axhline(\n", + " y=chem_accuracy, color=\"#BF5700\", linestyle=\"--\", label=\"chemical accuracy\"\n", + ")\n", + "axs[0].set_title(\"Approximated Ground State Energy Error vs SQD Iterations\")\n", + "axs[0].set_xlabel(\"Iteration Index\", fontdict={\"fontsize\": 12})\n", + "axs[0].set_ylabel(\"Energy Error (Ha)\", fontdict={\"fontsize\": 12})\n", + "axs[0].legend()\n", + "\n", + "# Plot orbital occupancy\n", + "axs[1].bar(x2, y2, width=0.8)\n", + "axs[1].set_xticks(x2)\n", + "axs[1].set_xticklabels(x2)\n", + "axs[1].set_title(\"Avg Occupancy per Spatial Orbital\")\n", + "axs[1].set_xlabel(\"Orbital Index\", fontdict={\"fontsize\": 12})\n", + "axs[1].set_ylabel(\"Avg Occupancy\", fontdict={\"fontsize\": 12})\n", + "\n", + "print(f\"Exact energy: {exact_energy:.5f} Ha\")\n", + "print(f\"SQD energy: {min_e[-1]:.5f} Ha\")\n", + "print(f\"Absolute error: {e_diff[-1]:.5f} Ha\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e1530472", + "metadata": {}, + "source": [ + "#### Exercise for the reader\n", + "Progressively increase the `samples_per_batch` parameter (for example, from $1000$ to $10000$ at a step of $1000$; permitted my your computer's memory) and compare the estimated ground state energies." + ] + }, + { + "cell_type": "markdown", + "id": "d2b2241f", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "\\[1] M. Motta et al., “Bridging physical intuition and hardware efficiency for correlated electronic states: the local unitary cluster Jastrow ansatz for electronic structure” (2023). [Chem. Sci., 2023, 14, 11213](https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k).\n", + "\n", + "\\[2] J. Robledo-Moreno et al., \"Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer\" (2024). [arXiv:quant-ph/2405.05068](https://arxiv.org/abs/2405.05068)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb b/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb index 9dd055a2cf7..f6565276a8f 100644 --- a/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb @@ -1,1090 +1,1092 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "6581d769-6f75-4b26-90e3-39c28f22a74c", - "metadata": {}, - "source": [ - "---\n", - "title: Variational Quantum Eigensolver\n", - "description: This introduction to VQE covers its components, a basic implementation, and discusses what factors determine its efficiency and usefulness.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore ansä IIIZ IZII IIZI ZIII IZIZ IIZZ ZIIZ IZZI ZZII ZIZI XXYY YYXX ansätze ansatze infty IZZX XIYX ZZXZ ZZZX IIXX IIXZ IZXZ IXXZ XZXZ ZIXZ */}\n", - "\n", - "# The variational quantum eigensolver (VQE)\n", - "\n", - "This lesson will introduce the variational quantum eigensolver, explain its importance as a foundational algorithm in quantum computing, and also explore its strengths and weaknesses. VQE by itself, without augmenting methods, is not likely to be sufficient for modern utility scale quantum computations. It is nevertheless important as an archetypal classical-quantum hybrid method, an it is an important foundation upon which many more advanced algorithms are built.\n", - "\n", - "This video gives an overview of VQE and factors that affect its efficiency. The text below adds more detail and implements VQE using Qiskit.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "21115450-3f01-4e19-860d-48e31f401b1f", - "metadata": {}, - "source": [ - "## 1. What is VQE?\n", - "\n", - "The variational quantum eigensolver is an algorithm that uses classical and quantum computing in conjunction to accomplish a task. There are four main components of a VQE calculation:\n", - "\n", - "* __An operator__: Often a Hamiltonian, which we’ll call $H$, that describes a property of your system that you wish to optimize. Another way of saying this is that you are seeking the eigenvector of this operator that corresponds to the minimum eigenvalue. We often call that eigenvector the “ground state”.\n", - "* __An “ansatz”__ (a German word meaning “approach”): this is a quantum circuit that prepares a quantum state approximating the eigenvector you’re seeking. Really the ansatz is a family of quantum circuits, because some of the gates in the ansatz are parametrized, that is, they are fed a parameter which we can vary. This family of quantum circuits can prepare a family of quantum states approximating the ground state.\n", - "* __An Estimator__: a means of estimating the expectation value of the operator $H$ over the current variational quantum state. Sometimes what we really care about is simply this expectation value, which we call a cost function. Sometimes, we care about a more complicated function that can still be written starting from one or more expectation values.\n", - "* __A classical optimizer__: an algorithm that varies parameters to try to minimize the cost function.\n", - "\n", - "Let's look at each of these components in more depth." - ] - }, - { - "cell_type": "markdown", - "id": "7dbd605e-a440-446b-bc05-e2e301796bf2", - "metadata": {}, - "source": [ - "### 1.1 The operator (Hamiltonian)\n", - "\n", - "At the core of a VQE problem is an operator that describes a system of interest. We will assume here that the lowest eigenvalue and the corresponding eigenvector of this operator are useful for some scientific or business purpose. Examples might include a chemical Hamiltonian describing a molecule, such that the lowest eigenvalue of the operator corresponds to the ground state energy of the molecule, and the corresponding eigenstate describes the geometry or electron configuration of the molecule. Or the operator could describe a cost of a certain process to be optimized, and the eigenstates could correspond to routes or practices. In some fields, like physics, a \"Hamiltonian\" almost always refers to an operator describing the energy of a physical system. But in quantum computing, it is common to see quantum operators that describe a business or logistical problem also referred to as a \"Hamiltonian\". We will adopt that convention here.\n", - "\n", - "![An image of atomic orbitals and an image of a network of many nodes and connections between them.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig1.svg)\n", - "\n", - "Mapping a physical or optimization problem to qubits is typically a non-trivial task, but those details are not the focus of this course. A general discussion of mapping a problem to a quantum operator can be found in [Quantum computing in practice](/learning/courses/quantum-computing-in-practice). A more detailed look at the mapping of chemistry problems into quantum operators can be found in [Quantum Chemistry with VQE](/learning/courses/quantum-chem-with-vqe).\n", - "\n", - "For the purposes of this course, we will assume the form of the Hamiltonian is known. For example, a Hamiltonian for a simple hydrogen molecule (under certain active space assumptions, and using the Jordan-Wigner mapper) is:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "89b425d8-f54f-4f98-996e-c303f77edb25", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "hamiltonian = SparsePauliOp(\n", - " [\n", - " \"IIII\",\n", - " \"IIIZ\",\n", - " \"IZII\",\n", - " \"IIZI\",\n", - " \"ZIII\",\n", - " \"IZIZ\",\n", - " \"IIZZ\",\n", - " \"ZIIZ\",\n", - " \"IZZI\",\n", - " \"ZZII\",\n", - " \"ZIZI\",\n", - " \"YYYY\",\n", - " \"XXYY\",\n", - " \"YYXX\",\n", - " \"XXXX\",\n", - " ],\n", - " coeffs=[\n", - " -0.09820182 + 0.0j,\n", - " -0.1740751 + 0.0j,\n", - " -0.1740751 + 0.0j,\n", - " 0.2242933 + 0.0j,\n", - " 0.2242933 + 0.0j,\n", - " 0.16891402 + 0.0j,\n", - " 0.1210099 + 0.0j,\n", - " 0.16631441 + 0.0j,\n", - " 0.16631441 + 0.0j,\n", - " 0.1210099 + 0.0j,\n", - " 0.17504456 + 0.0j,\n", - " 0.04530451 + 0.0j,\n", - " 0.04530451 + 0.0j,\n", - " 0.04530451 + 0.0j,\n", - " 0.04530451 + 0.0j,\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "14c20ee9-51ad-4cc9-be36-7a33fae6c56a", - "metadata": {}, - "source": [ - "Note that in the Hamiltonian above, there are terms like `ZZII` and `YYYY` that do not commute with each other. That is, to evaluate `ZZII`, we would need to measure the Pauli Z operator on qubit 3 (among other measurements). But to evaluate `YYYY`, we need to measure the Pauli Y operator on that same qubit, qubit 3. There is an uncertainty relation between Y and Z operators on the same qubit; we cannot measure both of those operators at the same time. We will revisit this point below, and indeed throughout the course.\n", - "The Hamiltonian above is a $16\\times 16$ matrix operator. Diagonalizing the operator to find its lowest energy eigenvalue is not difficult." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9703b61e-ff90-4e94-8999-242d0c6766c0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The ground state energy is -1.1459778447627311 hartrees\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "A = np.array(hamiltonian)\n", - "eigenvalues, eigenvectors = np.linalg.eigh(A)\n", - "print(\"The ground state energy is \", min(eigenvalues), \"hartrees\")" - ] - }, - { - "cell_type": "markdown", - "id": "01a76c42-29e7-455a-acd6-55490f36121b", - "metadata": {}, - "source": [ - "Brute force classical eigensolvers cannot scale to describe the energies or geometries of very large systems of atoms, like medications or proteins. VQE is one of the early attempts to leverage quantum computing in this problem.\n", - "\n", - "We will encounter Hamiltonians in this lesson much larger than that above. But it would be wasteful to push the limits of what VQE can do, before we introduce some of the more advanced tools that can augment or replace VQE, later in this course.\n", - "\n", - "### 1.2 Ansatz\n", - "\n", - "The word \"ansatz\" is German for \"approach\". The correct plural in German is \"ansätze\", though one often sees \"ansatzes\" or \"ansatze\". In the context of VQE, an ansatz is the quantum circuit you use to create a multi-qubit wave function that most closely approximates the ground state of the system you are studying, and which thus produces the lowest expectation value of your operator. This quantum circuit will contain variational parameters (often collected together in the vector of variables $\\vec{\\theta}$).\n", - "\n", - "![An image of a quantum circuit with variational parameters labeled \"theta\".](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig2.svg)\n", - "\n", - "An initial set of values $\\vec{\\theta_0}$ of the variational parameters is chosen. We will call the unitary operation of the ansatz on the circuit $U_{\\text{var}}(\\vec{\\theta_0})$. By default, all qubits in IBM® quantum computers are initialized to the $|0\\rangle$ state. When the circuit is run, the state of the qubits will be\n", - "\n", - "$$\n", - "|\\psi(\\vec{\\theta_0})\\rangle=U_{\\text{var}}(\\vec{\\theta_0})|0\\rangle^{\\otimes N}\n", - "$$\n", - "\n", - "If all we needed were the lowest energy (using the language of physical systems), we could estimate this by simply measuring the energy many times and taking the lowest. But we typically also want the configuration that yields that lowest energy or eigenvalue. So the next step is the estimation of the expectation value of the Hamiltonian, which is accomplished through quantum measurements. A lot goes into that. But we can understand this process qualitatively by noting that the probability $P_j$ of measuring an energy $E_j$ (again using the language of physical systems) is related to the expectation value by:\n", - "\n", - "$$\n", - "\\langle \\psi(\\vec{\\theta_0}) |H|\\psi (\\vec{\\theta_0}) \\rangle\n", - "$$\n", - "\n", - "The probability $P_j$ is also related to the overlap between the eigenstate $|\\phi_j\\rangle$ and the current state of the system $|\\psi(\\vec{\\theta_0})\\rangle$:\n", - "\n", - "$$\n", - "P_j=|\\langle \\phi_j|\\psi(\\vec{\\theta_0})\\rangle|^2 = |\\langle \\phi_j|U_{\\text{var}}(\\vec{\\theta_0})|0\\rangle^{\\otimes N}|^2\n", - "$$\n", - "\n", - "So by making many measurements of the Pauli operators making up our Hamiltonian, we can estimate the Hamiltonian's expectation value in the current state of the system $|\\psi(\\vec{\\theta_0})\\rangle$. The next step is to vary the parameters $\\vec{\\theta}$ and try to more closely approach the lowest-energy (ground) state of the system. Because of the variational parameters in the ansatz, one sometimes hears it referred to as the __variational form__.\n", - "\n", - "Before we move on to that variational process, note that it is often useful to start your state from a \"good guess\" state. You might know enough about your system to make a better initial guess than $|0\\rangle^{\\otimes N}$. For example, it is common to initialize qubits to the Hartree-Fock state in chemical applications. This starting guess which does not contain any variational parameters is called the __reference state__. Let us call the quantum circuit used to create reference state $U_{ref}$. Whenever it becomes important to distinguish the reference state from the rest of the ansatz, use: $U_{\\text{ansatz}}(\\vec{\\theta}) =U_{\\text{var}}(\\vec{\\theta})U_{\\text{ref}}.$ Equivalently\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "|\\psi_{\\text{ref}}\\rangle&=U_{\\text{ref}}|0\\rangle^{\\otimes N}\\\\\n", - "|\\psi_{\\text{ansatz}}(\\vec{\\theta})\\rangle&=U_{var}(\\vec{\\theta})|\\psi_{\\text{ref}}\\rangle = U_{\\text{var}}(\\vec{\\theta})U_{\\text{ref}}|0\\rangle^{\\otimes N}.\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "b7f66a1f-93cb-47ee-81c3-6fbf62392003", - "metadata": {}, - "source": [ - "### 1.3 Estimator\n", - "\n", - "We need a way to estimate the expectation value of our Hamiltonian in a particular variational state $|\\psi(\\vec{\\theta})\\rangle$. If we could directly measure the entire operator $H$, this would be as simple as making many (say $N$) measurements and averaging the measured values:\n", - "\n", - "$$\n", - "\\langle \\psi(\\vec{\\theta})|H|\\psi(\\vec{\\theta})\\rangle _N \\approx \\frac{1}{N}\\sum_{j=1}^N {E_j}\n", - "$$\n", - "Here, the $\\approx$ symbol reminds us that this expectation value would only be precisely correct in the limit as $N\\rightarrow \\infty$. But with thousands of measurements being made on a circuit, the sampling error of the expectation value is fairly low. There are other considerations such as noise that become an issue for very precise calculations.\n", - "\n", - "However, it is generally not possible to measure $H$ all at once. $H$ may contain multiple non-commuting Pauli X, Y, and Z operators. So the Hamiltonian must be broken up into groups of operators that can be simultaneously measured, and each such group must be estimated separately, and the results combined to obtain an expectation value. We will revisit this in greater detail in the next lesson, when we discuss the scaling of classical and quantum approaches. This complexity in measurement is one reason we need highly efficient code for carrying out such estimation. In this lesson and beyond, we will use the Qiskit Runtime primitive Estimator for this purpose." - ] - }, - { - "cell_type": "markdown", - "id": "8b68d38e-80ad-4e02-815f-576806ec3e1c", - "metadata": {}, - "source": [ - "### 1.4 Classical optimizers\n", - "\n", - "A classical optimizer is any classical algorithm designed to find extrema of a target function (typically a minimum). They search through the space of possible parameters looking for a set that minimizes some function of interest. They can be broadly categorized into gradient-based methods, which utilize gradient information, and gradient-free methods, which operate as black-box optimizers. The choice of classical optimizer can significantly impact an algorithm's performance, especially in the presence of noise in quantum hardware. Popular optimizers in this field include Adam, AMSGrad, and SPSA, which have shown promising results in noisy environments. More traditional optimizers include COBYLA and SLSQP.\n", - "\n", - "A common workflow (demonstrated in Section 3.3) is to use one of these algorithms as the method inside a minimizer like scipy's ```minimize``` function. This takes as its arguments:\n", - "* Some function to be minimized. This is often the energy expectation value. But these are generally referred to as \"cost functions\".\n", - "* A set of parameters from which to begin the search. Often called $x_0$ or $\\theta_0$.\n", - "* Arguments, including arguments of the cost function. In quantum computing with Qiskit, these arguments will include the ansatz, the Hamiltonian, and the Estimator primitive, which is discussed more in the next subsection.\n", - "* A 'method' of minimization. This refers to the specific algorithm used to search the parameter space. This is where we would specify, for example, COBYLA or SLSQP.\n", - "* Options. The options available may differ by method. But an example which practically all methods would include is the maximum number of iterations of the optimizer before ending the search: 'maxiter'.\n", - "\n", - "![An image showing a curved line representing energy with several points at which the value is being tested to find the minimum.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig3.svg)\n", - "\n", - "At each iterative step, the expectation value of the Hamiltonian is estimated by making many measurements. This estimated energy is returned by the cost function, and the minimizer updates the information it has about the energy landscape. Exactly what the optimizer does to choose the next step varies from method to method. Some use gradients and select the direction of steepest descent. Others may take noise into account and may require that the cost decrease by a large margin before accepting that the true energy decreases along that direction." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "9e0642f5-8127-4647-b3f2-deceff0651ec", - "metadata": {}, - "outputs": [], - "source": [ - "# Example syntax for minimization\n", - "# from scipy.optimize import minimize\n", - "# res = minimize(cost_func, x0, args=(ansatz, hamiltonian, estimator), method=\"cobyla\", options={'maxiter': 200})" - ] - }, - { - "cell_type": "markdown", - "id": "5bbcff76-ea91-4fb6-acca-35e9e6675ed1", - "metadata": {}, - "source": [ - "### 1.5 The variational principle\n", - "\n", - "In this context the variational principle is very important; it states that no variational wave function can yield an energy (or cost) expectation value lower than that yielded by the ground state wave function. Mathematically,\n", - "$$\n", - "E_\\text{var}=\\langle \\psi_\\text{var}|H|\\psi_\\text{var}\\rangle \\geq E_\\text{min}=\\langle \\psi_\\text{0}|H|\\psi_\\text{0}\\rangle\n", - "$$\n", - "This is easy to verify if we note that the set of all eigenstates $\\{|\\psi_0\\rangle, |\\psi_1\\rangle, |\\psi_2\\rangle, ...|\\psi_n \\rangle\\}$ of $H$ form a complete basis for the Hilbert space. In other words, any state and in particular $|\\psi_\\text{var}\\rangle$ can be written as a weighted (normalized) sum of these eigenstates of $H$:\n", - "$$\n", - "|\\psi_\\text{var}\\rangle=\\sum_{i=0}^n c_i |\\psi_i\\rangle\n", - "$$\n", - "where $c_i$ are constants to be determined, and $\\sum_{i=0} |c_i|^2 = 1$. We leave this as an exercise to the reader. But note the implication: the variational state that produces the lowest-energy expectation value *is* the best estimate of the true ground state.\n", - "\n", - "#### Check your understanding\n", - "\n", - "Verify mathematically that $E_\\text{var}\\geq E_0$ for any variational state $|\\psi_\\text{var}\\rangle$.\n", - "\n", - "\n", - "\n", - "\n", - "Using the given expansion of the variational state in terms of the energy eigenstates,\n", - "$$\n", - "|\\psi_\\text{var}\\rangle=\\sum_{i=0}^n c_i |\\psi_i\\rangle,\n", - "$$\n", - "we can write the variational energy expectation value as\n", - "$$\n", - "\\begin{aligned}\n", - "E_\\text{var}&=\\langle \\psi_\\text{var}|H|\\psi_\\text{var}\\rangle =\\left(\\sum_{i=0}^n c^*_i \\langle \\psi_i|\\right)H\\left(\\sum_{j=0}^n c_j |\\psi_j\\rangle\\right)\\\\\n", - "&=\\left(\\sum_{i=0}^n c^*_i \\langle \\psi_i|\\right)\\left(\\sum_{j=0}^n c_j E_j|\\psi_j\\rangle\\right)\\\\\n", - "&=\\sum_{i,j=0}^n c^*_i c_j E_j \\langle \\psi_i|\\psi_j\\rangle\\\\\n", - "&=\\sum_{i,j=0}^n c^*_i c_j E_j \\delta_{i,j}\\\\\n", - "&=\\sum_{i=0}^n |c_i|^2 E_i.\n", - "\\end{aligned}\n", - "$$\n", - "For all coefficients $0\\leq|c_i|^2\\leq 1$. So we can write\n", - "$$\n", - "\\begin{aligned}\n", - "E_\\text{var}&=\\sum_{i=0}^n |c_i|^2 E_i\\geq \\sum_{i=0}^n |c_i|^2 E_0 = E_0 \\sum_{i=0}^n |c_i|^2 = E_0(1) \\\\\n", - "E_\\text{var}&\\geq E_0\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "4d14d6df-7320-4f57-bc62-fbee4041957b", - "metadata": {}, - "source": [ - "## 2. Comparison with classical workflow\n", - "\n", - "Let’s say we are interested in a matrix with N rows and N columns. Suppose your matrix is so large that exact diagonalization is not an option. Suppose further that you know enough about your problem that you can make some guesses about the overall structure of the target eigenstate, and you want to probe states similar to your initial guess to see if your cost/energy can be lowered further. This is a variational approach, and it is one method that is used when exact diagonalization is not an option.\n", - "\n", - "### 2.1 Classical workflow\n", - "\n", - "Using a classical computer, this would work as follows:\n", - "* Make a guess state, with some parameters $\\vec{\\theta}_i$ that you will vary: $|\\psi(\\vec{\\theta}_i)\\rangle$. Although this initial guess could be random, that is not advisable. We want to use knowledge of the problem at hand to tailor our guess as much as possible.\n", - "* Calculate the expectation value of the operator with the system in that state: $\\langle\\psi(\\vec{\\theta}_i)|H|\\psi(\\vec{\\theta}_i)\\rangle$\n", - "* Alter the variational parameters and repeat: $\\vec{\\theta}_i\\rightarrow \\vec{\\theta}_{i+1}$.\n", - "* Use accumulated information about the landscape of possible states in your variational subspace to make better and better guesses and approach the target state. The variational principle guarantees that our variational state cannot yield an eigenvalue lower than that of the target ground state. So the lower the expectation value the better our approximation of the ground state:\n", - "$$\n", - "\\min_{\\vec{\\theta}} \\{ E_{\\text{var},i} = \\langle\\psi(\\vec{\\theta_i})|H|\\psi(\\vec{\\theta_i})\\rangle \\} \\geq E_0\n", - "$$\n", - "Let us examine the difficulty of each step in this approach. Setting or updating parameters is computationally easy; the difficulty there is in selecting useful, physically motivated initial parameters. Using accumulated information from prior iterations to update parameters in such a way that you approach the ground state is a non-trivial. But classical optimization algorithms exist that do this quite efficiently. This classical optimization is only expensive because it may require many iterations; in the worst case, the number of iterations may scale exponentially with N. The most computationally expensive single step is almost certainly calculating the expectation value of your matrix using a given state $|\\psi(\\vec{\\theta_i})\\rangle$: $\\langle\\psi(\\vec{\\theta_i})|H|\\psi(\\vec{\\theta_i})\\rangle.$\n", - "\n", - "The $N\\times N$ matrix must act on the $N$-element vector, which corresponds to: $O(N^2)$ multiplication operations in the worst case. This must be done at each iteration of parameters. For extremely large matrices, this has high computational cost.\n", - "\n", - "### 2.2 Quantum workflow and commuting Pauli groups\n", - "\n", - "Now imagine relegating this portion of the calculation to a quantum computer. Instead of calculating this expectation value, you estimate it by preparing the state $|\\psi(\\vec{\\theta_i})\\rangle$ on the quantum computer using your variational ansatz, and then making measurements.\n", - "\n", - "That may sound easier than it is. $H$ is generally not easy to measure. For example it could be made up of many non-commuting Pauli X, Y, and Z operators. But $H$ __can__ be written as a linear combination of terms, $h_\\alpha$, each of which is easily measurable (for example, Pauli operators or groups of qubit-wise commuting Pauli operators).\n", - "The expectation value of $H$ over some state $|\\Psi\\rangle$ is the weighted sum of expectation values of the constituent terms $h_\\alpha$. This expression holds for any state $|\\Psi⟩$, but we will specifically be using this with our variational states $|\\psi(\\theta_i)\\rangle$.\n", - "\n", - "$$\n", - "H = \\sum_{\\alpha = 1}^T{c_\\alpha h_\\alpha}\n", - "$$\n", - "where $h_\\alpha$ is a Pauli string like `IZZX…XIYX`, or several such strings that commute with each other. So a description of the expectation value that more closely matches the realities of measurement on quantum computers is\n", - "$$\n", - "\\langle \\Psi |H|\\Psi \\rangle =\\sum_{\\alpha} c_\\alpha \\langle \\Psi | h_\\alpha|\\Psi \\rangle.\n", - "$$\n", - "And in the context of our variational wave function:\n", - "$$\n", - "\\langle \\psi(\\vec{\\theta}_i) |H|\\psi(\\vec{\\theta}_i) \\rangle =\\sum_{\\alpha} c_\\alpha \\langle \\psi(\\vec{\\theta}_i) | h_\\alpha|\\psi(\\vec{\\theta}_i) \\rangle\n", - "$$\n", - "Each of the terms $h_\\alpha$ can be measured $M$ times yielding measurement samples $s_{\\alpha j}$ with $j=1…M$ and returns an expectation value $\\mu_\\alpha$ and a standard deviation $\\sigma_\\alpha$. We can sum these terms and propagate errors through the sum to obtain an overall expectation value $\\mu$ and standard deviation $\\sigma$.\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\langle \\psi(\\vec{\\theta}_i) |h_\\alpha|\\psi(\\vec{\\theta}_i) \\rangle &\\simeq \\mu _\\alpha \\pm \\frac{\\sigma_\\alpha}{\\sqrt{M}} &\\qquad \\mu_\\alpha &=\\frac{1}{M}\\sum_j s_{\\alpha,j} &\\qquad \\sigma^2_\\alpha &=\\frac{1}{M-1}\\sum_j (s_{\\alpha,j}-\\mu_\\alpha)^2\\\\\n", - "\n", - "\\langle \\psi(\\vec{\\theta}_i) |H|\\psi(\\vec{\\theta}_i) \\rangle &\\simeq \\mu \\pm \\sigma &\\qquad \\mu &= \\sum_\\alpha c_\\alpha \\mu_\\alpha &\\qquad \\sigma^2&=\\sum_\\alpha c^2_\\alpha \\frac{\\sigma^2_\\alpha }{M}\n", - "\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "ab02194a-bb0e-4bbc-81e1-c5f8f9582d9b", - "metadata": {}, - "source": [ - "This requires no large-scale multiplication, nor any process that necessarily scales like $N^2$. Instead it requires multiple measurements on the quantum computer. If you don’t need too many of those, this approach could be efficient. And that’s the quantum part of VQE.\n", - "\n", - "But let’s talk about reasons why this might not be efficient. One reason for many measurements is to reduce the statistical uncertainty in your estimates, for very high-precision calculations. Another reason is the number of Pauli strings required to span your entire matrix. Because Pauli matrices (plus the identity: X, Y, Z, and I) span the space of all operators of a given dimension, we are guaranteed that we can write our matrix of interest as a weighted sum of Pauli operators, as we did before.\n", - "$$\n", - "H = \\sum_{\\alpha = 1}^T{c_\\alpha h_\\alpha}\n", - "$$\n", - "where $h_\\alpha$ is a Pauli string acting on all the qubits describing your system like `IZZX…XIYX`, or several such strings that commute with each other. Recall that Qiskit uses *little endian* notation, in which the $n^\\text{th}$ Pauli operator from the right acts on the $n^\\text{th}$ qubit. So we can measure our operator by measuring a series of Pauli operators.\n", - "\n", - "But we cannot measure all those Pauli operators simultaneously. Pauli operators (excluding I) do not commute with each other if they are associated with the same qubit. For example, we can measure `IZIZ` and `ZZXZ` simultaneously, because we can measure I and Z simultaneously for the 3rd qubit, and we can know I and X simultaneously for the 1st qubit. But we cannot measure ZZZZ and ZZZX simultaneously, because Z and X do not commute, and both act on the 0th qubit.\n", - "\n", - "![A table of different Pauli strings, some of which commute and others which do not.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig4.svg)\n", - "\n", - "So we decompose our matrix $H$ into a sum of Paulis acting on different qubits. Some elements of that sum can be measured all at once; we call this a *group of commuting Paulis*. Depending on how many non-commuting terms there are, we may need many such groups. Call the number of such groups of commuting Pauli strings $N_\\text{GCP}$. If $N_\\text{GCP}$ is small, this could work well. If $H$ has millions of groups, this will not be useful.\n", - "\n", - "The processes required for estimation of the expectation value are collected together in the Qiskit Runtime primitive called Estimator. To learn more about Estimator, see the [API reference](/docs/api/qiskit-ibm-runtime/estimator-v2) in IBM Quantum® Documentation. One can simply use Estimator directly, but Estimator returns much more than just the lowest energy eigenvalue. For example, it also returns information on ensemble standard error. Thus, in the context of minimization problems, one often sees Estimator inside a cost function. To learn more about Estimator inputs and outputs see this [guide](/docs/guides/primitive-input-output#pubs) on IBM Quantum Documentation.\n", - "\n", - "You record the expectation value (or the cost function) for the set of parameters $\\vec{\\theta_i}$ used in your state, and then you update the parameters. Over time, you could use the expectation values or cost-function values you’ve estimated to approximate a gradient of your cost function in the subspace of states sampled by your ansatz. Both gradient-based, and gradient-free classical optimizers exist. Both suffer from potential trainability issues, like multiple local minima, and large regions of parameter space with near-zero gradient, called *barren plateaus*.\n", - "\n", - "![Two images of a curved line with a minimum value. In one, points are randomly checked in the search for a minimum, in the other a gradient is estimated by drawing a line between two adjacent points.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig5.svg)\n", - "\n", - "### 2.3 Factors that determine computational cost\n", - "\n", - "VQE will not solve all your toughest quantum chemistry problems. No. But being better at all calculations is not the point. We have shifted what determines the computational cost.\n", - "\n", - "![A table comparing classical and quantum variational approaches. Both require good initial guesses. Classically, the cost scales like the dimension of your matrix squared, and in the quantum approach it depends on how many groups of commuting Pauli operators you have.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig6.svg)\n", - "\n", - "We’ve shifted from a process whose complexity depends only on matrix dimension to one that depends on required precision and the number of non-commuting Pauli operators that make up the matrix. The last bit has no analog in classical computing.\n", - "\n", - "Based on these dependencies, for sparse matrices, or matrices involving few non-commuting Pauli strings, this process may be useful. This is the case for systems of interacting spins, for example. For dense matrices, it may be less useful. We know for example that chemical systems often have Hamiltonians that involve hundreds, thousands, even millions of Pauli strings. There has been interesting work done to reduce this number of terms. But chemical systems may be better suited to some of the other algorithms we will discuss in this course.\n", - "\n", - "#### Check your understanding\n", - "\n", - "Consider a Hamiltonian on four qubits that contains the terms:\n", - "\n", - "`IIXX`, `IIXZ`, `IIZZ`, `IZXZ`, `IXXZ`, `ZZXZ`, `XZXZ`, `ZIXZ`, `ZZZZ`, `XXXX`\n", - "\n", - "You want to sort these terms into groups such that all terms in a group can be measured simultaneously. What is the smallest number of such groups you can make such that all terms are accounted for?\n", - "\n", - "\n", - "\n", - "\n", - "It can be done in 5 groups. Note that such solutions are typically not unique.\n", - "\n", - "`IIXX`, `XXXX`\n", - "\n", - "`IIXZ`, `IZXZ`, `ZZXZ`\n", - "\n", - "`IIZZ`, `ZZZZ`\n", - "\n", - "`IXXZ`, `ZIXZ`\n", - "\n", - "`XZXZ`\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Which do you expect typically makes quantum chemistry with VQE difficult: the number of terms in the Hamiltonian, or finding a good ansatz?\n", - "\n", - "\n", - "\n", - "\n", - "It turns out there are ansätze that are highly optimized for chemical contexts. The number of terms in the Hamiltonian, and hence the number of measurements required typically cause more problems.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "a9a687e3-ae98-49f7-9b5a-0a2700ecd278", - "metadata": {}, - "source": [ - "## 3. Example Hamiltonian\n", - "\n", - "Let us put this algorithm into practice using a small Hamiltonian matrix so that we can see what is happening in each step. We will employ the Qiskit patterns framework:\n", - "\n", - "-__Step 1__: Map problem to quantum circuits and operators\n", - "-__Step 2__: Optimize for target hardware\n", - "-__Step 3__: Execute on target hardware\n", - "-__Step 4__: Post-process results\n", - "\n", - "\n", - "### 3.1 Step 1: Map the problem to quantum circuits and operators\n", - "\n", - "We will use the one defined above from the chemistry context. We start with some general imports." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "acb6fb0c-4b8c-4ab3-b632-ed201e99b45f", - "metadata": {}, - "outputs": [], - "source": [ - "# General imports\n", - "import numpy as np\n", - "\n", - "# SciPy minimizer routine\n", - "from scipy.optimize import minimize\n", - "\n", - "# Plotting functions\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "id": "a713b494-ab7a-49c6-845c-db728c4635eb", - "metadata": {}, - "source": [ - "Again, we assume the Hamiltonian of interest is known. We will use an extremely small Hamiltonian here, because other methods discussed in this course will be more efficient at solving larger problems." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0908f251-58af-49f9-98a8-af98110f6c15", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The ground state energy is -0.702930394459531\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "import numpy as np\n", - "\n", - "hamiltonian = SparsePauliOp.from_list(\n", - " [(\"YZ\", 0.3980), (\"ZI\", -0.3980), (\"ZZ\", -0.0113), (\"XX\", 0.1810)]\n", - ")\n", - "\n", - "A = np.array(hamiltonian)\n", - "eigenvalues, eigenvectors = np.linalg.eigh(A)\n", - "print(\"The ground state energy is \", min(eigenvalues))" - ] - }, - { - "cell_type": "markdown", - "id": "75bf2097-1a9c-48d5-8eff-393e0c9eb2f9", - "metadata": {}, - "source": [ - "There are many prefabricated ansatz choices in Qiskit. We will use ```efficient_su2```." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "84d6380e-8ee8-4340-a416-c07900fe4c5b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This circuit has 4 parameters\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", - "from qiskit.circuit.library import efficient_su2\n", - "\n", - "# Note that it is more common to place initial 'h' gates outside the ansatz. Here we specifically wanted this layer structure.\n", - "ansatz = efficient_su2(\n", - " hamiltonian.num_qubits, su2_gates=[\"h\", \"rz\", \"y\"], entanglement=\"circular\", reps=1\n", - ")\n", - "\n", - "num_params = ansatz.num_parameters\n", - "print(\"This circuit has \", num_params, \"parameters\")\n", - "\n", - "ansatz.decompose().draw(\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "15394107-6589-4d06-b611-8fa641784b33", - "metadata": {}, - "source": [ - "Different ansätze will have different entangling structures and different rotation gates. The one shown here uses CNOT gates for entangling, and both Y gates and parametrized RZ gates for rotations. Note the size of this parameter space; it means we must minimize the cost function over 4 variables (the parameters for the RZ gates). This can be scaled up, but not indefinitely. Running a similar problem on 4 qubits, using the default 3 reps for ```efficient_su2``` yields 16 variational parameters." - ] - }, - { - "cell_type": "markdown", - "id": "12ee2d18-b4db-4b94-ab19-8a0654ca4b2e", - "metadata": {}, - "source": [ - "### 3.2 Step 2: Optimize for target hardware\n", - "\n", - "The ansatz was written using familiar gates, but our circuit must be transpiled to make use of the basis gates that can be implemented on each quantum computer. We select the least busy backend." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "ba1a1493-e807-44b8-b971-69f098981da2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "# runtime imports\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Session\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "\n", - "print(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "84200023-d16f-41d4-9791-c6ef3488da90", - "metadata": {}, - "source": [ - "We can now transpile our circuit for this hardware and visualize our transpiled ansatz." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "4a1cdcf3-7d94-4a1f-b675-9549bf28d956", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "ansatz_isa = pm.run(ansatz)\n", - "\n", - "ansatz_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "6066c477-2d43-459f-bfc6-650011279587", - "metadata": {}, - "source": [ - "Note that the gates used have changed, and the qubits in our abstract circuit have been mapped to differently-numbered qubits on the quantum computer. We must map our Hamiltonian identically in order for our results to be meaningful." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "3543e888-6f84-4f12-88a8-948dec7f3f55", - "metadata": {}, - "outputs": [], - "source": [ - "hamiltonian_isa = hamiltonian.apply_layout(layout=ansatz_isa.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "ba1ae1c5-c26c-42f8-b3af-411970d4d39d", - "metadata": {}, - "source": [ - "### 3.3 Step 3: Execute on target hardware\n", - "\n", - "#### 3.3.1 Reporting out values\n", - "\n", - "We define a cost function here that takes as arguments the structures we have built in previous steps: the parameters, the ansatz, and the Hamiltonian. It also uses Estimator, which we have not yet defined. We include code to track the history of our cost function, so that we can check convergence behavior." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb1d8999-77aa-4c1a-adae-8974eda9e63b", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from Estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (EstimatorV2): Estimator primitive instance\n", - " cost_history_dict: Dictionary for storing intermediate results\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, [hamiltonian], [params])\n", - " result = estimator.run(pubs=[pub]).result()\n", - " energy = result[0].data.evs[0]\n", - "\n", - " cost_history_dict[\"iters\"] += 1\n", - " cost_history_dict[\"prev_vector\"] = params\n", - " cost_history_dict[\"cost_history\"].append(energy)\n", - " print(f\"Iters. done: {cost_history_dict['iters']} [Current cost: {energy}]\")\n", - "\n", - " return energy\n", - "\n", - "\n", - "cost_history_dict = {\n", - " \"prev_vector\": None,\n", - " \"iters\": 0,\n", - " \"cost_history\": [],\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "95537d30-ba5d-48ab-bf6a-82edc8f0a348", - "metadata": {}, - "source": [ - "It is highly advantageous if you can choose initial parameter values based on knowledge of the problem at hand and characteristics of the target state. We will make no assumptions of such knowledge and use random initial values." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "01a5d370-2947-4441-b71b-ffbdeddf52f0", - "metadata": {}, - "outputs": [], - "source": [ - "x0 = 2 * np.pi * np.random.random(num_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e0d33b8-7f9c-428d-a76d-d832747b8430", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iters. done: 1 [Current cost: 0.010575798722044727]\n", - "Iters. done: 2 [Current cost: 0.004040015974440895]\n", - "Iters. done: 3 [Current cost: 0.0020213258785942503]\n", - "Iters. done: 4 [Current cost: 0.18723082446726014]\n", - "Iters. done: 5 [Current cost: -0.2746792152068885]\n", - "Iters. done: 6 [Current cost: -0.3094547651648519]\n", - "Iters. done: 7 [Current cost: -0.05281985428356641]\n", - "Iters. done: 8 [Current cost: 0.00808560303514377]\n", - "Iters. done: 9 [Current cost: -0.0014821685303514388]\n", - "Iters. done: 10 [Current cost: -0.004759824281150161]\n", - "Iters. done: 11 [Current cost: 0.09942328705995292]\n", - "Iters. done: 12 [Current cost: 0.01092366214057508]\n", - "Iters. done: 13 [Current cost: 0.05017497496069776]\n", - "Iters. done: 14 [Current cost: 0.13028868414310696]\n", - "Iters. done: 15 [Current cost: 0.013747803514376994]\n", - "Iters. done: 16 [Current cost: 0.2583072432944498]\n", - "Iters. done: 17 [Current cost: -0.14422125655131562]\n", - "Iters. done: 18 [Current cost: -0.0004950150347678081]\n", - "Iters. done: 19 [Current cost: 0.00681082268370607]\n", - "Iters. done: 20 [Current cost: -0.0023377795527156544]\n", - "Iters. done: 21 [Current cost: 0.6027665591169237]\n", - "Iters. done: 22 [Current cost: 0.00596641373801917]\n", - "Iters. done: 23 [Current cost: -0.008318769968051117]\n", - "Iters. done: 24 [Current cost: -0.00026683306709265246]\n", - "Iters. done: 25 [Current cost: -0.007648222843450479]\n", - "Iters. done: 26 [Current cost: 0.004121086261980831]\n", - "Iters. done: 27 [Current cost: -0.004075019968051117]\n", - "Iters. done: 28 [Current cost: -0.004419369009584665]\n", - "Iters. done: 29 [Current cost: 0.213185460054037]\n", - "Iters. done: 30 [Current cost: -0.06505919572162797]\n", - "Iters. done: 31 [Current cost: -0.5334241316590271]\n", - "Iters. done: 32 [Current cost: 0.00218370607028754]\n", - "Iters. done: 33 [Current cost: 0.09579352143666908]\n", - "Iters. done: 34 [Current cost: -0.009274800319488819]\n", - "Iters. done: 35 [Current cost: -0.44395141360688106]\n", - "Iters. done: 36 [Current cost: 0.011747104632587858]\n", - "Iters. done: 37 [Current cost: -0.003344149361022364]\n", - "Iters. done: 38 [Current cost: 0.19138183916486304]\n", - "Iters. done: 39 [Current cost: 0.013513931813145209]\n" - ] - } - ], - "source": [ - "# This required 13 min, 20 s QPU time on an Eagle processor, 28 min total time.\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session)\n", - " estimator.options.default_shots = 10000\n", - "\n", - " res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa, hamiltonian_isa, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 50},\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "fe98bde2-b435-47d1-b227-ee897aa3fe3e", - "metadata": {}, - "source": [ - "We can look at the raw outputs." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "b1d07809-5f98-4c11-9bf6-fc5b5c5fb47f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -0.5334241316590271\n", - " x: [ 1.024e+00 6.459e+00 3.625e+00 4.007e+00]\n", - " nfev: 39\n", - " maxcv: 0.0" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "res" - ] - }, - { - "cell_type": "markdown", - "id": "80c958e5-f70e-4736-889a-f88a69c50890", - "metadata": {}, - "source": [ - "### 3.4 Step 4: Post-process results\n", - "\n", - "If the procedure terminates correctly, then the values in our dictionary should be equal to the solution vector and total number of function evaluations, respectively. This is easy to verify:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "e87046c1-bfe9-4bb3-b7fd-1e4da55149fe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'prev_vector': array([1.02397956, 6.45886604, 3.62479262, 4.00744128]),\n", - " 'iters': 39,\n", - " 'cost_history': [np.float64(0.010575798722044727),\n", - " np.float64(0.004040015974440895),\n", - " np.float64(0.0020213258785942503),\n", - " np.float64(0.18723082446726014),\n", - " np.float64(-0.2746792152068885),\n", - " np.float64(-0.3094547651648519),\n", - " np.float64(-0.05281985428356641),\n", - " np.float64(0.00808560303514377),\n", - " np.float64(-0.0014821685303514388),\n", - " np.float64(-0.004759824281150161),\n", - " np.float64(0.09942328705995292),\n", - " np.float64(0.01092366214057508),\n", - " np.float64(0.05017497496069776),\n", - " np.float64(0.13028868414310696),\n", - " np.float64(0.013747803514376994),\n", - " np.float64(0.2583072432944498),\n", - " np.float64(-0.14422125655131562),\n", - " np.float64(-0.0004950150347678081),\n", - " np.float64(0.00681082268370607),\n", - " np.float64(-0.0023377795527156544),\n", - " np.float64(0.6027665591169237),\n", - " np.float64(0.00596641373801917),\n", - " np.float64(-0.008318769968051117),\n", - " np.float64(-0.00026683306709265246),\n", - " np.float64(-0.007648222843450479),\n", - " np.float64(0.004121086261980831),\n", - " np.float64(-0.004075019968051117),\n", - " np.float64(-0.004419369009584665),\n", - " np.float64(0.213185460054037),\n", - " np.float64(-0.06505919572162797),\n", - " np.float64(-0.5334241316590271),\n", - " np.float64(0.00218370607028754),\n", - " np.float64(0.09579352143666908),\n", - " np.float64(-0.009274800319488819),\n", - " np.float64(-0.44395141360688106),\n", - " np.float64(0.011747104632587858),\n", - " np.float64(-0.003344149361022364),\n", - " np.float64(0.19138183916486304),\n", - " np.float64(0.013513931813145209)]}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cost_history_dict" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "a789373a-8d32-4761-ba21-6b2f98a7ae5a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "x = np.linspace(0, 10, 50)\n", - "\n", - "# Define the constant function\n", - "constant = -0.7029\n", - "y_constant = np.full_like(x, constant)\n", - "ax.plot(\n", - " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", - ")\n", - "ax.set_xlabel(\"Iterations\")\n", - "ax.set_ylabel(\"Cost\")\n", - "ax.plot(y_constant, label=\"Target\")\n", - "plt.legend()\n", - "plt.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "4be8cb84-ec8c-4c83-880f-b9c296282040", - "metadata": {}, - "source": [ - "IBM Quantum has other upskilling offerings related to VQE. If you are ready to put VQE into practice, see our tutorial: [Ground-state energy estimation of the Heisenberg chain with VQE](/docs/tutorials/spin-chain-vqe). If you want more information on creating molecular Hamiltonians, see [this lesson](/learning/courses/quantum-chem-with-vqe/hamiltonian-construction) in our course on [Quantum chemistry with VQE](/learning/courses/quantum-chem-with-vqe). If you are interested in a deeper understanding of how variational algorithms like VQE work, we recommend the course [Variational Algorithm Design](/learning/courses/variational-algorithm-design/optimization-loops).\n", - "\n", - "\n", - "#### Check your understanding\n", - "\n", - "In this section, we calculated a ground state energy from a Hamiltonian. If we wanted to apply this to say, determining the geometry of a molecule, how would we extend this?\n", - "\n", - "\n", - "\n", - "\n", - "We would need to introduce variables for inter-atomic spacing, and the angles between bonds. We would need to vary these. For every variation of these, we would produce a new Hamiltonian (since the operators describing the energy certainly depend on the geometry). For each such Hamiltonian produced and mapped onto qubits, we would need to carry out optimization like that done above. Of all those many converged optimization problems, the geometry that produced the lowest energy would be the one adopted by nature. This is quite a bit more involved than what was shown above. Such a calculation is done for the simplest molecule, $\\text{H}_2$, [here](/learning/courses/quantum-chem-with-vqe/geometry).\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "33231f54-a9c1-499c-a4ad-b4404b843909", - "metadata": {}, - "source": [ - "## 4. VQE's relationship to other methods\n", - "\n", - "In this section we will review the advantages and disadvantages of the original VQE approach and point out its relationships to other, more recent algorithms.\n", - "\n", - "### 4.1 The strengths and weaknesses of VQE\n", - "\n", - "Some strengths have already been pointed out. They include:\n", - "\n", - "* Suitability to modern hardware: Some quantum algorithms require much lower error rates, approaching large scale fault tolerance. VQE does not; it can be implemented on current quantum computers.\n", - "* Shallow circuits: VQE often employs relatively shallow quantum circuits. This makes VQE less susceptible to accumulated gate errors and makes it suitable for many error mitigation techniques. Of course, the circuits are not always shallow; this depends on the ansatz used.\n", - "* Versatility: VQE can (in principle) be applied to any problem that can be cast as an eigenvalue/eigenvector problem. There are many caveats that make VQE impractical or disadvantageous for some problems. Some of these are recapped below.\n", - "\n", - "Some weaknesses of VQE and problems for which it is impractical have also been described above. These include:\n", - "\n", - "* Heuristic nature: VQE does not guarantee convergence to the correct ground state energy, as its performance depends on the choice of ansatz and optimization methods[\\[1-2\\]](#references). If a poor ansatz is chosen that lacks the requisite entanglement for the desired ground state, no classical optimizer can reach that ground state.\n", - "* Potentially numerous parameters: A very expressive ansatz may have so many parameters that the minimization iterations are very time-consuming.\n", - "* High measurement overhead: In VQE, Estimator is used to estimate the expectation value of each term in the Hamiltonian. Most Hamiltonians of interest will have terms that cannot be simultaneously estimated. This can make VQE resource-intensive for large systems with complicated Hamiltonians[\\[1\\]](#references).\n", - "* Effects of noise: When the classical optimizer is searching for a minimum, noisy calculations can confuse it and steer it away from the true minimum or delay its convergence. One possible solution for this is leveraging state-of-the-art error mitigation and error suppression techniques[\\[2-3\\]](#references) from IBM.\n", - "* Barren plateaus: These regions of vanishing gradients[\\[2-3\\]](#references) exist even in the absence of noise, but noise makes them more troublesome since the change in expectation values due to noise could be larger than the change from updating parameters in these barren regions.\n", - "\n", - "### 4.2 Relationship to other approaches\n", - "\n", - "#### Adapt-VQE\n", - "\n", - "The **ADAPT-VQE** (Adaptive Derivative-Assembled Pseudo-Trotter Variational Quantum Eigensolver) algorithm is an enhancement of the original VQE algorithm, designed to improve efficiency, accuracy, and scalability for quantum simulations, particularly in quantum chemistry.\n", - "\n", - "The original VQE algorithm described throughout this lesson uses a predefined, fixed ansatz to approximate the ground state of the system. In our case, we used `efficient_su2`, with a single repetition, using Y and RZ rotation gates. Although the parameters in the RZ gates changed, the structure of this ansatz and the gates used did not change.\n", - "\n", - "ADAPT-VQE addresses the limitations of VQE through adaptive ansatz construction. Instead of starting with a fixed ansatz, ADAPT-VQE dynamically builds the ansatz iteratively. At each step, it selects the operator from a predefined pool (such as fermionic excitation operators) that has the largest gradient with respect to the energy. This ensures that only the most impactful operators are added, leading to a compact and efficient ansatz[\\[4-6\\]](#references). This approach can have several beneficial effects:\n", - "\n", - "1. **Reduced Circuit Depth**: By growing the ansatz incrementally and focusing only on necessary operators, ADAPT-VQE minimizes gate operations compared to traditional VQE approaches[\\[5,7\\]](#references).\n", - "2. **Improved Accuracy**: The adaptive nature allows ADAPT-VQE to recover more correlation energy at each step, making it particularly effective for strongly correlated systems where traditional VQE struggles[\\[8,9\\]](#references).\n", - "3. **Scalability and Noise Robustness**: The compact ansatz reduces the accumulation of gate errors, reduces computational overhead, and limits the number of variational parameters which must be minimized.\n", - "\n", - "ADAPT-VQE is still not perfect. In some cases it can become trapped or slowed by local minima, and it may suffer from over-parameterization. It can also be fairly resource intensive, since it requires the calculation of gradients and parameter optimization with many gate structures.\n", - "\n", - "#### Quantum phase estimation (QPE)\n", - "\n", - "QPE is similar in purpose to VQE, but very different in implementation. QPE requires fault-tolerant quantum computers due to its generally deep quantum circuits and the high level of coherence it requires. Once QPE can be implemented, it would be more precise than VQE. One way of describing the difference is through the precision as a function of circuit depth. QPE achieves precision $\\epsilon$ with circuit depths scaling as $O(1/\\epsilon)$ [\\[10\\]](#references). VQE requires $O(1/\\epsilon^2)$ samples to achieve the same precision[\\[10,11\\]](#references).\n", - "\n", - "#### Krylov, SQD, QSCI, and others in this course\n", - "\n", - "VQE helped establish quantum algorithms that still depend on classical computers, not just for operating the quantum computer, but for substantial parts of the algorithm. Several such algorithms are the focus of the remainder of this course. Here, we give a cursory explanation of a few, simply to compare and contrast them to VQE. They will be explained in much greater detail in subsequent lessons.\n", - "\n", - "__Krylov quantum diagonalization (KQD)__\n", - "\n", - "__Krylov subspace methods__ are ways of projecting a matrix onto a subspace to reduce its dimension and make it more manageable, while keeping the most important features. One trick in this method is to generate a subspace that keeps these features; it turns out that generating this subspace is closely related to a well-established method on quantum computers called __Trotterization__.\n", - "\n", - "There are a few variants of quantum Krylov methods, but generally the approach is:\n", - "* Use the quantum computer to generate a subspace (the Krylov subspace) through Trotterization\n", - "* Project the matrix of interest onto that Krylov subspace\n", - "* Diagonalize the new projected Hamiltonian using a classical computer\n", - "\n", - "__Sampling-based quantum diagonalization (SQD)__\n", - "\n", - "__Sampling-based quantum diagonalization (SQD)__ is related to the Krylov method in that it also attempts to reduce the dimension of a matrix to be diagonalized while preserving key features. SQD does this in the following way:\n", - "* Begin with an initial guess for your ground state and prepare the system in that ground state.\n", - "* Use Sampler to sample the bitstrings that make up this state.\n", - "* Use the collection of computational basis states from sampler as the subspace onto which you project your matrix of interest.\n", - "* Diagonalize the smaller, projected matrix using a classical computer.\n", - "\n", - "This is related to VQE in that it leverages classical and quantum computing for substantial algorithm components. They both also share the requirement that we prepare a good initial guess or ansatz. But the distribution of work between the classical and quantum computers in SQD is more like that of the Krylov method.\n", - "\n", - "In fact, the Krylov method and SQD have recently been combined into the sampling-based Krylov quantum diagonalization (SKQD) method [\\[12\\]](#references).\n", - "\n", - "__Quantum subspace configuration interaction__\n", - "\n", - "__Quantum Selected Configuration Interaction (QSCI)__[\\[13\\]](#references) is an algorithm that produces an approximated ground state of a Hamiltonian by sampling a trial wave function to identify the significant computational basis states to generate a subspace for a classical diagonalization.\n", - "Both SQD and QSCI use a quantum computer to construct a reduced subspace. QSCI's additional strength is in its state preparation, especially in the context of chemistry problems. It leverages various strategies such as using time-evolved states [\\[14\\]](#references) and a set of chemistry-inspired ansätze. By focusing on efficient state preparation, QSCI reduces quantum computational costs for chemical Hamiltonians while maintaining high fidelity and leveraging the noise robustness from quantum state sampling techniques [\\[15\\]](#references). QSCI also provides an adaptive construction technique which provides more ansätze for a better result.\n", - "\n", - "The default workflow of QSCI for chemistry problem is as follows:\n", - "* Build the molecular Hamiltonian using your software of choice (such as SciPy).\n", - "* Prepare a QSCI algorithm by selecting a proper initial state and a chemistry-inspired ansatz with a pre-selected set of parameters.\n", - "* Sample significant basis states and diagonalize the Hamiltonian using a classical computer to obtain the ground state energy.\n", - "* Often one uses configuration recovery [\\[16\\]](#references) and symmetry postselection [\\[15\\]](#references) as a post processing technique.\n", - "* Optionally, the workflow of adaptive QSCI has an additional optimization loop from step2 to step3, by using more ansätze with a random initial states.\n", - "\n", - "\n", - "#### Check your understanding\n", - "\n", - "What does VQE have in common with all the other methods listed above (except QPE which is not described in great detail)\n", - "\n", - "\n", - "\n", - "\n", - "All involve a trial state or wave function of some sort. All work best when the initial guess for this trial state is excellent.\n", - "\n", - "Another correct answer is that they are all easiest to implement when the Hamiltonian is easy to measure (can be sorted into relatively few groups of commuting Pauli operators).\n", - "\n", - "\n", - "\n", - "\n", - "What does VQE have in common with none of the other methods listed above?\n", - "\n", - "\n", - "\n", - "\n", - "Classical optimizers. None of the others use classical optimization algorithms to select variational parameters.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "8086b6a5-daf2-457b-bc7a-84db943b333f", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "[2] https://en.wikipedia.org/wiki/Variational_quantum_eigensolver\n", - "\n", - "[3] https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.19.024047\n", - "\n", - "[4] https://arxiv.org/abs/2111.05176\n", - "\n", - "[6] https://inquanto.quantinuum.com/tutorials/InQ_tut_fe4n2_2.html\n", - "\n", - "[7] https://www.nature.com/articles/s41467-019-10988-2\n", - "\n", - "[8] https://arxiv.org/abs/2210.15438\n", - "\n", - "[9] https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.013254\n", - "\n", - "[10] https://arxiv.org/html/2403.09624v1\n", - "\n", - "[11] https://www.nature.com/articles/s42005-023-01312-y\n", - "\n", - "[13] https://arxiv.org/abs/1802.00171\n", - "\n", - "[14] https://arxiv.org/abs/2103.08505\n", - "\n", - "[15] https://arxiv.org/html/2501.09702v1\n", - "\n", - "[16] https://quri-sdk.qunasys.com/docs/examples/quri-algo-vm/qsci/\n", - "\n", - "[17] https://arxiv.org/abs/2412.13839\n", - "\n", - "[18] https://arxiv.org/abs/2302.11320v1\n", - "\n", - "[19] https://arxiv.org/pdf/2405.05068v1" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6581d769-6f75-4b26-90e3-39c28f22a74c", + "metadata": {}, + "source": [ + "---\n", + "title: Variational Quantum Eigensolver\n", + "description: This introduction to VQE covers its components, a basic implementation, and discusses what factors determine its efficiency and usefulness.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore ansä IIIZ IZII IIZI ZIII IZIZ IIZZ ZIIZ IZZI ZZII ZIZI XXYY YYXX ansätze ansatze infty IZZX XIYX ZZXZ ZZZX IIXX IIXZ IZXZ IXXZ XZXZ ZIXZ */}\n", + "\n", + "# The variational quantum eigensolver (VQE)\n", + "\n", + "This lesson will introduce the variational quantum eigensolver, explain its importance as a foundational algorithm in quantum computing, and also explore its strengths and weaknesses. VQE by itself, without augmenting methods, is not likely to be sufficient for modern utility scale quantum computations. It is nevertheless important as an archetypal classical-quantum hybrid method, an it is an important foundation upon which many more advanced algorithms are built.\n", + "\n", + "This video gives an overview of VQE and factors that affect its efficiency. The text below adds more detail and implements VQE using Qiskit.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "21115450-3f01-4e19-860d-48e31f401b1f", + "metadata": {}, + "source": [ + "## 1. What is VQE?\n", + "\n", + "The variational quantum eigensolver is an algorithm that uses classical and quantum computing in conjunction to accomplish a task. There are four main components of a VQE calculation:\n", + "\n", + "* __An operator__: Often a Hamiltonian, which we’ll call $H$, that describes a property of your system that you wish to optimize. Another way of saying this is that you are seeking the eigenvector of this operator that corresponds to the minimum eigenvalue. We often call that eigenvector the “ground state”.\n", + "* __An “ansatz”__ (a German word meaning “approach”): this is a quantum circuit that prepares a quantum state approximating the eigenvector you’re seeking. Really the ansatz is a family of quantum circuits, because some of the gates in the ansatz are parametrized, that is, they are fed a parameter which we can vary. This family of quantum circuits can prepare a family of quantum states approximating the ground state.\n", + "* __An Estimator__: a means of estimating the expectation value of the operator $H$ over the current variational quantum state. Sometimes what we really care about is simply this expectation value, which we call a cost function. Sometimes, we care about a more complicated function that can still be written starting from one or more expectation values.\n", + "* __A classical optimizer__: an algorithm that varies parameters to try to minimize the cost function.\n", + "\n", + "Let's look at each of these components in more depth." + ] + }, + { + "cell_type": "markdown", + "id": "7dbd605e-a440-446b-bc05-e2e301796bf2", + "metadata": {}, + "source": [ + "### 1.1 The operator (Hamiltonian)\n", + "\n", + "At the core of a VQE problem is an operator that describes a system of interest. We will assume here that the lowest eigenvalue and the corresponding eigenvector of this operator are useful for some scientific or business purpose. Examples might include a chemical Hamiltonian describing a molecule, such that the lowest eigenvalue of the operator corresponds to the ground state energy of the molecule, and the corresponding eigenstate describes the geometry or electron configuration of the molecule. Or the operator could describe a cost of a certain process to be optimized, and the eigenstates could correspond to routes or practices. In some fields, like physics, a \"Hamiltonian\" almost always refers to an operator describing the energy of a physical system. But in quantum computing, it is common to see quantum operators that describe a business or logistical problem also referred to as a \"Hamiltonian\". We will adopt that convention here.\n", + "\n", + "![An image of atomic orbitals and an image of a network of many nodes and connections between them.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig1.svg)\n", + "\n", + "Mapping a physical or optimization problem to qubits is typically a non-trivial task, but those details are not the focus of this course. A general discussion of mapping a problem to a quantum operator can be found in [Quantum computing in practice](/learning/courses/quantum-computing-in-practice). A more detailed look at the mapping of chemistry problems into quantum operators can be found in [Quantum Chemistry with VQE](/learning/courses/quantum-chem-with-vqe).\n", + "\n", + "For the purposes of this course, we will assume the form of the Hamiltonian is known. For example, a Hamiltonian for a simple hydrogen molecule (under certain active space assumptions, and using the Jordan-Wigner mapper) is:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "89b425d8-f54f-4f98-996e-c303f77edb25", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "hamiltonian = SparsePauliOp(\n", + " [\n", + " \"IIII\",\n", + " \"IIIZ\",\n", + " \"IZII\",\n", + " \"IIZI\",\n", + " \"ZIII\",\n", + " \"IZIZ\",\n", + " \"IIZZ\",\n", + " \"ZIIZ\",\n", + " \"IZZI\",\n", + " \"ZZII\",\n", + " \"ZIZI\",\n", + " \"YYYY\",\n", + " \"XXYY\",\n", + " \"YYXX\",\n", + " \"XXXX\",\n", + " ],\n", + " coeffs=[\n", + " -0.09820182 + 0.0j,\n", + " -0.1740751 + 0.0j,\n", + " -0.1740751 + 0.0j,\n", + " 0.2242933 + 0.0j,\n", + " 0.2242933 + 0.0j,\n", + " 0.16891402 + 0.0j,\n", + " 0.1210099 + 0.0j,\n", + " 0.16631441 + 0.0j,\n", + " 0.16631441 + 0.0j,\n", + " 0.1210099 + 0.0j,\n", + " 0.17504456 + 0.0j,\n", + " 0.04530451 + 0.0j,\n", + " 0.04530451 + 0.0j,\n", + " 0.04530451 + 0.0j,\n", + " 0.04530451 + 0.0j,\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "14c20ee9-51ad-4cc9-be36-7a33fae6c56a", + "metadata": {}, + "source": [ + "Note that in the Hamiltonian above, there are terms like `ZZII` and `YYYY` that do not commute with each other. That is, to evaluate `ZZII`, we would need to measure the Pauli Z operator on qubit 3 (among other measurements). But to evaluate `YYYY`, we need to measure the Pauli Y operator on that same qubit, qubit 3. There is an uncertainty relation between Y and Z operators on the same qubit; we cannot measure both of those operators at the same time. We will revisit this point below, and indeed throughout the course.\n", + "The Hamiltonian above is a $16\\times 16$ matrix operator. Diagonalizing the operator to find its lowest energy eigenvalue is not difficult." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9703b61e-ff90-4e94-8999-242d0c6766c0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The ground state energy is -1.1459778447627311 hartrees\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "A = np.array(hamiltonian)\n", + "eigenvalues, eigenvectors = np.linalg.eigh(A)\n", + "print(\"The ground state energy is \", min(eigenvalues), \"hartrees\")" + ] + }, + { + "cell_type": "markdown", + "id": "01a76c42-29e7-455a-acd6-55490f36121b", + "metadata": {}, + "source": [ + "Brute force classical eigensolvers cannot scale to describe the energies or geometries of very large systems of atoms, like medications or proteins. VQE is one of the early attempts to leverage quantum computing in this problem.\n", + "\n", + "We will encounter Hamiltonians in this lesson much larger than that above. But it would be wasteful to push the limits of what VQE can do, before we introduce some of the more advanced tools that can augment or replace VQE, later in this course.\n", + "\n", + "### 1.2 Ansatz\n", + "\n", + "The word \"ansatz\" is German for \"approach\". The correct plural in German is \"ansätze\", though one often sees \"ansatzes\" or \"ansatze\". In the context of VQE, an ansatz is the quantum circuit you use to create a multi-qubit wave function that most closely approximates the ground state of the system you are studying, and which thus produces the lowest expectation value of your operator. This quantum circuit will contain variational parameters (often collected together in the vector of variables $\\vec{\\theta}$).\n", + "\n", + "![An image of a quantum circuit with variational parameters labeled \"theta\".](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig2.svg)\n", + "\n", + "An initial set of values $\\vec{\\theta_0}$ of the variational parameters is chosen. We will call the unitary operation of the ansatz on the circuit $U_{\\text{var}}(\\vec{\\theta_0})$. By default, all qubits in IBM® quantum computers are initialized to the $|0\\rangle$ state. When the circuit is run, the state of the qubits will be\n", + "\n", + "$$\n", + "|\\psi(\\vec{\\theta_0})\\rangle=U_{\\text{var}}(\\vec{\\theta_0})|0\\rangle^{\\otimes N}\n", + "$$\n", + "\n", + "If all we needed were the lowest energy (using the language of physical systems), we could estimate this by simply measuring the energy many times and taking the lowest. But we typically also want the configuration that yields that lowest energy or eigenvalue. So the next step is the estimation of the expectation value of the Hamiltonian, which is accomplished through quantum measurements. A lot goes into that. But we can understand this process qualitatively by noting that the probability $P_j$ of measuring an energy $E_j$ (again using the language of physical systems) is related to the expectation value by:\n", + "\n", + "$$\n", + "\\langle \\psi(\\vec{\\theta_0}) |H|\\psi (\\vec{\\theta_0}) \\rangle\n", + "$$\n", + "\n", + "The probability $P_j$ is also related to the overlap between the eigenstate $|\\phi_j\\rangle$ and the current state of the system $|\\psi(\\vec{\\theta_0})\\rangle$:\n", + "\n", + "$$\n", + "P_j=|\\langle \\phi_j|\\psi(\\vec{\\theta_0})\\rangle|^2 = |\\langle \\phi_j|U_{\\text{var}}(\\vec{\\theta_0})|0\\rangle^{\\otimes N}|^2\n", + "$$\n", + "\n", + "So by making many measurements of the Pauli operators making up our Hamiltonian, we can estimate the Hamiltonian's expectation value in the current state of the system $|\\psi(\\vec{\\theta_0})\\rangle$. The next step is to vary the parameters $\\vec{\\theta}$ and try to more closely approach the lowest-energy (ground) state of the system. Because of the variational parameters in the ansatz, one sometimes hears it referred to as the __variational form__.\n", + "\n", + "Before we move on to that variational process, note that it is often useful to start your state from a \"good guess\" state. You might know enough about your system to make a better initial guess than $|0\\rangle^{\\otimes N}$. For example, it is common to initialize qubits to the Hartree-Fock state in chemical applications. This starting guess which does not contain any variational parameters is called the __reference state__. Let us call the quantum circuit used to create reference state $U_{ref}$. Whenever it becomes important to distinguish the reference state from the rest of the ansatz, use: $U_{\\text{ansatz}}(\\vec{\\theta}) =U_{\\text{var}}(\\vec{\\theta})U_{\\text{ref}}.$ Equivalently\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "|\\psi_{\\text{ref}}\\rangle&=U_{\\text{ref}}|0\\rangle^{\\otimes N}\\\\\n", + "|\\psi_{\\text{ansatz}}(\\vec{\\theta})\\rangle&=U_{var}(\\vec{\\theta})|\\psi_{\\text{ref}}\\rangle = U_{\\text{var}}(\\vec{\\theta})U_{\\text{ref}}|0\\rangle^{\\otimes N}.\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "b7f66a1f-93cb-47ee-81c3-6fbf62392003", + "metadata": {}, + "source": [ + "### 1.3 Estimator\n", + "\n", + "We need a way to estimate the expectation value of our Hamiltonian in a particular variational state $|\\psi(\\vec{\\theta})\\rangle$. If we could directly measure the entire operator $H$, this would be as simple as making many (say $N$) measurements and averaging the measured values:\n", + "\n", + "$$\n", + "\\langle \\psi(\\vec{\\theta})|H|\\psi(\\vec{\\theta})\\rangle _N \\approx \\frac{1}{N}\\sum_{j=1}^N {E_j}\n", + "$$\n", + "Here, the $\\approx$ symbol reminds us that this expectation value would only be precisely correct in the limit as $N\\rightarrow \\infty$. But with thousands of measurements being made on a circuit, the sampling error of the expectation value is fairly low. There are other considerations such as noise that become an issue for very precise calculations.\n", + "\n", + "However, it is generally not possible to measure $H$ all at once. $H$ may contain multiple non-commuting Pauli X, Y, and Z operators. So the Hamiltonian must be broken up into groups of operators that can be simultaneously measured, and each such group must be estimated separately, and the results combined to obtain an expectation value. We will revisit this in greater detail in the next lesson, when we discuss the scaling of classical and quantum approaches. This complexity in measurement is one reason we need highly efficient code for carrying out such estimation. In this lesson and beyond, we will use the Qiskit Runtime primitive Estimator for this purpose." + ] + }, + { + "cell_type": "markdown", + "id": "8b68d38e-80ad-4e02-815f-576806ec3e1c", + "metadata": {}, + "source": [ + "### 1.4 Classical optimizers\n", + "\n", + "A classical optimizer is any classical algorithm designed to find extrema of a target function (typically a minimum). They search through the space of possible parameters looking for a set that minimizes some function of interest. They can be broadly categorized into gradient-based methods, which utilize gradient information, and gradient-free methods, which operate as black-box optimizers. The choice of classical optimizer can significantly impact an algorithm's performance, especially in the presence of noise in quantum hardware. Popular optimizers in this field include Adam, AMSGrad, and SPSA, which have shown promising results in noisy environments. More traditional optimizers include COBYLA and SLSQP.\n", + "\n", + "A common workflow (demonstrated in Section 3.3) is to use one of these algorithms as the method inside a minimizer like scipy's ```minimize``` function. This takes as its arguments:\n", + "* Some function to be minimized. This is often the energy expectation value. But these are generally referred to as \"cost functions\".\n", + "* A set of parameters from which to begin the search. Often called $x_0$ or $\\theta_0$.\n", + "* Arguments, including arguments of the cost function. In quantum computing with Qiskit, these arguments will include the ansatz, the Hamiltonian, and the Estimator primitive, which is discussed more in the next subsection.\n", + "* A 'method' of minimization. This refers to the specific algorithm used to search the parameter space. This is where we would specify, for example, COBYLA or SLSQP.\n", + "* Options. The options available may differ by method. But an example which practically all methods would include is the maximum number of iterations of the optimizer before ending the search: 'maxiter'.\n", + "\n", + "![An image showing a curved line representing energy with several points at which the value is being tested to find the minimum.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig3.svg)\n", + "\n", + "At each iterative step, the expectation value of the Hamiltonian is estimated by making many measurements. This estimated energy is returned by the cost function, and the minimizer updates the information it has about the energy landscape. Exactly what the optimizer does to choose the next step varies from method to method. Some use gradients and select the direction of steepest descent. Others may take noise into account and may require that the cost decrease by a large margin before accepting that the true energy decreases along that direction." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9e0642f5-8127-4647-b3f2-deceff0651ec", + "metadata": {}, + "outputs": [], + "source": [ + "# Example syntax for minimization\n", + "# from scipy.optimize import minimize\n", + "# res = minimize(cost_func, x0, args=(ansatz, hamiltonian, estimator), method=\"cobyla\",\n", + "# options={'maxiter': 200})" + ] + }, + { + "cell_type": "markdown", + "id": "5bbcff76-ea91-4fb6-acca-35e9e6675ed1", + "metadata": {}, + "source": [ + "### 1.5 The variational principle\n", + "\n", + "In this context the variational principle is very important; it states that no variational wave function can yield an energy (or cost) expectation value lower than that yielded by the ground state wave function. Mathematically,\n", + "$$\n", + "E_\\text{var}=\\langle \\psi_\\text{var}|H|\\psi_\\text{var}\\rangle \\geq E_\\text{min}=\\langle \\psi_\\text{0}|H|\\psi_\\text{0}\\rangle\n", + "$$\n", + "This is easy to verify if we note that the set of all eigenstates $\\{|\\psi_0\\rangle, |\\psi_1\\rangle, |\\psi_2\\rangle, ...|\\psi_n \\rangle\\}$ of $H$ form a complete basis for the Hilbert space. In other words, any state and in particular $|\\psi_\\text{var}\\rangle$ can be written as a weighted (normalized) sum of these eigenstates of $H$:\n", + "$$\n", + "|\\psi_\\text{var}\\rangle=\\sum_{i=0}^n c_i |\\psi_i\\rangle\n", + "$$\n", + "where $c_i$ are constants to be determined, and $\\sum_{i=0} |c_i|^2 = 1$. We leave this as an exercise to the reader. But note the implication: the variational state that produces the lowest-energy expectation value *is* the best estimate of the true ground state.\n", + "\n", + "#### Check your understanding\n", + "\n", + "Verify mathematically that $E_\\text{var}\\geq E_0$ for any variational state $|\\psi_\\text{var}\\rangle$.\n", + "\n", + "\n", + "\n", + "\n", + "Using the given expansion of the variational state in terms of the energy eigenstates,\n", + "$$\n", + "|\\psi_\\text{var}\\rangle=\\sum_{i=0}^n c_i |\\psi_i\\rangle,\n", + "$$\n", + "we can write the variational energy expectation value as\n", + "$$\n", + "\\begin{aligned}\n", + "E_\\text{var}&=\\langle \\psi_\\text{var}|H|\\psi_\\text{var}\\rangle =\\left(\\sum_{i=0}^n c^*_i \\langle \\psi_i|\\right)H\\left(\\sum_{j=0}^n c_j |\\psi_j\\rangle\\right)\\\\\n", + "&=\\left(\\sum_{i=0}^n c^*_i \\langle \\psi_i|\\right)\\left(\\sum_{j=0}^n c_j E_j|\\psi_j\\rangle\\right)\\\\\n", + "&=\\sum_{i,j=0}^n c^*_i c_j E_j \\langle \\psi_i|\\psi_j\\rangle\\\\\n", + "&=\\sum_{i,j=0}^n c^*_i c_j E_j \\delta_{i,j}\\\\\n", + "&=\\sum_{i=0}^n |c_i|^2 E_i.\n", + "\\end{aligned}\n", + "$$\n", + "For all coefficients $0\\leq|c_i|^2\\leq 1$. So we can write\n", + "$$\n", + "\\begin{aligned}\n", + "E_\\text{var}&=\\sum_{i=0}^n |c_i|^2 E_i\\geq \\sum_{i=0}^n |c_i|^2 E_0 = E_0 \\sum_{i=0}^n |c_i|^2 = E_0(1) \\\\\n", + "E_\\text{var}&\\geq E_0\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "4d14d6df-7320-4f57-bc62-fbee4041957b", + "metadata": {}, + "source": [ + "## 2. Comparison with classical workflow\n", + "\n", + "Let’s say we are interested in a matrix with N rows and N columns. Suppose your matrix is so large that exact diagonalization is not an option. Suppose further that you know enough about your problem that you can make some guesses about the overall structure of the target eigenstate, and you want to probe states similar to your initial guess to see if your cost/energy can be lowered further. This is a variational approach, and it is one method that is used when exact diagonalization is not an option.\n", + "\n", + "### 2.1 Classical workflow\n", + "\n", + "Using a classical computer, this would work as follows:\n", + "* Make a guess state, with some parameters $\\vec{\\theta}_i$ that you will vary: $|\\psi(\\vec{\\theta}_i)\\rangle$. Although this initial guess could be random, that is not advisable. We want to use knowledge of the problem at hand to tailor our guess as much as possible.\n", + "* Calculate the expectation value of the operator with the system in that state: $\\langle\\psi(\\vec{\\theta}_i)|H|\\psi(\\vec{\\theta}_i)\\rangle$\n", + "* Alter the variational parameters and repeat: $\\vec{\\theta}_i\\rightarrow \\vec{\\theta}_{i+1}$.\n", + "* Use accumulated information about the landscape of possible states in your variational subspace to make better and better guesses and approach the target state. The variational principle guarantees that our variational state cannot yield an eigenvalue lower than that of the target ground state. So the lower the expectation value the better our approximation of the ground state:\n", + "$$\n", + "\\min_{\\vec{\\theta}} \\{ E_{\\text{var},i} = \\langle\\psi(\\vec{\\theta_i})|H|\\psi(\\vec{\\theta_i})\\rangle \\} \\geq E_0\n", + "$$\n", + "Let us examine the difficulty of each step in this approach. Setting or updating parameters is computationally easy; the difficulty there is in selecting useful, physically motivated initial parameters. Using accumulated information from prior iterations to update parameters in such a way that you approach the ground state is a non-trivial. But classical optimization algorithms exist that do this quite efficiently. This classical optimization is only expensive because it may require many iterations; in the worst case, the number of iterations may scale exponentially with N. The most computationally expensive single step is almost certainly calculating the expectation value of your matrix using a given state $|\\psi(\\vec{\\theta_i})\\rangle$: $\\langle\\psi(\\vec{\\theta_i})|H|\\psi(\\vec{\\theta_i})\\rangle.$\n", + "\n", + "The $N\\times N$ matrix must act on the $N$-element vector, which corresponds to: $O(N^2)$ multiplication operations in the worst case. This must be done at each iteration of parameters. For extremely large matrices, this has high computational cost.\n", + "\n", + "### 2.2 Quantum workflow and commuting Pauli groups\n", + "\n", + "Now imagine relegating this portion of the calculation to a quantum computer. Instead of calculating this expectation value, you estimate it by preparing the state $|\\psi(\\vec{\\theta_i})\\rangle$ on the quantum computer using your variational ansatz, and then making measurements.\n", + "\n", + "That may sound easier than it is. $H$ is generally not easy to measure. For example it could be made up of many non-commuting Pauli X, Y, and Z operators. But $H$ __can__ be written as a linear combination of terms, $h_\\alpha$, each of which is easily measurable (for example, Pauli operators or groups of qubit-wise commuting Pauli operators).\n", + "The expectation value of $H$ over some state $|\\Psi\\rangle$ is the weighted sum of expectation values of the constituent terms $h_\\alpha$. This expression holds for any state $|\\Psi⟩$, but we will specifically be using this with our variational states $|\\psi(\\theta_i)\\rangle$.\n", + "\n", + "$$\n", + "H = \\sum_{\\alpha = 1}^T{c_\\alpha h_\\alpha}\n", + "$$\n", + "where $h_\\alpha$ is a Pauli string like `IZZX…XIYX`, or several such strings that commute with each other. So a description of the expectation value that more closely matches the realities of measurement on quantum computers is\n", + "$$\n", + "\\langle \\Psi |H|\\Psi \\rangle =\\sum_{\\alpha} c_\\alpha \\langle \\Psi | h_\\alpha|\\Psi \\rangle.\n", + "$$\n", + "And in the context of our variational wave function:\n", + "$$\n", + "\\langle \\psi(\\vec{\\theta}_i) |H|\\psi(\\vec{\\theta}_i) \\rangle =\\sum_{\\alpha} c_\\alpha \\langle \\psi(\\vec{\\theta}_i) | h_\\alpha|\\psi(\\vec{\\theta}_i) \\rangle\n", + "$$\n", + "Each of the terms $h_\\alpha$ can be measured $M$ times yielding measurement samples $s_{\\alpha j}$ with $j=1…M$ and returns an expectation value $\\mu_\\alpha$ and a standard deviation $\\sigma_\\alpha$. We can sum these terms and propagate errors through the sum to obtain an overall expectation value $\\mu$ and standard deviation $\\sigma$.\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\langle \\psi(\\vec{\\theta}_i) |h_\\alpha|\\psi(\\vec{\\theta}_i) \\rangle &\\simeq \\mu _\\alpha \\pm \\frac{\\sigma_\\alpha}{\\sqrt{M}} &\\qquad \\mu_\\alpha &=\\frac{1}{M}\\sum_j s_{\\alpha,j} &\\qquad \\sigma^2_\\alpha &=\\frac{1}{M-1}\\sum_j (s_{\\alpha,j}-\\mu_\\alpha)^2\\\\\n", + "\n", + "\\langle \\psi(\\vec{\\theta}_i) |H|\\psi(\\vec{\\theta}_i) \\rangle &\\simeq \\mu \\pm \\sigma &\\qquad \\mu &= \\sum_\\alpha c_\\alpha \\mu_\\alpha &\\qquad \\sigma^2&=\\sum_\\alpha c^2_\\alpha \\frac{\\sigma^2_\\alpha }{M}\n", + "\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "ab02194a-bb0e-4bbc-81e1-c5f8f9582d9b", + "metadata": {}, + "source": [ + "This requires no large-scale multiplication, nor any process that necessarily scales like $N^2$. Instead it requires multiple measurements on the quantum computer. If you don’t need too many of those, this approach could be efficient. And that’s the quantum part of VQE.\n", + "\n", + "But let’s talk about reasons why this might not be efficient. One reason for many measurements is to reduce the statistical uncertainty in your estimates, for very high-precision calculations. Another reason is the number of Pauli strings required to span your entire matrix. Because Pauli matrices (plus the identity: X, Y, Z, and I) span the space of all operators of a given dimension, we are guaranteed that we can write our matrix of interest as a weighted sum of Pauli operators, as we did before.\n", + "$$\n", + "H = \\sum_{\\alpha = 1}^T{c_\\alpha h_\\alpha}\n", + "$$\n", + "where $h_\\alpha$ is a Pauli string acting on all the qubits describing your system like `IZZX…XIYX`, or several such strings that commute with each other. Recall that Qiskit uses *little endian* notation, in which the $n^\\text{th}$ Pauli operator from the right acts on the $n^\\text{th}$ qubit. So we can measure our operator by measuring a series of Pauli operators.\n", + "\n", + "But we cannot measure all those Pauli operators simultaneously. Pauli operators (excluding I) do not commute with each other if they are associated with the same qubit. For example, we can measure `IZIZ` and `ZZXZ` simultaneously, because we can measure I and Z simultaneously for the 3rd qubit, and we can know I and X simultaneously for the 1st qubit. But we cannot measure ZZZZ and ZZZX simultaneously, because Z and X do not commute, and both act on the 0th qubit.\n", + "\n", + "![A table of different Pauli strings, some of which commute and others which do not.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig4.svg)\n", + "\n", + "So we decompose our matrix $H$ into a sum of Paulis acting on different qubits. Some elements of that sum can be measured all at once; we call this a *group of commuting Paulis*. Depending on how many non-commuting terms there are, we may need many such groups. Call the number of such groups of commuting Pauli strings $N_\\text{GCP}$. If $N_\\text{GCP}$ is small, this could work well. If $H$ has millions of groups, this will not be useful.\n", + "\n", + "The processes required for estimation of the expectation value are collected together in the Qiskit Runtime primitive called Estimator. To learn more about Estimator, see the [API reference](/docs/api/qiskit-ibm-runtime/estimator-v2) in IBM Quantum® Documentation. One can simply use Estimator directly, but Estimator returns much more than just the lowest energy eigenvalue. For example, it also returns information on ensemble standard error. Thus, in the context of minimization problems, one often sees Estimator inside a cost function. To learn more about Estimator inputs and outputs see this [guide](/docs/guides/primitive-input-output#pubs) on IBM Quantum Documentation.\n", + "\n", + "You record the expectation value (or the cost function) for the set of parameters $\\vec{\\theta_i}$ used in your state, and then you update the parameters. Over time, you could use the expectation values or cost-function values you’ve estimated to approximate a gradient of your cost function in the subspace of states sampled by your ansatz. Both gradient-based, and gradient-free classical optimizers exist. Both suffer from potential trainability issues, like multiple local minima, and large regions of parameter space with near-zero gradient, called *barren plateaus*.\n", + "\n", + "![Two images of a curved line with a minimum value. In one, points are randomly checked in the search for a minimum, in the other a gradient is estimated by drawing a line between two adjacent points.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig5.svg)\n", + "\n", + "### 2.3 Factors that determine computational cost\n", + "\n", + "VQE will not solve all your toughest quantum chemistry problems. No. But being better at all calculations is not the point. We have shifted what determines the computational cost.\n", + "\n", + "![A table comparing classical and quantum variational approaches. Both require good initial guesses. Classically, the cost scales like the dimension of your matrix squared, and in the quantum approach it depends on how many groups of commuting Pauli operators you have.](/learning/images/courses/quantum-diagonalization-algorithms/vqe/vqe-fig6.svg)\n", + "\n", + "We’ve shifted from a process whose complexity depends only on matrix dimension to one that depends on required precision and the number of non-commuting Pauli operators that make up the matrix. The last bit has no analog in classical computing.\n", + "\n", + "Based on these dependencies, for sparse matrices, or matrices involving few non-commuting Pauli strings, this process may be useful. This is the case for systems of interacting spins, for example. For dense matrices, it may be less useful. We know for example that chemical systems often have Hamiltonians that involve hundreds, thousands, even millions of Pauli strings. There has been interesting work done to reduce this number of terms. But chemical systems may be better suited to some of the other algorithms we will discuss in this course.\n", + "\n", + "#### Check your understanding\n", + "\n", + "Consider a Hamiltonian on four qubits that contains the terms:\n", + "\n", + "`IIXX`, `IIXZ`, `IIZZ`, `IZXZ`, `IXXZ`, `ZZXZ`, `XZXZ`, `ZIXZ`, `ZZZZ`, `XXXX`\n", + "\n", + "You want to sort these terms into groups such that all terms in a group can be measured simultaneously. What is the smallest number of such groups you can make such that all terms are accounted for?\n", + "\n", + "\n", + "\n", + "\n", + "It can be done in 5 groups. Note that such solutions are typically not unique.\n", + "\n", + "`IIXX`, `XXXX`\n", + "\n", + "`IIXZ`, `IZXZ`, `ZZXZ`\n", + "\n", + "`IIZZ`, `ZZZZ`\n", + "\n", + "`IXXZ`, `ZIXZ`\n", + "\n", + "`XZXZ`\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Which do you expect typically makes quantum chemistry with VQE difficult: the number of terms in the Hamiltonian, or finding a good ansatz?\n", + "\n", + "\n", + "\n", + "\n", + "It turns out there are ansätze that are highly optimized for chemical contexts. The number of terms in the Hamiltonian, and hence the number of measurements required typically cause more problems.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "a9a687e3-ae98-49f7-9b5a-0a2700ecd278", + "metadata": {}, + "source": [ + "## 3. Example Hamiltonian\n", + "\n", + "Let us put this algorithm into practice using a small Hamiltonian matrix so that we can see what is happening in each step. We will employ the Qiskit patterns framework:\n", + "\n", + "-__Step 1__: Map problem to quantum circuits and operators\n", + "-__Step 2__: Optimize for target hardware\n", + "-__Step 3__: Execute on target hardware\n", + "-__Step 4__: Post-process results\n", + "\n", + "\n", + "### 3.1 Step 1: Map the problem to quantum circuits and operators\n", + "\n", + "We will use the one defined above from the chemistry context. We start with some general imports." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "acb6fb0c-4b8c-4ab3-b632-ed201e99b45f", + "metadata": {}, + "outputs": [], + "source": [ + "# General imports\n", + "import numpy as np\n", + "\n", + "# SciPy minimizer routine\n", + "from scipy.optimize import minimize\n", + "\n", + "# Plotting functions\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "a713b494-ab7a-49c6-845c-db728c4635eb", + "metadata": {}, + "source": [ + "Again, we assume the Hamiltonian of interest is known. We will use an extremely small Hamiltonian here, because other methods discussed in this course will be more efficient at solving larger problems." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0908f251-58af-49f9-98a8-af98110f6c15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The ground state energy is -0.702930394459531\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "import numpy as np\n", + "\n", + "hamiltonian = SparsePauliOp.from_list(\n", + " [(\"YZ\", 0.3980), (\"ZI\", -0.3980), (\"ZZ\", -0.0113), (\"XX\", 0.1810)]\n", + ")\n", + "\n", + "A = np.array(hamiltonian)\n", + "eigenvalues, eigenvectors = np.linalg.eigh(A)\n", + "print(\"The ground state energy is \", min(eigenvalues))" + ] + }, + { + "cell_type": "markdown", + "id": "75bf2097-1a9c-48d5-8eff-393e0c9eb2f9", + "metadata": {}, + "source": [ + "There are many prefabricated ansatz choices in Qiskit. We will use ```efficient_su2```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84d6380e-8ee8-4340-a416-c07900fe4c5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This circuit has 4 parameters\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", + "from qiskit.circuit.library import efficient_su2\n", + "\n", + "# Note that it is more common to place initial 'h' gates outside the ansatz. Here we specifically\n", + "# wanted this layer structure.\n", + "ansatz = efficient_su2(\n", + " hamiltonian.num_qubits, su2_gates=[\"h\", \"rz\", \"y\"], entanglement=\"circular\", reps=1\n", + ")\n", + "\n", + "num_params = ansatz.num_parameters\n", + "print(\"This circuit has \", num_params, \"parameters\")\n", + "\n", + "ansatz.decompose().draw(\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "15394107-6589-4d06-b611-8fa641784b33", + "metadata": {}, + "source": [ + "Different ansätze will have different entangling structures and different rotation gates. The one shown here uses CNOT gates for entangling, and both Y gates and parametrized RZ gates for rotations. Note the size of this parameter space; it means we must minimize the cost function over 4 variables (the parameters for the RZ gates). This can be scaled up, but not indefinitely. Running a similar problem on 4 qubits, using the default 3 reps for ```efficient_su2``` yields 16 variational parameters." + ] + }, + { + "cell_type": "markdown", + "id": "12ee2d18-b4db-4b94-ab19-8a0654ca4b2e", + "metadata": {}, + "source": [ + "### 3.2 Step 2: Optimize for target hardware\n", + "\n", + "The ansatz was written using familiar gates, but our circuit must be transpiled to make use of the basis gates that can be implemented on each quantum computer. We select the least busy backend." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ba1a1493-e807-44b8-b971-69f098981da2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# runtime imports\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Session\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "print(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "84200023-d16f-41d4-9791-c6ef3488da90", + "metadata": {}, + "source": [ + "We can now transpile our circuit for this hardware and visualize our transpiled ansatz." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4a1cdcf3-7d94-4a1f-b675-9549bf28d956", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "ansatz_isa = pm.run(ansatz)\n", + "\n", + "ansatz_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "6066c477-2d43-459f-bfc6-650011279587", + "metadata": {}, + "source": [ + "Note that the gates used have changed, and the qubits in our abstract circuit have been mapped to differently-numbered qubits on the quantum computer. We must map our Hamiltonian identically in order for our results to be meaningful." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3543e888-6f84-4f12-88a8-948dec7f3f55", + "metadata": {}, + "outputs": [], + "source": [ + "hamiltonian_isa = hamiltonian.apply_layout(layout=ansatz_isa.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "ba1ae1c5-c26c-42f8-b3af-411970d4d39d", + "metadata": {}, + "source": [ + "### 3.3 Step 3: Execute on target hardware\n", + "\n", + "#### 3.3.1 Reporting out values\n", + "\n", + "We define a cost function here that takes as arguments the structures we have built in previous steps: the parameters, the ansatz, and the Hamiltonian. It also uses Estimator, which we have not yet defined. We include code to track the history of our cost function, so that we can check convergence behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb1d8999-77aa-4c1a-adae-8974eda9e63b", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from Estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (EstimatorV2): Estimator primitive instance\n", + " cost_history_dict: Dictionary for storing intermediate results\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, [hamiltonian], [params])\n", + " result = estimator.run(pubs=[pub]).result()\n", + " energy = result[0].data.evs[0]\n", + "\n", + " cost_history_dict[\"iters\"] += 1\n", + " cost_history_dict[\"prev_vector\"] = params\n", + " cost_history_dict[\"cost_history\"].append(energy)\n", + " print(f\"Iters. done: {cost_history_dict['iters']} [Current cost: {energy}]\")\n", + "\n", + " return energy\n", + "\n", + "\n", + "cost_history_dict = {\n", + " \"prev_vector\": None,\n", + " \"iters\": 0,\n", + " \"cost_history\": [],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "95537d30-ba5d-48ab-bf6a-82edc8f0a348", + "metadata": {}, + "source": [ + "It is highly advantageous if you can choose initial parameter values based on knowledge of the problem at hand and characteristics of the target state. We will make no assumptions of such knowledge and use random initial values." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "01a5d370-2947-4441-b71b-ffbdeddf52f0", + "metadata": {}, + "outputs": [], + "source": [ + "x0 = 2 * np.pi * np.random.random(num_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e0d33b8-7f9c-428d-a76d-d832747b8430", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iters. done: 1 [Current cost: 0.010575798722044727]\n", + "Iters. done: 2 [Current cost: 0.004040015974440895]\n", + "Iters. done: 3 [Current cost: 0.0020213258785942503]\n", + "Iters. done: 4 [Current cost: 0.18723082446726014]\n", + "Iters. done: 5 [Current cost: -0.2746792152068885]\n", + "Iters. done: 6 [Current cost: -0.3094547651648519]\n", + "Iters. done: 7 [Current cost: -0.05281985428356641]\n", + "Iters. done: 8 [Current cost: 0.00808560303514377]\n", + "Iters. done: 9 [Current cost: -0.0014821685303514388]\n", + "Iters. done: 10 [Current cost: -0.004759824281150161]\n", + "Iters. done: 11 [Current cost: 0.09942328705995292]\n", + "Iters. done: 12 [Current cost: 0.01092366214057508]\n", + "Iters. done: 13 [Current cost: 0.05017497496069776]\n", + "Iters. done: 14 [Current cost: 0.13028868414310696]\n", + "Iters. done: 15 [Current cost: 0.013747803514376994]\n", + "Iters. done: 16 [Current cost: 0.2583072432944498]\n", + "Iters. done: 17 [Current cost: -0.14422125655131562]\n", + "Iters. done: 18 [Current cost: -0.0004950150347678081]\n", + "Iters. done: 19 [Current cost: 0.00681082268370607]\n", + "Iters. done: 20 [Current cost: -0.0023377795527156544]\n", + "Iters. done: 21 [Current cost: 0.6027665591169237]\n", + "Iters. done: 22 [Current cost: 0.00596641373801917]\n", + "Iters. done: 23 [Current cost: -0.008318769968051117]\n", + "Iters. done: 24 [Current cost: -0.00026683306709265246]\n", + "Iters. done: 25 [Current cost: -0.007648222843450479]\n", + "Iters. done: 26 [Current cost: 0.004121086261980831]\n", + "Iters. done: 27 [Current cost: -0.004075019968051117]\n", + "Iters. done: 28 [Current cost: -0.004419369009584665]\n", + "Iters. done: 29 [Current cost: 0.213185460054037]\n", + "Iters. done: 30 [Current cost: -0.06505919572162797]\n", + "Iters. done: 31 [Current cost: -0.5334241316590271]\n", + "Iters. done: 32 [Current cost: 0.00218370607028754]\n", + "Iters. done: 33 [Current cost: 0.09579352143666908]\n", + "Iters. done: 34 [Current cost: -0.009274800319488819]\n", + "Iters. done: 35 [Current cost: -0.44395141360688106]\n", + "Iters. done: 36 [Current cost: 0.011747104632587858]\n", + "Iters. done: 37 [Current cost: -0.003344149361022364]\n", + "Iters. done: 38 [Current cost: 0.19138183916486304]\n", + "Iters. done: 39 [Current cost: 0.013513931813145209]\n" + ] + } + ], + "source": [ + "# This required 13 min, 20 s QPU time on an Eagle processor, 28 min total time.\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session)\n", + " estimator.options.default_shots = 10000\n", + "\n", + " res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa, hamiltonian_isa, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 50},\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "fe98bde2-b435-47d1-b227-ee897aa3fe3e", + "metadata": {}, + "source": [ + "We can look at the raw outputs." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b1d07809-5f98-4c11-9bf6-fc5b5c5fb47f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -0.5334241316590271\n", + " x: [ 1.024e+00 6.459e+00 3.625e+00 4.007e+00]\n", + " nfev: 39\n", + " maxcv: 0.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res" + ] + }, + { + "cell_type": "markdown", + "id": "80c958e5-f70e-4736-889a-f88a69c50890", + "metadata": {}, + "source": [ + "### 3.4 Step 4: Post-process results\n", + "\n", + "If the procedure terminates correctly, then the values in our dictionary should be equal to the solution vector and total number of function evaluations, respectively. This is easy to verify:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e87046c1-bfe9-4bb3-b7fd-1e4da55149fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'prev_vector': array([1.02397956, 6.45886604, 3.62479262, 4.00744128]),\n", + " 'iters': 39,\n", + " 'cost_history': [np.float64(0.010575798722044727),\n", + " np.float64(0.004040015974440895),\n", + " np.float64(0.0020213258785942503),\n", + " np.float64(0.18723082446726014),\n", + " np.float64(-0.2746792152068885),\n", + " np.float64(-0.3094547651648519),\n", + " np.float64(-0.05281985428356641),\n", + " np.float64(0.00808560303514377),\n", + " np.float64(-0.0014821685303514388),\n", + " np.float64(-0.004759824281150161),\n", + " np.float64(0.09942328705995292),\n", + " np.float64(0.01092366214057508),\n", + " np.float64(0.05017497496069776),\n", + " np.float64(0.13028868414310696),\n", + " np.float64(0.013747803514376994),\n", + " np.float64(0.2583072432944498),\n", + " np.float64(-0.14422125655131562),\n", + " np.float64(-0.0004950150347678081),\n", + " np.float64(0.00681082268370607),\n", + " np.float64(-0.0023377795527156544),\n", + " np.float64(0.6027665591169237),\n", + " np.float64(0.00596641373801917),\n", + " np.float64(-0.008318769968051117),\n", + " np.float64(-0.00026683306709265246),\n", + " np.float64(-0.007648222843450479),\n", + " np.float64(0.004121086261980831),\n", + " np.float64(-0.004075019968051117),\n", + " np.float64(-0.004419369009584665),\n", + " np.float64(0.213185460054037),\n", + " np.float64(-0.06505919572162797),\n", + " np.float64(-0.5334241316590271),\n", + " np.float64(0.00218370607028754),\n", + " np.float64(0.09579352143666908),\n", + " np.float64(-0.009274800319488819),\n", + " np.float64(-0.44395141360688106),\n", + " np.float64(0.011747104632587858),\n", + " np.float64(-0.003344149361022364),\n", + " np.float64(0.19138183916486304),\n", + " np.float64(0.013513931813145209)]}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost_history_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a789373a-8d32-4761-ba21-6b2f98a7ae5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "x = np.linspace(0, 10, 50)\n", + "\n", + "# Define the constant function\n", + "constant = -0.7029\n", + "y_constant = np.full_like(x, constant)\n", + "ax.plot(\n", + " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", + ")\n", + "ax.set_xlabel(\"Iterations\")\n", + "ax.set_ylabel(\"Cost\")\n", + "ax.plot(y_constant, label=\"Target\")\n", + "plt.legend()\n", + "plt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "4be8cb84-ec8c-4c83-880f-b9c296282040", + "metadata": {}, + "source": [ + "IBM Quantum has other upskilling offerings related to VQE. If you are ready to put VQE into practice, see our tutorial: [Ground-state energy estimation of the Heisenberg chain with VQE](/docs/tutorials/spin-chain-vqe). If you want more information on creating molecular Hamiltonians, see [this lesson](/learning/courses/quantum-chem-with-vqe/hamiltonian-construction) in our course on [Quantum chemistry with VQE](/learning/courses/quantum-chem-with-vqe). If you are interested in a deeper understanding of how variational algorithms like VQE work, we recommend the course [Variational Algorithm Design](/learning/courses/variational-algorithm-design/optimization-loops).\n", + "\n", + "\n", + "#### Check your understanding\n", + "\n", + "In this section, we calculated a ground state energy from a Hamiltonian. If we wanted to apply this to say, determining the geometry of a molecule, how would we extend this?\n", + "\n", + "\n", + "\n", + "\n", + "We would need to introduce variables for inter-atomic spacing, and the angles between bonds. We would need to vary these. For every variation of these, we would produce a new Hamiltonian (since the operators describing the energy certainly depend on the geometry). For each such Hamiltonian produced and mapped onto qubits, we would need to carry out optimization like that done above. Of all those many converged optimization problems, the geometry that produced the lowest energy would be the one adopted by nature. This is quite a bit more involved than what was shown above. Such a calculation is done for the simplest molecule, $\\text{H}_2$, [here](/learning/courses/quantum-chem-with-vqe/geometry).\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "33231f54-a9c1-499c-a4ad-b4404b843909", + "metadata": {}, + "source": [ + "## 4. VQE's relationship to other methods\n", + "\n", + "In this section we will review the advantages and disadvantages of the original VQE approach and point out its relationships to other, more recent algorithms.\n", + "\n", + "### 4.1 The strengths and weaknesses of VQE\n", + "\n", + "Some strengths have already been pointed out. They include:\n", + "\n", + "* Suitability to modern hardware: Some quantum algorithms require much lower error rates, approaching large scale fault tolerance. VQE does not; it can be implemented on current quantum computers.\n", + "* Shallow circuits: VQE often employs relatively shallow quantum circuits. This makes VQE less susceptible to accumulated gate errors and makes it suitable for many error mitigation techniques. Of course, the circuits are not always shallow; this depends on the ansatz used.\n", + "* Versatility: VQE can (in principle) be applied to any problem that can be cast as an eigenvalue/eigenvector problem. There are many caveats that make VQE impractical or disadvantageous for some problems. Some of these are recapped below.\n", + "\n", + "Some weaknesses of VQE and problems for which it is impractical have also been described above. These include:\n", + "\n", + "* Heuristic nature: VQE does not guarantee convergence to the correct ground state energy, as its performance depends on the choice of ansatz and optimization methods[\\[1-2\\]](#references). If a poor ansatz is chosen that lacks the requisite entanglement for the desired ground state, no classical optimizer can reach that ground state.\n", + "* Potentially numerous parameters: A very expressive ansatz may have so many parameters that the minimization iterations are very time-consuming.\n", + "* High measurement overhead: In VQE, Estimator is used to estimate the expectation value of each term in the Hamiltonian. Most Hamiltonians of interest will have terms that cannot be simultaneously estimated. This can make VQE resource-intensive for large systems with complicated Hamiltonians[\\[1\\]](#references).\n", + "* Effects of noise: When the classical optimizer is searching for a minimum, noisy calculations can confuse it and steer it away from the true minimum or delay its convergence. One possible solution for this is leveraging state-of-the-art error mitigation and error suppression techniques[\\[2-3\\]](#references) from IBM.\n", + "* Barren plateaus: These regions of vanishing gradients[\\[2-3\\]](#references) exist even in the absence of noise, but noise makes them more troublesome since the change in expectation values due to noise could be larger than the change from updating parameters in these barren regions.\n", + "\n", + "### 4.2 Relationship to other approaches\n", + "\n", + "#### Adapt-VQE\n", + "\n", + "The **ADAPT-VQE** (Adaptive Derivative-Assembled Pseudo-Trotter Variational Quantum Eigensolver) algorithm is an enhancement of the original VQE algorithm, designed to improve efficiency, accuracy, and scalability for quantum simulations, particularly in quantum chemistry.\n", + "\n", + "The original VQE algorithm described throughout this lesson uses a predefined, fixed ansatz to approximate the ground state of the system. In our case, we used `efficient_su2`, with a single repetition, using Y and RZ rotation gates. Although the parameters in the RZ gates changed, the structure of this ansatz and the gates used did not change.\n", + "\n", + "ADAPT-VQE addresses the limitations of VQE through adaptive ansatz construction. Instead of starting with a fixed ansatz, ADAPT-VQE dynamically builds the ansatz iteratively. At each step, it selects the operator from a predefined pool (such as fermionic excitation operators) that has the largest gradient with respect to the energy. This ensures that only the most impactful operators are added, leading to a compact and efficient ansatz[\\[4-6\\]](#references). This approach can have several beneficial effects:\n", + "\n", + "1. **Reduced Circuit Depth**: By growing the ansatz incrementally and focusing only on necessary operators, ADAPT-VQE minimizes gate operations compared to traditional VQE approaches[\\[5,7\\]](#references).\n", + "2. **Improved Accuracy**: The adaptive nature allows ADAPT-VQE to recover more correlation energy at each step, making it particularly effective for strongly correlated systems where traditional VQE struggles[\\[8,9\\]](#references).\n", + "3. **Scalability and Noise Robustness**: The compact ansatz reduces the accumulation of gate errors, reduces computational overhead, and limits the number of variational parameters which must be minimized.\n", + "\n", + "ADAPT-VQE is still not perfect. In some cases it can become trapped or slowed by local minima, and it may suffer from over-parameterization. It can also be fairly resource intensive, since it requires the calculation of gradients and parameter optimization with many gate structures.\n", + "\n", + "#### Quantum phase estimation (QPE)\n", + "\n", + "QPE is similar in purpose to VQE, but very different in implementation. QPE requires fault-tolerant quantum computers due to its generally deep quantum circuits and the high level of coherence it requires. Once QPE can be implemented, it would be more precise than VQE. One way of describing the difference is through the precision as a function of circuit depth. QPE achieves precision $\\epsilon$ with circuit depths scaling as $O(1/\\epsilon)$ [\\[10\\]](#references). VQE requires $O(1/\\epsilon^2)$ samples to achieve the same precision[\\[10,11\\]](#references).\n", + "\n", + "#### Krylov, SQD, QSCI, and others in this course\n", + "\n", + "VQE helped establish quantum algorithms that still depend on classical computers, not just for operating the quantum computer, but for substantial parts of the algorithm. Several such algorithms are the focus of the remainder of this course. Here, we give a cursory explanation of a few, simply to compare and contrast them to VQE. They will be explained in much greater detail in subsequent lessons.\n", + "\n", + "__Krylov quantum diagonalization (KQD)__\n", + "\n", + "__Krylov subspace methods__ are ways of projecting a matrix onto a subspace to reduce its dimension and make it more manageable, while keeping the most important features. One trick in this method is to generate a subspace that keeps these features; it turns out that generating this subspace is closely related to a well-established method on quantum computers called __Trotterization__.\n", + "\n", + "There are a few variants of quantum Krylov methods, but generally the approach is:\n", + "* Use the quantum computer to generate a subspace (the Krylov subspace) through Trotterization\n", + "* Project the matrix of interest onto that Krylov subspace\n", + "* Diagonalize the new projected Hamiltonian using a classical computer\n", + "\n", + "__Sampling-based quantum diagonalization (SQD)__\n", + "\n", + "__Sampling-based quantum diagonalization (SQD)__ is related to the Krylov method in that it also attempts to reduce the dimension of a matrix to be diagonalized while preserving key features. SQD does this in the following way:\n", + "* Begin with an initial guess for your ground state and prepare the system in that ground state.\n", + "* Use Sampler to sample the bitstrings that make up this state.\n", + "* Use the collection of computational basis states from sampler as the subspace onto which you project your matrix of interest.\n", + "* Diagonalize the smaller, projected matrix using a classical computer.\n", + "\n", + "This is related to VQE in that it leverages classical and quantum computing for substantial algorithm components. They both also share the requirement that we prepare a good initial guess or ansatz. But the distribution of work between the classical and quantum computers in SQD is more like that of the Krylov method.\n", + "\n", + "In fact, the Krylov method and SQD have recently been combined into the sampling-based Krylov quantum diagonalization (SKQD) method [\\[12\\]](#references).\n", + "\n", + "__Quantum subspace configuration interaction__\n", + "\n", + "__Quantum Selected Configuration Interaction (QSCI)__[\\[13\\]](#references) is an algorithm that produces an approximated ground state of a Hamiltonian by sampling a trial wave function to identify the significant computational basis states to generate a subspace for a classical diagonalization.\n", + "Both SQD and QSCI use a quantum computer to construct a reduced subspace. QSCI's additional strength is in its state preparation, especially in the context of chemistry problems. It leverages various strategies such as using time-evolved states [\\[14\\]](#references) and a set of chemistry-inspired ansätze. By focusing on efficient state preparation, QSCI reduces quantum computational costs for chemical Hamiltonians while maintaining high fidelity and leveraging the noise robustness from quantum state sampling techniques [\\[15\\]](#references). QSCI also provides an adaptive construction technique which provides more ansätze for a better result.\n", + "\n", + "The default workflow of QSCI for chemistry problem is as follows:\n", + "* Build the molecular Hamiltonian using your software of choice (such as SciPy).\n", + "* Prepare a QSCI algorithm by selecting a proper initial state and a chemistry-inspired ansatz with a pre-selected set of parameters.\n", + "* Sample significant basis states and diagonalize the Hamiltonian using a classical computer to obtain the ground state energy.\n", + "* Often one uses configuration recovery [\\[16\\]](#references) and symmetry postselection [\\[15\\]](#references) as a post processing technique.\n", + "* Optionally, the workflow of adaptive QSCI has an additional optimization loop from step2 to step3, by using more ansätze with a random initial states.\n", + "\n", + "\n", + "#### Check your understanding\n", + "\n", + "What does VQE have in common with all the other methods listed above (except QPE which is not described in great detail)\n", + "\n", + "\n", + "\n", + "\n", + "All involve a trial state or wave function of some sort. All work best when the initial guess for this trial state is excellent.\n", + "\n", + "Another correct answer is that they are all easiest to implement when the Hamiltonian is easy to measure (can be sorted into relatively few groups of commuting Pauli operators).\n", + "\n", + "\n", + "\n", + "\n", + "What does VQE have in common with none of the other methods listed above?\n", + "\n", + "\n", + "\n", + "\n", + "Classical optimizers. None of the others use classical optimization algorithms to select variational parameters.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "8086b6a5-daf2-457b-bc7a-84db943b333f", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[2] https://en.wikipedia.org/wiki/Variational_quantum_eigensolver\n", + "\n", + "[3] https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.19.024047\n", + "\n", + "[4] https://arxiv.org/abs/2111.05176\n", + "\n", + "[6] https://inquanto.quantinuum.com/tutorials/InQ_tut_fe4n2_2.html\n", + "\n", + "[7] https://www.nature.com/articles/s41467-019-10988-2\n", + "\n", + "[8] https://arxiv.org/abs/2210.15438\n", + "\n", + "[9] https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.013254\n", + "\n", + "[10] https://arxiv.org/html/2403.09624v1\n", + "\n", + "[11] https://www.nature.com/articles/s42005-023-01312-y\n", + "\n", + "[13] https://arxiv.org/abs/1802.00171\n", + "\n", + "[14] https://arxiv.org/abs/2103.08505\n", + "\n", + "[15] https://arxiv.org/html/2501.09702v1\n", + "\n", + "[16] https://quri-sdk.qunasys.com/docs/examples/quri-algo-vm/qsci/\n", + "\n", + "[17] https://arxiv.org/abs/2412.13839\n", + "\n", + "[18] https://arxiv.org/abs/2302.11320v1\n", + "\n", + "[19] https://arxiv.org/pdf/2405.05068v1" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning/courses/quantum-machine-learning/data-encoding.ipynb b/learning/courses/quantum-machine-learning/data-encoding.ipynb index f48c9a8199f..a0ad7f22b40 100644 --- a/learning/courses/quantum-machine-learning/data-encoding.ipynb +++ b/learning/courses/quantum-machine-learning/data-encoding.ipynb @@ -1,1653 +1,1654 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0210ee7c-f989-4eb0-9231-ae5deacb6091", - "metadata": {}, - "source": [ - "---\n", - "title: Data encoding\n", - "description: An overview of data encoding methods in quantum machine learning. Encoding schemes covered include basis, amplitude, angle, and dense encoding.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore pfmap backgroundcolor linesize, imshow, minpos, nonumber, zzcircuit, zcircuit, pcircuit, zzcx, Schuld, Francesco, Petruccione, Vojtech, Adrian, Perez, Cervera, Lierta, Elies, Fuster, Jose, Latorre, Sweke, Jakob, Adrián, Pérez-Salinas, Alba, Cervera-Lierta, Elies, Gil-Fuster, José, QSVMs, VQCs, pnts, ZZFM */}\n", - "\n", - "# Data encoding\n", - "\n", - "## Introduction and notation\n", - "\n", - "To use a quantum algorithm, classical data must somehow be brought into a quantum circuit. This is usually referred to as data *encoding*, but is also called data *loading*. Recall from previous lessons the notion of a feature mapping, a mapping of data features from one space to another. Just transferring classical data to a quantum computer is a sort of mapping, and could be called a feature mapping. In practice, the built-in feature mappings in Qiskit (like `z_Feature Map and ZZ Feature Map) will typically include rotation layers and entangling layers that extend the state to many dimensions in the Hilbert space. This encoding process is a critical part of quantum machine learning algorithms and directly affects their computational capabilities.\n", - "\n", - "Some of the encoding techniques below can be efficiently classically simulated; this is particularly easy to see in encoding methods that yield product states (that is, they do not entangle qubits). And remember that quantum utility is most likely to lie where the quantum-like complexity of the dataset is well-matched by the encoding method. So it is very likely that you will end up writing your own encoding circuits. Here, we show a wide variety of possible encoding strategies simply so that you can compare and contrast them, and see what is possible. There are some very general statements that can be made about the usefulness of encoding techniques. For example, `efficient_su2` (see below) with a full entangling scheme is much more likely to capture quantum features of data than methods that yield product states (like `z_feature_map`). But this does not mean `efficient_su2` is sufficient, or sufficiently well-matched to your dataset, to yield a quantum speed-up. That requires careful consideration of the structure of the data being modeled or classified. There is also a balancing act with circuit depth, since many feature maps which fully entangle the qubits in a circuit yield very deep circuits, too deep to get usable results on today's quantum computers.\n", - "\n", - "### Notation\n", - "\n", - "A dataset is a set of $M$ data vectors: $\\text{X} = \\{\\vec{x}^{(j)}\\,|\\,j\\in [M]\\}$, where each vector is $N$ dimensional, that is, $\\vec{x}^{(j)}=(\\vec{x}^{(j)}_1,\\ldots,\\vec{x}^{(j)}_N)\\in\\mathbb{R}^N$. This could be extended to complex data features. In this lesson, we may occasionally use these notations for the full set $(\\text{X}),$ and its specific elements like $\\vec{x}^{(j)}$. But we will mostly refer to the loading of a single vector from our dataset at a time, and will often simply refer to a single vector of $N$ features as $\\vec{x}$.\n", - "\n", - "Additionally, it is common to use the symbol $\\Phi(\\vec{x})$ to refer to the feature mapping $\\Phi$ of data vector $\\vec{x}$. In quantum computing specifically, it is common to refer to mappings in quantum computing using $U(\\vec{x}),$ a notation that reinforces the unitary nature of these operations. One could correctly use the same symbol for both; both are feature mappings. Throughout this course, we tend to use:\n", - "* $\\Phi(\\vec{x})$ when discussing feature mappings in machine learning, generally, and\n", - "* $U(\\vec{x})$ when discussing circuit implementations of feature mappings.\n", - "\n", - "### Normalization and information loss\n", - "\n", - "In classical machine learning, training data features are often \"normalized\" or rescaled which often improves model performance. One common way of doing this is by using min-max normalization or standardization. In min-max normalization, feature columns of the data matrix $\\text{X}$ (say, feature $k$) are normalized:\n", - "\n", - "$$\n", - "x^{'(i)}_k = \\frac{x^{(i)}_k - \\text{min}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}}{\\text{max}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}-\\text{min}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}}\n", - "$$\n", - "\n", - "where min and max refer to the minimum and maximum of feature $k$ over the $M$ data vectors in the dataset $\\text{X}$. All the feature values then fall in the unit interval: $x^{'(i)}_k \\in [0,1]$ for all $i\\in [M]$, $k\\in[N]$.\n", - "\n", - "Normalization is also a fundamental concept in quantum mechanics and quantum computing, but it is slightly different from min-max normalization. Normalization in quantum mechanics requires that the length (in the context of quantum computing, the 2-norm) of a state vector $|\\psi\\rangle$ is equal to unity: $\\|\\psi\\|=\\sqrt{\\langle\\psi|\\psi\\rangle} = 1$, ensuring that measurement probabilities sum to 1. The state is normalized by dividing by the 2-norm; that is, by rescaling\n", - "$$\n", - "|\\psi\\rangle\\rightarrow\\|\\psi\\|^{-1}|\\psi\\rangle\n", - "$$\n", - "In quantum computing and quantum mechanics, this is not a normalization imposed by people on the data, but a fundamental property of quantum states. Depending on your encoding scheme, this constraint may affect how your data are rescaled. For example, in amplitude encoding (see below), the data vector is normalized $\\vert\\vec{x}^{(j)}\\vert = 1$ as is required by quantum mechanics, and this affects the scaling of the data being encoded. In phase encoding, feature values are recommended to be rescaled as $\\vec{x}^{(j)}_i \\in (0,2\\pi]$ so that there is no information loss due to the modulo-$2\\pi$ effect of encoding to a qubit phase angle[\\[1,2\\]](#references)." - ] - }, - { - "cell_type": "markdown", - "id": "930db404-47dc-4dfd-8ca0-cff02fb7cb90", - "metadata": { - "formulas": { - "_ket-dataset": { - "meaning": "This is a quantum statevector that represents our dataset, 𝒳.", - "say": "Ket script X" - }, - "_ket-x": { - "meaning": "This vertical bar and angled bracket mean we're referring to a ket (column vector) with label 'x'.", - "say": "Ket x" - }, - "_m": { - "meaning": "This is the number of entries in our dataset." - }, - "_sum-m": { - "meaning": "This notation means we add together |xm〉 (the mth element of our dataset) for all values of m between 1 and M (that is, the entire dataset).", - "say": "Sum of all computational basis states in our dataset (𝒳)" - }, - "_x-lil-m": { - "meaning": "This is the mth element of the dataset. It is an N-dimensional vector. Here, the 'm' is just used to mean \"any number between 1 and M\"", - "say": "X little-M" - }, - "brace": { - "meaning": "These curly brackets (braces) mean everything inside the brackets forms a set.", - "say": "Brace (or \"curly bracket\")", - "type": "Universal notation" - }, - "ellipsis": { - "meaning": "These dots omit things where the pattern can be implied.", - "say": "Ellipsis", - "type": "Universal notation" - }, - "in": { - "meaning": "This symbol means the things to the left of the symbol are contained in the set to the right of the symbol.", - "say": "In", - "type": "Universal notation" - }, - "script-x": { - "meaning": "This is a symbol we’ve chosen to represent our dataset.", - "say": "Script X", - "type": "Locally defined variable" - } - } - }, - "source": [ - "## Methods of encoding\n", - "\n", - "In the next few sections, we will refer to a small example classical dataset $\\text{X}_\\text{ex}$ consisting of $M=5$ data vectors, each with $N=3$ features:\n", - "\n", - "$$\n", - "\\text{X}_{\\text{ex}}=\\{(4,8,5),(9,8,6),(2,9,2),(5,7,0),(3,7,5)\\}\n", - "$$\n", - "\n", - "In the notation introduced above, we might say the $1^\\text{st}$ feature of the $4^\\text{th}$ data vector in our set $\\text{X}_{\\text{ex}}$ is $\\vec{x}^{(4)}_1 = 5,$ for example." - ] - }, - { - "cell_type": "markdown", - "id": "288c73ec-bdfe-4879-b918-0aed7f0c3c5c", - "metadata": {}, - "source": [ - "### Basis encoding\n", - "\n", - "Basis encoding encodes a classical $P$-bit string into a computational basis state of a $P$-qubit system. Take for example $\\vec{x}^{(1)}_3 = 5 = 0(2^3)+1(2^2)+0(2^1)+1(2^0).$ This can be represented as a $4$-bit string as $(0101)$, and by a $4$-qubit system as the quantum state $|0101\\rangle$. More generally, for a $P$-bit string: $\\vec{x}^{(j)}_k = (b_1, b_2, ... , b_P)$, the corresponding $P$-qubit state is $|x^{(j)}_k\\rangle = | b_1, b_2, ... , b_P \\rangle$ with $b_n \\in \\{0,1\\}$ for $n = 1 , \\dots , P$. Note that this is just for a single feature.\n", - "\n", - "Basis encoding in quantum computing represents each classical bit as a separate qubit, mapping the binary representation of data directly onto quantum states in the computational basis. When multiple features need to be encoded, each feature is first converted to its binary form and then assigned to a distinct group of qubits — one group per feature — where each qubit reflects a bit in the binary representation of that feature.\n", - "\n", - "As an example, let us encode the vector (5, 7, 0).\n", - "\n", - "Suppose all features are stored in four bits (more than we need, but enough to represent any integer that is single-digit in base 10):\n", - "\n", - " 5 → binary 0101\n", - "\n", - " 7 → binary 0111\n", - "\n", - " 0 → binary 0000\n", - "\n", - "These bit strings are assigned to three sets of four qubits, so the overall 12-qubit basis state is:\n", - "$$\n", - "∣0101 0111 0000⟩\n", - "$$\n", - "\n", - "Here, the first four qubits represent the first feature, the next four qubits the second feature, and the last four qubits the third feature. The code below converts the data vector (5,7,0) to a quantum state, and is generalized to do so for other single-digit features." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "85ee995f-1e50-4860-a24c-16bbc8b5c8b0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "# Data point to encode\n", - "x = 5 # binary: 0101\n", - "y = 7 # binary: 0111\n", - "z = 0 # binary: 0000\n", - "\n", - "# Convert each to 4-bit binary list\n", - "x_bits = [int(b) for b in format(x, \"04b\")] # [0,1,0,1]\n", - "y_bits = [int(b) for b in format(y, \"04b\")] # [0,1,1,1]\n", - "z_bits = [int(b) for b in format(z, \"04b\")] # [0,0,0,0]\n", - "\n", - "# Combine all bits\n", - "all_bits = x_bits + y_bits + z_bits # [0,1,0,1,0,1,1,1,0,0,0,0]\n", - "\n", - "# Initialize a 12-qubit quantum circuit\n", - "qc = QuantumCircuit(12)\n", - "\n", - "# Apply x-gates where the bit is 1\n", - "for idx, bit in enumerate(all_bits):\n", - " if bit == 1:\n", - " qc.x(idx)\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "887174c0-6a8b-438d-84cc-80451352d9e9", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, - "source": [ - "#### Check your understanding\n", - "\n", - "Write code to encode the first vector in our example data set $\\text{X}_{\\text{ex}}$:\n", - "\n", - "$$\\vec{x}^{(1)}=(4,8,5)$$\n", - "\n", - "using basis encoding.\n", - "\n", - "\n", - "\n", - "\n", - "```python\n", - "import math\n", - "from qiskit import QuantumCircuit\n", - "\n", - "# Data point to encode\n", - "x = 4 # binary: 0100\n", - "y = 8 # binary: 1000\n", - "z = 5 # binary: 0101\n", - "\n", - "# Convert each to 4-bit binary list\n", - "x_bits = [int(b) for b in format(x, '04b')] # [0,1,0,0]\n", - "y_bits = [int(b) for b in format(y, '04b')] # [1,0,0,0]\n", - "z_bits = [int(b) for b in format(z, '04b')] # [0,1,0,1]\n", - "\n", - "# Combine all bits\n", - "all_bits = x_bits + y_bits + z_bits # [0,1,0,0,1,0,0,0,0,1,0,1]\n", - "\n", - "# Initialize a 12-qubit quantum circuit\n", - "qc = QuantumCircuit(12)\n", - "\n", - "# Apply x-gates where the bit is 1\n", - "for idx, bit in enumerate(all_bits):\n", - " if bit == 1:\n", - " qc.x(idx)\n", - "\n", - "qc.draw('mpl')\n", - "```\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "c47289a9-64f0-4541-b25c-41b2a51f86ad", - "metadata": { - "formulas": { - "_a-norm": { - "meaning": "This is a normalization constant. We can calculate it from the inverse of the Euclidean (l2) norm of the datapoint vector.", - "say": "A norm" - } - } - }, - "source": [ - "### Amplitude encoding\n", - "\n", - "Amplitude encoding encodes data into the amplitudes of a quantum state. It represents a normalized classical $N$-dimensional data vector, $\\vec{x}^{(j)}$, as the amplitudes of a $n$-qubit quantum state, $|\\psi_x\\rangle$:\n", - "\n", - "$$\n", - "|\\psi^{(j)}_x\\rangle = \\frac{1}{\\alpha}\\sum_{i=1}^N x^{(j)}_i |i\\rangle\n", - "$$\n", - "\n", - "where $N$ is the same dimension of the data vectors as before, $\\vec{x}^{(j)}_i$ is the $i^{th}$ element of $\\vec{x}^{(j)}$ and $|i\\rangle$ is the $i^{th}$ computational basis state. Here, $\\alpha$ is a normalization constant to be determined from the data being encoded. This is the normalization condition imposed by quantum mechanics:\n", - "\n", - "$$\n", - "\\sum_{i=1}^N \\left|x^{(j)}_i\\right|^2 = \\left|\\alpha\\right|^2.\n", - "$$\n", - "\n", - "In general, this is a different condition than the min/max normalization used for each feature across all data vectors. Precisely how this is navigated will depend on your problem. But there is no way around the quantum mechanical normalization condition above.\n", - "\n", - "In amplitude encoding, each feature in a data vector is stored as an amplitude of a different quantum state. As a system of $n$ qubits provides $2^n$ amplitudes, amplitude encoding of $N$ features requires $n \\ge \\mathrm{log}_2(N)$ qubits.\n", - "\n", - "As an example, let's encode the first vector in our example dataset $\\text{X}_\\text{ex}$, $\\vec{x}^{(1)} = (4,8,5)$ using amplitude encoding. Normalizing the resulting vector, we get:\n", - "\n", - "$$\n", - "\\sum_{i=1}^N \\left|x^{(1)}_i\\right|^2 = 4^2+8^2+5^2 = 105 = \\left|\\alpha\\right|^2 \\rightarrow \\alpha = \\sqrt{105}\n", - "$$\n", - "\n", - "and the resulting 2-qubit quantum state would be:\n", - "\n", - "$$\n", - "|\\psi(\\vec{x}^{(1)})\\rangle = \\frac{1}{\\sqrt{105}}(4|00\\rangle+8|01\\rangle+5|10\\rangle+0|11\\rangle)\n", - "$$\n", - "\n", - "In the example above, the number of features in the vector $N=3$, is not a power of 2. When $N$ is not a power of 2, we simply choose a value for the number of qubits $n$ such that $2^n\\geq N$ and pad the amplitude vector with uninformative constants (here, a zero).\n", - "\n", - "Like in basis encoding, once we calculate what state will encode our dataset, in Qiskit we can use the `initialize` function to prepare it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19810c6d-8d60-49ee-bd6f-6f6fbd5e7363", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import math\n", - "\n", - "desired_state = [\n", - " 1 / math.sqrt(105) * 4,\n", - " 1 / math.sqrt(105) * 8,\n", - " 1 / math.sqrt(105) * 5,\n", - " 1 / math.sqrt(105) * 0,\n", - "]\n", - "\n", - "qc = QuantumCircuit(2)\n", - "qc.initialize(desired_state, [0, 1])\n", - "\n", - "qc.decompose(reps=5).draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "58509d89-68ba-4fd5-92b9-278c47497eb9", - "metadata": {}, - "source": [ - "An advantage of amplitude encoding is the aforementioned requirement of only $\\mathrm{log}_2(N)$ qubits to encode. However, subsequent algorithms must operate on the amplitudes of a quantum state, and methods to prepare and measure the quantum states tend not to be efficient." - ] - }, - { - "cell_type": "markdown", - "id": "639dd02e-28ad-4d82-8091-ddc00d066666", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "Write down the normalized state for encoding the following vector (made of two vectors from our example dataset):\n", - "\n", - "$$\\vec{x}=(9,8,6,2,9,2)$$\n", - "\n", - "using amplitude encoding.\n", - "\n", - "\n", - "\n", - "To encode 6 numbers, we will need to have at least 6 available states on whose amplitudes we can encode. This will require 3 qubits. Using an unknown normalization factor $\\alpha$, we can write this as:\n", - "\n", - "$$\n", - "|\\psi\\rangle = \\alpha(9|000\\rangle+8|001\\rangle+6|010\\rangle+2|011\\rangle+9|100\\rangle+2|101\\rangle+0|110\\rangle+0|111\\rangle)\n", - "$$\n", - "Note that\n", - "$$\n", - "\\langle \\psi|\\psi\\rangle = |\\alpha|^2\\times(9^2+8^2+6^2+2^2+9^2+2^2+0^2+0^2) = |\\alpha|^2\\times(270)=1 \\rightarrow \\alpha = \\frac{1}{\\sqrt{270}}\n", - "$$\n", - "So finally,\n", - "$$\n", - "|\\psi\\rangle = \\frac{1}{\\sqrt{270}}(9|000\\rangle+8|001\\rangle+6|010\\rangle+2|011\\rangle+9|100\\rangle+2|101\\rangle+0|110\\rangle+0|111\\rangle)\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "For the same data vector $\\vec{x}=(9,8,6,2,9,2),$ write code to create a circuit that loads these data features using amplitude encoding.\n", - "\n", - "\n", - "\n", - "\n", - "```python\n", - "desired_state = [\n", - " 9 / math.sqrt(270),\n", - " 8 / math.sqrt(270),\n", - " 6 / math.sqrt(270),\n", - " 2 / math.sqrt(270),\n", - " 9 / math.sqrt(270),\n", - " 2 / math.sqrt(270),\n", - " 0,\n", - " 0,\n", - "]\n", - "\n", - "print(desired_state)\n", - "\n", - "qc = QuantumCircuit(3)\n", - "qc.initialize(desired_state, [0, 1, 2])\n", - "qc.decompose(reps=8).draw(output=\"mpl\")\n", - "```\n", - "\n", - "[0.5477225575051662, 0.48686449556014766, 0.36514837167011077, 0.12171612389003691, 0.5477225575051662, 0.12171612389003691, 0, 0]\n", - "\n", - "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin2.avif)\n", - "\n", - "\n", - "\n", - "\n", - "You may need to deal with very large data vectors. Consider the vector\n", - "\n", - "$$\\vec{x}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5).$$\n", - "\n", - "Write code to automate the normalization, and generate a quantum circuit for amplitude encoding.\n", - "\n", - "\n", - "\n", - "\n", - "There are many possible answers. Here is code that prints a few steps along the way:\n", - "\n", - "```python\n", - "import numpy as np\n", - "from math import sqrt\n", - "\n", - "init_list = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5]\n", - "qubits = round(np.log(len(init_list)) / np.log(2) + 0.4999999999)\n", - "need_length = 2**qubits\n", - "pad = need_length - len(init_list)\n", - "for i in range(0, pad):\n", - " init_list.append(0)\n", - "\n", - "init_array = np.array(init_list) # Unnormalized data vector\n", - "length = sqrt(\n", - " sum(init_array[i] ** 2 for i in range(0, len(init_array)))\n", - ") # Vector length\n", - "norm_array = init_array / length # Normalized array\n", - "print(\"Normalized array:\")\n", - "print(norm_array)\n", - "print()\n", - "\n", - "qubit_numbers = []\n", - "for i in range(0, qubits):\n", - " qubit_numbers.append(i)\n", - "print(qubit_numbers)\n", - "\n", - "qc = QuantumCircuit(qubits)\n", - "qc.initialize(norm_array, qubit_numbers)\n", - "qc.decompose(reps=7).draw(output=\"mpl\")\n", - "```\n", - "\n", - "Normalized array:\n", - "[0.17342199 0.34684399 0.21677749 0.39019949 0.34684399 0.26013299\n", - " 0.086711 0.39019949 0.086711 0.21677749 0.30348849 0.\n", - " 0.1300665 0.30348849 0.21677749 0. ]\n", - "\n", - "[0, 1, 2, 3]\n", - "\n", - "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin3.avif)\n", - "\n", - "\n", - "\n", - "\n", - "Do you see advantages to amplitude encoding over basis encoding? If so, explain.\n", - "\n", - "\n", - "\n", - "\n", - "There may be several answers. One answer is that, given the fixed ordering of the basis states, this amplitude encoding preserves the order of the numbers encoded. It will often also be encoded more densely.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "cbedf6cc-33a8-4be7-90ec-81af4d224871", - "metadata": {}, - "source": [ - "A benefit of amplitude encoding is that only $\\log_2(N)$ qubits are required for an $N$-dimensional ($N$-feature) data vector $\\vec{x}\\rightarrow|\\vec{x}\\rangle$. However, amplitude encoding is generally an inefficient procedure that requires arbitrary state preparation, which is exponential in the number of CNOT gates. Stated differently, the state preparation has a polynomial runtime complexity of $\\mathcal O(N)$ in the number of dimensions, where $N = 2^n$, and $n$ is the number of qubits. Amplitude encoding “provides an exponential saving in space at the cost of an exponential increase in time”[\\[3\\]](#references); however, runtime increases to $\\mathcal O(\\log N)$ are achievable in certain cases[\\[4\\]](#references). For an end-to-end quantum speedup, the data loading runtime complexity needs to be considered." - ] - }, - { - "cell_type": "markdown", - "id": "4ef15d5f-4730-4ea8-95fd-75aff06487ff", - "metadata": { - "formulas": { - "_big-o-times-n": { - "meaning": "This represents the tensor product operation over N qubits.", - "say": "big o-times" - }, - "_big-o-times-n2": { - "meaning": "This represents the tensor product operation over N/2 qubits.", - "say": "big o-times" - } - } - }, - "source": [ - "### Angle encoding\n", - "\n", - "Angle encoding is of interest in many QML models using Pauli feature maps such as quantum support vector machines (QSVMs) and variational quantum circuits (VQCs), among others. Angle encoding is closely related to phase encoding and dense angle encoding which are presented below. Here we will use \"angle encoding\" to refer to a rotation in $\\theta$, that is, a rotation away from the $z$ axis accomplished for example by an $R_X$ gate or an $R_Y$ gate[\\[1,3\\]](#references). Really, one can encode data in *any* rotation or combination of rotations. But $R_Y$ is common in the literature, so we emphasize it here.\n", - "\n", - "When applied to a single qubit, angle encoding imparts a Y-axis rotation proportional to the data value. Consider the encoding of a single ($k^\\text{th}$)feature from the $j^\\text{th}$ data vector in a dataset, $\\vec{x}^{(j)}_k$:\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}_k\\rangle = R_Y(\\theta=\\vec{x}^{(j)}_k)|0\\rangle = \\textstyle\\cos\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|0\\rangle + \\sin\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|1\\rangle.\n", - "$$\n", - "\n", - "Alternatively, angle encoding can be performed using $R_X(\\theta)$ gates, although the encoded state would have a complex relative phase compared to $R_Y(\\theta)$.\n", - "\n", - "Angle encoding is different from the previous two methods discussed in several ways. In angle encoding:\n", - "- Each feature value is mapped to a corresponding qubit, $\\vec{x}^{(j)}_k \\rightarrow Q_k$, leaving the qubits in a product state.\n", - "- One numerical value is encoded at a time, rather than a whole set of features from a data point.\n", - "- $n$ qubits are required for $N$ data features, where $n\\leq N$. Often equality holds, here. We'll see how $n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import numpy as np\n", - "from qiskit.visualization.bloch import Bloch\n", - "from qiskit.visualization.state_visualization import _bloch_multivector_data\n", - "\n", - "\n", - "def plot_Nstates(states, axis, plot_trace_points=True):\n", - " \"\"\"This function plots N states to 1 Bloch sphere\"\"\"\n", - " bloch_vecs = [_bloch_multivector_data(s)[0] for s in states]\n", - "\n", - " if axis is None:\n", - " bloch_plot = Bloch()\n", - " else:\n", - " bloch_plot = Bloch(axes=axis)\n", - "\n", - " bloch_plot.add_vectors(bloch_vecs)\n", - "\n", - " if len(states) > 1:\n", - "\n", - " def rgba_map(x, num):\n", - " g = (0.95 - 0.05) / (num - 1)\n", - " i = 0.95 - g * num\n", - " y = g * x + i\n", - " return (0.0, y, 0.0, 0.7)\n", - "\n", - " num = len(states)\n", - " bloch_plot.vector_color = [rgba_map(x, num) for x in range(1, num + 1)]\n", - "\n", - " bloch_plot.vector_width = 3\n", - " bloch_plot.vector_style = \"simple\"\n", - "\n", - " if plot_trace_points:\n", - "\n", - " def trace_points(bloch_vec1, bloch_vec2):\n", - " # bloch_vec = (x,y,z)\n", - " n_points = 15\n", - " thetas = np.arccos([bloch_vec1[2], bloch_vec2[2]])\n", - " phis = np.arctan2(\n", - " [bloch_vec1[1], bloch_vec2[1]], [bloch_vec1[0], bloch_vec2[0]]\n", - " )\n", - " if phis[1] < 0:\n", - " phis[1] = phis[1] + 2 * pi\n", - " angles0 = np.linspace(phis[0], phis[1], n_points)\n", - " angles1 = np.linspace(thetas[0], thetas[1], n_points)\n", - "\n", - " xp = np.cos(angles0) * np.sin(angles1)\n", - " yp = np.sin(angles0) * np.sin(angles1)\n", - " zp = np.cos(angles1)\n", - " pnts = [xp, yp, zp]\n", - " bloch_plot.add_points(pnts)\n", - " bloch_plot.point_color = \"k\"\n", - " bloch_plot.point_size = [4] * len(bloch_plot.points)\n", - " bloch_plot.point_marker = [\"o\"]\n", - "\n", - " for i in range(len(bloch_vecs) - 1):\n", - " trace_points(bloch_vecs[i], bloch_vecs[i + 1])\n", - "\n", - " bloch_plot.sphere_alpha = 0.05\n", - " bloch_plot.frame_alpha = 0.15\n", - " bloch_plot.figsize = [4, 4]\n", - "\n", - " bloch_plot.render()\n", - "\n", - "\n", - "plot_Nstates(states, axis=None, plot_trace_points=True)" - ] - }, - { - "cell_type": "markdown", - "id": "6ba88e2e-4774-4a78-9971-b8b1df927229", - "metadata": {}, - "source": [ - "That was just a single feature of a single data vector. When encoding $N$ features into the rotation angles of $n$ qubits, say for the $j^\\text{th}$ data vector $\\vec{x}^{(j)} = (x_1,...,x_N),$ the encoded product state will look like this:\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}\\rangle = \\bigotimes^N_{k=1} \\cos(\\vec{x}^{(j)}_k)|0\\rangle + \\sin(\\vec{x}^{(j)}_k)|1\\rangle\n", - "$$\n", - "\n", - "We note that this is equivalent to\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}\\rangle = \\bigotimes^N_{k=1} R_Y(2\\vec{x}^{(j)}_k)|0\\rangle.\n", - "$$\n", - "\n", - "#### Check your understanding\n", - "\n", - "Encode the data vector $\\vec{x} = (0, \\pi/4, \\pi/2)$ using angle encoding, as described above.\n", - "\n", - "\n", - "\n", - "\n", - "```python\n", - "qc = QuantumCircuit(3)\n", - "qc.ry(0, 0)\n", - "qc.ry(2 * math.pi / 4, 1)\n", - "qc.ry(2 * math.pi / 2, 2)\n", - "qc.draw(output=\"mpl\")\n", - "```\n", - "\n", - "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin4.avif)\n", - "\n", - "\n", - "\n", - "\n", - "Using angle encoding as described above, how many qubits are required to encode 5 features?\n", - "\n", - "\n", - "\n", - "\n", - "5\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "64cd9b41-00f5-4212-b52e-939ca96f84c7", - "metadata": {}, - "source": [ - "### Phase encoding\n", - "\n", - "Phase encoding is very similar to the angle encoding described above. The phase angle of a qubit is a real-valued angle $\\phi$ about the $z$-axis from the +$x$-axis. Data are mapped with a phase rotation, $P(\\phi) = e^{i\\phi/2}R_Z(\\phi)$, where $\\phi \\in (0,2\\pi]$ (see [Qiskit PhaseGate](/docs/api/qiskit/qiskit.circuit.library.PhaseGate) for more information). It is recommended to rescale data so that $\\vec{x}^{(j)}_k \\in (0,2\\pi]$. This prevents information loss and other potentially unwanted effects[\\[1,2\\]](#references).\n", - "\n", - "A qubit is often initialized in the state $|0\\rangle$, which is an eigenstate of the phase rotation operator, meaning that the qubit state first needs to be rotated for phase encoding to be implemented. It therefore makes sense to initialize the state with a Hadamard gate: $H|0\\rangle = |+\\rangle = \\textstyle\\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$. Phase encoding on a single qubit means imparting a relative phase proportional to the data value:\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}_k\\rangle = P(\\phi=\\vec{x}^{(j)}_k)|+\\rangle = \\textstyle\\frac{1}{\\sqrt{2}}\\big(|0\\rangle + e^{i\\vec{x}^{(j)}_k}|1\\rangle\\big).\n", - "$$\n", - "\n", - "The phase encoding procedure maps each feature value to the phase of a corresponding qubit, $\\vec{x}^{(j)}_k \\rightarrow Q_k$. In total, phase encoding has a circuit depth of 2, including the Hadamard layer, which makes it an efficient encoding scheme. The phase-encoded multi-qubit state ($n$ qubits for $N=n$ features) is a product state:\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}\\rangle = \\bigotimes_{k=1}^{N} P_k(\\phi = \\vec{x}^{(j)}_k)|+\\rangle^{\\otimes N} = {\\textstyle\\frac{1}{\\sqrt{2^N}}} \\bigotimes_{k=1}^{N}\\big(|0\\rangle + e^{i\\vec{x}^{(j)}_k}|1\\rangle\\big).\n", - "$$\n", - "\n", - "The following Qiskit code first prepares the initial state of a single qubit by rotating it with a Hadamard gate, then rotates it again using a phase gate to encode a data feature $\\vec{x}^{(j)}_k=\\frac{1}{2}\\pi$." - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "ba0886eb-1c56-4b15-a731-d94d805254e1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "qc.h(0) # Hadamard gate rotates state down to Bloch equator\n", - "state1 = Statevector.from_instruction(qc)\n", - "\n", - "qc.p(pi / 2, 0) # Phase gate rotates by an angle pi/2\n", - "state2 = Statevector.from_instruction(qc)\n", - "\n", - "states = state1, state2\n", - "\n", - "qc.draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "02ee25db-7368-40d0-9ba8-52e231ede3e0", - "metadata": {}, - "source": [ - "We can visualize the rotation in $\\phi$ using the plot_Nstates function we defined." - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "f7c9cf29-2ad6-43af-a7e3-e590e41d7e67", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_Nstates(states, axis=None, plot_trace_points=True)" - ] - }, - { - "cell_type": "markdown", - "id": "803fcc6b-a6c2-463d-8e46-e83074faf025", - "metadata": {}, - "source": [ - "The Bloch sphere plot shows the Z-axis rotation $|+\\rangle \\rightarrow P(\\frac{1}{2}\\pi)|+\\rangle$ where $\\vec{x}^{(j)}_k=\\frac{1}{2}\\pi$. The light green arrow shows the final state.\n", - "\n", - "Phase encoding is used in many quantum feature maps, particularly $Z$ and $ZZ$ feature maps, and general Pauli feature maps, among others." - ] - }, - { - "cell_type": "markdown", - "id": "f9f2786b-9fff-4425-8387-0a60ce690689", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "How many qubits are required in order to use phase encoding as described above to store 8 features?\n", - "\n", - "\n", - "\n", - "\n", - "8\n", - "\n", - "\n", - "\n", - "\n", - "Write code to the vector $$\\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0)$$ using phase encoding.\n", - "\n", - "\n", - "\n", - "\n", - "There may be many answers. Here is one example:\n", - "\n", - "```python\n", - "phase_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0]\n", - "qc = QuantumCircuit(len(phase_data))\n", - "for i in range(0, len(phase_data)):\n", - " qc.h(i)\n", - " qc.rz(phase_data[i] * 2 * math.pi / float(max(phase_data)), i)\n", - "qc.draw(output=\"mpl\")\n", - "```\n", - "\n", - "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin5.avif)\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "910625ad-d798-4a78-835c-2b92a046dbb1", - "metadata": {}, - "source": [ - "### Dense angle encoding\n", - "\n", - "Dense angle encoding (DAE) is a combination of angle encoding and phase encoding. DAE allows two feature values to be encoded in a single qubit: one angle with a Y-axis rotation angle, and the other with a $z$-axis rotation angle: $\\vec{x}^{(j)}_k,$ $\\vec{x}^{(j)}_\\ell \\rightarrow \\theta, \\phi$. It encodes two features as follows:\n", - "\n", - "$$\n", - "|\\vec{x}^{(j)}_k,\\vec{x}^{(j)}_\\ell\\rangle = R_Z(\\phi=\\vec{x}^{(j)}_\\ell) R_Y(\\theta=\\vec{x}^{(j)}_k)|0\\rangle = \\cos\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|0\\rangle + e^{i\\vec{x}^{(j)}_\\ell} \\sin\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|1\\rangle.\n", - "$$\n", - "\n", - "Encoding two data features to one qubit results in a $2\\times$ reduction in the number of qubits required for the encoding. Extending this to more features, the data vector $\\vec{x} = (x_1,...,x_N)$ can be encoded as:\n", - "\n", - "$$\n", - "|\\vec{x}\\rangle = \\bigotimes_{k=1}^{N/2} \\cos(x_{2k-1})|0\\rangle + e^{i x_{2k}}\\sin(x_{2k-1})|1\\rangle\n", - "$$\n", - "\n", - "DAE can be generalized to arbitrary functions of the two features instead of the sinusoidal functions used here. This is called general qubit encoding[\\[7\\]](#references).\n", - "\n", - "As an example of DAE, the code below encodes and visualizes the encoding of the features $x_1=\\theta = 3\\pi/8$ and $x_2=\\phi = 7\\pi/4$." - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "9a6bb041-d7a1-4e29-a463-81b93b900e96", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "state1 = Statevector.from_instruction(qc)\n", - "qc.ry(3 * pi / 8, 0)\n", - "state2 = Statevector.from_instruction(qc)\n", - "qc.rz(7 * pi / 4, 0)\n", - "state3 = Statevector.from_instruction(qc)\n", - "states = state1, state2, state3\n", - "\n", - "plot_Nstates(states, axis=None, plot_trace_points=True)" - ] - }, - { - "cell_type": "markdown", - "id": "cdc69e45-3651-453b-9e04-5e6f6c82f8b1", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "Given the treatment above, how many qubits are needed to encode 6 features using dense encoding?\n", - "\n", - "\n", - "\n", - "\n", - "3\n", - "\n", - "\n", - "\n", - "\n", - "Write code to load the vector $$\\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5)$$ using dense angle encoding.\n", - "\n", - "\n", - "\n", - "\n", - "Note that we have padded the list with a \"0\" to avoid the problem of there being a single unused parameter in our encoding scheme.\n", - "\n", - "```python\n", - "dense_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5, 0]\n", - "qc = QuantumCircuit(int(len(dense_data) / 2))\n", - "entry = 0\n", - "for i in range(0, int(len(dense_data) / 2)):\n", - " qc.ry(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)\n", - " entry = entry + 1\n", - " qc.rz(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)\n", - " entry = entry + 1\n", - "qc.draw(output=\"mpl\")\n", - "```\n", - "\n", - "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin6.avif)\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "1035f985-3ab9-476d-8173-c4b0a764d46f", - "metadata": {}, - "source": [ - "## Encoding with built-in feature maps\n", - "\n", - "### Encoding at arbitrary points\n", - "\n", - "Angle encoding, phase encoding, and dense encoding prepared product states with a feature encoded on each qubit (or two features per qubit). This is different from basis encoding and amplitude encoding, in that those methods make use of entangled states. There is not a 1:1 correspondence between data feature and qubit. In amplitude encoding, for example, you might have one feature as the amplitude of the state $|01\\rangle$ and another feature as the amplitude for $|10\\rangle$. Generally, methods that encode in product states yield shallower circuits and can store 1 or 2 features on each qubit. Methods that use entanglement and associate a feature with a state rather than a qubit result in deeper circuits, and can store more features per qubit on average.\n", - "\n", - "But encoding need not be entirely in product states or entirely in entangled states as in amplitude encoding. Indeed, many encoding schemes built into Qiskit allow encoding both before and after an entanglement layer, as opposed to just at the beginning. This is known as \"data reuploading\". For related work, see references [5] and [6].\n", - "\n", - "In this section, we will use and visualize a few of the built-in encoding schemes. All the methods in this section encode $N$ features as rotations on $N$ parameterized gates on $n$ qubits, where $n \\leq N$. Note that maximizing data loading for a given number of qubits is not the only consideration. In many cases, circuit depth may be an even more important consideration than qubit count." - ] - }, - { - "cell_type": "markdown", - "id": "0f95bc3b-bff1-469f-92ff-b2f6f523b978", - "metadata": {}, - "source": [ - "### Efficient SU2\n", - "\n", - "A common and useful example of encoding with entanglement is Qiskit's [`efficient_su2`](/docs/api/qiskit/qiskit.circuit.library.EfficientSU2) circuit. Impressively, this circuit can, for example, encode 8 features on only 2 qubits. Let's see this, and then try to understand how it is possible." - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "6b657226-ae95-41f6-b78b-5def930d0080", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import efficient_su2\n", - "\n", - "circuit = efficient_su2(num_qubits=2, reps=1, insert_barriers=True)\n", - "circuit.decompose().draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "0f573f85-c2a2-449e-bcd4-74d42072e0db", - "metadata": {}, - "source": [ - "As we write our state, we will use the Qiskit convention that least-significant qubits are ordered to the far right, as in $|q_2,q_1,q_0\\rangle$ or $|q_2\\rangle\\otimes|q_1\\rangle\\otimes|q_0\\rangle.$ These states can become very complicated very quickly, and this rare example may help explain why such states are seldom written out explicitly.\n", - "\n", - "Our system starts in the state $|00\\rangle.$ Up to the first barrier (a point we label $b1$), our states are:\n", - "\n", - "$$\n", - "|\\psi\\rangle_{b1} = \\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)|0\\rangle+\\sin\\left(\\frac{\\theta_1}{2}\\right)e^{i\\theta_3}|1\\rangle\\right)\\otimes\\left(\\cos\\left(\\frac{\\theta_0}{2}\\right)|0\\rangle+\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}|1\\rangle\\right)\n", - "$$\n", - "\n", - "That's just dense encoding, which we've seen before. Now after the CNOT gate, at the second barrier ($b2$), our state is" - ] - }, - { - "cell_type": "markdown", - "id": "b3008413-246f-459f-b8b3-4f7610e93f2b", - "metadata": {}, - "source": [ - "$$\n", - "\\begin{aligned}\n", - "|\\psi\\rangle_{b2} = & \\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_0}{2}\\right)|00\\rangle+\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}|11\\rangle\\\\\n", - "+ & \\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_3}|10\\rangle+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}e^{i\\theta_3}|01\\rangle\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "52d99dcf-2cd7-4777-a92b-765f8f8c02c2", - "metadata": {}, - "source": [ - "We now apply the last set of single-qubit rotations and collect like states to obtain:" - ] - }, - { - "cell_type": "markdown", - "id": "fae33b09-fc78-42d8-98f0-7fdd6fa0ae5c", - "metadata": {}, - "source": [ - "$$\n", - "\\begin{align*}\n", - "|\\psi\\rangle_{\\text{final}} = &\n", - "\\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", - "\n", - "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", - "|00\\rangle\\\\\n", - "\n", - "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", - "\n", - "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(-\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", - "e^{i\\theta_6}|01\\rangle\\\\\n", - "\n", - "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", - "\n", - "- & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", - "e^{i\\theta_7}|10\\rangle\\\\\n", - "\n", - "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", - "\n", - "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", - "e^{i\\theta_6}e^{i\\theta_7}|11\\rangle\n", - "\n", - "\\end{align*}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "93e9e6ae-85e8-4077-9c25-f947daebe610", - "metadata": {}, - "source": [ - "This is likely too complicated to parse. Instead, just step back and think about how many parameters we loaded onto the state: eight. But we have with just four computational basis states. At first glance, it may appear that we have loaded more parameters than makes sense, since the final state can be written as $\\psi_\\text{final} = c_0|00\\rangle+c_1|01\\rangle+c_2|10\\rangle+c_3|11\\rangle$. Note, however, that each prefactor is complex! Written like this:\n", - "$$\n", - "\\psi_\\text{final} = (a_0+ib_0)|00\\rangle+(a_1+ib_1)|01\\rangle+(a_2+ib_2)|10\\rangle+(a_3+ib_3)|11\\rangle\n", - "$$\n", - "One can see that we do, indeed, have eight parameters on the state on which to encode our eight features.\n", - "\n", - "By increasing the number of qubits and increasing the number of repetitions of entangling and rotation layers, one can encode much more data. Writing out the wave functions quickly becomes intractable. But we can still see the encoding in action." - ] - }, - { - "cell_type": "markdown", - "id": "63ee5098-028a-4e40-a03d-c6f0e7c75605", - "metadata": {}, - "source": [ - "Here we encode the data vector $\\vec{x}$ with 12 features, on a 3-qubit `efficient_su2` circuit, using each of the parameterized gates to encode a different feature.\n", - "\n", - "$$\n", - "\\vec{x} = (0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2)\n", - "$$\n", - "\n", - "In this data vector, the features are shown in a particular order. In isolation, it doesn't matter if they are encoded in this order or in the reverse. What is important is keeping track of it and being consistent. Note in the circuit diagram that `efficient_su2` assumes a certain ordering of encoding, specifically filling the first layer of parameterized gates from qubit 0 to qubit 2, and then moving to the next layer. This is neither consistent nor inconsistent with little-endian notation, since here the data features cannot be ordered by qubit *a priori*, before an encoding circuit has been specified." - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "73fc00fe-b98f-4d63-a327-54958a8f5498", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2]\n", - "circuit = efficient_su2(num_qubits=3, reps=1, insert_barriers=True)\n", - "encode = circuit.assign_parameters(x)\n", - "encode.decompose().draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "92a323d6-58e6-4e4d-b02f-9c63a0e78aaf", - "metadata": {}, - "source": [ - "Instead of increasing the number of qubits, you might choose to increase the number of repetitions of entangling and rotation layers. But there are limits to how many repetitions are useful." - ] - }, - { - "cell_type": "markdown", - "id": "90a4abfc-7d1e-4495-b36d-6f7edeed8e96", - "metadata": {}, - "source": [ - "As previously stated, there is a tradeoff: circuits with more qubits or more repetitions of entangling and rotation layers may store more parameters, but do so with greater circuit depth. We will return to the depths of some built-in feature maps, below." - ] - }, - { - "cell_type": "markdown", - "id": "95126be4-3e8e-4ee4-9420-1b362d88bef8", - "metadata": {}, - "source": [ - "The next few encoding methods that are built into Qiskit have \"feature map\" as part of their names. Let us reiterate that encoding data into a quantum circuit *is* a feature mapping, in the sense that it takes data into a new space: the Hilbert space of the qubits involved. The relationship between the dimensionality of the original feature space and that of the Hilbert space will depend on the circuit you use for encoding." - ] - }, - { - "cell_type": "markdown", - "id": "a3fb23b0-ee8c-49ba-99b7-f256fdbba9d5", - "metadata": {}, - "source": [ - "### $Z$ feature map\n", - "\n", - "The $Z$ feature map (ZFM) can be interpreted as a natural extension of phase encoding. The ZFM consists of alternating layers of single-qubit gates: Hadamard gate layers and phase gate layers. Let the data vector $\\vec{x}$ have $N$ features. The quantum circuit that performs the feature mapping is represented as a unitary operator that acts on the initial state:\n", - "\n", - "$$\n", - "\\mathscr{U}_{\\text{ZFM}}(\\vec{x})|0\\rangle^{\\otimes N}=|\\phi(\\vec{x})\\rangle\n", - "$$\n", - "where $|0\\rangle^{\\otimes N}$ is the $N$-qubit ground state. This notation is used for consistency with reference [\\[4\\]](#references) Havlicek et al. The data features $x_i$ are mapped one-to-one with corresponding qubits. For example, if you have 8 features in a data vector, then you would use 8 qubits. The ZFM circuit is composed of $r$ repetitions of a subcircuit comprised of Hadamard gate layers and phase gate layers. A Hadamard layer is made up of a Hadamard gate acting on every qubit in an $n$-qubit register, $H \\otimes H \\otimes \\dots \\otimes H = H^{\\otimes n}$, within the same stage of the algorithm. This description also applies to a phase gate layer in which the $i^\\text{th}$ qubit is acted on by $P(\\vec{x}_i)$. Each $P$ gate has one feature as an argument, but the phase gate layer ($P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)$ is a function of the data vector. The full ZFM circuit unitary with a single repetition is:\n", - "$$\n", - "\\mathscr{U}_{\\text{ZFM}}=\\big(P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)H^{\\otimes N}\\big)=\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\n", - "$$\n", - "Then $r$ repetitions of this unitary would be\n", - "$$\n", - "\\mathscr{U}^{(r)}_{\\text{ZFM}}\\left(\\vec{x}\\right)=\\prod_{s=1}^{r}\\left[\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\\right]\n", - "$$\n", - "The data features, $x_k$, are mapped to the phase gates in the same way in all $r$ repetitions. The ZFM feature map state is a product state and is efficient for classical simulation[\\[4\\]](#references).\n", - "\n", - "To start with a small example, a two-qubit ZFM circuit is coded using Qiskit and drawn to display the simple circuit structure. In the example, a single repetition, $r=1$, is implemented with the data vector $\\vec{x} = \\left(\\textstyle\\frac{1}{2}\\pi, \\textstyle\\frac{1}{3}\\pi\\right)$. Note that this is written in the standard order of a vector in Python, meaning the $0^\\text{th}$ element is $\\textstyle\\frac{1}{2}\\pi.$ We are free to encode this $0^\\text{th}$ feature onto our $0^\\text{th}$ qubit, or onto our $N^\\text{th}.$ Again, there cannot always be a single 1:1 mapping from feature order to qubit order, since different feature maps encode different numbers of features to each qubit. Again what is important is that we are aware of where each feature is being encoded. When providing a parameter list to the $Z$ feature map, it will encode feature 0 from the list to the least-significant qubit with a parameterized gate, as in qubit 0. So we will follow that convention when doing this by hand. We will encode $\\textstyle\\frac{1}{2}\\pi$ on the $0^\\text{th}$ qubit, and $\\textstyle\\frac{1}{3}\\pi$ on the $1^\\text{st}$ qubit.\n", - "\n", - "The ZFM circuit unitary operator acts on the initial state in the following way:\n", - "\n", - "$$\n", - "\\mathscr{U}_{\\text{ZFM}}(\\bar{x})|00\\rangle = P(\\bar{x})^{\\otimes 2} H^{\\otimes 2}|00\\rangle = \\left( P\\left(\\textstyle\\frac{1}{3}\\pi\\right)H|0\\rangle \\right) \\otimes \\left(P\\left(\\textstyle\\frac{1}{2}\\pi\\right)H|0\\rangle\\right).\n", - "$$\n", - "\n", - "The formula has been rearranged around the tensor product to emphasize the operations on each qubit. The following Qiskit code uses Hadamard and phase gates explicitly to show the structure of the ZFM:" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "f5c70df4-faea-4817-a870-95638eb97dbd", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc0 = QuantumCircuit(1)\n", - "qc1 = QuantumCircuit(1)\n", - "\n", - "qc0.h(0)\n", - "qc0.p(pi / 2, 0)\n", - "\n", - "qc1.h(0)\n", - "qc1.p(pi / 3, 0)\n", - "\n", - "# Combine circuits qc0 and qc1 into 1 circuit\n", - "qc = QuantumCircuit(2)\n", - "qc.compose(qc0, [0], inplace=True)\n", - "qc.compose(qc1, [1], inplace=True)\n", - "\n", - "qc.draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "b1be7f51-3a4e-4d2e-8d51-d3ac62f4fd88", - "metadata": {}, - "source": [ - "We now encode the same data vector $\\vec{x} = \\left(\\textstyle\\frac{1}{2}\\pi, \\textstyle\\frac{1}{3}\\pi\\right)$ to a ZFM circuit with three repetitions, $r=3$, using the Qiskit [`z_feature_map`](/docs/api/qiskit/qiskit.circuit.library.ZFeatureMap) class, which altogether gives us the quantum feature map $\\mathscr{U}_{\\text{ZFM}}(\\vec{x})$. By default in the `z_feature_map` class, parameters $\\beta$ are multiplied by 2 before mapping to the phase gate $\\beta \\rightarrow P(\\theta = 2\\beta)$. To reproduce the same encodings as above, we divide by 2." - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "c15b8fe2-ae83-4c76-a908-71596deb7d82", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import z_feature_map\n", - "\n", - "zfeature_map = z_feature_map(feature_dimension=2, reps=3)\n", - "zfeature_map = zfeature_map.assign_parameters([(1 / 2) * pi / 2, (1 / 2) * pi / 3])\n", - "zfeature_map.decompose().draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "6551f7cd-8e7e-4313-9255-7c1a3928f625", - "metadata": {}, - "source": [ - "Clearly this is a different mapping from the one done by hand above, but note the consistency in parameter ordering: $\\textstyle\\frac{1}{2}\\pi$ was again encoded on the $0^\\text{th}$ qubit.\n", - "\n", - "You may use ZFM via Qiskit's ZFM class; you can also use this structure as inspiration to construct your own feature mapping." - ] - }, - { - "cell_type": "markdown", - "id": "08bbc7eb-b48c-44fa-be70-852b27b73343", - "metadata": {}, - "source": [ - "### $ZZ$ feature map\n", - "\n", - "The $ZZ$ feature map (ZZFM) extends the ZFM with the inclusion of two-qubit entangling gates, specifically the $ZZ$-rotation gate $R_{ZZ}(\\theta)$. The ZZFM is conjectured to be generally expensive to compute on a classical computer, unlike the ZFM.\n", - "\n", - "$R_{ZZ}(\\theta)$ implements a $ZZ$-interaction and is maximally entangling for $\\theta = \\textstyle{\\frac{1}{2}}\\pi$. $R_{ZZ}(\\theta)$ can be decomposed into a series of gates on two qubits, as shown in the following Qiskit code using the [RZZ gate](/docs/api/qiskit/qiskit.circuit.library.RZZGate) and the `QuantumCircuit` class method ```decompose```. We encode a single feature of the data vector $\\vec{x}$: $\\vec{x}_k=\\pi.$" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "id": "6c312a5f-91a5-499c-a391-efc73cd0e4e1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.rzz(pi, 0, 1)\n", - "qc.draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "0795b997-4193-44cc-8310-4b8f5490aff0", - "metadata": {}, - "source": [ - "As is often the case, we see this represented as a single gate-like unit, until we use .decompose() to see all constituent gates." - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "92062646-78f2-4dd6-82ad-7a543cb6a566", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc.decompose().draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "2c8a07ad-be40-4c18-b9e2-905a46fc7f4b", - "metadata": {}, - "source": [ - "Data is mapped with a phase rotation $P(\\theta) = e^{i\\theta/2}R_Z(\\theta)$ on the second qubit. The $R_{ZZ}(\\theta)$ gate entangles the two qubits on which it operates by a degree of entanglement determined by the encoded feature value.\n", - "\n", - "The full ZZFM circuit consists of a Hadamard gate and phase gate, as in the ZFM, followed by the entanglement described above. A single repetition of the ZZFM circuit is:\n", - "\n", - "$$\n", - "\\mathscr{U}_{\\text{ZZFM}}(\\vec{x}) = U_{ZZ}(\\vec{x})\\big(P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)H^{\\otimes N}\\big)=U_{ZZ}(\\vec{x})\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N},\n", - "$$\n", - "\n", - "where $U_{ZZ}(\\vec{x})$ contains ZZ-gate layer structured by an entanglement scheme. Several entanglement schemes are shown in code blocks below. The structure of $U_{ZZ}(\\vec{x})$ also includes a function that combines the data features from qubits being entangled in the following way. Let us say that the $R_{ZZ}$ gate is to be applied to qubits $p$ and $q$. In the phase layer, these qubits have phase gates that encode $\\vec{x}_p$ and $\\vec{x}_q$ on them, respectively. The argument $\\theta_{q,p}$ of the $R_{ZZ,q,p}(\\theta_{q,p})$ will not simply be one of these features or the other, but a function often denoted by $\\phi$ (not to be confused with the azimuthal angle):\n", - "$$\n", - "\\theta_{q,p} \\rightarrow \\phi(\\vec{x}_q, \\vec{x}_p) = 2(\\pi-\\vec{x}_q)(\\pi-\\vec{x}_p).\n", - "$$\n", - "We will see this in several examples below. The extension to multiple repetitions is the same as in the `z_feature_map` case:\n", - "$$\n", - "\\mathscr{U}^{(r)}_{\\text{ZZFM}}\\left(\\vec{x}\\right)=\\prod_{s=1}^{r}\\left[U_{ZZ}(\\vec{x})\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\\right].\n", - "$$\n", - "As the operators have increased in complexity, let us first encode a data vector $\\vec{x} = (x_0, x_1)$ with a two-qubit ZZFM and one repetition using the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "1ec2f2e1-b665-4dde-a223-35489f17c695", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import zz_feature_map\n", - "\n", - "feature_dim = 2\n", - "zzfeature_map = zz_feature_map(\n", - " feature_dimension=feature_dim, entanglement=\"linear\", reps=1\n", - ")\n", - "zzfeature_map.decompose(reps=1).draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "2fa647fb-587d-4343-8fcb-e1bb34fea584", - "metadata": {}, - "source": [ - "By default in Qiskit, the features $(\\vec{x}_1, \\vec{x}_2)$ are mapped together to $R_{ZZ}(\\theta)$ by this mapping function $\\theta_{1,2} = \\phi(\\vec{x}_1, \\vec{x}_2) = 2(\\pi-\\vec{x}_1)(\\pi-\\vec{x}_2)$. Qiskit allows the user to customize the function $\\phi$ (or $\\phi_S$ where $S$ is the set of qubit pairs coupled through $R_{ZZ}$ gates) as a preprocessing step.\n", - "\n", - "Moving to a four-dimensional data vector $\\vec{x} = (\\vec{x}_1, \\vec{x}_2, \\vec{x}_3, \\vec{x}_4)$ and mapping to a four-qubit ZZFM with one repetition, we can start to see the mapping $\\phi$ for various qubit pairs. We can also see the meaning of \"linear\" entanglement:" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "979c765a-e3d8-4e3e-8e52-3f816203a934", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 53, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "feature_dim = 4\n", - "zzfeature_map = zz_feature_map(\n", - " feature_dimension=feature_dim, entanglement=\"linear\", reps=1\n", - ")\n", - "zzfeature_map.decompose().draw(\"mpl\", scale=1)" - ] - }, - { - "cell_type": "markdown", - "id": "77d42bee-a283-4d19-85a6-bd9c8deedae4", - "metadata": {}, - "source": [ - "In the linear entanglement scheme, nearest-neighbor (numbered) pairs of qubits in this circuit are entangled. There are other built-in entanglement schemes in Qiskit, including `circular` and `full`." - ] - }, - { - "cell_type": "markdown", - "id": "eec9d682-f698-47a1-bc2f-86174c2efb0f", - "metadata": {}, - "source": [ - "### Pauli feature map\n", - "\n", - "The Pauli feature map (PFM) is the generalization of the ZFM and ZZFM to use arbitrary Pauli gates. The Pauli feature map takes a very similar form to the previous two feature maps. For $r$ repetitions of the encoding of the $N$ features of vector $\\vec{x},$\n", - "\n", - "$$\n", - "\\mathscr{U}_{\\text{PFM}}(\\vec{x}) = \\prod_{s=1}^{r} U(\\vec{x}) H^{\\otimes n}.\n", - "$$\n", - "\n", - "For PFM, $U(\\vec{x})$ is generalized to a Pauli expansion unitary operator. Here we present a more generalized form of the feature maps considered so far:\n", - "\n", - "$$\n", - "U(\\vec{x}) = \\exp\\left(i \\sum_{S \\in\\mathcal{I}} \\phi_S(\\vec{x}) \\prod_{i \\in S} \\sigma_i \\right),\n", - "$$\n", - "\n", - "where $\\sigma_i$ is a Pauli operator, $\\sigma_i \\in {I,X,Y,Z}$. Here $\\mathcal{I}$ is the set of all qubit connectivities as determined by the feature map, including the set of qubits acted on by single-qubit gates. That is, for a feature map in which qubit 0 was acted upon by a phase gate, and qubits 2 and 3 were acted upon by an $R_{ZZ}$ gate, the set $\\mathcal{I}$ would include $\\{\\{0\\},\\{2,3\\}\\}$. $S$ runs through all elements of that set. In previous feature maps, the function $\\phi_S(\\vec{x})$ was involved either exclusively with single-qubit gates or exclusively with two-qubit gates. Here, we define it in general:\n", - "$$\n", - "\\phi_S(\\vec{x})=\n", - " \\begin{cases}\n", - " x_i & \\text{if } S= \\{i\\} \\text{ (single-qubit)}\\\\\n", - " \\prod_{j\\in{S}}(\\pi-x_j) & \\text{if } |S|\\ge2 \\text{ (multi-qubit)}\\\\\n", - " \\end{cases}\n", - "$$\n", - "\n", - "For documentation, see the [Qiskit `Pauli feature map` class documentation](/docs/api/qiskit/qiskit.circuit.library.PauliFeatureMap)). In the ZZFM, the operator $\\sigma_i$ is restricted to $Z_i$.\n", - "\n", - "One way to understand the above unitary is through analogy with the propagator in a physical system. The unitary above is a unitary evolution operator, $\\exp(it\\mathcal{H})$, for a Hamiltonian, $\\mathcal{H}$, similar to the Ising model, where the time parameter, $t$, is replaced with data values to drive the evolution. The expansion of this unitary operator gives the PFM circuit. The entangling connectivities in $S$ can be interpreted as Ising couplings in a spin lattice." - ] - }, - { - "cell_type": "markdown", - "id": "29290b2a-fe6d-4c0c-805a-ae37bb2e48a9", - "metadata": {}, - "source": [ - "Let us consider an example of Pauli $Y$ and $XX$ operators representing those Ising-type interactions. Qiskit provides a `pauli_feature_map` class for instantiating a PFM with a choice of single- and $n$-qubit gates, which in this example will be passed as Pauli strings `‘Y’` and `‘XX’`. Typically, $n$ is 1 or 2 for single- and two-qubit interactions, respectively. The entanglement scheme is “linear,” meaning that only nearest-neighbor qubits in the quantum circuit are coupled. Note that this does not correspond to nearest-neighbor qubits on the quantum computer itself, as this quantum circuit is an abstraction layer." - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "5ba5df82-83c1-428c-a269-baea5f75c3dc", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import pauli_feature_map\n", - "\n", - "feature_dim = 3\n", - "pfmap = pauli_feature_map(\n", - " feature_dimension=feature_dim, entanglement=\"linear\", reps=1, paulis=[\"Y\", \"XX\"]\n", - ")\n", - "\n", - "pfmap.decompose().draw(\"mpl\", scale=1.5)" - ] - }, - { - "cell_type": "markdown", - "id": "d1adef80-5504-4cf2-8d78-3a129637f67c", - "metadata": {}, - "source": [ - "Qiskit provides a parameter, $\\alpha$, in Pauli feature maps to control the scaling of Pauli rotations.\n", - "\n", - "$$\n", - "U(\\bar{x}) = \\exp\\left(i \\alpha \\sum_{S\\subseteq[n]} \\phi_S(\\bar{x}) \\prod_{i \\in S} \\sigma_i \\right)\n", - "$$\n", - "\n", - "The default value of $\\alpha$ is $2$. By optimizing its value in the interval, for example, $[0,4],$ one can better align a quantum kernel to the data." - ] - }, - { - "cell_type": "markdown", - "id": "c457b46a-9982-45eb-b2df-12fc7251d91e", - "metadata": {}, - "source": [ - "### Gallery of Pauli feature maps\n", - "\n", - "Here we visualize various Pauli feature maps for two-qubit circuits to get a better picture of the range of possibilities." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "69225757-193a-490b-8ca4-d1b187b774b3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit.visualization import circuit_drawer\n", - "import matplotlib.pyplot as plt\n", - "\n", - "feature_dim = 2\n", - "fig, axs = plt.subplots(9, 2)\n", - "i_plot = 0\n", - "for paulis in [\n", - " [\"I\"],\n", - " [\"X\"],\n", - " [\"Y\"],\n", - " [\"Z\"],\n", - " [\"XX\"],\n", - " [\"XY\"],\n", - " [\"XZ\"],\n", - " [\"YY\"],\n", - " [\"YZ\"],\n", - " [\"ZZ\"],\n", - " [\"X\", \"ZZ\"],\n", - " [\"Y\", \"ZZ\"],\n", - " [\"Z\", \"ZZ\"],\n", - " [\"X\", \"YZ\"],\n", - " [\"Y\", \"YZ\"],\n", - " [\"Z\", \"YZ\"],\n", - " [\"YY\", \"ZZ\"],\n", - " [\"XY\", \"ZZ\"],\n", - "]:\n", - " pfmap = pauli_feature_map(feature_dimension=feature_dim, paulis=paulis, reps=1)\n", - " circuit_drawer(\n", - " pfmap.decompose(),\n", - " output=\"mpl\",\n", - " style={\"backgroundcolor\": \"#EEEEEE\"},\n", - " ax=axs[int((i_plot - i_plot % 2) / 2), i_plot % 2],\n", - " )\n", - " axs[int((i_plot - i_plot % 2) / 2), i_plot % 2].title.set_text(paulis)\n", - " i_plot += 1\n", - "\n", - "fig.set_figheight(16)\n", - "fig.set_figwidth(16)" - ] - }, - { - "cell_type": "markdown", - "id": "61e585cc-8e84-496e-9e51-21a818731be8", - "metadata": {}, - "source": [ - "The above can, of course, be extended to include other permutations and repetitions of Pauli matrices. Learners are encouraged to experiment with those options." - ] - }, - { - "cell_type": "markdown", - "id": "44a27c71-acb8-4b4f-a344-836cae1e3896", - "metadata": {}, - "source": [ - "## Review of built-in feature maps\n", - "\n", - "You have seen several schemes for encoding data into a quantum circuit:\n", - "- Basis encoding\n", - "- Amplitude encoding\n", - "- Angle encoding\n", - "- Phase encoding\n", - "- Dense encoding\n", - "\n", - "You have seen how to construct your own feature maps using these encoding schemes, and you have seen four built-in feature maps which take advantage of angle and phase encoding:\n", - "- Efficient SU2\n", - "- Z feature map\n", - "- ZZ feature map\n", - "- Pauli feature map\n", - "\n", - "These built-in feature maps differed from each other in several ways:\n", - "- The depth for a given number of encoded features\n", - "- The number of qubits required for a given number of features\n", - "- The degree of entanglement (obviously related to the other differences)\n", - "\n", - "The code below applies these four built-in feature maps to the encoding of a feature set, and plots the two-qubit depth of the resulting circuit. Since two-qubit error rates are much higher than single-qubit gate error rates, one might reasonably be most interested in the depth of two-qubit gates. In the code below, we obtain counts of all gates in a circuit by first decomposing the circuit and then using count_ops(), as shown below. Here the two-qubit gates we are interested in are 'cx' gates:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7f428edb-ea96-48bb-adb2-aed91535dbeb", - "metadata": {}, - "outputs": [], - "source": [ - "# Initializing parameters and empty lists for depths\n", - "x = [0.1, 0.2]\n", - "n_data = []\n", - "zz2gates = []\n", - "su22gates = []\n", - "z2gates = []\n", - "p2gates = []\n", - "\n", - "# Generating feature maps\n", - "for n in range(3, 10):\n", - " x.append(n / 10)\n", - " zzcircuit = zz_feature_map(n, reps=1, insert_barriers=True)\n", - " zcircuit = z_feature_map(n, reps=1, insert_barriers=True)\n", - " su2circuit = efficient_su2(n, reps=1, insert_barriers=True)\n", - " pcircuit = pauli_feature_map(n, reps=1, paulis=[\"XX\"], insert_barriers=True)\n", - " # Getting the cx depths\n", - " zzcx = zzcircuit.decompose().count_ops().get(\"cx\")\n", - " zcx = zcircuit.decompose().count_ops().get(\"cx\")\n", - " su2cx = su2circuit.decompose().count_ops().get(\"cx\")\n", - " pcx = pcircuit.decompose().count_ops().get(\"cx\")\n", - "\n", - " # Appending the cx gate counts to the lists. We shift the zz and pauli data points, because they overlap.\n", - " n_data.append(n)\n", - " zz2gates.append(zzcx - 0.5)\n", - " z2gates.append(0)\n", - " su22gates.append(su2cx)\n", - " p2gates.append(pcx + 0.5)\n", - "\n", - "# Plot the output\n", - "plt.plot(n_data, p2gates, \"bo\")\n", - "plt.plot(n_data, zz2gates, \"ro\")\n", - "plt.plot(n_data, su22gates, \"yo\")\n", - "plt.plot(n_data, z2gates, \"go\")\n", - "plt.ylabel(\"CX Gates\")\n", - "plt.xlabel(\"Data elements\")\n", - "plt.legend([\"Pauli\", \"ZZ\", \"SU2\", \"Z\"])\n", - "# plt.suptitle('zz_feature_map(n)')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "777bf066-af13-4065-a795-af19f67da099", - "metadata": {}, - "source": [ - "Generally Pauli and ZZ feature maps will result in greater circuit depth and higher numbers of 2-qubit gates than `efficient_su2` and Z feature maps.\n", - "\n", - "Because the feature maps built into Qiskit are widely applicable, we will often not need to design our own, especially in the learning phase. However, experts in quantum machine learning will likely return to the subject of designing their own feature mapping, as they tackle two complicated challenges:\n", - "\n", - "1. Modern hardware: the presence of noise and the large overhead of error-correcting code mean that present-day applications will need to consider things like hardware efficiency and minimizing two-qubit gate depth.\n", - "\n", - "2. Mappings that fit the problem at hand: It is one thing to say that the `zz_feature_map`, for example, is difficult to simulate classically, and therefore interesting. It is quite another thing for the `zz_feature_map` to be ideally suited to __your__ machine learning task or data set. The performance of different parameterized quantum circuits on different types of data is an active area of investigation.\n", - "\n", - "We close with a note on hardware efficiency." - ] - }, - { - "cell_type": "markdown", - "id": "8b795f8d-1f9f-4e43-82df-f2725ff709e9", - "metadata": {}, - "source": [ - "## Hardware-efficient feature mapping\n", - "\n", - "A hardware-efficient feature mapping is one that takes into account constraints of real quantum computers, in the interest of reducing noise and errors in the computation. When running quantum circuits on near-term quantum computers, there are many strategies to mitigate noise inherent to the hardware. One main strategy for hardware efficiency is the minimization of the depth of the quantum circuit so that noise and decoherence have less time to corrupt the computation. The depth of a quantum circuit is the number of time-aligned gate steps required to complete the entire computation (after circuit optimization)[\\[5\\]](#references). Recall that the depth of the abstract, logical circuit may be much lower than the depth once the circuit is transpiled for a real quantum computer.\n", - "\n", - "Transpilation is the process of converting the quantum circuit from a high-level abstraction to one that is ready to run on a real quantum computer, taking into account constraints of the hardware. A quantum computer has a native set of single- and two-qubit gates. This means all gates in Qiskit code have to be transpiled into the set of native hardware gates. For example, in ibm_torino, a QPU sporting a Heron r1 processor and completed in 2023, the native or basis gates are `{CZ, ID, RZ, SX, X}`. These are the two-qubit controlled-Z gate, and single-qubit gates called identity, $Z$-rotation, square root of NOT, and NOT, respectively, providing a universal set. When implementing multi-qubit gates as an equivalent subcircuit, physical two-qubit $CZ$ gates are required, along with other single-qubit gates available in hardware. In addition, to perform a two-qubit gate on a pair of qubits that are not physically coupled, SWAP gates are added to move qubit states between qubits to enable coupling, which leads to an unavoidable extension of the circuit. Using the ```optimization``` argument that can be set from 0 up to a highest level of 3. For greater control and customizability, the transpiler pipeline can be managed with the [Qiskit Pass Manager](/docs/api/qiskit/qiskit.transpiler.PassManager). Refer to the [Qiskit Transpiler documentation](/docs/api/qiskit/transpiler) for more information on transpilation.\n", - "\n", - "In Havlicek et al. 2019 [\\[2\\]](#references), one way the authors achieve hardware efficiency is by using the $ZZ$ feature map because it is a second-order expansion (see the “$ZZ$ feature map” section above). An $N$-order expansion has $N$-qubit gates. IBM® quantum computers do not have native $N$-qubit gates, where $N>2$, so to implement them would require decomposition into two-qubit CNOT gates available in hardware. A second way the authors minimize depth is by choosing a $ZZ$ coupling topology that maps directly to the architecture couplings. A further optimization they undertake is targeting a higher-performing, suitably connected hardware subcircuit. Additional things to consider are minimizing the number of feature map repetitions and choosing a customized low-depth or “linear” entangling scheme instead of the “full” scheme that entangles all qubits.\n", - "\n", - "![Data encoding image](/learning/images/courses/quantum-machine-learning/data-encoding/qml-03-data-encoding-24.avif)\n", - "\n", - "The above graphic shows a network of nodes and edges that represent physical qubits and hardware couplings, respectively. The coupling map and performance of ibm_torino is shown with all possible two-qubit CZ coupling gates. Qubits are color-coded on a scale based on the T1 relaxation time in microseconds (μs), where longer T1 times are better and in a lighter shade. The coupling edges are color-coded by CZ error, where darker shades are better. Information on the hardware specification can be accessed in the hardware backend configuration schema ```IBMQBackend.configuration()```." - ] - }, - { - "cell_type": "markdown", - "id": "98db3e11-2e00-406c-a4ee-88dc3ee82b96", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "1. Maria Schuld and Francesco Petruccione, *Supervised Learning with Quantum Computers*, Springer 2018, [doi:10.1007/978-3-319-96424-9](https://www.springer.com/gp/book/9783319964232).\n", - "2. Vojtech Havlicek et al., “Supervised Learning with Quantum Enhanced Feature Spaces.” *Nature*, vol. 567 (2019): 209–212. https://arxiv.org/abs/1804.11326.\n", - "3. Ryan LaRose and Brian Coyle, \"Robust data encodings for quantum classifiers\", Physical Review A 102, 032420 (2020), [doi:10.1103/PhysRevA.102.032420](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.102.032420), [arXiv:2003.01695](https://arxiv.org/abs/2003.01695).\n", - "4. Lou Grover and Terry Rudolph. “Creating Superpositions That Correspond to Efficiently Integrable Probability Distributions.” arXiv:quant-ph/0208112, August 15, 2002, https://arxiv.org/abs/quant-ph/0208112.\n", - "5. Adrián Pérez-Salinas, Alba Cervera-Lierta, Elies Gil-Fuster, José I. Latorre, \"Data re-uploading for a universal quantum classifier\", [Quantum 4, 226 (2020)](https://quantum-journal.org/papers/q-2020-02-06-226/), [ArXiv.org/abs/1907.02085](https://arxiv.org/abs/1907.02085).\n", - "6. Maria Schuld, Ryan Sweke, Johannes Jakob Meyer, \"The effect of data encoding on the expressive power of variational quantum machine learning models\", [Phys. Rev. A 103, 032430 (2021)](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.103.032430), [arxiv.org/abs/2008.08605](https://arxiv.org/abs/2008.08605)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "571dc6bf-7c16-4623-be61-64a5b215bd69", - "metadata": {}, - "outputs": [], - "source": [ - "import qiskit\n", - "\n", - "qiskit.version.get_version_info()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0210ee7c-f989-4eb0-9231-ae5deacb6091", + "metadata": {}, + "source": [ + "---\n", + "title: Data encoding\n", + "description: An overview of data encoding methods in quantum machine learning. Encoding schemes covered include basis, amplitude, angle, and dense encoding.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore pfmap backgroundcolor linesize, imshow, minpos, nonumber, zzcircuit, zcircuit, pcircuit, zzcx, Schuld, Francesco, Petruccione, Vojtech, Adrian, Perez, Cervera, Lierta, Elies, Fuster, Jose, Latorre, Sweke, Jakob, Adrián, Pérez-Salinas, Alba, Cervera-Lierta, Elies, Gil-Fuster, José, QSVMs, VQCs, pnts, ZZFM */}\n", + "\n", + "# Data encoding\n", + "\n", + "## Introduction and notation\n", + "\n", + "To use a quantum algorithm, classical data must somehow be brought into a quantum circuit. This is usually referred to as data *encoding*, but is also called data *loading*. Recall from previous lessons the notion of a feature mapping, a mapping of data features from one space to another. Just transferring classical data to a quantum computer is a sort of mapping, and could be called a feature mapping. In practice, the built-in feature mappings in Qiskit (like `z_Feature Map and ZZ Feature Map) will typically include rotation layers and entangling layers that extend the state to many dimensions in the Hilbert space. This encoding process is a critical part of quantum machine learning algorithms and directly affects their computational capabilities.\n", + "\n", + "Some of the encoding techniques below can be efficiently classically simulated; this is particularly easy to see in encoding methods that yield product states (that is, they do not entangle qubits). And remember that quantum utility is most likely to lie where the quantum-like complexity of the dataset is well-matched by the encoding method. So it is very likely that you will end up writing your own encoding circuits. Here, we show a wide variety of possible encoding strategies simply so that you can compare and contrast them, and see what is possible. There are some very general statements that can be made about the usefulness of encoding techniques. For example, `efficient_su2` (see below) with a full entangling scheme is much more likely to capture quantum features of data than methods that yield product states (like `z_feature_map`). But this does not mean `efficient_su2` is sufficient, or sufficiently well-matched to your dataset, to yield a quantum speed-up. That requires careful consideration of the structure of the data being modeled or classified. There is also a balancing act with circuit depth, since many feature maps which fully entangle the qubits in a circuit yield very deep circuits, too deep to get usable results on today's quantum computers.\n", + "\n", + "### Notation\n", + "\n", + "A dataset is a set of $M$ data vectors: $\\text{X} = \\{\\vec{x}^{(j)}\\,|\\,j\\in [M]\\}$, where each vector is $N$ dimensional, that is, $\\vec{x}^{(j)}=(\\vec{x}^{(j)}_1,\\ldots,\\vec{x}^{(j)}_N)\\in\\mathbb{R}^N$. This could be extended to complex data features. In this lesson, we may occasionally use these notations for the full set $(\\text{X}),$ and its specific elements like $\\vec{x}^{(j)}$. But we will mostly refer to the loading of a single vector from our dataset at a time, and will often simply refer to a single vector of $N$ features as $\\vec{x}$.\n", + "\n", + "Additionally, it is common to use the symbol $\\Phi(\\vec{x})$ to refer to the feature mapping $\\Phi$ of data vector $\\vec{x}$. In quantum computing specifically, it is common to refer to mappings in quantum computing using $U(\\vec{x}),$ a notation that reinforces the unitary nature of these operations. One could correctly use the same symbol for both; both are feature mappings. Throughout this course, we tend to use:\n", + "* $\\Phi(\\vec{x})$ when discussing feature mappings in machine learning, generally, and\n", + "* $U(\\vec{x})$ when discussing circuit implementations of feature mappings.\n", + "\n", + "### Normalization and information loss\n", + "\n", + "In classical machine learning, training data features are often \"normalized\" or rescaled which often improves model performance. One common way of doing this is by using min-max normalization or standardization. In min-max normalization, feature columns of the data matrix $\\text{X}$ (say, feature $k$) are normalized:\n", + "\n", + "$$\n", + "x^{'(i)}_k = \\frac{x^{(i)}_k - \\text{min}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}}{\\text{max}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}-\\text{min}\\{x^{(j)}_k\\,|\\,\\vec{x}^{(j)}\\in [\\text{X}]\\}}\n", + "$$\n", + "\n", + "where min and max refer to the minimum and maximum of feature $k$ over the $M$ data vectors in the dataset $\\text{X}$. All the feature values then fall in the unit interval: $x^{'(i)}_k \\in [0,1]$ for all $i\\in [M]$, $k\\in[N]$.\n", + "\n", + "Normalization is also a fundamental concept in quantum mechanics and quantum computing, but it is slightly different from min-max normalization. Normalization in quantum mechanics requires that the length (in the context of quantum computing, the 2-norm) of a state vector $|\\psi\\rangle$ is equal to unity: $\\|\\psi\\|=\\sqrt{\\langle\\psi|\\psi\\rangle} = 1$, ensuring that measurement probabilities sum to 1. The state is normalized by dividing by the 2-norm; that is, by rescaling\n", + "$$\n", + "|\\psi\\rangle\\rightarrow\\|\\psi\\|^{-1}|\\psi\\rangle\n", + "$$\n", + "In quantum computing and quantum mechanics, this is not a normalization imposed by people on the data, but a fundamental property of quantum states. Depending on your encoding scheme, this constraint may affect how your data are rescaled. For example, in amplitude encoding (see below), the data vector is normalized $\\vert\\vec{x}^{(j)}\\vert = 1$ as is required by quantum mechanics, and this affects the scaling of the data being encoded. In phase encoding, feature values are recommended to be rescaled as $\\vec{x}^{(j)}_i \\in (0,2\\pi]$ so that there is no information loss due to the modulo-$2\\pi$ effect of encoding to a qubit phase angle[\\[1,2\\]](#references)." + ] + }, + { + "cell_type": "markdown", + "id": "930db404-47dc-4dfd-8ca0-cff02fb7cb90", + "metadata": { + "formulas": { + "_ket-dataset": { + "meaning": "This is a quantum statevector that represents our dataset, 𝒳.", + "say": "Ket script X" + }, + "_ket-x": { + "meaning": "This vertical bar and angled bracket mean we're referring to a ket (column vector) with label 'x'.", + "say": "Ket x" + }, + "_m": { + "meaning": "This is the number of entries in our dataset." + }, + "_sum-m": { + "meaning": "This notation means we add together |xm〉 (the mth element of our dataset) for all values of m between 1 and M (that is, the entire dataset).", + "say": "Sum of all computational basis states in our dataset (𝒳)" + }, + "_x-lil-m": { + "meaning": "This is the mth element of the dataset. It is an N-dimensional vector. Here, the 'm' is just used to mean \"any number between 1 and M\"", + "say": "X little-M" + }, + "brace": { + "meaning": "These curly brackets (braces) mean everything inside the brackets forms a set.", + "say": "Brace (or \"curly bracket\")", + "type": "Universal notation" + }, + "ellipsis": { + "meaning": "These dots omit things where the pattern can be implied.", + "say": "Ellipsis", + "type": "Universal notation" + }, + "in": { + "meaning": "This symbol means the things to the left of the symbol are contained in the set to the right of the symbol.", + "say": "In", + "type": "Universal notation" + }, + "script-x": { + "meaning": "This is a symbol we’ve chosen to represent our dataset.", + "say": "Script X", + "type": "Locally defined variable" + } + } + }, + "source": [ + "## Methods of encoding\n", + "\n", + "In the next few sections, we will refer to a small example classical dataset $\\text{X}_\\text{ex}$ consisting of $M=5$ data vectors, each with $N=3$ features:\n", + "\n", + "$$\n", + "\\text{X}_{\\text{ex}}=\\{(4,8,5),(9,8,6),(2,9,2),(5,7,0),(3,7,5)\\}\n", + "$$\n", + "\n", + "In the notation introduced above, we might say the $1^\\text{st}$ feature of the $4^\\text{th}$ data vector in our set $\\text{X}_{\\text{ex}}$ is $\\vec{x}^{(4)}_1 = 5,$ for example." + ] + }, + { + "cell_type": "markdown", + "id": "288c73ec-bdfe-4879-b918-0aed7f0c3c5c", + "metadata": {}, + "source": [ + "### Basis encoding\n", + "\n", + "Basis encoding encodes a classical $P$-bit string into a computational basis state of a $P$-qubit system. Take for example $\\vec{x}^{(1)}_3 = 5 = 0(2^3)+1(2^2)+0(2^1)+1(2^0).$ This can be represented as a $4$-bit string as $(0101)$, and by a $4$-qubit system as the quantum state $|0101\\rangle$. More generally, for a $P$-bit string: $\\vec{x}^{(j)}_k = (b_1, b_2, ... , b_P)$, the corresponding $P$-qubit state is $|x^{(j)}_k\\rangle = | b_1, b_2, ... , b_P \\rangle$ with $b_n \\in \\{0,1\\}$ for $n = 1 , \\dots , P$. Note that this is just for a single feature.\n", + "\n", + "Basis encoding in quantum computing represents each classical bit as a separate qubit, mapping the binary representation of data directly onto quantum states in the computational basis. When multiple features need to be encoded, each feature is first converted to its binary form and then assigned to a distinct group of qubits — one group per feature — where each qubit reflects a bit in the binary representation of that feature.\n", + "\n", + "As an example, let us encode the vector (5, 7, 0).\n", + "\n", + "Suppose all features are stored in four bits (more than we need, but enough to represent any integer that is single-digit in base 10):\n", + "\n", + " 5 → binary 0101\n", + "\n", + " 7 → binary 0111\n", + "\n", + " 0 → binary 0000\n", + "\n", + "These bit strings are assigned to three sets of four qubits, so the overall 12-qubit basis state is:\n", + "$$\n", + "∣0101 0111 0000⟩\n", + "$$\n", + "\n", + "Here, the first four qubits represent the first feature, the next four qubits the second feature, and the last four qubits the third feature. The code below converts the data vector (5,7,0) to a quantum state, and is generalized to do so for other single-digit features." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "85ee995f-1e50-4860-a24c-16bbc8b5c8b0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "# Data point to encode\n", + "x = 5 # binary: 0101\n", + "y = 7 # binary: 0111\n", + "z = 0 # binary: 0000\n", + "\n", + "# Convert each to 4-bit binary list\n", + "x_bits = [int(b) for b in format(x, \"04b\")] # [0,1,0,1]\n", + "y_bits = [int(b) for b in format(y, \"04b\")] # [0,1,1,1]\n", + "z_bits = [int(b) for b in format(z, \"04b\")] # [0,0,0,0]\n", + "\n", + "# Combine all bits\n", + "all_bits = x_bits + y_bits + z_bits # [0,1,0,1,0,1,1,1,0,0,0,0]\n", + "\n", + "# Initialize a 12-qubit quantum circuit\n", + "qc = QuantumCircuit(12)\n", + "\n", + "# Apply x-gates where the bit is 1\n", + "for idx, bit in enumerate(all_bits):\n", + " if bit == 1:\n", + " qc.x(idx)\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "887174c0-6a8b-438d-84cc-80451352d9e9", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "#### Check your understanding\n", + "\n", + "Write code to encode the first vector in our example data set $\\text{X}_{\\text{ex}}$:\n", + "\n", + "$$\\vec{x}^{(1)}=(4,8,5)$$\n", + "\n", + "using basis encoding.\n", + "\n", + "\n", + "\n", + "\n", + "```python\n", + "import math\n", + "from qiskit import QuantumCircuit\n", + "\n", + "# Data point to encode\n", + "x = 4 # binary: 0100\n", + "y = 8 # binary: 1000\n", + "z = 5 # binary: 0101\n", + "\n", + "# Convert each to 4-bit binary list\n", + "x_bits = [int(b) for b in format(x, '04b')] # [0,1,0,0]\n", + "y_bits = [int(b) for b in format(y, '04b')] # [1,0,0,0]\n", + "z_bits = [int(b) for b in format(z, '04b')] # [0,1,0,1]\n", + "\n", + "# Combine all bits\n", + "all_bits = x_bits + y_bits + z_bits # [0,1,0,0,1,0,0,0,0,1,0,1]\n", + "\n", + "# Initialize a 12-qubit quantum circuit\n", + "qc = QuantumCircuit(12)\n", + "\n", + "# Apply x-gates where the bit is 1\n", + "for idx, bit in enumerate(all_bits):\n", + " if bit == 1:\n", + " qc.x(idx)\n", + "\n", + "qc.draw('mpl')\n", + "```\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "c47289a9-64f0-4541-b25c-41b2a51f86ad", + "metadata": { + "formulas": { + "_a-norm": { + "meaning": "This is a normalization constant. We can calculate it from the inverse of the Euclidean (l2) norm of the datapoint vector.", + "say": "A norm" + } + } + }, + "source": [ + "### Amplitude encoding\n", + "\n", + "Amplitude encoding encodes data into the amplitudes of a quantum state. It represents a normalized classical $N$-dimensional data vector, $\\vec{x}^{(j)}$, as the amplitudes of a $n$-qubit quantum state, $|\\psi_x\\rangle$:\n", + "\n", + "$$\n", + "|\\psi^{(j)}_x\\rangle = \\frac{1}{\\alpha}\\sum_{i=1}^N x^{(j)}_i |i\\rangle\n", + "$$\n", + "\n", + "where $N$ is the same dimension of the data vectors as before, $\\vec{x}^{(j)}_i$ is the $i^{th}$ element of $\\vec{x}^{(j)}$ and $|i\\rangle$ is the $i^{th}$ computational basis state. Here, $\\alpha$ is a normalization constant to be determined from the data being encoded. This is the normalization condition imposed by quantum mechanics:\n", + "\n", + "$$\n", + "\\sum_{i=1}^N \\left|x^{(j)}_i\\right|^2 = \\left|\\alpha\\right|^2.\n", + "$$\n", + "\n", + "In general, this is a different condition than the min/max normalization used for each feature across all data vectors. Precisely how this is navigated will depend on your problem. But there is no way around the quantum mechanical normalization condition above.\n", + "\n", + "In amplitude encoding, each feature in a data vector is stored as an amplitude of a different quantum state. As a system of $n$ qubits provides $2^n$ amplitudes, amplitude encoding of $N$ features requires $n \\ge \\mathrm{log}_2(N)$ qubits.\n", + "\n", + "As an example, let's encode the first vector in our example dataset $\\text{X}_\\text{ex}$, $\\vec{x}^{(1)} = (4,8,5)$ using amplitude encoding. Normalizing the resulting vector, we get:\n", + "\n", + "$$\n", + "\\sum_{i=1}^N \\left|x^{(1)}_i\\right|^2 = 4^2+8^2+5^2 = 105 = \\left|\\alpha\\right|^2 \\rightarrow \\alpha = \\sqrt{105}\n", + "$$\n", + "\n", + "and the resulting 2-qubit quantum state would be:\n", + "\n", + "$$\n", + "|\\psi(\\vec{x}^{(1)})\\rangle = \\frac{1}{\\sqrt{105}}(4|00\\rangle+8|01\\rangle+5|10\\rangle+0|11\\rangle)\n", + "$$\n", + "\n", + "In the example above, the number of features in the vector $N=3$, is not a power of 2. When $N$ is not a power of 2, we simply choose a value for the number of qubits $n$ such that $2^n\\geq N$ and pad the amplitude vector with uninformative constants (here, a zero).\n", + "\n", + "Like in basis encoding, once we calculate what state will encode our dataset, in Qiskit we can use the `initialize` function to prepare it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19810c6d-8d60-49ee-bd6f-6f6fbd5e7363", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import math\n", + "\n", + "desired_state = [\n", + " 1 / math.sqrt(105) * 4,\n", + " 1 / math.sqrt(105) * 8,\n", + " 1 / math.sqrt(105) * 5,\n", + " 1 / math.sqrt(105) * 0,\n", + "]\n", + "\n", + "qc = QuantumCircuit(2)\n", + "qc.initialize(desired_state, [0, 1])\n", + "\n", + "qc.decompose(reps=5).draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "58509d89-68ba-4fd5-92b9-278c47497eb9", + "metadata": {}, + "source": [ + "An advantage of amplitude encoding is the aforementioned requirement of only $\\mathrm{log}_2(N)$ qubits to encode. However, subsequent algorithms must operate on the amplitudes of a quantum state, and methods to prepare and measure the quantum states tend not to be efficient." + ] + }, + { + "cell_type": "markdown", + "id": "639dd02e-28ad-4d82-8091-ddc00d066666", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "Write down the normalized state for encoding the following vector (made of two vectors from our example dataset):\n", + "\n", + "$$\\vec{x}=(9,8,6,2,9,2)$$\n", + "\n", + "using amplitude encoding.\n", + "\n", + "\n", + "\n", + "To encode 6 numbers, we will need to have at least 6 available states on whose amplitudes we can encode. This will require 3 qubits. Using an unknown normalization factor $\\alpha$, we can write this as:\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\alpha(9|000\\rangle+8|001\\rangle+6|010\\rangle+2|011\\rangle+9|100\\rangle+2|101\\rangle+0|110\\rangle+0|111\\rangle)\n", + "$$\n", + "Note that\n", + "$$\n", + "\\langle \\psi|\\psi\\rangle = |\\alpha|^2\\times(9^2+8^2+6^2+2^2+9^2+2^2+0^2+0^2) = |\\alpha|^2\\times(270)=1 \\rightarrow \\alpha = \\frac{1}{\\sqrt{270}}\n", + "$$\n", + "So finally,\n", + "$$\n", + "|\\psi\\rangle = \\frac{1}{\\sqrt{270}}(9|000\\rangle+8|001\\rangle+6|010\\rangle+2|011\\rangle+9|100\\rangle+2|101\\rangle+0|110\\rangle+0|111\\rangle)\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "For the same data vector $\\vec{x}=(9,8,6,2,9,2),$ write code to create a circuit that loads these data features using amplitude encoding.\n", + "\n", + "\n", + "\n", + "\n", + "```python\n", + "desired_state = [\n", + " 9 / math.sqrt(270),\n", + " 8 / math.sqrt(270),\n", + " 6 / math.sqrt(270),\n", + " 2 / math.sqrt(270),\n", + " 9 / math.sqrt(270),\n", + " 2 / math.sqrt(270),\n", + " 0,\n", + " 0,\n", + "]\n", + "\n", + "print(desired_state)\n", + "\n", + "qc = QuantumCircuit(3)\n", + "qc.initialize(desired_state, [0, 1, 2])\n", + "qc.decompose(reps=8).draw(output=\"mpl\")\n", + "```\n", + "\n", + "[0.5477225575051662, 0.48686449556014766, 0.36514837167011077, 0.12171612389003691, 0.5477225575051662, 0.12171612389003691, 0, 0]\n", + "\n", + "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin2.avif)\n", + "\n", + "\n", + "\n", + "\n", + "You may need to deal with very large data vectors. Consider the vector\n", + "\n", + "$$\\vec{x}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5).$$\n", + "\n", + "Write code to automate the normalization, and generate a quantum circuit for amplitude encoding.\n", + "\n", + "\n", + "\n", + "\n", + "There are many possible answers. Here is code that prints a few steps along the way:\n", + "\n", + "```python\n", + "import numpy as np\n", + "from math import sqrt\n", + "\n", + "init_list = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5]\n", + "qubits = round(np.log(len(init_list)) / np.log(2) + 0.4999999999)\n", + "need_length = 2**qubits\n", + "pad = need_length - len(init_list)\n", + "for i in range(0, pad):\n", + " init_list.append(0)\n", + "\n", + "init_array = np.array(init_list) # Unnormalized data vector\n", + "length = sqrt(\n", + " sum(init_array[i] ** 2 for i in range(0, len(init_array)))\n", + ") # Vector length\n", + "norm_array = init_array / length # Normalized array\n", + "print(\"Normalized array:\")\n", + "print(norm_array)\n", + "print()\n", + "\n", + "qubit_numbers = []\n", + "for i in range(0, qubits):\n", + " qubit_numbers.append(i)\n", + "print(qubit_numbers)\n", + "\n", + "qc = QuantumCircuit(qubits)\n", + "qc.initialize(norm_array, qubit_numbers)\n", + "qc.decompose(reps=7).draw(output=\"mpl\")\n", + "```\n", + "\n", + "Normalized array:\n", + "[0.17342199 0.34684399 0.21677749 0.39019949 0.34684399 0.26013299\n", + " 0.086711 0.39019949 0.086711 0.21677749 0.30348849 0.\n", + " 0.1300665 0.30348849 0.21677749 0. ]\n", + "\n", + "[0, 1, 2, 3]\n", + "\n", + "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin3.avif)\n", + "\n", + "\n", + "\n", + "\n", + "Do you see advantages to amplitude encoding over basis encoding? If so, explain.\n", + "\n", + "\n", + "\n", + "\n", + "There may be several answers. One answer is that, given the fixed ordering of the basis states, this amplitude encoding preserves the order of the numbers encoded. It will often also be encoded more densely.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "cbedf6cc-33a8-4be7-90ec-81af4d224871", + "metadata": {}, + "source": [ + "A benefit of amplitude encoding is that only $\\log_2(N)$ qubits are required for an $N$-dimensional ($N$-feature) data vector $\\vec{x}\\rightarrow|\\vec{x}\\rangle$. However, amplitude encoding is generally an inefficient procedure that requires arbitrary state preparation, which is exponential in the number of CNOT gates. Stated differently, the state preparation has a polynomial runtime complexity of $\\mathcal O(N)$ in the number of dimensions, where $N = 2^n$, and $n$ is the number of qubits. Amplitude encoding “provides an exponential saving in space at the cost of an exponential increase in time”[\\[3\\]](#references); however, runtime increases to $\\mathcal O(\\log N)$ are achievable in certain cases[\\[4\\]](#references). For an end-to-end quantum speedup, the data loading runtime complexity needs to be considered." + ] + }, + { + "cell_type": "markdown", + "id": "4ef15d5f-4730-4ea8-95fd-75aff06487ff", + "metadata": { + "formulas": { + "_big-o-times-n": { + "meaning": "This represents the tensor product operation over N qubits.", + "say": "big o-times" + }, + "_big-o-times-n2": { + "meaning": "This represents the tensor product operation over N/2 qubits.", + "say": "big o-times" + } + } + }, + "source": [ + "### Angle encoding\n", + "\n", + "Angle encoding is of interest in many QML models using Pauli feature maps such as quantum support vector machines (QSVMs) and variational quantum circuits (VQCs), among others. Angle encoding is closely related to phase encoding and dense angle encoding which are presented below. Here we will use \"angle encoding\" to refer to a rotation in $\\theta$, that is, a rotation away from the $z$ axis accomplished for example by an $R_X$ gate or an $R_Y$ gate[\\[1,3\\]](#references). Really, one can encode data in *any* rotation or combination of rotations. But $R_Y$ is common in the literature, so we emphasize it here.\n", + "\n", + "When applied to a single qubit, angle encoding imparts a Y-axis rotation proportional to the data value. Consider the encoding of a single ($k^\\text{th}$)feature from the $j^\\text{th}$ data vector in a dataset, $\\vec{x}^{(j)}_k$:\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}_k\\rangle = R_Y(\\theta=\\vec{x}^{(j)}_k)|0\\rangle = \\textstyle\\cos\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|0\\rangle + \\sin\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|1\\rangle.\n", + "$$\n", + "\n", + "Alternatively, angle encoding can be performed using $R_X(\\theta)$ gates, although the encoded state would have a complex relative phase compared to $R_Y(\\theta)$.\n", + "\n", + "Angle encoding is different from the previous two methods discussed in several ways. In angle encoding:\n", + "- Each feature value is mapped to a corresponding qubit, $\\vec{x}^{(j)}_k \\rightarrow Q_k$, leaving the qubits in a product state.\n", + "- One numerical value is encoded at a time, rather than a whole set of features from a data point.\n", + "- $n$ qubits are required for $N$ data features, where $n\\leq N$. Often equality holds, here. We'll see how $n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from qiskit.visualization.bloch import Bloch\n", + "from qiskit.visualization.state_visualization import _bloch_multivector_data\n", + "\n", + "\n", + "def plot_Nstates(states, axis, plot_trace_points=True):\n", + " \"\"\"This function plots N states to 1 Bloch sphere\"\"\"\n", + " bloch_vecs = [_bloch_multivector_data(s)[0] for s in states]\n", + "\n", + " if axis is None:\n", + " bloch_plot = Bloch()\n", + " else:\n", + " bloch_plot = Bloch(axes=axis)\n", + "\n", + " bloch_plot.add_vectors(bloch_vecs)\n", + "\n", + " if len(states) > 1:\n", + "\n", + " def rgba_map(x, num):\n", + " g = (0.95 - 0.05) / (num - 1)\n", + " i = 0.95 - g * num\n", + " y = g * x + i\n", + " return (0.0, y, 0.0, 0.7)\n", + "\n", + " num = len(states)\n", + " bloch_plot.vector_color = [rgba_map(x, num) for x in range(1, num + 1)]\n", + "\n", + " bloch_plot.vector_width = 3\n", + " bloch_plot.vector_style = \"simple\"\n", + "\n", + " if plot_trace_points:\n", + "\n", + " def trace_points(bloch_vec1, bloch_vec2):\n", + " # bloch_vec = (x,y,z)\n", + " n_points = 15\n", + " thetas = np.arccos([bloch_vec1[2], bloch_vec2[2]])\n", + " phis = np.arctan2(\n", + " [bloch_vec1[1], bloch_vec2[1]], [bloch_vec1[0], bloch_vec2[0]]\n", + " )\n", + " if phis[1] < 0:\n", + " phis[1] = phis[1] + 2 * pi\n", + " angles0 = np.linspace(phis[0], phis[1], n_points)\n", + " angles1 = np.linspace(thetas[0], thetas[1], n_points)\n", + "\n", + " xp = np.cos(angles0) * np.sin(angles1)\n", + " yp = np.sin(angles0) * np.sin(angles1)\n", + " zp = np.cos(angles1)\n", + " pnts = [xp, yp, zp]\n", + " bloch_plot.add_points(pnts)\n", + " bloch_plot.point_color = \"k\"\n", + " bloch_plot.point_size = [4] * len(bloch_plot.points)\n", + " bloch_plot.point_marker = [\"o\"]\n", + "\n", + " for i in range(len(bloch_vecs) - 1):\n", + " trace_points(bloch_vecs[i], bloch_vecs[i + 1])\n", + "\n", + " bloch_plot.sphere_alpha = 0.05\n", + " bloch_plot.frame_alpha = 0.15\n", + " bloch_plot.figsize = [4, 4]\n", + "\n", + " bloch_plot.render()\n", + "\n", + "\n", + "plot_Nstates(states, axis=None, plot_trace_points=True)" + ] + }, + { + "cell_type": "markdown", + "id": "6ba88e2e-4774-4a78-9971-b8b1df927229", + "metadata": {}, + "source": [ + "That was just a single feature of a single data vector. When encoding $N$ features into the rotation angles of $n$ qubits, say for the $j^\\text{th}$ data vector $\\vec{x}^{(j)} = (x_1,...,x_N),$ the encoded product state will look like this:\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}\\rangle = \\bigotimes^N_{k=1} \\cos(\\vec{x}^{(j)}_k)|0\\rangle + \\sin(\\vec{x}^{(j)}_k)|1\\rangle\n", + "$$\n", + "\n", + "We note that this is equivalent to\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}\\rangle = \\bigotimes^N_{k=1} R_Y(2\\vec{x}^{(j)}_k)|0\\rangle.\n", + "$$\n", + "\n", + "#### Check your understanding\n", + "\n", + "Encode the data vector $\\vec{x} = (0, \\pi/4, \\pi/2)$ using angle encoding, as described above.\n", + "\n", + "\n", + "\n", + "\n", + "```python\n", + "qc = QuantumCircuit(3)\n", + "qc.ry(0, 0)\n", + "qc.ry(2 * math.pi / 4, 1)\n", + "qc.ry(2 * math.pi / 2, 2)\n", + "qc.draw(output=\"mpl\")\n", + "```\n", + "\n", + "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin4.avif)\n", + "\n", + "\n", + "\n", + "\n", + "Using angle encoding as described above, how many qubits are required to encode 5 features?\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "64cd9b41-00f5-4212-b52e-939ca96f84c7", + "metadata": {}, + "source": [ + "### Phase encoding\n", + "\n", + "Phase encoding is very similar to the angle encoding described above. The phase angle of a qubit is a real-valued angle $\\phi$ about the $z$-axis from the +$x$-axis. Data are mapped with a phase rotation, $P(\\phi) = e^{i\\phi/2}R_Z(\\phi)$, where $\\phi \\in (0,2\\pi]$ (see [Qiskit PhaseGate](/docs/api/qiskit/qiskit.circuit.library.PhaseGate) for more information). It is recommended to rescale data so that $\\vec{x}^{(j)}_k \\in (0,2\\pi]$. This prevents information loss and other potentially unwanted effects[\\[1,2\\]](#references).\n", + "\n", + "A qubit is often initialized in the state $|0\\rangle$, which is an eigenstate of the phase rotation operator, meaning that the qubit state first needs to be rotated for phase encoding to be implemented. It therefore makes sense to initialize the state with a Hadamard gate: $H|0\\rangle = |+\\rangle = \\textstyle\\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$. Phase encoding on a single qubit means imparting a relative phase proportional to the data value:\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}_k\\rangle = P(\\phi=\\vec{x}^{(j)}_k)|+\\rangle = \\textstyle\\frac{1}{\\sqrt{2}}\\big(|0\\rangle + e^{i\\vec{x}^{(j)}_k}|1\\rangle\\big).\n", + "$$\n", + "\n", + "The phase encoding procedure maps each feature value to the phase of a corresponding qubit, $\\vec{x}^{(j)}_k \\rightarrow Q_k$. In total, phase encoding has a circuit depth of 2, including the Hadamard layer, which makes it an efficient encoding scheme. The phase-encoded multi-qubit state ($n$ qubits for $N=n$ features) is a product state:\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}\\rangle = \\bigotimes_{k=1}^{N} P_k(\\phi = \\vec{x}^{(j)}_k)|+\\rangle^{\\otimes N} = {\\textstyle\\frac{1}{\\sqrt{2^N}}} \\bigotimes_{k=1}^{N}\\big(|0\\rangle + e^{i\\vec{x}^{(j)}_k}|1\\rangle\\big).\n", + "$$\n", + "\n", + "The following Qiskit code first prepares the initial state of a single qubit by rotating it with a Hadamard gate, then rotates it again using a phase gate to encode a data feature $\\vec{x}^{(j)}_k=\\frac{1}{2}\\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "ba0886eb-1c56-4b15-a731-d94d805254e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "qc.h(0) # Hadamard gate rotates state down to Bloch equator\n", + "state1 = Statevector.from_instruction(qc)\n", + "\n", + "qc.p(pi / 2, 0) # Phase gate rotates by an angle pi/2\n", + "state2 = Statevector.from_instruction(qc)\n", + "\n", + "states = state1, state2\n", + "\n", + "qc.draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "02ee25db-7368-40d0-9ba8-52e231ede3e0", + "metadata": {}, + "source": [ + "We can visualize the rotation in $\\phi$ using the plot_Nstates function we defined." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "f7c9cf29-2ad6-43af-a7e3-e590e41d7e67", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_Nstates(states, axis=None, plot_trace_points=True)" + ] + }, + { + "cell_type": "markdown", + "id": "803fcc6b-a6c2-463d-8e46-e83074faf025", + "metadata": {}, + "source": [ + "The Bloch sphere plot shows the Z-axis rotation $|+\\rangle \\rightarrow P(\\frac{1}{2}\\pi)|+\\rangle$ where $\\vec{x}^{(j)}_k=\\frac{1}{2}\\pi$. The light green arrow shows the final state.\n", + "\n", + "Phase encoding is used in many quantum feature maps, particularly $Z$ and $ZZ$ feature maps, and general Pauli feature maps, among others." + ] + }, + { + "cell_type": "markdown", + "id": "f9f2786b-9fff-4425-8387-0a60ce690689", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "How many qubits are required in order to use phase encoding as described above to store 8 features?\n", + "\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "\n", + "Write code to the vector $$\\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0)$$ using phase encoding.\n", + "\n", + "\n", + "\n", + "\n", + "There may be many answers. Here is one example:\n", + "\n", + "```python\n", + "phase_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0]\n", + "qc = QuantumCircuit(len(phase_data))\n", + "for i in range(0, len(phase_data)):\n", + " qc.h(i)\n", + " qc.rz(phase_data[i] * 2 * math.pi / float(max(phase_data)), i)\n", + "qc.draw(output=\"mpl\")\n", + "```\n", + "\n", + "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin5.avif)\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "910625ad-d798-4a78-835c-2b92a046dbb1", + "metadata": {}, + "source": [ + "### Dense angle encoding\n", + "\n", + "Dense angle encoding (DAE) is a combination of angle encoding and phase encoding. DAE allows two feature values to be encoded in a single qubit: one angle with a Y-axis rotation angle, and the other with a $z$-axis rotation angle: $\\vec{x}^{(j)}_k,$ $\\vec{x}^{(j)}_\\ell \\rightarrow \\theta, \\phi$. It encodes two features as follows:\n", + "\n", + "$$\n", + "|\\vec{x}^{(j)}_k,\\vec{x}^{(j)}_\\ell\\rangle = R_Z(\\phi=\\vec{x}^{(j)}_\\ell) R_Y(\\theta=\\vec{x}^{(j)}_k)|0\\rangle = \\cos\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|0\\rangle + e^{i\\vec{x}^{(j)}_\\ell} \\sin\\left(\\frac{\\vec{x}^{(j)}_k}{2}\\right)|1\\rangle.\n", + "$$\n", + "\n", + "Encoding two data features to one qubit results in a $2\\times$ reduction in the number of qubits required for the encoding. Extending this to more features, the data vector $\\vec{x} = (x_1,...,x_N)$ can be encoded as:\n", + "\n", + "$$\n", + "|\\vec{x}\\rangle = \\bigotimes_{k=1}^{N/2} \\cos(x_{2k-1})|0\\rangle + e^{i x_{2k}}\\sin(x_{2k-1})|1\\rangle\n", + "$$\n", + "\n", + "DAE can be generalized to arbitrary functions of the two features instead of the sinusoidal functions used here. This is called general qubit encoding[\\[7\\]](#references).\n", + "\n", + "As an example of DAE, the code below encodes and visualizes the encoding of the features $x_1=\\theta = 3\\pi/8$ and $x_2=\\phi = 7\\pi/4$." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "9a6bb041-d7a1-4e29-a463-81b93b900e96", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "state1 = Statevector.from_instruction(qc)\n", + "qc.ry(3 * pi / 8, 0)\n", + "state2 = Statevector.from_instruction(qc)\n", + "qc.rz(7 * pi / 4, 0)\n", + "state3 = Statevector.from_instruction(qc)\n", + "states = state1, state2, state3\n", + "\n", + "plot_Nstates(states, axis=None, plot_trace_points=True)" + ] + }, + { + "cell_type": "markdown", + "id": "cdc69e45-3651-453b-9e04-5e6f6c82f8b1", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "Given the treatment above, how many qubits are needed to encode 6 features using dense encoding?\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "\n", + "Write code to load the vector $$\\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5)$$ using dense angle encoding.\n", + "\n", + "\n", + "\n", + "\n", + "Note that we have padded the list with a \"0\" to avoid the problem of there being a single unused parameter in our encoding scheme.\n", + "\n", + "```python\n", + "dense_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5, 0]\n", + "qc = QuantumCircuit(int(len(dense_data) / 2))\n", + "entry = 0\n", + "for i in range(0, int(len(dense_data) / 2)):\n", + " qc.ry(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)\n", + " entry = entry + 1\n", + " qc.rz(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)\n", + " entry = entry + 1\n", + "qc.draw(output=\"mpl\")\n", + "```\n", + "\n", + "![\"Output of the previous code cell\"](/learning/images/courses/quantum-machine-learning/data-encoding/checkin6.avif)\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "1035f985-3ab9-476d-8173-c4b0a764d46f", + "metadata": {}, + "source": [ + "## Encoding with built-in feature maps\n", + "\n", + "### Encoding at arbitrary points\n", + "\n", + "Angle encoding, phase encoding, and dense encoding prepared product states with a feature encoded on each qubit (or two features per qubit). This is different from basis encoding and amplitude encoding, in that those methods make use of entangled states. There is not a 1:1 correspondence between data feature and qubit. In amplitude encoding, for example, you might have one feature as the amplitude of the state $|01\\rangle$ and another feature as the amplitude for $|10\\rangle$. Generally, methods that encode in product states yield shallower circuits and can store 1 or 2 features on each qubit. Methods that use entanglement and associate a feature with a state rather than a qubit result in deeper circuits, and can store more features per qubit on average.\n", + "\n", + "But encoding need not be entirely in product states or entirely in entangled states as in amplitude encoding. Indeed, many encoding schemes built into Qiskit allow encoding both before and after an entanglement layer, as opposed to just at the beginning. This is known as \"data reuploading\". For related work, see references [5] and [6].\n", + "\n", + "In this section, we will use and visualize a few of the built-in encoding schemes. All the methods in this section encode $N$ features as rotations on $N$ parameterized gates on $n$ qubits, where $n \\leq N$. Note that maximizing data loading for a given number of qubits is not the only consideration. In many cases, circuit depth may be an even more important consideration than qubit count." + ] + }, + { + "cell_type": "markdown", + "id": "0f95bc3b-bff1-469f-92ff-b2f6f523b978", + "metadata": {}, + "source": [ + "### Efficient SU2\n", + "\n", + "A common and useful example of encoding with entanglement is Qiskit's [`efficient_su2`](/docs/api/qiskit/qiskit.circuit.library.EfficientSU2) circuit. Impressively, this circuit can, for example, encode 8 features on only 2 qubits. Let's see this, and then try to understand how it is possible." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "6b657226-ae95-41f6-b78b-5def930d0080", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import efficient_su2\n", + "\n", + "circuit = efficient_su2(num_qubits=2, reps=1, insert_barriers=True)\n", + "circuit.decompose().draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "0f573f85-c2a2-449e-bcd4-74d42072e0db", + "metadata": {}, + "source": [ + "As we write our state, we will use the Qiskit convention that least-significant qubits are ordered to the far right, as in $|q_2,q_1,q_0\\rangle$ or $|q_2\\rangle\\otimes|q_1\\rangle\\otimes|q_0\\rangle.$ These states can become very complicated very quickly, and this rare example may help explain why such states are seldom written out explicitly.\n", + "\n", + "Our system starts in the state $|00\\rangle.$ Up to the first barrier (a point we label $b1$), our states are:\n", + "\n", + "$$\n", + "|\\psi\\rangle_{b1} = \\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)|0\\rangle+\\sin\\left(\\frac{\\theta_1}{2}\\right)e^{i\\theta_3}|1\\rangle\\right)\\otimes\\left(\\cos\\left(\\frac{\\theta_0}{2}\\right)|0\\rangle+\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}|1\\rangle\\right)\n", + "$$\n", + "\n", + "That's just dense encoding, which we've seen before. Now after the CNOT gate, at the second barrier ($b2$), our state is" + ] + }, + { + "cell_type": "markdown", + "id": "b3008413-246f-459f-b8b3-4f7610e93f2b", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{aligned}\n", + "|\\psi\\rangle_{b2} = & \\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_0}{2}\\right)|00\\rangle+\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}|11\\rangle\\\\\n", + "+ & \\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_3}|10\\rangle+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_0}{2}\\right)e^{i\\theta_2}e^{i\\theta_3}|01\\rangle\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "52d99dcf-2cd7-4777-a92b-765f8f8c02c2", + "metadata": {}, + "source": [ + "We now apply the last set of single-qubit rotations and collect like states to obtain:" + ] + }, + { + "cell_type": "markdown", + "id": "fae33b09-fc78-42d8-98f0-7fdd6fa0ae5c", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "|\\psi\\rangle_{\\text{final}} = &\n", + "\\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", + "\n", + "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", + "|00\\rangle\\\\\n", + "\n", + "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)-\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", + "\n", + "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(-\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", + "e^{i\\theta_6}|01\\rangle\\\\\n", + "\n", + "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", + "\n", + "- & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", + "e^{i\\theta_7}|10\\rangle\\\\\n", + "\n", + "+ & \\left[\\cos\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\sin\\left(\\frac{\\theta_4}{2}\\right)\\right.\\\\\n", + "\n", + "+ & \\left.\\sin\\left(\\frac{\\theta_0}{2}\\right)\\left(\\cos\\left(\\frac{\\theta_1}{2}\\right)\\cos\\left(\\frac{\\theta_5}{2}\\right)+\\sin\\left(\\frac{\\theta_1}{2}\\right)\\sin\\left(\\frac{\\theta_5}{2}\\right)e^{i\\theta_3}\\right)\\cos\\left(\\frac{\\theta_4}{2}\\right)e^{i\\theta_2}\\right]\n", + "e^{i\\theta_6}e^{i\\theta_7}|11\\rangle\n", + "\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "93e9e6ae-85e8-4077-9c25-f947daebe610", + "metadata": {}, + "source": [ + "This is likely too complicated to parse. Instead, just step back and think about how many parameters we loaded onto the state: eight. But we have with just four computational basis states. At first glance, it may appear that we have loaded more parameters than makes sense, since the final state can be written as $\\psi_\\text{final} = c_0|00\\rangle+c_1|01\\rangle+c_2|10\\rangle+c_3|11\\rangle$. Note, however, that each prefactor is complex! Written like this:\n", + "$$\n", + "\\psi_\\text{final} = (a_0+ib_0)|00\\rangle+(a_1+ib_1)|01\\rangle+(a_2+ib_2)|10\\rangle+(a_3+ib_3)|11\\rangle\n", + "$$\n", + "One can see that we do, indeed, have eight parameters on the state on which to encode our eight features.\n", + "\n", + "By increasing the number of qubits and increasing the number of repetitions of entangling and rotation layers, one can encode much more data. Writing out the wave functions quickly becomes intractable. But we can still see the encoding in action." + ] + }, + { + "cell_type": "markdown", + "id": "63ee5098-028a-4e40-a03d-c6f0e7c75605", + "metadata": {}, + "source": [ + "Here we encode the data vector $\\vec{x}$ with 12 features, on a 3-qubit `efficient_su2` circuit, using each of the parameterized gates to encode a different feature.\n", + "\n", + "$$\n", + "\\vec{x} = (0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2)\n", + "$$\n", + "\n", + "In this data vector, the features are shown in a particular order. In isolation, it doesn't matter if they are encoded in this order or in the reverse. What is important is keeping track of it and being consistent. Note in the circuit diagram that `efficient_su2` assumes a certain ordering of encoding, specifically filling the first layer of parameterized gates from qubit 0 to qubit 2, and then moving to the next layer. This is neither consistent nor inconsistent with little-endian notation, since here the data features cannot be ordered by qubit *a priori*, before an encoding circuit has been specified." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "73fc00fe-b98f-4d63-a327-54958a8f5498", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2]\n", + "circuit = efficient_su2(num_qubits=3, reps=1, insert_barriers=True)\n", + "encode = circuit.assign_parameters(x)\n", + "encode.decompose().draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "92a323d6-58e6-4e4d-b02f-9c63a0e78aaf", + "metadata": {}, + "source": [ + "Instead of increasing the number of qubits, you might choose to increase the number of repetitions of entangling and rotation layers. But there are limits to how many repetitions are useful." + ] + }, + { + "cell_type": "markdown", + "id": "90a4abfc-7d1e-4495-b36d-6f7edeed8e96", + "metadata": {}, + "source": [ + "As previously stated, there is a tradeoff: circuits with more qubits or more repetitions of entangling and rotation layers may store more parameters, but do so with greater circuit depth. We will return to the depths of some built-in feature maps, below." + ] + }, + { + "cell_type": "markdown", + "id": "95126be4-3e8e-4ee4-9420-1b362d88bef8", + "metadata": {}, + "source": [ + "The next few encoding methods that are built into Qiskit have \"feature map\" as part of their names. Let us reiterate that encoding data into a quantum circuit *is* a feature mapping, in the sense that it takes data into a new space: the Hilbert space of the qubits involved. The relationship between the dimensionality of the original feature space and that of the Hilbert space will depend on the circuit you use for encoding." + ] + }, + { + "cell_type": "markdown", + "id": "a3fb23b0-ee8c-49ba-99b7-f256fdbba9d5", + "metadata": {}, + "source": [ + "### $Z$ feature map\n", + "\n", + "The $Z$ feature map (ZFM) can be interpreted as a natural extension of phase encoding. The ZFM consists of alternating layers of single-qubit gates: Hadamard gate layers and phase gate layers. Let the data vector $\\vec{x}$ have $N$ features. The quantum circuit that performs the feature mapping is represented as a unitary operator that acts on the initial state:\n", + "\n", + "$$\n", + "\\mathscr{U}_{\\text{ZFM}}(\\vec{x})|0\\rangle^{\\otimes N}=|\\phi(\\vec{x})\\rangle\n", + "$$\n", + "where $|0\\rangle^{\\otimes N}$ is the $N$-qubit ground state. This notation is used for consistency with reference [\\[4\\]](#references) Havlicek et al. The data features $x_i$ are mapped one-to-one with corresponding qubits. For example, if you have 8 features in a data vector, then you would use 8 qubits. The ZFM circuit is composed of $r$ repetitions of a subcircuit comprised of Hadamard gate layers and phase gate layers. A Hadamard layer is made up of a Hadamard gate acting on every qubit in an $n$-qubit register, $H \\otimes H \\otimes \\dots \\otimes H = H^{\\otimes n}$, within the same stage of the algorithm. This description also applies to a phase gate layer in which the $i^\\text{th}$ qubit is acted on by $P(\\vec{x}_i)$. Each $P$ gate has one feature as an argument, but the phase gate layer ($P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)$ is a function of the data vector. The full ZFM circuit unitary with a single repetition is:\n", + "$$\n", + "\\mathscr{U}_{\\text{ZFM}}=\\big(P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)H^{\\otimes N}\\big)=\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\n", + "$$\n", + "Then $r$ repetitions of this unitary would be\n", + "$$\n", + "\\mathscr{U}^{(r)}_{\\text{ZFM}}\\left(\\vec{x}\\right)=\\prod_{s=1}^{r}\\left[\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\\right]\n", + "$$\n", + "The data features, $x_k$, are mapped to the phase gates in the same way in all $r$ repetitions. The ZFM feature map state is a product state and is efficient for classical simulation[\\[4\\]](#references).\n", + "\n", + "To start with a small example, a two-qubit ZFM circuit is coded using Qiskit and drawn to display the simple circuit structure. In the example, a single repetition, $r=1$, is implemented with the data vector $\\vec{x} = \\left(\\textstyle\\frac{1}{2}\\pi, \\textstyle\\frac{1}{3}\\pi\\right)$. Note that this is written in the standard order of a vector in Python, meaning the $0^\\text{th}$ element is $\\textstyle\\frac{1}{2}\\pi.$ We are free to encode this $0^\\text{th}$ feature onto our $0^\\text{th}$ qubit, or onto our $N^\\text{th}.$ Again, there cannot always be a single 1:1 mapping from feature order to qubit order, since different feature maps encode different numbers of features to each qubit. Again what is important is that we are aware of where each feature is being encoded. When providing a parameter list to the $Z$ feature map, it will encode feature 0 from the list to the least-significant qubit with a parameterized gate, as in qubit 0. So we will follow that convention when doing this by hand. We will encode $\\textstyle\\frac{1}{2}\\pi$ on the $0^\\text{th}$ qubit, and $\\textstyle\\frac{1}{3}\\pi$ on the $1^\\text{st}$ qubit.\n", + "\n", + "The ZFM circuit unitary operator acts on the initial state in the following way:\n", + "\n", + "$$\n", + "\\mathscr{U}_{\\text{ZFM}}(\\bar{x})|00\\rangle = P(\\bar{x})^{\\otimes 2} H^{\\otimes 2}|00\\rangle = \\left( P\\left(\\textstyle\\frac{1}{3}\\pi\\right)H|0\\rangle \\right) \\otimes \\left(P\\left(\\textstyle\\frac{1}{2}\\pi\\right)H|0\\rangle\\right).\n", + "$$\n", + "\n", + "The formula has been rearranged around the tensor product to emphasize the operations on each qubit. The following Qiskit code uses Hadamard and phase gates explicitly to show the structure of the ZFM:" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "f5c70df4-faea-4817-a870-95638eb97dbd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc0 = QuantumCircuit(1)\n", + "qc1 = QuantumCircuit(1)\n", + "\n", + "qc0.h(0)\n", + "qc0.p(pi / 2, 0)\n", + "\n", + "qc1.h(0)\n", + "qc1.p(pi / 3, 0)\n", + "\n", + "# Combine circuits qc0 and qc1 into 1 circuit\n", + "qc = QuantumCircuit(2)\n", + "qc.compose(qc0, [0], inplace=True)\n", + "qc.compose(qc1, [1], inplace=True)\n", + "\n", + "qc.draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "b1be7f51-3a4e-4d2e-8d51-d3ac62f4fd88", + "metadata": {}, + "source": [ + "We now encode the same data vector $\\vec{x} = \\left(\\textstyle\\frac{1}{2}\\pi, \\textstyle\\frac{1}{3}\\pi\\right)$ to a ZFM circuit with three repetitions, $r=3$, using the Qiskit [`z_feature_map`](/docs/api/qiskit/qiskit.circuit.library.ZFeatureMap) class, which altogether gives us the quantum feature map $\\mathscr{U}_{\\text{ZFM}}(\\vec{x})$. By default in the `z_feature_map` class, parameters $\\beta$ are multiplied by 2 before mapping to the phase gate $\\beta \\rightarrow P(\\theta = 2\\beta)$. To reproduce the same encodings as above, we divide by 2." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "c15b8fe2-ae83-4c76-a908-71596deb7d82", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import z_feature_map\n", + "\n", + "zfeature_map = z_feature_map(feature_dimension=2, reps=3)\n", + "zfeature_map = zfeature_map.assign_parameters([(1 / 2) * pi / 2, (1 / 2) * pi / 3])\n", + "zfeature_map.decompose().draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "6551f7cd-8e7e-4313-9255-7c1a3928f625", + "metadata": {}, + "source": [ + "Clearly this is a different mapping from the one done by hand above, but note the consistency in parameter ordering: $\\textstyle\\frac{1}{2}\\pi$ was again encoded on the $0^\\text{th}$ qubit.\n", + "\n", + "You may use ZFM via Qiskit's ZFM class; you can also use this structure as inspiration to construct your own feature mapping." + ] + }, + { + "cell_type": "markdown", + "id": "08bbc7eb-b48c-44fa-be70-852b27b73343", + "metadata": {}, + "source": [ + "### $ZZ$ feature map\n", + "\n", + "The $ZZ$ feature map (ZZFM) extends the ZFM with the inclusion of two-qubit entangling gates, specifically the $ZZ$-rotation gate $R_{ZZ}(\\theta)$. The ZZFM is conjectured to be generally expensive to compute on a classical computer, unlike the ZFM.\n", + "\n", + "$R_{ZZ}(\\theta)$ implements a $ZZ$-interaction and is maximally entangling for $\\theta = \\textstyle{\\frac{1}{2}}\\pi$. $R_{ZZ}(\\theta)$ can be decomposed into a series of gates on two qubits, as shown in the following Qiskit code using the [RZZ gate](/docs/api/qiskit/qiskit.circuit.library.RZZGate) and the `QuantumCircuit` class method ```decompose```. We encode a single feature of the data vector $\\vec{x}$: $\\vec{x}_k=\\pi.$" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "6c312a5f-91a5-499c-a391-efc73cd0e4e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.rzz(pi, 0, 1)\n", + "qc.draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "0795b997-4193-44cc-8310-4b8f5490aff0", + "metadata": {}, + "source": [ + "As is often the case, we see this represented as a single gate-like unit, until we use .decompose() to see all constituent gates." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "92062646-78f2-4dd6-82ad-7a543cb6a566", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc.decompose().draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "2c8a07ad-be40-4c18-b9e2-905a46fc7f4b", + "metadata": {}, + "source": [ + "Data is mapped with a phase rotation $P(\\theta) = e^{i\\theta/2}R_Z(\\theta)$ on the second qubit. The $R_{ZZ}(\\theta)$ gate entangles the two qubits on which it operates by a degree of entanglement determined by the encoded feature value.\n", + "\n", + "The full ZZFM circuit consists of a Hadamard gate and phase gate, as in the ZFM, followed by the entanglement described above. A single repetition of the ZZFM circuit is:\n", + "\n", + "$$\n", + "\\mathscr{U}_{\\text{ZZFM}}(\\vec{x}) = U_{ZZ}(\\vec{x})\\big(P(\\vec{x}_1)\\otimes\\ldots P(\\vec{x}_k)\\otimes\\ldots P(\\vec{x}_N)H^{\\otimes N}\\big)=U_{ZZ}(\\vec{x})\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N},\n", + "$$\n", + "\n", + "where $U_{ZZ}(\\vec{x})$ contains ZZ-gate layer structured by an entanglement scheme. Several entanglement schemes are shown in code blocks below. The structure of $U_{ZZ}(\\vec{x})$ also includes a function that combines the data features from qubits being entangled in the following way. Let us say that the $R_{ZZ}$ gate is to be applied to qubits $p$ and $q$. In the phase layer, these qubits have phase gates that encode $\\vec{x}_p$ and $\\vec{x}_q$ on them, respectively. The argument $\\theta_{q,p}$ of the $R_{ZZ,q,p}(\\theta_{q,p})$ will not simply be one of these features or the other, but a function often denoted by $\\phi$ (not to be confused with the azimuthal angle):\n", + "$$\n", + "\\theta_{q,p} \\rightarrow \\phi(\\vec{x}_q, \\vec{x}_p) = 2(\\pi-\\vec{x}_q)(\\pi-\\vec{x}_p).\n", + "$$\n", + "We will see this in several examples below. The extension to multiple repetitions is the same as in the `z_feature_map` case:\n", + "$$\n", + "\\mathscr{U}^{(r)}_{\\text{ZZFM}}\\left(\\vec{x}\\right)=\\prod_{s=1}^{r}\\left[U_{ZZ}(\\vec{x})\\left(\\bigotimes_{k = 1}^N P(\\vec{x}_k)\\right)H^{\\otimes N}\\right].\n", + "$$\n", + "As the operators have increased in complexity, let us first encode a data vector $\\vec{x} = (x_0, x_1)$ with a two-qubit ZZFM and one repetition using the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "1ec2f2e1-b665-4dde-a223-35489f17c695", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import zz_feature_map\n", + "\n", + "feature_dim = 2\n", + "zzfeature_map = zz_feature_map(\n", + " feature_dimension=feature_dim, entanglement=\"linear\", reps=1\n", + ")\n", + "zzfeature_map.decompose(reps=1).draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "2fa647fb-587d-4343-8fcb-e1bb34fea584", + "metadata": {}, + "source": [ + "By default in Qiskit, the features $(\\vec{x}_1, \\vec{x}_2)$ are mapped together to $R_{ZZ}(\\theta)$ by this mapping function $\\theta_{1,2} = \\phi(\\vec{x}_1, \\vec{x}_2) = 2(\\pi-\\vec{x}_1)(\\pi-\\vec{x}_2)$. Qiskit allows the user to customize the function $\\phi$ (or $\\phi_S$ where $S$ is the set of qubit pairs coupled through $R_{ZZ}$ gates) as a preprocessing step.\n", + "\n", + "Moving to a four-dimensional data vector $\\vec{x} = (\\vec{x}_1, \\vec{x}_2, \\vec{x}_3, \\vec{x}_4)$ and mapping to a four-qubit ZZFM with one repetition, we can start to see the mapping $\\phi$ for various qubit pairs. We can also see the meaning of \"linear\" entanglement:" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "979c765a-e3d8-4e3e-8e52-3f816203a934", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "feature_dim = 4\n", + "zzfeature_map = zz_feature_map(\n", + " feature_dimension=feature_dim, entanglement=\"linear\", reps=1\n", + ")\n", + "zzfeature_map.decompose().draw(\"mpl\", scale=1)" + ] + }, + { + "cell_type": "markdown", + "id": "77d42bee-a283-4d19-85a6-bd9c8deedae4", + "metadata": {}, + "source": [ + "In the linear entanglement scheme, nearest-neighbor (numbered) pairs of qubits in this circuit are entangled. There are other built-in entanglement schemes in Qiskit, including `circular` and `full`." + ] + }, + { + "cell_type": "markdown", + "id": "eec9d682-f698-47a1-bc2f-86174c2efb0f", + "metadata": {}, + "source": [ + "### Pauli feature map\n", + "\n", + "The Pauli feature map (PFM) is the generalization of the ZFM and ZZFM to use arbitrary Pauli gates. The Pauli feature map takes a very similar form to the previous two feature maps. For $r$ repetitions of the encoding of the $N$ features of vector $\\vec{x},$\n", + "\n", + "$$\n", + "\\mathscr{U}_{\\text{PFM}}(\\vec{x}) = \\prod_{s=1}^{r} U(\\vec{x}) H^{\\otimes n}.\n", + "$$\n", + "\n", + "For PFM, $U(\\vec{x})$ is generalized to a Pauli expansion unitary operator. Here we present a more generalized form of the feature maps considered so far:\n", + "\n", + "$$\n", + "U(\\vec{x}) = \\exp\\left(i \\sum_{S \\in\\mathcal{I}} \\phi_S(\\vec{x}) \\prod_{i \\in S} \\sigma_i \\right),\n", + "$$\n", + "\n", + "where $\\sigma_i$ is a Pauli operator, $\\sigma_i \\in {I,X,Y,Z}$. Here $\\mathcal{I}$ is the set of all qubit connectivities as determined by the feature map, including the set of qubits acted on by single-qubit gates. That is, for a feature map in which qubit 0 was acted upon by a phase gate, and qubits 2 and 3 were acted upon by an $R_{ZZ}$ gate, the set $\\mathcal{I}$ would include $\\{\\{0\\},\\{2,3\\}\\}$. $S$ runs through all elements of that set. In previous feature maps, the function $\\phi_S(\\vec{x})$ was involved either exclusively with single-qubit gates or exclusively with two-qubit gates. Here, we define it in general:\n", + "$$\n", + "\\phi_S(\\vec{x})=\n", + " \\begin{cases}\n", + " x_i & \\text{if } S= \\{i\\} \\text{ (single-qubit)}\\\\\n", + " \\prod_{j\\in{S}}(\\pi-x_j) & \\text{if } |S|\\ge2 \\text{ (multi-qubit)}\\\\\n", + " \\end{cases}\n", + "$$\n", + "\n", + "For documentation, see the [Qiskit `Pauli feature map` class documentation](/docs/api/qiskit/qiskit.circuit.library.PauliFeatureMap)). In the ZZFM, the operator $\\sigma_i$ is restricted to $Z_i$.\n", + "\n", + "One way to understand the above unitary is through analogy with the propagator in a physical system. The unitary above is a unitary evolution operator, $\\exp(it\\mathcal{H})$, for a Hamiltonian, $\\mathcal{H}$, similar to the Ising model, where the time parameter, $t$, is replaced with data values to drive the evolution. The expansion of this unitary operator gives the PFM circuit. The entangling connectivities in $S$ can be interpreted as Ising couplings in a spin lattice." + ] + }, + { + "cell_type": "markdown", + "id": "29290b2a-fe6d-4c0c-805a-ae37bb2e48a9", + "metadata": {}, + "source": [ + "Let us consider an example of Pauli $Y$ and $XX$ operators representing those Ising-type interactions. Qiskit provides a `pauli_feature_map` class for instantiating a PFM with a choice of single- and $n$-qubit gates, which in this example will be passed as Pauli strings `‘Y’` and `‘XX’`. Typically, $n$ is 1 or 2 for single- and two-qubit interactions, respectively. The entanglement scheme is “linear,” meaning that only nearest-neighbor qubits in the quantum circuit are coupled. Note that this does not correspond to nearest-neighbor qubits on the quantum computer itself, as this quantum circuit is an abstraction layer." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "5ba5df82-83c1-428c-a269-baea5f75c3dc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import pauli_feature_map\n", + "\n", + "feature_dim = 3\n", + "pfmap = pauli_feature_map(\n", + " feature_dimension=feature_dim, entanglement=\"linear\", reps=1, paulis=[\"Y\", \"XX\"]\n", + ")\n", + "\n", + "pfmap.decompose().draw(\"mpl\", scale=1.5)" + ] + }, + { + "cell_type": "markdown", + "id": "d1adef80-5504-4cf2-8d78-3a129637f67c", + "metadata": {}, + "source": [ + "Qiskit provides a parameter, $\\alpha$, in Pauli feature maps to control the scaling of Pauli rotations.\n", + "\n", + "$$\n", + "U(\\bar{x}) = \\exp\\left(i \\alpha \\sum_{S\\subseteq[n]} \\phi_S(\\bar{x}) \\prod_{i \\in S} \\sigma_i \\right)\n", + "$$\n", + "\n", + "The default value of $\\alpha$ is $2$. By optimizing its value in the interval, for example, $[0,4],$ one can better align a quantum kernel to the data." + ] + }, + { + "cell_type": "markdown", + "id": "c457b46a-9982-45eb-b2df-12fc7251d91e", + "metadata": {}, + "source": [ + "### Gallery of Pauli feature maps\n", + "\n", + "Here we visualize various Pauli feature maps for two-qubit circuits to get a better picture of the range of possibilities." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "69225757-193a-490b-8ca4-d1b187b774b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.visualization import circuit_drawer\n", + "import matplotlib.pyplot as plt\n", + "\n", + "feature_dim = 2\n", + "fig, axs = plt.subplots(9, 2)\n", + "i_plot = 0\n", + "for paulis in [\n", + " [\"I\"],\n", + " [\"X\"],\n", + " [\"Y\"],\n", + " [\"Z\"],\n", + " [\"XX\"],\n", + " [\"XY\"],\n", + " [\"XZ\"],\n", + " [\"YY\"],\n", + " [\"YZ\"],\n", + " [\"ZZ\"],\n", + " [\"X\", \"ZZ\"],\n", + " [\"Y\", \"ZZ\"],\n", + " [\"Z\", \"ZZ\"],\n", + " [\"X\", \"YZ\"],\n", + " [\"Y\", \"YZ\"],\n", + " [\"Z\", \"YZ\"],\n", + " [\"YY\", \"ZZ\"],\n", + " [\"XY\", \"ZZ\"],\n", + "]:\n", + " pfmap = pauli_feature_map(feature_dimension=feature_dim, paulis=paulis, reps=1)\n", + " circuit_drawer(\n", + " pfmap.decompose(),\n", + " output=\"mpl\",\n", + " style={\"backgroundcolor\": \"#EEEEEE\"},\n", + " ax=axs[int((i_plot - i_plot % 2) / 2), i_plot % 2],\n", + " )\n", + " axs[int((i_plot - i_plot % 2) / 2), i_plot % 2].title.set_text(paulis)\n", + " i_plot += 1\n", + "\n", + "fig.set_figheight(16)\n", + "fig.set_figwidth(16)" + ] + }, + { + "cell_type": "markdown", + "id": "61e585cc-8e84-496e-9e51-21a818731be8", + "metadata": {}, + "source": [ + "The above can, of course, be extended to include other permutations and repetitions of Pauli matrices. Learners are encouraged to experiment with those options." + ] + }, + { + "cell_type": "markdown", + "id": "44a27c71-acb8-4b4f-a344-836cae1e3896", + "metadata": {}, + "source": [ + "## Review of built-in feature maps\n", + "\n", + "You have seen several schemes for encoding data into a quantum circuit:\n", + "- Basis encoding\n", + "- Amplitude encoding\n", + "- Angle encoding\n", + "- Phase encoding\n", + "- Dense encoding\n", + "\n", + "You have seen how to construct your own feature maps using these encoding schemes, and you have seen four built-in feature maps which take advantage of angle and phase encoding:\n", + "- Efficient SU2\n", + "- Z feature map\n", + "- ZZ feature map\n", + "- Pauli feature map\n", + "\n", + "These built-in feature maps differed from each other in several ways:\n", + "- The depth for a given number of encoded features\n", + "- The number of qubits required for a given number of features\n", + "- The degree of entanglement (obviously related to the other differences)\n", + "\n", + "The code below applies these four built-in feature maps to the encoding of a feature set, and plots the two-qubit depth of the resulting circuit. Since two-qubit error rates are much higher than single-qubit gate error rates, one might reasonably be most interested in the depth of two-qubit gates. In the code below, we obtain counts of all gates in a circuit by first decomposing the circuit and then using count_ops(), as shown below. Here the two-qubit gates we are interested in are 'cx' gates:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f428edb-ea96-48bb-adb2-aed91535dbeb", + "metadata": {}, + "outputs": [], + "source": [ + "# Initializing parameters and empty lists for depths\n", + "x = [0.1, 0.2]\n", + "n_data = []\n", + "zz2gates = []\n", + "su22gates = []\n", + "z2gates = []\n", + "p2gates = []\n", + "\n", + "# Generating feature maps\n", + "for n in range(3, 10):\n", + " x.append(n / 10)\n", + " zzcircuit = zz_feature_map(n, reps=1, insert_barriers=True)\n", + " zcircuit = z_feature_map(n, reps=1, insert_barriers=True)\n", + " su2circuit = efficient_su2(n, reps=1, insert_barriers=True)\n", + " pcircuit = pauli_feature_map(n, reps=1, paulis=[\"XX\"], insert_barriers=True)\n", + " # Getting the cx depths\n", + " zzcx = zzcircuit.decompose().count_ops().get(\"cx\")\n", + " zcx = zcircuit.decompose().count_ops().get(\"cx\")\n", + " su2cx = su2circuit.decompose().count_ops().get(\"cx\")\n", + " pcx = pcircuit.decompose().count_ops().get(\"cx\")\n", + "\n", + " # Appending the cx gate counts to the lists. We shift the zz and pauli data points, because they\n", + " # overlap.\n", + " n_data.append(n)\n", + " zz2gates.append(zzcx - 0.5)\n", + " z2gates.append(0)\n", + " su22gates.append(su2cx)\n", + " p2gates.append(pcx + 0.5)\n", + "\n", + "# Plot the output\n", + "plt.plot(n_data, p2gates, \"bo\")\n", + "plt.plot(n_data, zz2gates, \"ro\")\n", + "plt.plot(n_data, su22gates, \"yo\")\n", + "plt.plot(n_data, z2gates, \"go\")\n", + "plt.ylabel(\"CX Gates\")\n", + "plt.xlabel(\"Data elements\")\n", + "plt.legend([\"Pauli\", \"ZZ\", \"SU2\", \"Z\"])\n", + "# plt.suptitle('zz_feature_map(n)')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "777bf066-af13-4065-a795-af19f67da099", + "metadata": {}, + "source": [ + "Generally Pauli and ZZ feature maps will result in greater circuit depth and higher numbers of 2-qubit gates than `efficient_su2` and Z feature maps.\n", + "\n", + "Because the feature maps built into Qiskit are widely applicable, we will often not need to design our own, especially in the learning phase. However, experts in quantum machine learning will likely return to the subject of designing their own feature mapping, as they tackle two complicated challenges:\n", + "\n", + "1. Modern hardware: the presence of noise and the large overhead of error-correcting code mean that present-day applications will need to consider things like hardware efficiency and minimizing two-qubit gate depth.\n", + "\n", + "2. Mappings that fit the problem at hand: It is one thing to say that the `zz_feature_map`, for example, is difficult to simulate classically, and therefore interesting. It is quite another thing for the `zz_feature_map` to be ideally suited to __your__ machine learning task or data set. The performance of different parameterized quantum circuits on different types of data is an active area of investigation.\n", + "\n", + "We close with a note on hardware efficiency." + ] + }, + { + "cell_type": "markdown", + "id": "8b795f8d-1f9f-4e43-82df-f2725ff709e9", + "metadata": {}, + "source": [ + "## Hardware-efficient feature mapping\n", + "\n", + "A hardware-efficient feature mapping is one that takes into account constraints of real quantum computers, in the interest of reducing noise and errors in the computation. When running quantum circuits on near-term quantum computers, there are many strategies to mitigate noise inherent to the hardware. One main strategy for hardware efficiency is the minimization of the depth of the quantum circuit so that noise and decoherence have less time to corrupt the computation. The depth of a quantum circuit is the number of time-aligned gate steps required to complete the entire computation (after circuit optimization)[\\[5\\]](#references). Recall that the depth of the abstract, logical circuit may be much lower than the depth once the circuit is transpiled for a real quantum computer.\n", + "\n", + "Transpilation is the process of converting the quantum circuit from a high-level abstraction to one that is ready to run on a real quantum computer, taking into account constraints of the hardware. A quantum computer has a native set of single- and two-qubit gates. This means all gates in Qiskit code have to be transpiled into the set of native hardware gates. For example, in ibm_torino, a QPU sporting a Heron r1 processor and completed in 2023, the native or basis gates are `{CZ, ID, RZ, SX, X}`. These are the two-qubit controlled-Z gate, and single-qubit gates called identity, $Z$-rotation, square root of NOT, and NOT, respectively, providing a universal set. When implementing multi-qubit gates as an equivalent subcircuit, physical two-qubit $CZ$ gates are required, along with other single-qubit gates available in hardware. In addition, to perform a two-qubit gate on a pair of qubits that are not physically coupled, SWAP gates are added to move qubit states between qubits to enable coupling, which leads to an unavoidable extension of the circuit. Using the ```optimization``` argument that can be set from 0 up to a highest level of 3. For greater control and customizability, the transpiler pipeline can be managed with the [Qiskit Pass Manager](/docs/api/qiskit/qiskit.transpiler.PassManager). Refer to the [Qiskit Transpiler documentation](/docs/api/qiskit/transpiler) for more information on transpilation.\n", + "\n", + "In Havlicek et al. 2019 [\\[2\\]](#references), one way the authors achieve hardware efficiency is by using the $ZZ$ feature map because it is a second-order expansion (see the “$ZZ$ feature map” section above). An $N$-order expansion has $N$-qubit gates. IBM® quantum computers do not have native $N$-qubit gates, where $N>2$, so to implement them would require decomposition into two-qubit CNOT gates available in hardware. A second way the authors minimize depth is by choosing a $ZZ$ coupling topology that maps directly to the architecture couplings. A further optimization they undertake is targeting a higher-performing, suitably connected hardware subcircuit. Additional things to consider are minimizing the number of feature map repetitions and choosing a customized low-depth or “linear” entangling scheme instead of the “full” scheme that entangles all qubits.\n", + "\n", + "![Data encoding image](/learning/images/courses/quantum-machine-learning/data-encoding/qml-03-data-encoding-24.avif)\n", + "\n", + "The above graphic shows a network of nodes and edges that represent physical qubits and hardware couplings, respectively. The coupling map and performance of ibm_torino is shown with all possible two-qubit CZ coupling gates. Qubits are color-coded on a scale based on the T1 relaxation time in microseconds (μs), where longer T1 times are better and in a lighter shade. The coupling edges are color-coded by CZ error, where darker shades are better. Information on the hardware specification can be accessed in the hardware backend configuration schema ```IBMQBackend.configuration()```." + ] + }, + { + "cell_type": "markdown", + "id": "98db3e11-2e00-406c-a4ee-88dc3ee82b96", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1. Maria Schuld and Francesco Petruccione, *Supervised Learning with Quantum Computers*, Springer 2018, [doi:10.1007/978-3-319-96424-9](https://www.springer.com/gp/book/9783319964232).\n", + "2. Vojtech Havlicek et al., “Supervised Learning with Quantum Enhanced Feature Spaces.” *Nature*, vol. 567 (2019): 209–212. https://arxiv.org/abs/1804.11326.\n", + "3. Ryan LaRose and Brian Coyle, \"Robust data encodings for quantum classifiers\", Physical Review A 102, 032420 (2020), [doi:10.1103/PhysRevA.102.032420](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.102.032420), [arXiv:2003.01695](https://arxiv.org/abs/2003.01695).\n", + "4. Lou Grover and Terry Rudolph. “Creating Superpositions That Correspond to Efficiently Integrable Probability Distributions.” arXiv:quant-ph/0208112, August 15, 2002, https://arxiv.org/abs/quant-ph/0208112.\n", + "5. Adrián Pérez-Salinas, Alba Cervera-Lierta, Elies Gil-Fuster, José I. Latorre, \"Data re-uploading for a universal quantum classifier\", [Quantum 4, 226 (2020)](https://quantum-journal.org/papers/q-2020-02-06-226/), [ArXiv.org/abs/1907.02085](https://arxiv.org/abs/1907.02085).\n", + "6. Maria Schuld, Ryan Sweke, Johannes Jakob Meyer, \"The effect of data encoding on the expressive power of variational quantum machine learning models\", [Phys. Rev. A 103, 032430 (2021)](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.103.032430), [arxiv.org/abs/2008.08605](https://arxiv.org/abs/2008.08605)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "571dc6bf-7c16-4623-be61-64a5b215bd69", + "metadata": {}, + "outputs": [], + "source": [ + "import qiskit\n", + "\n", + "qiskit.version.get_version_info()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/courses/quantum-machine-learning/introduction.ipynb b/learning/courses/quantum-machine-learning/introduction.ipynb index 57d54df0b48..12874a9ff32 100644 --- a/learning/courses/quantum-machine-learning/introduction.ipynb +++ b/learning/courses/quantum-machine-learning/introduction.ipynb @@ -1,395 +1,398 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0de576a4-9a1a-46e9-8d6b-e2c5178d6051", - "metadata": { - "gloss": { - "computational-basis-state": { - "text": "Also known as Z-basis states, these are the states we measure when we measure in the Z (or 'computational') basis. These are the states with labels like |00〉 and |00110100〉. IBM® quantum computers always measure in the Z-basis.", - "title": "computational basis state" - }, - "features": { - "text": "A feature is a property of the things we're trying to learn about that we can assign a number to. If we were learning something about cats, the features might be \"height\" or \"age\" or \"propensity to consume treats\".", - "title": "features" - }, - "unitary": { - "text": "A unitary operation is a reversible operation that preserves the norm (that is, makes sure our probabilities always sum to 1). [Read more](https://en.wikipedia.org/wiki/Unitary_matrix).", - "title": "unitary" - } - } - }, - "source": [ - "---\n", - "title: Introduction\n", - "description: An introduction to quantum machine learning that sets expectations, describes the course structure, and gives an initial example.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore webkitallowfullscreen allowfullscreen quantumly Ewin Yunchao Srinivasan Arunachalam Kristan */}\n", - "\n", - "# Introduction to Quantum Machine Learning\n", - "\n", - "## Overview and motivation\n", - "\n", - "Welcome to quantum machine learning!\n", - "\n", - "The video below will give a brief introduction that is supplemented by the text below.\n", - "\n", - "\n", - "\n", - "To briefly recap and augment the video:\n", - "\n", - "- We have seen a problem be solved for the first time on a quantum computer, and then subsequently people find a way to do it on a classical supercomputer. This cycle of classical and quantum computing pushing each other to their limits will likely continue for a few years.\n", - "- There are specific problems where quantum computing can have a provable advantage over classical computing, given progress in areas such as reduction of errors and the number of qubits available. But this is still a time of exploration, searching for quantum-amenable datasets and useful quantum feature maps.\n", - "- Quantum machine learning (QML) is one of many exciting areas where quantum computing can augment or complement existing classical workflows.\n", - "\n", - "Machine learning (ML) applies algorithms to data sets, and so QML might plausibly include quantum mechanics in either the data or algorithmic sides, or both. All of these possibilities are potentially interesting. But we will mostly restrict ourselves to discussions of quantum algorithms applied to classical data. One reason for this is that ML problems with classical data are already so well studied and widely available. There is broad interest in solving problems that start with classical data. Another reason is the lack of QRAM. Without the ability to store large amounts of quantum data on a relatively long timescale, methods that begin with quantum data are still fairly far from applicability to industry. It is also unclear how to \"quantumly-access\" classical data in an efficient manner. Two types of ML of particular interest are supervised learning, in which you train an algorithm using a labeled data set, and unsupervised learning, in which the algorithm attempts to learn about a distribution from unlabeled samples. An unsupervised algorithm might, for example, learn how to generate new samples from the same distribution, or how to cluster the samples into groups with similar characteristics.\n", - "\n", - "![QML_CR_background_Sup_Unsup.avif](/learning/images/courses/quantum-machine-learning/introduction/qml-cr-background-sup-unsup.avif)\n", - "\n", - "The left image shows two categories of labeled data as in supervised learning. In this case, the categories are linearly separable. The right image shows clusters of data. In an unsupervised learning task, these data would not initially be labeled and the algorithm would study the distribution, perhaps looking for clusters. For the purposes of visualizing example clusters the algorithm might identify, the data points have now been labeled. A key difference between the two is that the supervised learning process starts with the data already labeled and the unsupervised process starts with unlabeled data, even if the data are labeled at the end.\n", - "\n", - "Those with background in machine learning will already know that many solution methods involve mapping data into higher-dimensional spaces. This is especially well-explored in the context of kernels. As a brief reminder, sometimes data may be separable into categories by a line, plane, or hyperplane (we will often simply say \"hyperplane\" for compactness), in the same number of dimensions as the data are given. This is shown in the first image above. Other times, data may not be separable by a hyperplane in those dimensions, as shown in the second image. But there can still be structure to the data that can be exploited in a mapping to higher dimensions, which then leaves the data separable in that higher-dimensional space. This is illustrated in the mapping of the 2D data with circular symmetry into the 3D space in which the data points are arranged along a paraboloid surface.\n", - "\n", - "![QML_CR_background_2D-3D.avif](/learning/images/courses/quantum-machine-learning/introduction/qml-cr-background-2d-3d.avif)\n", - "\n", - "A common goal in QML is to find a mapping from the lower-dimensional set of features into a higher-dimensional space, that effectively separates our data points so we can use the mapping to classify new data points.\n", - "But this is not an easy task, and any discussion of the potential usefulness of quantum computing in machine learning must be accompanied by the appropriate caveats. In particular, we must address the nuance in dataset selection and the challenges in reaching utility scale. We must also shift away from trying to outperform classical ML algorithms on data that are already handled efficiently and well by classical algorithms and refocus the discussion to investigating new feature maps that could be useful.\n", - "\n", - "## Managing expectations\n", - "\n", - "Many data sets used in QML applications described in literature are “feature engineered”, meaning a dataset is selected or generated specifically to show a narrow use case in which quantum computing is useful. If this seems like cheating then we’re misunderstanding the task at hand. It is __not__ the case that some quantum feature maps enable us to solve all or many classification tasks more efficiently or scalably than classical machine learning algorithms. Rather, some quantum feature maps (not all) behave differently from classical feature maps. The task at hand is then to explore quantum circuits in the context of complex data structures. Some specific questions to address are:\n", - "1.\tWhat quantum circuits are most likely to behave in novel ways, compared to classical alternatives?\n", - "2.\tAre there real-world problems that involve data with properties best explored using such novel quantum circuits?\n", - "3.\tDo these quantum circuits scale on near-term quantum computers?\n", - "\n", - "### Insufficient explanation\n", - "\n", - "One often encounters a simplified explanation of how quantum computing can be powerful. It goes something like this:\n", - "\n", - "Just as classical computers use bits of information, quantum computers use qubits. Given a number of bits, say 4, a classical computer can take on any one of $2^4 = 16$ possible states, whereas a quantum computer can exist in a superposition of all 16 states simultaneously, and operations can be performed on this entire superposition. In some cases, this naturally allows us to design potentially interesting learning algorithms based on mappings to higher dimensional spaces.\n", - "\n", - "This is a true statement, but it is inadequate, and a bit misleading as we will explain. One also sees the differences between complex and real coefficients emphasized, as in:\n", - "\n", - "A probabilistic classical system in which a system can be described as having certain probabilities of being in different states, can be described as follows.\n", - "$$\n", - "|s\\rangle = a|0000\\rangle+b|0001\\rangle+c|0010\\rangle+... a, b, c \\in \\reals\n", - "$$\n", - "In such a system, the coefficients $a$, $b$, $c$, and so on can only be meaningful if they are positive, real numbers. The states in quantum computers are described by probability amplitudes that can be complex numbers.\n", - "\n", - "$$\n", - "|\\psi \\rangle = A|0000\\rangle+B|0001\\rangle+C|0010\\rangle+... A, B, C \\in \\mathbb{C}\n", - "$$\n", - "\n", - "The above statements have been made very carefully such that they are true (many superficially similar statements are incorrect). But these correct statements are not an explanation of the power of quantum computing in machine learning. For one thing, any application of quantum computing to machine learning will involve measurements and we cannot measure a qubit to be in multiple states at once. We can prepare a qubit in a superposition like $|\\psi\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)$ but a measurement will yield either $|0\\rangle$ or $|1\\rangle$. So at a bare minimum, this story about increasing dimensionality is incomplete. Further, in the context of kernels, increased dimensions in quantum computing cannot be a sufficient condition for computational power over classical alternatives, since Gaussian kernels are infinite dimensional. There are subtleties there, in that Gaussian feature maps are only used in conjunction with the “kernel trick” that sidesteps the need to ever calculate an infinite-dimensional mapped vector. But the point remains:\n", - "\n", - "__High dimensionality of entangled quantum states is not exponential parallelism, and is not a sufficient condition for increased power in machine learning.__\n", - "\n", - "In the lessons that follow, we present workflows for incorporating quantum circuits into machine learning tasks, and we do this for the explicit purpose of facilitating exploration of the power of quantum computing. No feature map or algorithm in this course is put forth as a quick path to better machine learning results for general problems, because no such feature map or algorithm exist. Rather, we present a wide array of quantum tools to be used in exploration of useful quantum computing.\n", - "\n", - "### Dequantization\n", - "\n", - "Dequantization refers to the replacement of a given quantum algorithm with a classical one that performs similarly to a quantum algorithm for a given set of tasks, typically including scaling. By some definitions, the classical algorithm should perform only polynomially slower than the quantum algorithm.\n", - "\n", - "Several quantum machine learning (QML) algorithms that were initially thought to provide significant speedups over classical algorithms have been dequantized in recent years. This process of dequantization has led to important insights into the potential advantages and limitations of quantum approaches to machine learning.\n", - "\n", - "One of the most notable dequantization results came from Ewin Tang's [work on recommendation systems](https://arxiv.org/abs/1807.04271) Tang discovered a classical algorithm that could perform recommendation tasks at speeds previously thought to be achievable only by quantum computers. This discovery challenged the assumption that quantum algorithms had an exponential advantage for this problem. More recent work by [Shin et al.](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.023218) has focused on identifying conditions on the dequantizability of a variational quantum machine learning model's function class.\n", - "\n", - "One common approach to dequantization (though not the only trick) is through consideration of data loading overhead. That is, any quantum algorithm applied to classical data will have a step in which classical data are encoded into the quantum computer. If a quantum algorithm assumes a starting point at which quantum data are already available, then one effectively hides the time required for encoding. There are contexts in which assuming quantum data may be reasonable, but many applications of interest will start with classical data. Some dequantization cases have shown that when this encoding time is included, and when classical data loading can be accomplished efficiently, the quantum algorithm no longer outperforms its classical counterpart.\n", - "\n", - "Even if an algorithm cannot be dequantized, that does not mean it is more efficient or scalable than all classical algorithms. As an extreme, contrived example: imagine an algorithm to select the largest j elements from a set of size k. One could write a quantum algorithm that uses Shor’s algorithm to factor each of the k elements into prime factors, and then determine the largest elements using the prime factors. Such an algorithm likely cannot be dequantized, but is drastically less efficient than classical algorithms to accomplish the same selection of largest elements (though not the unnecessary factoring part).\n", - "\n", - "## Existence proof\n", - "\n", - "In 2021, IBM Quantum® researchers Yunchao Liu, Srinivasan Arunachalam, and Kristan Temme published a paper in Nature, [A rigorous and robust quantum speed-up in supervised machine learning.](https://www.nature.com/articles/s41567-021-01287-z) Consistent with the above caveats, a classification problem was carefully chosen for this work that is (1) known to be classically hard, and (2) suitable for quantum algorithms to show a speed-up.\n", - "\n", - "The paper addresses the classification of data based on discrete logarithms. To quote the paper, “For a large prime number $p$ and a generator $g$ of $\\mathbb{Z}^*_p = {1, 2, . . . , p − 1}$, it is a widely-believed conjecture that no classical algorithm can compute $\\text{log}_g(x)$ on input $x \\in \\mathbb{Z}^*_p $, in time polynomial in $n = \\lceil{\\text{log}_2(p)}\\rceil$, the number of bits needed to represent $p$.” In contrast, [Shor’s algorithm](https://epubs.siam.org/doi/10.1137/S0097539795293172) is known to solve the discrete log problem in polynomial time. This choice of problems thus simultaneously satisfies the criteria above: classical hardness (unlikely to be dequantized), and known to be suitable for quantum algorithms.\n", - "\n", - "Through this judicious choice of classification problem, the authors were able to show an exponential speed-up using quantum kernel methods (sketched briefly below and discussed in later lessons) that is both end-to-end and robust. Here, “end-to end” refers to the assumptions about starting with classical data; the authors in this case do include the time for data encoding. Here, “robust” refers to the fact that the data to be classified are separated by a wide margin using the quantum algorithm, such that the classification success is robust to real-world considerations like finite sampling error.\n", - "\n", - "All this is to say that problems do exist in which quantum kernels can yield an exponential speed-up. But the current state of the science is that such problems are selected based on observations or theoretical justification that they should be amenable to quantum algorithms. It is not realistic to expect a quantum speed-up for machine learning tasks that classical computers already do quite well.\n", - "\n", - "Identifying such ideal cases for the exploration of quantum utility is an enormous responsibility for learners in this course. And it is not a task that can be accomplished in a course such as this. That exploration is a task for the IBM Quantum Network as a whole, made up of researchers like yourself. This course will demonstrate QML workflows and encoding strategies so that you can begin to explore for quantum utility in your area of subject matter expertise.\n", - "\n", - "We hope this introduction has made a few things clear about quantum machine learning:\n", - "1. Quantum algorithms can offer an exponential speed-up over classical algorithms for very specific problems that are classically hard, and well-suited to quantum algorithms.\n", - "2. High dimensionality of entangled states in quantum computing matters, but it is not sufficient to simply gain an advantage over classical algorithms.\n", - "3. Finding problems that are well-suited to quantum algorithms is an extremely difficult task, and one that will largely fall to the learners in this course." - ] - }, - { - "cell_type": "markdown", - "id": "8ff490cb-1b17-4ac7-bd32-9edca1aa051e", - "metadata": {}, - "source": [ - "## Check-in questions\n", - "\n", - "What makes quantum states different from classical states?\n", - "\n", - "\n", - "\n", - "\n", - "A lot. Notably: complex coefficients, and superposition with a single copy. There are many other differences that will be discussed in future lessons, including entanglement and interference.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "True or False? Highly entangled quantum states enable us to solve most machine learning problems more efficiently on a quantum computer.\n", - "\n", - "\n", - "\n", - "\n", - "False. Most machine learning problems are solved very efficiently by classical algorithms and quantum algorithms are not likely to offer any substantial speed-up. The goal in QML is to finding datasets with features that are well-described by quantum states and/or to find mappings of data features that optimize the accuracy of models.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "0158cbee-9b6e-4b74-a449-a4cb6ef27f18", - "metadata": {}, - "source": [ - "## Course learning goals\n", - "\n", - "Through completing this course, you can expect to build the following core skills and competencies. Learners will be able to:\n", - "\n", - "1. Explain what QML is and where quantum connects to classical machine learning.\n", - "\n", - "2. Apply quantum vocabulary and key terms to ML workflows.\n", - "\n", - "3. Identify key components of a QML workflow (various types).\n", - "\n", - "4. Identify different types of QML and distinguish between them.\n", - "\n", - "5. Implement quantum kernel methods and variational quantum classifiers using Qiskit Runtime primitives and following Qiskit patterns.\n", - "\n", - "6. Identify where QML is most promising and where it is not.\n", - "\n", - "7. Adjust an example problem to their own data set.\n", - "\n", - "8. Be aware of issues in QML like training time, noise, and compounding error in multiple-state readouts.\n", - "\n", - "9. Make recommendations for where QML might benefit their organization." - ] - }, - { - "cell_type": "markdown", - "id": "cb9bf2a7-8487-482f-8fcc-95d2210bc314", - "metadata": {}, - "source": [ - "## Course structure\n", - "\n", - "This course is made up of several lessons. Each lesson has several check-in questions throughout the text, so you can practice new skills or check your understanding as you go. These are not required.\n", - "\n", - "At the end of the course, there is a 20-item quiz. You must score at least 70% on this quiz in order to obtain your Quantum Machine Learning badge, via Credly. If you score at least 70%, your badge will be automatically emailed to you, shortly thereafter. You may only submit the quiz twice. After the first submission, you will have the opportunity to take a second try at the questions you missed. After the second submission, your score is final. See the quiz for further details.\n", - "\n", - "The course structure is as follows:\n", - "\n", - "- Lesson 1: Introduction and overview\n", - "- Lesson 2: Recap of machine learning\n", - "- Lesson 3: Data encoding\n", - "- Lesson 4: Quantum kernel methods and support vector machines\n", - "- Lesson 5: Variational quantum classifiers / neural networks\n", - "- Exam for badge" - ] - }, - { - "cell_type": "markdown", - "id": "622774e8-c23c-4610-b204-80390b639852", - "metadata": {}, - "source": [ - "## Run your first QML code\n", - "\n", - "It is often helpful to see where we're going, before breaking it down into pieces, and delving into background. The code cells below carry out a simple instance of a quantum kernel method. Specifically, a single kernel matrix element is calculated. Users new to kernel methods or quantum kernels should not be intimidated by this; multiple lessons in this course will be devoted to dissecting exactly what is being done in these cells.\n", - "\n", - "With this code we simultaneously introduce Qiskit patterns: a framework for approaching quantum computing at the utility scale. This framework consists of four steps that are very general and can be applied to most problems (though in some workstreams, certain steps may be iterated multiple times).\n", - "\n", - "### Qiskit patterns:\n", - "\n", - "* Step 1: Map classical inputs to a quantum problem\n", - "* Step 2: Optimize problem for quantum execution\n", - "* Step 3: Execute using Qiskit Runtime Primitives\n", - "* Step 4: Analyzing / post-processing\n", - "\n", - "In the cells below, we offer only cursory explanations of the various steps, just enough for you to find the appropriate lesson to learn more." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63b85808-7f02-446e-9ef6-38369c95a4bb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--2025-05-09 10:04:28-- https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", - "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.109.133, 185.199.108.133, ...\n", - "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n", - "HTTP request sent, awaiting response... 200 OK\n", - "Length: 49405 (48K) [text/plain]\n", - "Saving to: ‘dataset_graph7.csv.2’\n", - "\n", - "dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.03s \n", - "\n", - "2025-05-09 10:04:29 (1.37 MB/s) - ‘dataset_graph7.csv.2’ saved [49405/49405]\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "0.8199" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Import some qiskit packages required for setting up our quantum circuits.\n", - "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", - "from qiskit.circuit.library import unitary_overlap\n", - "\n", - "# Import StatevectorSampler as our sampler.\n", - "from qiskit.primitives import StatevectorSampler\n", - "\n", - "# Step 1: Map classical inputs to a quantum problem:\n", - "\n", - "# Start by getting some appropriate data. The data imported below consist of 128 rows or data points.\n", - "# Each row has 14 columns that correspond to data features, and a 15th column with a label (+/-1).\n", - "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", - "\n", - "# Import some required packages, and write a function to pull some training data out of the csv file you got above.\n", - "import pandas as pd\n", - "import numpy as np\n", - "\n", - "\n", - "def get_training_data():\n", - " \"\"\"Read the training data.\"\"\"\n", - " df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n", - " training_data = df.values[:20, :]\n", - " ind = np.argsort(training_data[:, -1])\n", - " X_train = training_data[ind][:, :-1]\n", - "\n", - " return X_train\n", - "\n", - "\n", - "# Prepare training data\n", - "X_train = get_training_data()\n", - "\n", - "# Empty kernel matrix\n", - "num_samples = np.shape(X_train)[0]\n", - "\n", - "# Prepare feature map for computing overlap between two data points.\n", - "# This could be pre-built feature maps like ZZFeatureMap, or a custom quantum circuit, as shown here.\n", - "num_features = np.shape(X_train)[1]\n", - "num_qubits = int(num_features / 2)\n", - "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n", - "fm = QuantumCircuit(num_qubits)\n", - "training_param = Parameter(\"θ\")\n", - "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", - "fm.ry(training_param, fm.qubits)\n", - "for cz in entangler_map:\n", - " fm.cz(cz[0], cz[1])\n", - "for i in range(num_qubits):\n", - " fm.rz(-2 * feature_params[2 * i + 1], i)\n", - " fm.rx(-2 * feature_params[2 * i], i)\n", - "\n", - "# Pick two data points, here 14 and 19, and assign the features to the circuits as parameters.\n", - "x1 = 14\n", - "x2 = 19\n", - "unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n", - "unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", - "\n", - "# Create the overlap circuit\n", - "overlap_circ = unitary_overlap(unitary1, unitary2)\n", - "overlap_circ.measure_all()\n", - "overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")\n", - "\n", - "# Step 2: Optimize problem for quantum execution\n", - "\n", - "# Use Qiskit Runtime service to get the least busy backend for running on real quantum computers.\n", - "# from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# service = QiskitRuntimeService(channel=\"ibm_quantum\")\n", - "# backend = service.least_busy(\n", - "# operational=True, simulator=False, min_num_qubits=overlap_circ.num_qubits\n", - "# )\n", - "\n", - "# Transpile the circuits optimally for the chosen backend using a pass manager.\n", - "# from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "# pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", - "# overlap_ibm = pm.run(overlap_circ)\n", - "\n", - "# Step 3: Execute using Qiskit Runtime Primitives\n", - "\n", - "# Specify the number of shots to use.\n", - "num_shots = 10_000\n", - "\n", - "## Evaluate the problem using statevector-based primitives from Qiskit\n", - "sampler = StatevectorSampler()\n", - "counts = (\n", - " sampler.run([overlap_circ], shots=num_shots).result()[0].data.meas.get_int_counts()\n", - ")\n", - "\n", - "# Step 4: Analyze and post-processing\n", - "\n", - "# Find the probability of 0.\n", - "counts.get(0, 0.0) / num_shots" - ] - }, - { - "cell_type": "markdown", - "id": "453db58e-80b2-41df-8b9c-52dc17be6174", - "metadata": {}, - "source": [ - "Although you don't need to understand all the steps above, we should try to understand the output, so we know why we are doing this. Many processes in machine learning use inner products as part of binary classification (among other things). Quantum mechanics has an obvious connection with this, since the probabilities of measuring various states $|\\phi_i\\rangle$ are given by the inner product with an initial state $|\\psi\\rangle$ through the inner product: $P_i = |\\langle\\phi_i|\\psi\\rangle|^2$. So what we have done above is created a quantum circuit that contains the features of our two data points, and maps them into the space of a quantum vector, then estimates the inner product in that space via making measurements. This is an example of quantum kernel estimation. Note we only implemented this process for two of the data points (the 14th and 19th). If we did this for all possible pairs, we could take the output (in this case the number 0.821...) and populate a matrix of results describing the overlap between all points in the training data set. This is the \"kernel matrix\"." - ] - }, - { - "cell_type": "markdown", - "id": "29a0f015-46a3-4b6d-8afa-c188cc5aab36", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "In the process above, we calculated a kernel matrix entry for the 14th and 19th data points. What value should we obtain if we use the same data point twice, here (like 14th and 14th again)? In other words, what should be the diagonal entries in the kernel matrix? Answer this question in the absence of noise, but note that deviations from your answer are possible in the presence of noise.\n", - "\n", - "\n", - "\n", - "\n", - "The diagonals should be 1.0. This process should be calculating the normalized inner product of a vector with itself, which must always be one.\n", - "\n", - "\n", - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0de576a4-9a1a-46e9-8d6b-e2c5178d6051", + "metadata": { + "gloss": { + "computational-basis-state": { + "text": "Also known as Z-basis states, these are the states we measure when we measure in the Z (or 'computational') basis. These are the states with labels like |00〉 and |00110100〉. IBM® quantum computers always measure in the Z-basis.", + "title": "computational basis state" + }, + "features": { + "text": "A feature is a property of the things we're trying to learn about that we can assign a number to. If we were learning something about cats, the features might be \"height\" or \"age\" or \"propensity to consume treats\".", + "title": "features" + }, + "unitary": { + "text": "A unitary operation is a reversible operation that preserves the norm (that is, makes sure our probabilities always sum to 1). [Read more](https://en.wikipedia.org/wiki/Unitary_matrix).", + "title": "unitary" + } + } + }, + "source": [ + "---\n", + "title: Introduction\n", + "description: An introduction to quantum machine learning that sets expectations, describes the course structure, and gives an initial example.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore webkitallowfullscreen allowfullscreen quantumly Ewin Yunchao Srinivasan Arunachalam Kristan */}\n", + "\n", + "# Introduction to Quantum Machine Learning\n", + "\n", + "## Overview and motivation\n", + "\n", + "Welcome to quantum machine learning!\n", + "\n", + "The video below will give a brief introduction that is supplemented by the text below.\n", + "\n", + "\n", + "\n", + "To briefly recap and augment the video:\n", + "\n", + "- We have seen a problem be solved for the first time on a quantum computer, and then subsequently people find a way to do it on a classical supercomputer. This cycle of classical and quantum computing pushing each other to their limits will likely continue for a few years.\n", + "- There are specific problems where quantum computing can have a provable advantage over classical computing, given progress in areas such as reduction of errors and the number of qubits available. But this is still a time of exploration, searching for quantum-amenable datasets and useful quantum feature maps.\n", + "- Quantum machine learning (QML) is one of many exciting areas where quantum computing can augment or complement existing classical workflows.\n", + "\n", + "Machine learning (ML) applies algorithms to data sets, and so QML might plausibly include quantum mechanics in either the data or algorithmic sides, or both. All of these possibilities are potentially interesting. But we will mostly restrict ourselves to discussions of quantum algorithms applied to classical data. One reason for this is that ML problems with classical data are already so well studied and widely available. There is broad interest in solving problems that start with classical data. Another reason is the lack of QRAM. Without the ability to store large amounts of quantum data on a relatively long timescale, methods that begin with quantum data are still fairly far from applicability to industry. It is also unclear how to \"quantumly-access\" classical data in an efficient manner. Two types of ML of particular interest are supervised learning, in which you train an algorithm using a labeled data set, and unsupervised learning, in which the algorithm attempts to learn about a distribution from unlabeled samples. An unsupervised algorithm might, for example, learn how to generate new samples from the same distribution, or how to cluster the samples into groups with similar characteristics.\n", + "\n", + "![QML_CR_background_Sup_Unsup.avif](/learning/images/courses/quantum-machine-learning/introduction/qml-cr-background-sup-unsup.avif)\n", + "\n", + "The left image shows two categories of labeled data as in supervised learning. In this case, the categories are linearly separable. The right image shows clusters of data. In an unsupervised learning task, these data would not initially be labeled and the algorithm would study the distribution, perhaps looking for clusters. For the purposes of visualizing example clusters the algorithm might identify, the data points have now been labeled. A key difference between the two is that the supervised learning process starts with the data already labeled and the unsupervised process starts with unlabeled data, even if the data are labeled at the end.\n", + "\n", + "Those with background in machine learning will already know that many solution methods involve mapping data into higher-dimensional spaces. This is especially well-explored in the context of kernels. As a brief reminder, sometimes data may be separable into categories by a line, plane, or hyperplane (we will often simply say \"hyperplane\" for compactness), in the same number of dimensions as the data are given. This is shown in the first image above. Other times, data may not be separable by a hyperplane in those dimensions, as shown in the second image. But there can still be structure to the data that can be exploited in a mapping to higher dimensions, which then leaves the data separable in that higher-dimensional space. This is illustrated in the mapping of the 2D data with circular symmetry into the 3D space in which the data points are arranged along a paraboloid surface.\n", + "\n", + "![QML_CR_background_2D-3D.avif](/learning/images/courses/quantum-machine-learning/introduction/qml-cr-background-2d-3d.avif)\n", + "\n", + "A common goal in QML is to find a mapping from the lower-dimensional set of features into a higher-dimensional space, that effectively separates our data points so we can use the mapping to classify new data points.\n", + "But this is not an easy task, and any discussion of the potential usefulness of quantum computing in machine learning must be accompanied by the appropriate caveats. In particular, we must address the nuance in dataset selection and the challenges in reaching utility scale. We must also shift away from trying to outperform classical ML algorithms on data that are already handled efficiently and well by classical algorithms and refocus the discussion to investigating new feature maps that could be useful.\n", + "\n", + "## Managing expectations\n", + "\n", + "Many data sets used in QML applications described in literature are “feature engineered”, meaning a dataset is selected or generated specifically to show a narrow use case in which quantum computing is useful. If this seems like cheating then we’re misunderstanding the task at hand. It is __not__ the case that some quantum feature maps enable us to solve all or many classification tasks more efficiently or scalably than classical machine learning algorithms. Rather, some quantum feature maps (not all) behave differently from classical feature maps. The task at hand is then to explore quantum circuits in the context of complex data structures. Some specific questions to address are:\n", + "1.\tWhat quantum circuits are most likely to behave in novel ways, compared to classical alternatives?\n", + "2.\tAre there real-world problems that involve data with properties best explored using such novel quantum circuits?\n", + "3.\tDo these quantum circuits scale on near-term quantum computers?\n", + "\n", + "### Insufficient explanation\n", + "\n", + "One often encounters a simplified explanation of how quantum computing can be powerful. It goes something like this:\n", + "\n", + "Just as classical computers use bits of information, quantum computers use qubits. Given a number of bits, say 4, a classical computer can take on any one of $2^4 = 16$ possible states, whereas a quantum computer can exist in a superposition of all 16 states simultaneously, and operations can be performed on this entire superposition. In some cases, this naturally allows us to design potentially interesting learning algorithms based on mappings to higher dimensional spaces.\n", + "\n", + "This is a true statement, but it is inadequate, and a bit misleading as we will explain. One also sees the differences between complex and real coefficients emphasized, as in:\n", + "\n", + "A probabilistic classical system in which a system can be described as having certain probabilities of being in different states, can be described as follows.\n", + "$$\n", + "|s\\rangle = a|0000\\rangle+b|0001\\rangle+c|0010\\rangle+... a, b, c \\in \\reals\n", + "$$\n", + "In such a system, the coefficients $a$, $b$, $c$, and so on can only be meaningful if they are positive, real numbers. The states in quantum computers are described by probability amplitudes that can be complex numbers.\n", + "\n", + "$$\n", + "|\\psi \\rangle = A|0000\\rangle+B|0001\\rangle+C|0010\\rangle+... A, B, C \\in \\mathbb{C}\n", + "$$\n", + "\n", + "The above statements have been made very carefully such that they are true (many superficially similar statements are incorrect). But these correct statements are not an explanation of the power of quantum computing in machine learning. For one thing, any application of quantum computing to machine learning will involve measurements and we cannot measure a qubit to be in multiple states at once. We can prepare a qubit in a superposition like $|\\psi\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)$ but a measurement will yield either $|0\\rangle$ or $|1\\rangle$. So at a bare minimum, this story about increasing dimensionality is incomplete. Further, in the context of kernels, increased dimensions in quantum computing cannot be a sufficient condition for computational power over classical alternatives, since Gaussian kernels are infinite dimensional. There are subtleties there, in that Gaussian feature maps are only used in conjunction with the “kernel trick” that sidesteps the need to ever calculate an infinite-dimensional mapped vector. But the point remains:\n", + "\n", + "__High dimensionality of entangled quantum states is not exponential parallelism, and is not a sufficient condition for increased power in machine learning.__\n", + "\n", + "In the lessons that follow, we present workflows for incorporating quantum circuits into machine learning tasks, and we do this for the explicit purpose of facilitating exploration of the power of quantum computing. No feature map or algorithm in this course is put forth as a quick path to better machine learning results for general problems, because no such feature map or algorithm exist. Rather, we present a wide array of quantum tools to be used in exploration of useful quantum computing.\n", + "\n", + "### Dequantization\n", + "\n", + "Dequantization refers to the replacement of a given quantum algorithm with a classical one that performs similarly to a quantum algorithm for a given set of tasks, typically including scaling. By some definitions, the classical algorithm should perform only polynomially slower than the quantum algorithm.\n", + "\n", + "Several quantum machine learning (QML) algorithms that were initially thought to provide significant speedups over classical algorithms have been dequantized in recent years. This process of dequantization has led to important insights into the potential advantages and limitations of quantum approaches to machine learning.\n", + "\n", + "One of the most notable dequantization results came from Ewin Tang's [work on recommendation systems](https://arxiv.org/abs/1807.04271) Tang discovered a classical algorithm that could perform recommendation tasks at speeds previously thought to be achievable only by quantum computers. This discovery challenged the assumption that quantum algorithms had an exponential advantage for this problem. More recent work by [Shin et al.](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.023218) has focused on identifying conditions on the dequantizability of a variational quantum machine learning model's function class.\n", + "\n", + "One common approach to dequantization (though not the only trick) is through consideration of data loading overhead. That is, any quantum algorithm applied to classical data will have a step in which classical data are encoded into the quantum computer. If a quantum algorithm assumes a starting point at which quantum data are already available, then one effectively hides the time required for encoding. There are contexts in which assuming quantum data may be reasonable, but many applications of interest will start with classical data. Some dequantization cases have shown that when this encoding time is included, and when classical data loading can be accomplished efficiently, the quantum algorithm no longer outperforms its classical counterpart.\n", + "\n", + "Even if an algorithm cannot be dequantized, that does not mean it is more efficient or scalable than all classical algorithms. As an extreme, contrived example: imagine an algorithm to select the largest j elements from a set of size k. One could write a quantum algorithm that uses Shor’s algorithm to factor each of the k elements into prime factors, and then determine the largest elements using the prime factors. Such an algorithm likely cannot be dequantized, but is drastically less efficient than classical algorithms to accomplish the same selection of largest elements (though not the unnecessary factoring part).\n", + "\n", + "## Existence proof\n", + "\n", + "In 2021, IBM Quantum® researchers Yunchao Liu, Srinivasan Arunachalam, and Kristan Temme published a paper in Nature, [A rigorous and robust quantum speed-up in supervised machine learning.](https://www.nature.com/articles/s41567-021-01287-z) Consistent with the above caveats, a classification problem was carefully chosen for this work that is (1) known to be classically hard, and (2) suitable for quantum algorithms to show a speed-up.\n", + "\n", + "The paper addresses the classification of data based on discrete logarithms. To quote the paper, “For a large prime number $p$ and a generator $g$ of $\\mathbb{Z}^*_p = {1, 2, . . . , p − 1}$, it is a widely-believed conjecture that no classical algorithm can compute $\\text{log}_g(x)$ on input $x \\in \\mathbb{Z}^*_p $, in time polynomial in $n = \\lceil{\\text{log}_2(p)}\\rceil$, the number of bits needed to represent $p$.” In contrast, [Shor’s algorithm](https://epubs.siam.org/doi/10.1137/S0097539795293172) is known to solve the discrete log problem in polynomial time. This choice of problems thus simultaneously satisfies the criteria above: classical hardness (unlikely to be dequantized), and known to be suitable for quantum algorithms.\n", + "\n", + "Through this judicious choice of classification problem, the authors were able to show an exponential speed-up using quantum kernel methods (sketched briefly below and discussed in later lessons) that is both end-to-end and robust. Here, “end-to end” refers to the assumptions about starting with classical data; the authors in this case do include the time for data encoding. Here, “robust” refers to the fact that the data to be classified are separated by a wide margin using the quantum algorithm, such that the classification success is robust to real-world considerations like finite sampling error.\n", + "\n", + "All this is to say that problems do exist in which quantum kernels can yield an exponential speed-up. But the current state of the science is that such problems are selected based on observations or theoretical justification that they should be amenable to quantum algorithms. It is not realistic to expect a quantum speed-up for machine learning tasks that classical computers already do quite well.\n", + "\n", + "Identifying such ideal cases for the exploration of quantum utility is an enormous responsibility for learners in this course. And it is not a task that can be accomplished in a course such as this. That exploration is a task for the IBM Quantum Network as a whole, made up of researchers like yourself. This course will demonstrate QML workflows and encoding strategies so that you can begin to explore for quantum utility in your area of subject matter expertise.\n", + "\n", + "We hope this introduction has made a few things clear about quantum machine learning:\n", + "1. Quantum algorithms can offer an exponential speed-up over classical algorithms for very specific problems that are classically hard, and well-suited to quantum algorithms.\n", + "2. High dimensionality of entangled states in quantum computing matters, but it is not sufficient to simply gain an advantage over classical algorithms.\n", + "3. Finding problems that are well-suited to quantum algorithms is an extremely difficult task, and one that will largely fall to the learners in this course." + ] + }, + { + "cell_type": "markdown", + "id": "8ff490cb-1b17-4ac7-bd32-9edca1aa051e", + "metadata": {}, + "source": [ + "## Check-in questions\n", + "\n", + "What makes quantum states different from classical states?\n", + "\n", + "\n", + "\n", + "\n", + "A lot. Notably: complex coefficients, and superposition with a single copy. There are many other differences that will be discussed in future lessons, including entanglement and interference.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "True or False? Highly entangled quantum states enable us to solve most machine learning problems more efficiently on a quantum computer.\n", + "\n", + "\n", + "\n", + "\n", + "False. Most machine learning problems are solved very efficiently by classical algorithms and quantum algorithms are not likely to offer any substantial speed-up. The goal in QML is to finding datasets with features that are well-described by quantum states and/or to find mappings of data features that optimize the accuracy of models.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "0158cbee-9b6e-4b74-a449-a4cb6ef27f18", + "metadata": {}, + "source": [ + "## Course learning goals\n", + "\n", + "Through completing this course, you can expect to build the following core skills and competencies. Learners will be able to:\n", + "\n", + "1. Explain what QML is and where quantum connects to classical machine learning.\n", + "\n", + "2. Apply quantum vocabulary and key terms to ML workflows.\n", + "\n", + "3. Identify key components of a QML workflow (various types).\n", + "\n", + "4. Identify different types of QML and distinguish between them.\n", + "\n", + "5. Implement quantum kernel methods and variational quantum classifiers using Qiskit Runtime primitives and following Qiskit patterns.\n", + "\n", + "6. Identify where QML is most promising and where it is not.\n", + "\n", + "7. Adjust an example problem to their own data set.\n", + "\n", + "8. Be aware of issues in QML like training time, noise, and compounding error in multiple-state readouts.\n", + "\n", + "9. Make recommendations for where QML might benefit their organization." + ] + }, + { + "cell_type": "markdown", + "id": "cb9bf2a7-8487-482f-8fcc-95d2210bc314", + "metadata": {}, + "source": [ + "## Course structure\n", + "\n", + "This course is made up of several lessons. Each lesson has several check-in questions throughout the text, so you can practice new skills or check your understanding as you go. These are not required.\n", + "\n", + "At the end of the course, there is a 20-item quiz. You must score at least 70% on this quiz in order to obtain your Quantum Machine Learning badge, via Credly. If you score at least 70%, your badge will be automatically emailed to you, shortly thereafter. You may only submit the quiz twice. After the first submission, you will have the opportunity to take a second try at the questions you missed. After the second submission, your score is final. See the quiz for further details.\n", + "\n", + "The course structure is as follows:\n", + "\n", + "- Lesson 1: Introduction and overview\n", + "- Lesson 2: Recap of machine learning\n", + "- Lesson 3: Data encoding\n", + "- Lesson 4: Quantum kernel methods and support vector machines\n", + "- Lesson 5: Variational quantum classifiers / neural networks\n", + "- Exam for badge" + ] + }, + { + "cell_type": "markdown", + "id": "622774e8-c23c-4610-b204-80390b639852", + "metadata": {}, + "source": [ + "## Run your first QML code\n", + "\n", + "It is often helpful to see where we're going, before breaking it down into pieces, and delving into background. The code cells below carry out a simple instance of a quantum kernel method. Specifically, a single kernel matrix element is calculated. Users new to kernel methods or quantum kernels should not be intimidated by this; multiple lessons in this course will be devoted to dissecting exactly what is being done in these cells.\n", + "\n", + "With this code we simultaneously introduce Qiskit patterns: a framework for approaching quantum computing at the utility scale. This framework consists of four steps that are very general and can be applied to most problems (though in some workstreams, certain steps may be iterated multiple times).\n", + "\n", + "### Qiskit patterns:\n", + "\n", + "* Step 1: Map classical inputs to a quantum problem\n", + "* Step 2: Optimize problem for quantum execution\n", + "* Step 3: Execute using Qiskit Runtime Primitives\n", + "* Step 4: Analyzing / post-processing\n", + "\n", + "In the cells below, we offer only cursory explanations of the various steps, just enough for you to find the appropriate lesson to learn more." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63b85808-7f02-446e-9ef6-38369c95a4bb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--2025-05-09 10:04:28-- https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", + "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.109.133, 185.199.108.133, ...\n", + "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n", + "HTTP request sent, awaiting response... 200 OK\n", + "Length: 49405 (48K) [text/plain]\n", + "Saving to: ‘dataset_graph7.csv.2’\n", + "\n", + "dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.03s \n", + "\n", + "2025-05-09 10:04:29 (1.37 MB/s) - ‘dataset_graph7.csv.2’ saved [49405/49405]\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "0.8199" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Import some qiskit packages required for setting up our quantum circuits.\n", + "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", + "from qiskit.circuit.library import unitary_overlap\n", + "\n", + "# Import StatevectorSampler as our sampler.\n", + "from qiskit.primitives import StatevectorSampler\n", + "\n", + "# Step 1: Map classical inputs to a quantum problem:\n", + "\n", + "# Start by getting some appropriate data. The data imported below consist of 128 rows or data\n", + "# points.\n", + "# Each row has 14 columns that correspond to data features, and a 15th column with a label (+/-1).\n", + "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", + "\n", + "# Import some required packages, and write a function to pull some training data out of the csv file\n", + "# you got above.\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "\n", + "def get_training_data():\n", + " \"\"\"Read the training data.\"\"\"\n", + " df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n", + " training_data = df.values[:20, :]\n", + " ind = np.argsort(training_data[:, -1])\n", + " X_train = training_data[ind][:, :-1]\n", + "\n", + " return X_train\n", + "\n", + "\n", + "# Prepare training data\n", + "X_train = get_training_data()\n", + "\n", + "# Empty kernel matrix\n", + "num_samples = np.shape(X_train)[0]\n", + "\n", + "# Prepare feature map for computing overlap between two data points.\n", + "# This could be pre-built feature maps like ZZFeatureMap, or a custom quantum circuit, as shown\n", + "# here.\n", + "num_features = np.shape(X_train)[1]\n", + "num_qubits = int(num_features / 2)\n", + "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n", + "fm = QuantumCircuit(num_qubits)\n", + "training_param = Parameter(\"θ\")\n", + "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", + "fm.ry(training_param, fm.qubits)\n", + "for cz in entangler_map:\n", + " fm.cz(cz[0], cz[1])\n", + "for i in range(num_qubits):\n", + " fm.rz(-2 * feature_params[2 * i + 1], i)\n", + " fm.rx(-2 * feature_params[2 * i], i)\n", + "\n", + "# Pick two data points, here 14 and 19, and assign the features to the circuits as parameters.\n", + "x1 = 14\n", + "x2 = 19\n", + "unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n", + "unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", + "\n", + "# Create the overlap circuit\n", + "overlap_circ = unitary_overlap(unitary1, unitary2)\n", + "overlap_circ.measure_all()\n", + "overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")\n", + "\n", + "# Step 2: Optimize problem for quantum execution\n", + "\n", + "# Use Qiskit Runtime service to get the least busy backend for running on real quantum computers.\n", + "# from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# service = QiskitRuntimeService(channel=\"ibm_quantum\")\n", + "# backend = service.least_busy(\n", + "# operational=True, simulator=False, min_num_qubits=overlap_circ.num_qubits\n", + "# )\n", + "\n", + "# Transpile the circuits optimally for the chosen backend using a pass manager.\n", + "# from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "# pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", + "# overlap_ibm = pm.run(overlap_circ)\n", + "\n", + "# Step 3: Execute using Qiskit Runtime Primitives\n", + "\n", + "# Specify the number of shots to use.\n", + "num_shots = 10_000\n", + "\n", + "## Evaluate the problem using statevector-based primitives from Qiskit\n", + "sampler = StatevectorSampler()\n", + "counts = (\n", + " sampler.run([overlap_circ], shots=num_shots).result()[0].data.meas.get_int_counts()\n", + ")\n", + "\n", + "# Step 4: Analyze and post-processing\n", + "\n", + "# Find the probability of 0.\n", + "counts.get(0, 0.0) / num_shots" + ] + }, + { + "cell_type": "markdown", + "id": "453db58e-80b2-41df-8b9c-52dc17be6174", + "metadata": {}, + "source": [ + "Although you don't need to understand all the steps above, we should try to understand the output, so we know why we are doing this. Many processes in machine learning use inner products as part of binary classification (among other things). Quantum mechanics has an obvious connection with this, since the probabilities of measuring various states $|\\phi_i\\rangle$ are given by the inner product with an initial state $|\\psi\\rangle$ through the inner product: $P_i = |\\langle\\phi_i|\\psi\\rangle|^2$. So what we have done above is created a quantum circuit that contains the features of our two data points, and maps them into the space of a quantum vector, then estimates the inner product in that space via making measurements. This is an example of quantum kernel estimation. Note we only implemented this process for two of the data points (the 14th and 19th). If we did this for all possible pairs, we could take the output (in this case the number 0.821...) and populate a matrix of results describing the overlap between all points in the training data set. This is the \"kernel matrix\"." + ] + }, + { + "cell_type": "markdown", + "id": "29a0f015-46a3-4b6d-8afa-c188cc5aab36", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "In the process above, we calculated a kernel matrix entry for the 14th and 19th data points. What value should we obtain if we use the same data point twice, here (like 14th and 14th again)? In other words, what should be the diagonal entries in the kernel matrix? Answer this question in the absence of noise, but note that deviations from your answer are possible in the presence of noise.\n", + "\n", + "\n", + "\n", + "\n", + "The diagonals should be 1.0. This process should be calculating the normalized inner product of a vector with itself, which must always be one.\n", + "\n", + "\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb b/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb index 6b9360c6e30..c7bca8e931a 100644 --- a/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb +++ b/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb @@ -1,1290 +1,1297 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "33c0bc6b-0506-4e17-9823-07a644d3093d", - "metadata": {}, - "source": [ - "---\n", - "title: Quantum kernel methods\n", - "description: Quantum kernels are used initially to determine a kernel matrix element, a full kernel matrix and the interface with classical kernel tools is presented.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore workstreams workstream xvals yvals xval Arunachalam scikit */}\n", - "\n", - "# Quantum Kernels\n", - "\n", - "## Introduction to quantum kernels\n", - "\n", - "The \"Quantum kernel method\" refers to any method that uses quantum computers to estimate a kernel. In this context, \"kernel\" will refer to the kernel matrix or individual entries therein. Recall that a feature mapping $\\Phi(\\vec{x})$ is a mapping from $\\vec{x}\\in \\mathbb{R}^d$ to $\\Phi(\\vec{x})\\in \\mathbb{R}^{d'},$ where usually $d'>d$ and where the goal of this mapping is to make the categories of data separable by a hyperplane. The kernel function takes vectors in the feature-mapped space as arguments and returns their inner product, that is, $K:\\mathbb{R}^d\\times\\mathbb{R}^d\\rightarrow \\mathbb{R}$ with $K(x,y) = \\langle \\Phi(x)|\\Phi(y)\\rangle$. Classically, we are interested in feature maps for which the kernel function is easy to evaluate. This often means finding a kernel function for which the inner product in the feature-mapped space can be written in terms of the original data vectors, without having to ever construct $\\Phi(x)$ and $\\Phi(y)$. In the method of quantum kernels, the feature mapping is done by a quantum circuit, and the kernel is estimated using measurements on that circuit and the relative measurement probabilities.\n", - "\n", - "In this lesson we will examine the depths of pre-coded encoding circuits that use substantial entanglement and compare those to depths of circuits we code by hand. This is not to advocate for one method over another. You may find that pre-coded circuits are too deep, and that the entanglement in the custom-built circuit is insufficient to be useful. Again, these are shown only to enable your exploration.\n", - "\n", - "Before walking through a kernel matrix estimation in detail, let us outline the workflow using the language of Qiskit patterns.\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "* Input: Training dataset\n", - "* Output: Abstract circuit for calculating a kernel matrix entry\n", - "\n", - "Given the dataset, the starting point is to encode the data into a quantum circuit. In other words, we need to map our data into the Hilbert space of states of our quantum computer. We do this by constructing a data-dependent circuit. There are many ways of doing this, and the previous lesson outlined a number of options. You can construct your own circuit to encode your data, or you can use a pre-made feature map like `zz_feature_map`. In this lesson, we will do both.\n", - "\n", - "Note that in order to calculate a single kernel matrix element, we will want to encode two different points, so we can estimate their inner product. A full quantum kernel workflow will of course, involve many such inner products between mapped data vectors, as well as classical machine learning methods. But the core step being iterated is the estimation of a single kernel matrix element. For this we select a data-dependent quantum circuit and map two data vectors into the feature space.\n", - "\n", - "![Classical_Review_background_kernel_circuit](/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/classical-review-background-kernel-circuit.avif)\n", - "\n", - "For the task of generating a kernel matrix, we are particularly interested in the probability of measuring the $|0\\rangle^{\\otimes N}$ state, in which all $N$ qubits are in the $|0\\rangle$ state. To see this, consider that the circuit responsible for encoding and mapping of one data vector $\\vec{x}_i$ can be written as $\\Phi(\\vec{x}_i)$, and the one responsible for encoding and mapping $\\vec{x}_j$ is $\\Phi(\\vec{x}_j)$, and denote the mapped states\n", - "$$\n", - "|\\psi(\\vec{x}_i)\\rangle = \\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}\n", - "$$\n", - "$$\n", - "|\\psi(\\vec{x}_j)\\rangle = \\Phi(\\vec{x}_j)|0\\rangle^{\\otimes N}.\n", - "$$\n", - "\n", - "These states _are_ the mapping of the data to higher dimensions, so our desired kernel entry is the inner product\n", - "$$\n", - "\\langle\\psi(\\vec{x}_j)|\\psi(\\vec{x}_i)\\rangle = \\langle 0 |^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}.\n", - "$$\n", - "If we operate on the default initial state $|0\\rangle^{\\otimes N}$ with both circuits $\\Phi^\\dagger(\\vec{x}_j)$ and $\\Phi(\\vec{x}_i)$, the probability of then measuring the state $|0\\rangle^{\\otimes N}$ is\n", - "$$\n", - "P_0 = |\\langle0|^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}|^2.\n", - "$$\n", - "This is exactly the value we want (up to $||^2$). The measurement layer of our circuit will return measurement probabilities (or so-called \"quasi-probabilities\", if certain error mitigation methods are used). The probability of interest is that of the zero state, $|0\\rangle^{\\otimes N}$.\n", - "\n", - "\n", - "### Step 2: Optimize problem for quantum execution\n", - "\n", - "* Input: Abstract circuit, not optimized for a particular backend\n", - "* Output: Target circuit and observable, optimized for the selected QPU\n", - "\n", - "In this step, we will use the `generate_preset_pass_manager` function from Qiskit to specify an optimization routine for our circuit with respect to the real quantum computer on which we plan to run the experiment. We set `optimization_level=3` , which means we will use the preset pass manager which provides the highest level of optimization. In this context, \"optimization\" refers to optimizing the implementation of the circuit on a real quantum computer. This includes considerations like selecting physical qubits to correspond to qubits in the abstract quantum circuit that will minimize gate depth, or selecting physical qubits with the lowest available error rates. This is not directly related to optimization of the machine learning problem (as in classical optimizers like COBYLA).\n", - "\n", - "Depending on how you implement step 2, you may have to optimize the circuit more than once, since each pair of points involved in a matrix element produce a different circuit to be measured.\n", - "\n", - "### Step 3: Execute using Qiskit Runtime Primitives\n", - "\n", - "* Input: Target circuit\n", - "* Output: Probability distribution\n", - "\n", - "Use the `Sampler` primitive from Qiskit Runtime to reconstruct a probability distribution of states yielded from sampling the circuit. Note that you may see this referred to as a \"quasi-probability distribution\", a term which is applicable where noise is an issue and when extra steps are introduced, such as in error mitigation. In such cases, the sum of all probabilities may not exactly equal 1; hence \"quasi-probability\".\n", - "\n", - "### Step 4: Post-process, return result in classical format\n", - "\n", - "* Input: Probability distribution\n", - "* Output: A single kernel matrix element, or a kernel matrix if repeating\n", - "\n", - "Calculate the probability of measuring $|0\\rangle^{\\otimes N}$ on the quantum circuit, and populate the kernel matrix in the position corresponding to the two data vectors used. To fill out the entire kernel matrix, we need to run a quantum experiment for each entry. Once we have a kernel matrix, we can use it in many classical machine learning algorithms that accept `pre-calculated kernels`. For example: `qml_svc = SVC(kernel=\"precomputed\")`. We can then use classical workstreams to apply our model on our testing data, and get an accuracy score. Depending on our satisfaction with our accuracy score, we may need to revisit aspects of our calculation, such as our feature map.\n", - "\n", - "### Lesson outline\n", - "\n", - "In this lesson we will carry out these steps several ways to make optimal use of your time on real quantum computers. We will apply a quantum kernel method to\n", - "* A single kernel matrix entry for data with relatively few features, using a real backend, so that we can easily follow what is happening at each step.\n", - "* An entire data set with relatively few features, using a simulated backend, so that we can see how the quantum workstream connects with classical machine learning methods\n", - "* A single kernel matrix entry for data with many features, using a real quantum computer. We will not estimate an entire kernel matrix for a large dataset, in order to respect time on IBM® quantum computers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2fedaf0c-efb4-4425-8e31-89ca0319375d", - "metadata": {}, - "outputs": [], - "source": [ - "# If you have not already, install scikit learn\n", - "#!pip install scikit-learn" - ] - }, - { - "cell_type": "markdown", - "id": "f05e564c-ae91-4365-88fb-a57183c525aa", - "metadata": {}, - "source": [ - "## Single kernel matrix entry\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Let us first consider a data set with just a few features, say 10. The data set could be as large as you like, since we are calculating the kernel matrix elements one at a time. We need at least two points, so we will start with that (in the next example, we will import a full dataset). Let's import a few needed packages:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "61f15d16-993a-4405-9454-685829cb08b4", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# Two mock data points, including category labels, as in training\n", - "small_data = [\n", - " [-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],\n", - " [-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],\n", - "]\n", - "\n", - "# Data points with labels removed, for inner product\n", - "train_data = [small_data[0][:-1], small_data[1][:-1]]" - ] - }, - { - "cell_type": "markdown", - "id": "e9acb5ee-8c7a-467d-a7e9-e6219f4d443b", - "metadata": {}, - "source": [ - "We can try using the `z_feature_map`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d8e424e-751a-4057-a879-cc39d83af953", - "metadata": {}, - "outputs": [], - "source": [ - "# from qiskit.circuit.library import zz_feature_map\n", - "# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)\n", - "\n", - "from qiskit.circuit.library import z_feature_map\n", - "\n", - "fm = z_feature_map(feature_dimension=np.shape(train_data)[1])\n", - "\n", - "\n", - "unitary1 = fm.assign_parameters(train_data[0])\n", - "unitary2 = fm.assign_parameters(train_data[1])" - ] - }, - { - "cell_type": "markdown", - "id": "2b41da49-ce41-45eb-83ff-cfcbaa8c2c92", - "metadata": {}, - "source": [ - "The two unitaries above exactly correspond to $U_1$ and $U_2$ described in the introduction. We can combine them using `unitary_overlap`. As always, we want to keep an eye on our circuit depth." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4273dcf7-784c-4edb-a9cd-9b249d24dbb8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "circuit depth = 9\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import unitary_overlap\n", - "\n", - "\n", - "overlap_circ = unitary_overlap(unitary1, unitary2)\n", - "overlap_circ.measure_all()\n", - "\n", - "print(\"circuit depth = \", overlap_circ.decompose().depth())\n", - "overlap_circ.decompose().draw(\"mpl\", scale=0.6, style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "0a9f9df0-d543-496a-99fd-8b66a53941a7", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum execution\n", - "\n", - "We start by selecting the least busy backend, then optimize our circuit for running on that backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27f62452-2534-4f97-a738-52e4a19a7772", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "# Import needed packages\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Get the least busy backend\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", - ")\n", - "print(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "36cfd4e9-55c9-46c4-8afe-bd6cd0a40f45", - "metadata": {}, - "outputs": [], - "source": [ - "# Apply level 3 optimization to our overlap circuit\n", - "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", - "overlap_ibm = pm.run(overlap_circ)" - ] - }, - { - "cell_type": "markdown", - "id": "13def807-7e1a-44c8-a7d8-2db5ce0f75ec", - "metadata": {}, - "source": [ - "For complicated circuits, this step will substantially increase the circuit depth as it maps to native gates for real quantum computers, and information may need to be moved from qubit to qubit. In this simple case, the depth is hardly affected at all." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "79348ff5-4d6d-46d7-ae2b-e6ffbb22dd25", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "circuit depth = 10\n" - ] - }, - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(\"circuit depth = \", overlap_ibm.decompose().depth())\n", - "overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)" - ] - }, - { - "cell_type": "markdown", - "id": "ec97b263-4037-4ed9-9588-110335d2c51d", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit Runtime Primitives\n", - "\n", - "The syntax for running on a simulator is commented out below. For this dataset, with a small number of features, running on a simulator is still an option. For utility-scale calculations, simulation is not typically feasible. Simulators should only be used to debug scaled-down code." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "36a81e65-d14f-4dc2-b64c-4acbd5a185cd", - "metadata": {}, - "outputs": [], - "source": [ - "# Run this for a simulator\n", - "# from qiskit.primitives import StatevectorSampler\n", - "\n", - "# from qiskit_ibm_runtime import Options, Session, Sampler\n", - "\n", - "# num_shots = 10000\n", - "\n", - "# Evaluate the problem using state vector-based primitives from Qiskit\n", - "# sampler = StatevectorSampler()\n", - "# results = sampler.run([overlap_circ], shots=num_shots).result()\n", - "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", - "# counts_bit = results[0].data.meas.get_counts()\n", - "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.\n", - "# counts = results[0].data.meas.get_int_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ad869b2-ad50-4a82-bc4c-70fd0ab802c5", - "metadata": {}, - "outputs": [], - "source": [ - "# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.\n", - "\n", - "# Import our runtime primitive\n", - "from qiskit_ibm_runtime import Session, SamplerV2 as Sampler\n", - "\n", - "num_shots = 10000\n", - "\n", - "# Use sampler and get the counts\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "results = sampler.run([overlap_ibm], shots=num_shots).result()\n", - "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", - "counts_bit = results[0].data.meas.get_counts()\n", - "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.\n", - "counts = results[0].data.meas.get_int_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "16d66085-047a-4718-80fe-eb64c20bd398", - "metadata": {}, - "source": [ - "### Step 4: Post-process, return result in classical format\n", - "\n", - "As described in the introduction, the most useful measurement here is the probability of measuring the zero state $|00000\\rangle$." - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "id": "33522847-21dc-48b8-9270-ce11fd1ec529", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.6525" - ] - }, - "execution_count": 62, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "counts.get(0, 0.0) / num_shots" - ] - }, - { - "cell_type": "markdown", - "id": "65dfa3e4-bfe4-45d7-815f-e8c1aa24d916", - "metadata": {}, - "source": [ - "This is the outcome we wanted: an estimate of the inner product (up to mod squared) of the vectors corresponding to two data points. If we want to look at the full distribution of measurement probabilities (or quasiprobabilities), we can do so using the ```plot_distribution``` function as shown below. One sees that for a large number of qubits, pictures like this quickly become intractable." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "29aaf5d6-ea7e-4373-b33c-9299699a3964", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_distribution\n", - "\n", - "plot_distribution(counts_bit)" - ] - }, - { - "cell_type": "markdown", - "id": "d0a52344-34f2-498b-8f46-489d581bd8ba", - "metadata": {}, - "source": [ - "Alternatively, one might define a visualization like the one below to look only at the top 10 most probable measurements. This could be important for troubleshooting or trying to glean more intuition for the data. But the measurement probability of the zero state is our kernel matrix element." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "c141e800-79eb-400a-9b8c-75e2393ce24e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def visualize_counts(probs, num_qubits):\n", - " \"\"\"Visualize the outputs from the Qiskit Sampler primitive.\"\"\"\n", - " zero_prob = probs.get(0, 0.0)\n", - " top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])\n", - " top_10.update({0: zero_prob})\n", - " by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))\n", - " xvals, yvals = list(zip(*by_key.items()))\n", - " xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]\n", - " plt.bar(xvals, yvals)\n", - " plt.xticks(rotation=75)\n", - " plt.title(\"Results of sampling\")\n", - " plt.xlabel(\"Measured bitstring\")\n", - " plt.ylabel(\"Counts\")\n", - " plt.show()\n", - "\n", - "\n", - "visualize_counts(counts, overlap_circ.num_qubits)" - ] - }, - { - "cell_type": "markdown", - "id": "47e85d94-ee94-4ed9-8223-ccd5fc9442d4", - "metadata": {}, - "source": [ - "From this information about only one inner product between two data points in the higher dimensional feature space, all we can say is that their overlap is fairly large compared to the maximum overlap (which would be 1.0). This could be an indicator that these two data points are somehow similar in nature and will be categorized in the same class classes. Or it could be an indicator that our feature map is not effective at mapping into a space where like data has a strong overlap and unlike data has a small overlap. In order to know which is true, we must apply our feature map to the entire set of data and see if the resulting kernel matrix can be manipulated to effectively separate classes with high accuracy.\n", - "\n", - "It is worth noting that we used the `z_feature_map` which resulted in low two-qubit transpiled depth (depth 1, in fact). If your circuits become too deep, it is sure to result in a lot of noise, and this will make the probability of measuring the zero state very low, even if your feature map is well-matched to your data. For example, a repetition of the above process using ```zz_feature_map``` and ```, entanglement='linear', reps=1``` yielded ```dist.get(0,0.0) = 0.0015``` using the same data points. This is due to the much greater circuit depths and two-qubit depths from ```zz_feature_map```. The figure below shows the probability distribution for that calculation.\n", - "\n", - "![Bad results from a zz feature map.](/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/zzfeaturemap-bad-results.avif)\n", - "\n", - "It is worth playing around with a few data points from the same category to see how low your depth must be to obtain good results. The following is rough advice that is sure to have exceptions. Generally, a two-qubit, transpiled depth of 10 or fewer should be no problem. A two-qubit, transpiled depth of 50-60 is state-of-the-art and will require advanced error mitigation among other tools. In between, your results may vary with data similarity, feature map expressivity, circuit width, and other factors." - ] - }, - { - "cell_type": "markdown", - "id": "33eda2a4-cd97-4bba-8acd-4017bfe2cd12", - "metadata": {}, - "source": [ - "Ordinarily the post-processing step would also include classical machine learning processes. In the next section we will extend this process to an entire dataset, and show the classical machine learning workflow." - ] - }, - { - "cell_type": "markdown", - "id": "41850d1d-3800-4aa0-aa01-fe6e72c872be", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "In a 10-qubit quantum circuit, generally, how many different states are there that could possibly be measured?\n", - "\n", - "\n", - "\n", - "\n", - "$2^{10}$ or 1024.\n", - "\n", - "\n", - "\n", - "\n", - "Let us suppose that someone new to quantum computing attempts to use a quantum circuit that has very high two-qubit depth, and they do not use error mitigation. Let us further suppose that this results in an error rate of 10% on each qubit. If the true (error-free) kernel matrix element corresponding to this circuit is very large, say 1.0, what would be the probability of measuring all 10 qubits to be in the state with every qubit $|0\\rangle$?\n", - "\n", - "\n", - "\n", - "\n", - "The probability of each qubit being correctly found in the |0> state is 0.90. The probability for all 10 qubits to be found in the correct state is $0.90^{10}$ or about 35%.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Explain in your own words why it is so important to monitor circuit depths. This is true generally, but explain it in the context of quantum kernel estimation.\n", - "\n", - "\n", - "\n", - "\n", - "In this QKE workflow, our estimates are based on the measurements of the zero state, meaning the state in which every qubit is found in the $|0\\rangle$ state. Very deep circuits will introduce high error rates. When that error rate is compounded over many qubits, this will reduce the probability of measuring the zero state, substantially.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "0fde5aca-0d54-40b1-82d4-9d1b2a1b67bb", - "metadata": {}, - "source": [ - "## Full kernel matrix\n", - "\n", - "In this section, we will extend the above process to the binary classification of a full dataset. This will introduce two important components: (1) we can now implement classical machine learning in post-processing, and (2) we can obtain accuracy scores for our training.\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Now we will import an existing dataset for our classification. This dataset consists of 128 rows (data points) and 14 features on each point. There is a 15th element that indicates the binary category of each point ($\\pm 1$). The dataset is imported below, or you can access the dataset and view its structure [here](https://github.com/qiskit-community/prototype-quantum-kernel-training/blob/main/data/dataset_graph7.csv).\n", - "\n", - "We will use the first 90 data points for training, and the next 30 points for testing." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "60f2699d-ef47-44f7-931a-4a70d4363c0e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--2024-07-11 23:05:22-- https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", - "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...\n", - "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n", - "HTTP request sent, awaiting response... 200 OK\n", - "Length: 49405 (48K) [text/plain]\n", - "Saving to: ‘dataset_graph7.csv.15’\n", - "\n", - "dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s \n", - "\n", - "2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]\n", - "\n" - ] - } - ], - "source": [ - "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", - "\n", - "df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n", - "\n", - "# Prepare training data\n", - "\n", - "train_size = 90\n", - "X_train = df.values[0:train_size, :-1]\n", - "train_labels = df.values[0:train_size, -1]\n", - "\n", - "# Prepare testing data\n", - "test_size = 30\n", - "X_test = df.values[train_size : train_size + test_size, :-1]\n", - "test_labels = df.values[train_size : train_size + test_size, -1]" - ] - }, - { - "cell_type": "markdown", - "id": "4a665fcb-bd27-4713-9c0a-b7747de5fb45", - "metadata": {}, - "source": [ - "We will already prepare for storing multiple outputs by constructing a kernel matrix and a test matrix of appropriate dimensions." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "34d92041-0501-4c20-b780-ae5352510637", - "metadata": {}, - "outputs": [], - "source": [ - "# Empty kernel matrix\n", - "num_samples = np.shape(X_train)[0]\n", - "kernel_matrix = np.full((num_samples, num_samples), np.nan)\n", - "test_matrix = np.full((test_size, num_samples), np.nan)" - ] - }, - { - "cell_type": "markdown", - "id": "920233dd-cfac-498d-be88-746719083cc2", - "metadata": {}, - "source": [ - "Now we create a feature map for encoding and mapping our classical data in a quantum circuit. We are free to construct our own feature map or use a pre-fabricated one. Feel free to modify the feature map below, or switch back to ZFeatureMap. But always pay attention to circuit depth. Recall that in the previous 6-qubit example the transpiled circuit depth was intractably high when using `zz_feature_map`. As the scale and complexity of the circuit increase, the depth could rapidly increase to a point where noise overwhelms our results. Whenever you know something about your data structure that may inform what feature map structure would be most useful, it is advisable to create your own custom feature map that leverages that knowledge." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "ca813500-5856-4b50-a213-a7e87d825c18", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", - "\n", - "# Prepare feature map for computing overlap\n", - "num_features = np.shape(X_train)[1]\n", - "num_qubits = int(num_features / 2)\n", - "\n", - "# To use a custom feature map use the lines below.\n", - "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n", - "\n", - "fm = QuantumCircuit(num_qubits)\n", - "training_param = Parameter(\"θ\")\n", - "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", - "fm.ry(training_param, fm.qubits)\n", - "for cz in entangler_map:\n", - " fm.cz(cz[0], cz[1])\n", - "for i in range(num_qubits):\n", - " fm.rz(-2 * feature_params[2 * i + 1], i)\n", - " fm.rx(-2 * feature_params[2 * i], i)" - ] - }, - { - "cell_type": "markdown", - "id": "3c6416b5-9eb1-40a4-b72c-8912df931139", - "metadata": {}, - "source": [ - "### Steps 2 and 3: Optimize problem and execute using primitives\n", - "\n", - "We will construct an overlap circuit, and if we were running on a real quantum computer in this example, we would optimize it for execution as before. But in this case, we intend to step over all data points and calculate the full kernel matrix. For each pair of data vectors $\\vec{x}_i$ and $\\vec{x}_j$, we create a different overlap circuit. Thus we must optimize our circuit for each data point pair. So steps 2 and 3 would be done together in the multiple iterations.\n", - "\n", - "The code cell below does exactly the same process as before for a single data point pair. This time it is simply executed inside two `for` loops, and there is the additional line at the end `kernel_matrix[x_1,x_2] = ...` to store the results of each calculation. Note that we have leveraged the symmetry of a kernel matrix to reduce the number of calculations by 1/2. We have also simply set the diagonal elements to 1, as they should be in the absence of noise. Depending on your implementation and required precision, you could also use the diagonal elements to estimate noise or learn about it for error mitigation purposes.\n", - "\n", - "Once the kernel matrix has been fully populated, we repeat the process for the test data and populate the test_matrix. This is really also a kernel matrix; we simply give it a different name to distinguish the two." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7995748f-fc97-4eda-bed6-8b189c1e65ea", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "training done\n", - "test matrix done\n" - ] - } - ], - "source": [ - "# To use a simulator\n", - "from qiskit.primitives import StatevectorSampler\n", - "\n", - "# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers\n", - "# service = QiskitRuntimeService()\n", - "# backend = service.least_busy(\n", - "# operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", - "# )\n", - "\n", - "num_shots = 10000\n", - "\n", - "# Evaluate the problem using state vector-based primitives from Qiskit.\n", - "sampler = StatevectorSampler()\n", - "\n", - "for x1 in range(0, train_size):\n", - " for x2 in range(x1 + 1, train_size):\n", - " unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n", - " unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", - "\n", - " # Create the overlap circuit\n", - " overlap_circ = unitary_overlap(unitary1, unitary2)\n", - " overlap_circ.measure_all()\n", - "\n", - " # These lines run the qiskit sampler primitive.\n", - " counts = (\n", - " sampler.run([overlap_circ], shots=num_shots)\n", - " .result()[0]\n", - " .data.meas.get_int_counts()\n", - " )\n", - "\n", - " # Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)\n", - " kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n", - " kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots\n", - " # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)\n", - " kernel_matrix[x1, x1] = 1\n", - "\n", - "print(\"training done\")\n", - "\n", - "# Similar process to above, but for testing data.\n", - "for x1 in range(0, test_size):\n", - " for x2 in range(0, train_size):\n", - " unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])\n", - " unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", - "\n", - " # Create the overlap circuit\n", - " overlap_circ = unitary_overlap(unitary1, unitary2)\n", - " overlap_circ.measure_all()\n", - "\n", - " counts = (\n", - " sampler.run([overlap_circ], shots=num_shots)\n", - " .result()[0]\n", - " .data.meas.get_int_counts()\n", - " )\n", - "\n", - " test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n", - "\n", - "print(\"test matrix done\")" - ] - }, - { - "cell_type": "markdown", - "id": "ea65cd41-4ba3-423c-a98f-2f8e3751adf4", - "metadata": {}, - "source": [ - "### Step 4: Post-process, return result in classical format\n", - "\n", - "Now that we have a kernel matrix and a similarly formatted test_matrix from quantum kernel methods, we can apply classical machine learning algorithms to make predictions about our test data and check its accuracy. We will start by importing Scikit-Learn's `sklearn.svc`, a support vector classifier (SVC). We must specify that we want the SVC to use our precomputed kernel using `kernel = precomputed`." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "78d9e291-54c3-4c45-827e-066c6d012da5", - "metadata": {}, - "outputs": [], - "source": [ - "# import a support vector classifier from a classical ML package.\n", - "from sklearn.svm import SVC\n", - "\n", - "# Specify that you want to use a pre-computed kernel matrix\n", - "qml_svc = SVC(kernel=\"precomputed\")" - ] - }, - { - "cell_type": "markdown", - "id": "9bd69e5f-dafd-492c-bd5d-f5e16925f611", - "metadata": {}, - "source": [ - "Using `SVC.fit`, we can now feed in the kernel matrix and the training labels to obtain a fit. `SVC.score` will then score our test data against that fit using our test_matrix, and return our accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "d8708973-c97c-494c-af9c-e4a0fe430452", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Precomputed kernel classification test score: 1.0\n" - ] - } - ], - "source": [ - "# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.\n", - "qml_svc.fit(kernel_matrix, train_labels)\n", - "\n", - "# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.\n", - "qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)\n", - "print(f\"Precomputed kernel classification test score: {qml_score_precomputed_kernel}\")" - ] - }, - { - "cell_type": "markdown", - "id": "0200dceb-6d61-4e16-8c3d-d08dbc5529e5", - "metadata": {}, - "source": [ - "We see that the accuracy of our trained model was 100%. This is great, and it shows that QKE can work. But that is very different from quantum advantage. Classical kernels would likely have been able to solve this classification problem with 100% accuracy as well. There is much work to be done characterizing different data types and data relationships to see where quantum kernels will be most useful in the current utility era.\n", - "We leave it to the learner to modify parts of this workflow and study the effectiveness of various quantum feature maps. Here are a few things to consider:\n", - "* How robust is the accuracy? Does it hold for broad types of data or just this specific training data?\n", - "* What structure in your data makes you suspect that a quantum feature map is useful?\n", - "* How is the accuracy affected by increasing/decreasing the amount of training data?\n", - "* What feature maps can you use and how do the results vary with feature maps?\n", - "* How are the accuracy and running time affected by increasing the number of features?\n", - "* Which trends, if any, do you expect to hold on real quantum computers?" - ] - }, - { - "cell_type": "markdown", - "id": "da2427c4-a1a1-4284-b10f-95d5c5230755", - "metadata": {}, - "source": [ - "## Scaling to more features and qubits\n", - "\n", - "In this section, we will repeat the calculation of a single matrix element, but for a much larger number of features, sketching the path to scale toward utility. The restriction to a single matrix element is done so that the process can be shown without using up too much of your allotted time on quantum computers.\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "We will assume a starting point of a dataset in which each data point has 42 features. As in the first example, we will calculate a single kernel matrix element, requiring two data points. The two points below have 42 features and a single category variable ($\\pm 1$)." - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "014ba70a-5202-4c86-b886-7ff27ae9a1b3", - "metadata": {}, - "outputs": [], - "source": [ - "# Two mock data points, including category labels, as in training\n", - "\n", - "large_data = [\n", - " [\n", - " -0.028,\n", - " -1.49,\n", - " -1.698,\n", - " 0.107,\n", - " -1.536,\n", - " -1.538,\n", - " -1.356,\n", - " -1.514,\n", - " -0.109,\n", - " -1.8,\n", - " -0.122,\n", - " -1.651,\n", - " -1.955,\n", - " -0.123,\n", - " -1.732,\n", - " 0.091,\n", - " -0.048,\n", - " -0.128,\n", - " -0.026,\n", - " 0.082,\n", - " -1.263,\n", - " 0.065,\n", - " 0.004,\n", - " -0.055,\n", - " -0.08,\n", - " -0.173,\n", - " -1.734,\n", - " -0.39,\n", - " -1.451,\n", - " 0.078,\n", - " -1.578,\n", - " -0.025,\n", - " -0.184,\n", - " -0.119,\n", - " -1.336,\n", - " 0.055,\n", - " -0.204,\n", - " -1.578,\n", - " 0.132,\n", - " -0.121,\n", - " -1.599,\n", - " -0.187,\n", - " -1,\n", - " ],\n", - " [\n", - " -1.414,\n", - " -1.439,\n", - " -1.606,\n", - " 0.246,\n", - " -1.673,\n", - " 0.002,\n", - " -1.317,\n", - " -1.262,\n", - " -0.178,\n", - " -1.814,\n", - " 0.013,\n", - " -1.619,\n", - " -1.86,\n", - " -0.25,\n", - " -0.212,\n", - " -0.214,\n", - " -0.033,\n", - " 0.071,\n", - " -0.11,\n", - " -1.607,\n", - " 0.441,\n", - " -0.143,\n", - " -0.009,\n", - " -1.655,\n", - " -1.579,\n", - " 0.381,\n", - " -1.86,\n", - " -0.079,\n", - " -0.088,\n", - " -0.058,\n", - " -1.481,\n", - " -0.064,\n", - " -0.065,\n", - " -1.507,\n", - " 0.177,\n", - " -0.131,\n", - " -0.153,\n", - " 0.07,\n", - " -1.627,\n", - " 0.593,\n", - " -1.547,\n", - " -0.16,\n", - " -1,\n", - " ],\n", - "]\n", - "train_data = [large_data[0][:-1], large_data[1][:-1]]" - ] - }, - { - "cell_type": "markdown", - "id": "3e4dd6a7-e51c-4dc2-99ea-66e2746e411d", - "metadata": {}, - "source": [ - "Recall that the `zz_feature_map` produced rather deep circuits in the case of relatively few features (14 features). As we increase the number of features, we need to closely monitor circuit depth. To illustrate this, we will first try using the `zz_feature_map` and check the depth of the resulting circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bdb1ecc4-7827-4eb7-bf7b-5a09838775ca", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import zz_feature_map\n", - "\n", - "fm = zz_feature_map(\n", - " feature_dimension=np.shape(train_data)[1], entanglement=\"linear\", reps=1\n", - ")\n", - "\n", - "unitary1 = fm.assign_parameters(train_data[0])\n", - "unitary2 = fm.assign_parameters(train_data[1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5bc865cb-8f8b-4812-9a05-1bd34dbcbcb9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "circuit depth = 251\n", - "two-qubit depth 165\n" - ] - } - ], - "source": [ - "from qiskit.circuit.library import unitary_overlap\n", - "\n", - "\n", - "overlap_circ = unitary_overlap(unitary1, unitary2)\n", - "overlap_circ.measure_all()\n", - "\n", - "print(\"circuit depth = \", overlap_circ.decompose(reps=2).depth())\n", - "print(\n", - " \"two-qubit depth\",\n", - " overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),\n", - ")\n", - "# overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "b3348ab6-ef4e-40d2-b803-e86484e0b8f3", - "metadata": {}, - "source": [ - "As described before, determining exactly how deep is too deep is nuanced. But a two-qubit depth of more than 100, even before transpilation is a non-starter. This is why custom feature maps have been emphasized throughout this lesson. If you know something about the structure of your entire dataset, you should design an entanglement map with that structure in mind. Here, since we are only calculating the inner product between two such data points, we have prioritized low circuit depth over any detailed consideration of data structure." - ] - }, - { - "cell_type": "code", - "execution_count": 95, - "id": "784f8a68-0d77-433b-8482-bac38ddb8adc", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", - "\n", - "# Prepare feature map for computing overlap\n", - "\n", - "entangler_map = [\n", - " [3, 4],\n", - " [2, 5],\n", - " [1, 4],\n", - " [2, 3],\n", - " [4, 6],\n", - " [7, 9],\n", - " [10, 11],\n", - " [9, 12],\n", - " [8, 11],\n", - " [9, 10],\n", - " [11, 13],\n", - " [14, 16],\n", - " [17, 18],\n", - " [16, 19],\n", - " [15, 18],\n", - " [16, 17],\n", - " [18, 20],\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 96, - "id": "587daff1-7c5b-4505-8064-827c1fbdc742", - "metadata": {}, - "outputs": [], - "source": [ - "# Use the entangler map above to build a feature map\n", - "\n", - "num_features = np.shape(train_data)[1]\n", - "num_qubits = int(num_features / 2)\n", - "\n", - "fm = QuantumCircuit(num_qubits)\n", - "training_param = Parameter(\"θ\")\n", - "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", - "fm.ry(training_param, fm.qubits)\n", - "for cz in entangler_map:\n", - " fm.cz(cz[0], cz[1])\n", - "for i in range(num_qubits):\n", - " fm.rz(-2 * feature_params[2 * i + 1], i)\n", - " fm.rx(-2 * feature_params[2 * i], i)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "02576ae9-8f24-4f47-8c37-ae0c3f710d4c", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import unitary_overlap\n", - "\n", - "# Assign features of each data point to a unitary, an instance of the general feature map.\n", - "\n", - "unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])\n", - "unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])\n", - "\n", - "# Create the overlap circuit\n", - "\n", - "overlap_circ = unitary_overlap(unitary1, unitary2)\n", - "overlap_circ.measure_all()" - ] - }, - { - "cell_type": "markdown", - "id": "fbe9ba90-6463-488d-b6f2-e9d4ee8f0871", - "metadata": {}, - "source": [ - "We won't bother checking the depths yet, since what really matters is the transpiled two-qubit depth." - ] - }, - { - "cell_type": "markdown", - "id": "9a5c14fc-aa58-461f-a1d5-fc644e042ecc", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum execution\n", - "\n", - "We start by selecting the least busy backend, then optimize our circuit for running on that backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "99e3a8c7-9aba-4890-a492-cb3779b5de03", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "# Import needed packages\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Get the least busy backend\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", - ")\n", - "print(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "0b524533-7422-4d91-a3d1-82dfdeaf1588", - "metadata": {}, - "source": [ - "On small-scale jobs, a preset pass manager will often return the same circuit with the same depth, reliably. But in very large, complex circuits the pass manager can return different transpiled circuits each time it runs. This is because it is using heuristics, and because very large circuits will have a complicated landscape of possible optimizations. It is often useful to transpile a few times and take the shallowest circuit. This only introduces classical overhead and may substantially improve the results from the quantum computer.\n", - "\n", - "Here, we transpile the unitary overlap circuit 20 times, and look at the depths of the circuits obtained." - ] - }, - { - "cell_type": "code", - "execution_count": 99, - "id": "f5f8f833-8ac2-4df0-8268-4d83f841fd16", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "circuit depth = 61\n" - ] - } - ], - "source": [ - "# Apply level 3 optimization to our overlap circuit\n", - "transpiled_qcs = []\n", - "transpiled_depths = []\n", - "transpiled_twoqubit_depths = []\n", - "for i in range(1, 20):\n", - " pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", - " overlap_ibm = pm.run(overlap_circ)\n", - " transpiled_qcs.append(overlap_ibm)\n", - " transpiled_depths.append(overlap_ibm.decompose().depth())\n", - " transpiled_twoqubit_depths.append(\n", - " overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n", - " )\n", - "\n", - "print(\"circuit depth = \", overlap_ibm.decompose().depth())" - ] - }, - { - "cell_type": "code", - "execution_count": 100, - "id": "fd604f87-32d1-42dd-a1fb-f2315cb4dfdc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]\n", - "[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]\n" - ] - } - ], - "source": [ - "print(transpiled_depths)\n", - "print(transpiled_twoqubit_depths)" - ] - }, - { - "cell_type": "markdown", - "id": "30fad0d8-f5e4-4dcf-946a-956f23334e9c", - "metadata": {}, - "source": [ - "Here you can see that there is some variation in the total gate depth with different transpilation passes. Our circuit is not yet deep/wide enough to see variation in the two-qubit transpiled depths. We will use the `transpiled_qcs[1]`, which has a depth of 60, just slightly lower than the depth of the deepest circuit obtained, which was 77." - ] - }, - { - "cell_type": "code", - "execution_count": 101, - "id": "28f00388-0e6e-41db-a0b4-9200d58ba5cb", - "metadata": {}, - "outputs": [], - "source": [ - "overlap_ibm = transpiled_qcs[1]" - ] - }, - { - "cell_type": "markdown", - "id": "e96cee51-e79f-4eab-829b-397645bc1080", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit Runtime Primitives\n", - "\n", - "As we scale closer to utility, simulators will not be useful. Only the syntax for real quantum computers is shown here." - ] - }, - { - "cell_type": "code", - "execution_count": 102, - "id": "7eedcf5f-5ead-4eb0-b423-669901f572ba", - "metadata": {}, - "outputs": [], - "source": [ - "# Run on ibm_osaka, 7-12-24, required 22 sec.\n", - "\n", - "# Import our runtime primitive\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "# Open a Runtime session:\n", - "session = Session(backend=backend)\n", - "num_shots = 10000\n", - "# Use sampler and get the counts\n", - "\n", - "sampler = Sampler(mode=session)\n", - "options = sampler.options\n", - "options.dynamical_decoupling.enable = True\n", - "options.twirling.enable_gates = True\n", - "counts = (\n", - " sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()\n", - ")\n", - "\n", - "# Close session after done\n", - "session.close()" - ] - }, - { - "cell_type": "markdown", - "id": "f40540b8-146d-4165-af12-db902232300d", - "metadata": {}, - "source": [ - "### Step 4: Post-process, return result in classical format\n", - "\n", - "As described in the introduction, the most useful measurement here is the probability of measuring the zero state $|00000\\rangle$." - ] - }, - { - "cell_type": "code", - "execution_count": 103, - "id": "ce9f4dc1-b562-437e-b9fe-5a1781b67bfe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.0138" - ] - }, - "execution_count": 103, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "counts.get(0, 0.0) / num_shots" - ] - }, - { - "cell_type": "markdown", - "id": "9b6c78b4-6788-404f-bd90-d98d7b288d42", - "metadata": {}, - "source": [ - "This process for the single kernel matrix element could be repeated between other data pairs in your set to obtain the full kernel matrix. The dimension of the kernel matrix is dictated by the number of points in your training data, not the number of features. So the computing cost of manipulating the kernel matrix into a predictive model does not scale like the number of features or qubits. Even for relatively small datasets with large numbers of features, the data would still need to be matched to a feature map that yields effective classification.\n", - "\n", - "### Scaling and future work\n", - "\n", - "The kernel method requires that we measure the $|0\\rangle$ as accurately as possible. But gate errors and readout errors mean that there is some non-zero probability $p$ that any given qubit will be erroneously measured to be in the $|1\\rangle$ state. Even with the oversimplification that the probability of $|0\\rangle$ should be $100\\%$, for many features encoded on, say, $N$ bits, the probability of correctly measuring all bits to be $|0\\rangle$ is reduced to $(1-p)^N$. As $N$ becomes large, this method becomes less and less reliable. Overcoming this difficulty and scaling kernel estimation to more and more features is an area of current research. To learn more about this issue, see this work by [Thanasilp, Wang, Cerezo, and Holmes.](https://www.nature.com/articles/s41467-024-49287-w) We recommend you explore what can be done with current quantum computers, and also look forward to what will be possible in the era of error correction." - ] - }, - { - "cell_type": "markdown", - "id": "556eb119-2b22-4bd9-b82e-c3a6ea31cfa1", - "metadata": {}, - "source": [ - "### Review\n", - "\n", - "Calculating a quantum kernel involves\n", - "* calculating kernel matrix entries, using pairs of training data points\n", - "* encoding the data and mapping it via a feature mapping\n", - "* optimizing your circuit for running on real quantum computers / backends\n", - "\n", - "The quantum kernel can then be used in classical machine learning algorithms, as in this lesson.\n", - "\n", - "Some key things to keep in mind when using quantum kernels include:\n", - "* Is the dataset likely to benefit from quantum kernel methods?\n", - "* Try different feature maps and entanglement schemes.\n", - "* Is the circuit depth acceptable?\n", - "* Try running a pass manager multiple times and use the smallest-depth circuit you can get.\n", - "\n", - "Quantum kernel methods are potentially powerful tools given a proper match between datasets with quantum-amenable features, and a suitable quantum feature map. To better understand where quantum kernels are likely to be useful, we recommend reading [Liu, Arunachalam & Temme (2021)](https://www.nature.com/articles/s41567-021-01287-z)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "33c0bc6b-0506-4e17-9823-07a644d3093d", + "metadata": {}, + "source": [ + "---\n", + "title: Quantum kernel methods\n", + "description: Quantum kernels are used initially to determine a kernel matrix element, a full kernel matrix and the interface with classical kernel tools is presented.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore workstreams workstream xvals yvals xval Arunachalam scikit */}\n", + "\n", + "# Quantum Kernels\n", + "\n", + "## Introduction to quantum kernels\n", + "\n", + "The \"Quantum kernel method\" refers to any method that uses quantum computers to estimate a kernel. In this context, \"kernel\" will refer to the kernel matrix or individual entries therein. Recall that a feature mapping $\\Phi(\\vec{x})$ is a mapping from $\\vec{x}\\in \\mathbb{R}^d$ to $\\Phi(\\vec{x})\\in \\mathbb{R}^{d'},$ where usually $d'>d$ and where the goal of this mapping is to make the categories of data separable by a hyperplane. The kernel function takes vectors in the feature-mapped space as arguments and returns their inner product, that is, $K:\\mathbb{R}^d\\times\\mathbb{R}^d\\rightarrow \\mathbb{R}$ with $K(x,y) = \\langle \\Phi(x)|\\Phi(y)\\rangle$. Classically, we are interested in feature maps for which the kernel function is easy to evaluate. This often means finding a kernel function for which the inner product in the feature-mapped space can be written in terms of the original data vectors, without having to ever construct $\\Phi(x)$ and $\\Phi(y)$. In the method of quantum kernels, the feature mapping is done by a quantum circuit, and the kernel is estimated using measurements on that circuit and the relative measurement probabilities.\n", + "\n", + "In this lesson we will examine the depths of pre-coded encoding circuits that use substantial entanglement and compare those to depths of circuits we code by hand. This is not to advocate for one method over another. You may find that pre-coded circuits are too deep, and that the entanglement in the custom-built circuit is insufficient to be useful. Again, these are shown only to enable your exploration.\n", + "\n", + "Before walking through a kernel matrix estimation in detail, let us outline the workflow using the language of Qiskit patterns.\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "* Input: Training dataset\n", + "* Output: Abstract circuit for calculating a kernel matrix entry\n", + "\n", + "Given the dataset, the starting point is to encode the data into a quantum circuit. In other words, we need to map our data into the Hilbert space of states of our quantum computer. We do this by constructing a data-dependent circuit. There are many ways of doing this, and the previous lesson outlined a number of options. You can construct your own circuit to encode your data, or you can use a pre-made feature map like `zz_feature_map`. In this lesson, we will do both.\n", + "\n", + "Note that in order to calculate a single kernel matrix element, we will want to encode two different points, so we can estimate their inner product. A full quantum kernel workflow will of course, involve many such inner products between mapped data vectors, as well as classical machine learning methods. But the core step being iterated is the estimation of a single kernel matrix element. For this we select a data-dependent quantum circuit and map two data vectors into the feature space.\n", + "\n", + "![Classical_Review_background_kernel_circuit](/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/classical-review-background-kernel-circuit.avif)\n", + "\n", + "For the task of generating a kernel matrix, we are particularly interested in the probability of measuring the $|0\\rangle^{\\otimes N}$ state, in which all $N$ qubits are in the $|0\\rangle$ state. To see this, consider that the circuit responsible for encoding and mapping of one data vector $\\vec{x}_i$ can be written as $\\Phi(\\vec{x}_i)$, and the one responsible for encoding and mapping $\\vec{x}_j$ is $\\Phi(\\vec{x}_j)$, and denote the mapped states\n", + "$$\n", + "|\\psi(\\vec{x}_i)\\rangle = \\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}\n", + "$$\n", + "$$\n", + "|\\psi(\\vec{x}_j)\\rangle = \\Phi(\\vec{x}_j)|0\\rangle^{\\otimes N}.\n", + "$$\n", + "\n", + "These states _are_ the mapping of the data to higher dimensions, so our desired kernel entry is the inner product\n", + "$$\n", + "\\langle\\psi(\\vec{x}_j)|\\psi(\\vec{x}_i)\\rangle = \\langle 0 |^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}.\n", + "$$\n", + "If we operate on the default initial state $|0\\rangle^{\\otimes N}$ with both circuits $\\Phi^\\dagger(\\vec{x}_j)$ and $\\Phi(\\vec{x}_i)$, the probability of then measuring the state $|0\\rangle^{\\otimes N}$ is\n", + "$$\n", + "P_0 = |\\langle0|^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}|^2.\n", + "$$\n", + "This is exactly the value we want (up to $||^2$). The measurement layer of our circuit will return measurement probabilities (or so-called \"quasi-probabilities\", if certain error mitigation methods are used). The probability of interest is that of the zero state, $|0\\rangle^{\\otimes N}$.\n", + "\n", + "\n", + "### Step 2: Optimize problem for quantum execution\n", + "\n", + "* Input: Abstract circuit, not optimized for a particular backend\n", + "* Output: Target circuit and observable, optimized for the selected QPU\n", + "\n", + "In this step, we will use the `generate_preset_pass_manager` function from Qiskit to specify an optimization routine for our circuit with respect to the real quantum computer on which we plan to run the experiment. We set `optimization_level=3` , which means we will use the preset pass manager which provides the highest level of optimization. In this context, \"optimization\" refers to optimizing the implementation of the circuit on a real quantum computer. This includes considerations like selecting physical qubits to correspond to qubits in the abstract quantum circuit that will minimize gate depth, or selecting physical qubits with the lowest available error rates. This is not directly related to optimization of the machine learning problem (as in classical optimizers like COBYLA).\n", + "\n", + "Depending on how you implement step 2, you may have to optimize the circuit more than once, since each pair of points involved in a matrix element produce a different circuit to be measured.\n", + "\n", + "### Step 3: Execute using Qiskit Runtime Primitives\n", + "\n", + "* Input: Target circuit\n", + "* Output: Probability distribution\n", + "\n", + "Use the `Sampler` primitive from Qiskit Runtime to reconstruct a probability distribution of states yielded from sampling the circuit. Note that you may see this referred to as a \"quasi-probability distribution\", a term which is applicable where noise is an issue and when extra steps are introduced, such as in error mitigation. In such cases, the sum of all probabilities may not exactly equal 1; hence \"quasi-probability\".\n", + "\n", + "### Step 4: Post-process, return result in classical format\n", + "\n", + "* Input: Probability distribution\n", + "* Output: A single kernel matrix element, or a kernel matrix if repeating\n", + "\n", + "Calculate the probability of measuring $|0\\rangle^{\\otimes N}$ on the quantum circuit, and populate the kernel matrix in the position corresponding to the two data vectors used. To fill out the entire kernel matrix, we need to run a quantum experiment for each entry. Once we have a kernel matrix, we can use it in many classical machine learning algorithms that accept `pre-calculated kernels`. For example: `qml_svc = SVC(kernel=\"precomputed\")`. We can then use classical workstreams to apply our model on our testing data, and get an accuracy score. Depending on our satisfaction with our accuracy score, we may need to revisit aspects of our calculation, such as our feature map.\n", + "\n", + "### Lesson outline\n", + "\n", + "In this lesson we will carry out these steps several ways to make optimal use of your time on real quantum computers. We will apply a quantum kernel method to\n", + "* A single kernel matrix entry for data with relatively few features, using a real backend, so that we can easily follow what is happening at each step.\n", + "* An entire data set with relatively few features, using a simulated backend, so that we can see how the quantum workstream connects with classical machine learning methods\n", + "* A single kernel matrix entry for data with many features, using a real quantum computer. We will not estimate an entire kernel matrix for a large dataset, in order to respect time on IBM® quantum computers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fedaf0c-efb4-4425-8e31-89ca0319375d", + "metadata": {}, + "outputs": [], + "source": [ + "# If you have not already, install scikit learn\n", + "#!pip install scikit-learn" + ] + }, + { + "cell_type": "markdown", + "id": "f05e564c-ae91-4365-88fb-a57183c525aa", + "metadata": {}, + "source": [ + "## Single kernel matrix entry\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Let us first consider a data set with just a few features, say 10. The data set could be as large as you like, since we are calculating the kernel matrix elements one at a time. We need at least two points, so we will start with that (in the next example, we will import a full dataset). Let's import a few needed packages:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "61f15d16-993a-4405-9454-685829cb08b4", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Two mock data points, including category labels, as in training\n", + "small_data = [\n", + " [-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],\n", + " [-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],\n", + "]\n", + "\n", + "# Data points with labels removed, for inner product\n", + "train_data = [small_data[0][:-1], small_data[1][:-1]]" + ] + }, + { + "cell_type": "markdown", + "id": "e9acb5ee-8c7a-467d-a7e9-e6219f4d443b", + "metadata": {}, + "source": [ + "We can try using the `z_feature_map`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d8e424e-751a-4057-a879-cc39d83af953", + "metadata": {}, + "outputs": [], + "source": [ + "# from qiskit.circuit.library import zz_feature_map\n", + "# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)\n", + "\n", + "from qiskit.circuit.library import z_feature_map\n", + "\n", + "fm = z_feature_map(feature_dimension=np.shape(train_data)[1])\n", + "\n", + "\n", + "unitary1 = fm.assign_parameters(train_data[0])\n", + "unitary2 = fm.assign_parameters(train_data[1])" + ] + }, + { + "cell_type": "markdown", + "id": "2b41da49-ce41-45eb-83ff-cfcbaa8c2c92", + "metadata": {}, + "source": [ + "The two unitaries above exactly correspond to $U_1$ and $U_2$ described in the introduction. We can combine them using `unitary_overlap`. As always, we want to keep an eye on our circuit depth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4273dcf7-784c-4edb-a9cd-9b249d24dbb8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "circuit depth = 9\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import unitary_overlap\n", + "\n", + "\n", + "overlap_circ = unitary_overlap(unitary1, unitary2)\n", + "overlap_circ.measure_all()\n", + "\n", + "print(\"circuit depth = \", overlap_circ.decompose().depth())\n", + "overlap_circ.decompose().draw(\"mpl\", scale=0.6, style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "0a9f9df0-d543-496a-99fd-8b66a53941a7", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum execution\n", + "\n", + "We start by selecting the least busy backend, then optimize our circuit for running on that backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27f62452-2534-4f97-a738-52e4a19a7772", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Import needed packages\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Get the least busy backend\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", + ")\n", + "print(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "36cfd4e9-55c9-46c4-8afe-bd6cd0a40f45", + "metadata": {}, + "outputs": [], + "source": [ + "# Apply level 3 optimization to our overlap circuit\n", + "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", + "overlap_ibm = pm.run(overlap_circ)" + ] + }, + { + "cell_type": "markdown", + "id": "13def807-7e1a-44c8-a7d8-2db5ce0f75ec", + "metadata": {}, + "source": [ + "For complicated circuits, this step will substantially increase the circuit depth as it maps to native gates for real quantum computers, and information may need to be moved from qubit to qubit. In this simple case, the depth is hardly affected at all." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "79348ff5-4d6d-46d7-ae2b-e6ffbb22dd25", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "circuit depth = 10\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(\"circuit depth = \", overlap_ibm.decompose().depth())\n", + "overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)" + ] + }, + { + "cell_type": "markdown", + "id": "ec97b263-4037-4ed9-9588-110335d2c51d", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit Runtime Primitives\n", + "\n", + "The syntax for running on a simulator is commented out below. For this dataset, with a small number of features, running on a simulator is still an option. For utility-scale calculations, simulation is not typically feasible. Simulators should only be used to debug scaled-down code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36a81e65-d14f-4dc2-b64c-4acbd5a185cd", + "metadata": {}, + "outputs": [], + "source": [ + "# Run this for a simulator\n", + "# from qiskit.primitives import StatevectorSampler\n", + "\n", + "# from qiskit_ibm_runtime import Options, Session, Sampler\n", + "\n", + "# num_shots = 10000\n", + "\n", + "# Evaluate the problem using state vector-based primitives from Qiskit\n", + "# sampler = StatevectorSampler()\n", + "# results = sampler.run([overlap_circ], shots=num_shots).result()\n", + "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", + "# counts_bit = results[0].data.meas.get_counts()\n", + "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit\n", + "# string.\n", + "# counts = results[0].data.meas.get_int_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ad869b2-ad50-4a82-bc4c-70fd0ab802c5", + "metadata": {}, + "outputs": [], + "source": [ + "# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.\n", + "\n", + "# Import our runtime primitive\n", + "from qiskit_ibm_runtime import Session, SamplerV2 as Sampler\n", + "\n", + "num_shots = 10000\n", + "\n", + "# Use sampler and get the counts\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "results = sampler.run([overlap_ibm], shots=num_shots).result()\n", + "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", + "counts_bit = results[0].data.meas.get_counts()\n", + "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit\n", + "# string.\n", + "counts = results[0].data.meas.get_int_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "16d66085-047a-4718-80fe-eb64c20bd398", + "metadata": {}, + "source": [ + "### Step 4: Post-process, return result in classical format\n", + "\n", + "As described in the introduction, the most useful measurement here is the probability of measuring the zero state $|00000\\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "33522847-21dc-48b8-9270-ce11fd1ec529", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.6525" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "counts.get(0, 0.0) / num_shots" + ] + }, + { + "cell_type": "markdown", + "id": "65dfa3e4-bfe4-45d7-815f-e8c1aa24d916", + "metadata": {}, + "source": [ + "This is the outcome we wanted: an estimate of the inner product (up to mod squared) of the vectors corresponding to two data points. If we want to look at the full distribution of measurement probabilities (or quasiprobabilities), we can do so using the ```plot_distribution``` function as shown below. One sees that for a large number of qubits, pictures like this quickly become intractable." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "29aaf5d6-ea7e-4373-b33c-9299699a3964", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_distribution\n", + "\n", + "plot_distribution(counts_bit)" + ] + }, + { + "cell_type": "markdown", + "id": "d0a52344-34f2-498b-8f46-489d581bd8ba", + "metadata": {}, + "source": [ + "Alternatively, one might define a visualization like the one below to look only at the top 10 most probable measurements. This could be important for troubleshooting or trying to glean more intuition for the data. But the measurement probability of the zero state is our kernel matrix element." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "c141e800-79eb-400a-9b8c-75e2393ce24e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def visualize_counts(probs, num_qubits):\n", + " \"\"\"Visualize the outputs from the Qiskit Sampler primitive.\"\"\"\n", + " zero_prob = probs.get(0, 0.0)\n", + " top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])\n", + " top_10.update({0: zero_prob})\n", + " by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))\n", + " xvals, yvals = list(zip(*by_key.items()))\n", + " xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]\n", + " plt.bar(xvals, yvals)\n", + " plt.xticks(rotation=75)\n", + " plt.title(\"Results of sampling\")\n", + " plt.xlabel(\"Measured bitstring\")\n", + " plt.ylabel(\"Counts\")\n", + " plt.show()\n", + "\n", + "\n", + "visualize_counts(counts, overlap_circ.num_qubits)" + ] + }, + { + "cell_type": "markdown", + "id": "47e85d94-ee94-4ed9-8223-ccd5fc9442d4", + "metadata": {}, + "source": [ + "From this information about only one inner product between two data points in the higher dimensional feature space, all we can say is that their overlap is fairly large compared to the maximum overlap (which would be 1.0). This could be an indicator that these two data points are somehow similar in nature and will be categorized in the same class classes. Or it could be an indicator that our feature map is not effective at mapping into a space where like data has a strong overlap and unlike data has a small overlap. In order to know which is true, we must apply our feature map to the entire set of data and see if the resulting kernel matrix can be manipulated to effectively separate classes with high accuracy.\n", + "\n", + "It is worth noting that we used the `z_feature_map` which resulted in low two-qubit transpiled depth (depth 1, in fact). If your circuits become too deep, it is sure to result in a lot of noise, and this will make the probability of measuring the zero state very low, even if your feature map is well-matched to your data. For example, a repetition of the above process using ```zz_feature_map``` and ```, entanglement='linear', reps=1``` yielded ```dist.get(0,0.0) = 0.0015``` using the same data points. This is due to the much greater circuit depths and two-qubit depths from ```zz_feature_map```. The figure below shows the probability distribution for that calculation.\n", + "\n", + "![Bad results from a zz feature map.](/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/zzfeaturemap-bad-results.avif)\n", + "\n", + "It is worth playing around with a few data points from the same category to see how low your depth must be to obtain good results. The following is rough advice that is sure to have exceptions. Generally, a two-qubit, transpiled depth of 10 or fewer should be no problem. A two-qubit, transpiled depth of 50-60 is state-of-the-art and will require advanced error mitigation among other tools. In between, your results may vary with data similarity, feature map expressivity, circuit width, and other factors." + ] + }, + { + "cell_type": "markdown", + "id": "33eda2a4-cd97-4bba-8acd-4017bfe2cd12", + "metadata": {}, + "source": [ + "Ordinarily the post-processing step would also include classical machine learning processes. In the next section we will extend this process to an entire dataset, and show the classical machine learning workflow." + ] + }, + { + "cell_type": "markdown", + "id": "41850d1d-3800-4aa0-aa01-fe6e72c872be", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "In a 10-qubit quantum circuit, generally, how many different states are there that could possibly be measured?\n", + "\n", + "\n", + "\n", + "\n", + "$2^{10}$ or 1024.\n", + "\n", + "\n", + "\n", + "\n", + "Let us suppose that someone new to quantum computing attempts to use a quantum circuit that has very high two-qubit depth, and they do not use error mitigation. Let us further suppose that this results in an error rate of 10% on each qubit. If the true (error-free) kernel matrix element corresponding to this circuit is very large, say 1.0, what would be the probability of measuring all 10 qubits to be in the state with every qubit $|0\\rangle$?\n", + "\n", + "\n", + "\n", + "\n", + "The probability of each qubit being correctly found in the |0> state is 0.90. The probability for all 10 qubits to be found in the correct state is $0.90^{10}$ or about 35%.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Explain in your own words why it is so important to monitor circuit depths. This is true generally, but explain it in the context of quantum kernel estimation.\n", + "\n", + "\n", + "\n", + "\n", + "In this QKE workflow, our estimates are based on the measurements of the zero state, meaning the state in which every qubit is found in the $|0\\rangle$ state. Very deep circuits will introduce high error rates. When that error rate is compounded over many qubits, this will reduce the probability of measuring the zero state, substantially.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "0fde5aca-0d54-40b1-82d4-9d1b2a1b67bb", + "metadata": {}, + "source": [ + "## Full kernel matrix\n", + "\n", + "In this section, we will extend the above process to the binary classification of a full dataset. This will introduce two important components: (1) we can now implement classical machine learning in post-processing, and (2) we can obtain accuracy scores for our training.\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Now we will import an existing dataset for our classification. This dataset consists of 128 rows (data points) and 14 features on each point. There is a 15th element that indicates the binary category of each point ($\\pm 1$). The dataset is imported below, or you can access the dataset and view its structure [here](https://github.com/qiskit-community/prototype-quantum-kernel-training/blob/main/data/dataset_graph7.csv).\n", + "\n", + "We will use the first 90 data points for training, and the next 30 points for testing." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "60f2699d-ef47-44f7-931a-4a70d4363c0e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--2024-07-11 23:05:22-- https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", + "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...\n", + "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n", + "HTTP request sent, awaiting response... 200 OK\n", + "Length: 49405 (48K) [text/plain]\n", + "Saving to: ‘dataset_graph7.csv.15’\n", + "\n", + "dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s \n", + "\n", + "2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]\n", + "\n" + ] + } + ], + "source": [ + "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", + "\n", + "df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n", + "\n", + "# Prepare training data\n", + "\n", + "train_size = 90\n", + "X_train = df.values[0:train_size, :-1]\n", + "train_labels = df.values[0:train_size, -1]\n", + "\n", + "# Prepare testing data\n", + "test_size = 30\n", + "X_test = df.values[train_size : train_size + test_size, :-1]\n", + "test_labels = df.values[train_size : train_size + test_size, -1]" + ] + }, + { + "cell_type": "markdown", + "id": "4a665fcb-bd27-4713-9c0a-b7747de5fb45", + "metadata": {}, + "source": [ + "We will already prepare for storing multiple outputs by constructing a kernel matrix and a test matrix of appropriate dimensions." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "34d92041-0501-4c20-b780-ae5352510637", + "metadata": {}, + "outputs": [], + "source": [ + "# Empty kernel matrix\n", + "num_samples = np.shape(X_train)[0]\n", + "kernel_matrix = np.full((num_samples, num_samples), np.nan)\n", + "test_matrix = np.full((test_size, num_samples), np.nan)" + ] + }, + { + "cell_type": "markdown", + "id": "920233dd-cfac-498d-be88-746719083cc2", + "metadata": {}, + "source": [ + "Now we create a feature map for encoding and mapping our classical data in a quantum circuit. We are free to construct our own feature map or use a pre-fabricated one. Feel free to modify the feature map below, or switch back to ZFeatureMap. But always pay attention to circuit depth. Recall that in the previous 6-qubit example the transpiled circuit depth was intractably high when using `zz_feature_map`. As the scale and complexity of the circuit increase, the depth could rapidly increase to a point where noise overwhelms our results. Whenever you know something about your data structure that may inform what feature map structure would be most useful, it is advisable to create your own custom feature map that leverages that knowledge." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ca813500-5856-4b50-a213-a7e87d825c18", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", + "\n", + "# Prepare feature map for computing overlap\n", + "num_features = np.shape(X_train)[1]\n", + "num_qubits = int(num_features / 2)\n", + "\n", + "# To use a custom feature map use the lines below.\n", + "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n", + "\n", + "fm = QuantumCircuit(num_qubits)\n", + "training_param = Parameter(\"θ\")\n", + "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", + "fm.ry(training_param, fm.qubits)\n", + "for cz in entangler_map:\n", + " fm.cz(cz[0], cz[1])\n", + "for i in range(num_qubits):\n", + " fm.rz(-2 * feature_params[2 * i + 1], i)\n", + " fm.rx(-2 * feature_params[2 * i], i)" + ] + }, + { + "cell_type": "markdown", + "id": "3c6416b5-9eb1-40a4-b72c-8912df931139", + "metadata": {}, + "source": [ + "### Steps 2 and 3: Optimize problem and execute using primitives\n", + "\n", + "We will construct an overlap circuit, and if we were running on a real quantum computer in this example, we would optimize it for execution as before. But in this case, we intend to step over all data points and calculate the full kernel matrix. For each pair of data vectors $\\vec{x}_i$ and $\\vec{x}_j$, we create a different overlap circuit. Thus we must optimize our circuit for each data point pair. So steps 2 and 3 would be done together in the multiple iterations.\n", + "\n", + "The code cell below does exactly the same process as before for a single data point pair. This time it is simply executed inside two `for` loops, and there is the additional line at the end `kernel_matrix[x_1,x_2] = ...` to store the results of each calculation. Note that we have leveraged the symmetry of a kernel matrix to reduce the number of calculations by 1/2. We have also simply set the diagonal elements to 1, as they should be in the absence of noise. Depending on your implementation and required precision, you could also use the diagonal elements to estimate noise or learn about it for error mitigation purposes.\n", + "\n", + "Once the kernel matrix has been fully populated, we repeat the process for the test data and populate the test_matrix. This is really also a kernel matrix; we simply give it a different name to distinguish the two." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7995748f-fc97-4eda-bed6-8b189c1e65ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "training done\n", + "test matrix done\n" + ] + } + ], + "source": [ + "# To use a simulator\n", + "from qiskit.primitives import StatevectorSampler\n", + "\n", + "# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum\n", + "# computers\n", + "# service = QiskitRuntimeService()\n", + "# backend = service.least_busy(\n", + "# operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", + "# )\n", + "\n", + "num_shots = 10000\n", + "\n", + "# Evaluate the problem using state vector-based primitives from Qiskit.\n", + "sampler = StatevectorSampler()\n", + "\n", + "for x1 in range(0, train_size):\n", + " for x2 in range(x1 + 1, train_size):\n", + " unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n", + " unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", + "\n", + " # Create the overlap circuit\n", + " overlap_circ = unitary_overlap(unitary1, unitary2)\n", + " overlap_circ.measure_all()\n", + "\n", + " # These lines run the qiskit sampler primitive.\n", + " counts = (\n", + " sampler.run([overlap_circ], shots=num_shots)\n", + " .result()[0]\n", + " .data.meas.get_int_counts()\n", + " )\n", + "\n", + " # Assign the probability of the 0 state to the kernel matrix, and the transposed element\n", + " # (since this is an inner product)\n", + " kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n", + " kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots\n", + " # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to\n", + " # probability (or alter the code to check these entries and verify they yield 1)\n", + " kernel_matrix[x1, x1] = 1\n", + "\n", + "print(\"training done\")\n", + "\n", + "# Similar process to above, but for testing data.\n", + "for x1 in range(0, test_size):\n", + " for x2 in range(0, train_size):\n", + " unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])\n", + " unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n", + "\n", + " # Create the overlap circuit\n", + " overlap_circ = unitary_overlap(unitary1, unitary2)\n", + " overlap_circ.measure_all()\n", + "\n", + " counts = (\n", + " sampler.run([overlap_circ], shots=num_shots)\n", + " .result()[0]\n", + " .data.meas.get_int_counts()\n", + " )\n", + "\n", + " test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n", + "\n", + "print(\"test matrix done\")" + ] + }, + { + "cell_type": "markdown", + "id": "ea65cd41-4ba3-423c-a98f-2f8e3751adf4", + "metadata": {}, + "source": [ + "### Step 4: Post-process, return result in classical format\n", + "\n", + "Now that we have a kernel matrix and a similarly formatted test_matrix from quantum kernel methods, we can apply classical machine learning algorithms to make predictions about our test data and check its accuracy. We will start by importing Scikit-Learn's `sklearn.svc`, a support vector classifier (SVC). We must specify that we want the SVC to use our precomputed kernel using `kernel = precomputed`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "78d9e291-54c3-4c45-827e-066c6d012da5", + "metadata": {}, + "outputs": [], + "source": [ + "# import a support vector classifier from a classical ML package.\n", + "from sklearn.svm import SVC\n", + "\n", + "# Specify that you want to use a pre-computed kernel matrix\n", + "qml_svc = SVC(kernel=\"precomputed\")" + ] + }, + { + "cell_type": "markdown", + "id": "9bd69e5f-dafd-492c-bd5d-f5e16925f611", + "metadata": {}, + "source": [ + "Using `SVC.fit`, we can now feed in the kernel matrix and the training labels to obtain a fit. `SVC.score` will then score our test data against that fit using our test_matrix, and return our accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "d8708973-c97c-494c-af9c-e4a0fe430452", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precomputed kernel classification test score: 1.0\n" + ] + } + ], + "source": [ + "# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives\n", + "# you a fit.\n", + "qml_svc.fit(kernel_matrix, train_labels)\n", + "\n", + "# Now use the .score to test your data, using the matrix of test data, and test labels as your\n", + "# inputs.\n", + "qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)\n", + "print(f\"Precomputed kernel classification test score: {qml_score_precomputed_kernel}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0200dceb-6d61-4e16-8c3d-d08dbc5529e5", + "metadata": {}, + "source": [ + "We see that the accuracy of our trained model was 100%. This is great, and it shows that QKE can work. But that is very different from quantum advantage. Classical kernels would likely have been able to solve this classification problem with 100% accuracy as well. There is much work to be done characterizing different data types and data relationships to see where quantum kernels will be most useful in the current utility era.\n", + "We leave it to the learner to modify parts of this workflow and study the effectiveness of various quantum feature maps. Here are a few things to consider:\n", + "* How robust is the accuracy? Does it hold for broad types of data or just this specific training data?\n", + "* What structure in your data makes you suspect that a quantum feature map is useful?\n", + "* How is the accuracy affected by increasing/decreasing the amount of training data?\n", + "* What feature maps can you use and how do the results vary with feature maps?\n", + "* How are the accuracy and running time affected by increasing the number of features?\n", + "* Which trends, if any, do you expect to hold on real quantum computers?" + ] + }, + { + "cell_type": "markdown", + "id": "da2427c4-a1a1-4284-b10f-95d5c5230755", + "metadata": {}, + "source": [ + "## Scaling to more features and qubits\n", + "\n", + "In this section, we will repeat the calculation of a single matrix element, but for a much larger number of features, sketching the path to scale toward utility. The restriction to a single matrix element is done so that the process can be shown without using up too much of your allotted time on quantum computers.\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "We will assume a starting point of a dataset in which each data point has 42 features. As in the first example, we will calculate a single kernel matrix element, requiring two data points. The two points below have 42 features and a single category variable ($\\pm 1$)." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "014ba70a-5202-4c86-b886-7ff27ae9a1b3", + "metadata": {}, + "outputs": [], + "source": [ + "# Two mock data points, including category labels, as in training\n", + "\n", + "large_data = [\n", + " [\n", + " -0.028,\n", + " -1.49,\n", + " -1.698,\n", + " 0.107,\n", + " -1.536,\n", + " -1.538,\n", + " -1.356,\n", + " -1.514,\n", + " -0.109,\n", + " -1.8,\n", + " -0.122,\n", + " -1.651,\n", + " -1.955,\n", + " -0.123,\n", + " -1.732,\n", + " 0.091,\n", + " -0.048,\n", + " -0.128,\n", + " -0.026,\n", + " 0.082,\n", + " -1.263,\n", + " 0.065,\n", + " 0.004,\n", + " -0.055,\n", + " -0.08,\n", + " -0.173,\n", + " -1.734,\n", + " -0.39,\n", + " -1.451,\n", + " 0.078,\n", + " -1.578,\n", + " -0.025,\n", + " -0.184,\n", + " -0.119,\n", + " -1.336,\n", + " 0.055,\n", + " -0.204,\n", + " -1.578,\n", + " 0.132,\n", + " -0.121,\n", + " -1.599,\n", + " -0.187,\n", + " -1,\n", + " ],\n", + " [\n", + " -1.414,\n", + " -1.439,\n", + " -1.606,\n", + " 0.246,\n", + " -1.673,\n", + " 0.002,\n", + " -1.317,\n", + " -1.262,\n", + " -0.178,\n", + " -1.814,\n", + " 0.013,\n", + " -1.619,\n", + " -1.86,\n", + " -0.25,\n", + " -0.212,\n", + " -0.214,\n", + " -0.033,\n", + " 0.071,\n", + " -0.11,\n", + " -1.607,\n", + " 0.441,\n", + " -0.143,\n", + " -0.009,\n", + " -1.655,\n", + " -1.579,\n", + " 0.381,\n", + " -1.86,\n", + " -0.079,\n", + " -0.088,\n", + " -0.058,\n", + " -1.481,\n", + " -0.064,\n", + " -0.065,\n", + " -1.507,\n", + " 0.177,\n", + " -0.131,\n", + " -0.153,\n", + " 0.07,\n", + " -1.627,\n", + " 0.593,\n", + " -1.547,\n", + " -0.16,\n", + " -1,\n", + " ],\n", + "]\n", + "train_data = [large_data[0][:-1], large_data[1][:-1]]" + ] + }, + { + "cell_type": "markdown", + "id": "3e4dd6a7-e51c-4dc2-99ea-66e2746e411d", + "metadata": {}, + "source": [ + "Recall that the `zz_feature_map` produced rather deep circuits in the case of relatively few features (14 features). As we increase the number of features, we need to closely monitor circuit depth. To illustrate this, we will first try using the `zz_feature_map` and check the depth of the resulting circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdb1ecc4-7827-4eb7-bf7b-5a09838775ca", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import zz_feature_map\n", + "\n", + "fm = zz_feature_map(\n", + " feature_dimension=np.shape(train_data)[1], entanglement=\"linear\", reps=1\n", + ")\n", + "\n", + "unitary1 = fm.assign_parameters(train_data[0])\n", + "unitary2 = fm.assign_parameters(train_data[1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bc865cb-8f8b-4812-9a05-1bd34dbcbcb9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "circuit depth = 251\n", + "two-qubit depth 165\n" + ] + } + ], + "source": [ + "from qiskit.circuit.library import unitary_overlap\n", + "\n", + "\n", + "overlap_circ = unitary_overlap(unitary1, unitary2)\n", + "overlap_circ.measure_all()\n", + "\n", + "print(\"circuit depth = \", overlap_circ.decompose(reps=2).depth())\n", + "print(\n", + " \"two-qubit depth\",\n", + " overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),\n", + ")\n", + "# overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "b3348ab6-ef4e-40d2-b803-e86484e0b8f3", + "metadata": {}, + "source": [ + "As described before, determining exactly how deep is too deep is nuanced. But a two-qubit depth of more than 100, even before transpilation is a non-starter. This is why custom feature maps have been emphasized throughout this lesson. If you know something about the structure of your entire dataset, you should design an entanglement map with that structure in mind. Here, since we are only calculating the inner product between two such data points, we have prioritized low circuit depth over any detailed consideration of data structure." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "784f8a68-0d77-433b-8482-bac38ddb8adc", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n", + "\n", + "# Prepare feature map for computing overlap\n", + "\n", + "entangler_map = [\n", + " [3, 4],\n", + " [2, 5],\n", + " [1, 4],\n", + " [2, 3],\n", + " [4, 6],\n", + " [7, 9],\n", + " [10, 11],\n", + " [9, 12],\n", + " [8, 11],\n", + " [9, 10],\n", + " [11, 13],\n", + " [14, 16],\n", + " [17, 18],\n", + " [16, 19],\n", + " [15, 18],\n", + " [16, 17],\n", + " [18, 20],\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "587daff1-7c5b-4505-8064-827c1fbdc742", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the entangler map above to build a feature map\n", + "\n", + "num_features = np.shape(train_data)[1]\n", + "num_qubits = int(num_features / 2)\n", + "\n", + "fm = QuantumCircuit(num_qubits)\n", + "training_param = Parameter(\"θ\")\n", + "feature_params = ParameterVector(\"x\", num_qubits * 2)\n", + "fm.ry(training_param, fm.qubits)\n", + "for cz in entangler_map:\n", + " fm.cz(cz[0], cz[1])\n", + "for i in range(num_qubits):\n", + " fm.rz(-2 * feature_params[2 * i + 1], i)\n", + " fm.rx(-2 * feature_params[2 * i], i)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02576ae9-8f24-4f47-8c37-ae0c3f710d4c", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import unitary_overlap\n", + "\n", + "# Assign features of each data point to a unitary, an instance of the general feature map.\n", + "\n", + "unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])\n", + "unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])\n", + "\n", + "# Create the overlap circuit\n", + "\n", + "overlap_circ = unitary_overlap(unitary1, unitary2)\n", + "overlap_circ.measure_all()" + ] + }, + { + "cell_type": "markdown", + "id": "fbe9ba90-6463-488d-b6f2-e9d4ee8f0871", + "metadata": {}, + "source": [ + "We won't bother checking the depths yet, since what really matters is the transpiled two-qubit depth." + ] + }, + { + "cell_type": "markdown", + "id": "9a5c14fc-aa58-461f-a1d5-fc644e042ecc", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum execution\n", + "\n", + "We start by selecting the least busy backend, then optimize our circuit for running on that backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99e3a8c7-9aba-4890-a492-cb3779b5de03", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Import needed packages\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Get the least busy backend\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", + ")\n", + "print(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "0b524533-7422-4d91-a3d1-82dfdeaf1588", + "metadata": {}, + "source": [ + "On small-scale jobs, a preset pass manager will often return the same circuit with the same depth, reliably. But in very large, complex circuits the pass manager can return different transpiled circuits each time it runs. This is because it is using heuristics, and because very large circuits will have a complicated landscape of possible optimizations. It is often useful to transpile a few times and take the shallowest circuit. This only introduces classical overhead and may substantially improve the results from the quantum computer.\n", + "\n", + "Here, we transpile the unitary overlap circuit 20 times, and look at the depths of the circuits obtained." + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "f5f8f833-8ac2-4df0-8268-4d83f841fd16", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "circuit depth = 61\n" + ] + } + ], + "source": [ + "# Apply level 3 optimization to our overlap circuit\n", + "transpiled_qcs = []\n", + "transpiled_depths = []\n", + "transpiled_twoqubit_depths = []\n", + "for i in range(1, 20):\n", + " pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", + " overlap_ibm = pm.run(overlap_circ)\n", + " transpiled_qcs.append(overlap_ibm)\n", + " transpiled_depths.append(overlap_ibm.decompose().depth())\n", + " transpiled_twoqubit_depths.append(\n", + " overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n", + " )\n", + "\n", + "print(\"circuit depth = \", overlap_ibm.decompose().depth())" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "fd604f87-32d1-42dd-a1fb-f2315cb4dfdc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]\n", + "[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]\n" + ] + } + ], + "source": [ + "print(transpiled_depths)\n", + "print(transpiled_twoqubit_depths)" + ] + }, + { + "cell_type": "markdown", + "id": "30fad0d8-f5e4-4dcf-946a-956f23334e9c", + "metadata": {}, + "source": [ + "Here you can see that there is some variation in the total gate depth with different transpilation passes. Our circuit is not yet deep/wide enough to see variation in the two-qubit transpiled depths. We will use the `transpiled_qcs[1]`, which has a depth of 60, just slightly lower than the depth of the deepest circuit obtained, which was 77." + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "28f00388-0e6e-41db-a0b4-9200d58ba5cb", + "metadata": {}, + "outputs": [], + "source": [ + "overlap_ibm = transpiled_qcs[1]" + ] + }, + { + "cell_type": "markdown", + "id": "e96cee51-e79f-4eab-829b-397645bc1080", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit Runtime Primitives\n", + "\n", + "As we scale closer to utility, simulators will not be useful. Only the syntax for real quantum computers is shown here." + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "7eedcf5f-5ead-4eb0-b423-669901f572ba", + "metadata": {}, + "outputs": [], + "source": [ + "# Run on ibm_osaka, 7-12-24, required 22 sec.\n", + "\n", + "# Import our runtime primitive\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "# Open a Runtime session:\n", + "session = Session(backend=backend)\n", + "num_shots = 10000\n", + "# Use sampler and get the counts\n", + "\n", + "sampler = Sampler(mode=session)\n", + "options = sampler.options\n", + "options.dynamical_decoupling.enable = True\n", + "options.twirling.enable_gates = True\n", + "counts = (\n", + " sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()\n", + ")\n", + "\n", + "# Close session after done\n", + "session.close()" + ] + }, + { + "cell_type": "markdown", + "id": "f40540b8-146d-4165-af12-db902232300d", + "metadata": {}, + "source": [ + "### Step 4: Post-process, return result in classical format\n", + "\n", + "As described in the introduction, the most useful measurement here is the probability of measuring the zero state $|00000\\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "id": "ce9f4dc1-b562-437e-b9fe-5a1781b67bfe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0138" + ] + }, + "execution_count": 103, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "counts.get(0, 0.0) / num_shots" + ] + }, + { + "cell_type": "markdown", + "id": "9b6c78b4-6788-404f-bd90-d98d7b288d42", + "metadata": {}, + "source": [ + "This process for the single kernel matrix element could be repeated between other data pairs in your set to obtain the full kernel matrix. The dimension of the kernel matrix is dictated by the number of points in your training data, not the number of features. So the computing cost of manipulating the kernel matrix into a predictive model does not scale like the number of features or qubits. Even for relatively small datasets with large numbers of features, the data would still need to be matched to a feature map that yields effective classification.\n", + "\n", + "### Scaling and future work\n", + "\n", + "The kernel method requires that we measure the $|0\\rangle$ as accurately as possible. But gate errors and readout errors mean that there is some non-zero probability $p$ that any given qubit will be erroneously measured to be in the $|1\\rangle$ state. Even with the oversimplification that the probability of $|0\\rangle$ should be $100\\%$, for many features encoded on, say, $N$ bits, the probability of correctly measuring all bits to be $|0\\rangle$ is reduced to $(1-p)^N$. As $N$ becomes large, this method becomes less and less reliable. Overcoming this difficulty and scaling kernel estimation to more and more features is an area of current research. To learn more about this issue, see this work by [Thanasilp, Wang, Cerezo, and Holmes.](https://www.nature.com/articles/s41467-024-49287-w) We recommend you explore what can be done with current quantum computers, and also look forward to what will be possible in the era of error correction." + ] + }, + { + "cell_type": "markdown", + "id": "556eb119-2b22-4bd9-b82e-c3a6ea31cfa1", + "metadata": {}, + "source": [ + "### Review\n", + "\n", + "Calculating a quantum kernel involves\n", + "* calculating kernel matrix entries, using pairs of training data points\n", + "* encoding the data and mapping it via a feature mapping\n", + "* optimizing your circuit for running on real quantum computers / backends\n", + "\n", + "The quantum kernel can then be used in classical machine learning algorithms, as in this lesson.\n", + "\n", + "Some key things to keep in mind when using quantum kernels include:\n", + "* Is the dataset likely to benefit from quantum kernel methods?\n", + "* Try different feature maps and entanglement schemes.\n", + "* Is the circuit depth acceptable?\n", + "* Try running a pass manager multiple times and use the smallest-depth circuit you can get.\n", + "\n", + "Quantum kernel methods are potentially powerful tools given a proper match between datasets with quantum-amenable features, and a suitable quantum feature map. To better understand where quantum kernels are likely to be useful, we recommend reading [Liu, Arunachalam & Temme (2021)](https://www.nature.com/articles/s41567-021-01287-z)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/courses/quantum-machine-learning/qvc-qnn.ipynb b/learning/courses/quantum-machine-learning/qvc-qnn.ipynb index 2afc3b891ee..ecd468005d5 100644 --- a/learning/courses/quantum-machine-learning/qvc-qnn.ipynb +++ b/learning/courses/quantum-machine-learning/qvc-qnn.ipynb @@ -1,2026 +1,2041 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c458cae8-4c92-46f8-b914-49fc03920c01", - "metadata": {}, - "source": [ - "---\n", - "title: QVCs and QNNs\n", - "description: Quantum variational circuits are applied to the recognition of patterns in an image. The parallels with classical neural networks are discussed.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore yticks hspace VQCs linesize, imshow, minpos, nonumber, zzcircuit, zcircuit, pcircuit, zzcx, Schuld, Francesco, Peruccione, Vojtech, Adrian, Perez, Cervera, Lierta, Elies, Fuster, Jose, Latorre, Sweke, Jakob */}\n", - "\n", - "# Quantum Variational Circuits and Quantum Neural Networks\n", - "\n", - "In this lesson, we implement several variational quantum circuits for a data classification task, so-called variational quantum classifiers (VQCs). At one point, it was common to refer to a subset of VQCs as quantum neural networks (QNNs) in analogy with classical neural networks. Indeed, there are cases where structures borrowed from classical neural networks, such as convolution layers, play an important role in VQCs. In such cases where the analogy is strong, QNNs may be a useful description. But parameterized quantum circuits need not follow the general structure of a neural network; for example, not all data need to be loaded in the first (input) layer; we can load some data in the first layer, apply some gates and then load additional data (a process called data \"reuploading\"). We should therefore think of QNNs as a subset of parameterized quantum circuits, and we should not be limited in our exploration of useful quantum circuits by the analogy to classical neural networks.\n", - "\n", - "The dataset being addressed in this lesson consists of images containing horizontal and vertical stripes, and our goal is to label unseen images into one of the two categories depending on the orientation of their line. We will accomplish this with a VQC. As we go, we will address ways in which the calculation can be improved and scaled. The dataset here is exceptionally easy to classify classically. It has been chosen for its simplicity so we can focus on the quantum part of this problem, and look at how a dataset attribute might translate to a part of a quantum circuit. It is not reasonable to expect a quantum speed-up for such simple cases where classical algorithms are so efficient.\n", - "\n", - "By the end of this lesson you should be able to:\n", - "* Load data from an image into a quantum circuit\n", - "* Construct an ansatz for a VQC (or QNN), and adjust it to fit your problem\n", - "* Train your VQC/QNN and use it to make accurate predictions on test data\n", - "* Scale the problem, and recognize limits of current quantum computers" - ] - }, - { - "cell_type": "markdown", - "id": "5a4c9c5f-9c86-402f-b50d-8632806423f4", - "metadata": {}, - "source": [ - "## Data generation\n", - "\n", - "We will start by constructing the data. Data sets are often not explicitly generated as part of the Qiskit patterns framework. But data type and preparation is critical to successfully applying quantum computing to machine learning. The code below defines a data set of images with set pixel dimensions. One full row or column of the image is assigned the value $\\pi/2$, and the remaining pixels are assigned random values on the interval $(0,\\pi/4)$. The random values are noise in our data. Glance through the code to make sure you understand how the images are generated. Later on we will scale up the images." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af2b9990-e1be-4f3a-ae66-cecef55ede92", - "metadata": {}, - "outputs": [], - "source": [ - "# This code defines the images to be classified:\n", - "\n", - "import numpy as np\n", - "\n", - "# Total number of \"pixels\"/qubits\n", - "size = 8\n", - "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", - "vert_size = 2\n", - "# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`\n", - "line_size = 2\n", - "\n", - "\n", - "def generate_dataset(num_images):\n", - " images = []\n", - " labels = []\n", - " hor_array = np.zeros((size - (line_size - 1) * vert_size, size))\n", - " ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))\n", - "\n", - " j = 0\n", - " for i in range(0, size - 1):\n", - " if i % (size / vert_size) <= (size / vert_size) - line_size:\n", - " for p in range(0, line_size):\n", - " hor_array[j][i + p] = np.pi / 2\n", - " j += 1\n", - "\n", - " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\" at size/vert_size - linesize, because we want to fold this list into a grid.\n", - "\n", - " j = 0\n", - " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", - " for p in range(0, line_size):\n", - " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", - " j += 1\n", - "\n", - " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.\n", - "\n", - " for n in range(num_images):\n", - " rng = np.random.randint(0, 2)\n", - " if rng == 0:\n", - " labels.append(-1)\n", - " random_image = np.random.randint(0, len(hor_array))\n", - " images.append(np.array(hor_array[random_image]))\n", - "\n", - " elif rng == 1:\n", - " labels.append(1)\n", - " random_image = np.random.randint(0, len(ver_array))\n", - " images.append(np.array(ver_array[random_image]))\n", - " # Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding label.\n", - "\n", - " # Create noise\n", - " for i in range(size):\n", - " if images[-1][i] == 0:\n", - " images[-1][i] = np.random.rand() * np.pi / 4\n", - " return images, labels\n", - "\n", - "\n", - "hor_size = round(size / vert_size)" - ] - }, - { - "cell_type": "markdown", - "id": "e6fb6709-158b-43c0-a1f5-fc6f54b56beb", - "metadata": {}, - "source": [ - "Note that the code above has also generated labels indicated whether the images contain a vertical (+1) or horizontal (-1) line. We will now use sklearn to split a data set of 100 images into a training and testing set (along with their corresponding labels). Here, we use $70%$ of the data set for training, with the remaining $30%$ withheld for testing." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c87a2336-ea3e-4902-a6c2-02b638da4585", - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.model_selection import train_test_split\n", - "\n", - "np.random.seed(42)\n", - "images, labels = generate_dataset(200)\n", - "\n", - "train_images, test_images, train_labels, test_labels = train_test_split(\n", - " images, labels, test_size=0.3, random_state=246\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "cf4576f1-3494-48d0-b5f0-7090af5b3b32", - "metadata": {}, - "source": [ - "Let's plot a few elements of our data set to see what these lines look like:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "ac9e4239-8c1a-4798-a8e7-80347c29150c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "# Make subplot titles so we can identify categories\n", - "titles = []\n", - "for i in range(8):\n", - " title = \"category: \" + str(train_labels[i])\n", - " titles.append(title)\n", - "\n", - "# Generate a figure with nested images using subplots.\n", - "fig, ax = plt.subplots(4, 2, figsize=(10, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", - "\n", - "for i in range(8):\n", - " ax[i // 2, i % 2].imshow(\n", - " train_images[i].reshape(vert_size, hor_size),\n", - " aspect=\"equal\",\n", - " )\n", - " ax[i // 2, i % 2].set_title(titles[i])\n", - "plt.subplots_adjust(wspace=0.1, hspace=0.3)" - ] - }, - { - "cell_type": "markdown", - "id": "ffae1e62-4462-474f-a0ee-30f797fbffba", - "metadata": {}, - "source": [ - "Each of these images is still paired with its label in ```train_labels``` in a simple list form:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "93ccb862-ad75-403d-81aa-04c5f841c432", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1, 1, 1, 1, -1, 1, 1, 1]\n" - ] - } - ], - "source": [ - "print(train_labels[:8])" - ] - }, - { - "cell_type": "markdown", - "id": "c1e55d16-3af4-4629-848b-89c575b6f6df", - "metadata": {}, - "source": [ - "## Variational quantum classifier: a first attempt\n", - "\n", - "### Qiskit patterns step 1: Map the problem to a quantum circuit\n", - "\n", - "The goal is to find a function $f$ with parameters $\\theta$ that maps a data vector / image $\\vec{x}$ to the correct category: $f_\\theta(\\vec{x}) \\rightarrow \\pm1$. This will be accomplished using a VQC with few layers that can be identified by their distinct purposes:\n", - "$$\n", - "f_\\theta(\\vec{x}) = \\langle 0|U^{\\dagger}(\\vec{x})W^\\dagger(\\theta)OW(\\theta)U(\\vec{x})|0\\rangle\n", - "$$\n", - "Here, $U(\\vec{x})$ is the encoding circuit, for which we have many options as seen in previous lessons. $W(\\theta)$ is a variational, or trainable circuit block, and $\\theta$ is the set of parameters to be trained. Those parameters will be varied by classical optimization algorithms to find the set of parameters that yields the best classification of images by the quantum circuit. This variational circuit is sometimes called the \"ansatz\". Finally, $O$ is some observable that will be estimated using the Estimator primitive. There is no constraint that forces the layers to come in this order, or even to be fully separate. One could have multiple variational and/or encoding layers in any order that is technically motivated.\n", - "\n", - "We start by choosing a feature map to encode our data. We will use the ```z_feature_map```, as it keeps circuit depths low compared to some other feature mappings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "83adf797-5c8f-4bba-826a-fa3c82d7affb", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import z_feature_map\n", - "\n", - "# One qubit per data feature\n", - "num_qubits = len(train_images[0])\n", - "\n", - "# Data encoding\n", - "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure our data encoding goes to the first part of the circuit, the feature mapping.\n", - "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" - ] - }, - { - "cell_type": "markdown", - "id": "3dd4d3d3-7a94-410d-856b-be27dad481a7", - "metadata": {}, - "source": [ - "We must now decide on an ansatz to be trained. There are many considerations when selecting an ansatz. A complete description is beyond the scope of this introduction; here we simply point out a few categories of considerations.\n", - "\n", - "1. **Hardware:** All modern quantum computers are more prone to errors and more susceptible to noise than their classical counterparts. Using an ansatz that is excessively deep (especially in transpiled, two-qubit depth) will not produce good results. A related issue is that quantum computers have some qubit layout, meaning that some physical qubits are adjacent on the quantum computer, and others may be very far from each other. Entangling adjacent qubits does not increase the depth by too much, but entangling very distant qubits can increase depth substantially, as we must insert swap gates to move information onto qubits that are adjacent in order for them to be entangled.\n", - "2. **The problem:** Whenever you have some information about your problem that could guide your ansatz, make use of it. For example, the data in this lesson is made up of images of horizontal and vertical lines. One could consider what correlation between adjacent colors/values identifies an image of a horizontal or vertical line. What attributes of an ansatz would correspond to this correlation between adjacent pixels? We will revisit this point more technically later in this lesson. But for now, let us simply say that including entanglement and CNOT gates between qubits corresponding to adjacent pixels seems like a good idea. In the bigger picture, consider whether the problem is actually best solved using a quantum circuit, or whether classical algorithms might exist that can do as good a job.\n", - "3. **Number of parameters:** Each independently parameterized quantum gate in the circuit increases the space to be classically optimized, and this results in slower convergence. But as problems scale up, one may encounter *barren plateaus*. This term refers to a phenomenon where the optimization landscape of a variational quantum algorithm becomes exponentially flat and featureless as the problem size increases. This causes vanishing gradients, making it difficult to effectively train the algorithm[\\[1\\]](#references). Barren plateaus are relevant to variational quantum algorithms like VQCs/QNNs. It should be noted that the increasing number of parameters is not the only consideration in avoiding barren plateaus; other considerations include global cost functions and random parameter initialization.\n", - "\n", - "In this lesson we will see a few simple examples of good practices in ansatz construction. Let us first try the ansatz below. We will return to revise it, later." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf308e60-9366-4c23-8079-366f95ba4790", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "5\n", - "2+ qubit depth: 3\n" - ] - }, - { - "data": { - "text/plain": [ - " ┌──────────┐ ┌──────────┐ \n", - "q_0: ┤ Ry(θ[0]) ├──────■──────┤ Rx(θ[8]) ├─────────────────────────\n", - " ├──────────┤ ┌─┴─┐ └──────────┘┌──────────┐ \n", - "q_1: ┤ Ry(θ[1]) ├────┤ X ├─────────■──────┤ Rx(θ[9]) ├─────────────\n", - " ├──────────┤ └───┘ ┌─┴─┐ └──────────┘┌───────────┐\n", - "q_2: ┤ Ry(θ[2]) ├────────────────┤ X ├─────────■──────┤ Rx(θ[10]) ├\n", - " ├──────────┤ └───┘ ┌─┴─┐ ├───────────┤\n", - "q_3: ┤ Ry(θ[3]) ├────────────────────────────┤ X ├────┤ Rx(θ[11]) ├\n", - " ├──────────┤┌───────────┐ └───┘ └───────────┘\n", - "q_4: ┤ Ry(θ[4]) ├┤ Rx(θ[12]) ├─────────────────────────────────────\n", - " ├──────────┤├───────────┤ \n", - "q_5: ┤ Ry(θ[5]) ├┤ Rx(θ[13]) ├─────────────────────────────────────\n", - " ├──────────┤├───────────┤ \n", - "q_6: ┤ Ry(θ[6]) ├┤ Rx(θ[14]) ├─────────────────────────────────────\n", - " ├──────────┤├───────────┤ \n", - "q_7: ┤ Ry(θ[7]) ├┤ Rx(θ[15]) ├─────────────────────────────────────\n", - " └──────────┘└───────────┘ " - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Import the necessary packages\n", - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit import ParameterVector\n", - "\n", - "# Initialize the circuit using the same number of qubits as the image has pixels\n", - "qnn_circuit = QuantumCircuit(size)\n", - "\n", - "# We choose to have two variational parameters for each qubit.\n", - "params = ParameterVector(\"θ\", length=2 * size)\n", - "\n", - "# A first variational layer:\n", - "for i in range(size):\n", - " qnn_circuit.ry(params[i], i)\n", - "\n", - "# Here is a list of qubit pairs between which we want CNOT gates. The choice of these is not yet obvious.\n", - "qnn_cnot_list = [[0, 1], [1, 2], [2, 3]]\n", - "\n", - "for i in range(len(qnn_cnot_list)):\n", - " qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])\n", - "\n", - "# The second variational layer:\n", - "for i in range(size):\n", - " qnn_circuit.rx(params[size + i], i)\n", - "\n", - "# Check the circuit depth, and the two-qubit gate depth\n", - "print(qnn_circuit.decompose().depth())\n", - "print(\n", - " f\"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", - ")\n", - "\n", - "# Draw the circuit\n", - "qnn_circuit.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "c01b0b2a-9ce7-442d-a7be-faa4562e1da4", - "metadata": {}, - "source": [ - "With the data encoding and variational circuit prepared, we can combine them to form our full ansatz. In this case, the components of our quantum circuit are quite analogous to those in neural networks, with $U(\\vec{x})$ being most similar to the layer that loads input values from the image, and $W(\\theta)$ being like the layer of variable \"weights\". Since this analogy holds in this case, we are adopting \"qnn\" in some of our naming conventions; but this analogy should not be limiting in your exploration of VQCs.\n", - "\n", - "![QML_CR_background_QNN_circuit-2.png](/learning/images/courses/quantum-machine-learning/qvc-qnn/qml-cr-background-qnn-circuit.avif)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "e84e5ac1-d5fb-4ec0-8a23-5a2c417a4088", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# QNN ansatz\n", - "ansatz = qnn_circuit\n", - "\n", - "# Combine the feature map with the ansatz\n", - "full_circuit = QuantumCircuit(num_qubits)\n", - "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", - "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", - "\n", - "# Display the circuit\n", - "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "90ff05da-0435-4fcf-94cc-181c1649490b", - "metadata": {}, - "source": [ - "We must now define an observable, so we can use it in our cost function. We will obtain an expectation value for this observable using Estimator. If we have selected a good, problem-motivated ansatz, then each qubit will contain information relevant to classification. One can add layers to combine information onto fewer qubits (called a *convolutional layer*), such that measurements are only needed on a subset of the qubits in the circuit (as in convolutional neural networks). Or one can measure some attribute from each qubit. Here we will opt for the latter, so we include a ```Z``` operator for each qubit. There is nothing unique about choosing $Z$, but it is well motivated:\n", - "* This is a binary classification task, and a measurement of $Z$ can yield two possible outcomes.\n", - "* The eigenvalues of $Z$ ($\\pm 1$) are reasonably well separated, and result in an estimator outcome in interval [-1, +1], where 0 can simply be used as a cutoff value.\n", - "* It is straightforward to measure in Pauli Z basis with no extra gate overhead.\n", - "\n", - "So, Z is a very natural choice." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "9cf2eb5f-2455-4f2f-a420-c68836ebe917", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "observable = SparsePauliOp.from_list([(\"Z\" * (num_qubits), 1)])" - ] - }, - { - "cell_type": "markdown", - "id": "3854607b-ee8d-4b9d-913d-b8fd6b722008", - "metadata": {}, - "source": [ - "We have our quantum circuit and the observable we want to estimate. Now we need a few things in order to run and optimize this circuit. First, we need a function to run a forward pass. Note that the function below takes in the ```input_params``` and ```weight_params``` separately. The former is the set of static parameters describing the data in an image, and the latter is the set of variable parameters to be optimized." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "9b702a54-89a1-4973-9f11-8baf7ddc7248", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.primitives import BaseEstimatorV2\n", - "from qiskit.quantum_info.operators.base_operator import BaseOperator\n", - "\n", - "\n", - "def forward(\n", - " circuit: QuantumCircuit,\n", - " input_params: np.ndarray,\n", - " weight_params: np.ndarray,\n", - " estimator: BaseEstimatorV2,\n", - " observable: BaseOperator,\n", - ") -> np.ndarray:\n", - " \"\"\"\n", - " Forward pass of the neural network.\n", - "\n", - " Args:\n", - " circuit: circuit consisting of data loader gates and the neural network ansatz.\n", - " input_params: data encoding parameters.\n", - " weight_params: neural network ansatz parameters.\n", - " estimator: EstimatorV2 primitive.\n", - " observable: a single observable to compute the expectation over.\n", - "\n", - " Returns:\n", - " expectation_values: an array (for one observable) or a matrix (for a sequence of observables) of expectation values.\n", - " Rows correspond to observables and columns to data samples.\n", - " \"\"\"\n", - " num_samples = input_params.shape[0]\n", - " weights = np.broadcast_to(weight_params, (num_samples, len(weight_params)))\n", - " params = np.concatenate((input_params, weights), axis=1)\n", - " pub = (circuit, observable, params)\n", - " job = estimator.run([pub])\n", - " result = job.result()[0]\n", - " expectation_values = result.data.evs\n", - "\n", - " return expectation_values" - ] - }, - { - "cell_type": "markdown", - "id": "11b0403f-e3a6-44c9-b461-9d059160ecf6", - "metadata": {}, - "source": [ - "### Loss function\n", - "Next, we need a loss function to calculate the difference between the predicted and calculated values of the labels. The function will take in the labels predicted by the algorithm and the correct labels and return the mean squared difference. There any many different loss functions. Here, MSE is an example that we chose." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "5c79d67a-e1b4-41cf-aa29-5bd4a4f8ddd3", - "metadata": {}, - "outputs": [], - "source": [ - "def mse_loss(predict: np.ndarray, target: np.ndarray) -> np.ndarray:\n", - " \"\"\"\n", - " Mean squared error (MSE).\n", - "\n", - " prediction: predictions from the forward pass of neural network.\n", - " target: true labels.\n", - "\n", - " output: MSE loss.\n", - " \"\"\"\n", - " if len(predict.shape) <= 1:\n", - " return ((predict - target) ** 2).mean()\n", - " else:\n", - " raise AssertionError(\"input should be 1d-array\")" - ] - }, - { - "cell_type": "markdown", - "id": "b1d13267-a336-489a-bf7c-55ef0c4e4f20", - "metadata": {}, - "source": [ - "Let us also define a slightly different loss function that is a function of the variable parameters (weights), for use by the classical optimizer. This function only takes the ansatz parameters as input; other variables for the forward pass and the loss are set as global parameters. The optimizer will train the model by sampling different weights and attempting to lower the output of the cost/loss function." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "3bc33b0e-eace-4ebe-acff-ee15a8273fc5", - "metadata": {}, - "outputs": [], - "source": [ - "def mse_loss_weights(weight_params: np.ndarray) -> np.ndarray:\n", - " \"\"\"\n", - " Cost function for the optimizer to update the ansatz parameters.\n", - "\n", - " weight_params: ansatz parameters to be updated by the optimizer.\n", - "\n", - " output: MSE loss.\n", - " \"\"\"\n", - " predictions = forward(\n", - " circuit=circuit,\n", - " input_params=input_params,\n", - " weight_params=weight_params,\n", - " estimator=estimator,\n", - " observable=observable,\n", - " )\n", - "\n", - " cost = mse_loss(predict=predictions, target=target)\n", - " objective_func_vals.append(cost)\n", - "\n", - " global iter\n", - " if iter % 50 == 0:\n", - " print(f\"Iter: {iter}, loss: {cost}\")\n", - " iter += 1\n", - "\n", - " return cost" - ] - }, - { - "cell_type": "markdown", - "id": "f8c9b744-5c01-49e4-a8b0-3d39b0be7678", - "metadata": {}, - "source": [ - "Above we referred to using a classical optimizer. When we get to searching through weights to minimize the cost function, we will use the optimizer COBYLA:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d4d24583-5328-48b8-9cd5-f55655ce9c6e", - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.optimize import minimize" - ] - }, - { - "cell_type": "markdown", - "id": "7a580ff4-e93a-40d1-8ebd-e2f91ad79fcf", - "metadata": {}, - "source": [ - "We will set some initial global variables for the cost function." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "1f115364-ab1d-45e1-a83e-03adab7af304", - "metadata": {}, - "outputs": [], - "source": [ - "# Globals\n", - "circuit = full_circuit\n", - "observables = observable\n", - "# input_params = train_images_batch\n", - "# target = train_labels_batch\n", - "objective_func_vals = []\n", - "iter = 0" - ] - }, - { - "cell_type": "markdown", - "id": "f6ff34b6-cd3a-4755-8f69-ce891a5b0b8f", - "metadata": {}, - "source": [ - "## Qiskit Patterns Step 2: Optimize problem for quantum execution\n", - "We start by selecting a backend for execution. In this case, we will use the least-busy backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d68f1b36-f181-4880-9cb8-260bfc817b09", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_brisbane\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "print(backend.name)" - ] - }, - { - "cell_type": "markdown", - "id": "c8657861-ffee-4815-a7a4-2a51369c6301", - "metadata": {}, - "source": [ - "Here we optimize the circuit for running on a real backend by specifying the optimization_level and adding dynamical decoupling. The code below generates a pass manager using preset pass managers from qiskit.transpiler." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d2db83d-3d33-4159-aa3f-297e9507a497", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import XGate\n", - "from qiskit.transpiler import PassManager\n", - "from qiskit.transpiler.passes import (\n", - " ALAPScheduleAnalysis,\n", - " ConstrainedReschedule,\n", - " PadDynamicalDecoupling,\n", - ")\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "pm.scheduling = PassManager(\n", - " [\n", - " ALAPScheduleAnalysis(target=target),\n", - " ConstrainedReschedule(\n", - " acquire_alignment=target.acquire_alignment,\n", - " pulse_alignment=target.pulse_alignment,\n", - " target=target,\n", - " ),\n", - " PadDynamicalDecoupling(\n", - " target=target,\n", - " dd_sequence=[XGate(), XGate()],\n", - " pulse_alignment=target.pulse_alignment,\n", - " ),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "9fb30d51-4455-4e2e-806c-8090819383b4", - "metadata": {}, - "source": [ - "Now we use the pass manager on the circuit. The layout changes that result must be applied to the observable as well. For very large circuits, the heuristics used in circuit optimization may not always yield the best and shallowest circuit. In those cases, it makes sense to run such pass managers several times and use the best circuit. We will see this later when we scale up our calculation." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "5d19c24d-48f5-48a0-bcee-12eab0468935", - "metadata": {}, - "outputs": [], - "source": [ - "circuit_ibm = pm.run(full_circuit)\n", - "observable_ibm = observable.apply_layout(circuit_ibm.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "6734be44-e544-4fab-aaaa-fe8a85257ecb", - "metadata": {}, - "source": [ - "## Qiskit Patterns Step 3: Execute using Qiskit Primitives\n", - "\n", - "### Loop over the dataset in batches and epochs\n", - "We first implement the full algorithm using a simulator for cursory debugging and for estimates of error. We can now go over the entire dataset in batches in desired number of epochs to train our quantum neural network." - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "5ef31f11-e8c3-4e8d-86d3-2373113e4cdd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch: 0, batch: 0\n", - "Iter: 0, loss: 1.0002309063537163\n", - "Iter: 50, loss: 0.9434121445008878\n" - ] - } - ], - "source": [ - "from qiskit.primitives import StatevectorEstimator as Estimator\n", - "\n", - "batch_size = 140\n", - "num_epochs = 1\n", - "num_samples = len(train_images)\n", - "\n", - "# Globals\n", - "circuit = full_circuit\n", - "estimator = Estimator() # simulator for debugging\n", - "observables = observable\n", - "objective_func_vals = []\n", - "iter = 0\n", - "\n", - "# Random initial weights for the ansatz\n", - "np.random.seed(42)\n", - "weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", - "\n", - "for epoch in range(num_epochs):\n", - " for i in range((num_samples - 1) // batch_size + 1):\n", - " print(f\"Epoch: {epoch}, batch: {i}\")\n", - " start_i = i * batch_size\n", - " end_i = start_i + batch_size\n", - " train_images_batch = np.array(train_images[start_i:end_i])\n", - " train_labels_batch = np.array(train_labels[start_i:end_i])\n", - " input_params = train_images_batch\n", - " target = train_labels_batch\n", - " iter = 0\n", - " res = minimize(\n", - " mse_loss_weights, weight_params, method=\"COBYLA\", options={\"maxiter\": 100}\n", - " )\n", - " weight_params = res[\"x\"]" - ] - }, - { - "cell_type": "markdown", - "id": "6198a923-3edb-44ff-a081-cff91e62c325", - "metadata": {}, - "source": [ - "## Qiskit Patterns Step 4: Post-process, return result in classical format\n", - "### Testing and accuracy\n", - "We now interpret the results from training. We first test the training accuracy over the training set." - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "d4badf34-7f0a-4319-8f6c-c74ea7c32ce5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-2.27688499e-02 -1.46227204e-02 -1.73927452e-02 9.93331786e-02\n", - " -4.85553548e-01 1.43558565e-01 8.34567054e-02 -1.40133992e-02\n", - " 1.52169596e-01 -1.95082515e-01 8.24373578e-03 -9.90696638e-02\n", - " -3.54268344e-02 -4.77017954e-01 1.38713848e-02 -2.99706215e-01\n", - " -5.78378029e-02 3.25528779e-02 -4.11354239e-02 -1.06483708e-01\n", - " 1.53095800e-01 2.90110884e-02 1.25745450e-02 6.46323079e-02\n", - " -1.53538943e-01 -1.57694952e-02 -1.67800067e-02 -1.99820822e-01\n", - " 1.70360075e-01 7.86148038e-03 -2.33373818e-02 6.64233020e-02\n", - " -1.14895445e-01 -1.11296215e-01 1.15120303e-01 -2.94096140e-01\n", - " -1.00531392e-03 -1.69209726e-01 -1.26120885e-01 3.26298176e-02\n", - " -1.33517383e-02 -5.86983444e-02 -4.32341361e-01 -4.36509551e-01\n", - " -4.17940102e-02 1.76935235e-03 8.14479984e-03 1.86985655e-01\n", - " -2.75525019e-01 -1.63229907e-03 -1.08571055e-01 -7.37452387e-04\n", - " -6.44440657e-02 6.72812834e-04 2.16785530e-03 1.41381850e-01\n", - " -9.82570410e-02 4.35973325e-01 -7.62261965e-02 -1.86193980e-01\n", - " -1.56971183e-02 -4.02757541e-01 -1.53869367e-01 2.29262129e-02\n", - " -7.02788246e-03 3.65719683e-02 4.68232163e-01 2.36434668e-02\n", - " -2.59520939e-02 3.70550137e-01 -1.19630110e-01 -5.79555318e-02\n", - " 2.09554455e-01 5.04689780e-02 7.39494314e-02 -1.77647326e-02\n", - " -1.45407207e-01 -9.54908878e-02 7.56029640e-02 -2.74049696e-02\n", - " 3.34885873e-01 1.58546171e-03 1.09339091e-01 -8.84693274e-02\n", - " -2.36450457e-02 1.41892239e-01 -2.34453218e-01 -7.50717757e-02\n", - " -1.13281310e-01 -1.66649414e-01 -3.17224197e-01 -6.38220597e-02\n", - " 3.28916563e-02 3.04739203e-02 2.67720196e-02 -1.16485785e-01\n", - " -3.08115732e-02 -2.95372010e-02 -7.54669023e-02 6.20013872e-02\n", - " -3.85258710e-01 -1.16456443e-01 -7.38548075e-02 -3.20558243e-02\n", - " -4.22284741e-02 1.01285659e-01 -1.76949246e-01 -2.02767491e-01\n", - " -1.12407344e-01 -3.81408267e-02 -4.33345231e-01 -9.24507501e-02\n", - " -4.21765393e-02 -6.06533771e-02 -2.22257783e-01 -1.17312535e-01\n", - " -6.74132262e-02 -2.76206274e-01 -9.13971800e-02 -2.27653991e-01\n", - " 1.66358563e-01 2.17230774e-04 5.76426304e-02 -2.82079169e-02\n", - " -1.15482051e-01 -3.46716009e-01 -3.21448755e-01 -5.20041405e-02\n", - " -2.16833625e-01 -1.06154654e-02 -7.74854811e-02 -3.28257935e-01\n", - " -7.83242410e-02 1.65547682e-01 -2.55294862e-01 -8.89085025e-02\n", - " 4.47581491e-01 1.92351832e-02 2.74083885e-02 -3.61304571e-01]\n", - "[-1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. -1. -1. -1. 1. -1. -1. 1.\n", - " -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. 1. 1. -1. 1. -1. -1. 1. -1.\n", - " -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1. -1. -1. 1.\n", - " 1. 1. -1. 1. -1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. 1. -1. -1.\n", - " 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.\n", - " -1. -1. 1. 1. 1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. -1. -1.\n", - " -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1.\n", - " -1. -1. -1. -1. -1. -1. -1. 1. -1. -1. 1. 1. 1. -1.]\n", - "[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]\n", - "Train accuracy: 60.0%\n" - ] - } - ], - "source": [ - "import copy\n", - "from sklearn.metrics import accuracy_score\n", - "from qiskit.primitives import StatevectorEstimator as Estimator # simulator\n", - "# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer\n", - "\n", - "estimator = Estimator()\n", - "# estimator = Estimator(backend=backend)\n", - "\n", - "pred_train = forward(circuit, np.array(train_images), res[\"x\"], estimator, observable)\n", - "# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)\n", - "\n", - "print(pred_train)\n", - "\n", - "pred_train_labels = copy.deepcopy(pred_train)\n", - "pred_train_labels[pred_train_labels >= 0] = 1\n", - "pred_train_labels[pred_train_labels < 0] = -1\n", - "print(pred_train_labels)\n", - "print(train_labels)\n", - "\n", - "accuracy = accuracy_score(train_labels, pred_train_labels)\n", - "print(f\"Train accuracy: {accuracy * 100}%\")" - ] - }, - { - "cell_type": "markdown", - "id": "68003805-e24e-41db-a0c5-c19dab10a286", - "metadata": {}, - "source": [ - "The training accuracy is only $60%$, which is definitely not good. It is hard to imagine that the model's performance on the test set could be any better. Let's verify." - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "50876c60-9fb5-4e0a-8a08-b7ca3694679b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-2.77978120e-01 -2.62194862e-01 4.59636095e-02 -8.09344165e-02\n", - " -2.97362966e-01 9.22947242e-02 2.06693174e-01 3.31629460e-02\n", - " 1.10971762e-03 -2.14602152e-01 -1.62671993e-01 -6.07179155e-04\n", - " -1.59948633e-01 -8.55722523e-02 -1.13057027e-01 -3.00187433e-01\n", - " -2.92832827e-01 7.38580629e-02 -6.03706270e-02 -8.57643552e-02\n", - " -1.52402062e-02 -3.57505447e-01 -3.54890597e-02 1.36534749e-01\n", - " -1.54688180e-01 -2.93714726e-01 1.89548513e-02 -6.15715564e-02\n", - " 1.11042670e-01 -2.22861100e-02 -3.84230105e-02 1.67351034e-01\n", - " -8.38766333e-02 2.56348613e-01 -1.10653111e-01 -1.18989476e-01\n", - " -6.75723266e-05 -6.88580547e-02 1.02431393e-02 -2.42125353e-01\n", - " -1.09142367e-01 -1.22540757e-01 -1.63735850e-01 3.93334838e-01\n", - " 2.36705685e-01 -2.34259814e-02 -3.91877756e-02 -1.95106746e-01\n", - " 1.86707523e-01 4.74775215e-02 -4.24907432e-02 -2.06453265e-01\n", - " 4.09184710e-02 -3.54762080e-02 -9.47513112e-02 2.97270112e-01\n", - " -2.99708696e-02 9.93941064e-03 -1.26760302e-01 -1.36183355e-01]\n", - "[-1. -1. 1. -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. -1. -1. -1. -1. 1.\n", - " -1. -1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. -1. 1. -1. 1. -1. -1.\n", - " -1. -1. 1. -1. -1. -1. -1. 1. 1. -1. -1. -1. 1. 1. -1. -1. 1. -1.\n", - " -1. 1. -1. 1. -1. -1.]\n", - "[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]\n", - "Test accuracy: 60.0%\n" - ] - } - ], - "source": [ - "pred_test = forward(circuit, np.array(test_images), res[\"x\"], estimator, observable)\n", - "# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)\n", - "\n", - "print(pred_test)\n", - "\n", - "pred_test_labels = copy.deepcopy(pred_test)\n", - "pred_test_labels[pred_test_labels >= 0] = 1\n", - "pred_test_labels[pred_test_labels < 0] = -1\n", - "print(pred_test_labels)\n", - "print(test_labels)\n", - "\n", - "accuracy = accuracy_score(test_labels, pred_test_labels)\n", - "print(f\"Test accuracy: {accuracy * 100}%\")" - ] - }, - { - "cell_type": "markdown", - "id": "06d5336b-5ec6-4080-a983-9ca70faec269", - "metadata": {}, - "source": [ - "The model is not classifying these data well. We should ask why this is, and in particular, we should check:\n", - "* Did we stop the training too soon? Were more optimization steps needed?\n", - "* Did we construct a bad ansatz? This could mean a lot of things. When we work on real quantum computers, circuit depth will be a major consideration. The number of parameters is also potentially important, as is the entangling between qubits.\n", - "* Combining the two above, did we construct an ansatz with too many parameters to be trainable?\n", - "\n", - "We can start by checking for convergence in the optimization:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ecfddaa-7bd4-43d0-bfec-bd112854261e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "obj_func_vals_first = objective_func_vals\n", - "# import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 6))\n", - "plt.plot(obj_func_vals_first, label=\"first ansatz\")\n", - "plt.xlabel(\"iteration\")\n", - "plt.ylabel(\"loss\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "c48ea583-7746-45ee-9437-c14ea11c773e", - "metadata": {}, - "source": [ - "We might try extending the optimization steps to make sure the optimizer didn't just get stuck in a local minimum in parameter space. But it looks fairly converged. Let's take a closer look at the images that were *not* classified correctly, and see if we can understand what is happening." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "6ce6c563-dbc2-4b79-8cad-05c8e6db4197", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "24\n" - ] - } - ], - "source": [ - "missed = []\n", - "for i in range(len(test_labels)):\n", - " if pred_test_labels[i] != test_labels[i]:\n", - " missed.append(test_images[i])\n", - "print(len(missed))" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "81ee504e-1c94-4600-8ab2-6406c473df73", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots(12, 2, figsize=(6, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", - "for i in range(len(missed)):\n", - " ax[i // 2, i % 2].imshow(\n", - " missed[i].reshape(vert_size, hor_size),\n", - " aspect=\"equal\",\n", - " )\n", - "plt.subplots_adjust(wspace=0.02, hspace=0.025)" - ] - }, - { - "cell_type": "markdown", - "id": "d8c9e984-b21c-4aaf-8d14-bc1458bb7878", - "metadata": {}, - "source": [ - "Here we can see that the vast majority of the wrongly-classified images have a vertical line. Something about our model is failing to capture information about those. You may have seen this coming, based on the first variational circuit. Let's look at it more closely.\n", - "\n", - "## Improving the model\n", - "\n", - "### Step 1 revisited\n", - "\n", - "In mapping our problem to a quantum circuit, we should have explicitly thought about the how the information in adjacent pixels determines class. In order to identify horizontal lines, we want to know \"if pixel $i$ is yellow, is pixel $i+1$ yellow\" for all the pixels across each row. We also want to know about vertical lines. But since the classification is binary, one could imagine simply saying that if such a horizontal line is *not* detected, then it is a vertical line. Our previous variational circuit contained CNOT gates between qubits (and therefore pixels) 0 and 1, 1 and 2, and 2 and 3. That covers any horizontal lines across the top of the image, but it does not directly detect vertical lines, nor does it completely detect horizontal lines, as it ignores the lower row. To fully detect all horizontal lines, we would want to have a similar set of CNOT gates between qubits (pixels) 4 and 5, 5 and 6, and 6 and 7. We could keep in mind that adding CNOT gates between qubits corresponding to vertical lines (like 0 and 4, or 2 and 6) may also be useful. But we will first check whether it is sufficient to detect that there *is* or *is not* a horizontal line." - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "0f0a7fd1-803a-446e-a761-3c9b41c9e3c6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "5\n", - "2+ qubit depth: 3\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Initialize the circuit using the same number of qubits as the image has pixels\n", - "qnn_circuit = QuantumCircuit(size)\n", - "\n", - "# We choose to have two variational parameters for each qubit.\n", - "params = ParameterVector(\"θ\", length=2 * size)\n", - "\n", - "# A first variational layer:\n", - "for i in range(size):\n", - " qnn_circuit.ry(params[i], i)\n", - "\n", - "# Here is an extended list of qubit pairs between which we want CNOT gates. This now covers all pixels connected by horizontal lines.\n", - "qnn_cnot_list = [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 7]]\n", - "\n", - "for i in range(len(qnn_cnot_list)):\n", - " qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])\n", - "\n", - "# The second variational layer:\n", - "for i in range(size):\n", - " qnn_circuit.rx(params[size + i], i)\n", - "\n", - "# Check the circuit depth, and the two-qubit gate depth\n", - "print(qnn_circuit.decompose().depth())\n", - "print(\n", - " f\"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", - ")\n", - "\n", - "# Combine the feature map and variational circuit\n", - "ansatz = qnn_circuit\n", - "\n", - "# Combine the feature map with the ansatz\n", - "full_circuit = QuantumCircuit(num_qubits)\n", - "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", - "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", - "\n", - "# Display the circuit\n", - "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "23005560-df84-4f8b-b59c-da19bee66d93", - "metadata": {}, - "source": [ - "We have not increased the depth of the circuit. Let's see if we have increased its ability to model our images.\n", - "\n", - "### Step 2 revisited\n", - "\n", - "We will need to transpile this new circuit for running on a real quantum backend. Let's skip this step for now to see if our revision of the variational circuit has had the desired effect on simulators. We will go deeper into transpilation in the next subsection.\n", - "\n", - "### Step 3 revisited\n", - "\n", - "We now apply the updated model to our training data." - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "id": "400c8fda-ed07-4c50-8791-31f98a30e53c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch: 0, batch: 0\n", - "Iter: 0, loss: 1.0049762969140237\n", - "Iter: 50, loss: 0.8274276543780351\n" - ] - } - ], - "source": [ - "from qiskit.primitives import StatevectorEstimator as Estimator\n", - "\n", - "batch_size = 140\n", - "num_epochs = 1\n", - "num_samples = len(train_images)\n", - "\n", - "# Globals\n", - "circuit = full_circuit\n", - "estimator = Estimator() # simulator for debugging\n", - "observables = observable\n", - "objective_func_vals = []\n", - "iter = 0\n", - "\n", - "# Random initial weights for the ansatz\n", - "np.random.seed(42)\n", - "weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", - "\n", - "for epoch in range(num_epochs):\n", - " for i in range((num_samples - 1) // batch_size + 1):\n", - " print(f\"Epoch: {epoch}, batch: {i}\")\n", - " start_i = i * batch_size\n", - " end_i = start_i + batch_size\n", - " train_images_batch = np.array(train_images[start_i:end_i])\n", - " train_labels_batch = np.array(train_labels[start_i:end_i])\n", - " input_params = train_images_batch\n", - " target = train_labels_batch\n", - " iter = 0\n", - " res = minimize(\n", - " mse_loss_weights, weight_params, method=\"COBYLA\", options={\"maxiter\": 100}\n", - " )\n", - " weight_params = res[\"x\"]" - ] - }, - { - "cell_type": "markdown", - "id": "c91461ca-f80f-45dc-8de9-65dc86419b23", - "metadata": {}, - "source": [ - "### Step 4 revisited\n", - "\n", - "Let's start by checking whether our optimizer fully converged." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "05c6f664-4043-446c-8fcc-11e77d6fe280", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "obj_func_vals_revised = objective_func_vals\n", - "# import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 6))\n", - "plt.plot(obj_func_vals_revised, label=\"revised ansatz\")\n", - "plt.xlabel(\"iteration\")\n", - "plt.ylabel(\"loss\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "6307a177-8d7c-4843-b3cd-45d7d3025e50", - "metadata": {}, - "source": [ - "This does not appear fully converged, as the loss function has not remained roughly level for substantially many steps. But the loss function is already ~60% lower than when using the previous variational circuit. If this were a research project, we would want to ensure full convergence. But for the purposes of exploration, this is sufficient. Let's check the accuracy on our training and testing data." - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "3247933c-e0a2-413f-9658-8c001ef800f1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ 0.46144755 0.42579688 0.35255977 0.55207273 -0.48578418 0.50805845\n", - " 0.44892649 0.6173847 -0.62428139 0.40405121 0.46862421 0.29503395\n", - " -0.5740469 -0.71794562 -0.45022095 -0.45330418 -0.19795258 -0.46821777\n", - " -0.5622049 -0.32114059 0.54947838 -0.4889812 0.28327445 0.58149728\n", - " -0.27026749 0.41328304 0.21119412 0.60108606 0.39204178 -0.24974605\n", - " 0.38496469 0.39867586 -0.38946996 0.62616766 0.61212525 -0.49719567\n", - " 0.30860002 0.68443904 -0.27505907 -0.41508947 -0.49666422 0.67716994\n", - " -0.54696613 -0.70058779 0.42711815 -0.5285338 0.37678572 0.43888249\n", - " -0.30844464 0.42347715 -0.4250844 0.67324132 0.59914067 -0.45184567\n", - " 0.13604098 0.65336342 0.26099853 0.60316559 -0.38743183 -0.54784284\n", - " -0.29549031 -0.45592302 0.41613453 -0.38781528 0.56903087 0.54955451\n", - " 0.55532336 -0.3931852 -0.57599675 0.61246236 0.42014135 -0.38171749\n", - " 0.56760389 0.45383135 -0.50473943 -0.47551181 0.54221517 -0.64987023\n", - " 0.28845851 0.54403865 0.53841148 0.64477078 0.71912049 -0.63178323\n", - " -0.50764757 0.50304637 -0.38099972 -0.27707127 -0.24353841 -0.52045267\n", - " -0.61500665 0.65443173 0.31902266 -0.64969037 -0.4814051 0.47980608\n", - " -0.649786 -0.43048551 0.34562588 0.308998 -0.32454238 0.29558168\n", - " -0.45410187 0.54600712 0.33204827 0.22627804 0.4283921 0.56191874\n", - " -0.25400294 -0.6493613 -0.47445293 0.42272138 -0.35472546 -0.52240474\n", - " -0.45207595 0.40292125 -0.3361856 -0.46620886 0.60202719 -0.56505744\n", - " 0.47169796 -0.43577622 0.40689437 0.48869108 -0.39701189 -0.57698634\n", - " -0.39236332 0.31294648 0.41797597 0.63004836 -0.52884541 -0.43805812\n", - " -0.3193499 0.36860211 -0.49190995 0.65000193 0.50260077 -0.56737168\n", - " -0.29693083 -0.40956432]\n", - "[ 1. 1. 1. 1. -1. 1. 1. 1. -1. 1. 1. 1. -1. -1. -1. -1. -1. -1.\n", - " -1. -1. 1. -1. 1. 1. -1. 1. 1. 1. 1. -1. 1. 1. -1. 1. 1. -1.\n", - " 1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. 1. -1.\n", - " 1. 1. 1. 1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. 1. -1.\n", - " 1. 1. -1. -1. 1. -1. 1. 1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.\n", - " -1. 1. 1. -1. -1. 1. -1. -1. 1. 1. -1. 1. -1. 1. 1. 1. 1. 1.\n", - " -1. -1. -1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. 1. 1. -1. -1.\n", - " -1. 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. -1. -1. -1.]\n", - "[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]\n", - "Train accuracy: 100.0%\n" - ] - } - ], - "source": [ - "from sklearn.metrics import accuracy_score\n", - "from qiskit.primitives import StatevectorEstimator as Estimator # simulator\n", - "# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer\n", - "\n", - "estimator = Estimator()\n", - "# estimator = Estimator(backend=backend)\n", - "\n", - "pred_train = forward(circuit, np.array(train_images), res[\"x\"], estimator, observable)\n", - "# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)\n", - "\n", - "print(pred_train)\n", - "\n", - "pred_train_labels = copy.deepcopy(pred_train)\n", - "pred_train_labels[pred_train_labels >= 0] = 1\n", - "pred_train_labels[pred_train_labels < 0] = -1\n", - "print(pred_train_labels)\n", - "print(train_labels)\n", - "\n", - "accuracy = accuracy_score(train_labels, pred_train_labels)\n", - "print(f\"Train accuracy: {accuracy * 100}%\")" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "b2119e4a-0f7d-43be-a9d4-cf524ecb543d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-0.48396136 -0.57123828 0.28373249 0.38983869 -0.45799092 -0.63643031\n", - " 0.69164877 -0.47749808 0.16965244 -0.39669469 0.39366915 0.44206948\n", - " 0.69733951 0.40445979 -0.33663432 0.54511581 -0.49397081 0.55934553\n", - " 0.69269512 0.38875983 0.39724004 -0.49635863 -0.19131387 0.38813936\n", - " 0.39537369 -0.46262489 0.5307315 0.21783317 0.31949453 -0.49772087\n", - " 0.56409526 -0.66254365 -0.57507262 0.37363552 0.35154205 0.69295687\n", - " -0.31205475 0.37787066 0.67903997 -0.29984861 -0.46435535 -0.32610974\n", - " 0.4327188 0.64626537 0.37592731 -0.14328906 0.59694745 0.71880638\n", - " 0.32414334 0.42119333 -0.60745236 -0.42520033 0.28334222 0.21699081\n", - " 0.34837252 0.31538989 0.30754545 0.5995197 -0.34678026 -0.46587602]\n", - "[-1. -1. 1. 1. -1. -1. 1. -1. 1. -1. 1. 1. 1. 1. -1. 1. -1. 1.\n", - " 1. 1. 1. -1. -1. 1. 1. -1. 1. 1. 1. -1. 1. -1. -1. 1. 1. 1.\n", - " -1. 1. 1. -1. -1. -1. 1. 1. 1. -1. 1. 1. 1. 1. -1. -1. 1. 1.\n", - " 1. 1. 1. 1. -1. -1.]\n", - "[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]\n", - "Test accuracy: 100.0%\n" - ] - } - ], - "source": [ - "pred_test = forward(circuit, np.array(test_images), res[\"x\"], estimator, observable)\n", - "# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)\n", - "\n", - "print(pred_test)\n", - "\n", - "pred_test_labels = copy.deepcopy(pred_test)\n", - "pred_test_labels[pred_test_labels >= 0] = 1\n", - "pred_test_labels[pred_test_labels < 0] = -1\n", - "print(pred_test_labels)\n", - "print(test_labels)\n", - "\n", - "accuracy = accuracy_score(test_labels, pred_test_labels)\n", - "print(f\"Test accuracy: {accuracy * 100}%\")" - ] - }, - { - "cell_type": "markdown", - "id": "a4fd4b77-bd33-40a2-8b58-291519a4ccca", - "metadata": {}, - "source": [ - "$100\\%$ accuracy on both sets! Our suspicion about accurate detection of horizontal lines being sufficient was correct! Further, our mapping from required information about the pixels to the CNOT gates in the quantum circuit was effective. Let's now look at how this process scales for running on real quantum computers." - ] - }, - { - "cell_type": "markdown", - "id": "0dc08c4a-0987-4bba-9bdf-2961e7ce794e", - "metadata": {}, - "source": [ - "## Scaling and running on real quantum computers\n", - "\n", - "### Data\n", - "\n", - "Let us begin by increasing the size of our images. There is nothing special about the choice of a 6x6 grid, except that it exceeds the number of qubits (32) that we can simulate for circuits using non-Clifford gates." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63ad50d2-6edf-419a-9516-b1e30c232045", - "metadata": {}, - "outputs": [], - "source": [ - "# This code defines the images to be classified:\n", - "\n", - "import numpy as np\n", - "\n", - "# Total number of \"pixels\"/qubits\n", - "size = 36\n", - "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", - "vert_size = 6\n", - "# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`\n", - "line_size = 6\n", - "\n", - "\n", - "def generate_dataset(num_images):\n", - " images = []\n", - " labels = []\n", - " hor_array = np.zeros((size - (line_size - 1) * vert_size, size))\n", - " ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))\n", - "\n", - " j = 0\n", - " for i in range(0, size - 1):\n", - " if i % (size / vert_size) <= (size / vert_size) - line_size:\n", - " for p in range(0, line_size):\n", - " hor_array[j][i + p] = np.pi / 2\n", - " j += 1\n", - "\n", - " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\" at size/vert_size - linesize, because we want to fold this list into a grid.\n", - "\n", - " j = 0\n", - " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", - " for p in range(0, line_size):\n", - " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", - " j += 1\n", - "\n", - " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.\n", - "\n", - " for n in range(num_images):\n", - " rng = np.random.randint(0, 2)\n", - " if rng == 0:\n", - " labels.append(-1)\n", - " random_image = np.random.randint(0, len(hor_array))\n", - " images.append(np.array(hor_array[random_image]))\n", - " # Randomly select one of the several rows you made above.\n", - " elif rng == 1:\n", - " labels.append(1)\n", - " random_image = np.random.randint(0, len(ver_array))\n", - " images.append(np.array(ver_array[random_image]))\n", - " # Randomly select one of the several rows you made above.\n", - "\n", - " # Create noise\n", - " for i in range(size):\n", - " if images[-1][i] == 0:\n", - " images[-1][i] = np.random.rand() * np.pi / 4\n", - " return images, labels\n", - "\n", - "\n", - "hor_size = round(size / vert_size)" - ] - }, - { - "cell_type": "markdown", - "id": "079d21bb-585b-41f8-96d1-a1e0eb7766b3", - "metadata": {}, - "source": [ - "Because quantum computing time is a precious commodity, we will use a very small training set, and very few optimization steps. This will be sufficient to demonstrate the workflow." - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "474d3a43-268b-425c-a3a9-165a34589d72", - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.model_selection import train_test_split\n", - "\n", - "np.random.seed(42)\n", - "# Here we specify a very small data set. Increase for realism, but monitor use of quantum computing time.\n", - "images, labels = generate_dataset(10)\n", - "\n", - "train_images, test_images, train_labels, test_labels = train_test_split(\n", - " images, labels, test_size=0.3, random_state=246\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "b106d32c-cd1f-4463-97fe-2f41695402fb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "# Generate a figure with nested images using subplots.\n", - "\n", - "fig, ax = plt.subplots(2, 2, figsize=(10, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", - "for i in range(4):\n", - " ax[i // 2, i % 2].imshow(\n", - " train_images[i].reshape(vert_size, hor_size),\n", - " aspect=\"equal\",\n", - " )\n", - "plt.subplots_adjust(wspace=0.1, hspace=0.025)" - ] - }, - { - "cell_type": "markdown", - "id": "21e4abd9-60bf-4a61-829b-c8b7b850530f", - "metadata": {}, - "source": [ - "### Step 1: Map the problem to a quantum circuit" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63c8619a-ec0b-4b6e-91fa-41a264fa7e32", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import z_feature_map\n", - "\n", - "# One qubit per data feature\n", - "num_qubits = len(train_images[0])\n", - "\n", - "# Data encoding\n", - "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure our data encoding goes to the first part of the circuit, the feature mapping.\n", - "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "id": "34603184-d0cc-4ce2-87c4-a1e8b1bdbf7d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "7\n", - "2+ qubit depth: 5\n" - ] - } - ], - "source": [ - "# This creates a circuit with the cxs in the compressed order.\n", - "\n", - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit import ParameterVector\n", - "\n", - "qnn_circuit = QuantumCircuit(size)\n", - "params = ParameterVector(\"θ\", length=2 * size)\n", - "for i in range(size):\n", - " qnn_circuit.ry(params[i], i)\n", - "\n", - "# CNOT gates between horizontally adjacent qubits.\n", - "for i in range(vert_size):\n", - " for j in range(hor_size):\n", - " if j < hor_size - 1:\n", - " qnn_circuit.cx((i * hor_size) + j, (i * hor_size) + j + 1)\n", - "\n", - "# CNOT gates between vertically adjacent qubits, likely not necessary based on our preliminary simulation.\n", - "# if i 1)}\"\n", - ")\n", - "# qnn_circuit_large.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "38153b01-8832-4eca-9892-262465d0f137", - "metadata": {}, - "source": [ - "This is a reasonable two-qubit depth. We should be able to get high-quality results from a real quantum computer." - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "10cb9de5-3893-4174-a36b-7607ea908721", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "11\n", - "2+ qubit depth: 5\n" - ] - } - ], - "source": [ - "# Combine the feature map and variational circuit\n", - "ansatz = qnn_circuit\n", - "\n", - "# Combine the feature map with the ansatz\n", - "full_circuit = QuantumCircuit(num_qubits)\n", - "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", - "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", - "\n", - "# Check the depth of the full circuit\n", - "print(full_circuit.decompose().depth())\n", - "print(\n", - " f\"2+ qubit depth: {full_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "53c81996-7269-4aca-893a-53082287ef6f", - "metadata": {}, - "source": [ - "Because we are using the ```z_feature_map```, which has no CNOT gates, adding the encoding layer does not increase our two-qubit depth. We can visualize the full circuit here." - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "id": "b43806aa-7d41-40b8-9e43-a604f0e42c26", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 69, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "e9cbfbd8-337c-4f10-b2de-5de8165a8fd1", - "metadata": {}, - "source": [ - "You may note that if minimizing two-qubit depth were of paramount importance, we could actually reduce it a bit by changing the order of the CNOTs. For example, the CNOTs on $q_{35}$ and $q_{34}$ could be moved to the left in the circuit diagram above, and could be placed directly below the CNOTs on $q_{30}$ and $q_{31}$, for example. For a two-qubit gate depth of 5, it isn't obvious that this will make a difference after transpilation, but it is something to keep in mind. If the order of the CNOT gates is important for logically matching the problem at hand, the depth here is fine. If the order of CNOTs is not critical to modeling the data structure in our images, then we could write a script to re-order these CNOT gates to minimize depth.\n", - "\n", - "We also need to re-define our observable with our larger images:" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "28932196-a5a2-4168-8101-3e5711e148f9", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "observable = SparsePauliOp.from_list([(\"Z\" * (num_qubits), 1)])" - ] - }, - { - "cell_type": "markdown", - "id": "c1eb102f-ab7e-4b9f-b09a-853cd2c7f9e7", - "metadata": {}, - "source": [ - "## Qiskit Patterns Step 2: Optimize problem for quantum execution\n", - "We start by selecting a backend for execution. In this case, we will use the least-busy backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "552bb4ef-a3c8-42fc-9ced-13c88564bf4c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_brisbane\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# To run on hardware, select the least busy quantum computer or specify a particular one.\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "# backend = service.backend(\"ibm_brisbaneane\")\n", - "\n", - "print(backend.name)" - ] - }, - { - "cell_type": "markdown", - "id": "94162dcb-e753-4faf-82e9-43096c6a56b0", - "metadata": {}, - "source": [ - "Once again, we are defining a pass manager, with optimization level set to 3." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16b93237-bbea-4da9-9f07-89c4f3e78717", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit.library import XGate\n", - "from qiskit.transpiler import PassManager\n", - "from qiskit.transpiler.passes import (\n", - " ALAPScheduleAnalysis,\n", - " ConstrainedReschedule,\n", - " PadDynamicalDecoupling,\n", - ")\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "pm.scheduling = PassManager(\n", - " [\n", - " ALAPScheduleAnalysis(target=target),\n", - " ConstrainedReschedule(\n", - " acquire_alignment=target.acquire_alignment,\n", - " pulse_alignment=target.pulse_alignment,\n", - " target=target,\n", - " ),\n", - " PadDynamicalDecoupling(\n", - " target=target,\n", - " dd_sequence=[XGate(), XGate()],\n", - " pulse_alignment=target.pulse_alignment,\n", - " ),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "74e91976-6c32-4731-8b46-ab6b9b903c88", - "metadata": {}, - "source": [ - "Now we will apply the pass manager several times. For very wide or very deep circuits, there can be large variability in the transpiled two-qubit depths. For such circuits it is important to try the pass manager many times and use the best (shallowest) result." - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "1f12e420-7ac7-4744-b7ab-b664a0fa7b59", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[85, 85, 81, 89, 81, 81, 89, 85, 85]\n", - "[10, 10, 10, 10, 10, 10, 10, 10, 10]\n" - ] - } - ], - "source": [ - "# Try pass manager several times, since heuristics can return various transpilations on large circuits, and we want the shallowest.\n", - "\n", - "transpiled_qcs = []\n", - "transpiled_depths = []\n", - "transpiled_2q_depths = []\n", - "for i in range(1, 10):\n", - " circuit_ibm = pm.run(full_circuit)\n", - " transpiled_qcs.append(circuit_ibm)\n", - " transpiled_depths.append(circuit_ibm.decompose().depth())\n", - " transpiled_2q_depths.append(\n", - " circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n", - " )\n", - " # print(i)\n", - "\n", - "print(transpiled_depths)\n", - "print(transpiled_2q_depths)\n", - "\n", - "# Use the shallowest\n", - "\n", - "minpos = transpiled_2q_depths.index(min(transpiled_2q_depths))" - ] - }, - { - "cell_type": "markdown", - "id": "d12d9c3b-f93e-481f-bd46-70dc26ba6840", - "metadata": {}, - "source": [ - "We see that in this case, the transpiled two-qubit depth was always 10. There was minor variation in the single-qubit depth, and we will use the shallowest one. But on this 36-qubit circuit, this is not a critical improvement. We can visualize this transpiled circuit, although at this scale it becomes increasingly difficult to parse, visually." - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "f58ba159-a866-4e46-9871-794c88e894b8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "81\n", - "2+ qubit depth: 10\n" - ] - } - ], - "source": [ - "circuit_ibm = transpiled_qcs[2]\n", - "observable_ibm = observable.apply_layout(circuit_ibm.layout)\n", - "print(circuit_ibm.decompose().depth())\n", - "print(\n", - " f\"2+ qubit depth: {circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "342d4324-2abb-492d-86c9-401a39938e3d", - "metadata": {}, - "source": [ - "## Qiskit Patterns Step 3: Execute using Qiskit Primitives\n", - "\n", - "In order to limit time used on real quantum computers, we will only carry out a few optimization steps here, and we are doing so on a very small training set. But the scaling of this to more optimization steps and larger testing data sets should be clear from instructions throughout the lesson." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9e3c0b51-588e-4807-8802-e377b3e89846", - "metadata": {}, - "outputs": [], - "source": [ - "# This was run on an Eagle r3 processor on 10-4-24, and took 7 min.\n", - "\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session\n", - "\n", - "batch_size = 7\n", - "num_epochs = 1\n", - "num_samples = len(train_images)\n", - "\n", - "# Globals\n", - "circuit = circuit_ibm\n", - "observable = observable_ibm\n", - "objective_func_vals = []\n", - "iter = 0\n", - "\n", - "# Random initial weights for the ansatz\n", - "np.random.seed(42)\n", - "# weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", - "# Or re-load weights from a previous calculation\n", - "weight_params = np.array(\n", - " [\n", - " 3.35330497,\n", - " 5.97351416,\n", - " 4.59925358,\n", - " 3.76148219,\n", - " 0.98029403,\n", - " 0.98014248,\n", - " 0.3649501,\n", - " 6.44234523,\n", - " 3.77691701,\n", - " 4.44895122,\n", - " 0.12933619,\n", - " 6.09412333,\n", - " 5.23039137,\n", - " 1.33416598,\n", - " 1.14243996,\n", - " 1.15236452,\n", - " 1.91161039,\n", - " 3.2971419,\n", - " 3.71399059,\n", - " 1.82984665,\n", - " 3.84438512,\n", - " 0.87646578,\n", - " 1.83559896,\n", - " 2.30191935,\n", - " 2.86557222,\n", - " 4.93340606,\n", - " 1.25458737,\n", - " 3.23103027,\n", - " 3.72225051,\n", - " 0.29185655,\n", - " 3.81731689,\n", - " 1.07143467,\n", - " 0.40873121,\n", - " 5.96202367,\n", - " 6.067245,\n", - " 5.07931034,\n", - " 1.91394476,\n", - " 0.61369199,\n", - " 4.2991629,\n", - " 2.76555968,\n", - " 0.76678884,\n", - " 3.11128829,\n", - " 0.21606945,\n", - " 5.71342859,\n", - " 1.62596258,\n", - " 4.16275028,\n", - " 1.95853845,\n", - " 3.26768375,\n", - " 3.43508199,\n", - " 1.1614748,\n", - " 6.09207989,\n", - " 4.87030317,\n", - " 5.90304595,\n", - " 5.62236606,\n", - " 3.75671636,\n", - " 5.79230665,\n", - " 0.55601479,\n", - " 1.23139664,\n", - " 0.28417144,\n", - " 2.04411075,\n", - " 2.44213144,\n", - " 1.70493625,\n", - " 5.20711134,\n", - " 2.24154726,\n", - " 1.76516358,\n", - " 3.40986006,\n", - " 0.88545302,\n", - " 5.04035228,\n", - " 0.46841551,\n", - " 6.2007935,\n", - " 4.85215699,\n", - " 1.24856745,\n", - " ]\n", - ")\n", - "\n", - "# Running in a session avoids repeated queuing. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options={\"resilience_level\": 1})\n", - "\n", - " for epoch in range(num_epochs):\n", - " for i in range((num_samples - 1) // batch_size + 1):\n", - " print(f\"Epoch: {epoch}, batch: {i}\")\n", - " start_i = i * batch_size\n", - " end_i = start_i + batch_size\n", - " train_images_batch = np.array(train_images[start_i:end_i])\n", - " train_labels_batch = np.array(train_labels[start_i:end_i])\n", - " input_params = train_images_batch\n", - " target = train_labels_batch\n", - " iter = 0\n", - " # We can increase maxiter to do a full optimization.\n", - " res = minimize(\n", - " mse_loss_weights,\n", - " weight_params,\n", - " method=\"COBYLA\",\n", - " options={\"maxiter\": 20},\n", - " )\n", - " weight_params = res[\"x\"]\n", - "session.close()\n", - "\n", - "# Open users can carry out the same calculation using a batch, but repeated queuing is possible.\n", - "# from qiskit_ibm_runtime import Batch\n", - "\n", - "# with Batch(backend=backend) as batch:\n", - "# estimator = Estimator(\n", - "# mode=batch, options={\"resilience_level\": 1}\n", - "# )\n", - "#\n", - "# for epoch in range(num_epochs):\n", - "# for i in range((num_samples - 1) // batch_size + 1):\n", - "# print(f\"Epoch: {epoch}, batch: {i}\")\n", - "# start_i = i * batch_size\n", - "# end_i = start_i + batch_size\n", - "# train_images_batch = np.array(train_images[start_i:end_i])\n", - "# train_labels_batch = np.array(train_labels[start_i:end_i])\n", - "# input_params = train_images_batch\n", - "# target = train_labels_batch\n", - "# iter = 0\n", - "# # We can increase maxiter to do a full optimization.\n", - "# res = minimize(\n", - "# mse_loss_weights,\n", - "# weight_params,\n", - "# method=\"COBYLA\",\n", - "# options={\"maxiter\": 20},\n", - "# )\n", - "# weight_params = res[\"x\"]\n", - "# batch.close()" - ] - }, - { - "cell_type": "markdown", - "id": "cb530676-4807-445c-a09b-e96903a3cd4e", - "metadata": {}, - "source": [ - "It is recommended that you save the weight parameters returned from this computation, should you decide to iterate further." - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "id": "cf939ac1-3811-42bf-94c6-ea4eb60fb77c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([3.35330497, 6.97351416, 5.59925358, 3.76148219, 0.98029403,\n", - " 0.98014248, 0.3649501 , 6.44234523, 3.77691701, 4.44895122,\n", - " 1.12933619, 7.09412333, 5.23039137, 1.33416598, 1.14243996,\n", - " 1.15236452, 1.91161039, 3.2971419 , 3.71399059, 1.82984665,\n", - " 3.84438512, 0.87646578, 1.83559896, 2.30191935, 2.86557222,\n", - " 4.93340606, 1.25458737, 3.23103027, 3.72225051, 0.29185655,\n", - " 3.81731689, 1.07143467, 0.40873121, 5.96202367, 6.067245 ,\n", - " 5.07931034, 1.91394476, 0.61369199, 4.2991629 , 2.76555968,\n", - " 0.76678884, 3.11128829, 0.21606945, 5.71342859, 1.62596258,\n", - " 4.16275028, 1.95853845, 3.26768375, 3.43508199, 1.1614748 ,\n", - " 6.09207989, 4.87030317, 5.90304595, 5.62236606, 3.75671636,\n", - " 5.79230665, 0.55601479, 1.23139664, 0.28417144, 2.04411075,\n", - " 2.44213144, 1.70493625, 5.20711134, 2.24154726, 1.76516358,\n", - " 3.40986006, 0.88545302, 5.04035228, 0.46841551, 6.2007935 ,\n", - " 4.85215699, 1.24856745])" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "weight_params" - ] - }, - { - "cell_type": "markdown", - "id": "36e36864-f02f-4700-83aa-cd69fb08dcfd", - "metadata": {}, - "source": [ - "We can plot these first few optimization steps, although we would not expect any convergence after just a few optimization steps. These curves have been relatively flat for the first few steps, even using simulators. We should note, however, that the optimization currently has 72 free parameters. This can be reduced by at least a factor of 2-3 without compromising results by, for example, parameterizing qubits with data corresponding to a subset of full rows and columns. Indeed, the parameter space should be reduced before spending more quantum computing time on minimizing the loss function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8244ff1-a7c5-46e1-87b1-e20b506ef4d8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "obj_func_vals_qc = objective_func_vals\n", - "# import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 6))\n", - "plt.plot(obj_func_vals_qc, label=\"revised ansatz\")\n", - "plt.xlabel(\"iteration\")\n", - "plt.ylabel(\"loss\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "b2e4dd0b-9951-4a6a-8920-fb756b39ea73", - "metadata": {}, - "source": [ - "### Closing\n", - "\n", - "To recap, in this lesson we learned the workflow for binary classification of images using a quantum neural network. Some key considerations in each Qiskit patterns step were:\n", - "\n", - "__Step 1:__ Map the problem to a quantum circuit\n", - "* Load training data. This could be done \"by hand\" or using a pre-built feature map like ```z_feature_map```.\n", - "* Construct an ansatz containing rotation and entanglement layers that are appropriate for your problem.\n", - "* Monitor circuit depth to ensure quality results on quantum computers.\n", - "\n", - "__Step 2:__ Optimize problem for quantum execution\n", - "* Select a backend, often the least busy one.\n", - "* Use a pass manager to transpile both the circuit and the observables to the architecture of the chosen backend.\n", - "* For very deep or wide circuits, transpile multiple times, and select the shallowest circuit.\n", - "\n", - "__Step 3:__ Execute using Qiskit (Runtime) Primitives\n", - "* Carry out preliminary trials on simulators to debug and optimize your ansatz.\n", - "* Execute on an IBM® quantum computer.\n", - "\n", - "__Step 4:__ Post-process, return result in classical format\n", - "* Calculate model accuracy on training data, and on testing data.\n", - "* Monitor convergence of the classical optimization." - ] - }, - { - "cell_type": "markdown", - "id": "8ca1b39a-95ca-4a28-9689-a07085532c91", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "[1] https://arxiv.org/abs/2405.00781" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c458cae8-4c92-46f8-b914-49fc03920c01", + "metadata": {}, + "source": [ + "---\n", + "title: QVCs and QNNs\n", + "description: Quantum variational circuits are applied to the recognition of patterns in an image. The parallels with classical neural networks are discussed.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore yticks hspace VQCs linesize, imshow, minpos, nonumber, zzcircuit, zcircuit, pcircuit, zzcx, Schuld, Francesco, Peruccione, Vojtech, Adrian, Perez, Cervera, Lierta, Elies, Fuster, Jose, Latorre, Sweke, Jakob */}\n", + "\n", + "# Quantum Variational Circuits and Quantum Neural Networks\n", + "\n", + "In this lesson, we implement several variational quantum circuits for a data classification task, so-called variational quantum classifiers (VQCs). At one point, it was common to refer to a subset of VQCs as quantum neural networks (QNNs) in analogy with classical neural networks. Indeed, there are cases where structures borrowed from classical neural networks, such as convolution layers, play an important role in VQCs. In such cases where the analogy is strong, QNNs may be a useful description. But parameterized quantum circuits need not follow the general structure of a neural network; for example, not all data need to be loaded in the first (input) layer; we can load some data in the first layer, apply some gates and then load additional data (a process called data \"reuploading\"). We should therefore think of QNNs as a subset of parameterized quantum circuits, and we should not be limited in our exploration of useful quantum circuits by the analogy to classical neural networks.\n", + "\n", + "The dataset being addressed in this lesson consists of images containing horizontal and vertical stripes, and our goal is to label unseen images into one of the two categories depending on the orientation of their line. We will accomplish this with a VQC. As we go, we will address ways in which the calculation can be improved and scaled. The dataset here is exceptionally easy to classify classically. It has been chosen for its simplicity so we can focus on the quantum part of this problem, and look at how a dataset attribute might translate to a part of a quantum circuit. It is not reasonable to expect a quantum speed-up for such simple cases where classical algorithms are so efficient.\n", + "\n", + "By the end of this lesson you should be able to:\n", + "* Load data from an image into a quantum circuit\n", + "* Construct an ansatz for a VQC (or QNN), and adjust it to fit your problem\n", + "* Train your VQC/QNN and use it to make accurate predictions on test data\n", + "* Scale the problem, and recognize limits of current quantum computers" + ] + }, + { + "cell_type": "markdown", + "id": "5a4c9c5f-9c86-402f-b50d-8632806423f4", + "metadata": {}, + "source": [ + "## Data generation\n", + "\n", + "We will start by constructing the data. Data sets are often not explicitly generated as part of the Qiskit patterns framework. But data type and preparation is critical to successfully applying quantum computing to machine learning. The code below defines a data set of images with set pixel dimensions. One full row or column of the image is assigned the value $\\pi/2$, and the remaining pixels are assigned random values on the interval $(0,\\pi/4)$. The random values are noise in our data. Glance through the code to make sure you understand how the images are generated. Later on we will scale up the images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af2b9990-e1be-4f3a-ae66-cecef55ede92", + "metadata": {}, + "outputs": [], + "source": [ + "# This code defines the images to be classified:\n", + "\n", + "import numpy as np\n", + "\n", + "# Total number of \"pixels\"/qubits\n", + "size = 8\n", + "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", + "vert_size = 2\n", + "# The length of the line to be detected (yellow). Must be less than or equal to the smallest\n", + "# dimension of the image (`<=min(vert_size,size/vert_size)`\n", + "line_size = 2\n", + "\n", + "\n", + "def generate_dataset(num_images):\n", + " images = []\n", + " labels = []\n", + " hor_array = np.zeros((size - (line_size - 1) * vert_size, size))\n", + " ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))\n", + "\n", + " j = 0\n", + " for i in range(0, size - 1):\n", + " if i % (size / vert_size) <= (size / vert_size) - line_size:\n", + " for p in range(0, line_size):\n", + " hor_array[j][i + p] = np.pi / 2\n", + " j += 1\n", + "\n", + " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\"\n", + " # at size/vert_size - linesize, because we want to fold this list into a grid.\n", + "\n", + " j = 0\n", + " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", + " for p in range(0, line_size):\n", + " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", + " j += 1\n", + "\n", + " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top\n", + " # of each other.\n", + "\n", + " for n in range(num_images):\n", + " rng = np.random.randint(0, 2)\n", + " if rng == 0:\n", + " labels.append(-1)\n", + " random_image = np.random.randint(0, len(hor_array))\n", + " images.append(np.array(hor_array[random_image]))\n", + "\n", + " elif rng == 1:\n", + " labels.append(1)\n", + " random_image = np.random.randint(0, len(ver_array))\n", + " images.append(np.array(ver_array[random_image]))\n", + " # Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding\n", + " # label.\n", + "\n", + " # Create noise\n", + " for i in range(size):\n", + " if images[-1][i] == 0:\n", + " images[-1][i] = np.random.rand() * np.pi / 4\n", + " return images, labels\n", + "\n", + "\n", + "hor_size = round(size / vert_size)" + ] + }, + { + "cell_type": "markdown", + "id": "e6fb6709-158b-43c0-a1f5-fc6f54b56beb", + "metadata": {}, + "source": [ + "Note that the code above has also generated labels indicated whether the images contain a vertical (+1) or horizontal (-1) line. We will now use sklearn to split a data set of 100 images into a training and testing set (along with their corresponding labels). Here, we use $70%$ of the data set for training, with the remaining $30%$ withheld for testing." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c87a2336-ea3e-4902-a6c2-02b638da4585", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "\n", + "np.random.seed(42)\n", + "images, labels = generate_dataset(200)\n", + "\n", + "train_images, test_images, train_labels, test_labels = train_test_split(\n", + " images, labels, test_size=0.3, random_state=246\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cf4576f1-3494-48d0-b5f0-7090af5b3b32", + "metadata": {}, + "source": [ + "Let's plot a few elements of our data set to see what these lines look like:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ac9e4239-8c1a-4798-a8e7-80347c29150c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Make subplot titles so we can identify categories\n", + "titles = []\n", + "for i in range(8):\n", + " title = \"category: \" + str(train_labels[i])\n", + " titles.append(title)\n", + "\n", + "# Generate a figure with nested images using subplots.\n", + "fig, ax = plt.subplots(4, 2, figsize=(10, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", + "\n", + "for i in range(8):\n", + " ax[i // 2, i % 2].imshow(\n", + " train_images[i].reshape(vert_size, hor_size),\n", + " aspect=\"equal\",\n", + " )\n", + " ax[i // 2, i % 2].set_title(titles[i])\n", + "plt.subplots_adjust(wspace=0.1, hspace=0.3)" + ] + }, + { + "cell_type": "markdown", + "id": "ffae1e62-4462-474f-a0ee-30f797fbffba", + "metadata": {}, + "source": [ + "Each of these images is still paired with its label in ```train_labels``` in a simple list form:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "93ccb862-ad75-403d-81aa-04c5f841c432", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 1, 1, 1, -1, 1, 1, 1]\n" + ] + } + ], + "source": [ + "print(train_labels[:8])" + ] + }, + { + "cell_type": "markdown", + "id": "c1e55d16-3af4-4629-848b-89c575b6f6df", + "metadata": {}, + "source": [ + "## Variational quantum classifier: a first attempt\n", + "\n", + "### Qiskit patterns step 1: Map the problem to a quantum circuit\n", + "\n", + "The goal is to find a function $f$ with parameters $\\theta$ that maps a data vector / image $\\vec{x}$ to the correct category: $f_\\theta(\\vec{x}) \\rightarrow \\pm1$. This will be accomplished using a VQC with few layers that can be identified by their distinct purposes:\n", + "$$\n", + "f_\\theta(\\vec{x}) = \\langle 0|U^{\\dagger}(\\vec{x})W^\\dagger(\\theta)OW(\\theta)U(\\vec{x})|0\\rangle\n", + "$$\n", + "Here, $U(\\vec{x})$ is the encoding circuit, for which we have many options as seen in previous lessons. $W(\\theta)$ is a variational, or trainable circuit block, and $\\theta$ is the set of parameters to be trained. Those parameters will be varied by classical optimization algorithms to find the set of parameters that yields the best classification of images by the quantum circuit. This variational circuit is sometimes called the \"ansatz\". Finally, $O$ is some observable that will be estimated using the Estimator primitive. There is no constraint that forces the layers to come in this order, or even to be fully separate. One could have multiple variational and/or encoding layers in any order that is technically motivated.\n", + "\n", + "We start by choosing a feature map to encode our data. We will use the ```z_feature_map```, as it keeps circuit depths low compared to some other feature mappings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83adf797-5c8f-4bba-826a-fa3c82d7affb", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import z_feature_map\n", + "\n", + "# One qubit per data feature\n", + "num_qubits = len(train_images[0])\n", + "\n", + "# Data encoding\n", + "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure\n", + "# our data encoding goes to the first part of the circuit, the feature mapping.\n", + "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" + ] + }, + { + "cell_type": "markdown", + "id": "3dd4d3d3-7a94-410d-856b-be27dad481a7", + "metadata": {}, + "source": [ + "We must now decide on an ansatz to be trained. There are many considerations when selecting an ansatz. A complete description is beyond the scope of this introduction; here we simply point out a few categories of considerations.\n", + "\n", + "1. **Hardware:** All modern quantum computers are more prone to errors and more susceptible to noise than their classical counterparts. Using an ansatz that is excessively deep (especially in transpiled, two-qubit depth) will not produce good results. A related issue is that quantum computers have some qubit layout, meaning that some physical qubits are adjacent on the quantum computer, and others may be very far from each other. Entangling adjacent qubits does not increase the depth by too much, but entangling very distant qubits can increase depth substantially, as we must insert swap gates to move information onto qubits that are adjacent in order for them to be entangled.\n", + "2. **The problem:** Whenever you have some information about your problem that could guide your ansatz, make use of it. For example, the data in this lesson is made up of images of horizontal and vertical lines. One could consider what correlation between adjacent colors/values identifies an image of a horizontal or vertical line. What attributes of an ansatz would correspond to this correlation between adjacent pixels? We will revisit this point more technically later in this lesson. But for now, let us simply say that including entanglement and CNOT gates between qubits corresponding to adjacent pixels seems like a good idea. In the bigger picture, consider whether the problem is actually best solved using a quantum circuit, or whether classical algorithms might exist that can do as good a job.\n", + "3. **Number of parameters:** Each independently parameterized quantum gate in the circuit increases the space to be classically optimized, and this results in slower convergence. But as problems scale up, one may encounter *barren plateaus*. This term refers to a phenomenon where the optimization landscape of a variational quantum algorithm becomes exponentially flat and featureless as the problem size increases. This causes vanishing gradients, making it difficult to effectively train the algorithm[\\[1\\]](#references). Barren plateaus are relevant to variational quantum algorithms like VQCs/QNNs. It should be noted that the increasing number of parameters is not the only consideration in avoiding barren plateaus; other considerations include global cost functions and random parameter initialization.\n", + "\n", + "In this lesson we will see a few simple examples of good practices in ansatz construction. Let us first try the ansatz below. We will return to revise it, later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf308e60-9366-4c23-8079-366f95ba4790", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n", + "2+ qubit depth: 3\n" + ] + }, + { + "data": { + "text/plain": [ + " ┌──────────┐ ┌──────────┐ \n", + "q_0: ┤ Ry(θ[0]) ├──────■──────┤ Rx(θ[8]) ├─────────────────────────\n", + " ├──────────┤ ┌─┴─┐ └──────────┘┌──────────┐ \n", + "q_1: ┤ Ry(θ[1]) ├────┤ X ├─────────■──────┤ Rx(θ[9]) ├─────────────\n", + " ├──────────┤ └───┘ ┌─┴─┐ └──────────┘┌───────────┐\n", + "q_2: ┤ Ry(θ[2]) ├────────────────┤ X ├─────────■──────┤ Rx(θ[10]) ├\n", + " ├──────────┤ └───┘ ┌─┴─┐ ├───────────┤\n", + "q_3: ┤ Ry(θ[3]) ├────────────────────────────┤ X ├────┤ Rx(θ[11]) ├\n", + " ├──────────┤┌───────────┐ └───┘ └───────────┘\n", + "q_4: ┤ Ry(θ[4]) ├┤ Rx(θ[12]) ├─────────────────────────────────────\n", + " ├──────────┤├───────────┤ \n", + "q_5: ┤ Ry(θ[5]) ├┤ Rx(θ[13]) ├─────────────────────────────────────\n", + " ├──────────┤├───────────┤ \n", + "q_6: ┤ Ry(θ[6]) ├┤ Rx(θ[14]) ├─────────────────────────────────────\n", + " ├──────────┤├───────────┤ \n", + "q_7: ┤ Ry(θ[7]) ├┤ Rx(θ[15]) ├─────────────────────────────────────\n", + " └──────────┘└───────────┘ " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Import the necessary packages\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import ParameterVector\n", + "\n", + "# Initialize the circuit using the same number of qubits as the image has pixels\n", + "qnn_circuit = QuantumCircuit(size)\n", + "\n", + "# We choose to have two variational parameters for each qubit.\n", + "params = ParameterVector(\"θ\", length=2 * size)\n", + "\n", + "# A first variational layer:\n", + "for i in range(size):\n", + " qnn_circuit.ry(params[i], i)\n", + "\n", + "# Here is a list of qubit pairs between which we want CNOT gates. The choice of these is not yet\n", + "# obvious.\n", + "qnn_cnot_list = [[0, 1], [1, 2], [2, 3]]\n", + "\n", + "for i in range(len(qnn_cnot_list)):\n", + " qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])\n", + "\n", + "# The second variational layer:\n", + "for i in range(size):\n", + " qnn_circuit.rx(params[size + i], i)\n", + "\n", + "# Check the circuit depth, and the two-qubit gate depth\n", + "print(qnn_circuit.decompose().depth())\n", + "print(\n", + " f\"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", + ")\n", + "\n", + "# Draw the circuit\n", + "qnn_circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "c01b0b2a-9ce7-442d-a7be-faa4562e1da4", + "metadata": {}, + "source": [ + "With the data encoding and variational circuit prepared, we can combine them to form our full ansatz. In this case, the components of our quantum circuit are quite analogous to those in neural networks, with $U(\\vec{x})$ being most similar to the layer that loads input values from the image, and $W(\\theta)$ being like the layer of variable \"weights\". Since this analogy holds in this case, we are adopting \"qnn\" in some of our naming conventions; but this analogy should not be limiting in your exploration of VQCs.\n", + "\n", + "![QML_CR_background_QNN_circuit-2.png](/learning/images/courses/quantum-machine-learning/qvc-qnn/qml-cr-background-qnn-circuit.avif)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e84e5ac1-d5fb-4ec0-8a23-5a2c417a4088", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# QNN ansatz\n", + "ansatz = qnn_circuit\n", + "\n", + "# Combine the feature map with the ansatz\n", + "full_circuit = QuantumCircuit(num_qubits)\n", + "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", + "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", + "\n", + "# Display the circuit\n", + "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "90ff05da-0435-4fcf-94cc-181c1649490b", + "metadata": {}, + "source": [ + "We must now define an observable, so we can use it in our cost function. We will obtain an expectation value for this observable using Estimator. If we have selected a good, problem-motivated ansatz, then each qubit will contain information relevant to classification. One can add layers to combine information onto fewer qubits (called a *convolutional layer*), such that measurements are only needed on a subset of the qubits in the circuit (as in convolutional neural networks). Or one can measure some attribute from each qubit. Here we will opt for the latter, so we include a ```Z``` operator for each qubit. There is nothing unique about choosing $Z$, but it is well motivated:\n", + "* This is a binary classification task, and a measurement of $Z$ can yield two possible outcomes.\n", + "* The eigenvalues of $Z$ ($\\pm 1$) are reasonably well separated, and result in an estimator outcome in interval [-1, +1], where 0 can simply be used as a cutoff value.\n", + "* It is straightforward to measure in Pauli Z basis with no extra gate overhead.\n", + "\n", + "So, Z is a very natural choice." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9cf2eb5f-2455-4f2f-a420-c68836ebe917", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "observable = SparsePauliOp.from_list([(\"Z\" * (num_qubits), 1)])" + ] + }, + { + "cell_type": "markdown", + "id": "3854607b-ee8d-4b9d-913d-b8fd6b722008", + "metadata": {}, + "source": [ + "We have our quantum circuit and the observable we want to estimate. Now we need a few things in order to run and optimize this circuit. First, we need a function to run a forward pass. Note that the function below takes in the ```input_params``` and ```weight_params``` separately. The former is the set of static parameters describing the data in an image, and the latter is the set of variable parameters to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9b702a54-89a1-4973-9f11-8baf7ddc7248", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import BaseEstimatorV2\n", + "from qiskit.quantum_info.operators.base_operator import BaseOperator\n", + "\n", + "\n", + "def forward(\n", + " circuit: QuantumCircuit,\n", + " input_params: np.ndarray,\n", + " weight_params: np.ndarray,\n", + " estimator: BaseEstimatorV2,\n", + " observable: BaseOperator,\n", + ") -> np.ndarray:\n", + " \"\"\"\n", + " Forward pass of the neural network.\n", + "\n", + " Args:\n", + " circuit: circuit consisting of data loader gates and the neural network ansatz.\n", + " input_params: data encoding parameters.\n", + " weight_params: neural network ansatz parameters.\n", + " estimator: EstimatorV2 primitive.\n", + " observable: a single observable to compute the expectation over.\n", + "\n", + " Returns:\n", + " expectation_values: an array (for one observable) or a matrix (for a sequence of observables) of expectation values.\n", + " Rows correspond to observables and columns to data samples.\n", + " \"\"\"\n", + " num_samples = input_params.shape[0]\n", + " weights = np.broadcast_to(weight_params, (num_samples, len(weight_params)))\n", + " params = np.concatenate((input_params, weights), axis=1)\n", + " pub = (circuit, observable, params)\n", + " job = estimator.run([pub])\n", + " result = job.result()[0]\n", + " expectation_values = result.data.evs\n", + "\n", + " return expectation_values" + ] + }, + { + "cell_type": "markdown", + "id": "11b0403f-e3a6-44c9-b461-9d059160ecf6", + "metadata": {}, + "source": [ + "### Loss function\n", + "Next, we need a loss function to calculate the difference between the predicted and calculated values of the labels. The function will take in the labels predicted by the algorithm and the correct labels and return the mean squared difference. There any many different loss functions. Here, MSE is an example that we chose." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5c79d67a-e1b4-41cf-aa29-5bd4a4f8ddd3", + "metadata": {}, + "outputs": [], + "source": [ + "def mse_loss(predict: np.ndarray, target: np.ndarray) -> np.ndarray:\n", + " \"\"\"\n", + " Mean squared error (MSE).\n", + "\n", + " prediction: predictions from the forward pass of neural network.\n", + " target: true labels.\n", + "\n", + " output: MSE loss.\n", + " \"\"\"\n", + " if len(predict.shape) <= 1:\n", + " return ((predict - target) ** 2).mean()\n", + " else:\n", + " raise AssertionError(\"input should be 1d-array\")" + ] + }, + { + "cell_type": "markdown", + "id": "b1d13267-a336-489a-bf7c-55ef0c4e4f20", + "metadata": {}, + "source": [ + "Let us also define a slightly different loss function that is a function of the variable parameters (weights), for use by the classical optimizer. This function only takes the ansatz parameters as input; other variables for the forward pass and the loss are set as global parameters. The optimizer will train the model by sampling different weights and attempting to lower the output of the cost/loss function." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3bc33b0e-eace-4ebe-acff-ee15a8273fc5", + "metadata": {}, + "outputs": [], + "source": [ + "def mse_loss_weights(weight_params: np.ndarray) -> np.ndarray:\n", + " \"\"\"\n", + " Cost function for the optimizer to update the ansatz parameters.\n", + "\n", + " weight_params: ansatz parameters to be updated by the optimizer.\n", + "\n", + " output: MSE loss.\n", + " \"\"\"\n", + " predictions = forward(\n", + " circuit=circuit,\n", + " input_params=input_params,\n", + " weight_params=weight_params,\n", + " estimator=estimator,\n", + " observable=observable,\n", + " )\n", + "\n", + " cost = mse_loss(predict=predictions, target=target)\n", + " objective_func_vals.append(cost)\n", + "\n", + " global iter\n", + " if iter % 50 == 0:\n", + " print(f\"Iter: {iter}, loss: {cost}\")\n", + " iter += 1\n", + "\n", + " return cost" + ] + }, + { + "cell_type": "markdown", + "id": "f8c9b744-5c01-49e4-a8b0-3d39b0be7678", + "metadata": {}, + "source": [ + "Above we referred to using a classical optimizer. When we get to searching through weights to minimize the cost function, we will use the optimizer COBYLA:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d4d24583-5328-48b8-9cd5-f55655ce9c6e", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.optimize import minimize" + ] + }, + { + "cell_type": "markdown", + "id": "7a580ff4-e93a-40d1-8ebd-e2f91ad79fcf", + "metadata": {}, + "source": [ + "We will set some initial global variables for the cost function." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1f115364-ab1d-45e1-a83e-03adab7af304", + "metadata": {}, + "outputs": [], + "source": [ + "# Globals\n", + "circuit = full_circuit\n", + "observables = observable\n", + "# input_params = train_images_batch\n", + "# target = train_labels_batch\n", + "objective_func_vals = []\n", + "iter = 0" + ] + }, + { + "cell_type": "markdown", + "id": "f6ff34b6-cd3a-4755-8f69-ce891a5b0b8f", + "metadata": {}, + "source": [ + "## Qiskit Patterns Step 2: Optimize problem for quantum execution\n", + "We start by selecting a backend for execution. In this case, we will use the least-busy backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d68f1b36-f181-4880-9cb8-260bfc817b09", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_brisbane\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "print(backend.name)" + ] + }, + { + "cell_type": "markdown", + "id": "c8657861-ffee-4815-a7a4-2a51369c6301", + "metadata": {}, + "source": [ + "Here we optimize the circuit for running on a real backend by specifying the optimization_level and adding dynamical decoupling. The code below generates a pass manager using preset pass managers from qiskit.transpiler." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d2db83d-3d33-4159-aa3f-297e9507a497", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import XGate\n", + "from qiskit.transpiler import PassManager\n", + "from qiskit.transpiler.passes import (\n", + " ALAPScheduleAnalysis,\n", + " ConstrainedReschedule,\n", + " PadDynamicalDecoupling,\n", + ")\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "pm.scheduling = PassManager(\n", + " [\n", + " ALAPScheduleAnalysis(target=target),\n", + " ConstrainedReschedule(\n", + " acquire_alignment=target.acquire_alignment,\n", + " pulse_alignment=target.pulse_alignment,\n", + " target=target,\n", + " ),\n", + " PadDynamicalDecoupling(\n", + " target=target,\n", + " dd_sequence=[XGate(), XGate()],\n", + " pulse_alignment=target.pulse_alignment,\n", + " ),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9fb30d51-4455-4e2e-806c-8090819383b4", + "metadata": {}, + "source": [ + "Now we use the pass manager on the circuit. The layout changes that result must be applied to the observable as well. For very large circuits, the heuristics used in circuit optimization may not always yield the best and shallowest circuit. In those cases, it makes sense to run such pass managers several times and use the best circuit. We will see this later when we scale up our calculation." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "5d19c24d-48f5-48a0-bcee-12eab0468935", + "metadata": {}, + "outputs": [], + "source": [ + "circuit_ibm = pm.run(full_circuit)\n", + "observable_ibm = observable.apply_layout(circuit_ibm.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "6734be44-e544-4fab-aaaa-fe8a85257ecb", + "metadata": {}, + "source": [ + "## Qiskit Patterns Step 3: Execute using Qiskit Primitives\n", + "\n", + "### Loop over the dataset in batches and epochs\n", + "We first implement the full algorithm using a simulator for cursory debugging and for estimates of error. We can now go over the entire dataset in batches in desired number of epochs to train our quantum neural network." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "5ef31f11-e8c3-4e8d-86d3-2373113e4cdd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 0, batch: 0\n", + "Iter: 0, loss: 1.0002309063537163\n", + "Iter: 50, loss: 0.9434121445008878\n" + ] + } + ], + "source": [ + "from qiskit.primitives import StatevectorEstimator as Estimator\n", + "\n", + "batch_size = 140\n", + "num_epochs = 1\n", + "num_samples = len(train_images)\n", + "\n", + "# Globals\n", + "circuit = full_circuit\n", + "estimator = Estimator() # simulator for debugging\n", + "observables = observable\n", + "objective_func_vals = []\n", + "iter = 0\n", + "\n", + "# Random initial weights for the ansatz\n", + "np.random.seed(42)\n", + "weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", + "\n", + "for epoch in range(num_epochs):\n", + " for i in range((num_samples - 1) // batch_size + 1):\n", + " print(f\"Epoch: {epoch}, batch: {i}\")\n", + " start_i = i * batch_size\n", + " end_i = start_i + batch_size\n", + " train_images_batch = np.array(train_images[start_i:end_i])\n", + " train_labels_batch = np.array(train_labels[start_i:end_i])\n", + " input_params = train_images_batch\n", + " target = train_labels_batch\n", + " iter = 0\n", + " res = minimize(\n", + " mse_loss_weights, weight_params, method=\"COBYLA\", options={\"maxiter\": 100}\n", + " )\n", + " weight_params = res[\"x\"]" + ] + }, + { + "cell_type": "markdown", + "id": "6198a923-3edb-44ff-a081-cff91e62c325", + "metadata": {}, + "source": [ + "## Qiskit Patterns Step 4: Post-process, return result in classical format\n", + "### Testing and accuracy\n", + "We now interpret the results from training. We first test the training accuracy over the training set." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "d4badf34-7f0a-4319-8f6c-c74ea7c32ce5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-2.27688499e-02 -1.46227204e-02 -1.73927452e-02 9.93331786e-02\n", + " -4.85553548e-01 1.43558565e-01 8.34567054e-02 -1.40133992e-02\n", + " 1.52169596e-01 -1.95082515e-01 8.24373578e-03 -9.90696638e-02\n", + " -3.54268344e-02 -4.77017954e-01 1.38713848e-02 -2.99706215e-01\n", + " -5.78378029e-02 3.25528779e-02 -4.11354239e-02 -1.06483708e-01\n", + " 1.53095800e-01 2.90110884e-02 1.25745450e-02 6.46323079e-02\n", + " -1.53538943e-01 -1.57694952e-02 -1.67800067e-02 -1.99820822e-01\n", + " 1.70360075e-01 7.86148038e-03 -2.33373818e-02 6.64233020e-02\n", + " -1.14895445e-01 -1.11296215e-01 1.15120303e-01 -2.94096140e-01\n", + " -1.00531392e-03 -1.69209726e-01 -1.26120885e-01 3.26298176e-02\n", + " -1.33517383e-02 -5.86983444e-02 -4.32341361e-01 -4.36509551e-01\n", + " -4.17940102e-02 1.76935235e-03 8.14479984e-03 1.86985655e-01\n", + " -2.75525019e-01 -1.63229907e-03 -1.08571055e-01 -7.37452387e-04\n", + " -6.44440657e-02 6.72812834e-04 2.16785530e-03 1.41381850e-01\n", + " -9.82570410e-02 4.35973325e-01 -7.62261965e-02 -1.86193980e-01\n", + " -1.56971183e-02 -4.02757541e-01 -1.53869367e-01 2.29262129e-02\n", + " -7.02788246e-03 3.65719683e-02 4.68232163e-01 2.36434668e-02\n", + " -2.59520939e-02 3.70550137e-01 -1.19630110e-01 -5.79555318e-02\n", + " 2.09554455e-01 5.04689780e-02 7.39494314e-02 -1.77647326e-02\n", + " -1.45407207e-01 -9.54908878e-02 7.56029640e-02 -2.74049696e-02\n", + " 3.34885873e-01 1.58546171e-03 1.09339091e-01 -8.84693274e-02\n", + " -2.36450457e-02 1.41892239e-01 -2.34453218e-01 -7.50717757e-02\n", + " -1.13281310e-01 -1.66649414e-01 -3.17224197e-01 -6.38220597e-02\n", + " 3.28916563e-02 3.04739203e-02 2.67720196e-02 -1.16485785e-01\n", + " -3.08115732e-02 -2.95372010e-02 -7.54669023e-02 6.20013872e-02\n", + " -3.85258710e-01 -1.16456443e-01 -7.38548075e-02 -3.20558243e-02\n", + " -4.22284741e-02 1.01285659e-01 -1.76949246e-01 -2.02767491e-01\n", + " -1.12407344e-01 -3.81408267e-02 -4.33345231e-01 -9.24507501e-02\n", + " -4.21765393e-02 -6.06533771e-02 -2.22257783e-01 -1.17312535e-01\n", + " -6.74132262e-02 -2.76206274e-01 -9.13971800e-02 -2.27653991e-01\n", + " 1.66358563e-01 2.17230774e-04 5.76426304e-02 -2.82079169e-02\n", + " -1.15482051e-01 -3.46716009e-01 -3.21448755e-01 -5.20041405e-02\n", + " -2.16833625e-01 -1.06154654e-02 -7.74854811e-02 -3.28257935e-01\n", + " -7.83242410e-02 1.65547682e-01 -2.55294862e-01 -8.89085025e-02\n", + " 4.47581491e-01 1.92351832e-02 2.74083885e-02 -3.61304571e-01]\n", + "[-1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. -1. -1. -1. 1. -1. -1. 1.\n", + " -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. 1. 1. -1. 1. -1. -1. 1. -1.\n", + " -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1. -1. -1. 1.\n", + " 1. 1. -1. 1. -1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. 1. -1. -1.\n", + " 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.\n", + " -1. -1. 1. 1. 1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. -1. -1.\n", + " -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1.\n", + " -1. -1. -1. -1. -1. -1. -1. 1. -1. -1. 1. 1. 1. -1.]\n", + "[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]\n", + "Train accuracy: 60.0%\n" + ] + } + ], + "source": [ + "import copy\n", + "from sklearn.metrics import accuracy_score\n", + "from qiskit.primitives import StatevectorEstimator as Estimator # simulator\n", + "# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer\n", + "\n", + "estimator = Estimator()\n", + "# estimator = Estimator(backend=backend)\n", + "\n", + "pred_train = forward(circuit, np.array(train_images), res[\"x\"], estimator, observable)\n", + "# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)\n", + "\n", + "print(pred_train)\n", + "\n", + "pred_train_labels = copy.deepcopy(pred_train)\n", + "pred_train_labels[pred_train_labels >= 0] = 1\n", + "pred_train_labels[pred_train_labels < 0] = -1\n", + "print(pred_train_labels)\n", + "print(train_labels)\n", + "\n", + "accuracy = accuracy_score(train_labels, pred_train_labels)\n", + "print(f\"Train accuracy: {accuracy * 100}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "68003805-e24e-41db-a0c5-c19dab10a286", + "metadata": {}, + "source": [ + "The training accuracy is only $60%$, which is definitely not good. It is hard to imagine that the model's performance on the test set could be any better. Let's verify." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "50876c60-9fb5-4e0a-8a08-b7ca3694679b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-2.77978120e-01 -2.62194862e-01 4.59636095e-02 -8.09344165e-02\n", + " -2.97362966e-01 9.22947242e-02 2.06693174e-01 3.31629460e-02\n", + " 1.10971762e-03 -2.14602152e-01 -1.62671993e-01 -6.07179155e-04\n", + " -1.59948633e-01 -8.55722523e-02 -1.13057027e-01 -3.00187433e-01\n", + " -2.92832827e-01 7.38580629e-02 -6.03706270e-02 -8.57643552e-02\n", + " -1.52402062e-02 -3.57505447e-01 -3.54890597e-02 1.36534749e-01\n", + " -1.54688180e-01 -2.93714726e-01 1.89548513e-02 -6.15715564e-02\n", + " 1.11042670e-01 -2.22861100e-02 -3.84230105e-02 1.67351034e-01\n", + " -8.38766333e-02 2.56348613e-01 -1.10653111e-01 -1.18989476e-01\n", + " -6.75723266e-05 -6.88580547e-02 1.02431393e-02 -2.42125353e-01\n", + " -1.09142367e-01 -1.22540757e-01 -1.63735850e-01 3.93334838e-01\n", + " 2.36705685e-01 -2.34259814e-02 -3.91877756e-02 -1.95106746e-01\n", + " 1.86707523e-01 4.74775215e-02 -4.24907432e-02 -2.06453265e-01\n", + " 4.09184710e-02 -3.54762080e-02 -9.47513112e-02 2.97270112e-01\n", + " -2.99708696e-02 9.93941064e-03 -1.26760302e-01 -1.36183355e-01]\n", + "[-1. -1. 1. -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. -1. -1. -1. -1. 1.\n", + " -1. -1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. -1. 1. -1. 1. -1. -1.\n", + " -1. -1. 1. -1. -1. -1. -1. 1. 1. -1. -1. -1. 1. 1. -1. -1. 1. -1.\n", + " -1. 1. -1. 1. -1. -1.]\n", + "[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]\n", + "Test accuracy: 60.0%\n" + ] + } + ], + "source": [ + "pred_test = forward(circuit, np.array(test_images), res[\"x\"], estimator, observable)\n", + "# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)\n", + "\n", + "print(pred_test)\n", + "\n", + "pred_test_labels = copy.deepcopy(pred_test)\n", + "pred_test_labels[pred_test_labels >= 0] = 1\n", + "pred_test_labels[pred_test_labels < 0] = -1\n", + "print(pred_test_labels)\n", + "print(test_labels)\n", + "\n", + "accuracy = accuracy_score(test_labels, pred_test_labels)\n", + "print(f\"Test accuracy: {accuracy * 100}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "06d5336b-5ec6-4080-a983-9ca70faec269", + "metadata": {}, + "source": [ + "The model is not classifying these data well. We should ask why this is, and in particular, we should check:\n", + "* Did we stop the training too soon? Were more optimization steps needed?\n", + "* Did we construct a bad ansatz? This could mean a lot of things. When we work on real quantum computers, circuit depth will be a major consideration. The number of parameters is also potentially important, as is the entangling between qubits.\n", + "* Combining the two above, did we construct an ansatz with too many parameters to be trainable?\n", + "\n", + "We can start by checking for convergence in the optimization:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ecfddaa-7bd4-43d0-bfec-bd112854261e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "obj_func_vals_first = objective_func_vals\n", + "# import matplotlib.pyplot as plt\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(obj_func_vals_first, label=\"first ansatz\")\n", + "plt.xlabel(\"iteration\")\n", + "plt.ylabel(\"loss\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "c48ea583-7746-45ee-9437-c14ea11c773e", + "metadata": {}, + "source": [ + "We might try extending the optimization steps to make sure the optimizer didn't just get stuck in a local minimum in parameter space. But it looks fairly converged. Let's take a closer look at the images that were *not* classified correctly, and see if we can understand what is happening." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "6ce6c563-dbc2-4b79-8cad-05c8e6db4197", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "24\n" + ] + } + ], + "source": [ + "missed = []\n", + "for i in range(len(test_labels)):\n", + " if pred_test_labels[i] != test_labels[i]:\n", + " missed.append(test_images[i])\n", + "print(len(missed))" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "81ee504e-1c94-4600-8ab2-6406c473df73", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(12, 2, figsize=(6, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", + "for i in range(len(missed)):\n", + " ax[i // 2, i % 2].imshow(\n", + " missed[i].reshape(vert_size, hor_size),\n", + " aspect=\"equal\",\n", + " )\n", + "plt.subplots_adjust(wspace=0.02, hspace=0.025)" + ] + }, + { + "cell_type": "markdown", + "id": "d8c9e984-b21c-4aaf-8d14-bc1458bb7878", + "metadata": {}, + "source": [ + "Here we can see that the vast majority of the wrongly-classified images have a vertical line. Something about our model is failing to capture information about those. You may have seen this coming, based on the first variational circuit. Let's look at it more closely.\n", + "\n", + "## Improving the model\n", + "\n", + "### Step 1 revisited\n", + "\n", + "In mapping our problem to a quantum circuit, we should have explicitly thought about the how the information in adjacent pixels determines class. In order to identify horizontal lines, we want to know \"if pixel $i$ is yellow, is pixel $i+1$ yellow\" for all the pixels across each row. We also want to know about vertical lines. But since the classification is binary, one could imagine simply saying that if such a horizontal line is *not* detected, then it is a vertical line. Our previous variational circuit contained CNOT gates between qubits (and therefore pixels) 0 and 1, 1 and 2, and 2 and 3. That covers any horizontal lines across the top of the image, but it does not directly detect vertical lines, nor does it completely detect horizontal lines, as it ignores the lower row. To fully detect all horizontal lines, we would want to have a similar set of CNOT gates between qubits (pixels) 4 and 5, 5 and 6, and 6 and 7. We could keep in mind that adding CNOT gates between qubits corresponding to vertical lines (like 0 and 4, or 2 and 6) may also be useful. But we will first check whether it is sufficient to detect that there *is* or *is not* a horizontal line." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "0f0a7fd1-803a-446e-a761-3c9b41c9e3c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n", + "2+ qubit depth: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Initialize the circuit using the same number of qubits as the image has pixels\n", + "qnn_circuit = QuantumCircuit(size)\n", + "\n", + "# We choose to have two variational parameters for each qubit.\n", + "params = ParameterVector(\"θ\", length=2 * size)\n", + "\n", + "# A first variational layer:\n", + "for i in range(size):\n", + " qnn_circuit.ry(params[i], i)\n", + "\n", + "# Here is an extended list of qubit pairs between which we want CNOT gates. This now covers all\n", + "# pixels connected by horizontal lines.\n", + "qnn_cnot_list = [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 7]]\n", + "\n", + "for i in range(len(qnn_cnot_list)):\n", + " qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])\n", + "\n", + "# The second variational layer:\n", + "for i in range(size):\n", + " qnn_circuit.rx(params[size + i], i)\n", + "\n", + "# Check the circuit depth, and the two-qubit gate depth\n", + "print(qnn_circuit.decompose().depth())\n", + "print(\n", + " f\"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", + ")\n", + "\n", + "# Combine the feature map and variational circuit\n", + "ansatz = qnn_circuit\n", + "\n", + "# Combine the feature map with the ansatz\n", + "full_circuit = QuantumCircuit(num_qubits)\n", + "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", + "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", + "\n", + "# Display the circuit\n", + "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "23005560-df84-4f8b-b59c-da19bee66d93", + "metadata": {}, + "source": [ + "We have not increased the depth of the circuit. Let's see if we have increased its ability to model our images.\n", + "\n", + "### Step 2 revisited\n", + "\n", + "We will need to transpile this new circuit for running on a real quantum backend. Let's skip this step for now to see if our revision of the variational circuit has had the desired effect on simulators. We will go deeper into transpilation in the next subsection.\n", + "\n", + "### Step 3 revisited\n", + "\n", + "We now apply the updated model to our training data." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "400c8fda-ed07-4c50-8791-31f98a30e53c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 0, batch: 0\n", + "Iter: 0, loss: 1.0049762969140237\n", + "Iter: 50, loss: 0.8274276543780351\n" + ] + } + ], + "source": [ + "from qiskit.primitives import StatevectorEstimator as Estimator\n", + "\n", + "batch_size = 140\n", + "num_epochs = 1\n", + "num_samples = len(train_images)\n", + "\n", + "# Globals\n", + "circuit = full_circuit\n", + "estimator = Estimator() # simulator for debugging\n", + "observables = observable\n", + "objective_func_vals = []\n", + "iter = 0\n", + "\n", + "# Random initial weights for the ansatz\n", + "np.random.seed(42)\n", + "weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", + "\n", + "for epoch in range(num_epochs):\n", + " for i in range((num_samples - 1) // batch_size + 1):\n", + " print(f\"Epoch: {epoch}, batch: {i}\")\n", + " start_i = i * batch_size\n", + " end_i = start_i + batch_size\n", + " train_images_batch = np.array(train_images[start_i:end_i])\n", + " train_labels_batch = np.array(train_labels[start_i:end_i])\n", + " input_params = train_images_batch\n", + " target = train_labels_batch\n", + " iter = 0\n", + " res = minimize(\n", + " mse_loss_weights, weight_params, method=\"COBYLA\", options={\"maxiter\": 100}\n", + " )\n", + " weight_params = res[\"x\"]" + ] + }, + { + "cell_type": "markdown", + "id": "c91461ca-f80f-45dc-8de9-65dc86419b23", + "metadata": {}, + "source": [ + "### Step 4 revisited\n", + "\n", + "Let's start by checking whether our optimizer fully converged." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05c6f664-4043-446c-8fcc-11e77d6fe280", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "obj_func_vals_revised = objective_func_vals\n", + "# import matplotlib.pyplot as plt\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(obj_func_vals_revised, label=\"revised ansatz\")\n", + "plt.xlabel(\"iteration\")\n", + "plt.ylabel(\"loss\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6307a177-8d7c-4843-b3cd-45d7d3025e50", + "metadata": {}, + "source": [ + "This does not appear fully converged, as the loss function has not remained roughly level for substantially many steps. But the loss function is already ~60% lower than when using the previous variational circuit. If this were a research project, we would want to ensure full convergence. But for the purposes of exploration, this is sufficient. Let's check the accuracy on our training and testing data." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "3247933c-e0a2-413f-9658-8c001ef800f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 0.46144755 0.42579688 0.35255977 0.55207273 -0.48578418 0.50805845\n", + " 0.44892649 0.6173847 -0.62428139 0.40405121 0.46862421 0.29503395\n", + " -0.5740469 -0.71794562 -0.45022095 -0.45330418 -0.19795258 -0.46821777\n", + " -0.5622049 -0.32114059 0.54947838 -0.4889812 0.28327445 0.58149728\n", + " -0.27026749 0.41328304 0.21119412 0.60108606 0.39204178 -0.24974605\n", + " 0.38496469 0.39867586 -0.38946996 0.62616766 0.61212525 -0.49719567\n", + " 0.30860002 0.68443904 -0.27505907 -0.41508947 -0.49666422 0.67716994\n", + " -0.54696613 -0.70058779 0.42711815 -0.5285338 0.37678572 0.43888249\n", + " -0.30844464 0.42347715 -0.4250844 0.67324132 0.59914067 -0.45184567\n", + " 0.13604098 0.65336342 0.26099853 0.60316559 -0.38743183 -0.54784284\n", + " -0.29549031 -0.45592302 0.41613453 -0.38781528 0.56903087 0.54955451\n", + " 0.55532336 -0.3931852 -0.57599675 0.61246236 0.42014135 -0.38171749\n", + " 0.56760389 0.45383135 -0.50473943 -0.47551181 0.54221517 -0.64987023\n", + " 0.28845851 0.54403865 0.53841148 0.64477078 0.71912049 -0.63178323\n", + " -0.50764757 0.50304637 -0.38099972 -0.27707127 -0.24353841 -0.52045267\n", + " -0.61500665 0.65443173 0.31902266 -0.64969037 -0.4814051 0.47980608\n", + " -0.649786 -0.43048551 0.34562588 0.308998 -0.32454238 0.29558168\n", + " -0.45410187 0.54600712 0.33204827 0.22627804 0.4283921 0.56191874\n", + " -0.25400294 -0.6493613 -0.47445293 0.42272138 -0.35472546 -0.52240474\n", + " -0.45207595 0.40292125 -0.3361856 -0.46620886 0.60202719 -0.56505744\n", + " 0.47169796 -0.43577622 0.40689437 0.48869108 -0.39701189 -0.57698634\n", + " -0.39236332 0.31294648 0.41797597 0.63004836 -0.52884541 -0.43805812\n", + " -0.3193499 0.36860211 -0.49190995 0.65000193 0.50260077 -0.56737168\n", + " -0.29693083 -0.40956432]\n", + "[ 1. 1. 1. 1. -1. 1. 1. 1. -1. 1. 1. 1. -1. -1. -1. -1. -1. -1.\n", + " -1. -1. 1. -1. 1. 1. -1. 1. 1. 1. 1. -1. 1. 1. -1. 1. 1. -1.\n", + " 1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. 1. -1.\n", + " 1. 1. 1. 1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. 1. -1.\n", + " 1. 1. -1. -1. 1. -1. 1. 1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.\n", + " -1. 1. 1. -1. -1. 1. -1. -1. 1. 1. -1. 1. -1. 1. 1. 1. 1. 1.\n", + " -1. -1. -1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. 1. 1. -1. -1.\n", + " -1. 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. -1. -1. -1.]\n", + "[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]\n", + "Train accuracy: 100.0%\n" + ] + } + ], + "source": [ + "from sklearn.metrics import accuracy_score\n", + "from qiskit.primitives import StatevectorEstimator as Estimator # simulator\n", + "# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer\n", + "\n", + "estimator = Estimator()\n", + "# estimator = Estimator(backend=backend)\n", + "\n", + "pred_train = forward(circuit, np.array(train_images), res[\"x\"], estimator, observable)\n", + "# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)\n", + "\n", + "print(pred_train)\n", + "\n", + "pred_train_labels = copy.deepcopy(pred_train)\n", + "pred_train_labels[pred_train_labels >= 0] = 1\n", + "pred_train_labels[pred_train_labels < 0] = -1\n", + "print(pred_train_labels)\n", + "print(train_labels)\n", + "\n", + "accuracy = accuracy_score(train_labels, pred_train_labels)\n", + "print(f\"Train accuracy: {accuracy * 100}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "b2119e4a-0f7d-43be-a9d4-cf524ecb543d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.48396136 -0.57123828 0.28373249 0.38983869 -0.45799092 -0.63643031\n", + " 0.69164877 -0.47749808 0.16965244 -0.39669469 0.39366915 0.44206948\n", + " 0.69733951 0.40445979 -0.33663432 0.54511581 -0.49397081 0.55934553\n", + " 0.69269512 0.38875983 0.39724004 -0.49635863 -0.19131387 0.38813936\n", + " 0.39537369 -0.46262489 0.5307315 0.21783317 0.31949453 -0.49772087\n", + " 0.56409526 -0.66254365 -0.57507262 0.37363552 0.35154205 0.69295687\n", + " -0.31205475 0.37787066 0.67903997 -0.29984861 -0.46435535 -0.32610974\n", + " 0.4327188 0.64626537 0.37592731 -0.14328906 0.59694745 0.71880638\n", + " 0.32414334 0.42119333 -0.60745236 -0.42520033 0.28334222 0.21699081\n", + " 0.34837252 0.31538989 0.30754545 0.5995197 -0.34678026 -0.46587602]\n", + "[-1. -1. 1. 1. -1. -1. 1. -1. 1. -1. 1. 1. 1. 1. -1. 1. -1. 1.\n", + " 1. 1. 1. -1. -1. 1. 1. -1. 1. 1. 1. -1. 1. -1. -1. 1. 1. 1.\n", + " -1. 1. 1. -1. -1. -1. 1. 1. 1. -1. 1. 1. 1. 1. -1. -1. 1. 1.\n", + " 1. 1. 1. 1. -1. -1.]\n", + "[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]\n", + "Test accuracy: 100.0%\n" + ] + } + ], + "source": [ + "pred_test = forward(circuit, np.array(test_images), res[\"x\"], estimator, observable)\n", + "# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)\n", + "\n", + "print(pred_test)\n", + "\n", + "pred_test_labels = copy.deepcopy(pred_test)\n", + "pred_test_labels[pred_test_labels >= 0] = 1\n", + "pred_test_labels[pred_test_labels < 0] = -1\n", + "print(pred_test_labels)\n", + "print(test_labels)\n", + "\n", + "accuracy = accuracy_score(test_labels, pred_test_labels)\n", + "print(f\"Test accuracy: {accuracy * 100}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "a4fd4b77-bd33-40a2-8b58-291519a4ccca", + "metadata": {}, + "source": [ + "$100\\%$ accuracy on both sets! Our suspicion about accurate detection of horizontal lines being sufficient was correct! Further, our mapping from required information about the pixels to the CNOT gates in the quantum circuit was effective. Let's now look at how this process scales for running on real quantum computers." + ] + }, + { + "cell_type": "markdown", + "id": "0dc08c4a-0987-4bba-9bdf-2961e7ce794e", + "metadata": {}, + "source": [ + "## Scaling and running on real quantum computers\n", + "\n", + "### Data\n", + "\n", + "Let us begin by increasing the size of our images. There is nothing special about the choice of a 6x6 grid, except that it exceeds the number of qubits (32) that we can simulate for circuits using non-Clifford gates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63ad50d2-6edf-419a-9516-b1e30c232045", + "metadata": {}, + "outputs": [], + "source": [ + "# This code defines the images to be classified:\n", + "\n", + "import numpy as np\n", + "\n", + "# Total number of \"pixels\"/qubits\n", + "size = 36\n", + "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", + "vert_size = 6\n", + "# The length of the line to be detected (yellow). Must be less than or equal to the smallest\n", + "# dimension of the image (`<=min(vert_size,size/vert_size)`\n", + "line_size = 6\n", + "\n", + "\n", + "def generate_dataset(num_images):\n", + " images = []\n", + " labels = []\n", + " hor_array = np.zeros((size - (line_size - 1) * vert_size, size))\n", + " ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))\n", + "\n", + " j = 0\n", + " for i in range(0, size - 1):\n", + " if i % (size / vert_size) <= (size / vert_size) - line_size:\n", + " for p in range(0, line_size):\n", + " hor_array[j][i + p] = np.pi / 2\n", + " j += 1\n", + "\n", + " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\"\n", + " # at size/vert_size - linesize, because we want to fold this list into a grid.\n", + "\n", + " j = 0\n", + " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", + " for p in range(0, line_size):\n", + " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", + " j += 1\n", + "\n", + " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top\n", + " # of each other.\n", + "\n", + " for n in range(num_images):\n", + " rng = np.random.randint(0, 2)\n", + " if rng == 0:\n", + " labels.append(-1)\n", + " random_image = np.random.randint(0, len(hor_array))\n", + " images.append(np.array(hor_array[random_image]))\n", + " # Randomly select one of the several rows you made above.\n", + " elif rng == 1:\n", + " labels.append(1)\n", + " random_image = np.random.randint(0, len(ver_array))\n", + " images.append(np.array(ver_array[random_image]))\n", + " # Randomly select one of the several rows you made above.\n", + "\n", + " # Create noise\n", + " for i in range(size):\n", + " if images[-1][i] == 0:\n", + " images[-1][i] = np.random.rand() * np.pi / 4\n", + " return images, labels\n", + "\n", + "\n", + "hor_size = round(size / vert_size)" + ] + }, + { + "cell_type": "markdown", + "id": "079d21bb-585b-41f8-96d1-a1e0eb7766b3", + "metadata": {}, + "source": [ + "Because quantum computing time is a precious commodity, we will use a very small training set, and very few optimization steps. This will be sufficient to demonstrate the workflow." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "474d3a43-268b-425c-a3a9-165a34589d72", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "\n", + "np.random.seed(42)\n", + "# Here we specify a very small data set. Increase for realism, but monitor use of quantum computing\n", + "# time.\n", + "images, labels = generate_dataset(10)\n", + "\n", + "train_images, test_images, train_labels, test_labels = train_test_split(\n", + " images, labels, test_size=0.3, random_state=246\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "b106d32c-cd1f-4463-97fe-2f41695402fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Generate a figure with nested images using subplots.\n", + "\n", + "fig, ax = plt.subplots(2, 2, figsize=(10, 6), subplot_kw={\"xticks\": [], \"yticks\": []})\n", + "for i in range(4):\n", + " ax[i // 2, i % 2].imshow(\n", + " train_images[i].reshape(vert_size, hor_size),\n", + " aspect=\"equal\",\n", + " )\n", + "plt.subplots_adjust(wspace=0.1, hspace=0.025)" + ] + }, + { + "cell_type": "markdown", + "id": "21e4abd9-60bf-4a61-829b-c8b7b850530f", + "metadata": {}, + "source": [ + "### Step 1: Map the problem to a quantum circuit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c8619a-ec0b-4b6e-91fa-41a264fa7e32", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import z_feature_map\n", + "\n", + "# One qubit per data feature\n", + "num_qubits = len(train_images[0])\n", + "\n", + "# Data encoding\n", + "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure\n", + "# our data encoding goes to the first part of the circuit, the feature mapping.\n", + "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "34603184-d0cc-4ce2-87c4-a1e8b1bdbf7d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7\n", + "2+ qubit depth: 5\n" + ] + } + ], + "source": [ + "# This creates a circuit with the cxs in the compressed order.\n", + "\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import ParameterVector\n", + "\n", + "qnn_circuit = QuantumCircuit(size)\n", + "params = ParameterVector(\"θ\", length=2 * size)\n", + "for i in range(size):\n", + " qnn_circuit.ry(params[i], i)\n", + "\n", + "# CNOT gates between horizontally adjacent qubits.\n", + "for i in range(vert_size):\n", + " for j in range(hor_size):\n", + " if j < hor_size - 1:\n", + " qnn_circuit.cx((i * hor_size) + j, (i * hor_size) + j + 1)\n", + "\n", + "# CNOT gates between vertically adjacent qubits, likely not necessary based on our preliminary\n", + "# simulation.\n", + "# if i 1)}\"\n", + ")\n", + "# qnn_circuit_large.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "38153b01-8832-4eca-9892-262465d0f137", + "metadata": {}, + "source": [ + "This is a reasonable two-qubit depth. We should be able to get high-quality results from a real quantum computer." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "10cb9de5-3893-4174-a36b-7607ea908721", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11\n", + "2+ qubit depth: 5\n" + ] + } + ], + "source": [ + "# Combine the feature map and variational circuit\n", + "ansatz = qnn_circuit\n", + "\n", + "# Combine the feature map with the ansatz\n", + "full_circuit = QuantumCircuit(num_qubits)\n", + "full_circuit.compose(feature_map, range(num_qubits), inplace=True)\n", + "full_circuit.compose(ansatz, range(num_qubits), inplace=True)\n", + "\n", + "# Check the depth of the full circuit\n", + "print(full_circuit.decompose().depth())\n", + "print(\n", + " f\"2+ qubit depth: {full_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "53c81996-7269-4aca-893a-53082287ef6f", + "metadata": {}, + "source": [ + "Because we are using the ```z_feature_map```, which has no CNOT gates, adding the encoding layer does not increase our two-qubit depth. We can visualize the full circuit here." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "b43806aa-7d41-40b8-9e43-a604f0e42c26", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "full_circuit.decompose().draw(\"mpl\", style=\"clifford\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "e9cbfbd8-337c-4f10-b2de-5de8165a8fd1", + "metadata": {}, + "source": [ + "You may note that if minimizing two-qubit depth were of paramount importance, we could actually reduce it a bit by changing the order of the CNOTs. For example, the CNOTs on $q_{35}$ and $q_{34}$ could be moved to the left in the circuit diagram above, and could be placed directly below the CNOTs on $q_{30}$ and $q_{31}$, for example. For a two-qubit gate depth of 5, it isn't obvious that this will make a difference after transpilation, but it is something to keep in mind. If the order of the CNOT gates is important for logically matching the problem at hand, the depth here is fine. If the order of CNOTs is not critical to modeling the data structure in our images, then we could write a script to re-order these CNOT gates to minimize depth.\n", + "\n", + "We also need to re-define our observable with our larger images:" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "28932196-a5a2-4168-8101-3e5711e148f9", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "observable = SparsePauliOp.from_list([(\"Z\" * (num_qubits), 1)])" + ] + }, + { + "cell_type": "markdown", + "id": "c1eb102f-ab7e-4b9f-b09a-853cd2c7f9e7", + "metadata": {}, + "source": [ + "## Qiskit Patterns Step 2: Optimize problem for quantum execution\n", + "We start by selecting a backend for execution. In this case, we will use the least-busy backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "552bb4ef-a3c8-42fc-9ced-13c88564bf4c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_brisbane\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# To run on hardware, select the least busy quantum computer or specify a particular one.\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "# backend = service.backend(\"ibm_brisbaneane\")\n", + "\n", + "print(backend.name)" + ] + }, + { + "cell_type": "markdown", + "id": "94162dcb-e753-4faf-82e9-43096c6a56b0", + "metadata": {}, + "source": [ + "Once again, we are defining a pass manager, with optimization level set to 3." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16b93237-bbea-4da9-9f07-89c4f3e78717", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit.library import XGate\n", + "from qiskit.transpiler import PassManager\n", + "from qiskit.transpiler.passes import (\n", + " ALAPScheduleAnalysis,\n", + " ConstrainedReschedule,\n", + " PadDynamicalDecoupling,\n", + ")\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "pm.scheduling = PassManager(\n", + " [\n", + " ALAPScheduleAnalysis(target=target),\n", + " ConstrainedReschedule(\n", + " acquire_alignment=target.acquire_alignment,\n", + " pulse_alignment=target.pulse_alignment,\n", + " target=target,\n", + " ),\n", + " PadDynamicalDecoupling(\n", + " target=target,\n", + " dd_sequence=[XGate(), XGate()],\n", + " pulse_alignment=target.pulse_alignment,\n", + " ),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "74e91976-6c32-4731-8b46-ab6b9b903c88", + "metadata": {}, + "source": [ + "Now we will apply the pass manager several times. For very wide or very deep circuits, there can be large variability in the transpiled two-qubit depths. For such circuits it is important to try the pass manager many times and use the best (shallowest) result." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "1f12e420-7ac7-4744-b7ab-b664a0fa7b59", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[85, 85, 81, 89, 81, 81, 89, 85, 85]\n", + "[10, 10, 10, 10, 10, 10, 10, 10, 10]\n" + ] + } + ], + "source": [ + "# Try pass manager several times, since heuristics can return various transpilations on large\n", + "# circuits, and we want the shallowest.\n", + "\n", + "transpiled_qcs = []\n", + "transpiled_depths = []\n", + "transpiled_2q_depths = []\n", + "for i in range(1, 10):\n", + " circuit_ibm = pm.run(full_circuit)\n", + " transpiled_qcs.append(circuit_ibm)\n", + " transpiled_depths.append(circuit_ibm.decompose().depth())\n", + " transpiled_2q_depths.append(\n", + " circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n", + " )\n", + " # print(i)\n", + "\n", + "print(transpiled_depths)\n", + "print(transpiled_2q_depths)\n", + "\n", + "# Use the shallowest\n", + "\n", + "minpos = transpiled_2q_depths.index(min(transpiled_2q_depths))" + ] + }, + { + "cell_type": "markdown", + "id": "d12d9c3b-f93e-481f-bd46-70dc26ba6840", + "metadata": {}, + "source": [ + "We see that in this case, the transpiled two-qubit depth was always 10. There was minor variation in the single-qubit depth, and we will use the shallowest one. But on this 36-qubit circuit, this is not a critical improvement. We can visualize this transpiled circuit, although at this scale it becomes increasingly difficult to parse, visually." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "f58ba159-a866-4e46-9871-794c88e894b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "81\n", + "2+ qubit depth: 10\n" + ] + } + ], + "source": [ + "circuit_ibm = transpiled_qcs[2]\n", + "observable_ibm = observable.apply_layout(circuit_ibm.layout)\n", + "print(circuit_ibm.decompose().depth())\n", + "print(\n", + " f\"2+ qubit depth: {circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "342d4324-2abb-492d-86c9-401a39938e3d", + "metadata": {}, + "source": [ + "## Qiskit Patterns Step 3: Execute using Qiskit Primitives\n", + "\n", + "In order to limit time used on real quantum computers, we will only carry out a few optimization steps here, and we are doing so on a very small training set. But the scaling of this to more optimization steps and larger testing data sets should be clear from instructions throughout the lesson." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e3c0b51-588e-4807-8802-e377b3e89846", + "metadata": {}, + "outputs": [], + "source": [ + "# This was run on an Eagle r3 processor on 10-4-24, and took 7 min.\n", + "\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session\n", + "\n", + "batch_size = 7\n", + "num_epochs = 1\n", + "num_samples = len(train_images)\n", + "\n", + "# Globals\n", + "circuit = circuit_ibm\n", + "observable = observable_ibm\n", + "objective_func_vals = []\n", + "iter = 0\n", + "\n", + "# Random initial weights for the ansatz\n", + "np.random.seed(42)\n", + "# weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi\n", + "# Or re-load weights from a previous calculation\n", + "weight_params = np.array(\n", + " [\n", + " 3.35330497,\n", + " 5.97351416,\n", + " 4.59925358,\n", + " 3.76148219,\n", + " 0.98029403,\n", + " 0.98014248,\n", + " 0.3649501,\n", + " 6.44234523,\n", + " 3.77691701,\n", + " 4.44895122,\n", + " 0.12933619,\n", + " 6.09412333,\n", + " 5.23039137,\n", + " 1.33416598,\n", + " 1.14243996,\n", + " 1.15236452,\n", + " 1.91161039,\n", + " 3.2971419,\n", + " 3.71399059,\n", + " 1.82984665,\n", + " 3.84438512,\n", + " 0.87646578,\n", + " 1.83559896,\n", + " 2.30191935,\n", + " 2.86557222,\n", + " 4.93340606,\n", + " 1.25458737,\n", + " 3.23103027,\n", + " 3.72225051,\n", + " 0.29185655,\n", + " 3.81731689,\n", + " 1.07143467,\n", + " 0.40873121,\n", + " 5.96202367,\n", + " 6.067245,\n", + " 5.07931034,\n", + " 1.91394476,\n", + " 0.61369199,\n", + " 4.2991629,\n", + " 2.76555968,\n", + " 0.76678884,\n", + " 3.11128829,\n", + " 0.21606945,\n", + " 5.71342859,\n", + " 1.62596258,\n", + " 4.16275028,\n", + " 1.95853845,\n", + " 3.26768375,\n", + " 3.43508199,\n", + " 1.1614748,\n", + " 6.09207989,\n", + " 4.87030317,\n", + " 5.90304595,\n", + " 5.62236606,\n", + " 3.75671636,\n", + " 5.79230665,\n", + " 0.55601479,\n", + " 1.23139664,\n", + " 0.28417144,\n", + " 2.04411075,\n", + " 2.44213144,\n", + " 1.70493625,\n", + " 5.20711134,\n", + " 2.24154726,\n", + " 1.76516358,\n", + " 3.40986006,\n", + " 0.88545302,\n", + " 5.04035228,\n", + " 0.46841551,\n", + " 6.2007935,\n", + " 4.85215699,\n", + " 1.24856745,\n", + " ]\n", + ")\n", + "\n", + "# Running in a session avoids repeated queuing. This is available to Premium Plan, Flex Plan, and\n", + "# On-Prem (IBM Quantum Platform API) Plan users.\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options={\"resilience_level\": 1})\n", + "\n", + " for epoch in range(num_epochs):\n", + " for i in range((num_samples - 1) // batch_size + 1):\n", + " print(f\"Epoch: {epoch}, batch: {i}\")\n", + " start_i = i * batch_size\n", + " end_i = start_i + batch_size\n", + " train_images_batch = np.array(train_images[start_i:end_i])\n", + " train_labels_batch = np.array(train_labels[start_i:end_i])\n", + " input_params = train_images_batch\n", + " target = train_labels_batch\n", + " iter = 0\n", + " # We can increase maxiter to do a full optimization.\n", + " res = minimize(\n", + " mse_loss_weights,\n", + " weight_params,\n", + " method=\"COBYLA\",\n", + " options={\"maxiter\": 20},\n", + " )\n", + " weight_params = res[\"x\"]\n", + "session.close()\n", + "\n", + "# Open users can carry out the same calculation using a batch, but repeated queuing is possible.\n", + "# from qiskit_ibm_runtime import Batch\n", + "\n", + "# with Batch(backend=backend) as batch:\n", + "# estimator = Estimator(\n", + "# mode=batch, options={\"resilience_level\": 1}\n", + "# )\n", + "#\n", + "# for epoch in range(num_epochs):\n", + "# for i in range((num_samples - 1) // batch_size + 1):\n", + "# print(f\"Epoch: {epoch}, batch: {i}\")\n", + "# start_i = i * batch_size\n", + "# end_i = start_i + batch_size\n", + "# train_images_batch = np.array(train_images[start_i:end_i])\n", + "# train_labels_batch = np.array(train_labels[start_i:end_i])\n", + "# input_params = train_images_batch\n", + "# target = train_labels_batch\n", + "# iter = 0\n", + "# # We can increase maxiter to do a full optimization.\n", + "# res = minimize(\n", + "# mse_loss_weights,\n", + "# weight_params,\n", + "# method=\"COBYLA\",\n", + "# options={\"maxiter\": 20},\n", + "# )\n", + "# weight_params = res[\"x\"]\n", + "# batch.close()" + ] + }, + { + "cell_type": "markdown", + "id": "cb530676-4807-445c-a09b-e96903a3cd4e", + "metadata": {}, + "source": [ + "It is recommended that you save the weight parameters returned from this computation, should you decide to iterate further." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "cf939ac1-3811-42bf-94c6-ea4eb60fb77c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([3.35330497, 6.97351416, 5.59925358, 3.76148219, 0.98029403,\n", + " 0.98014248, 0.3649501 , 6.44234523, 3.77691701, 4.44895122,\n", + " 1.12933619, 7.09412333, 5.23039137, 1.33416598, 1.14243996,\n", + " 1.15236452, 1.91161039, 3.2971419 , 3.71399059, 1.82984665,\n", + " 3.84438512, 0.87646578, 1.83559896, 2.30191935, 2.86557222,\n", + " 4.93340606, 1.25458737, 3.23103027, 3.72225051, 0.29185655,\n", + " 3.81731689, 1.07143467, 0.40873121, 5.96202367, 6.067245 ,\n", + " 5.07931034, 1.91394476, 0.61369199, 4.2991629 , 2.76555968,\n", + " 0.76678884, 3.11128829, 0.21606945, 5.71342859, 1.62596258,\n", + " 4.16275028, 1.95853845, 3.26768375, 3.43508199, 1.1614748 ,\n", + " 6.09207989, 4.87030317, 5.90304595, 5.62236606, 3.75671636,\n", + " 5.79230665, 0.55601479, 1.23139664, 0.28417144, 2.04411075,\n", + " 2.44213144, 1.70493625, 5.20711134, 2.24154726, 1.76516358,\n", + " 3.40986006, 0.88545302, 5.04035228, 0.46841551, 6.2007935 ,\n", + " 4.85215699, 1.24856745])" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "weight_params" + ] + }, + { + "cell_type": "markdown", + "id": "36e36864-f02f-4700-83aa-cd69fb08dcfd", + "metadata": {}, + "source": [ + "We can plot these first few optimization steps, although we would not expect any convergence after just a few optimization steps. These curves have been relatively flat for the first few steps, even using simulators. We should note, however, that the optimization currently has 72 free parameters. This can be reduced by at least a factor of 2-3 without compromising results by, for example, parameterizing qubits with data corresponding to a subset of full rows and columns. Indeed, the parameter space should be reduced before spending more quantum computing time on minimizing the loss function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8244ff1-a7c5-46e1-87b1-e20b506ef4d8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "obj_func_vals_qc = objective_func_vals\n", + "# import matplotlib.pyplot as plt\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(obj_func_vals_qc, label=\"revised ansatz\")\n", + "plt.xlabel(\"iteration\")\n", + "plt.ylabel(\"loss\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b2e4dd0b-9951-4a6a-8920-fb756b39ea73", + "metadata": {}, + "source": [ + "### Closing\n", + "\n", + "To recap, in this lesson we learned the workflow for binary classification of images using a quantum neural network. Some key considerations in each Qiskit patterns step were:\n", + "\n", + "__Step 1:__ Map the problem to a quantum circuit\n", + "* Load training data. This could be done \"by hand\" or using a pre-built feature map like ```z_feature_map```.\n", + "* Construct an ansatz containing rotation and entanglement layers that are appropriate for your problem.\n", + "* Monitor circuit depth to ensure quality results on quantum computers.\n", + "\n", + "__Step 2:__ Optimize problem for quantum execution\n", + "* Select a backend, often the least busy one.\n", + "* Use a pass manager to transpile both the circuit and the observables to the architecture of the chosen backend.\n", + "* For very deep or wide circuits, transpile multiple times, and select the shallowest circuit.\n", + "\n", + "__Step 3:__ Execute using Qiskit (Runtime) Primitives\n", + "* Carry out preliminary trials on simulators to debug and optimize your ansatz.\n", + "* Execute on an IBM® quantum computer.\n", + "\n", + "__Step 4:__ Post-process, return result in classical format\n", + "* Calculate model accuracy on training data, and on testing data.\n", + "* Monitor convergence of the classical optimization." + ] + }, + { + "cell_type": "markdown", + "id": "8ca1b39a-95ca-4a28-9689-a07085532c91", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] https://arxiv.org/abs/2405.00781" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/courses/quantum-safe-cryptography/symmetric-key-cryptography.ipynb b/learning/courses/quantum-safe-cryptography/symmetric-key-cryptography.ipynb index 717719dd3da..b9b0de004da 100644 --- a/learning/courses/quantum-safe-cryptography/symmetric-key-cryptography.ipynb +++ b/learning/courses/quantum-safe-cryptography/symmetric-key-cryptography.ipynb @@ -1,649 +1,653 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "14dd451a", - "metadata": {}, - "source": [ - "---\n", - "title: Symmetric key cryptography\n", - "description: In this lesson we will look at symmetric key cryptography which secures much of the data at rest and in transit by virtue of its efficiency.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore ciphertext, cryptosystems, Rijndael, Daemen, Rijmen, monoalphabetic, secretpy, Smid, Foti, lambda, encryptor, encypt, Schneier, ciphertexts, informationally, polyalphabetic, Vigenère, anagramming, Ciphertext, Prichett, Cryptosystems, Caeser, Vigenere, FIPS, Nechvatal, Dworkin, Roback, Technol, Codebook, cryptologists, keystreams, keystream, indcpa, Ehrsam, Tuchman, Bonnetain, Naya, Plasencia, IACR, multiround, Feistel, fips, Twofish, sbox, Rivest, QUIC, quic, Tomoiaga, Stratulat, digitalsignature, STOC, Leurent, qcaa, quantizations, secmargin, Cryptoanalytic, Malviya, Tiwari, Chawla, EUROCRYPT, Coron, semsec, Shamir, Adleman, Nazario */}" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "ea2ca7f5", - "metadata": {}, - "source": [ - "# Symmetric key cryptography\n", - "\n", - "In this lesson we will look at symmetric key cryptography which secures much of the data at rest and in transit by virtue of its efficiency.\n", - "\n", - "By the end of the lesson we will have covered:\n", - " - What symmetric key cryptography is\n", - " - Python code examples demonstrating the use of symmetric key cryptography\n", - " - A look at applications of symmetric key cryptography\n", - " - Symmetric key cryptography applications\n", - " - The security of symmetric key cryptography\n", - " - Threats to these algorithms from both classical and quantum computers" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "59728408", - "metadata": {}, - "source": [ - "## Introduction to symmetric key cryptography\n", - "\n", - "*Symmetric key cryptography (SKC)* is the oldest and most intuitive form of cryptography. With SKC, confidential information is secured through symmetric key encryption (SKE), that is, by using a *single secret key* for both encryption and decryption.\n", - "\n", - "SKC involves:\n", - "- An encryption function that converts a given plain text instance to ciphertext while utilizing a secret key\n", - "- A decryption function that inverts the operation by converting the ciphertext back to plain text using the same secret key\n", - "\n", - "Plain text can mean any kind of unencrypted data such as natural language text or binary code whose information content is in principle directly accessible, while ciphertext refers to encrypted data whose information content is intended to be inaccessible prior to decryption.\n", - "\n", - "An algorithm that describes the encryption and decryption operations using a shared secret key is also called a symmetric cipher.\n", - "\n", - "![Fig 1: Symmetric key encryption of a given plaintext to ciphertext and decryption back to plaintext using the same key.](/learning/images/courses/quantum-safe-cryptography/symmetric-key-cryptography/skc.svg)\n", - "\n", - "*Figure 1. Symmetric key encryption of a given plain text to ciphertext and decryption back to plain text using the same key.*" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3b1ddcc3", - "metadata": {}, - "source": [ - "## Properties of symmetric key cryptosystems\n", - "\n", - "A symmetric key cryptosystem should ensure the following properties to secure messages-both statically stored data and/or communications over some transmission channel:\n", - "\n", - "- **Confidentiality:** Refers to the property that the information content of encrypted messages is protected from unauthorized access.\n", - "- **Integrity:** Refers to the property that any tampering of encrypted messages during storage or transmission can be detected.\n", - "- **Authenticity:** Refers to the property that the receiver of a message can verify the identity of the sender and detect impersonation by an unauthorized party.\n", - "\n", - "Furthermore, these properties should be realized in a setting where the algorithms or ciphers used for encryption and decryption may be public and where access to the information content of encrypted messages is controlled exclusively through access to the secret key.\n", - "\n", - "Implementing a secure symmetric key cryptosystem therefore involves two main tasks:\n", - "\n", - "1. Employing a robust symmetric key encryption algorithm resistant to cryptographic attacks.\n", - "2. Ensuring confidentiality in the distribution and management of secret keys.\n", - "\n", - "In this lesson, we will discuss aspects related to the first task, which forms the primary concern of SKC technology. The second task, however, needs solutions that fall outside of SKC itself and will be introduced later." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "442a9abb", - "metadata": {}, - "source": [ - "## Illustration of symmetric key encryption using python\n", - "\n", - "We show a simple example of the encrypt and decrypt operations using the classical Caesar shift cipher and the modern Advanced Encryption System (AES), which has been the standard for symmetric key encryption since 2001." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "45e8c800", - "metadata": {}, - "source": [ - "First we set up some Python libraries that provide the needed symmetric key encryption ciphers, and then define the plain text we wish to encrypt." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "06552f57", - "metadata": {}, - "outputs": [], - "source": [ - "# Install the library if needed\n", - "# %pip install secretpy\n", - "\n", - "# import the required crypto functions which will be demonstrated later\n", - "from secretpy import Caesar\n", - "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n", - "from functools import reduce\n", - "import numpy as np\n", - "\n", - "# Set the plaintext we want to encrypt\n", - "plaintext = \"this is a strict top secret message for intended recipients only\"\n", - "print(f\"\\nGiven plaintext: {plaintext}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "0a215aa9", - "metadata": {}, - "source": [ - "We will see how to encrypt and decrypt it using two different symmetric key encryption methods:\n", - "\n", - "1. The classic [Caesar shift cipher](https://en.wikipedia.org/w/index.php?title=Caesar_cipher&oldid=1151194063)\n", - "2. The modern [Advanced Encryption Standard](https://www.nist.gov/publications/development-advanced-encryption-standard) AES-256 protocol" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "eb46068d", - "metadata": {}, - "source": [ - "### Caesar shift cipher:\n", - "\n", - "Caesar shift encryption involves defining\n", - " - An alphabet of possible characters to encode\n", - " - A *shift value* which can be between 0 (unencrypted) and the length of the alphabet. We consider this the *key*.\n", - "\n", - "It is known as a *monoalphabetic substitution cipher* since each letter of the plain text is substituted with another in the ciphertext.\n", - "\n", - "In this example we will use lowercase letters of the alphabet.\n", - "\n", - "Let's start by setting things up." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ecaf0d3c", - "metadata": {}, - "outputs": [], - "source": [ - "# initialize the required python object for doing Caesar shift encryption\n", - "caesar_cipher = Caesar()\n", - "\n", - "# Define the shift, ie the key\n", - "caesar_key = 5\n", - "print(f\"Caesar shift secret key: {caesar_key}\")\n", - "\n", - "# Define the alphabet\n", - "alphabet = (\n", - " \"a\",\n", - " \"b\",\n", - " \"c\",\n", - " \"d\",\n", - " \"e\",\n", - " \"f\",\n", - " \"g\",\n", - " \"h\",\n", - " \"i\",\n", - " \"j\",\n", - " \"k\",\n", - " \"l\",\n", - " \"m\",\n", - " \"n\",\n", - " \"o\",\n", - " \"p\",\n", - " \"q\",\n", - " \"r\",\n", - " \"s\",\n", - " \"t\",\n", - " \"u\",\n", - " \"v\",\n", - " \"w\",\n", - " \"x\",\n", - " \"y\",\n", - " \"z\",\n", - " \" \",\n", - ")\n", - "print(f\"alphabet: {alphabet}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "bb662f02", - "metadata": {}, - "source": [ - "Encrypt the plain text to get ciphertext for the Caesar cipher." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af9f3eb4", - "metadata": {}, - "outputs": [], - "source": [ - "caeser_ciphertext = caesar_cipher.encrypt(plaintext, caesar_key, alphabet)\n", - "print(f\"Encrypted caeser shift ciphertext: {caeser_ciphertext}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a74671e1", - "metadata": {}, - "source": [ - "Decrypt the ciphertext back to the original plain text using the same key used for encryption." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "da614545", - "metadata": {}, - "outputs": [], - "source": [ - "caeser_plaintext = caesar_cipher.decrypt(caeser_ciphertext, caesar_key, alphabet)\n", - "print(f\"Decrypted caeser shift plaintext: {caeser_plaintext}\\n\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "224337cd", - "metadata": {}, - "source": [ - "### Advanced encryption standard (AES) cipher\n", - "\n", - "We now encrypt the plain text using AES, a popular symmetric key encryption algorithm.\n", - "\n", - "We start by creating the key, in this case, a random 16-letter string." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a7d8af57", - "metadata": {}, - "outputs": [], - "source": [ - "# lambda defines an inline function in this case that takes two values a,b with the resulting expression of a+b\n", - "# reduce uses a two-argument function(above), and applies this to all the entries in the list (random alphabet characters) cumulatively\n", - "aes_key = reduce(lambda a, b: a + b, [np.random.choice(alphabet) for i in range(16)])\n", - "\n", - "print(f\"AES secret key: {aes_key}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "15868918", - "metadata": {}, - "source": [ - "AES supports multiple operating modes and requires we specify which to use.\n", - "\n", - "We choose the *Cipher Block Chaining* (CBC) mode provided by the `modes.CBC` class of the `cryptography` library. The CBC mode of AES uses randomness for additional security. This requires specifying a random *Initialization Vector* (IV), also called a *nonce*. We will use a random string for this as well, just like we did for the key." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2e4df17", - "metadata": {}, - "outputs": [], - "source": [ - "aes_initialization_vector = reduce(\n", - " lambda a, b: a + b, [np.random.choice(alphabet) for i in range(16)]\n", - ")\n", - "print(f\"AES initialization vector: {aes_initialization_vector}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b9f32932", - "metadata": {}, - "source": [ - "We can now instantiate an AES cipher on behalf of the sender of the secret message. Note that the initialization vector is passed to the `modes.CBC` class to set up the CBC mode of operation.\n", - "\n", - "We will then encrypt the plain text to send." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa477669", - "metadata": {}, - "outputs": [], - "source": [ - "# The encryptor is setup using the key and CBC. In both cases we need to convert the string (utf-8) into bytes\n", - "sender_aes_cipher = Cipher(\n", - " algorithms.AES(bytes(aes_key, \"utf-8\")),\n", - " modes.CBC(bytes(aes_initialization_vector, \"utf-8\")),\n", - ")\n", - "aes_encryptor = sender_aes_cipher.encryptor()\n", - "\n", - "# update can add text to encypt in chunks, and then finalize is needed to complete the encryption process\n", - "aes_ciphertext = (\n", - " aes_encryptor.update(bytes(plaintext, \"utf-8\")) + aes_encryptor.finalize()\n", - ")\n", - "\n", - "# Note the output is a string of bytes\n", - "print(f\"Encrypted AES ciphertext: {aes_ciphertext}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d4c641b9", - "metadata": {}, - "source": [ - "To decrypt it, let us instantiate an AES cipher on behalf of the receiver. Note that the intended receiver has access to both the secret key and the initialization vector, but the latter is not required to be secret." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10571495", - "metadata": {}, - "outputs": [], - "source": [ - "# Similar setup of AES to what we did for encryption, but this time, for decryption\n", - "receiver_aes_cipher = Cipher(\n", - " algorithms.AES(bytes(aes_key, \"utf-8\")),\n", - " modes.CBC(bytes(aes_initialization_vector, \"utf-8\")),\n", - ")\n", - "aes_decryptor = receiver_aes_cipher.decryptor()\n", - "\n", - "# Do the decryption\n", - "aes_plaintext_bytes = aes_decryptor.update(aes_ciphertext) + aes_decryptor.finalize()\n", - "\n", - "# convert back to a character string (we assume utf-8)\n", - "aes_plaintext = aes_plaintext_bytes.decode(\"utf-8\")\n", - "\n", - "print(f\"Decrypted AES plaintext: {aes_plaintext}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "80ef03c1", - "metadata": {}, - "source": [ - "## Applications of symmetric key cryptography\n", - "\n", - "While classical ciphers such as the Caesar cipher fell out of use a long time ago, modern symmetric cryptosystems such as AES are [deployed in a wide range of applications](https://books.google.com/books?id=01vfjgEACAAJ), including:\n", - "\n", - "1. **Data encryption and decryption:** SKC is widely used to protect sensitive data, whether statically stored on a device or transmitted over a network. Examples include protecting user credentials, encrypting email messages, and securing financial transactions, among others.\n", - "\n", - "2. **Secure communication:** Common communication protocols such as SSL/[TLS](https://en.wikipedia.org/w/index.php?title=Transport_Layer_Security&oldid=1155601531) use a combination of symmetric and asymmetric key encryption to ensure the confidentiality and integrity of data exchanged between two parties. These messages are encrypted and decrypted using symmetric key encryption which uses a shared key. The key used in symmetric key encryption is securely exchanged using asymmetric key encryption which uses a public-private key pair. Symmetric key encryption is much faster and hence can be used for encryption of messages of large size.\n", - "\n", - "3. **Authenticity verification:** In some settings, SKC is employed through techniques like [message authentication codes](https://en.wikipedia.org/w/index.php?title=Message_authentication_code&oldid=1151014755) (MACs) and keyed-hash MACs (HMAC) to verify the authenticity and integrity of messages, ensuring tamper-resistant communication.\n", - "\n", - "4. **File and disk encryption:** Full-disk encryption software and file encryption tools employ SKC to protect sensitive data stored on hard disks or portable storage devices.\n", - "\n", - "5. **Virtual private networks:** [VPN](https://en.wikipedia.org/w/index.php?title=Virtual_private_network&oldid=1154342615) technologies, which aim to provide confidential communication channels free from eavesdropping, can use symmetric or asymmetric key encryption to connect remote users as well as corporate networks.\n", - "\n", - "The diverse array of applications in which SKC is deployed in turn require that symmetric cryptosystems satisfy a certain set of criteria." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "492f9e09", - "metadata": {}, - "source": [ - "## Principles of symmetric key encryption\n", - "In this section, we will discuss some of the basic [principles underlying the security of symmetric key encryption algorithms](https://doi.org/10.1080/07468342.1987.11973000).\n", - "\n", - "**Resistance to brute force attack:** The most basic requirement for the security of an encryption cipher is that the key space size — in other words, the number of possible distinct keys from which someone using the algorithm could have chosen — is very large.\n", - "\n", - "**Resistance to cryptanalytic attack:** The second basic requirement for a cipher, symmetric or otherwise, is that it can generate ciphertexts that are informationally inscrutable. To this end, a necessary but not sufficient condition from an information theory perspective is that [ciphertexts should be characterized by high entropy](https://en.wikipedia.org/w/index.php?title=Entropy_(information_theory)&oldid=1154931420), making them indistinguishable from random text with no discernible patterns or correlations. This way, an attacker can gain no information about the plain text or secret key by trying to analyze the ciphertext using frequency analysis or other statistical techniques.\n", - "\n", - "Resistance to general forms of cryptanalytic attacks sufficient to ensure [semantic security](https://en.wikipedia.org/w/index.php?title=Semantic_security&oldid=1133491450) is formalized via the notion of [*indistinguishability*](https://en.wikipedia.org/w/index.php?title=Ciphertext_indistinguishability&oldid=1130147047). While there are several variants of indistinguishability with distinct requirements, a symmetric cryptosystem is considered to be semantically secure if it satisfies the criterion of [*Indistinguishability under Chosen Plain Text Attack*](https://en.wikipedia.org/w/index.php?title=Ciphertext_indistinguishability&oldid=1130147047) (IND-CPA). This means that an attacker cannot distinguish between the encryptions of two different messages even if allowed to send multiple plain texts of their choosing to the algorithm and view the corresponding ciphertexts.\n", - "\n", - "As we will consider later, IND-CPA typically requires the use of randomness to ensure that each time a given plain text is encrypted with a given secret key, the resulting ciphertext is unpredictably different for each encryption.\n", - "\n", - "**Failure modes of classical ciphers:** Before the advent of modern cryptography in the 1970s, most classical ciphers in practical use failed to satisfy one or both above requirements. For instance, early substitution ciphers such as the monoalphabetic [Caesar shift cipher](https://en.wikipedia.org/w/index.php?title=Caesar_cipher&oldid=1151194063) were characterized by both a small key space size (see Table 1) and low entropy ciphertext, making them insecure against a variety of cryptanalytic attacks such as brute force attacks, frequency analysis, and known-plain-text (KPT) attacks.\n", - "\n", - "Subsequent polyalphabetic substitution ciphers such as the [Vigenère cipher](https://en.wikipedia.org/w/index.php?title=Vigen%C3%A8re_cipher&oldid=1155012941) and the [Enigma machine cipher](https://en.wikipedia.org/w/index.php?title=Enigma_machine&oldid=1154440070) featured effectively large key space sizes, making them resistant to brute force attacks, but they were susceptible to frequency analysis and KPT attacks, respectively. Similarly to substitution ciphers, [classic transposition ciphers](https://en.wikipedia.org/w/index.php?title=Transposition_cipher&oldid=1151499771), which rearrange letters in a message instead of substituting them, are also compromised by a variety of attacks such as anagramming, statistical analysis, brute force, and KPT attacks, among others.\n", - "\n", - "Theoretically, a polyalphabetic substitution cipher known as the [one-time pad](https://en.wikipedia.org/w/index.php?title=One-time_pad&oldid=1155039884) (OTP) is known to be cryptographically secure. An OTP features a secret key that should be (1) composed of randomly chosen letters or bits, (2) at least as long as the original plain text, and (3) used only once. An OTP is impractical for actual applications because if the secret key — which is required to be as long as the plain text and can be used only once — could be shared securely, then so could the original plain text. The OTP instead illustrates the utility of randomness in generating secure ciphertexts.\n", - "\n", - "An attacker trying to implement a brute force search through the key space to find a key that decrypts the message has to perform a number of operations proportional to the key space size.\n", - "\n", - "Therefore, a large key space size provides resistance against brute force attacks by rendering them computationally infeasible. Table 1 lists the key space sizes of some well-known ciphers." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "47b2e4e8", - "metadata": {}, - "source": [ - "### Table 1: Key space sizes of some symmetric ciphers\n", - "\n", - "\n", - "\n", - "| Cipher | Key length | Key space size |\n", - "|--------------|------------------|-------------------------------------------|\n", - "| Caeser shift | 1 | alphabet-size |\n", - "| Vigenere | n | alphabet-size$^\\mathrm{n}$ |\n", - "| One-time-pad | plaintext-length | alphabet-size$^\\mathrm{plaintext-length}$ |\n", - "| DES | 56 | 2$^\\mathrm{56}$ |\n", - "| AES-128 | 128 | 2$^\\mathrm{128}$ |\n", - "| AES-192 | 192 | 2$^\\mathrm{192}$ |\n", - "| AES-256 | 256 | 2$^\\mathrm{256}$ |\n", - "| ChaCha20 | 256 | 2$^\\mathrm{256}$ |" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "66cd329b", - "metadata": {}, - "source": [ - "Modern symmetric key encryption schemes largely overcome the limitations of the classical ciphers. They produce ciphertext resistant to and feature large key space sizes while also being much more practically efficient than an OTP.\n", - "\n", - "**Block ciphers**: One class of modern ciphers — such as [DES](https://en.wikipedia.org/w/index.php?title=Data_Encryption_Standard&oldid=1154751166) and [AES](https://www.nist.gov/publications/development-advanced-encryption-standard) — achieve security by combining the principles of confusion and diffusion [originally introduced by Claude Shannon](https://en.wikipedia.org/wiki/A_Mathematical_Theory_of_Communication#:~:text=%22A%20Mathematical%20Theory%20of%20Communication,the%20generality%20of%20this%20work). We discuss these notions in a setting where encryption schemes work with binary representations of messages:\n", - "\n", - "- **Confusion**: Confusion is the characteristic whereby each bit in the ciphertext depends on multiple bits of the secret key. It ensures that a small change in the secret key modifies almost all the bits of the ciphertext, obscuring the relationship between the ciphertext and the secret key.\n", - "\n", - "- **Diffusion**: Diffusion is the characteristic whereby flipping a single bit in the plain text should modify roughly half the bits in the ciphertext and vice versa. Diffusion hides statistical relationships between the plain text and ciphertext. Ciphers with adequate diffusion satisfy the so-called *avalanche criterion* of cryptography.\n", - "\n", - "Block ciphers implement confusion and diffusion using cryptographic structures known as [substitution-permutation networks](https://en.wikipedia.org/w/index.php?title=Substitution%E2%80%93permutation_network&oldid=1098155951) (SPNs) operating on discrete blocks of data. An SPN accepts a block of plain text and the secret key as inputs and performs a specified number of [*rounds*](https://en.wikipedia.org/w/index.php?title=Round_(cryptography)&oldid=1149554107) of transformations to yield a ciphertext block. Each round is composed of alternating mathematical structures known as substitution boxes [(S-boxes)](https://en.wikipedia.org/w/index.php?title=S-box&oldid=1154458964) and permutation boxes [(P-boxes)](https://en.wikipedia.org/w/index.php?title=Permutation_box&oldid=1151700196) or equivalent operations.\n", - "\n", - "These respectively implement complex nonlinear and linear transformations on the input blocks, leading to [*avalanche effects*](https://en.wikipedia.org/w/index.php?title=Avalanche_effect&oldid=1148010716) in the ciphertext.\n", - "\n", - "SPNs are designed in such a way that increasing the number of rounds typically increases the security of the cipher. This leads to the notion of [*security margin*](https://pmc.ncbi.nlm.nih.gov/articles/PMC4878865/)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a159587c", - "metadata": {}, - "source": [ - "**Security margin:** The security margin of a given SPN-based cryptographic cipher is the difference between the number of rounds in the complete implementation of the cipher and the maximum number of rounds that are known to be breakable using the best-known real-world attack.\n", - "\n", - "For instance, currently the best-known faster-than-brute-force attacks against AES-256 can break up to 9 rounds out of the total 14 rounds in the full cipher when used in the standard mode known as *Electronic Codebook* (ECB) mode. Therefore, [currently the security margin of AES-256 is 5 rounds](https://doi.org/10.13154/tosc.v2019.i2.55-93).\n", - "\n", - "**Stream ciphers:** As an alternative to block ciphers, modern cryptologists have also designed practically secure [*stream ciphers*](https://en.wikipedia.org/w/index.php?title=Stream_cipher&oldid=1143750236) such as [Chacha20](https://en.wikipedia.org/w/index.php?title=Salsa20&oldid=1145010249). These ciphers utilize randomness as a fundamental part of their design and operate on pseudorandom *keystreams* of bits instead of discrete blocks of data.\n", - "\n", - "Accordingly, stream ciphers combine a secret key and an [initialization vector](https://en.wikipedia.org/w/index.php?title=Initialization_vector&oldid=1136156102) (IV) to seed a [pseudorandom random number generator](https://en.wikipedia.org/w/index.php?title=Pseudorandom_number_generator&oldid=1153255020) (PRNG) to produce a keystream of random bits which are then combined with the given plain text to yield the ciphertext. In this sense, stream ciphers are similar to a one-time pad (OTP) but feature shorter secret key lengths and reusable keys, which makes them more practical. However, for the same reason, they do not guarantee perfect secrecy, unlike an OTP.\n", - "\n", - "**Semantic security**: We conclude this subsection by returning to the notion of semantic security or IND-CPA level security introduced above. The basic operations implemented by [block ciphers](https://en.wikipedia.org/w/index.php?title=Block_cipher_mode_of_operation&oldid=1154901199#cite_note-23) such as S-box and P-box are deterministic. This means that in standard operating modes such as ECB, a given plain-text key pair always yields the same ciphertext, a state of affairs that is susceptible to chosen-plain-text attacks.\n", - "\n", - "To achieve IND-CPA level security, block ciphers need to operate in a mode that utilizes randomness introduced via a pseudorandom initialization vector (IV) with the additional requirement that no two encryption operations use the same key-IV pair. AES supports several modes of operation, such as [cipher block chaining](https://patents.google.com/patent/US4074066A/en) (CBC), which are IND-CPA secure. A similar requirement also holds for stream ciphers whereby the same key-IV pair should not be used to seed the PRNG more than once if IND-CPA is desired." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "004fa4ed", - "metadata": {}, - "source": [ - "## Popular symmetric key algorithms\n", - "\n", - "Having introduced some basic principles of SKC, we now list a few popular symmetric key algorithms to illustrate a variety of the approaches pursued in modern cryptosystems. Modern block ciphers and stream ciphers are both employed in different contexts as illustrated below.\n", - "\n", - "1. [**Advanced Encryption Standard:**](https://www.nist.gov/publications/development-advanced-encryption-standard) AES, already introduced above, is currently the de facto standard for SKC owing to its security, efficiency, and performance characteristics. AES features fixed key sizes of 128, 192, and 256 bits and uses a multiround substitution-permutation network (SPN). AES is known to be resistant to a wide range of cryptanalytic attacks. AES was announced as a Federal Information Processing Standard (FIPS) for symmetric key encryption in the United States in 2001.`\n", - "\n", - "2. [**Data Encryption Standard (DES) and Triple Data Encryption Standard (3DES):**](https://en.wikipedia.org/w/index.php?title=Data_Encryption_Standard&oldid=1154751166) DES was a block cipher originally invented by Horst Feistel and coworkers at IBM® in the 1970s and employed a SPN with a relatively short 56-bit key. DES was adopted as a FIPS for symmetric key encryption in the United States until the late 1990s when it was shown to be breakable using brute force attacks with specialized hardware due to its small key space size. Subsequently, 3DES was introduced as a replacement and applies the DES algorithm three times with different keys, increasing the key length to 168 bits. Nevertheless, 3DES is largely superseded by AES.\n", - "\n", - "3. [**Blowfish and Twofish:**](https://en.wikipedia.org/w/index.php?title=Blowfish_(cipher)&oldid=1120053771) Blowfish and its successor, Twofish, are block ciphers proposed by cryptographer Bruce Schneier in the 1990s. Blowfish and Twofish allow variable key lengths of up to 448 bits and 256 bits respectively, offering some flexibility in the tradeoff between security and performance. Unlike AES, they also feature key-dependent S-boxes. Twofish was one of the finalists in the NIST competition to select the Advanced Encryption Standard but ultimately was not chosen. Both algorithms are currently considered secure.\n", - "\n", - "4. [**Rivest Ciphers (RC2, RC4, RC5, and RC6):**](https://en.wikipedia.org/w/index.php?title=RC_algorithm&oldid=1072978900) The Rivest Cipher (RC) family of symmetric key algorithms was designed by Ron Rivest starting in the 1980s. RC2 was an early 64-bit block cipher while RC4 was stream cipher widely-used in security protocols related to web-traffic due to its simplicity and speed. Neither is currently considered secure. RC5 and RC6 are SPN based block ciphers designed with customizable block size, key size, and number of rounds. Like Twofish above, RC6 was a finalist in the NIST AES competition and is considered secure.\n", - "\n", - "5. [**Salsa20 and ChaCha20:**](https://en.wikipedia.org/w/index.php?title=Salsa20&oldid=1145010249) Salsa20 and ChaCha20 refer to a related family of stream ciphers designed by cryptographer Daniel Bernstein in the 2000s. Salsa20 is a part of the eSTREAM European Union cryptographic validation project's profile-1 portfolio. ChaCha20, a modification of Salsa20, was designed to increase diffusion characteristics and performance. Currently, ChaCha20 is considered secure and offers better performance in the absence of dedicated AES hardware acceleration. Therefore, ChaCha20 finds use in certain settings such as network protocols like QUIC and mobile devices with ARM-based CPUs." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "ac739199", - "metadata": {}, - "source": [ - "## Advantages of symmetric key cryptography\n", - "\n", - "After outlining the properties of symmetric key cryptosystems and some of the principles that underlie their development, we now list some of the main advantages of [SKC](https://books.google.com/books?id=01vfjgEACAAJ) relative to asymmetric key cryptography. The latter will be discussed in subsequent lessons.\n", - "\n", - "1. **Speed and efficiency:** Symmetric key algorithms are more suitable for encrypting large volumes of data or for use in real-time communication scenarios since they are generally faster and less resource intensive than their asymmetric counterparts. SKC algorithms such as AES scale linearly with the size of the plain text and do not involve algebraically intensive mathematical operations. See [Tomoiaga et al.](https://doi.org/10.1109/ICSNC.2010.33) for a detailed review of the performance characteristics of AES.\n", - "\n", - "2. **Scalability:** Owing to their relatively low computational overhead, symmetric key algorithms scale well with the number of users and the amount of data being encrypted.\n", - "\n", - "3. **Simplicity:** Symmetric encryption protocols are often easier to implement and understand compared to asymmetric key approaches, making them attractive for developers and users." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "86419a6c", - "metadata": {}, - "source": [ - "## Challenges and limitations of symmetric key cryptography\n", - "\n", - "Despite the advantages, symmetric key cryptography also has some challenges and limitations:\n", - "\n", - "1. **Key distribution and management:** In SKC, both the sender and the receiver of a message must have access to the same key, which must be kept confidential from unauthorized parties. If the key is somehow intercepted or compromised by a third party then the security of the encrypted data is also lost. The secure distribution and management of the secret key is therefore a major challenge. However, the solution to this challenge lies outside of SKC itself.\n", - "\n", - "2. **Lack of non-repudiation:** [Non-repudiation](https://en.wikipedia.org/w/index.php?title=Non-repudiation&oldid=1148337707) refers to the ability to prove that a specific party has sent a message. In SKC, since the same key is used for both encryption and decryption, it is not possible to determine which party has created a particular ciphertext. In contrast, asymmetric key cryptography provides non-repudiation through the use of digital signature.\n", - "\n", - "To address these challenges, symmetric key cryptography is often used in combination with asymmetric key cryptography. For instance, one often uses asymmetric key encryption to securely transmit a relatively short shared secret key between sender and receiver. This enables the subsequent use of symmetric key encryption to transmit much larger data and messages efficiently." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1abe52d3", - "metadata": {}, - "source": [ - "## Quantum computing and symmetric key encryption: Risks and mitigation\n", - "\n", - "Quantum cryptography offers a promising avenue for risk mitigation in the digital age, with the adoption of quantum-safe products poised to secure our information against the looming threat of quantum computing advancements.\n", - "\n", - "In what follows, we discuss the risks posed by quantum computers to symmetric key encryption schemes introduced in the previous section and outline some potential pathways to mitigating the risks." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "24c45b60", - "metadata": {}, - "source": [ - "### Quantum cryptographic attacks\n", - "\n", - "There are two distinct classes of quantum threats to traditional cryptographic algorithms:\n", - "\n", - "1. **Quantum brute force attacks**: These refer to situations where the attacker uses a quantum computer to execute a specialized quantum algorithm to conduct a brute force search through the key space of a symmetric cipher. The most relevant quantum primitive for enabling this kind of attack is [Grover's algorithm](https://dl.acm.org/doi/10.1145/237814.237866).\n", - "\n", - "2. **Quantum cryptanalytic attacks**: These refer to situations where quantum computers are deployed to execute cryptanalytic attacks that aim to recover either the secret key or plain text in a more efficient manner than a brute force search. The possibility of executing successful quantum attacks depends on many factors having to do with the mathematical structure of the cipher being analyzed as well as potential weaknesses in specific implementations." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "0f9d8da1", - "metadata": {}, - "source": [ - "### Risk mitigation strategies for quantum attacks\n", - "\n", - "Before we discuss risk mitigation strategies for quantum attacks, let's introduce the notion of a cryptographic cipher's [security level](https://en.wikipedia.org/w/index.php?title=Security_level&oldid=1131789113):\n", - "\n", - "**Security level** is a measure of the difficulty of breaking a cipher measured in terms of the number of computational operations that a successful break of the cipher would require.\n", - "\n", - "Typically, the security level is expressed in bits; that is, in general, a cipher offers N-bit security if it requires $\\mathcal{O}(2^{N})$ operations to break it. On classical computers, assuming a symmetric cipher is otherwise cryptographically secure, the security level is roughly synonymous with the key length.\n", - "\n", - "For instance, the security level of AES-128, which features a 128-bit key, is generally considered to be 128 bits because it would require on the order of 2$^{128}$ operations for an attacker employing a classical computer to try out all possible 128-bit keys in the key space.\n", - "\n", - "### Brute force attacks and mitigation\n", - "\n", - "**Quantum brute force attack risk:** A [quantum brute force attack](https://inria.hal.science/hal-01237242) changes the above assessment because Grover's algorithm enables an attacker with a suitable quantum computer to search the key space of a cipher quadratically faster than any classical computer.\n", - "\n", - "For instance, the same brute force attack on AES-128 with Grover's algorithm could potentially be achieved with just 2$^{64}$ operations. Therefore the security level of AES-128 is reduced from 128 bits to 64 bits when faced with a quantum adversary running Grover search. Since computational power has traditionally grown exponentially with time, currently a security level of 64 bits is considered insecure, which means that once sufficiently capable quantum computers are realized, AES-128 will have to be abandoned.\n", - "\n", - "The same kind of calculation applies to other symmetric block or stream ciphers whereby the security level for a given key length is effectively halved by Grover's algorithm.\n", - "\n", - "**Quantum brute force attack risk mitigation:** The above considerations imply that an obvious way to resist quantum brute force attacks is to at least [double the minimum key lengths used for symmetric key encryption](https://inria.hal.science/hal-01237242).\n", - "\n", - "Therefore, to ensure 128-bit security with regard to quantum brute force attacks, one would simply use ciphers such as AES-256 or ChaCha20 that employ 256-bit keys. This is considered secure because even with quantum computers, performing 2$^{128}$ operations to break ciphers is infeasible in the foreseeable future.\n", - "\n", - "While theoretically simple, this proposed solution of doubling key sizes is not without costs, as longer key sizes imply additional computational cost for routine encryption-decryption tasks, along with slower performance, more memory requirement, and additional energy use." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b22fca89", - "metadata": {}, - "source": [ - "### Cryptanalytic attacks and mitigation\n", - "\n", - "**Quantum cryptanalytic attacks risk:** The risk to symmetric key cryptosystems posed by quantum cryptanalytic attacks is currently being [actively researched by cryptographers](https://www.sciencedirect.com/science/article/pii/S0045790622003743). The combination of classical and quantum computing potentially expands the array of tools available to attackers to probe weaknesses in the mathematical structure of ciphers, and a wide range of new quantum cryptanalytic attacks are currently being proposed. These include quantizations of known classical techniques such as [linear and differential cryptanalysis](https://inria.hal.science/hal-01237242) as well as new attack modes with no classical counterparts.\n", - "\n", - "A [recent quantum cryptanalytic study of the Advanced Encryption Standard (AES)](https://doi.org/10.13154/tosc.v2019.i2.55-93) found that the cipher remained resistant to various known quantum cryptanalytic attacks and continued to exhibit an adequate post-quantum security margin. However, some studies have found that various symmetric ciphers considered classically secure are easily compromised by so-called [*quantum chosen plain text attack*](https://doi.org/10.1007/978-3-319-56617-7_3). Therefore, new primitives for symmetric key encryption designed specifically for the post-quantum era have also been proposed.\n", - "\n", - "**Quantum cryptanalytic attacks risk mitigation:** Given that quantum cryptanalysis as a discipline is in its infancy, it may be the case that post-quantum symmetric cryptography will undergo rapid evolution as new quantum cryptanalytic attacks arise and as new ciphers resistant to them are proposed and evaluated. Therefore, the best strategy to mitigate the risk of quantum cryptanalytic attacks in the foreseeable future is [*cryptographic agility*](https://en.wikipedia.org/w/index.php?title=Cryptographic_agility&oldid=1155073704) (or crypto-agility). Crypto-agility refers to the ability of an information system to quickly and easily adopt alternative cryptographic primitives without disruptive changes to the system infrastructure.\n", - "\n", - "Crypto-agility requires the ability to replace obsolete algorithms used for encryption, decryption, digital signatures, or other cryptographic functions with minimal effort and disruption. Crypto-agile systems will be well positioned to manage the transition to post-quantum symmetric key cryptography." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "06ac8475", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "Symmetric key cryptography provides robust and efficient solutions for securing digital information. The simplicity of using the same key for both encryption and decryption enables high performance and scalability, making SKC suitable for a wide range of applications.\n", - "\n", - "The security of SKC relies on algorithmic resistance to cryptographic attacks as well as proper secret key management. Modern symmetric key cryptosystems combine the principles of confusion, diffusion, and randomness, in conjunction with adequate key sizes, to achieve semantic security. Secret key management, while crucial, cannot be achieved with SKC alone.\n", - "\n", - "Understanding the properties and limitations of SKC will enable developers to design, implement, and deploy secure information technology solutions using approaches included longer key sizes as needed, and the use of new algorithms.\n", - "\n", - "The advancement of quantum computing and quantum learning introduces a new dimension to symmetric key cryptography. Quantum computers have the potential to unravel the security provided by classical symmetric key algorithms, prompting the need for quantum-resistant cryptographic approaches to ensure data privacy and protection in the face of evolving technological landscapes." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "14dd451a", + "metadata": {}, + "source": [ + "---\n", + "title: Symmetric key cryptography\n", + "description: In this lesson we will look at symmetric key cryptography which secures much of the data at rest and in transit by virtue of its efficiency.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore ciphertext, cryptosystems, Rijndael, Daemen, Rijmen, monoalphabetic, secretpy, Smid, Foti, lambda, encryptor, encypt, Schneier, ciphertexts, informationally, polyalphabetic, Vigenère, anagramming, Ciphertext, Prichett, Cryptosystems, Caeser, Vigenere, FIPS, Nechvatal, Dworkin, Roback, Technol, Codebook, cryptologists, keystreams, keystream, indcpa, Ehrsam, Tuchman, Bonnetain, Naya, Plasencia, IACR, multiround, Feistel, fips, Twofish, sbox, Rivest, QUIC, quic, Tomoiaga, Stratulat, digitalsignature, STOC, Leurent, qcaa, quantizations, secmargin, Cryptoanalytic, Malviya, Tiwari, Chawla, EUROCRYPT, Coron, semsec, Shamir, Adleman, Nazario */}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ea2ca7f5", + "metadata": {}, + "source": [ + "# Symmetric key cryptography\n", + "\n", + "In this lesson we will look at symmetric key cryptography which secures much of the data at rest and in transit by virtue of its efficiency.\n", + "\n", + "By the end of the lesson we will have covered:\n", + " - What symmetric key cryptography is\n", + " - Python code examples demonstrating the use of symmetric key cryptography\n", + " - A look at applications of symmetric key cryptography\n", + " - Symmetric key cryptography applications\n", + " - The security of symmetric key cryptography\n", + " - Threats to these algorithms from both classical and quantum computers" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59728408", + "metadata": {}, + "source": [ + "## Introduction to symmetric key cryptography\n", + "\n", + "*Symmetric key cryptography (SKC)* is the oldest and most intuitive form of cryptography. With SKC, confidential information is secured through symmetric key encryption (SKE), that is, by using a *single secret key* for both encryption and decryption.\n", + "\n", + "SKC involves:\n", + "- An encryption function that converts a given plain text instance to ciphertext while utilizing a secret key\n", + "- A decryption function that inverts the operation by converting the ciphertext back to plain text using the same secret key\n", + "\n", + "Plain text can mean any kind of unencrypted data such as natural language text or binary code whose information content is in principle directly accessible, while ciphertext refers to encrypted data whose information content is intended to be inaccessible prior to decryption.\n", + "\n", + "An algorithm that describes the encryption and decryption operations using a shared secret key is also called a symmetric cipher.\n", + "\n", + "![Fig 1: Symmetric key encryption of a given plaintext to ciphertext and decryption back to plaintext using the same key.](/learning/images/courses/quantum-safe-cryptography/symmetric-key-cryptography/skc.svg)\n", + "\n", + "*Figure 1. Symmetric key encryption of a given plain text to ciphertext and decryption back to plain text using the same key.*" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3b1ddcc3", + "metadata": {}, + "source": [ + "## Properties of symmetric key cryptosystems\n", + "\n", + "A symmetric key cryptosystem should ensure the following properties to secure messages-both statically stored data and/or communications over some transmission channel:\n", + "\n", + "- **Confidentiality:** Refers to the property that the information content of encrypted messages is protected from unauthorized access.\n", + "- **Integrity:** Refers to the property that any tampering of encrypted messages during storage or transmission can be detected.\n", + "- **Authenticity:** Refers to the property that the receiver of a message can verify the identity of the sender and detect impersonation by an unauthorized party.\n", + "\n", + "Furthermore, these properties should be realized in a setting where the algorithms or ciphers used for encryption and decryption may be public and where access to the information content of encrypted messages is controlled exclusively through access to the secret key.\n", + "\n", + "Implementing a secure symmetric key cryptosystem therefore involves two main tasks:\n", + "\n", + "1. Employing a robust symmetric key encryption algorithm resistant to cryptographic attacks.\n", + "2. Ensuring confidentiality in the distribution and management of secret keys.\n", + "\n", + "In this lesson, we will discuss aspects related to the first task, which forms the primary concern of SKC technology. The second task, however, needs solutions that fall outside of SKC itself and will be introduced later." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "442a9abb", + "metadata": {}, + "source": [ + "## Illustration of symmetric key encryption using python\n", + "\n", + "We show a simple example of the encrypt and decrypt operations using the classical Caesar shift cipher and the modern Advanced Encryption System (AES), which has been the standard for symmetric key encryption since 2001." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "45e8c800", + "metadata": {}, + "source": [ + "First we set up some Python libraries that provide the needed symmetric key encryption ciphers, and then define the plain text we wish to encrypt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06552f57", + "metadata": {}, + "outputs": [], + "source": [ + "# Install the library if needed\n", + "# %pip install secretpy\n", + "\n", + "# import the required crypto functions which will be demonstrated later\n", + "from secretpy import Caesar\n", + "from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n", + "from functools import reduce\n", + "import numpy as np\n", + "\n", + "# Set the plaintext we want to encrypt\n", + "plaintext = \"this is a strict top secret message for intended recipients only\"\n", + "print(f\"\\nGiven plaintext: {plaintext}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0a215aa9", + "metadata": {}, + "source": [ + "We will see how to encrypt and decrypt it using two different symmetric key encryption methods:\n", + "\n", + "1. The classic [Caesar shift cipher](https://en.wikipedia.org/w/index.php?title=Caesar_cipher&oldid=1151194063)\n", + "2. The modern [Advanced Encryption Standard](https://www.nist.gov/publications/development-advanced-encryption-standard) AES-256 protocol" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "eb46068d", + "metadata": {}, + "source": [ + "### Caesar shift cipher:\n", + "\n", + "Caesar shift encryption involves defining\n", + " - An alphabet of possible characters to encode\n", + " - A *shift value* which can be between 0 (unencrypted) and the length of the alphabet. We consider this the *key*.\n", + "\n", + "It is known as a *monoalphabetic substitution cipher* since each letter of the plain text is substituted with another in the ciphertext.\n", + "\n", + "In this example we will use lowercase letters of the alphabet.\n", + "\n", + "Let's start by setting things up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecaf0d3c", + "metadata": {}, + "outputs": [], + "source": [ + "# initialize the required python object for doing Caesar shift encryption\n", + "caesar_cipher = Caesar()\n", + "\n", + "# Define the shift, ie the key\n", + "caesar_key = 5\n", + "print(f\"Caesar shift secret key: {caesar_key}\")\n", + "\n", + "# Define the alphabet\n", + "alphabet = (\n", + " \"a\",\n", + " \"b\",\n", + " \"c\",\n", + " \"d\",\n", + " \"e\",\n", + " \"f\",\n", + " \"g\",\n", + " \"h\",\n", + " \"i\",\n", + " \"j\",\n", + " \"k\",\n", + " \"l\",\n", + " \"m\",\n", + " \"n\",\n", + " \"o\",\n", + " \"p\",\n", + " \"q\",\n", + " \"r\",\n", + " \"s\",\n", + " \"t\",\n", + " \"u\",\n", + " \"v\",\n", + " \"w\",\n", + " \"x\",\n", + " \"y\",\n", + " \"z\",\n", + " \" \",\n", + ")\n", + "print(f\"alphabet: {alphabet}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "bb662f02", + "metadata": {}, + "source": [ + "Encrypt the plain text to get ciphertext for the Caesar cipher." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af9f3eb4", + "metadata": {}, + "outputs": [], + "source": [ + "caeser_ciphertext = caesar_cipher.encrypt(plaintext, caesar_key, alphabet)\n", + "print(f\"Encrypted caeser shift ciphertext: {caeser_ciphertext}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a74671e1", + "metadata": {}, + "source": [ + "Decrypt the ciphertext back to the original plain text using the same key used for encryption." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da614545", + "metadata": {}, + "outputs": [], + "source": [ + "caeser_plaintext = caesar_cipher.decrypt(caeser_ciphertext, caesar_key, alphabet)\n", + "print(f\"Decrypted caeser shift plaintext: {caeser_plaintext}\\n\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "224337cd", + "metadata": {}, + "source": [ + "### Advanced encryption standard (AES) cipher\n", + "\n", + "We now encrypt the plain text using AES, a popular symmetric key encryption algorithm.\n", + "\n", + "We start by creating the key, in this case, a random 16-letter string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7d8af57", + "metadata": {}, + "outputs": [], + "source": [ + "# lambda defines an inline function in this case that takes two values a,b with the resulting\n", + "# expression of a+b\n", + "# reduce uses a two-argument function(above), and applies this to all the entries in the list\n", + "# (random alphabet characters) cumulatively\n", + "aes_key = reduce(lambda a, b: a + b, [np.random.choice(alphabet) for i in range(16)])\n", + "\n", + "print(f\"AES secret key: {aes_key}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "15868918", + "metadata": {}, + "source": [ + "AES supports multiple operating modes and requires we specify which to use.\n", + "\n", + "We choose the *Cipher Block Chaining* (CBC) mode provided by the `modes.CBC` class of the `cryptography` library. The CBC mode of AES uses randomness for additional security. This requires specifying a random *Initialization Vector* (IV), also called a *nonce*. We will use a random string for this as well, just like we did for the key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2e4df17", + "metadata": {}, + "outputs": [], + "source": [ + "aes_initialization_vector = reduce(\n", + " lambda a, b: a + b, [np.random.choice(alphabet) for i in range(16)]\n", + ")\n", + "print(f\"AES initialization vector: {aes_initialization_vector}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b9f32932", + "metadata": {}, + "source": [ + "We can now instantiate an AES cipher on behalf of the sender of the secret message. Note that the initialization vector is passed to the `modes.CBC` class to set up the CBC mode of operation.\n", + "\n", + "We will then encrypt the plain text to send." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa477669", + "metadata": {}, + "outputs": [], + "source": [ + "# The encryptor is setup using the key and CBC. In both cases we need to convert the string (utf-8)\n", + "# into bytes\n", + "sender_aes_cipher = Cipher(\n", + " algorithms.AES(bytes(aes_key, \"utf-8\")),\n", + " modes.CBC(bytes(aes_initialization_vector, \"utf-8\")),\n", + ")\n", + "aes_encryptor = sender_aes_cipher.encryptor()\n", + "\n", + "# update can add text to encypt in chunks, and then finalize is needed to complete the encryption\n", + "# process\n", + "aes_ciphertext = (\n", + " aes_encryptor.update(bytes(plaintext, \"utf-8\")) + aes_encryptor.finalize()\n", + ")\n", + "\n", + "# Note the output is a string of bytes\n", + "print(f\"Encrypted AES ciphertext: {aes_ciphertext}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d4c641b9", + "metadata": {}, + "source": [ + "To decrypt it, let us instantiate an AES cipher on behalf of the receiver. Note that the intended receiver has access to both the secret key and the initialization vector, but the latter is not required to be secret." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10571495", + "metadata": {}, + "outputs": [], + "source": [ + "# Similar setup of AES to what we did for encryption, but this time, for decryption\n", + "receiver_aes_cipher = Cipher(\n", + " algorithms.AES(bytes(aes_key, \"utf-8\")),\n", + " modes.CBC(bytes(aes_initialization_vector, \"utf-8\")),\n", + ")\n", + "aes_decryptor = receiver_aes_cipher.decryptor()\n", + "\n", + "# Do the decryption\n", + "aes_plaintext_bytes = aes_decryptor.update(aes_ciphertext) + aes_decryptor.finalize()\n", + "\n", + "# convert back to a character string (we assume utf-8)\n", + "aes_plaintext = aes_plaintext_bytes.decode(\"utf-8\")\n", + "\n", + "print(f\"Decrypted AES plaintext: {aes_plaintext}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "80ef03c1", + "metadata": {}, + "source": [ + "## Applications of symmetric key cryptography\n", + "\n", + "While classical ciphers such as the Caesar cipher fell out of use a long time ago, modern symmetric cryptosystems such as AES are [deployed in a wide range of applications](https://books.google.com/books?id=01vfjgEACAAJ), including:\n", + "\n", + "1. **Data encryption and decryption:** SKC is widely used to protect sensitive data, whether statically stored on a device or transmitted over a network. Examples include protecting user credentials, encrypting email messages, and securing financial transactions, among others.\n", + "\n", + "2. **Secure communication:** Common communication protocols such as SSL/[TLS](https://en.wikipedia.org/w/index.php?title=Transport_Layer_Security&oldid=1155601531) use a combination of symmetric and asymmetric key encryption to ensure the confidentiality and integrity of data exchanged between two parties. These messages are encrypted and decrypted using symmetric key encryption which uses a shared key. The key used in symmetric key encryption is securely exchanged using asymmetric key encryption which uses a public-private key pair. Symmetric key encryption is much faster and hence can be used for encryption of messages of large size.\n", + "\n", + "3. **Authenticity verification:** In some settings, SKC is employed through techniques like [message authentication codes](https://en.wikipedia.org/w/index.php?title=Message_authentication_code&oldid=1151014755) (MACs) and keyed-hash MACs (HMAC) to verify the authenticity and integrity of messages, ensuring tamper-resistant communication.\n", + "\n", + "4. **File and disk encryption:** Full-disk encryption software and file encryption tools employ SKC to protect sensitive data stored on hard disks or portable storage devices.\n", + "\n", + "5. **Virtual private networks:** [VPN](https://en.wikipedia.org/w/index.php?title=Virtual_private_network&oldid=1154342615) technologies, which aim to provide confidential communication channels free from eavesdropping, can use symmetric or asymmetric key encryption to connect remote users as well as corporate networks.\n", + "\n", + "The diverse array of applications in which SKC is deployed in turn require that symmetric cryptosystems satisfy a certain set of criteria." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "492f9e09", + "metadata": {}, + "source": [ + "## Principles of symmetric key encryption\n", + "In this section, we will discuss some of the basic [principles underlying the security of symmetric key encryption algorithms](https://doi.org/10.1080/07468342.1987.11973000).\n", + "\n", + "**Resistance to brute force attack:** The most basic requirement for the security of an encryption cipher is that the key space size — in other words, the number of possible distinct keys from which someone using the algorithm could have chosen — is very large.\n", + "\n", + "**Resistance to cryptanalytic attack:** The second basic requirement for a cipher, symmetric or otherwise, is that it can generate ciphertexts that are informationally inscrutable. To this end, a necessary but not sufficient condition from an information theory perspective is that [ciphertexts should be characterized by high entropy](https://en.wikipedia.org/w/index.php?title=Entropy_(information_theory)&oldid=1154931420), making them indistinguishable from random text with no discernible patterns or correlations. This way, an attacker can gain no information about the plain text or secret key by trying to analyze the ciphertext using frequency analysis or other statistical techniques.\n", + "\n", + "Resistance to general forms of cryptanalytic attacks sufficient to ensure [semantic security](https://en.wikipedia.org/w/index.php?title=Semantic_security&oldid=1133491450) is formalized via the notion of [*indistinguishability*](https://en.wikipedia.org/w/index.php?title=Ciphertext_indistinguishability&oldid=1130147047). While there are several variants of indistinguishability with distinct requirements, a symmetric cryptosystem is considered to be semantically secure if it satisfies the criterion of [*Indistinguishability under Chosen Plain Text Attack*](https://en.wikipedia.org/w/index.php?title=Ciphertext_indistinguishability&oldid=1130147047) (IND-CPA). This means that an attacker cannot distinguish between the encryptions of two different messages even if allowed to send multiple plain texts of their choosing to the algorithm and view the corresponding ciphertexts.\n", + "\n", + "As we will consider later, IND-CPA typically requires the use of randomness to ensure that each time a given plain text is encrypted with a given secret key, the resulting ciphertext is unpredictably different for each encryption.\n", + "\n", + "**Failure modes of classical ciphers:** Before the advent of modern cryptography in the 1970s, most classical ciphers in practical use failed to satisfy one or both above requirements. For instance, early substitution ciphers such as the monoalphabetic [Caesar shift cipher](https://en.wikipedia.org/w/index.php?title=Caesar_cipher&oldid=1151194063) were characterized by both a small key space size (see Table 1) and low entropy ciphertext, making them insecure against a variety of cryptanalytic attacks such as brute force attacks, frequency analysis, and known-plain-text (KPT) attacks.\n", + "\n", + "Subsequent polyalphabetic substitution ciphers such as the [Vigenère cipher](https://en.wikipedia.org/w/index.php?title=Vigen%C3%A8re_cipher&oldid=1155012941) and the [Enigma machine cipher](https://en.wikipedia.org/w/index.php?title=Enigma_machine&oldid=1154440070) featured effectively large key space sizes, making them resistant to brute force attacks, but they were susceptible to frequency analysis and KPT attacks, respectively. Similarly to substitution ciphers, [classic transposition ciphers](https://en.wikipedia.org/w/index.php?title=Transposition_cipher&oldid=1151499771), which rearrange letters in a message instead of substituting them, are also compromised by a variety of attacks such as anagramming, statistical analysis, brute force, and KPT attacks, among others.\n", + "\n", + "Theoretically, a polyalphabetic substitution cipher known as the [one-time pad](https://en.wikipedia.org/w/index.php?title=One-time_pad&oldid=1155039884) (OTP) is known to be cryptographically secure. An OTP features a secret key that should be (1) composed of randomly chosen letters or bits, (2) at least as long as the original plain text, and (3) used only once. An OTP is impractical for actual applications because if the secret key — which is required to be as long as the plain text and can be used only once — could be shared securely, then so could the original plain text. The OTP instead illustrates the utility of randomness in generating secure ciphertexts.\n", + "\n", + "An attacker trying to implement a brute force search through the key space to find a key that decrypts the message has to perform a number of operations proportional to the key space size.\n", + "\n", + "Therefore, a large key space size provides resistance against brute force attacks by rendering them computationally infeasible. Table 1 lists the key space sizes of some well-known ciphers." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "47b2e4e8", + "metadata": {}, + "source": [ + "### Table 1: Key space sizes of some symmetric ciphers\n", + "\n", + "\n", + "\n", + "| Cipher | Key length | Key space size |\n", + "|--------------|------------------|-------------------------------------------|\n", + "| Caeser shift | 1 | alphabet-size |\n", + "| Vigenere | n | alphabet-size$^\\mathrm{n}$ |\n", + "| One-time-pad | plaintext-length | alphabet-size$^\\mathrm{plaintext-length}$ |\n", + "| DES | 56 | 2$^\\mathrm{56}$ |\n", + "| AES-128 | 128 | 2$^\\mathrm{128}$ |\n", + "| AES-192 | 192 | 2$^\\mathrm{192}$ |\n", + "| AES-256 | 256 | 2$^\\mathrm{256}$ |\n", + "| ChaCha20 | 256 | 2$^\\mathrm{256}$ |" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "66cd329b", + "metadata": {}, + "source": [ + "Modern symmetric key encryption schemes largely overcome the limitations of the classical ciphers. They produce ciphertext resistant to and feature large key space sizes while also being much more practically efficient than an OTP.\n", + "\n", + "**Block ciphers**: One class of modern ciphers — such as [DES](https://en.wikipedia.org/w/index.php?title=Data_Encryption_Standard&oldid=1154751166) and [AES](https://www.nist.gov/publications/development-advanced-encryption-standard) — achieve security by combining the principles of confusion and diffusion [originally introduced by Claude Shannon](https://en.wikipedia.org/wiki/A_Mathematical_Theory_of_Communication#:~:text=%22A%20Mathematical%20Theory%20of%20Communication,the%20generality%20of%20this%20work). We discuss these notions in a setting where encryption schemes work with binary representations of messages:\n", + "\n", + "- **Confusion**: Confusion is the characteristic whereby each bit in the ciphertext depends on multiple bits of the secret key. It ensures that a small change in the secret key modifies almost all the bits of the ciphertext, obscuring the relationship between the ciphertext and the secret key.\n", + "\n", + "- **Diffusion**: Diffusion is the characteristic whereby flipping a single bit in the plain text should modify roughly half the bits in the ciphertext and vice versa. Diffusion hides statistical relationships between the plain text and ciphertext. Ciphers with adequate diffusion satisfy the so-called *avalanche criterion* of cryptography.\n", + "\n", + "Block ciphers implement confusion and diffusion using cryptographic structures known as [substitution-permutation networks](https://en.wikipedia.org/w/index.php?title=Substitution%E2%80%93permutation_network&oldid=1098155951) (SPNs) operating on discrete blocks of data. An SPN accepts a block of plain text and the secret key as inputs and performs a specified number of [*rounds*](https://en.wikipedia.org/w/index.php?title=Round_(cryptography)&oldid=1149554107) of transformations to yield a ciphertext block. Each round is composed of alternating mathematical structures known as substitution boxes [(S-boxes)](https://en.wikipedia.org/w/index.php?title=S-box&oldid=1154458964) and permutation boxes [(P-boxes)](https://en.wikipedia.org/w/index.php?title=Permutation_box&oldid=1151700196) or equivalent operations.\n", + "\n", + "These respectively implement complex nonlinear and linear transformations on the input blocks, leading to [*avalanche effects*](https://en.wikipedia.org/w/index.php?title=Avalanche_effect&oldid=1148010716) in the ciphertext.\n", + "\n", + "SPNs are designed in such a way that increasing the number of rounds typically increases the security of the cipher. This leads to the notion of [*security margin*](https://pmc.ncbi.nlm.nih.gov/articles/PMC4878865/)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a159587c", + "metadata": {}, + "source": [ + "**Security margin:** The security margin of a given SPN-based cryptographic cipher is the difference between the number of rounds in the complete implementation of the cipher and the maximum number of rounds that are known to be breakable using the best-known real-world attack.\n", + "\n", + "For instance, currently the best-known faster-than-brute-force attacks against AES-256 can break up to 9 rounds out of the total 14 rounds in the full cipher when used in the standard mode known as *Electronic Codebook* (ECB) mode. Therefore, [currently the security margin of AES-256 is 5 rounds](https://doi.org/10.13154/tosc.v2019.i2.55-93).\n", + "\n", + "**Stream ciphers:** As an alternative to block ciphers, modern cryptologists have also designed practically secure [*stream ciphers*](https://en.wikipedia.org/w/index.php?title=Stream_cipher&oldid=1143750236) such as [Chacha20](https://en.wikipedia.org/w/index.php?title=Salsa20&oldid=1145010249). These ciphers utilize randomness as a fundamental part of their design and operate on pseudorandom *keystreams* of bits instead of discrete blocks of data.\n", + "\n", + "Accordingly, stream ciphers combine a secret key and an [initialization vector](https://en.wikipedia.org/w/index.php?title=Initialization_vector&oldid=1136156102) (IV) to seed a [pseudorandom random number generator](https://en.wikipedia.org/w/index.php?title=Pseudorandom_number_generator&oldid=1153255020) (PRNG) to produce a keystream of random bits which are then combined with the given plain text to yield the ciphertext. In this sense, stream ciphers are similar to a one-time pad (OTP) but feature shorter secret key lengths and reusable keys, which makes them more practical. However, for the same reason, they do not guarantee perfect secrecy, unlike an OTP.\n", + "\n", + "**Semantic security**: We conclude this subsection by returning to the notion of semantic security or IND-CPA level security introduced above. The basic operations implemented by [block ciphers](https://en.wikipedia.org/w/index.php?title=Block_cipher_mode_of_operation&oldid=1154901199#cite_note-23) such as S-box and P-box are deterministic. This means that in standard operating modes such as ECB, a given plain-text key pair always yields the same ciphertext, a state of affairs that is susceptible to chosen-plain-text attacks.\n", + "\n", + "To achieve IND-CPA level security, block ciphers need to operate in a mode that utilizes randomness introduced via a pseudorandom initialization vector (IV) with the additional requirement that no two encryption operations use the same key-IV pair. AES supports several modes of operation, such as [cipher block chaining](https://patents.google.com/patent/US4074066A/en) (CBC), which are IND-CPA secure. A similar requirement also holds for stream ciphers whereby the same key-IV pair should not be used to seed the PRNG more than once if IND-CPA is desired." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "004fa4ed", + "metadata": {}, + "source": [ + "## Popular symmetric key algorithms\n", + "\n", + "Having introduced some basic principles of SKC, we now list a few popular symmetric key algorithms to illustrate a variety of the approaches pursued in modern cryptosystems. Modern block ciphers and stream ciphers are both employed in different contexts as illustrated below.\n", + "\n", + "1. [**Advanced Encryption Standard:**](https://www.nist.gov/publications/development-advanced-encryption-standard) AES, already introduced above, is currently the de facto standard for SKC owing to its security, efficiency, and performance characteristics. AES features fixed key sizes of 128, 192, and 256 bits and uses a multiround substitution-permutation network (SPN). AES is known to be resistant to a wide range of cryptanalytic attacks. AES was announced as a Federal Information Processing Standard (FIPS) for symmetric key encryption in the United States in 2001.`\n", + "\n", + "2. [**Data Encryption Standard (DES) and Triple Data Encryption Standard (3DES):**](https://en.wikipedia.org/w/index.php?title=Data_Encryption_Standard&oldid=1154751166) DES was a block cipher originally invented by Horst Feistel and coworkers at IBM® in the 1970s and employed a SPN with a relatively short 56-bit key. DES was adopted as a FIPS for symmetric key encryption in the United States until the late 1990s when it was shown to be breakable using brute force attacks with specialized hardware due to its small key space size. Subsequently, 3DES was introduced as a replacement and applies the DES algorithm three times with different keys, increasing the key length to 168 bits. Nevertheless, 3DES is largely superseded by AES.\n", + "\n", + "3. [**Blowfish and Twofish:**](https://en.wikipedia.org/w/index.php?title=Blowfish_(cipher)&oldid=1120053771) Blowfish and its successor, Twofish, are block ciphers proposed by cryptographer Bruce Schneier in the 1990s. Blowfish and Twofish allow variable key lengths of up to 448 bits and 256 bits respectively, offering some flexibility in the tradeoff between security and performance. Unlike AES, they also feature key-dependent S-boxes. Twofish was one of the finalists in the NIST competition to select the Advanced Encryption Standard but ultimately was not chosen. Both algorithms are currently considered secure.\n", + "\n", + "4. [**Rivest Ciphers (RC2, RC4, RC5, and RC6):**](https://en.wikipedia.org/w/index.php?title=RC_algorithm&oldid=1072978900) The Rivest Cipher (RC) family of symmetric key algorithms was designed by Ron Rivest starting in the 1980s. RC2 was an early 64-bit block cipher while RC4 was stream cipher widely-used in security protocols related to web-traffic due to its simplicity and speed. Neither is currently considered secure. RC5 and RC6 are SPN based block ciphers designed with customizable block size, key size, and number of rounds. Like Twofish above, RC6 was a finalist in the NIST AES competition and is considered secure.\n", + "\n", + "5. [**Salsa20 and ChaCha20:**](https://en.wikipedia.org/w/index.php?title=Salsa20&oldid=1145010249) Salsa20 and ChaCha20 refer to a related family of stream ciphers designed by cryptographer Daniel Bernstein in the 2000s. Salsa20 is a part of the eSTREAM European Union cryptographic validation project's profile-1 portfolio. ChaCha20, a modification of Salsa20, was designed to increase diffusion characteristics and performance. Currently, ChaCha20 is considered secure and offers better performance in the absence of dedicated AES hardware acceleration. Therefore, ChaCha20 finds use in certain settings such as network protocols like QUIC and mobile devices with ARM-based CPUs." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ac739199", + "metadata": {}, + "source": [ + "## Advantages of symmetric key cryptography\n", + "\n", + "After outlining the properties of symmetric key cryptosystems and some of the principles that underlie their development, we now list some of the main advantages of [SKC](https://books.google.com/books?id=01vfjgEACAAJ) relative to asymmetric key cryptography. The latter will be discussed in subsequent lessons.\n", + "\n", + "1. **Speed and efficiency:** Symmetric key algorithms are more suitable for encrypting large volumes of data or for use in real-time communication scenarios since they are generally faster and less resource intensive than their asymmetric counterparts. SKC algorithms such as AES scale linearly with the size of the plain text and do not involve algebraically intensive mathematical operations. See [Tomoiaga et al.](https://doi.org/10.1109/ICSNC.2010.33) for a detailed review of the performance characteristics of AES.\n", + "\n", + "2. **Scalability:** Owing to their relatively low computational overhead, symmetric key algorithms scale well with the number of users and the amount of data being encrypted.\n", + "\n", + "3. **Simplicity:** Symmetric encryption protocols are often easier to implement and understand compared to asymmetric key approaches, making them attractive for developers and users." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "86419a6c", + "metadata": {}, + "source": [ + "## Challenges and limitations of symmetric key cryptography\n", + "\n", + "Despite the advantages, symmetric key cryptography also has some challenges and limitations:\n", + "\n", + "1. **Key distribution and management:** In SKC, both the sender and the receiver of a message must have access to the same key, which must be kept confidential from unauthorized parties. If the key is somehow intercepted or compromised by a third party then the security of the encrypted data is also lost. The secure distribution and management of the secret key is therefore a major challenge. However, the solution to this challenge lies outside of SKC itself.\n", + "\n", + "2. **Lack of non-repudiation:** [Non-repudiation](https://en.wikipedia.org/w/index.php?title=Non-repudiation&oldid=1148337707) refers to the ability to prove that a specific party has sent a message. In SKC, since the same key is used for both encryption and decryption, it is not possible to determine which party has created a particular ciphertext. In contrast, asymmetric key cryptography provides non-repudiation through the use of digital signature.\n", + "\n", + "To address these challenges, symmetric key cryptography is often used in combination with asymmetric key cryptography. For instance, one often uses asymmetric key encryption to securely transmit a relatively short shared secret key between sender and receiver. This enables the subsequent use of symmetric key encryption to transmit much larger data and messages efficiently." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1abe52d3", + "metadata": {}, + "source": [ + "## Quantum computing and symmetric key encryption: Risks and mitigation\n", + "\n", + "Quantum cryptography offers a promising avenue for risk mitigation in the digital age, with the adoption of quantum-safe products poised to secure our information against the looming threat of quantum computing advancements.\n", + "\n", + "In what follows, we discuss the risks posed by quantum computers to symmetric key encryption schemes introduced in the previous section and outline some potential pathways to mitigating the risks." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "24c45b60", + "metadata": {}, + "source": [ + "### Quantum cryptographic attacks\n", + "\n", + "There are two distinct classes of quantum threats to traditional cryptographic algorithms:\n", + "\n", + "1. **Quantum brute force attacks**: These refer to situations where the attacker uses a quantum computer to execute a specialized quantum algorithm to conduct a brute force search through the key space of a symmetric cipher. The most relevant quantum primitive for enabling this kind of attack is [Grover's algorithm](https://dl.acm.org/doi/10.1145/237814.237866).\n", + "\n", + "2. **Quantum cryptanalytic attacks**: These refer to situations where quantum computers are deployed to execute cryptanalytic attacks that aim to recover either the secret key or plain text in a more efficient manner than a brute force search. The possibility of executing successful quantum attacks depends on many factors having to do with the mathematical structure of the cipher being analyzed as well as potential weaknesses in specific implementations." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0f9d8da1", + "metadata": {}, + "source": [ + "### Risk mitigation strategies for quantum attacks\n", + "\n", + "Before we discuss risk mitigation strategies for quantum attacks, let's introduce the notion of a cryptographic cipher's [security level](https://en.wikipedia.org/w/index.php?title=Security_level&oldid=1131789113):\n", + "\n", + "**Security level** is a measure of the difficulty of breaking a cipher measured in terms of the number of computational operations that a successful break of the cipher would require.\n", + "\n", + "Typically, the security level is expressed in bits; that is, in general, a cipher offers N-bit security if it requires $\\mathcal{O}(2^{N})$ operations to break it. On classical computers, assuming a symmetric cipher is otherwise cryptographically secure, the security level is roughly synonymous with the key length.\n", + "\n", + "For instance, the security level of AES-128, which features a 128-bit key, is generally considered to be 128 bits because it would require on the order of 2$^{128}$ operations for an attacker employing a classical computer to try out all possible 128-bit keys in the key space.\n", + "\n", + "### Brute force attacks and mitigation\n", + "\n", + "**Quantum brute force attack risk:** A [quantum brute force attack](https://inria.hal.science/hal-01237242) changes the above assessment because Grover's algorithm enables an attacker with a suitable quantum computer to search the key space of a cipher quadratically faster than any classical computer.\n", + "\n", + "For instance, the same brute force attack on AES-128 with Grover's algorithm could potentially be achieved with just 2$^{64}$ operations. Therefore the security level of AES-128 is reduced from 128 bits to 64 bits when faced with a quantum adversary running Grover search. Since computational power has traditionally grown exponentially with time, currently a security level of 64 bits is considered insecure, which means that once sufficiently capable quantum computers are realized, AES-128 will have to be abandoned.\n", + "\n", + "The same kind of calculation applies to other symmetric block or stream ciphers whereby the security level for a given key length is effectively halved by Grover's algorithm.\n", + "\n", + "**Quantum brute force attack risk mitigation:** The above considerations imply that an obvious way to resist quantum brute force attacks is to at least [double the minimum key lengths used for symmetric key encryption](https://inria.hal.science/hal-01237242).\n", + "\n", + "Therefore, to ensure 128-bit security with regard to quantum brute force attacks, one would simply use ciphers such as AES-256 or ChaCha20 that employ 256-bit keys. This is considered secure because even with quantum computers, performing 2$^{128}$ operations to break ciphers is infeasible in the foreseeable future.\n", + "\n", + "While theoretically simple, this proposed solution of doubling key sizes is not without costs, as longer key sizes imply additional computational cost for routine encryption-decryption tasks, along with slower performance, more memory requirement, and additional energy use." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b22fca89", + "metadata": {}, + "source": [ + "### Cryptanalytic attacks and mitigation\n", + "\n", + "**Quantum cryptanalytic attacks risk:** The risk to symmetric key cryptosystems posed by quantum cryptanalytic attacks is currently being [actively researched by cryptographers](https://www.sciencedirect.com/science/article/pii/S0045790622003743). The combination of classical and quantum computing potentially expands the array of tools available to attackers to probe weaknesses in the mathematical structure of ciphers, and a wide range of new quantum cryptanalytic attacks are currently being proposed. These include quantizations of known classical techniques such as [linear and differential cryptanalysis](https://inria.hal.science/hal-01237242) as well as new attack modes with no classical counterparts.\n", + "\n", + "A [recent quantum cryptanalytic study of the Advanced Encryption Standard (AES)](https://doi.org/10.13154/tosc.v2019.i2.55-93) found that the cipher remained resistant to various known quantum cryptanalytic attacks and continued to exhibit an adequate post-quantum security margin. However, some studies have found that various symmetric ciphers considered classically secure are easily compromised by so-called [*quantum chosen plain text attack*](https://doi.org/10.1007/978-3-319-56617-7_3). Therefore, new primitives for symmetric key encryption designed specifically for the post-quantum era have also been proposed.\n", + "\n", + "**Quantum cryptanalytic attacks risk mitigation:** Given that quantum cryptanalysis as a discipline is in its infancy, it may be the case that post-quantum symmetric cryptography will undergo rapid evolution as new quantum cryptanalytic attacks arise and as new ciphers resistant to them are proposed and evaluated. Therefore, the best strategy to mitigate the risk of quantum cryptanalytic attacks in the foreseeable future is [*cryptographic agility*](https://en.wikipedia.org/w/index.php?title=Cryptographic_agility&oldid=1155073704) (or crypto-agility). Crypto-agility refers to the ability of an information system to quickly and easily adopt alternative cryptographic primitives without disruptive changes to the system infrastructure.\n", + "\n", + "Crypto-agility requires the ability to replace obsolete algorithms used for encryption, decryption, digital signatures, or other cryptographic functions with minimal effort and disruption. Crypto-agile systems will be well positioned to manage the transition to post-quantum symmetric key cryptography." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "06ac8475", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "Symmetric key cryptography provides robust and efficient solutions for securing digital information. The simplicity of using the same key for both encryption and decryption enables high performance and scalability, making SKC suitable for a wide range of applications.\n", + "\n", + "The security of SKC relies on algorithmic resistance to cryptographic attacks as well as proper secret key management. Modern symmetric key cryptosystems combine the principles of confusion, diffusion, and randomness, in conjunction with adequate key sizes, to achieve semantic security. Secret key management, while crucial, cannot be achieved with SKC alone.\n", + "\n", + "Understanding the properties and limitations of SKC will enable developers to design, implement, and deploy secure information technology solutions using approaches included longer key sizes as needed, and the use of new algorithms.\n", + "\n", + "The advancement of quantum computing and quantum learning introduces a new dimension to symmetric key cryptography. Quantum computers have the potential to unravel the security provided by classical symmetric key algorithms, prompting the need for quantum-resistant cryptographic approaches to ensure data privacy and protection in the face of evolving technological landscapes." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/bits-gates-and-circuits.ipynb b/learning/courses/utility-scale-quantum-computing/bits-gates-and-circuits.ipynb index cab409d42a7..da2190c8140 100644 --- a/learning/courses/utility-scale-quantum-computing/bits-gates-and-circuits.ipynb +++ b/learning/courses/utility-scale-quantum-computing/bits-gates-and-circuits.ipynb @@ -1,1999 +1,2000 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "4233533a-fcd2-4bd7-bcb8-8e0be95326e6", - "metadata": {}, - "source": [ - "---\n", - "title: Bits, gates, and circuits\n", - "description: you will learn quantum computation with circuit model using quantum bits (qubits) and gates.\n", - "---\n", - "\n", - "\n", - "# Quantum bits, gates, and circuits\n", - "\n", - "\n", - "\n", - "Kifumi Numata (19 Apr 2024)\n", - "\n", - "Click [here](https://ibm.ent.box.com/public/static/vee9e1kkxxiih5g8yqdhoo6t0o3dw7xf.zip) to download the pdf of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 5 seconds.*\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "ec475aee-075f-4411-8dbe-8a427865714b", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "Bits, gates, and circuits are the basic building blocks of quantum computing. You will learn quantum computation with the circuit model using quantum bits and gates, and also review the superposition, measurement, and entanglement.\n", - "\n", - "In this lesson you will learn:\n", - "- Single-qubit gates\n", - "- Bloch sphere\n", - "- Superposition\n", - "- Measurement\n", - "- Two-qubit gates and entanglement state\n", - "\n", - "At the end of this lecture, you will learn about circuit depth, which is essential for utility-scale quantum computing." - ] - }, - { - "cell_type": "markdown", - "id": "ed761072-54aa-47af-a2e1-a5c40a0471d1", - "metadata": {}, - "source": [ - "## 2. Computation as a diagram\n", - "\n", - "When using qubits or bits, we need to manipulate them in order to turn the inputs we have into the outputs we need. For the simplest programs with very few bits, it is useful to represent this process in a diagram known as a *circuit diagram*.\n", - "\n", - "The bottom-left figure is an example of a classical circuit, and the bottom-right figure is an example of a quantum circuit. In both cases, the inputs are on the left and the outputs are on the right, while the operations are represented by symbols. The symbols used for the operations are called “gates”, mostly for historical reasons.\n", - "\n", - "![\"classical logic and quantum circuit\"](/learning/images/courses/utility-scale-quantum-computing/bits-gates-and-circuits/classical-vs-quantum.avif)" - ] - }, - { - "cell_type": "markdown", - "id": "87f8d07a-1544-4acb-82fb-3a43f1a99f8c", - "metadata": {}, - "source": [ - "## 3. Single-qubit quantum gate\n", - "\n", - "### 3.1 Quantum state and Bloch sphere\n", - "\n", - "A qubit's state is represented as a superposition of $|0\\rangle$ and $|1\\rangle$. An arbitrary quantum state is represented as\n", - "\n", - "$$\n", - "|\\psi\\rangle =\\alpha|0\\rangle+ \\beta|1\\rangle\n", - "$$\n", - "\n", - "where $\\alpha$ and $\\beta$ are complex numbers such that $|\\alpha|^2+|\\beta|^2=1$.\n", - "\n", - "$|0\\rangle$ and $|1\\rangle$ are vectors in the two-dimensional complex vector space:\n", - "\n", - "$$\n", - "|0\\rangle = \\begin{pmatrix}\n", - "1 \\\\0\n", - "\\end{pmatrix},\n", - "|1\\rangle = \\begin{pmatrix}\n", - "0\\\\1\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "Therefore, an arbitrary quantum state is also represented as\n", - "\n", - "$$\n", - "|\\psi\\rangle = \\alpha\\begin{pmatrix}\n", - "1 \\\\ 0\n", - "\\end{pmatrix} + \\beta\\begin{pmatrix}0\\\\\n", - "1\n", - "\\end{pmatrix} = \\begin{pmatrix}\n", - "\\alpha \\\\ \\beta\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "From this, we can see that the state of a quantum bit is a unit vector in a two-dimensional complex inner product space with an orthonormal basis of $|0\\rangle$ and $|1\\rangle$. It is normalized to 1.\n", - "\n", - "$$\n", - "\\langle\\psi|\\psi\\rangle = \\begin{pmatrix}\n", - "\\alpha^* & \\beta^*\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "\\alpha \\\\\n", - "\\beta\n", - "\\end{pmatrix} = 1\n", - "$$\n", - "\n", - "$ |\\psi\\rangle =\\begin{pmatrix}\n", - "\\alpha \\\\ \\beta\n", - "\\end{pmatrix}$ is also called the statevector.\n", - "\n", - "A single-qubit quantum state is also represented as\n", - "\n", - "$$\n", - "|\\psi\\rangle\n", - "=\\cos\\frac{\\theta}{2}|0\\rangle+e^{i\\varphi}\\sin\\frac{\\theta}{2}|1\\rangle\n", - "=\\left( \\begin{pmatrix} \\cos\\frac{\\theta}{2}\\\\ e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", - "\\end{pmatrix}\\right)\n", - "$$\n", - "\n", - "where $\\theta$ and $\\varphi$ are the angles of the Bloch sphere in the following figure.\n", - "\n", - "![Bloch sphere](/learning/images/courses/utility-scale-quantum-computing/bits-gates-and-circuits/bloch.avif)" - ] - }, - { - "cell_type": "markdown", - "id": "86a3a654-3fb7-492c-bf4b-ecf23b4b71a6", - "metadata": {}, - "source": [ - "In the next few code cells, we will build up basic calculations from constituent pieces in Qiskit. We'll construct an empty circuit and then add quantum operations, discussing the gates and visualizing their effects as we go.\n", - "You can run the cell by \"Shift\" + \"Enter\". Import the libraries first." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "113bc3fe-3295-4524-9bf8-c1f36e51554b", - "metadata": {}, - "outputs": [], - "source": [ - "# Import the qiskit library\n", - "from qiskit import QuantumCircuit\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit.quantum_info import Statevector\n", - "from qiskit.visualization import plot_bloch_multivector\n", - "from qiskit_ibm_runtime import Sampler\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.visualization import plot_histogram" - ] - }, - { - "cell_type": "markdown", - "id": "dbb2272c-996f-46a9-b5b0-f62d60c4deb3", - "metadata": {}, - "source": [ - "#### Prepare the quantum circuit\n", - "We will create and draw a single-qubit circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "fdef9137-9e39-4b05-afa9-9091766053f6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create the single-qubit quantum circuit\n", - "qc = QuantumCircuit(1)\n", - "\n", - "# Draw the circuit\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "50ffcbab-e427-4582-9864-92e1491544a1", - "metadata": {}, - "source": [ - "#### X gate\n", - "\n", - "The X gate is a $\\pi$ rotation around the $x$ axis of the Bloch sphere.\n", - "Applying the X gate to $|0\\rangle$ results in $|1\\rangle$, and applying the X gate to $|1\\rangle$ results in $|0\\rangle$, so it is an operation similar to the classical NOT gate, and is also known as bit flip. The matrix representation of the X gate is below.\n", - "\n", - "$$\n", - "X = \\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 0 \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b3468430-0cbc-44ab-bd21-901436662a4b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1) # Prepare the single-qubit quantum circuit\n", - "\n", - "# Apply a X gate to qubit 0\n", - "qc.x(0)\n", - "\n", - "# Draw the circuit\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "c944e04b-09b3-429b-82d8-12be6b94d86d", - "metadata": {}, - "source": [ - "In IBM Quantum®, the initial state is set to $|0\\rangle$, so the quantum circuit above in matrix representation is\n", - "\n", - "$$\n", - "X|0\\rangle= \\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 0\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "1 \\\\ 0\n", - "\\end{pmatrix}\n", - " =\\begin{pmatrix}\n", - "0 \\\\ 1\n", - "\\end{pmatrix} = |1\\rangle\n", - "$$\n", - "\n", - "Next, let's run this circuit using a statevector simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "efb727e8-f51e-4903-aa0f-e4c099316815", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.+0.j, 1.+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "b4262b2d-10d8-42a8-b632-4d4872ccc3c3", - "metadata": {}, - "source": [ - "Vertical vector is displayed as row vector, with complex numbers (the imaginary part is indexed by $j$ ).\n", - "\n", - "#### H gate\n", - "The Hadamard gate is a $\\pi$ rotation around an axis halfway between the $x$ and $z$ axes on the Bloch sphere. Applying the H gate to $|0\\rangle$ creates a superposition state such as $\\frac{|0\\rangle + |1\\rangle}{\\sqrt{2}}$. The matrix representation of the H gate is below.\n", - "\n", - "$$\n", - "H = \\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", - "1 & 1 \\\\\n", - "1 & -1 \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "574c35f0-26a9-423a-8d65-7140ba7398e3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1) # Create the single-qubit quantum circuit\n", - "\n", - "# Apply an Hadamard gate to qubit 0\n", - "qc.h(0)\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "7adf0e93-6db3-4aa8-800f-c91c8cc46001", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.70710678+0.j, 0.70710678+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "2c9b271a-7dcd-426f-b79d-378575f41dfb", - "metadata": {}, - "source": [ - "This is\n", - "$$\n", - "H|0\\rangle= \\frac{1}{\\sqrt{2}} \\begin{pmatrix}\n", - "1 & 1 \\\\\n", - "1 & -1\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "1 \\\\0\n", - "\\end{pmatrix}\n", - " =\\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", - "1 \\\\\n", - "1\n", - "\\end{pmatrix}\n", - "=\\begin{pmatrix}\n", - "0.707 \\\\\n", - "0.707\n", - "\\end{pmatrix}\n", - "=\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)\n", - "$$\n", - "\n", - "This superposition state is so common and important, that it is given its own symbol:\n", - "\n", - "$$\n", - "|+\\rangle \\equiv \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle).\n", - "$$\n", - "\n", - "By applying the $H$ gate on the $|0\\rangle$, we created a superposition of $|0\\rangle$ and $|1\\rangle$ where measurement in the computational basis (along z in the Bloch sphere picture) would give you each state with equal probabilities." - ] - }, - { - "cell_type": "markdown", - "id": "07fdfe64-f6ae-4995-8857-e6559ae4a3b6", - "metadata": {}, - "source": [ - "#### $|-\\rangle$ state\n", - "\n", - "You might have guessed that there is a corresponding $|-\\rangle$ state:\n", - "$$\n", - "|-\\rangle \\equiv \\frac{|0\\rangle -|1\\rangle}{\\sqrt{2}}.\n", - "$$\n", - "To create this state, first apply an X gate to make $|1\\rangle$, then apply an H gate." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "b8e96fc4-6708-415e-b8fc-ac69eaeae750", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1) # Create the single-qubit quantum circuit\n", - "\n", - "# Apply a X gate to qubit 0\n", - "qc.x(0)\n", - "\n", - "# Apply an Hadamard gate to qubit 0\n", - "qc.h(0)\n", - "\n", - "# draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f42b82aa-7a15-4f81-8530-b997d4c6e0de", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([ 0.70710678+0.j, -0.70710678+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "2d27e9ca-67be-4c69-8930-b70bbdf03b8d", - "metadata": {}, - "source": [ - "This is\n", - "\n", - "$$\n", - "H|1\\rangle= \\frac{1}{\\sqrt{2}} \\begin{pmatrix}\n", - "1 & 1 \\\\\\\n", - "1 & -1\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "0 \\\\\\\n", - "1\n", - "\\end{pmatrix}\n", - " =\\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", - "1 \\\\\\\n", - "-1\n", - "\\end{pmatrix}\n", - "=\\begin{pmatrix}\n", - "0.707 \\\\\\\n", - "-0.707\n", - "\\end{pmatrix}\n", - "=\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle) = |-\\rangle\n", - "$$\n", - "\n", - "\n", - "\n", - "Applying the $H$ gate on $|1\\rangle$ results in an equal superposition of $|0\\rangle$ and $|1\\rangle$, but the sign of $|1\\rangle$ is negative." - ] - }, - { - "cell_type": "markdown", - "id": "bbc2aac6-cf44-45dd-a635-7b75fa197262", - "metadata": {}, - "source": [ - "### 3.2 Single-qubit quantum state and unitary evolution\n", - "\n", - "The actions of all the gates we have seen so far have been *unitary*, which means they can be represented by a unitary operator. In other words, the output state can be obtained by acting on the initial state with a unitary matrix:\n", - "\n", - "$$\n", - "|\\psi^{'}\\rangle = U|\\psi\\rangle\n", - "$$\n", - "\n", - "A unitary matrix is a matrix satisfying\n", - "\n", - "$$\n", - "U^{\\dagger}U =U U^{\\dagger} = I.\n", - "$$\n", - "\n", - "In terms of quantum computer operation, we would say that applying a quantum gate to the qubit evolves the quantum state. Common single-qubit gates include the following.\n", - "\n", - "Pauli gates:\n", - "\n", - "$$\n", - "X = \\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 0 \\\\\n", - "\\end{pmatrix}\n", - "= |0\\rangle \\langle 1|+|1\\rangle \\langle 0|\n", - "$$\n", - "\n", - "$$\n", - "Y = \\begin{pmatrix}\n", - "0 & -i \\\\\n", - "i & 0 \\\\\n", - "\\end{pmatrix}\n", - "= -i|0\\rangle \\langle 1|+i|1\\rangle \\langle 0|\n", - "$$\n", - "\n", - "$$\n", - "Z = \\begin{pmatrix}\n", - "1 & 0 \\\\\n", - "0 & -1 \\\\\n", - "\\end{pmatrix}\n", - "= |0\\rangle \\langle 0|-|1\\rangle \\langle 1|\n", - "$$\n", - "\n", - "where the outer product was calculated as follows:\n", - "$$\n", - "|0\\rangle \\langle 0|=\n", - "\\begin{bmatrix}\n", - "1 \\\\\n", - "0\n", - "\\end{bmatrix}\n", - "\\begin{bmatrix}\n", - "1 & 0\n", - "\\end{bmatrix}\n", - "=\\begin{bmatrix}\n", - "1 & 0 \\\\\n", - "0 & 0 \\\\\n", - "\\end{bmatrix}, \\quad\n", - "|1\\rangle \\langle 0|=\n", - "\\begin{bmatrix}\n", - "0 \\\\\n", - "1\n", - "\\end{bmatrix}\n", - "\\begin{bmatrix}\n", - "1 & 0\n", - "\\end{bmatrix}\n", - "=\\begin{bmatrix}\n", - "0 & 0 \\\\\n", - "1 & 0 \\\\\n", - "\\end{bmatrix}, \\quad\n", - "$$\n", - "\n", - "$$\n", - "|0\\rangle \\langle 1|=\n", - "\\begin{bmatrix}\n", - "1 \\\\\n", - "0\n", - "\\end{bmatrix}\n", - "\\begin{bmatrix}\n", - "0 & 1\n", - "\\end{bmatrix}\n", - "=\\begin{bmatrix}\n", - "0 & 1 \\\\\n", - "0 & 0 \\\\\n", - "\\end{bmatrix}, \\quad\n", - "|1\\rangle \\langle 1|=\n", - "\\begin{bmatrix}\n", - "0 \\\\\n", - "1\n", - "\\end{bmatrix}\n", - "\\begin{bmatrix}\n", - "0 & 1\n", - "\\end{bmatrix}\n", - "=\\begin{bmatrix}\n", - "0 & 0 \\\\\n", - "0 & 1 \\\\\n", - "\\end{bmatrix}, \\quad\n", - "$$\n", - "\n", - "Other typical single-qubit gates:\n", - "$$\n", - "H= \\frac{1}{\\sqrt{2}}\\begin{bmatrix}\n", - "1 & 1 \\\\\n", - "1 & -1 \\\\\n", - "\\end{bmatrix},\\quad\n", - "S = \\begin{bmatrix}\n", - "1 & 0 \\\\\n", - "0 & i \\\\\n", - "\\end{bmatrix}, \\quad\n", - "T = \\begin{bmatrix}\n", - "1 & 0 \\\\\n", - "0 & exp(i\\pi/4) \\\\\n", - "\\end{bmatrix}\n", - "$$\n", - "\n", - "$$\n", - "R_x(\\theta) = e^{-i\\theta X/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}X = \\begin{bmatrix}\n", - "cos\\frac{\\theta}{2} & -i sin \\frac{\\theta}{2} \\\\\n", - "-i sin \\frac{\\theta}{2} & cos\\frac{\\theta}{2} \\\\\n", - "\\end{bmatrix}\n", - "$$\n", - "\n", - "$$\n", - "R_y(\\theta) = e^{-i\\theta Y/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}Y = \\begin{bmatrix}\n", - "cos\\frac{\\theta}{2} & - sin \\frac{\\theta}{2} \\\\\n", - "sin \\frac{\\theta}{2} & cos\\frac{\\theta}{2} \\\\\n", - "\\end{bmatrix}\n", - "$$\n", - "\n", - "$$\n", - "R_z(\\theta) = e^{-i\\theta Z/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}Z = \\begin{bmatrix}\n", - "e^{-i\\theta /2} & 0 \\\\\n", - "0 & e^{i\\theta /2} \\\\\n", - "\\end{bmatrix}\n", - "$$\n", - "\n", - "The meaning and use of these are described in more detail in the [Basics of Quantum Information](/learning/courses/basics-of-quantum-information) course." - ] - }, - { - "cell_type": "markdown", - "id": "39da8188-4ea3-420a-8467-29dd249ca93b", - "metadata": {}, - "source": [ - "### Exercise 1\n", - "\n", - "Use Qiskit to create quantum circuits that prepare the states described below. Then run each circuit using the statevector simulator and display the resulting state on the Bloch sphere. As a bonus, see if you can anticipate what the final state should be based on intuition about the gates and rotations in the Bloch sphere.\n", - "\n", - "(1) $XX|0\\rangle$\n", - "\n", - "(2) $HH|0\\rangle$\n", - "\n", - "(3) $HZH|0\\rangle$\n", - "\n", - "Tips: Z gate can be used by\n", - "\n", - " qc.z(0)\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "148fdaf9-0398-4dd3-84b3-1a71a4f5d34e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "### (1) XX|0> ###\n", - "\n", - "# Create the single-qubit quantum circuit\n", - "qc = QuantumCircuit(1) ##your code goes here##\n", - "\n", - "# Add a X gate to qubit 0\n", - "qc.x(0) ##your code goes here##\n", - "\n", - "# Add a X gate to qubit 0\n", - "qc.x(0) ##your code goes here##\n", - "\n", - "# Draw a circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "4720c4a0-acc7-46cc-b8ba-be2ca87ab976", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([1.+0.j, 0.+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "361c09fe-c206-4355-b827-da7ede0848de", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "### (2) HH|0> ###\n", - "##your code goes here##\n", - "qc = QuantumCircuit(1)\n", - "qc.h(0)\n", - "qc.h(0)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "a4976a54-77e2-42b5-b492-49c8cdaf9c9f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([1.+0.j, 0.+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "cd01ed0c-8f9e-4689-b1ae-f028d588f81a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "### (3) HZH|0> ###\n", - "##your code goes here##\n", - "qc = QuantumCircuit(1)\n", - "qc.h(0)\n", - "qc.z(0)\n", - "qc.h(0)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "4bc91c2a-e097-44db-95bb-acf0d0ea0844", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.+0.j, 1.+0.j],\n", - " dims=(2,))\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)\n", - "\n", - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "59f4ee8b-7243-418c-8094-ae52e750fac1", - "metadata": {}, - "source": [ - "### 3.3 Measurement\n", - "\n", - "Measurement is theoretically a very complicated topic. But in practical terms, making a measurement along $z$ (as all IBM® quantum computers do) simply forces the qubit’s state $\\alpha|0\\rangle+\\beta|1\\rangle \\quad (s.t.|\\alpha|^2+|\\beta|^2=1)$ either to $|0\\rangle$ or to $|1\\rangle,$ and we observe the outcome.\n", - "- $|\\alpha|^2$ is the probability we will get $|0\\rangle$ when we measure.\n", - "- $|\\beta|^2$ is the probability we will get $|1\\rangle$ when we measure.\n", - "\n", - "So, $\\alpha$ and $\\beta$ are called probability amplitudes. (see \"Born rule\")\n", - "\n", - "For example, $\\frac{\\sqrt{2}}{2}|0\\rangle+\\frac{\\sqrt{2}}{2}|1\\rangle$ has an equal probability of becoming $|0\\rangle$ or $|1\\rangle$ upon measurement. $\\frac{\\sqrt{3}}{2}|0\\rangle-\\frac{1}{2}i|1\\rangle$ has a 75% chance of becoming $|0\\rangle$." - ] - }, - { - "cell_type": "markdown", - "id": "fb2c925a-e906-467b-b971-4663eb4751c5", - "metadata": {}, - "source": [ - "#### Qiskit Aer Simulator\n", - "Next, let's measure a circuit that prepares the equal probability superposition above.\n", - "We should add the measurement gates, as the Qiskit Aer simulator simulates an ideal (with no noise) quantum hardware by default. Note: The Aer simulator can also apply a noise model based on real quantum computer. We will return to noise models later." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3956e602-9f1b-4083-b58d-26c91b50b75e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a new circuit with one qubits (first argument) and one classical bits (second argument)\n", - "qc = QuantumCircuit(1, 1)\n", - "qc.h(0)\n", - "qc.measure(0, 0) # Add the measurement gate\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "95abf094-bd82-4a4a-ad12-29bec082d9c8", - "metadata": {}, - "source": [ - "We are now ready to run our circuit on the Aer simulator. In this example, we will apply the default shots=1024, which means we will measure 1024 times. Then we will plot those counts in a histogram." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "7cb7c5ab-7b12-4f77-aaca-1d7e6cff213c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'0': 521, '1': 503}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Run the circuit on a simulator to get the results\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc])\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "ea8c3435-8e76-4f04-9d50-d1eb575158d3", - "metadata": {}, - "source": [ - "We see that 0s and 1s were measured with a probability of almost 50% each. Although noise has not been simulated here, the states are still probabilistic. So while we expect roughly a 50-50 distribution, we will rarely find exactly that. Just as 100 flips of a coin would rarely yield exactly 50 instances of each side." - ] - }, - { - "cell_type": "markdown", - "id": "33d5a0ef-4eca-46d0-9c87-2f42b9a70ebb", - "metadata": {}, - "source": [ - "## 4. Multi-qubit quantum gate and entanglement\n", - "\n", - "### 4.1 Multi-qubit quantum circuit\n", - "\n", - "We can create a two-qubit quantum circuit with following code. We will apply an H gate to each qubit." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "e880f7eb-108c-4387-8ef4-97219fd5691c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create the two qubits quantum circuit\n", - "qc = QuantumCircuit(2)\n", - "\n", - "# Apply an H gate to qubit 0\n", - "qc.h(0)\n", - "\n", - "# Apply an H gate to qubit 1\n", - "qc.h(1)\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "28c4fdb1-122b-4399-a3ee-a24f6b2d36ca", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j],\n", - " dims=(2, 2))\n" - ] - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "fa6412ee", - "metadata": {}, - "source": [ - "#### Note: Qiskit bit ordering\n", - "\n", - "Qiskit uses **Little Endian** notation when ordering qubits and bits, meaning **qubit 0 is the rightmost bit** in the bitstrings. Example: $|01\\rangle$ means q0 is $|1\\rangle$ and q1 is $|0\\rangle$. Be careful because some literature in quantum computing use the Big Endian notation (qubit 0 is the leftmost bit) and a great deal of quantum mechanics literature does, too.\n", - "\n", - "Another thing to notice is that when representing a quantum circuit, $|q_0\\rangle$ is always placed at the top of the circuit." - ] - }, - { - "cell_type": "markdown", - "id": "905f20cf-3836-4766-bca1-05a4bd220d64", - "metadata": {}, - "source": [ - "With this in mind, the quantum state of the above circuit can be written as a tensor product of single-qubit quantum state.\n", - "\n", - "$\n", - "|q1\\rangle \\otimes|q0\\rangle = (a|0\\rangle+b|1\\rangle) \\otimes (c|0\\rangle+d|1\\rangle)\n", - "$\n", - "\n", - "$\n", - "= ac|0\\rangle|0\\rangle+ad|0\\rangle|1\\rangle+bc|1\\rangle|0\\rangle+bd|1\\rangle|1\\rangle\n", - "$\n", - "\n", - "$\n", - "= ac|00\\rangle+ad|01\\rangle+bc|10\\rangle+bd|11\\rangle\n", - "$\n", - "\n", - "( $|ac|^2+ |ad|^2+ |bc|^2+ |bd|^2=1$ )\n", - "\n", - "\n", - "The initial state of Qiskit is $|0\\rangle|0\\rangle=|00\\rangle$, so by applying $H$ to each qubit, it changes to a state of equal superposition.\n", - "\n", - "$H|0\\rangle \\otimes H|0\\rangle=\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle) \\otimes \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle) = \\frac{1}{2}(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle)$\n", - "\n", - "$$\n", - "=\\frac{1}{2}\\left( \\begin{pmatrix} 1 \\\\ 1 \\end{pmatrix} \\otimes \\begin{pmatrix} 1 \\\\ 1 \\end{pmatrix}\\right) = \\frac{1}{2}\\begin{pmatrix} 1 \\\\ 1 \\\\ 1 \\\\ 1 \\end{pmatrix}=\\frac{1}{2}\\left(\\begin{pmatrix} 1 \\\\ 0 \\\\ 0 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 1 \\\\ 0 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 0 \\\\ 1 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 0 \\\\ 0 \\\\ 1 \\end{pmatrix}\\right)\n", - "$$\n", - "\n", - "The measurement rule is also same as a single qubit case, the probability of measuring $|00\\rangle$ is $|ac|^2$." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "ebdf591f-18d0-4fd1-a3a3-b1cdac13bd97", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Draw a Bloch sphere\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "528cbdb3-eb30-42f2-a493-00ed22c09056", - "metadata": {}, - "source": [ - "Next, let's measure this circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "2afdd5ac-e51a-4d94-9307-955cc8fbf821", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a new circuit with two qubits (first argument) and two classical bits (second argument)\n", - "qc = QuantumCircuit(2, 2)\n", - "\n", - "# Apply the gates\n", - "qc.h(0)\n", - "qc.h(1)\n", - "\n", - "# Add the measurement gates\n", - "qc.measure(0, 0) # Measure qubit 0 and save the result in bit 0\n", - "qc.measure(1, 1) # Measure qubit 1 and save the result in bit 1\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "e94b24d5-1c35-46f4-a7f7-fd4849f10473", - "metadata": {}, - "source": [ - "Now, we will use a Aer simulator, again, to experimentally verify that the relative probabilities of all possible output states are roughly equal." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "060512ac-a106-4ecd-94f5-119048a08467", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'10': 262, '01': 246, '00': 265, '11': 251}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Run the circuit on a simulator to get the results\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc])\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "c4875883-3bb3-4537-aa6e-0d46b67c49e3", - "metadata": {}, - "source": [ - "As expected, the states $|00\\rangle$, $|01\\rangle$, $|10\\rangle$, $|11\\rangle$ were measured almost 25% each." - ] - }, - { - "cell_type": "markdown", - "id": "d28f149c-b02a-4c4f-b0c5-2252b5dd01fc", - "metadata": {}, - "source": [ - "### 4.2 Multi-qubit quantum gates\n", - "#### CNOT gate\n", - "\n", - "A CNOT(\"controlled NOT\" or CX) gate is a two-qubit gate, meaning its action involves two qubits at once: the control qubit and the target qubit. A CNOT flips the target qubit only when the control qubit is $|1\\rangle$.\n", - "\n", - "| Input (target,control) | Output (target,control) |\n", - "|:-----------:|:------------:|\n", - "| 00 | 00 |\n", - "| 01 | 11 |\n", - "| 10 | 10 |\n", - "| 11 | 01 |\n", - "\n", - "Let us first simulate the action of this two-qubit gate when q0 and q1 are both $|0\\rangle$, and obtain the output statevector. The Qiskit syntax used is ```qc.cx(control qubit, target qubit)```." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "a5621efd-dd9f-4ea4-bf3a-be3cacb45287", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a circuit with two quantum registers and two classical registers\n", - "qc = QuantumCircuit(2, 2)\n", - "\n", - "# Apply the CNOT (cx) gate to a |00> state.\n", - "qc.cx(0, 1) # Here the control is set to q0 and the target is set to q1.\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "6dcea9ac-c9c3-4211-8d62-6ae9beb9e723", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],\n", - " dims=(2, 2))\n" - ] - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "62d9fb42-6498-4e5a-b625-f5e3be15c36c", - "metadata": {}, - "source": [ - "As expected, applying a CNOT gate on $|00\\rangle$ did not change the state, since the control qubit was in the $|0\\rangle$ state." - ] - }, - { - "cell_type": "markdown", - "id": "af0c98e9-d440-4899-bee5-22591d395f33", - "metadata": {}, - "source": [ - "Let's get back to our CNOT operation. This time we will apply a CNOT gate to $|01\\rangle$ and see what happens." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "5a02de82-44ff-40eb-b64c-2389d6eae3ee", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2, 2)\n", - "\n", - "# q0=1, q1=0\n", - "qc.x(0) # Apply a X gate to initialize q0 to 1\n", - "qc.cx(0, 1) # Set the control bit to q0 and the target bit to q1.\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "f878f9fb-4142-4a6e-a4dd-ef2e2e2d5118", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],\n", - " dims=(2, 2))\n" - ] - } - ], - "source": [ - "# See the statevector\n", - "out_vector = Statevector(qc)\n", - "print(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "0ed70dbf-4161-4edc-8035-681034acb4e0", - "metadata": {}, - "source": [ - "By applying a CNOT gate, the $|01\\rangle$ state has now become $|11\\rangle$.\n", - "\n", - "Let us verify these results by running the circuit on a simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "abcafb77-20bf-4bb7-bf8d-57f33af280c1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Add measurements\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "\n", - "# Draw the circuit\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "88b76f1d-083b-4f28-b415-380738d22599", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'11': 1024}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Run the circuit on a simulator to get the results\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([isa_qc])\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "86b95716-0817-4466-9f8f-3a212e0c08e3", - "metadata": {}, - "source": [ - "The results should show you that $|11\\rangle$ has been measured with 100% probability.\n", - "\n", - "### 4.3 Quantum entanglement and execution on a real quantum device\n", - "\n", - "Let's start by introducing a specific entangled state which is particularly important in quantum computation, then we'll define the term \"entangled\":\n", - "\n", - "$$\n", - "\\frac{1}{\\sqrt{2}}|00\\rangle + \\frac{1}{\\sqrt{2}}|11\\rangle\n", - "$$\n", - "and this state is called a **Bell state**.\n", - "\n", - "An entangled state is a state $|\\psi_{AB}\\rangle$ consisting of quantum states $|\\psi_A\\rangle$ and $|\\psi_B\\rangle$ that cannot be represented by a tensor product of individual quantum states.\n", - "\n", - "If $|\\psi_{AB}\\rangle$ below has two states $|\\psi\\rangle_A$ and $|\\psi\\rangle_B$;\n", - "\n", - "$$\n", - "|\\psi_{AB}\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle +|11\\rangle) = \\frac{1}{\\sqrt{2}}(|0\\rangle_A|0\\rangle_B +|1\\rangle_A|1\\rangle_B)\n", - "$$\n", - "\n", - "$$\n", - "|\\psi\\rangle_A = a_0|0\\rangle+a_1|1\\rangle\n", - "$$\n", - "$$\n", - "|\\psi\\rangle_B = b_0|0\\rangle+b_1|1\\rangle\n", - "$$\n", - "\n", - "the tensor product of these two states is the following\n", - "\n", - "$$\n", - "|\\psi\\rangle _A\\otimes |\\psi\\rangle _B = a_0 b_0|00\\rangle+a_0 b_1|01\\rangle+a_1 b_0|10\\rangle+a_1 b_1|11\\rangle\n", - "$$\n", - "\n", - "but there are no coefficients $a_0, a_1, b_0, $ and $b_1$ to satisfy these two equations. Therefore, $|\\psi_{AB}\\rangle$ is not represented by a tensor product of individual quantum state, $|\\psi\\rangle_A$ and $|\\psi\\rangle_B$, and this means that $|\\psi_{AB}\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle +|11\\rangle)$ is entangled state.\n", - "\n", - "Let us create the Bell state and run it on a real quantum computer. Now we will follow the four steps to writing a quantum program, called **Qiskit patterns**:\n", - "\n", - " 1. Map problem to quantum circuits and operators\n", - " 2. Optimize for target hardware\n", - " 3. Execute on target hardware\n", - " 4. Post-process the results" - ] - }, - { - "cell_type": "markdown", - "id": "126a32f5-9a11-4428-aa50-6cb3193cd1d6", - "metadata": {}, - "source": [ - "#### Step 1. Map problem to quantum circuits and operators\n", - "\n", - "In a quantum program, quantum circuits are the native format in which to represent quantum instructions. When creating a circuit, you'll usually create a new QuantumCircuit object, then add instructions to it in sequence.\n", - "\n", - "The following code cell creates a circuit that produces a Bell state, the specific two-qubit entangled state from above." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "5fdd9317-8fab-46ee-ac6d-25ef031dd52b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2, 2)\n", - "\n", - "qc.h(0)\n", - "qc.cx(0, 1)\n", - "\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "b3c9d5c2-7f53-4283-84bf-fbaa68e620c6", - "metadata": {}, - "source": [ - "#### Step 2. Optimize for target hardware\n", - "\n", - "Qiskit converts abstract circuits to QISA (Quantum Instruction Set Architecture) circuits that respect the constraints of the target hardware and optimizes circuit performance. So before the optimization, we will specify the target hardware." - ] - }, - { - "cell_type": "markdown", - "id": "a426fb27-ac0b-4d1e-840c-21729ca630ce", - "metadata": {}, - "source": [ - "If you do not have `qiskit-ibm-runtime`, you will need to install this first. For more information about Qiskit Runtime, [check out the API reference.](/docs/api/qiskit-ibm-runtime/runtime-service)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "1ef65ed1-926d-4f00-850d-4fb8750123a3", - "metadata": {}, - "outputs": [], - "source": [ - "# Install\n", - "# !pip install qiskit-ibm-runtime" - ] - }, - { - "cell_type": "markdown", - "id": "7170222f-2cd5-4482-8044-e126d8d53797", - "metadata": {}, - "source": [ - "We will specify the target hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a152031e-d4b6-468c-8ea4-441fe1c3c948", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "service.backends()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f07a360-adf5-467b-ac97-d466c0907dc6", - "metadata": {}, - "outputs": [], - "source": [ - "# You can specify the device\n", - "# backend = service.backend('ibm_kingston')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c3072b3-69fb-4f56-9a92-6d49c5598c5e", - "metadata": {}, - "outputs": [], - "source": [ - "# You can also identify the least busy device\n", - "backend = service.least_busy(operational=True)\n", - "print(\"The least busy device is \", backend)" - ] - }, - { - "cell_type": "markdown", - "id": "f7a015bb-1813-4acf-97e5-8489711b53fb", - "metadata": {}, - "source": [ - "Transpiling the circuit is yet another complex process. Very briefly, this rewrites the circuit into a logically equivalent one using \"native gates\" (gates that a particular quantum computer can implement) and maps the qubits in your circuit to optimal real qubits on the target quantum computer. For more on transpilation, see this [documentation](/docs/api/qiskit/transpiler#overview)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0fd980c2-386b-4583-b032-fa8375cb286c", - "metadata": {}, - "outputs": [], - "source": [ - "# Transpile the circuit into basis gates executable on the hardware\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "target_circuit = pm.run(qc)\n", - "\n", - "target_circuit.draw(\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "1f689582-b474-46aa-aa9f-208428db538a", - "metadata": {}, - "source": [ - "You can see that in the transpilation the circuit was rewritten using new gates. For more information, refer to the [ECRGate](/docs/api/qiskit/qiskit.circuit.library.ECRGate#ecrgate) documentation." - ] - }, - { - "cell_type": "markdown", - "id": "fdd46180-c519-4c03-a09b-382be5140306", - "metadata": {}, - "source": [ - "#### Step 3. Execute the target circuit\n", - "\n", - "Now, we will run the target circuit on the real device." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b4c27e3-8696-4246-b09d-0a4174eefeb9", - "metadata": {}, - "outputs": [], - "source": [ - "sampler = Sampler(backend)\n", - "job_real = sampler.run([target_circuit])\n", - "\n", - "job_id = job_real.job_id()\n", - "print(\"job id:\", job_id)" - ] - }, - { - "cell_type": "markdown", - "id": "aa8c88f1-d1ec-43c8-aafe-c9ab7cbb6dc2", - "metadata": {}, - "source": [ - "Execution on the real device might require waiting in a queue, since quantum computers are valuable resources, and very much in demand. The job_id is used to check the execution status and results of the job later." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "666985b8-90bb-4724-bb13-9965930a84cc", - "metadata": {}, - "outputs": [], - "source": [ - "# Check the job status (replace the job id below with your own)\n", - "job_real.status(job_id)" - ] - }, - { - "cell_type": "markdown", - "id": "d568cee6-69f4-4e3a-89a2-4826720d2d60", - "metadata": {}, - "source": [ - "You can also check the job status from your IBM Quantum dashboard:https://quantum.cloud.ibm.com/workloads" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "664ce0ec-0ddb-4438-8e68-d36e8b67204f", - "metadata": {}, - "outputs": [], - "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "job_real = service.job(job_id) # Input your job-id between the quotations\n", - "job_real.status()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b69e3456-6047-436a-8276-13ca6a69188c", - "metadata": {}, - "outputs": [], - "source": [ - "# Execute after job has successfully run\n", - "result_real = job_real.result()\n", - "print(result_real[0].data.c.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "554b7575-413e-4957-83db-9131a0b4bdaf", - "metadata": {}, - "source": [ - "#### Step 4. Post-process the results\n", - "\n", - "Finally, we must post-process our results to create outputs in the expected format, like values or graphs." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e22b1077-8311-4887-a7b3-c81006ba4eff", - "metadata": {}, - "outputs": [], - "source": [ - "plot_histogram(result_real[0].data.c.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "240c2033-9f06-4f58-9075-5da1aa0f2dc5", - "metadata": {}, - "source": [ - "As you can see, $|00\\rangle$ and $|11\\rangle$ are the most frequently observed. There are a few results other than the expected data, and they are due to noise and qubit decoherence. We will learn more about errors and noise in quantum computers in the later lessons of this course." - ] - }, - { - "cell_type": "markdown", - "id": "8dfaf680-c957-4250-bd40-b39d60afd38e", - "metadata": {}, - "source": [ - "### 4.4 GHZ state\n", - "\n", - "The concept of entanglement can be extended to systems of more than two qubits. The GHZ state (Greenberger-Horne-Zeilinger state) is a maximally entangled state of three or more qubits. The GHZ state for three qubits is defined as\n", - "\n", - "$$\n", - "\\frac{1}{\\sqrt 2}(|000\\rangle + |111\\rangle)\n", - "$$\n", - "\n", - "It can be created with the following quantum circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "459b6162-3a54-41fe-9b67-9c1f8e731ebc", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(3, 3)\n", - "\n", - "qc.h(0)\n", - "qc.cx(0, 1)\n", - "qc.cx(1, 2)\n", - "\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "qc.measure(2, 2)\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "6fa6efe6-d4bb-42ef-a7cc-3faf4ddf7115", - "metadata": {}, - "source": [ - "The \"depth\" of a quantum circuit is a useful and common metric to describe quantum circuits. Trace a path through the quantum circuit, moving left to right, only changing qubits when they are connected by a multi-qubit gate. Count the number of gates along that path. The maximum number of gates for any such path through a circuit is the depth. In modern noisy quantum computers, low-depth circuits have fewer errors and are likely to return good results. Very deep circuits are not.\n", - "\n", - "Using `QuantumCircuit.depth()`, we can check the depth of our quantum circuit. The depth of the above circuit is 4. The top qubit has only three gates including the measurement. But there is a path from the top qubit down to either qubit 1 or qubit 2 which involves another CNOT gate." - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "612505ab-1e11-467b-b5bf-6e18bd81256d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc.depth()" - ] - }, - { - "cell_type": "markdown", - "id": "91c157ed-0fc5-40a8-8092-341bca361da3", - "metadata": {}, - "source": [ - "### Exercise 2\n", - "\n", - "The GHZ state of an 8-qubit system is\n", - "\n", - "$$\n", - "\\frac{1}{\\sqrt 2}(|00000000\\rangle + |11111111\\rangle)\n", - "$$\n", - "\n", - "\n", - "Write code to prepare this state with the shallowest possible circuit. The depth of the shallowest quantum circuit is 5, including the measurement gates.\n", - "\n", - "__Solution__:" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "153eefc8-f021-41ee-97c4-a9555370f197", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1\n", - "qc = QuantumCircuit(8, 8)\n", - "\n", - "##your code goes here##\n", - "qc.h(0)\n", - "qc.cx(0, 4)\n", - "qc.cx(4, 6)\n", - "qc.cx(6, 7)\n", - "\n", - "qc.cx(4, 5)\n", - "\n", - "qc.cx(0, 2)\n", - "qc.cx(2, 3)\n", - "\n", - "qc.cx(0, 1)\n", - "qc.barrier() # for visual separation\n", - "\n", - "# measure\n", - "for i in range(8):\n", - " qc.measure(i, i)\n", - "\n", - "qc.draw(\"mpl\")\n", - "# print(qc.depth())" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "0d44521b-e08b-4445-a6ed-6ec2ac550142", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "5\n" - ] - } - ], - "source": [ - "print(qc.depth())" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "0daedbeb-c75c-4bcf-b6e0-1f9fa0403b0d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'11111111': 535, '00000000': 489}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "# Step 2\n", - "# For this exercise, the circuit and operators are simple, so no optimizations are needed.\n", - "\n", - "# Step 3\n", - "# Run the circuit on a simulator to get the results\n", - "backend = AerSimulator()\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc], shots=1024)\n", - "result = job.result()\n", - "\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Step 4\n", - "# Plot the counts in a histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "d4057c5b-2dab-4590-afda-89d8cd7d26d5", - "metadata": {}, - "source": [ - "## 5. Summary\n", - "\n", - "You have learned quantum computation with the circuit model using quantum bits and gates, and you have reviewed superposition, measurement, and entanglement. You also have learned the method to execute the quantum circuit on the real quantum device.\n", - "\n", - "In the final exercise to create a GHZ circuit, you have tried to reduce the circuit depth, which is an important factor for obtaining a utility scale solution in a noisy quantum computer. In the later lessons of this course, you will learn about noise and about error mitigation methods in detail. In this lesson, as an introduction, we considered reducing the circuit depth in an ideal device, but in reality, we must consider the constraints of real device, such as qubit connectivity. You will learn more about this in subsequent lessons in this course." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "eb838405-b298-4a81-ab23-9d0744090b9c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the version of Qiskit\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4233533a-fcd2-4bd7-bcb8-8e0be95326e6", + "metadata": {}, + "source": [ + "---\n", + "title: Bits, gates, and circuits\n", + "description: you will learn quantum computation with circuit model using quantum bits (qubits) and gates.\n", + "---\n", + "\n", + "\n", + "# Quantum bits, gates, and circuits\n", + "\n", + "\n", + "\n", + "Kifumi Numata (19 Apr 2024)\n", + "\n", + "Click [here](https://ibm.ent.box.com/public/static/vee9e1kkxxiih5g8yqdhoo6t0o3dw7xf.zip) to download the pdf of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 5 seconds.*\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "ec475aee-075f-4411-8dbe-8a427865714b", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "Bits, gates, and circuits are the basic building blocks of quantum computing. You will learn quantum computation with the circuit model using quantum bits and gates, and also review the superposition, measurement, and entanglement.\n", + "\n", + "In this lesson you will learn:\n", + "- Single-qubit gates\n", + "- Bloch sphere\n", + "- Superposition\n", + "- Measurement\n", + "- Two-qubit gates and entanglement state\n", + "\n", + "At the end of this lecture, you will learn about circuit depth, which is essential for utility-scale quantum computing." + ] + }, + { + "cell_type": "markdown", + "id": "ed761072-54aa-47af-a2e1-a5c40a0471d1", + "metadata": {}, + "source": [ + "## 2. Computation as a diagram\n", + "\n", + "When using qubits or bits, we need to manipulate them in order to turn the inputs we have into the outputs we need. For the simplest programs with very few bits, it is useful to represent this process in a diagram known as a *circuit diagram*.\n", + "\n", + "The bottom-left figure is an example of a classical circuit, and the bottom-right figure is an example of a quantum circuit. In both cases, the inputs are on the left and the outputs are on the right, while the operations are represented by symbols. The symbols used for the operations are called “gates”, mostly for historical reasons.\n", + "\n", + "![\"classical logic and quantum circuit\"](/learning/images/courses/utility-scale-quantum-computing/bits-gates-and-circuits/classical-vs-quantum.avif)" + ] + }, + { + "cell_type": "markdown", + "id": "87f8d07a-1544-4acb-82fb-3a43f1a99f8c", + "metadata": {}, + "source": [ + "## 3. Single-qubit quantum gate\n", + "\n", + "### 3.1 Quantum state and Bloch sphere\n", + "\n", + "A qubit's state is represented as a superposition of $|0\\rangle$ and $|1\\rangle$. An arbitrary quantum state is represented as\n", + "\n", + "$$\n", + "|\\psi\\rangle =\\alpha|0\\rangle+ \\beta|1\\rangle\n", + "$$\n", + "\n", + "where $\\alpha$ and $\\beta$ are complex numbers such that $|\\alpha|^2+|\\beta|^2=1$.\n", + "\n", + "$|0\\rangle$ and $|1\\rangle$ are vectors in the two-dimensional complex vector space:\n", + "\n", + "$$\n", + "|0\\rangle = \\begin{pmatrix}\n", + "1 \\\\0\n", + "\\end{pmatrix},\n", + "|1\\rangle = \\begin{pmatrix}\n", + "0\\\\1\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "Therefore, an arbitrary quantum state is also represented as\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\alpha\\begin{pmatrix}\n", + "1 \\\\ 0\n", + "\\end{pmatrix} + \\beta\\begin{pmatrix}0\\\\\n", + "1\n", + "\\end{pmatrix} = \\begin{pmatrix}\n", + "\\alpha \\\\ \\beta\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "From this, we can see that the state of a quantum bit is a unit vector in a two-dimensional complex inner product space with an orthonormal basis of $|0\\rangle$ and $|1\\rangle$. It is normalized to 1.\n", + "\n", + "$$\n", + "\\langle\\psi|\\psi\\rangle = \\begin{pmatrix}\n", + "\\alpha^* & \\beta^*\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "\\alpha \\\\\n", + "\\beta\n", + "\\end{pmatrix} = 1\n", + "$$\n", + "\n", + "$ |\\psi\\rangle =\\begin{pmatrix}\n", + "\\alpha \\\\ \\beta\n", + "\\end{pmatrix}$ is also called the statevector.\n", + "\n", + "A single-qubit quantum state is also represented as\n", + "\n", + "$$\n", + "|\\psi\\rangle\n", + "=\\cos\\frac{\\theta}{2}|0\\rangle+e^{i\\varphi}\\sin\\frac{\\theta}{2}|1\\rangle\n", + "=\\left( \\begin{pmatrix} \\cos\\frac{\\theta}{2}\\\\ e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", + "\\end{pmatrix}\\right)\n", + "$$\n", + "\n", + "where $\\theta$ and $\\varphi$ are the angles of the Bloch sphere in the following figure.\n", + "\n", + "![Bloch sphere](/learning/images/courses/utility-scale-quantum-computing/bits-gates-and-circuits/bloch.avif)" + ] + }, + { + "cell_type": "markdown", + "id": "86a3a654-3fb7-492c-bf4b-ecf23b4b71a6", + "metadata": {}, + "source": [ + "In the next few code cells, we will build up basic calculations from constituent pieces in Qiskit. We'll construct an empty circuit and then add quantum operations, discussing the gates and visualizing their effects as we go.\n", + "You can run the cell by \"Shift\" + \"Enter\". Import the libraries first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "113bc3fe-3295-4524-9bf8-c1f36e51554b", + "metadata": {}, + "outputs": [], + "source": [ + "# Import the qiskit library\n", + "from qiskit import QuantumCircuit\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit.quantum_info import Statevector\n", + "from qiskit.visualization import plot_bloch_multivector\n", + "from qiskit_ibm_runtime import Sampler\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.visualization import plot_histogram" + ] + }, + { + "cell_type": "markdown", + "id": "dbb2272c-996f-46a9-b5b0-f62d60c4deb3", + "metadata": {}, + "source": [ + "#### Prepare the quantum circuit\n", + "We will create and draw a single-qubit circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fdef9137-9e39-4b05-afa9-9091766053f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the single-qubit quantum circuit\n", + "qc = QuantumCircuit(1)\n", + "\n", + "# Draw the circuit\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "50ffcbab-e427-4582-9864-92e1491544a1", + "metadata": {}, + "source": [ + "#### X gate\n", + "\n", + "The X gate is a $\\pi$ rotation around the $x$ axis of the Bloch sphere.\n", + "Applying the X gate to $|0\\rangle$ results in $|1\\rangle$, and applying the X gate to $|1\\rangle$ results in $|0\\rangle$, so it is an operation similar to the classical NOT gate, and is also known as bit flip. The matrix representation of the X gate is below.\n", + "\n", + "$$\n", + "X = \\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 0 \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b3468430-0cbc-44ab-bd21-901436662a4b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1) # Prepare the single-qubit quantum circuit\n", + "\n", + "# Apply a X gate to qubit 0\n", + "qc.x(0)\n", + "\n", + "# Draw the circuit\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "c944e04b-09b3-429b-82d8-12be6b94d86d", + "metadata": {}, + "source": [ + "In IBM Quantum®, the initial state is set to $|0\\rangle$, so the quantum circuit above in matrix representation is\n", + "\n", + "$$\n", + "X|0\\rangle= \\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 0\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "1 \\\\ 0\n", + "\\end{pmatrix}\n", + " =\\begin{pmatrix}\n", + "0 \\\\ 1\n", + "\\end{pmatrix} = |1\\rangle\n", + "$$\n", + "\n", + "Next, let's run this circuit using a statevector simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "efb727e8-f51e-4903-aa0f-e4c099316815", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.+0.j, 1.+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "b4262b2d-10d8-42a8-b632-4d4872ccc3c3", + "metadata": {}, + "source": [ + "Vertical vector is displayed as row vector, with complex numbers (the imaginary part is indexed by $j$ ).\n", + "\n", + "#### H gate\n", + "The Hadamard gate is a $\\pi$ rotation around an axis halfway between the $x$ and $z$ axes on the Bloch sphere. Applying the H gate to $|0\\rangle$ creates a superposition state such as $\\frac{|0\\rangle + |1\\rangle}{\\sqrt{2}}$. The matrix representation of the H gate is below.\n", + "\n", + "$$\n", + "H = \\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", + "1 & 1 \\\\\n", + "1 & -1 \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "574c35f0-26a9-423a-8d65-7140ba7398e3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1) # Create the single-qubit quantum circuit\n", + "\n", + "# Apply an Hadamard gate to qubit 0\n", + "qc.h(0)\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7adf0e93-6db3-4aa8-800f-c91c8cc46001", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.70710678+0.j, 0.70710678+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "2c9b271a-7dcd-426f-b79d-378575f41dfb", + "metadata": {}, + "source": [ + "This is\n", + "$$\n", + "H|0\\rangle= \\frac{1}{\\sqrt{2}} \\begin{pmatrix}\n", + "1 & 1 \\\\\n", + "1 & -1\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "1 \\\\0\n", + "\\end{pmatrix}\n", + " =\\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", + "1 \\\\\n", + "1\n", + "\\end{pmatrix}\n", + "=\\begin{pmatrix}\n", + "0.707 \\\\\n", + "0.707\n", + "\\end{pmatrix}\n", + "=\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)\n", + "$$\n", + "\n", + "This superposition state is so common and important, that it is given its own symbol:\n", + "\n", + "$$\n", + "|+\\rangle \\equiv \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle).\n", + "$$\n", + "\n", + "By applying the $H$ gate on the $|0\\rangle$, we created a superposition of $|0\\rangle$ and $|1\\rangle$ where measurement in the computational basis (along z in the Bloch sphere picture) would give you each state with equal probabilities." + ] + }, + { + "cell_type": "markdown", + "id": "07fdfe64-f6ae-4995-8857-e6559ae4a3b6", + "metadata": {}, + "source": [ + "#### $|-\\rangle$ state\n", + "\n", + "You might have guessed that there is a corresponding $|-\\rangle$ state:\n", + "$$\n", + "|-\\rangle \\equiv \\frac{|0\\rangle -|1\\rangle}{\\sqrt{2}}.\n", + "$$\n", + "To create this state, first apply an X gate to make $|1\\rangle$, then apply an H gate." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b8e96fc4-6708-415e-b8fc-ac69eaeae750", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1) # Create the single-qubit quantum circuit\n", + "\n", + "# Apply a X gate to qubit 0\n", + "qc.x(0)\n", + "\n", + "# Apply an Hadamard gate to qubit 0\n", + "qc.h(0)\n", + "\n", + "# draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f42b82aa-7a15-4f81-8530-b997d4c6e0de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([ 0.70710678+0.j, -0.70710678+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "2d27e9ca-67be-4c69-8930-b70bbdf03b8d", + "metadata": {}, + "source": [ + "This is\n", + "\n", + "$$\n", + "H|1\\rangle= \\frac{1}{\\sqrt{2}} \\begin{pmatrix}\n", + "1 & 1 \\\\\\\n", + "1 & -1\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "0 \\\\\\\n", + "1\n", + "\\end{pmatrix}\n", + " =\\frac{1}{\\sqrt{2}}\\begin{pmatrix}\n", + "1 \\\\\\\n", + "-1\n", + "\\end{pmatrix}\n", + "=\\begin{pmatrix}\n", + "0.707 \\\\\\\n", + "-0.707\n", + "\\end{pmatrix}\n", + "=\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle) = |-\\rangle\n", + "$$\n", + "\n", + "\n", + "\n", + "Applying the $H$ gate on $|1\\rangle$ results in an equal superposition of $|0\\rangle$ and $|1\\rangle$, but the sign of $|1\\rangle$ is negative." + ] + }, + { + "cell_type": "markdown", + "id": "bbc2aac6-cf44-45dd-a635-7b75fa197262", + "metadata": {}, + "source": [ + "### 3.2 Single-qubit quantum state and unitary evolution\n", + "\n", + "The actions of all the gates we have seen so far have been *unitary*, which means they can be represented by a unitary operator. In other words, the output state can be obtained by acting on the initial state with a unitary matrix:\n", + "\n", + "$$\n", + "|\\psi^{'}\\rangle = U|\\psi\\rangle\n", + "$$\n", + "\n", + "A unitary matrix is a matrix satisfying\n", + "\n", + "$$\n", + "U^{\\dagger}U =U U^{\\dagger} = I.\n", + "$$\n", + "\n", + "In terms of quantum computer operation, we would say that applying a quantum gate to the qubit evolves the quantum state. Common single-qubit gates include the following.\n", + "\n", + "Pauli gates:\n", + "\n", + "$$\n", + "X = \\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 0 \\\\\n", + "\\end{pmatrix}\n", + "= |0\\rangle \\langle 1|+|1\\rangle \\langle 0|\n", + "$$\n", + "\n", + "$$\n", + "Y = \\begin{pmatrix}\n", + "0 & -i \\\\\n", + "i & 0 \\\\\n", + "\\end{pmatrix}\n", + "= -i|0\\rangle \\langle 1|+i|1\\rangle \\langle 0|\n", + "$$\n", + "\n", + "$$\n", + "Z = \\begin{pmatrix}\n", + "1 & 0 \\\\\n", + "0 & -1 \\\\\n", + "\\end{pmatrix}\n", + "= |0\\rangle \\langle 0|-|1\\rangle \\langle 1|\n", + "$$\n", + "\n", + "where the outer product was calculated as follows:\n", + "$$\n", + "|0\\rangle \\langle 0|=\n", + "\\begin{bmatrix}\n", + "1 \\\\\n", + "0\n", + "\\end{bmatrix}\n", + "\\begin{bmatrix}\n", + "1 & 0\n", + "\\end{bmatrix}\n", + "=\\begin{bmatrix}\n", + "1 & 0 \\\\\n", + "0 & 0 \\\\\n", + "\\end{bmatrix}, \\quad\n", + "|1\\rangle \\langle 0|=\n", + "\\begin{bmatrix}\n", + "0 \\\\\n", + "1\n", + "\\end{bmatrix}\n", + "\\begin{bmatrix}\n", + "1 & 0\n", + "\\end{bmatrix}\n", + "=\\begin{bmatrix}\n", + "0 & 0 \\\\\n", + "1 & 0 \\\\\n", + "\\end{bmatrix}, \\quad\n", + "$$\n", + "\n", + "$$\n", + "|0\\rangle \\langle 1|=\n", + "\\begin{bmatrix}\n", + "1 \\\\\n", + "0\n", + "\\end{bmatrix}\n", + "\\begin{bmatrix}\n", + "0 & 1\n", + "\\end{bmatrix}\n", + "=\\begin{bmatrix}\n", + "0 & 1 \\\\\n", + "0 & 0 \\\\\n", + "\\end{bmatrix}, \\quad\n", + "|1\\rangle \\langle 1|=\n", + "\\begin{bmatrix}\n", + "0 \\\\\n", + "1\n", + "\\end{bmatrix}\n", + "\\begin{bmatrix}\n", + "0 & 1\n", + "\\end{bmatrix}\n", + "=\\begin{bmatrix}\n", + "0 & 0 \\\\\n", + "0 & 1 \\\\\n", + "\\end{bmatrix}, \\quad\n", + "$$\n", + "\n", + "Other typical single-qubit gates:\n", + "$$\n", + "H= \\frac{1}{\\sqrt{2}}\\begin{bmatrix}\n", + "1 & 1 \\\\\n", + "1 & -1 \\\\\n", + "\\end{bmatrix},\\quad\n", + "S = \\begin{bmatrix}\n", + "1 & 0 \\\\\n", + "0 & i \\\\\n", + "\\end{bmatrix}, \\quad\n", + "T = \\begin{bmatrix}\n", + "1 & 0 \\\\\n", + "0 & exp(i\\pi/4) \\\\\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "$$\n", + "R_x(\\theta) = e^{-i\\theta X/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}X = \\begin{bmatrix}\n", + "cos\\frac{\\theta}{2} & -i sin \\frac{\\theta}{2} \\\\\n", + "-i sin \\frac{\\theta}{2} & cos\\frac{\\theta}{2} \\\\\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "$$\n", + "R_y(\\theta) = e^{-i\\theta Y/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}Y = \\begin{bmatrix}\n", + "cos\\frac{\\theta}{2} & - sin \\frac{\\theta}{2} \\\\\n", + "sin \\frac{\\theta}{2} & cos\\frac{\\theta}{2} \\\\\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "$$\n", + "R_z(\\theta) = e^{-i\\theta Z/2} = cos\\frac{\\theta}{2}I - i sin \\frac{\\theta}{2}Z = \\begin{bmatrix}\n", + "e^{-i\\theta /2} & 0 \\\\\n", + "0 & e^{i\\theta /2} \\\\\n", + "\\end{bmatrix}\n", + "$$\n", + "\n", + "The meaning and use of these are described in more detail in the [Basics of Quantum Information](/learning/courses/basics-of-quantum-information) course." + ] + }, + { + "cell_type": "markdown", + "id": "39da8188-4ea3-420a-8467-29dd249ca93b", + "metadata": {}, + "source": [ + "### Exercise 1\n", + "\n", + "Use Qiskit to create quantum circuits that prepare the states described below. Then run each circuit using the statevector simulator and display the resulting state on the Bloch sphere. As a bonus, see if you can anticipate what the final state should be based on intuition about the gates and rotations in the Bloch sphere.\n", + "\n", + "(1) $XX|0\\rangle$\n", + "\n", + "(2) $HH|0\\rangle$\n", + "\n", + "(3) $HZH|0\\rangle$\n", + "\n", + "Tips: Z gate can be used by\n", + "\n", + " qc.z(0)\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "148fdaf9-0398-4dd3-84b3-1a71a4f5d34e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "### (1) XX|0> ###\n", + "\n", + "# Create the single-qubit quantum circuit\n", + "qc = QuantumCircuit(1) ##your code goes here##\n", + "\n", + "# Add a X gate to qubit 0\n", + "qc.x(0) ##your code goes here##\n", + "\n", + "# Add a X gate to qubit 0\n", + "qc.x(0) ##your code goes here##\n", + "\n", + "# Draw a circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4720c4a0-acc7-46cc-b8ba-be2ca87ab976", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([1.+0.j, 0.+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "361c09fe-c206-4355-b827-da7ede0848de", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "### (2) HH|0> ###\n", + "##your code goes here##\n", + "qc = QuantumCircuit(1)\n", + "qc.h(0)\n", + "qc.h(0)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a4976a54-77e2-42b5-b492-49c8cdaf9c9f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([1.+0.j, 0.+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "cd01ed0c-8f9e-4689-b1ae-f028d588f81a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "### (3) HZH|0> ###\n", + "##your code goes here##\n", + "qc = QuantumCircuit(1)\n", + "qc.h(0)\n", + "qc.z(0)\n", + "qc.h(0)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4bc91c2a-e097-44db-95bb-acf0d0ea0844", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.+0.j, 1.+0.j],\n", + " dims=(2,))\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)\n", + "\n", + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "59f4ee8b-7243-418c-8094-ae52e750fac1", + "metadata": {}, + "source": [ + "### 3.3 Measurement\n", + "\n", + "Measurement is theoretically a very complicated topic. But in practical terms, making a measurement along $z$ (as all IBM® quantum computers do) simply forces the qubit’s state $\\alpha|0\\rangle+\\beta|1\\rangle \\quad (s.t.|\\alpha|^2+|\\beta|^2=1)$ either to $|0\\rangle$ or to $|1\\rangle,$ and we observe the outcome.\n", + "- $|\\alpha|^2$ is the probability we will get $|0\\rangle$ when we measure.\n", + "- $|\\beta|^2$ is the probability we will get $|1\\rangle$ when we measure.\n", + "\n", + "So, $\\alpha$ and $\\beta$ are called probability amplitudes. (see \"Born rule\")\n", + "\n", + "For example, $\\frac{\\sqrt{2}}{2}|0\\rangle+\\frac{\\sqrt{2}}{2}|1\\rangle$ has an equal probability of becoming $|0\\rangle$ or $|1\\rangle$ upon measurement. $\\frac{\\sqrt{3}}{2}|0\\rangle-\\frac{1}{2}i|1\\rangle$ has a 75% chance of becoming $|0\\rangle$." + ] + }, + { + "cell_type": "markdown", + "id": "fb2c925a-e906-467b-b971-4663eb4751c5", + "metadata": {}, + "source": [ + "#### Qiskit Aer Simulator\n", + "Next, let's measure a circuit that prepares the equal probability superposition above.\n", + "We should add the measurement gates, as the Qiskit Aer simulator simulates an ideal (with no noise) quantum hardware by default. Note: The Aer simulator can also apply a noise model based on real quantum computer. We will return to noise models later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3956e602-9f1b-4083-b58d-26c91b50b75e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a new circuit with one qubits (first argument) and one classical bits (second argument)\n", + "qc = QuantumCircuit(1, 1)\n", + "qc.h(0)\n", + "qc.measure(0, 0) # Add the measurement gate\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "95abf094-bd82-4a4a-ad12-29bec082d9c8", + "metadata": {}, + "source": [ + "We are now ready to run our circuit on the Aer simulator. In this example, we will apply the default shots=1024, which means we will measure 1024 times. Then we will plot those counts in a histogram." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "7cb7c5ab-7b12-4f77-aaca-1d7e6cff213c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'0': 521, '1': 503}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the circuit on a simulator to get the results\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc])\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "ea8c3435-8e76-4f04-9d50-d1eb575158d3", + "metadata": {}, + "source": [ + "We see that 0s and 1s were measured with a probability of almost 50% each. Although noise has not been simulated here, the states are still probabilistic. So while we expect roughly a 50-50 distribution, we will rarely find exactly that. Just as 100 flips of a coin would rarely yield exactly 50 instances of each side." + ] + }, + { + "cell_type": "markdown", + "id": "33d5a0ef-4eca-46d0-9c87-2f42b9a70ebb", + "metadata": {}, + "source": [ + "## 4. Multi-qubit quantum gate and entanglement\n", + "\n", + "### 4.1 Multi-qubit quantum circuit\n", + "\n", + "We can create a two-qubit quantum circuit with following code. We will apply an H gate to each qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e880f7eb-108c-4387-8ef4-97219fd5691c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the two qubits quantum circuit\n", + "qc = QuantumCircuit(2)\n", + "\n", + "# Apply an H gate to qubit 0\n", + "qc.h(0)\n", + "\n", + "# Apply an H gate to qubit 1\n", + "qc.h(1)\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "28c4fdb1-122b-4399-a3ee-a24f6b2d36ca", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j],\n", + " dims=(2, 2))\n" + ] + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "fa6412ee", + "metadata": {}, + "source": [ + "#### Note: Qiskit bit ordering\n", + "\n", + "Qiskit uses **Little Endian** notation when ordering qubits and bits, meaning **qubit 0 is the rightmost bit** in the bitstrings. Example: $|01\\rangle$ means q0 is $|1\\rangle$ and q1 is $|0\\rangle$. Be careful because some literature in quantum computing use the Big Endian notation (qubit 0 is the leftmost bit) and a great deal of quantum mechanics literature does, too.\n", + "\n", + "Another thing to notice is that when representing a quantum circuit, $|q_0\\rangle$ is always placed at the top of the circuit." + ] + }, + { + "cell_type": "markdown", + "id": "905f20cf-3836-4766-bca1-05a4bd220d64", + "metadata": {}, + "source": [ + "With this in mind, the quantum state of the above circuit can be written as a tensor product of single-qubit quantum state.\n", + "\n", + "$\n", + "|q1\\rangle \\otimes|q0\\rangle = (a|0\\rangle+b|1\\rangle) \\otimes (c|0\\rangle+d|1\\rangle)\n", + "$\n", + "\n", + "$\n", + "= ac|0\\rangle|0\\rangle+ad|0\\rangle|1\\rangle+bc|1\\rangle|0\\rangle+bd|1\\rangle|1\\rangle\n", + "$\n", + "\n", + "$\n", + "= ac|00\\rangle+ad|01\\rangle+bc|10\\rangle+bd|11\\rangle\n", + "$\n", + "\n", + "( $|ac|^2+ |ad|^2+ |bc|^2+ |bd|^2=1$ )\n", + "\n", + "\n", + "The initial state of Qiskit is $|0\\rangle|0\\rangle=|00\\rangle$, so by applying $H$ to each qubit, it changes to a state of equal superposition.\n", + "\n", + "$H|0\\rangle \\otimes H|0\\rangle=\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle) \\otimes \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle) = \\frac{1}{2}(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle)$\n", + "\n", + "$$\n", + "=\\frac{1}{2}\\left( \\begin{pmatrix} 1 \\\\ 1 \\end{pmatrix} \\otimes \\begin{pmatrix} 1 \\\\ 1 \\end{pmatrix}\\right) = \\frac{1}{2}\\begin{pmatrix} 1 \\\\ 1 \\\\ 1 \\\\ 1 \\end{pmatrix}=\\frac{1}{2}\\left(\\begin{pmatrix} 1 \\\\ 0 \\\\ 0 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 1 \\\\ 0 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 0 \\\\ 1 \\\\ 0 \\end{pmatrix}+\\begin{pmatrix} 0 \\\\ 0 \\\\ 0 \\\\ 1 \\end{pmatrix}\\right)\n", + "$$\n", + "\n", + "The measurement rule is also same as a single qubit case, the probability of measuring $|00\\rangle$ is $|ac|^2$." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "ebdf591f-18d0-4fd1-a3a3-b1cdac13bd97", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Draw a Bloch sphere\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "528cbdb3-eb30-42f2-a493-00ed22c09056", + "metadata": {}, + "source": [ + "Next, let's measure this circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "2afdd5ac-e51a-4d94-9307-955cc8fbf821", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a new circuit with two qubits (first argument) and two classical bits (second argument)\n", + "qc = QuantumCircuit(2, 2)\n", + "\n", + "# Apply the gates\n", + "qc.h(0)\n", + "qc.h(1)\n", + "\n", + "# Add the measurement gates\n", + "qc.measure(0, 0) # Measure qubit 0 and save the result in bit 0\n", + "qc.measure(1, 1) # Measure qubit 1 and save the result in bit 1\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "e94b24d5-1c35-46f4-a7f7-fd4849f10473", + "metadata": {}, + "source": [ + "Now, we will use a Aer simulator, again, to experimentally verify that the relative probabilities of all possible output states are roughly equal." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "060512ac-a106-4ecd-94f5-119048a08467", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'10': 262, '01': 246, '00': 265, '11': 251}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the circuit on a simulator to get the results\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc])\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "c4875883-3bb3-4537-aa6e-0d46b67c49e3", + "metadata": {}, + "source": [ + "As expected, the states $|00\\rangle$, $|01\\rangle$, $|10\\rangle$, $|11\\rangle$ were measured almost 25% each." + ] + }, + { + "cell_type": "markdown", + "id": "d28f149c-b02a-4c4f-b0c5-2252b5dd01fc", + "metadata": {}, + "source": [ + "### 4.2 Multi-qubit quantum gates\n", + "#### CNOT gate\n", + "\n", + "A CNOT(\"controlled NOT\" or CX) gate is a two-qubit gate, meaning its action involves two qubits at once: the control qubit and the target qubit. A CNOT flips the target qubit only when the control qubit is $|1\\rangle$.\n", + "\n", + "| Input (target,control) | Output (target,control) |\n", + "|:-----------:|:------------:|\n", + "| 00 | 00 |\n", + "| 01 | 11 |\n", + "| 10 | 10 |\n", + "| 11 | 01 |\n", + "\n", + "Let us first simulate the action of this two-qubit gate when q0 and q1 are both $|0\\rangle$, and obtain the output statevector. The Qiskit syntax used is ```qc.cx(control qubit, target qubit)```." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "a5621efd-dd9f-4ea4-bf3a-be3cacb45287", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a circuit with two quantum registers and two classical registers\n", + "qc = QuantumCircuit(2, 2)\n", + "\n", + "# Apply the CNOT (cx) gate to a |00> state.\n", + "qc.cx(0, 1) # Here the control is set to q0 and the target is set to q1.\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "6dcea9ac-c9c3-4211-8d62-6ae9beb9e723", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],\n", + " dims=(2, 2))\n" + ] + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "62d9fb42-6498-4e5a-b625-f5e3be15c36c", + "metadata": {}, + "source": [ + "As expected, applying a CNOT gate on $|00\\rangle$ did not change the state, since the control qubit was in the $|0\\rangle$ state." + ] + }, + { + "cell_type": "markdown", + "id": "af0c98e9-d440-4899-bee5-22591d395f33", + "metadata": {}, + "source": [ + "Let's get back to our CNOT operation. This time we will apply a CNOT gate to $|01\\rangle$ and see what happens." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "5a02de82-44ff-40eb-b64c-2389d6eae3ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2, 2)\n", + "\n", + "# q0=1, q1=0\n", + "qc.x(0) # Apply a X gate to initialize q0 to 1\n", + "qc.cx(0, 1) # Set the control bit to q0 and the target bit to q1.\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "f878f9fb-4142-4a6e-a4dd-ef2e2e2d5118", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],\n", + " dims=(2, 2))\n" + ] + } + ], + "source": [ + "# See the statevector\n", + "out_vector = Statevector(qc)\n", + "print(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "0ed70dbf-4161-4edc-8035-681034acb4e0", + "metadata": {}, + "source": [ + "By applying a CNOT gate, the $|01\\rangle$ state has now become $|11\\rangle$.\n", + "\n", + "Let us verify these results by running the circuit on a simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "abcafb77-20bf-4bb7-bf8d-57f33af280c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Add measurements\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "\n", + "# Draw the circuit\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "88b76f1d-083b-4f28-b415-380738d22599", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'11': 1024}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the circuit on a simulator to get the results\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([isa_qc])\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "86b95716-0817-4466-9f8f-3a212e0c08e3", + "metadata": {}, + "source": [ + "The results should show you that $|11\\rangle$ has been measured with 100% probability.\n", + "\n", + "### 4.3 Quantum entanglement and execution on a real quantum device\n", + "\n", + "Let's start by introducing a specific entangled state which is particularly important in quantum computation, then we'll define the term \"entangled\":\n", + "\n", + "$$\n", + "\\frac{1}{\\sqrt{2}}|00\\rangle + \\frac{1}{\\sqrt{2}}|11\\rangle\n", + "$$\n", + "and this state is called a **Bell state**.\n", + "\n", + "An entangled state is a state $|\\psi_{AB}\\rangle$ consisting of quantum states $|\\psi_A\\rangle$ and $|\\psi_B\\rangle$ that cannot be represented by a tensor product of individual quantum states.\n", + "\n", + "If $|\\psi_{AB}\\rangle$ below has two states $|\\psi\\rangle_A$ and $|\\psi\\rangle_B$;\n", + "\n", + "$$\n", + "|\\psi_{AB}\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle +|11\\rangle) = \\frac{1}{\\sqrt{2}}(|0\\rangle_A|0\\rangle_B +|1\\rangle_A|1\\rangle_B)\n", + "$$\n", + "\n", + "$$\n", + "|\\psi\\rangle_A = a_0|0\\rangle+a_1|1\\rangle\n", + "$$\n", + "$$\n", + "|\\psi\\rangle_B = b_0|0\\rangle+b_1|1\\rangle\n", + "$$\n", + "\n", + "the tensor product of these two states is the following\n", + "\n", + "$$\n", + "|\\psi\\rangle _A\\otimes |\\psi\\rangle _B = a_0 b_0|00\\rangle+a_0 b_1|01\\rangle+a_1 b_0|10\\rangle+a_1 b_1|11\\rangle\n", + "$$\n", + "\n", + "but there are no coefficients $a_0, a_1, b_0, $ and $b_1$ to satisfy these two equations. Therefore, $|\\psi_{AB}\\rangle$ is not represented by a tensor product of individual quantum state, $|\\psi\\rangle_A$ and $|\\psi\\rangle_B$, and this means that $|\\psi_{AB}\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle +|11\\rangle)$ is entangled state.\n", + "\n", + "Let us create the Bell state and run it on a real quantum computer. Now we will follow the four steps to writing a quantum program, called **Qiskit patterns**:\n", + "\n", + " 1. Map problem to quantum circuits and operators\n", + " 2. Optimize for target hardware\n", + " 3. Execute on target hardware\n", + " 4. Post-process the results" + ] + }, + { + "cell_type": "markdown", + "id": "126a32f5-9a11-4428-aa50-6cb3193cd1d6", + "metadata": {}, + "source": [ + "#### Step 1. Map problem to quantum circuits and operators\n", + "\n", + "In a quantum program, quantum circuits are the native format in which to represent quantum instructions. When creating a circuit, you'll usually create a new QuantumCircuit object, then add instructions to it in sequence.\n", + "\n", + "The following code cell creates a circuit that produces a Bell state, the specific two-qubit entangled state from above." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "5fdd9317-8fab-46ee-ac6d-25ef031dd52b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2, 2)\n", + "\n", + "qc.h(0)\n", + "qc.cx(0, 1)\n", + "\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "b3c9d5c2-7f53-4283-84bf-fbaa68e620c6", + "metadata": {}, + "source": [ + "#### Step 2. Optimize for target hardware\n", + "\n", + "Qiskit converts abstract circuits to QISA (Quantum Instruction Set Architecture) circuits that respect the constraints of the target hardware and optimizes circuit performance. So before the optimization, we will specify the target hardware." + ] + }, + { + "cell_type": "markdown", + "id": "a426fb27-ac0b-4d1e-840c-21729ca630ce", + "metadata": {}, + "source": [ + "If you do not have `qiskit-ibm-runtime`, you will need to install this first. For more information about Qiskit Runtime, [check out the API reference.](/docs/api/qiskit-ibm-runtime/runtime-service)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "1ef65ed1-926d-4f00-850d-4fb8750123a3", + "metadata": {}, + "outputs": [], + "source": [ + "# Install\n", + "# !pip install qiskit-ibm-runtime" + ] + }, + { + "cell_type": "markdown", + "id": "7170222f-2cd5-4482-8044-e126d8d53797", + "metadata": {}, + "source": [ + "We will specify the target hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a152031e-d4b6-468c-8ea4-441fe1c3c948", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "service.backends()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f07a360-adf5-467b-ac97-d466c0907dc6", + "metadata": {}, + "outputs": [], + "source": [ + "# You can specify the device\n", + "# backend = service.backend('ibm_kingston')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c3072b3-69fb-4f56-9a92-6d49c5598c5e", + "metadata": {}, + "outputs": [], + "source": [ + "# You can also identify the least busy device\n", + "backend = service.least_busy(operational=True)\n", + "print(\"The least busy device is \", backend)" + ] + }, + { + "cell_type": "markdown", + "id": "f7a015bb-1813-4acf-97e5-8489711b53fb", + "metadata": {}, + "source": [ + "Transpiling the circuit is yet another complex process. Very briefly, this rewrites the circuit into a logically equivalent one using \"native gates\" (gates that a particular quantum computer can implement) and maps the qubits in your circuit to optimal real qubits on the target quantum computer. For more on transpilation, see this [documentation](/docs/api/qiskit/transpiler#overview)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fd980c2-386b-4583-b032-fa8375cb286c", + "metadata": {}, + "outputs": [], + "source": [ + "# Transpile the circuit into basis gates executable on the hardware\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "target_circuit = pm.run(qc)\n", + "\n", + "target_circuit.draw(\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "1f689582-b474-46aa-aa9f-208428db538a", + "metadata": {}, + "source": [ + "You can see that in the transpilation the circuit was rewritten using new gates. For more information, refer to the [ECRGate](/docs/api/qiskit/qiskit.circuit.library.ECRGate#ecrgate) documentation." + ] + }, + { + "cell_type": "markdown", + "id": "fdd46180-c519-4c03-a09b-382be5140306", + "metadata": {}, + "source": [ + "#### Step 3. Execute the target circuit\n", + "\n", + "Now, we will run the target circuit on the real device." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b4c27e3-8696-4246-b09d-0a4174eefeb9", + "metadata": {}, + "outputs": [], + "source": [ + "sampler = Sampler(backend)\n", + "job_real = sampler.run([target_circuit])\n", + "\n", + "job_id = job_real.job_id()\n", + "print(\"job id:\", job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "aa8c88f1-d1ec-43c8-aafe-c9ab7cbb6dc2", + "metadata": {}, + "source": [ + "Execution on the real device might require waiting in a queue, since quantum computers are valuable resources, and very much in demand. The job_id is used to check the execution status and results of the job later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "666985b8-90bb-4724-bb13-9965930a84cc", + "metadata": {}, + "outputs": [], + "source": [ + "# Check the job status (replace the job id below with your own)\n", + "job_real.status(job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "d568cee6-69f4-4e3a-89a2-4826720d2d60", + "metadata": {}, + "source": [ + "You can also check the job status from your IBM Quantum dashboard:https://quantum.cloud.ibm.com/workloads" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "664ce0ec-0ddb-4438-8e68-d36e8b67204f", + "metadata": {}, + "outputs": [], + "source": [ + "# If the Notebook session got disconnected you can also check your job status by running the\n", + "# following code\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "job_real = service.job(job_id) # Input your job-id between the quotations\n", + "job_real.status()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b69e3456-6047-436a-8276-13ca6a69188c", + "metadata": {}, + "outputs": [], + "source": [ + "# Execute after job has successfully run\n", + "result_real = job_real.result()\n", + "print(result_real[0].data.c.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "554b7575-413e-4957-83db-9131a0b4bdaf", + "metadata": {}, + "source": [ + "#### Step 4. Post-process the results\n", + "\n", + "Finally, we must post-process our results to create outputs in the expected format, like values or graphs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e22b1077-8311-4887-a7b3-c81006ba4eff", + "metadata": {}, + "outputs": [], + "source": [ + "plot_histogram(result_real[0].data.c.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "240c2033-9f06-4f58-9075-5da1aa0f2dc5", + "metadata": {}, + "source": [ + "As you can see, $|00\\rangle$ and $|11\\rangle$ are the most frequently observed. There are a few results other than the expected data, and they are due to noise and qubit decoherence. We will learn more about errors and noise in quantum computers in the later lessons of this course." + ] + }, + { + "cell_type": "markdown", + "id": "8dfaf680-c957-4250-bd40-b39d60afd38e", + "metadata": {}, + "source": [ + "### 4.4 GHZ state\n", + "\n", + "The concept of entanglement can be extended to systems of more than two qubits. The GHZ state (Greenberger-Horne-Zeilinger state) is a maximally entangled state of three or more qubits. The GHZ state for three qubits is defined as\n", + "\n", + "$$\n", + "\\frac{1}{\\sqrt 2}(|000\\rangle + |111\\rangle)\n", + "$$\n", + "\n", + "It can be created with the following quantum circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "459b6162-3a54-41fe-9b67-9c1f8e731ebc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(3, 3)\n", + "\n", + "qc.h(0)\n", + "qc.cx(0, 1)\n", + "qc.cx(1, 2)\n", + "\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "qc.measure(2, 2)\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "6fa6efe6-d4bb-42ef-a7cc-3faf4ddf7115", + "metadata": {}, + "source": [ + "The \"depth\" of a quantum circuit is a useful and common metric to describe quantum circuits. Trace a path through the quantum circuit, moving left to right, only changing qubits when they are connected by a multi-qubit gate. Count the number of gates along that path. The maximum number of gates for any such path through a circuit is the depth. In modern noisy quantum computers, low-depth circuits have fewer errors and are likely to return good results. Very deep circuits are not.\n", + "\n", + "Using `QuantumCircuit.depth()`, we can check the depth of our quantum circuit. The depth of the above circuit is 4. The top qubit has only three gates including the measurement. But there is a path from the top qubit down to either qubit 1 or qubit 2 which involves another CNOT gate." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "612505ab-1e11-467b-b5bf-6e18bd81256d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc.depth()" + ] + }, + { + "cell_type": "markdown", + "id": "91c157ed-0fc5-40a8-8092-341bca361da3", + "metadata": {}, + "source": [ + "### Exercise 2\n", + "\n", + "The GHZ state of an 8-qubit system is\n", + "\n", + "$$\n", + "\\frac{1}{\\sqrt 2}(|00000000\\rangle + |11111111\\rangle)\n", + "$$\n", + "\n", + "\n", + "Write code to prepare this state with the shallowest possible circuit. The depth of the shallowest quantum circuit is 5, including the measurement gates.\n", + "\n", + "__Solution__:" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "153eefc8-f021-41ee-97c4-a9555370f197", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1\n", + "qc = QuantumCircuit(8, 8)\n", + "\n", + "##your code goes here##\n", + "qc.h(0)\n", + "qc.cx(0, 4)\n", + "qc.cx(4, 6)\n", + "qc.cx(6, 7)\n", + "\n", + "qc.cx(4, 5)\n", + "\n", + "qc.cx(0, 2)\n", + "qc.cx(2, 3)\n", + "\n", + "qc.cx(0, 1)\n", + "qc.barrier() # for visual separation\n", + "\n", + "# measure\n", + "for i in range(8):\n", + " qc.measure(i, i)\n", + "\n", + "qc.draw(\"mpl\")\n", + "# print(qc.depth())" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "0d44521b-e08b-4445-a6ed-6ec2ac550142", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n" + ] + } + ], + "source": [ + "print(qc.depth())" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "0daedbeb-c75c-4bcf-b6e0-1f9fa0403b0d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'11111111': 535, '00000000': 489}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "# Step 2\n", + "# For this exercise, the circuit and operators are simple, so no optimizations are needed.\n", + "\n", + "# Step 3\n", + "# Run the circuit on a simulator to get the results\n", + "backend = AerSimulator()\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc], shots=1024)\n", + "result = job.result()\n", + "\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Step 4\n", + "# Plot the counts in a histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "d4057c5b-2dab-4590-afda-89d8cd7d26d5", + "metadata": {}, + "source": [ + "## 5. Summary\n", + "\n", + "You have learned quantum computation with the circuit model using quantum bits and gates, and you have reviewed superposition, measurement, and entanglement. You also have learned the method to execute the quantum circuit on the real quantum device.\n", + "\n", + "In the final exercise to create a GHZ circuit, you have tried to reduce the circuit depth, which is an important factor for obtaining a utility scale solution in a noisy quantum computer. In the later lessons of this course, you will learn about noise and about error mitigation methods in detail. In this lesson, as an introduction, we considered reducing the circuit depth in an ideal device, but in reality, we must consider the constraints of real device, such as qubit connectivity. You will learn more about this in subsequent lessons in this course." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "eb838405-b298-4a81-ab23-9d0744090b9c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the version of Qiskit\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/error-mitigation.ipynb b/learning/courses/utility-scale-quantum-computing/error-mitigation.ipynb index 5b92b7718ef..647030db939 100644 --- a/learning/courses/utility-scale-quantum-computing/error-mitigation.ipynb +++ b/learning/courses/utility-scale-quantum-computing/error-mitigation.ipynb @@ -1,973 +1,973 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "fad91a68", - "metadata": {}, - "source": [ - "---\n", - "title: Error mitigation\n", - "description: Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers.\n", - "---\n", - "\n", - "\n", - " # Quantum noise and error mitigation\n", - "\n", - "\n", - "\n", - "\n", - "Toshinari Itoko (28 June 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/a0zgies7bh91hm2lwev9o0bfeybxc6n6.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 1 m 40 s.*\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "e936adb0-bc31-41a8-b988-02ce7e68b902", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers. We will begin by looking at the effects of noise using a simulator that can simulate noise in a few ways, including using noise profiles from real quantum computers. Then we will move on to real quantum computers, in which noise is inherent. We will look at the effects of error mitigation, including combinations of things like zero-noise extrapolation (ZNE) and gate-twirling.\n", - "\n", - "We will start by loading some packages." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "38cf024b", - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install qiskit qiskit_aer qiskit_ibm_runtime\n", - "# !pip install jupyter\n", - "# !pip install matplotlib pylatexenc" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "897008ea", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "486b4d35", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.17.1'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_aer\n", - "\n", - "qiskit_aer.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "994c44e5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.40.1'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_ibm_runtime\n", - "\n", - "qiskit_ibm_runtime.__version__" - ] - }, - { - "cell_type": "markdown", - "id": "ee48a826-fda3-4fb4-b50b-d69c38eb887f", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "## 2. Noisy simulation without error mitigation\n", - "\n", - "\n", - "Qiskit Aer is a classical simulator for quantum computing. It can simulate not only ideal execution but also noisy execution of quantum circuits. This notebook demonstrates how to run noisy simulation using Qiskit Aer:\n", - "\n", - "1. Build a noise model\n", - "2. Build a noisy sampler (simulator) with the noise model\n", - "3. Run a quantum circuit on the noisy sampler\n", - "\n", - "```\n", - "noise_model = NoiseModel()\n", - "...\n", - "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})\n", - "job = noisy_sampler.run([circuit])\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "d1490ab8", - "metadata": {}, - "source": [ - "### 2.1 Build a test circuit\n", - "\n", - "We consider toy 1-qubit circuits which just repeat X gates `d` times (`d`=0 ... 100) and measure the `Z` observable." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b4863c66", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "\n", - "MAX_DEPTH = 100\n", - "circuits = []\n", - "for d in range(MAX_DEPTH + 1):\n", - " circ = QuantumCircuit(1)\n", - " for _ in range(d):\n", - " circ.x(0)\n", - " circ.barrier(0)\n", - " circ.measure_all()\n", - " circuits.append(circ)\n", - "\n", - "display(circuits[3].draw(output=\"mpl\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a366502c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SparsePauliOp(['Z'],\n", - " coeffs=[1.+0.j])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "obs = SparsePauliOp.from_list([(\"Z\", 1.0)])\n", - "obs" - ] - }, - { - "cell_type": "markdown", - "id": "6200c2ed", - "metadata": {}, - "source": [ - "### 2.2 Build a noise model\n", - "\n", - "To do noisy simulation, we need to specify `NoiseModel`. We show how to build `NoiseModel` in this section." - ] - }, - { - "cell_type": "markdown", - "id": "dae0c070", - "metadata": {}, - "source": [ - "We first need to define quantum (or readout) errors to add to a noise model." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1e0f1c12", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.noise.errors import (\n", - " coherent_unitary_error,\n", - " amplitude_damping_error,\n", - " ReadoutError,\n", - ")\n", - "from qiskit.circuit.library import RXGate\n", - "\n", - "# Coherent (unitary) error: Over X-rotation error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.coherent_unitary_error.html#qiskit_aer.noise.coherent_unitary_error\n", - "OVER_ROTATION_ANGLE = 0.05\n", - "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", - "\n", - "# Incoherent error: Amplitude dumping error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.amplitude_damping_error.html#qiskit_aer.noise.amplitude_damping_error\n", - "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", - "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", - "\n", - "# Readout (measurement) error: Readout error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.ReadoutError.html#qiskit_aer.noise.ReadoutError\n", - "PREP0_MEAS1 = 0.03 # P(1|0): Probability of preparing 0 and measuring 1\n", - "PREP1_MEAS0 = 0.08 # P(0|1): Probability of preparing 1 and measuring 0\n", - "readout_error = ReadoutError(\n", - " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e23c26ba", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "noise_model = NoiseModel()\n", - "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", - "noise_model.add_readout_error(readout_error, (0,))" - ] - }, - { - "cell_type": "markdown", - "id": "5a786224", - "metadata": {}, - "source": [ - "### 2.3 Build a noisy sampler with the noise model" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f8aded6f", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.primitives import SamplerV2 as Sampler\n", - "\n", - "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})" - ] - }, - { - "cell_type": "markdown", - "id": "922ba1ac", - "metadata": {}, - "source": [ - "### 2.4 Run quantum circuits on the noisy sampler" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "4ae504e7", - "metadata": {}, - "outputs": [], - "source": [ - "job = noisy_sampler.run(circuits, shots=400)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "3b2fff25", - "metadata": {}, - "outputs": [], - "source": [ - "result = job.result()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "4dc12337", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'0': 389, '1': 11}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "7e654c5e", - "metadata": {}, - "source": [ - "### 2.5 Plot results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a25e394", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.title(\"Noisy simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " color=\"gray\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.scatter(ds, [result[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Circuit depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "09ef68bb", - "metadata": {}, - "source": [ - "### 2.6 Ideal simulation" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "041abc81", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ideal_sampler = Sampler()\n", - "job_ideal = ideal_sampler.run(circuits)\n", - "result_ideal = job_ideal.result()\n", - "plt.title(\"Ideal simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " color=\"gray\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.scatter(\n", - " ds, [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\"\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.xlabel(\"Circuit depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "a1e1b6e6", - "metadata": {}, - "source": [ - "### 2.7 Exercise\n", - "\n", - "By tweaking the code below,\n", - "- [ ] Try 25x number of shots (= 10_000 shots) and ensure that a smoother plot is obtained\n", - "- [ ] Change noise parameters (OVER_ROTATION_ANGLE, AMPLITUDE_DAMPING_PARAM, PREP0_MEAS1, or PREP1_MEAS0) and see how the plot changes" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "502b9cfe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "OVER_ROTATION_ANGLE = 0.05\n", - "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", - "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", - "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", - "PREP0_MEAS1 = 0.1 # P(1|0): Probability of preparing 0 and measuring 1\n", - "PREP1_MEAS0 = 0.05 # P(0|1): Probability of preparing 1 and measuring 0\n", - "readout_error = ReadoutError(\n", - " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", - ")\n", - "noise_model = NoiseModel()\n", - "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", - "noise_model.add_readout_error(readout_error, (0,))\n", - "options = {\n", - " \"backend_options\": {\"noise_model\": noise_model},\n", - "}\n", - "noisy_sampler = Sampler(options=options)\n", - "job = noisy_sampler.run(circuits, shots=400)\n", - "result = job.result()\n", - "plt.title(\"Noisy simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "3cfe1a93", - "metadata": {}, - "source": [ - "### 2.8 More realistic noisy simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8d607bb5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_aer import AerSimulator\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "real_backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "81a67f2f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "aer = AerSimulator.from_backend(real_backend)\n", - "noisy_sampler = Sampler(mode=aer)\n", - "job = noisy_sampler.run(circuits)\n", - "result = job.result()\n", - "plt.title(\"Noisy simulation with noise model from real backend\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "567d30e9", - "metadata": {}, - "source": [ - "## 3. Real quantum computation with error mitigation\n", - "\n", - "In this part, we demonstrate how to obtain error mitigated results (expectation values) using Qiskit Estimator.\n", - "We consider 6-qubit Trotterized circuits for simulating the time evolution of one dimensional Ising model and see how the error scales with respect to the number of time steps." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "2d301499", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle\n", - "backend" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "6c90b89c", - "metadata": {}, - "outputs": [], - "source": [ - "NUM_QUBITS = 6\n", - "NUM_TIME_STEPS = list(range(8))\n", - "RX_ANGLE = 0.1\n", - "RZZ_ANGLE = 0.1" - ] - }, - { - "cell_type": "markdown", - "id": "b1489739", - "metadata": {}, - "source": [ - "### 3.1 Build circuits" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "1a77956e", - "metadata": {}, - "outputs": [], - "source": [ - "# Build circuits with different number of time steps\n", - "circuits = []\n", - "for n_steps in NUM_TIME_STEPS:\n", - " circ = QuantumCircuit(NUM_QUBITS)\n", - " for i in range(n_steps):\n", - " # rx layer\n", - " for q in range(NUM_QUBITS):\n", - " circ.rx(RX_ANGLE, q)\n", - " # 1st rzz layer\n", - " for q in range(1, NUM_QUBITS - 1, 2):\n", - " circ.rzz(RZZ_ANGLE, q, q + 1)\n", - " # 2nd rzz layer\n", - " for q in range(0, NUM_QUBITS - 1, 2):\n", - " circ.rzz(RZZ_ANGLE, q, q + 1)\n", - " circ.barrier() # need not to optimize the circuit\n", - " # Uncompute stage\n", - " for i in range(n_steps):\n", - " for q in range(0, NUM_QUBITS - 1, 2):\n", - " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", - " for q in range(1, NUM_QUBITS - 1, 2):\n", - " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", - " for q in range(NUM_QUBITS):\n", - " circ.rx(-RX_ANGLE, q)\n", - " circuits.append(circ)" - ] - }, - { - "cell_type": "markdown", - "id": "94eb9062", - "metadata": {}, - "source": [ - "To know the ideal output in advance, we use compute-uncompute circuits that consist of a first stage where the original circuit $U$ is applied, and a second stage where it is reversed $U^\\dagger$.\n", - "Note that the ideal outcome of such circuits will trivially be the input state $|000000\\rangle$, which has the trivial expectation values for any Pauli observables, for example, $\\langle IIIIIZ \\rangle = 1$." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "20296b5a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Print the circuit with 2 time steps\n", - "circuits[2].draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "cde8d3ea", - "metadata": {}, - "source": [ - "Note: As shown above, the circuit with $k$ time steps will have $4k$ two-qubit gate layers." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "af0d03e9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SparsePauliOp(['IIIIIZ'],\n", - " coeffs=[1.+0.j])" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "obs = SparsePauliOp.from_sparse_list([(\"Z\", [0], 1.0)], num_qubits=NUM_QUBITS)\n", - "obs" - ] - }, - { - "cell_type": "markdown", - "id": "ebd6ea9b", - "metadata": {}, - "source": [ - "### 3.2 Transpile the circuits" - ] - }, - { - "cell_type": "markdown", - "id": "8eaa552e", - "metadata": {}, - "source": [ - "We transpile the circuits for the backend with optimization (`optimization_level=1`)." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "87b861e2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", - "isa_circuits = pm.run(circuits)\n", - "display(isa_circuits[2].draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "743ad427", - "metadata": {}, - "source": [ - "### 3.3 Execute using Estimator (with different resilience levels)\n", - "\n", - "Setting the resilienece level (`estimator.options.resilience_level`) is the easiest way to apply error mitigation when using Qiskit Estimator. Estimator supports the following resilience levels (as of 2024/06/28). See more details in the [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) guide.\n", - "\n", - "![image.png](/learning/images/courses/utility-scale-quantum-computing/error-mitigation/res_level.avif)" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "328f71f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID (rl=0): d146vcnmya70008emprg\n", - "Job ID (rl=1): d146vdnqf56g0081sva0\n", - "Job ID (rl=2): d146ven5z6q00087c61g\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import Batch\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "jobs = []\n", - "job_ids = []\n", - "with Batch(backend=backend):\n", - " for resilience_level in [0, 1, 2]:\n", - " estimator = Estimator()\n", - " estimator.options.resilience_level = resilience_level\n", - " job = estimator.run(\n", - " [(circ, obs.apply_layout(circ.layout)) for circ in isa_circuits]\n", - " )\n", - " job_ids.append(job.job_id())\n", - " print(f\"Job ID (rl={resilience_level}): {job.job_id()}\")\n", - " jobs.append(job)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "1dd804e4-0d7d-4782-9559-7b3796cab121", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DONE\n", - "DONE\n", - "DONE\n" - ] - } - ], - "source": [ - "# check job status\n", - "for job in jobs:\n", - " print(job.status())" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "d138e1e9", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "jobs = [service.job(job_id) for job_id in job_ids]" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "ea820e98", - "metadata": {}, - "outputs": [], - "source": [ - "# Get results\n", - "results = [job.result() for job in jobs]" - ] - }, - { - "cell_type": "markdown", - "id": "e3691462", - "metadata": {}, - "source": [ - "### 3.4 Plot results" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "7527976e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.title(\"Error mitigation with different resilience levels\")\n", - "labels = [\"0 (No mitigation)\", \"1 (TREX)\", \"2 (ZNE + Gate twirling)\"]\n", - "steps = NUM_TIME_STEPS\n", - "for result, label in zip(results, labels):\n", - " plt.errorbar(\n", - " x=steps,\n", - " y=[result[s].data.evs for s in steps],\n", - " yerr=[result[s].data.stds for s in steps],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - " capsize=4,\n", - " label=label,\n", - " )\n", - "plt.hlines(\n", - " 1.0, min(steps), max(steps), linestyle=\"dashed\", label=\"Ideal\", colors=\"black\"\n", - ")\n", - "plt.xlabel(\"Time steps\")\n", - "plt.ylabel(\"Mitigated \")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "e4def30b", - "metadata": {}, - "source": [ - "## 4. (Optional) Customize error mitigation options" - ] - }, - { - "cell_type": "markdown", - "id": "f099e16c", - "metadata": {}, - "source": [ - "We can customize the application of error mitigation techniques via options as shown below." - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "a22e6b8c", - "metadata": {}, - "outputs": [], - "source": [ - "# TREX\n", - "estimator.options.twirling.enable_measure = True\n", - "estimator.options.twirling.num_randomizations = \"auto\"\n", - "estimator.options.twirling.shots_per_randomization = \"auto\"\n", - "\n", - "# Gate twirling\n", - "estimator.options.twirling.enable_gates = True\n", - "# ZNE\n", - "estimator.options.resilience.zne_mitigation = True\n", - "estimator.options.resilience.zne.noise_factors = [1, 3, 5]\n", - "estimator.options.resilience.zne.extrapolator = (\"exponential\", \"linear\")\n", - "\n", - "# Dynamical decoupling\n", - "estimator.options.dynamical_decoupling.enable = True # Default: False\n", - "estimator.options.dynamical_decoupling.sequence_type = \"XX\"\n", - "\n", - "# Other options\n", - "estimator.options.default_shots = 10_000" - ] - }, - { - "cell_type": "markdown", - "id": "79dd06fb", - "metadata": {}, - "source": [ - "See the following guides and API reference for the details of error mitigation options.\n", - "- [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques)\n", - "- [Introduction to options](/docs/guides/runtime-options-overview)\n", - "- [EstimatorOptions](/docs/api/qiskit-ibm-runtime/options-estimator-options)\n", - "- [SamplerOptions](/docs/api/qiskit-ibm-runtime/options-sampler-options)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fad91a68", + "metadata": {}, + "source": [ + "---\n", + "title: Error mitigation\n", + "description: Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers.\n", + "---\n", + "\n", + "\n", + " # Quantum noise and error mitigation\n", + "\n", + "\n", + "\n", + "\n", + "Toshinari Itoko (28 June 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/a0zgies7bh91hm2lwev9o0bfeybxc6n6.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 1 m 40 s.*\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "e936adb0-bc31-41a8-b988-02ce7e68b902", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers. We will begin by looking at the effects of noise using a simulator that can simulate noise in a few ways, including using noise profiles from real quantum computers. Then we will move on to real quantum computers, in which noise is inherent. We will look at the effects of error mitigation, including combinations of things like zero-noise extrapolation (ZNE) and gate-twirling.\n", + "\n", + "We will start by loading some packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "38cf024b", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install qiskit qiskit_aer qiskit_ibm_runtime\n", + "# !pip install jupyter\n", + "# !pip install matplotlib pylatexenc" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "897008ea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "486b4d35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.17.1'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_aer\n", + "\n", + "qiskit_aer.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "994c44e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.40.1'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_ibm_runtime\n", + "\n", + "qiskit_ibm_runtime.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "ee48a826-fda3-4fb4-b50b-d69c38eb887f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## 2. Noisy simulation without error mitigation\n", + "\n", + "\n", + "Qiskit Aer is a classical simulator for quantum computing. It can simulate not only ideal execution but also noisy execution of quantum circuits. This notebook demonstrates how to run noisy simulation using Qiskit Aer:\n", + "\n", + "1. Build a noise model\n", + "2. Build a noisy sampler (simulator) with the noise model\n", + "3. Run a quantum circuit on the noisy sampler\n", + "\n", + "```\n", + "noise_model = NoiseModel()\n", + "...\n", + "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})\n", + "job = noisy_sampler.run([circuit])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d1490ab8", + "metadata": {}, + "source": [ + "### 2.1 Build a test circuit\n", + "\n", + "We consider toy 1-qubit circuits which just repeat X gates `d` times (`d`=0 ... 100) and measure the `Z` observable." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b4863c66", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "\n", + "MAX_DEPTH = 100\n", + "circuits = []\n", + "for d in range(MAX_DEPTH + 1):\n", + " circ = QuantumCircuit(1)\n", + " for _ in range(d):\n", + " circ.x(0)\n", + " circ.barrier(0)\n", + " circ.measure_all()\n", + " circuits.append(circ)\n", + "\n", + "display(circuits[3].draw(output=\"mpl\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a366502c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SparsePauliOp(['Z'],\n", + " coeffs=[1.+0.j])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "obs = SparsePauliOp.from_list([(\"Z\", 1.0)])\n", + "obs" + ] + }, + { + "cell_type": "markdown", + "id": "6200c2ed", + "metadata": {}, + "source": [ + "### 2.2 Build a noise model\n", + "\n", + "To do noisy simulation, we need to specify `NoiseModel`. We show how to build `NoiseModel` in this section." + ] + }, + { + "cell_type": "markdown", + "id": "dae0c070", + "metadata": {}, + "source": [ + "We first need to define quantum (or readout) errors to add to a noise model." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1e0f1c12", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.noise.errors import (\n", + " coherent_unitary_error,\n", + " amplitude_damping_error,\n", + " ReadoutError,\n", + ")\n", + "from qiskit.circuit.library import RXGate\n", + "\n", + "# Coherent (unitary) error: Over X-rotation error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.coherent_unitary_error.html#qiskit_aer.noise.coherent_unitary_error\n", + "OVER_ROTATION_ANGLE = 0.05\n", + "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", + "\n", + "# Incoherent error: Amplitude dumping error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.amplitude_damping_error.html#qiskit_aer.noise.amplitude_damping_error\n", + "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", + "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", + "\n", + "# Readout (measurement) error: Readout error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.ReadoutError.html#qiskit_aer.noise.ReadoutError\n", + "PREP0_MEAS1 = 0.03 # P(1|0): Probability of preparing 0 and measuring 1\n", + "PREP1_MEAS0 = 0.08 # P(0|1): Probability of preparing 1 and measuring 0\n", + "readout_error = ReadoutError(\n", + " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e23c26ba", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "noise_model = NoiseModel()\n", + "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", + "noise_model.add_readout_error(readout_error, (0,))" + ] + }, + { + "cell_type": "markdown", + "id": "5a786224", + "metadata": {}, + "source": [ + "### 2.3 Build a noisy sampler with the noise model" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f8aded6f", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.primitives import SamplerV2 as Sampler\n", + "\n", + "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})" + ] + }, + { + "cell_type": "markdown", + "id": "922ba1ac", + "metadata": {}, + "source": [ + "### 2.4 Run quantum circuits on the noisy sampler" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4ae504e7", + "metadata": {}, + "outputs": [], + "source": [ + "job = noisy_sampler.run(circuits, shots=400)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3b2fff25", + "metadata": {}, + "outputs": [], + "source": [ + "result = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4dc12337", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'0': 389, '1': 11}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "7e654c5e", + "metadata": {}, + "source": [ + "### 2.5 Plot results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a25e394", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.title(\"Noisy simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " color=\"gray\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.scatter(ds, [result[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Circuit depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "09ef68bb", + "metadata": {}, + "source": [ + "### 2.6 Ideal simulation" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "041abc81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ideal_sampler = Sampler()\n", + "job_ideal = ideal_sampler.run(circuits)\n", + "result_ideal = job_ideal.result()\n", + "plt.title(\"Ideal simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " color=\"gray\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.scatter(\n", + " ds, [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\"\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.xlabel(\"Circuit depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a1e1b6e6", + "metadata": {}, + "source": [ + "### 2.7 Exercise\n", + "\n", + "By tweaking the code below,\n", + "- [ ] Try 25x number of shots (= 10_000 shots) and ensure that a smoother plot is obtained\n", + "- [ ] Change noise parameters (OVER_ROTATION_ANGLE, AMPLITUDE_DAMPING_PARAM, PREP0_MEAS1, or PREP1_MEAS0) and see how the plot changes" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "502b9cfe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "OVER_ROTATION_ANGLE = 0.05\n", + "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", + "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", + "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", + "PREP0_MEAS1 = 0.1 # P(1|0): Probability of preparing 0 and measuring 1\n", + "PREP1_MEAS0 = 0.05 # P(0|1): Probability of preparing 1 and measuring 0\n", + "readout_error = ReadoutError(\n", + " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", + ")\n", + "noise_model = NoiseModel()\n", + "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", + "noise_model.add_readout_error(readout_error, (0,))\n", + "options = {\n", + " \"backend_options\": {\"noise_model\": noise_model},\n", + "}\n", + "noisy_sampler = Sampler(options=options)\n", + "job = noisy_sampler.run(circuits, shots=400)\n", + "result = job.result()\n", + "plt.title(\"Noisy simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3cfe1a93", + "metadata": {}, + "source": [ + "### 2.8 More realistic noisy simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d607bb5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_aer import AerSimulator\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "real_backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "81a67f2f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "aer = AerSimulator.from_backend(real_backend)\n", + "noisy_sampler = Sampler(mode=aer)\n", + "job = noisy_sampler.run(circuits)\n", + "result = job.result()\n", + "plt.title(\"Noisy simulation with noise model from real backend\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "567d30e9", + "metadata": {}, + "source": [ + "## 3. Real quantum computation with error mitigation\n", + "\n", + "In this part, we demonstrate how to obtain error mitigated results (expectation values) using Qiskit Estimator.\n", + "We consider 6-qubit Trotterized circuits for simulating the time evolution of one dimensional Ising model and see how the error scales with respect to the number of time steps." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2d301499", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle\n", + "backend" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "6c90b89c", + "metadata": {}, + "outputs": [], + "source": [ + "NUM_QUBITS = 6\n", + "NUM_TIME_STEPS = list(range(8))\n", + "RX_ANGLE = 0.1\n", + "RZZ_ANGLE = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "b1489739", + "metadata": {}, + "source": [ + "### 3.1 Build circuits" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "1a77956e", + "metadata": {}, + "outputs": [], + "source": [ + "# Build circuits with different number of time steps\n", + "circuits = []\n", + "for n_steps in NUM_TIME_STEPS:\n", + " circ = QuantumCircuit(NUM_QUBITS)\n", + " for i in range(n_steps):\n", + " # rx layer\n", + " for q in range(NUM_QUBITS):\n", + " circ.rx(RX_ANGLE, q)\n", + " # 1st rzz layer\n", + " for q in range(1, NUM_QUBITS - 1, 2):\n", + " circ.rzz(RZZ_ANGLE, q, q + 1)\n", + " # 2nd rzz layer\n", + " for q in range(0, NUM_QUBITS - 1, 2):\n", + " circ.rzz(RZZ_ANGLE, q, q + 1)\n", + " circ.barrier() # need not to optimize the circuit\n", + " # Uncompute stage\n", + " for i in range(n_steps):\n", + " for q in range(0, NUM_QUBITS - 1, 2):\n", + " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", + " for q in range(1, NUM_QUBITS - 1, 2):\n", + " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", + " for q in range(NUM_QUBITS):\n", + " circ.rx(-RX_ANGLE, q)\n", + " circuits.append(circ)" + ] + }, + { + "cell_type": "markdown", + "id": "94eb9062", + "metadata": {}, + "source": [ + "To know the ideal output in advance, we use compute-uncompute circuits that consist of a first stage where the original circuit $U$ is applied, and a second stage where it is reversed $U^\\dagger$.\n", + "Note that the ideal outcome of such circuits will trivially be the input state $|000000\\rangle$, which has the trivial expectation values for any Pauli observables, for example, $\\langle IIIIIZ \\rangle = 1$." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "20296b5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the circuit with 2 time steps\n", + "circuits[2].draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "cde8d3ea", + "metadata": {}, + "source": [ + "Note: As shown above, the circuit with $k$ time steps will have $4k$ two-qubit gate layers." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "af0d03e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SparsePauliOp(['IIIIIZ'],\n", + " coeffs=[1.+0.j])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "obs = SparsePauliOp.from_sparse_list([(\"Z\", [0], 1.0)], num_qubits=NUM_QUBITS)\n", + "obs" + ] + }, + { + "cell_type": "markdown", + "id": "ebd6ea9b", + "metadata": {}, + "source": [ + "### 3.2 Transpile the circuits" + ] + }, + { + "cell_type": "markdown", + "id": "8eaa552e", + "metadata": {}, + "source": [ + "We transpile the circuits for the backend with optimization (`optimization_level=1`)." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "87b861e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", + "isa_circuits = pm.run(circuits)\n", + "display(isa_circuits[2].draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "743ad427", + "metadata": {}, + "source": [ + "### 3.3 Execute using Estimator (with different resilience levels)\n", + "\n", + "Setting the resilienece level (`estimator.options.resilience_level`) is the easiest way to apply error mitigation when using Qiskit Estimator. Estimator supports the following resilience levels (as of 2024/06/28). See more details in the [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) guide.\n", + "\n", + "![image.png](/learning/images/courses/utility-scale-quantum-computing/error-mitigation/res_level.avif)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "328f71f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID (rl=0): d146vcnmya70008emprg\n", + "Job ID (rl=1): d146vdnqf56g0081sva0\n", + "Job ID (rl=2): d146ven5z6q00087c61g\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import Batch\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "jobs = []\n", + "job_ids = []\n", + "with Batch(backend=backend):\n", + " for resilience_level in [0, 1, 2]:\n", + " estimator = Estimator()\n", + " estimator.options.resilience_level = resilience_level\n", + " job = estimator.run(\n", + " [(circ, obs.apply_layout(circ.layout)) for circ in isa_circuits]\n", + " )\n", + " job_ids.append(job.job_id())\n", + " print(f\"Job ID (rl={resilience_level}): {job.job_id()}\")\n", + " jobs.append(job)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "1dd804e4-0d7d-4782-9559-7b3796cab121", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DONE\n", + "DONE\n", + "DONE\n" + ] + } + ], + "source": [ + "# check job status\n", + "for job in jobs:\n", + " print(job.status())" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "d138e1e9", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "jobs = [service.job(job_id) for job_id in job_ids]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "ea820e98", + "metadata": {}, + "outputs": [], + "source": [ + "# Get results\n", + "results = [job.result() for job in jobs]" + ] + }, + { + "cell_type": "markdown", + "id": "e3691462", + "metadata": {}, + "source": [ + "### 3.4 Plot results" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "7527976e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.title(\"Error mitigation with different resilience levels\")\n", + "labels = [\"0 (No mitigation)\", \"1 (TREX)\", \"2 (ZNE + Gate twirling)\"]\n", + "steps = NUM_TIME_STEPS\n", + "for result, label in zip(results, labels):\n", + " plt.errorbar(\n", + " x=steps,\n", + " y=[result[s].data.evs for s in steps],\n", + " yerr=[result[s].data.stds for s in steps],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + " capsize=4,\n", + " label=label,\n", + " )\n", + "plt.hlines(\n", + " 1.0, min(steps), max(steps), linestyle=\"dashed\", label=\"Ideal\", colors=\"black\"\n", + ")\n", + "plt.xlabel(\"Time steps\")\n", + "plt.ylabel(\"Mitigated \")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e4def30b", + "metadata": {}, + "source": [ + "## 4. (Optional) Customize error mitigation options" + ] + }, + { + "cell_type": "markdown", + "id": "f099e16c", + "metadata": {}, + "source": [ + "We can customize the application of error mitigation techniques via options as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "a22e6b8c", + "metadata": {}, + "outputs": [], + "source": [ + "# TREX\n", + "estimator.options.twirling.enable_measure = True\n", + "estimator.options.twirling.num_randomizations = \"auto\"\n", + "estimator.options.twirling.shots_per_randomization = \"auto\"\n", + "\n", + "# Gate twirling\n", + "estimator.options.twirling.enable_gates = True\n", + "# ZNE\n", + "estimator.options.resilience.zne_mitigation = True\n", + "estimator.options.resilience.zne.noise_factors = [1, 3, 5]\n", + "estimator.options.resilience.zne.extrapolator = (\"exponential\", \"linear\")\n", + "\n", + "# Dynamical decoupling\n", + "estimator.options.dynamical_decoupling.enable = True # Default: False\n", + "estimator.options.dynamical_decoupling.sequence_type = \"XX\"\n", + "\n", + "# Other options\n", + "estimator.options.default_shots = 10_000" + ] + }, + { + "cell_type": "markdown", + "id": "79dd06fb", + "metadata": {}, + "source": [ + "See the following guides and API reference for the details of error mitigation options.\n", + "- [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques)\n", + "- [Introduction to options](/docs/guides/runtime-options-overview)\n", + "- [EstimatorOptions](/docs/api/qiskit-ibm-runtime/options-estimator-options)\n", + "- [SamplerOptions](/docs/api/qiskit-ibm-runtime/options-sampler-options)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb b/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb index 55019fc30c9..8c946d3fd1d 100644 --- a/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb +++ b/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb @@ -1,1074 +1,1075 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "52b561f0-ca7b-4c48-801e-ec0323c1a9e8", - "metadata": {}, - "source": [ - "---\n", - "title: Grover algorithm\n", - "description: This notebook is the fourth in a series of modules. In this notebook, we will learn about Grover's algorithm.\n", - "---\n", - "\n", - "\n", - "# Quantum algorithms: Grover Search and applications\n", - "\n", - "\n", - "\n", - "Atsushi Matsuo (May 10, 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/3s99zptw6c7nrfzmneogs4y7ezrx5nbh.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 2 seconds.*\n", - "\n", - "\n", - "\n", - "\n", - "## 1. Introduction to Grover's algorithm\n", - "This notebook is the fourth in a series of lectures on the Path to Utility in Quantum Computing. In this notebook, we will learn about Grover's algorithm.\n", - "\n", - "Grover's algorithm is one of the most well-known quantum algorithms due to its quadratic speedup over classical search methods. In classical computing, searching an unsorted database of $N$ items requires $O(N)$ time complexity, meaning that in the worst case, one might have to examine each item individually. However, Grover's algorithm allows us to achieve this search in $O(\\sqrt{N})$ time, leveraging the principles of quantum mechanics to identify the target item more efficiently.\n", - "\n", - "The algorithm uses amplitude amplification, a process that increases the probability amplitude of the correct answer state in a quantum superposition, allowing it to be measured with higher probability. This speedup makes Grover's algorithm valuable in various applications beyond simple database search, especially when the dataset size is large. Detailed explanations of the algorithm is provided in the [Grover's algorithm notebook](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/introduction).\n", - "\n", - "\n", - "### The Basic Structure of Grover's Algorithm\n", - "\n", - "Grover's algorithm comprises four main components:\n", - "1. **Initialization**: Setting up the superposition over all possible states.\n", - "2. **Oracle**: Applying an oracle function that marks the target state by flipping its phase.\n", - "3. **Diffusion Operator**: Applying a series of operations to amplify the probability of the marked state.\n", - "\n", - "Each of these steps plays a critical role in making the algorithm work efficiently. Detailed explanations for each step are provided later." - ] - }, - { - "cell_type": "markdown", - "id": "a7e988cb-7412-4fde-9d04-a2e5ce380291", - "metadata": {}, - "source": [ - "## 2. Implementing Grover's Algorithm\n", - "\n", - "### 2.1 Preparation\n", - "Import the necessary libraries and set up the environment for running the quantum circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "3f4b286c-f36e-4b33-a7bf-84f76ee9bf50", - "metadata": {}, - "outputs": [], - "source": [ - "%config InlineBackend.figure_format = 'svg' # Makes the images look nice" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0d9aef29-e9f7-41a6-9dcc-70d4026a9c3e", - "metadata": {}, - "outputs": [], - "source": [ - "# importing Qiskit\n", - "from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister\n", - "\n", - "# import basic plot tools\n", - "from qiskit.visualization import plot_histogram" - ] - }, - { - "cell_type": "markdown", - "id": "d04e9ed4-3de7-43d7-932e-df4d5c6dd468", - "metadata": {}, - "source": [ - "### Step 1: Map problem to quantum circuits and operators\n", - "\n", - "Consider a list of 4 elements, where our goal is to identify the index of an element that meets a specific condition. For instance, we want to find the index of the element equal to 2. In this example, the quantum state $|01\\rangle$ represents the index of the element that satisfies this condition, as it points to the position where the value 2 is located." - ] - }, - { - "cell_type": "markdown", - "id": "0dcc1d65-ba42-48e1-8758-2acdb00ca588", - "metadata": {}, - "source": [ - "### Step 2: Optimize for target hardware" - ] - }, - { - "cell_type": "markdown", - "id": "78e8a990-6e61-4342-8b04-d39b5650664d", - "metadata": {}, - "source": [ - "### 1: Initialization\n", - "\n", - "In the initialization step, we create a superposition of all possible states. This is achieved by applying a Hadamard gate to each qubit in an n-qubit register, which will result in an equal superposition of $2^n$ states. Mathematically, this can be represented as:\n", - "\n", - "$$\n", - "\\frac{1}{\\sqrt{N}} \\sum_{x=0}^{N-1} |x\\rangle\n", - "$$\n", - "\n", - "where $N = 2^n$ is the total number of possible states. We also change the state of the ancilla bit to $|-\\rangle$." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "1ef67cc1-51b4-46f5-acd8-65382500234a", - "metadata": {}, - "outputs": [], - "source": [ - "def initialization(circuit):\n", - " # Initialization\n", - " n = circuit.num_qubits\n", - " # For input qubits\n", - " for qubit in range(n - 1):\n", - " circuit.h(qubit)\n", - " # For the ancilla bit\n", - " circuit.x(n - 1)\n", - " circuit.h(n - 1)\n", - " circuit.barrier()\n", - " return circuit" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "852b1b8c-688f-4d55-bcf3-91cfd7ef02c5", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "n = 2\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "initialization_circuit = QuantumCircuit(qr, anc)\n", - "\n", - "initialization(initialization_circuit)\n", - "initialization_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "60942cc5-c116-43b0-8fdd-14af9f5b7804", - "metadata": {}, - "source": [ - "### 2: Oracle\n", - "\n", - "The oracle is a key part of Grover's algorithm. It marks the target state by applying a phase shift, typically flipping the sign of the amplitude associated with that state. The oracle is often problem-specific and constructed based on the criteria for identifying the target state. In mathematical terms, the oracle applies the following transformation:\n", - "\n", - "$\n", - "f(x) =\n", - "\\begin{cases}\n", - "1, & \\text{if } x = x_{\\text{target}} \\\\\n", - "0, & \\text{otherwise}\n", - "\\end{cases}\n", - "$\n", - "\n", - "This phase flip is achieved by applying a negative sign to the amplitude of the target state via phase kickback." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "212c4ba3-284b-43e8-bdbe-a16b9bcee341", - "metadata": {}, - "outputs": [], - "source": [ - "def oracle(circuit):\n", - " circuit.x(1)\n", - " circuit.ccx(0, 1, 2)\n", - " circuit.x(1)\n", - " circuit.barrier()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "e9e331a6-d696-4b95-99ca-56bec2519e25", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "n = 2\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "oracle_circuit = QuantumCircuit(qr, anc)\n", - "\n", - "oracle(oracle_circuit)\n", - "oracle_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "38f8f3bb-e78f-45b0-b3f3-84c31bb47bb8", - "metadata": {}, - "source": [ - "### 3: Diffusion Operator\n", - "\n", - "The amplitude amplification process is what differentiates Grover's algorithm from a classical search. After the oracle has marked the target state, we apply a series of operations that increase the amplitude of this marked state, making it more likely to be observed upon measurement. This process is achieved through the **Diffusion operator**, which effectively performs an inversion about the average amplitude. The mathematical operation is as follows:\n", - "\n", - "$\n", - "D = 2|\\psi\\rangle\\langle\\psi| - I\n", - "$\n", - "\n", - "where $D$ is the diffusion operator, $I$ is the identity matrix, and $|\\psi\\rangle$ is the equal superposition state. The combination of the oracle and the diffusion operator is applied approximately $\\sqrt{N}$ times to achieve maximum probability for measuring the marked state." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8f2047d7-aae2-436a-994a-78065eb0ea6e", - "metadata": {}, - "outputs": [], - "source": [ - "def diffusion(circuit):\n", - " input_qubits = circuit.num_qubits - 1\n", - " circuit.h(range(0, input_qubits))\n", - " circuit.x(range(0, input_qubits))\n", - " circuit.h(input_qubits - 1)\n", - " circuit.mcx([i for i in range(0, input_qubits - 1)], input_qubits - 1)\n", - " circuit.h(input_qubits - 1)\n", - " circuit.x(range(0, input_qubits))\n", - " circuit.h(range(0, input_qubits))\n", - " circuit.barrier()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "42661bfd-5abd-40e7-9ca2-10356c54fc7f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "n = 2\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "diffusion_circuit = QuantumCircuit(qr, anc)\n", - "\n", - "diffusion(diffusion_circuit)\n", - "diffusion_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "247e549b-51ba-4cf0-b54a-dd1ffb4d2be2", - "metadata": {}, - "source": [ - "### 2.2 A 2-qubit Grover search example." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "ba81c65f-a1fb-46b3-aaed-308e75ecd762", - "metadata": {}, - "outputs": [], - "source": [ - "n = 2\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "meas = ClassicalRegister(3, \"meas\")\n", - "grover_circuit = QuantumCircuit(qr, anc, meas)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e7fbafd4-b787-4d8c-b740-ab4968b88267", - "metadata": {}, - "outputs": [], - "source": [ - "# the number of iterations\n", - "num_iterations = 1" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "720d3fa7-a293-45c4-8566-493618381a52", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Let's do Grover search\n", - "initialization(grover_circuit)\n", - "\n", - "for i in range(0, num_iterations):\n", - " oracle(grover_circuit)\n", - " diffusion(grover_circuit)\n", - "\n", - "# Clear the ancilla bit\n", - "grover_circuit.h(n)\n", - "grover_circuit.x(n)\n", - "grover_circuit.measure_all(add_bits=False)\n", - "\n", - "grover_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "ab23ed2a-0d1b-45b2-92e3-4f9ded5e3eff", - "metadata": {}, - "source": [ - "### 2.3 Experiment with Simulators\n", - "### Step 3: Executing the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "a8e78531-fb9d-4626-9394-6eda2ae91c52", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer import AerSimulator\n", - "from qiskit_ibm_runtime import Sampler\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(grover_circuit)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc])\n", - "result = job.result()" - ] - }, - { - "cell_type": "markdown", - "id": "d4f5af3c-fd74-4d90-8fdb-1ee9cc969513", - "metadata": {}, - "source": [ - "### Step 4: Post-processing the results." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "394fb643-f19a-47e5-be57-02a5bfe0d231", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'001': 1024}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Print the results\n", - "counts = result[0].data.meas.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "b115c94a-59c2-4378-a056-0c90cf5d681e", - "metadata": {}, - "source": [ - "We got the correct answer $|01\\rangle$. Note that be careful with the order of qubits" - ] - }, - { - "cell_type": "markdown", - "id": "673040a9-d1ac-4fd7-92d5-6592c8977375", - "metadata": {}, - "source": [ - "## 3. Experiment with Real Devices" - ] - }, - { - "cell_type": "markdown", - "id": "192b529d-647a-452e-8a51-804fd8bf7f0b", - "metadata": {}, - "source": [ - "### Step 2: Optimize for target hardware" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "899a3af2-c454-44f5-8a82-18b055b673a6", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "real_backend = service.backend(\"ENTER-QPU-NAME-HERE\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f7ce88b-af9e-4a0e-aee6-673bb6d836ca", - "metadata": {}, - "outputs": [], - "source": [ - "# You can also identify the least busy device\n", - "\n", - "real_backend = service.least_busy(simulator=False, operational=True)\n", - "print(\"The least busy device is \", real_backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "c0c698e1-0708-4715-a005-853248a79874", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Transpile the circuit into basis gates executable on the hardware\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(backend=real_backend, optimization_level=1)\n", - "target_circuit = pm.run(grover_circuit)\n", - "\n", - "target_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "f9e2539b-9f50-4349-9613-9c3cf559d18c", - "metadata": {}, - "source": [ - "By transpiling the circuit, it was converted to a circuit using the native basis gates of the device." - ] - }, - { - "cell_type": "markdown", - "id": "697b0789-6d25-4674-bf7d-f2a36676e092", - "metadata": {}, - "source": [ - "### Step 3: Executing the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "540a63fe-5983-4f3c-a887-6b1e9f2e79f9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "job id: cw69csv19rzg0080yfkg\n" - ] - } - ], - "source": [ - "sampler = Sampler(real_backend)\n", - "job_real = sampler.run([target_circuit])\n", - "job_id = job_real.job_id()\n", - "print(\"job id:\", job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "cae77939-e328-48ae-90b4-3c9efba7f7b5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'QUEUED'" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check the job status\n", - "job_real.status()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "822f8be4-884c-46e1-af3f-071668ecf023", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", - "job_real = service.job(job_id) # Input your job-id between the quotations\n", - "job_real.status()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "1c4c7fe4-52fd-49ab-ada0-85dcae87f514", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'101': 540, '001': 2253, '011': 476, '000': 251, '110': 105, '100': 100, '010': 168, '111': 203}\n" - ] - } - ], - "source": [ - "# Execute after job has successfully run\n", - "result_real = job_real.result()\n", - "print(result_real[0].data.meas.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "3006e82c-476c-4dcd-b2d2-af14b72df990", - "metadata": {}, - "source": [ - "### Step 4: Post-processing the results." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "6c425810-eaec-4d61-a76e-34583664542d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_histogram(result_real[0].data.meas.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "a2875c8d-877b-4199-990a-c4dd61a3de9f", - "metadata": {}, - "source": [ - "## 4. A 3-qubit Grover Search\n", - "\n", - "Now, let's try a 3-qubit Grover search example." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "294426ce-c2c1-4922-9d64-8404fc276415", - "metadata": {}, - "outputs": [], - "source": [ - "n = 3\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancilla\")\n", - "grover_circuit = QuantumCircuit(qr, anc)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "e9687fee-a32a-40ca-ba60-a213adaa3832", - "metadata": {}, - "outputs": [], - "source": [ - "# the number of iterations\n", - "num_iterations = 2" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "fd14dde4-15c9-4aff-b8ae-a18c2ec9d3bb", - "metadata": {}, - "outputs": [], - "source": [ - "def oracle(circuit):\n", - " circuit.mcx([0, 1, 2], 3)\n", - " circuit.barrier()" - ] - }, - { - "cell_type": "markdown", - "id": "a2ad7966-ac11-49ef-83ae-5a8e91f165f3", - "metadata": {}, - "source": [ - "This time, $|111\\rangle$ is the \"good\" state." - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "eab82e12-1450-47cf-94e0-e631c463575e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Let's do Grover search\n", - "initialization(grover_circuit)\n", - "\n", - "for i in range(0, num_iterations):\n", - " oracle(grover_circuit)\n", - " diffusion(grover_circuit)\n", - "\n", - "# Clear the ancilla bit\n", - "grover_circuit.h(n)\n", - "grover_circuit.x(n)\n", - "\n", - "\n", - "grover_circuit.draw(output=\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "21f8e416-9574-4883-b6a5-f7b38908706d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'0111': 977, '0100': 11, '0001': 8, '0000': 8, '0011': 5, '0010': 12, '0110': 3}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grover_circuit.measure_all()\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(grover_circuit)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([isa_qc], shots=1024)\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.meas.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "70ebbf9a-55f0-496b-9e4f-5b97ff514cc0", - "metadata": {}, - "source": [ - "$|0111\\rangle$ is observed with the highest probability, as expected. Note that two iterations are optimal in this case. However, the probability of obtaining the correct answer is not 100%, which is usual in Grover's search." - ] - }, - { - "cell_type": "markdown", - "id": "ae3f55ba-623f-42e7-be48-6e91c02b4f0b", - "metadata": {}, - "source": [ - "#### What happens if we iterate 3 times?\n", - "\n", - "Now, let's try iterating 3 times." - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "id": "cba0be83-8490-49d0-894d-e0c8843502c1", - "metadata": {}, - "outputs": [], - "source": [ - "n = 3\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "grover_circuit = QuantumCircuit(qr, anc)" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "fb210c9b-6385-403e-a262-5cd86ee8ef1e", - "metadata": {}, - "outputs": [], - "source": [ - "# the number of iterations\n", - "num_iterations = 3" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "31b2963c-d1af-4f7f-8e61-5c81f37e19c1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Let's do Grover search\n", - "initialization(grover_circuit)\n", - "\n", - "for i in range(0, num_iterations):\n", - " oracle(grover_circuit)\n", - " diffusion(grover_circuit)\n", - "\n", - "# Clear the ancilla bit\n", - "grover_circuit.h(n)\n", - "grover_circuit.x(n)\n", - "\n", - "\n", - "grover_circuit.draw(output=\"mpl\", idle_wires=False, fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "17361c99-50c4-4a47-8d26-4c7a97bce3bf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'0010': 88, '0001': 103, '0000': 94, '0111': 334, '0100': 112, '0110': 106, '0101': 99, '0011': 88}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 53, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grover_circuit.measure_all()\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(grover_circuit)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([isa_qc], shots=1024)\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.meas.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "0970b628-72b3-4a3a-8d7e-2b8df41cc6bc", - "metadata": {}, - "source": [ - "$|0111\\rangle$ is still observed with the highest probability, though the probability of obtaining the correct answer has slightly decreased." - ] - }, - { - "cell_type": "markdown", - "id": "cbbb18fe-0bbe-49be-a230-4228bc98f19c", - "metadata": {}, - "source": [ - "#### How about 4 times?\n", - "\n", - "Now, let's try iterating 4 times." - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "3fa91ca1-e7b0-4e56-9859-0e97070019d1", - "metadata": {}, - "outputs": [], - "source": [ - "n = 3\n", - "qr = QuantumRegister(n, \"q\")\n", - "anc = QuantumRegister(1, \"ancillary\")\n", - "grover_circuit = QuantumCircuit(qr, anc)" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "0c1d4a36-ca5e-4a96-9ea6-cbc673918d5f", - "metadata": {}, - "outputs": [], - "source": [ - "# the number of iterations\n", - "num_iterations = 4" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "7297a8bd-3ace-481e-9631-6559c468b8e4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Let's do Grover search\n", - "initialization(grover_circuit)\n", - "\n", - "for i in range(0, num_iterations):\n", - " oracle(grover_circuit)\n", - " diffusion(grover_circuit)\n", - "\n", - "# Clear the ancilla bit\n", - "grover_circuit.h(n)\n", - "grover_circuit.x(n)\n", - "\n", - "\n", - "grover_circuit.draw(output=\"mpl\", idle_wires=False, fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "65017ef5-503f-47a4-83d8-92605521329f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'0110': 127, '0000': 135, '0001': 150, '0101': 164, '0010': 153, '0011': 131, '0100': 150, '0111': 14}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grover_circuit.measure_all()\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(grover_circuit)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([isa_qc])\n", - "result = job.result()\n", - "\n", - "# Print the results\n", - "counts = result[0].data.meas.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "54c7b13c-9ded-4e33-a65e-b42876fc178d", - "metadata": {}, - "source": [ - "$|0111\\rangle$ is observed with the lowest probability, and the probability of obtaining the correct answer has decreased further.\n", - "This demonstrates the importance of choosing the optimal number of iterations for Grover's algorithm to achieve the best results." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "cfb211be-cdf6-494c-b4f0-071cf06bc00f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the version of Qiskit\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "52b561f0-ca7b-4c48-801e-ec0323c1a9e8", + "metadata": {}, + "source": [ + "---\n", + "title: Grover algorithm\n", + "description: This notebook is the fourth in a series of modules. In this notebook, we will learn about Grover's algorithm.\n", + "---\n", + "\n", + "\n", + "# Quantum algorithms: Grover Search and applications\n", + "\n", + "\n", + "\n", + "Atsushi Matsuo (May 10, 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/3s99zptw6c7nrfzmneogs4y7ezrx5nbh.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 2 seconds.*\n", + "\n", + "\n", + "\n", + "\n", + "## 1. Introduction to Grover's algorithm\n", + "This notebook is the fourth in a series of lectures on the Path to Utility in Quantum Computing. In this notebook, we will learn about Grover's algorithm.\n", + "\n", + "Grover's algorithm is one of the most well-known quantum algorithms due to its quadratic speedup over classical search methods. In classical computing, searching an unsorted database of $N$ items requires $O(N)$ time complexity, meaning that in the worst case, one might have to examine each item individually. However, Grover's algorithm allows us to achieve this search in $O(\\sqrt{N})$ time, leveraging the principles of quantum mechanics to identify the target item more efficiently.\n", + "\n", + "The algorithm uses amplitude amplification, a process that increases the probability amplitude of the correct answer state in a quantum superposition, allowing it to be measured with higher probability. This speedup makes Grover's algorithm valuable in various applications beyond simple database search, especially when the dataset size is large. Detailed explanations of the algorithm is provided in the [Grover's algorithm notebook](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/introduction).\n", + "\n", + "\n", + "### The Basic Structure of Grover's Algorithm\n", + "\n", + "Grover's algorithm comprises four main components:\n", + "1. **Initialization**: Setting up the superposition over all possible states.\n", + "2. **Oracle**: Applying an oracle function that marks the target state by flipping its phase.\n", + "3. **Diffusion Operator**: Applying a series of operations to amplify the probability of the marked state.\n", + "\n", + "Each of these steps plays a critical role in making the algorithm work efficiently. Detailed explanations for each step are provided later." + ] + }, + { + "cell_type": "markdown", + "id": "a7e988cb-7412-4fde-9d04-a2e5ce380291", + "metadata": {}, + "source": [ + "## 2. Implementing Grover's Algorithm\n", + "\n", + "### 2.1 Preparation\n", + "Import the necessary libraries and set up the environment for running the quantum circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3f4b286c-f36e-4b33-a7bf-84f76ee9bf50", + "metadata": {}, + "outputs": [], + "source": [ + "%config InlineBackend.figure_format = 'svg' # Makes the images look nice" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0d9aef29-e9f7-41a6-9dcc-70d4026a9c3e", + "metadata": {}, + "outputs": [], + "source": [ + "# importing Qiskit\n", + "from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister\n", + "\n", + "# import basic plot tools\n", + "from qiskit.visualization import plot_histogram" + ] + }, + { + "cell_type": "markdown", + "id": "d04e9ed4-3de7-43d7-932e-df4d5c6dd468", + "metadata": {}, + "source": [ + "### Step 1: Map problem to quantum circuits and operators\n", + "\n", + "Consider a list of 4 elements, where our goal is to identify the index of an element that meets a specific condition. For instance, we want to find the index of the element equal to 2. In this example, the quantum state $|01\\rangle$ represents the index of the element that satisfies this condition, as it points to the position where the value 2 is located." + ] + }, + { + "cell_type": "markdown", + "id": "0dcc1d65-ba42-48e1-8758-2acdb00ca588", + "metadata": {}, + "source": [ + "### Step 2: Optimize for target hardware" + ] + }, + { + "cell_type": "markdown", + "id": "78e8a990-6e61-4342-8b04-d39b5650664d", + "metadata": {}, + "source": [ + "### 1: Initialization\n", + "\n", + "In the initialization step, we create a superposition of all possible states. This is achieved by applying a Hadamard gate to each qubit in an n-qubit register, which will result in an equal superposition of $2^n$ states. Mathematically, this can be represented as:\n", + "\n", + "$$\n", + "\\frac{1}{\\sqrt{N}} \\sum_{x=0}^{N-1} |x\\rangle\n", + "$$\n", + "\n", + "where $N = 2^n$ is the total number of possible states. We also change the state of the ancilla bit to $|-\\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1ef67cc1-51b4-46f5-acd8-65382500234a", + "metadata": {}, + "outputs": [], + "source": [ + "def initialization(circuit):\n", + " # Initialization\n", + " n = circuit.num_qubits\n", + " # For input qubits\n", + " for qubit in range(n - 1):\n", + " circuit.h(qubit)\n", + " # For the ancilla bit\n", + " circuit.x(n - 1)\n", + " circuit.h(n - 1)\n", + " circuit.barrier()\n", + " return circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "852b1b8c-688f-4d55-bcf3-91cfd7ef02c5", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 2\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "initialization_circuit = QuantumCircuit(qr, anc)\n", + "\n", + "initialization(initialization_circuit)\n", + "initialization_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "60942cc5-c116-43b0-8fdd-14af9f5b7804", + "metadata": {}, + "source": [ + "### 2: Oracle\n", + "\n", + "The oracle is a key part of Grover's algorithm. It marks the target state by applying a phase shift, typically flipping the sign of the amplitude associated with that state. The oracle is often problem-specific and constructed based on the criteria for identifying the target state. In mathematical terms, the oracle applies the following transformation:\n", + "\n", + "$\n", + "f(x) =\n", + "\\begin{cases}\n", + "1, & \\text{if } x = x_{\\text{target}} \\\\\n", + "0, & \\text{otherwise}\n", + "\\end{cases}\n", + "$\n", + "\n", + "This phase flip is achieved by applying a negative sign to the amplitude of the target state via phase kickback." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "212c4ba3-284b-43e8-bdbe-a16b9bcee341", + "metadata": {}, + "outputs": [], + "source": [ + "def oracle(circuit):\n", + " circuit.x(1)\n", + " circuit.ccx(0, 1, 2)\n", + " circuit.x(1)\n", + " circuit.barrier()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e9e331a6-d696-4b95-99ca-56bec2519e25", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 2\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "oracle_circuit = QuantumCircuit(qr, anc)\n", + "\n", + "oracle(oracle_circuit)\n", + "oracle_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "38f8f3bb-e78f-45b0-b3f3-84c31bb47bb8", + "metadata": {}, + "source": [ + "### 3: Diffusion Operator\n", + "\n", + "The amplitude amplification process is what differentiates Grover's algorithm from a classical search. After the oracle has marked the target state, we apply a series of operations that increase the amplitude of this marked state, making it more likely to be observed upon measurement. This process is achieved through the **Diffusion operator**, which effectively performs an inversion about the average amplitude. The mathematical operation is as follows:\n", + "\n", + "$\n", + "D = 2|\\psi\\rangle\\langle\\psi| - I\n", + "$\n", + "\n", + "where $D$ is the diffusion operator, $I$ is the identity matrix, and $|\\psi\\rangle$ is the equal superposition state. The combination of the oracle and the diffusion operator is applied approximately $\\sqrt{N}$ times to achieve maximum probability for measuring the marked state." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8f2047d7-aae2-436a-994a-78065eb0ea6e", + "metadata": {}, + "outputs": [], + "source": [ + "def diffusion(circuit):\n", + " input_qubits = circuit.num_qubits - 1\n", + " circuit.h(range(0, input_qubits))\n", + " circuit.x(range(0, input_qubits))\n", + " circuit.h(input_qubits - 1)\n", + " circuit.mcx([i for i in range(0, input_qubits - 1)], input_qubits - 1)\n", + " circuit.h(input_qubits - 1)\n", + " circuit.x(range(0, input_qubits))\n", + " circuit.h(range(0, input_qubits))\n", + " circuit.barrier()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "42661bfd-5abd-40e7-9ca2-10356c54fc7f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 2\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "diffusion_circuit = QuantumCircuit(qr, anc)\n", + "\n", + "diffusion(diffusion_circuit)\n", + "diffusion_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "247e549b-51ba-4cf0-b54a-dd1ffb4d2be2", + "metadata": {}, + "source": [ + "### 2.2 A 2-qubit Grover search example." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ba81c65f-a1fb-46b3-aaed-308e75ecd762", + "metadata": {}, + "outputs": [], + "source": [ + "n = 2\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "meas = ClassicalRegister(3, \"meas\")\n", + "grover_circuit = QuantumCircuit(qr, anc, meas)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e7fbafd4-b787-4d8c-b740-ab4968b88267", + "metadata": {}, + "outputs": [], + "source": [ + "# the number of iterations\n", + "num_iterations = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "720d3fa7-a293-45c4-8566-493618381a52", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's do Grover search\n", + "initialization(grover_circuit)\n", + "\n", + "for i in range(0, num_iterations):\n", + " oracle(grover_circuit)\n", + " diffusion(grover_circuit)\n", + "\n", + "# Clear the ancilla bit\n", + "grover_circuit.h(n)\n", + "grover_circuit.x(n)\n", + "grover_circuit.measure_all(add_bits=False)\n", + "\n", + "grover_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "ab23ed2a-0d1b-45b2-92e3-4f9ded5e3eff", + "metadata": {}, + "source": [ + "### 2.3 Experiment with Simulators\n", + "### Step 3: Executing the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a8e78531-fb9d-4626-9394-6eda2ae91c52", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "from qiskit_ibm_runtime import Sampler\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(grover_circuit)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc])\n", + "result = job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "d4f5af3c-fd74-4d90-8fdb-1ee9cc969513", + "metadata": {}, + "source": [ + "### Step 4: Post-processing the results." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "394fb643-f19a-47e5-be57-02a5bfe0d231", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'001': 1024}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the results\n", + "counts = result[0].data.meas.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "b115c94a-59c2-4378-a056-0c90cf5d681e", + "metadata": {}, + "source": [ + "We got the correct answer $|01\\rangle$. Note that be careful with the order of qubits" + ] + }, + { + "cell_type": "markdown", + "id": "673040a9-d1ac-4fd7-92d5-6592c8977375", + "metadata": {}, + "source": [ + "## 3. Experiment with Real Devices" + ] + }, + { + "cell_type": "markdown", + "id": "192b529d-647a-452e-8a51-804fd8bf7f0b", + "metadata": {}, + "source": [ + "### Step 2: Optimize for target hardware" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "899a3af2-c454-44f5-8a82-18b055b673a6", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "real_backend = service.backend(\"ENTER-QPU-NAME-HERE\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f7ce88b-af9e-4a0e-aee6-673bb6d836ca", + "metadata": {}, + "outputs": [], + "source": [ + "# You can also identify the least busy device\n", + "\n", + "real_backend = service.least_busy(simulator=False, operational=True)\n", + "print(\"The least busy device is \", real_backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c0c698e1-0708-4715-a005-853248a79874", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Transpile the circuit into basis gates executable on the hardware\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(backend=real_backend, optimization_level=1)\n", + "target_circuit = pm.run(grover_circuit)\n", + "\n", + "target_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "f9e2539b-9f50-4349-9613-9c3cf559d18c", + "metadata": {}, + "source": [ + "By transpiling the circuit, it was converted to a circuit using the native basis gates of the device." + ] + }, + { + "cell_type": "markdown", + "id": "697b0789-6d25-4674-bf7d-f2a36676e092", + "metadata": {}, + "source": [ + "### Step 3: Executing the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "540a63fe-5983-4f3c-a887-6b1e9f2e79f9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job id: cw69csv19rzg0080yfkg\n" + ] + } + ], + "source": [ + "sampler = Sampler(real_backend)\n", + "job_real = sampler.run([target_circuit])\n", + "job_id = job_real.job_id()\n", + "print(\"job id:\", job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "cae77939-e328-48ae-90b4-3c9efba7f7b5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'QUEUED'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check the job status\n", + "job_real.status()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "822f8be4-884c-46e1-af3f-071668ecf023", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If the Notebook session got disconnected you can also check your job status by running the\n", + "# following code\n", + "job_real = service.job(job_id) # Input your job-id between the quotations\n", + "job_real.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "1c4c7fe4-52fd-49ab-ada0-85dcae87f514", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'101': 540, '001': 2253, '011': 476, '000': 251, '110': 105, '100': 100, '010': 168, '111': 203}\n" + ] + } + ], + "source": [ + "# Execute after job has successfully run\n", + "result_real = job_real.result()\n", + "print(result_real[0].data.meas.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "3006e82c-476c-4dcd-b2d2-af14b72df990", + "metadata": {}, + "source": [ + "### Step 4: Post-processing the results." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "6c425810-eaec-4d61-a76e-34583664542d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_histogram(result_real[0].data.meas.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "a2875c8d-877b-4199-990a-c4dd61a3de9f", + "metadata": {}, + "source": [ + "## 4. A 3-qubit Grover Search\n", + "\n", + "Now, let's try a 3-qubit Grover search example." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "294426ce-c2c1-4922-9d64-8404fc276415", + "metadata": {}, + "outputs": [], + "source": [ + "n = 3\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancilla\")\n", + "grover_circuit = QuantumCircuit(qr, anc)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "e9687fee-a32a-40ca-ba60-a213adaa3832", + "metadata": {}, + "outputs": [], + "source": [ + "# the number of iterations\n", + "num_iterations = 2" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "fd14dde4-15c9-4aff-b8ae-a18c2ec9d3bb", + "metadata": {}, + "outputs": [], + "source": [ + "def oracle(circuit):\n", + " circuit.mcx([0, 1, 2], 3)\n", + " circuit.barrier()" + ] + }, + { + "cell_type": "markdown", + "id": "a2ad7966-ac11-49ef-83ae-5a8e91f165f3", + "metadata": {}, + "source": [ + "This time, $|111\\rangle$ is the \"good\" state." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "eab82e12-1450-47cf-94e0-e631c463575e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's do Grover search\n", + "initialization(grover_circuit)\n", + "\n", + "for i in range(0, num_iterations):\n", + " oracle(grover_circuit)\n", + " diffusion(grover_circuit)\n", + "\n", + "# Clear the ancilla bit\n", + "grover_circuit.h(n)\n", + "grover_circuit.x(n)\n", + "\n", + "\n", + "grover_circuit.draw(output=\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "21f8e416-9574-4883-b6a5-f7b38908706d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'0111': 977, '0100': 11, '0001': 8, '0000': 8, '0011': 5, '0010': 12, '0110': 3}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grover_circuit.measure_all()\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(grover_circuit)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([isa_qc], shots=1024)\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.meas.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "70ebbf9a-55f0-496b-9e4f-5b97ff514cc0", + "metadata": {}, + "source": [ + "$|0111\\rangle$ is observed with the highest probability, as expected. Note that two iterations are optimal in this case. However, the probability of obtaining the correct answer is not 100%, which is usual in Grover's search." + ] + }, + { + "cell_type": "markdown", + "id": "ae3f55ba-623f-42e7-be48-6e91c02b4f0b", + "metadata": {}, + "source": [ + "#### What happens if we iterate 3 times?\n", + "\n", + "Now, let's try iterating 3 times." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "cba0be83-8490-49d0-894d-e0c8843502c1", + "metadata": {}, + "outputs": [], + "source": [ + "n = 3\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "grover_circuit = QuantumCircuit(qr, anc)" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "fb210c9b-6385-403e-a262-5cd86ee8ef1e", + "metadata": {}, + "outputs": [], + "source": [ + "# the number of iterations\n", + "num_iterations = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "31b2963c-d1af-4f7f-8e61-5c81f37e19c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's do Grover search\n", + "initialization(grover_circuit)\n", + "\n", + "for i in range(0, num_iterations):\n", + " oracle(grover_circuit)\n", + " diffusion(grover_circuit)\n", + "\n", + "# Clear the ancilla bit\n", + "grover_circuit.h(n)\n", + "grover_circuit.x(n)\n", + "\n", + "\n", + "grover_circuit.draw(output=\"mpl\", idle_wires=False, fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "17361c99-50c4-4a47-8d26-4c7a97bce3bf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'0010': 88, '0001': 103, '0000': 94, '0111': 334, '0100': 112, '0110': 106, '0101': 99, '0011': 88}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grover_circuit.measure_all()\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(grover_circuit)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([isa_qc], shots=1024)\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.meas.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "0970b628-72b3-4a3a-8d7e-2b8df41cc6bc", + "metadata": {}, + "source": [ + "$|0111\\rangle$ is still observed with the highest probability, though the probability of obtaining the correct answer has slightly decreased." + ] + }, + { + "cell_type": "markdown", + "id": "cbbb18fe-0bbe-49be-a230-4228bc98f19c", + "metadata": {}, + "source": [ + "#### How about 4 times?\n", + "\n", + "Now, let's try iterating 4 times." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "3fa91ca1-e7b0-4e56-9859-0e97070019d1", + "metadata": {}, + "outputs": [], + "source": [ + "n = 3\n", + "qr = QuantumRegister(n, \"q\")\n", + "anc = QuantumRegister(1, \"ancillary\")\n", + "grover_circuit = QuantumCircuit(qr, anc)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "0c1d4a36-ca5e-4a96-9ea6-cbc673918d5f", + "metadata": {}, + "outputs": [], + "source": [ + "# the number of iterations\n", + "num_iterations = 4" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "7297a8bd-3ace-481e-9631-6559c468b8e4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's do Grover search\n", + "initialization(grover_circuit)\n", + "\n", + "for i in range(0, num_iterations):\n", + " oracle(grover_circuit)\n", + " diffusion(grover_circuit)\n", + "\n", + "# Clear the ancilla bit\n", + "grover_circuit.h(n)\n", + "grover_circuit.x(n)\n", + "\n", + "\n", + "grover_circuit.draw(output=\"mpl\", idle_wires=False, fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "65017ef5-503f-47a4-83d8-92605521329f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'0110': 127, '0000': 135, '0001': 150, '0101': 164, '0010': 153, '0011': 131, '0100': 150, '0111': 14}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grover_circuit.measure_all()\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(grover_circuit)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([isa_qc])\n", + "result = job.result()\n", + "\n", + "# Print the results\n", + "counts = result[0].data.meas.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "54c7b13c-9ded-4e33-a65e-b42876fc178d", + "metadata": {}, + "source": [ + "$|0111\\rangle$ is observed with the lowest probability, and the probability of obtaining the correct answer has decreased further.\n", + "This demonstrates the importance of choosing the optimal number of iterations for Grover's algorithm to achieve the best results." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cfb211be-cdf6-494c-b4f0-071cf06bc00f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the version of Qiskit\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/hardware.ipynb b/learning/courses/utility-scale-quantum-computing/hardware.ipynb index 574ccc62b66..96ca35277ad 100644 --- a/learning/courses/utility-scale-quantum-computing/hardware.ipynb +++ b/learning/courses/utility-scale-quantum-computing/hardware.ipynb @@ -1,594 +1,595 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8fe3ca32", - "metadata": {}, - "source": [ - "---\n", - "title: Hardware\n", - "description: This lesson explores modern quantum computing hardware. It is based on a live course delivered at the University of Tokyo.\n", - "---\n", - "\n", - "\n", - "# Hardware\n", - "\n", - "\n", - "\n", - "Masao Tokunari and Tamiya Onodera (14 June 2024)\n", - "\n", - "This course is based on a live course delivered at the University of Tokyo.\n", - "\n", - "This lesson's lecture pdf was split into two parts. [Download part 1](https://ibm.ent.box.com/public/static/ruz8wf353hncenmaywjlfjilflaumnzt.zip) and [download part 2](https://ibm.ent.box.com/public/static/tg8vv00ern2bmxmm033xt9oe0fcvwamc.zip). Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "e0cf0747", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "This lesson explores modern quantum computing hardware.\n", - "\n", - "We will start by verifying some versions and importing some relevant packages." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "798d0ab4-34f5-4c64-83f0-02ef5149e6f3", - "metadata": {}, - "outputs": [], - "source": [ - "import statistics\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService" - ] - }, - { - "cell_type": "markdown", - "id": "2cc2f2a3-947f-439b-b683-911539f1e6f2", - "metadata": {}, - "source": [ - "## 2. Backend and Target\n", - "\n", - "Qiskit provides an API to obtain the information, both static and dynamic, about a quantum device. We use a Backend instance to interface with a device, which includes a Target instance, an abstract machine model that summarizes the pertinent features such as its instruction set architecture (ISA) and any properties or constraints associated with it.\n", - "Let us use these backend instances to get some of the information you see on the [Compute resources](https://quantum.cloud.ibm.com/computers) page\n", - "on IBM Quantum® Platform. First, we create a backend instance for a device of interest. In the following, we pick \"ibm_kyoto\" , \"ibm_kawasaki\" or the least busy Eagle machine. Your access to QPUs might differ; update the backend name accordingly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b92ad1bf-6ad4-420c-8a3a-5bfc488d5923", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'ibm_strasbourg'" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "service = QiskitRuntimeService()\n", - "# backend = service.backend(\"ibm_kawasaki\") # an Eagle, if you have access to ibm_kawasaki\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle\n", - "backend.name" - ] - }, - { - "cell_type": "markdown", - "id": "c082da02-fcc5-4506-b4fe-9015e99e63dc", - "metadata": {}, - "source": [ - "We start with some basic (static) information about the device." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "33c39786-d44a-47bb-8245-72eb6b97e394", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "ibm_strasbourg, 127 qubits\n", - "processor type = {'family': 'Eagle', 'revision': 3} \n", - "basis gates = ['ecr', 'id', 'rz', 'sx', 'x']\n", - "\n" - ] - } - ], - "source": [ - "print(\n", - " f\"\"\"\n", - "{backend.name}, {backend.num_qubits} qubits\n", - "processor type = {backend.processor_type}\n", - "basis gates = {backend.basis_gates}\n", - "\"\"\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "31675447-1a26-4168-a038-09cc7535e037", - "metadata": {}, - "source": [ - "### 2.1 Exercise\n", - "\n", - "Try to get the basic information about a Heron device, \"ibm_strasbourg\". Try this on your own, but code has been added below for you to check yourself." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f29afd7e-af40-4cd5-bc0e-a864663a616d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "ibm_strasbourg, 133 qubits\n", - "processor type = {'family': 'Heron', 'revision': '1'} \n", - "basis gates = ['cz', 'id', 'rz', 'sx', 'x']\n", - "\n" - ] - } - ], - "source": [ - "a_heron = service.backend(\"ibm_strasbourg\") # a Heron\n", - "\n", - "# your code here\n", - "print(\n", - " f\"\"\"\n", - "{backend.name}, {a_heron.num_qubits} qubits\n", - "processor type = {a_heron.processor_type}\n", - "basis gates = {a_heron.basis_gates}\n", - "\"\"\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "08fef064-3fc6-4c27-9288-a2e3aa7f5d08", - "metadata": {}, - "source": [ - "### 2.2 Coupling map\n", - "\n", - "We now draw the coupling map of the device. As you can see, nodes are qubits which are numbered. Edges indicate pairs to which you can directly apply the 2-qubit entangling gate. The topology is called a \"heavy-hex lattice\"." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "51b87458-a5e8-4e77-a34d-39fe425a5f01", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# This function requires that Graphviz is installed. If you need to install Graphviz you can refer to:\n", - "# https://graphviz.org/download/#executable-packages for instructions.\n", - "try:\n", - " fig = backend.coupling_map.draw()\n", - "except RuntimeError as ex:\n", - " print(ex)\n", - "fig" - ] - }, - { - "cell_type": "markdown", - "id": "881a5350-282c-4a89-b9e2-ac6bf61c6571", - "metadata": {}, - "source": [ - "## 3. Qubit properties\n", - "\n", - "The Eagle device has 127 qubits. Let us obtain the properties of some of them." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "9bcb9ce2-5ea8-487b-a7ac-a2956e8cbc34", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0: QubitProperties(t1=0.000183686508736532, t2=0.00023613944465408068, frequency=4832100227.116953)\n", - "1: QubitProperties(t1=0.00048794378526038294, t2=9.007098375327869e-05, frequency=4736264354.075363)\n", - "2: QubitProperties(t1=0.00021247781834456527, t2=7.81037910324034e-05, frequency=4859349851.150393)\n", - "3: QubitProperties(t1=0.0002936462084765663, t2=0.00011400214529510604, frequency=4679749549.503852)\n", - "4: QubitProperties(t1=0.00044229440258559125, t2=0.0003181648356339447, frequency=4845872064.050596)\n" - ] - } - ], - "source": [ - "for qn in range(backend.num_qubits):\n", - " if qn >= 5:\n", - " break\n", - " print(f\"{qn}: {backend.qubit_properties(qn)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "be601762", - "metadata": {}, - "source": [ - "Let us calculate the median of T1 times of the qubits. Compare the result to the one shown for the device on [IBM Quantum Platform.](https://quantum.cloud.ibm.com/)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e8f398b2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Median T1: 285.43 μs'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "t1s = [backend.qubit_properties(qq).t1 for qq in range(backend.num_qubits)]\n", - "f\"Median T1: {(statistics.median(t1s)*10**6):.2f} \\u03bcs\"" - ] - }, - { - "cell_type": "markdown", - "id": "01695904-2cb2-4f1d-9396-82cc94429d81", - "metadata": {}, - "source": [ - "### 3.1 Exercise\n", - "\n", - "Pease calculate the median of T2 times of the qubits. Try this on your own, but code has been added below for you to check yourself." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "49fbe7a4-3dea-442f-ae3f-e82df93d406a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Median T2: 173.10 μs'" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Your code here\n", - "\n", - "t2s = [backend.qubit_properties(qq).t2 for qq in range(backend.num_qubits)]\n", - "f\"Median T2: {(statistics.median(t2s)*10**6):.2f} \\u03bcs\"" - ] - }, - { - "cell_type": "markdown", - "id": "7bdf8873-359e-4b03-98af-ede989a4a96d", - "metadata": {}, - "source": [ - "### 3.2 Gate and readout errors\n", - "\n", - "We now turn to gate errors. To begin with, we study the data structure of the target instance. It is a dictionary whose keys are operation names." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "c9188662", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "dict_keys(['measure', 'id', 'sx', 'delay', 'x', 'for_loop', 'rz', 'if_else', 'ecr', 'reset', 'switch_case'])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "target = backend.target\n", - "target.keys()" - ] - }, - { - "cell_type": "markdown", - "id": "46e3a8ac-65dc-4973-bd72-820676727f4e", - "metadata": {}, - "source": [ - "Its values are also dictionaries. Let us look at some of the items of the value (dictionary) for the 'sx' operation." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "c9b30ede-c00f-4e18-bb72-3bfc06e6afa5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 (0,) InstructionProperties(duration=6e-08, error=0.0007401311759115297)\n", - "1 (1,) InstructionProperties(duration=6e-08, error=0.0003163759907528654)\n", - "2 (2,) InstructionProperties(duration=6e-08, error=0.0003183859004638003)\n", - "3 (3,) InstructionProperties(duration=6e-08, error=0.00042235914178831863)\n", - "4 (4,) InstructionProperties(duration=6e-08, error=0.011163151923589715)\n" - ] - } - ], - "source": [ - "for i, qq in enumerate(target[\"sx\"]):\n", - " if i >= 5:\n", - " break\n", - " print(i, qq, target[\"sx\"][qq])" - ] - }, - { - "cell_type": "markdown", - "id": "5a322b40-bbf0-4b54-a151-9ba52f7bffe1", - "metadata": {}, - "source": [ - "Let us do the same for the 'ecr' and 'measure' operations." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "f9cac843-4789-4ca3-84bd-0a4c165820a9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 (0, 14) InstructionProperties(duration=6.6e-07, error=0.01486295709788732)\n", - "1 (1, 0) InstructionProperties(duration=6.6e-07, error=0.015201590794522601)\n", - "2 (2, 1) InstructionProperties(duration=6.6e-07, error=0.00697838102630724)\n", - "3 (2, 3) InstructionProperties(duration=6.6e-07, error=0.008075067943986797)\n", - "4 (3, 4) InstructionProperties(duration=6.6e-07, error=0.0630164507876913)\n" - ] - } - ], - "source": [ - "for i, edge in enumerate(target[\"ecr\"]):\n", - " if i >= 5:\n", - " break\n", - " print(i, edge, target[\"ecr\"][edge])" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "af36138a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0 (0,) InstructionProperties(duration=1.6e-06, error=0.0078125)\n", - "1 (1,) InstructionProperties(duration=1.6e-06, error=0.155029296875)\n", - "2 (2,) InstructionProperties(duration=1.6e-06, error=0.057373046875)\n", - "3 (3,) InstructionProperties(duration=1.6e-06, error=0.02880859375)\n", - "4 (4,) InstructionProperties(duration=1.6e-06, error=0.01318359375)\n" - ] - } - ], - "source": [ - "for i, qq in enumerate(target[\"measure\"]):\n", - " if i >= 5:\n", - " break\n", - " print(i, qq, target[\"measure\"][qq])" - ] - }, - { - "cell_type": "markdown", - "id": "5d0c106a-3641-42ed-9230-7dc6dbd47252", - "metadata": {}, - "source": [ - "As you can see, the errors of readout tend to be larger than those of the 2-qubit operation, which in turn tend to be larger than the 1-qubit operation.\n", - "\n", - "Having understood the data structures, we are ready to calculate the median errors for the 'sx' and the 'ecr' gates. Again, compare the results with the ones shown for the device on the [IBM Quantum Platform.](https://quantum.cloud.ibm.com/)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "b239d726", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Median SX error: 2.277e-04'" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sx_errors = [inst_prop.error for inst_prop in target[\"sx\"].values()]\n", - "f\"Median SX error: {(statistics.median(sx_errors)):.3e}\"" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "8003f34b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Median ECR error: 6.895e-03'" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ecr_errors = [inst_prop.error for inst_prop in target[\"ecr\"].values()]\n", - "f\"Median ECR error: {(statistics.median(ecr_errors)):.3e}\"" - ] - }, - { - "cell_type": "markdown", - "id": "695ee4bd", - "metadata": {}, - "source": [ - "## 4. Appendix" - ] - }, - { - "cell_type": "markdown", - "id": "b538e8b5", - "metadata": {}, - "source": [ - "A popular feature of Qiskit is its visualization capability. It includes circuit visualizers, state and distribution visualizers, and target visualizer. You already used the first two in the previous jupyter notebooks. Let us use some capabilities of the target visualizer." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "97fead46", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_gate_map\n", - "\n", - "plot_gate_map(backend, font_size=14)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "c9e05530", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_error_map\n", - "\n", - "plot_error_map(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "22a48124-e6b4-4144-bee1-f01fa4c7ccbb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check Qiskit version\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8fe3ca32", + "metadata": {}, + "source": [ + "---\n", + "title: Hardware\n", + "description: This lesson explores modern quantum computing hardware. It is based on a live course delivered at the University of Tokyo.\n", + "---\n", + "\n", + "\n", + "# Hardware\n", + "\n", + "\n", + "\n", + "Masao Tokunari and Tamiya Onodera (14 June 2024)\n", + "\n", + "This course is based on a live course delivered at the University of Tokyo.\n", + "\n", + "This lesson's lecture pdf was split into two parts. [Download part 1](https://ibm.ent.box.com/public/static/ruz8wf353hncenmaywjlfjilflaumnzt.zip) and [download part 2](https://ibm.ent.box.com/public/static/tg8vv00ern2bmxmm033xt9oe0fcvwamc.zip). Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "e0cf0747", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "This lesson explores modern quantum computing hardware.\n", + "\n", + "We will start by verifying some versions and importing some relevant packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "798d0ab4-34f5-4c64-83f0-02ef5149e6f3", + "metadata": {}, + "outputs": [], + "source": [ + "import statistics\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService" + ] + }, + { + "cell_type": "markdown", + "id": "2cc2f2a3-947f-439b-b683-911539f1e6f2", + "metadata": {}, + "source": [ + "## 2. Backend and Target\n", + "\n", + "Qiskit provides an API to obtain the information, both static and dynamic, about a quantum device. We use a Backend instance to interface with a device, which includes a Target instance, an abstract machine model that summarizes the pertinent features such as its instruction set architecture (ISA) and any properties or constraints associated with it.\n", + "Let us use these backend instances to get some of the information you see on the [Compute resources](https://quantum.cloud.ibm.com/computers) page\n", + "on IBM Quantum® Platform. First, we create a backend instance for a device of interest. In the following, we pick \"ibm_kyoto\" , \"ibm_kawasaki\" or the least busy Eagle machine. Your access to QPUs might differ; update the backend name accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b92ad1bf-6ad4-420c-8a3a-5bfc488d5923", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'ibm_strasbourg'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "service = QiskitRuntimeService()\n", + "# backend = service.backend(\"ibm_kawasaki\") # an Eagle, if you have access to ibm_kawasaki\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle\n", + "backend.name" + ] + }, + { + "cell_type": "markdown", + "id": "c082da02-fcc5-4506-b4fe-9015e99e63dc", + "metadata": {}, + "source": [ + "We start with some basic (static) information about the device." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "33c39786-d44a-47bb-8245-72eb6b97e394", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "ibm_strasbourg, 127 qubits\n", + "processor type = {'family': 'Eagle', 'revision': 3} \n", + "basis gates = ['ecr', 'id', 'rz', 'sx', 'x']\n", + "\n" + ] + } + ], + "source": [ + "print(\n", + " f\"\"\"\n", + "{backend.name}, {backend.num_qubits} qubits\n", + "processor type = {backend.processor_type}\n", + "basis gates = {backend.basis_gates}\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "31675447-1a26-4168-a038-09cc7535e037", + "metadata": {}, + "source": [ + "### 2.1 Exercise\n", + "\n", + "Try to get the basic information about a Heron device, \"ibm_strasbourg\". Try this on your own, but code has been added below for you to check yourself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f29afd7e-af40-4cd5-bc0e-a864663a616d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "ibm_strasbourg, 133 qubits\n", + "processor type = {'family': 'Heron', 'revision': '1'} \n", + "basis gates = ['cz', 'id', 'rz', 'sx', 'x']\n", + "\n" + ] + } + ], + "source": [ + "a_heron = service.backend(\"ibm_strasbourg\") # a Heron\n", + "\n", + "# your code here\n", + "print(\n", + " f\"\"\"\n", + "{backend.name}, {a_heron.num_qubits} qubits\n", + "processor type = {a_heron.processor_type}\n", + "basis gates = {a_heron.basis_gates}\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "08fef064-3fc6-4c27-9288-a2e3aa7f5d08", + "metadata": {}, + "source": [ + "### 2.2 Coupling map\n", + "\n", + "We now draw the coupling map of the device. As you can see, nodes are qubits which are numbered. Edges indicate pairs to which you can directly apply the 2-qubit entangling gate. The topology is called a \"heavy-hex lattice\"." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "51b87458-a5e8-4e77-a34d-39fe425a5f01", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This function requires that Graphviz is installed. If you need to install Graphviz you can refer\n", + "# to:\n", + "# https://graphviz.org/download/#executable-packages for instructions.\n", + "try:\n", + " fig = backend.coupling_map.draw()\n", + "except RuntimeError as ex:\n", + " print(ex)\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "id": "881a5350-282c-4a89-b9e2-ac6bf61c6571", + "metadata": {}, + "source": [ + "## 3. Qubit properties\n", + "\n", + "The Eagle device has 127 qubits. Let us obtain the properties of some of them." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9bcb9ce2-5ea8-487b-a7ac-a2956e8cbc34", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0: QubitProperties(t1=0.000183686508736532, t2=0.00023613944465408068, frequency=4832100227.116953)\n", + "1: QubitProperties(t1=0.00048794378526038294, t2=9.007098375327869e-05, frequency=4736264354.075363)\n", + "2: QubitProperties(t1=0.00021247781834456527, t2=7.81037910324034e-05, frequency=4859349851.150393)\n", + "3: QubitProperties(t1=0.0002936462084765663, t2=0.00011400214529510604, frequency=4679749549.503852)\n", + "4: QubitProperties(t1=0.00044229440258559125, t2=0.0003181648356339447, frequency=4845872064.050596)\n" + ] + } + ], + "source": [ + "for qn in range(backend.num_qubits):\n", + " if qn >= 5:\n", + " break\n", + " print(f\"{qn}: {backend.qubit_properties(qn)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "be601762", + "metadata": {}, + "source": [ + "Let us calculate the median of T1 times of the qubits. Compare the result to the one shown for the device on [IBM Quantum Platform.](https://quantum.cloud.ibm.com/)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e8f398b2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Median T1: 285.43 μs'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t1s = [backend.qubit_properties(qq).t1 for qq in range(backend.num_qubits)]\n", + "f\"Median T1: {(statistics.median(t1s)*10**6):.2f} \\u03bcs\"" + ] + }, + { + "cell_type": "markdown", + "id": "01695904-2cb2-4f1d-9396-82cc94429d81", + "metadata": {}, + "source": [ + "### 3.1 Exercise\n", + "\n", + "Pease calculate the median of T2 times of the qubits. Try this on your own, but code has been added below for you to check yourself." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "49fbe7a4-3dea-442f-ae3f-e82df93d406a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Median T2: 173.10 μs'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Your code here\n", + "\n", + "t2s = [backend.qubit_properties(qq).t2 for qq in range(backend.num_qubits)]\n", + "f\"Median T2: {(statistics.median(t2s)*10**6):.2f} \\u03bcs\"" + ] + }, + { + "cell_type": "markdown", + "id": "7bdf8873-359e-4b03-98af-ede989a4a96d", + "metadata": {}, + "source": [ + "### 3.2 Gate and readout errors\n", + "\n", + "We now turn to gate errors. To begin with, we study the data structure of the target instance. It is a dictionary whose keys are operation names." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "c9188662", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['measure', 'id', 'sx', 'delay', 'x', 'for_loop', 'rz', 'if_else', 'ecr', 'reset', 'switch_case'])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "target = backend.target\n", + "target.keys()" + ] + }, + { + "cell_type": "markdown", + "id": "46e3a8ac-65dc-4973-bd72-820676727f4e", + "metadata": {}, + "source": [ + "Its values are also dictionaries. Let us look at some of the items of the value (dictionary) for the 'sx' operation." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c9b30ede-c00f-4e18-bb72-3bfc06e6afa5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 (0,) InstructionProperties(duration=6e-08, error=0.0007401311759115297)\n", + "1 (1,) InstructionProperties(duration=6e-08, error=0.0003163759907528654)\n", + "2 (2,) InstructionProperties(duration=6e-08, error=0.0003183859004638003)\n", + "3 (3,) InstructionProperties(duration=6e-08, error=0.00042235914178831863)\n", + "4 (4,) InstructionProperties(duration=6e-08, error=0.011163151923589715)\n" + ] + } + ], + "source": [ + "for i, qq in enumerate(target[\"sx\"]):\n", + " if i >= 5:\n", + " break\n", + " print(i, qq, target[\"sx\"][qq])" + ] + }, + { + "cell_type": "markdown", + "id": "5a322b40-bbf0-4b54-a151-9ba52f7bffe1", + "metadata": {}, + "source": [ + "Let us do the same for the 'ecr' and 'measure' operations." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "f9cac843-4789-4ca3-84bd-0a4c165820a9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 (0, 14) InstructionProperties(duration=6.6e-07, error=0.01486295709788732)\n", + "1 (1, 0) InstructionProperties(duration=6.6e-07, error=0.015201590794522601)\n", + "2 (2, 1) InstructionProperties(duration=6.6e-07, error=0.00697838102630724)\n", + "3 (2, 3) InstructionProperties(duration=6.6e-07, error=0.008075067943986797)\n", + "4 (3, 4) InstructionProperties(duration=6.6e-07, error=0.0630164507876913)\n" + ] + } + ], + "source": [ + "for i, edge in enumerate(target[\"ecr\"]):\n", + " if i >= 5:\n", + " break\n", + " print(i, edge, target[\"ecr\"][edge])" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "af36138a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 (0,) InstructionProperties(duration=1.6e-06, error=0.0078125)\n", + "1 (1,) InstructionProperties(duration=1.6e-06, error=0.155029296875)\n", + "2 (2,) InstructionProperties(duration=1.6e-06, error=0.057373046875)\n", + "3 (3,) InstructionProperties(duration=1.6e-06, error=0.02880859375)\n", + "4 (4,) InstructionProperties(duration=1.6e-06, error=0.01318359375)\n" + ] + } + ], + "source": [ + "for i, qq in enumerate(target[\"measure\"]):\n", + " if i >= 5:\n", + " break\n", + " print(i, qq, target[\"measure\"][qq])" + ] + }, + { + "cell_type": "markdown", + "id": "5d0c106a-3641-42ed-9230-7dc6dbd47252", + "metadata": {}, + "source": [ + "As you can see, the errors of readout tend to be larger than those of the 2-qubit operation, which in turn tend to be larger than the 1-qubit operation.\n", + "\n", + "Having understood the data structures, we are ready to calculate the median errors for the 'sx' and the 'ecr' gates. Again, compare the results with the ones shown for the device on the [IBM Quantum Platform.](https://quantum.cloud.ibm.com/)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b239d726", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Median SX error: 2.277e-04'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sx_errors = [inst_prop.error for inst_prop in target[\"sx\"].values()]\n", + "f\"Median SX error: {(statistics.median(sx_errors)):.3e}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "8003f34b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Median ECR error: 6.895e-03'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ecr_errors = [inst_prop.error for inst_prop in target[\"ecr\"].values()]\n", + "f\"Median ECR error: {(statistics.median(ecr_errors)):.3e}\"" + ] + }, + { + "cell_type": "markdown", + "id": "695ee4bd", + "metadata": {}, + "source": [ + "## 4. Appendix" + ] + }, + { + "cell_type": "markdown", + "id": "b538e8b5", + "metadata": {}, + "source": [ + "A popular feature of Qiskit is its visualization capability. It includes circuit visualizers, state and distribution visualizers, and target visualizer. You already used the first two in the previous jupyter notebooks. Let us use some capabilities of the target visualizer." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "97fead46", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_gate_map\n", + "\n", + "plot_gate_map(backend, font_size=14)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c9e05530", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_error_map\n", + "\n", + "plot_error_map(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "22a48124-e6b4-4144-bee1-f01fa4c7ccbb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check Qiskit version\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb b/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb index 2a18f355aeb..35150b22c8a 100644 --- a/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb +++ b/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb @@ -1,2157 +1,2158 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "fad91a68", - "metadata": {}, - "source": [ - "---\n", - "title: Quantum circuit optimization\n", - "description: This lesson will address several aspects of circuit optimization in quantum computing.\n", - "---\n", - "\n", - "\n", - " # Quantum circuit optimization\n", - "\n", - "\n", - "\n", - "Toshinari Itoko (21 June 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/0hvvgr1gnwx64x2ukgk04sss6sxc4zko.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 15 s.*\n", - "\n", - "(Note: Some cells of part 2 are copied from the notebook \"Qiskit Deep dive\", written by Matthew Treinish (Qiskit maintainer))\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "38cf024b", - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install 'qiskit[visualization]'\n", - "# !pip install qiskit_ibm_runtime qiskit_aer\n", - "# !pip install jupyter\n", - "# !pip install matplotlib pylatexenc pydot pillow" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "5bc656fa-6376-436e-adfc-59676edc719b", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b5d0220b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.40.1'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_ibm_runtime\n", - "\n", - "qiskit_ibm_runtime.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "1032e247", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.17.1'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_aer\n", - "\n", - "qiskit_aer.__version__" - ] - }, - { - "cell_type": "markdown", - "id": "335804e7", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "This lesson will address several aspects of circuit optimization in quantum computing. Specifically, we will see the value of circuit optimization by using optimization settings built into Qiskit. Then we will go a bit deeper and see what you can do as an expert in your particular application area to build circuits in a smart way. Finally, we will take a close look at what goes on during transpilation that helps us optimize our circuits." - ] - }, - { - "cell_type": "markdown", - "id": "187c0ab1", - "metadata": {}, - "source": [ - "## 2. Circuit optimization matters\n", - "\n", - "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b1570aa9", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.primitives import BackendSamplerV2 as Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6be6161", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime.fake_provider import FakeBrisbane\n", - "\n", - "backend = FakeBrisbane()" - ] - }, - { - "cell_type": "markdown", - "id": "9cb38fa1", - "metadata": {}, - "source": [ - "We first use a GHZ circuit naively synthesized as follows." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "262b97e5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "num_qubits = 5\n", - "\n", - "ghz_circ = QuantumCircuit(num_qubits)\n", - "ghz_circ.h(0)\n", - "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", - "ghz_circ.measure_all()\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "f5134657", - "metadata": {}, - "source": [ - "### 2.1 Optimization level\n", - "`optimization_level` has four options, from 0 to 3. The higher the optimization level, the more computational effort is spent to optimize the circuit. Level 0 performs no optimization and just does the minimal amount of work to make the circuit runnable on the selected backend. Level 3 spends the most amount if effort (and typically runtime) to try to optimize the circuit. Level 1 is the default optimization level." - ] - }, - { - "cell_type": "markdown", - "id": "ed964d46", - "metadata": {}, - "source": [ - "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", - "We see a big difference in the circuit length of transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "042d2bbc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=0:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=2:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm0 = generate_preset_pass_manager(\n", - " optimization_level=0, backend=backend, seed_transpiler=777\n", - ")\n", - "pm2 = generate_preset_pass_manager(\n", - " optimization_level=2, backend=backend, seed_transpiler=777\n", - ")\n", - "circ0 = pm0.run(ghz_circ)\n", - "circ2 = pm2.run(ghz_circ)\n", - "print(\"optimization_level=0:\")\n", - "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"optimization_level=2:\")\n", - "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "acd14f8c", - "metadata": {}, - "source": [ - "### 2.2 Exercise\n", - "Try `optimization_level=1` as well and compare the resulting circuit with the above two. Try it by modifying the code above.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6e8389e1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=1:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm1 = generate_preset_pass_manager(\n", - " optimization_level=1, backend=backend, seed_transpiler=777\n", - ")\n", - "circ1 = pm1.run(ghz_circ)\n", - "print(\"optimization_level=1:\")\n", - "display(circ1.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "641ec604", - "metadata": {}, - "source": [ - "Run on a fake backend (noisy simulation). See Appendix 1 for how to run on a real backend." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "dfb1b0ad", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076\n" - ] - } - ], - "source": [ - "# run the circuits on the fake backend (noisy simulator)\n", - "sampler = Sampler(backend=backend)\n", - "job = sampler.run([circ0, circ2], shots=10000)\n", - "print(f\"Job ID: {job.job_id()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "fde8d64b", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "unoptimized_result = result[0].data.meas.get_counts()\n", - "optimized_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "5d344bb9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "\n", - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"with optimization\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f6998d1e", - "metadata": {}, - "source": [ - "## 3. Circuit synthesis matters" - ] - }, - { - "cell_type": "markdown", - "id": "1f6208e0", - "metadata": {}, - "source": [ - "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "896dc520", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Original GHZ circuit (naive synthesis)\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "d27a9d9b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# A cleverly-synthesized GHZ circuit\n", - "ghz_circ2 = QuantumCircuit(5)\n", - "ghz_circ2.h(2)\n", - "ghz_circ2.cx(2, 1)\n", - "ghz_circ2.cx(2, 3)\n", - "ghz_circ2.cx(1, 0)\n", - "ghz_circ2.cx(3, 4)\n", - "ghz_circ2.measure_all()\n", - "ghz_circ2.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d4e16053", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "new synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# transpile both with the same optimization level 2\n", - "circ_org = pm2.run(ghz_circ)\n", - "circ_new = pm2.run(ghz_circ2)\n", - "print(\"original synthesis:\")\n", - "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"new synthesis:\")\n", - "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "da0dbc8f", - "metadata": {}, - "source": [ - "The new synthesis produces a shallower circuit. Why?\n", - "\n", - "This is because the new circuit can be laid out on linearly connected qubits, so on IBM® Brisbane's heavy-hexagon coupling graph as well, while the original circuit requires star-shaped connectivity (a degree-4 node) and hence cannot be laid out on the heavy-hex coupling graph, which has nodes at most degree 3. As a result, the original circuit requires qubit routing that adds SWAP gates, increasing the gate count.\n", - "\n", - "What we have done in the new circuit can be seen as a manual \"coupling constraint-aware\" circuit synthesis. In other words: manually solving circuit synthesis and circuit mapping at the same time." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "5f884bba", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend=backend)\n", - "job = sampler.run([circ_org, circ_new], shots=10000)\n", - "print(f\"Job ID: {job.job_id()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3af5e682", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "synthesis_org_result = result[0].data.meas.get_counts()\n", - "synthesis_new_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "80f8ef81", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [\n", - " result\n", - " for result in [\n", - " sim_result,\n", - " unoptimized_result,\n", - " synthesis_org_result,\n", - " synthesis_new_result,\n", - " ]\n", - " ],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"synthesis_org\",\n", - " \"synthesis_new\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "96746945", - "metadata": {}, - "source": [ - "In general, circuit synthesis depends on application and it's too difficult for a software to cover all possible applications. Qiskit transpiler happens to have no functions of synthesizing GHZ state preparation circuit. In such a case, manual circuit synthesis as shown above is worth considering." - ] - }, - { - "cell_type": "markdown", - "id": "73844ec5", - "metadata": {}, - "source": [ - "In this section, we look into the details of how Qiskit transpiler works using the following toy example circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f2228937", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Build a toy example circuit\n", - "from math import pi\n", - "import itertools\n", - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.circuit.library import excitation_preserving\n", - "\n", - "circuit = QuantumCircuit(4, name=\"Example circuit\")\n", - "circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))\n", - "circuit.measure_all()\n", - "\n", - "value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])\n", - "circuit.assign_parameters(\n", - " [x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True\n", - ")\n", - "circuit.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "aacd9748", - "metadata": {}, - "source": [ - "### 3.1 Draw the entire Qiskit transpilation flow" - ] - }, - { - "cell_type": "markdown", - "id": "c5e8511a", - "metadata": {}, - "source": [ - "We look into the transpiler passes (tasks) for `optimization_level=1`." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "74bd20af", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "# There is no need to read this entire image, but this outputs all the steps in the transpile() call\n", - "# for optimization level 1\n", - "pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)\n", - "pm.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "833a8bcc", - "metadata": {}, - "source": [ - "The flow consists of six stages:" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "f58a6711", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')\n" - ] - } - ], - "source": [ - "print(pm.stages)" - ] - }, - { - "cell_type": "markdown", - "id": "f34ba488-1f4a-429e-8c1c-3b623ae4826c", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "### 3.2 Draw an individual stage\n", - "\n", - "First, let's draw all the tasks (transpiler passes) done in the `init` stage." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "09b4ffbe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pm.init.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "e7e4329f", - "metadata": {}, - "source": [ - "We can run each individual stage. Let's run `init` stage for our circuit. By enabling logger, we can see the details of the run." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "a139da85-e5b4-4c7c-900f-da8a0b8a5989", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import logging\n", - "\n", - "logger = logging.getLogger()\n", - "logger.setLevel(\"INFO\")\n", - "\n", - "init_out = pm.init.run(circuit)\n", - "init_out.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "c97816d8", - "metadata": {}, - "source": [ - "### 3.3 Exercise\n", - "Draw `layout` stage passes and run the stage for the output circuit of the `init` stage (`init_out`), by modifying cells used above.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "56024db6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(pm.layout.draw())\n", - "layout_out = pm.layout.run(init_out)\n", - "layout_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "6db6618a", - "metadata": {}, - "source": [ - "Do the same thing for `translation` stage.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "fd7cec6b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(pm.translation.draw())\n", - "basis_out = pm.translation.run(layout_out)\n", - "basis_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "e3d78127-46f4-498a-958b-7c8ba107ae9d", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "Note: Any individual stage cannot always be run independently (as some of them need to carry over information from one previous stage)." - ] - }, - { - "cell_type": "markdown", - "id": "ff7370b9-6588-49c0-b82f-dbef823a973c", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "### 3.4 Optimization Stage\n", - "\n", - "The last default stage in the pipeline is optimization. After we've embedded the circuit for the target the circuit has expanded quite a bit. Most of this is due to inefficiencies in the equivalence relationships from basis translation and swap insertion. The optimization stage is used to try and minimize the size and depth of the circuit. It runs a series of passes in a ```do while``` loop until it reaches a steady output." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "f86b9045", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# pm.pre_optimization.draw()\n", - "pm.optimization.draw()" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "c2ee9c96-b595-4882-a581-1dbd28ac980e", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)\n" - ] - } - ], - "source": [ - "logger = logging.getLogger()\n", - "logger.setLevel(\"INFO\")\n", - "opt_out = pm.optimization.run(basis_out)" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "65d650b0-ec27-4b1b-a121-f1bb958b18e2", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "opt_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "24c2abce-393a-41c5-9d1d-6273ad94a707", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "source": [ - "## 4. In-depth examples\n", - "### 4.1 Two-qubit block optimization using two-qubit unitary synthesis\n", - "\n", - "For level 2 and 3, we have more passes (`Collect2qBlocks`, `ConsolidateBlocks`, `UnitarySynthesis`) for more optimization, namely two-qubit block optimization. (Compare the optimization stage flow for level 2 with that above for level 1)\n", - "\n", - "The two-qubit block optimization is composed of two steps: Collecting and consolidating 2-qubit blocks and synthesizing the 2-qubit unitary matrices." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "179b1440", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)\n", - "pm2.optimization.draw()" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "e3f9e358-cc38-46cc-b2f7-f3bb9b9b179d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.transpiler import PassManager\n", - "from qiskit.transpiler.passes import (\n", - " Collect2qBlocks,\n", - " ConsolidateBlocks,\n", - " UnitarySynthesis,\n", - ")\n", - "\n", - "# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count for that unitary\n", - "consolidate_pm = PassManager(\n", - " [\n", - " Collect2qBlocks(),\n", - " ConsolidateBlocks(target=backend.target),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "bbf4fa9a-6b49-4833-82fd-b3821f6bcb78", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(basis_out.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "\n", - "consolidated = consolidate_pm.run(basis_out)\n", - "consolidated.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "7e7e0d3b-d267-4b1c-b207-42556d1ff3f2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Synthesize unitaries\n", - "UnitarySynthesis(target=backend.target)(consolidated).draw(\n", - " \"mpl\", idle_wires=False, fold=-1\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "a5c6dcec", - "metadata": {}, - "outputs": [], - "source": [ - "logger.setLevel(\"WARNING\")" - ] - }, - { - "cell_type": "markdown", - "id": "59a51954-c737-4ee2-b1be-fee896388c83", - "metadata": {}, - "source": [ - "We saw in Part 2 that the real quantum compiler flow is not that simple and is composed of many passes (tasks). This is mainly due to the software engineering required to ensure performance for a wide range of application circuits and maintainability of the software. Qiskit transpiler would work well in most cases but if you happen to see your circuit is not well optimized by Qiskit transpiler, it would be a good opportunity to research your own application-specific circuit optimization as shown in Part 1. Transpiler technology is evolving, your R&D contribution is welcome." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "86452399", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ff499b3", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "sampler = Sampler(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "30a84ca1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "circ = QuantumCircuit(3)\n", - "circ.ccx(0, 1, 2)\n", - "circ.measure_all()\n", - "circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "f58d9cb9", - "metadata": {}, - "outputs": [ - { - "ename": "IBMInputValueError", - "evalue": "'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (/docs/guides/transpile) for instructions to transform circuits and the primitive examples (/docs/guides/sampler-examples) to see this coupled with operator transformations.'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mIBMInputValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[44]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43msampler\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcirc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# IBMInputValueError will be raised\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/sampler.py:111\u001b[39m, in \u001b[36mSamplerV2.run\u001b[39m\u001b[34m(self, pubs, shots)\u001b[39m\n\u001b[32m 107\u001b[39m coerced_pubs = [SamplerPub.coerce(pub, shots) \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs]\n\u001b[32m 109\u001b[39m validate_classical_registers(coerced_pubs)\n\u001b[32m--> \u001b[39m\u001b[32m111\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoerced_pubs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/base_primitive.py:158\u001b[39m, in \u001b[36mBasePrimitiveV2._run\u001b[39m\u001b[34m(self, pubs)\u001b[39m\n\u001b[32m 156\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs:\n\u001b[32m 157\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, \u001b[33m\"\u001b[39m\u001b[33mtarget\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_simulator(\u001b[38;5;28mself\u001b[39m._backend):\n\u001b[32m--> \u001b[39m\u001b[32m158\u001b[39m \u001b[43mvalidate_isa_circuits\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mpub\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcircuit\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_backend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtarget\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 160\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, IBMBackend):\n\u001b[32m 161\u001b[39m \u001b[38;5;28mself\u001b[39m._backend.check_faulty(pub.circuit)\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/utils/validations.py:96\u001b[39m, in \u001b[36mvalidate_isa_circuits\u001b[39m\u001b[34m(circuits, target)\u001b[39m\n\u001b[32m 94\u001b[39m message = is_isa_circuit(circuit, target)\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message:\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m IBMInputValueError(\n\u001b[32m 97\u001b[39m message\n\u001b[32m 98\u001b[39m + \u001b[33m\"\u001b[39m\u001b[33m Circuits that do not match the target hardware definition are no longer \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 99\u001b[39m \u001b[33m\"\u001b[39m\u001b[33msupported after March 4, 2024. See the transpilation documentation \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 100\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mto transform circuits and the primitive examples \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 102\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 103\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mthis coupled with operator transformations.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 104\u001b[39m )\n", - "\u001b[31mIBMInputValueError\u001b[39m: 'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions to transform circuits and the primitive examples (https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see this coupled with operator transformations.'" - ] - } - ], - "source": [ - "sampler.run([circ]) # IBMInputValueError will be raised" - ] - }, - { - "cell_type": "markdown", - "id": "2a261d51", - "metadata": {}, - "source": [ - "### 4.2 Circuit optimization matters\n", - "\n", - "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "9483b736", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "52e99762", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "270db9aa", - "metadata": {}, - "outputs": [], - "source": [ - "# backend = service.backend('ibm_brisbane')\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle\n", - "backend" - ] - }, - { - "cell_type": "markdown", - "id": "29d29a39", - "metadata": {}, - "source": [ - "We first use a GHZ circuit naively synthesized as follows." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "485b8ce6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "num_qubits = 5\n", - "\n", - "ghz_circ = QuantumCircuit(num_qubits)\n", - "ghz_circ.h(0)\n", - "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", - "ghz_circ.measure_all()\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "8eaa552e", - "metadata": {}, - "source": [ - "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", - "As you can see, there is a big difference in the circuit length of transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "87b861e2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=0:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=2:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm0 = generate_preset_pass_manager(\n", - " optimization_level=0, backend=backend, seed_transpiler=777\n", - ")\n", - "pm2 = generate_preset_pass_manager(\n", - " optimization_level=2, backend=backend, seed_transpiler=777\n", - ")\n", - "circ0 = pm0.run(ghz_circ)\n", - "circ2 = pm2.run(ghz_circ)\n", - "print(\"optimization_level=0:\")\n", - "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"optimization_level=2:\")\n", - "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "328f71f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: d13rnnemya70008ek1zg\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([circ0, circ2], shots=10000)\n", - "job_id = job.job_id()\n", - "print(f\"Job ID: {job_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "id": "d138e1e9", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "job = service.job(job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "id": "c137e06a", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "unoptimized_result = result[0].data.meas.get_counts()\n", - "optimized_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "7527976e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 60, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "\n", - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"with optimization\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d0d49ab7", - "metadata": {}, - "source": [ - "### 4.3 Circuit synthesis matters" - ] - }, - { - "cell_type": "markdown", - "id": "c6643c6d", - "metadata": {}, - "source": [ - "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "886d9b45", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 61, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Original GHZ circuit (naive synthesis)\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "id": "3b559186", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 62, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# A better GHZ circuit (smarter synthesis), you learned in a previous lecture\n", - "ghz_circ2 = QuantumCircuit(5)\n", - "ghz_circ2.h(2)\n", - "ghz_circ2.cx(2, 1)\n", - "ghz_circ2.cx(2, 3)\n", - "ghz_circ2.cx(1, 0)\n", - "ghz_circ2.cx(3, 4)\n", - "ghz_circ2.measure_all()\n", - "ghz_circ2.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "id": "054890b6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "new synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "circ_org = pm2.run(ghz_circ)\n", - "circ_new = pm2.run(ghz_circ2)\n", - "print(\"original synthesis:\")\n", - "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"new synthesis:\")\n", - "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "de0c8577", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: d13rp283grvg008j12fg\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([circ_org, circ_new], shots=10000)\n", - "job_id = job.job_id()\n", - "print(f\"Job ID: {job_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "id": "a6fcb968", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "job = service.job(job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "id": "82165302", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "synthesis_org_result = result[0].data.meas.get_counts()\n", - "synthesis_new_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "b9021da5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 68, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, synthesis_org_result, synthesis_new_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"synthesis_org\",\n", - " \"synthesis_new\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "07f7638d", - "metadata": {}, - "source": [ - "### 4.4 General 1-qubit gate decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "id": "f08c76bb", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit, transpile\n", - "from qiskit.circuit import Parameter\n", - "from qiskit.circuit.library.standard_gates import UGate\n", - "\n", - "phi, theta, lam = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\")" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "ed93f69a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "qc.append(UGate(theta, phi, lam), [0])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "2fa17bd2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "transpile(qc, basis_gates=[\"rz\", \"sx\"]).draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "d8af1753", - "metadata": {}, - "source": [ - "### 4.5 One-qubit block optimization" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "6f64d07d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 71, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "qc = QuantumCircuit(1)\n", - "qc.x(0)\n", - "qc.y(0)\n", - "qc.z(0)\n", - "qc.rx(1.23, 0)\n", - "qc.ry(1.23, 0)\n", - "qc.rz(1.23, 0)\n", - "qc.h(0)\n", - "qc.s(0)\n", - "qc.t(0)\n", - "qc.sx(0)\n", - "qc.sdg(0)\n", - "qc.tdg(0)\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "0aa1b908", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", - " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", - " input_dims=(2,), output_dims=(2,))\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import Operator\n", - "\n", - "Operator(qc)" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "c06f5e75", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 73, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import transpile\n", - "\n", - "qc_opt = transpile(qc, basis_gates=[\"rz\", \"sx\"])\n", - "qc_opt.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "a9ec0568", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", - " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", - " input_dims=(2,), output_dims=(2,))\n" - ] - } - ], - "source": [ - "Operator(qc_opt)" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "e83779af", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 75, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Operator(qc).equiv(Operator(qc_opt))" - ] - }, - { - "cell_type": "markdown", - "id": "1ebdc297", - "metadata": {}, - "source": [ - "### 4.6 Toffoli decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "f802c5df", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(3)\n", - "qc.ccx(0, 1, 2)\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "id": "330cea7e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 77, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit, transpile\n", - "\n", - "qc = QuantumCircuit(3)\n", - "qc.ccx(0, 1, 2)\n", - "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "24004df8", - "metadata": {}, - "source": [ - "### 4.7 CU gate decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "1df5876d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library.standard_gates import CUGate\n", - "\n", - "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", - "qc = QuantumCircuit(2)\n", - "# qc.cu(theta, phi, lam, gamma, 0, 1)\n", - "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "64f7e5f3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library.standard_gates import CUGate\n", - "\n", - "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", - "qc = QuantumCircuit(2)\n", - "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", - "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "88e7a028", - "metadata": {}, - "source": [ - "### 4.8 CX, ECR, CZ equal up to local Cliffords\n", - "\n", - "Note that $H$(Hadamard), $S$($\\pi/2$ Z-rotation), $S^\\dagger$($-\\pi/2$ Z-rotation), $X$(Pauli X) are all Clifford gates." - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "id": "f5b362b6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 80, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "qc.draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "id": "8740d07b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"x\", \"s\", \"h\", \"sdg\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "id": "5113a7c5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"h\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "markdown", - "id": "f19867b3", - "metadata": {}, - "source": [ - "Using IBM backend 1q basis gates \"rz\", \"sx\" and \"x\"." - ] - }, - { - "cell_type": "code", - "execution_count": 83, - "id": "9d9b54d4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 83, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "id": "c395cd24", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 84, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "id": "4eab683f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check Qiskit version\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fad91a68", + "metadata": {}, + "source": [ + "---\n", + "title: Quantum circuit optimization\n", + "description: This lesson will address several aspects of circuit optimization in quantum computing.\n", + "---\n", + "\n", + "\n", + " # Quantum circuit optimization\n", + "\n", + "\n", + "\n", + "Toshinari Itoko (21 June 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/0hvvgr1gnwx64x2ukgk04sss6sxc4zko.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 15 s.*\n", + "\n", + "(Note: Some cells of part 2 are copied from the notebook \"Qiskit Deep dive\", written by Matthew Treinish (Qiskit maintainer))\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "38cf024b", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install 'qiskit[visualization]'\n", + "# !pip install qiskit_ibm_runtime qiskit_aer\n", + "# !pip install jupyter\n", + "# !pip install matplotlib pylatexenc pydot pillow" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5bc656fa-6376-436e-adfc-59676edc719b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b5d0220b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.40.1'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_ibm_runtime\n", + "\n", + "qiskit_ibm_runtime.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1032e247", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.17.1'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_aer\n", + "\n", + "qiskit_aer.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "335804e7", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "This lesson will address several aspects of circuit optimization in quantum computing. Specifically, we will see the value of circuit optimization by using optimization settings built into Qiskit. Then we will go a bit deeper and see what you can do as an expert in your particular application area to build circuits in a smart way. Finally, we will take a close look at what goes on during transpilation that helps us optimize our circuits." + ] + }, + { + "cell_type": "markdown", + "id": "187c0ab1", + "metadata": {}, + "source": [ + "## 2. Circuit optimization matters\n", + "\n", + "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b1570aa9", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.primitives import BackendSamplerV2 as Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6be6161", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime.fake_provider import FakeBrisbane\n", + "\n", + "backend = FakeBrisbane()" + ] + }, + { + "cell_type": "markdown", + "id": "9cb38fa1", + "metadata": {}, + "source": [ + "We first use a GHZ circuit naively synthesized as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "262b97e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "num_qubits = 5\n", + "\n", + "ghz_circ = QuantumCircuit(num_qubits)\n", + "ghz_circ.h(0)\n", + "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", + "ghz_circ.measure_all()\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "f5134657", + "metadata": {}, + "source": [ + "### 2.1 Optimization level\n", + "`optimization_level` has four options, from 0 to 3. The higher the optimization level, the more computational effort is spent to optimize the circuit. Level 0 performs no optimization and just does the minimal amount of work to make the circuit runnable on the selected backend. Level 3 spends the most amount if effort (and typically runtime) to try to optimize the circuit. Level 1 is the default optimization level." + ] + }, + { + "cell_type": "markdown", + "id": "ed964d46", + "metadata": {}, + "source": [ + "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", + "We see a big difference in the circuit length of transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "042d2bbc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=0:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=2:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm0 = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, seed_transpiler=777\n", + ")\n", + "pm2 = generate_preset_pass_manager(\n", + " optimization_level=2, backend=backend, seed_transpiler=777\n", + ")\n", + "circ0 = pm0.run(ghz_circ)\n", + "circ2 = pm2.run(ghz_circ)\n", + "print(\"optimization_level=0:\")\n", + "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"optimization_level=2:\")\n", + "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "acd14f8c", + "metadata": {}, + "source": [ + "### 2.2 Exercise\n", + "Try `optimization_level=1` as well and compare the resulting circuit with the above two. Try it by modifying the code above.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6e8389e1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=1:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm1 = generate_preset_pass_manager(\n", + " optimization_level=1, backend=backend, seed_transpiler=777\n", + ")\n", + "circ1 = pm1.run(ghz_circ)\n", + "print(\"optimization_level=1:\")\n", + "display(circ1.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "641ec604", + "metadata": {}, + "source": [ + "Run on a fake backend (noisy simulation). See Appendix 1 for how to run on a real backend." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "dfb1b0ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076\n" + ] + } + ], + "source": [ + "# run the circuits on the fake backend (noisy simulator)\n", + "sampler = Sampler(backend=backend)\n", + "job = sampler.run([circ0, circ2], shots=10000)\n", + "print(f\"Job ID: {job.job_id()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "fde8d64b", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "unoptimized_result = result[0].data.meas.get_counts()\n", + "optimized_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5d344bb9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "\n", + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"with optimization\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f6998d1e", + "metadata": {}, + "source": [ + "## 3. Circuit synthesis matters" + ] + }, + { + "cell_type": "markdown", + "id": "1f6208e0", + "metadata": {}, + "source": [ + "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "896dc520", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Original GHZ circuit (naive synthesis)\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d27a9d9b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A cleverly-synthesized GHZ circuit\n", + "ghz_circ2 = QuantumCircuit(5)\n", + "ghz_circ2.h(2)\n", + "ghz_circ2.cx(2, 1)\n", + "ghz_circ2.cx(2, 3)\n", + "ghz_circ2.cx(1, 0)\n", + "ghz_circ2.cx(3, 4)\n", + "ghz_circ2.measure_all()\n", + "ghz_circ2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d4e16053", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "new synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# transpile both with the same optimization level 2\n", + "circ_org = pm2.run(ghz_circ)\n", + "circ_new = pm2.run(ghz_circ2)\n", + "print(\"original synthesis:\")\n", + "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"new synthesis:\")\n", + "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "da0dbc8f", + "metadata": {}, + "source": [ + "The new synthesis produces a shallower circuit. Why?\n", + "\n", + "This is because the new circuit can be laid out on linearly connected qubits, so on IBM® Brisbane's heavy-hexagon coupling graph as well, while the original circuit requires star-shaped connectivity (a degree-4 node) and hence cannot be laid out on the heavy-hex coupling graph, which has nodes at most degree 3. As a result, the original circuit requires qubit routing that adds SWAP gates, increasing the gate count.\n", + "\n", + "What we have done in the new circuit can be seen as a manual \"coupling constraint-aware\" circuit synthesis. In other words: manually solving circuit synthesis and circuit mapping at the same time." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "5f884bba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend=backend)\n", + "job = sampler.run([circ_org, circ_new], shots=10000)\n", + "print(f\"Job ID: {job.job_id()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3af5e682", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "synthesis_org_result = result[0].data.meas.get_counts()\n", + "synthesis_new_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "80f8ef81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [\n", + " result\n", + " for result in [\n", + " sim_result,\n", + " unoptimized_result,\n", + " synthesis_org_result,\n", + " synthesis_new_result,\n", + " ]\n", + " ],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"synthesis_org\",\n", + " \"synthesis_new\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "96746945", + "metadata": {}, + "source": [ + "In general, circuit synthesis depends on application and it's too difficult for a software to cover all possible applications. Qiskit transpiler happens to have no functions of synthesizing GHZ state preparation circuit. In such a case, manual circuit synthesis as shown above is worth considering." + ] + }, + { + "cell_type": "markdown", + "id": "73844ec5", + "metadata": {}, + "source": [ + "In this section, we look into the details of how Qiskit transpiler works using the following toy example circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2228937", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Build a toy example circuit\n", + "from math import pi\n", + "import itertools\n", + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.circuit.library import excitation_preserving\n", + "\n", + "circuit = QuantumCircuit(4, name=\"Example circuit\")\n", + "circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))\n", + "circuit.measure_all()\n", + "\n", + "value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])\n", + "circuit.assign_parameters(\n", + " [x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True\n", + ")\n", + "circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "aacd9748", + "metadata": {}, + "source": [ + "### 3.1 Draw the entire Qiskit transpilation flow" + ] + }, + { + "cell_type": "markdown", + "id": "c5e8511a", + "metadata": {}, + "source": [ + "We look into the transpiler passes (tasks) for `optimization_level=1`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "74bd20af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "# There is no need to read this entire image, but this outputs all the steps in the transpile() call\n", + "# for optimization level 1\n", + "pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)\n", + "pm.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "833a8bcc", + "metadata": {}, + "source": [ + "The flow consists of six stages:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f58a6711", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')\n" + ] + } + ], + "source": [ + "print(pm.stages)" + ] + }, + { + "cell_type": "markdown", + "id": "f34ba488-1f4a-429e-8c1c-3b623ae4826c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "### 3.2 Draw an individual stage\n", + "\n", + "First, let's draw all the tasks (transpiler passes) done in the `init` stage." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "09b4ffbe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pm.init.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "e7e4329f", + "metadata": {}, + "source": [ + "We can run each individual stage. Let's run `init` stage for our circuit. By enabling logger, we can see the details of the run." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "a139da85-e5b4-4c7c-900f-da8a0b8a5989", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import logging\n", + "\n", + "logger = logging.getLogger()\n", + "logger.setLevel(\"INFO\")\n", + "\n", + "init_out = pm.init.run(circuit)\n", + "init_out.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "c97816d8", + "metadata": {}, + "source": [ + "### 3.3 Exercise\n", + "Draw `layout` stage passes and run the stage for the output circuit of the `init` stage (`init_out`), by modifying cells used above.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "56024db6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(pm.layout.draw())\n", + "layout_out = pm.layout.run(init_out)\n", + "layout_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "6db6618a", + "metadata": {}, + "source": [ + "Do the same thing for `translation` stage.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "fd7cec6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(pm.translation.draw())\n", + "basis_out = pm.translation.run(layout_out)\n", + "basis_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "e3d78127-46f4-498a-958b-7c8ba107ae9d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "Note: Any individual stage cannot always be run independently (as some of them need to carry over information from one previous stage)." + ] + }, + { + "cell_type": "markdown", + "id": "ff7370b9-6588-49c0-b82f-dbef823a973c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "### 3.4 Optimization Stage\n", + "\n", + "The last default stage in the pipeline is optimization. After we've embedded the circuit for the target the circuit has expanded quite a bit. Most of this is due to inefficiencies in the equivalence relationships from basis translation and swap insertion. The optimization stage is used to try and minimize the size and depth of the circuit. It runs a series of passes in a ```do while``` loop until it reaches a steady output." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "f86b9045", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# pm.pre_optimization.draw()\n", + "pm.optimization.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "c2ee9c96-b595-4882-a581-1dbd28ac980e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)\n" + ] + } + ], + "source": [ + "logger = logging.getLogger()\n", + "logger.setLevel(\"INFO\")\n", + "opt_out = pm.optimization.run(basis_out)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "65d650b0-ec27-4b1b-a121-f1bb958b18e2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "opt_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "24c2abce-393a-41c5-9d1d-6273ad94a707", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "source": [ + "## 4. In-depth examples\n", + "### 4.1 Two-qubit block optimization using two-qubit unitary synthesis\n", + "\n", + "For level 2 and 3, we have more passes (`Collect2qBlocks`, `ConsolidateBlocks`, `UnitarySynthesis`) for more optimization, namely two-qubit block optimization. (Compare the optimization stage flow for level 2 with that above for level 1)\n", + "\n", + "The two-qubit block optimization is composed of two steps: Collecting and consolidating 2-qubit blocks and synthesizing the 2-qubit unitary matrices." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "179b1440", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)\n", + "pm2.optimization.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "e3f9e358-cc38-46cc-b2f7-f3bb9b9b179d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler import PassManager\n", + "from qiskit.transpiler.passes import (\n", + " Collect2qBlocks,\n", + " ConsolidateBlocks,\n", + " UnitarySynthesis,\n", + ")\n", + "\n", + "# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count\n", + "# for that unitary\n", + "consolidate_pm = PassManager(\n", + " [\n", + " Collect2qBlocks(),\n", + " ConsolidateBlocks(target=backend.target),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "bbf4fa9a-6b49-4833-82fd-b3821f6bcb78", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(basis_out.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "\n", + "consolidated = consolidate_pm.run(basis_out)\n", + "consolidated.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "7e7e0d3b-d267-4b1c-b207-42556d1ff3f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Synthesize unitaries\n", + "UnitarySynthesis(target=backend.target)(consolidated).draw(\n", + " \"mpl\", idle_wires=False, fold=-1\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "a5c6dcec", + "metadata": {}, + "outputs": [], + "source": [ + "logger.setLevel(\"WARNING\")" + ] + }, + { + "cell_type": "markdown", + "id": "59a51954-c737-4ee2-b1be-fee896388c83", + "metadata": {}, + "source": [ + "We saw in Part 2 that the real quantum compiler flow is not that simple and is composed of many passes (tasks). This is mainly due to the software engineering required to ensure performance for a wide range of application circuits and maintainability of the software. Qiskit transpiler would work well in most cases but if you happen to see your circuit is not well optimized by Qiskit transpiler, it would be a good opportunity to research your own application-specific circuit optimization as shown in Part 1. Transpiler technology is evolving, your R&D contribution is welcome." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "86452399", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ff499b3", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "sampler = Sampler(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "30a84ca1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circ = QuantumCircuit(3)\n", + "circ.ccx(0, 1, 2)\n", + "circ.measure_all()\n", + "circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "f58d9cb9", + "metadata": {}, + "outputs": [ + { + "ename": "IBMInputValueError", + "evalue": "'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (/docs/guides/transpile) for instructions to transform circuits and the primitive examples (/docs/guides/sampler-examples) to see this coupled with operator transformations.'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mIBMInputValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[44]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43msampler\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcirc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# IBMInputValueError will be raised\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/sampler.py:111\u001b[39m, in \u001b[36mSamplerV2.run\u001b[39m\u001b[34m(self, pubs, shots)\u001b[39m\n\u001b[32m 107\u001b[39m coerced_pubs = [SamplerPub.coerce(pub, shots) \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs]\n\u001b[32m 109\u001b[39m validate_classical_registers(coerced_pubs)\n\u001b[32m--> \u001b[39m\u001b[32m111\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoerced_pubs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/base_primitive.py:158\u001b[39m, in \u001b[36mBasePrimitiveV2._run\u001b[39m\u001b[34m(self, pubs)\u001b[39m\n\u001b[32m 156\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs:\n\u001b[32m 157\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, \u001b[33m\"\u001b[39m\u001b[33mtarget\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_simulator(\u001b[38;5;28mself\u001b[39m._backend):\n\u001b[32m--> \u001b[39m\u001b[32m158\u001b[39m \u001b[43mvalidate_isa_circuits\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mpub\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcircuit\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_backend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtarget\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 160\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, IBMBackend):\n\u001b[32m 161\u001b[39m \u001b[38;5;28mself\u001b[39m._backend.check_faulty(pub.circuit)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/utils/validations.py:96\u001b[39m, in \u001b[36mvalidate_isa_circuits\u001b[39m\u001b[34m(circuits, target)\u001b[39m\n\u001b[32m 94\u001b[39m message = is_isa_circuit(circuit, target)\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message:\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m IBMInputValueError(\n\u001b[32m 97\u001b[39m message\n\u001b[32m 98\u001b[39m + \u001b[33m\"\u001b[39m\u001b[33m Circuits that do not match the target hardware definition are no longer \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 99\u001b[39m \u001b[33m\"\u001b[39m\u001b[33msupported after March 4, 2024. See the transpilation documentation \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 100\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mto transform circuits and the primitive examples \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 102\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 103\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mthis coupled with operator transformations.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 104\u001b[39m )\n", + "\u001b[31mIBMInputValueError\u001b[39m: 'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions to transform circuits and the primitive examples (https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see this coupled with operator transformations.'" + ] + } + ], + "source": [ + "sampler.run([circ]) # IBMInputValueError will be raised" + ] + }, + { + "cell_type": "markdown", + "id": "2a261d51", + "metadata": {}, + "source": [ + "### 4.2 Circuit optimization matters\n", + "\n", + "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "9483b736", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52e99762", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "270db9aa", + "metadata": {}, + "outputs": [], + "source": [ + "# backend = service.backend('ibm_brisbane')\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle\n", + "backend" + ] + }, + { + "cell_type": "markdown", + "id": "29d29a39", + "metadata": {}, + "source": [ + "We first use a GHZ circuit naively synthesized as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "485b8ce6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "num_qubits = 5\n", + "\n", + "ghz_circ = QuantumCircuit(num_qubits)\n", + "ghz_circ.h(0)\n", + "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", + "ghz_circ.measure_all()\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "8eaa552e", + "metadata": {}, + "source": [ + "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", + "As you can see, there is a big difference in the circuit length of transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "87b861e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=0:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=2:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm0 = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, seed_transpiler=777\n", + ")\n", + "pm2 = generate_preset_pass_manager(\n", + " optimization_level=2, backend=backend, seed_transpiler=777\n", + ")\n", + "circ0 = pm0.run(ghz_circ)\n", + "circ2 = pm2.run(ghz_circ)\n", + "print(\"optimization_level=0:\")\n", + "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"optimization_level=2:\")\n", + "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "328f71f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: d13rnnemya70008ek1zg\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([circ0, circ2], shots=10000)\n", + "job_id = job.job_id()\n", + "print(f\"Job ID: {job_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "d138e1e9", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "job = service.job(job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "c137e06a", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "unoptimized_result = result[0].data.meas.get_counts()\n", + "optimized_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "7527976e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "\n", + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"with optimization\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d0d49ab7", + "metadata": {}, + "source": [ + "### 4.3 Circuit synthesis matters" + ] + }, + { + "cell_type": "markdown", + "id": "c6643c6d", + "metadata": {}, + "source": [ + "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "886d9b45", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Original GHZ circuit (naive synthesis)\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "3b559186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A better GHZ circuit (smarter synthesis), you learned in a previous lecture\n", + "ghz_circ2 = QuantumCircuit(5)\n", + "ghz_circ2.h(2)\n", + "ghz_circ2.cx(2, 1)\n", + "ghz_circ2.cx(2, 3)\n", + "ghz_circ2.cx(1, 0)\n", + "ghz_circ2.cx(3, 4)\n", + "ghz_circ2.measure_all()\n", + "ghz_circ2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "054890b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "new synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "circ_org = pm2.run(ghz_circ)\n", + "circ_new = pm2.run(ghz_circ2)\n", + "print(\"original synthesis:\")\n", + "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"new synthesis:\")\n", + "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "de0c8577", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: d13rp283grvg008j12fg\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([circ_org, circ_new], shots=10000)\n", + "job_id = job.job_id()\n", + "print(f\"Job ID: {job_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "a6fcb968", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "job = service.job(job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "82165302", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "synthesis_org_result = result[0].data.meas.get_counts()\n", + "synthesis_new_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "b9021da5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, synthesis_org_result, synthesis_new_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"synthesis_org\",\n", + " \"synthesis_new\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "07f7638d", + "metadata": {}, + "source": [ + "### 4.4 General 1-qubit gate decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "f08c76bb", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit, transpile\n", + "from qiskit.circuit import Parameter\n", + "from qiskit.circuit.library.standard_gates import UGate\n", + "\n", + "phi, theta, lam = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\")" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "ed93f69a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "qc.append(UGate(theta, phi, lam), [0])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "2fa17bd2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transpile(qc, basis_gates=[\"rz\", \"sx\"]).draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "d8af1753", + "metadata": {}, + "source": [ + "### 4.5 One-qubit block optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "6f64d07d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "qc = QuantumCircuit(1)\n", + "qc.x(0)\n", + "qc.y(0)\n", + "qc.z(0)\n", + "qc.rx(1.23, 0)\n", + "qc.ry(1.23, 0)\n", + "qc.rz(1.23, 0)\n", + "qc.h(0)\n", + "qc.s(0)\n", + "qc.t(0)\n", + "qc.sx(0)\n", + "qc.sdg(0)\n", + "qc.tdg(0)\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "0aa1b908", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", + " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", + " input_dims=(2,), output_dims=(2,))\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import Operator\n", + "\n", + "Operator(qc)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "c06f5e75", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import transpile\n", + "\n", + "qc_opt = transpile(qc, basis_gates=[\"rz\", \"sx\"])\n", + "qc_opt.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "a9ec0568", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", + " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", + " input_dims=(2,), output_dims=(2,))\n" + ] + } + ], + "source": [ + "Operator(qc_opt)" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "e83779af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Operator(qc).equiv(Operator(qc_opt))" + ] + }, + { + "cell_type": "markdown", + "id": "1ebdc297", + "metadata": {}, + "source": [ + "### 4.6 Toffoli decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "f802c5df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(3)\n", + "qc.ccx(0, 1, 2)\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "330cea7e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit, transpile\n", + "\n", + "qc = QuantumCircuit(3)\n", + "qc.ccx(0, 1, 2)\n", + "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "24004df8", + "metadata": {}, + "source": [ + "### 4.7 CU gate decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "1df5876d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library.standard_gates import CUGate\n", + "\n", + "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", + "qc = QuantumCircuit(2)\n", + "# qc.cu(theta, phi, lam, gamma, 0, 1)\n", + "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "64f7e5f3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library.standard_gates import CUGate\n", + "\n", + "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", + "qc = QuantumCircuit(2)\n", + "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", + "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "88e7a028", + "metadata": {}, + "source": [ + "### 4.8 CX, ECR, CZ equal up to local Cliffords\n", + "\n", + "Note that $H$(Hadamard), $S$($\\pi/2$ Z-rotation), $S^\\dagger$($-\\pi/2$ Z-rotation), $X$(Pauli X) are all Clifford gates." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "f5b362b6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "qc.draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "8740d07b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"x\", \"s\", \"h\", \"sdg\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "5113a7c5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"h\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "markdown", + "id": "f19867b3", + "metadata": {}, + "source": [ + "Using IBM backend 1q basis gates \"rz\", \"sx\" and \"x\"." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "9d9b54d4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "id": "c395cd24", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "4eab683f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check Qiskit version\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/teleportation.ipynb b/learning/courses/utility-scale-quantum-computing/teleportation.ipynb index b123ec4921a..81299bf59af 100644 --- a/learning/courses/utility-scale-quantum-computing/teleportation.ipynb +++ b/learning/courses/utility-scale-quantum-computing/teleportation.ipynb @@ -1,1860 +1,1862 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "dff1c53f-6f45-44cc-b725-95ae93474789", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "---\n", - "title: Teleportation\n", - "description: Throughout this lesson, you will learn about how to move quantum information from one to another by quantum teleportation\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore nshots */}\n", - "\n", - "# Quantum teleportation and superdense coding\n", - "\n", - "\n", - "\n", - "\n", - "Kifumi Numata (26 Apr 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/ws8x00xu0pksjzixyyspxzrilvt36iff.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 10 seconds.*\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "72ad29d7-df08-4ef1-a337-95c1b60c7935", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "To solve any utility-scale quantum problem, we will need to move information around on a quantum computer from one qubit to another. There are well-known protocols for doing this, but some of the most foundational were cast in the context of sending information between distant parties. Throughout this lesson, we will sometimes use language consistent with this context, such as \"distant friends sending information\". But keep in mind that these protocols have broader significance in quantum computing. In this lesson we consider the following quantum communication protocols:\n", - "- **Quantum teleportation**\n", - " Using a shared entangled state (sometimes called an e-bit) to send an unknown quantum state to a distant friend, requiring supplemental classical communication.\n", - "- **Quantum superdense coding**\n", - " How to send two bits of information by sending a single qubit to a distant friend (again using prior shared entangled qubits).\n", - "\n", - "For more background relevant to these topics, we recommend lesson 4 in Basics of Quantum Information on [Entanglement in action](/learning/courses/basics-of-quantum-information/entanglement-in-action/introduction).\n", - "\n", - "In the above description, an \"unknown quantum state\" simply refers to a state of the form described in the previous lesson:\n", - "$$\n", - "|\\psi\\rangle =\\alpha|0\\rangle+\\beta|1\\rangle\n", - "$$\n", - "where $\\alpha$ and $\\beta$ are complex numbers such that $|\\alpha|^2+|\\beta|^2 = 1$. This allows us to write the quantum state as\n", - "$$\n", - "|\\psi\\rangle =\\cos\\frac{\\theta}{2}|0\\rangle+e^{i\\varphi}\\sin\\frac{\\theta}{2}|1\\rangle=\n", - "\\left(\n", - "\\begin{matrix}\n", - "\\cos\\frac{\\theta}{2}\\\\\n", - "e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", - "\\end{matrix}\n", - "\\right)\n", - "$$\n", - "Since we want to be able to transfer the information in any random quantum state, generating such a state is where we will begin this lesson." - ] - }, - { - "cell_type": "markdown", - "id": "b0880998-6f81-4a5e-8a89-30edb8cdc40c", - "metadata": {}, - "source": [ - "## 2. Density matrices\n", - "\n", - "We can also write the quantum state $|\\psi \\rangle$ as its density matrix. This form is useful for denoting probabilistic mixture\n", - "of pure quantum states. In the case of a single qubit, we can write\n", - "\n", - "$$\n", - "|\\psi \\rangle \\langle \\psi| \\equiv \\rho = \\left(\n", - "\\begin{pmatrix}\n", - "\\cos\\frac{\\theta}{2}\\\\\n", - "e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", - "\\end{pmatrix}\n", - "\\right)\n", - "\\left(\n", - "\\begin{pmatrix}\n", - "\\cos\\frac{\\theta}{2} & e^{-i\\varphi}\\sin\\frac{\\theta}{2}\n", - "\\end{pmatrix}\n", - "\\right)\n", - "=\\frac{1}{2}\\left(\\begin{pmatrix}\n", - "1+\\cos\\theta & e^{-i\\varphi}\\sin\\theta\\\\\n", - "e^{-i\\varphi}\\sin\\theta & 1-\\cos\\theta\n", - "\\end{pmatrix}\\right)\n", - "$$\n", - "\n", - "Note that the density matrix $\\rho$ is a linear summation of Pauli matrices, as below,\n", - "\n", - "$$\n", - "\\rho = \\frac{1}{2}\\bigl( \\textbf{I} + (\\sin{\\theta}\\cos{\\varphi})\\textbf{X}+ (\\sin{\\theta}\\sin{\\varphi})\\textbf{Y} + (\\cos{\\theta})\\textbf{Z} \\bigr)\n", - "$$\n", - "\n", - "Or, in general,\n", - "\n", - "$$\n", - "\\rho = \\frac{1}{2}(\\textbf{I} + r_{x}\\textbf{X}+ r_{y}\\textbf{Y} + r_{z}\\textbf{Z})\n", - "$$\n", - "\n", - "where\n", - "$r_{x}^2+r_{y}^2+r_{z}^2=1$.\n", - "\n", - "And, the Bloch vector is $\\textbf{r} = (r_{x}, r_{y}, r_{z})$.\n", - "\n", - "Now, let's make an arbitrary quantum state using random numbers." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "6a70a1af-16a1-4499-910f-5427231cd676", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "theta=1.3101132663588946 ,varphi=4.525932273597346\n", - "(rx, ry, rz) = (-0.1791150283307452, -0.9494670044331133, 0.2577405946274022)\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "# create a random 1-qubit state from a random (theta, varphi) to define r vector\n", - "np.random.seed(1) # fixing seed for repeatibility\n", - "\n", - "theta = np.random.uniform(0.0, 1.0) * np.pi # from 0 to pi\n", - "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", - "\n", - "\n", - "def get_r_vec(theta, varphi):\n", - " rx = np.sin(theta) * np.cos(varphi)\n", - " ry = np.sin(theta) * np.sin(varphi)\n", - " rz = np.cos(theta)\n", - " return (rx, ry, rz)\n", - "\n", - "\n", - "# get r vector\n", - "rx, ry, rz = get_r_vec(theta, varphi)\n", - "\n", - "print(\"theta=\" + str(theta), \",varphi=\" + str(varphi))\n", - "print(\"(rx, ry, rz) = (\" + str(rx) + \", \" + str(ry) + \", \" + str(rz) + \")\")" - ] - }, - { - "cell_type": "markdown", - "id": "70ca84c8-b097-438e-aa4c-ba396cb8fe74", - "metadata": {}, - "source": [ - "We can show this Bloch vector on the Bloch sphere." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1ae3f71f-859e-4515-bea8-3945fe2fc1d4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_bloch_vector\n", - "\n", - "r = [rx, ry, rz]\n", - "plot_bloch_vector(r)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "dceecaec-cb4f-4797-820a-eaa51e797cd7", - "metadata": {}, - "source": [ - "## 3. Quantum state tomography\n", - "\n", - "If you only measure the quantum state in the computational basis ($|0 \\rangle$ and $|1 \\rangle$), the phase information (the complex number information) will be lost. But if we have many copies of $|\\psi \\rangle$ by repeating the preparation process (we can't clone states, but we can repeat preparation processes), we can estimate the value of $r_{x}, r_{y}, r_{z}$ by performing *quantum state tomography* for density matrix $\\rho$. Given the form:\n", - "\n", - "$$\n", - "\\rho = \\frac{1}{2}(\\textbf{I} + r_{x}\\textbf{X}+ r_{y}\\textbf{Y} + r_{z}\\textbf{Z})\n", - "$$\n", - "\n", - "it holds that\n", - "\n", - "$$\n", - "Tr(\\textbf{X} \\rho) = r_{x}, \\quad Tr(\\textbf{Y} \\rho) = r_{y}, \\quad Tr(\\textbf{Z} \\rho) = r_{z}\n", - "$$\n", - "\n", - "In $r_{z}$ case,\n", - "\n", - "$$\n", - "Tr(\\textbf{Z} \\rho) = \\langle 0|\\textbf{Z} \\rho|0 \\rangle + \\langle 1|\\textbf{Z} \\rho|1 \\rangle\n", - "$$\n", - "$$\n", - "= \\langle 0|(|0 \\rangle\\langle 0|-|1 \\rangle\\langle 1|) \\rho|0 \\rangle +\\langle 1|(|0 \\rangle\\langle 0|-|1 \\rangle\\langle 1|) \\rho|1 \\rangle\n", - "$$\n", - "$$\n", - "=\\langle 0|\\rho|0 \\rangle- \\langle 1| \\rho|1 \\rangle\n", - "$$\n", - "$$\n", - "=\\langle 0|\\psi\\rangle\\langle \\psi|0 \\rangle - \\langle 1| \\psi\\rangle\\langle \\psi|1 \\rangle\n", - "$$\n", - "$$\n", - "=|\\alpha|^2-|\\beta|^2\n", - "$$\n", - "\n", - "The last transformation of the equation is for $|\\psi \\rangle =\\alpha|0\\rangle+\\beta|1\\rangle$. Therefore, we can obtain $r_{z}$ by probability of $|0 \\rangle$ - Probability of $|1 \\rangle$.\n", - "\n", - "### Estimate $r_z$ value\n", - "\n", - "In order to estimate $r_z$, we create a quantum state and measure it. We then repeat the preparation and measurement many times. Finally we use the statistics of the measurement to estimate the probabilities above and thus estimate $r_z$.\n", - "\n", - "For creating the random quantum state, we will use the general unitary gate $U$ with the parameters $ \\theta, \\varphi$. (Refer to [U-gate](/docs/api/qiskit/qiskit.circuit.library.UGate) for more information.)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "7efb343a-56f6-4aef-a068-4c89767572e3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "# create a 1-qubit quantum state psi from theta, varphi parameters\n", - "qc = QuantumCircuit(1, 1)\n", - "qc.u(theta, varphi, 0.0, 0)\n", - "\n", - "# measure in computational basis\n", - "qc.measure(0, 0)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "bea0a2d7-f018-4578-850c-9800f409fc0e", - "metadata": {}, - "source": [ - "Using the `AerSimulator`, we will measure it in the computational basis to estimate $r_z$." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "284f455c-0281-4d9e-9a03-4da1cb94a177", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'1': 375, '0': 625}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# see if the expected value of measuring in the computational basis\n", - "# approaches the limit of rz\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import Sampler\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "nshots = 1000 # or 10000\n", - "# nshots = 10000\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc], shots=nshots)\n", - "result = job.result()\n", - "\n", - "# Extract counts data\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "448b940d-6778-4faf-9c37-52d2f583da81", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "rz = 0.2577405946274022 and approx of rz = 0.25\n" - ] - } - ], - "source": [ - "rz_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", - "\n", - "print(\"rz = \", rz, \" and approx of rz = \", rz_approx)" - ] - }, - { - "cell_type": "markdown", - "id": "f3f70b33-f607-4715-95eb-a62abfbc63d6", - "metadata": {}, - "source": [ - "Using the quantum state tomography method, we estimated the $r_z$ value. In this case, since we chose a parameter for the \"random\" state, we know the value of $r_z$ and can check our work. But by its very nature, utility-scale work is not always so trivial to check. We will discuss more about checking quantum results later in this course. For now, simply note that our estimation was reasonably accurate." - ] - }, - { - "cell_type": "markdown", - "id": "eb20e79d-9d60-4d06-b927-d6bc33ee7ff5", - "metadata": {}, - "source": [ - "### Exercise 1: Estimate $r_x$ value\n", - "Recall that IBM® quantum computers measure along the $z$-axis (sometimes stated \"in the $z$ basis\" or \"in the computational basis\"). However, by using rotations before the measurement, we can measure the quantum state's projection on the x-axis, also. To be more precise, if we rotate our system such that things that did point along $x$ now point along $z$, then we can keep the same measurement hardware along $z$, but learn about the state that was just along $x$ a moment ago. This is how most quantum computers (and all IBM quantum computers) perform measurements along multiple axes.\n", - "\n", - "With this understanding, try writing code to estimate the value of $r_x,$ using quantum state tomography.\n", - "\n", - "__Solution__:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c588cd02-8468-4008-a9b9-381354ec279c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# create a 1-qubit quantum state psi from theta, varphi parameters\n", - "qc = QuantumCircuit(1, 1)\n", - "qc.u(theta, varphi, 0.0, 0)\n", - "\n", - "qc.h(0)\n", - "qc.measure(0, 0)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5d52d55b-d565-4f1c-83ef-5626dce5622d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'1': 5925, '0': 4075}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Define backend\n", - "backend = AerSimulator()\n", - "nshots = 10000\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc], shots=nshots)\n", - "result = job.result()\n", - "\n", - "# Extract counts data\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "5790312b-4578-423b-babd-354d9045f9c9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "rx = -0.1791150283307452 and approx of rx = -0.185\n" - ] - } - ], - "source": [ - "rx_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", - "\n", - "print(\"rx = \", rx, \" and approx of rx = \", rx_approx)" - ] - }, - { - "cell_type": "markdown", - "id": "be59e531-2445-45a0-b585-e19289b4fea4", - "metadata": {}, - "source": [ - "### Exercise 2: Estimate $r_y$ value\n", - "Using the same logical arguments as before, we can rotate the system prior to measurement to learn about the $r_y$.\n", - "Try writing code yourself to estimate the value of $r_y$ using the quantum state tomography. You could start with the previous example, but make different rotations. (For more information about the various gates used, including ```sdg```, refer [to the API reference.](/docs/api/qiskit/qiskit.circuit.library.SdgGate))\n", - "\n", - "__Solution__:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "69e9d298-1073-4b93-8598-c21711fce33b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# create a 1-qubit quantum state psi from theta, varphi parameters\n", - "qc = QuantumCircuit(1, 1)\n", - "qc.u(theta, varphi, 0.0, 0)\n", - "\n", - "qc.sdg(0)\n", - "qc.h(0)\n", - "qc.measure(0, 0)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "6f2656fb-dde7-489c-ad40-d0f5f24f48a3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'1': 9759, '0': 241}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Define backend\n", - "backend = AerSimulator()\n", - "nshots = 10000\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc], shots=nshots)\n", - "result = job.result()\n", - "\n", - "# Extract counts data\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "c2f537b4-9f7d-4c6d-b1ef-adc125a0cbe7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ry = -0.9494670044331133 and approx of ry = -0.9518\n" - ] - } - ], - "source": [ - "ry_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", - "\n", - "print(\"ry = \", ry, \" and approx of ry = \", ry_approx)" - ] - }, - { - "cell_type": "markdown", - "id": "46175f37-0988-43c6-9217-1d16fa1c5de7", - "metadata": {}, - "source": [ - "We have now estimated all components of $\\vec{r}$ and can write out the full vector." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "fc539459-44c3-4ce3-8260-1924b999596c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Estimated vector is ( -0.185 , -0.9518 , 0.25 ).\n", - "Original random vector was (-0.1791150283307452, -0.9494670044331133, 0.2577405946274022).\n" - ] - } - ], - "source": [ - "print(\"Estimated vector is (\", rx_approx, \",\", ry_approx, \",\", rz_approx, \").\")\n", - "print(\"Original random vector was (\" + str(rx) + \", \" + str(ry) + \", \" + str(rz) + \").\")" - ] - }, - { - "cell_type": "markdown", - "id": "64c95009-dd20-497b-95cd-1f5620d785d7", - "metadata": {}, - "source": [ - "You obtained the estimation of the original random vector fairly accurately using this quantum state tomography method." - ] - }, - { - "cell_type": "markdown", - "id": "f37ac1a6-c48e-47cb-a2fa-40329d184100", - "metadata": {}, - "source": [ - "## 4. Quantum teleportation\n", - "\n", - "Let us consider the situation when a character Alice wants to send an unknown quantum state $|\\psi \\rangle$ to her friend Bob, who is far away. Assume they can only communicate with classical communication (like using email or a phone). Alice cannot copy the quantum state (due to the no-cloning theorem). If she repeated the same preparation process many times, she could build up statistics as we just did. But what if there is only a single unknown state? This state might have emerged from a physical process you want to study. Or it could be part of a larger quantum computation. In that case, how could Alice send the state to Bob? She can, if she and Bob share a valuable quantum resource: a shared entangled state, like the Bell state introduced in the previous lesson: $\\frac {|00\\rangle + |11\\rangle}{\\sqrt 2}.$ You might sometimes also see this referred to as an \"EPR pair\" or an \"e-bit\" (a fundamental unit of entanglement). If Alice shares such an entangled state with Bob, she can *teleport* the unknown quantum state to Bob by performing a series of quantum operations and sending him two bits of classical information.\n", - "\n", - "### 4.1 The protocol of Quantum teleportation\n", - "**Assumption**: Alice has an unknown quantum state $|\\psi \\rangle$ to be sent to Bob. Alice and Bob shares a 2-qubit entangled state, or e-bit, each having one of the qubits physically at their location.\n", - "\n", - "Here we outline the procedure without explanation. These will be implemented in detail below.\n", - "1. Alice entangles $|\\psi \\rangle$ with her part of the e-bit using the CNOT gate.\n", - "2. Alice applies a Hadamard gate to $|\\psi \\rangle$, and measures both her qubits in the computational basis.\n", - "3. Alice sends Bob her measurement results (either “00”, “01”, “10”, or “11”)\n", - "4. Bob performs a *correction* operator based on Alice’s two-bit of information on his part of the e-bit pair.\n", - " - If “00”, Bob does nothing\n", - " - If “01”, Bob applies X gate\n", - " - If “10”, Bob applies Z gate\n", - " - If “11”, Bob applies iY = ZX gate\n", - "5. Bob's part of the e-bit becomes $|\\psi \\rangle$.\n", - "\n", - "This is also worked out in more detail in [Basics of Quantum Information](/learning/courses/basics-of-quantum-information/entanglement-in-action/introduction). But the situation will become more clear as we instantiate this in Qiskit." - ] - }, - { - "cell_type": "markdown", - "id": "ab3e777f-7db8-457b-b90c-957b6c4055f4", - "metadata": {}, - "source": [ - "### 4.2 Quantum circuit simulating the quantum teleportation\n", - "\n", - "As always, we will apply the Qiskit patterns framework. This subsection will focus on mapping only.\n", - "\n", - "### Step 1: Map problem to quantum circuits and operators\n", - "\n", - "To describe the scenario above, we need a circuit with three qubits: two for the entangled pair shared by Alice and Bob, and one for the unknown quantum state $|\\psi\\rangle$." - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "47692345-76c7-43bd-ad10-2e12293dbed0", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "52bc0117-2009-45eb-b18e-f2e49ba8c708", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# create 3-qubits circuit\n", - "qc = QuantumCircuit(3, 3)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "fe9f1bdf-06d3-4282-91d3-100278e25351", - "metadata": {}, - "source": [ - "At the start, Alice has an unknown quantum state $|\\psi \\rangle.$ We will create this using the $U$ gate." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "b597a613-12b5-48db-9d07-a490853fcd07", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create the unknown quantum state using the u-gate. Alice has this.\n", - "qc.u(theta, varphi, 0.0, 0)\n", - "qc.barrier() # for visual separation\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "379ddc02-ed29-423a-94eb-a545574ef386", - "metadata": {}, - "source": [ - "We can visualize the state we've created, but only because we know what parameters were used in the $U$ gate. If this state had emerged from a complicated quantum process, the state would not be knowable without running the process to create the state many times, and collecting statistics as in tomography." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "d4a00147-de30-457a-a4e8-0d351fb8a0f9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# show the quantum state on bloch sphere\n", - "from qiskit.quantum_info import Statevector\n", - "from qiskit.visualization import plot_bloch_multivector\n", - "\n", - "out_vector = Statevector(qc)\n", - "\n", - "\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "df1e58f4-c678-49d6-a7dc-fe0af47da2ab", - "metadata": {}, - "source": [ - "Before this protocol even begins, we assume Alice and Bob have a shared entangled pair. If Alice and Bob are truly in different locations, they might have set up the shared state *before* the unknown state $|\\psi\\rangle$ was ever created. Because those things are happening on different qubits, there order here won't matter, and this order is convenient for visualization." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "6de6ff3d-e728-4432-a7c5-b6d5b7616f14", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Alice and Bob are together in the same place and set up an entangled pair.\n", - "qc.h(1)\n", - "qc.cx(1, 2)\n", - "qc.barrier() # for visual separation.\n", - "# We can consider that Alice and Bob might move their qubits to different physical locations, now.\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "61512866-dd0b-4985-8ee3-5601921cfa24", - "metadata": {}, - "source": [ - "Next, Alice entangles $|\\psi \\rangle$ with her part of the shared e-bit, using the $CX$ gate and $H$ gate, and measures them in the computational basis." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1281b988-183b-43bf-b338-9eb63e0b1b8d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Alice entangles the unknown state with her part of the e-bit, using the CNOT gate and H gate.\n", - "qc.cx(0, 1)\n", - "qc.h(0)\n", - "qc.barrier()\n", - "\n", - "# Alice measures the two qubits.\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "daf20da0-7d1d-454f-87c8-132ff402c1f5", - "metadata": {}, - "source": [ - "Alice sends Bob her measurement results (either “00”, “01”, “10”, or “11”), and Bob performs a correction operator based on Alice’s two bits of information on his part of the shared e-bit. Then, Bob's becomes $|\\psi \\rangle$." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "2de8ec3e-a77b-4c8c-b589-877205b6df80", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Alice sent the results to Bob. Bob applies correction\n", - "with qc.if_test((0, 1)):\n", - " qc.z(2)\n", - "with qc.if_test((1, 1)):\n", - " qc.x(2)\n", - "qc.barrier()\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "512a1559-3b94-4888-b70b-bc621f6882c7", - "metadata": {}, - "source": [ - "You have completed a quantum teleportation circuit! Let's see the output state of this circuit using the statevector simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "f265fc08-0275-4032-8fc0-3578b05abec9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_aer import StatevectorSimulator\n", - "\n", - "backend = StatevectorSimulator()\n", - "out_vector = backend.run(qc, shots=1).result().get_statevector() # set shots = 1\n", - "\n", - "plot_bloch_multivector(out_vector)" - ] - }, - { - "cell_type": "markdown", - "id": "8800546b-6afe-4b20-b8ad-3750e957ba45", - "metadata": {}, - "source": [ - "You can see that the quantum state created by the $U$-gate of qubit 0 (the qubit originally holding the secret state) has been transferred to qubit 2 (Bob's qubit).\n", - "\n", - "You can run above cell a few times to make sure. You might notice that the qubits 0 and 1 change states, but qubit 2 is always in the state $|\\psi\\rangle $." - ] - }, - { - "cell_type": "markdown", - "id": "dd90f2ae-3a57-4f87-a230-aad61709d795", - "metadata": {}, - "source": [ - "### 4.3 Execute it and confirm the result by applying U inverse\n", - "\n", - "Above, we checked visually that the teleported state looked correct. Another way to check if the quantum state has been teleported correctly, is to apply the inverse of the $U$ gate on Bob's qubit so that we can measure '0'. That is, since $U^{-1}U$ is the identity, if Bob's qubit is in the state created from $U|0\\rangle,$ then applying the inverse should yield $U^{-1}U|0\\rangle=|0\\rangle.$" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "2fed8e6b-0fa2-4341-b71f-a5f63d7899bd", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Apply the inverse of u-gate to measure |0>\n", - "qc.u(theta, varphi, 0.0, 2).inverse() # inverse of u(theta,varphi,0.0)\n", - "qc.measure(2, 2) # add measurement gate\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "0474b50d-f0d3-4e4a-96de-d8152b5607bd", - "metadata": {}, - "source": [ - "We will execute the circuit first using the AerSimulator, before moving on to a real quantum comptuer." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "bff4ac5d-3d47-4d99-856e-bd31181464f9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'011': 2510, '010': 2417, '000': 2635, '001': 2438}\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_aer import AerSimulator\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import Sampler\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([isa_qc], shots=nshots)\n", - "result = job.result()\n", - "\n", - "# Extract counts data\n", - "counts = result[0].data.c.get_counts()\n", - "print(counts)\n", - "\n", - "# Plot the counts in a histogram\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "8d8d0125-0353-4ab9-9c36-09be6b37443d", - "metadata": {}, - "source": [ - "Recall that in little endian notation, qubit 2 is the left-most (or bottom-most, in the column labels) qubit. Note that the left- and bottom-most qubit in the column labels is a 0 for all possible outcomes. This shows we have a 100% chance of measuring $q_2$ in the state $|0\\rangle $. This is the expected result, and indicates the teleportation protocol has worked properly." - ] - }, - { - "cell_type": "markdown", - "id": "efb5b496-b5b0-40d4-b615-507bbdcbae98", - "metadata": {}, - "source": [ - "### 4.4 Teleportation on a real quantum computer\n", - "\n", - "\n", - "Next, we will perform teleportation on a real quantum computer. Using the dynamic circuit function, we can operate mid-circuit using measurement outcomes, implementing in real-time the conditionals operations in the teleportation circuit. For solving problems with real quantum computers, we will follow the four steps of Qiskit patterns.\n", - "\n", - " 1. Map problem to quantum circuits and operators\n", - " 2. Optimize for target hardware\n", - " 3. Execute on target hardware\n", - " 4. Post-process the results" - ] - }, - { - "cell_type": "markdown", - "id": "3d0c7ba0-8195-44bb-9de9-850e3b42db00", - "metadata": {}, - "source": [ - "### Exercise 3: Build the teleportation circuit\n", - "\n", - "Try building the whole teleportation circuit from scratch to test your understanding. Scroll back up if you need a reminder.\n", - "\n", - "__Solution__:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f35290c5-f14f-4768-b0cf-ae753dbbd1d0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map problem to quantum circuits and operators\n", - "# Create the circuit with 3-qubits and 1-bit\n", - "qc = QuantumCircuit(3, 3)\n", - "\n", - "# Alice creates an unknown quantum state using the u-gate.\n", - "qc.u(theta, varphi, 0.0, 0)\n", - "qc.barrier() # for visual separation\n", - "\n", - "# Eve creates EPR pair and sends q1 to Alice and q2 to Bob\n", - "##your code goes here##\n", - "qc.h(1)\n", - "qc.cx(1, 2)\n", - "qc.barrier()\n", - "\n", - "# Alice entangles the unknown state with her EPR part, using the CNOT gate and H gate.\n", - "##your code goes here##\n", - "qc.cx(0, 1)\n", - "qc.h(0)\n", - "qc.barrier()\n", - "\n", - "# Alice measures the two qubits.\n", - "##your code goes here##\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "\n", - "# Alice sent the results to Bob. Now, Bob applies correction\n", - "##your code goes here##\n", - "with qc.if_test((0, 1)):\n", - " qc.z(2)\n", - "with qc.if_test((1, 1)):\n", - " qc.x(2)\n", - "qc.barrier()\n", - "\n", - "# Apply the inverse of u-gate to measure |0>\n", - "qc.u(theta, varphi, 0.0, 2).inverse()\n", - "qc.measure(2, 2)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "53cb0711-2d1b-4c46-9ce0-af72f00ce030", - "metadata": {}, - "source": [ - "As a reminder, applying the inverse of the $U$ gate is just so we can verify the expected behavior. It isn't part of sending the state to Bob, and we would not use that inverse $U$ gate if the only goal was to transfer quantum information.\n", - "\n", - "### Step 2: Optimize for target hardware\n", - "\n", - "To run on hardware, import `QiskitRuntimeService` and load your saved credentials. Select the backend with the fewest number of jobs in the queue." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1236f91b-adbf-4c15-bedc-e8778a018a5f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "service.backends()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "ad63605f-8e84-41c9-a40e-90a566cf4406", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The least busy device is \n" - ] - } - ], - "source": [ - "# You can also identify the least busy device\n", - "backend = service.least_busy(operational=True)\n", - "print(\"The least busy device is \", backend)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23b925f7-477e-42ff-bf92-077b304e7bdd", - "metadata": {}, - "outputs": [], - "source": [ - "# You can specify the device\n", - "# backend = service.backend('ibm_brisbane')" - ] - }, - { - "cell_type": "markdown", - "id": "7974c87f-bc4a-4329-b5d6-2fe91227c6e6", - "metadata": {}, - "source": [ - "Let's see the coupling map of the device that you selected." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "bfccaa7e-78fe-4ca4-8c37-e9f64be924d3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_gate_map\n", - "\n", - "plot_gate_map(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "940be0f5-ca1a-4d8f-9ae8-866d1b58190e", - "metadata": {}, - "source": [ - "Different devices might have different coupling maps, and each device has some qubits and couplers that are more performant than others. Finally, different quantum computers might have different *native gates* (gates the hardware can execute). Transpiling the circuit rewrites the abstract quantum circuit using gates the target quantum computer can execute, and selects the optimal mapping to physical qubits (among other things). Transpilation is a rich and complicated topic. For more on transpilation, see the [API reference.](/docs/api/qiskit/transpiler#overview)" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "524768df-1070-4f51-b9ec-6b8672551fb5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 2: Optimize for target hardware\n", - "# Transpile the circuit into basis gates executable on the hardware\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", - "qc_compiled = pm.run(qc)\n", - "\n", - "qc_compiled.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "3d27becd-577c-4366-a35c-583387b831b5", - "metadata": {}, - "source": [ - "### Step 3: Execute the circuit.\n", - "\n", - "Using the `Sampler` Runtime primitive, we will execute the target circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "f172d44a-7e5a-4433-8700-07c61e41fd13", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "job id: d13nkhpn2txg008jt0d0\n" - ] - } - ], - "source": [ - "# Step 3: Execute the target circuit\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([qc_compiled])\n", - "job_id = job.job_id()\n", - "print(\"job id:\", job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "43ab9cc3-0412-434d-a3f1-008c8e6d0e5a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check the job status\n", - "job.status()" - ] - }, - { - "cell_type": "markdown", - "id": "9b3dbd44-61df-4838-9bdc-d36d6fba5881", - "metadata": {}, - "source": [ - "You can also check the job status from your [IBM Quantum® dashboard.](https://quantum.cloud.ibm.com/workloads)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f791c0d-5788-4ad1-bd15-5f6314d48a73", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "job_real = service.job(job.job_id()) # Input your job-id between the quotations\n", - "job_real.status()" - ] - }, - { - "cell_type": "markdown", - "id": "d6e53566-db3b-4ba8-bd7c-1c1365380268", - "metadata": {}, - "source": [ - "If you see `'DONE'` is displayed, you can get the result by executing below cell." - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "dc075997-a531-4d9f-8d9f-3c44a4e261f1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'001': 992, '110': 430, '011': 579, '010': 605, '111': 402, '000': 925, '100': 57, '101': 106}\n" - ] - } - ], - "source": [ - "# Execute after 'DONE' is displayed\n", - "result_real = job_real.result()\n", - "print(result_real[0].data.c.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "c43db6d1-8572-4f52-9b2d-80104b0311d0", - "metadata": {}, - "source": [ - "### Step 4: Post-process the results" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "0f89e380-48ff-4d76-a3da-4af78e86912d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 4: Post-process the results\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(result_real[0].data.c.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "909ec2a4-ebb3-4d56-8d0b-8a167465bf0b", - "metadata": {}, - "source": [ - "You can interpret the results above directly. Or, using `marginal_count`, you can trace out Bob's results on qubit 2." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "eb20ac6f-2a96-43af-b070-cd7eae6d3f8e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# trace out Bob's results on qubit 2\n", - "from qiskit.result import marginal_counts\n", - "\n", - "bobs_qubit = 2\n", - "real_counts = result_real[0].data.c.get_counts()\n", - "bobs_counts = marginal_counts(real_counts, [bobs_qubit])\n", - "plot_histogram(bobs_counts)" - ] - }, - { - "cell_type": "markdown", - "id": "a9db1801-a5b6-443a-adff-fcd25b0f6591", - "metadata": {}, - "source": [ - "As we see here, there are a few results in which we measured $|1 \\rangle$. These are due to noise and errors. In particular, dynamic circuits tend to have a higher error rate because of the time-consuming measurement in the middle of the circuit." - ] - }, - { - "cell_type": "markdown", - "id": "729bfcae-5cd1-46e5-8b0c-230c63b09fe0", - "metadata": {}, - "source": [ - "### 4.5 Key takeaways on quantum teleportation\n", - "\n", - "We can transport a quantum state to a distant friend by sharing a pair of entangled qubits (an e-bit).\n", - "\n", - "1. Can quantum teleportation send the quantum state faster than light?\n", - "\tNo, because Alice has to tell Bob the measurement results in a classical way.\n", - "\n", - "2. Would quantum teleportation break the \"no cloning theorem\", which forbids copying of a quantum state?\n", - "\tNo, because the original quantum state given to Alice on one of her qubits was lost in measurement. It collapsed to a $|0\\rangle$ or $|1\\rangle$." - ] - }, - { - "cell_type": "markdown", - "id": "869349fa-5593-442e-9390-5f20e5af8f46", - "metadata": {}, - "source": [ - "## 5. Superdense coding\n", - "\n", - "Almost the same setup can be used for a different purpose. Suppose Alice wants to send Bob two bits of classical information, but she has no means of classical communication with Bob. She does, however, share an entangled pair with Bob and she is allowed to send her qubit to Bob's location. Notice the contrast with the quantum teleportation protocol. In teleportation, classical communication _was_ available to the friends, and the goal was to send a quantum state. Here, classical communication is not accessible and they use the transfer of a qubit to share two bits of classical information." - ] - }, - { - "cell_type": "markdown", - "id": "53401dfc-f81b-4706-93de-07970e594021", - "metadata": {}, - "source": [ - "### 5.1 The protocol of superdense coding\n", - "\n", - "**Assumption**: Alice has two bits of information, say, $a_1a_2 \\in \\{00, 01, 10, 11\\}$. Alice and Bob share an entangled pair (e-bit), but they cannot communicate classically.\n", - "\n", - "1. Alice performs one of the following operations on her part of e-bit.\n", - " - If $a_1a_2 = 00$, she does nothing\n", - " - If $a_1a_2 = 01$, she applies Z gate\n", - " - If $a_1a_2 = 10$, she applies X gate\n", - " - If $a_1a_2 = 11$, she applies Z gate and X gate.\n", - "2. Alice sends her part of the e-bit to Bob's location.\n", - "3. Bob applies a CNOT gate with the qubit from Alice as control and his qubit as target, then applies H gate to the qubit from Alice, and measures the two qubits. The possible starting states and results of Bob's operations are:\n", - "$$\n", - "\\frac {|00\\rangle + |11\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |00\\rangle\n", - "$$\n", - "$$\n", - "\\frac {|00\\rangle - |11\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |01\\rangle\n", - "$$\n", - "$$\n", - "\\frac {|10\\rangle + |01\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |10\\rangle\n", - "$$\n", - "$$\n", - "\\frac {|10\\rangle - |01\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow -|11\\rangle\n", - "$$\n", - "\n", - "Note that a negative sign of $-|11\\rangle$ is global phase, so it is not measurable." - ] - }, - { - "cell_type": "markdown", - "id": "820767fd-1f07-4426-8bb1-1db270c53009", - "metadata": {}, - "source": [ - "### 5.2 Quantum circuit simulating the superdense coding\n", - "\n", - "Based on the protocol of superdense coding, you can build the superdense coding circuit as below. Try to change the message, `msg`, which Alice wants to transform to Bob." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "440c4e7d-a93c-4098-a5ce-bc2c4cc1dce7", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit" - ] - }, - { - "cell_type": "markdown", - "id": "020a35a6-83f1-4bb9-8076-d7f0c2d6a5d0", - "metadata": {}, - "source": [ - "Qiskit pattern steps are identified in the code comments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "998cc84b-86d9-4d0a-89e8-30ac38e4ba0b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map problem to quantum circuits and operators\n", - "# Create 2-qubits circuit\n", - "qc = QuantumCircuit(2, 2)\n", - "\n", - "# Eve creates EPR pair and send q0 to Alice and q1 to Bob\n", - "qc.h(0)\n", - "qc.cx(0, 1)\n", - "qc.barrier()\n", - "\n", - "# set message which Alice wants to transform to Bob\n", - "msg = \"11\" # You can change the message\n", - "\n", - "if msg == \"00\":\n", - " pass\n", - "elif msg == \"10\":\n", - " qc.x(0)\n", - "elif msg == \"01\":\n", - " qc.z(0)\n", - "elif msg == \"11\":\n", - " qc.z(0)\n", - " qc.x(0)\n", - "\n", - "qc.barrier()\n", - "# Bob receives EPR qubit from Alice and performs unitary operations\n", - "qc.cx(0, 1)\n", - "qc.h(0)\n", - "qc.barrier()\n", - "\n", - "# Bob measures q0 and q1\n", - "qc.measure(0, 0)\n", - "qc.measure(1, 1)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "41838ac1-0eab-4f83-860b-2bf2a0732243", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'11': 1000}\n" - ] - } - ], - "source": [ - "# We will execute on a simulator first\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import Sampler\n", - "\n", - "# Define backend\n", - "backend = AerSimulator()\n", - "shots = 1000\n", - "\n", - "# Transpile to backend\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_qc = pm.run(qc)\n", - "\n", - "# Run the job\n", - "sampler = Sampler(mode=backend)\n", - "job_sim = sampler.run([isa_qc], shots=shots)\n", - "result_sim = job_sim.result()\n", - "\n", - "# Extract counts data\n", - "counts = result_sim[0].data.c.get_counts()\n", - "print(counts)" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "db2c0b52-0643-4185-8082-453c03ff59f3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Visualize the results\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "c14acbcd-6da5-416d-a2c0-aabe68e83872", - "metadata": {}, - "source": [ - "You can see that Bob received the message that Alice wanted to send to him.\n", - "\n", - "Next, let's try it with a real quantum computer." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c7d49410-a62f-4aa1-8d89-27fd92ca4609", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The least busy device is \n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True)\n", - "print(\"The least busy device is \", backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "ec22b1d6-37d3-4e0e-833f-e2fd9af45015", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1 was already completed before the simulator job above.\n", - "# Step 2: Optimize for target hardware\n", - "# Transpile the circuit into basis gates executable on the hardware\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", - "qc_compiled = pm.run(qc)\n", - "\n", - "qc_compiled.draw(\"mpl\", idle_wires=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "971b28e1-b76a-4485-946a-8a2107998380", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "job id: d13nnyq3grvg008j0zag\n" - ] - } - ], - "source": [ - "# Step 3:Execute the target circuit\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([qc_compiled])\n", - "job_id = job.job_id()\n", - "print(\"job id:\", job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "391fda7a-7cfd-4ccc-8f59-9e1453f7b3b0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check the job status\n", - "job.status()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81e4c3ab-70d4-486c-934e-feb032330b8c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'DONE'" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", - "# from qiskit_ibm_runtime import QiskitRuntimeService\n", - "# service = QiskitRuntimeService()\n", - "job = service.job(job_id) # Input your job-id between the quotations\n", - "job.status()" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "0b797f05-55bc-4d6e-85cd-4ccfb5048416", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'11': 3942, '01': 107, '10': 41, '00': 6}\n" - ] - } - ], - "source": [ - "# Execute after job has successfully run\n", - "real_result = job.result()\n", - "print(real_result[0].data.c.get_counts())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "072c6e01-3744-47ba-bbc5-0d0eebb80f03", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 4: post-process the results\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(real_result[0].data.c.get_counts())" - ] - }, - { - "cell_type": "markdown", - "id": "03938e24-ae13-4272-b8ac-672d9d88c429", - "metadata": {}, - "source": [ - "The result is what we expected. Note that superdense coding on a real quantum computer showed fewer errors than in the case of quantum teleportation on a real quantum computer. One reason for this might be that quantum teleportation uses dynamic circuits, and superdense coding does not. We will learn more about errors in quantum circuits in later lessons." - ] - }, - { - "cell_type": "markdown", - "id": "c6a1c499-962a-4d5e-81b0-b7df2082c44c", - "metadata": {}, - "source": [ - "## 6. Summary\n", - "\n", - "In this session, we have implemented two quantum protocols. Although the scenarios for both involving distant friends are somewhat removed from quantum computing on a single QPU, they have applications in quantum computing, and help us understand the transfer of quantum information better.\n", - "\n", - "- **Quantum teleportation**: Although we cannot copy quantum states, we can teleport unknown quantum states by having shared entanglement.\n", - "- **Quantum superdense coding**: A shared entangled pair, and transfer of one qubit, enable the communication of two bits of classical information." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "13f6ebcf-e4a8-4427-b951-7549fca9c350", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# See the version of Qiskit\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dff1c53f-6f45-44cc-b725-95ae93474789", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "---\n", + "title: Teleportation\n", + "description: Throughout this lesson, you will learn about how to move quantum information from one to another by quantum teleportation\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore nshots */}\n", + "\n", + "# Quantum teleportation and superdense coding\n", + "\n", + "\n", + "\n", + "\n", + "Kifumi Numata (26 Apr 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/ws8x00xu0pksjzixyyspxzrilvt36iff.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 10 seconds.*\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "72ad29d7-df08-4ef1-a337-95c1b60c7935", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "To solve any utility-scale quantum problem, we will need to move information around on a quantum computer from one qubit to another. There are well-known protocols for doing this, but some of the most foundational were cast in the context of sending information between distant parties. Throughout this lesson, we will sometimes use language consistent with this context, such as \"distant friends sending information\". But keep in mind that these protocols have broader significance in quantum computing. In this lesson we consider the following quantum communication protocols:\n", + "- **Quantum teleportation**\n", + " Using a shared entangled state (sometimes called an e-bit) to send an unknown quantum state to a distant friend, requiring supplemental classical communication.\n", + "- **Quantum superdense coding**\n", + " How to send two bits of information by sending a single qubit to a distant friend (again using prior shared entangled qubits).\n", + "\n", + "For more background relevant to these topics, we recommend lesson 4 in Basics of Quantum Information on [Entanglement in action](/learning/courses/basics-of-quantum-information/entanglement-in-action/introduction).\n", + "\n", + "In the above description, an \"unknown quantum state\" simply refers to a state of the form described in the previous lesson:\n", + "$$\n", + "|\\psi\\rangle =\\alpha|0\\rangle+\\beta|1\\rangle\n", + "$$\n", + "where $\\alpha$ and $\\beta$ are complex numbers such that $|\\alpha|^2+|\\beta|^2 = 1$. This allows us to write the quantum state as\n", + "$$\n", + "|\\psi\\rangle =\\cos\\frac{\\theta}{2}|0\\rangle+e^{i\\varphi}\\sin\\frac{\\theta}{2}|1\\rangle=\n", + "\\left(\n", + "\\begin{matrix}\n", + "\\cos\\frac{\\theta}{2}\\\\\n", + "e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", + "\\end{matrix}\n", + "\\right)\n", + "$$\n", + "Since we want to be able to transfer the information in any random quantum state, generating such a state is where we will begin this lesson." + ] + }, + { + "cell_type": "markdown", + "id": "b0880998-6f81-4a5e-8a89-30edb8cdc40c", + "metadata": {}, + "source": [ + "## 2. Density matrices\n", + "\n", + "We can also write the quantum state $|\\psi \\rangle$ as its density matrix. This form is useful for denoting probabilistic mixture\n", + "of pure quantum states. In the case of a single qubit, we can write\n", + "\n", + "$$\n", + "|\\psi \\rangle \\langle \\psi| \\equiv \\rho = \\left(\n", + "\\begin{pmatrix}\n", + "\\cos\\frac{\\theta}{2}\\\\\n", + "e^{i\\varphi}\\sin\\frac{\\theta}{2}\n", + "\\end{pmatrix}\n", + "\\right)\n", + "\\left(\n", + "\\begin{pmatrix}\n", + "\\cos\\frac{\\theta}{2} & e^{-i\\varphi}\\sin\\frac{\\theta}{2}\n", + "\\end{pmatrix}\n", + "\\right)\n", + "=\\frac{1}{2}\\left(\\begin{pmatrix}\n", + "1+\\cos\\theta & e^{-i\\varphi}\\sin\\theta\\\\\n", + "e^{-i\\varphi}\\sin\\theta & 1-\\cos\\theta\n", + "\\end{pmatrix}\\right)\n", + "$$\n", + "\n", + "Note that the density matrix $\\rho$ is a linear summation of Pauli matrices, as below,\n", + "\n", + "$$\n", + "\\rho = \\frac{1}{2}\\bigl( \\textbf{I} + (\\sin{\\theta}\\cos{\\varphi})\\textbf{X}+ (\\sin{\\theta}\\sin{\\varphi})\\textbf{Y} + (\\cos{\\theta})\\textbf{Z} \\bigr)\n", + "$$\n", + "\n", + "Or, in general,\n", + "\n", + "$$\n", + "\\rho = \\frac{1}{2}(\\textbf{I} + r_{x}\\textbf{X}+ r_{y}\\textbf{Y} + r_{z}\\textbf{Z})\n", + "$$\n", + "\n", + "where\n", + "$r_{x}^2+r_{y}^2+r_{z}^2=1$.\n", + "\n", + "And, the Bloch vector is $\\textbf{r} = (r_{x}, r_{y}, r_{z})$.\n", + "\n", + "Now, let's make an arbitrary quantum state using random numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6a70a1af-16a1-4499-910f-5427231cd676", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "theta=1.3101132663588946 ,varphi=4.525932273597346\n", + "(rx, ry, rz) = (-0.1791150283307452, -0.9494670044331133, 0.2577405946274022)\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "# create a random 1-qubit state from a random (theta, varphi) to define r vector\n", + "np.random.seed(1) # fixing seed for repeatibility\n", + "\n", + "theta = np.random.uniform(0.0, 1.0) * np.pi # from 0 to pi\n", + "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", + "\n", + "\n", + "def get_r_vec(theta, varphi):\n", + " rx = np.sin(theta) * np.cos(varphi)\n", + " ry = np.sin(theta) * np.sin(varphi)\n", + " rz = np.cos(theta)\n", + " return (rx, ry, rz)\n", + "\n", + "\n", + "# get r vector\n", + "rx, ry, rz = get_r_vec(theta, varphi)\n", + "\n", + "print(\"theta=\" + str(theta), \",varphi=\" + str(varphi))\n", + "print(\"(rx, ry, rz) = (\" + str(rx) + \", \" + str(ry) + \", \" + str(rz) + \")\")" + ] + }, + { + "cell_type": "markdown", + "id": "70ca84c8-b097-438e-aa4c-ba396cb8fe74", + "metadata": {}, + "source": [ + "We can show this Bloch vector on the Bloch sphere." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ae3f71f-859e-4515-bea8-3945fe2fc1d4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_bloch_vector\n", + "\n", + "r = [rx, ry, rz]\n", + "plot_bloch_vector(r)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "dceecaec-cb4f-4797-820a-eaa51e797cd7", + "metadata": {}, + "source": [ + "## 3. Quantum state tomography\n", + "\n", + "If you only measure the quantum state in the computational basis ($|0 \\rangle$ and $|1 \\rangle$), the phase information (the complex number information) will be lost. But if we have many copies of $|\\psi \\rangle$ by repeating the preparation process (we can't clone states, but we can repeat preparation processes), we can estimate the value of $r_{x}, r_{y}, r_{z}$ by performing *quantum state tomography* for density matrix $\\rho$. Given the form:\n", + "\n", + "$$\n", + "\\rho = \\frac{1}{2}(\\textbf{I} + r_{x}\\textbf{X}+ r_{y}\\textbf{Y} + r_{z}\\textbf{Z})\n", + "$$\n", + "\n", + "it holds that\n", + "\n", + "$$\n", + "Tr(\\textbf{X} \\rho) = r_{x}, \\quad Tr(\\textbf{Y} \\rho) = r_{y}, \\quad Tr(\\textbf{Z} \\rho) = r_{z}\n", + "$$\n", + "\n", + "In $r_{z}$ case,\n", + "\n", + "$$\n", + "Tr(\\textbf{Z} \\rho) = \\langle 0|\\textbf{Z} \\rho|0 \\rangle + \\langle 1|\\textbf{Z} \\rho|1 \\rangle\n", + "$$\n", + "$$\n", + "= \\langle 0|(|0 \\rangle\\langle 0|-|1 \\rangle\\langle 1|) \\rho|0 \\rangle +\\langle 1|(|0 \\rangle\\langle 0|-|1 \\rangle\\langle 1|) \\rho|1 \\rangle\n", + "$$\n", + "$$\n", + "=\\langle 0|\\rho|0 \\rangle- \\langle 1| \\rho|1 \\rangle\n", + "$$\n", + "$$\n", + "=\\langle 0|\\psi\\rangle\\langle \\psi|0 \\rangle - \\langle 1| \\psi\\rangle\\langle \\psi|1 \\rangle\n", + "$$\n", + "$$\n", + "=|\\alpha|^2-|\\beta|^2\n", + "$$\n", + "\n", + "The last transformation of the equation is for $|\\psi \\rangle =\\alpha|0\\rangle+\\beta|1\\rangle$. Therefore, we can obtain $r_{z}$ by probability of $|0 \\rangle$ - Probability of $|1 \\rangle$.\n", + "\n", + "### Estimate $r_z$ value\n", + "\n", + "In order to estimate $r_z$, we create a quantum state and measure it. We then repeat the preparation and measurement many times. Finally we use the statistics of the measurement to estimate the probabilities above and thus estimate $r_z$.\n", + "\n", + "For creating the random quantum state, we will use the general unitary gate $U$ with the parameters $ \\theta, \\varphi$. (Refer to [U-gate](/docs/api/qiskit/qiskit.circuit.library.UGate) for more information.)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7efb343a-56f6-4aef-a068-4c89767572e3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "# create a 1-qubit quantum state psi from theta, varphi parameters\n", + "qc = QuantumCircuit(1, 1)\n", + "qc.u(theta, varphi, 0.0, 0)\n", + "\n", + "# measure in computational basis\n", + "qc.measure(0, 0)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "bea0a2d7-f018-4578-850c-9800f409fc0e", + "metadata": {}, + "source": [ + "Using the `AerSimulator`, we will measure it in the computational basis to estimate $r_z$." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "284f455c-0281-4d9e-9a03-4da1cb94a177", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'1': 375, '0': 625}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# see if the expected value of measuring in the computational basis\n", + "# approaches the limit of rz\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import Sampler\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "nshots = 1000 # or 10000\n", + "# nshots = 10000\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc], shots=nshots)\n", + "result = job.result()\n", + "\n", + "# Extract counts data\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "448b940d-6778-4faf-9c37-52d2f583da81", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "rz = 0.2577405946274022 and approx of rz = 0.25\n" + ] + } + ], + "source": [ + "rz_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", + "\n", + "print(\"rz = \", rz, \" and approx of rz = \", rz_approx)" + ] + }, + { + "cell_type": "markdown", + "id": "f3f70b33-f607-4715-95eb-a62abfbc63d6", + "metadata": {}, + "source": [ + "Using the quantum state tomography method, we estimated the $r_z$ value. In this case, since we chose a parameter for the \"random\" state, we know the value of $r_z$ and can check our work. But by its very nature, utility-scale work is not always so trivial to check. We will discuss more about checking quantum results later in this course. For now, simply note that our estimation was reasonably accurate." + ] + }, + { + "cell_type": "markdown", + "id": "eb20e79d-9d60-4d06-b927-d6bc33ee7ff5", + "metadata": {}, + "source": [ + "### Exercise 1: Estimate $r_x$ value\n", + "Recall that IBM® quantum computers measure along the $z$-axis (sometimes stated \"in the $z$ basis\" or \"in the computational basis\"). However, by using rotations before the measurement, we can measure the quantum state's projection on the x-axis, also. To be more precise, if we rotate our system such that things that did point along $x$ now point along $z$, then we can keep the same measurement hardware along $z$, but learn about the state that was just along $x$ a moment ago. This is how most quantum computers (and all IBM quantum computers) perform measurements along multiple axes.\n", + "\n", + "With this understanding, try writing code to estimate the value of $r_x,$ using quantum state tomography.\n", + "\n", + "__Solution__:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c588cd02-8468-4008-a9b9-381354ec279c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a 1-qubit quantum state psi from theta, varphi parameters\n", + "qc = QuantumCircuit(1, 1)\n", + "qc.u(theta, varphi, 0.0, 0)\n", + "\n", + "qc.h(0)\n", + "qc.measure(0, 0)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5d52d55b-d565-4f1c-83ef-5626dce5622d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'1': 5925, '0': 4075}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define backend\n", + "backend = AerSimulator()\n", + "nshots = 10000\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc], shots=nshots)\n", + "result = job.result()\n", + "\n", + "# Extract counts data\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5790312b-4578-423b-babd-354d9045f9c9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "rx = -0.1791150283307452 and approx of rx = -0.185\n" + ] + } + ], + "source": [ + "rx_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", + "\n", + "print(\"rx = \", rx, \" and approx of rx = \", rx_approx)" + ] + }, + { + "cell_type": "markdown", + "id": "be59e531-2445-45a0-b585-e19289b4fea4", + "metadata": {}, + "source": [ + "### Exercise 2: Estimate $r_y$ value\n", + "Using the same logical arguments as before, we can rotate the system prior to measurement to learn about the $r_y$.\n", + "Try writing code yourself to estimate the value of $r_y$ using the quantum state tomography. You could start with the previous example, but make different rotations. (For more information about the various gates used, including ```sdg```, refer [to the API reference.](/docs/api/qiskit/qiskit.circuit.library.SdgGate))\n", + "\n", + "__Solution__:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "69e9d298-1073-4b93-8598-c21711fce33b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a 1-qubit quantum state psi from theta, varphi parameters\n", + "qc = QuantumCircuit(1, 1)\n", + "qc.u(theta, varphi, 0.0, 0)\n", + "\n", + "qc.sdg(0)\n", + "qc.h(0)\n", + "qc.measure(0, 0)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6f2656fb-dde7-489c-ad40-d0f5f24f48a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'1': 9759, '0': 241}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define backend\n", + "backend = AerSimulator()\n", + "nshots = 10000\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc], shots=nshots)\n", + "result = job.result()\n", + "\n", + "# Extract counts data\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c2f537b4-9f7d-4c6d-b1ef-adc125a0cbe7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ry = -0.9494670044331133 and approx of ry = -0.9518\n" + ] + } + ], + "source": [ + "ry_approx = (counts[\"0\"] - counts[\"1\"]) / nshots\n", + "\n", + "print(\"ry = \", ry, \" and approx of ry = \", ry_approx)" + ] + }, + { + "cell_type": "markdown", + "id": "46175f37-0988-43c6-9217-1d16fa1c5de7", + "metadata": {}, + "source": [ + "We have now estimated all components of $\\vec{r}$ and can write out the full vector." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fc539459-44c3-4ce3-8260-1924b999596c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Estimated vector is ( -0.185 , -0.9518 , 0.25 ).\n", + "Original random vector was (-0.1791150283307452, -0.9494670044331133, 0.2577405946274022).\n" + ] + } + ], + "source": [ + "print(\"Estimated vector is (\", rx_approx, \",\", ry_approx, \",\", rz_approx, \").\")\n", + "print(\"Original random vector was (\" + str(rx) + \", \" + str(ry) + \", \" + str(rz) + \").\")" + ] + }, + { + "cell_type": "markdown", + "id": "64c95009-dd20-497b-95cd-1f5620d785d7", + "metadata": {}, + "source": [ + "You obtained the estimation of the original random vector fairly accurately using this quantum state tomography method." + ] + }, + { + "cell_type": "markdown", + "id": "f37ac1a6-c48e-47cb-a2fa-40329d184100", + "metadata": {}, + "source": [ + "## 4. Quantum teleportation\n", + "\n", + "Let us consider the situation when a character Alice wants to send an unknown quantum state $|\\psi \\rangle$ to her friend Bob, who is far away. Assume they can only communicate with classical communication (like using email or a phone). Alice cannot copy the quantum state (due to the no-cloning theorem). If she repeated the same preparation process many times, she could build up statistics as we just did. But what if there is only a single unknown state? This state might have emerged from a physical process you want to study. Or it could be part of a larger quantum computation. In that case, how could Alice send the state to Bob? She can, if she and Bob share a valuable quantum resource: a shared entangled state, like the Bell state introduced in the previous lesson: $\\frac {|00\\rangle + |11\\rangle}{\\sqrt 2}.$ You might sometimes also see this referred to as an \"EPR pair\" or an \"e-bit\" (a fundamental unit of entanglement). If Alice shares such an entangled state with Bob, she can *teleport* the unknown quantum state to Bob by performing a series of quantum operations and sending him two bits of classical information.\n", + "\n", + "### 4.1 The protocol of Quantum teleportation\n", + "**Assumption**: Alice has an unknown quantum state $|\\psi \\rangle$ to be sent to Bob. Alice and Bob shares a 2-qubit entangled state, or e-bit, each having one of the qubits physically at their location.\n", + "\n", + "Here we outline the procedure without explanation. These will be implemented in detail below.\n", + "1. Alice entangles $|\\psi \\rangle$ with her part of the e-bit using the CNOT gate.\n", + "2. Alice applies a Hadamard gate to $|\\psi \\rangle$, and measures both her qubits in the computational basis.\n", + "3. Alice sends Bob her measurement results (either “00”, “01”, “10”, or “11”)\n", + "4. Bob performs a *correction* operator based on Alice’s two-bit of information on his part of the e-bit pair.\n", + " - If “00”, Bob does nothing\n", + " - If “01”, Bob applies X gate\n", + " - If “10”, Bob applies Z gate\n", + " - If “11”, Bob applies iY = ZX gate\n", + "5. Bob's part of the e-bit becomes $|\\psi \\rangle$.\n", + "\n", + "This is also worked out in more detail in [Basics of Quantum Information](/learning/courses/basics-of-quantum-information/entanglement-in-action/introduction). But the situation will become more clear as we instantiate this in Qiskit." + ] + }, + { + "cell_type": "markdown", + "id": "ab3e777f-7db8-457b-b90c-957b6c4055f4", + "metadata": {}, + "source": [ + "### 4.2 Quantum circuit simulating the quantum teleportation\n", + "\n", + "As always, we will apply the Qiskit patterns framework. This subsection will focus on mapping only.\n", + "\n", + "### Step 1: Map problem to quantum circuits and operators\n", + "\n", + "To describe the scenario above, we need a circuit with three qubits: two for the entangled pair shared by Alice and Bob, and one for the unknown quantum state $|\\psi\\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "47692345-76c7-43bd-ad10-2e12293dbed0", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "52bc0117-2009-45eb-b18e-f2e49ba8c708", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create 3-qubits circuit\n", + "qc = QuantumCircuit(3, 3)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "fe9f1bdf-06d3-4282-91d3-100278e25351", + "metadata": {}, + "source": [ + "At the start, Alice has an unknown quantum state $|\\psi \\rangle.$ We will create this using the $U$ gate." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b597a613-12b5-48db-9d07-a490853fcd07", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the unknown quantum state using the u-gate. Alice has this.\n", + "qc.u(theta, varphi, 0.0, 0)\n", + "qc.barrier() # for visual separation\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "379ddc02-ed29-423a-94eb-a545574ef386", + "metadata": {}, + "source": [ + "We can visualize the state we've created, but only because we know what parameters were used in the $U$ gate. If this state had emerged from a complicated quantum process, the state would not be knowable without running the process to create the state many times, and collecting statistics as in tomography." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d4a00147-de30-457a-a4e8-0d351fb8a0f9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show the quantum state on bloch sphere\n", + "from qiskit.quantum_info import Statevector\n", + "from qiskit.visualization import plot_bloch_multivector\n", + "\n", + "out_vector = Statevector(qc)\n", + "\n", + "\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "df1e58f4-c678-49d6-a7dc-fe0af47da2ab", + "metadata": {}, + "source": [ + "Before this protocol even begins, we assume Alice and Bob have a shared entangled pair. If Alice and Bob are truly in different locations, they might have set up the shared state *before* the unknown state $|\\psi\\rangle$ was ever created. Because those things are happening on different qubits, there order here won't matter, and this order is convenient for visualization." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "6de6ff3d-e728-4432-a7c5-b6d5b7616f14", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Alice and Bob are together in the same place and set up an entangled pair.\n", + "qc.h(1)\n", + "qc.cx(1, 2)\n", + "qc.barrier() # for visual separation.\n", + "# We can consider that Alice and Bob might move their qubits to different physical locations, now.\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "61512866-dd0b-4985-8ee3-5601921cfa24", + "metadata": {}, + "source": [ + "Next, Alice entangles $|\\psi \\rangle$ with her part of the shared e-bit, using the $CX$ gate and $H$ gate, and measures them in the computational basis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1281b988-183b-43bf-b338-9eb63e0b1b8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Alice entangles the unknown state with her part of the e-bit, using the CNOT gate and H gate.\n", + "qc.cx(0, 1)\n", + "qc.h(0)\n", + "qc.barrier()\n", + "\n", + "# Alice measures the two qubits.\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "daf20da0-7d1d-454f-87c8-132ff402c1f5", + "metadata": {}, + "source": [ + "Alice sends Bob her measurement results (either “00”, “01”, “10”, or “11”), and Bob performs a correction operator based on Alice’s two bits of information on his part of the shared e-bit. Then, Bob's becomes $|\\psi \\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "2de8ec3e-a77b-4c8c-b589-877205b6df80", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Alice sent the results to Bob. Bob applies correction\n", + "with qc.if_test((0, 1)):\n", + " qc.z(2)\n", + "with qc.if_test((1, 1)):\n", + " qc.x(2)\n", + "qc.barrier()\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "512a1559-3b94-4888-b70b-bc621f6882c7", + "metadata": {}, + "source": [ + "You have completed a quantum teleportation circuit! Let's see the output state of this circuit using the statevector simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f265fc08-0275-4032-8fc0-3578b05abec9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_aer import StatevectorSimulator\n", + "\n", + "backend = StatevectorSimulator()\n", + "out_vector = backend.run(qc, shots=1).result().get_statevector() # set shots = 1\n", + "\n", + "plot_bloch_multivector(out_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "8800546b-6afe-4b20-b8ad-3750e957ba45", + "metadata": {}, + "source": [ + "You can see that the quantum state created by the $U$-gate of qubit 0 (the qubit originally holding the secret state) has been transferred to qubit 2 (Bob's qubit).\n", + "\n", + "You can run above cell a few times to make sure. You might notice that the qubits 0 and 1 change states, but qubit 2 is always in the state $|\\psi\\rangle $." + ] + }, + { + "cell_type": "markdown", + "id": "dd90f2ae-3a57-4f87-a230-aad61709d795", + "metadata": {}, + "source": [ + "### 4.3 Execute it and confirm the result by applying U inverse\n", + "\n", + "Above, we checked visually that the teleported state looked correct. Another way to check if the quantum state has been teleported correctly, is to apply the inverse of the $U$ gate on Bob's qubit so that we can measure '0'. That is, since $U^{-1}U$ is the identity, if Bob's qubit is in the state created from $U|0\\rangle,$ then applying the inverse should yield $U^{-1}U|0\\rangle=|0\\rangle.$" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "2fed8e6b-0fa2-4341-b71f-a5f63d7899bd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Apply the inverse of u-gate to measure |0>\n", + "qc.u(theta, varphi, 0.0, 2).inverse() # inverse of u(theta,varphi,0.0)\n", + "qc.measure(2, 2) # add measurement gate\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "0474b50d-f0d3-4e4a-96de-d8152b5607bd", + "metadata": {}, + "source": [ + "We will execute the circuit first using the AerSimulator, before moving on to a real quantum comptuer." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "bff4ac5d-3d47-4d99-856e-bd31181464f9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'011': 2510, '010': 2417, '000': 2635, '001': 2438}\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_aer import AerSimulator\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import Sampler\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([isa_qc], shots=nshots)\n", + "result = job.result()\n", + "\n", + "# Extract counts data\n", + "counts = result[0].data.c.get_counts()\n", + "print(counts)\n", + "\n", + "# Plot the counts in a histogram\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "8d8d0125-0353-4ab9-9c36-09be6b37443d", + "metadata": {}, + "source": [ + "Recall that in little endian notation, qubit 2 is the left-most (or bottom-most, in the column labels) qubit. Note that the left- and bottom-most qubit in the column labels is a 0 for all possible outcomes. This shows we have a 100% chance of measuring $q_2$ in the state $|0\\rangle $. This is the expected result, and indicates the teleportation protocol has worked properly." + ] + }, + { + "cell_type": "markdown", + "id": "efb5b496-b5b0-40d4-b615-507bbdcbae98", + "metadata": {}, + "source": [ + "### 4.4 Teleportation on a real quantum computer\n", + "\n", + "\n", + "Next, we will perform teleportation on a real quantum computer. Using the dynamic circuit function, we can operate mid-circuit using measurement outcomes, implementing in real-time the conditionals operations in the teleportation circuit. For solving problems with real quantum computers, we will follow the four steps of Qiskit patterns.\n", + "\n", + " 1. Map problem to quantum circuits and operators\n", + " 2. Optimize for target hardware\n", + " 3. Execute on target hardware\n", + " 4. Post-process the results" + ] + }, + { + "cell_type": "markdown", + "id": "3d0c7ba0-8195-44bb-9de9-850e3b42db00", + "metadata": {}, + "source": [ + "### Exercise 3: Build the teleportation circuit\n", + "\n", + "Try building the whole teleportation circuit from scratch to test your understanding. Scroll back up if you need a reminder.\n", + "\n", + "__Solution__:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f35290c5-f14f-4768-b0cf-ae753dbbd1d0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map problem to quantum circuits and operators\n", + "# Create the circuit with 3-qubits and 1-bit\n", + "qc = QuantumCircuit(3, 3)\n", + "\n", + "# Alice creates an unknown quantum state using the u-gate.\n", + "qc.u(theta, varphi, 0.0, 0)\n", + "qc.barrier() # for visual separation\n", + "\n", + "# Eve creates EPR pair and sends q1 to Alice and q2 to Bob\n", + "##your code goes here##\n", + "qc.h(1)\n", + "qc.cx(1, 2)\n", + "qc.barrier()\n", + "\n", + "# Alice entangles the unknown state with her EPR part, using the CNOT gate and H gate.\n", + "##your code goes here##\n", + "qc.cx(0, 1)\n", + "qc.h(0)\n", + "qc.barrier()\n", + "\n", + "# Alice measures the two qubits.\n", + "##your code goes here##\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "\n", + "# Alice sent the results to Bob. Now, Bob applies correction\n", + "##your code goes here##\n", + "with qc.if_test((0, 1)):\n", + " qc.z(2)\n", + "with qc.if_test((1, 1)):\n", + " qc.x(2)\n", + "qc.barrier()\n", + "\n", + "# Apply the inverse of u-gate to measure |0>\n", + "qc.u(theta, varphi, 0.0, 2).inverse()\n", + "qc.measure(2, 2)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "53cb0711-2d1b-4c46-9ce0-af72f00ce030", + "metadata": {}, + "source": [ + "As a reminder, applying the inverse of the $U$ gate is just so we can verify the expected behavior. It isn't part of sending the state to Bob, and we would not use that inverse $U$ gate if the only goal was to transfer quantum information.\n", + "\n", + "### Step 2: Optimize for target hardware\n", + "\n", + "To run on hardware, import `QiskitRuntimeService` and load your saved credentials. Select the backend with the fewest number of jobs in the queue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1236f91b-adbf-4c15-bedc-e8778a018a5f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "service.backends()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "ad63605f-8e84-41c9-a40e-90a566cf4406", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The least busy device is \n" + ] + } + ], + "source": [ + "# You can also identify the least busy device\n", + "backend = service.least_busy(operational=True)\n", + "print(\"The least busy device is \", backend)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23b925f7-477e-42ff-bf92-077b304e7bdd", + "metadata": {}, + "outputs": [], + "source": [ + "# You can specify the device\n", + "# backend = service.backend('ibm_brisbane')" + ] + }, + { + "cell_type": "markdown", + "id": "7974c87f-bc4a-4329-b5d6-2fe91227c6e6", + "metadata": {}, + "source": [ + "Let's see the coupling map of the device that you selected." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "bfccaa7e-78fe-4ca4-8c37-e9f64be924d3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_gate_map\n", + "\n", + "plot_gate_map(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "940be0f5-ca1a-4d8f-9ae8-866d1b58190e", + "metadata": {}, + "source": [ + "Different devices might have different coupling maps, and each device has some qubits and couplers that are more performant than others. Finally, different quantum computers might have different *native gates* (gates the hardware can execute). Transpiling the circuit rewrites the abstract quantum circuit using gates the target quantum computer can execute, and selects the optimal mapping to physical qubits (among other things). Transpilation is a rich and complicated topic. For more on transpilation, see the [API reference.](/docs/api/qiskit/transpiler#overview)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "524768df-1070-4f51-b9ec-6b8672551fb5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 2: Optimize for target hardware\n", + "# Transpile the circuit into basis gates executable on the hardware\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", + "qc_compiled = pm.run(qc)\n", + "\n", + "qc_compiled.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "3d27becd-577c-4366-a35c-583387b831b5", + "metadata": {}, + "source": [ + "### Step 3: Execute the circuit.\n", + "\n", + "Using the `Sampler` Runtime primitive, we will execute the target circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "f172d44a-7e5a-4433-8700-07c61e41fd13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job id: d13nkhpn2txg008jt0d0\n" + ] + } + ], + "source": [ + "# Step 3: Execute the target circuit\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([qc_compiled])\n", + "job_id = job.job_id()\n", + "print(\"job id:\", job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "43ab9cc3-0412-434d-a3f1-008c8e6d0e5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check the job status\n", + "job.status()" + ] + }, + { + "cell_type": "markdown", + "id": "9b3dbd44-61df-4838-9bdc-d36d6fba5881", + "metadata": {}, + "source": [ + "You can also check the job status from your [IBM Quantum® dashboard.](https://quantum.cloud.ibm.com/workloads)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f791c0d-5788-4ad1-bd15-5f6314d48a73", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If the Notebook session got disconnected you can also check your job status by running the\n", + "# following code\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "job_real = service.job(job.job_id()) # Input your job-id between the quotations\n", + "job_real.status()" + ] + }, + { + "cell_type": "markdown", + "id": "d6e53566-db3b-4ba8-bd7c-1c1365380268", + "metadata": {}, + "source": [ + "If you see `'DONE'` is displayed, you can get the result by executing below cell." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "dc075997-a531-4d9f-8d9f-3c44a4e261f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'001': 992, '110': 430, '011': 579, '010': 605, '111': 402, '000': 925, '100': 57, '101': 106}\n" + ] + } + ], + "source": [ + "# Execute after 'DONE' is displayed\n", + "result_real = job_real.result()\n", + "print(result_real[0].data.c.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "c43db6d1-8572-4f52-9b2d-80104b0311d0", + "metadata": {}, + "source": [ + "### Step 4: Post-process the results" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "0f89e380-48ff-4d76-a3da-4af78e86912d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 4: Post-process the results\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(result_real[0].data.c.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "909ec2a4-ebb3-4d56-8d0b-8a167465bf0b", + "metadata": {}, + "source": [ + "You can interpret the results above directly. Or, using `marginal_count`, you can trace out Bob's results on qubit 2." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "eb20ac6f-2a96-43af-b070-cd7eae6d3f8e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# trace out Bob's results on qubit 2\n", + "from qiskit.result import marginal_counts\n", + "\n", + "bobs_qubit = 2\n", + "real_counts = result_real[0].data.c.get_counts()\n", + "bobs_counts = marginal_counts(real_counts, [bobs_qubit])\n", + "plot_histogram(bobs_counts)" + ] + }, + { + "cell_type": "markdown", + "id": "a9db1801-a5b6-443a-adff-fcd25b0f6591", + "metadata": {}, + "source": [ + "As we see here, there are a few results in which we measured $|1 \\rangle$. These are due to noise and errors. In particular, dynamic circuits tend to have a higher error rate because of the time-consuming measurement in the middle of the circuit." + ] + }, + { + "cell_type": "markdown", + "id": "729bfcae-5cd1-46e5-8b0c-230c63b09fe0", + "metadata": {}, + "source": [ + "### 4.5 Key takeaways on quantum teleportation\n", + "\n", + "We can transport a quantum state to a distant friend by sharing a pair of entangled qubits (an e-bit).\n", + "\n", + "1. Can quantum teleportation send the quantum state faster than light?\n", + "\tNo, because Alice has to tell Bob the measurement results in a classical way.\n", + "\n", + "2. Would quantum teleportation break the \"no cloning theorem\", which forbids copying of a quantum state?\n", + "\tNo, because the original quantum state given to Alice on one of her qubits was lost in measurement. It collapsed to a $|0\\rangle$ or $|1\\rangle$." + ] + }, + { + "cell_type": "markdown", + "id": "869349fa-5593-442e-9390-5f20e5af8f46", + "metadata": {}, + "source": [ + "## 5. Superdense coding\n", + "\n", + "Almost the same setup can be used for a different purpose. Suppose Alice wants to send Bob two bits of classical information, but she has no means of classical communication with Bob. She does, however, share an entangled pair with Bob and she is allowed to send her qubit to Bob's location. Notice the contrast with the quantum teleportation protocol. In teleportation, classical communication _was_ available to the friends, and the goal was to send a quantum state. Here, classical communication is not accessible and they use the transfer of a qubit to share two bits of classical information." + ] + }, + { + "cell_type": "markdown", + "id": "53401dfc-f81b-4706-93de-07970e594021", + "metadata": {}, + "source": [ + "### 5.1 The protocol of superdense coding\n", + "\n", + "**Assumption**: Alice has two bits of information, say, $a_1a_2 \\in \\{00, 01, 10, 11\\}$. Alice and Bob share an entangled pair (e-bit), but they cannot communicate classically.\n", + "\n", + "1. Alice performs one of the following operations on her part of e-bit.\n", + " - If $a_1a_2 = 00$, she does nothing\n", + " - If $a_1a_2 = 01$, she applies Z gate\n", + " - If $a_1a_2 = 10$, she applies X gate\n", + " - If $a_1a_2 = 11$, she applies Z gate and X gate.\n", + "2. Alice sends her part of the e-bit to Bob's location.\n", + "3. Bob applies a CNOT gate with the qubit from Alice as control and his qubit as target, then applies H gate to the qubit from Alice, and measures the two qubits. The possible starting states and results of Bob's operations are:\n", + "$$\n", + "\\frac {|00\\rangle + |11\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |00\\rangle\n", + "$$\n", + "$$\n", + "\\frac {|00\\rangle - |11\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |01\\rangle\n", + "$$\n", + "$$\n", + "\\frac {|10\\rangle + |01\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow |10\\rangle\n", + "$$\n", + "$$\n", + "\\frac {|10\\rangle - |01\\rangle}{\\sqrt 2} \\rightarrow CX_{01}\\otimes H_0 \\rightarrow -|11\\rangle\n", + "$$\n", + "\n", + "Note that a negative sign of $-|11\\rangle$ is global phase, so it is not measurable." + ] + }, + { + "cell_type": "markdown", + "id": "820767fd-1f07-4426-8bb1-1db270c53009", + "metadata": {}, + "source": [ + "### 5.2 Quantum circuit simulating the superdense coding\n", + "\n", + "Based on the protocol of superdense coding, you can build the superdense coding circuit as below. Try to change the message, `msg`, which Alice wants to transform to Bob." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "440c4e7d-a93c-4098-a5ce-bc2c4cc1dce7", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit" + ] + }, + { + "cell_type": "markdown", + "id": "020a35a6-83f1-4bb9-8076-d7f0c2d6a5d0", + "metadata": {}, + "source": [ + "Qiskit pattern steps are identified in the code comments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "998cc84b-86d9-4d0a-89e8-30ac38e4ba0b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map problem to quantum circuits and operators\n", + "# Create 2-qubits circuit\n", + "qc = QuantumCircuit(2, 2)\n", + "\n", + "# Eve creates EPR pair and send q0 to Alice and q1 to Bob\n", + "qc.h(0)\n", + "qc.cx(0, 1)\n", + "qc.barrier()\n", + "\n", + "# set message which Alice wants to transform to Bob\n", + "msg = \"11\" # You can change the message\n", + "\n", + "if msg == \"00\":\n", + " pass\n", + "elif msg == \"10\":\n", + " qc.x(0)\n", + "elif msg == \"01\":\n", + " qc.z(0)\n", + "elif msg == \"11\":\n", + " qc.z(0)\n", + " qc.x(0)\n", + "\n", + "qc.barrier()\n", + "# Bob receives EPR qubit from Alice and performs unitary operations\n", + "qc.cx(0, 1)\n", + "qc.h(0)\n", + "qc.barrier()\n", + "\n", + "# Bob measures q0 and q1\n", + "qc.measure(0, 0)\n", + "qc.measure(1, 1)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "41838ac1-0eab-4f83-860b-2bf2a0732243", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'11': 1000}\n" + ] + } + ], + "source": [ + "# We will execute on a simulator first\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import Sampler\n", + "\n", + "# Define backend\n", + "backend = AerSimulator()\n", + "shots = 1000\n", + "\n", + "# Transpile to backend\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_qc = pm.run(qc)\n", + "\n", + "# Run the job\n", + "sampler = Sampler(mode=backend)\n", + "job_sim = sampler.run([isa_qc], shots=shots)\n", + "result_sim = job_sim.result()\n", + "\n", + "# Extract counts data\n", + "counts = result_sim[0].data.c.get_counts()\n", + "print(counts)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "db2c0b52-0643-4185-8082-453c03ff59f3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Visualize the results\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "c14acbcd-6da5-416d-a2c0-aabe68e83872", + "metadata": {}, + "source": [ + "You can see that Bob received the message that Alice wanted to send to him.\n", + "\n", + "Next, let's try it with a real quantum computer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7d49410-a62f-4aa1-8d89-27fd92ca4609", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The least busy device is \n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True)\n", + "print(\"The least busy device is \", backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "ec22b1d6-37d3-4e0e-833f-e2fd9af45015", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1 was already completed before the simulator job above.\n", + "# Step 2: Optimize for target hardware\n", + "# Transpile the circuit into basis gates executable on the hardware\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", + "qc_compiled = pm.run(qc)\n", + "\n", + "qc_compiled.draw(\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "971b28e1-b76a-4485-946a-8a2107998380", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job id: d13nnyq3grvg008j0zag\n" + ] + } + ], + "source": [ + "# Step 3:Execute the target circuit\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([qc_compiled])\n", + "job_id = job.job_id()\n", + "print(\"job id:\", job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "391fda7a-7cfd-4ccc-8f59-9e1453f7b3b0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check the job status\n", + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81e4c3ab-70d4-486c-934e-feb032330b8c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'DONE'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If the Notebook session got disconnected you can also check your job status by running the\n", + "# following code\n", + "# from qiskit_ibm_runtime import QiskitRuntimeService\n", + "# service = QiskitRuntimeService()\n", + "job = service.job(job_id) # Input your job-id between the quotations\n", + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "0b797f05-55bc-4d6e-85cd-4ccfb5048416", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'11': 3942, '01': 107, '10': 41, '00': 6}\n" + ] + } + ], + "source": [ + "# Execute after job has successfully run\n", + "real_result = job.result()\n", + "print(real_result[0].data.c.get_counts())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "072c6e01-3744-47ba-bbc5-0d0eebb80f03", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 4: post-process the results\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(real_result[0].data.c.get_counts())" + ] + }, + { + "cell_type": "markdown", + "id": "03938e24-ae13-4272-b8ac-672d9d88c429", + "metadata": {}, + "source": [ + "The result is what we expected. Note that superdense coding on a real quantum computer showed fewer errors than in the case of quantum teleportation on a real quantum computer. One reason for this might be that quantum teleportation uses dynamic circuits, and superdense coding does not. We will learn more about errors in quantum circuits in later lessons." + ] + }, + { + "cell_type": "markdown", + "id": "c6a1c499-962a-4d5e-81b0-b7df2082c44c", + "metadata": {}, + "source": [ + "## 6. Summary\n", + "\n", + "In this session, we have implemented two quantum protocols. Although the scenarios for both involving distant friends are somewhat removed from quantum computing on a single QPU, they have applications in quantum computing, and help us understand the transfer of quantum information better.\n", + "\n", + "- **Quantum teleportation**: Although we cannot copy quantum states, we can teleport unknown quantum states by having shared entanglement.\n", + "- **Quantum superdense coding**: A shared entangled pair, and transfer of one qubit, enable the communication of two bits of classical information." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "13f6ebcf-e4a8-4427-b951-7549fca9c350", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# See the version of Qiskit\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/courses/variational-algorithm-design/cost-functions.ipynb b/learning/courses/variational-algorithm-design/cost-functions.ipynb index c35d1081522..8bdf9043c5a 100644 --- a/learning/courses/variational-algorithm-design/cost-functions.ipynb +++ b/learning/courses/variational-algorithm-design/cost-functions.ipynb @@ -1,1499 +1,1500 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "da8ce695-8435-4099-b0fd-ade7b298f540", - "metadata": {}, - "source": [ - "---\n", - "title: Cost functions\n", - "description: This lesson explains what a cost function is, how it is used in variational algorithms, and how it can differ from the Hamiltonian.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore resil nbsp eigenbasis expvals IIZZ IZIZ IZZI ZIIZ ZZII */}\n", - "\n", - "# Cost functions\n", - "\n", - "During this lesson, we'll learn how to evaluate a *cost function*:\n", - "\n", - "- First, we'll learn about [Qiskit Runtime primitives](/docs/guides/primitives)\n", - "- Define a *cost function* $C(\\vec\\theta)$. This is a problem-specific function that defines the problem's goal for the optimizer to minimize (or maximize)\n", - "- Defining a measurement strategy with the Qiskit Runtime primitives to optimize speed versus accuracy\n", - "\n", - " \n", - "\n", - "![A diagram showing key components of a cost function including using primitives like estimator and sampler.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-workflow.svg)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4a9b79a9-c040-43d3-a4cf-314277e30eea", - "metadata": { - "gloss": { - "primitives": { - "text": "Basic and fundamental operations or data type. Qiskit has the Sampler and Estimator primitives to serve as building blocks to easily construct complex workloads, like variational algorithms.", - "title": "Primitives" - } - } - }, - "source": [ - "## Primitives\n", - "\n", - "All physical systems, whether classical or quantum, can exist in different states. For example, a car on a road can have a certain mass, position, speed, or acceleration that characterize its state. Similarly, quantum systems can also have different configurations or states, but they differ from classical systems in how we deal with measurements and state evolution. This leads to unique properties such as *superposition* and *entanglement* that are exclusive to quantum mechanics. Just like we can describe a car's state using physical properties like speed or acceleration, we can also describe the state of a quantum system using *observables*, which are mathematical objects.\n", - "\n", - "In quantum mechanics, states are represented by normalized complex column vectors, or *kets* ($|\\psi\\rangle$), and observables are Hermitian linear operators ($\\hat{H}=\\hat{H}^{\\dagger}$) that act on the kets. An eigenvector ($|\\lambda\\rangle$) of an observable is known as an *eigenstate*. Measuring an observable for one of its eigenstates ($|\\lambda\\rangle$) will give us the corresponding eigenvalue ($\\lambda$) as readout.\n", - "\n", - "\n", - "If you're wondering how to measure a quantum system and what you can measure, Qiskit offers two primitives that can help:\n", - "\n", - "* `Sampler`: Given a quantum state $|\\psi\\rangle$, this primitive obtains the probability of each possible computational basis state.\n", - "* `Estimator`: Given a quantum observable $\\hat{H}$ and a state $|\\psi\\rangle$, this primitive computes the expected value of $\\hat{H}$." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "00aa7dfa-34f1-440e-bbdb-19b6e6a1aff8", - "metadata": {}, - "source": [ - "### The Sampler primitive\n", - "\n", - "The `Sampler` primitive calculates the probability of obtaining each possible state $|k\\rangle$ from the computational basis, given a quantum circuit that prepares the state $|\\psi\\rangle$. It calculates\n", - "\n", - "$$\n", - "p_k = |\\langle k | \\psi \\rangle|^2 \\quad \\forall k \\in \\mathbb{Z}_2^n \\equiv \\{0,1,\\cdots,2^n-1\\},\n", - "$$\n", - "\n", - "Where $n$ is the number of qubits, and $k$ the integer representation of any possible output binary string $\\{0,1\\}^n$ (that is, integers base $2$).\n", - "\n", - "\n", - "The Qiskit Runtime [`Sampler`](/docs/api/qiskit-ibm-runtime/sampler-v2) runs the circuit multiple times on a quantum device, performing measurements on each run, and reconstructing the probability distribution from the recovered bitstrings. The more runs (or *shots*) it performs, the more accurate the results will be, but this requires more time and quantum resources.\n", - "\n", - "\n", - "However, since the number of possible outputs grows exponentially with the number of qubits $n$ (that is, $2^n$), the number of shots will need to grow exponentially as well in order to capture a _dense_ probability distribution. Therefore, `Sampler` is only efficient for *sparse* probability distributions; where the target state $|\\psi\\rangle$ must be expressible as a linear combination of the computational basis states, with the number of terms growing at most polynomially with the number of qubits:\n", - "\n", - "$$\n", - "|\\psi\\rangle = \\sum^{\\text{Poly}(n)}_k w_k |k\\rangle.\n", - "$$\n", - "\n", - "\n", - "The `Sampler` can also be configured to retrieve probabilities from a subsection of the circuit, representing a subset of the total possible states." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "96a74b2b-6aa0-4ef1-a8d1-448b591429e4", - "metadata": { - "gloss": { - "pauli": { - "text": "Set of matrices commonly used in quantum computing to represent and manipulate quantum states, consisting of the identity matrix and the three Pauli matrices (X, Y, and Z).", - "title": "Pauli Operators" - } - } - }, - "source": [ - "### The Estimator primitive\n", - "\n", - "\n", - "The `Estimator` primitive calculates the expectation value of an observable $\\hat{H}$ for a quantum state $|\\psi\\rangle$; where the observable probabilities can be expressed as $p_\\lambda = |\\langle\\lambda|\\psi\\rangle|^2$, being $|\\lambda\\rangle$ the eigenstates of the observable $\\hat{H}$. The expectation value is then defined as the average of all possible outcomes $\\lambda$ (that is, the eigenvalues of the observable) of a measurement of the state $|\\psi\\rangle$, weighted by the corresponding probabilities:\n", - "\n", - "$$\n", - "\\langle\\hat{H}\\rangle_\\psi := \\sum_\\lambda p_\\lambda \\lambda = \\langle \\psi | \\hat{H} | \\psi \\rangle\n", - "$$\n", - "\n", - "\n", - "However, calculating the expectation value of an observable is not always possible, as we often don't know its eigenbasis. The Qiskit Runtime [`Estimator`](/docs/api/qiskit-ibm-runtime/estimator-v2) uses a complex algebraic process to estimate the expectation value on a real quantum device by breaking down the observable into a combination of other observables whose eigenbasis we do know.\n", - "\n", - "In simpler terms, `Estimator` breaks down any observable that it doesn't know how to measure into simpler, measurable observables called Pauli operators." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6dcabf55-4462-4cf9-b6e4-e7860fd25b31", - "metadata": {}, - "source": [ - "Any operator can be expressed as a combination of $4^n$ Pauli operators.\n", - "\n", - "$$\n", - "\\hat{P}_k :=\n", - "\\sigma_{k_{n-1}}\\otimes \\cdots \\otimes \\sigma_{k_0} \\quad\n", - "\\forall k \\in \\mathbb{Z}_4^n \\equiv \\{0,1,\\cdots,4^n-1\\}, \\\\\n", - "$$\n", - "\n", - "such that\n", - "\n", - "$$\n", - "\\hat{H} = \\sum^{4^n-1}_{k=0} w_k \\hat{P}_k\n", - "$$\n", - "\n", - "where $n$ is the number of qubits, $k \\equiv k_{n-1} \\cdots k_0$ for $k_l \\in \\mathbb{Z}_4 \\equiv \\{0, 1, 2, 3\\}$ (that is, integers base $4$), and $(\\sigma_0, \\sigma_1, \\sigma_2, \\sigma_3) := (I, X, Y, Z)$.\n", - "\n", - "After performing this decomposition, `Estimator` derives a new circuit $V_k|\\psi\\rangle$ for each observable $\\hat{P}_k$ (from the original circuit), to effectively *diagonalize* the Pauli observable in the computational basis and measure it. We can easily measure Pauli observables because we know $V_k$ ahead of time, which is not the case generally for other observables.\n", - "\n", - "For each $\\hat{P}_{k}$, the `Estimator` runs the corresponding circuit on a quantum device multiple times, measures the output state in the computational basis, and calculates the probability $p_{kj}$ of obtaining each possible output $j$. It then looks for the eigenvalue $\\lambda_{kj}$ of $P_k$ corresponding to each output $j$, multiplies by $w_k$, and adds all the results together to obtain the expected value of the observable $\\hat{H}$ for the given state $|\\psi\\rangle$.\n", - "\n", - "$$\n", - "\\langle\\hat{H}\\rangle_\\psi =\n", - "\\sum_{k=0}^{4^n-1} w_k \\sum_{j=0}^{2^n-1}p_{kj} \\lambda_{kj},\n", - "$$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "dcffb899-ef2d-4dca-b2a7-11c197d45ed0", - "metadata": { - "gloss": { - "sampling": { - "text": "The process of taking several measurements of one or several things.", - "title": "Sampling" - } - } - }, - "source": [ - "Since calculating the expectation value of $4^n$ Paulis is impractical (that is, exponentially growing), `Estimator` can only be efficient when a large amount of $w_k$ are zero (that is, *sparse* Pauli decomposition instead of *dense*). Formally we say that, for this computation to be *efficiently solvable*, the number of non-zero terms has to grow at most polynomially with the number of qubits $n$: $\\hat{H} = \\sum^{\\text{Poly}(n)}_k w_k \\hat{P}_k.$\n", - "\n", - "The reader may notice the implicit assumption that probability sampling also needs to be efficient as explained for `Sampler`, which means\n", - "\n", - "$$\n", - "\\langle\\hat{H}\\rangle_\\psi =\n", - "\\sum_{k}^{\\text{Poly}(n)} w_k \\sum_{j}^{\\text{Poly}(n)}p_{kj} \\lambda_{kj}.\n", - "$$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "c1547f3e-3c05-46d9-be5a-3e7e4141dc92", - "metadata": {}, - "source": [ - "### Guided example to calculate expectation values\n", - "\n", - "Let's assume the single-qubit state $|+\\rangle := H|0\\rangle = \\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$, and observable\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\hat{H}\n", - "& = \\begin{pmatrix}\n", - "-1 & 2 \\\\\n", - "2 & 1 \\\\\n", - "\\end{pmatrix}\\\\[1mm]\n", - "& = 2X - Z\n", - "\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "with the following theoretical expectation value $\\langle\\hat{H}\\rangle_+ = \\langle+|\\hat{H}|+\\rangle = 2.$\n", - "\n", - "Since we do not know how to measure this observable, we cannot compute its expectation value directly, and we need to re-express it as $\\langle\\hat{H}\\rangle_+ = 2\\langle X \\rangle_+ - \\langle Z \\rangle_+ $. Which can be shown to evaluate to the same result by virtue of noting that $\\langle+|X|+\\rangle = 1$, and $\\langle+|Z|+\\rangle = 0$.\n", - "\n", - "Let see how to compute $\\langle X \\rangle_+$ and $\\langle Z \\rangle_+$ directly. Since $X$ and $Z$ do not commute (that is, they don't share the same eigenbasis), they cannot be measured simultaneously, therefore we need the auxiliary circuits:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7c32e970-f9cb-48ec-a60b-2dccb0744141", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "# The following code will work for any other initial single-qubit state and observable\n", - "original_circuit = QuantumCircuit(1)\n", - "original_circuit.h(0)\n", - "\n", - "H = SparsePauliOp([\"X\", \"Z\"], [2, -1])\n", - "\n", - "aux_circuits = []\n", - "for pauli in H.paulis:\n", - " aux_circ = original_circuit.copy()\n", - " aux_circ.barrier()\n", - " if str(pauli) == \"X\":\n", - " aux_circ.h(0)\n", - " elif str(pauli) == \"Y\":\n", - " aux_circ.sdg(0)\n", - " aux_circ.h(0)\n", - " else:\n", - " aux_circ.id(0)\n", - " aux_circ.measure_all()\n", - " aux_circuits.append(aux_circ)\n", - "\n", - "original_circuit.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "ec3cf7af-66b2-4ea9-94d7-6ec6ba2bfc5e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Auxiliary circuit for X\n", - "aux_circuits[0].draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "678d3bd0-d8fd-4767-b0a6-77cae945810a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Auxiliary circuit for Z\n", - "aux_circuits[1].draw(\"mpl\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "beef97f0-c968-4a3f-9b7b-fed39b88cbe3", - "metadata": {}, - "source": [ - "We can now carry out the computation manually using `Sampler` and check the results on `Estimator`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d59dffe0-85cb-4849-8421-03bbcc9668df", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sampler results:\n", - " >> Expected value of X: 1.00000\n", - " >> Expected value of Z: 0.00420\n", - " >> Total expected value: 1.99580\n", - "Estimator results:\n", - " >> Expected value of X: 1.00000\n", - " >> Expected value of Z: 0.00000\n", - " >> Total expected value: 2.00000\n" - ] - } - ], - "source": [ - "from qiskit.primitives import StatevectorSampler, StatevectorEstimator\n", - "from qiskit.result import QuasiDistribution\n", - "import numpy as np\n", - "\n", - "\n", - "## SAMPLER\n", - "shots = 10000\n", - "sampler = StatevectorSampler()\n", - "job = sampler.run(aux_circuits, shots=shots)\n", - "\n", - "# Run the sampler job and step through results\n", - "expvals = []\n", - "for index, pauli in enumerate(H.paulis):\n", - " data_pub = job.result()[index].data\n", - " bitstrings = data_pub.meas.get_bitstrings()\n", - " counts = data_pub.meas.get_counts()\n", - " quasi_dist = QuasiDistribution(\n", - " {outcome: freq / shots for outcome, freq in counts.items()}\n", - " )\n", - "\n", - " # Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation value.\n", - " val = 0\n", - "\n", - " if str(pauli) == \"X\":\n", - " val += -1 * quasi_dist.get(1, 0)\n", - " val += 1 * quasi_dist.get(0, 0)\n", - "\n", - " if str(pauli) == \"Y\":\n", - " val += -1 * quasi_dist.get(1, 0)\n", - " val += 1 * quasi_dist.get(0, 0)\n", - "\n", - " if str(pauli) == \"Z\":\n", - " val += 1 * quasi_dist.get(0, 0)\n", - " val += -1 * quasi_dist.get(1, 0)\n", - "\n", - " expvals.append(val)\n", - "\n", - "# Print expectation values\n", - "\n", - "print(\"Sampler results:\")\n", - "for pauli, expval in zip(H.paulis, expvals):\n", - " print(f\" >> Expected value of {str(pauli)}: {expval:.5f}\")\n", - "\n", - "total_expval = np.sum(H.coeffs * expvals).real\n", - "print(f\" >> Total expected value: {total_expval:.5f}\")\n", - "\n", - "# Use estimator for comparison\n", - "observables = [\n", - " *H.paulis,\n", - " H,\n", - "] # Note: run for individual Paulis as well as full observable H\n", - "\n", - "estimator = StatevectorEstimator()\n", - "job = estimator.run([(original_circuit, observables)])\n", - "estimator_expvals = job.result()[0].data.evs\n", - "\n", - "# Print results\n", - "print(\"Estimator results:\")\n", - "for obs, expval in zip(observables, estimator_expvals):\n", - " if obs is not H:\n", - " print(f\" >> Expected value of {str(obs)}: {expval:.5f}\")\n", - " else:\n", - " print(f\" >> Total expected value: {expval:.5f}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "44f13c37-50e3-426c-90cb-f730a81e20d1", - "metadata": {}, - "source": [ - "### Mathematical rigor (optional)\n", - "\n", - "Expressing $|\\psi\\rangle$ with respect to the basis of eigenstates of $\\hat{H}$, $|\\psi\\rangle = \\sum_\\lambda a_\\lambda |\\lambda\\rangle$, it follows:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\langle \\psi | \\hat{H} | \\psi \\rangle\n", - "& = \\bigg(\\sum_{\\lambda'}a^*_{\\lambda'} \\langle \\lambda'|\\bigg) \\hat{H}\n", - " \\bigg(\\sum_{\\lambda} a_\\lambda | \\lambda\\rangle\\bigg)\\\\[1mm]\n", - "\n", - "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda}\n", - " \\langle \\lambda'|\\hat{H}| \\lambda\\rangle\\\\[1mm]\n", - "\n", - "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda} \\lambda\n", - "\\langle \\lambda'| \\lambda\\rangle\\\\[1mm]\n", - "\n", - "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda} \\lambda\n", - "\\cdot \\delta_{\\lambda, \\lambda'}\\\\[1mm]\n", - "\n", - "& = \\sum_\\lambda |a_\\lambda|^2 \\lambda\\\\[1mm]\n", - "\n", - "& = \\sum_\\lambda p_\\lambda \\lambda\\\\[1mm]\n", - "\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6a3bea3d-a2f6-4033-85f9-9a10c5d5c64c", - "metadata": { - "gloss": { - "hermitian": { - "text": "A hermitian is a square matrix that is equal to its own conjugate transpose, or a linear operator that is self-adjoint.", - "title": "Hermitian" - } - } - }, - "source": [ - "Since we do not know the eigenvalues or eigenstates of the target observable $\\hat{H}$, first we need to consider its diagonalization. Given that $\\hat{H}$ is Hermitian, there exists a unitary transformation $V$ such that $\\hat{H}=V^\\dagger \\Lambda V,$ where $\\Lambda$ is the diagonal eigenvalue matrix, so $\\langle j | \\Lambda | k \\rangle = 0$ if $j\\neq k$, and $\\langle j | \\Lambda | j \\rangle = \\lambda_j$.\n", - "\n", - "This implies that the expected value can be rewritten as:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\langle\\psi|\\hat{H}|\\psi\\rangle\n", - "& = \\langle\\psi|V^\\dagger \\Lambda V|\\psi\\rangle\\\\[1mm]\n", - "\n", - "& = \\langle\\psi|V^\\dagger \\bigg(\\sum_{j=0}^{2^n-1} |j\\rangle\n", - "\\langle j|\\bigg) \\Lambda \\bigg(\\sum_{k=0}^{2^n-1} |k\\rangle \\langle k|\\bigg) V|\\psi\\rangle\\\\[1mm]\n", - "\n", - "& = \\sum_{j=0}^{2^n-1} \\sum_{k=0}^{2^n-1}\\langle\\psi|V^\\dagger |j\\rangle\n", - "\\langle j| \\Lambda |k\\rangle \\langle k| V|\\psi\\rangle\\\\[1mm]\n", - "\n", - "& = \\sum_{j=0}^{2^n-1}\\langle\\psi|V^\\dagger |j\\rangle\n", - "\\langle j| \\Lambda |j\\rangle \\langle j| V|\\psi\\rangle\\\\[1mm]\n", - "\n", - "& = \\sum_{j=0}^{2^n-1}|\\langle j| V|\\psi\\rangle|^2 \\lambda_j\\\\[1mm]\n", - "\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Given that if a system is in the state $|\\phi\\rangle = V |\\psi\\rangle$ the probability of measuring $| j\\rangle$ is $p_j = |\\langle j|\\phi \\rangle|^2$, the above expected value can be expressed as:\n", - "\n", - "$$\n", - "\\langle\\psi|\\hat{H}|\\psi\\rangle =\n", - "\\sum_{j=0}^{2^n-1} p_j \\lambda_j.\n", - "$$\n", - "\n", - "It is very important to note that the probabilities are taken from the state $V |\\psi\\rangle$ instead of $|\\psi\\rangle$. This is why the matrix $V$ is absolutely necessary." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "c21a9a05-6e07-45dc-a090-4414dfdf2521", - "metadata": {}, - "source": [ - "You might be wondering how to obtain the matrix $V$ and the eigenvalues $\\Lambda$. If you already had the eigenvalues, then there would be no need to use a quantum computer since the goal of variational algorithms is to find these eigenvalues of $\\hat{H}$.\n", - "\n", - "Fortunately, there is a way around that: any $2^n \\times 2^n$ matrix can be written as a linear combination of $4^n$ tensor products of $n$ Pauli matrices and identities, all of which are both hermitian and unitary with known $V$ and $\\Lambda$. This is what Runtime's `Estimator` does internally by decomposing any [`Operator`](/docs/api/qiskit/qiskit.quantum_info.Operator) object into a [`SparsePauliOp`](/docs/api/qiskit/qiskit.quantum_info.SparsePauliOp).\n", - "\n", - "Here are the Operators that can be used:\n", - "\n", - "$$\n", - "\\begin{array}{c|c|c|c}\n", - " \\text{Operator} & \\sigma & V & \\Lambda \\\\[1mm]\n", - " \\hline\n", - " I & \\sigma_0 = \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix} & V_0 = I & \\Lambda_0 = I = \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix} \\\\[4mm]\n", - "\n", - " X & \\sigma_1 = \\begin{pmatrix} 0 & 1 \\\\ 1 & 0 \\end{pmatrix} & V_1 = H =\\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix} & \\Lambda_1 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\\\[4mm]\n", - "\n", - " Y & \\sigma_2 = \\begin{pmatrix} 0 & -i \\\\ i & 0 \\end{pmatrix} & V_2 = HS^\\dagger =\\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix}\\cdot \\begin{pmatrix} 1 & 0 \\\\ 0 & -i \\end{pmatrix} = \\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & -i \\\\ 1 & i \\end{pmatrix}\\quad & \\Lambda_2 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\\\[4mm]\n", - "\n", - " Z & \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} & V_3 = I & \\Lambda_3 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix}\n", - "\\end{array}\n", - "$$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "9e1c0933-5fbe-466d-9114-92d66d21785f", - "metadata": {}, - "source": [ - "So let's rewrite $\\hat{H}$ with respect to the Paulis and identities:\n", - "\n", - "$$\n", - "\\hat{H} =\n", - "\\sum_{k_{n-1}=0}^3...\n", - "\\sum_{k_0=0}^3 w_{k_{n-1}...k_0}\n", - "\\sigma_{k_{n-1}}\\otimes ... \\otimes \\sigma_{k_0} = \\sum_{k=0}^{4^n-1} w_k \\hat{P}_k,\n", - "$$\n", - "\n", - "where $k = \\sum_{l=0}^{n-1} 4^l k_l \\equiv k_{n-1}...k_0$ for $k_{n-1},...,k_0\\in \\{0,1,2,3\\}$ (that is, base $4$), and $\\hat{P}_{k} := \\sigma_{k_{n-1}}\\otimes ... \\otimes \\sigma_{k_0}$:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\langle\\psi|\\hat{H}|\\psi\\rangle\n", - "& = \\sum_{k=0}^{4^n-1} w_k\n", - "\\sum_{j=0}^{2^n-1}|\\langle j| V_k|\\psi\\rangle|^2 \\langle j| \\Lambda_k |j\\rangle \\\\[1mm]\n", - "\n", - "& = \\sum_{k=0}^{4^n-1} w_k \\sum_{j=0}^{2^n-1}p_{kj} \\lambda_{kj}, \\\\[1mm]\n", - "\n", - "\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "\n", - "where $V_k := V_{k_{n-1}}\\otimes ... \\otimes V_{k_0}$ and $\\Lambda_k := \\Lambda_{k_{n-1}}\\otimes ... \\otimes \\Lambda_{k_0}$, such that: $\\hat{P_k}=V_k^\\dagger \\Lambda_k V_k.$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "42f771a0-486d-4a5f-9df1-a1720d7bf252", - "metadata": { - "gloss": { - "oracle": { - "text": " A hypothetical device or software component that performs a specific function, but the internal workings of which are unknown. A user is only aware of the inputs and outputs of the black box, and has no knowledge of how the black box processes the inputs to produce the outputs.", - "title": "Black-box oracle" - } - } - }, - "source": [ - "## Cost functions\n", - "\n", - "In general, cost functions are used to describe the goal of a problem and how well a trial state is performing with respect to that goal. This definition can be applied to various examples in chemistry, machine learning, finance, optimization, and so on.\n", - "\n", - "Let's consider a simple example of finding the ground state of a system. Our objective is to minimize the expectation value of the observable representing energy (Hamiltonian $\\hat{\\mathcal{H}}$):\n", - "\n", - "$$\n", - "\\min_{\\vec\\theta} \\langle\\psi(\\vec\\theta)|\\hat{\\mathcal{H}}|\\psi(\\vec\\theta)\\rangle\n", - "$$\n", - "\n", - "We can use the `Estimator` to evaluate the expectation value and pass this value to an optimizer to minimize. If the optimization is successful, it will return a set of optimal parameter values $\\vec\\theta^*$, from which we will be able to construct the proposed solution state $|\\psi(\\vec\\theta^*)\\rangle$ and compute the observed expectation value as $C(\\vec\\theta^*)$.\n", - "\n", - "Notice how we will only be able to minimize the cost function for the limited set of states that we are considering. This leads us to two separate possibilities:\n", - "\n", - "- **Our ansatz does not define the solution state across the search space**: If this is the case, our optimizer will never find the solution, and we need to experiment with other ansatzes that might be able to represent our search space more accurately.\n", - "- **Our optimizer is unable to find this valid solution**: Optimization can be globally defined and locally defined. We'll explore what this means in the later section.\n", - "\n", - "All in all, we will be performing a classical optimization loop but relying on the evaluation of the cost function to a quantum computer. From this perspective, one could think of the optimization as a purely classical endeavor where we call some black-box quantum oracle each time the optimizer needs to evaluate the cost function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "650faaab-b964-4587-a39f-d6cc80b225b2", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func_vqe(params, circuit, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (Estimator): Estimator primitive instance\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (circuit, hamiltonian, params)\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - " return cost" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a01a54f2-da16-4008-b422-fa7ded531f67", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import TwoLocal\n", - "\n", - "observable = SparsePauliOp.from_list([(\"XX\", 1), (\"YY\", -3)])\n", - "\n", - "reference_circuit = QuantumCircuit(2)\n", - "reference_circuit.x(0)\n", - "\n", - "variational_form = TwoLocal(\n", - " 2,\n", - " rotation_blocks=[\"rz\", \"ry\"],\n", - " entanglement_blocks=\"cx\",\n", - " entanglement=\"linear\",\n", - " reps=1,\n", - ")\n", - "ansatz = reference_circuit.compose(variational_form)\n", - "\n", - "theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()\n", - "ansatz.decompose().draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "7357e05e-f68a-4cd5-bf0d-77eab963a86d", - "metadata": {}, - "source": [ - "We will first carry this out using a simulator: the StatevectorEstimator. This is usually advisable for debugging, but we will immediately follow the debugging run with a calculation on real quantum hardware. Increasingly, problems of interest are no longer classically simulable without state-of-the-art supercomputing facilities." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "883bcb28-15d0-4046-a654-f43d9480a642", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-0.58744589]\n" - ] - } - ], - "source": [ - "estimator = StatevectorEstimator()\n", - "cost = cost_func_vqe(theta_list, ansatz, observable, estimator)\n", - "print(cost)" - ] - }, - { - "cell_type": "markdown", - "id": "1a94eba3-146d-4eee-970d-9a966d1e15f3", - "metadata": {}, - "source": [ - "We will now proceed with running on a real quantum computer. Note the syntax changes. The steps involving the pass_manager will be discussed further in the next example. One step of particular importance in variational algorithms is the use of a Qiskit Runtime session. Starting a session allows you to run multiple iterations of a variational algorithm without waiting in a new queue each time parameters are updated. This is important if queue times are long and/or many iterations are needed. Only partners in the IBM Quantum® Network can use Runtime sessions. If you do not have access to sessions, you can reduce the number of iterations you submit at a given time, and save the most recent parameters for use in future runs. If you submit too many iterations or encounter queue times that are too long, you may encounter error code 1217, which refers to long delays between job submissions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4ca4722-46ab-418f-8a1f-740a65e92eb9", - "metadata": {}, - "outputs": [], - "source": [ - "# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor\n", - "# Load necessary packages:\n", - "\n", - "from qiskit_ibm_runtime import (\n", - " QiskitRuntimeService,\n", - " Session,\n", - " EstimatorOptions,\n", - " EstimatorV2 as Estimator,\n", - ")\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "# Select the least busy backend:\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", - ")\n", - "# Or get a specific backend:\n", - "# backend = service.backend(\"ibm_brisbane\")\n", - "\n", - "# Use a pass manager to transpile the circuit and observable for the specific backend being used:\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_ansatz = pm.run(ansatz)\n", - "isa_observable = observable.apply_layout(layout=isa_ansatz.layout)\n", - "\n", - "\n", - "# Set estimator options\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", - "\n", - "# Open a Runtime session:\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - " cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)\n", - "\n", - "session.close()\n", - "print(cost)" - ] - }, - { - "cell_type": "markdown", - "id": "95dc0e83-46ce-4839-91c3-af550aef9ddf", - "metadata": {}, - "source": [ - "Note that the values obtained from the two calculations above are very similar. Techniques for improving results will be discussed further below." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7f3951a3-47ad-4ddb-90a0-2218b1e11d15", - "metadata": {}, - "source": [ - "### Example mapping to non-physical systems\n", - "\n", - "The maximum cut (max-cut) problem is a combinatorial optimization problem that involves dividing the vertices of a graph into two disjoint sets such that the number of edges between the two sets is maximized. More formally, given an undirected graph $G=(V,E)$, where $V$ is the set of vertices and $E$ is the set of edges, the max-cut problem asks to partition the vertices into two disjoint subsets, $S$ and $T$, such that the number of edges with one endpoint in $S$ and the other in $T$ is maximized.\n", - "\n", - "We can apply max-cut to solve a various problems including: clustering, network design, phase transitions, and so on. We'll start by creating a problem graph:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "739ca7a0-fae3-4389-a8de-7ae93c13b9e1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import rustworkx as rx\n", - "from rustworkx.visualization import mpl_draw\n", - "\n", - "n = 4\n", - "G = rx.PyGraph()\n", - "G.add_nodes_from(range(n))\n", - "# The edge syntax is (start, end, weight)\n", - "edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]\n", - "G.add_edges_from(edges)\n", - "\n", - "mpl_draw(\n", - " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=\"#1192E8\"\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3246e173-b1a8-48c2-bce8-f80a1524bcfb", - "metadata": {}, - "source": [ - "This problem can be expressed as a binary optimization problem. For each node $0 \\leq i < n$, where $n$ is the number of nodes of the graph (in this case $n=4$), we will consider the binary variable $x_i$. This variable will have the value $1$ if node $i$ is one of the groups that we'll label $1$ and $0$ if it's in the other group, that we'll label as $0$. We will also denote as $w_{ij}$ (element $(i,j)$ of the adjacency matrix $w$) the weight of the edge that goes from node $i$ to node $j$. Because the graph is undirected, $w_{ij}=w_{ji}$. Then we can formulate our problem as maximizing the following cost function:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "C(\\vec{x})\n", - "& =\\sum_{i,j=0}^n w_{ij} x_i(1-x_j)\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i,j=0}^n w_{ij} x_ix_j\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i=0}^n \\sum_{j=0}^i 2w_{ij} x_ix_j\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "\n", - "To solve this problem with a quantum computer, we are going to express the cost function as the expected value of an observable. However, the observables that Qiskit admits natively consist of Pauli operators, that have eigenvalues $1$ and $-1$ instead of $0$ and $1$. That's why we are going to make the following change of variable:\n", - "\n", - "Where $\\vec{x}=(x_0,x_1,\\cdots ,x_{n-1})$. We can use the adjacency matrix $w$ to comfortably access the weights of all the edge. This will be used to obtain our cost function:\n", - "\n", - "$$\n", - "z_i = 1-2x_i \\rightarrow x_i = \\frac{1-z_i}{2}\n", - "$$\n", - "\n", - "This implies that:\n", - "\n", - "$$\n", - "\\begin{array}{lcl} x_i=0 & \\rightarrow & z_i=1 \\\\ x_i=1 & \\rightarrow & z_i=-1.\\end{array}\n", - "$$\n", - "\n", - "So the new cost function we want to maximize is:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "C(\\vec{z})\n", - "& = \\sum_{i,j=0}^n w_{ij} \\bigg(\\frac{1-z_i}{2}\\bigg)\\bigg(1-\\frac{1-z_j}{2}\\bigg)\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n \\frac{w_{ij}}{4} - \\sum_{i,j=0}^n \\frac{w_{ij}}{4} z_iz_j\\\\[1mm]\n", - "\n", - "& = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Moreover, the natural tendency of a quantum computer is to find minima (usually the lowest energy) instead of maxima so instead of maximizing $C(\\vec{z})$ we are going to minimize:\n", - "\n", - "$$\n", - "-C(\\vec{z}) = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", - "$$\n", - "\n", - "Now that we have a cost function to minimize whose variables can have the values $-1$ and $1$, we can make the following analogy with the Pauli $Z$:\n", - "\n", - "$$\n", - "z_i \\equiv Z_i = \\overbrace{I}^{n-1}\\otimes ... \\otimes \\overbrace{Z}^{i} \\otimes ... \\otimes \\overbrace{I}^{0}\n", - "$$\n", - "\n", - "In other words, the variable $z_i$ will be equivalent to a $Z$ gate acting on qubit $i$. Moreover:\n", - "\n", - "$$\n", - "Z_i|x_{n-1}\\cdots x_0\\rangle = z_i|x_{n-1}\\cdots x_0\\rangle \\rightarrow \\langle x_{n-1}\\cdots x_0 |Z_i|x_{n-1}\\cdots x_0\\rangle = z_i\n", - "$$\n", - "\n", - "Then the observable we are going to consider is:\n", - "\n", - "$$\n", - "\\hat{H} = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} Z_iZ_j\n", - "$$\n", - "\n", - "to which we will have to add the independent term afterwards:\n", - "\n", - "$$\n", - "\\texttt{offset} = - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "83c86ba1-8b9d-4222-bb7e-637e6f29e5cd", - "metadata": {}, - "source": [ - "The operator is a linear combination of terms with Z operators on nodes connected by an edge (recall that the 0th qubit is farthest right): $IIZZ + IZIZ + IZZI + ZIIZ + ZZII$. Once the operator is constructed, the ansatz for the QAOA algorithm can easily be built by using the `QAOAAnsatz` circuit from the Qiskit circuit library." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "29398315-3baf-4363-88fc-69b6438e4afa", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import QAOAAnsatz\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "hamiltonian = SparsePauliOp.from_list(\n", - " [(\"IIZZ\", 1), (\"IZIZ\", 1), (\"IZZI\", 1), (\"ZIIZ\", 1), (\"ZZII\", 1)]\n", - ")\n", - "\n", - "\n", - "ansatz = QAOAAnsatz(hamiltonian, reps=2)\n", - "# Draw\n", - "ansatz.decompose(reps=3).draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "91d02379-9da6-426b-a5f0-d1fb75a891ee", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Offset: -2.5\n" - ] - } - ], - "source": [ - "# Sum the weights, and divide by 2\n", - "\n", - "offset = -sum(edge[2] for edge in edges) / 2\n", - "print(f\"\"\"Offset: {offset}\"\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "84a98a08-00e4-4b05-a3f0-de29a7e73a60", - "metadata": {}, - "source": [ - "With the Runtime Estimator directly taking a Hamiltonian and parameterized ansatz, and returning the necessary energy, The cost function for a QAOA instance is quite simple:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "73ac80dd-87cf-4a43-b19b-e89a4782a44a", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (Estimator): Estimator primitive instance\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, hamiltonian, params)\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", - " return cost" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "fc6af87b-a838-43b8-96ce-0af790fe0172", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.473098768180865\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)\n", - "\n", - "estimator = StatevectorEstimator()\n", - "cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)\n", - "print(cost)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "389b7e86-cd61-4184-96c3-ae91b8ad59f5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.1120776913677988\n" - ] - } - ], - "source": [ - "# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24\n", - "# Load some necessary packages:\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator\n", - "\n", - "# Select the least busy backend:\n", - "\n", - "backend = service.least_busy(\n", - " operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", - ")\n", - "\n", - "# Or get a specific backend:\n", - "# backend = service.backend(\"ibm_brisbane\")\n", - "\n", - "# Use a pass manager to transpile the circuit and observable for the specific backend being used:\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", - "isa_ansatz = pm.run(ansatz)\n", - "isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)\n", - "\n", - "# Set estimator options\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", - "\n", - "# Open a Runtime session:\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - " cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)\n", - "\n", - "# Close session after done\n", - "session.close()\n", - "print(cost)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "16c94186-366c-4ac4-8745-4e7da283f8da", - "metadata": {}, - "source": [ - "We will revisit this example in Applications to explore how to leverage an optimizer to iterate through the search space. Generally speaking, this includes:\n", - "\n", - "- Leveraging an optimizer to find optimal parameters\n", - "- Binding optimal parameters to the ansatz to find the eigenvalues\n", - "- Translating the eigenvalues to our problem definition" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d14d5800-24c1-4e4b-b692-9bdb9ef4440f", - "metadata": {}, - "source": [ - "## Measurement strategy: speed versus accuracy\n", - "\n", - "As mentioned, we are using a noisy quantum computer as a *black-box oracle*, where noise can make the retrieved values non-deterministic, leading to random fluctuations which, in turn, will harm — or even completely prevent — convergence of certain optimizers to a proposed solution. This is a general problem that we must address as we incrementally explore quantum utility and progress towards quantum advantage:\n", - "\n", - "![A graph showing how simulation cost varies with circuit complexity. Using a classical computer it grows exponentially. With quantum error mitigation, there should be a crossover at which that becomes advantageous. Quantum error correction allows for linear growth of the simulation cost and will certainly lead to advantage.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-path-to-quantum-advantage.svg)\n", - "\n", - "We can use Qiskit Runtime Primitive's error suppression and error mitigation options to address noise and maximize the utility of today's quantum computers." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f23f0d82-d4f9-4485-aad3-cd2f8fd19b4f", - "metadata": { - "gloss": { - "overhead": { - "text": "Extra costs introduced by new techniques, relative to a base implementation.", - "title": "Overhead" - } - } - }, - "source": [ - "### Error Suppression\n", - "\n", - "[Error suppression](/docs/guides/error-mitigation-and-suppression-techniques) refers to techniques used to optimize and transform a circuit during compilation in order to minimize errors. This is a basic error handling technique that usually results in some classical pre-processing overhead to the overall runtime. The overhead includes transpiling circuits to run on quantum hardware by:\n", - "\n", - "- Expressing the circuit using the native gates available on a quantum system\n", - "- Mapping the virtual qubits to physical qubits\n", - "- Adding SWAPs based on connectivity requirements\n", - "- Optimizing 1Q and 2Q gates\n", - "- Adding dynamical decoupling to idle qubits to prevent the effects of decoherence.\n", - "\n", - "\n", - "Primitives allow for the use of error suppression techniques by setting the `optimization_level` option and selecting advanced transpilation options. In a later course, we will delve into different circuit construction methods to improve results, but for most cases, we recommend setting `optimization_level=3`.\n", - "\n", - "We will visualize the value of increasing optimization in the transpilation process by looking at an example circuit with a simple ideal behavior." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "9e0bf552-32b0-477e-9ac3-4ab643c30623", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit import Parameter, QuantumCircuit\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "theta = Parameter(\"theta\")\n", - "\n", - "qc = QuantumCircuit(2)\n", - "qc.x(1)\n", - "qc.h(0)\n", - "qc.cp(theta, 0, 1)\n", - "qc.h(0)\n", - "observables = SparsePauliOp.from_list([(\"ZZ\", 1)])\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "1c2b0408-e004-4a04-8aa5-cf519744082b", - "metadata": {}, - "source": [ - "The circuit above can yield sinusoidal expectation values of the observable given, provided we insert phases spanning an appropriate interval, such as $[0,2\\pi]$." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "99b23f88-ddcb-45dc-a86d-3d48ff990319", - "metadata": {}, - "outputs": [], - "source": [ - "## Setup phases\n", - "import numpy as np\n", - "\n", - "phases = np.linspace(0, 2 * np.pi, 50)\n", - "\n", - "# phases need to be expressed as a list of lists in order to work\n", - "individual_phases = [[phase] for phase in phases]" - ] - }, - { - "cell_type": "markdown", - "id": "ca040383-9c8c-4a38-8e40-bad1b26c6085", - "metadata": {}, - "source": [ - "We can use a simulator to show the usefulness of an optimized transpilation. We will return below to using real hardware to demonstrate the usefulness of error mitigation. We will use QiskitRuntimeService to get a real backend (in this case, ibm_brisbane), and use AerSimulator to simulate that backend, including its noise behavior." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f0e6f32-02f7-471f-a9f3-f30d22f8cca4", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_aer import AerSimulator\n", - "\n", - "# get a real backend from the runtime service\n", - "service = QiskitRuntimeService()\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "\n", - "# generate a simulator that mimics the real quantum system with the latest calibration results\n", - "backend_sim = AerSimulator.from_backend(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "38df43e8-b11e-4671-b8fb-7a2eb74f648a", - "metadata": {}, - "source": [ - "We can now use a pass manager to transpile the circuit into the \"instruction set architecture\" or ISA of the backend. This is a new requirement in Qiskit Runtime: all circuits submitted to a backend must conform to the constraints of the backend’s target, meaning they must be written in terms of the backend's ISA — that is, the set of instructions the device can understand and execute. These target constraints are defined by factors like the device’s native basis gates, its qubit connectivity, and - when relevant - its pulse and other instruction timing specifications.\n", - "\n", - "Note that in the present case, we will do this twice: once with optimization_level = 0, and once with it set to 3. Each time we will use the Estimator primitive to estimate the expectation values of the observable at different values of phase." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4dc699cd-0bb4-4b48-adcc-0bbe99218a26", - "metadata": {}, - "outputs": [], - "source": [ - "# Import estimator and specify that we are using the simulated backend:\n", - "\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "estimator = Estimator(mode=backend_sim)\n", - "\n", - "circuit = qc" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7990eb62-879d-4beb-b452-f3d86b164d11", - "metadata": {}, - "outputs": [], - "source": [ - "# Use a pass manager to transpile the circuit and observable for the backend being simulated.\n", - "# Start with no optimization:\n", - "\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)\n", - "isa_circuit = pm.run(circuit)\n", - "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", - "\n", - "noisy_exp_values = []\n", - "pub = (isa_circuit, isa_observables, [individual_phases])\n", - "cost = estimator.run([pub]).result()[0].data.evs\n", - "noisy_exp_values = cost[0]\n", - "\n", - "# Repeat above steps, but now with optimization = 3:\n", - "\n", - "exp_values_with_opt_es = []\n", - "pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)\n", - "isa_circuit = pm.run(circuit)\n", - "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", - "\n", - "pub = (isa_circuit, isa_observables, [individual_phases])\n", - "cost = estimator.run([pub]).result()[0].data.evs\n", - "exp_values_with_opt_es = cost[0]" - ] - }, - { - "cell_type": "markdown", - "id": "0cc8d4ea-81ef-4146-b8ed-9c7edae72be3", - "metadata": {}, - "source": [ - "Finally, we can plot the results, and we see that the precision of the calculation was fairly good even without optimization, but it definitely improved by increasing optimization to level 3. Note that in deeper, more complicated circuits, the difference between optimization levels of 0 and 3 are likely to be more significant. This is a very simple circuit used as a toy model." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "f3d3f805-f8d0-474d-9a88-2d1f164639b0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.plot(phases, noisy_exp_values, \"o\", label=\"opt=0\")\n", - "plt.plot(phases, exp_values_with_opt_es, \"o\", label=\"opt=3\")\n", - "plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label=\"ideal\")\n", - "plt.ylabel(\"Expectation\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2394d082-2278-48cf-95b0-3363ed723ec1", - "metadata": { - "gloss": { - "bias": { - "text": "A systematic drift in the measured quantities, usually caused by errors.", - "title": "Bias" - } - } - }, - "source": [ - "### Error Mitigation\n", - "\n", - "[Error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) refers to techniques that allow users to reduce circuit errors by modeling the device noise at the time of execution. Typically, this results in quantum pre-processing overhead related to model training and classical post-processing overhead to mitigate errors in the raw results by using the generated model.\n", - "\n", - "The Qiskit Runtime primitive's `resilience_level` option specifies the amount of resilience to build against errors. Higher levels generate more accurate results at the expense of longer processing times due to quantum sampling overhead. Resilience levels can be used to configure the trade-off between cost and accuracy when applying error mitigation to your primitive query.\n", - "\n", - "When implementing any error mitigation technique, we expect the bias in our results to be reduced with respect to the previous, unmitigated bias. In some cases, the bias may even disappear. However, this comes at a cost. As we reduce the bias in our estimated quantities, the statistical variability will increase (that is, variance), which we can account for by further increasing the number of shots per circuit in our sampling process. This will introduce overhead beyond that needed to reduce the bias, so it is not done by default. We can easily opt-in to this behavior by adjusting the number of shots per circuit in options.executions.shots, as shown in the example below.\n", - "\n", - "![A diagram showing broader or narrowing distributions as in the bias/variance tradeoff.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-bias-variance-trade-off.svg)\n", - "\n", - "For this course, we will explore these error mitigation models at a high level to illustrate the error mitigation that Qiskit Runtime primitives can perform without requiring full implementation details.\n", - "\n", - "\n", - "### Twirled readout error extinction (T-REx)\n", - "\n", - "Twirled readout error extinction (T-REx) uses a technique known as Pauli twirling to reduce the noise introduced during the process of quantum measurement. This technique assumes no specific form of noise, which makes it very general and effective.\n", - "\n", - "Overall workflow:\n", - "\n", - "1. Acquire data for the zero state with randomized bit flips (Pauli X before measurement)\n", - "2. Acquire data for the desired (noisy) state with randomized bit flips (Pauli X before measurement)\n", - "3. Compute the special function for each data set, and divide.\n", - "\n", - " \n", - "\n", - "![A diagram showing measurement and calibration circuits for T-REX.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-trex-data-collection.svg)\n", - "\n", - "We can set this with `options.resilience_level = 1`, demonstrated in the example below.\n", - "\n", - "### Zero noise extrapolation\n", - "\n", - "Zero noise extrapolation (ZNE) works by first amplifying the noise in the circuit that is preparing the desired quantum state, obtaining measurements for several different levels of noise, and using those measurements to infer the noiseless result.\n", - "\n", - "Overall workflow:\n", - "\n", - "1. Amplify circuit noise for several noise factors\n", - "2. Run every noise amplified circuit\n", - "3. Extrapolate back to the zero noise limit\n", - "\n", - " \n", - "\n", - "![A diagram showing steps in ZNE. Noise is artificially amplified by different factors. Then the values are extrapolated to what they should be at zero noise.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-zne-stages.svg)\n", - "\n", - "We can set this with `options.resilience_level = 2`. We can optimize this further by exploring a variety of `noise_factors`, `noise_amplifiers`, and `extrapolators`, but this is outside the scope of this course. We encourage you to experiment with these [options as described here](/docs/guides/error-mitigation-and-suppression-techniques).\n", - "\n", - "Each method comes with its own associated overhead: a trade-off between the number of quantum computations needed (time) and the accuracy of our results:\n", - "\n", - "$$\n", - "\\begin{array}{c|c|c|c}\n", - " \\text{Methods} & R=1 \\text{, T-REx} & R=2 \\text{, ZNE} \\\\[1mm]\n", - " \\hline\n", - " \\text{Assumptions} & \\text{None} & \\text{Ability to scale noise} \\\\[1mm]\n", - " \\text{Qubit overhead} & 1 & 1 \\\\[1mm]\n", - " \\text{Sampling overhead} & 2 & N_{\\text{noise-factors}} \\\\[1mm]\n", - " \\text{Bias} & 0 & \\mathcal{O}(\\lambda^{N_{\\text{noise-factors}}}) \\\\[1mm]\n", - "\\end{array}\n", - "$$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8b46120f-5312-4450-bf51-13be198428fa", - "metadata": {}, - "source": [ - "### Using Qiskit Runtime's mitigation and suppression options\n", - "\n", - "Here's how to calculate an expectation value while using error mitigation and suppression in Qiskit Runtime. We can make use of precisely the same circuit and observable as before, but this time keeping the optimization level fixed at level 2, and now tuning the _resilience_ or the error mitigation technique(s) being used. This error mitigation process occurs multiple times throughout an optimization loop.\n", - "\n", - "We perform this part on real hardware, since error mitigation is not available on simulators." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57a6b723-6e73-4693-95bf-f0782a8eb854", - "metadata": {}, - "outputs": [], - "source": [ - "# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import (\n", - " Session,\n", - " EstimatorOptions,\n", - " EstimatorV2 as Estimator,\n", - ")\n", - "\n", - "# We select the least busy backend\n", - "\n", - "# Select the least busy backend\n", - "# backend = service.least_busy(\n", - "# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", - "# )\n", - "\n", - "# Or use a specific backend\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "\n", - "# Initialize some variables to save the results from different runs:\n", - "\n", - "exp_values_with_em0_es = []\n", - "exp_values_with_em1_es = []\n", - "exp_values_with_em2_es = []\n", - "\n", - "# Use a pass manager to optimize the circuit and observables for the backend chosen:\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", - "isa_circuit = pm.run(circuit)\n", - "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", - "\n", - "# Open a session and run with no error mitigation:\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - "\n", - " pub = (isa_circuit, isa_observables, [individual_phases])\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - "\n", - "session.close()\n", - "\n", - "exp_values_with_em0_es = cost[0]\n", - "\n", - "# Open a session and run with resilience = 1:\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - "\n", - " pub = (isa_circuit, isa_observables, [individual_phases])\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - "\n", - "session.close()\n", - "\n", - "exp_values_with_em1_es = cost[0]\n", - "\n", - "# Open a session and run with resilience = 2:\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - "\n", - " pub = (isa_circuit, isa_observables, [individual_phases])\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - "\n", - "session.close()\n", - "\n", - "exp_values_with_em2_es = cost[0]" - ] - }, - { - "cell_type": "markdown", - "id": "48215ea6-3045-4880-8671-265e2fb33e5e", - "metadata": {}, - "source": [ - "As before, we can plot the resulting expectation values as a function of phase angle for the three levels of error mitigation used. With great difficulty, one can see that error mitigation improves the results slightly. Again, this effect is much more pronounced in deeper, more complicated circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 93, - "id": "474f1a1a-ee90-468d-9390-fdb455aeb142", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.plot(phases, exp_values_with_em0_es, \"o\", label=\"unmitigated\")\n", - "plt.plot(phases, exp_values_with_em1_es, \"o\", label=\"resil = 1\")\n", - "plt.plot(phases, exp_values_with_em2_es, \"o\", label=\"resil = 2\")\n", - "plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label=\"ideal\")\n", - "plt.ylabel(\"Expectation\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1e683c07-96f3-47d5-8981-8d3aeb8be81f", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "With this lesson, you learned how to create a cost function:\n", - "\n", - "- Create a cost function\n", - "- How to leverage Qiskit Runtime primitives to mitigate and suppression noise\n", - "- How to define a measurement strategy to optimize speed vs accuracy\n", - "\n", - "Here's our high-level variational workload:\n", - "\n", - "![A diagram showing the quantum circuit with unitaries preparing the reference state and variational state, followed by measurements. These are used to evaluate the cost function.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-circuit.svg)\n", - "\n", - "Our cost function runs during every iteration of the optimization loop. The next lesson will explore how the classical optimizer uses our cost function evaluation to select new parameters." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "2751c16d-a9c1-407b-ab88-a9dcf768bc2e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.1.0\n", - "0.23.0\n" - ] - } - ], - "source": [ - "import qiskit\n", - "import qiskit_ibm_runtime\n", - "\n", - "print(qiskit.version.get_version_info())\n", - "print(qiskit_ibm_runtime.version.get_version_info())" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "da8ce695-8435-4099-b0fd-ade7b298f540", + "metadata": {}, + "source": [ + "---\n", + "title: Cost functions\n", + "description: This lesson explains what a cost function is, how it is used in variational algorithms, and how it can differ from the Hamiltonian.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore resil nbsp eigenbasis expvals IIZZ IZIZ IZZI ZIIZ ZZII */}\n", + "\n", + "# Cost functions\n", + "\n", + "During this lesson, we'll learn how to evaluate a *cost function*:\n", + "\n", + "- First, we'll learn about [Qiskit Runtime primitives](/docs/guides/primitives)\n", + "- Define a *cost function* $C(\\vec\\theta)$. This is a problem-specific function that defines the problem's goal for the optimizer to minimize (or maximize)\n", + "- Defining a measurement strategy with the Qiskit Runtime primitives to optimize speed versus accuracy\n", + "\n", + " \n", + "\n", + "![A diagram showing key components of a cost function including using primitives like estimator and sampler.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-workflow.svg)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "4a9b79a9-c040-43d3-a4cf-314277e30eea", + "metadata": { + "gloss": { + "primitives": { + "text": "Basic and fundamental operations or data type. Qiskit has the Sampler and Estimator primitives to serve as building blocks to easily construct complex workloads, like variational algorithms.", + "title": "Primitives" + } + } + }, + "source": [ + "## Primitives\n", + "\n", + "All physical systems, whether classical or quantum, can exist in different states. For example, a car on a road can have a certain mass, position, speed, or acceleration that characterize its state. Similarly, quantum systems can also have different configurations or states, but they differ from classical systems in how we deal with measurements and state evolution. This leads to unique properties such as *superposition* and *entanglement* that are exclusive to quantum mechanics. Just like we can describe a car's state using physical properties like speed or acceleration, we can also describe the state of a quantum system using *observables*, which are mathematical objects.\n", + "\n", + "In quantum mechanics, states are represented by normalized complex column vectors, or *kets* ($|\\psi\\rangle$), and observables are Hermitian linear operators ($\\hat{H}=\\hat{H}^{\\dagger}$) that act on the kets. An eigenvector ($|\\lambda\\rangle$) of an observable is known as an *eigenstate*. Measuring an observable for one of its eigenstates ($|\\lambda\\rangle$) will give us the corresponding eigenvalue ($\\lambda$) as readout.\n", + "\n", + "\n", + "If you're wondering how to measure a quantum system and what you can measure, Qiskit offers two primitives that can help:\n", + "\n", + "* `Sampler`: Given a quantum state $|\\psi\\rangle$, this primitive obtains the probability of each possible computational basis state.\n", + "* `Estimator`: Given a quantum observable $\\hat{H}$ and a state $|\\psi\\rangle$, this primitive computes the expected value of $\\hat{H}$." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "00aa7dfa-34f1-440e-bbdb-19b6e6a1aff8", + "metadata": {}, + "source": [ + "### The Sampler primitive\n", + "\n", + "The `Sampler` primitive calculates the probability of obtaining each possible state $|k\\rangle$ from the computational basis, given a quantum circuit that prepares the state $|\\psi\\rangle$. It calculates\n", + "\n", + "$$\n", + "p_k = |\\langle k | \\psi \\rangle|^2 \\quad \\forall k \\in \\mathbb{Z}_2^n \\equiv \\{0,1,\\cdots,2^n-1\\},\n", + "$$\n", + "\n", + "Where $n$ is the number of qubits, and $k$ the integer representation of any possible output binary string $\\{0,1\\}^n$ (that is, integers base $2$).\n", + "\n", + "\n", + "The Qiskit Runtime [`Sampler`](/docs/api/qiskit-ibm-runtime/sampler-v2) runs the circuit multiple times on a quantum device, performing measurements on each run, and reconstructing the probability distribution from the recovered bitstrings. The more runs (or *shots*) it performs, the more accurate the results will be, but this requires more time and quantum resources.\n", + "\n", + "\n", + "However, since the number of possible outputs grows exponentially with the number of qubits $n$ (that is, $2^n$), the number of shots will need to grow exponentially as well in order to capture a _dense_ probability distribution. Therefore, `Sampler` is only efficient for *sparse* probability distributions; where the target state $|\\psi\\rangle$ must be expressible as a linear combination of the computational basis states, with the number of terms growing at most polynomially with the number of qubits:\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\sum^{\\text{Poly}(n)}_k w_k |k\\rangle.\n", + "$$\n", + "\n", + "\n", + "The `Sampler` can also be configured to retrieve probabilities from a subsection of the circuit, representing a subset of the total possible states." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "96a74b2b-6aa0-4ef1-a8d1-448b591429e4", + "metadata": { + "gloss": { + "pauli": { + "text": "Set of matrices commonly used in quantum computing to represent and manipulate quantum states, consisting of the identity matrix and the three Pauli matrices (X, Y, and Z).", + "title": "Pauli Operators" + } + } + }, + "source": [ + "### The Estimator primitive\n", + "\n", + "\n", + "The `Estimator` primitive calculates the expectation value of an observable $\\hat{H}$ for a quantum state $|\\psi\\rangle$; where the observable probabilities can be expressed as $p_\\lambda = |\\langle\\lambda|\\psi\\rangle|^2$, being $|\\lambda\\rangle$ the eigenstates of the observable $\\hat{H}$. The expectation value is then defined as the average of all possible outcomes $\\lambda$ (that is, the eigenvalues of the observable) of a measurement of the state $|\\psi\\rangle$, weighted by the corresponding probabilities:\n", + "\n", + "$$\n", + "\\langle\\hat{H}\\rangle_\\psi := \\sum_\\lambda p_\\lambda \\lambda = \\langle \\psi | \\hat{H} | \\psi \\rangle\n", + "$$\n", + "\n", + "\n", + "However, calculating the expectation value of an observable is not always possible, as we often don't know its eigenbasis. The Qiskit Runtime [`Estimator`](/docs/api/qiskit-ibm-runtime/estimator-v2) uses a complex algebraic process to estimate the expectation value on a real quantum device by breaking down the observable into a combination of other observables whose eigenbasis we do know.\n", + "\n", + "In simpler terms, `Estimator` breaks down any observable that it doesn't know how to measure into simpler, measurable observables called Pauli operators." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6dcabf55-4462-4cf9-b6e4-e7860fd25b31", + "metadata": {}, + "source": [ + "Any operator can be expressed as a combination of $4^n$ Pauli operators.\n", + "\n", + "$$\n", + "\\hat{P}_k :=\n", + "\\sigma_{k_{n-1}}\\otimes \\cdots \\otimes \\sigma_{k_0} \\quad\n", + "\\forall k \\in \\mathbb{Z}_4^n \\equiv \\{0,1,\\cdots,4^n-1\\}, \\\\\n", + "$$\n", + "\n", + "such that\n", + "\n", + "$$\n", + "\\hat{H} = \\sum^{4^n-1}_{k=0} w_k \\hat{P}_k\n", + "$$\n", + "\n", + "where $n$ is the number of qubits, $k \\equiv k_{n-1} \\cdots k_0$ for $k_l \\in \\mathbb{Z}_4 \\equiv \\{0, 1, 2, 3\\}$ (that is, integers base $4$), and $(\\sigma_0, \\sigma_1, \\sigma_2, \\sigma_3) := (I, X, Y, Z)$.\n", + "\n", + "After performing this decomposition, `Estimator` derives a new circuit $V_k|\\psi\\rangle$ for each observable $\\hat{P}_k$ (from the original circuit), to effectively *diagonalize* the Pauli observable in the computational basis and measure it. We can easily measure Pauli observables because we know $V_k$ ahead of time, which is not the case generally for other observables.\n", + "\n", + "For each $\\hat{P}_{k}$, the `Estimator` runs the corresponding circuit on a quantum device multiple times, measures the output state in the computational basis, and calculates the probability $p_{kj}$ of obtaining each possible output $j$. It then looks for the eigenvalue $\\lambda_{kj}$ of $P_k$ corresponding to each output $j$, multiplies by $w_k$, and adds all the results together to obtain the expected value of the observable $\\hat{H}$ for the given state $|\\psi\\rangle$.\n", + "\n", + "$$\n", + "\\langle\\hat{H}\\rangle_\\psi =\n", + "\\sum_{k=0}^{4^n-1} w_k \\sum_{j=0}^{2^n-1}p_{kj} \\lambda_{kj},\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "dcffb899-ef2d-4dca-b2a7-11c197d45ed0", + "metadata": { + "gloss": { + "sampling": { + "text": "The process of taking several measurements of one or several things.", + "title": "Sampling" + } + } + }, + "source": [ + "Since calculating the expectation value of $4^n$ Paulis is impractical (that is, exponentially growing), `Estimator` can only be efficient when a large amount of $w_k$ are zero (that is, *sparse* Pauli decomposition instead of *dense*). Formally we say that, for this computation to be *efficiently solvable*, the number of non-zero terms has to grow at most polynomially with the number of qubits $n$: $\\hat{H} = \\sum^{\\text{Poly}(n)}_k w_k \\hat{P}_k.$\n", + "\n", + "The reader may notice the implicit assumption that probability sampling also needs to be efficient as explained for `Sampler`, which means\n", + "\n", + "$$\n", + "\\langle\\hat{H}\\rangle_\\psi =\n", + "\\sum_{k}^{\\text{Poly}(n)} w_k \\sum_{j}^{\\text{Poly}(n)}p_{kj} \\lambda_{kj}.\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c1547f3e-3c05-46d9-be5a-3e7e4141dc92", + "metadata": {}, + "source": [ + "### Guided example to calculate expectation values\n", + "\n", + "Let's assume the single-qubit state $|+\\rangle := H|0\\rangle = \\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$, and observable\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\hat{H}\n", + "& = \\begin{pmatrix}\n", + "-1 & 2 \\\\\n", + "2 & 1 \\\\\n", + "\\end{pmatrix}\\\\[1mm]\n", + "& = 2X - Z\n", + "\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "with the following theoretical expectation value $\\langle\\hat{H}\\rangle_+ = \\langle+|\\hat{H}|+\\rangle = 2.$\n", + "\n", + "Since we do not know how to measure this observable, we cannot compute its expectation value directly, and we need to re-express it as $\\langle\\hat{H}\\rangle_+ = 2\\langle X \\rangle_+ - \\langle Z \\rangle_+ $. Which can be shown to evaluate to the same result by virtue of noting that $\\langle+|X|+\\rangle = 1$, and $\\langle+|Z|+\\rangle = 0$.\n", + "\n", + "Let see how to compute $\\langle X \\rangle_+$ and $\\langle Z \\rangle_+$ directly. Since $X$ and $Z$ do not commute (that is, they don't share the same eigenbasis), they cannot be measured simultaneously, therefore we need the auxiliary circuits:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7c32e970-f9cb-48ec-a60b-2dccb0744141", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "# The following code will work for any other initial single-qubit state and observable\n", + "original_circuit = QuantumCircuit(1)\n", + "original_circuit.h(0)\n", + "\n", + "H = SparsePauliOp([\"X\", \"Z\"], [2, -1])\n", + "\n", + "aux_circuits = []\n", + "for pauli in H.paulis:\n", + " aux_circ = original_circuit.copy()\n", + " aux_circ.barrier()\n", + " if str(pauli) == \"X\":\n", + " aux_circ.h(0)\n", + " elif str(pauli) == \"Y\":\n", + " aux_circ.sdg(0)\n", + " aux_circ.h(0)\n", + " else:\n", + " aux_circ.id(0)\n", + " aux_circ.measure_all()\n", + " aux_circuits.append(aux_circ)\n", + "\n", + "original_circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ec3cf7af-66b2-4ea9-94d7-6ec6ba2bfc5e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Auxiliary circuit for X\n", + "aux_circuits[0].draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "678d3bd0-d8fd-4767-b0a6-77cae945810a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Auxiliary circuit for Z\n", + "aux_circuits[1].draw(\"mpl\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "beef97f0-c968-4a3f-9b7b-fed39b88cbe3", + "metadata": {}, + "source": [ + "We can now carry out the computation manually using `Sampler` and check the results on `Estimator`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d59dffe0-85cb-4849-8421-03bbcc9668df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sampler results:\n", + " >> Expected value of X: 1.00000\n", + " >> Expected value of Z: 0.00420\n", + " >> Total expected value: 1.99580\n", + "Estimator results:\n", + " >> Expected value of X: 1.00000\n", + " >> Expected value of Z: 0.00000\n", + " >> Total expected value: 2.00000\n" + ] + } + ], + "source": [ + "from qiskit.primitives import StatevectorSampler, StatevectorEstimator\n", + "from qiskit.result import QuasiDistribution\n", + "import numpy as np\n", + "\n", + "\n", + "## SAMPLER\n", + "shots = 10000\n", + "sampler = StatevectorSampler()\n", + "job = sampler.run(aux_circuits, shots=shots)\n", + "\n", + "# Run the sampler job and step through results\n", + "expvals = []\n", + "for index, pauli in enumerate(H.paulis):\n", + " data_pub = job.result()[index].data\n", + " bitstrings = data_pub.meas.get_bitstrings()\n", + " counts = data_pub.meas.get_counts()\n", + " quasi_dist = QuasiDistribution(\n", + " {outcome: freq / shots for outcome, freq in counts.items()}\n", + " )\n", + "\n", + " # Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation\n", + " # value.\n", + " val = 0\n", + "\n", + " if str(pauli) == \"X\":\n", + " val += -1 * quasi_dist.get(1, 0)\n", + " val += 1 * quasi_dist.get(0, 0)\n", + "\n", + " if str(pauli) == \"Y\":\n", + " val += -1 * quasi_dist.get(1, 0)\n", + " val += 1 * quasi_dist.get(0, 0)\n", + "\n", + " if str(pauli) == \"Z\":\n", + " val += 1 * quasi_dist.get(0, 0)\n", + " val += -1 * quasi_dist.get(1, 0)\n", + "\n", + " expvals.append(val)\n", + "\n", + "# Print expectation values\n", + "\n", + "print(\"Sampler results:\")\n", + "for pauli, expval in zip(H.paulis, expvals):\n", + " print(f\" >> Expected value of {str(pauli)}: {expval:.5f}\")\n", + "\n", + "total_expval = np.sum(H.coeffs * expvals).real\n", + "print(f\" >> Total expected value: {total_expval:.5f}\")\n", + "\n", + "# Use estimator for comparison\n", + "observables = [\n", + " *H.paulis,\n", + " H,\n", + "] # Note: run for individual Paulis as well as full observable H\n", + "\n", + "estimator = StatevectorEstimator()\n", + "job = estimator.run([(original_circuit, observables)])\n", + "estimator_expvals = job.result()[0].data.evs\n", + "\n", + "# Print results\n", + "print(\"Estimator results:\")\n", + "for obs, expval in zip(observables, estimator_expvals):\n", + " if obs is not H:\n", + " print(f\" >> Expected value of {str(obs)}: {expval:.5f}\")\n", + " else:\n", + " print(f\" >> Total expected value: {expval:.5f}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "44f13c37-50e3-426c-90cb-f730a81e20d1", + "metadata": {}, + "source": [ + "### Mathematical rigor (optional)\n", + "\n", + "Expressing $|\\psi\\rangle$ with respect to the basis of eigenstates of $\\hat{H}$, $|\\psi\\rangle = \\sum_\\lambda a_\\lambda |\\lambda\\rangle$, it follows:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\langle \\psi | \\hat{H} | \\psi \\rangle\n", + "& = \\bigg(\\sum_{\\lambda'}a^*_{\\lambda'} \\langle \\lambda'|\\bigg) \\hat{H}\n", + " \\bigg(\\sum_{\\lambda} a_\\lambda | \\lambda\\rangle\\bigg)\\\\[1mm]\n", + "\n", + "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda}\n", + " \\langle \\lambda'|\\hat{H}| \\lambda\\rangle\\\\[1mm]\n", + "\n", + "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda} \\lambda\n", + "\\langle \\lambda'| \\lambda\\rangle\\\\[1mm]\n", + "\n", + "& = \\sum_{\\lambda}\\sum_{\\lambda'} a^*_{\\lambda'}a_{\\lambda} \\lambda\n", + "\\cdot \\delta_{\\lambda, \\lambda'}\\\\[1mm]\n", + "\n", + "& = \\sum_\\lambda |a_\\lambda|^2 \\lambda\\\\[1mm]\n", + "\n", + "& = \\sum_\\lambda p_\\lambda \\lambda\\\\[1mm]\n", + "\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6a3bea3d-a2f6-4033-85f9-9a10c5d5c64c", + "metadata": { + "gloss": { + "hermitian": { + "text": "A hermitian is a square matrix that is equal to its own conjugate transpose, or a linear operator that is self-adjoint.", + "title": "Hermitian" + } + } + }, + "source": [ + "Since we do not know the eigenvalues or eigenstates of the target observable $\\hat{H}$, first we need to consider its diagonalization. Given that $\\hat{H}$ is Hermitian, there exists a unitary transformation $V$ such that $\\hat{H}=V^\\dagger \\Lambda V,$ where $\\Lambda$ is the diagonal eigenvalue matrix, so $\\langle j | \\Lambda | k \\rangle = 0$ if $j\\neq k$, and $\\langle j | \\Lambda | j \\rangle = \\lambda_j$.\n", + "\n", + "This implies that the expected value can be rewritten as:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\langle\\psi|\\hat{H}|\\psi\\rangle\n", + "& = \\langle\\psi|V^\\dagger \\Lambda V|\\psi\\rangle\\\\[1mm]\n", + "\n", + "& = \\langle\\psi|V^\\dagger \\bigg(\\sum_{j=0}^{2^n-1} |j\\rangle\n", + "\\langle j|\\bigg) \\Lambda \\bigg(\\sum_{k=0}^{2^n-1} |k\\rangle \\langle k|\\bigg) V|\\psi\\rangle\\\\[1mm]\n", + "\n", + "& = \\sum_{j=0}^{2^n-1} \\sum_{k=0}^{2^n-1}\\langle\\psi|V^\\dagger |j\\rangle\n", + "\\langle j| \\Lambda |k\\rangle \\langle k| V|\\psi\\rangle\\\\[1mm]\n", + "\n", + "& = \\sum_{j=0}^{2^n-1}\\langle\\psi|V^\\dagger |j\\rangle\n", + "\\langle j| \\Lambda |j\\rangle \\langle j| V|\\psi\\rangle\\\\[1mm]\n", + "\n", + "& = \\sum_{j=0}^{2^n-1}|\\langle j| V|\\psi\\rangle|^2 \\lambda_j\\\\[1mm]\n", + "\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Given that if a system is in the state $|\\phi\\rangle = V |\\psi\\rangle$ the probability of measuring $| j\\rangle$ is $p_j = |\\langle j|\\phi \\rangle|^2$, the above expected value can be expressed as:\n", + "\n", + "$$\n", + "\\langle\\psi|\\hat{H}|\\psi\\rangle =\n", + "\\sum_{j=0}^{2^n-1} p_j \\lambda_j.\n", + "$$\n", + "\n", + "It is very important to note that the probabilities are taken from the state $V |\\psi\\rangle$ instead of $|\\psi\\rangle$. This is why the matrix $V$ is absolutely necessary." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c21a9a05-6e07-45dc-a090-4414dfdf2521", + "metadata": {}, + "source": [ + "You might be wondering how to obtain the matrix $V$ and the eigenvalues $\\Lambda$. If you already had the eigenvalues, then there would be no need to use a quantum computer since the goal of variational algorithms is to find these eigenvalues of $\\hat{H}$.\n", + "\n", + "Fortunately, there is a way around that: any $2^n \\times 2^n$ matrix can be written as a linear combination of $4^n$ tensor products of $n$ Pauli matrices and identities, all of which are both hermitian and unitary with known $V$ and $\\Lambda$. This is what Runtime's `Estimator` does internally by decomposing any [`Operator`](/docs/api/qiskit/qiskit.quantum_info.Operator) object into a [`SparsePauliOp`](/docs/api/qiskit/qiskit.quantum_info.SparsePauliOp).\n", + "\n", + "Here are the Operators that can be used:\n", + "\n", + "$$\n", + "\\begin{array}{c|c|c|c}\n", + " \\text{Operator} & \\sigma & V & \\Lambda \\\\[1mm]\n", + " \\hline\n", + " I & \\sigma_0 = \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix} & V_0 = I & \\Lambda_0 = I = \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix} \\\\[4mm]\n", + "\n", + " X & \\sigma_1 = \\begin{pmatrix} 0 & 1 \\\\ 1 & 0 \\end{pmatrix} & V_1 = H =\\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix} & \\Lambda_1 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\\\[4mm]\n", + "\n", + " Y & \\sigma_2 = \\begin{pmatrix} 0 & -i \\\\ i & 0 \\end{pmatrix} & V_2 = HS^\\dagger =\\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix}\\cdot \\begin{pmatrix} 1 & 0 \\\\ 0 & -i \\end{pmatrix} = \\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & -i \\\\ 1 & i \\end{pmatrix}\\quad & \\Lambda_2 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\\\[4mm]\n", + "\n", + " Z & \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix} & V_3 = I & \\Lambda_3 = \\sigma_3 = \\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix}\n", + "\\end{array}\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "9e1c0933-5fbe-466d-9114-92d66d21785f", + "metadata": {}, + "source": [ + "So let's rewrite $\\hat{H}$ with respect to the Paulis and identities:\n", + "\n", + "$$\n", + "\\hat{H} =\n", + "\\sum_{k_{n-1}=0}^3...\n", + "\\sum_{k_0=0}^3 w_{k_{n-1}...k_0}\n", + "\\sigma_{k_{n-1}}\\otimes ... \\otimes \\sigma_{k_0} = \\sum_{k=0}^{4^n-1} w_k \\hat{P}_k,\n", + "$$\n", + "\n", + "where $k = \\sum_{l=0}^{n-1} 4^l k_l \\equiv k_{n-1}...k_0$ for $k_{n-1},...,k_0\\in \\{0,1,2,3\\}$ (that is, base $4$), and $\\hat{P}_{k} := \\sigma_{k_{n-1}}\\otimes ... \\otimes \\sigma_{k_0}$:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\\langle\\psi|\\hat{H}|\\psi\\rangle\n", + "& = \\sum_{k=0}^{4^n-1} w_k\n", + "\\sum_{j=0}^{2^n-1}|\\langle j| V_k|\\psi\\rangle|^2 \\langle j| \\Lambda_k |j\\rangle \\\\[1mm]\n", + "\n", + "& = \\sum_{k=0}^{4^n-1} w_k \\sum_{j=0}^{2^n-1}p_{kj} \\lambda_{kj}, \\\\[1mm]\n", + "\n", + "\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "\n", + "where $V_k := V_{k_{n-1}}\\otimes ... \\otimes V_{k_0}$ and $\\Lambda_k := \\Lambda_{k_{n-1}}\\otimes ... \\otimes \\Lambda_{k_0}$, such that: $\\hat{P_k}=V_k^\\dagger \\Lambda_k V_k.$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "42f771a0-486d-4a5f-9df1-a1720d7bf252", + "metadata": { + "gloss": { + "oracle": { + "text": " A hypothetical device or software component that performs a specific function, but the internal workings of which are unknown. A user is only aware of the inputs and outputs of the black box, and has no knowledge of how the black box processes the inputs to produce the outputs.", + "title": "Black-box oracle" + } + } + }, + "source": [ + "## Cost functions\n", + "\n", + "In general, cost functions are used to describe the goal of a problem and how well a trial state is performing with respect to that goal. This definition can be applied to various examples in chemistry, machine learning, finance, optimization, and so on.\n", + "\n", + "Let's consider a simple example of finding the ground state of a system. Our objective is to minimize the expectation value of the observable representing energy (Hamiltonian $\\hat{\\mathcal{H}}$):\n", + "\n", + "$$\n", + "\\min_{\\vec\\theta} \\langle\\psi(\\vec\\theta)|\\hat{\\mathcal{H}}|\\psi(\\vec\\theta)\\rangle\n", + "$$\n", + "\n", + "We can use the `Estimator` to evaluate the expectation value and pass this value to an optimizer to minimize. If the optimization is successful, it will return a set of optimal parameter values $\\vec\\theta^*$, from which we will be able to construct the proposed solution state $|\\psi(\\vec\\theta^*)\\rangle$ and compute the observed expectation value as $C(\\vec\\theta^*)$.\n", + "\n", + "Notice how we will only be able to minimize the cost function for the limited set of states that we are considering. This leads us to two separate possibilities:\n", + "\n", + "- **Our ansatz does not define the solution state across the search space**: If this is the case, our optimizer will never find the solution, and we need to experiment with other ansatzes that might be able to represent our search space more accurately.\n", + "- **Our optimizer is unable to find this valid solution**: Optimization can be globally defined and locally defined. We'll explore what this means in the later section.\n", + "\n", + "All in all, we will be performing a classical optimization loop but relying on the evaluation of the cost function to a quantum computer. From this perspective, one could think of the optimization as a purely classical endeavor where we call some black-box quantum oracle each time the optimizer needs to evaluate the cost function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "650faaab-b964-4587-a39f-d6cc80b225b2", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func_vqe(params, circuit, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (Estimator): Estimator primitive instance\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (circuit, hamiltonian, params)\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + " return cost" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a01a54f2-da16-4008-b422-fa7ded531f67", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import TwoLocal\n", + "\n", + "observable = SparsePauliOp.from_list([(\"XX\", 1), (\"YY\", -3)])\n", + "\n", + "reference_circuit = QuantumCircuit(2)\n", + "reference_circuit.x(0)\n", + "\n", + "variational_form = TwoLocal(\n", + " 2,\n", + " rotation_blocks=[\"rz\", \"ry\"],\n", + " entanglement_blocks=\"cx\",\n", + " entanglement=\"linear\",\n", + " reps=1,\n", + ")\n", + "ansatz = reference_circuit.compose(variational_form)\n", + "\n", + "theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()\n", + "ansatz.decompose().draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "7357e05e-f68a-4cd5-bf0d-77eab963a86d", + "metadata": {}, + "source": [ + "We will first carry this out using a simulator: the StatevectorEstimator. This is usually advisable for debugging, but we will immediately follow the debugging run with a calculation on real quantum hardware. Increasingly, problems of interest are no longer classically simulable without state-of-the-art supercomputing facilities." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "883bcb28-15d0-4046-a654-f43d9480a642", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.58744589]\n" + ] + } + ], + "source": [ + "estimator = StatevectorEstimator()\n", + "cost = cost_func_vqe(theta_list, ansatz, observable, estimator)\n", + "print(cost)" + ] + }, + { + "cell_type": "markdown", + "id": "1a94eba3-146d-4eee-970d-9a966d1e15f3", + "metadata": {}, + "source": [ + "We will now proceed with running on a real quantum computer. Note the syntax changes. The steps involving the pass_manager will be discussed further in the next example. One step of particular importance in variational algorithms is the use of a Qiskit Runtime session. Starting a session allows you to run multiple iterations of a variational algorithm without waiting in a new queue each time parameters are updated. This is important if queue times are long and/or many iterations are needed. Only partners in the IBM Quantum® Network can use Runtime sessions. If you do not have access to sessions, you can reduce the number of iterations you submit at a given time, and save the most recent parameters for use in future runs. If you submit too many iterations or encounter queue times that are too long, you may encounter error code 1217, which refers to long delays between job submissions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4ca4722-46ab-418f-8a1f-740a65e92eb9", + "metadata": {}, + "outputs": [], + "source": [ + "# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor\n", + "# Load necessary packages:\n", + "\n", + "from qiskit_ibm_runtime import (\n", + " QiskitRuntimeService,\n", + " Session,\n", + " EstimatorOptions,\n", + " EstimatorV2 as Estimator,\n", + ")\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "# Select the least busy backend:\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", + ")\n", + "# Or get a specific backend:\n", + "# backend = service.backend(\"ibm_brisbane\")\n", + "\n", + "# Use a pass manager to transpile the circuit and observable for the specific backend being used:\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_ansatz = pm.run(ansatz)\n", + "isa_observable = observable.apply_layout(layout=isa_ansatz.layout)\n", + "\n", + "\n", + "# Set estimator options\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", + "\n", + "# Open a Runtime session:\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + " cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)\n", + "\n", + "session.close()\n", + "print(cost)" + ] + }, + { + "cell_type": "markdown", + "id": "95dc0e83-46ce-4839-91c3-af550aef9ddf", + "metadata": {}, + "source": [ + "Note that the values obtained from the two calculations above are very similar. Techniques for improving results will be discussed further below." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7f3951a3-47ad-4ddb-90a0-2218b1e11d15", + "metadata": {}, + "source": [ + "### Example mapping to non-physical systems\n", + "\n", + "The maximum cut (max-cut) problem is a combinatorial optimization problem that involves dividing the vertices of a graph into two disjoint sets such that the number of edges between the two sets is maximized. More formally, given an undirected graph $G=(V,E)$, where $V$ is the set of vertices and $E$ is the set of edges, the max-cut problem asks to partition the vertices into two disjoint subsets, $S$ and $T$, such that the number of edges with one endpoint in $S$ and the other in $T$ is maximized.\n", + "\n", + "We can apply max-cut to solve a various problems including: clustering, network design, phase transitions, and so on. We'll start by creating a problem graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "739ca7a0-fae3-4389-a8de-7ae93c13b9e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import rustworkx as rx\n", + "from rustworkx.visualization import mpl_draw\n", + "\n", + "n = 4\n", + "G = rx.PyGraph()\n", + "G.add_nodes_from(range(n))\n", + "# The edge syntax is (start, end, weight)\n", + "edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]\n", + "G.add_edges_from(edges)\n", + "\n", + "mpl_draw(\n", + " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=\"#1192E8\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3246e173-b1a8-48c2-bce8-f80a1524bcfb", + "metadata": {}, + "source": [ + "This problem can be expressed as a binary optimization problem. For each node $0 \\leq i < n$, where $n$ is the number of nodes of the graph (in this case $n=4$), we will consider the binary variable $x_i$. This variable will have the value $1$ if node $i$ is one of the groups that we'll label $1$ and $0$ if it's in the other group, that we'll label as $0$. We will also denote as $w_{ij}$ (element $(i,j)$ of the adjacency matrix $w$) the weight of the edge that goes from node $i$ to node $j$. Because the graph is undirected, $w_{ij}=w_{ji}$. Then we can formulate our problem as maximizing the following cost function:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "C(\\vec{x})\n", + "& =\\sum_{i,j=0}^n w_{ij} x_i(1-x_j)\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i,j=0}^n w_{ij} x_ix_j\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i=0}^n \\sum_{j=0}^i 2w_{ij} x_ix_j\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "\n", + "To solve this problem with a quantum computer, we are going to express the cost function as the expected value of an observable. However, the observables that Qiskit admits natively consist of Pauli operators, that have eigenvalues $1$ and $-1$ instead of $0$ and $1$. That's why we are going to make the following change of variable:\n", + "\n", + "Where $\\vec{x}=(x_0,x_1,\\cdots ,x_{n-1})$. We can use the adjacency matrix $w$ to comfortably access the weights of all the edge. This will be used to obtain our cost function:\n", + "\n", + "$$\n", + "z_i = 1-2x_i \\rightarrow x_i = \\frac{1-z_i}{2}\n", + "$$\n", + "\n", + "This implies that:\n", + "\n", + "$$\n", + "\\begin{array}{lcl} x_i=0 & \\rightarrow & z_i=1 \\\\ x_i=1 & \\rightarrow & z_i=-1.\\end{array}\n", + "$$\n", + "\n", + "So the new cost function we want to maximize is:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "C(\\vec{z})\n", + "& = \\sum_{i,j=0}^n w_{ij} \\bigg(\\frac{1-z_i}{2}\\bigg)\\bigg(1-\\frac{1-z_j}{2}\\bigg)\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n \\frac{w_{ij}}{4} - \\sum_{i,j=0}^n \\frac{w_{ij}}{4} z_iz_j\\\\[1mm]\n", + "\n", + "& = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Moreover, the natural tendency of a quantum computer is to find minima (usually the lowest energy) instead of maxima so instead of maximizing $C(\\vec{z})$ we are going to minimize:\n", + "\n", + "$$\n", + "-C(\\vec{z}) = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", + "$$\n", + "\n", + "Now that we have a cost function to minimize whose variables can have the values $-1$ and $1$, we can make the following analogy with the Pauli $Z$:\n", + "\n", + "$$\n", + "z_i \\equiv Z_i = \\overbrace{I}^{n-1}\\otimes ... \\otimes \\overbrace{Z}^{i} \\otimes ... \\otimes \\overbrace{I}^{0}\n", + "$$\n", + "\n", + "In other words, the variable $z_i$ will be equivalent to a $Z$ gate acting on qubit $i$. Moreover:\n", + "\n", + "$$\n", + "Z_i|x_{n-1}\\cdots x_0\\rangle = z_i|x_{n-1}\\cdots x_0\\rangle \\rightarrow \\langle x_{n-1}\\cdots x_0 |Z_i|x_{n-1}\\cdots x_0\\rangle = z_i\n", + "$$\n", + "\n", + "Then the observable we are going to consider is:\n", + "\n", + "$$\n", + "\\hat{H} = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} Z_iZ_j\n", + "$$\n", + "\n", + "to which we will have to add the independent term afterwards:\n", + "\n", + "$$\n", + "\\texttt{offset} = - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "83c86ba1-8b9d-4222-bb7e-637e6f29e5cd", + "metadata": {}, + "source": [ + "The operator is a linear combination of terms with Z operators on nodes connected by an edge (recall that the 0th qubit is farthest right): $IIZZ + IZIZ + IZZI + ZIIZ + ZZII$. Once the operator is constructed, the ansatz for the QAOA algorithm can easily be built by using the `QAOAAnsatz` circuit from the Qiskit circuit library." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "29398315-3baf-4363-88fc-69b6438e4afa", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import QAOAAnsatz\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "hamiltonian = SparsePauliOp.from_list(\n", + " [(\"IIZZ\", 1), (\"IZIZ\", 1), (\"IZZI\", 1), (\"ZIIZ\", 1), (\"ZZII\", 1)]\n", + ")\n", + "\n", + "\n", + "ansatz = QAOAAnsatz(hamiltonian, reps=2)\n", + "# Draw\n", + "ansatz.decompose(reps=3).draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "91d02379-9da6-426b-a5f0-d1fb75a891ee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Offset: -2.5\n" + ] + } + ], + "source": [ + "# Sum the weights, and divide by 2\n", + "\n", + "offset = -sum(edge[2] for edge in edges) / 2\n", + "print(f\"\"\"Offset: {offset}\"\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "84a98a08-00e4-4b05-a3f0-de29a7e73a60", + "metadata": {}, + "source": [ + "With the Runtime Estimator directly taking a Hamiltonian and parameterized ansatz, and returning the necessary energy, The cost function for a QAOA instance is quite simple:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "73ac80dd-87cf-4a43-b19b-e89a4782a44a", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (Estimator): Estimator primitive instance\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, hamiltonian, params)\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", + " return cost" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "fc6af87b-a838-43b8-96ce-0af790fe0172", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.473098768180865\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)\n", + "\n", + "estimator = StatevectorEstimator()\n", + "cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)\n", + "print(cost)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "389b7e86-cd61-4184-96c3-ae91b8ad59f5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.1120776913677988\n" + ] + } + ], + "source": [ + "# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24\n", + "# Load some necessary packages:\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator\n", + "\n", + "# Select the least busy backend:\n", + "\n", + "backend = service.least_busy(\n", + " operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", + ")\n", + "\n", + "# Or get a specific backend:\n", + "# backend = service.backend(\"ibm_brisbane\")\n", + "\n", + "# Use a pass manager to transpile the circuit and observable for the specific backend being used:\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_ansatz = pm.run(ansatz)\n", + "isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)\n", + "\n", + "# Set estimator options\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", + "\n", + "# Open a Runtime session:\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + " cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)\n", + "\n", + "# Close session after done\n", + "session.close()\n", + "print(cost)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "16c94186-366c-4ac4-8745-4e7da283f8da", + "metadata": {}, + "source": [ + "We will revisit this example in Applications to explore how to leverage an optimizer to iterate through the search space. Generally speaking, this includes:\n", + "\n", + "- Leveraging an optimizer to find optimal parameters\n", + "- Binding optimal parameters to the ansatz to find the eigenvalues\n", + "- Translating the eigenvalues to our problem definition" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d14d5800-24c1-4e4b-b692-9bdb9ef4440f", + "metadata": {}, + "source": [ + "## Measurement strategy: speed versus accuracy\n", + "\n", + "As mentioned, we are using a noisy quantum computer as a *black-box oracle*, where noise can make the retrieved values non-deterministic, leading to random fluctuations which, in turn, will harm — or even completely prevent — convergence of certain optimizers to a proposed solution. This is a general problem that we must address as we incrementally explore quantum utility and progress towards quantum advantage:\n", + "\n", + "![A graph showing how simulation cost varies with circuit complexity. Using a classical computer it grows exponentially. With quantum error mitigation, there should be a crossover at which that becomes advantageous. Quantum error correction allows for linear growth of the simulation cost and will certainly lead to advantage.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-path-to-quantum-advantage.svg)\n", + "\n", + "We can use Qiskit Runtime Primitive's error suppression and error mitigation options to address noise and maximize the utility of today's quantum computers." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f23f0d82-d4f9-4485-aad3-cd2f8fd19b4f", + "metadata": { + "gloss": { + "overhead": { + "text": "Extra costs introduced by new techniques, relative to a base implementation.", + "title": "Overhead" + } + } + }, + "source": [ + "### Error Suppression\n", + "\n", + "[Error suppression](/docs/guides/error-mitigation-and-suppression-techniques) refers to techniques used to optimize and transform a circuit during compilation in order to minimize errors. This is a basic error handling technique that usually results in some classical pre-processing overhead to the overall runtime. The overhead includes transpiling circuits to run on quantum hardware by:\n", + "\n", + "- Expressing the circuit using the native gates available on a quantum system\n", + "- Mapping the virtual qubits to physical qubits\n", + "- Adding SWAPs based on connectivity requirements\n", + "- Optimizing 1Q and 2Q gates\n", + "- Adding dynamical decoupling to idle qubits to prevent the effects of decoherence.\n", + "\n", + "\n", + "Primitives allow for the use of error suppression techniques by setting the `optimization_level` option and selecting advanced transpilation options. In a later course, we will delve into different circuit construction methods to improve results, but for most cases, we recommend setting `optimization_level=3`.\n", + "\n", + "We will visualize the value of increasing optimization in the transpilation process by looking at an example circuit with a simple ideal behavior." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "9e0bf552-32b0-477e-9ac3-4ab643c30623", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit import Parameter, QuantumCircuit\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "theta = Parameter(\"theta\")\n", + "\n", + "qc = QuantumCircuit(2)\n", + "qc.x(1)\n", + "qc.h(0)\n", + "qc.cp(theta, 0, 1)\n", + "qc.h(0)\n", + "observables = SparsePauliOp.from_list([(\"ZZ\", 1)])\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "1c2b0408-e004-4a04-8aa5-cf519744082b", + "metadata": {}, + "source": [ + "The circuit above can yield sinusoidal expectation values of the observable given, provided we insert phases spanning an appropriate interval, such as $[0,2\\pi]$." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "99b23f88-ddcb-45dc-a86d-3d48ff990319", + "metadata": {}, + "outputs": [], + "source": [ + "## Setup phases\n", + "import numpy as np\n", + "\n", + "phases = np.linspace(0, 2 * np.pi, 50)\n", + "\n", + "# phases need to be expressed as a list of lists in order to work\n", + "individual_phases = [[phase] for phase in phases]" + ] + }, + { + "cell_type": "markdown", + "id": "ca040383-9c8c-4a38-8e40-bad1b26c6085", + "metadata": {}, + "source": [ + "We can use a simulator to show the usefulness of an optimized transpilation. We will return below to using real hardware to demonstrate the usefulness of error mitigation. We will use QiskitRuntimeService to get a real backend (in this case, ibm_brisbane), and use AerSimulator to simulate that backend, including its noise behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f0e6f32-02f7-471f-a9f3-f30d22f8cca4", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_aer import AerSimulator\n", + "\n", + "# get a real backend from the runtime service\n", + "service = QiskitRuntimeService()\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "\n", + "# generate a simulator that mimics the real quantum system with the latest calibration results\n", + "backend_sim = AerSimulator.from_backend(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "38df43e8-b11e-4671-b8fb-7a2eb74f648a", + "metadata": {}, + "source": [ + "We can now use a pass manager to transpile the circuit into the \"instruction set architecture\" or ISA of the backend. This is a new requirement in Qiskit Runtime: all circuits submitted to a backend must conform to the constraints of the backend’s target, meaning they must be written in terms of the backend's ISA — that is, the set of instructions the device can understand and execute. These target constraints are defined by factors like the device’s native basis gates, its qubit connectivity, and - when relevant - its pulse and other instruction timing specifications.\n", + "\n", + "Note that in the present case, we will do this twice: once with optimization_level = 0, and once with it set to 3. Each time we will use the Estimator primitive to estimate the expectation values of the observable at different values of phase." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dc699cd-0bb4-4b48-adcc-0bbe99218a26", + "metadata": {}, + "outputs": [], + "source": [ + "# Import estimator and specify that we are using the simulated backend:\n", + "\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "estimator = Estimator(mode=backend_sim)\n", + "\n", + "circuit = qc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7990eb62-879d-4beb-b452-f3d86b164d11", + "metadata": {}, + "outputs": [], + "source": [ + "# Use a pass manager to transpile the circuit and observable for the backend being simulated.\n", + "# Start with no optimization:\n", + "\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)\n", + "isa_circuit = pm.run(circuit)\n", + "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", + "\n", + "noisy_exp_values = []\n", + "pub = (isa_circuit, isa_observables, [individual_phases])\n", + "cost = estimator.run([pub]).result()[0].data.evs\n", + "noisy_exp_values = cost[0]\n", + "\n", + "# Repeat above steps, but now with optimization = 3:\n", + "\n", + "exp_values_with_opt_es = []\n", + "pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)\n", + "isa_circuit = pm.run(circuit)\n", + "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", + "\n", + "pub = (isa_circuit, isa_observables, [individual_phases])\n", + "cost = estimator.run([pub]).result()[0].data.evs\n", + "exp_values_with_opt_es = cost[0]" + ] + }, + { + "cell_type": "markdown", + "id": "0cc8d4ea-81ef-4146-b8ed-9c7edae72be3", + "metadata": {}, + "source": [ + "Finally, we can plot the results, and we see that the precision of the calculation was fairly good even without optimization, but it definitely improved by increasing optimization to level 3. Note that in deeper, more complicated circuits, the difference between optimization levels of 0 and 3 are likely to be more significant. This is a very simple circuit used as a toy model." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "f3d3f805-f8d0-474d-9a88-2d1f164639b0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.plot(phases, noisy_exp_values, \"o\", label=\"opt=0\")\n", + "plt.plot(phases, exp_values_with_opt_es, \"o\", label=\"opt=3\")\n", + "plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label=\"ideal\")\n", + "plt.ylabel(\"Expectation\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2394d082-2278-48cf-95b0-3363ed723ec1", + "metadata": { + "gloss": { + "bias": { + "text": "A systematic drift in the measured quantities, usually caused by errors.", + "title": "Bias" + } + } + }, + "source": [ + "### Error Mitigation\n", + "\n", + "[Error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) refers to techniques that allow users to reduce circuit errors by modeling the device noise at the time of execution. Typically, this results in quantum pre-processing overhead related to model training and classical post-processing overhead to mitigate errors in the raw results by using the generated model.\n", + "\n", + "The Qiskit Runtime primitive's `resilience_level` option specifies the amount of resilience to build against errors. Higher levels generate more accurate results at the expense of longer processing times due to quantum sampling overhead. Resilience levels can be used to configure the trade-off between cost and accuracy when applying error mitigation to your primitive query.\n", + "\n", + "When implementing any error mitigation technique, we expect the bias in our results to be reduced with respect to the previous, unmitigated bias. In some cases, the bias may even disappear. However, this comes at a cost. As we reduce the bias in our estimated quantities, the statistical variability will increase (that is, variance), which we can account for by further increasing the number of shots per circuit in our sampling process. This will introduce overhead beyond that needed to reduce the bias, so it is not done by default. We can easily opt-in to this behavior by adjusting the number of shots per circuit in options.executions.shots, as shown in the example below.\n", + "\n", + "![A diagram showing broader or narrowing distributions as in the bias/variance tradeoff.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-bias-variance-trade-off.svg)\n", + "\n", + "For this course, we will explore these error mitigation models at a high level to illustrate the error mitigation that Qiskit Runtime primitives can perform without requiring full implementation details.\n", + "\n", + "\n", + "### Twirled readout error extinction (T-REx)\n", + "\n", + "Twirled readout error extinction (T-REx) uses a technique known as Pauli twirling to reduce the noise introduced during the process of quantum measurement. This technique assumes no specific form of noise, which makes it very general and effective.\n", + "\n", + "Overall workflow:\n", + "\n", + "1. Acquire data for the zero state with randomized bit flips (Pauli X before measurement)\n", + "2. Acquire data for the desired (noisy) state with randomized bit flips (Pauli X before measurement)\n", + "3. Compute the special function for each data set, and divide.\n", + "\n", + " \n", + "\n", + "![A diagram showing measurement and calibration circuits for T-REX.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-trex-data-collection.svg)\n", + "\n", + "We can set this with `options.resilience_level = 1`, demonstrated in the example below.\n", + "\n", + "### Zero noise extrapolation\n", + "\n", + "Zero noise extrapolation (ZNE) works by first amplifying the noise in the circuit that is preparing the desired quantum state, obtaining measurements for several different levels of noise, and using those measurements to infer the noiseless result.\n", + "\n", + "Overall workflow:\n", + "\n", + "1. Amplify circuit noise for several noise factors\n", + "2. Run every noise amplified circuit\n", + "3. Extrapolate back to the zero noise limit\n", + "\n", + " \n", + "\n", + "![A diagram showing steps in ZNE. Noise is artificially amplified by different factors. Then the values are extrapolated to what they should be at zero noise.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-zne-stages.svg)\n", + "\n", + "We can set this with `options.resilience_level = 2`. We can optimize this further by exploring a variety of `noise_factors`, `noise_amplifiers`, and `extrapolators`, but this is outside the scope of this course. We encourage you to experiment with these [options as described here](/docs/guides/error-mitigation-and-suppression-techniques).\n", + "\n", + "Each method comes with its own associated overhead: a trade-off between the number of quantum computations needed (time) and the accuracy of our results:\n", + "\n", + "$$\n", + "\\begin{array}{c|c|c|c}\n", + " \\text{Methods} & R=1 \\text{, T-REx} & R=2 \\text{, ZNE} \\\\[1mm]\n", + " \\hline\n", + " \\text{Assumptions} & \\text{None} & \\text{Ability to scale noise} \\\\[1mm]\n", + " \\text{Qubit overhead} & 1 & 1 \\\\[1mm]\n", + " \\text{Sampling overhead} & 2 & N_{\\text{noise-factors}} \\\\[1mm]\n", + " \\text{Bias} & 0 & \\mathcal{O}(\\lambda^{N_{\\text{noise-factors}}}) \\\\[1mm]\n", + "\\end{array}\n", + "$$" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8b46120f-5312-4450-bf51-13be198428fa", + "metadata": {}, + "source": [ + "### Using Qiskit Runtime's mitigation and suppression options\n", + "\n", + "Here's how to calculate an expectation value while using error mitigation and suppression in Qiskit Runtime. We can make use of precisely the same circuit and observable as before, but this time keeping the optimization level fixed at level 2, and now tuning the _resilience_ or the error mitigation technique(s) being used. This error mitigation process occurs multiple times throughout an optimization loop.\n", + "\n", + "We perform this part on real hardware, since error mitigation is not available on simulators." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57a6b723-6e73-4693-95bf-f0782a8eb854", + "metadata": {}, + "outputs": [], + "source": [ + "# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_runtime import (\n", + " Session,\n", + " EstimatorOptions,\n", + " EstimatorV2 as Estimator,\n", + ")\n", + "\n", + "# We select the least busy backend\n", + "\n", + "# Select the least busy backend\n", + "# backend = service.least_busy(\n", + "# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False\n", + "# )\n", + "\n", + "# Or use a specific backend\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "\n", + "# Initialize some variables to save the results from different runs:\n", + "\n", + "exp_values_with_em0_es = []\n", + "exp_values_with_em1_es = []\n", + "exp_values_with_em2_es = []\n", + "\n", + "# Use a pass manager to optimize the circuit and observables for the backend chosen:\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=2)\n", + "isa_circuit = pm.run(circuit)\n", + "isa_observables = observables.apply_layout(layout=isa_circuit.layout)\n", + "\n", + "# Open a session and run with no error mitigation:\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + "\n", + " pub = (isa_circuit, isa_observables, [individual_phases])\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + "\n", + "session.close()\n", + "\n", + "exp_values_with_em0_es = cost[0]\n", + "\n", + "# Open a session and run with resilience = 1:\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + "\n", + " pub = (isa_circuit, isa_observables, [individual_phases])\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + "\n", + "session.close()\n", + "\n", + "exp_values_with_em1_es = cost[0]\n", + "\n", + "# Open a session and run with resilience = 2:\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + "\n", + " pub = (isa_circuit, isa_observables, [individual_phases])\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + "\n", + "session.close()\n", + "\n", + "exp_values_with_em2_es = cost[0]" + ] + }, + { + "cell_type": "markdown", + "id": "48215ea6-3045-4880-8671-265e2fb33e5e", + "metadata": {}, + "source": [ + "As before, we can plot the resulting expectation values as a function of phase angle for the three levels of error mitigation used. With great difficulty, one can see that error mitigation improves the results slightly. Again, this effect is much more pronounced in deeper, more complicated circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "id": "474f1a1a-ee90-468d-9390-fdb455aeb142", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.plot(phases, exp_values_with_em0_es, \"o\", label=\"unmitigated\")\n", + "plt.plot(phases, exp_values_with_em1_es, \"o\", label=\"resil = 1\")\n", + "plt.plot(phases, exp_values_with_em2_es, \"o\", label=\"resil = 2\")\n", + "plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label=\"ideal\")\n", + "plt.ylabel(\"Expectation\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1e683c07-96f3-47d5-8981-8d3aeb8be81f", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "With this lesson, you learned how to create a cost function:\n", + "\n", + "- Create a cost function\n", + "- How to leverage Qiskit Runtime primitives to mitigate and suppression noise\n", + "- How to define a measurement strategy to optimize speed vs accuracy\n", + "\n", + "Here's our high-level variational workload:\n", + "\n", + "![A diagram showing the quantum circuit with unitaries preparing the reference state and variational state, followed by measurements. These are used to evaluate the cost function.](/learning/images/courses/variational-algorithm-design/cost-functions/cost-function-circuit.svg)\n", + "\n", + "Our cost function runs during every iteration of the optimization loop. The next lesson will explore how the classical optimizer uses our cost function evaluation to select new parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2751c16d-a9c1-407b-ab88-a9dcf768bc2e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.1.0\n", + "0.23.0\n" + ] + } + ], + "source": [ + "import qiskit\n", + "import qiskit_ibm_runtime\n", + "\n", + "print(qiskit.version.get_version_info())\n", + "print(qiskit_ibm_runtime.version.get_version_info())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning/courses/variational-algorithm-design/examples-and-applications.ipynb b/learning/courses/variational-algorithm-design/examples-and-applications.ipynb index 8a827f218c2..6baf24713d9 100644 --- a/learning/courses/variational-algorithm-design/examples-and-applications.ipynb +++ b/learning/courses/variational-algorithm-design/examples-and-applications.ipynb @@ -1,2357 +1,2358 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "b45c3fbc-3d24-4da0-88a4-8db2150aba7c", - "metadata": {}, - "source": [ - "---\n", - "title: Examples and applications\n", - "description: Here we describe how variational quantum algorithms can be applied to chemistry problems, max-cut, and more!\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore disp eigvalsh realbackend IIZZ IZIZ IZZI ZIIZ ZZII */}\n", - "\n", - "# Examples and applications\n", - "\n", - "During this lesson, we'll explore some variational algorithm examples and how to apply them:\n", - "\n", - "- How to write a custom variational algorithm\n", - "- How to apply a variational algorithm to find minimum eigenvalues\n", - "- How to utilize variational algorithms to solve application use cases\n", - "\n", - "Note that the Qiskit patterns framework can be applied to all the problems we introduce here. However, to avoid repetition, we will only explicitly call out the framework steps in one example case, run on real hardware." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8b7f0c00-af07-4043-8af3-0b3d514f1a32", - "metadata": {}, - "source": [ - "## Problem definitions\n", - "\n", - "Imagine that we want to use a variational algorithm to find the eigenvalue of the following observable:\n", - "\n", - "$$\n", - "\\hat{O}_1 = 2 II - 2 XX + 3 YY - 3 ZZ,\n", - "$$\n", - "\n", - "This observable has the following eigenvalues:\n", - "\n", - "$$\n", - "\\left\\{\n", - "\\begin{array}{c}\n", - "\\lambda_0 = -6 \\\\\n", - "\\lambda_1 = 4 \\\\\n", - "\\lambda_2 = 4 \\\\\n", - "\\lambda_3 = 6\n", - "\\end{array}\n", - "\\right\\}\n", - "$$\n", - "\n", - "And eigenstates:\n", - "\n", - "$$\n", - "\\left\\{\n", - "\\begin{array}{c}\n", - "|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)\\\\\n", - "|\\phi_1\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle - |11\\rangle)\\\\\n", - "|\\phi_2\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle - |10\\rangle)\\\\\n", - "|\\phi_3\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle + |10\\rangle)\n", - "\\end{array}\n", - "\\right\\}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "1cb9f137-3cb0-45dd-a49a-61d0be119c68", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "observable_1 = SparsePauliOp.from_list([(\"II\", 2), (\"XX\", -2), (\"YY\", 3), (\"ZZ\", -3)])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "297600c7-8f3d-4070-b066-b2a0496f7f9c", - "metadata": {}, - "source": [ - "## Custom VQE\n", - "\n", - "We'll first explore how to construct a VQE instance manually to find the lowest eigenvalue for $\\hat{O}_1$. This will incorporate a variety of techniques that we have covered throughout this course." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "36120a96-c625-49d7-9ad3-ca70bfb08346", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func_vqe(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (Estimator): Estimator primitive instance\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, hamiltonian, params)\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - "\n", - " return cost" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57c071aa-e8ab-4b39-b5db-773016da2550", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library.n_local import n_local\n", - "from qiskit import QuantumCircuit\n", - "\n", - "import numpy as np\n", - "\n", - "reference_circuit = QuantumCircuit(2)\n", - "reference_circuit.x(0)\n", - "\n", - "variational_form = n_local(\n", - " num_qubits=2,\n", - " rotation_blocks=[\"rz\", \"ry\"],\n", - " entanglement_blocks=\"cx\",\n", - " entanglement=\"linear\",\n", - " reps=1,\n", - ")\n", - "\n", - "raw_ansatz = reference_circuit.compose(variational_form)\n", - "raw_ansatz.decompose().draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "4a138028-d14c-4146-921e-476698102568", - "metadata": {}, - "source": [ - "We will start debugging on local simulators." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "2f7c0fab-d6c2-4c90-b122-a5a90976bdaf", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.primitives import StatevectorEstimator as Estimator\n", - "from qiskit.primitives import StatevectorSampler as Sampler\n", - "\n", - "estimator = Estimator()\n", - "sampler = Sampler()" - ] - }, - { - "cell_type": "markdown", - "id": "824c57f9-8bf0-478a-99fd-3f89a48a1aff", - "metadata": {}, - "source": [ - "We now set an initial set of parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "cd0e029d-0807-4387-aef0-96ea5866c063", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1. 1. 1. 1. 1. 1. 1. 1.]\n" - ] - } - ], - "source": [ - "x0 = np.ones(raw_ansatz.num_parameters)\n", - "print(x0)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "0d70046a-16ac-48b4-9a0d-5afe3b65af09", - "metadata": {}, - "source": [ - "We can minimize this cost function to calculate optimal parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "25df1d56-ad05-43a8-b2cb-60b16105f6e4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Return from COBYLA because the trust region radius reaches its lower bound.\n", - "Number of function values = 103 Least value of F = -5.999999998357189\n", - "The corresponding X is:\n", - "[2.27483579e+00 8.37593091e-01 1.57080508e+00 5.82932911e-06\n", - " 2.49973063e+00 6.41884255e-01 6.33686904e-01 6.33688223e-01]\n", - "\n" - ] - } - ], - "source": [ - "# SciPy minimizer routine\n", - "from scipy.optimize import minimize\n", - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "result = minimize(\n", - " cost_func_vqe,\n", - " x0,\n", - " args=(raw_ansatz, observable_1, estimator),\n", - " method=\"COBYLA\",\n", - " options={\"maxiter\": 1000, \"disp\": True},\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "fbf9450b-acfb-43bd-88b3-cfe4633b54ae", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -5.999999998357189\n", - " x: [ 2.275e+00 8.376e-01 1.571e+00 5.829e-06 2.500e+00\n", - " 6.419e-01 6.337e-01 6.337e-01]\n", - " nfev: 103\n", - " maxcv: 0.0" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result" - ] - }, - { - "cell_type": "markdown", - "id": "a3bcf55d-cfee-415f-85c9-5923dd3e4513", - "metadata": {}, - "source": [ - "Because this toy problem uses only two qubits, we can check this by using NumPy's linear algebra eigensolver." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "e99d0559-c392-4023-93be-7e0ea0361f5b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of iterations: 103\n", - "Time (s): 0.4394676685333252\n", - "Percent error: 2.74e-08\n" - ] - } - ], - "source": [ - "from numpy.linalg import eigvalsh\n", - "\n", - "solution_eigenvalue = min(eigvalsh(observable_1.to_matrix()))\n", - "\n", - "print(f\"\"\"Number of iterations: {result.nfev}\"\"\")\n", - "print(f\"\"\"Time (s): {execution_time}\"\"\")\n", - "\n", - "print(\n", - " f\"Percent error: {100*abs((result.fun - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "17f4f2b7-3721-4708-969d-3f07dc74d2f1", - "metadata": {}, - "source": [ - "As you can see, the result is extremely close to the ideal." - ] - }, - { - "cell_type": "markdown", - "id": "f795f1dc-dd4b-478a-84d0-4a981a3187cf", - "metadata": {}, - "source": [ - "## Experimenting to improve speed and accuracy" - ] - }, - { - "cell_type": "markdown", - "id": "68de1e76-a6d8-4585-83ec-9bb57747dbf7", - "metadata": {}, - "source": [ - "### Add reference state\n", - "\n", - "In the previous example we have not used any reference operator $U_R$. Now let us think about how the ideal eigenstate $\\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$ can be obtained. Consider the following circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "55b26ae7-ad26-4b54-b4f3-2004bb2db0c9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "ideal_qc = QuantumCircuit(2)\n", - "ideal_qc.h(0)\n", - "ideal_qc.cx(0, 1)\n", - "\n", - "ideal_qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "e5368863-cf78-4f07-93bf-3a215aae5476", - "metadata": {}, - "source": [ - "We can quickly check that this circuit gives us the desired state." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "2595ad2a-bea2-4770-8f77-f38eb0c07815", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Statevector([0.70710678+0.j, 0. +0.j, 0. +0.j,\n", - " 0.70710678+0.j],\n", - " dims=(2, 2))\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import Statevector\n", - "\n", - "Statevector(ideal_qc)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b075a92d-4162-4cb1-bb4f-ab9feea67f61", - "metadata": {}, - "source": [ - "Now that we have seen how a circuit preparing the solution state looks like, it seems reasonable to use a Hadamard gate as a reference circuit, so that the full ansatz becomes:" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "14e04c7a-e81f-41e1-b0cf-790954581be9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "reference = QuantumCircuit(2)\n", - "reference.h(0)\n", - "reference.cx(0, 1)\n", - "# Include barrier to separate reference from variational form\n", - "reference.barrier()\n", - "\n", - "ref_ansatz = variational_form.decompose().compose(reference, front=True)\n", - "\n", - "ref_ansatz.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "d032410e-eb2e-4410-859c-7c5f8ef8c2db", - "metadata": {}, - "source": [ - "For this new circuit, the ideal solution could be reached with all the parameters set to $0$, so this confirms that the choice of reference circuit is reasonable.\n", - "\n", - "Now let us compare the number of cost function evaluations, optimizer iterations and time taken with those of the previous attempt." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "968dedc7-4073-4014-b807-8451a50c0f13", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "ref_result = minimize(\n", - " cost_func_vqe, x0, args=(ref_ansatz, observable_1, estimator), method=\"COBYLA\"\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time" - ] - }, - { - "cell_type": "markdown", - "id": "20f61d98-86ef-4e9a-a85e-fa333309508c", - "metadata": {}, - "source": [ - "Using our optimal parameters to calculate the minimum eigenvalue:" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "241a5b4d-b5cf-41c9-a8d2-4ebf65b47a5e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-5.999999996759607\n" - ] - } - ], - "source": [ - "experimental_min_eigenvalue_ref = cost_func_vqe(\n", - " ref_result.x, ref_ansatz, observable_1, estimator\n", - ")\n", - "print(experimental_min_eigenvalue_ref)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "878ff8e1-f1d7-47c2-acda-d2541ac0b3cd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ADDED REFERENCE STATE:\n", - "Number of iterations: 127\n", - "Time (s): 0.5620882511138916\n", - "Percent error: 5.40e-08\n" - ] - } - ], - "source": [ - "print(\"ADDED REFERENCE STATE:\")\n", - "print(f\"\"\"Number of iterations: {ref_result.nfev}\"\"\")\n", - "print(f\"\"\"Time (s): {execution_time}\"\"\")\n", - "print(\n", - " f\"Percent error: {100*abs((experimental_min_eigenvalue_ref - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "28048443-9890-4168-bb19-cf95f999b6c2", - "metadata": {}, - "source": [ - "Depending on your specific system, this may or may not result in an improvement in speed or accuracy in this very small scale example. The point is that starting with physically-motivated reference states becomes increasingly important in improving speed and accuracy as problems scale." - ] - }, - { - "cell_type": "markdown", - "id": "41cdb430-27a3-4a28-92f1-d73ad6ed0dc5", - "metadata": {}, - "source": [ - "### Change the initial point" - ] - }, - { - "cell_type": "markdown", - "id": "f528cb99-dcab-4f76-ad38-00ddf4d75f50", - "metadata": {}, - "source": [ - "Now that we have seen the effect of adding the reference state, we will go into what happens when we choose different initial points $\\vec{\\theta_0}$. In particular we will use $\\vec{\\theta_0}=(0,0,0,0,6,0,0,0)$ and $\\vec{\\theta_0}=(6,6,6,6,6,6,6,6,6)$.\n", - "\n", - "Remember that, as discussed when the reference state was introduced, the ideal solution would be found when all the parameters are $0$, so the first initial point should give fewer evaluations." - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "9ef75002-f058-4946-a38d-564775a7268f", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "x0 = [0, 0, 0, 0, 6, 0, 0, 0]\n", - "\n", - "x0_1_result = minimize(\n", - " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"COBYLA\"\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "f52592f3-0af5-4f58-84ce-d1e9960d8dc2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INITIAL POINT 1:\n", - "Number of iterations: 108\n", - "Time (s): 0.4492197036743164\n" - ] - } - ], - "source": [ - "print(\"INITIAL POINT 1:\")\n", - "print(f\"\"\"Number of iterations: {x0_1_result.nfev}\"\"\")\n", - "print(f\"\"\"Time (s): {execution_time}\"\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "a8bfac60-cb21-4661-a412-ce8a41f25597", - "metadata": {}, - "source": [ - "Adjusting initial point to $\\vec{\\theta_0}=(6,6,6,6,6,6,6,6,6)$:" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "c33df46e-bd4b-490d-8704-750e271724ff", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "x0 = 6 * np.ones(raw_ansatz.num_parameters)\n", - "\n", - "x0_2_result = minimize(\n", - " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"COBYLA\"\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "a04e7171-26c3-4b38-afa7-bfe4e1b95600", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INITIAL POINT 2:\n", - "Number of iterations: 107\n", - "Time (s): 0.40889453887939453\n" - ] - } - ], - "source": [ - "print(\"INITIAL POINT 2:\")\n", - "print(f\"\"\"Number of iterations: {x0_2_result.nfev}\"\"\")\n", - "print(f\"\"\"Time (s): {execution_time}\"\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "6b842e53-bfa2-48ec-8ae9-54e3be0efaab", - "metadata": {}, - "source": [ - "By experimenting with different initial points, you might be able to achieve convergence faster and with fewer function evaluations." - ] - }, - { - "cell_type": "markdown", - "id": "9fd53a82-758c-4081-8fef-3b0bb109f0db", - "metadata": {}, - "source": [ - "### Experimenting with different optimizers\n", - "\n", - "We can adjust the optimizer using SciPy `minimize`'s `method` argument, with more options [found here](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html). We originally used a constrained minimizer (`COBYLA`). In this example, we'll explore using an unconstrained minimizer (`BFGS`) instead" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "7e21aa4c-67b1-46db-8d32-d216f7ea8f40", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "result = minimize(\n", - " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"BFGS\"\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "063f98c5-a281-47c9-978d-438ddb35b556", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CHANGED TO BFGS OPTIMIZER:\n", - "Number of iterations: 117\n", - "Time (s): 0.31656408309936523\n" - ] - } - ], - "source": [ - "print(\"CHANGED TO BFGS OPTIMIZER:\")\n", - "print(f\"\"\"Number of iterations: {result.nfev}\"\"\")\n", - "print(f\"\"\"Time (s): {execution_time}\"\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "4eb3e4e4-ae77-4098-95cc-2d29ea04301b", - "metadata": {}, - "source": [ - "## VQD example" - ] - }, - { - "cell_type": "markdown", - "id": "a0f974f7-cf29-4d30-93a7-da9ee8689c2e", - "metadata": {}, - "source": [ - "Here we implement the Qiskit patterns framework, explicitly." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3ef8f695-9694-4ccc-9fba-c24ba312a066", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Now instead of looking for only the lowest eigenvalue of our observables, we will look for all $4$, (where $k=4$).\n", - "\n", - "Remember that the cost functions of VQD are:\n", - "\n", - "$$\n", - "C_{l}(\\vec{\\theta}) :=\n", - "\\langle \\psi(\\vec{\\theta}) | \\hat{H} | \\psi(\\vec{\\theta})\\rangle +\n", - "\\sum_{j=0}^{l-1}\\beta_j |\\langle \\psi(\\vec{\\theta})| \\psi(\\vec{\\theta^j})\\rangle |^2\n", - "\\quad \\forall l\\in\\{1,\\cdots,k\\}=\\{1,\\cdots,4\\}\n", - "$$\n", - "\n", - "This is particularly important because a vector $\\vec{\\beta}=(\\beta_0,\\cdots,\\beta_{k-1})$ (in this case $(\\beta_0, \\beta_1, \\beta_2, \\beta_3)$) must be passed as an argument when we define the `VQD` object.\n", - "\n", - "Also, in Qiskit's implementation of VQD, instead of considering the effective observables described in the previous notebook, the fidelities $|\\langle \\psi(\\vec{\\theta})| \\psi(\\vec{\\theta^j})\\rangle |^2$ are calculated directly via the `ComputeUncompute` algorithm, that leverages a `Sampler` primitive to sample the probability of obtaining $|0\\rangle$ for the circuit\n", - "$U_A^\\dagger(\\vec{\\theta})U_A(\\vec{\\theta^j})$. This works precisely because this probability is\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\n", - "p_0\n", - "\n", - "& = |\\langle 0|U_A^\\dagger(\\vec{\\theta})U_A(\\vec{\\theta^j})|0\\rangle|^2 \\\\[1mm]\n", - "\n", - "& = |\\big(\\langle 0|U_A^\\dagger(\\vec{\\theta})\\big)\\big(U_A(\\vec{\\theta^j})|0\\rangle\\big)|^2 \\\\[1mm]\n", - "\n", - "& = |\\langle \\psi(\\vec{\\theta}) |\\psi(\\vec{\\theta^j}) \\rangle|^2 \\\\[1mm]\n", - "\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4d59381-43ff-4322-a6f2-df55cef18536", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ansatz = n_local(\n", - " num_qubits=2,\n", - " rotation_blocks=[\"ry\", \"rz\"],\n", - " entanglement_blocks=\"cz\",\n", - " # entanglement=\"linear\",\n", - " reps=1,\n", - ")\n", - "\n", - "ansatz.decompose().draw(\"mpl\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f49e35ed-2a4b-48bb-a060-f6fea16d7e7d", - "metadata": {}, - "source": [ - "Let's start by examining the following observable:\n", - "\n", - "$$\n", - "\\hat{O}_2 := 2 II - 3 XX + 2 YY - 4 ZZ\n", - "$$\n", - "\n", - "This observable has the following eigenvalues:\n", - "\n", - "$$\n", - "\\left\\{\n", - "\\begin{array}{c}\n", - "\\lambda_0 = -7 \\\\\n", - "\\lambda_1 = 3\\\\\n", - "\\lambda_2 = 5 \\\\\n", - "\\lambda_3 = 7\n", - "\\end{array}\n", - "\\right\\}\n", - "$$\n", - "\n", - "And eigenstates:\n", - "\n", - "$$\n", - "\\left\\{\n", - "\\begin{array}{c}\n", - "|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)\\\\\n", - "|\\phi_1\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle - |11\\rangle)\\\\\n", - "|\\phi_2\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle + |10\\rangle)\\\\\n", - "|\\phi_3\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle - |10\\rangle)\n", - "\\end{array}\n", - "\\right\\}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "917749d0-c396-4045-9df7-a61ece924485", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "observable_2 = SparsePauliOp.from_list([(\"II\", 2), (\"XX\", -3), (\"YY\", 2), (\"ZZ\", -4)])" - ] - }, - { - "cell_type": "markdown", - "id": "17bbde42-da64-44c3-9257-8c2ed4c48667", - "metadata": {}, - "source": [ - "We'll be using the following function to calculate the overlap penalty. Note that this is still part of mapping the problem to quantum circuits. However, as discussed in the previous lesson, this function calculates the overlap between a current variational circuit and the optimized circuit from a previous, lower-energy/cost state obtained. The new circuit being generated also has to be transpiled to run on real hardware. We have seen this function before, used on a simulator. Here, we must already consider the transpiling and related optimization for when we use a real backend, hence the lines around `if realbackend == 1`. This is mixing a bit of step 2, but we will call out step 2 explicitly later." - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "9999ed16-2b59-4dfb-8313-17dde10dcbf1", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "\n", - "def calculate_overlaps(\n", - " ansatz, prev_circuits, parameters, sampler, realbackend, backend\n", - "):\n", - " def create_fidelity_circuit(circuit_1, circuit_2):\n", - " if len(circuit_1.clbits) > 0:\n", - " circuit_1.remove_final_measurements()\n", - " if len(circuit_2.clbits) > 0:\n", - " circuit_2.remove_final_measurements()\n", - "\n", - " circuit = circuit_1.compose(circuit_2.inverse())\n", - " circuit.measure_all()\n", - " return circuit\n", - "\n", - " overlaps = []\n", - "\n", - " for prev_circuit in prev_circuits:\n", - " fidelity_circuit = create_fidelity_circuit(ansatz, prev_circuit)\n", - " if realbackend == 1:\n", - " pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", - " fidelity_circuit = pm.run(fidelity_circuit)\n", - " sampler_job = sampler.run([(fidelity_circuit, parameters)])\n", - " meas_data = sampler_job.result()[0].data.meas\n", - "\n", - " counts_0 = meas_data.get_int_counts().get(0, 0)\n", - " shots = meas_data.num_shots\n", - " overlap = counts_0 / shots\n", - " overlaps.append(overlap)\n", - "\n", - " return np.array(overlaps)" - ] - }, - { - "cell_type": "markdown", - "id": "5321dc7f-f546-4223-8de3-fe29008ec8d2", - "metadata": {}, - "source": [ - "Now we add VQD's cost function. Note that compared to the previous lesson, we now have two additional arguments (`realbackend` and `backend`) to help us with transpilation when we use real backends." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "07ce634a-9921-42e8-be37-74fbf1631b97", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func_vqd(\n", - " parameters,\n", - " ansatz,\n", - " prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " hamiltonian,\n", - " realbackend,\n", - " backend,\n", - "):\n", - " estimator_job = estimator.run([(ansatz, hamiltonian, [parameters])])\n", - "\n", - " total_cost = 0\n", - "\n", - " if step > 1:\n", - " overlaps = calculate_overlaps(\n", - " ansatz, prev_states, parameters, sampler, realbackend, backend\n", - " )\n", - " total_cost = np.sum(\n", - " [np.real(betas[state] * overlap) for state, overlap in enumerate(overlaps)]\n", - " )\n", - "\n", - " estimator_result = estimator_job.result()[0]\n", - "\n", - " value = estimator_result.data.evs[0] + total_cost\n", - "\n", - " return value" - ] - }, - { - "cell_type": "markdown", - "id": "722f238d-2789-47a3-9998-267b74f9e481", - "metadata": {}, - "source": [ - "Once again, we will use simulators for debugging, and then move on to real hardware." - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "c6f418c0-4cab-4334-96ed-14f26162ffcc", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.primitives import StatevectorSampler\n", - "from qiskit.primitives import StatevectorEstimator\n", - "\n", - "sampler = StatevectorSampler(default_shots=4092)\n", - "estimator = StatevectorEstimator()" - ] - }, - { - "cell_type": "markdown", - "id": "1cdb7151-2408-4f70-8381-af70255b7c34", - "metadata": {}, - "source": [ - "Here we introduce the number of states we wish to calculate, the penalties, and a set of initial parameters, x0." - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "b6c71453-4466-4fdf-8442-82483d16ff8d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "k = 4\n", - "betas = [50, 60, 40]\n", - "x0 = np.ones(8)" - ] - }, - { - "cell_type": "markdown", - "id": "60152310-abcb-439d-ae51-5627797fe821", - "metadata": {}, - "source": [ - "We will now test the algorithm using simulators:" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "0c846737-4766-454f-a5a0-d09bc04cf5c2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -6.9999999999996\n", - " x: [ 1.571e+00 1.571e+00 2.519e+00 2.100e+00 1.242e+00\n", - " 6.935e-01 2.298e+00 1.991e+00]\n", - " nfev: 151\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: 3.698974255258432\n", - " x: [ 1.269e+00 1.109e+00 1.080e+00 1.200e+00 1.094e+00\n", - " 1.163e+00 9.752e-01 9.519e-01]\n", - " nfev: 103\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: 4.731320121938101\n", - " x: [ 1.533e+00 2.451e+00 2.526e+00 2.406e+00 1.968e+00\n", - " 2.105e+00 8.537e-01 8.442e-01]\n", - " nfev: 110\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: 7.008239313655201\n", - " x: [ 4.150e+00 2.120e+00 3.495e+00 7.262e-01 1.953e+00\n", - " -1.982e-01 3.263e-01 2.563e+00]\n", - " nfev: 126\n", - " maxcv: 0.0\n" - ] - } - ], - "source": [ - "from scipy.optimize import minimize\n", - "\n", - "prev_states = []\n", - "prev_opt_parameters = []\n", - "eigenvalues = []\n", - "\n", - "realbackend = 0\n", - "\n", - "for step in range(1, k + 1):\n", - " if step > 1:\n", - " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", - "\n", - " result = minimize(\n", - " cost_func_vqd,\n", - " x0,\n", - " args=(\n", - " ansatz,\n", - " prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " observable_2,\n", - " realbackend,\n", - " None,\n", - " ),\n", - " method=\"COBYLA\",\n", - " options={\"maxiter\": 200, \"tol\": 0.000001},\n", - " )\n", - " print(result)\n", - "\n", - " prev_opt_parameters = result.x\n", - " eigenvalues.append(result.fun)" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "91546335-2f0f-4e05-bb92-62c49f67a005", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[np.float64(-6.9999999999996),\n", - " np.float64(3.698974255258432),\n", - " np.float64(4.731320121938101),\n", - " np.float64(7.008239313655201)]" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eigenvalues" - ] - }, - { - "cell_type": "markdown", - "id": "14d995bd-e486-48cc-b73d-b2ca042dce07", - "metadata": {}, - "source": [ - "These results are fairly close to the expected ones except for approximation error and global phase. We could adjust the tolerance on the classical optimizer and/or the penalties for statevector overlap to obtain more precise values." - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "id": "6e18fcde-09b2-45a0-a04b-3ee7a6e9bde6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Percent error: 5.71e-14\n", - "Percent error: 2.33e-01\n", - "Percent error: 5.37e-02\n", - "Percent error: 1.18e-03\n" - ] - } - ], - "source": [ - "solution_eigenvalues = [-7, 3, 5, 7]\n", - "\n", - "for index, experimental_eigenvalue in enumerate(eigenvalues):\n", - " solution_eigenvalue = solution_eigenvalues[index]\n", - "\n", - " print(\n", - " f\"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", - " )" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "13fc804e-6248-4738-9187-dbebfc2fbfd6", - "metadata": {}, - "source": [ - "### Change betas" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "36b1bee9-e6f2-4978-be4f-509f67d7f5da", - "metadata": {}, - "source": [ - "As mentioned in the previous lesson, the values of $\\vec{\\beta}$ should be bigger than the difference between eigenvalues. Let us see what happens when they do not satisfy that condition with $\\hat{O}_2$\n", - "\n", - "$$\n", - "\\hat{O}_2 = 2 II - 3 XX + 2 YY - 4 ZZ\n", - "$$\n", - "\n", - "with eigenvalues\n", - "\n", - "$$\n", - "\\left\\{\n", - "\\begin{array}{c}\n", - "\\lambda_0 = -7 \\\\\n", - "\\lambda_1 = 3\\\\\n", - "\\lambda_2 = 5 \\\\\n", - "\\lambda_3 = 7\n", - "\\end{array}\n", - "\\right\\}\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "91f7f00a-b843-4b8d-98a6-836d41643f7a", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "k = 4\n", - "betas = np.ones(3)\n", - "x0 = np.zeros(8)" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "011f21d7-358b-4f8f-ac3e-fd348e486c9e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -6.999916534745094\n", - " x: [ 1.568e+00 -1.569e+00 1.385e-01 1.398e-01 -7.972e-01\n", - " 7.835e-01 -2.375e-01 4.539e-02]\n", - " nfev: 125\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -1.515139929812874\n", - " x: [-5.317e-04 -2.514e-03 1.016e+00 9.998e-01 3.890e-04\n", - " 1.772e-04 1.568e-04 8.497e-04]\n", - " nfev: 35\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: -0.509948114293115\n", - " x: [-3.796e-03 8.853e-03 3.015e-04 9.997e-01 6.271e-04\n", - " -2.554e-03 1.017e-04 2.766e-04]\n", - " nfev: 37\n", - " maxcv: 0.0\n", - " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", - " success: True\n", - " status: 0\n", - " fun: 0.4914672235935682\n", - " x: [-7.178e-03 -8.652e-03 1.125e+00 -5.428e-02 -1.586e-03\n", - " 2.031e-03 -3.462e-03 5.734e-03]\n", - " nfev: 35\n", - " maxcv: 0.0\n" - ] - } - ], - "source": [ - "from scipy.optimize import minimize\n", - "\n", - "prev_states = []\n", - "prev_opt_parameters = []\n", - "eigenvalues = []\n", - "\n", - "realbackend = 0\n", - "\n", - "for step in range(1, k + 1):\n", - " if step > 1:\n", - " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", - "\n", - " result = minimize(\n", - " cost_func_vqd,\n", - " x0,\n", - " args=(\n", - " ansatz,\n", - " prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " observable_2,\n", - " realbackend,\n", - " None,\n", - " ),\n", - " method=\"COBYLA\",\n", - " options={\"tol\": 0.01, \"maxiter\": 200},\n", - " )\n", - " print(result)\n", - "\n", - " prev_opt_parameters = result.x\n", - " eigenvalues.append(result.fun)" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "b7076fc6-0929-4032-a4a3-b50ba39a59a2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Percent error: 1.19e-05\n", - "Percent error: 1.51e+00\n", - "Percent error: 1.10e+00\n", - "Percent error: 9.30e-01\n" - ] - } - ], - "source": [ - "solution_eigenvalues = [-7, 3, 5, 7]\n", - "\n", - "for index, experimental_eigenvalue in enumerate(eigenvalues):\n", - " solution_eigenvalue = solution_eigenvalues[index]\n", - "\n", - " print(\n", - " f\"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", - " )" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a47a9ce5-b591-4550-bd39-2fffb699b832", - "metadata": {}, - "source": [ - "This time, the optimizer returns the same state $|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$ as a proposed solution to all eigenstates: which is clearly wrong. This happens because the betas were too small to penalize the minimum eigenstate in the successive cost functions. Therefore, it was not excluded from the effective search space in later iterations of the algorithm, and always chosen as the best possible solution.\n", - "\n", - "We recommend experimenting with the values of $\\vec{\\beta}$, and ensuring they are bigger than the difference between eigenvalues." - ] - }, - { - "cell_type": "markdown", - "id": "6978f795-f949-4b10-9420-6d64bc907b4a", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum execution\n", - "\n", - "To run this on real hardware, we must optimize the quantum circuits for our quantum computer of choice. For our purposes here, we will simply use the least busy backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e9b485a5-8446-4667-af29-81a2a30d9fee", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "from qiskit_ibm_runtime import Session, EstimatorOptions\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "# Or use a specific backend\n", - "# backend = service.backend(\"ibm_brisbane\")\n", - "print(backend)" - ] - }, - { - "cell_type": "markdown", - "id": "12f03728-656d-45cc-a1c5-5366ec301304", - "metadata": {}, - "source": [ - "We will transpile our circuit using a preset pass manager and optimization level 3." - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "ba4a98a3-159b-4863-84bd-298be5b6d091", - "metadata": {}, - "outputs": [], - "source": [ - "pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", - "isa_ansatz = pm.run(ansatz)\n", - "isa_observable = observable_2.apply_layout(layout=isa_ansatz.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "a29f9139-3c91-424f-97d2-80b31cce5d91", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "Taking care to reset our betas to sufficiently high values, we can now run our calculation on real quantum hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3e373e99-19d9-4fb2-b26e-208f1a2b7c9b", - "metadata": {}, - "outputs": [], - "source": [ - "# Estimated compute resource usage: 25 minutes. Benchmarked at 24 min, 30 sec on an Eagle r3 processor on 5-30-24\n", - "\n", - "k = 2\n", - "betas = [30, 50, 80]\n", - "x0 = np.zeros(8)\n", - "\n", - "real_prev_states = []\n", - "real_prev_opt_parameters = []\n", - "real_eigenvalues = []\n", - "\n", - "realbackend = 1\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session, options=estimator_options)\n", - " sampler = Sampler(mode=session)\n", - "\n", - " for step in range(1, k + 1):\n", - " if step > 1:\n", - " real_prev_states.append(isa_ansatz.assign_parameters(prev_opt_parameters))\n", - "\n", - " result = minimize(\n", - " cost_func_vqd,\n", - " x0,\n", - " args=(\n", - " isa_ansatz,\n", - " real_prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " isa_observable,\n", - " realbackend,\n", - " backend,\n", - " ),\n", - " method=\"COBYLA\",\n", - " options={\"maxiter\": 200},\n", - " )\n", - " print(result)\n", - "\n", - " real_prev_opt_parameters = result.x\n", - " real_eigenvalues.append(result.fun)\n", - "\n", - "session.close()\n", - "print(real_eigenvalues)" - ] - }, - { - "cell_type": "markdown", - "id": "f8e976c0-5d73-4244-b4be-4808925a3e40", - "metadata": {}, - "source": [ - "### Step 4: Post-process, return result in classical format\n", - "\n", - "Our output is structurally similar to what has been discussed in previous lessons and examples. But there is something problematic in the results above, from which we can derive a cautionary message for the context of excited states. To limit computing time used on this learning example, we set a maximum number of iterations for classical optimizer that was potentially too low: 200 iterations. A previous calculation above, on a simulator, failed to converge in 200 iterations. Here, ours did converge... but to what tolerance? We have not specified a tolerance for COBYLA to consider itself \"converged\". A glance at the function value and comparison with previous runs tells us that COBYLA was not close to converging to the precision we require.\n", - "\n", - "There is another issue: the energy of the first excited state appears to be lower than the energy of the ground state! See if you can explain how this could happen. Hint: it is related to the convergence point we just addressed. This behavior is explained in detail below after VQD is applied to the H2 molecule." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4b6857c6-09fe-44ca-ba9f-822eb9ee5ad8", - "metadata": {}, - "source": [ - "## Quantum chemistry: ground state and excited energy solver\n", - "\n", - "Our objective is to minimize the expectation value of the observable representing energy (Hamiltonian $\\hat{\\mathcal{H}}$):\n", - "\n", - "$$\n", - "\\min_{\\vec\\theta} \\langle\\psi(\\vec\\theta)|\\hat{\\mathcal{H}}|\\psi(\\vec\\theta)\\rangle\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "de006d72-db04-49d4-806b-89bd1e5b9947", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.circuit.library import efficient_su2\n", - "\n", - "H2_op = SparsePauliOp.from_list(\n", - " [\n", - " (\"II\", -1.052373245772859),\n", - " (\"IZ\", 0.39793742484318045),\n", - " (\"ZI\", -0.39793742484318045),\n", - " (\"ZZ\", -0.01128010425623538),\n", - " (\"XX\", 0.18093119978423156),\n", - " ]\n", - ")\n", - "\n", - "chem_ansatz = efficient_su2(H2_op.num_qubits)\n", - "\n", - "chem_ansatz.decompose().draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "1e67cecd-cb3d-4593-a9e8-1058e816c9ae", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "\n", - "def cost_func_vqe(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (Estimator): Estimator primitive instance\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, hamiltonian, params)\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", - " return cost" - ] - }, - { - "cell_type": "markdown", - "id": "21b278c0-719c-4240-ae6e-f21360cb21f5", - "metadata": {}, - "source": [ - "We now set an initial set of parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "d6a2f152-6642-4b66-aa34-951d86973f8c", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "x0 = np.ones(chem_ansatz.num_parameters)" - ] - }, - { - "cell_type": "markdown", - "id": "1ad424a9-4c4c-4c36-8408-2fc76318eeb9", - "metadata": {}, - "source": [ - "We can minimize this cost function to calculate optimal parameters, and we can check our code first by using a local simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "69785252-e337-404a-8fbf-ae2a5a875525", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.primitives import StatevectorEstimator as Estimator\n", - "from qiskit.primitives import StatevectorSampler as Sampler\n", - "\n", - "estimator = Estimator()\n", - "sampler = Sampler()" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "79fd09b2-d886-4281-9381-b802b4d2f7a7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - " message: Optimization terminated successfully.\n", - " success: True\n", - " status: 1\n", - " fun: -1.857275029048451\n", - " x: [ 7.326e-01 1.354e+00 ... 1.040e+00 1.508e+00]\n", - " nfev: 242\n", - " maxcv: 0.0" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# SciPy minimizer routine\n", - "from scipy.optimize import minimize\n", - "import time\n", - "\n", - "start_time = time.time()\n", - "\n", - "result = minimize(\n", - " cost_func_vqe, x0, args=(chem_ansatz, H2_op, estimator), method=\"COBYLA\"\n", - ")\n", - "\n", - "end_time = time.time()\n", - "execution_time = end_time - start_time\n", - "\n", - "result" - ] - }, - { - "cell_type": "markdown", - "id": "0b2beab7-816d-438b-8502-5e7f9d65ae1d", - "metadata": {}, - "source": [ - "The minimum value of the cost function (-1.857...) is the ground state energy of the H2 molecule, in units of hartrees." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "54a8a55c-6aa4-4e7c-a999-d3e468079571", - "metadata": {}, - "source": [ - "### Excited States\n", - "\n", - "We can also leverage VQD to solve for $k=2$ total states (the ground state and the first excited state)." - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "678ed2ab-c572-43a8-9a87-26cb0c470b68", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "import numpy as np\n", - "\n", - "k = 2\n", - "betas = [33, 33]\n", - "# x0 = np.zeros(ansatz.num_parameters)\n", - "x0 = [\n", - " 1.164e00,\n", - " -2.438e-01,\n", - " 9.358e-04,\n", - " 6.745e-02,\n", - " 1.990e00,\n", - " 9.810e-02,\n", - " 6.154e-01,\n", - " 5.454e-01,\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "760ebdc7-596c-4781-824a-3b7972cdba53", - "metadata": {}, - "source": [ - "We'll add our overlap calculation:" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "id": "d755f8b7-9aab-4c74-84ff-80f62a9b3c23", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " message: Optimization terminated successfully.\n", - " success: True\n", - " status: 1\n", - " fun: -1.8572671093941977\n", - " x: [ 1.164e+00 -2.437e-01 2.118e-03 6.448e-02 1.990e+00\n", - " 9.870e-02 6.167e-01 5.476e-01]\n", - " nfev: 58\n", - " maxcv: 0.0\n", - " message: Optimization terminated successfully.\n", - " success: True\n", - " status: 1\n", - " fun: -1.0322873777662176\n", - " x: [ 3.205e+00 1.502e+00 1.699e+00 -1.107e-02 3.086e+00\n", - " 1.530e+00 4.445e-02 7.013e-02]\n", - " nfev: 99\n", - " maxcv: 0.0\n" - ] - } - ], - "source": [ - "from scipy.optimize import minimize\n", - "\n", - "prev_states = []\n", - "prev_opt_parameters = []\n", - "eigenvalues = []\n", - "\n", - "realbackend = 0\n", - "\n", - "for step in range(1, k + 1):\n", - " if step > 1:\n", - " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", - "\n", - " result = minimize(\n", - " cost_func_vqd,\n", - " x0,\n", - " args=(\n", - " ansatz,\n", - " prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " H2_op,\n", - " realbackend,\n", - " None,\n", - " ),\n", - " method=\"COBYLA\",\n", - " options={\"tol\": 0.001, \"maxiter\": 2000},\n", - " )\n", - " print(result)\n", - "\n", - " prev_opt_parameters = result.x\n", - " eigenvalues.append(result.fun)" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "212c3e80-1b5e-4155-a3d3-d3bf12c2e97a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[-1.8572671093941977, -1.0322873777662176]" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eigenvalues" - ] - }, - { - "cell_type": "markdown", - "id": "deec3d64-7f91-4623-8236-63a7de6aa6e6", - "metadata": {}, - "source": [ - "### Real hardware and a final cautionary message\n", - "\n", - "To run this on real hardware, we must optimize the quantum circuits for our quantum computer of choice. For our purposes here, we will simply use the least busy backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5fdaae83-47cf-40ab-95d9-d1ff048ed825", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "from qiskit_ibm_runtime import Session, EstimatorOptions\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)" - ] - }, - { - "cell_type": "markdown", - "id": "ad33ec78-d495-48f2-b64f-6042d5319da6", - "metadata": {}, - "source": [ - "We will use a preset pass manager for transpilation, and we will maximally optimize our circuit using optimization level 3." - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "9733e0b3-fc05-4c5b-b29a-1ba197106fad", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", - "isa_ansatz = pm.run(ansatz)\n", - "isa_observable = H2_op.apply_layout(layout=isa_ansatz.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "262a1b78-a9bc-4850-bef8-60e290f2f2e0", - "metadata": {}, - "source": [ - "Because VQD is highly iterative, we will carry out all steps inside a Runtime session, such that our jobs will only be queued at the beginning, and not between every parameter update. Nothing else changes about the syntax for the cost function or estimator." - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "12843daa-4b84-408d-ae46-f72d24509ae1", - "metadata": {}, - "outputs": [], - "source": [ - "x0 = [\n", - " 1.306e00,\n", - " -2.284e-01,\n", - " 6.913e-02,\n", - " -2.530e-02,\n", - " 1.849e00,\n", - " 7.433e-02,\n", - " 6.366e-01,\n", - " 5.600e-01,\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd8e07fd-5968-4741-919e-4d9c46d58de5", - "metadata": {}, - "outputs": [], - "source": [ - "# Estimated hardware usage: 20 min benchmarked on an Eagle r3 processor on 5-30-24\n", - "\n", - "real_prev_states = []\n", - "real_prev_opt_parameters = []\n", - "real_eigenvalues = []\n", - "\n", - "realbackend = 1\n", - "\n", - "estimator_options = EstimatorOptions(resilience_level=1, default_shots=4096)\n", - "\n", - "with Session(backend=backend) as session:\n", - " estimator = Estimator(mode=session)\n", - " sampler = Sampler(mode=session)\n", - "\n", - " for step in range(1, k + 1):\n", - " if step > 1:\n", - " real_prev_states.append(\n", - " isa_ansatz.assign_parameters(real_prev_opt_parameters)\n", - " )\n", - "\n", - " result = minimize(\n", - " cost_func_vqd,\n", - " x0,\n", - " args=(\n", - " isa_ansatz,\n", - " real_prev_states,\n", - " step,\n", - " betas,\n", - " estimator,\n", - " sampler,\n", - " isa_observable,\n", - " realbackend,\n", - " backend,\n", - " ),\n", - " method=\"COBYLA\",\n", - " options={\"tol\": 0.001, \"maxiter\": 300},\n", - " )\n", - " print(result)\n", - "\n", - " real_prev_opt_parameters = result.x\n", - " real_eigenvalues.append(result.fun)\n", - "\n", - "session.close()\n", - "print(real_eigenvalues)" - ] - }, - { - "cell_type": "markdown", - "id": "4c5985c2-86af-4969-b0aa-1932502e58e6", - "metadata": {}, - "source": [ - "The ground state energy obtained (-1.83 hartrees) is not too far from the correct value (-1.85 hartrees). However, the excited state energy is quite a bit off. This is similar to the erroneous behavior we saw earlier in this lesson. The energy reported for the excited state is nearly the same as that for the ground state. In the previous case, we even saw an excited state energy that was _lower_ than the reported ground state energy.\n", - "\n", - "It is not possible for a variational calculation to yield an energy that is lower than the true ground state energy. In the earlier instance, the ground state energy we obtained was not very close to the true ground state. Since we did not obtain the true ground state energy in that case, there is no contradiction. In the present case, the ground state energy was fairly close to the correct value, and yet the excited state energy seems strangely close to that same value.\n", - "\n", - "To understand better how this happened, recall that the way we find an excited state is by requiring that the variational state be orthogonal to the ground state (using the overlap circuits and penalty terms). If we fail to obtain an accurate ground state energy (or are off by a few percent), then we also fail to obtain an accurate ground state vector! So when we require that the excited state be orthogonal to the first state we found, we were not imposing orthogonality with the true ground state, but rather with some approximation of it (sometimes a poor approximation of it). Thus, the excited state was not forced to be orthogonal to the true ground state, and our energy estimates for the excited states were actually quite close to the ground state energy.\n", - "\n", - "This will always be a concern in VQD. But in principle, this can be corrected by increasing the maximum number of iterations for the classical optimizer, imposing lower tolerance for the classical optimizer, and possibly also trying a different ansatz if we are habitually missing the true ground state. As we have seen, one may also need to modify the overlap penalties (betas). But that is really a separate issue. No penalty for overlap will keep you away from the true ground state, if you haven't found a very good estimate of the true ground state for the overlap circuit." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "af2202c6-2d1b-4213-a244-1dc1e34e5e34", - "metadata": {}, - "source": [ - "## Optimization: max-cut\n", - "\n", - "The maximum cut (max-cut) problem is a combinatorial optimization problem that involves dividing the vertices of a graph into two disjoint sets such that the number of edges between the two sets is maximized. More formally, given an undirected graph $G=(V,E)$, where $V$ is the set of vertices and $E$ is the set of edges, the max-cut problem asks to partition the vertices into two disjoint subsets, $S$ and $T$, such that the number of edges with one endpoint in $S$ and the other in $T$ is maximized.\n", - "\n", - "We can apply max-cut to solve a various problems, such as clustering, network design, and phase transitions. We'll start by creating a problem graph:" - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "id": "3eb71b79-a988-4807-aa88-7fb25a78a236", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import rustworkx as rx\n", - "from rustworkx.visualization import mpl_draw\n", - "\n", - "n = 4\n", - "G = rx.PyGraph()\n", - "G.add_nodes_from(range(n))\n", - "# The edge syntax is (start, end, weight)\n", - "edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]\n", - "G.add_edges_from(edges)\n", - "\n", - "mpl_draw(\n", - " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=\"#1192E8\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "8771ec8d-9913-4e25-94dd-96dc57c9675d", - "metadata": {}, - "source": [ - "This problem can be expressed as a binary optimization problem. For each node $0 \\leq i < n$, where $n$ is the number of nodes of the graph (in this case $n=4$), we will consider the binary variable $x_i$. This variable will have the value $1$ if node $i$ is one of the groups that we'll label $1$ and $0$ if it's in the other group, that we'll label as $0$. We will also denote as $w_{ij}$ (element $(i,j)$ of the adjacency matrix $w$) the weight of the edge that goes from node $i$ to node $j$. Because the graph is undirected, $w_{ij}=w_{ji}$. Then we can formulate our problem as maximizing the following cost function:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "C(\\vec{x})\n", - "& =\\sum_{i,j=0}^n w_{ij} x_i(1-x_j)\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i,j=0}^n w_{ij} x_ix_j\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i=0}^n \\sum_{j=0}^i 2w_{ij} x_ix_j\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "\n", - "To solve this problem with a quantum computer, we are going to express the cost function as the expected value of an observable. However, the observables that Qiskit admits natively consist of Pauli operators, that have eigenvalues $1$ and $-1$ instead of $0$ and $1$. That's why we are going to make the following change of variable:\n", - "\n", - "Where $\\vec{x}=(x_0,x_1,\\cdots ,x_{n-1})$. We can use the adjacency matrix $w$ to comfortably access the weights of all the edge. This will be used to obtain our cost function:\n", - "\n", - "$$\n", - "z_i = 1-2x_i \\rightarrow x_i = \\frac{1-z_i}{2}\n", - "$$\n", - "\n", - "This implies that:\n", - "\n", - "$$\n", - "\\begin{array}{lcl} x_i=0 & \\rightarrow & z_i=1 \\\\ x_i=1 & \\rightarrow & z_i=-1.\\end{array}\n", - "$$\n", - "\n", - "So the new cost function we want to maximize is:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "C(\\vec{z})\n", - "& = \\sum_{i,j=0}^n w_{ij} \\bigg(\\frac{1-z_i}{2}\\bigg)\\bigg(1-\\frac{1-z_j}{2}\\bigg)\\\\[1mm]\n", - "\n", - "& = \\sum_{i,j=0}^n \\frac{w_{ij}}{4} - \\sum_{i,j=0}^n \\frac{w_{ij}}{4} z_iz_j\\\\[1mm]\n", - "\n", - "& = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Moreover, the natural tendency of a quantum computer is to find minima (usually the lowest energy) instead of maxima so instead of maximizing $C(\\vec{z})$ we are going to minimize:\n", - "\n", - "$$\n", - "-C(\\vec{z}) = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", - "$$\n", - "\n", - "Now that we have a cost function to minimize whose variables can have the values $-1$ and $1$, we can make the following analogy with the Pauli $Z$:\n", - "\n", - "$$\n", - "z_i \\equiv Z_i = \\overbrace{I}^{n-1}\\otimes ... \\otimes \\overbrace{Z}^{i} \\otimes ... \\otimes \\overbrace{I}^{0}\n", - "$$\n", - "\n", - "In other words, the variable $z_i$ will be equivalent to a $Z$ gate acting on qubit $i$. Moreover:\n", - "\n", - "$$\n", - "Z_i|x_{n-1}\\cdots x_0\\rangle = z_i|x_{n-1}\\cdots x_0\\rangle \\rightarrow \\langle x_{n-1}\\cdots x_0 |Z_i|x_{n-1}\\cdots x_0\\rangle = z_i\n", - "$$\n", - "\n", - "Then the observable we are going to consider is:\n", - "\n", - "$$\n", - "\\hat{H} = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} Z_iZ_j\n", - "$$\n", - "\n", - "to which we will have to add the independent term afterwards:\n", - "\n", - "$$\n", - "\\texttt{offset} = - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "84bbf27e-6def-4ce0-b563-ae5ae2fcfe34", - "metadata": {}, - "source": [ - "The operator is a linear combination of terms with Z operators on nodes connected by an edge (recall that the 0th qubit is farthest right): $IIZZ + IZIZ + IZZI + ZIIZ + ZZII$. Once the operator is constructed, the ansatz for the QAOA algorithm can easily be built by using the `QAOAAnsatz` circuit from the Qiskit circuit library." - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "id": "d28c4262-a080-4fe4-9427-762326184cd8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 59, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import QAOAAnsatz\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "max_hamiltonian = SparsePauliOp.from_list(\n", - " [(\"IIZZ\", 1), (\"IZIZ\", 1), (\"IZZI\", 1), (\"ZIIZ\", 1), (\"ZZII\", 1)]\n", - ")\n", - "\n", - "\n", - "max_ansatz = QAOAAnsatz(max_hamiltonian, reps=2)\n", - "# Draw\n", - "max_ansatz.decompose(reps=3).draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "1d3f4fc4-6804-40fd-a470-ddd2c09a94d4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Offset: -2.5\n" - ] - } - ], - "source": [ - "# Sum the weights, and divide by 2\n", - "\n", - "offset = -sum(edge[2] for edge in edges) / 2\n", - "print(f\"\"\"Offset: {offset}\"\"\")" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "d6bf5f9c-5277-4833-8864-1e0277c1b6c1", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (Estimator): Estimator primitive instance\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, hamiltonian, params)\n", - " cost = estimator.run([pub]).result()[0].data.evs\n", - " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", - " return cost" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "159e0521-4870-4e8d-829a-317d6dafce79", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.primitives import StatevectorEstimator as Estimator\n", - "from qiskit.primitives import StatevectorSampler as Sampler\n", - "\n", - "estimator = Estimator()\n", - "sampler = Sampler()" - ] - }, - { - "cell_type": "markdown", - "id": "6ca87213-311a-46bd-b2c5-814e2c8ffe27", - "metadata": {}, - "source": [ - "We now set an initial set of random parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "babd595f-a1b0-4513-9a97-3b23428b83f6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[6.0252949 0.58448176 2.15785731 1.13646074]\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "x0 = 2 * np.pi * np.random.rand(max_ansatz.num_parameters)\n", - "print(x0)" - ] - }, - { - "cell_type": "markdown", - "id": "3447ab1d-e2c1-4d8d-98f6-ee231f3bbc7a", - "metadata": {}, - "source": [ - "Any classical optimizer can be used to minimize the cost function. On a real quantum system, an optimizer designed for non-smooth cost function landscapes usually does better. Here we use the [COBYLA routine](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-cobyla.html#optimize-minimize-cobyla) from SciPy via the minimize function.\n", - "\n", - "Because we are iteratively executing many calls to Runtime, we make use of a session to execute all calls within a single block. Moreover, for QAOA, the solution is encoded in the output distribution of the ansatz circuit bound with the optimal parameters from the minimization. Therefore, we will need a Sampler primitive, and will instantiate it with the same session" - ] - }, - { - "cell_type": "markdown", - "id": "ecc3e775-4e9a-4a4b-ab78-ed53d690b64b", - "metadata": {}, - "source": [ - "And run our minimization routine:" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "03c79980-1510-4a11-83aa-797111c02187", - "metadata": {}, - "outputs": [], - "source": [ - "result = minimize(\n", - " cost_func, x0, args=(max_ansatz, max_hamiltonian, estimator), method=\"COBYLA\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "3be090d4-4adb-4943-931b-5f603df663a1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " message: Optimization terminated successfully.\n", - " success: True\n", - " status: 1\n", - " fun: -2.585287311689236\n", - " x: [ 7.332e+00 3.904e-01 2.045e+00 1.028e+00]\n", - " nfev: 80\n", - " maxcv: 0.0\n" - ] - } - ], - "source": [ - "print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "c2e3ae85-7eb1-4c41-9ab8-0a38008374c1", - "metadata": {}, - "source": [ - "The solution vector of parameter angles (`x`), when plugged into the ansatz circuit, yields the graph partitioning that we were looking for." - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "141b2c86-3902-46f0-bb67-46f4565c3975", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Eigenvalue: -2.585287311689236\n", - "Max-Cut Objective: -5.085287311689235\n" - ] - } - ], - "source": [ - "eigenvalue = cost_func(result.x, max_ansatz, max_hamiltonian, estimator)\n", - "print(f\"\"\"Eigenvalue: {eigenvalue}\"\"\")\n", - "print(f\"\"\"Max-Cut Objective: {eigenvalue + offset}\"\"\")" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "b882490f-745a-4b07-ae94-bb3673316ce6", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.result import QuasiDistribution\n", - "from qiskit.primitives import StatevectorSampler\n", - "\n", - "sampler = StatevectorSampler()\n", - "\n", - "# Assign solution parameters to ansatz\n", - "qc = max_ansatz.assign_parameters(result.x)\n", - "\n", - "# Add measurements to our circuit\n", - "qc.measure_all()\n", - "\n", - "# Sample ansatz at optimal parameters\n", - "# samp_dist = sampler.run(qc).result().quasi_dists[0]\n", - "\n", - "shots = 1024\n", - "job = sampler.run([qc], shots=shots)" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "id": "686ee28e-9327-40d1-8d7c-3ec527160f01", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 77, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc.decompose().draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "c2e4ebb9-530a-4b72-acb1-1356634dc7a5", - "metadata": {}, - "outputs": [], - "source": [ - "data_pub = job.result()[0].data\n", - "bitstrings = data_pub.meas.get_bitstrings()\n", - "counts = data_pub.meas.get_counts()\n", - "quasi_dist = QuasiDistribution(\n", - " {outcome: freq / shots for outcome, freq in counts.items()}\n", - ")\n", - "probabilities = quasi_dist\n", - "\n", - "# Close the session since we are now done with it\n", - "# session.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "13a4c371-fd40-4ce3-b737-268d4a4c9c2c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_distribution\n", - "\n", - "plot_distribution(counts)" - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "id": "4238cc64-6910-43d2-889e-6322bf2864eb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "binary_string = max(counts.items(), key=lambda kv: kv[1])[0]\n", - "x = np.asarray([int(y) for y in reversed(list(binary_string))])\n", - "\n", - "colors = [\"r\" if x[i] == 0 else \"c\" for i in range(n)]\n", - "mpl_draw(\n", - " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=colors\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "761b55e2-2e6b-4d00-ad1a-b0d3d546881e", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "With this lesson, you have learned:\n", - "\n", - "- How to write a custom variational algorithm\n", - "- How to apply a variational algorithm to find minimum eigenvalues\n", - "- How to utilize variational algorithms to solve application use cases\n", - "\n", - "Proceed to the final lesson to take your assessment and earn your badge!" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "b45c3fbc-3d24-4da0-88a4-8db2150aba7c", + "metadata": {}, + "source": [ + "---\n", + "title: Examples and applications\n", + "description: Here we describe how variational quantum algorithms can be applied to chemistry problems, max-cut, and more!\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore disp eigvalsh realbackend IIZZ IZIZ IZZI ZIIZ ZZII */}\n", + "\n", + "# Examples and applications\n", + "\n", + "During this lesson, we'll explore some variational algorithm examples and how to apply them:\n", + "\n", + "- How to write a custom variational algorithm\n", + "- How to apply a variational algorithm to find minimum eigenvalues\n", + "- How to utilize variational algorithms to solve application use cases\n", + "\n", + "Note that the Qiskit patterns framework can be applied to all the problems we introduce here. However, to avoid repetition, we will only explicitly call out the framework steps in one example case, run on real hardware." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8b7f0c00-af07-4043-8af3-0b3d514f1a32", + "metadata": {}, + "source": [ + "## Problem definitions\n", + "\n", + "Imagine that we want to use a variational algorithm to find the eigenvalue of the following observable:\n", + "\n", + "$$\n", + "\\hat{O}_1 = 2 II - 2 XX + 3 YY - 3 ZZ,\n", + "$$\n", + "\n", + "This observable has the following eigenvalues:\n", + "\n", + "$$\n", + "\\left\\{\n", + "\\begin{array}{c}\n", + "\\lambda_0 = -6 \\\\\n", + "\\lambda_1 = 4 \\\\\n", + "\\lambda_2 = 4 \\\\\n", + "\\lambda_3 = 6\n", + "\\end{array}\n", + "\\right\\}\n", + "$$\n", + "\n", + "And eigenstates:\n", + "\n", + "$$\n", + "\\left\\{\n", + "\\begin{array}{c}\n", + "|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)\\\\\n", + "|\\phi_1\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle - |11\\rangle)\\\\\n", + "|\\phi_2\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle - |10\\rangle)\\\\\n", + "|\\phi_3\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle + |10\\rangle)\n", + "\\end{array}\n", + "\\right\\}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "1cb9f137-3cb0-45dd-a49a-61d0be119c68", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "observable_1 = SparsePauliOp.from_list([(\"II\", 2), (\"XX\", -2), (\"YY\", 3), (\"ZZ\", -3)])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "297600c7-8f3d-4070-b066-b2a0496f7f9c", + "metadata": {}, + "source": [ + "## Custom VQE\n", + "\n", + "We'll first explore how to construct a VQE instance manually to find the lowest eigenvalue for $\\hat{O}_1$. This will incorporate a variety of techniques that we have covered throughout this course." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "36120a96-c625-49d7-9ad3-ca70bfb08346", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func_vqe(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (Estimator): Estimator primitive instance\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, hamiltonian, params)\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + "\n", + " return cost" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57c071aa-e8ab-4b39-b5db-773016da2550", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library.n_local import n_local\n", + "from qiskit import QuantumCircuit\n", + "\n", + "import numpy as np\n", + "\n", + "reference_circuit = QuantumCircuit(2)\n", + "reference_circuit.x(0)\n", + "\n", + "variational_form = n_local(\n", + " num_qubits=2,\n", + " rotation_blocks=[\"rz\", \"ry\"],\n", + " entanglement_blocks=\"cx\",\n", + " entanglement=\"linear\",\n", + " reps=1,\n", + ")\n", + "\n", + "raw_ansatz = reference_circuit.compose(variational_form)\n", + "raw_ansatz.decompose().draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "4a138028-d14c-4146-921e-476698102568", + "metadata": {}, + "source": [ + "We will start debugging on local simulators." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "2f7c0fab-d6c2-4c90-b122-a5a90976bdaf", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import StatevectorEstimator as Estimator\n", + "from qiskit.primitives import StatevectorSampler as Sampler\n", + "\n", + "estimator = Estimator()\n", + "sampler = Sampler()" + ] + }, + { + "cell_type": "markdown", + "id": "824c57f9-8bf0-478a-99fd-3f89a48a1aff", + "metadata": {}, + "source": [ + "We now set an initial set of parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "cd0e029d-0807-4387-aef0-96ea5866c063", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1. 1. 1. 1. 1. 1. 1. 1.]\n" + ] + } + ], + "source": [ + "x0 = np.ones(raw_ansatz.num_parameters)\n", + "print(x0)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0d70046a-16ac-48b4-9a0d-5afe3b65af09", + "metadata": {}, + "source": [ + "We can minimize this cost function to calculate optimal parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "25df1d56-ad05-43a8-b2cb-60b16105f6e4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Return from COBYLA because the trust region radius reaches its lower bound.\n", + "Number of function values = 103 Least value of F = -5.999999998357189\n", + "The corresponding X is:\n", + "[2.27483579e+00 8.37593091e-01 1.57080508e+00 5.82932911e-06\n", + " 2.49973063e+00 6.41884255e-01 6.33686904e-01 6.33688223e-01]\n", + "\n" + ] + } + ], + "source": [ + "# SciPy minimizer routine\n", + "from scipy.optimize import minimize\n", + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "result = minimize(\n", + " cost_func_vqe,\n", + " x0,\n", + " args=(raw_ansatz, observable_1, estimator),\n", + " method=\"COBYLA\",\n", + " options={\"maxiter\": 1000, \"disp\": True},\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "fbf9450b-acfb-43bd-88b3-cfe4633b54ae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -5.999999998357189\n", + " x: [ 2.275e+00 8.376e-01 1.571e+00 5.829e-06 2.500e+00\n", + " 6.419e-01 6.337e-01 6.337e-01]\n", + " nfev: 103\n", + " maxcv: 0.0" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result" + ] + }, + { + "cell_type": "markdown", + "id": "a3bcf55d-cfee-415f-85c9-5923dd3e4513", + "metadata": {}, + "source": [ + "Because this toy problem uses only two qubits, we can check this by using NumPy's linear algebra eigensolver." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "e99d0559-c392-4023-93be-7e0ea0361f5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of iterations: 103\n", + "Time (s): 0.4394676685333252\n", + "Percent error: 2.74e-08\n" + ] + } + ], + "source": [ + "from numpy.linalg import eigvalsh\n", + "\n", + "solution_eigenvalue = min(eigvalsh(observable_1.to_matrix()))\n", + "\n", + "print(f\"\"\"Number of iterations: {result.nfev}\"\"\")\n", + "print(f\"\"\"Time (s): {execution_time}\"\"\")\n", + "\n", + "print(\n", + " f\"Percent error: {100*abs((result.fun - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "17f4f2b7-3721-4708-969d-3f07dc74d2f1", + "metadata": {}, + "source": [ + "As you can see, the result is extremely close to the ideal." + ] + }, + { + "cell_type": "markdown", + "id": "f795f1dc-dd4b-478a-84d0-4a981a3187cf", + "metadata": {}, + "source": [ + "## Experimenting to improve speed and accuracy" + ] + }, + { + "cell_type": "markdown", + "id": "68de1e76-a6d8-4585-83ec-9bb57747dbf7", + "metadata": {}, + "source": [ + "### Add reference state\n", + "\n", + "In the previous example we have not used any reference operator $U_R$. Now let us think about how the ideal eigenstate $\\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$ can be obtained. Consider the following circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "55b26ae7-ad26-4b54-b4f3-2004bb2db0c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "ideal_qc = QuantumCircuit(2)\n", + "ideal_qc.h(0)\n", + "ideal_qc.cx(0, 1)\n", + "\n", + "ideal_qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5368863-cf78-4f07-93bf-3a215aae5476", + "metadata": {}, + "source": [ + "We can quickly check that this circuit gives us the desired state." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "2595ad2a-bea2-4770-8f77-f38eb0c07815", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Statevector([0.70710678+0.j, 0. +0.j, 0. +0.j,\n", + " 0.70710678+0.j],\n", + " dims=(2, 2))\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import Statevector\n", + "\n", + "Statevector(ideal_qc)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b075a92d-4162-4cb1-bb4f-ab9feea67f61", + "metadata": {}, + "source": [ + "Now that we have seen how a circuit preparing the solution state looks like, it seems reasonable to use a Hadamard gate as a reference circuit, so that the full ansatz becomes:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "14e04c7a-e81f-41e1-b0cf-790954581be9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reference = QuantumCircuit(2)\n", + "reference.h(0)\n", + "reference.cx(0, 1)\n", + "# Include barrier to separate reference from variational form\n", + "reference.barrier()\n", + "\n", + "ref_ansatz = variational_form.decompose().compose(reference, front=True)\n", + "\n", + "ref_ansatz.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "d032410e-eb2e-4410-859c-7c5f8ef8c2db", + "metadata": {}, + "source": [ + "For this new circuit, the ideal solution could be reached with all the parameters set to $0$, so this confirms that the choice of reference circuit is reasonable.\n", + "\n", + "Now let us compare the number of cost function evaluations, optimizer iterations and time taken with those of the previous attempt." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "968dedc7-4073-4014-b807-8451a50c0f13", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "ref_result = minimize(\n", + " cost_func_vqe, x0, args=(ref_ansatz, observable_1, estimator), method=\"COBYLA\"\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time" + ] + }, + { + "cell_type": "markdown", + "id": "20f61d98-86ef-4e9a-a85e-fa333309508c", + "metadata": {}, + "source": [ + "Using our optimal parameters to calculate the minimum eigenvalue:" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "241a5b4d-b5cf-41c9-a8d2-4ebf65b47a5e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-5.999999996759607\n" + ] + } + ], + "source": [ + "experimental_min_eigenvalue_ref = cost_func_vqe(\n", + " ref_result.x, ref_ansatz, observable_1, estimator\n", + ")\n", + "print(experimental_min_eigenvalue_ref)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "878ff8e1-f1d7-47c2-acda-d2541ac0b3cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ADDED REFERENCE STATE:\n", + "Number of iterations: 127\n", + "Time (s): 0.5620882511138916\n", + "Percent error: 5.40e-08\n" + ] + } + ], + "source": [ + "print(\"ADDED REFERENCE STATE:\")\n", + "print(f\"\"\"Number of iterations: {ref_result.nfev}\"\"\")\n", + "print(f\"\"\"Time (s): {execution_time}\"\"\")\n", + "print(\n", + " f\"Percent error: {100*abs((experimental_min_eigenvalue_ref - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "28048443-9890-4168-bb19-cf95f999b6c2", + "metadata": {}, + "source": [ + "Depending on your specific system, this may or may not result in an improvement in speed or accuracy in this very small scale example. The point is that starting with physically-motivated reference states becomes increasingly important in improving speed and accuracy as problems scale." + ] + }, + { + "cell_type": "markdown", + "id": "41cdb430-27a3-4a28-92f1-d73ad6ed0dc5", + "metadata": {}, + "source": [ + "### Change the initial point" + ] + }, + { + "cell_type": "markdown", + "id": "f528cb99-dcab-4f76-ad38-00ddf4d75f50", + "metadata": {}, + "source": [ + "Now that we have seen the effect of adding the reference state, we will go into what happens when we choose different initial points $\\vec{\\theta_0}$. In particular we will use $\\vec{\\theta_0}=(0,0,0,0,6,0,0,0)$ and $\\vec{\\theta_0}=(6,6,6,6,6,6,6,6,6)$.\n", + "\n", + "Remember that, as discussed when the reference state was introduced, the ideal solution would be found when all the parameters are $0$, so the first initial point should give fewer evaluations." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "9ef75002-f058-4946-a38d-564775a7268f", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "x0 = [0, 0, 0, 0, 6, 0, 0, 0]\n", + "\n", + "x0_1_result = minimize(\n", + " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"COBYLA\"\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "f52592f3-0af5-4f58-84ce-d1e9960d8dc2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INITIAL POINT 1:\n", + "Number of iterations: 108\n", + "Time (s): 0.4492197036743164\n" + ] + } + ], + "source": [ + "print(\"INITIAL POINT 1:\")\n", + "print(f\"\"\"Number of iterations: {x0_1_result.nfev}\"\"\")\n", + "print(f\"\"\"Time (s): {execution_time}\"\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "a8bfac60-cb21-4661-a412-ce8a41f25597", + "metadata": {}, + "source": [ + "Adjusting initial point to $\\vec{\\theta_0}=(6,6,6,6,6,6,6,6,6)$:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "c33df46e-bd4b-490d-8704-750e271724ff", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "x0 = 6 * np.ones(raw_ansatz.num_parameters)\n", + "\n", + "x0_2_result = minimize(\n", + " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"COBYLA\"\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "a04e7171-26c3-4b38-afa7-bfe4e1b95600", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INITIAL POINT 2:\n", + "Number of iterations: 107\n", + "Time (s): 0.40889453887939453\n" + ] + } + ], + "source": [ + "print(\"INITIAL POINT 2:\")\n", + "print(f\"\"\"Number of iterations: {x0_2_result.nfev}\"\"\")\n", + "print(f\"\"\"Time (s): {execution_time}\"\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "6b842e53-bfa2-48ec-8ae9-54e3be0efaab", + "metadata": {}, + "source": [ + "By experimenting with different initial points, you might be able to achieve convergence faster and with fewer function evaluations." + ] + }, + { + "cell_type": "markdown", + "id": "9fd53a82-758c-4081-8fef-3b0bb109f0db", + "metadata": {}, + "source": [ + "### Experimenting with different optimizers\n", + "\n", + "We can adjust the optimizer using SciPy `minimize`'s `method` argument, with more options [found here](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html). We originally used a constrained minimizer (`COBYLA`). In this example, we'll explore using an unconstrained minimizer (`BFGS`) instead" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "7e21aa4c-67b1-46db-8d32-d216f7ea8f40", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "result = minimize(\n", + " cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method=\"BFGS\"\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "063f98c5-a281-47c9-978d-438ddb35b556", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CHANGED TO BFGS OPTIMIZER:\n", + "Number of iterations: 117\n", + "Time (s): 0.31656408309936523\n" + ] + } + ], + "source": [ + "print(\"CHANGED TO BFGS OPTIMIZER:\")\n", + "print(f\"\"\"Number of iterations: {result.nfev}\"\"\")\n", + "print(f\"\"\"Time (s): {execution_time}\"\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "4eb3e4e4-ae77-4098-95cc-2d29ea04301b", + "metadata": {}, + "source": [ + "## VQD example" + ] + }, + { + "cell_type": "markdown", + "id": "a0f974f7-cf29-4d30-93a7-da9ee8689c2e", + "metadata": {}, + "source": [ + "Here we implement the Qiskit patterns framework, explicitly." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3ef8f695-9694-4ccc-9fba-c24ba312a066", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Now instead of looking for only the lowest eigenvalue of our observables, we will look for all $4$, (where $k=4$).\n", + "\n", + "Remember that the cost functions of VQD are:\n", + "\n", + "$$\n", + "C_{l}(\\vec{\\theta}) :=\n", + "\\langle \\psi(\\vec{\\theta}) | \\hat{H} | \\psi(\\vec{\\theta})\\rangle +\n", + "\\sum_{j=0}^{l-1}\\beta_j |\\langle \\psi(\\vec{\\theta})| \\psi(\\vec{\\theta^j})\\rangle |^2\n", + "\\quad \\forall l\\in\\{1,\\cdots,k\\}=\\{1,\\cdots,4\\}\n", + "$$\n", + "\n", + "This is particularly important because a vector $\\vec{\\beta}=(\\beta_0,\\cdots,\\beta_{k-1})$ (in this case $(\\beta_0, \\beta_1, \\beta_2, \\beta_3)$) must be passed as an argument when we define the `VQD` object.\n", + "\n", + "Also, in Qiskit's implementation of VQD, instead of considering the effective observables described in the previous notebook, the fidelities $|\\langle \\psi(\\vec{\\theta})| \\psi(\\vec{\\theta^j})\\rangle |^2$ are calculated directly via the `ComputeUncompute` algorithm, that leverages a `Sampler` primitive to sample the probability of obtaining $|0\\rangle$ for the circuit\n", + "$U_A^\\dagger(\\vec{\\theta})U_A(\\vec{\\theta^j})$. This works precisely because this probability is\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "\n", + "p_0\n", + "\n", + "& = |\\langle 0|U_A^\\dagger(\\vec{\\theta})U_A(\\vec{\\theta^j})|0\\rangle|^2 \\\\[1mm]\n", + "\n", + "& = |\\big(\\langle 0|U_A^\\dagger(\\vec{\\theta})\\big)\\big(U_A(\\vec{\\theta^j})|0\\rangle\\big)|^2 \\\\[1mm]\n", + "\n", + "& = |\\langle \\psi(\\vec{\\theta}) |\\psi(\\vec{\\theta^j}) \\rangle|^2 \\\\[1mm]\n", + "\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4d59381-43ff-4322-a6f2-df55cef18536", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ansatz = n_local(\n", + " num_qubits=2,\n", + " rotation_blocks=[\"ry\", \"rz\"],\n", + " entanglement_blocks=\"cz\",\n", + " # entanglement=\"linear\",\n", + " reps=1,\n", + ")\n", + "\n", + "ansatz.decompose().draw(\"mpl\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f49e35ed-2a4b-48bb-a060-f6fea16d7e7d", + "metadata": {}, + "source": [ + "Let's start by examining the following observable:\n", + "\n", + "$$\n", + "\\hat{O}_2 := 2 II - 3 XX + 2 YY - 4 ZZ\n", + "$$\n", + "\n", + "This observable has the following eigenvalues:\n", + "\n", + "$$\n", + "\\left\\{\n", + "\\begin{array}{c}\n", + "\\lambda_0 = -7 \\\\\n", + "\\lambda_1 = 3\\\\\n", + "\\lambda_2 = 5 \\\\\n", + "\\lambda_3 = 7\n", + "\\end{array}\n", + "\\right\\}\n", + "$$\n", + "\n", + "And eigenstates:\n", + "\n", + "$$\n", + "\\left\\{\n", + "\\begin{array}{c}\n", + "|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)\\\\\n", + "|\\phi_1\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle - |11\\rangle)\\\\\n", + "|\\phi_2\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle + |10\\rangle)\\\\\n", + "|\\phi_3\\rangle = \\frac{1}{\\sqrt{2}}(|01\\rangle - |10\\rangle)\n", + "\\end{array}\n", + "\\right\\}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "917749d0-c396-4045-9df7-a61ece924485", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "observable_2 = SparsePauliOp.from_list([(\"II\", 2), (\"XX\", -3), (\"YY\", 2), (\"ZZ\", -4)])" + ] + }, + { + "cell_type": "markdown", + "id": "17bbde42-da64-44c3-9257-8c2ed4c48667", + "metadata": {}, + "source": [ + "We'll be using the following function to calculate the overlap penalty. Note that this is still part of mapping the problem to quantum circuits. However, as discussed in the previous lesson, this function calculates the overlap between a current variational circuit and the optimized circuit from a previous, lower-energy/cost state obtained. The new circuit being generated also has to be transpiled to run on real hardware. We have seen this function before, used on a simulator. Here, we must already consider the transpiling and related optimization for when we use a real backend, hence the lines around `if realbackend == 1`. This is mixing a bit of step 2, but we will call out step 2 explicitly later." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "9999ed16-2b59-4dfb-8313-17dde10dcbf1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "\n", + "def calculate_overlaps(\n", + " ansatz, prev_circuits, parameters, sampler, realbackend, backend\n", + "):\n", + " def create_fidelity_circuit(circuit_1, circuit_2):\n", + " if len(circuit_1.clbits) > 0:\n", + " circuit_1.remove_final_measurements()\n", + " if len(circuit_2.clbits) > 0:\n", + " circuit_2.remove_final_measurements()\n", + "\n", + " circuit = circuit_1.compose(circuit_2.inverse())\n", + " circuit.measure_all()\n", + " return circuit\n", + "\n", + " overlaps = []\n", + "\n", + " for prev_circuit in prev_circuits:\n", + " fidelity_circuit = create_fidelity_circuit(ansatz, prev_circuit)\n", + " if realbackend == 1:\n", + " pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", + " fidelity_circuit = pm.run(fidelity_circuit)\n", + " sampler_job = sampler.run([(fidelity_circuit, parameters)])\n", + " meas_data = sampler_job.result()[0].data.meas\n", + "\n", + " counts_0 = meas_data.get_int_counts().get(0, 0)\n", + " shots = meas_data.num_shots\n", + " overlap = counts_0 / shots\n", + " overlaps.append(overlap)\n", + "\n", + " return np.array(overlaps)" + ] + }, + { + "cell_type": "markdown", + "id": "5321dc7f-f546-4223-8de3-fe29008ec8d2", + "metadata": {}, + "source": [ + "Now we add VQD's cost function. Note that compared to the previous lesson, we now have two additional arguments (`realbackend` and `backend`) to help us with transpilation when we use real backends." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "07ce634a-9921-42e8-be37-74fbf1631b97", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func_vqd(\n", + " parameters,\n", + " ansatz,\n", + " prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " hamiltonian,\n", + " realbackend,\n", + " backend,\n", + "):\n", + " estimator_job = estimator.run([(ansatz, hamiltonian, [parameters])])\n", + "\n", + " total_cost = 0\n", + "\n", + " if step > 1:\n", + " overlaps = calculate_overlaps(\n", + " ansatz, prev_states, parameters, sampler, realbackend, backend\n", + " )\n", + " total_cost = np.sum(\n", + " [np.real(betas[state] * overlap) for state, overlap in enumerate(overlaps)]\n", + " )\n", + "\n", + " estimator_result = estimator_job.result()[0]\n", + "\n", + " value = estimator_result.data.evs[0] + total_cost\n", + "\n", + " return value" + ] + }, + { + "cell_type": "markdown", + "id": "722f238d-2789-47a3-9998-267b74f9e481", + "metadata": {}, + "source": [ + "Once again, we will use simulators for debugging, and then move on to real hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "c6f418c0-4cab-4334-96ed-14f26162ffcc", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import StatevectorSampler\n", + "from qiskit.primitives import StatevectorEstimator\n", + "\n", + "sampler = StatevectorSampler(default_shots=4092)\n", + "estimator = StatevectorEstimator()" + ] + }, + { + "cell_type": "markdown", + "id": "1cdb7151-2408-4f70-8381-af70255b7c34", + "metadata": {}, + "source": [ + "Here we introduce the number of states we wish to calculate, the penalties, and a set of initial parameters, x0." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "b6c71453-4466-4fdf-8442-82483d16ff8d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "k = 4\n", + "betas = [50, 60, 40]\n", + "x0 = np.ones(8)" + ] + }, + { + "cell_type": "markdown", + "id": "60152310-abcb-439d-ae51-5627797fe821", + "metadata": {}, + "source": [ + "We will now test the algorithm using simulators:" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "0c846737-4766-454f-a5a0-d09bc04cf5c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -6.9999999999996\n", + " x: [ 1.571e+00 1.571e+00 2.519e+00 2.100e+00 1.242e+00\n", + " 6.935e-01 2.298e+00 1.991e+00]\n", + " nfev: 151\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: 3.698974255258432\n", + " x: [ 1.269e+00 1.109e+00 1.080e+00 1.200e+00 1.094e+00\n", + " 1.163e+00 9.752e-01 9.519e-01]\n", + " nfev: 103\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: 4.731320121938101\n", + " x: [ 1.533e+00 2.451e+00 2.526e+00 2.406e+00 1.968e+00\n", + " 2.105e+00 8.537e-01 8.442e-01]\n", + " nfev: 110\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: 7.008239313655201\n", + " x: [ 4.150e+00 2.120e+00 3.495e+00 7.262e-01 1.953e+00\n", + " -1.982e-01 3.263e-01 2.563e+00]\n", + " nfev: 126\n", + " maxcv: 0.0\n" + ] + } + ], + "source": [ + "from scipy.optimize import minimize\n", + "\n", + "prev_states = []\n", + "prev_opt_parameters = []\n", + "eigenvalues = []\n", + "\n", + "realbackend = 0\n", + "\n", + "for step in range(1, k + 1):\n", + " if step > 1:\n", + " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", + "\n", + " result = minimize(\n", + " cost_func_vqd,\n", + " x0,\n", + " args=(\n", + " ansatz,\n", + " prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " observable_2,\n", + " realbackend,\n", + " None,\n", + " ),\n", + " method=\"COBYLA\",\n", + " options={\"maxiter\": 200, \"tol\": 0.000001},\n", + " )\n", + " print(result)\n", + "\n", + " prev_opt_parameters = result.x\n", + " eigenvalues.append(result.fun)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "91546335-2f0f-4e05-bb92-62c49f67a005", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[np.float64(-6.9999999999996),\n", + " np.float64(3.698974255258432),\n", + " np.float64(4.731320121938101),\n", + " np.float64(7.008239313655201)]" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eigenvalues" + ] + }, + { + "cell_type": "markdown", + "id": "14d995bd-e486-48cc-b73d-b2ca042dce07", + "metadata": {}, + "source": [ + "These results are fairly close to the expected ones except for approximation error and global phase. We could adjust the tolerance on the classical optimizer and/or the penalties for statevector overlap to obtain more precise values." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "6e18fcde-09b2-45a0-a04b-3ee7a6e9bde6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percent error: 5.71e-14\n", + "Percent error: 2.33e-01\n", + "Percent error: 5.37e-02\n", + "Percent error: 1.18e-03\n" + ] + } + ], + "source": [ + "solution_eigenvalues = [-7, 3, 5, 7]\n", + "\n", + "for index, experimental_eigenvalue in enumerate(eigenvalues):\n", + " solution_eigenvalue = solution_eigenvalues[index]\n", + "\n", + " print(\n", + " f\"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13fc804e-6248-4738-9187-dbebfc2fbfd6", + "metadata": {}, + "source": [ + "### Change betas" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "36b1bee9-e6f2-4978-be4f-509f67d7f5da", + "metadata": {}, + "source": [ + "As mentioned in the previous lesson, the values of $\\vec{\\beta}$ should be bigger than the difference between eigenvalues. Let us see what happens when they do not satisfy that condition with $\\hat{O}_2$\n", + "\n", + "$$\n", + "\\hat{O}_2 = 2 II - 3 XX + 2 YY - 4 ZZ\n", + "$$\n", + "\n", + "with eigenvalues\n", + "\n", + "$$\n", + "\\left\\{\n", + "\\begin{array}{c}\n", + "\\lambda_0 = -7 \\\\\n", + "\\lambda_1 = 3\\\\\n", + "\\lambda_2 = 5 \\\\\n", + "\\lambda_3 = 7\n", + "\\end{array}\n", + "\\right\\}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "91f7f00a-b843-4b8d-98a6-836d41643f7a", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "k = 4\n", + "betas = np.ones(3)\n", + "x0 = np.zeros(8)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "011f21d7-358b-4f8f-ac3e-fd348e486c9e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -6.999916534745094\n", + " x: [ 1.568e+00 -1.569e+00 1.385e-01 1.398e-01 -7.972e-01\n", + " 7.835e-01 -2.375e-01 4.539e-02]\n", + " nfev: 125\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -1.515139929812874\n", + " x: [-5.317e-04 -2.514e-03 1.016e+00 9.998e-01 3.890e-04\n", + " 1.772e-04 1.568e-04 8.497e-04]\n", + " nfev: 35\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: -0.509948114293115\n", + " x: [-3.796e-03 8.853e-03 3.015e-04 9.997e-01 6.271e-04\n", + " -2.554e-03 1.017e-04 2.766e-04]\n", + " nfev: 37\n", + " maxcv: 0.0\n", + " message: Return from COBYLA because the trust region radius reaches its lower bound.\n", + " success: True\n", + " status: 0\n", + " fun: 0.4914672235935682\n", + " x: [-7.178e-03 -8.652e-03 1.125e+00 -5.428e-02 -1.586e-03\n", + " 2.031e-03 -3.462e-03 5.734e-03]\n", + " nfev: 35\n", + " maxcv: 0.0\n" + ] + } + ], + "source": [ + "from scipy.optimize import minimize\n", + "\n", + "prev_states = []\n", + "prev_opt_parameters = []\n", + "eigenvalues = []\n", + "\n", + "realbackend = 0\n", + "\n", + "for step in range(1, k + 1):\n", + " if step > 1:\n", + " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", + "\n", + " result = minimize(\n", + " cost_func_vqd,\n", + " x0,\n", + " args=(\n", + " ansatz,\n", + " prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " observable_2,\n", + " realbackend,\n", + " None,\n", + " ),\n", + " method=\"COBYLA\",\n", + " options={\"tol\": 0.01, \"maxiter\": 200},\n", + " )\n", + " print(result)\n", + "\n", + " prev_opt_parameters = result.x\n", + " eigenvalues.append(result.fun)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "b7076fc6-0929-4032-a4a3-b50ba39a59a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percent error: 1.19e-05\n", + "Percent error: 1.51e+00\n", + "Percent error: 1.10e+00\n", + "Percent error: 9.30e-01\n" + ] + } + ], + "source": [ + "solution_eigenvalues = [-7, 3, 5, 7]\n", + "\n", + "for index, experimental_eigenvalue in enumerate(eigenvalues):\n", + " solution_eigenvalue = solution_eigenvalues[index]\n", + "\n", + " print(\n", + " f\"Percent error: {abs((experimental_eigenvalue - solution_eigenvalue)/solution_eigenvalue):.2e}\"\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a47a9ce5-b591-4550-bd39-2fffb699b832", + "metadata": {}, + "source": [ + "This time, the optimizer returns the same state $|\\phi_0\\rangle = \\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$ as a proposed solution to all eigenstates: which is clearly wrong. This happens because the betas were too small to penalize the minimum eigenstate in the successive cost functions. Therefore, it was not excluded from the effective search space in later iterations of the algorithm, and always chosen as the best possible solution.\n", + "\n", + "We recommend experimenting with the values of $\\vec{\\beta}$, and ensuring they are bigger than the difference between eigenvalues." + ] + }, + { + "cell_type": "markdown", + "id": "6978f795-f949-4b10-9420-6d64bc907b4a", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum execution\n", + "\n", + "To run this on real hardware, we must optimize the quantum circuits for our quantum computer of choice. For our purposes here, we will simply use the least busy backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9b485a5-8446-4667-af29-81a2a30d9fee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "from qiskit_ibm_runtime import Session, EstimatorOptions\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "# Or use a specific backend\n", + "# backend = service.backend(\"ibm_brisbane\")\n", + "print(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "12f03728-656d-45cc-a1c5-5366ec301304", + "metadata": {}, + "source": [ + "We will transpile our circuit using a preset pass manager and optimization level 3." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "ba4a98a3-159b-4863-84bd-298be5b6d091", + "metadata": {}, + "outputs": [], + "source": [ + "pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", + "isa_ansatz = pm.run(ansatz)\n", + "isa_observable = observable_2.apply_layout(layout=isa_ansatz.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "a29f9139-3c91-424f-97d2-80b31cce5d91", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "Taking care to reset our betas to sufficiently high values, we can now run our calculation on real quantum hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e373e99-19d9-4fb2-b26e-208f1a2b7c9b", + "metadata": {}, + "outputs": [], + "source": [ + "# Estimated compute resource usage: 25 minutes. Benchmarked at 24 min, 30 sec on an Eagle r3\n", + "# processor on 5-30-24\n", + "\n", + "k = 2\n", + "betas = [30, 50, 80]\n", + "x0 = np.zeros(8)\n", + "\n", + "real_prev_states = []\n", + "real_prev_opt_parameters = []\n", + "real_eigenvalues = []\n", + "\n", + "realbackend = 1\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session, options=estimator_options)\n", + " sampler = Sampler(mode=session)\n", + "\n", + " for step in range(1, k + 1):\n", + " if step > 1:\n", + " real_prev_states.append(isa_ansatz.assign_parameters(prev_opt_parameters))\n", + "\n", + " result = minimize(\n", + " cost_func_vqd,\n", + " x0,\n", + " args=(\n", + " isa_ansatz,\n", + " real_prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " isa_observable,\n", + " realbackend,\n", + " backend,\n", + " ),\n", + " method=\"COBYLA\",\n", + " options={\"maxiter\": 200},\n", + " )\n", + " print(result)\n", + "\n", + " real_prev_opt_parameters = result.x\n", + " real_eigenvalues.append(result.fun)\n", + "\n", + "session.close()\n", + "print(real_eigenvalues)" + ] + }, + { + "cell_type": "markdown", + "id": "f8e976c0-5d73-4244-b4be-4808925a3e40", + "metadata": {}, + "source": [ + "### Step 4: Post-process, return result in classical format\n", + "\n", + "Our output is structurally similar to what has been discussed in previous lessons and examples. But there is something problematic in the results above, from which we can derive a cautionary message for the context of excited states. To limit computing time used on this learning example, we set a maximum number of iterations for classical optimizer that was potentially too low: 200 iterations. A previous calculation above, on a simulator, failed to converge in 200 iterations. Here, ours did converge... but to what tolerance? We have not specified a tolerance for COBYLA to consider itself \"converged\". A glance at the function value and comparison with previous runs tells us that COBYLA was not close to converging to the precision we require.\n", + "\n", + "There is another issue: the energy of the first excited state appears to be lower than the energy of the ground state! See if you can explain how this could happen. Hint: it is related to the convergence point we just addressed. This behavior is explained in detail below after VQD is applied to the H2 molecule." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "4b6857c6-09fe-44ca-ba9f-822eb9ee5ad8", + "metadata": {}, + "source": [ + "## Quantum chemistry: ground state and excited energy solver\n", + "\n", + "Our objective is to minimize the expectation value of the observable representing energy (Hamiltonian $\\hat{\\mathcal{H}}$):\n", + "\n", + "$$\n", + "\\min_{\\vec\\theta} \\langle\\psi(\\vec\\theta)|\\hat{\\mathcal{H}}|\\psi(\\vec\\theta)\\rangle\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "de006d72-db04-49d4-806b-89bd1e5b9947", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.circuit.library import efficient_su2\n", + "\n", + "H2_op = SparsePauliOp.from_list(\n", + " [\n", + " (\"II\", -1.052373245772859),\n", + " (\"IZ\", 0.39793742484318045),\n", + " (\"ZI\", -0.39793742484318045),\n", + " (\"ZZ\", -0.01128010425623538),\n", + " (\"XX\", 0.18093119978423156),\n", + " ]\n", + ")\n", + "\n", + "chem_ansatz = efficient_su2(H2_op.num_qubits)\n", + "\n", + "chem_ansatz.decompose().draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "1e67cecd-cb3d-4593-a9e8-1058e816c9ae", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "\n", + "def cost_func_vqe(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (Estimator): Estimator primitive instance\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, hamiltonian, params)\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", + " return cost" + ] + }, + { + "cell_type": "markdown", + "id": "21b278c0-719c-4240-ae6e-f21360cb21f5", + "metadata": {}, + "source": [ + "We now set an initial set of parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "d6a2f152-6642-4b66-aa34-951d86973f8c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "x0 = np.ones(chem_ansatz.num_parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "1ad424a9-4c4c-4c36-8408-2fc76318eeb9", + "metadata": {}, + "source": [ + "We can minimize this cost function to calculate optimal parameters, and we can check our code first by using a local simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "69785252-e337-404a-8fbf-ae2a5a875525", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import StatevectorEstimator as Estimator\n", + "from qiskit.primitives import StatevectorSampler as Sampler\n", + "\n", + "estimator = Estimator()\n", + "sampler = Sampler()" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "79fd09b2-d886-4281-9381-b802b4d2f7a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " message: Optimization terminated successfully.\n", + " success: True\n", + " status: 1\n", + " fun: -1.857275029048451\n", + " x: [ 7.326e-01 1.354e+00 ... 1.040e+00 1.508e+00]\n", + " nfev: 242\n", + " maxcv: 0.0" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# SciPy minimizer routine\n", + "from scipy.optimize import minimize\n", + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "result = minimize(\n", + " cost_func_vqe, x0, args=(chem_ansatz, H2_op, estimator), method=\"COBYLA\"\n", + ")\n", + "\n", + "end_time = time.time()\n", + "execution_time = end_time - start_time\n", + "\n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "0b2beab7-816d-438b-8502-5e7f9d65ae1d", + "metadata": {}, + "source": [ + "The minimum value of the cost function (-1.857...) is the ground state energy of the H2 molecule, in units of hartrees." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "54a8a55c-6aa4-4e7c-a999-d3e468079571", + "metadata": {}, + "source": [ + "### Excited States\n", + "\n", + "We can also leverage VQD to solve for $k=2$ total states (the ground state and the first excited state)." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "678ed2ab-c572-43a8-9a87-26cb0c470b68", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "import numpy as np\n", + "\n", + "k = 2\n", + "betas = [33, 33]\n", + "# x0 = np.zeros(ansatz.num_parameters)\n", + "x0 = [\n", + " 1.164e00,\n", + " -2.438e-01,\n", + " 9.358e-04,\n", + " 6.745e-02,\n", + " 1.990e00,\n", + " 9.810e-02,\n", + " 6.154e-01,\n", + " 5.454e-01,\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "760ebdc7-596c-4781-824a-3b7972cdba53", + "metadata": {}, + "source": [ + "We'll add our overlap calculation:" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "d755f8b7-9aab-4c74-84ff-80f62a9b3c23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " message: Optimization terminated successfully.\n", + " success: True\n", + " status: 1\n", + " fun: -1.8572671093941977\n", + " x: [ 1.164e+00 -2.437e-01 2.118e-03 6.448e-02 1.990e+00\n", + " 9.870e-02 6.167e-01 5.476e-01]\n", + " nfev: 58\n", + " maxcv: 0.0\n", + " message: Optimization terminated successfully.\n", + " success: True\n", + " status: 1\n", + " fun: -1.0322873777662176\n", + " x: [ 3.205e+00 1.502e+00 1.699e+00 -1.107e-02 3.086e+00\n", + " 1.530e+00 4.445e-02 7.013e-02]\n", + " nfev: 99\n", + " maxcv: 0.0\n" + ] + } + ], + "source": [ + "from scipy.optimize import minimize\n", + "\n", + "prev_states = []\n", + "prev_opt_parameters = []\n", + "eigenvalues = []\n", + "\n", + "realbackend = 0\n", + "\n", + "for step in range(1, k + 1):\n", + " if step > 1:\n", + " prev_states.append(ansatz.assign_parameters(prev_opt_parameters))\n", + "\n", + " result = minimize(\n", + " cost_func_vqd,\n", + " x0,\n", + " args=(\n", + " ansatz,\n", + " prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " H2_op,\n", + " realbackend,\n", + " None,\n", + " ),\n", + " method=\"COBYLA\",\n", + " options={\"tol\": 0.001, \"maxiter\": 2000},\n", + " )\n", + " print(result)\n", + "\n", + " prev_opt_parameters = result.x\n", + " eigenvalues.append(result.fun)" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "212c3e80-1b5e-4155-a3d3-d3bf12c2e97a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[-1.8572671093941977, -1.0322873777662176]" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eigenvalues" + ] + }, + { + "cell_type": "markdown", + "id": "deec3d64-7f91-4623-8236-63a7de6aa6e6", + "metadata": {}, + "source": [ + "### Real hardware and a final cautionary message\n", + "\n", + "To run this on real hardware, we must optimize the quantum circuits for our quantum computer of choice. For our purposes here, we will simply use the least busy backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5fdaae83-47cf-40ab-95d9-d1ff048ed825", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "from qiskit_ibm_runtime import Session, EstimatorOptions\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)" + ] + }, + { + "cell_type": "markdown", + "id": "ad33ec78-d495-48f2-b64f-6042d5319da6", + "metadata": {}, + "source": [ + "We will use a preset pass manager for transpilation, and we will maximally optimize our circuit using optimization level 3." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "9733e0b3-fc05-4c5b-b29a-1ba197106fad", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=3)\n", + "isa_ansatz = pm.run(ansatz)\n", + "isa_observable = H2_op.apply_layout(layout=isa_ansatz.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "262a1b78-a9bc-4850-bef8-60e290f2f2e0", + "metadata": {}, + "source": [ + "Because VQD is highly iterative, we will carry out all steps inside a Runtime session, such that our jobs will only be queued at the beginning, and not between every parameter update. Nothing else changes about the syntax for the cost function or estimator." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "12843daa-4b84-408d-ae46-f72d24509ae1", + "metadata": {}, + "outputs": [], + "source": [ + "x0 = [\n", + " 1.306e00,\n", + " -2.284e-01,\n", + " 6.913e-02,\n", + " -2.530e-02,\n", + " 1.849e00,\n", + " 7.433e-02,\n", + " 6.366e-01,\n", + " 5.600e-01,\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8e07fd-5968-4741-919e-4d9c46d58de5", + "metadata": {}, + "outputs": [], + "source": [ + "# Estimated hardware usage: 20 min benchmarked on an Eagle r3 processor on 5-30-24\n", + "\n", + "real_prev_states = []\n", + "real_prev_opt_parameters = []\n", + "real_eigenvalues = []\n", + "\n", + "realbackend = 1\n", + "\n", + "estimator_options = EstimatorOptions(resilience_level=1, default_shots=4096)\n", + "\n", + "with Session(backend=backend) as session:\n", + " estimator = Estimator(mode=session)\n", + " sampler = Sampler(mode=session)\n", + "\n", + " for step in range(1, k + 1):\n", + " if step > 1:\n", + " real_prev_states.append(\n", + " isa_ansatz.assign_parameters(real_prev_opt_parameters)\n", + " )\n", + "\n", + " result = minimize(\n", + " cost_func_vqd,\n", + " x0,\n", + " args=(\n", + " isa_ansatz,\n", + " real_prev_states,\n", + " step,\n", + " betas,\n", + " estimator,\n", + " sampler,\n", + " isa_observable,\n", + " realbackend,\n", + " backend,\n", + " ),\n", + " method=\"COBYLA\",\n", + " options={\"tol\": 0.001, \"maxiter\": 300},\n", + " )\n", + " print(result)\n", + "\n", + " real_prev_opt_parameters = result.x\n", + " real_eigenvalues.append(result.fun)\n", + "\n", + "session.close()\n", + "print(real_eigenvalues)" + ] + }, + { + "cell_type": "markdown", + "id": "4c5985c2-86af-4969-b0aa-1932502e58e6", + "metadata": {}, + "source": [ + "The ground state energy obtained (-1.83 hartrees) is not too far from the correct value (-1.85 hartrees). However, the excited state energy is quite a bit off. This is similar to the erroneous behavior we saw earlier in this lesson. The energy reported for the excited state is nearly the same as that for the ground state. In the previous case, we even saw an excited state energy that was _lower_ than the reported ground state energy.\n", + "\n", + "It is not possible for a variational calculation to yield an energy that is lower than the true ground state energy. In the earlier instance, the ground state energy we obtained was not very close to the true ground state. Since we did not obtain the true ground state energy in that case, there is no contradiction. In the present case, the ground state energy was fairly close to the correct value, and yet the excited state energy seems strangely close to that same value.\n", + "\n", + "To understand better how this happened, recall that the way we find an excited state is by requiring that the variational state be orthogonal to the ground state (using the overlap circuits and penalty terms). If we fail to obtain an accurate ground state energy (or are off by a few percent), then we also fail to obtain an accurate ground state vector! So when we require that the excited state be orthogonal to the first state we found, we were not imposing orthogonality with the true ground state, but rather with some approximation of it (sometimes a poor approximation of it). Thus, the excited state was not forced to be orthogonal to the true ground state, and our energy estimates for the excited states were actually quite close to the ground state energy.\n", + "\n", + "This will always be a concern in VQD. But in principle, this can be corrected by increasing the maximum number of iterations for the classical optimizer, imposing lower tolerance for the classical optimizer, and possibly also trying a different ansatz if we are habitually missing the true ground state. As we have seen, one may also need to modify the overlap penalties (betas). But that is really a separate issue. No penalty for overlap will keep you away from the true ground state, if you haven't found a very good estimate of the true ground state for the overlap circuit." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "af2202c6-2d1b-4213-a244-1dc1e34e5e34", + "metadata": {}, + "source": [ + "## Optimization: max-cut\n", + "\n", + "The maximum cut (max-cut) problem is a combinatorial optimization problem that involves dividing the vertices of a graph into two disjoint sets such that the number of edges between the two sets is maximized. More formally, given an undirected graph $G=(V,E)$, where $V$ is the set of vertices and $E$ is the set of edges, the max-cut problem asks to partition the vertices into two disjoint subsets, $S$ and $T$, such that the number of edges with one endpoint in $S$ and the other in $T$ is maximized.\n", + "\n", + "We can apply max-cut to solve a various problems, such as clustering, network design, and phase transitions. We'll start by creating a problem graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "3eb71b79-a988-4807-aa88-7fb25a78a236", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import rustworkx as rx\n", + "from rustworkx.visualization import mpl_draw\n", + "\n", + "n = 4\n", + "G = rx.PyGraph()\n", + "G.add_nodes_from(range(n))\n", + "# The edge syntax is (start, end, weight)\n", + "edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]\n", + "G.add_edges_from(edges)\n", + "\n", + "mpl_draw(\n", + " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=\"#1192E8\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8771ec8d-9913-4e25-94dd-96dc57c9675d", + "metadata": {}, + "source": [ + "This problem can be expressed as a binary optimization problem. For each node $0 \\leq i < n$, where $n$ is the number of nodes of the graph (in this case $n=4$), we will consider the binary variable $x_i$. This variable will have the value $1$ if node $i$ is one of the groups that we'll label $1$ and $0$ if it's in the other group, that we'll label as $0$. We will also denote as $w_{ij}$ (element $(i,j)$ of the adjacency matrix $w$) the weight of the edge that goes from node $i$ to node $j$. Because the graph is undirected, $w_{ij}=w_{ji}$. Then we can formulate our problem as maximizing the following cost function:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "C(\\vec{x})\n", + "& =\\sum_{i,j=0}^n w_{ij} x_i(1-x_j)\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i,j=0}^n w_{ij} x_ix_j\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n w_{ij} x_i - \\sum_{i=0}^n \\sum_{j=0}^i 2w_{ij} x_ix_j\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "\n", + "To solve this problem with a quantum computer, we are going to express the cost function as the expected value of an observable. However, the observables that Qiskit admits natively consist of Pauli operators, that have eigenvalues $1$ and $-1$ instead of $0$ and $1$. That's why we are going to make the following change of variable:\n", + "\n", + "Where $\\vec{x}=(x_0,x_1,\\cdots ,x_{n-1})$. We can use the adjacency matrix $w$ to comfortably access the weights of all the edge. This will be used to obtain our cost function:\n", + "\n", + "$$\n", + "z_i = 1-2x_i \\rightarrow x_i = \\frac{1-z_i}{2}\n", + "$$\n", + "\n", + "This implies that:\n", + "\n", + "$$\n", + "\\begin{array}{lcl} x_i=0 & \\rightarrow & z_i=1 \\\\ x_i=1 & \\rightarrow & z_i=-1.\\end{array}\n", + "$$\n", + "\n", + "So the new cost function we want to maximize is:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "C(\\vec{z})\n", + "& = \\sum_{i,j=0}^n w_{ij} \\bigg(\\frac{1-z_i}{2}\\bigg)\\bigg(1-\\frac{1-z_j}{2}\\bigg)\\\\[1mm]\n", + "\n", + "& = \\sum_{i,j=0}^n \\frac{w_{ij}}{4} - \\sum_{i,j=0}^n \\frac{w_{ij}}{4} z_iz_j\\\\[1mm]\n", + "\n", + "& = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Moreover, the natural tendency of a quantum computer is to find minima (usually the lowest energy) instead of maxima so instead of maximizing $C(\\vec{z})$ we are going to minimize:\n", + "\n", + "$$\n", + "-C(\\vec{z}) = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} z_iz_j - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", + "$$\n", + "\n", + "Now that we have a cost function to minimize whose variables can have the values $-1$ and $1$, we can make the following analogy with the Pauli $Z$:\n", + "\n", + "$$\n", + "z_i \\equiv Z_i = \\overbrace{I}^{n-1}\\otimes ... \\otimes \\overbrace{Z}^{i} \\otimes ... \\otimes \\overbrace{I}^{0}\n", + "$$\n", + "\n", + "In other words, the variable $z_i$ will be equivalent to a $Z$ gate acting on qubit $i$. Moreover:\n", + "\n", + "$$\n", + "Z_i|x_{n-1}\\cdots x_0\\rangle = z_i|x_{n-1}\\cdots x_0\\rangle \\rightarrow \\langle x_{n-1}\\cdots x_0 |Z_i|x_{n-1}\\cdots x_0\\rangle = z_i\n", + "$$\n", + "\n", + "Then the observable we are going to consider is:\n", + "\n", + "$$\n", + "\\hat{H} = \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2} Z_iZ_j\n", + "$$\n", + "\n", + "to which we will have to add the independent term afterwards:\n", + "\n", + "$$\n", + "\\texttt{offset} = - \\sum_{i=0}^n \\sum_{j=0}^i \\frac{w_{ij}}{2}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "84bbf27e-6def-4ce0-b563-ae5ae2fcfe34", + "metadata": {}, + "source": [ + "The operator is a linear combination of terms with Z operators on nodes connected by an edge (recall that the 0th qubit is farthest right): $IIZZ + IZIZ + IZZI + ZIIZ + ZZII$. Once the operator is constructed, the ansatz for the QAOA algorithm can easily be built by using the `QAOAAnsatz` circuit from the Qiskit circuit library." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "d28c4262-a080-4fe4-9427-762326184cd8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import QAOAAnsatz\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "max_hamiltonian = SparsePauliOp.from_list(\n", + " [(\"IIZZ\", 1), (\"IZIZ\", 1), (\"IZZI\", 1), (\"ZIIZ\", 1), (\"ZZII\", 1)]\n", + ")\n", + "\n", + "\n", + "max_ansatz = QAOAAnsatz(max_hamiltonian, reps=2)\n", + "# Draw\n", + "max_ansatz.decompose(reps=3).draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "1d3f4fc4-6804-40fd-a470-ddd2c09a94d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Offset: -2.5\n" + ] + } + ], + "source": [ + "# Sum the weights, and divide by 2\n", + "\n", + "offset = -sum(edge[2] for edge in edges) / 2\n", + "print(f\"\"\"Offset: {offset}\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "d6bf5f9c-5277-4833-8864-1e0277c1b6c1", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (Estimator): Estimator primitive instance\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, hamiltonian, params)\n", + " cost = estimator.run([pub]).result()[0].data.evs\n", + " # cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]\n", + " return cost" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "159e0521-4870-4e8d-829a-317d6dafce79", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.primitives import StatevectorEstimator as Estimator\n", + "from qiskit.primitives import StatevectorSampler as Sampler\n", + "\n", + "estimator = Estimator()\n", + "sampler = Sampler()" + ] + }, + { + "cell_type": "markdown", + "id": "6ca87213-311a-46bd-b2c5-814e2c8ffe27", + "metadata": {}, + "source": [ + "We now set an initial set of random parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "babd595f-a1b0-4513-9a97-3b23428b83f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[6.0252949 0.58448176 2.15785731 1.13646074]\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "x0 = 2 * np.pi * np.random.rand(max_ansatz.num_parameters)\n", + "print(x0)" + ] + }, + { + "cell_type": "markdown", + "id": "3447ab1d-e2c1-4d8d-98f6-ee231f3bbc7a", + "metadata": {}, + "source": [ + "Any classical optimizer can be used to minimize the cost function. On a real quantum system, an optimizer designed for non-smooth cost function landscapes usually does better. Here we use the [COBYLA routine](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-cobyla.html#optimize-minimize-cobyla) from SciPy via the minimize function.\n", + "\n", + "Because we are iteratively executing many calls to Runtime, we make use of a session to execute all calls within a single block. Moreover, for QAOA, the solution is encoded in the output distribution of the ansatz circuit bound with the optimal parameters from the minimization. Therefore, we will need a Sampler primitive, and will instantiate it with the same session" + ] + }, + { + "cell_type": "markdown", + "id": "ecc3e775-4e9a-4a4b-ab78-ed53d690b64b", + "metadata": {}, + "source": [ + "And run our minimization routine:" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "03c79980-1510-4a11-83aa-797111c02187", + "metadata": {}, + "outputs": [], + "source": [ + "result = minimize(\n", + " cost_func, x0, args=(max_ansatz, max_hamiltonian, estimator), method=\"COBYLA\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "3be090d4-4adb-4943-931b-5f603df663a1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " message: Optimization terminated successfully.\n", + " success: True\n", + " status: 1\n", + " fun: -2.585287311689236\n", + " x: [ 7.332e+00 3.904e-01 2.045e+00 1.028e+00]\n", + " nfev: 80\n", + " maxcv: 0.0\n" + ] + } + ], + "source": [ + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "c2e3ae85-7eb1-4c41-9ab8-0a38008374c1", + "metadata": {}, + "source": [ + "The solution vector of parameter angles (`x`), when plugged into the ansatz circuit, yields the graph partitioning that we were looking for." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "141b2c86-3902-46f0-bb67-46f4565c3975", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Eigenvalue: -2.585287311689236\n", + "Max-Cut Objective: -5.085287311689235\n" + ] + } + ], + "source": [ + "eigenvalue = cost_func(result.x, max_ansatz, max_hamiltonian, estimator)\n", + "print(f\"\"\"Eigenvalue: {eigenvalue}\"\"\")\n", + "print(f\"\"\"Max-Cut Objective: {eigenvalue + offset}\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "b882490f-745a-4b07-ae94-bb3673316ce6", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.result import QuasiDistribution\n", + "from qiskit.primitives import StatevectorSampler\n", + "\n", + "sampler = StatevectorSampler()\n", + "\n", + "# Assign solution parameters to ansatz\n", + "qc = max_ansatz.assign_parameters(result.x)\n", + "\n", + "# Add measurements to our circuit\n", + "qc.measure_all()\n", + "\n", + "# Sample ansatz at optimal parameters\n", + "# samp_dist = sampler.run(qc).result().quasi_dists[0]\n", + "\n", + "shots = 1024\n", + "job = sampler.run([qc], shots=shots)" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "686ee28e-9327-40d1-8d7c-3ec527160f01", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc.decompose().draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "c2e4ebb9-530a-4b72-acb1-1356634dc7a5", + "metadata": {}, + "outputs": [], + "source": [ + "data_pub = job.result()[0].data\n", + "bitstrings = data_pub.meas.get_bitstrings()\n", + "counts = data_pub.meas.get_counts()\n", + "quasi_dist = QuasiDistribution(\n", + " {outcome: freq / shots for outcome, freq in counts.items()}\n", + ")\n", + "probabilities = quasi_dist\n", + "\n", + "# Close the session since we are now done with it\n", + "# session.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "13a4c371-fd40-4ce3-b737-268d4a4c9c2c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_distribution\n", + "\n", + "plot_distribution(counts)" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "4238cc64-6910-43d2-889e-6322bf2864eb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "binary_string = max(counts.items(), key=lambda kv: kv[1])[0]\n", + "x = np.asarray([int(y) for y in reversed(list(binary_string))])\n", + "\n", + "colors = [\"r\" if x[i] == 0 else \"c\" for i in range(n)]\n", + "mpl_draw(\n", + " G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color=colors\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "761b55e2-2e6b-4d00-ad1a-b0d3d546881e", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "With this lesson, you have learned:\n", + "\n", + "- How to write a custom variational algorithm\n", + "- How to apply a variational algorithm to find minimum eigenvalues\n", + "- How to utilize variational algorithms to solve application use cases\n", + "\n", + "Proceed to the final lesson to take your assessment and earn your badge!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning/modules/computer-science/deutsch-jozsa.ipynb b/learning/modules/computer-science/deutsch-jozsa.ipynb index fc465c5f2e3..e6fafbb5b17 100644 --- a/learning/modules/computer-science/deutsch-jozsa.ipynb +++ b/learning/modules/computer-science/deutsch-jozsa.ipynb @@ -1,1098 +1,1103 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "bfa8f443", - "metadata": {}, - "source": [ - "---\n", - "title: The Deutsch-Jozsa Algorithm\n", - "description: Learn how the Deutsch-Jozsa algorithm uses quantum parallelism and interference to achieve an exponential speedup over classical algorithms.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore blackbox Hadamards */}" - ] - }, - { - "cell_type": "markdown", - "id": "e761a401-3dd0-4c3c-9333-0d89da48fb34", - "metadata": {}, - "source": [ - "# The Deutsch-Jozsa algorithm\n", - "\n", - "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", - "- `qiskit` v2.1.0 or newer\n", - "- `qiskit-ibm-runtime` v0.40.1 or newer\n", - "- `qiskit-aer` v0.17.0 or newer\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "\n", - "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", - "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "This module was tested and used four seconds of QPU time. This is an estimate only. Your actual usage may vary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24a83c6d", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "026f7f82-ac54-413d-8158-e58461bc2afd", - "metadata": {}, - "source": [ - "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/QcK0GK7DUh8?si=8e0Lmjgylxmgl2y7) to watch it on YouTube.\n", - "\n", - "-------\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "34b2aac3-848f-46b4-8c95-8236b6ad7f8e", - "metadata": {}, - "source": [ - "## Intro\n", - "\n", - "In the early 1980's, quantum physicists and computer scientists had a vague notion that quantum mechanics could be harnessed to make computations that were far more powerful than classical computers can make. Their reasoning was this: it's difficult for a classical computer to simulate quantum systems, but a *quantum* computer should be able to do it more efficiently. And if a quantum computer could simulate quantum systems more efficiently, perhaps there were other tasks that it could perform more efficiently than a classical computer.\n", - "\n", - "The logic was sound, but the details remained to be worked out. This began in 1985, when David Deutsch described the first \"universal quantum computer.\" In this same paper, he provided the first example problem for which a quantum computer could solve something more efficiently than a classical computer could. This first toy example is now known as \"Deutsch's algorithm.\" The improvement in Deutsch's algorithm was modest, but Deutsch worked with Richard Jozsa a few years later to further widen the gap between classical and quantum computers.\n", - "\n", - "These algorithms — Deutsch's, and the Deutsch-Jozsa extension — are not particularly useful, but they are still really important for a few reasons:\n", - "\n", - "1. Historically, they were some of the first quantum algorithms that were demonstrated to beat their classical counterparts. Understanding them can help us understand how the community's thinking on quantum computing has evolved over time.\n", - "2. They can help us understand some aspects of the answer to a surprisingly subtle question: What gives quantum computing its power? Sometimes, quantum computers are compared to giant, exponentially-scaling parallel processors. But this isn't quite right. While a piece of the answer to this question lies in so-called \"quantum parallelism,\" extracting as much information as possible in a single run is a subtle art. The Deutsch and Deutsch-Jozsa algorithms show how this can be done.\n", - "\n", - "In this module, we'll learn about Deutsch's algorithm, the Deutsch-Jozsa algorithm, and what they teach us about the power of quantum computing." - ] - }, - { - "cell_type": "markdown", - "id": "096da154-5663-4f46-8d9e-b6f163260706", - "metadata": {}, - "source": [ - "## Quantum parallelism and its limits\n", - "\n", - "Part of the power of quantum computing is derived from \"quantum parallelism.\" which is essentially the ability to perform operations on multiple inputs at the same time, since the qubit input states could be in a superposition of multiple classically allowed states. HOWEVER, while a quantum circuit might be able to evaluate multiple input states at once, extracting all of that information in one go is impossible.\n", - "\n", - "To see what I mean here, let's say we have a bit, $x$ and some function applied to that bit, $f(x)$. There are four possible binary functions taking a single bit to another single bit:\n", - "\n", - "| $x$ | $f_1(x)$ | $f_2(x)$ | $f_3(x)$ | $f_4(x)$ |\n", - "| ----------- | ------- |-------| -------- | ------- |\n", - "| 0 | 0 | 0 | 1 | 1 |\n", - "| 1 | 0 | 1 | 0 | 1 |\n", - "\n", - "We'd like to find out which of these functions (1-4) our $f(x)$ is. Classically, we would need to run the function twice — once for $x=0$, once for $x=1$. But let's see if we can do better with a quantum circuit. We can learn about the function with the following gate:\n", - "\n", - "![quantum_parallelism](/learning/images/modules/computer-science/deutsch-jozsa/quantum-parallelism.avif)\n", - "\n", - "Here, the $U_f$ gate computes $f(x)$, where $x$ is the state of qubit 0, and applies that to qubit 1. So, the resulting state, $|x\\rangle|y\\oplus f(x)\\rangle$, simply becomes $|x\\rangle|f(x)\\rangle$ when $|y\\rangle = |0\\rangle$. This contains all the information we need to know the function $f(x)$: qubit 0 tells us what $x$ is, and qubit 1 tells us what $f(x)$ is. So, if we initialize $|x\\rangle = \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$, then the final state of both qubits will be: $|y\\rangle|x\\rangle = \\frac{1}{\\sqrt{2}}(|f(0)\\rangle|0\\rangle+|f(1)\\rangle|1\\rangle)$. But how do we access that information?\n", - "\n", - "### 2.1. Try it on Qiskit:\n", - "\n", - "Using Qiskit we'll randomly select one of the four possible functions above and run the circuit. Then your task is to use the measurements of the quantum circuit to learn the function in as few runs as possible.\n", - "\n", - "In this first experiment and throughout the module, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks workflows into the following steps:\n", - "\n", - "- Step 1: Map classical inputs to a quantum problem\n", - "- Step 2: Optimize problem for quantum execution\n", - "- Step 3: Execute using Qiskit Runtime Primitives\n", - "- Step 4: Post-processing and classical analysis\n", - "\n", - "Let's start by loading some necessary packages, including the Qiskit Runtime primitives. We will also select the least busy quantum computer available to us.\n", - "\n", - "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ccc7364-7b6b-45f5-94b8-b1274006ee2f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_brisbane\n" - ] - } - ], - "source": [ - "# Load the Qiskit Runtime service\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Load the Runtime primitive and session\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", - "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", - "\n", - "# Load saved credentials\n", - "service = QiskitRuntimeService()\n", - "\n", - "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", - "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "print(backend.name)\n", - "\n", - "\n", - "sampler = Sampler(mode=backend)" - ] - }, - { - "cell_type": "markdown", - "id": "9912a7c5-ce2b-4eaa-abe7-82ebd4e494c8", - "metadata": {}, - "source": [ - "The cell below will allow you to switch between using the simulator or real hardware throughout the notebook. We recommend running it now:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29468e63-ce36-4eb7-95b4-176788a97e54", - "metadata": {}, - "outputs": [], - "source": [ - "# Load the backend sampler\n", - "from qiskit.primitives import BackendSamplerV2\n", - "\n", - "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "# Alternatively, load a fake backend with generic properties and define a simulator.\n", - "\n", - "\n", - "noise_model = NoiseModel.from_backend(backend)\n", - "\n", - "# Define a simulator using Aer, and use it in Sampler.\n", - "backend_sim = AerSimulator(noise_model=noise_model)\n", - "sampler_sim = BackendSamplerV2(backend=backend_sim)\n", - "\n", - "# You could also define a simulator-based sampler using a generic backend:\n", - "# backend_gen = GenericBackendV2(num_qubits=18)\n", - "# sampler_gen = BackendSamplerV2(backend=backend_gen)" - ] - }, - { - "cell_type": "markdown", - "id": "19e9b62f-6e1c-43a1-bdda-75766b1ff7d3", - "metadata": {}, - "source": [ - "Now that we've loaded the necessary packages, we can proceed with the Qiskit patterns workflow. In the mapping step below, we first make function that selects among the four possible functions taking a single bit to another single bit." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "5e67183b-42b9-44c2-bd4b-b5e2d192a796", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map\n", - "\n", - "from qiskit import QuantumCircuit\n", - "\n", - "qc = QuantumCircuit(2)\n", - "\n", - "\n", - "def twobit_function(case: int):\n", - " \"\"\"\n", - " Generate a valid two-bit function as a `QuantumCircuit`.\n", - " \"\"\"\n", - " if case not in [1, 2, 3, 4]:\n", - " raise ValueError(\"`case` must be 1, 2, 3, or 4.\")\n", - "\n", - " f = QuantumCircuit(2)\n", - " if case in [2, 3]:\n", - " f.cx(0, 1)\n", - " if case in [3, 4]:\n", - " f.x(1)\n", - " return f\n", - "\n", - "\n", - "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the circuit is too large to display\n", - "# blackbox = twobit_function(2).to_gate() # you may edit the number inside \"twobit_function()\" to select among the four valid functions\n", - "# blackbox.label = \"$U_f$\"\n", - "\n", - "qc.h(0)\n", - "qc.barrier()\n", - "qc.compose(twobit_function(2), inplace=True)\n", - "qc.measure_all()\n", - "\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "5cf08592-f32e-4ae8-ad69-afb160e43ab4", - "metadata": {}, - "source": [ - "In the above circuit, the Hadamard gate \"H\" takes qubit 0, which is initially in the state $|0\\rangle$, to the superposition state $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$. Then, $U_f$ evaluates the function $f(x)$ and applies that to qubit 1.\n", - "\n", - "Next we need to optimize and transpile the circuit to be run on the quantum computer:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d8d77417-0295-4f20-aff6-b2a007d5d02f", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "qc_isa = pm.run(qc)" - ] - }, - { - "cell_type": "markdown", - "id": "51e7b705-a4b8-460a-aa5a-6122c84f9b2f", - "metadata": {}, - "source": [ - "Finally, we execute our transpiled circuit on the quantum computer and visualize our results:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0495256b-2a80-422e-9adf-2fef1c039a6d", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 3: Run the job on a real quantum computer\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", - "res = job.result()\n", - "counts = res[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "6d2904cc-c730-4dca-a167-438018230299", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 4: Visualize and analyze results\n", - "\n", - "## Analysis\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "47710bb8-372c-4949-9468-bd2480c4ae5b", - "metadata": {}, - "source": [ - "The above is a histogram of our results. Depending on the number of shots you chose to run the circuit in step 3 above, you could see one or two bars, representing the measured states of the two qubits in each shot. As always with Qiskit and in this notebook, we use \"little endian\" notation, meaning the states of qubits 0 through n are written in ascending order from right to left, so qubit 0 is always farthest right.\n", - "\n", - "So, because qubit 0 was in a superposition state, the circuit evaluated the function for *both* $x=0$ and $x=1$ *at the same time* — something classical computers cannot do! But the catch comes when we want to learn about the function $f(x)$ — when we measure the qubits, we collapse their state. If you select \"shots = 1\" to only run the circuit once, you will only see one bar in the histogram above, and your information about the function will be incomplete.\n", - "\n", - "#### Check your understanding\n", - "\n", - "How many times must we run the above algorithm to learn the function $f(x)$? Is this any better than the classical case? Would you rather have a classical or quantum computer to solve this problem?\n", - "\n", - "\n", - "\n", - "\n", - "Since the measurement will collapse the superposition and return only one value, we need to run the circuit *at least* twice to return both outputs of the function $f(0)$ and $f(1)$. Best case, this performs as well as the classical case, where we compute both $f(0)$ and $f(1)$ in the first two queries. But there's a chance that we'll need to run it more than two times, since the final measurement is probabilistic and might return the same $f(x)$ value the first two times. I would rather have a classical computer in this case.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "So, while quantum parallelism can be powerful when used in the right way, it is not correct to say that a quantum computer works just like a massive, classical parallel processor. The act of measurement collapses the quantum states, so we can only ever access a single output of the computation." - ] - }, - { - "cell_type": "markdown", - "id": "cda50fdf-c354-4021-9b5a-c8cde5cc5edd", - "metadata": {}, - "source": [ - "## Deutsch's algorithm\n", - "\n", - "While quantum parallelism alone doesn't give us an advantage over classical computers, we can pair this with another quantum phenomena, interference, to achieve a speed-up. The algorithm now known as \"Deutsch's algorithm\" is the first example of an algorithm that accomplishes this.\n", - "\n", - "### The problem\n", - "\n", - "Here was the problem:\n", - "\n", - "Given an input bit, $x = \\{0,1\\}$, and an input function $f(x) = \\{0,1\\}$, determine whether the function is *balanced* or *constant*. That is, if it's balanced, then the output of the function is 0 half the time and 1 the other half the time. If it's constant, then the output of the function is either always 0 or always 1. Recall the table of four possible functions taking a single bit to another a single bit:\n", - "\n", - "\n", - "| $x$ | $f_1(x)$ | $f_2(x)$ | $f_3(x)$ | $f_4(x)$ |\n", - "| ----------- | ------- |-------| -------- | ------- |\n", - "| 0 | 0 | 0 | 1 | 1 |\n", - "| 1 | 0 | 1 | 0 | 1 |\n", - "\n", - "The first and the last functions, $f_1(x)$ and $f_4(x)$, are constant, while the middle two functions, $f_2(x)$ and $f_3(x)$, are balanced." - ] - }, - { - "cell_type": "markdown", - "id": "a34f1c24-0ed5-4458-8d4e-5957c691cadb", - "metadata": {}, - "source": [ - "### The algorithm\n", - "\n", - "The way Deutsch approached this problem was through the \"query model.\" In the query model, the input function ($f_i(x)$ above) is contained in a \"black box\" — we don't have direct access to its contents, but we can query the black box and it will give us the output of the function. We sometimes say that an \"oracle\" provides this information. See [Lesson 1: Quantum Query Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/introduction) of the Fundamentals of Quantum Algorithms course for more on the query model.\n", - "\n", - "To determine whether a quantum algorithm is more efficient than a classical algorithm in the query model, we can simply compare the number of queries we need to make of the black box in each case. In the classical case, in order to know if the function contained in the black box were balanced or constant, we would need to query the box two times to get both $f(0)$ and $f(1)$.\n", - "\n", - "In Deutsch's quantum algorithm, though, he found a way to get the information with only one query! He made one adjustment to the \"quantum parallelism\" circuit above, so that he prepared a superposition state on *both* qubits, instead of only on qubit 0. Then the two outputs of the function, $f(0)$ and $f(1)$ interfered to return 0 if they were either both 0 or both 1 (the function was constant), and returned 1 if they were different (the function was balanced). In this way, Deutsch could differentiate between a constant and a balanced function with a single query.\n", - "\n", - "Here's a circuit diagram of Deutsch's algorithm:\n", - "\n", - "![Circuit diagram of Deutsch's algorithm](/learning/images/modules/computer-science/deutsch-jozsa/Deutsch_algo.avif)\n", - "\n", - "To understand how this algorithm works, let's look at the quantum states of the qubits at the three points noted on the diagram above. Try to work out the states for yourself before clicking to view the answers:\n", - "\n", - "\n", - "#### Check your understanding\n", - "\n", - "What is the state $|\\pi_1\\rangle$?\n", - "\n", - "\n", - "\n", - "\n", - "Applying a Hadamard transforms the state $|0\\rangle$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ and the state $|1\\rangle$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$. So, the full state becomes: $|\\pi_1\\rangle = [\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle+|1\\rangle}{\\sqrt{2}}]$\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "What is the state $|\\pi_2\\rangle$?\n", - "\n", - "\n", - "\n", - "\n", - "Before we apply $U_f$, remember what it does. It will change the state of qubit 1 based on the state of qubit 0. So, it makes sense to factor the state of qubit 0 out: $|\\pi_1\\rangle = \\frac{1}{2} (|0\\rangle-|1\\rangle)|0\\rangle+\\frac{1}{2}(|0\\rangle-|1\\rangle)|1\\rangle$. Then, if $f(0)=f(1)$, the two terms will transform in the same way and the relative sign between the two terms remains positive, but if $f(0)\\neq f(1)$, then that means the second term will pick up a minus sign relative to the first term, changing the state of qubit 0 from $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$. So:\n", - "$$\n", - "|\\pi_2\\rangle = \\begin{cases}\n", - "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle+|1\\rangle}{\\sqrt{2}}] & \\text{if} & f(0) = f(1) \\\\\n", - "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}] &\\text{if} & f(0) \\neq f(1) \\\\\n", - "\\end{cases}\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "What is the state $|\\pi_3\\rangle$?\n", - "\n", - "\n", - "\n", - "\n", - "Now, the state of qubit 0 is either $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ or $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$, depending on the function. Applying the Hadamard will yield either $|0\\rangle$ or $|1\\rangle$, respectively.\n", - "\n", - "$$\n", - "|\\pi_3\\rangle = \\begin{cases}\n", - "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}]|0\\rangle & \\text{if} & f(0) = f(1) \\\\\n", - "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}]|1\\rangle &\\text{if} & f(0) \\neq f(1) \\\\\n", - "\\end{cases}\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Looking through your answers for the above questions, note that something a bit surprising happens. Although $U_f$ does nothing explicitly to the state of qubit 0, because it changes qubit 1 based on the state of qubit 0, it can happen that this causes a phase shift in qubit 0. This is known as the \"phase-kickback\" phenomenon, and is discussed in more detail in [Lesson 1: Quantum Query Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/introduction) of the Fundamentals of Quantum Algorithms course.\n", - "\n", - "Now that we understand how this algorithm works, let's implement it with Qiskit." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "4d9129df-f2ef-4f94-9508-21ed986fd823", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "## Deutsch's algorithm:\n", - "\n", - "## Step 1: Map the problem\n", - "\n", - "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the circuit is too large to display\n", - "blackbox = twobit_function(\n", - " 3\n", - ").to_gate() # you may edit the number (1-4) inside \"twobit_function()\" to select among the four valid functions\n", - "blackbox.label = \"$U_f$\"\n", - "\n", - "\n", - "qc_deutsch = QuantumCircuit(2, 1)\n", - "\n", - "qc_deutsch.x(1)\n", - "qc_deutsch.h(range(2))\n", - "\n", - "qc_deutsch.barrier()\n", - "qc_deutsch.compose(twobit_function(2), inplace=True)\n", - "qc_deutsch.barrier()\n", - "\n", - "qc_deutsch.h(0)\n", - "qc_deutsch.measure(0, 0)\n", - "\n", - "qc_deutsch.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ef0196b4-d4f0-4581-96f8-97893e652ee8", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "qc_isa = pm.run(qc_deutsch)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "51ad01d0-fa90-4e80-a55d-e55e146e2065", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 3: Run the job on a real quantum computer\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", - "res = job.result()\n", - "counts = res[0].data.c.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "5465d833-49e0-4779-94a3-0adb18f6aa76", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'1': 1}\n", - "balanced\n" - ] - } - ], - "source": [ - "# Step 4: Visualize and analyze results\n", - "\n", - "## Analysis\n", - "print(counts)\n", - "if \"1\" in counts:\n", - " print(\"balanced\")\n", - "else:\n", - " print(\"constant\")" - ] - }, - { - "cell_type": "markdown", - "id": "82f6da25-0f9c-47b3-aa24-02482f008383", - "metadata": {}, - "source": [ - "## The Deutsch-Jozsa algorithm\n", - "\n", - "Deutsch's algorithm was an important first step in demonstrating how a quantum computer might be more efficient than a classical computer, but it was only a modest improvement: it required just one query, compared to two in the classical case. In 1992, Deutsch and his colleague, Richard Jozsa, extended the original two-qubit algorithm to more qubits. The problem remained the same: determine whether a function is *balanced* or *constant*. But this time, the function goes from $n$ bits to a single bit. Either the function returns 0 and 1 an equal number of times (it's *balanced*) or the function returns always 1 or always 0 (it's *constant*).\n", - "\n", - "Here's a circuit diagram of the algorithm:\n", - "\n", - "![DJ_algo.png](/learning/images/modules/computer-science/deutsch-jozsa/DJ_algo.avif)\n", - "\n", - "This algorithm works in the same way as Deutsch's algorithm: the phase-kickback allows one to read out the state of qubit 0 to determine whether the function is constant or balanced. It's a bit trickier to see than for the two-qubit Deutsch's algorithm case, since the states will include sums over the $n$ qubits, and so working out those states will be left as an optional exercise for you at the end of the module. The algorithm will return a bitstring of all 0's if the function is constant, and a bitstring containing at least one 1 if the function is balanced.\n", - "\n", - "To see how the algorithm works in Qiskit, first, we need to generate our oracle: the random function that is guaranteed to be either constant or balanced. The code below will generate a balanced function 50% of the time, and a constant function 50% of the time. Don't worry if you don't entirely follow the code — it's complicated and not necessary for our understanding of the quantum algorithm." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ca2a51c0-3e62-4536-b891-0834e325a3d6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "import numpy as np\n", - "\n", - "\n", - "def dj_function(num_qubits):\n", - " \"\"\"\n", - " Create a random Deutsch-Jozsa function.\n", - " \"\"\"\n", - "\n", - " qc_dj = QuantumCircuit(num_qubits + 1)\n", - " if np.random.randint(0, 2):\n", - " # Flip output qubits with 50% chance\n", - " qc_dj.x(num_qubits)\n", - " if np.random.randint(0, 2):\n", - " # return constant circuit with 50% chance.\n", - " return qc_dj\n", - "\n", - " # If the \"if\" statement above was \"TRUE\" then we've returned the constant\n", - " # function and the function is complete. If not, we proceed in creating our\n", - " # balanced function. Everything below is to produce the balanced function:\n", - "\n", - " # select half of all possible states at random:\n", - " on_states = np.random.choice(\n", - " range(2**num_qubits), # numbers to sample from\n", - " 2**num_qubits // 2, # number of samples\n", - " replace=False, # makes sure states are only sampled once\n", - " )\n", - "\n", - " def add_cx(qc_dj, bit_string):\n", - " for qubit, bit in enumerate(reversed(bit_string)):\n", - " if bit == \"1\":\n", - " qc_dj.x(qubit)\n", - " return qc_dj\n", - "\n", - " for state in on_states:\n", - " # qc_dj.barrier() # Barriers are added to help visualize how the functions are created. They can safely be removed.\n", - " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", - " qc_dj.mcx(list(range(num_qubits)), num_qubits)\n", - " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", - "\n", - " # qc_dj.barrier()\n", - "\n", - " return qc_dj\n", - "\n", - "\n", - "n = 3 # number of input qubits\n", - "\n", - "oracle = dj_function(n)\n", - "\n", - "display(oracle.draw(\"mpl\"))" - ] - }, - { - "cell_type": "markdown", - "id": "78096e00-a29b-418c-a620-726675c2a792", - "metadata": {}, - "source": [ - "This is the oracle function, which is either balanced or constant. Can you see by looking at it whether the output on the last qubit depends on the values put in for the first $n$ qubits? If the output for the last qubit depends on the first $n$ qubits, can you tell if that dependent output is balanced or not?\n", - "\n", - "We can tell whether the function is balanced or constant by looking at the above circuit, but remember, for the sake of this problem, we think of this function as a \"black box.\" We can't peek into the box to look at the circuit diagram. Instead, we need to query the box.\n", - "\n", - "To query the box, we use the Deutsch-Jozsa algorithm and determine whether the function is constant or balanced:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "fe7ee688-f052-4a7e-bcc7-a14bea57e5c6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "blackbox = oracle.to_gate()\n", - "blackbox.label = \"$U_f$\"\n", - "\n", - "\n", - "qc_dj = QuantumCircuit(n + 1, n)\n", - "qc_dj.x(n)\n", - "qc_dj.h(range(n + 1))\n", - "qc_dj.barrier()\n", - "qc_dj.compose(blackbox, inplace=True)\n", - "qc_dj.barrier()\n", - "qc_dj.h(range(n))\n", - "qc_dj.measure(range(n), range(n))\n", - "\n", - "qc_dj.decompose().decompose()\n", - "\n", - "\n", - "qc_dj.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "bf3aedfa-7454-424e-85cb-c446a8918417", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map the problem\n", - "\n", - "qc_dj = QuantumCircuit(n + 1, n)\n", - "qc_dj.x(n)\n", - "qc_dj.h(range(n + 1))\n", - "qc_dj.barrier()\n", - "qc_dj.compose(oracle, inplace=True)\n", - "qc_dj.barrier()\n", - "qc_dj.h(range(n))\n", - "qc_dj.measure(range(n), range(n))\n", - "\n", - "qc_dj.decompose().decompose()\n", - "\n", - "\n", - "qc_dj.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "5497c1aa-c427-419b-b22c-a0c2fa0c4028", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "qc_isa = pm.run(qc_dj)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "974f3db9-1b55-414c-9fe4-d891cf22f78f", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 3: Run the job on a real quantum computer\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", - "res = job.result()\n", - "counts = res[0].data.c.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "39af76b4-f380-4a61-82a4-1e9203c20408", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'110': 1}\n", - "balanced\n" - ] - } - ], - "source": [ - "# Step 4: Visualize and analyze results\n", - "\n", - "## Analysis\n", - "print(counts)\n", - "\n", - "if (\n", - " \"0\" * n in counts\n", - "): # The D-J algorithm returns all zeroes if the function was constant\n", - " print(\"constant\")\n", - "else:\n", - " print(\"balanced\") # anything other than all zeroes means the function is balanced." - ] - }, - { - "cell_type": "markdown", - "id": "c601a252-d1d4-4def-b9a4-d05494d34899", - "metadata": {}, - "source": [ - "Above, the first line of the output is the bitstring of measurement outcomes. The second line outputs whether the bitstring implies that the function was balanced or constant. If the bitstring contained all zeroes, then it was constant; if not, it was balanced. So, with just a single run of the above quantum circuit, we can determine whether the function is constant or balanced!\n", - "\n", - "#### Check your understanding\n", - "\n", - "How many queries would it take a classical computer to determine with 100% certainty whether a function were constant or balanced? Remember, classically, a single query only allows you to apply the function to a single bitstring.\n", - "\n", - "\n", - "\n", - "\n", - "There are $2^n$ possible bitstrings to check, and in the worst case, you would need to test $2^n/2+1$ of these. For example, if the function were constant, and you kept measuring \"1\" as the output of the function, then you couldn't be certain that it was truly constant until you checked over half of the results. Before then, you might have just been very unlucky to keep measuring \"1\" on a balanced function. It's like flipping a coin over and over and it landing heads every time. It's unlikely, but not impossible.\n", - "\n", - "\n", - "\n", - "\n", - "How would your above answer change if you just had to just measure until one outcome (balanced or constant) is more likely than the other? How many queries would it take in this case?\n", - "\n", - "\n", - "\n", - "\n", - "In this case, you could just measure twice. If the two measurements are different, you know the function is balanced. If the two measurements are the same, then it could be balanced, or it could be constant. The probability that it's balanced with this set of measurements is: $\\frac{1}{2}\\frac{2^n /2 - 1}{2^n-1}$. This is less than 1/2, so it's more likely that the function is constant in this case.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "So, the Deutsch-Jozsa algorithm demonstrated an exponential speed-up over a *deterministic* classical algorithm (one that returns the answer with 100% certainty), but no significant speed-up over a *probabilistic* one (one that returns a result that is *likely* to be the correct answer)." - ] - }, - { - "cell_type": "markdown", - "id": "37d8d1b5-1593-480e-afb9-cdae1debb8ea", - "metadata": {}, - "source": [ - "### The Bernstein - Vazirani problem\n", - "\n", - "In 1997, Ethan Bernstein and Umesh Vazirani used the Deutsch-Jozsa algorithm to solve a more specific, restricted problem compared to the Deutsch-Jozsa problem. Rather than simply try to distinguish between two different classes of functions, as in the D-J case, Bernstein and Vazirani used the Deutsch-Jozsa algorithm to actually learn a string encoded in a function. Here's the problem:\n", - "\n", - "The function $f:\\{0,1\\}^n \\rightarrow \\{0,1\\}$ still takes an $n$-bit string and outputs a single bit. But now, instead of promising that the function is balanced or constant, we're now promised that the function is the dot product between the input string $x$ and some secret $n$-bit string $s$, modulo 2. (This dot product modulo 2 is called the \"binary dot product.\") The problem is to figure out what the secret, $n$-bit string is.\n", - "\n", - "Written another way, we're given a black-box function $f: {0,1}^n \\rightarrow {0,1}$ that satisfies $f(x) = s \\cdot x$ for some string $s$, and we want to learn the string $s$.\n", - "\n", - "\n", - "Let's take a look at how the D-J algorithm solves this problem:\n", - "\n", - "1. First, a Hadamard gate is applied to the $n$ input qubits, and a NOT gate plus a Hadamard is applied to the output qubit, making the state:\n", - "\n", - "$$\n", - "|\\Psi\\rangle = |-\\rangle_{n} \\otimes |+\\rangle_{n-1} \\otimes |+\\rangle_{n-2} \\otimes ... \\otimes |+\\rangle_0\n", - "$$\n", - "\n", - " The state of qubits 1 through $n$ can be written more simply as a sum over all $2^n$ the $n$-qubit basis states $|00...00\\rangle, |00...01\\rangle, |000...11\\rangle, ..., |111...11\\rangle$. We call the set of these basis states $\\Sigma^n$. (See [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/deutsch-jozsa-algorithm) for more details.)\n", - "\n", - "$$\n", - "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{x \\in \\Sigma^n}{|x\\rangle}\n", - "$$\n", - "\n", - "2. Next, the $U_f$ gate is applied to the qubits. This gate will take the first n qubits as input (which are now in an equal superposition of all possible n-bit strings) and applies the function $f(x)=s \\cdot x$ to the output qubit, so that this qubit is now in the state: $ |- \\oplus f(x)\\rangle$. Thanks to the phase kickback mechanism, the state of this qubit remains unchanged, but some of the terms in the input qubit state pick up a minus sign:\n", - "\n", - "$$\n", - "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{x \\in \\Sigma^n}{(-1)^{f(x)}|x\\rangle}\n", - "$$\n", - "\n", - "3. Now, the next set of Hadamards are applied to qubits 0 through $n-1$. Keeping track of the minus signs in this case can be tricky. It's helpful to know that applying a layer of Hadamards to $n$ qubits in a standard basis state $|x\\rangle$ can be written as:\n", - "\n", - "$$\n", - "H^{\\otimes n} |x\\rangle = \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{y \\in \\Sigma^n}{(-1)^{x \\cdot y}|y\\rangle}\n", - "$$\n", - "\n", - "So the state becomes:\n", - "\n", - "$$\n", - "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{2^n}\\sum\\limits_{x \\in \\Sigma^n}\\sum\\limits_{y \\in \\Sigma^n}{(-1)^{(s \\cdot x) + (x \\cdot y)}|y\\rangle}\n", - "$$\n", - "\n", - "4. Next step is to measure the first $n$ bits. But what will we measure? It turns out that the state above simplifies to: $|\\Psi\\rangle = |-\\rangle \\otimes |s\\rangle$, but that's far from obvious. If you'd like to follow through the math, see John Watrous' [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/deutsch-jozsa-algorithm#the-bernstein-vazirani-problem) course. The point is, though, that the phase kickback mechanism leads to the input qubits being in the state $|s\\rangle$. So, to find out what the secret string $s$ was, you simply need to measure the qubits!\n", - "\n", - "\n", - "#### Check your understanding\n", - "\n", - "Verify that the state from Step 3 above is indeed the state $|s\\rangle$ for the special case of $n=1$.\n", - "\n", - "\n", - "\n", - "\n", - "When you explicitly write out the two summations, you should get a state with four terms (let's omit the output state $|-\\rangle$ for this):\n", - "\n", - "$$\n", - "|\\Psi\\rangle = \\frac{1}{2}[|0\\rangle + (-1)^s |0\\rangle + |1\\rangle + (-1)^{(s+1)} |1\\rangle]\n", - "$$\n", - "\n", - "If $s=0$, then the first two terms add constructively and the last two terms cancel, leaving us with $|\\Psi\\rangle = |0\\rangle$. If $s=1$, then the last two terms add constructively and the first two terms cancel, leaving us with $|\\Psi\\rangle = |1\\rangle$. So, in either case, $|\\Psi\\rangle = |s\\rangle$. Hopefully this simplest case gives you a sense for how the general case with $n$ qubits works: all terms that are not $|s\\rangle$ interfere away, leaving just the state $|s\\rangle$.\n", - "\n", - "\n", - "\n", - "\n", - "How can the same algorithm solve both the Bernstein-Vazirani and Deutsch-Jozsa problems? To make sense of this, think about Bernstein-Vazirani functions, which are of the form $f(x) = s \\cdot x$. Are these functions also Deutsch-Jozsa functions? That is, determine whether functions of this form satisfy the Deutsch-Jozsa problem promise: that they're either *constant* or *balanced*. How does this help us understand how the same algorithm solves two different problems?\n", - "\n", - "\n", - "\n", - "\n", - "Every Bernstein-Vazirani function of the form $f(x) = s \\cdot x$ also satisfies the Deutsch-Jozsa problem promise: if s=00...00, then the function is constant (always returns 0 for every string x). If s is any other string, then the function is balanced. So, applying the Deutsch-Jozsa algorithm to one of these functions simultaneously solves both problems! It returns the string, and if that string is 00...00 then we know it's constant; if there's at least one \"1\" in the string, we know it's balanced.\n", - "\n", - "\n", - "\n", - "\n", - "We can also verify that this algorithm successfully solves the Bernstein-Vazirani problem by testing it experimentally. First, we create the B-V function that lives inside the black box:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "45449a26-0bd0-4244-87be-3309937955b9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Step 1: Map the problem\n", - "\n", - "\n", - "def bv_function(s):\n", - " \"\"\"\n", - " Create a Bernstein-Vazirani function from a string of 1s and 0s.\n", - " \"\"\"\n", - " qc = QuantumCircuit(len(s) + 1)\n", - " for index, bit in enumerate(reversed(s)):\n", - " if bit == \"1\":\n", - " qc.cx(index, len(s))\n", - " return qc\n", - "\n", - "\n", - "display(bv_function(\"1000\").draw(\"mpl\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "0cf6f2bc-3b5e-46d2-ab82-1a190e77c42b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "string = \"1000\" # secret string that we'll pretend we don't know or have access to\n", - "n = len(string)\n", - "\n", - "qc = QuantumCircuit(n + 1, n)\n", - "qc.x(n)\n", - "qc.h(range(n + 1))\n", - "qc.barrier()\n", - "# qc.compose(oracle, inplace = True)\n", - "qc.compose(bv_function(string), inplace=True)\n", - "qc.barrier()\n", - "qc.h(range(n))\n", - "qc.measure(range(n), range(n))\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "5d225a6e-e3d0-4c08-8aeb-f03337bfffc4", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "qc_isa = pm.run(qc)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8fef6a65-227a-4f27-af3e-348513e1cd33", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 3: Run the job on a real quantum computer\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", - "res = job.result()\n", - "counts = res[0].data.c.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "ec576787-d9ba-4406-b799-9c0de21a8088", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'0000': 1}\n" - ] - } - ], - "source": [ - "# Step 4: Visualize and analyze results\n", - "\n", - "## Analysis\n", - "print(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "46ff0418-570d-4e78-be36-f403aeccc392", - "metadata": {}, - "source": [ - "So, with just a single query, the Deutsch-Jozsa algorithm will return the string $s$ used in the function: $f(x)=x \\cdot s$ when we apply it to the Bernstein-Vazirani problem. With a classical algorithm, one would need $n$ queries to solve the same problem.\n", - "\n", - "## Conclusion\n", - "\n", - "We hope that by examining these simple examples, we've given you a better intuition for how quantum computers are able to harness superposition, entanglement, and interference to achieve their power over classical computers.\n", - "\n", - "The Deutsch-Jozsa algorithm has huge historical importance because it was the first to demonstrate any speedup over a classical algorithm, but it was only a polynomial speedup. The Deutsch-Jozsa algorithm is just the beginning of the story.\n", - "\n", - "After they used the algorithm to solve their problem, Bernstein and Vazirani used this as the basis for a more complicated, recursive problem called the *recursive Fourier sampling problem*. Their solution offered a super-polynomial speedup over classical algorithms. And even before Bernstein and Vazirani, Peter Shor had already come up with his famous algorithm that enabled quantum computers to factor large numbers exponentially faster than any classical algorithm could. These results, collectively showed the exciting promise of future quantum computer, and spurred physicists and engineers to make this future a reality." - ] - }, - { - "cell_type": "markdown", - "id": "c76273ac-ad3c-4c82-94e8-213e887dc7b7", - "metadata": {}, - "source": [ - "## Questions\n", - "\n", - "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", - "\n", - "### Critical concepts\n", - "- the Deutsch and Deutsch-Jozsa algorithms use quantum parallelism combined with interference to find an answer to a problem faster than a classical computer can.\n", - "- the phase kickback mechanism is a counterintuitive quantum phenomena that transfers operations on one qubit to the phase of another qubit. The Deutsch and Deutsch-Jozsa algorithms utilize this mechanism.\n", - "- The Deutsch-Jozsa algorithm offers a polynomial speedup over any deterministic classical algorithm.\n", - "- The Deutsch-Jozsa algorithm can be applied to a different problem, called the Bernstein-Vazirani problem, to find a hidden string encoded in a function.\n", - "\n", - "### True/false\n", - "1. T/F Deutsch's algorithm is a special case of the Deutsch-Jozsa algorithm where the input is a single qubit.\n", - "2. T/F The Deutsch and Deutsch-Jozsa algorithms use quantum superposition and interference to achieve their efficiency.\n", - "4. T/F The Deutsch-Jozsa algorithm requires multiple function evaluations to determine if a function is constant or balanced.\n", - "5. T/F The \"Bernstein-Vazirani algorithm\" is actually the same as the Deutsch-Jozsa algorithm, applied to a different problem.\n", - "6. T/F The Bernstein-Vazirani algorithm can find multiple secret strings simultaneously.\n", - "\n", - "### Short answer\n", - "\n", - "1. How long would it take a classical algorithm to solve the Deutsch-Jozsa problem in the worst case?\n", - "\n", - "2. How long would it take a classical algorithm to solve the Bernstein-Vazirani problem? What speedup does the DJ algorithm offer in this case?\n", - "\n", - "3. Describe the phase-kickback mechanism and how it works to solve the Deutsch-Jozsa and Bernstein-Vazirani problems.\n", - "\n", - "### Challenge problem\n", - "1. The Deutsch-Jozsa algorithm: Recall that you had a question above asking you to work out the intermediate qubit states $\\pi_1$, and $\\pi_2$ of the Deutsch's algorithm. Do the same for the intermediate $n+1$-qubit states $\\pi_1$, and $\\pi_2$ of the Deutsch-Jozsa algorithm, for the specific case that $n=2$. Then, verify that $\\pi_3 = |-\\rangle \\otimes \\sum\\limits_{x_0...x_n}(-1)^{f(x_0...x_n)}|x_0 ... x_n\\rangle$, again, for the specific case that $n=2$." - ] - } - ], - "metadata": { - "in_page_toc_max_heading_level": 2, - "in_page_toc_min_heading_level": 2, - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bfa8f443", + "metadata": {}, + "source": [ + "---\n", + "title: The Deutsch-Jozsa Algorithm\n", + "description: Learn how the Deutsch-Jozsa algorithm uses quantum parallelism and interference to achieve an exponential speedup over classical algorithms.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore blackbox Hadamards */}" + ] + }, + { + "cell_type": "markdown", + "id": "e761a401-3dd0-4c3c-9333-0d89da48fb34", + "metadata": {}, + "source": [ + "# The Deutsch-Jozsa algorithm\n", + "\n", + "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", + "- `qiskit` v2.1.0 or newer\n", + "- `qiskit-ibm-runtime` v0.40.1 or newer\n", + "- `qiskit-aer` v0.17.0 or newer\n", + "- `qiskit.visualization`\n", + "- `numpy`\n", + "- `pylatexenc`\n", + "\n", + "\n", + "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", + "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", + "\n", + "This module was tested and used four seconds of QPU time. This is an estimate only. Your actual usage may vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24a83c6d", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and modify this line as needed to install dependencies\n", + "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" + ] + }, + { + "cell_type": "markdown", + "id": "026f7f82-ac54-413d-8158-e58461bc2afd", + "metadata": {}, + "source": [ + "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/QcK0GK7DUh8?si=8e0Lmjgylxmgl2y7) to watch it on YouTube.\n", + "\n", + "-------\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "34b2aac3-848f-46b4-8c95-8236b6ad7f8e", + "metadata": {}, + "source": [ + "## Intro\n", + "\n", + "In the early 1980's, quantum physicists and computer scientists had a vague notion that quantum mechanics could be harnessed to make computations that were far more powerful than classical computers can make. Their reasoning was this: it's difficult for a classical computer to simulate quantum systems, but a *quantum* computer should be able to do it more efficiently. And if a quantum computer could simulate quantum systems more efficiently, perhaps there were other tasks that it could perform more efficiently than a classical computer.\n", + "\n", + "The logic was sound, but the details remained to be worked out. This began in 1985, when David Deutsch described the first \"universal quantum computer.\" In this same paper, he provided the first example problem for which a quantum computer could solve something more efficiently than a classical computer could. This first toy example is now known as \"Deutsch's algorithm.\" The improvement in Deutsch's algorithm was modest, but Deutsch worked with Richard Jozsa a few years later to further widen the gap between classical and quantum computers.\n", + "\n", + "These algorithms — Deutsch's, and the Deutsch-Jozsa extension — are not particularly useful, but they are still really important for a few reasons:\n", + "\n", + "1. Historically, they were some of the first quantum algorithms that were demonstrated to beat their classical counterparts. Understanding them can help us understand how the community's thinking on quantum computing has evolved over time.\n", + "2. They can help us understand some aspects of the answer to a surprisingly subtle question: What gives quantum computing its power? Sometimes, quantum computers are compared to giant, exponentially-scaling parallel processors. But this isn't quite right. While a piece of the answer to this question lies in so-called \"quantum parallelism,\" extracting as much information as possible in a single run is a subtle art. The Deutsch and Deutsch-Jozsa algorithms show how this can be done.\n", + "\n", + "In this module, we'll learn about Deutsch's algorithm, the Deutsch-Jozsa algorithm, and what they teach us about the power of quantum computing." + ] + }, + { + "cell_type": "markdown", + "id": "096da154-5663-4f46-8d9e-b6f163260706", + "metadata": {}, + "source": [ + "## Quantum parallelism and its limits\n", + "\n", + "Part of the power of quantum computing is derived from \"quantum parallelism.\" which is essentially the ability to perform operations on multiple inputs at the same time, since the qubit input states could be in a superposition of multiple classically allowed states. HOWEVER, while a quantum circuit might be able to evaluate multiple input states at once, extracting all of that information in one go is impossible.\n", + "\n", + "To see what I mean here, let's say we have a bit, $x$ and some function applied to that bit, $f(x)$. There are four possible binary functions taking a single bit to another single bit:\n", + "\n", + "| $x$ | $f_1(x)$ | $f_2(x)$ | $f_3(x)$ | $f_4(x)$ |\n", + "| ----------- | ------- |-------| -------- | ------- |\n", + "| 0 | 0 | 0 | 1 | 1 |\n", + "| 1 | 0 | 1 | 0 | 1 |\n", + "\n", + "We'd like to find out which of these functions (1-4) our $f(x)$ is. Classically, we would need to run the function twice — once for $x=0$, once for $x=1$. But let's see if we can do better with a quantum circuit. We can learn about the function with the following gate:\n", + "\n", + "![quantum_parallelism](/learning/images/modules/computer-science/deutsch-jozsa/quantum-parallelism.avif)\n", + "\n", + "Here, the $U_f$ gate computes $f(x)$, where $x$ is the state of qubit 0, and applies that to qubit 1. So, the resulting state, $|x\\rangle|y\\oplus f(x)\\rangle$, simply becomes $|x\\rangle|f(x)\\rangle$ when $|y\\rangle = |0\\rangle$. This contains all the information we need to know the function $f(x)$: qubit 0 tells us what $x$ is, and qubit 1 tells us what $f(x)$ is. So, if we initialize $|x\\rangle = \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$, then the final state of both qubits will be: $|y\\rangle|x\\rangle = \\frac{1}{\\sqrt{2}}(|f(0)\\rangle|0\\rangle+|f(1)\\rangle|1\\rangle)$. But how do we access that information?\n", + "\n", + "### 2.1. Try it on Qiskit:\n", + "\n", + "Using Qiskit we'll randomly select one of the four possible functions above and run the circuit. Then your task is to use the measurements of the quantum circuit to learn the function in as few runs as possible.\n", + "\n", + "In this first experiment and throughout the module, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks workflows into the following steps:\n", + "\n", + "- Step 1: Map classical inputs to a quantum problem\n", + "- Step 2: Optimize problem for quantum execution\n", + "- Step 3: Execute using Qiskit Runtime Primitives\n", + "- Step 4: Post-processing and classical analysis\n", + "\n", + "Let's start by loading some necessary packages, including the Qiskit Runtime primitives. We will also select the least busy quantum computer available to us.\n", + "\n", + "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ccc7364-7b6b-45f5-94b8-b1274006ee2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_brisbane\n" + ] + } + ], + "source": [ + "# Load the Qiskit Runtime service\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Load the Runtime primitive and session\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance =\n", + "# '', token='', overwrite=True, set_as_default=True)\n", + "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", + "\n", + "# Load saved credentials\n", + "service = QiskitRuntimeService()\n", + "\n", + "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", + "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "print(backend.name)\n", + "\n", + "\n", + "sampler = Sampler(mode=backend)" + ] + }, + { + "cell_type": "markdown", + "id": "9912a7c5-ce2b-4eaa-abe7-82ebd4e494c8", + "metadata": {}, + "source": [ + "The cell below will allow you to switch between using the simulator or real hardware throughout the notebook. We recommend running it now:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29468e63-ce36-4eb7-95b4-176788a97e54", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the backend sampler\n", + "from qiskit.primitives import BackendSamplerV2\n", + "\n", + "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "# Alternatively, load a fake backend with generic properties and define a simulator.\n", + "\n", + "\n", + "noise_model = NoiseModel.from_backend(backend)\n", + "\n", + "# Define a simulator using Aer, and use it in Sampler.\n", + "backend_sim = AerSimulator(noise_model=noise_model)\n", + "sampler_sim = BackendSamplerV2(backend=backend_sim)\n", + "\n", + "# You could also define a simulator-based sampler using a generic backend:\n", + "# backend_gen = GenericBackendV2(num_qubits=18)\n", + "# sampler_gen = BackendSamplerV2(backend=backend_gen)" + ] + }, + { + "cell_type": "markdown", + "id": "19e9b62f-6e1c-43a1-bdda-75766b1ff7d3", + "metadata": {}, + "source": [ + "Now that we've loaded the necessary packages, we can proceed with the Qiskit patterns workflow. In the mapping step below, we first make function that selects among the four possible functions taking a single bit to another single bit." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5e67183b-42b9-44c2-bd4b-b5e2d192a796", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map\n", + "\n", + "from qiskit import QuantumCircuit\n", + "\n", + "qc = QuantumCircuit(2)\n", + "\n", + "\n", + "def twobit_function(case: int):\n", + " \"\"\"\n", + " Generate a valid two-bit function as a `QuantumCircuit`.\n", + " \"\"\"\n", + " if case not in [1, 2, 3, 4]:\n", + " raise ValueError(\"`case` must be 1, 2, 3, or 4.\")\n", + "\n", + " f = QuantumCircuit(2)\n", + " if case in [2, 3]:\n", + " f.cx(0, 1)\n", + " if case in [3, 4]:\n", + " f.x(1)\n", + " return f\n", + "\n", + "\n", + "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the\n", + "# circuit is too large to display\n", + "# blackbox = twobit_function(2).to_gate() # you may edit the number inside \"twobit_function()\" to\n", + "# select among the four valid functions\n", + "# blackbox.label = \"$U_f$\"\n", + "\n", + "qc.h(0)\n", + "qc.barrier()\n", + "qc.compose(twobit_function(2), inplace=True)\n", + "qc.measure_all()\n", + "\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "5cf08592-f32e-4ae8-ad69-afb160e43ab4", + "metadata": {}, + "source": [ + "In the above circuit, the Hadamard gate \"H\" takes qubit 0, which is initially in the state $|0\\rangle$, to the superposition state $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$. Then, $U_f$ evaluates the function $f(x)$ and applies that to qubit 1.\n", + "\n", + "Next we need to optimize and transpile the circuit to be run on the quantum computer:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d8d77417-0295-4f20-aff6-b2a007d5d02f", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "qc_isa = pm.run(qc)" + ] + }, + { + "cell_type": "markdown", + "id": "51e7b705-a4b8-460a-aa5a-6122c84f9b2f", + "metadata": {}, + "source": [ + "Finally, we execute our transpiled circuit on the quantum computer and visualize our results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0495256b-2a80-422e-9adf-2fef1c039a6d", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Run the job on a real quantum computer\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", + "res = job.result()\n", + "counts = res[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6d2904cc-c730-4dca-a167-438018230299", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 4: Visualize and analyze results\n", + "\n", + "## Analysis\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "47710bb8-372c-4949-9468-bd2480c4ae5b", + "metadata": {}, + "source": [ + "The above is a histogram of our results. Depending on the number of shots you chose to run the circuit in step 3 above, you could see one or two bars, representing the measured states of the two qubits in each shot. As always with Qiskit and in this notebook, we use \"little endian\" notation, meaning the states of qubits 0 through n are written in ascending order from right to left, so qubit 0 is always farthest right.\n", + "\n", + "So, because qubit 0 was in a superposition state, the circuit evaluated the function for *both* $x=0$ and $x=1$ *at the same time* — something classical computers cannot do! But the catch comes when we want to learn about the function $f(x)$ — when we measure the qubits, we collapse their state. If you select \"shots = 1\" to only run the circuit once, you will only see one bar in the histogram above, and your information about the function will be incomplete.\n", + "\n", + "#### Check your understanding\n", + "\n", + "How many times must we run the above algorithm to learn the function $f(x)$? Is this any better than the classical case? Would you rather have a classical or quantum computer to solve this problem?\n", + "\n", + "\n", + "\n", + "\n", + "Since the measurement will collapse the superposition and return only one value, we need to run the circuit *at least* twice to return both outputs of the function $f(0)$ and $f(1)$. Best case, this performs as well as the classical case, where we compute both $f(0)$ and $f(1)$ in the first two queries. But there's a chance that we'll need to run it more than two times, since the final measurement is probabilistic and might return the same $f(x)$ value the first two times. I would rather have a classical computer in this case.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "So, while quantum parallelism can be powerful when used in the right way, it is not correct to say that a quantum computer works just like a massive, classical parallel processor. The act of measurement collapses the quantum states, so we can only ever access a single output of the computation." + ] + }, + { + "cell_type": "markdown", + "id": "cda50fdf-c354-4021-9b5a-c8cde5cc5edd", + "metadata": {}, + "source": [ + "## Deutsch's algorithm\n", + "\n", + "While quantum parallelism alone doesn't give us an advantage over classical computers, we can pair this with another quantum phenomena, interference, to achieve a speed-up. The algorithm now known as \"Deutsch's algorithm\" is the first example of an algorithm that accomplishes this.\n", + "\n", + "### The problem\n", + "\n", + "Here was the problem:\n", + "\n", + "Given an input bit, $x = \\{0,1\\}$, and an input function $f(x) = \\{0,1\\}$, determine whether the function is *balanced* or *constant*. That is, if it's balanced, then the output of the function is 0 half the time and 1 the other half the time. If it's constant, then the output of the function is either always 0 or always 1. Recall the table of four possible functions taking a single bit to another a single bit:\n", + "\n", + "\n", + "| $x$ | $f_1(x)$ | $f_2(x)$ | $f_3(x)$ | $f_4(x)$ |\n", + "| ----------- | ------- |-------| -------- | ------- |\n", + "| 0 | 0 | 0 | 1 | 1 |\n", + "| 1 | 0 | 1 | 0 | 1 |\n", + "\n", + "The first and the last functions, $f_1(x)$ and $f_4(x)$, are constant, while the middle two functions, $f_2(x)$ and $f_3(x)$, are balanced." + ] + }, + { + "cell_type": "markdown", + "id": "a34f1c24-0ed5-4458-8d4e-5957c691cadb", + "metadata": {}, + "source": [ + "### The algorithm\n", + "\n", + "The way Deutsch approached this problem was through the \"query model.\" In the query model, the input function ($f_i(x)$ above) is contained in a \"black box\" — we don't have direct access to its contents, but we can query the black box and it will give us the output of the function. We sometimes say that an \"oracle\" provides this information. See [Lesson 1: Quantum Query Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/introduction) of the Fundamentals of Quantum Algorithms course for more on the query model.\n", + "\n", + "To determine whether a quantum algorithm is more efficient than a classical algorithm in the query model, we can simply compare the number of queries we need to make of the black box in each case. In the classical case, in order to know if the function contained in the black box were balanced or constant, we would need to query the box two times to get both $f(0)$ and $f(1)$.\n", + "\n", + "In Deutsch's quantum algorithm, though, he found a way to get the information with only one query! He made one adjustment to the \"quantum parallelism\" circuit above, so that he prepared a superposition state on *both* qubits, instead of only on qubit 0. Then the two outputs of the function, $f(0)$ and $f(1)$ interfered to return 0 if they were either both 0 or both 1 (the function was constant), and returned 1 if they were different (the function was balanced). In this way, Deutsch could differentiate between a constant and a balanced function with a single query.\n", + "\n", + "Here's a circuit diagram of Deutsch's algorithm:\n", + "\n", + "![Circuit diagram of Deutsch's algorithm](/learning/images/modules/computer-science/deutsch-jozsa/Deutsch_algo.avif)\n", + "\n", + "To understand how this algorithm works, let's look at the quantum states of the qubits at the three points noted on the diagram above. Try to work out the states for yourself before clicking to view the answers:\n", + "\n", + "\n", + "#### Check your understanding\n", + "\n", + "What is the state $|\\pi_1\\rangle$?\n", + "\n", + "\n", + "\n", + "\n", + "Applying a Hadamard transforms the state $|0\\rangle$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ and the state $|1\\rangle$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$. So, the full state becomes: $|\\pi_1\\rangle = [\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle+|1\\rangle}{\\sqrt{2}}]$\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "What is the state $|\\pi_2\\rangle$?\n", + "\n", + "\n", + "\n", + "\n", + "Before we apply $U_f$, remember what it does. It will change the state of qubit 1 based on the state of qubit 0. So, it makes sense to factor the state of qubit 0 out: $|\\pi_1\\rangle = \\frac{1}{2} (|0\\rangle-|1\\rangle)|0\\rangle+\\frac{1}{2}(|0\\rangle-|1\\rangle)|1\\rangle$. Then, if $f(0)=f(1)$, the two terms will transform in the same way and the relative sign between the two terms remains positive, but if $f(0)\\neq f(1)$, then that means the second term will pick up a minus sign relative to the first term, changing the state of qubit 0 from $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ to $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$. So:\n", + "$$\n", + "|\\pi_2\\rangle = \\begin{cases}\n", + "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle+|1\\rangle}{\\sqrt{2}}] & \\text{if} & f(0) = f(1) \\\\\n", + "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}][\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}] &\\text{if} & f(0) \\neq f(1) \\\\\n", + "\\end{cases}\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "What is the state $|\\pi_3\\rangle$?\n", + "\n", + "\n", + "\n", + "\n", + "Now, the state of qubit 0 is either $\\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$ or $\\frac{1}{\\sqrt{2}}(|0\\rangle-|1\\rangle)$, depending on the function. Applying the Hadamard will yield either $|0\\rangle$ or $|1\\rangle$, respectively.\n", + "\n", + "$$\n", + "|\\pi_3\\rangle = \\begin{cases}\n", + "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}]|0\\rangle & \\text{if} & f(0) = f(1) \\\\\n", + "\\pm[\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}]|1\\rangle &\\text{if} & f(0) \\neq f(1) \\\\\n", + "\\end{cases}\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Looking through your answers for the above questions, note that something a bit surprising happens. Although $U_f$ does nothing explicitly to the state of qubit 0, because it changes qubit 1 based on the state of qubit 0, it can happen that this causes a phase shift in qubit 0. This is known as the \"phase-kickback\" phenomenon, and is discussed in more detail in [Lesson 1: Quantum Query Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/introduction) of the Fundamentals of Quantum Algorithms course.\n", + "\n", + "Now that we understand how this algorithm works, let's implement it with Qiskit." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4d9129df-f2ef-4f94-9508-21ed986fd823", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Deutsch's algorithm:\n", + "\n", + "## Step 1: Map the problem\n", + "\n", + "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the\n", + "# circuit is too large to display\n", + "blackbox = twobit_function(\n", + " 3\n", + ").to_gate() # you may edit the number (1-4) inside \"twobit_function()\" to select among the four valid functions\n", + "blackbox.label = \"$U_f$\"\n", + "\n", + "\n", + "qc_deutsch = QuantumCircuit(2, 1)\n", + "\n", + "qc_deutsch.x(1)\n", + "qc_deutsch.h(range(2))\n", + "\n", + "qc_deutsch.barrier()\n", + "qc_deutsch.compose(twobit_function(2), inplace=True)\n", + "qc_deutsch.barrier()\n", + "\n", + "qc_deutsch.h(0)\n", + "qc_deutsch.measure(0, 0)\n", + "\n", + "qc_deutsch.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ef0196b4-d4f0-4581-96f8-97893e652ee8", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "qc_isa = pm.run(qc_deutsch)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51ad01d0-fa90-4e80-a55d-e55e146e2065", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Run the job on a real quantum computer\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", + "res = job.result()\n", + "counts = res[0].data.c.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5465d833-49e0-4779-94a3-0adb18f6aa76", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'1': 1}\n", + "balanced\n" + ] + } + ], + "source": [ + "# Step 4: Visualize and analyze results\n", + "\n", + "## Analysis\n", + "print(counts)\n", + "if \"1\" in counts:\n", + " print(\"balanced\")\n", + "else:\n", + " print(\"constant\")" + ] + }, + { + "cell_type": "markdown", + "id": "82f6da25-0f9c-47b3-aa24-02482f008383", + "metadata": {}, + "source": [ + "## The Deutsch-Jozsa algorithm\n", + "\n", + "Deutsch's algorithm was an important first step in demonstrating how a quantum computer might be more efficient than a classical computer, but it was only a modest improvement: it required just one query, compared to two in the classical case. In 1992, Deutsch and his colleague, Richard Jozsa, extended the original two-qubit algorithm to more qubits. The problem remained the same: determine whether a function is *balanced* or *constant*. But this time, the function goes from $n$ bits to a single bit. Either the function returns 0 and 1 an equal number of times (it's *balanced*) or the function returns always 1 or always 0 (it's *constant*).\n", + "\n", + "Here's a circuit diagram of the algorithm:\n", + "\n", + "![DJ_algo.png](/learning/images/modules/computer-science/deutsch-jozsa/DJ_algo.avif)\n", + "\n", + "This algorithm works in the same way as Deutsch's algorithm: the phase-kickback allows one to read out the state of qubit 0 to determine whether the function is constant or balanced. It's a bit trickier to see than for the two-qubit Deutsch's algorithm case, since the states will include sums over the $n$ qubits, and so working out those states will be left as an optional exercise for you at the end of the module. The algorithm will return a bitstring of all 0's if the function is constant, and a bitstring containing at least one 1 if the function is balanced.\n", + "\n", + "To see how the algorithm works in Qiskit, first, we need to generate our oracle: the random function that is guaranteed to be either constant or balanced. The code below will generate a balanced function 50% of the time, and a constant function 50% of the time. Don't worry if you don't entirely follow the code — it's complicated and not necessary for our understanding of the quantum algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ca2a51c0-3e62-4536-b891-0834e325a3d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "import numpy as np\n", + "\n", + "\n", + "def dj_function(num_qubits):\n", + " \"\"\"\n", + " Create a random Deutsch-Jozsa function.\n", + " \"\"\"\n", + "\n", + " qc_dj = QuantumCircuit(num_qubits + 1)\n", + " if np.random.randint(0, 2):\n", + " # Flip output qubits with 50% chance\n", + " qc_dj.x(num_qubits)\n", + " if np.random.randint(0, 2):\n", + " # return constant circuit with 50% chance.\n", + " return qc_dj\n", + "\n", + " # If the \"if\" statement above was \"TRUE\" then we've returned the constant\n", + " # function and the function is complete. If not, we proceed in creating our\n", + " # balanced function. Everything below is to produce the balanced function:\n", + "\n", + " # select half of all possible states at random:\n", + " on_states = np.random.choice(\n", + " range(2**num_qubits), # numbers to sample from\n", + " 2**num_qubits // 2, # number of samples\n", + " replace=False, # makes sure states are only sampled once\n", + " )\n", + "\n", + " def add_cx(qc_dj, bit_string):\n", + " for qubit, bit in enumerate(reversed(bit_string)):\n", + " if bit == \"1\":\n", + " qc_dj.x(qubit)\n", + " return qc_dj\n", + "\n", + " for state in on_states:\n", + " # qc_dj.barrier() # Barriers are added to help visualize how the functions are created.\n", + " # They can safely be removed.\n", + " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", + " qc_dj.mcx(list(range(num_qubits)), num_qubits)\n", + " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", + "\n", + " # qc_dj.barrier()\n", + "\n", + " return qc_dj\n", + "\n", + "\n", + "n = 3 # number of input qubits\n", + "\n", + "oracle = dj_function(n)\n", + "\n", + "display(oracle.draw(\"mpl\"))" + ] + }, + { + "cell_type": "markdown", + "id": "78096e00-a29b-418c-a620-726675c2a792", + "metadata": {}, + "source": [ + "This is the oracle function, which is either balanced or constant. Can you see by looking at it whether the output on the last qubit depends on the values put in for the first $n$ qubits? If the output for the last qubit depends on the first $n$ qubits, can you tell if that dependent output is balanced or not?\n", + "\n", + "We can tell whether the function is balanced or constant by looking at the above circuit, but remember, for the sake of this problem, we think of this function as a \"black box.\" We can't peek into the box to look at the circuit diagram. Instead, we need to query the box.\n", + "\n", + "To query the box, we use the Deutsch-Jozsa algorithm and determine whether the function is constant or balanced:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fe7ee688-f052-4a7e-bcc7-a14bea57e5c6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "blackbox = oracle.to_gate()\n", + "blackbox.label = \"$U_f$\"\n", + "\n", + "\n", + "qc_dj = QuantumCircuit(n + 1, n)\n", + "qc_dj.x(n)\n", + "qc_dj.h(range(n + 1))\n", + "qc_dj.barrier()\n", + "qc_dj.compose(blackbox, inplace=True)\n", + "qc_dj.barrier()\n", + "qc_dj.h(range(n))\n", + "qc_dj.measure(range(n), range(n))\n", + "\n", + "qc_dj.decompose().decompose()\n", + "\n", + "\n", + "qc_dj.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "bf3aedfa-7454-424e-85cb-c446a8918417", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map the problem\n", + "\n", + "qc_dj = QuantumCircuit(n + 1, n)\n", + "qc_dj.x(n)\n", + "qc_dj.h(range(n + 1))\n", + "qc_dj.barrier()\n", + "qc_dj.compose(oracle, inplace=True)\n", + "qc_dj.barrier()\n", + "qc_dj.h(range(n))\n", + "qc_dj.measure(range(n), range(n))\n", + "\n", + "qc_dj.decompose().decompose()\n", + "\n", + "\n", + "qc_dj.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5497c1aa-c427-419b-b22c-a0c2fa0c4028", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "qc_isa = pm.run(qc_dj)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "974f3db9-1b55-414c-9fe4-d891cf22f78f", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Run the job on a real quantum computer\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", + "res = job.result()\n", + "counts = res[0].data.c.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "39af76b4-f380-4a61-82a4-1e9203c20408", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'110': 1}\n", + "balanced\n" + ] + } + ], + "source": [ + "# Step 4: Visualize and analyze results\n", + "\n", + "## Analysis\n", + "print(counts)\n", + "\n", + "if (\n", + " \"0\" * n in counts\n", + "): # The D-J algorithm returns all zeroes if the function was constant\n", + " print(\"constant\")\n", + "else:\n", + " print(\"balanced\") # anything other than all zeroes means the function is balanced." + ] + }, + { + "cell_type": "markdown", + "id": "c601a252-d1d4-4def-b9a4-d05494d34899", + "metadata": {}, + "source": [ + "Above, the first line of the output is the bitstring of measurement outcomes. The second line outputs whether the bitstring implies that the function was balanced or constant. If the bitstring contained all zeroes, then it was constant; if not, it was balanced. So, with just a single run of the above quantum circuit, we can determine whether the function is constant or balanced!\n", + "\n", + "#### Check your understanding\n", + "\n", + "How many queries would it take a classical computer to determine with 100% certainty whether a function were constant or balanced? Remember, classically, a single query only allows you to apply the function to a single bitstring.\n", + "\n", + "\n", + "\n", + "\n", + "There are $2^n$ possible bitstrings to check, and in the worst case, you would need to test $2^n/2+1$ of these. For example, if the function were constant, and you kept measuring \"1\" as the output of the function, then you couldn't be certain that it was truly constant until you checked over half of the results. Before then, you might have just been very unlucky to keep measuring \"1\" on a balanced function. It's like flipping a coin over and over and it landing heads every time. It's unlikely, but not impossible.\n", + "\n", + "\n", + "\n", + "\n", + "How would your above answer change if you just had to just measure until one outcome (balanced or constant) is more likely than the other? How many queries would it take in this case?\n", + "\n", + "\n", + "\n", + "\n", + "In this case, you could just measure twice. If the two measurements are different, you know the function is balanced. If the two measurements are the same, then it could be balanced, or it could be constant. The probability that it's balanced with this set of measurements is: $\\frac{1}{2}\\frac{2^n /2 - 1}{2^n-1}$. This is less than 1/2, so it's more likely that the function is constant in this case.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "So, the Deutsch-Jozsa algorithm demonstrated an exponential speed-up over a *deterministic* classical algorithm (one that returns the answer with 100% certainty), but no significant speed-up over a *probabilistic* one (one that returns a result that is *likely* to be the correct answer)." + ] + }, + { + "cell_type": "markdown", + "id": "37d8d1b5-1593-480e-afb9-cdae1debb8ea", + "metadata": {}, + "source": [ + "### The Bernstein - Vazirani problem\n", + "\n", + "In 1997, Ethan Bernstein and Umesh Vazirani used the Deutsch-Jozsa algorithm to solve a more specific, restricted problem compared to the Deutsch-Jozsa problem. Rather than simply try to distinguish between two different classes of functions, as in the D-J case, Bernstein and Vazirani used the Deutsch-Jozsa algorithm to actually learn a string encoded in a function. Here's the problem:\n", + "\n", + "The function $f:\\{0,1\\}^n \\rightarrow \\{0,1\\}$ still takes an $n$-bit string and outputs a single bit. But now, instead of promising that the function is balanced or constant, we're now promised that the function is the dot product between the input string $x$ and some secret $n$-bit string $s$, modulo 2. (This dot product modulo 2 is called the \"binary dot product.\") The problem is to figure out what the secret, $n$-bit string is.\n", + "\n", + "Written another way, we're given a black-box function $f: {0,1}^n \\rightarrow {0,1}$ that satisfies $f(x) = s \\cdot x$ for some string $s$, and we want to learn the string $s$.\n", + "\n", + "\n", + "Let's take a look at how the D-J algorithm solves this problem:\n", + "\n", + "1. First, a Hadamard gate is applied to the $n$ input qubits, and a NOT gate plus a Hadamard is applied to the output qubit, making the state:\n", + "\n", + "$$\n", + "|\\Psi\\rangle = |-\\rangle_{n} \\otimes |+\\rangle_{n-1} \\otimes |+\\rangle_{n-2} \\otimes ... \\otimes |+\\rangle_0\n", + "$$\n", + "\n", + " The state of qubits 1 through $n$ can be written more simply as a sum over all $2^n$ the $n$-qubit basis states $|00...00\\rangle, |00...01\\rangle, |000...11\\rangle, ..., |111...11\\rangle$. We call the set of these basis states $\\Sigma^n$. (See [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/deutsch-jozsa-algorithm) for more details.)\n", + "\n", + "$$\n", + "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{x \\in \\Sigma^n}{|x\\rangle}\n", + "$$\n", + "\n", + "2. Next, the $U_f$ gate is applied to the qubits. This gate will take the first n qubits as input (which are now in an equal superposition of all possible n-bit strings) and applies the function $f(x)=s \\cdot x$ to the output qubit, so that this qubit is now in the state: $ |- \\oplus f(x)\\rangle$. Thanks to the phase kickback mechanism, the state of this qubit remains unchanged, but some of the terms in the input qubit state pick up a minus sign:\n", + "\n", + "$$\n", + "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{x \\in \\Sigma^n}{(-1)^{f(x)}|x\\rangle}\n", + "$$\n", + "\n", + "3. Now, the next set of Hadamards are applied to qubits 0 through $n-1$. Keeping track of the minus signs in this case can be tricky. It's helpful to know that applying a layer of Hadamards to $n$ qubits in a standard basis state $|x\\rangle$ can be written as:\n", + "\n", + "$$\n", + "H^{\\otimes n} |x\\rangle = \\frac{1}{\\sqrt{2^n}}\\sum\\limits_{y \\in \\Sigma^n}{(-1)^{x \\cdot y}|y\\rangle}\n", + "$$\n", + "\n", + "So the state becomes:\n", + "\n", + "$$\n", + "|\\Psi\\rangle = |-\\rangle \\otimes \\frac{1}{2^n}\\sum\\limits_{x \\in \\Sigma^n}\\sum\\limits_{y \\in \\Sigma^n}{(-1)^{(s \\cdot x) + (x \\cdot y)}|y\\rangle}\n", + "$$\n", + "\n", + "4. Next step is to measure the first $n$ bits. But what will we measure? It turns out that the state above simplifies to: $|\\Psi\\rangle = |-\\rangle \\otimes |s\\rangle$, but that's far from obvious. If you'd like to follow through the math, see John Watrous' [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/quantum-query-algorithms/deutsch-jozsa-algorithm#the-bernstein-vazirani-problem) course. The point is, though, that the phase kickback mechanism leads to the input qubits being in the state $|s\\rangle$. So, to find out what the secret string $s$ was, you simply need to measure the qubits!\n", + "\n", + "\n", + "#### Check your understanding\n", + "\n", + "Verify that the state from Step 3 above is indeed the state $|s\\rangle$ for the special case of $n=1$.\n", + "\n", + "\n", + "\n", + "\n", + "When you explicitly write out the two summations, you should get a state with four terms (let's omit the output state $|-\\rangle$ for this):\n", + "\n", + "$$\n", + "|\\Psi\\rangle = \\frac{1}{2}[|0\\rangle + (-1)^s |0\\rangle + |1\\rangle + (-1)^{(s+1)} |1\\rangle]\n", + "$$\n", + "\n", + "If $s=0$, then the first two terms add constructively and the last two terms cancel, leaving us with $|\\Psi\\rangle = |0\\rangle$. If $s=1$, then the last two terms add constructively and the first two terms cancel, leaving us with $|\\Psi\\rangle = |1\\rangle$. So, in either case, $|\\Psi\\rangle = |s\\rangle$. Hopefully this simplest case gives you a sense for how the general case with $n$ qubits works: all terms that are not $|s\\rangle$ interfere away, leaving just the state $|s\\rangle$.\n", + "\n", + "\n", + "\n", + "\n", + "How can the same algorithm solve both the Bernstein-Vazirani and Deutsch-Jozsa problems? To make sense of this, think about Bernstein-Vazirani functions, which are of the form $f(x) = s \\cdot x$. Are these functions also Deutsch-Jozsa functions? That is, determine whether functions of this form satisfy the Deutsch-Jozsa problem promise: that they're either *constant* or *balanced*. How does this help us understand how the same algorithm solves two different problems?\n", + "\n", + "\n", + "\n", + "\n", + "Every Bernstein-Vazirani function of the form $f(x) = s \\cdot x$ also satisfies the Deutsch-Jozsa problem promise: if s=00...00, then the function is constant (always returns 0 for every string x). If s is any other string, then the function is balanced. So, applying the Deutsch-Jozsa algorithm to one of these functions simultaneously solves both problems! It returns the string, and if that string is 00...00 then we know it's constant; if there's at least one \"1\" in the string, we know it's balanced.\n", + "\n", + "\n", + "\n", + "\n", + "We can also verify that this algorithm successfully solves the Bernstein-Vazirani problem by testing it experimentally. First, we create the B-V function that lives inside the black box:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "45449a26-0bd0-4244-87be-3309937955b9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Step 1: Map the problem\n", + "\n", + "\n", + "def bv_function(s):\n", + " \"\"\"\n", + " Create a Bernstein-Vazirani function from a string of 1s and 0s.\n", + " \"\"\"\n", + " qc = QuantumCircuit(len(s) + 1)\n", + " for index, bit in enumerate(reversed(s)):\n", + " if bit == \"1\":\n", + " qc.cx(index, len(s))\n", + " return qc\n", + "\n", + "\n", + "display(bv_function(\"1000\").draw(\"mpl\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0cf6f2bc-3b5e-46d2-ab82-1a190e77c42b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "string = \"1000\" # secret string that we'll pretend we don't know or have access to\n", + "n = len(string)\n", + "\n", + "qc = QuantumCircuit(n + 1, n)\n", + "qc.x(n)\n", + "qc.h(range(n + 1))\n", + "qc.barrier()\n", + "# qc.compose(oracle, inplace = True)\n", + "qc.compose(bv_function(string), inplace=True)\n", + "qc.barrier()\n", + "qc.h(range(n))\n", + "qc.measure(range(n), range(n))\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "5d225a6e-e3d0-4c08-8aeb-f03337bfffc4", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "qc_isa = pm.run(qc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fef6a65-227a-4f27-af3e-348513e1cd33", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Run the job on a real quantum computer\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "# job = sampler_sim.run([qc_isa],shots=1) # uncomment this line to run on simulator instead\n", + "res = job.result()\n", + "counts = res[0].data.c.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "ec576787-d9ba-4406-b799-9c0de21a8088", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'0000': 1}\n" + ] + } + ], + "source": [ + "# Step 4: Visualize and analyze results\n", + "\n", + "## Analysis\n", + "print(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "46ff0418-570d-4e78-be36-f403aeccc392", + "metadata": {}, + "source": [ + "So, with just a single query, the Deutsch-Jozsa algorithm will return the string $s$ used in the function: $f(x)=x \\cdot s$ when we apply it to the Bernstein-Vazirani problem. With a classical algorithm, one would need $n$ queries to solve the same problem.\n", + "\n", + "## Conclusion\n", + "\n", + "We hope that by examining these simple examples, we've given you a better intuition for how quantum computers are able to harness superposition, entanglement, and interference to achieve their power over classical computers.\n", + "\n", + "The Deutsch-Jozsa algorithm has huge historical importance because it was the first to demonstrate any speedup over a classical algorithm, but it was only a polynomial speedup. The Deutsch-Jozsa algorithm is just the beginning of the story.\n", + "\n", + "After they used the algorithm to solve their problem, Bernstein and Vazirani used this as the basis for a more complicated, recursive problem called the *recursive Fourier sampling problem*. Their solution offered a super-polynomial speedup over classical algorithms. And even before Bernstein and Vazirani, Peter Shor had already come up with his famous algorithm that enabled quantum computers to factor large numbers exponentially faster than any classical algorithm could. These results, collectively showed the exciting promise of future quantum computer, and spurred physicists and engineers to make this future a reality." + ] + }, + { + "cell_type": "markdown", + "id": "c76273ac-ad3c-4c82-94e8-213e887dc7b7", + "metadata": {}, + "source": [ + "## Questions\n", + "\n", + "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", + "\n", + "### Critical concepts\n", + "- the Deutsch and Deutsch-Jozsa algorithms use quantum parallelism combined with interference to find an answer to a problem faster than a classical computer can.\n", + "- the phase kickback mechanism is a counterintuitive quantum phenomena that transfers operations on one qubit to the phase of another qubit. The Deutsch and Deutsch-Jozsa algorithms utilize this mechanism.\n", + "- The Deutsch-Jozsa algorithm offers a polynomial speedup over any deterministic classical algorithm.\n", + "- The Deutsch-Jozsa algorithm can be applied to a different problem, called the Bernstein-Vazirani problem, to find a hidden string encoded in a function.\n", + "\n", + "### True/false\n", + "1. T/F Deutsch's algorithm is a special case of the Deutsch-Jozsa algorithm where the input is a single qubit.\n", + "2. T/F The Deutsch and Deutsch-Jozsa algorithms use quantum superposition and interference to achieve their efficiency.\n", + "4. T/F The Deutsch-Jozsa algorithm requires multiple function evaluations to determine if a function is constant or balanced.\n", + "5. T/F The \"Bernstein-Vazirani algorithm\" is actually the same as the Deutsch-Jozsa algorithm, applied to a different problem.\n", + "6. T/F The Bernstein-Vazirani algorithm can find multiple secret strings simultaneously.\n", + "\n", + "### Short answer\n", + "\n", + "1. How long would it take a classical algorithm to solve the Deutsch-Jozsa problem in the worst case?\n", + "\n", + "2. How long would it take a classical algorithm to solve the Bernstein-Vazirani problem? What speedup does the DJ algorithm offer in this case?\n", + "\n", + "3. Describe the phase-kickback mechanism and how it works to solve the Deutsch-Jozsa and Bernstein-Vazirani problems.\n", + "\n", + "### Challenge problem\n", + "1. The Deutsch-Jozsa algorithm: Recall that you had a question above asking you to work out the intermediate qubit states $\\pi_1$, and $\\pi_2$ of the Deutsch's algorithm. Do the same for the intermediate $n+1$-qubit states $\\pi_1$, and $\\pi_2$ of the Deutsch-Jozsa algorithm, for the specific case that $n=2$. Then, verify that $\\pi_3 = |-\\rangle \\otimes \\sum\\limits_{x_0...x_n}(-1)^{f(x_0...x_n)}|x_0 ... x_n\\rangle$, again, for the specific case that $n=2$." + ] + } + ], + "metadata": { + "in_page_toc_max_heading_level": 2, + "in_page_toc_min_heading_level": 2, + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/modules/computer-science/grovers.ipynb b/learning/modules/computer-science/grovers.ipynb index 747f1b6340b..fc4baa9e4c4 100644 --- a/learning/modules/computer-science/grovers.ipynb +++ b/learning/modules/computer-science/grovers.ipynb @@ -1,1504 +1,1508 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e98de65a", - "metadata": {}, - "source": [ - "---\n", - "title: Grover's algorithm\n", - "description: Learn how Grover's algorithm uses quantum computing to solve unstructured search problems.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore bitstr */}" - ] - }, - { - "cell_type": "markdown", - "id": "9857bace", - "metadata": {}, - "source": [ - "# Grover's algorithm" - ] - }, - { - "cell_type": "markdown", - "id": "5c6854a5", - "metadata": {}, - "source": [ - "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", - "- `qiskit` v2.1.0 or newer\n", - "- `qiskit-ibm-runtime` v0.40.1 or newer\n", - "- `qiskit-aer` v0.17.0 or newer\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", - "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "This module was tested and used 12 seconds of QPU time. This is a good-faith estimate; your actual usage may vary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e16858b0", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "e57d1e6b", - "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "**Grover's algorithm** is a foundational quantum algorithm that addresses the *unstructured search problem*: given a set of $N$ items and a way to check if any given item is the one you're looking for, how quickly can you find the desired item? In classical computing, if the data is unsorted and there is no structure to exploit, the best approach is to check each item one by one, leading to a query complexity of $O(N)$ — on average, you'll need to check about half the items before finding the target.\n", - "\n", - "\n", - "![A diagram of classical unstructured search.](/learning/images/modules/computer-science/grovers/classical-uss.avif)\n", - "\n", - "Grover's algorithm, introduced by Lov Grover in 1996, demonstrates how a quantum computer can solve this problem much more efficiently, requiring only $O(\\sqrt{N})$ steps to find the marked item with high probability. This represents a *quadratic speedup* over classical methods, which is significant for large datasets.\n", - "\n", - "The algorithm operates in the following context:\n", - "- **Problem setup:** You have a function $f(x)$ that returns 1 if $x$ is the item you want, and 0 otherwise. This function is often called an *oracle* or *black box*, since you can only learn about the data by querying $f(x)$.\n", - "- **Usefulness of quantum:** While classical algorithms for this problem require, on average, $N/2$ queries, Grover's algorithm can find the solution in roughly $\\pi\\sqrt{N}/4$ queries, which is much faster for large $N$.\n", - "- **How it works (at a high level):**\n", - " - The quantum computer first creates a *superposition* of all possible states, representing all possible items at once.\n", - " - It then repeatedly applies a sequence of quantum operations (the Grover iteration) that amplifies the probability of the correct answer and diminishes the others.\n", - " - After enough iterations, measuring the quantum state yields the correct answer with high probability.\n", - "\n", - "Here is a very basic diagram of Grover's algorithm that skips over a lot of nuance. For a more detailed diagram, see [this paper](https://arxiv.org/pdf/2211.04543).\n", - "\n", - "![A high-level diagram of the steps in implementing Grover's algorithm.](/learning/images/modules/computer-science/grovers/quantum-uss2.avif)\n", - "\n", - "A few things to note about Grover's algorithm:\n", - "- It is optimal for unstructured search: no quantum algorithm can solve the problem with fewer than $O(\\sqrt{N})$ queries.\n", - "- It provides only a quadratic, not exponential, speedup — unlike some other quantum algorithms (for example, Shor's algorithm for factoring).\n", - "- It has practical implications, such as potentially speeding up brute-force attacks on cryptographic systems, though the speedup is not enough to break most modern encryption by itself.\n", - "\n", - "For undergraduate students familiar with basic computing concepts and query models, Grover's algorithm offers a clear illustration of how quantum computing can outperform classical approaches for certain problems, even when the improvement is \"only\" quadratic. It also serves as a gateway to understanding more advanced quantum algorithms and the broader potential of quantum computing.\n", - "\n", - "Amplitude amplification is a general purpose quantum algorithm, or subroutine, that can be used to obtain a quadratic speedup over a handful of classical algorithms. [Grover’s algorithm](https://arxiv.org/abs/quant-ph/9605043) was the first to demonstrate this speedup on unstructured search problems. Formulating a Grover's search problem requires an oracle function that marks one or more computational basis states as the states we are interested in finding, and an amplification circuit that increases the amplitude of marked states, consequently suppressing the remaining states.\n", - "\n", - "Here, we demonstrate how to construct Grover oracles and use the `GroverOperator` from the Qiskit circuit library to easily set up a Grover's search instance. The runtime `Sampler` primitive allows seamless execution of Grover circuits." - ] - }, - { - "cell_type": "markdown", - "id": "4f900ae6", - "metadata": {}, - "source": [ - "## Theory\n", - "Suppose there exists a function $f$ that maps binary strings to a single binary variable, meaning\n", - "$$\n", - "f: \\Sigma^n \\rightarrow \\Sigma\n", - "$$\n", - "One example defined on $\\Sigma^6$ is\n", - "$$\n", - "f(x)= \\begin{cases} 1 \\qquad \\text{if }x=\\{010101\\}\\\\\n", - "0 \\qquad \\text{otherwise }\n", - "\\end{cases}\n", - "$$\n", - "Another example defined on $\\Sigma^{2n}$ is\n", - "$$\n", - "f(x)= \\begin{cases} 1 \\qquad \\text{if equal numbers of 1's and 0's in string}\\\\\n", - "0 \\qquad \\text{otherwise }\n", - "\\end{cases}\n", - "$$\n", - "You are tasked with finding quantum states corresponding to those arguments $x$ of $f(x)$ that are mapped to 1. In other words, find all $\\{x_1\\}\\in \\Sigma^n$ such that $f(x_1)=1$ (or if there is no solution, report that). We would refer to non-solutions as $x_0$. Of course, we will do this on a quantum computer, using quantum states, so it is useful to express these binary strings as states:\n", - "$$\n", - "\\{|x_1\\rangle\\} \\in |\\Sigma^n\\rangle\n", - "$$\n", - "Using the quantum state (Dirac) notation, we are looking for one or more special states $\\{|x_1\\rangle\\}$ in a set of $N=2^n$ possible states, where $n$ is the number of qubits, and with non-solutions denoted $\\{|x_0\\rangle\\}.$\n", - "\n", - "We can think of the function $f$ as being provided by an oracle: a black-box that we can query to determine its effect on a state $|x\\rangle.$ In practice, we will often know the function, but it may be very complicated to implement, meaning that reducing the number of queries or applications of $f$ could be important. Alternatively, we can imagine a paradigm in which one person is querying an oracle controlled by another person, such that we don't know the oracle function, we only know its action on particular states from querying.\n", - "\n", - "This is an \"unstructured search problem, in that there is nothing special about $f$ that aids us in our search. The outputs are not sorted nor are solutions known to cluster, and so on. Consider use old, paper phone books as an analogy. This unstructured search would be like scanning through it looking for a certain __number__, and not like looking through an alphabetized list of names.\n", - "\n", - "In the case where a single solution is sought, classically, this requires a number of queries that is linear in $N$. Clearly you might find a solution on the first try, or you might find no solutions in the first $N-1$ guesses, such that you need to query the $N^{th}$ input to see if there is any solution at all. Since the functions have no exploitable structure, you will require $N/2$ guesses on average. Grover's algorithm requires a number of queries or computations of $f$ that scales like $\\sqrt{N}.$" - ] - }, - { - "cell_type": "markdown", - "id": "a1a51eb6", - "metadata": {}, - "source": [ - "### Sketch of circuits in Grover's algorithm\n", - "\n", - "A full mathematical walkthrough of Grover's algorithm can be found, for example, in [Fundamentals of quantum algorithms](/learning/courses/fundamentals-of-quantum-algorithms), a course by John Watrous on IBM Quantum Learning. A condensed treatment is provided in an appendix at the end of this module. But for now, we will only review the overall structure of the quantum circuit that implements Grover's algorithm.\n", - "\n", - "Grover's algorithm can be broken down into the following stages:\n", - "* Preparation of an initial superposition (applying Hadamard gates to all qubits)\n", - "* \"Marking\" the target state(s) with a phase flip\n", - "* A \"diffusion\" stage in which Hadamard gates and a phase flip are applied to __all__ qubits.\n", - "* Possible repetitions of the marking and diffusion stages to maximize the probability of measuring the target state\n", - "* Measurement\n", - "\n", - "![A quantum circuit diagram showing the basic setup of Grover's algorithm. This example uses four qubits.](/learning/images/modules/computer-science/grovers/grover-circuit-diagram-2.avif)" - ] - }, - { - "cell_type": "markdown", - "id": "15b9e48c", - "metadata": {}, - "source": [ - "Often, the marking gate $Z_f$ and the diffusion layers consisting of $H,$ $Z_{\\text{OR}},$ and $H$ are collectively referred to as the \"Grover operator\". In this diagram, only a single repetition of the Grover operator is shown.\n", - "\n", - "Hadamard gates $H$ are well-known and used widely throughout quantum computing. The Hadamard gate creates superposition states. Specifically it is defined by\n", - "$$\n", - "H|0\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)\\\\\n", - "H|1\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)\n", - "$$\n", - "Its operation on any other state is defined through linearity.\n", - "In particularly, a layer of Hadamard gates allows us to go from the initial state with all qubits in $|0\\rangle$ (denoted $|0\\rangle^{\\otimes n}$) to a state where each qubit has some probability of being measured in either $|0\\rangle$ or $|1\\rangle;$ this lets us probe the space of all possible states differently from in classical computing.\n", - "\n", - "An important corollary property of the Hadamard gate is that acting a second time can undo such superposition states:\n", - "$$\n", - "H\\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)=|0\\rangle\\\\\n", - "H\\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)=|1\\rangle\n", - "$$\n", - "\n", - "This will be important in just a moment.\n", - "\n", - "#### Check your understanding\n", - "\n", - "Starting from the definition of the Hadamard gate, demonstrate that a second application of the Hadamard gate undoes such superpositions as claimed above.\n", - "\n", - "\n", - "\n", - "\n", - "When we apply X to the $|+\\rangle$ state, we get the value and +1 and to the state $|-\\rangle$ we get -1, so if we had a 50-50 distribution, we would get an expectation value of 0.\n", - "\n", - "\n", - "\n", - "\n", - "The $Z_\\text{OR}$ gate is less common, and is defined according to\n", - "$$\n", - "\\text{Z}_\\text{OR}|x\\rangle = \\begin{cases}\n", - "|x\\rangle & \\text{if } x = 0^n \\\\\n", - " -|x\\rangle & \\text{if } x \\neq 0^n\n", - "\\end{cases}\n", - "\\qquad \\forall x \\in \\Sigma^n\n", - "$$\n", - "\n", - "Finally, the $Z_f$ gate is defined by\n", - "$$\n", - "Z_f:|x\\rangle \\rightarrow (-1)^{f(x)}|x\\rangle \\qquad \\forall x \\in \\Sigma^n\n", - "$$\n", - "\n", - "Note the effect of this is that $Z_f$ flips the sign on a target state for which $f(x) = 1$ and leaves other states unaffected.\n", - "\n", - "At a very high, abstract level you can think about the steps in the circuit in the following ways:\n", - "* First Hadamard layer: puts the qubits into a superposition of all possible states.\n", - "* $Z_f$: mark the target state(s) by adding a \"-\" sign in front. This doesn't immediately change measurement probabilities, but it changes how the target state will behave in subsequent steps.\n", - "* Another Hadamard layer: The \"-\" sign introduced in the previous step will change the relative sign between some terms. Since Hadamard gates turn one mixture of computational states $(|0\\rangle+|1\\rangle)/\\sqrt{2}$ into one computational state, $|0\\rangle,$ and they turn $(|0\\rangle-|1\\rangle)/\\sqrt{2}$ into $|1\\rangle$ this relative sign difference can now begin to play a role in what states are measured.\n", - "* One final layer of Hadamard gates is applied, and then measurements are made.\n", - "We will see in more detail how this works in the next section." - ] - }, - { - "cell_type": "markdown", - "id": "bf41c02c", - "metadata": {}, - "source": [ - "### Example\n", - "To better understand how Grover's algorithm works, let us work through a small, two-qubit example. This may be considered optional for those not focused on quantum mechanics and Dirac notation. But for those who hope to work substantially with quantum computers, this is highly recommended.\n", - "\n", - "Here is the circuit diagram with the quantum states labeled at various positions throughout. Note that with only two qubits, there are only four possible states that could be measured under any circumstances: $|00\\rangle$, $|01\\rangle$, $|10\\rangle$, and $|11\\rangle$.\n", - "\n", - "![A diagram of a quantum circuit that implements Grover's algorithm on two qubits.](/learning/images/modules/computer-science/grovers/grover-circuit-diagram-2-q-ex.avif)\n", - "\n", - "Let us suppose that the oracle ($Z_f$, unknown to us) marks the state $|01\\rangle$. We will work through the actions of each set of quantum gates, including the oracle, and see what distribution of possible states come out at the time of measurement.\n", - "At the very beginning, we have\n", - "$$\n", - "|\\psi_0\\rangle = |00\\rangle\n", - "$$\n", - "Using the definition of Hadamard gates, we have\n", - "$$\n", - "|\\psi_1\\rangle = \\frac{1}{2}\\left(|0\\rangle+|1\\rangle\\right)\\left(|0\\rangle+|1\\rangle\\right)=\\frac{1}{2}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\n", - "$$\n", - "Now the oracle marks the target state:\n", - "$$\n", - "|\\psi_2\\rangle = \\frac{1}{2}\\left(|00\\rangle-|01\\rangle+|10\\rangle+|11\\rangle\\right)\n", - "$$\n", - "Note that in this state, all four possible outcomes have the same probability of being measured. They all have a weight of magnitude $1/2,$ meaning they each have a $|1/2|^2=1/4$ chance of being measured. So while the state $|01\\rangle$ is marked through the \"-\" phase, this has not yet resulted in any increased probability of measuring that state. We continue by applying the next layer of Hadamard gates.\n", - "$$\n", - "\\begin{aligned}\n", - "|\\psi_3\\rangle = &\\frac{1}{4}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\\\\\n", - "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\\\\\n", - "+&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle-|10\\rangle-|11\\rangle\\right)\\\\\n", - "+&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", - "\\end{aligned}\n", - "$$\n", - "Combining like terms, we find\n", - "$$\n", - "|\\psi_3\\rangle = \\frac{1}{2}\\left(|00\\rangle+|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", - "$$\n", - "Now $Z_{\\text{OR}}$ flips the sign on all states but $|00\\rangle$:\n", - "$$\n", - "|\\psi_4\\rangle = \\frac{1}{2}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\n", - "$$\n", - "And finally, we apply the last layer of Hadamard gates:\n", - "$$\n", - "\\begin{aligned}\n", - "|\\psi_5\\rangle =&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\\\\\n", - "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\\\\\n", - "+&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle-|10\\rangle-|11\\rangle\\right)\\\\\n", - "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", - "\\end{aligned}\n", - "$$\n", - "It is worth working through the combining of these terms to convince yourself that the result is indeed:\n", - "$$\n", - "|\\psi_5\\rangle =|01\\rangle\n", - "$$\n", - "That is, the probability of measuring $|01\\rangle$ is 100% (in the absence of noise and errors) and the probability for measuring any other state is zero.\n", - "\n", - "This two-qubit example was an especially clean case; Grover's algorithm will not always work out to yield a 100% chance of measuring the target state. Rather, it will amplify the probability of measuring the target state. Also, the Grover operator may need to be repeated more than once.\n", - "\n", - "In the next section, we will put this algorithm into practice using real IBM® quantum computers." - ] - }, - { - "cell_type": "markdown", - "id": "geo_picture_01", - "metadata": {}, - "source": [ - "### The geometric picture\n", - "\n", - "The two-qubit example above showed how the algebra works out for a small case, but there is a much more intuitive way to understand Grover's algorithm: as a sequence of geometric reflections in a two-dimensional plane. Below we describe this picture. You can also see John Watrous's course [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/analysis) for more details.\n", - "\n", +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e98de65a", + "metadata": {}, + "source": [ + "---\n", + "title: Grover's algorithm\n", + "description: Learn how Grover's algorithm uses quantum computing to solve unstructured search problems.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore bitstr */}" + ] + }, + { + "cell_type": "markdown", + "id": "9857bace", + "metadata": {}, + "source": [ + "# Grover's algorithm" + ] + }, + { + "cell_type": "markdown", + "id": "5c6854a5", + "metadata": {}, + "source": [ + "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", + "- `qiskit` v2.1.0 or newer\n", + "- `qiskit-ibm-runtime` v0.40.1 or newer\n", + "- `qiskit-aer` v0.17.0 or newer\n", + "- `qiskit.visualization`\n", + "- `numpy`\n", + "- `pylatexenc`\n", + "\n", + "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", + "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", + "\n", + "This module was tested and used 12 seconds of QPU time. This is a good-faith estimate; your actual usage may vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e16858b0", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and modify this line as needed to install dependencies\n", + "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" + ] + }, + { + "cell_type": "markdown", + "id": "e57d1e6b", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "**Grover's algorithm** is a foundational quantum algorithm that addresses the *unstructured search problem*: given a set of $N$ items and a way to check if any given item is the one you're looking for, how quickly can you find the desired item? In classical computing, if the data is unsorted and there is no structure to exploit, the best approach is to check each item one by one, leading to a query complexity of $O(N)$ — on average, you'll need to check about half the items before finding the target.\n", + "\n", + "\n", + "![A diagram of classical unstructured search.](/learning/images/modules/computer-science/grovers/classical-uss.avif)\n", + "\n", + "Grover's algorithm, introduced by Lov Grover in 1996, demonstrates how a quantum computer can solve this problem much more efficiently, requiring only $O(\\sqrt{N})$ steps to find the marked item with high probability. This represents a *quadratic speedup* over classical methods, which is significant for large datasets.\n", + "\n", + "The algorithm operates in the following context:\n", + "- **Problem setup:** You have a function $f(x)$ that returns 1 if $x$ is the item you want, and 0 otherwise. This function is often called an *oracle* or *black box*, since you can only learn about the data by querying $f(x)$.\n", + "- **Usefulness of quantum:** While classical algorithms for this problem require, on average, $N/2$ queries, Grover's algorithm can find the solution in roughly $\\pi\\sqrt{N}/4$ queries, which is much faster for large $N$.\n", + "- **How it works (at a high level):**\n", + " - The quantum computer first creates a *superposition* of all possible states, representing all possible items at once.\n", + " - It then repeatedly applies a sequence of quantum operations (the Grover iteration) that amplifies the probability of the correct answer and diminishes the others.\n", + " - After enough iterations, measuring the quantum state yields the correct answer with high probability.\n", + "\n", + "Here is a very basic diagram of Grover's algorithm that skips over a lot of nuance. For a more detailed diagram, see [this paper](https://arxiv.org/pdf/2211.04543).\n", + "\n", + "![A high-level diagram of the steps in implementing Grover's algorithm.](/learning/images/modules/computer-science/grovers/quantum-uss2.avif)\n", + "\n", + "A few things to note about Grover's algorithm:\n", + "- It is optimal for unstructured search: no quantum algorithm can solve the problem with fewer than $O(\\sqrt{N})$ queries.\n", + "- It provides only a quadratic, not exponential, speedup — unlike some other quantum algorithms (for example, Shor's algorithm for factoring).\n", + "- It has practical implications, such as potentially speeding up brute-force attacks on cryptographic systems, though the speedup is not enough to break most modern encryption by itself.\n", + "\n", + "For undergraduate students familiar with basic computing concepts and query models, Grover's algorithm offers a clear illustration of how quantum computing can outperform classical approaches for certain problems, even when the improvement is \"only\" quadratic. It also serves as a gateway to understanding more advanced quantum algorithms and the broader potential of quantum computing.\n", + "\n", + "Amplitude amplification is a general purpose quantum algorithm, or subroutine, that can be used to obtain a quadratic speedup over a handful of classical algorithms. [Grover’s algorithm](https://arxiv.org/abs/quant-ph/9605043) was the first to demonstrate this speedup on unstructured search problems. Formulating a Grover's search problem requires an oracle function that marks one or more computational basis states as the states we are interested in finding, and an amplification circuit that increases the amplitude of marked states, consequently suppressing the remaining states.\n", + "\n", + "Here, we demonstrate how to construct Grover oracles and use the `GroverOperator` from the Qiskit circuit library to easily set up a Grover's search instance. The runtime `Sampler` primitive allows seamless execution of Grover circuits." + ] + }, + { + "cell_type": "markdown", + "id": "4f900ae6", + "metadata": {}, + "source": [ + "## Theory\n", + "Suppose there exists a function $f$ that maps binary strings to a single binary variable, meaning\n", + "$$\n", + "f: \\Sigma^n \\rightarrow \\Sigma\n", + "$$\n", + "One example defined on $\\Sigma^6$ is\n", + "$$\n", + "f(x)= \\begin{cases} 1 \\qquad \\text{if }x=\\{010101\\}\\\\\n", + "0 \\qquad \\text{otherwise }\n", + "\\end{cases}\n", + "$$\n", + "Another example defined on $\\Sigma^{2n}$ is\n", + "$$\n", + "f(x)= \\begin{cases} 1 \\qquad \\text{if equal numbers of 1's and 0's in string}\\\\\n", + "0 \\qquad \\text{otherwise }\n", + "\\end{cases}\n", + "$$\n", + "You are tasked with finding quantum states corresponding to those arguments $x$ of $f(x)$ that are mapped to 1. In other words, find all $\\{x_1\\}\\in \\Sigma^n$ such that $f(x_1)=1$ (or if there is no solution, report that). We would refer to non-solutions as $x_0$. Of course, we will do this on a quantum computer, using quantum states, so it is useful to express these binary strings as states:\n", + "$$\n", + "\\{|x_1\\rangle\\} \\in |\\Sigma^n\\rangle\n", + "$$\n", + "Using the quantum state (Dirac) notation, we are looking for one or more special states $\\{|x_1\\rangle\\}$ in a set of $N=2^n$ possible states, where $n$ is the number of qubits, and with non-solutions denoted $\\{|x_0\\rangle\\}.$\n", + "\n", + "We can think of the function $f$ as being provided by an oracle: a black-box that we can query to determine its effect on a state $|x\\rangle.$ In practice, we will often know the function, but it may be very complicated to implement, meaning that reducing the number of queries or applications of $f$ could be important. Alternatively, we can imagine a paradigm in which one person is querying an oracle controlled by another person, such that we don't know the oracle function, we only know its action on particular states from querying.\n", + "\n", + "This is an \"unstructured search problem, in that there is nothing special about $f$ that aids us in our search. The outputs are not sorted nor are solutions known to cluster, and so on. Consider use old, paper phone books as an analogy. This unstructured search would be like scanning through it looking for a certain __number__, and not like looking through an alphabetized list of names.\n", + "\n", + "In the case where a single solution is sought, classically, this requires a number of queries that is linear in $N$. Clearly you might find a solution on the first try, or you might find no solutions in the first $N-1$ guesses, such that you need to query the $N^{th}$ input to see if there is any solution at all. Since the functions have no exploitable structure, you will require $N/2$ guesses on average. Grover's algorithm requires a number of queries or computations of $f$ that scales like $\\sqrt{N}.$" + ] + }, + { + "cell_type": "markdown", + "id": "a1a51eb6", + "metadata": {}, + "source": [ + "### Sketch of circuits in Grover's algorithm\n", + "\n", + "A full mathematical walkthrough of Grover's algorithm can be found, for example, in [Fundamentals of quantum algorithms](/learning/courses/fundamentals-of-quantum-algorithms), a course by John Watrous on IBM Quantum Learning. A condensed treatment is provided in an appendix at the end of this module. But for now, we will only review the overall structure of the quantum circuit that implements Grover's algorithm.\n", + "\n", + "Grover's algorithm can be broken down into the following stages:\n", + "* Preparation of an initial superposition (applying Hadamard gates to all qubits)\n", + "* \"Marking\" the target state(s) with a phase flip\n", + "* A \"diffusion\" stage in which Hadamard gates and a phase flip are applied to __all__ qubits.\n", + "* Possible repetitions of the marking and diffusion stages to maximize the probability of measuring the target state\n", + "* Measurement\n", + "\n", + "![A quantum circuit diagram showing the basic setup of Grover's algorithm. This example uses four qubits.](/learning/images/modules/computer-science/grovers/grover-circuit-diagram-2.avif)" + ] + }, + { + "cell_type": "markdown", + "id": "15b9e48c", + "metadata": {}, + "source": [ + "Often, the marking gate $Z_f$ and the diffusion layers consisting of $H,$ $Z_{\\text{OR}},$ and $H$ are collectively referred to as the \"Grover operator\". In this diagram, only a single repetition of the Grover operator is shown.\n", + "\n", + "Hadamard gates $H$ are well-known and used widely throughout quantum computing. The Hadamard gate creates superposition states. Specifically it is defined by\n", + "$$\n", + "H|0\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)\\\\\n", + "H|1\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)\n", + "$$\n", + "Its operation on any other state is defined through linearity.\n", + "In particularly, a layer of Hadamard gates allows us to go from the initial state with all qubits in $|0\\rangle$ (denoted $|0\\rangle^{\\otimes n}$) to a state where each qubit has some probability of being measured in either $|0\\rangle$ or $|1\\rangle;$ this lets us probe the space of all possible states differently from in classical computing.\n", + "\n", + "An important corollary property of the Hadamard gate is that acting a second time can undo such superposition states:\n", + "$$\n", + "H\\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)=|0\\rangle\\\\\n", + "H\\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)=|1\\rangle\n", + "$$\n", + "\n", + "This will be important in just a moment.\n", + "\n", + "#### Check your understanding\n", + "\n", + "Starting from the definition of the Hadamard gate, demonstrate that a second application of the Hadamard gate undoes such superpositions as claimed above.\n", + "\n", + "\n", + "\n", + "\n", + "When we apply X to the $|+\\rangle$ state, we get the value and +1 and to the state $|-\\rangle$ we get -1, so if we had a 50-50 distribution, we would get an expectation value of 0.\n", + "\n", + "\n", + "\n", + "\n", + "The $Z_\\text{OR}$ gate is less common, and is defined according to\n", + "$$\n", + "\\text{Z}_\\text{OR}|x\\rangle = \\begin{cases}\n", + "|x\\rangle & \\text{if } x = 0^n \\\\\n", + " -|x\\rangle & \\text{if } x \\neq 0^n\n", + "\\end{cases}\n", + "\\qquad \\forall x \\in \\Sigma^n\n", + "$$\n", + "\n", + "Finally, the $Z_f$ gate is defined by\n", + "$$\n", + "Z_f:|x\\rangle \\rightarrow (-1)^{f(x)}|x\\rangle \\qquad \\forall x \\in \\Sigma^n\n", + "$$\n", + "\n", + "Note the effect of this is that $Z_f$ flips the sign on a target state for which $f(x) = 1$ and leaves other states unaffected.\n", + "\n", + "At a very high, abstract level you can think about the steps in the circuit in the following ways:\n", + "* First Hadamard layer: puts the qubits into a superposition of all possible states.\n", + "* $Z_f$: mark the target state(s) by adding a \"-\" sign in front. This doesn't immediately change measurement probabilities, but it changes how the target state will behave in subsequent steps.\n", + "* Another Hadamard layer: The \"-\" sign introduced in the previous step will change the relative sign between some terms. Since Hadamard gates turn one mixture of computational states $(|0\\rangle+|1\\rangle)/\\sqrt{2}$ into one computational state, $|0\\rangle,$ and they turn $(|0\\rangle-|1\\rangle)/\\sqrt{2}$ into $|1\\rangle$ this relative sign difference can now begin to play a role in what states are measured.\n", + "* One final layer of Hadamard gates is applied, and then measurements are made.\n", + "We will see in more detail how this works in the next section." + ] + }, + { + "cell_type": "markdown", + "id": "bf41c02c", + "metadata": {}, + "source": [ + "### Example\n", + "To better understand how Grover's algorithm works, let us work through a small, two-qubit example. This may be considered optional for those not focused on quantum mechanics and Dirac notation. But for those who hope to work substantially with quantum computers, this is highly recommended.\n", + "\n", + "Here is the circuit diagram with the quantum states labeled at various positions throughout. Note that with only two qubits, there are only four possible states that could be measured under any circumstances: $|00\\rangle$, $|01\\rangle$, $|10\\rangle$, and $|11\\rangle$.\n", + "\n", + "![A diagram of a quantum circuit that implements Grover's algorithm on two qubits.](/learning/images/modules/computer-science/grovers/grover-circuit-diagram-2-q-ex.avif)\n", + "\n", + "Let us suppose that the oracle ($Z_f$, unknown to us) marks the state $|01\\rangle$. We will work through the actions of each set of quantum gates, including the oracle, and see what distribution of possible states come out at the time of measurement.\n", + "At the very beginning, we have\n", + "$$\n", + "|\\psi_0\\rangle = |00\\rangle\n", + "$$\n", + "Using the definition of Hadamard gates, we have\n", + "$$\n", + "|\\psi_1\\rangle = \\frac{1}{2}\\left(|0\\rangle+|1\\rangle\\right)\\left(|0\\rangle+|1\\rangle\\right)=\\frac{1}{2}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\n", + "$$\n", + "Now the oracle marks the target state:\n", + "$$\n", + "|\\psi_2\\rangle = \\frac{1}{2}\\left(|00\\rangle-|01\\rangle+|10\\rangle+|11\\rangle\\right)\n", + "$$\n", + "Note that in this state, all four possible outcomes have the same probability of being measured. They all have a weight of magnitude $1/2,$ meaning they each have a $|1/2|^2=1/4$ chance of being measured. So while the state $|01\\rangle$ is marked through the \"-\" phase, this has not yet resulted in any increased probability of measuring that state. We continue by applying the next layer of Hadamard gates.\n", + "$$\n", + "\\begin{aligned}\n", + "|\\psi_3\\rangle = &\\frac{1}{4}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\\\\\n", + "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\\\\\n", + "+&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle-|10\\rangle-|11\\rangle\\right)\\\\\n", + "+&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", + "\\end{aligned}\n", + "$$\n", + "Combining like terms, we find\n", + "$$\n", + "|\\psi_3\\rangle = \\frac{1}{2}\\left(|00\\rangle+|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", + "$$\n", + "Now $Z_{\\text{OR}}$ flips the sign on all states but $|00\\rangle$:\n", + "$$\n", + "|\\psi_4\\rangle = \\frac{1}{2}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\n", + "$$\n", + "And finally, we apply the last layer of Hadamard gates:\n", + "$$\n", + "\\begin{aligned}\n", + "|\\psi_5\\rangle =&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle\\right)\\\\\n", + "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle+|10\\rangle-|11\\rangle\\right)\\\\\n", + "+&\\frac{1}{4}\\left(|00\\rangle+|01\\rangle-|10\\rangle-|11\\rangle\\right)\\\\\n", + "-&\\frac{1}{4}\\left(|00\\rangle-|01\\rangle-|10\\rangle+|11\\rangle\\right)\n", + "\\end{aligned}\n", + "$$\n", + "It is worth working through the combining of these terms to convince yourself that the result is indeed:\n", + "$$\n", + "|\\psi_5\\rangle =|01\\rangle\n", + "$$\n", + "That is, the probability of measuring $|01\\rangle$ is 100% (in the absence of noise and errors) and the probability for measuring any other state is zero.\n", + "\n", + "This two-qubit example was an especially clean case; Grover's algorithm will not always work out to yield a 100% chance of measuring the target state. Rather, it will amplify the probability of measuring the target state. Also, the Grover operator may need to be repeated more than once.\n", + "\n", + "In the next section, we will put this algorithm into practice using real IBM® quantum computers." + ] + }, + { + "cell_type": "markdown", + "id": "geo_picture_01", + "metadata": {}, + "source": [ + "### The geometric picture\n", + "\n", + "The two-qubit example above showed how the algebra works out for a small case, but there is a much more intuitive way to understand Grover's algorithm: as a sequence of geometric reflections in a two-dimensional plane. Below we describe this picture. You can also see John Watrous's course [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/analysis) for more details.\n", + "\n", "**Setting up the plane.** We can decompose the initial superposition state $|\\psi\\rangle$ into two components. The correct state — the one we're searching for — we call $|A_1\\rangle$. Every other state, lumped together, we call $|A_0\\rangle$. By definition, $|A_1\\rangle$ and $|A_0\\rangle$ are orthogonal to one another, so we can plot them as perpendicular axes in an abstract, two-dimensional space. Since $|\\psi\\rangle$ is a linear combination of these two components, it sits at some small angle $\\theta$ to the $|A_0\\rangle$ axis — close to $|A_0\\rangle$, because at the start, only a tiny fraction of the state is in the correct component $|A_1\\rangle$.\n", - "\n", - "\n", - "**Reflections.** The key mathematical fact we need is that an operator of the form\n", - "$$\n", - "2|v\\rangle\\langle v| - I\n", - "$$\n", + "\n", + "\n", + "**Reflections.** The key mathematical fact we need is that an operator of the form\n", + "$$\n", + "2|v\\rangle\\langle v| - I\n", + "$$\n", "reflects any state about the axis defined by $|v\\rangle.$ To see why, consider two cases: a state along $|v\\rangle$ is left unchanged, and a state perpendicular to $|v\\rangle$ gets its sign flipped. Any other state can be decomposed into these two components, and the operator acts on each accordingly — which is exactly a reflection about $|v\\rangle$.\n", - "\n", - "It turns out that both the oracle and the diffusion steps in Grover's algorithm can be expressed as reflections in this geometric picture.\n", - "\n", - "**The oracle as a reflection.** The oracle flips the sign of the $|A_1\\rangle$ state and leaves everything else alone. That is the same as a reflection about the $|A_0\\rangle$ axis.\n", - "\n", - "![Geometric picture of the quantum state.](/learning/images/modules/computer-science/grovers/grover-geometric-setup.avif)\n", - "\n", - "**Diffusion as a reflection.** It is a little trickier to see how the diffusion operator is also a reflection. The diffusion operator is\n", - "$$\n", - "H^{\\otimes n}\\, Z_{\\text{OR}}\\, H^{\\otimes n}\n", - "$$\n", + "\n", + "It turns out that both the oracle and the diffusion steps in Grover's algorithm can be expressed as reflections in this geometric picture.\n", + "\n", + "**The oracle as a reflection.** The oracle flips the sign of the $|A_1\\rangle$ state and leaves everything else alone. That is the same as a reflection about the $|A_0\\rangle$ axis.\n", + "\n", + "![Geometric picture of the quantum state.](/learning/images/modules/computer-science/grovers/grover-geometric-setup.avif)\n", + "\n", + "**Diffusion as a reflection.** It is a little trickier to see how the diffusion operator is also a reflection. The diffusion operator is\n", + "$$\n", + "H^{\\otimes n}\\, Z_{\\text{OR}}\\, H^{\\otimes n}\n", + "$$\n", "$Z_{\\text{OR}}$ by itself is a reflection about the all-zero state, since it flips the sign of every state that is not $|0\\rangle^{\\otimes n}$. This can be written as $2|0\\rangle\\langle 0| - I$. The surrounding Hadamard layers effectively perform a change of basis, transforming the axis of reflection. Recall that $H^{\\otimes n}$ maps $|0\\rangle^{\\otimes n}$ to the uniform superposition $|u\\rangle = \\frac{1}{\\sqrt{N}}\\sum_{x}|x\\rangle$. Since the Hadamard is its own inverse, the full expression becomes\n", - "$$\n", - "H^{\\otimes n}\\left(2|0\\rangle\\langle 0| - I\\right)H^{\\otimes n} = 2|u\\rangle\\langle u| - I\n", - "$$\n", + "$$\n", + "H^{\\otimes n}\\left(2|0\\rangle\\langle 0| - I\\right)H^{\\otimes n} = 2|u\\rangle\\langle u| - I\n", + "$$\n", "which is a reflection about $|u\\rangle$. Since $|u\\rangle$ is very close to $|\\psi\\rangle$ (both are nearly along $|A_0\\rangle$), this second reflection sends the state to an angle $2\\theta$ from where it started.\n", - "\n", - "![Geometric interpretation of the Grover operator as a rotation.](/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif)\n", - "\n", + "\n", + "![Geometric interpretation of the Grover operator as a rotation.](/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif)\n", + "\n", "**Rotation by $2\\theta$.** The combined effect of these two reflections is a rotation by $2\\theta$ toward $|A_1\\rangle$. Each successive iteration of the Grover operator rotates the state by another $2\\theta.$\n", - "\n", + "\n", "**Optimal number of iterations.** Our goal is to rotate the state as close to $|A_1\\rangle$ as possible, which means rotating by a total of approximately $\\pi/2$ radians (a quarter turn). If each iteration contributes $2\\theta$, the optimal number of iterations $t$ satisfies\n", - "$$\n", - "(2t + 1)\\theta \\approx \\frac{\\pi}{2}\n", - "$$\n", - "For a single solution among $N$ states, the initial angle is $\\theta \\approx \\sin^{-1}(1/\\sqrt{N}) \\approx 1/\\sqrt{N}$ (for large $N$). Substituting,\n", - "$$\n", - "t \\approx \\frac{\\pi}{4}\\sqrt{N} - \\frac{1}{2}\n", - "$$\n", - "This is where the famous $\\sqrt{N}$ speedup comes from: we only need $O(\\sqrt{N})$ iterations to reach the target, rather than the $O(N)$ checks a classical search would require.\n", - "\n", - "More generally, if there are $|A_1|$ solution states among $N$ total states, the optimal number of iterations is\n", - "$$\n", - "t \\approx \\frac{\\pi}{4}\\sqrt{\\frac{N}{|A_1|}} - \\frac{1}{2}\n", - "$$\n", - "\n", - "Note that if you apply too many iterations, you rotate past $|A_1\\rangle$ and the probability of finding your target state will start to decrease again. Finding the right number of iterations is important, though on noisy quantum hardware the experimentally optimal number may differ from this ideal formula." - ] - }, - { - "cell_type": "markdown", - "id": "3fd8fbb3", - "metadata": {}, - "source": [ - "### Why is Grover's algorithm useful?\n", - "\n", + "$$\n", + "(2t + 1)\\theta \\approx \\frac{\\pi}{2}\n", + "$$\n", + "For a single solution among $N$ states, the initial angle is $\\theta \\approx \\sin^{-1}(1/\\sqrt{N}) \\approx 1/\\sqrt{N}$ (for large $N$). Substituting,\n", + "$$\n", + "t \\approx \\frac{\\pi}{4}\\sqrt{N} - \\frac{1}{2}\n", + "$$\n", + "This is where the famous $\\sqrt{N}$ speedup comes from: we only need $O(\\sqrt{N})$ iterations to reach the target, rather than the $O(N)$ checks a classical search would require.\n", + "\n", + "More generally, if there are $|A_1|$ solution states among $N$ total states, the optimal number of iterations is\n", + "$$\n", + "t \\approx \\frac{\\pi}{4}\\sqrt{\\frac{N}{|A_1|}} - \\frac{1}{2}\n", + "$$\n", + "\n", + "Note that if you apply too many iterations, you rotate past $|A_1\\rangle$ and the probability of finding your target state will start to decrease again. Finding the right number of iterations is important, though on noisy quantum hardware the experimentally optimal number may differ from this ideal formula." + ] + }, + { + "cell_type": "markdown", + "id": "3fd8fbb3", + "metadata": {}, + "source": [ + "### Why is Grover's algorithm useful?\n", + "\n", "At this point you may be wondering: we just built an oracle that marks a target state — but to build it, we had to know the target state. So what are we actually searching for?\n", - "\n", - "This is a fair question, and there are several good answers.\n", - "\n", - "- **The query model is a theoretical tool.** The query model of computation was never designed to be directly practical. Its purpose is to give us a clean way to analyze algorithmic complexity by separating a problem into two parts: the oracle, and everything else. How hard is the search, given that verification is free? How does the number of queries scale with the size of the input? These are useful questions even if no real system works exactly this way.\n", - "\n", - "- You can also think of it as a **two-party activity**: one person knows the target state and builds the oracle; the other person's job is to find the answer using the oracle as a black box, with no peeking inside. In Activity 2 below, you will do exactly this with a partner.\n", - "\n", + "\n", + "This is a fair question, and there are several good answers.\n", + "\n", + "- **The query model is a theoretical tool.** The query model of computation was never designed to be directly practical. Its purpose is to give us a clean way to analyze algorithmic complexity by separating a problem into two parts: the oracle, and everything else. How hard is the search, given that verification is free? How does the number of queries scale with the size of the input? These are useful questions even if no real system works exactly this way.\n", + "\n", + "- You can also think of it as a **two-party activity**: one person knows the target state and builds the oracle; the other person's job is to find the answer using the oracle as a black box, with no peeking inside. In Activity 2 below, you will do exactly this with a partner.\n", + "\n", "- **Amplitude amplification is a broadly useful subroutine.** Even if this first demonstration seems circular, the underlying mechanism — called *amplitude amplification* — shows up again and again in quantum computing. What we are really building here is an intuition for a tool that appears as a subroutine in many more complex quantum algorithms.\n", - "\n", + "\n", "- **There are problems where you can build an oracle without knowing the answer.** The key insight is that there exists a whole class of problems for which it is very hard to *find* a solution, but very easy to *check* that a given solution is correct. Factoring is one example: given a product of two large primes, it is extremely difficult to determine what those primes are, but once you have them, you can easily multiply them together to verify. (We have a better algorithm than Grover's for factoring specifically — see Shor's algorithm — but this is far from the only problem with this feature.) Sudoku, constraint satisfaction, and even the classic game of Minesweeper are all problems that are difficult to solve but easy to check.\n", - "\n", + "\n", "Why is that relevant? It means we can know all of the *conditions* and *requirements* that a solution must satisfy, and we can encode those requirements into a quantum circuit that serves as the oracle — even though we do not know the solution itself. Grover's algorithm will find it for us.\n", - "\n", - "With these ideas in mind, let us work through several examples. We will begin with an example in which the solution state is clearly specified so we can follow the logic of the algorithm. We will then move on to a two-party activity, and finally to an example in which the oracle is built from problem constraints rather than from knowledge of the answer." - ] - }, - { - "cell_type": "markdown", - "id": "90cfe463", - "metadata": {}, - "source": [ - "### General imports and approach\n", - "\n", - "We start by importing several necessary packages." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "27a7cc58", - "metadata": {}, - "outputs": [], - "source": [ - "# Built-in modules\n", - "import math\n", - "\n", - "# Imports from Qiskit\n", - "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n", - "from qiskit.circuit.library import grover_operator, MCMTGate, ZGate\n", - "from qiskit.visualization import plot_distribution\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager" - ] - }, - { - "cell_type": "markdown", - "id": "0d5be4ca", - "metadata": {}, - "source": [ - "Throughout this and other tutorials, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks workflows into the following steps:\n", - "\n", - "- Step 1: Map classical inputs to a quantum problem\n", - "- Step 2: Optimize problem for quantum execution\n", - "- Step 3: Execute using Qiskit Runtime Primitives\n", - "- Step 4: Post-processing and classical analysis\n", - "\n", - "We will generally follow these steps, though we may not always explicitly label them." - ] - }, - { - "cell_type": "markdown", - "id": "5e1d0a46", - "metadata": {}, - "source": [ - "## Activity 1: Find a single given target state" - ] - }, - { - "cell_type": "markdown", - "id": "23b5217f", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "We need the phase query gate to put an overall phase (-1) on solution states, and leave the non-solution states unaffected. Another way of saying this is that Grover's algorithm requires an oracle that specifies one or more marked computational basis states, where \"marked\" means a state with a phase of -1. This is done using a controlled-Z gate, or its multi-controlled generalization over $N$ qubits. To see how this works, consider a specific example of a bitstring `{110}`. We would like a circuit that acts on a state $|\\psi\\rangle = |q_2,q_1,q_0\\rangle$ and applies a phase if $|\\psi\\rangle = |011\\rangle$ (where we have flipped the order of the binary string, because of the notation in Qiskit, which puts the least significant (often 0) qubit on the right).\n", - "\n", - "Thus, we want a circuit $Z_f$ that accomplishes\n", - "\n", - "$$\n", - "Z_f|\\psi\\rangle = \\begin{cases} -|\\psi\\rangle \\qquad \\text{if} \\qquad |\\psi\\rangle = |011\\rangle \\\\ |\\psi\\rangle \\qquad \\text{if} \\qquad |\\psi\\rangle \\neq |011\\rangle\\end{cases}\n", - "$$\n", - "We can use the multiple control multiple target gate (`MCMTGate`) to apply a Z gate controlled by all qubits (flip the phase if all qubits are in the $|1\\rangle$ state). Of course, some of the qubits in our desired state may be $|0\\rangle$. Therefore, for those qubits we must first apply an X gate, then do the multiply-controlled Z gate, then apply another X gate to undo our change. The `MCMTGate` looks like this:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "66aeceae", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mcmt_ex = QuantumCircuit(3)\n", - "mcmt_ex.compose(MCMTGate(ZGate(), 3 - 1, 1), inplace=True)\n", - "mcmt_ex.draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "03b992b2", - "metadata": {}, - "source": [ - "Note that many qubits may be involved in the control process (here three qubits are), but no single qubit is denoted as a target. This is because the entire state gets an overall \"-\" sign (phase flip); the gate affects all the qubits equivalently. This is different from many other multiple qubit gates, like the `CX` gate, which has a single control qubit and a single target qubit.\n", - "\n", - "In the following code, we define a phase query gate (or oracle) that does what we just described above: marks one or more input basis states defined through their bitstring representation. The MCMT gate is used to implement the multi-controlled Z-gate." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "53f8763f", - "metadata": {}, - "outputs": [], - "source": [ - "def grover_oracle(marked_states):\n", - " \"\"\"Build a Grover oracle for multiple marked states\n", - "\n", - " Here we assume all input marked states have the same number of bits\n", - "\n", - " Parameters:\n", - " marked_states (str or list): Marked states of oracle\n", - "\n", - " Returns:\n", - " QuantumCircuit: Quantum circuit representing Grover oracle\n", - " \"\"\"\n", - " if not isinstance(marked_states, list):\n", - " marked_states = [marked_states]\n", - " # Compute the number of qubits in circuit\n", - " num_qubits = len(marked_states[0])\n", - "\n", - " qc = QuantumCircuit(num_qubits)\n", - " # Mark each target state in the input list\n", - " for target in marked_states:\n", - " # Flip target bitstring to match Qiskit bit-ordering\n", - " rev_target = target[::-1]\n", - " # Find the indices of all the '0' elements in bitstring\n", - " zero_inds = [\n", - " ind for ind in range(num_qubits) if rev_target.startswith(\"0\", ind)\n", - " ]\n", - " # Add a multi-controlled Z-gate with pre- and post-applied X-gates (open-controls)\n", - " # where the target bitstring has a '0' entry\n", - " qc.x(zero_inds)\n", - " qc.compose(MCMTGate(ZGate(), num_qubits - 1, 1), inplace=True)\n", - " qc.x(zero_inds)\n", - " return qc" - ] - }, - { - "cell_type": "markdown", - "id": "7349dd38", - "metadata": {}, - "source": [ - "Now we choose a specific \"marked\" state to be our target, and apply the function we just defined. Let's see what kind of circuit it created." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "6cb8ce21", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "marked_states = [\"1110\"]\n", - "oracle = grover_oracle(marked_states)\n", - "oracle.draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "5632a1dd", - "metadata": {}, - "source": [ - "If qubits 1-3 are in the $|1\\rangle$ state, and qubit 0 is initially in the $|0\\rangle$ state, the first X gate will flip qubit 0 to $|1\\rangle$ and all qubits will be in $|1\\rangle.$ This means the MCMT gate will apply an overall sign change or phase flip, as desired. For any other case, either qubits 1-3 are in the $|0\\rangle$ state, or qubit 0 is flipped to the $|0\\rangle$ state, and the phase flip will not be applied. We see that this circuit does indeed mark our desired state $|0111\\rangle,$ or the bitstring `{1110}`." - ] - }, - { - "cell_type": "markdown", - "id": "4cbe89d3", - "metadata": {}, - "source": [ - "The full Grover operator consists of the phase query gate (oracle), Hadamard layers, and the $Z_\\text{OR}$ operator. We can use the built-in `grover_operator` to construct this from the oracle we defined above." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9426f7a5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grover_op = grover_operator(oracle)\n", - "grover_op.decompose(reps=0).draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "fb293e00", - "metadata": {}, - "source": [ + "\n", + "With these ideas in mind, let us work through several examples. We will begin with an example in which the solution state is clearly specified so we can follow the logic of the algorithm. We will then move on to a two-party activity, and finally to an example in which the oracle is built from problem constraints rather than from knowledge of the answer." + ] + }, + { + "cell_type": "markdown", + "id": "90cfe463", + "metadata": {}, + "source": [ + "### General imports and approach\n", + "\n", + "We start by importing several necessary packages." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "27a7cc58", + "metadata": {}, + "outputs": [], + "source": [ + "# Built-in modules\n", + "import math\n", + "\n", + "# Imports from Qiskit\n", + "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n", + "from qiskit.circuit.library import grover_operator, MCMTGate, ZGate\n", + "from qiskit.visualization import plot_distribution\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager" + ] + }, + { + "cell_type": "markdown", + "id": "0d5be4ca", + "metadata": {}, + "source": [ + "Throughout this and other tutorials, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks workflows into the following steps:\n", + "\n", + "- Step 1: Map classical inputs to a quantum problem\n", + "- Step 2: Optimize problem for quantum execution\n", + "- Step 3: Execute using Qiskit Runtime Primitives\n", + "- Step 4: Post-processing and classical analysis\n", + "\n", + "We will generally follow these steps, though we may not always explicitly label them." + ] + }, + { + "cell_type": "markdown", + "id": "5e1d0a46", + "metadata": {}, + "source": [ + "## Activity 1: Find a single given target state" + ] + }, + { + "cell_type": "markdown", + "id": "23b5217f", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "We need the phase query gate to put an overall phase (-1) on solution states, and leave the non-solution states unaffected. Another way of saying this is that Grover's algorithm requires an oracle that specifies one or more marked computational basis states, where \"marked\" means a state with a phase of -1. This is done using a controlled-Z gate, or its multi-controlled generalization over $N$ qubits. To see how this works, consider a specific example of a bitstring `{110}`. We would like a circuit that acts on a state $|\\psi\\rangle = |q_2,q_1,q_0\\rangle$ and applies a phase if $|\\psi\\rangle = |011\\rangle$ (where we have flipped the order of the binary string, because of the notation in Qiskit, which puts the least significant (often 0) qubit on the right).\n", + "\n", + "Thus, we want a circuit $Z_f$ that accomplishes\n", + "\n", + "$$\n", + "Z_f|\\psi\\rangle = \\begin{cases} -|\\psi\\rangle \\qquad \\text{if} \\qquad |\\psi\\rangle = |011\\rangle \\\\ |\\psi\\rangle \\qquad \\text{if} \\qquad |\\psi\\rangle \\neq |011\\rangle\\end{cases}\n", + "$$\n", + "We can use the multiple control multiple target gate (`MCMTGate`) to apply a Z gate controlled by all qubits (flip the phase if all qubits are in the $|1\\rangle$ state). Of course, some of the qubits in our desired state may be $|0\\rangle$. Therefore, for those qubits we must first apply an X gate, then do the multiply-controlled Z gate, then apply another X gate to undo our change. The `MCMTGate` looks like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "66aeceae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mcmt_ex = QuantumCircuit(3)\n", + "mcmt_ex.compose(MCMTGate(ZGate(), 3 - 1, 1), inplace=True)\n", + "mcmt_ex.draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "03b992b2", + "metadata": {}, + "source": [ + "Note that many qubits may be involved in the control process (here three qubits are), but no single qubit is denoted as a target. This is because the entire state gets an overall \"-\" sign (phase flip); the gate affects all the qubits equivalently. This is different from many other multiple qubit gates, like the `CX` gate, which has a single control qubit and a single target qubit.\n", + "\n", + "In the following code, we define a phase query gate (or oracle) that does what we just described above: marks one or more input basis states defined through their bitstring representation. The MCMT gate is used to implement the multi-controlled Z-gate." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "53f8763f", + "metadata": {}, + "outputs": [], + "source": [ + "def grover_oracle(marked_states):\n", + " \"\"\"Build a Grover oracle for multiple marked states\n", + "\n", + " Here we assume all input marked states have the same number of bits\n", + "\n", + " Parameters:\n", + " marked_states (str or list): Marked states of oracle\n", + "\n", + " Returns:\n", + " QuantumCircuit: Quantum circuit representing Grover oracle\n", + " \"\"\"\n", + " if not isinstance(marked_states, list):\n", + " marked_states = [marked_states]\n", + " # Compute the number of qubits in circuit\n", + " num_qubits = len(marked_states[0])\n", + "\n", + " qc = QuantumCircuit(num_qubits)\n", + " # Mark each target state in the input list\n", + " for target in marked_states:\n", + " # Flip target bitstring to match Qiskit bit-ordering\n", + " rev_target = target[::-1]\n", + " # Find the indices of all the '0' elements in bitstring\n", + " zero_inds = [\n", + " ind for ind in range(num_qubits) if rev_target.startswith(\"0\", ind)\n", + " ]\n", + " # Add a multi-controlled Z-gate with pre- and post-applied X-gates (open-controls)\n", + " # where the target bitstring has a '0' entry\n", + " qc.x(zero_inds)\n", + " qc.compose(MCMTGate(ZGate(), num_qubits - 1, 1), inplace=True)\n", + " qc.x(zero_inds)\n", + " return qc" + ] + }, + { + "cell_type": "markdown", + "id": "7349dd38", + "metadata": {}, + "source": [ + "Now we choose a specific \"marked\" state to be our target, and apply the function we just defined. Let's see what kind of circuit it created." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "6cb8ce21", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "marked_states = [\"1110\"]\n", + "oracle = grover_oracle(marked_states)\n", + "oracle.draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "5632a1dd", + "metadata": {}, + "source": [ + "If qubits 1-3 are in the $|1\\rangle$ state, and qubit 0 is initially in the $|0\\rangle$ state, the first X gate will flip qubit 0 to $|1\\rangle$ and all qubits will be in $|1\\rangle.$ This means the MCMT gate will apply an overall sign change or phase flip, as desired. For any other case, either qubits 1-3 are in the $|0\\rangle$ state, or qubit 0 is flipped to the $|0\\rangle$ state, and the phase flip will not be applied. We see that this circuit does indeed mark our desired state $|0111\\rangle,$ or the bitstring `{1110}`." + ] + }, + { + "cell_type": "markdown", + "id": "4cbe89d3", + "metadata": {}, + "source": [ + "The full Grover operator consists of the phase query gate (oracle), Hadamard layers, and the $Z_\\text{OR}$ operator. We can use the built-in `grover_operator` to construct this from the oracle we defined above." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9426f7a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grover_op = grover_operator(oracle)\n", + "grover_op.decompose(reps=0).draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "fb293e00", + "metadata": {}, + "source": [ "As we discussed in the geometric picture above, we might need to apply the Grover operator multiple times. The optimal number of iterations $t$ to maximize the amplitude of the target state in the absence of noise is\n", - "$$\n", - "t\\approx \\frac{\\pi}{4} \\sqrt{\\frac{N}{|A_1|}}-\\frac{1}{2}\n", - "$$\n", + "$$\n", + "t\\approx \\frac{\\pi}{4} \\sqrt{\\frac{N}{|A_1|}}-\\frac{1}{2}\n", + "$$\n", "where $|A_1|$ is the number of solution states and $N=2^n$ is the total number of states. On modern noisy quantum computers, the experimentally optimal number of iterations might be different — but here we calculate and use this theoretical, optimal number using $|A_1|=1$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d07c701a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3\n" - ] - } - ], - "source": [ - "optimal_num_iterations = math.floor(\n", - " math.pi / (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))\n", - ")\n", - "print(optimal_num_iterations)" - ] - }, - { - "cell_type": "markdown", - "id": "4698589c", - "metadata": {}, - "source": [ - "Let us now construct a circuit that includes the initial Hadamard gates to create a superposition of all possible states, and apply the Grover operator the optimal number of times." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "63006e25", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(grover_op.num_qubits)\n", - "# Create even superposition of all basis states\n", - "qc.h(range(grover_op.num_qubits))\n", - "# Apply Grover operator the optimal number of times\n", - "qc.compose(grover_op.power(optimal_num_iterations), inplace=True)\n", - "# Measure all qubits\n", - "qc.measure_all()\n", - "qc.draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "f14e6ebd", - "metadata": {}, - "source": [ - "We have constructed our Grover circuit!\n", - "\n", - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "We have defined our abstract quantum circuit, but we need to rewrite it in terms of gates that are native to the quantum computer we actually want to use. We also need to specify which qubits on the quantum computer should be used. For these reasons and others, we now must transpile our circuit. First, let us specify the quantum computer we wish to use.\n", - "\n", - "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "994ef054", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qiskit_runtime_service._resolve_cloud_instances:WARNING:2025-08-08 14:14:19,931: Default instance not set. Searching all available instances.\n" - ] - }, - { - "data": { - "text/plain": [ - "'ibm_brisbane'" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", - "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", - "\n", - "# Load saved credentials\n", - "service = QiskitRuntimeService()\n", - "\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "backend.name" - ] - }, - { - "cell_type": "markdown", - "id": "a65cf945", - "metadata": {}, - "source": [ - "Now we use a preset pass manager to optimize our quantum circuit for the backend we selected." - ] - }, - { - "cell_type": "code", - "execution_count": 171, - "id": "35fbd6ef", - "metadata": {}, - "outputs": [], - "source": [ - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "circuit_isa = pm.run(qc)\n", - "# The transpiled circuit will be very large. Only draw it if you are really curious.\n", - "# circuit_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "014b7868", - "metadata": {}, - "source": [ - "It is worth noting at this time that the depth of the transpiled quantum circuit is substantial." - ] - }, - { - "cell_type": "code", - "execution_count": 172, - "id": "d168576f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The total depth is 439\n", - "The depth of two-qubit gates is 113\n" - ] - } - ], - "source": [ - "print(\"The total depth is \", circuit_isa.depth())\n", - "print(\n", - " \"The depth of two-qubit gates is \",\n", - " circuit_isa.depth(lambda instruction: instruction.operation.num_qubits == 2),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "aaea2d8d", - "metadata": {}, - "source": [ - "These are actually quite large numbers, even for this simple case. Since all quantum gates (and especially two-qubit gates) experience errors and are subject to noise, a series of over 100 two-qubit gates would result in nothing but noise if the qubits were not extremely high-performing. Let's see how these perform.\n", - "\n", - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "We want to make many measurements and see which state is the most likely. Such an amplitude amplification is a sampling problem that is suitable for execution with the `Sampler` Qiskit Runtime primitive.\n", - "\n", - "Note that the `run()` method of Qiskit Runtime SamplerV2 takes an iterable of primitive unified blocks (PUBs). For Sampler, each PUB is an iterable in the format (circuit, parameter_values). However, at a minimum, it takes a list of quantum circuit(s)." - ] - }, - { - "cell_type": "code", - "execution_count": 96, - "id": "2a272d9e", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU time)\n", - "\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "sampler.options.default_shots = 10_000\n", - "result = sampler.run([circuit_isa]).result()\n", - "dist = result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "7a123100", - "metadata": {}, - "source": [ - "To get the most out of this experience, we highly recommend you run your experiments on the real quantum computers available from IBM Quantum. However, if you have exhausted your QPU time, you can uncomment the lines below to complete this activity using a simulator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e60bcbec", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on local simulator:\n", - "# from qiskit.primitives import StatevectorSampler as Sampler\n", - "# sampler = Sampler()\n", - "# result = sampler.run([qc]).result()\n", - "# dist = result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "f19b1adb", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "\n", - "Now we can plot the results of our sampling in a histogram." - ] - }, - { - "cell_type": "code", - "execution_count": 97, - "id": "96a9107e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 97, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_distribution(dist)" - ] - }, - { - "cell_type": "markdown", - "id": "9dabb017", - "metadata": {}, - "source": [ - "We see that Grover's algorithm returned the desired state with the highest probability by far, at least an order of magnitude higher than other options. In the next activity, we will use the algorithm in a way that is more consistent with the two-party workflow of a query algorithm.\n", - "\n", - "#### Check your understanding\n", - "\n", - "We just searched for a single solution in a set of $2^4=16$ possible states. We determined the optimal number of repetitions of the Grover operator to be $t=3$. Would this optimal number have increased or decreased if we had searched for (a) any of several solutions, or (b) a single solution in a space of more possible states?\n", - "\n", - "\n", - "\n", - "\n", - "Recall that as long as the number of solutions is small compared to the entire space of solutions, we can expand the sine function around small angles and use\n", - "$$\n", - "(2t+1)\\theta = (2t+1) \\sin^{-1}{\\sqrt{\\frac{|\\mathcal{A}_1|}{N}}}\\approx (2t+1) \\sqrt{\\frac{|\\mathcal{A}_1|}{N}} \\approx \\pi/2\\\\\n", - "\n", - "t \\approx \\frac{\\pi}{4}\\sqrt{\\frac{N}{|\\mathcal{A}_1|}}-\\frac{1}{2}\n", - "$$\n", - "\n", - "(a) We see from the above expression that increasing the number of solution states would decrease the number of iterations. Provided that the fraction $\\frac{|\\mathcal{A}_1|}{N}$ is still small, we can describe how $t$ would decrease: $t~\\frac{1}{\\sqrt{|\\mathcal{A}_1|}}.$\n", - "\n", - "(b) As the space of possible solutions ($N$) increases, the number of required iterations increases, but only like $t~\\sqrt{N}$.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Suppose we could increase the size of the target bitstring to be arbitrarily long and still have the outcome that the target state has a probability amplitude that is at least an order of magnitude larger than any other state. Does this mean we could use Grover's algorithm to reliably find the target state?\n", - "\n", - "\n", - "\n", - "\n", - "No. Suppose we repeated the first activity with 20 qubits, and we run the quantum circuit a number of times `num_shots = 10,000`. A uniform probability distribution would mean that every state has a probability of $10,000/2^{20}=0.00954$ of being measured even a single time. If the probability of measuring the target state were 10 times that of non-solutions (and the probability of each non-solution were correspondingly slightly decreased), there would only be about a 10% chance of measuring the target state even once. It would be highly unlikely to measure the target state multiple times, which would make it indistinguishable from the many randomly-obtained non-solution states. The good news is that we can obtain even higher-fidelity results by using error suppression and mitigation.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "## Activity 2: An accurate query algorithm workflow\n", - "\n", - "We will start this activity exactly as the first one, except that now you will pair up with another Qiskit enthusiast. You will pick a secret bitstring, and your partner will pick a (generally) different bitstring. You will each generate a quantum circuit that functions as an oracle, and you will exchange them. You will then use Grover's algorithm with that oracle to determine your partner's secret bitstring.\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Using the `grover_oracle` function defined above, construct an oracle circuit for one or more marked states. Make sure you tell your partner how many states you have marked, so they can apply the Grover operator the optimal number of times. **Don't make your bitstring too long. 3-5 bits should work without much difficulty.** Longer bitstrings would result in deep circuits that require more advanced techniques like error mitigation." - ] - }, - { - "cell_type": "code", - "execution_count": 173, - "id": "5be9092e", - "metadata": {}, - "outputs": [], - "source": [ - "# Modify the marked states to mark those you wish to target.\n", - "marked_states = [\"1000\"]\n", - "oracle = grover_oracle(marked_states)" - ] - }, - { - "cell_type": "markdown", - "id": "b4874b93", - "metadata": {}, - "source": [ - "Now you have created a quantum circuit that flips the phase of your target state. You can save this circuit as `my_circuit.qpy` using the syntax below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77093258", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import qpy\n", - "\n", - "# Save to a QPY file at a location where you can easily find it.\n", - "# You might want to specify a global address.\n", - "with open(\"C:\\\\Users\\\\...put your own address here...\\\\my_circuit.qpy\", \"wb\") as f:\n", - " qpy.dump(oracle, f)" - ] - }, - { - "cell_type": "markdown", - "id": "524f5577", - "metadata": {}, - "source": [ - "Now send this file to your partner (via email, messaging service, a shared repo, and so forth). Have your partner send you their circuit as well. Make sure you save the file somewhere you can easily find it. Once you have your partner's circuit, you could visualize it - but that breaks the query model. That is, we're modeling a situation in which you can query the oracle (use the oracle circuit) but not examine it to determine what state it targets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24ba4869", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import qpy\n", - "\n", - "# Load the circuit from your partner's qpy file from the folder where you saved it.\n", - "with open(\"C:\\\\Users\\\\...file location here...\\\\my_circuit.qpy\", \"rb\") as f:\n", - " circuits = qpy.load(f)\n", - "\n", - "# qpy.load always returns a list of circuits\n", - "oracle_partner = circuits[0]\n", - "\n", - "# You could visualize the circuit, but this would break the model of a query algorithm.\n", - "# oracle_partner.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "0c7b7012", - "metadata": {}, - "source": [ - "Ask your partner how many target states they encoded and enter it below." - ] - }, - { - "cell_type": "code", - "execution_count": 174, - "id": "120e339c", - "metadata": {}, - "outputs": [], - "source": [ - "# Update according to your partner's number of target states.\n", - "num_marked_states = 1" - ] - }, - { - "cell_type": "markdown", - "id": "f2c4b911", - "metadata": {}, - "source": [ - "This is used in the next expression to determine the optimal number of Grover iterations." - ] - }, - { - "cell_type": "code", - "execution_count": 175, - "id": "d199a8cc", - "metadata": {}, - "outputs": [], - "source": [ - "grover_op = grover_operator(oracle_partner)\n", - "optimal_num_iterations = math.floor(\n", - " math.pi / (4 * math.asin(math.sqrt(num_marked_states / 2**grover_op.num_qubits)))\n", - ")\n", - "qc = QuantumCircuit(grover_op.num_qubits)\n", - "qc.h(range(grover_op.num_qubits))\n", - "qc.compose(grover_op.power(optimal_num_iterations), inplace=True)\n", - "qc.measure_all()" - ] - }, - { - "cell_type": "markdown", - "id": "37eb709b", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "This proceeds exactly as before." - ] - }, - { - "cell_type": "code", - "execution_count": 176, - "id": "e5e89707", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "backend.name\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "circuit_partner_isa = pm.run(qc)" - ] - }, - { - "cell_type": "markdown", - "id": "de2af3e1", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives\n", - "\n", - "This is also identical to the process in the first activity." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16f97083", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 seconds of QPU time)\n", - "\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "sampler.options.default_shots = 10_000\n", - "result = sampler.run([circuit_partner_isa]).result()\n", - "dist = result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "14bbde32", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "\n", - "Now display a histogram of your sampling results. One or more states should have much higher measurement probability than the others. Report these to your partner and check if you correctly determined the target states. By default, the histogram displayed is of the same circuit from the first activity. You should obtain different results from your partner's circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 114, - "id": "ee7a59ac", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 114, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_distribution(dist)" - ] - }, - { - "cell_type": "markdown", - "id": "9c03a936", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "You should have correctly obtained your partner's target state(s). If you did not, work with your partner to identify what went wrong. Click below for a few ideas.\n", - "\n", - "\n", - "\n", - "\n", - "* Visualize/draw your partner's circuit and make sure it loaded correctly.\n", - "* Compare the circuits used and compare the expected outcome to that which you obtained.\n", - "* Check the depth of the circuits used to make sure the bitstring wasn't too long or the number of Grover iterations prohibitively high.\n", - "\n", - "\n", - "\n", - "\n", - "If you haven't already, draw the oracle circuit your partner sent you. See if you can talk through the effect of each gate and argue what the target state must have been. This will be much easier for the case of a single marked state than for multiple.\n", - "\n", - "\n", - "\n", - "\n", - "* Recall that the job of the oracle is to flip the sign on the target state.\n", - "* Recall that the MCMTGate flips the sign on a state if and only if all qubits involved in the control are in the $|1\\rangle$ state.\n", - "* If your target state will already have a $|1\\rangle$ on a particular qubit, then you need not do anything to that qubit. If your target has a $|0\\rangle$ on a particular qubit and you want the MCMTGate to flip the sign, you need to apply an `X` gate to that qubit in your oracle (and then undo the `X` gate after the MCMTGate).\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Repeat the experiment with one fewer iteration of the Grover operator. Do you still obtain the correct answer? Why or why not?\n", - "\n", - "\n", - "\n", - "\n", - "You probably will, though it might depend on the number of solutions encoded. This highlights a subtlety: the \"optimal\" number of Grover iterations is the number that makes the probability of measuring the marked state as high as possible. But fewer iterations than that might still make the marked state substantially more likely than other states. Therefore, you might be able to get away with fewer iterations than the optimal number. This reduces circuit depth, and thus reduces error rates.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Why might someone want to use fewer Grover iterations than the \"optimal number\" identified here?\n", - "\n", - "\n", - "\n", - "\n", - "The \"optimal\" number of Grover iterations is the number that makes the probability of measuring the marked state as high as possible in the absence of noise. But fewer iterations than that might still make the marked state substantially more likely than other states. So you might be able to get away with fewer iterations than the optimal number. This reduces circuit depth, and thus reduces error rates.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "mine_intro_01", - "metadata": {}, - "source": [ + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d07c701a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + } + ], + "source": [ + "optimal_num_iterations = math.floor(\n", + " math.pi / (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))\n", + ")\n", + "print(optimal_num_iterations)" + ] + }, + { + "cell_type": "markdown", + "id": "4698589c", + "metadata": {}, + "source": [ + "Let us now construct a circuit that includes the initial Hadamard gates to create a superposition of all possible states, and apply the Grover operator the optimal number of times." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "63006e25", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(grover_op.num_qubits)\n", + "# Create even superposition of all basis states\n", + "qc.h(range(grover_op.num_qubits))\n", + "# Apply Grover operator the optimal number of times\n", + "qc.compose(grover_op.power(optimal_num_iterations), inplace=True)\n", + "# Measure all qubits\n", + "qc.measure_all()\n", + "qc.draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "f14e6ebd", + "metadata": {}, + "source": [ + "We have constructed our Grover circuit!\n", + "\n", + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "We have defined our abstract quantum circuit, but we need to rewrite it in terms of gates that are native to the quantum computer we actually want to use. We also need to specify which qubits on the quantum computer should be used. For these reasons and others, we now must transpile our circuit. First, let us specify the quantum computer we wish to use.\n", + "\n", + "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "994ef054", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "qiskit_runtime_service._resolve_cloud_instances:WARNING:2025-08-08 14:14:19,931: Default instance not set. Searching all available instances.\n" + ] + }, + { + "data": { + "text/plain": [ + "'ibm_brisbane'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance =\n", + "# '', token='', overwrite=True, set_as_default=True)\n", + "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", + "\n", + "# Load saved credentials\n", + "service = QiskitRuntimeService()\n", + "\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "backend.name" + ] + }, + { + "cell_type": "markdown", + "id": "a65cf945", + "metadata": {}, + "source": [ + "Now we use a preset pass manager to optimize our quantum circuit for the backend we selected." + ] + }, + { + "cell_type": "code", + "execution_count": 171, + "id": "35fbd6ef", + "metadata": {}, + "outputs": [], + "source": [ + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "circuit_isa = pm.run(qc)\n", + "# The transpiled circuit will be very large. Only draw it if you are really curious.\n", + "# circuit_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "014b7868", + "metadata": {}, + "source": [ + "It is worth noting at this time that the depth of the transpiled quantum circuit is substantial." + ] + }, + { + "cell_type": "code", + "execution_count": 172, + "id": "d168576f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The total depth is 439\n", + "The depth of two-qubit gates is 113\n" + ] + } + ], + "source": [ + "print(\"The total depth is \", circuit_isa.depth())\n", + "print(\n", + " \"The depth of two-qubit gates is \",\n", + " circuit_isa.depth(lambda instruction: instruction.operation.num_qubits == 2),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "aaea2d8d", + "metadata": {}, + "source": [ + "These are actually quite large numbers, even for this simple case. Since all quantum gates (and especially two-qubit gates) experience errors and are subject to noise, a series of over 100 two-qubit gates would result in nothing but noise if the qubits were not extremely high-performing. Let's see how these perform.\n", + "\n", + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "We want to make many measurements and see which state is the most likely. Such an amplitude amplification is a sampling problem that is suitable for execution with the `Sampler` Qiskit Runtime primitive.\n", + "\n", + "Note that the `run()` method of Qiskit Runtime SamplerV2 takes an iterable of primitive unified blocks (PUBs). For Sampler, each PUB is an iterable in the format (circuit, parameter_values). However, at a minimum, it takes a list of quantum circuit(s)." + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "2a272d9e", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU\n", + "# time)\n", + "\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "sampler.options.default_shots = 10_000\n", + "result = sampler.run([circuit_isa]).result()\n", + "dist = result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "7a123100", + "metadata": {}, + "source": [ + "To get the most out of this experience, we highly recommend you run your experiments on the real quantum computers available from IBM Quantum. However, if you have exhausted your QPU time, you can uncomment the lines below to complete this activity using a simulator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e60bcbec", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on local simulator:\n", + "# from qiskit.primitives import StatevectorSampler as Sampler\n", + "# sampler = Sampler()\n", + "# result = sampler.run([qc]).result()\n", + "# dist = result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "f19b1adb", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Now we can plot the results of our sampling in a histogram." + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "96a9107e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 97, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_distribution(dist)" + ] + }, + { + "cell_type": "markdown", + "id": "9dabb017", + "metadata": {}, + "source": [ + "We see that Grover's algorithm returned the desired state with the highest probability by far, at least an order of magnitude higher than other options. In the next activity, we will use the algorithm in a way that is more consistent with the two-party workflow of a query algorithm.\n", + "\n", + "#### Check your understanding\n", + "\n", + "We just searched for a single solution in a set of $2^4=16$ possible states. We determined the optimal number of repetitions of the Grover operator to be $t=3$. Would this optimal number have increased or decreased if we had searched for (a) any of several solutions, or (b) a single solution in a space of more possible states?\n", + "\n", + "\n", + "\n", + "\n", + "Recall that as long as the number of solutions is small compared to the entire space of solutions, we can expand the sine function around small angles and use\n", + "$$\n", + "(2t+1)\\theta = (2t+1) \\sin^{-1}{\\sqrt{\\frac{|\\mathcal{A}_1|}{N}}}\\approx (2t+1) \\sqrt{\\frac{|\\mathcal{A}_1|}{N}} \\approx \\pi/2\\\\\n", + "\n", + "t \\approx \\frac{\\pi}{4}\\sqrt{\\frac{N}{|\\mathcal{A}_1|}}-\\frac{1}{2}\n", + "$$\n", + "\n", + "(a) We see from the above expression that increasing the number of solution states would decrease the number of iterations. Provided that the fraction $\\frac{|\\mathcal{A}_1|}{N}$ is still small, we can describe how $t$ would decrease: $t~\\frac{1}{\\sqrt{|\\mathcal{A}_1|}}.$\n", + "\n", + "(b) As the space of possible solutions ($N$) increases, the number of required iterations increases, but only like $t~\\sqrt{N}$.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Suppose we could increase the size of the target bitstring to be arbitrarily long and still have the outcome that the target state has a probability amplitude that is at least an order of magnitude larger than any other state. Does this mean we could use Grover's algorithm to reliably find the target state?\n", + "\n", + "\n", + "\n", + "\n", + "No. Suppose we repeated the first activity with 20 qubits, and we run the quantum circuit a number of times `num_shots = 10,000`. A uniform probability distribution would mean that every state has a probability of $10,000/2^{20}=0.00954$ of being measured even a single time. If the probability of measuring the target state were 10 times that of non-solutions (and the probability of each non-solution were correspondingly slightly decreased), there would only be about a 10% chance of measuring the target state even once. It would be highly unlikely to measure the target state multiple times, which would make it indistinguishable from the many randomly-obtained non-solution states. The good news is that we can obtain even higher-fidelity results by using error suppression and mitigation.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "## Activity 2: An accurate query algorithm workflow\n", + "\n", + "We will start this activity exactly as the first one, except that now you will pair up with another Qiskit enthusiast. You will pick a secret bitstring, and your partner will pick a (generally) different bitstring. You will each generate a quantum circuit that functions as an oracle, and you will exchange them. You will then use Grover's algorithm with that oracle to determine your partner's secret bitstring.\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Using the `grover_oracle` function defined above, construct an oracle circuit for one or more marked states. Make sure you tell your partner how many states you have marked, so they can apply the Grover operator the optimal number of times. **Don't make your bitstring too long. 3-5 bits should work without much difficulty.** Longer bitstrings would result in deep circuits that require more advanced techniques like error mitigation." + ] + }, + { + "cell_type": "code", + "execution_count": 173, + "id": "5be9092e", + "metadata": {}, + "outputs": [], + "source": [ + "# Modify the marked states to mark those you wish to target.\n", + "marked_states = [\"1000\"]\n", + "oracle = grover_oracle(marked_states)" + ] + }, + { + "cell_type": "markdown", + "id": "b4874b93", + "metadata": {}, + "source": [ + "Now you have created a quantum circuit that flips the phase of your target state. You can save this circuit as `my_circuit.qpy` using the syntax below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77093258", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import qpy\n", + "\n", + "# Save to a QPY file at a location where you can easily find it.\n", + "# You might want to specify a global address.\n", + "with open(\"C:\\\\Users\\\\...put your own address here...\\\\my_circuit.qpy\", \"wb\") as f:\n", + " qpy.dump(oracle, f)" + ] + }, + { + "cell_type": "markdown", + "id": "524f5577", + "metadata": {}, + "source": [ + "Now send this file to your partner (via email, messaging service, a shared repo, and so forth). Have your partner send you their circuit as well. Make sure you save the file somewhere you can easily find it. Once you have your partner's circuit, you could visualize it - but that breaks the query model. That is, we're modeling a situation in which you can query the oracle (use the oracle circuit) but not examine it to determine what state it targets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24ba4869", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import qpy\n", + "\n", + "# Load the circuit from your partner's qpy file from the folder where you saved it.\n", + "with open(\"C:\\\\Users\\\\...file location here...\\\\my_circuit.qpy\", \"rb\") as f:\n", + " circuits = qpy.load(f)\n", + "\n", + "# qpy.load always returns a list of circuits\n", + "oracle_partner = circuits[0]\n", + "\n", + "# You could visualize the circuit, but this would break the model of a query algorithm.\n", + "# oracle_partner.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "0c7b7012", + "metadata": {}, + "source": [ + "Ask your partner how many target states they encoded and enter it below." + ] + }, + { + "cell_type": "code", + "execution_count": 174, + "id": "120e339c", + "metadata": {}, + "outputs": [], + "source": [ + "# Update according to your partner's number of target states.\n", + "num_marked_states = 1" + ] + }, + { + "cell_type": "markdown", + "id": "f2c4b911", + "metadata": {}, + "source": [ + "This is used in the next expression to determine the optimal number of Grover iterations." + ] + }, + { + "cell_type": "code", + "execution_count": 175, + "id": "d199a8cc", + "metadata": {}, + "outputs": [], + "source": [ + "grover_op = grover_operator(oracle_partner)\n", + "optimal_num_iterations = math.floor(\n", + " math.pi / (4 * math.asin(math.sqrt(num_marked_states / 2**grover_op.num_qubits)))\n", + ")\n", + "qc = QuantumCircuit(grover_op.num_qubits)\n", + "qc.h(range(grover_op.num_qubits))\n", + "qc.compose(grover_op.power(optimal_num_iterations), inplace=True)\n", + "qc.measure_all()" + ] + }, + { + "cell_type": "markdown", + "id": "37eb709b", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "This proceeds exactly as before." + ] + }, + { + "cell_type": "code", + "execution_count": 176, + "id": "e5e89707", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on hardware, select the backend with the fewest number of jobs in the queue\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "backend.name\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "circuit_partner_isa = pm.run(qc)" + ] + }, + { + "cell_type": "markdown", + "id": "de2af3e1", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "This is also identical to the process in the first activity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16f97083", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 seconds of\n", + "# QPU time)\n", + "\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "sampler.options.default_shots = 10_000\n", + "result = sampler.run([circuit_partner_isa]).result()\n", + "dist = result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "14bbde32", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Now display a histogram of your sampling results. One or more states should have much higher measurement probability than the others. Report these to your partner and check if you correctly determined the target states. By default, the histogram displayed is of the same circuit from the first activity. You should obtain different results from your partner's circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "id": "ee7a59ac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_distribution(dist)" + ] + }, + { + "cell_type": "markdown", + "id": "9c03a936", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "You should have correctly obtained your partner's target state(s). If you did not, work with your partner to identify what went wrong. Click below for a few ideas.\n", + "\n", + "\n", + "\n", + "\n", + "* Visualize/draw your partner's circuit and make sure it loaded correctly.\n", + "* Compare the circuits used and compare the expected outcome to that which you obtained.\n", + "* Check the depth of the circuits used to make sure the bitstring wasn't too long or the number of Grover iterations prohibitively high.\n", + "\n", + "\n", + "\n", + "\n", + "If you haven't already, draw the oracle circuit your partner sent you. See if you can talk through the effect of each gate and argue what the target state must have been. This will be much easier for the case of a single marked state than for multiple.\n", + "\n", + "\n", + "\n", + "\n", + "* Recall that the job of the oracle is to flip the sign on the target state.\n", + "* Recall that the MCMTGate flips the sign on a state if and only if all qubits involved in the control are in the $|1\\rangle$ state.\n", + "* If your target state will already have a $|1\\rangle$ on a particular qubit, then you need not do anything to that qubit. If your target has a $|0\\rangle$ on a particular qubit and you want the MCMTGate to flip the sign, you need to apply an `X` gate to that qubit in your oracle (and then undo the `X` gate after the MCMTGate).\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Repeat the experiment with one fewer iteration of the Grover operator. Do you still obtain the correct answer? Why or why not?\n", + "\n", + "\n", + "\n", + "\n", + "You probably will, though it might depend on the number of solutions encoded. This highlights a subtlety: the \"optimal\" number of Grover iterations is the number that makes the probability of measuring the marked state as high as possible. But fewer iterations than that might still make the marked state substantially more likely than other states. Therefore, you might be able to get away with fewer iterations than the optimal number. This reduces circuit depth, and thus reduces error rates.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Why might someone want to use fewer Grover iterations than the \"optimal number\" identified here?\n", + "\n", + "\n", + "\n", + "\n", + "The \"optimal\" number of Grover iterations is the number that makes the probability of measuring the marked state as high as possible in the absence of noise. But fewer iterations than that might still make the marked state substantially more likely than other states. So you might be able to get away with fewer iterations than the optimal number. This reduces circuit depth, and thus reduces error rates.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "mine_intro_01", + "metadata": {}, + "source": [ "## Activity 3: Solve a Minesweeper grid with Grover's algorithm\n", "\n", "In the previous section, we noted that Grover's algorithm becomes genuinely useful when we can build an oracle from the *constraints* of a problem, rather than from knowledge of the answer. Minesweeper is a perfect example: the numbered cells tell us how many mines are adjacent, and those constraints fully determine where the mines must be — but finding the configuration requires search.\n", "\n", "Minesweeper has been proven to be NP-complete: it is hard to solve but easy to check. That makes it a natural candidate for Grover's algorithm. Of course, we cannot yet solve a full 9$\\times$9 grid on a noisy quantum computer — the circuits would be far too deep. Instead, we will use a tiny grid as a toy demonstration of how one would approach a larger board on a future fault-tolerant machine.\n", - "\n", - "A few important caveats. Grover's algorithm provides only a quadratic speedup over *unstructured* classical search. Minesweeper almost certainly has exploitable structure that a clever classical algorithm could use. And for an exponentially growing search space, even the $\\sqrt{N}$ improvement only goes so far. But let us set those concerns aside and use this toy problem to illustrate how problem constraints get encoded into a quantum oracle.\n", - "\n", - "### The grid\n", - "\n", - "Here is our baby minesweeper grid:\n", - "\n", + "\n", + "A few important caveats. Grover's algorithm provides only a quadratic speedup over *unstructured* classical search. Minesweeper almost certainly has exploitable structure that a clever classical algorithm could use. And for an exponentially growing search space, even the $\\sqrt{N}$ improvement only goes so far. But let us set those concerns aside and use this toy problem to illustrate how problem constraints get encoded into a quantum oracle.\n", + "\n", + "### The grid\n", + "\n", + "Here is our baby minesweeper grid:\n", + "\n", "![A simple Minesweeper grid with three blank cells and three numbered cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid.avif)\n", - "\n", - "Each blank cell can be represented by a binary variable indicating whether it contains a mine. We label these $x_0$, $x_1$, and $x_2$, where $x_i = 1$ means there is a mine on that cell and $x_i = 0$ means there is not:\n", - "\n", + "\n", + "Each blank cell can be represented by a binary variable indicating whether it contains a mine. We label these $x_0$, $x_1$, and $x_2$, where $x_i = 1$ means there is a mine on that cell and $x_i = 0$ means there is not:\n", + "\n", "![The same Minesweeper grid with variables x0, x1, x2 labeling the blank cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif)\n", - "\n", - "We could solve this in our heads in about half a second, but we are using this toy problem to illustrate how a much harder board could be approached with a quantum computer." - ] - }, - { - "cell_type": "markdown", - "id": "mine_bool_01", - "metadata": {}, - "source": [ - "### Encode the constraints\n", - "\n", - "Each numbered cell places a condition on the adjacent blank cells. We need to express these conditions as Boolean expressions that can be encoded into a quantum circuit.\n", - "\n", - "The \"1\" cell adjacent to $x_0$ and $x_1$ says that exactly one of them contains a mine. This is precisely the exclusive-OR (XOR) operation, $\\oplus$, which returns true when exactly one of its inputs is true:\n", - "$$\n", - "(x_0 \\oplus x_1)\n", - "$$\n", - "\n", - "Similarly, the other \"1\" cell (adjacent to $x_1$ and $x_2$) gives us:\n", - "$$\n", - "(x_1 \\oplus x_2)\n", - "$$\n", - "\n", - "The \"2\" cell says that two of the three blank cells must contain mines. Since XOR is a parity operation, $x_0 \\oplus x_1 \\oplus x_2$ returns true when an *odd* number of the variables are true. We want an *even* number (specifically two) to be true, so we negate with $\\lnot$:\n", - "$$\n", - "\\lnot(x_0 \\oplus x_1 \\oplus x_2)\n", - "$$\n", - "On its own, this expression would be satisfied by either zero or two qubits in the $|1\\rangle$ state, since it is a statement about parity. But combined with the other two clauses, which each require at least one mine, the only satisfying assignment has exactly two mines.\n", - "\n", - "All three conditions must be simultaneously satisfied, so we join them with and symbols $\\land$:\n", - "$$\n", - "(x_0 \\oplus x_1) \\;\\land\\; (x_1 \\oplus x_2) \\;\\land\\; \\lnot(x_0 \\oplus x_1 \\oplus x_2)\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "mine_oracle_01", - "metadata": {}, - "source": [ - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "Now we need to encode this Boolean expression into a quantum circuit that serves as the oracle. The quantum version of XOR can be accomplished with CX (CNOT) gates: applying two CX gates from the data qubits to a workspace (ancilla) qubit effectively computes their XOR and stores the result in the ancilla.\n", - "\n", + "\n", + "We could solve this in our heads in about half a second, but we are using this toy problem to illustrate how a much harder board could be approached with a quantum computer." + ] + }, + { + "cell_type": "markdown", + "id": "mine_bool_01", + "metadata": {}, + "source": [ + "### Encode the constraints\n", + "\n", + "Each numbered cell places a condition on the adjacent blank cells. We need to express these conditions as Boolean expressions that can be encoded into a quantum circuit.\n", + "\n", + "The \"1\" cell adjacent to $x_0$ and $x_1$ says that exactly one of them contains a mine. This is precisely the exclusive-OR (XOR) operation, $\\oplus$, which returns true when exactly one of its inputs is true:\n", + "$$\n", + "(x_0 \\oplus x_1)\n", + "$$\n", + "\n", + "Similarly, the other \"1\" cell (adjacent to $x_1$ and $x_2$) gives us:\n", + "$$\n", + "(x_1 \\oplus x_2)\n", + "$$\n", + "\n", + "The \"2\" cell says that two of the three blank cells must contain mines. Since XOR is a parity operation, $x_0 \\oplus x_1 \\oplus x_2$ returns true when an *odd* number of the variables are true. We want an *even* number (specifically two) to be true, so we negate with $\\lnot$:\n", + "$$\n", + "\\lnot(x_0 \\oplus x_1 \\oplus x_2)\n", + "$$\n", + "On its own, this expression would be satisfied by either zero or two qubits in the $|1\\rangle$ state, since it is a statement about parity. But combined with the other two clauses, which each require at least one mine, the only satisfying assignment has exactly two mines.\n", + "\n", + "All three conditions must be simultaneously satisfied, so we join them with and symbols $\\land$:\n", + "$$\n", + "(x_0 \\oplus x_1) \\;\\land\\; (x_1 \\oplus x_2) \\;\\land\\; \\lnot(x_0 \\oplus x_1 \\oplus x_2)\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "mine_oracle_01", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Now we need to encode this Boolean expression into a quantum circuit that serves as the oracle. The quantum version of XOR can be accomplished with CX (CNOT) gates: applying two CX gates from the data qubits to a workspace (ancilla) qubit effectively computes their XOR and stores the result in the ancilla.\n", + "\n", "We introduce three workspace qubits — one for each clause. We store the result of each Boolean expression in its corresponding workspace qubit, then use a multi-controlled Z gate to flip the phase of the three-qubit state that makes all three workspace qubits $|1\\rangle$ (meaning all clauses are satisfied simultaneously).\n", "\n", "In the first code cell below, we build the \"compute\" half of the oracle — the part that evaluates each clause and writes the result into the workspace qubits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_oracle1", - "metadata": {}, - "outputs": [], - "source": [ - "x = QuantumRegister(3, \"x\")\n", - "a = QuantumRegister(3, \"a\")\n", - "qc = QuantumCircuit(x, a)\n", - "\n", - "# Clause 1: x0 XOR x1 -> stored in a[0]\n", - "qc.cx(x[0], a[0])\n", - "qc.cx(x[1], a[0])\n", - "\n", - "# Clause 2: x1 XOR x2 -> stored in a[1]\n", - "qc.cx(x[1], a[1])\n", - "qc.cx(x[2], a[1])\n", - "\n", - "# Clause 3: NOT(x0 XOR x1 XOR x2) -> stored in a[2]\n", - "qc.cx(x[0], a[2])\n", - "qc.cx(x[1], a[2])\n", - "qc.cx(x[2], a[2])\n", - "qc.x(a[2]) # The NOT\n", - "\n", - "qc.draw(\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "mine_mcz_01", - "metadata": {}, - "source": [ - "At this point, the result of each clause is stored in its corresponding workspace qubit. Now we need the three-qubit data state that makes all three workspace qubits $|1\\rangle$ to pick up a minus sign. We do this with a multi-controlled Z gate (implemented as an MCX gate sandwiched by Hadamard gates on the target).\n", - "\n", + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_oracle1", + "metadata": {}, + "outputs": [], + "source": [ + "x = QuantumRegister(3, \"x\")\n", + "a = QuantumRegister(3, \"a\")\n", + "qc = QuantumCircuit(x, a)\n", + "\n", + "# Clause 1: x0 XOR x1 -> stored in a[0]\n", + "qc.cx(x[0], a[0])\n", + "qc.cx(x[1], a[0])\n", + "\n", + "# Clause 2: x1 XOR x2 -> stored in a[1]\n", + "qc.cx(x[1], a[1])\n", + "qc.cx(x[2], a[1])\n", + "\n", + "# Clause 3: NOT(x0 XOR x1 XOR x2) -> stored in a[2]\n", + "qc.cx(x[0], a[2])\n", + "qc.cx(x[1], a[2])\n", + "qc.cx(x[2], a[2])\n", + "qc.x(a[2]) # The NOT\n", + "\n", + "qc.draw(\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "mine_mcz_01", + "metadata": {}, + "source": [ + "At this point, the result of each clause is stored in its corresponding workspace qubit. Now we need the three-qubit data state that makes all three workspace qubits $|1\\rangle$ to pick up a minus sign. We do this with a multi-controlled Z gate (implemented as an MCX gate sandwiched by Hadamard gates on the target).\n", + "\n", "After applying the phase flip, we must **uncompute** — undo all the clause-evaluation steps in reverse order — to reset the workspace qubits back to $|0\\rangle.$ This is essential so that the workspace qubits are clean for subsequent iterations of the Grover operator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_oracle2", - "metadata": {}, - "outputs": [], - "source": [ - "# Multi-controlled Z: flip phase if all workspace qubits are |1>\n", - "qc.h(a[2])\n", - "qc.mcx([a[0], a[1]], a[2])\n", - "qc.h(a[2])\n", - "\n", - "# Uncompute clause 3: NOT(x0 XOR x1 XOR x2)\n", - "qc.x(a[2])\n", - "qc.cx(x[2], a[2])\n", - "qc.cx(x[1], a[2])\n", - "qc.cx(x[0], a[2])\n", - "\n", - "# Uncompute clause 2: x1 XOR x2\n", - "qc.cx(x[2], a[1])\n", - "qc.cx(x[1], a[1])\n", - "\n", - "# Uncompute clause 1: x0 XOR x1\n", - "qc.cx(x[1], a[0])\n", - "qc.cx(x[0], a[0])\n", - "\n", - "qc.draw(\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "mine_groverop_01", - "metadata": {}, - "source": [ + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_oracle2", + "metadata": {}, + "outputs": [], + "source": [ + "# Multi-controlled Z: flip phase if all workspace qubits are |1>\n", + "qc.h(a[2])\n", + "qc.mcx([a[0], a[1]], a[2])\n", + "qc.h(a[2])\n", + "\n", + "# Uncompute clause 3: NOT(x0 XOR x1 XOR x2)\n", + "qc.x(a[2])\n", + "qc.cx(x[2], a[2])\n", + "qc.cx(x[1], a[2])\n", + "qc.cx(x[0], a[2])\n", + "\n", + "# Uncompute clause 2: x1 XOR x2\n", + "qc.cx(x[2], a[1])\n", + "qc.cx(x[1], a[1])\n", + "\n", + "# Uncompute clause 1: x0 XOR x1\n", + "qc.cx(x[1], a[0])\n", + "qc.cx(x[0], a[0])\n", + "\n", + "qc.draw(\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "mine_groverop_01", + "metadata": {}, + "source": [ "This circuit is our oracle: it flips the phase of the data-qubit state that satisfies all three Minesweeper constraints, and leaves the workspace qubits back in $|0\\rangle.$\n", - "\n", - "Now we construct the full Grover operator from this oracle. Note the `reflection_qubits` argument: we pass only the data qubits `x`, because the workspace qubits are not part of the search space. Their job is done once the oracle has been applied." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_groverop", - "metadata": {}, - "outputs": [], - "source": [ - "grover_op = grover_operator(qc, reflection_qubits=x)\n", - "grover_op.decompose(reps=0).draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "mine_circuit_01", - "metadata": {}, - "source": [ + "\n", + "Now we construct the full Grover operator from this oracle. Note the `reflection_qubits` argument: we pass only the data qubits `x`, because the workspace qubits are not part of the search space. Their job is done once the oracle has been applied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_groverop", + "metadata": {}, + "outputs": [], + "source": [ + "grover_op = grover_operator(qc, reflection_qubits=x)\n", + "grover_op.decompose(reps=0).draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "mine_circuit_01", + "metadata": {}, + "source": [ "With three data qubits and one solution state, the optimal number of Grover iterations is $t \\approx \\frac{\\pi}{4}\\sqrt{8} - \\frac{1}{2} \\approx 1.7$, so we use two iterations. We apply Hadamard gates to the data qubits to create the initial superposition, compose the Grover operator twice, and measure only the data qubits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_circuit", - "metadata": {}, - "outputs": [], - "source": [ - "x = QuantumRegister(3, \"x\")\n", - "a = QuantumRegister(4, \"a\")\n", - "meas = ClassicalRegister(3, \"meas\")\n", - "\n", - "qc = QuantumCircuit(x, a, meas)\n", - "# Create superposition over the data qubits only\n", - "qc.h(x)\n", - "# Apply 2 iterations of the Grover operator\n", - "qc.compose(grover_op.power(2), inplace=True)\n", - "# Measure only the data qubits\n", - "qc.measure(x, meas)\n", - "qc.decompose().draw(output=\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "mine_step2_01", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "As before, we transpile the circuit for the target backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_transpile", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(operational=True, simulator=False)\n", - "print(backend.name)\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "circuit_isa = pm.run(qc)" - ] - }, - { - "cell_type": "markdown", - "id": "mine_depth_01", - "metadata": {}, - "source": [ + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_circuit", + "metadata": {}, + "outputs": [], + "source": [ + "x = QuantumRegister(3, \"x\")\n", + "a = QuantumRegister(4, \"a\")\n", + "meas = ClassicalRegister(3, \"meas\")\n", + "\n", + "qc = QuantumCircuit(x, a, meas)\n", + "# Create superposition over the data qubits only\n", + "qc.h(x)\n", + "# Apply 2 iterations of the Grover operator\n", + "qc.compose(grover_op.power(2), inplace=True)\n", + "# Measure only the data qubits\n", + "qc.measure(x, meas)\n", + "qc.decompose().draw(output=\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "mine_step2_01", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "As before, we transpile the circuit for the target backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_transpile", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "print(backend.name)\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "circuit_isa = pm.run(qc)" + ] + }, + { + "cell_type": "markdown", + "id": "mine_depth_01", + "metadata": {}, + "source": [ "Now we can check the depth of the transpiled circuit. Because the Minesweeper oracle uses workspace qubits and multiple CX gates, the transpiled circuit will be deeper than those in the previous activities." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_depth", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"The total depth is \", circuit_isa.depth())\n", - "print(\n", - " \"The depth of two-qubit gates is \",\n", - " circuit_isa.depth(lambda instruction: instruction.operation.num_qubits == 2),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "mine_step3_01", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_run", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU time)\n", - "\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "sampler.options.default_shots = 10_000\n", - "result = sampler.run([circuit_isa]).result()\n", - "dist = result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_sim", - "metadata": {}, - "outputs": [], - "source": [ - "# To run on local simulator:\n", - "# from qiskit.primitives import StatevectorSampler as Sampler\n", - "# sampler = Sampler()\n", - "# result = sampler.run([qc]).result()\n", - "# dist = result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "mine_step4_01", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "mine_code_plot", - "metadata": {}, - "outputs": [], - "source": [ - "plot_distribution(dist)" - ] - }, - { - "cell_type": "markdown", - "id": "mine_conclusion_01", - "metadata": {}, - "source": [ + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_depth", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"The total depth is \", circuit_isa.depth())\n", + "print(\n", + " \"The depth of two-qubit gates is \",\n", + " circuit_isa.depth(lambda instruction: instruction.operation.num_qubits == 2),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "mine_step3_01", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_run", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU\n", + "# time)\n", + "\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "sampler.options.default_shots = 10_000\n", + "result = sampler.run([circuit_isa]).result()\n", + "dist = result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_sim", + "metadata": {}, + "outputs": [], + "source": [ + "# To run on local simulator:\n", + "# from qiskit.primitives import StatevectorSampler as Sampler\n", + "# sampler = Sampler()\n", + "# result = sampler.run([qc]).result()\n", + "# dist = result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "mine_step4_01", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mine_code_plot", + "metadata": {}, + "outputs": [], + "source": [ + "plot_distribution(dist)" + ] + }, + { + "cell_type": "markdown", + "id": "mine_conclusion_01", + "metadata": {}, + "source": [ "The `101` state should appear with far higher probability than any other, indicating that mines are located at $x_0$ and $x_2$. We have used a quantum computer to solve a tiny game of Minesweeper!\n", "\n", "Of course, the best classical algorithms for minesweeper are better than a brute-force search through all possible mine configurations — they exploit the structure of the grid. Grover's algorithm would only offer an advantage on extremely difficult boards designed to be maximally ambiguous, and even then, the quadratic speedup means it cannot keep pace with exponential growth indefinitely. But the real takeaway is the technique: encoding problem constraints into a quantum oracle is a powerful pattern that extends to constraint satisfaction, combinatorial optimization, and many other domains." - ] - }, - { - "cell_type": "markdown", - "id": "494c1799", - "metadata": {}, - "source": [ - "## Questions and critical concepts:\n", - "\n", - "\n", - "### Critical concepts:\n", - "\n", - "In this module, we learned some key features of Grover's algorithm:\n", - "- Whereas classical unstructured search algorithms require a number of queries that scales linearly in the size of the space, $N,$ Grover's algorithm requires a number of queries that scales like $\\sqrt{N}.$\n", - "- Grover's algorithm involves repeating a series of operations (commonly called the \"Grover operator\") a number of times $t,$ chosen to make the target states optimally likely to be measured.\n", - "- Grover's algorithm can be run with fewer than $t$ iterations and still amplify the target states.\n", - "- Grover's algorithm fits into the query model of computation and makes the most sense when one person controls the search and another controls/constructs the oracle. It may also be useful as a subroutine in other quantum computations.\n", + ] + }, + { + "cell_type": "markdown", + "id": "494c1799", + "metadata": {}, + "source": [ + "## Questions and critical concepts:\n", + "\n", + "\n", + "### Critical concepts:\n", + "\n", + "In this module, we learned some key features of Grover's algorithm:\n", + "- Whereas classical unstructured search algorithms require a number of queries that scales linearly in the size of the space, $N,$ Grover's algorithm requires a number of queries that scales like $\\sqrt{N}.$\n", + "- Grover's algorithm involves repeating a series of operations (commonly called the \"Grover operator\") a number of times $t,$ chosen to make the target states optimally likely to be measured.\n", + "- Grover's algorithm can be run with fewer than $t$ iterations and still amplify the target states.\n", + "- Grover's algorithm fits into the query model of computation and makes the most sense when one person controls the search and another controls/constructs the oracle. It may also be useful as a subroutine in other quantum computations.\n", "- An oracle can be built from *problem constraints* rather than from knowledge of the solution, as demonstrated with the Minesweeper example.\n", - "\n", - "\n", - "### T/F questions:\n", - "\n", - "1. T/F Grover's algorithm provides an exponential improvement over classical algorithms in the number of queries needed to find a single marked state in unstructured search.\n", - "\n", - "2. T/F Grover's algorithm works by iteratively increasing the probability that a solution state will be measured.\n", - "\n", - "3. T/F The more times you iterate the Grover operator, the higher the probability of measuring a solution state.\n", - "\n", - "### MC questions:\n", - "\n", - "1. Select the best option to complete the sentence. The best strategy to successfully use Grover's algorithm on modern quantum computers is to iterate the Grover operator...\n", - "- a. Only once.\n", - "- b. Always $t$ times, to maximize the solution state(s)' probability amplitude.\n", - "- c. Up to $t$ times, though fewer may be enough to make solution states stand out.\n", - "- d. No fewer than 10 times.\n", - "\n", - "\n", - "2. A phase query circuit is shown here that functions as an oracle to mark a certain state with a phase flip. Which of the following states get marked by this circuit?\n", - "\n", - "![An image of a simple grover oracle.](/learning/images/modules/computer-science/grovers/grover-oracle-question.avif)\n", - "\n", - "- a. $|0000\\rangle$\n", - "- b. $|0101\\rangle$\n", - "- c. $|0110\\rangle$\n", - "- d. $|1001\\rangle$\n", - "- e. $|1010\\rangle$\n", - "- f. $|1111\\rangle$\n", - "\n", - "\n", - "3. Suppose you want to search for three marked states from a set of 128. What is the optimal number of iterations of the Grover operator to maximize the amplitudes of the marked states?\n", - "- a. 1\n", - "- b. 3\n", - "- c. 5\n", - "- d. 6\n", - "- e. 20\n", - "- f. 33\n", - "\n", - "\n", - "### Discussion questions:\n", - "\n", - "1. What other problems could you formulate as a Grover search? Think of problems where it is difficult to find a solution but easy to verify one.\n", - "\n", - "\n", - "2. Can you see any problems with scaling Grover's algorithm on modern quantum computers?" - ] - } - ], - "metadata": { - "in_page_toc_max_heading_level": 2, - "in_page_toc_min_heading_level": 2, - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "\n", + "\n", + "### T/F questions:\n", + "\n", + "1. T/F Grover's algorithm provides an exponential improvement over classical algorithms in the number of queries needed to find a single marked state in unstructured search.\n", + "\n", + "2. T/F Grover's algorithm works by iteratively increasing the probability that a solution state will be measured.\n", + "\n", + "3. T/F The more times you iterate the Grover operator, the higher the probability of measuring a solution state.\n", + "\n", + "### MC questions:\n", + "\n", + "1. Select the best option to complete the sentence. The best strategy to successfully use Grover's algorithm on modern quantum computers is to iterate the Grover operator...\n", + "- a. Only once.\n", + "- b. Always $t$ times, to maximize the solution state(s)' probability amplitude.\n", + "- c. Up to $t$ times, though fewer may be enough to make solution states stand out.\n", + "- d. No fewer than 10 times.\n", + "\n", + "\n", + "2. A phase query circuit is shown here that functions as an oracle to mark a certain state with a phase flip. Which of the following states get marked by this circuit?\n", + "\n", + "![An image of a simple grover oracle.](/learning/images/modules/computer-science/grovers/grover-oracle-question.avif)\n", + "\n", + "- a. $|0000\\rangle$\n", + "- b. $|0101\\rangle$\n", + "- c. $|0110\\rangle$\n", + "- d. $|1001\\rangle$\n", + "- e. $|1010\\rangle$\n", + "- f. $|1111\\rangle$\n", + "\n", + "\n", + "3. Suppose you want to search for three marked states from a set of 128. What is the optimal number of iterations of the Grover operator to maximize the amplitudes of the marked states?\n", + "- a. 1\n", + "- b. 3\n", + "- c. 5\n", + "- d. 6\n", + "- e. 20\n", + "- f. 33\n", + "\n", + "\n", + "### Discussion questions:\n", + "\n", + "1. What other problems could you formulate as a Grover search? Think of problems where it is difficult to find a solution but easy to verify one.\n", + "\n", + "\n", + "2. Can you see any problems with scaling Grover's algorithm on modern quantum computers?" + ] + } + ], + "metadata": { + "in_page_toc_max_heading_level": 2, + "in_page_toc_min_heading_level": 2, + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/modules/computer-science/quantum-key-distribution.ipynb b/learning/modules/computer-science/quantum-key-distribution.ipynb index 09aee245f70..73a4504faa8 100644 --- a/learning/modules/computer-science/quantum-key-distribution.ipynb +++ b/learning/modules/computer-science/quantum-key-distribution.ipynb @@ -1,1362 +1,1368 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "290dd4cc-b40a-4d36-9bba-3deda83df5c2", - "metadata": {}, - "source": [ - "---\n", - "title: Quantum Key Distribution\n", - "description: This module explores how to use quantum states to securely share a key for encryption and detect potential eavesdroppers.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore cryptoquote WVXRWVW GSZG YVGGVI NZPV GSRH KIVGGB KVLKOV DROO SZEV VZHRVI GRNV HLOERMT SLKV NZWV HRNKOV carrat URYYP JIGGY EDGRPOJNCUWQZVMK */}" - ] - }, - { - "cell_type": "markdown", - "id": "eee1912d-afc9-40bc-be44-a1748492d753", - "metadata": {}, - "source": [ - "# Quantum key distribution\n", - "\n", - "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", - "- `qiskit` v2.1.0 or newer\n", - "- `qiskit-ibm-runtime` v0.40.1 or newer\n", - "- `qiskit-aer` v0.17.0 or newer\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", - "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "This module was tested and used 5 seconds of QPU time. This is an estimate only. Your actual usage may vary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "434034b6-22ab-484a-b4fd-8aab34d2b30a", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "6e0f0185-e80b-4560-84ae-c24ea7d5ab8a", - "metadata": {}, - "source": [ - "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/R0SOqLwLOR0?si=a0AujghPklDN4iBb) to watch it on YouTube.\n", - "\n", - "-------\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "f431a6f8-4c58-4549-9d32-fc4e2ba777aa", - "metadata": {}, - "source": [ - "## Introduction and motivation\n", - "\n", - "There are infinitely many ways of encrypting and decrypting information, and literally thousands of ways have been well-studied. Here, we will restrict ourselves to a very early and very simple method of encryption, called \"simple replacement\", in order to focus on the quantum part of this protocol. The quantum part could be adapted to many other protocols with relatively few changes.\n", - "\n", - "### Simple replacement\n", - "\n", - "A simple replacement encryption is one in which one letter or number is replaced with another, such that there is a 1:1 mapping from the letters and numbers in a message, to the letters and numbers being used in an encrypted sequence. A pop-culture instance of these is the cryptoquote or cryptogram puzzle, in which a quote or phrase is encrypted using simple replacement, and the player is tasked with decrypting it. These are easy to solve if they are long enough. Consider the example:\n", - "\n", - "R WVXRWVW GSZG R’W YVGGVI NZPV GSRH KIVGGB\n", - "OLMT. GSZG DZB, KVLKOV DROO SZEV ZM VZHRVI\n", - "GRNV HLOERMT RG. R SLKV R NZWV RG HRNKOV\n", - "VMLFTS.\n", - "\n", - "People who solve these by hand mostly use tricks involving familiarity with the structure of the language of the original message. For example, in English, the only one-letter words like the encrypted \"R\" are \"a\" and \"I\". The double letters encrypted in, for example, \"KIVGGB\" can only take certain values. There are subtler things that give clues like the most common word fitting the \"GSZG\" pattern is \"that\". People using code to solve this have many more options, including simply scanning through possibilities until an English word is recovered, and updating while preserving that word. One simple but powerful method is using letter frequency, especially when the message is long enough to constitute a representative sample of English.\n", - "\n", - "### Check-in question\n", - "\n", - "Try your hand at decrypting this if you like, though it is not necessary for the rest of the module. Click the \"Answer\" below to see the message.\n", - "\n", - "\n", - "\n", - "\n", - "I decided that I’d better make this pretty long. That way, people will have an easier time solving it. I hope I made it simple enough.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "e73c7eb5-6bf7-40d9-ab7b-cd407d737f23", - "metadata": {}, - "source": [ - "The example above is associated with a \"key\", a mapping from the encrypted to the decrypted letters. In this case, the key is:\n", - "\n", - "- A (not used, let’s call it Z)\n", - "- B->Y\n", - "- C (not used, let’s call it X)\n", - "- D->W\n", - "- E->V\n", - "- F->U\n", - "- ...\n", - "\n", - "And so on. To put it mildly, this is not a good key. Keys in which the encrypted and decrypted letters are simply shifted version of the alphabet (like A->B and B->C) are called \"Caesar shift\" ciphers.\n", - "\n", - "Note that these are very difficult if they are short. In fact, if they are very short, they are indeterminate. Consider:\n", - "\n", - "URYYP\n", - "\n", - "There are many possible decryptions, using different keys: HELLO, PETTY, HAPPY, JIGGY, STOOL. Can you think of others?\n", - "\n", - "But if you send many messages like this, eventually, the encryption will be cracked. So, you shouldn’t use the same “key” too often. In fact, best is if you use a certain substitution only once. Not in only one message, but *only for one single character!* By this, we mean you’ll have an encryption scheme or key for each character used in the message, in order. If you want to send a message to a friend using this message, you and your friend would need a pad a paper (in ye olden times) on which this ever-changing key is written. You will use this only once. This is called a “one-time pad”." - ] - }, - { - "cell_type": "markdown", - "id": "c890314b-7eb2-48f0-87af-63ffca346c58", - "metadata": {}, - "source": [ - "### The one-time pad\n", - "\n", - "Let’s see how this works with an example. One could do this entirely with letters, but it is common to convert from letters to numbers, say, by assigning A=0, B=1, C=2….\n", - "Suppose we are friends involved in clandestine activities and we have shared a pad. Ideally, we would share many pads, but today’s is:\n", - "\n", - "EDGRPOJNCUWQZVMK…\n", - "\n", - "Or, converting to numbers by placement in the alphabet:\n", - "\n", - "4,3,6,17,15, 14, 9, 13, 2, 20, 22, 16, 25, 21, 12, 10…\n", - "\n", - "Let us suppose, I want to share with you, the message:\n", - "\n", - "“I love quantum!”\n", - "\n", - "Or, equivalently:\n", - "\n", - "8, 11, 14, 21, 4, 16, 20, 0, 13, 19, 20, 12\n", - "\n", - "We don’t want to send the above code; that is a simple substitution, which is not at all secure. We want to combine this with our key in some way. A common way is addition modulo 26. We add the value of the message to the value of the key, mod 26, until we reach the end of the message. So, we would send\n", - "\n", - "8+4 (mod 26) = 12, 11+3 (mod 26) = 14, 14+6 (mod 26) = 20, 21+17 (mod 26) = 12…\n", - "\n", - "= 12, 14, 20, 12, 19, 4, 3, 13, 15, 13, 16, 2\n", - "\n", - "Note that if someone intercepts this and does NOT have the key, decrypting it is utterly hopeless! Not even the two “u”s in \"quantum\" are encoded with the same number! The first is a 3, and the second is a 16… in the same word!\n", - "\n", - "So, I send this to you, and you have the same key I do. You undo the addition modulo 26 which you know I carried out:\n", - "\n", - "12, 14, 20, 12, 19, 4, 3, 13, 15, 13, 16, 2\n", - "\n", - "=(4+x1) (mod 26), (3+x2) (mod 26), (6+x3) (mod 26), (17+x4) (mod 26),…\n", - "\n", - "Such that the message x1, x2, x3, x4… must be\n", - "\n", - "8, 11, 14, 21…\n", - "\n", - "Finally, converting this to text, we have\n", - "\n", - "“I love quantum”.\n", - "\n", - "This is a one-time pad.\n", - "\n", - "Note that if the key is shorter than the message, we start to repeat our encoding. That would still be a hard decryption problem to solve, but not impossible if it is repeated enough times. So, you need a long key (or “pad”).\n", - "\n", - "\n", - "\n", - "In many contexts, students will already be familiar with this encryption, such that this activity can be skipped. But it is a relatively quick, simple refresher.\n", - "\n", - "Step 1: Get a partner, and share a sequence of 4 letters to use as a key. Any class-appropriate 4-letter sequence will do. \\\n", - "Step 2: Select a 4-letter secret word you want to send to your partner (both partners do this so you send each other different secret words) \\\n", - "Step 3: Convert the 4-letter key/pad and each of the 4-letter secret words to numbers using A = 1, B = 2, and so on. \\\n", - "Step 4: Combine your 4-letter word with the one-time pad using modulo 26 addition. \\\n", - "Step 5: Hand your partner the sequence of numbers encoding your secret word, and your partner will hand you theirs. \\\n", - "Step 6: Decode each other’s words using modulo 26 subtraction. \\\n", - "Step 7: Verify. Did it work?\n", - "\n", - "#### Follow-up\n", - "\n", - "Swap encrypted words with a different group, that does not have access to your one-time pad. Can you decrypt it? Explain why or why not?\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "7959630e-aa0a-4a40-bfa9-af2cb58803f7", - "metadata": {}, - "source": [ - "Hopefully the activity above makes it clear that a one-time pad is an unbreakable form of encryption, given a few assumptions, like:\n", - "- The key is the same length as the message being sent, or longer\n", - "- The key is truly random\n", - "- The key is used only once and then discarded\n", - "\n", - "So this is great. We have unbreakable encryption... unless someone gets our key. If someone gets our key, everything is decrypted. This difference between unbreakable encryption and having all our secrets exposed makes the sharing of a secure key extremely important. The goal of quantum key distribution is to leverage constraints that nature has imposed on quantum information to secure a shared key/one-time pad." - ] - }, - { - "cell_type": "markdown", - "id": "d772bbcf-ed71-4ff3-8eca-06095ec430db", - "metadata": {}, - "source": [ - "## Using quantum states as a key\n", - "\n", - "Let's assume we are working with qubits (emphasizing that qubits have two eigenstates). One could use quantum systems with higher numbers of quantum states, but the state-of-the-art quantum computers at IBM® use qubits. It’s no problem to encode our A, B, C, into sequences of 0’s and 1’s. So, it is sufficient for us to share a key of 0’s and 1’s and do addition modulo 2 on each bit storing a letter.\n", - "\n", - "#### Check your understanding\n", - "\n", - "If we really only care about English letters, how many bits do we need?\n", - "\n", - "\n", - "\n", - "\n", - "$$\n", - "2^4=16\\\\\n", - "2^5 = 32 \\rightarrow 5 \\text{ bits}\n", - "$$\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Our friends, Alice and Bob would like to share a quantum key in such a way that no one else can intercept it (at least not without them knowing). They need to have a way of sending quantum states to each other. Doing this with high fidelity and no noise/errors is NOT trivial. But there are two approaches tha we should be able to understand at this point:\n", - "1. A fiber-optic cable allows you to send light… which is very quantum-mechanical. Single photons can be detected with high fidelity over many kilometers of fiber optic cable. This is not a perfect, error-free quantum channel, but it could be very good.\n", - "2. We could use quantum teleportation, as described in a previous module. That is, Alice and Bob could share entangled qubits and a state could be sent from Alice to Bob using the teleportation protocol.\n", - "\n", - "For this module, we don't want to require you to have high-fidelity optic setups for sharing photons, so we will use the second method for sharing quantum states. But this is not to say that it is the most realistic for long-distance sharing of quantum keys.\n", - "\n", - "We will now explore a protocol first laid out by [Charles Bennett and Gilles Brassard in 1984](https://www.sciencedirect.com/science/article/pii/S0304397514004241?via%3Dihub) for sharing states measured in different bases from Alice to Bob. We will use a clever measurement regimen to build up a key for use in later encryption. In other words, we are distributing a quantum key between two parties who wish to communicate, hence \"quantum key distribution\" (QKD).\n", - "\n", - "### QKD step 1: Alice's random bits and random bases\n", - "\n", - "Alice will start out by generating a random sequence of 0's and 1's. She will then randomly select a basis in which to prepare a quantum state, based on each random bit, using the table below (a table that Bob also has):\n", - "\n", - "| Basis | bit = 0 | bit = 1 |\n", - "|---------|----------------|----------------|\n", - "| Z | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ |\n", - "| X | $\\vert +\\rangle$ | $\\vert -\\rangle$ |\n", - "\n", - "For example, let us suppose Alice randomly generated a 0, and randomly selected the X basis. Then she would prepare a quantum state $|\\psi\\rangle = |+\\rangle_x = \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$. One can certainly leverage quantum randomness to generate a random set of 0's and 1's, and a random basis choice. For now, let's simply assume a random set has been generated, as follows:\n", - "\n", - "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", - "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", - "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", - "\n", - "This set of random bits, bases, and resulting states would continue in a long sequence, to give a key of sufficient length.\n", - "\n", - "### QKD step 2: Bob's random bases\n", - "\n", - "Bob also makes a random choice of bases. However, whereas Alice was using the basis choice to prepare her state, Bob will actually make measurements in these bases. If Bob makes a measurement in the same basis in which Alice prepared the the state, then we can predict the outcome of Bob's measurement. When Bob happens to pick a different basis from the basis Alice used in preparation, we cannot know the outcome of Bob's measurement.\n", - "\n", - "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", - "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", - "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", - "| Bob's states (a priori) | $\\vert +\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", - "| Bob's states (measured) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|" - ] - }, - { - "cell_type": "markdown", - "id": "2a414fb1-1f9a-47f1-aa1e-6f586539b5d4", - "metadata": {}, - "source": [ - "In the table below, consider the first column. Alice has prepared the state $\\vert +\\rangle,$ which is an eigenstate of X. Since Bob has also randomly chosen to measure in the X basis, there is only one possible outcome for Bob's measured state: $\\vert +\\rangle.$ In the second column, however, they have chosen different bases. The state Alice sent is $\\vert -\\rangle = \\frac{1}{\\sqrt{2}}(\\vert 0\\rangle-\\vert 1 \\rangle).$ This has a 50% chance of being measured by Bob in the $\\vert 0\\rangle$ state, and a 50% chance of being measured in $\\vert 1\\rangle.$ So the row showing what we know, a priori, about Bob's measurements cannot be filled in for column 2. But Bob will make a measurement and obtain an eigenstate of (in that column) Z. In the bottom row, we fill in what these measurements happened to yield." - ] - }, - { - "cell_type": "markdown", - "id": "0f097d83-7489-4427-9f5e-a8955589c47c", - "metadata": {}, - "source": [ - "### QKD step 3: Public discussion of bases\n", - "\n", - "Alice and Bob can now share with each other what basis they chose in each case. For all the columns in which they happened to choose the same basis, they each know for certain what state the other had. Bob can convert the state and basis to a 0 or 1 according to the convention shared by both parties. We can rewrite the table above to show only the instances where Alice's and Bob's bases matched:\n", - "\n", - "| Alice's bits | 0 | 0 | 1 | 0 | 0 | ... |\n", - "|---------|----------|----------|---------|----------|--------|---|\n", - "| Alice's bases | X | Z | X | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bases | X | Z | X | Z | X | X | ...|\n", - "| Bob's states (a priori) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's states (measured) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bits | 0 | 0 | 1 | 0 | 0 |...|\n", - "\n", - "Alice has successfully transmitted the bit string 00100... to Bob. If the friends agreed ahead of time to use 5-bit strings as numbers in their one-time pad, these first five bits would give them the number $4 = 0\\times2^4+0\\times2^3+1\\times2^2+0\\times2^1+0\\times2^0.$\n", - "\n", - "### QKD step 4: Verify and send secret\n", - "\n", - "Before Alice and Bob go any further, they should choose a subset of their classical bits to compare. Since they have only kept measurements of qubits which were prepared and measured using the same basis, all the measured values should agree. If there were a very small percentage that did not agree, this could be attributable to quantum noise or errors. But if many do not agree, something has gone wrong!\n", - "\n", - "Here we will not address what fraction of the key should be used for verification. For now, we will assume that this check goes well; we will revisit this in the section below on eavesdropping.\n", - "\n", - "The friends would then send an encrypted message to each other using classical channels. They would then use the numbers in their one-time pad to encrypt/decrypt secret messages, without ever transmitting the one-time pad from one location to another. For the next section on eavesdropping, please keep in mind that all this sharing of the key happens prior to the revelation of the encrypted secret via classical channels.\n", - "\n", - "Alice and Bob communicated their basis of choice via classical channels, so couldn't that be intercepted? Yes! But knowing the basis they used for measurement does not tell you what bit they sent or obtained. That is only possible if you also know Alice's starting bits. But then you would be in Alice's computer, where the secrets are stored, and secret communication of the secrets becomes moot. So interception of the classical communication does not break the encryption. But what about intercepting information in the quantum channel?" - ] - }, - { - "cell_type": "markdown", - "id": "6a57eebd-2525-4613-a4a0-2e40e507c4c7", - "metadata": {}, - "source": [ - "## Resistance of QKD to eavesdropping\n", - "\n", - "Alice and Bob have a friend Eve, who is notorious for eavesdropping. Eve wishes to intercept Alice's and Bob's quantum key, so that she may use it to decrypt messages sent between the two. This would necessarily happen between Alice's preparation of the states and Bob's measurement of the states, since the measurement collapses the quantum state. In particular, this means the eavesdropping would have to occur *before* there has been any sharing or comparison of bases.\n", - "\n", - "Eve must guess which base was used in encoding each bit. Again, if she is not able to access Alice's computer, she has nothing on which to base this guess, and it will be random. Let us assume Alice's start is the same as before, and let us further assume that Bob's random choice of measurement basis is the same as before. Let's fill in what Eve obtains if she makes measurements of the quantum channel. As before, if Eve happens to choose the same basis as Alice, we know what she will obtain. If not, she could obtain either of two outcomes, each with a 50% probability.\n", - "\n", - "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", - "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", - "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's guess bases | Z | X | X | Z | X | Z | Z | X | X | ...|\n", - "| Eve's states (a priori) | ? | $\\vert -\\rangle$ | ? | $\\vert 0\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", - "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", - "\n", - "Now because Eve has no idea whether she matched Alice's basis or not, she does not know what to transmit on to Bob to match Alice's original states. When Eve measures, for example, $|0\\rangle,$ all she knows for certain is that Alice did *not* prepare the state $|1\\rangle$ for that qubit. But Alice could have prepared $|0\\rangle,$ $|+\\rangle,$ or $|-\\rangle.$ All could be consistent with Eve's measurement. So Eve must make a choice. She might send on exactly the state she measured, or she might try to guess instances in which her measurement was not the eigenstate sent by Alice. We will include a mixture in our table:\n", - "\n", - "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", - "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", - "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's guess bases | Z | X | X | Z | X | Z | Z | X | X | ...|\n", - "| Eve's states (a priori) | ? | $\\vert -\\rangle$ | ? | $\\vert 0\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", - "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's states (sent on) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ |...|\n", - "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", - "| Bob's states (a priori) | ? | $\\vert 0\\rangle$ | ? | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's states (measured) | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bits | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | ... |\n", - "\n", - "At this point, it is reasonable to ask, \"Why doesn't Eve just make a copy of Alice's quantum state, keep one to measure, and transmit the other on to Bob?\" The answer is the [\"no-cloning\" theorem](https://en.wikipedia.org/wiki/No-cloning_theorem). Informally, it says that there is no unitary (quantum mechanical) operation that can make a second copy of an arbitrary quantum state, while preserving the first copy. The proof is relatively simple, and is left as a guided exercise. But for now, understand that Eve making copies of the quantum state is forbidden by fundamental laws of nature, and this is a principle strength of QKD." - ] - }, - { - "cell_type": "markdown", - "id": "df2fef68-8192-4cc3-90b4-16c22ab52299", - "metadata": {}, - "source": [ - "As before, Alice and Bob would call each other and compare bases. They will reduce this table to the cases where the two friends selected the same bases:\n", - "\n", - "| Alice's bits | 0 | 0 | 1 | 0 | 0 | ... |\n", - "|---------|----------|----------|---------|----------|--------|---|\n", - "| Alice's bases | X | Z | X | Z | X | ...|\n", - "| Alice's states | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's guess bases | Z | Z | Z | Z | X | ...|\n", - "| Eve's states (a priori) | ? | $\\vert 0\\rangle$ | ? | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Eve's states (sent on) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ |...|\n", - "| Bob's bases | X | Z | X | Z | X | ...|\n", - "| Bob's states (a priori) | ? | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's states (measured) | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", - "| Bob's bits | 1 | 0 | 0 | 0 | 0 | ... |\n", - "\n", - "Alice and Bob have once again communicated a bit string... but the strings don't match. The far left and middle bits are flipped. Looking at the previous table, you can trace this mismatch to the interference from Eve. Critically, note that we can do statistics on the match between our bitstrings now, while setting up the key, long before sharing our encrypted secret. Alice and Bob are free to use as many of their one-time pad bits as they like to check the security of their channel. If a single bit, or a very small percentage of bits did not match, this might be attributable to noise or errors. But a substantial fraction of mismatches indicates eavesdropping. The meaning of \"substantial\" here depends a bit on the noise in the setup being used; what is means for IBM® quantum computers is discussed below when we implement this protocol. If excess errors are detected, Alice and Bob do not share the secret, and they can begin hunting the eavesdropper.\n", - "\n", - "### Caveats\n", - "\n", - "Proving security is extremely difficult. In fact the protocol loosely described here was proposed in 1984, and wasn't proved secure until 16 years later [Shor & Preskill, 2000](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.85.441). There are many subtleties that are beyond the scope of this introduction. But we will briefly list a few to demonstrate that the topic is more complex than illustrated here.\n", - "* __Secure channels:__ When Alice sends her qubits through some quantum setup (a channel), and in particular when she hears classical responses back from someone, we have assumed that the someone is actually Bob. If Eve infiltrated this setup in such a way that __all__ Alice's communication was actually happening with Eve, and __all__ Bob's communication was actually being done with Eve, then Eve has effectively obtained a key, and can learn secrets. One must first ensure \"secure channels\", a process with a different set of protocols that we have not addressed here.\n", - "* __Assumptions about Eve:__ To truly prove security, we can't make assumptions about Eve's behavior; she could always confound our expectations. Here, to give concrete examples, we are making assumptions. For example, we might assume that the states Eve sends on to Bob are always exactly those she obtained upon measurement. Or we might assume that she randomly chooses a state experimentally consistent with her measurement. More fundamentally, the language here assumes that Eve actually makes a measurement, as opposed to storing the state on another quantum system and sending on a random qubit to Bob. These assumptions are fine to understand the protocol, but they do mean we are not proving anything in full generality.\n", - "* __Privacy amplification:__ Alice and Bob are not required to use the quantum key exactly as transmitted. They can, for example, apply a hash function to the shared key. This would exploit the fact that the eavesdropper has incomplete knowledge of the key to produce a shorter, but secure, shared key." - ] - }, - { - "cell_type": "markdown", - "id": "0ff2a010-6c55-475e-84e8-e57e832ff47c", - "metadata": {}, - "source": [ - "## Experiment 1: QKD with no eavesdropper\n", - "\n", - "Let us implement the above protocol in the absence of an eavesdropper. We will do this first using a simulator, simply to understand the workflow.\n", - "\n", - "First, a note on quantum simulators: Most quantum problems involving more than ~30 qubits cannot be simulated by most computers. No classical computer, supercomputer or GPU can simulate the full range of behavior of a 127-qubit quantum computer. Usually, the motivation for using real quantum computers is that the many entangled qubits cannot be simulated. In this case, there is no entanglement of qubits, unless we use the teleportation scheme to move information. In this case, the motivation for using real quantum computers is different: it is the no-cloning theorem. A classical computer simulating a qubit could send information about a quantum state from Alice to Bob, but if this classical information were intercepted, it could easily be duplicated, and Eve could keep a perfect copy, while sending another to Bob. This is not possible with real quantum states.\n", - "\n", - "IBM Quantum recommends tackling quantum computing problems using a framework we call \"Qiskit patterns\". It consists of the following steps.\n", - "- Step 1: Map your problem to a quantum circuit\n", - "- Step 2: Optimize your circuit for running on real quantum hardware\n", - "- Step 3: Execute your job on IBM quantum computers using Runtime primitives\n", - "- Step 4: Post-process the results\n", - "\n", - "### Qiskit patterns step 1: Map your problem to a quantum circuit\n", - "\n", - "In this case, the mapping of our problem to quantum circuits reduces to simply preparing Alice's states, and then including Bob's measurements. We start with the random bit and random basis selection." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "23a8a5c3-8f59-4130-8f2c-8b2bd50ee698", - "metadata": {}, - "outputs": [], - "source": [ - "# Qiskit patterns step 1: Map your problem to quantum circuit\n", - "# Import some generic packages\n", - "\n", - "import numpy as np\n", - "from qiskit import QuantumCircuit\n", - "\n", - "# Set up a random number generator and a quantum circuit. We choose to start with 20 bits, though any number <30 should be fine.\n", - "\n", - "rng = np.random.default_rng()\n", - "bit_num = 20\n", - "qc = QuantumCircuit(bit_num, bit_num)\n", - "\n", - "# QKD step 1: Random bits and bases for Alice\n", - "# generate Alice's random bits\n", - "\n", - "abits = np.round(rng.random(bit_num))\n", - "\n", - "# generate Alice's random measurement bases. Here we will associate a \"0\" with the Z basis, and a \"1\" with the X basis.\n", - "\n", - "abase = np.round(rng.random(bit_num))\n", - "\n", - "# Alice's state preparation. Check that this creates states according to table 1\n", - "\n", - "for n in range(bit_num):\n", - " if abits[n] == 0:\n", - " if abase[n] == 1:\n", - " qc.h(n)\n", - " if abits[n] == 1:\n", - " if abase[n] == 0:\n", - " qc.x(n)\n", - " if abase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "qc.barrier()\n", - "\n", - "# QKD step 2: Random bases for Bob\n", - "# generate Bob's random measurement bases.\n", - "\n", - "bbase = np.round(rng.random(bit_num))\n", - "\n", - "# Note that if Bob measures in Z no gates are necessary, since IBM Quantum computers measure in Z by default.\n", - "# If Bob measures in the X basis, we implement a hadamard gate qc.h to facilitate the measurement.\n", - "\n", - "for m in range(bit_num):\n", - " if bbase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(m, m)" - ] - }, - { - "cell_type": "markdown", - "id": "3965ff1b-65ed-42d3-8063-470233225660", - "metadata": {}, - "source": [ - "Let's visualize the bits, bases, and circuit. Note that sometimes the bases match, and sometimes they do not." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2b538952-9e01-43a0-a1e6-a798683f93f0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Alice's bits are [1. 1. 0. 1. 0. 1. 1. 0. 0. 1. 0. 0. 1. 0. 0. 0. 1. 0. 0. 0.]\n", - "Alice's bases are [0. 0. 0. 1. 1. 0. 0. 0. 0. 1. 1. 1. 1. 1. 0. 1. 1. 0. 1. 0.]\n", - "Bob's bases are [0. 1. 1. 0. 1. 0. 1. 1. 0. 0. 1. 1. 0. 0. 1. 0. 1. 1. 0. 0.]\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(\"Alice's bits are \", abits)\n", - "print(\"Alice's bases are \", abase)\n", - "print(\"Bob's bases are \", bbase)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "03e23821-4bf8-48eb-9b95-46be2d8d6cc8", - "metadata": {}, - "source": [ - "### Qiskit patterns step 2: Optimize problem for quantum execution\n", - "\n", - "This step takes the operations we want to perform and expresses them in terms of the functionality of a specific quantum computer. It also maps our problem onto the layout of the quantum computer.\n", - "\n", - "We will start by loading several packages that are required to communicate with IBM quantum computers. We must also select a backend on which to run. We can either choose the least busy backend, or select a specific backend whose properties we know. Although we will momentarily use a simulator, it is important to use a reasonable noise model in simulation, and it is good to keep the workflow as close as possible to what we will use later for real quantum computers.\n", - "\n", - "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8dd83eea-3fae-4ec8-b2d9-22128c6abb93", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_brisbane\n" - ] - } - ], - "source": [ - "# Load the Qiskit Runtime service\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Load the Qiskit Runtime service\n", - "\n", - "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", - "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", - "\n", - "# Load saved credentials\n", - "service = QiskitRuntimeService()\n", - "\n", - "\n", - "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", - "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "print(backend.name)" - ] - }, - { - "cell_type": "markdown", - "id": "5ce76c75-23c0-48ff-ac22-3f22a19bf1e3", - "metadata": {}, - "source": [ - "Below we select a simulator and noise model." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "9032820e-bf2c-4e3c-b61c-d83779121fa3", - "metadata": {}, - "outputs": [], - "source": [ - "# Load the backend sampler\n", - "from qiskit.primitives import BackendSamplerV2\n", - "\n", - "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "# Load the qiskit runtime sampler\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "\n", - "noise_model = NoiseModel.from_backend(backend)\n", - "\n", - "# Define a simulator using Aer, and use it in Sampler.\n", - "backend_sim = AerSimulator(noise_model=noise_model)\n", - "sampler_sim = BackendSamplerV2(backend=backend_sim)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "c21e3999-dedf-4577-b675-0ee57cef41a9", - "metadata": {}, - "outputs": [], - "source": [ - "# Qiskit patterns step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)" - ] - }, - { - "cell_type": "markdown", - "id": "bf22ac2d-e91c-49c7-bbea-751267929235", - "metadata": {}, - "source": [ - "### Qiskit patterns step 3: Execute\n", - "\n", - "Use the sampler to run your job, with the circuit as an argument." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9e41f310-6ae8-4a99-bd8b-5f70ad9d9ec4", - "metadata": {}, - "outputs": [], - "source": [ - "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([qc_isa], shots=1)\n", - "# job = sampler_sim.run([qc], shots = 1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "9877e564-3218-45ad-9c9a-c4cdf3cace4b", - "metadata": {}, - "source": [ - "### Qiskit patterns step 4: Post-processing\n", - "\n", - "Here we interpret our results and extract useful information. We might try visualizing the output of our sampler, but we have used sampler in an unconventional way. Rather than making many measurements of our circuit and developing statistics on the states, we have made only one measurement (Bob's). Any qubit with a state that was prepared and measured in the same basis should have a deterministic outcome, such that only one measurement is necessary. Those qubits with states prepared and measured in different bases (which would have probabilistic outcomes and would require many measurements to interpret) will not be used to build up our one-time pad/key." - ] - }, - { - "cell_type": "markdown", - "id": "84b34a33-a399-4cad-ab56-50b05e11801a", - "metadata": {}, - "source": [ - "Let us extract a list of measurement outcomes from this bitstring. Take care to reverse the order if comparing with Alice's bit array we used to generate the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "30ecc2db-65cf-433e-b0c7-962a725df013", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0]\n" - ] - } - ], - "source": [ - "# Get an array of bits\n", - "\n", - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "bmeas = list(key)\n", - "bmeas_ints = []\n", - "for n in range(bit_num):\n", - " bmeas_ints.append(int(bmeas[n]))\n", - "\n", - "# Reverse the order to match our input. See \"little endian\" notation.\n", - "\n", - "bbits = bmeas_ints[::-1]\n", - "\n", - "print(bbits)" - ] - }, - { - "cell_type": "markdown", - "id": "f60a50c5-7c4f-4da1-94a3-a1507b3753dd", - "metadata": {}, - "source": [ - "Let's compare the measurement bases randomly chosen by Alice and Bob. This was step 3 in our QKD protocol (public discussion of bases). Any time they chose the same basis for a qubit, we add the bits associated with that qubit to a list of bits for generating numbers in a one-time pad. When the bases do not match, the results are thrown out. Let us also check to see the two lists of bits agree, or if there were any losses due to noise or other factors." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a8dddc11-d4b9-4cf9-8944-be111f68c070", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1, 0, 1, 0, 0, 0, 1, 0]\n", - "[1, 0, 1, 0, 0, 0, 1, 0]\n", - "fidelity = 1.0\n", - "loss = 0.0\n" - ] - } - ], - "source": [ - "# QKD step 3: Public discussion of bases\n", - "\n", - "agoodbits = []\n", - "bgoodbits = []\n", - "match_count = 0\n", - "for n in range(bit_num):\n", - " # Check whether bases matched.\n", - " if abase[n] == bbase[n]:\n", - " agoodbits.append(int(abits[n]))\n", - " bgoodbits.append(bbits[n])\n", - " # If bits match when bases matched, increase count of matching bits\n", - " if int(abits[n]) == bbits[n]:\n", - " match_count += 1\n", - "\n", - "print(agoodbits)\n", - "print(bgoodbits)\n", - "print(\"fidelity = \", match_count / len(agoodbits))\n", - "print(\"loss = \", 1 - match_count / len(agoodbits))" - ] - }, - { - "cell_type": "markdown", - "id": "3842f053-a5c1-42c1-97e0-e64ac2e7364d", - "metadata": {}, - "source": [ - "Alice and Bob each have a list of bits, and they match with 100% fidelity. They can use these to generate numbers in a one-time pad. They can then use this in QKD step 4: sending and decrypting a secret. The present array of bits is too short to decrypt much of anything. We will return to this after including eavesdropping.\n", - "\n", - "#### Check your understanding\n", - "\n", - "Assume you need digits large enough to facilitate shifting letters in the English alphabet by that alphabet's full length, or more, although there are certainly other encoding schemes.\n", - "(a) How many letters long could a message be for it to be decrypted using the bits in the key above? (b) Must your answer agree with your classmates'? Why or why not?\n", - "\n", - "\n", - "\n", - "\n", - "(a) The answer depends on how many randomly-chosen bases matched between Alice and Bob. Since there is roughly a 50-50 chance of the bases matching for any given qubit, we expect close to 10 of our bits to be useful. 9 or 11 will be perfectly common. Even 4 or 15 are not outside the realm of possibility. 5 bits are required to shift by a number greater than or equal to the length of the English alphabet, meaning you can apply the shift to one letter for every 5 bits you have. If you have at least 5 bits shared by Alice and Bob, you can encode a single letter. If you have at least 10, you can encode 2 letters, and so on.\n", - "\n", - "(b) It need not agree, for the reasons outlined in (a).\n", - "\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "00a56f30-6b75-4f8d-b67a-47671063218b", - "metadata": {}, - "source": [ - "## Experiment 2: QKD with an eavesdropper\n", - "\n", - "We will implement exactly the same protocol as before. This time, we will insert another set of measurements, by Eve, between Alice and Bob." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "bfe1e1a9-c723-4bda-81f6-e5e3caf0efbf", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister\n", - "\n", - "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", - "# QKD step 1: Random bits and bases for Alice\n", - "\n", - "bit_num = 20\n", - "qr = QuantumRegister(bit_num, \"q\")\n", - "cr = ClassicalRegister(bit_num, \"c\")\n", - "qc = QuantumCircuit(qr, cr)\n", - "\n", - "# Alice's random bits and bases, as before\n", - "\n", - "abits = np.round(rng.random(bit_num))\n", - "abase = np.round(rng.random(bit_num))\n", - "\n", - "# Alice's state preparation, as before\n", - "\n", - "for n in range(bit_num):\n", - " if abits[n] == 0:\n", - " if abase[n] == 1:\n", - " qc.h(n)\n", - " if abits[n] == 1:\n", - " if abase[n] == 0:\n", - " qc.x(n)\n", - " if abase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "qc.barrier()\n", - "\n", - "# Eavesdropping happens here!\n", - "# Generate Eve's random measurement bases\n", - "\n", - "ebase = np.round(rng.random(bit_num))\n", - "\n", - "for m in range(bit_num):\n", - " if ebase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(qr[m], cr[m])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e20dd109-d1a2-4bec-86df-4b70c5b76cc4", - "metadata": {}, - "outputs": [], - "source": [ - "# Qiskit patterns step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "cfb2f614-8730-47ee-85c7-686b638e0def", - "metadata": {}, - "outputs": [], - "source": [ - "# Qiskit patterns step 3: Execute\n", - "job = sampler_sim.run([qc_isa], shots=1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "0ba084b8-9a71-4b20-a13f-59ba3fba67cf", - "metadata": {}, - "source": [ - "Qiskit patterns step 4 (post-processing) is simple in this case. There is no need to visualize the distribution of measurements, since we made only one measurement. Eve has the following bits:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "57dc8ffa-70aa-4a2b-9fdc-42a22bf9f52e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]\n" - ] - } - ], - "source": [ - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "emeas = list(key)\n", - "emeas_ints = []\n", - "for n in range(bit_num):\n", - " emeas_ints.append(int(emeas[n]))\n", - "ebits = emeas_ints[::-1]\n", - "\n", - "print(ebits)" - ] - }, - { - "cell_type": "markdown", - "id": "7fc91fcf-3704-498e-8291-58fe07fd3433", - "metadata": {}, - "source": [ - "Now Eve must reconstruct states to send on to Bob. A described in the introduction, she has no way of knowing if she guessed the encoding bases correctly, so she is not able to prepare exactly the same states that were sent. She could assume every basis choice was correct and encode exactly what she measured, or she could assume she chose the basis incorrectly and choose either eigenstate of the opposite basis. Here, we assume the former, for simplicity. We accomplish this by constructing a whole new quantum circuit, repeating Qiskit patterns steps as before." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "7feadfe3-c63e-41e0-9d51-a1b98b5ce67d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1]\n" - ] - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "# Qiskit patterns step 1: Mapping your problem onto a quantum circuit\n", - "# QKD step 1: Eve uses her measurements to prepare best guess states to send on to Bob\n", - "\n", - "qr = QuantumRegister(bit_num, \"q\")\n", - "cr = ClassicalRegister(bit_num, \"c\")\n", - "qc = QuantumCircuit(qr, cr)\n", - "\n", - "# Eve's state preparation\n", - "\n", - "for n in range(bit_num):\n", - " if ebits[n] == 0:\n", - " if ebase[n] == 1:\n", - " qc.h(n)\n", - " if ebits[n] == 1:\n", - " if ebase[n] == 0:\n", - " qc.x(n)\n", - " if ebase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "qc.barrier()\n", - "\n", - "# QKD step 2: Random bases for Bob\n", - "\n", - "bbase = np.round(rng.random(bit_num))\n", - "\n", - "for m in range(bit_num):\n", - " if bbase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(qr[m], cr[m])\n", - "\n", - "# Qiskit patterns step 2: Transpile\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)\n", - "\n", - "\n", - "# Qiskit patterns step 3: Execute\n", - "\n", - "job = sampler_sim.run([qc_isa], shots=1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()\n", - "\n", - "# Qiskit patterns step 4: Post-processing\n", - "\n", - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "bmeas = list(key)\n", - "bmeas_ints = []\n", - "for n in range(bit_num):\n", - " bmeas_ints.append(int(bmeas[n]))\n", - "bbits = bmeas_ints[::-1]\n", - "\n", - "print(bbits)" - ] - }, - { - "cell_type": "markdown", - "id": "211519fb-9f0e-4542-8ec6-49a8fc4032a3", - "metadata": {}, - "source": [ - "Let us now compare Alice's and Bob's bits:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d973b397-d67c-4d54-9a78-cb6606ca65aa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[1, 1, 0, 0, 0, 1, 1]\n", - "[1, 1, 0, 0, 0, 0, 1]\n", - "fidelity = 0.8571428571428571\n", - "loss = 0.1428571428571429\n" - ] - } - ], - "source": [ - "agoodbits = []\n", - "bgoodbits = []\n", - "match_count = 0\n", - "for n in range(bit_num):\n", - " if abase[n] == bbase[n]:\n", - " agoodbits.append(int(abits[n]))\n", - " bgoodbits.append(bbits[n])\n", - " if int(abits[n]) == bbits[n]:\n", - " match_count += 1\n", - "print(agoodbits)\n", - "print(bgoodbits)\n", - "print(\"fidelity = \", match_count / len(agoodbits))\n", - "print(\"loss = \", 1 - match_count / len(agoodbits))" - ] - }, - { - "cell_type": "markdown", - "id": "5783dd96-2fc3-4f9f-a822-51b7782121f5", - "metadata": {}, - "source": [ - "Previously, there was a perfect match between the bits in Alice's and Bob's keys. Now, from Eve's interference, we see the bits of Alice and Bob are different in 14% of cases that should match due to Alice and Bob selecting the same bases. This should be easy for Alice and Bob to detect. However, relying on a percentage of errors like this does mean that there is a limit to how much noise we can tolerate in the quantum channel.\n", - "\n", - "## Experiment 3: Compare QKD with and without eavesdropping on a real quantum computer\n", - "\n", - "Let's run this on a real quantum computer. That way we can leverage the no-cloning theorem. At the same time, real quantum computers have noise, and have higher error rates than classical computers. So let's compare loss in fidelity of our key bits with and without eavesdropping, to make sure the difference is detectable when using a real quantum comptuter. We'll start in the absence of eavesdropping:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "2697eace-2faf-49cc-99f2-d8d7bda28588", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Alice's bits = [0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1]\n", - "Bob's bits = [0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1]\n", - "fidelity = 0.9682539682539683\n", - "loss = 0.031746031746031744\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 3 sec to run, with 127 qubits.\n", - "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", - "\n", - "bit_num = 127\n", - "qc = QuantumCircuit(bit_num, bit_num)\n", - "\n", - "# QKD step 1: Generate Alice's random bits and bases\n", - "\n", - "abits = np.round(rng.random(bit_num))\n", - "abase = np.round(rng.random(bit_num))\n", - "\n", - "# Alice's state preparation\n", - "\n", - "for n in range(bit_num):\n", - " if abits[n] == 0:\n", - " if abase[n] == 1:\n", - " qc.h(n)\n", - " if abits[n] == 1:\n", - " if abase[n] == 0:\n", - " qc.x(n)\n", - " if abase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "# QKD step 2: Random bases for Bob\n", - "\n", - "bbase = np.round(rng.random(bit_num))\n", - "\n", - "for m in range(bit_num):\n", - " if bbase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(m, m)\n", - "\n", - "\n", - "# Qiskit patterns step 2: Transpilation\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)\n", - "\n", - "# Load the Runtime primitive and session\n", - "sampler = Sampler(mode=backend)\n", - "\n", - "# Qiskit patterns step 3: Execute\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()\n", - "\n", - "# Qiskit patterns step 4: Post-processing\n", - "# Extract Bob's bits\n", - "\n", - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "bmeas = list(key)\n", - "bmeas_ints = []\n", - "for n in range(bit_num):\n", - " bmeas_ints.append(int(bmeas[n]))\n", - "bbits = bmeas_ints[::-1]\n", - "\n", - "# Compare Alice's and Bob's measurement bases and collect usable bits\n", - "\n", - "agoodbits = []\n", - "bgoodbits = []\n", - "match_count = 0\n", - "for n in range(bit_num):\n", - " if abase[n] == bbase[n]:\n", - " agoodbits.append(int(abits[n]))\n", - " bgoodbits.append(bbits[n])\n", - " if int(abits[n]) == bbits[n]:\n", - " match_count += 1\n", - "\n", - "# Print some results\n", - "\n", - "print(\"Alice's bits = \", agoodbits)\n", - "print(\"Bob's bits = \", bgoodbits)\n", - "print(\"fidelity = \", match_count / len(agoodbits))\n", - "print(\"loss = \", 1 - match_count / len(agoodbits))" - ] - }, - { - "cell_type": "markdown", - "id": "22813855-8077-4598-bcf2-33bd7c2c37f8", - "metadata": {}, - "source": [ - "With no eavesdropping, we obtained 100% fidelity over this set of 127 trial bits, resulting in 55 matched bases and usable key bits.\n", - "Now let's repeat this experiment with Eve listening in:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "b311cf3a-ccea-43fe-8bcd-a054296e113e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Alice's bits = [1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1]\n", - "Bob's bits = [1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1]\n", - "fidelity = 0.7619047619047619\n", - "loss = 0.23809523809523814\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 2 s to run, with 127 qubits.\n", - "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", - "\n", - "bit_num = 127\n", - "qr = QuantumRegister(bit_num, \"q\")\n", - "cr = ClassicalRegister(bit_num, \"c\")\n", - "qc = QuantumCircuit(qr, cr)\n", - "\n", - "# QKD step 1: Generate Alice's random bits and bases\n", - "\n", - "abits = np.round(rng.random(bit_num))\n", - "abase = np.round(rng.random(bit_num))\n", - "\n", - "# Alice's state preparation\n", - "\n", - "for n in range(bit_num):\n", - " if abits[n] == 0:\n", - " if abase[n] == 1:\n", - " qc.h(n)\n", - " if abits[n] == 1:\n", - " if abase[n] == 0:\n", - " qc.x(n)\n", - " if abase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "\n", - "# Eavesdropping happens here!\n", - "# Generate Eve's random measurement bases\n", - "\n", - "ebase = np.round(rng.random(bit_num))\n", - "\n", - "for m in range(bit_num):\n", - " if ebase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(qr[m], cr[m])\n", - "\n", - "# Qiskit patterns step 2: Transpile\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)\n", - "\n", - "sampler = Sampler(mode=backend)\n", - "\n", - "# Qiskit patterns step 3: Execute\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()\n", - "\n", - "# Qiskit patterns step 4: Post-processing\n", - "# Extract Eve's bits\n", - "\n", - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "emeas = list(key)\n", - "emeas_ints = []\n", - "for n in range(bit_num):\n", - " emeas_ints.append(int(emeas[n]))\n", - "ebits = emeas_ints[::-1]\n", - "\n", - "# print(ebits)\n", - "\n", - "# Restart process\n", - "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", - "\n", - "# QKD step 1: Eve uses her measurements above to prepare best guess states to send on to Bob\n", - "\n", - "qr = QuantumRegister(bit_num, \"q\")\n", - "cr = ClassicalRegister(bit_num, \"c\")\n", - "qc = QuantumCircuit(qr, cr)\n", - "\n", - "\n", - "# Eve's state preparation\n", - "\n", - "for n in range(bit_num):\n", - " if ebits[n] == 0:\n", - " if ebase[n] == 1:\n", - " qc.h(n)\n", - " if ebits[n] == 1:\n", - " if ebase[n] == 0:\n", - " qc.x(n)\n", - " if ebase[n] == 1:\n", - " qc.x(n)\n", - " qc.h(n)\n", - "\n", - "# QKD step 2: Random bases for Bob\n", - "\n", - "bbase = np.round(rng.random(bit_num))\n", - "\n", - "for m in range(bit_num):\n", - " if bbase[m] == 1:\n", - " qc.h(m)\n", - " qc.measure(qr[m], cr[m])\n", - "\n", - "# Qiskit patterns step 2: Transpile\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)\n", - "\n", - "# Qiskit patterns step 3: Execute\n", - "\n", - "job = sampler.run([qc_isa], shots=1)\n", - "counts = job.result()[0].data.c.get_counts()\n", - "countsint = job.result()[0].data.c.get_int_counts()\n", - "\n", - "# Qiskit Patterns step 4: Post-processing\n", - "# Extract Bob's bits\n", - "\n", - "keys = counts.keys()\n", - "key = list(keys)[0]\n", - "bmeas = list(key)\n", - "bmeas_ints = []\n", - "for n in range(bit_num):\n", - " bmeas_ints.append(int(bmeas[n]))\n", - "bbits = bmeas_ints[::-1]\n", - "\n", - "# Compare Alice's and Bob's bases, when they are the same, keep the bits.\n", - "\n", - "agoodbits = []\n", - "bgoodbits = []\n", - "match_count = 0\n", - "for n in range(bit_num):\n", - " if abase[n] == bbase[n]:\n", - " agoodbits.append(int(abits[n]))\n", - " bgoodbits.append(bbits[n])\n", - " if int(abits[n]) == bbits[n]:\n", - " match_count += 1\n", - "\n", - "# Print some results\n", - "\n", - "print(\"Alice's bits = \", agoodbits)\n", - "print(\"Bob's bits = \", bgoodbits)\n", - "print(\"fidelity = \", match_count / len(agoodbits))\n", - "print(\"loss = \", 1 - match_count / len(agoodbits))" - ] - }, - { - "cell_type": "markdown", - "id": "1d3fc14a-00e2-476e-a7a2-711c0c9ea6ff", - "metadata": {}, - "source": [ - "Here we found almost 23% loss of fidelity in shared bits due to eavesdropping! This is very detectable! Note that transferring quantum information over long distances could still introduce additional noise and errors. Assuring that eavesdropping can be detected, even in the presence of noise, and even when Eve uses all tricks at her disposal is a complex field beyond this introduction." - ] - }, - { - "cell_type": "markdown", - "id": "4bb09e34-a249-4ab8-be2b-0205fe1ffefb", - "metadata": {}, - "source": [ - "## Questions\n", - "\n", - "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", - "\n", - "### Critical concepts\n", - "\n", - "- Quantum information cannot be copied or \"cloned\".\n", - "- You *can* repeat the same preparation process to make an ensemble of quantum states which are all the same, or nearly the same.\n", - "- An encryption/decryption key (a one-time pad) can be shared between two friends using quantum states.\n", - "- Two friends randomly choosing a measurement basis means that half the time they will choose differently, and will have to toss out the information on those qubits.\n", - "- The random choice of measurement basis also ensures that an eavesdropper cannot know the initial state prepared, and thus cannot recreate the sent state. This ensures that the eavesdropping will be detected.\n", - "\n", - "### T/F questions\n", - "\n", - "1. T/F In quantum key distribution, the two communicating partners measure each qubit in the same basis.\n", - "2. T/F An eavesdropper intercepting quantum information in QKD is prevented by the laws of nature from copying the quantum state they intercept.\n", - "3. T/F A one-time pad is a key for encrypting/decrypting secure messages in which a particular encoding scheme is used only once, for a single piece of information (like one letter of the alphabet).\n", - "\n", - "\n", - "### MC questions\n", - "\n", - "1. Select the option that best completes the statement. As described in this module, a one-time pad is a set of encryption/decryption keys that is used...\n", - "\n", - "- a. Only once for a single piece of information, like a single letter.\n", - "- b. Only once for a single message.\n", - "- c. Only once for a set period of time, like a day.\n", - "- d. Until there is evidence of eavesdropping.\n", - "\n", - "2. Assume Alice and Bob choose their measurement bases randomly. They measure. Then they share their measurement bases, and keep only the bits of information from cases where they used the same basis. Up to some random fluctuation, approximately what percentage of their qubits should yield usable bits of information?\n", - "- a. 100%\n", - "- b. 50%\n", - "- c. 25%\n", - "- d. 12.5%\n", - "- e. 0%\n", - "\n", - "3. After Alice and Bob select cases in which they used the same measurement bases, what percentage of those bits of information should match, if quantum noise and errors were negligible?\n", - "- a. 100%\n", - "- b. 50%\n", - "- c. 25%\n", - "- d. 12.5%\n", - "- e. 0%\n", - "\n", - "4. Assume Alice has chosen her measurement bases randomly. Eve also chooses her bases randomly, and she listens in (measures). She sends states on to Bob that are consistent with her measurements. Alice and Bob compare basis choices and keep only the qubits measured/prepared by them in the same bases. Up to some random fluctuation, approximately what percentage of those kept qubit measurements will match, according to Alice and Bob?\n", - "- a. 100%\n", - "- b. 75%\n", - "- c. 50%\n", - "- d. 25%\n", - "- e. 12.5%\n", - "- f. 0%\n", - "\n", - "### Discussion questions\n", - "\n", - "1. Assume all basis choices are random for all participants, Alice, Bob, and Eve. Assume that after Eve listens in, she sends a state to Bob that is prepared in the same basis in which she measured, and which is consistent with that measurement. Convince your partners that 12.5% of all qubits initialized by Alice will yield measurement mismatches between Alice and Bob, indicating eavesdropping (ignoring quantum errors and noise).\n", - "Hint 1: Since there is no preferred basis, if you consider just one initial choice for Alice, the ratio for that one choice should be the same as the ratio for the sum of all choices.\n", - "Hint 2: It may not be sufficient to count the number of ways something could happen, since some outcomes may occur with different probabilities.\n", - "\n", - "\n", - "2. Assume again that all basis choices are random for all participants, Alice, Bob, and Eve. But now, consider that Eve is free to send along any state she likes after her measurement. She could even try sending states that are inconsistent with her own measurements. Discuss with your partners/neighbors whether you think there is any choice of bases that could reduce the average percentage of qubits that indicate eavesdropping to Alice and Bob." - ] - } - ], - "metadata": { - "in_page_toc_max_heading_level": 2, - "in_page_toc_min_heading_level": 2, - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "290dd4cc-b40a-4d36-9bba-3deda83df5c2", + "metadata": {}, + "source": [ + "---\n", + "title: Quantum Key Distribution\n", + "description: This module explores how to use quantum states to securely share a key for encryption and detect potential eavesdroppers.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore cryptoquote WVXRWVW GSZG YVGGVI NZPV GSRH KIVGGB KVLKOV DROO SZEV VZHRVI GRNV HLOERMT SLKV NZWV HRNKOV carrat URYYP JIGGY EDGRPOJNCUWQZVMK */}" + ] + }, + { + "cell_type": "markdown", + "id": "eee1912d-afc9-40bc-be44-a1748492d753", + "metadata": {}, + "source": [ + "# Quantum key distribution\n", + "\n", + "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", + "- `qiskit` v2.1.0 or newer\n", + "- `qiskit-ibm-runtime` v0.40.1 or newer\n", + "- `qiskit-aer` v0.17.0 or newer\n", + "- `qiskit.visualization`\n", + "- `numpy`\n", + "- `pylatexenc`\n", + "\n", + "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", + "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", + "\n", + "This module was tested and used 5 seconds of QPU time. This is an estimate only. Your actual usage may vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "434034b6-22ab-484a-b4fd-8aab34d2b30a", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and modify this line as needed to install dependencies\n", + "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" + ] + }, + { + "cell_type": "markdown", + "id": "6e0f0185-e80b-4560-84ae-c24ea7d5ab8a", + "metadata": {}, + "source": [ + "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/R0SOqLwLOR0?si=a0AujghPklDN4iBb) to watch it on YouTube.\n", + "\n", + "-------\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "f431a6f8-4c58-4549-9d32-fc4e2ba777aa", + "metadata": {}, + "source": [ + "## Introduction and motivation\n", + "\n", + "There are infinitely many ways of encrypting and decrypting information, and literally thousands of ways have been well-studied. Here, we will restrict ourselves to a very early and very simple method of encryption, called \"simple replacement\", in order to focus on the quantum part of this protocol. The quantum part could be adapted to many other protocols with relatively few changes.\n", + "\n", + "### Simple replacement\n", + "\n", + "A simple replacement encryption is one in which one letter or number is replaced with another, such that there is a 1:1 mapping from the letters and numbers in a message, to the letters and numbers being used in an encrypted sequence. A pop-culture instance of these is the cryptoquote or cryptogram puzzle, in which a quote or phrase is encrypted using simple replacement, and the player is tasked with decrypting it. These are easy to solve if they are long enough. Consider the example:\n", + "\n", + "R WVXRWVW GSZG R’W YVGGVI NZPV GSRH KIVGGB\n", + "OLMT. GSZG DZB, KVLKOV DROO SZEV ZM VZHRVI\n", + "GRNV HLOERMT RG. R SLKV R NZWV RG HRNKOV\n", + "VMLFTS.\n", + "\n", + "People who solve these by hand mostly use tricks involving familiarity with the structure of the language of the original message. For example, in English, the only one-letter words like the encrypted \"R\" are \"a\" and \"I\". The double letters encrypted in, for example, \"KIVGGB\" can only take certain values. There are subtler things that give clues like the most common word fitting the \"GSZG\" pattern is \"that\". People using code to solve this have many more options, including simply scanning through possibilities until an English word is recovered, and updating while preserving that word. One simple but powerful method is using letter frequency, especially when the message is long enough to constitute a representative sample of English.\n", + "\n", + "### Check-in question\n", + "\n", + "Try your hand at decrypting this if you like, though it is not necessary for the rest of the module. Click the \"Answer\" below to see the message.\n", + "\n", + "\n", + "\n", + "\n", + "I decided that I’d better make this pretty long. That way, people will have an easier time solving it. I hope I made it simple enough.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "e73c7eb5-6bf7-40d9-ab7b-cd407d737f23", + "metadata": {}, + "source": [ + "The example above is associated with a \"key\", a mapping from the encrypted to the decrypted letters. In this case, the key is:\n", + "\n", + "- A (not used, let’s call it Z)\n", + "- B->Y\n", + "- C (not used, let’s call it X)\n", + "- D->W\n", + "- E->V\n", + "- F->U\n", + "- ...\n", + "\n", + "And so on. To put it mildly, this is not a good key. Keys in which the encrypted and decrypted letters are simply shifted version of the alphabet (like A->B and B->C) are called \"Caesar shift\" ciphers.\n", + "\n", + "Note that these are very difficult if they are short. In fact, if they are very short, they are indeterminate. Consider:\n", + "\n", + "URYYP\n", + "\n", + "There are many possible decryptions, using different keys: HELLO, PETTY, HAPPY, JIGGY, STOOL. Can you think of others?\n", + "\n", + "But if you send many messages like this, eventually, the encryption will be cracked. So, you shouldn’t use the same “key” too often. In fact, best is if you use a certain substitution only once. Not in only one message, but *only for one single character!* By this, we mean you’ll have an encryption scheme or key for each character used in the message, in order. If you want to send a message to a friend using this message, you and your friend would need a pad a paper (in ye olden times) on which this ever-changing key is written. You will use this only once. This is called a “one-time pad”." + ] + }, + { + "cell_type": "markdown", + "id": "c890314b-7eb2-48f0-87af-63ffca346c58", + "metadata": {}, + "source": [ + "### The one-time pad\n", + "\n", + "Let’s see how this works with an example. One could do this entirely with letters, but it is common to convert from letters to numbers, say, by assigning A=0, B=1, C=2….\n", + "Suppose we are friends involved in clandestine activities and we have shared a pad. Ideally, we would share many pads, but today’s is:\n", + "\n", + "EDGRPOJNCUWQZVMK…\n", + "\n", + "Or, converting to numbers by placement in the alphabet:\n", + "\n", + "4,3,6,17,15, 14, 9, 13, 2, 20, 22, 16, 25, 21, 12, 10…\n", + "\n", + "Let us suppose, I want to share with you, the message:\n", + "\n", + "“I love quantum!”\n", + "\n", + "Or, equivalently:\n", + "\n", + "8, 11, 14, 21, 4, 16, 20, 0, 13, 19, 20, 12\n", + "\n", + "We don’t want to send the above code; that is a simple substitution, which is not at all secure. We want to combine this with our key in some way. A common way is addition modulo 26. We add the value of the message to the value of the key, mod 26, until we reach the end of the message. So, we would send\n", + "\n", + "8+4 (mod 26) = 12, 11+3 (mod 26) = 14, 14+6 (mod 26) = 20, 21+17 (mod 26) = 12…\n", + "\n", + "= 12, 14, 20, 12, 19, 4, 3, 13, 15, 13, 16, 2\n", + "\n", + "Note that if someone intercepts this and does NOT have the key, decrypting it is utterly hopeless! Not even the two “u”s in \"quantum\" are encoded with the same number! The first is a 3, and the second is a 16… in the same word!\n", + "\n", + "So, I send this to you, and you have the same key I do. You undo the addition modulo 26 which you know I carried out:\n", + "\n", + "12, 14, 20, 12, 19, 4, 3, 13, 15, 13, 16, 2\n", + "\n", + "=(4+x1) (mod 26), (3+x2) (mod 26), (6+x3) (mod 26), (17+x4) (mod 26),…\n", + "\n", + "Such that the message x1, x2, x3, x4… must be\n", + "\n", + "8, 11, 14, 21…\n", + "\n", + "Finally, converting this to text, we have\n", + "\n", + "“I love quantum”.\n", + "\n", + "This is a one-time pad.\n", + "\n", + "Note that if the key is shorter than the message, we start to repeat our encoding. That would still be a hard decryption problem to solve, but not impossible if it is repeated enough times. So, you need a long key (or “pad”).\n", + "\n", + "\n", + "\n", + "In many contexts, students will already be familiar with this encryption, such that this activity can be skipped. But it is a relatively quick, simple refresher.\n", + "\n", + "Step 1: Get a partner, and share a sequence of 4 letters to use as a key. Any class-appropriate 4-letter sequence will do. \\\n", + "Step 2: Select a 4-letter secret word you want to send to your partner (both partners do this so you send each other different secret words) \\\n", + "Step 3: Convert the 4-letter key/pad and each of the 4-letter secret words to numbers using A = 1, B = 2, and so on. \\\n", + "Step 4: Combine your 4-letter word with the one-time pad using modulo 26 addition. \\\n", + "Step 5: Hand your partner the sequence of numbers encoding your secret word, and your partner will hand you theirs. \\\n", + "Step 6: Decode each other’s words using modulo 26 subtraction. \\\n", + "Step 7: Verify. Did it work?\n", + "\n", + "#### Follow-up\n", + "\n", + "Swap encrypted words with a different group, that does not have access to your one-time pad. Can you decrypt it? Explain why or why not?\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "7959630e-aa0a-4a40-bfa9-af2cb58803f7", + "metadata": {}, + "source": [ + "Hopefully the activity above makes it clear that a one-time pad is an unbreakable form of encryption, given a few assumptions, like:\n", + "- The key is the same length as the message being sent, or longer\n", + "- The key is truly random\n", + "- The key is used only once and then discarded\n", + "\n", + "So this is great. We have unbreakable encryption... unless someone gets our key. If someone gets our key, everything is decrypted. This difference between unbreakable encryption and having all our secrets exposed makes the sharing of a secure key extremely important. The goal of quantum key distribution is to leverage constraints that nature has imposed on quantum information to secure a shared key/one-time pad." + ] + }, + { + "cell_type": "markdown", + "id": "d772bbcf-ed71-4ff3-8eca-06095ec430db", + "metadata": {}, + "source": [ + "## Using quantum states as a key\n", + "\n", + "Let's assume we are working with qubits (emphasizing that qubits have two eigenstates). One could use quantum systems with higher numbers of quantum states, but the state-of-the-art quantum computers at IBM® use qubits. It’s no problem to encode our A, B, C, into sequences of 0’s and 1’s. So, it is sufficient for us to share a key of 0’s and 1’s and do addition modulo 2 on each bit storing a letter.\n", + "\n", + "#### Check your understanding\n", + "\n", + "If we really only care about English letters, how many bits do we need?\n", + "\n", + "\n", + "\n", + "\n", + "$$\n", + "2^4=16\\\\\n", + "2^5 = 32 \\rightarrow 5 \\text{ bits}\n", + "$$\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Our friends, Alice and Bob would like to share a quantum key in such a way that no one else can intercept it (at least not without them knowing). They need to have a way of sending quantum states to each other. Doing this with high fidelity and no noise/errors is NOT trivial. But there are two approaches tha we should be able to understand at this point:\n", + "1. A fiber-optic cable allows you to send light… which is very quantum-mechanical. Single photons can be detected with high fidelity over many kilometers of fiber optic cable. This is not a perfect, error-free quantum channel, but it could be very good.\n", + "2. We could use quantum teleportation, as described in a previous module. That is, Alice and Bob could share entangled qubits and a state could be sent from Alice to Bob using the teleportation protocol.\n", + "\n", + "For this module, we don't want to require you to have high-fidelity optic setups for sharing photons, so we will use the second method for sharing quantum states. But this is not to say that it is the most realistic for long-distance sharing of quantum keys.\n", + "\n", + "We will now explore a protocol first laid out by [Charles Bennett and Gilles Brassard in 1984](https://www.sciencedirect.com/science/article/pii/S0304397514004241?via%3Dihub) for sharing states measured in different bases from Alice to Bob. We will use a clever measurement regimen to build up a key for use in later encryption. In other words, we are distributing a quantum key between two parties who wish to communicate, hence \"quantum key distribution\" (QKD).\n", + "\n", + "### QKD step 1: Alice's random bits and random bases\n", + "\n", + "Alice will start out by generating a random sequence of 0's and 1's. She will then randomly select a basis in which to prepare a quantum state, based on each random bit, using the table below (a table that Bob also has):\n", + "\n", + "| Basis | bit = 0 | bit = 1 |\n", + "|---------|----------------|----------------|\n", + "| Z | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ |\n", + "| X | $\\vert +\\rangle$ | $\\vert -\\rangle$ |\n", + "\n", + "For example, let us suppose Alice randomly generated a 0, and randomly selected the X basis. Then she would prepare a quantum state $|\\psi\\rangle = |+\\rangle_x = \\frac{1}{\\sqrt{2}}(|0\\rangle+|1\\rangle)$. One can certainly leverage quantum randomness to generate a random set of 0's and 1's, and a random basis choice. For now, let's simply assume a random set has been generated, as follows:\n", + "\n", + "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", + "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", + "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", + "\n", + "This set of random bits, bases, and resulting states would continue in a long sequence, to give a key of sufficient length.\n", + "\n", + "### QKD step 2: Bob's random bases\n", + "\n", + "Bob also makes a random choice of bases. However, whereas Alice was using the basis choice to prepare her state, Bob will actually make measurements in these bases. If Bob makes a measurement in the same basis in which Alice prepared the the state, then we can predict the outcome of Bob's measurement. When Bob happens to pick a different basis from the basis Alice used in preparation, we cannot know the outcome of Bob's measurement.\n", + "\n", + "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", + "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", + "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", + "| Bob's states (a priori) | $\\vert +\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", + "| Bob's states (measured) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|" + ] + }, + { + "cell_type": "markdown", + "id": "2a414fb1-1f9a-47f1-aa1e-6f586539b5d4", + "metadata": {}, + "source": [ + "In the table below, consider the first column. Alice has prepared the state $\\vert +\\rangle,$ which is an eigenstate of X. Since Bob has also randomly chosen to measure in the X basis, there is only one possible outcome for Bob's measured state: $\\vert +\\rangle.$ In the second column, however, they have chosen different bases. The state Alice sent is $\\vert -\\rangle = \\frac{1}{\\sqrt{2}}(\\vert 0\\rangle-\\vert 1 \\rangle).$ This has a 50% chance of being measured by Bob in the $\\vert 0\\rangle$ state, and a 50% chance of being measured in $\\vert 1\\rangle.$ So the row showing what we know, a priori, about Bob's measurements cannot be filled in for column 2. But Bob will make a measurement and obtain an eigenstate of (in that column) Z. In the bottom row, we fill in what these measurements happened to yield." + ] + }, + { + "cell_type": "markdown", + "id": "0f097d83-7489-4427-9f5e-a8955589c47c", + "metadata": {}, + "source": [ + "### QKD step 3: Public discussion of bases\n", + "\n", + "Alice and Bob can now share with each other what basis they chose in each case. For all the columns in which they happened to choose the same basis, they each know for certain what state the other had. Bob can convert the state and basis to a 0 or 1 according to the convention shared by both parties. We can rewrite the table above to show only the instances where Alice's and Bob's bases matched:\n", + "\n", + "| Alice's bits | 0 | 0 | 1 | 0 | 0 | ... |\n", + "|---------|----------|----------|---------|----------|--------|---|\n", + "| Alice's bases | X | Z | X | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bases | X | Z | X | Z | X | X | ...|\n", + "| Bob's states (a priori) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's states (measured) | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bits | 0 | 0 | 1 | 0 | 0 |...|\n", + "\n", + "Alice has successfully transmitted the bit string 00100... to Bob. If the friends agreed ahead of time to use 5-bit strings as numbers in their one-time pad, these first five bits would give them the number $4 = 0\\times2^4+0\\times2^3+1\\times2^2+0\\times2^1+0\\times2^0.$\n", + "\n", + "### QKD step 4: Verify and send secret\n", + "\n", + "Before Alice and Bob go any further, they should choose a subset of their classical bits to compare. Since they have only kept measurements of qubits which were prepared and measured using the same basis, all the measured values should agree. If there were a very small percentage that did not agree, this could be attributable to quantum noise or errors. But if many do not agree, something has gone wrong!\n", + "\n", + "Here we will not address what fraction of the key should be used for verification. For now, we will assume that this check goes well; we will revisit this in the section below on eavesdropping.\n", + "\n", + "The friends would then send an encrypted message to each other using classical channels. They would then use the numbers in their one-time pad to encrypt/decrypt secret messages, without ever transmitting the one-time pad from one location to another. For the next section on eavesdropping, please keep in mind that all this sharing of the key happens prior to the revelation of the encrypted secret via classical channels.\n", + "\n", + "Alice and Bob communicated their basis of choice via classical channels, so couldn't that be intercepted? Yes! But knowing the basis they used for measurement does not tell you what bit they sent or obtained. That is only possible if you also know Alice's starting bits. But then you would be in Alice's computer, where the secrets are stored, and secret communication of the secrets becomes moot. So interception of the classical communication does not break the encryption. But what about intercepting information in the quantum channel?" + ] + }, + { + "cell_type": "markdown", + "id": "6a57eebd-2525-4613-a4a0-2e40e507c4c7", + "metadata": {}, + "source": [ + "## Resistance of QKD to eavesdropping\n", + "\n", + "Alice and Bob have a friend Eve, who is notorious for eavesdropping. Eve wishes to intercept Alice's and Bob's quantum key, so that she may use it to decrypt messages sent between the two. This would necessarily happen between Alice's preparation of the states and Bob's measurement of the states, since the measurement collapses the quantum state. In particular, this means the eavesdropping would have to occur *before* there has been any sharing or comparison of bases.\n", + "\n", + "Eve must guess which base was used in encoding each bit. Again, if she is not able to access Alice's computer, she has nothing on which to base this guess, and it will be random. Let us assume Alice's start is the same as before, and let us further assume that Bob's random choice of measurement basis is the same as before. Let's fill in what Eve obtains if she makes measurements of the quantum channel. As before, if Eve happens to choose the same basis as Alice, we know what she will obtain. If not, she could obtain either of two outcomes, each with a 50% probability.\n", + "\n", + "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", + "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", + "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's guess bases | Z | X | X | Z | X | Z | Z | X | X | ...|\n", + "| Eve's states (a priori) | ? | $\\vert -\\rangle$ | ? | $\\vert 0\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", + "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", + "\n", + "Now because Eve has no idea whether she matched Alice's basis or not, she does not know what to transmit on to Bob to match Alice's original states. When Eve measures, for example, $|0\\rangle,$ all she knows for certain is that Alice did *not* prepare the state $|1\\rangle$ for that qubit. But Alice could have prepared $|0\\rangle,$ $|+\\rangle,$ or $|-\\rangle.$ All could be consistent with Eve's measurement. So Eve must make a choice. She might send on exactly the state she measured, or she might try to guess instances in which her measurement was not the eigenstate sent by Alice. We will include a mixture in our table:\n", + "\n", + "| Alice's bits | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | ... |\n", + "|---------|----------|--------|---------|----------|--------|---------|----------|--------|--------|---|\n", + "| Alice's bases | X | X | Z | Z | Z | X | Z | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's guess bases | Z | X | X | Z | X | Z | Z | X | X | ...|\n", + "| Eve's states (a priori) | ? | $\\vert -\\rangle$ | ? | $\\vert 0\\rangle$ | ? | ? | $\\vert 0\\rangle$ | ? | $\\vert +\\rangle$ |...|\n", + "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's states (sent on) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ |...|\n", + "| Bob's bases | X | Z | X | Z | X | X | Z | X | X | ...|\n", + "| Bob's states (a priori) | ? | $\\vert 0\\rangle$ | ? | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's states (measured) | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bits | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | ... |\n", + "\n", + "At this point, it is reasonable to ask, \"Why doesn't Eve just make a copy of Alice's quantum state, keep one to measure, and transmit the other on to Bob?\" The answer is the [\"no-cloning\" theorem](https://en.wikipedia.org/wiki/No-cloning_theorem). Informally, it says that there is no unitary (quantum mechanical) operation that can make a second copy of an arbitrary quantum state, while preserving the first copy. The proof is relatively simple, and is left as a guided exercise. But for now, understand that Eve making copies of the quantum state is forbidden by fundamental laws of nature, and this is a principle strength of QKD." + ] + }, + { + "cell_type": "markdown", + "id": "df2fef68-8192-4cc3-90b4-16c22ab52299", + "metadata": {}, + "source": [ + "As before, Alice and Bob would call each other and compare bases. They will reduce this table to the cases where the two friends selected the same bases:\n", + "\n", + "| Alice's bits | 0 | 0 | 1 | 0 | 0 | ... |\n", + "|---------|----------|----------|---------|----------|--------|---|\n", + "| Alice's bases | X | Z | X | Z | X | ...|\n", + "| Alice's states | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's guess bases | Z | Z | Z | Z | X | ...|\n", + "| Eve's states (a priori) | ? | $\\vert 0\\rangle$ | ? | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's states (measured) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Eve's states (sent on) | $\\vert 1\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert 0\\rangle$ |...|\n", + "| Bob's bases | X | Z | X | Z | X | ...|\n", + "| Bob's states (a priori) | ? | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's states (measured) | $\\vert -\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ | $\\vert 0\\rangle$ | $\\vert +\\rangle$ |...|\n", + "| Bob's bits | 1 | 0 | 0 | 0 | 0 | ... |\n", + "\n", + "Alice and Bob have once again communicated a bit string... but the strings don't match. The far left and middle bits are flipped. Looking at the previous table, you can trace this mismatch to the interference from Eve. Critically, note that we can do statistics on the match between our bitstrings now, while setting up the key, long before sharing our encrypted secret. Alice and Bob are free to use as many of their one-time pad bits as they like to check the security of their channel. If a single bit, or a very small percentage of bits did not match, this might be attributable to noise or errors. But a substantial fraction of mismatches indicates eavesdropping. The meaning of \"substantial\" here depends a bit on the noise in the setup being used; what is means for IBM® quantum computers is discussed below when we implement this protocol. If excess errors are detected, Alice and Bob do not share the secret, and they can begin hunting the eavesdropper.\n", + "\n", + "### Caveats\n", + "\n", + "Proving security is extremely difficult. In fact the protocol loosely described here was proposed in 1984, and wasn't proved secure until 16 years later [Shor & Preskill, 2000](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.85.441). There are many subtleties that are beyond the scope of this introduction. But we will briefly list a few to demonstrate that the topic is more complex than illustrated here.\n", + "* __Secure channels:__ When Alice sends her qubits through some quantum setup (a channel), and in particular when she hears classical responses back from someone, we have assumed that the someone is actually Bob. If Eve infiltrated this setup in such a way that __all__ Alice's communication was actually happening with Eve, and __all__ Bob's communication was actually being done with Eve, then Eve has effectively obtained a key, and can learn secrets. One must first ensure \"secure channels\", a process with a different set of protocols that we have not addressed here.\n", + "* __Assumptions about Eve:__ To truly prove security, we can't make assumptions about Eve's behavior; she could always confound our expectations. Here, to give concrete examples, we are making assumptions. For example, we might assume that the states Eve sends on to Bob are always exactly those she obtained upon measurement. Or we might assume that she randomly chooses a state experimentally consistent with her measurement. More fundamentally, the language here assumes that Eve actually makes a measurement, as opposed to storing the state on another quantum system and sending on a random qubit to Bob. These assumptions are fine to understand the protocol, but they do mean we are not proving anything in full generality.\n", + "* __Privacy amplification:__ Alice and Bob are not required to use the quantum key exactly as transmitted. They can, for example, apply a hash function to the shared key. This would exploit the fact that the eavesdropper has incomplete knowledge of the key to produce a shorter, but secure, shared key." + ] + }, + { + "cell_type": "markdown", + "id": "0ff2a010-6c55-475e-84e8-e57e832ff47c", + "metadata": {}, + "source": [ + "## Experiment 1: QKD with no eavesdropper\n", + "\n", + "Let us implement the above protocol in the absence of an eavesdropper. We will do this first using a simulator, simply to understand the workflow.\n", + "\n", + "First, a note on quantum simulators: Most quantum problems involving more than ~30 qubits cannot be simulated by most computers. No classical computer, supercomputer or GPU can simulate the full range of behavior of a 127-qubit quantum computer. Usually, the motivation for using real quantum computers is that the many entangled qubits cannot be simulated. In this case, there is no entanglement of qubits, unless we use the teleportation scheme to move information. In this case, the motivation for using real quantum computers is different: it is the no-cloning theorem. A classical computer simulating a qubit could send information about a quantum state from Alice to Bob, but if this classical information were intercepted, it could easily be duplicated, and Eve could keep a perfect copy, while sending another to Bob. This is not possible with real quantum states.\n", + "\n", + "IBM Quantum recommends tackling quantum computing problems using a framework we call \"Qiskit patterns\". It consists of the following steps.\n", + "- Step 1: Map your problem to a quantum circuit\n", + "- Step 2: Optimize your circuit for running on real quantum hardware\n", + "- Step 3: Execute your job on IBM quantum computers using Runtime primitives\n", + "- Step 4: Post-process the results\n", + "\n", + "### Qiskit patterns step 1: Map your problem to a quantum circuit\n", + "\n", + "In this case, the mapping of our problem to quantum circuits reduces to simply preparing Alice's states, and then including Bob's measurements. We start with the random bit and random basis selection." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "23a8a5c3-8f59-4130-8f2c-8b2bd50ee698", + "metadata": {}, + "outputs": [], + "source": [ + "# Qiskit patterns step 1: Map your problem to quantum circuit\n", + "# Import some generic packages\n", + "\n", + "import numpy as np\n", + "from qiskit import QuantumCircuit\n", + "\n", + "# Set up a random number generator and a quantum circuit. We choose to start with 20 bits, though\n", + "# any number <30 should be fine.\n", + "\n", + "rng = np.random.default_rng()\n", + "bit_num = 20\n", + "qc = QuantumCircuit(bit_num, bit_num)\n", + "\n", + "# QKD step 1: Random bits and bases for Alice\n", + "# generate Alice's random bits\n", + "\n", + "abits = np.round(rng.random(bit_num))\n", + "\n", + "# generate Alice's random measurement bases. Here we will associate a \"0\" with the Z basis, and a\n", + "# \"1\" with the X basis.\n", + "\n", + "abase = np.round(rng.random(bit_num))\n", + "\n", + "# Alice's state preparation. Check that this creates states according to table 1\n", + "\n", + "for n in range(bit_num):\n", + " if abits[n] == 0:\n", + " if abase[n] == 1:\n", + " qc.h(n)\n", + " if abits[n] == 1:\n", + " if abase[n] == 0:\n", + " qc.x(n)\n", + " if abase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "qc.barrier()\n", + "\n", + "# QKD step 2: Random bases for Bob\n", + "# generate Bob's random measurement bases.\n", + "\n", + "bbase = np.round(rng.random(bit_num))\n", + "\n", + "# Note that if Bob measures in Z no gates are necessary, since IBM Quantum computers measure in Z by\n", + "# default.\n", + "# If Bob measures in the X basis, we implement a hadamard gate qc.h to facilitate the measurement.\n", + "\n", + "for m in range(bit_num):\n", + " if bbase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(m, m)" + ] + }, + { + "cell_type": "markdown", + "id": "3965ff1b-65ed-42d3-8063-470233225660", + "metadata": {}, + "source": [ + "Let's visualize the bits, bases, and circuit. Note that sometimes the bases match, and sometimes they do not." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2b538952-9e01-43a0-a1e6-a798683f93f0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Alice's bits are [1. 1. 0. 1. 0. 1. 1. 0. 0. 1. 0. 0. 1. 0. 0. 0. 1. 0. 0. 0.]\n", + "Alice's bases are [0. 0. 0. 1. 1. 0. 0. 0. 0. 1. 1. 1. 1. 1. 0. 1. 1. 0. 1. 0.]\n", + "Bob's bases are [0. 1. 1. 0. 1. 0. 1. 1. 0. 0. 1. 1. 0. 0. 1. 0. 1. 1. 0. 0.]\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(\"Alice's bits are \", abits)\n", + "print(\"Alice's bases are \", abase)\n", + "print(\"Bob's bases are \", bbase)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "03e23821-4bf8-48eb-9b95-46be2d8d6cc8", + "metadata": {}, + "source": [ + "### Qiskit patterns step 2: Optimize problem for quantum execution\n", + "\n", + "This step takes the operations we want to perform and expresses them in terms of the functionality of a specific quantum computer. It also maps our problem onto the layout of the quantum computer.\n", + "\n", + "We will start by loading several packages that are required to communicate with IBM quantum computers. We must also select a backend on which to run. We can either choose the least busy backend, or select a specific backend whose properties we know. Although we will momentarily use a simulator, it is important to use a reasonable noise model in simulation, and it is good to keep the workflow as close as possible to what we will use later for real quantum computers.\n", + "\n", + "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd83eea-3fae-4ec8-b2d9-22128c6abb93", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_brisbane\n" + ] + } + ], + "source": [ + "# Load the Qiskit Runtime service\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Load the Qiskit Runtime service\n", + "\n", + "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance =\n", + "# '', token='', overwrite=True, set_as_default=True)\n", + "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", + "\n", + "# Load saved credentials\n", + "service = QiskitRuntimeService()\n", + "\n", + "\n", + "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", + "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "print(backend.name)" + ] + }, + { + "cell_type": "markdown", + "id": "5ce76c75-23c0-48ff-ac22-3f22a19bf1e3", + "metadata": {}, + "source": [ + "Below we select a simulator and noise model." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9032820e-bf2c-4e3c-b61c-d83779121fa3", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the backend sampler\n", + "from qiskit.primitives import BackendSamplerV2\n", + "\n", + "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "# Load the qiskit runtime sampler\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "\n", + "noise_model = NoiseModel.from_backend(backend)\n", + "\n", + "# Define a simulator using Aer, and use it in Sampler.\n", + "backend_sim = AerSimulator(noise_model=noise_model)\n", + "sampler_sim = BackendSamplerV2(backend=backend_sim)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c21e3999-dedf-4577-b675-0ee57cef41a9", + "metadata": {}, + "outputs": [], + "source": [ + "# Qiskit patterns step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)" + ] + }, + { + "cell_type": "markdown", + "id": "bf22ac2d-e91c-49c7-bbea-751267929235", + "metadata": {}, + "source": [ + "### Qiskit patterns step 3: Execute\n", + "\n", + "Use the sampler to run your job, with the circuit as an argument." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9e41f310-6ae8-4a99-bd8b-5f70ad9d9ec4", + "metadata": {}, + "outputs": [], + "source": [ + "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([qc_isa], shots=1)\n", + "# job = sampler_sim.run([qc], shots = 1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "9877e564-3218-45ad-9c9a-c4cdf3cace4b", + "metadata": {}, + "source": [ + "### Qiskit patterns step 4: Post-processing\n", + "\n", + "Here we interpret our results and extract useful information. We might try visualizing the output of our sampler, but we have used sampler in an unconventional way. Rather than making many measurements of our circuit and developing statistics on the states, we have made only one measurement (Bob's). Any qubit with a state that was prepared and measured in the same basis should have a deterministic outcome, such that only one measurement is necessary. Those qubits with states prepared and measured in different bases (which would have probabilistic outcomes and would require many measurements to interpret) will not be used to build up our one-time pad/key." + ] + }, + { + "cell_type": "markdown", + "id": "84b34a33-a399-4cad-ab56-50b05e11801a", + "metadata": {}, + "source": [ + "Let us extract a list of measurement outcomes from this bitstring. Take care to reverse the order if comparing with Alice's bit array we used to generate the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "30ecc2db-65cf-433e-b0c7-962a725df013", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0]\n" + ] + } + ], + "source": [ + "# Get an array of bits\n", + "\n", + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "bmeas = list(key)\n", + "bmeas_ints = []\n", + "for n in range(bit_num):\n", + " bmeas_ints.append(int(bmeas[n]))\n", + "\n", + "# Reverse the order to match our input. See \"little endian\" notation.\n", + "\n", + "bbits = bmeas_ints[::-1]\n", + "\n", + "print(bbits)" + ] + }, + { + "cell_type": "markdown", + "id": "f60a50c5-7c4f-4da1-94a3-a1507b3753dd", + "metadata": {}, + "source": [ + "Let's compare the measurement bases randomly chosen by Alice and Bob. This was step 3 in our QKD protocol (public discussion of bases). Any time they chose the same basis for a qubit, we add the bits associated with that qubit to a list of bits for generating numbers in a one-time pad. When the bases do not match, the results are thrown out. Let us also check to see the two lists of bits agree, or if there were any losses due to noise or other factors." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a8dddc11-d4b9-4cf9-8944-be111f68c070", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 0, 1, 0, 0, 0, 1, 0]\n", + "[1, 0, 1, 0, 0, 0, 1, 0]\n", + "fidelity = 1.0\n", + "loss = 0.0\n" + ] + } + ], + "source": [ + "# QKD step 3: Public discussion of bases\n", + "\n", + "agoodbits = []\n", + "bgoodbits = []\n", + "match_count = 0\n", + "for n in range(bit_num):\n", + " # Check whether bases matched.\n", + " if abase[n] == bbase[n]:\n", + " agoodbits.append(int(abits[n]))\n", + " bgoodbits.append(bbits[n])\n", + " # If bits match when bases matched, increase count of matching bits\n", + " if int(abits[n]) == bbits[n]:\n", + " match_count += 1\n", + "\n", + "print(agoodbits)\n", + "print(bgoodbits)\n", + "print(\"fidelity = \", match_count / len(agoodbits))\n", + "print(\"loss = \", 1 - match_count / len(agoodbits))" + ] + }, + { + "cell_type": "markdown", + "id": "3842f053-a5c1-42c1-97e0-e64ac2e7364d", + "metadata": {}, + "source": [ + "Alice and Bob each have a list of bits, and they match with 100% fidelity. They can use these to generate numbers in a one-time pad. They can then use this in QKD step 4: sending and decrypting a secret. The present array of bits is too short to decrypt much of anything. We will return to this after including eavesdropping.\n", + "\n", + "#### Check your understanding\n", + "\n", + "Assume you need digits large enough to facilitate shifting letters in the English alphabet by that alphabet's full length, or more, although there are certainly other encoding schemes.\n", + "(a) How many letters long could a message be for it to be decrypted using the bits in the key above? (b) Must your answer agree with your classmates'? Why or why not?\n", + "\n", + "\n", + "\n", + "\n", + "(a) The answer depends on how many randomly-chosen bases matched between Alice and Bob. Since there is roughly a 50-50 chance of the bases matching for any given qubit, we expect close to 10 of our bits to be useful. 9 or 11 will be perfectly common. Even 4 or 15 are not outside the realm of possibility. 5 bits are required to shift by a number greater than or equal to the length of the English alphabet, meaning you can apply the shift to one letter for every 5 bits you have. If you have at least 5 bits shared by Alice and Bob, you can encode a single letter. If you have at least 10, you can encode 2 letters, and so on.\n", + "\n", + "(b) It need not agree, for the reasons outlined in (a).\n", + "\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "00a56f30-6b75-4f8d-b67a-47671063218b", + "metadata": {}, + "source": [ + "## Experiment 2: QKD with an eavesdropper\n", + "\n", + "We will implement exactly the same protocol as before. This time, we will insert another set of measurements, by Eve, between Alice and Bob." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bfe1e1a9-c723-4bda-81f6-e5e3caf0efbf", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister\n", + "\n", + "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", + "# QKD step 1: Random bits and bases for Alice\n", + "\n", + "bit_num = 20\n", + "qr = QuantumRegister(bit_num, \"q\")\n", + "cr = ClassicalRegister(bit_num, \"c\")\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "# Alice's random bits and bases, as before\n", + "\n", + "abits = np.round(rng.random(bit_num))\n", + "abase = np.round(rng.random(bit_num))\n", + "\n", + "# Alice's state preparation, as before\n", + "\n", + "for n in range(bit_num):\n", + " if abits[n] == 0:\n", + " if abase[n] == 1:\n", + " qc.h(n)\n", + " if abits[n] == 1:\n", + " if abase[n] == 0:\n", + " qc.x(n)\n", + " if abase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "qc.barrier()\n", + "\n", + "# Eavesdropping happens here!\n", + "# Generate Eve's random measurement bases\n", + "\n", + "ebase = np.round(rng.random(bit_num))\n", + "\n", + "for m in range(bit_num):\n", + " if ebase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(qr[m], cr[m])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e20dd109-d1a2-4bec-86df-4b70c5b76cc4", + "metadata": {}, + "outputs": [], + "source": [ + "# Qiskit patterns step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cfb2f614-8730-47ee-85c7-686b638e0def", + "metadata": {}, + "outputs": [], + "source": [ + "# Qiskit patterns step 3: Execute\n", + "job = sampler_sim.run([qc_isa], shots=1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "0ba084b8-9a71-4b20-a13f-59ba3fba67cf", + "metadata": {}, + "source": [ + "Qiskit patterns step 4 (post-processing) is simple in this case. There is no need to visualize the distribution of measurements, since we made only one measurement. Eve has the following bits:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "57dc8ffa-70aa-4a2b-9fdc-42a22bf9f52e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]\n" + ] + } + ], + "source": [ + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "emeas = list(key)\n", + "emeas_ints = []\n", + "for n in range(bit_num):\n", + " emeas_ints.append(int(emeas[n]))\n", + "ebits = emeas_ints[::-1]\n", + "\n", + "print(ebits)" + ] + }, + { + "cell_type": "markdown", + "id": "7fc91fcf-3704-498e-8291-58fe07fd3433", + "metadata": {}, + "source": [ + "Now Eve must reconstruct states to send on to Bob. A described in the introduction, she has no way of knowing if she guessed the encoding bases correctly, so she is not able to prepare exactly the same states that were sent. She could assume every basis choice was correct and encode exactly what she measured, or she could assume she chose the basis incorrectly and choose either eigenstate of the opposite basis. Here, we assume the former, for simplicity. We accomplish this by constructing a whole new quantum circuit, repeating Qiskit patterns steps as before." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7feadfe3-c63e-41e0-9d51-a1b98b5ce67d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1]\n" + ] + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "# Qiskit patterns step 1: Mapping your problem onto a quantum circuit\n", + "# QKD step 1: Eve uses her measurements to prepare best guess states to send on to Bob\n", + "\n", + "qr = QuantumRegister(bit_num, \"q\")\n", + "cr = ClassicalRegister(bit_num, \"c\")\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "# Eve's state preparation\n", + "\n", + "for n in range(bit_num):\n", + " if ebits[n] == 0:\n", + " if ebase[n] == 1:\n", + " qc.h(n)\n", + " if ebits[n] == 1:\n", + " if ebase[n] == 0:\n", + " qc.x(n)\n", + " if ebase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "qc.barrier()\n", + "\n", + "# QKD step 2: Random bases for Bob\n", + "\n", + "bbase = np.round(rng.random(bit_num))\n", + "\n", + "for m in range(bit_num):\n", + " if bbase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(qr[m], cr[m])\n", + "\n", + "# Qiskit patterns step 2: Transpile\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)\n", + "\n", + "\n", + "# Qiskit patterns step 3: Execute\n", + "\n", + "job = sampler_sim.run([qc_isa], shots=1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()\n", + "\n", + "# Qiskit patterns step 4: Post-processing\n", + "\n", + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "bmeas = list(key)\n", + "bmeas_ints = []\n", + "for n in range(bit_num):\n", + " bmeas_ints.append(int(bmeas[n]))\n", + "bbits = bmeas_ints[::-1]\n", + "\n", + "print(bbits)" + ] + }, + { + "cell_type": "markdown", + "id": "211519fb-9f0e-4542-8ec6-49a8fc4032a3", + "metadata": {}, + "source": [ + "Let us now compare Alice's and Bob's bits:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d973b397-d67c-4d54-9a78-cb6606ca65aa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 1, 0, 0, 0, 1, 1]\n", + "[1, 1, 0, 0, 0, 0, 1]\n", + "fidelity = 0.8571428571428571\n", + "loss = 0.1428571428571429\n" + ] + } + ], + "source": [ + "agoodbits = []\n", + "bgoodbits = []\n", + "match_count = 0\n", + "for n in range(bit_num):\n", + " if abase[n] == bbase[n]:\n", + " agoodbits.append(int(abits[n]))\n", + " bgoodbits.append(bbits[n])\n", + " if int(abits[n]) == bbits[n]:\n", + " match_count += 1\n", + "print(agoodbits)\n", + "print(bgoodbits)\n", + "print(\"fidelity = \", match_count / len(agoodbits))\n", + "print(\"loss = \", 1 - match_count / len(agoodbits))" + ] + }, + { + "cell_type": "markdown", + "id": "5783dd96-2fc3-4f9f-a822-51b7782121f5", + "metadata": {}, + "source": [ + "Previously, there was a perfect match between the bits in Alice's and Bob's keys. Now, from Eve's interference, we see the bits of Alice and Bob are different in 14% of cases that should match due to Alice and Bob selecting the same bases. This should be easy for Alice and Bob to detect. However, relying on a percentage of errors like this does mean that there is a limit to how much noise we can tolerate in the quantum channel.\n", + "\n", + "## Experiment 3: Compare QKD with and without eavesdropping on a real quantum computer\n", + "\n", + "Let's run this on a real quantum computer. That way we can leverage the no-cloning theorem. At the same time, real quantum computers have noise, and have higher error rates than classical computers. So let's compare loss in fidelity of our key bits with and without eavesdropping, to make sure the difference is detectable when using a real quantum comptuter. We'll start in the absence of eavesdropping:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "2697eace-2faf-49cc-99f2-d8d7bda28588", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Alice's bits = [0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1]\n", + "Bob's bits = [0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1]\n", + "fidelity = 0.9682539682539683\n", + "loss = 0.031746031746031744\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 3 sec to run, with 127\n", + "# qubits.\n", + "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", + "\n", + "bit_num = 127\n", + "qc = QuantumCircuit(bit_num, bit_num)\n", + "\n", + "# QKD step 1: Generate Alice's random bits and bases\n", + "\n", + "abits = np.round(rng.random(bit_num))\n", + "abase = np.round(rng.random(bit_num))\n", + "\n", + "# Alice's state preparation\n", + "\n", + "for n in range(bit_num):\n", + " if abits[n] == 0:\n", + " if abase[n] == 1:\n", + " qc.h(n)\n", + " if abits[n] == 1:\n", + " if abase[n] == 0:\n", + " qc.x(n)\n", + " if abase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "# QKD step 2: Random bases for Bob\n", + "\n", + "bbase = np.round(rng.random(bit_num))\n", + "\n", + "for m in range(bit_num):\n", + " if bbase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(m, m)\n", + "\n", + "\n", + "# Qiskit patterns step 2: Transpilation\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)\n", + "\n", + "# Load the Runtime primitive and session\n", + "sampler = Sampler(mode=backend)\n", + "\n", + "# Qiskit patterns step 3: Execute\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()\n", + "\n", + "# Qiskit patterns step 4: Post-processing\n", + "# Extract Bob's bits\n", + "\n", + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "bmeas = list(key)\n", + "bmeas_ints = []\n", + "for n in range(bit_num):\n", + " bmeas_ints.append(int(bmeas[n]))\n", + "bbits = bmeas_ints[::-1]\n", + "\n", + "# Compare Alice's and Bob's measurement bases and collect usable bits\n", + "\n", + "agoodbits = []\n", + "bgoodbits = []\n", + "match_count = 0\n", + "for n in range(bit_num):\n", + " if abase[n] == bbase[n]:\n", + " agoodbits.append(int(abits[n]))\n", + " bgoodbits.append(bbits[n])\n", + " if int(abits[n]) == bbits[n]:\n", + " match_count += 1\n", + "\n", + "# Print some results\n", + "\n", + "print(\"Alice's bits = \", agoodbits)\n", + "print(\"Bob's bits = \", bgoodbits)\n", + "print(\"fidelity = \", match_count / len(agoodbits))\n", + "print(\"loss = \", 1 - match_count / len(agoodbits))" + ] + }, + { + "cell_type": "markdown", + "id": "22813855-8077-4598-bcf2-33bd7c2c37f8", + "metadata": {}, + "source": [ + "With no eavesdropping, we obtained 100% fidelity over this set of 127 trial bits, resulting in 55 matched bases and usable key bits.\n", + "Now let's repeat this experiment with Eve listening in:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b311cf3a-ccea-43fe-8bcd-a054296e113e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Alice's bits = [1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1]\n", + "Bob's bits = [1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1]\n", + "fidelity = 0.7619047619047619\n", + "loss = 0.23809523809523814\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 2 s to run, with 127\n", + "# qubits.\n", + "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", + "\n", + "bit_num = 127\n", + "qr = QuantumRegister(bit_num, \"q\")\n", + "cr = ClassicalRegister(bit_num, \"c\")\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "# QKD step 1: Generate Alice's random bits and bases\n", + "\n", + "abits = np.round(rng.random(bit_num))\n", + "abase = np.round(rng.random(bit_num))\n", + "\n", + "# Alice's state preparation\n", + "\n", + "for n in range(bit_num):\n", + " if abits[n] == 0:\n", + " if abase[n] == 1:\n", + " qc.h(n)\n", + " if abits[n] == 1:\n", + " if abase[n] == 0:\n", + " qc.x(n)\n", + " if abase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "\n", + "# Eavesdropping happens here!\n", + "# Generate Eve's random measurement bases\n", + "\n", + "ebase = np.round(rng.random(bit_num))\n", + "\n", + "for m in range(bit_num):\n", + " if ebase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(qr[m], cr[m])\n", + "\n", + "# Qiskit patterns step 2: Transpile\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)\n", + "\n", + "sampler = Sampler(mode=backend)\n", + "\n", + "# Qiskit patterns step 3: Execute\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()\n", + "\n", + "# Qiskit patterns step 4: Post-processing\n", + "# Extract Eve's bits\n", + "\n", + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "emeas = list(key)\n", + "emeas_ints = []\n", + "for n in range(bit_num):\n", + " emeas_ints.append(int(emeas[n]))\n", + "ebits = emeas_ints[::-1]\n", + "\n", + "# print(ebits)\n", + "\n", + "# Restart process\n", + "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", + "\n", + "# QKD step 1: Eve uses her measurements above to prepare best guess states to send on to Bob\n", + "\n", + "qr = QuantumRegister(bit_num, \"q\")\n", + "cr = ClassicalRegister(bit_num, \"c\")\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "\n", + "# Eve's state preparation\n", + "\n", + "for n in range(bit_num):\n", + " if ebits[n] == 0:\n", + " if ebase[n] == 1:\n", + " qc.h(n)\n", + " if ebits[n] == 1:\n", + " if ebase[n] == 0:\n", + " qc.x(n)\n", + " if ebase[n] == 1:\n", + " qc.x(n)\n", + " qc.h(n)\n", + "\n", + "# QKD step 2: Random bases for Bob\n", + "\n", + "bbase = np.round(rng.random(bit_num))\n", + "\n", + "for m in range(bit_num):\n", + " if bbase[m] == 1:\n", + " qc.h(m)\n", + " qc.measure(qr[m], cr[m])\n", + "\n", + "# Qiskit patterns step 2: Transpile\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)\n", + "\n", + "# Qiskit patterns step 3: Execute\n", + "\n", + "job = sampler.run([qc_isa], shots=1)\n", + "counts = job.result()[0].data.c.get_counts()\n", + "countsint = job.result()[0].data.c.get_int_counts()\n", + "\n", + "# Qiskit Patterns step 4: Post-processing\n", + "# Extract Bob's bits\n", + "\n", + "keys = counts.keys()\n", + "key = list(keys)[0]\n", + "bmeas = list(key)\n", + "bmeas_ints = []\n", + "for n in range(bit_num):\n", + " bmeas_ints.append(int(bmeas[n]))\n", + "bbits = bmeas_ints[::-1]\n", + "\n", + "# Compare Alice's and Bob's bases, when they are the same, keep the bits.\n", + "\n", + "agoodbits = []\n", + "bgoodbits = []\n", + "match_count = 0\n", + "for n in range(bit_num):\n", + " if abase[n] == bbase[n]:\n", + " agoodbits.append(int(abits[n]))\n", + " bgoodbits.append(bbits[n])\n", + " if int(abits[n]) == bbits[n]:\n", + " match_count += 1\n", + "\n", + "# Print some results\n", + "\n", + "print(\"Alice's bits = \", agoodbits)\n", + "print(\"Bob's bits = \", bgoodbits)\n", + "print(\"fidelity = \", match_count / len(agoodbits))\n", + "print(\"loss = \", 1 - match_count / len(agoodbits))" + ] + }, + { + "cell_type": "markdown", + "id": "1d3fc14a-00e2-476e-a7a2-711c0c9ea6ff", + "metadata": {}, + "source": [ + "Here we found almost 23% loss of fidelity in shared bits due to eavesdropping! This is very detectable! Note that transferring quantum information over long distances could still introduce additional noise and errors. Assuring that eavesdropping can be detected, even in the presence of noise, and even when Eve uses all tricks at her disposal is a complex field beyond this introduction." + ] + }, + { + "cell_type": "markdown", + "id": "4bb09e34-a249-4ab8-be2b-0205fe1ffefb", + "metadata": {}, + "source": [ + "## Questions\n", + "\n", + "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", + "\n", + "### Critical concepts\n", + "\n", + "- Quantum information cannot be copied or \"cloned\".\n", + "- You *can* repeat the same preparation process to make an ensemble of quantum states which are all the same, or nearly the same.\n", + "- An encryption/decryption key (a one-time pad) can be shared between two friends using quantum states.\n", + "- Two friends randomly choosing a measurement basis means that half the time they will choose differently, and will have to toss out the information on those qubits.\n", + "- The random choice of measurement basis also ensures that an eavesdropper cannot know the initial state prepared, and thus cannot recreate the sent state. This ensures that the eavesdropping will be detected.\n", + "\n", + "### T/F questions\n", + "\n", + "1. T/F In quantum key distribution, the two communicating partners measure each qubit in the same basis.\n", + "2. T/F An eavesdropper intercepting quantum information in QKD is prevented by the laws of nature from copying the quantum state they intercept.\n", + "3. T/F A one-time pad is a key for encrypting/decrypting secure messages in which a particular encoding scheme is used only once, for a single piece of information (like one letter of the alphabet).\n", + "\n", + "\n", + "### MC questions\n", + "\n", + "1. Select the option that best completes the statement. As described in this module, a one-time pad is a set of encryption/decryption keys that is used...\n", + "\n", + "- a. Only once for a single piece of information, like a single letter.\n", + "- b. Only once for a single message.\n", + "- c. Only once for a set period of time, like a day.\n", + "- d. Until there is evidence of eavesdropping.\n", + "\n", + "2. Assume Alice and Bob choose their measurement bases randomly. They measure. Then they share their measurement bases, and keep only the bits of information from cases where they used the same basis. Up to some random fluctuation, approximately what percentage of their qubits should yield usable bits of information?\n", + "- a. 100%\n", + "- b. 50%\n", + "- c. 25%\n", + "- d. 12.5%\n", + "- e. 0%\n", + "\n", + "3. After Alice and Bob select cases in which they used the same measurement bases, what percentage of those bits of information should match, if quantum noise and errors were negligible?\n", + "- a. 100%\n", + "- b. 50%\n", + "- c. 25%\n", + "- d. 12.5%\n", + "- e. 0%\n", + "\n", + "4. Assume Alice has chosen her measurement bases randomly. Eve also chooses her bases randomly, and she listens in (measures). She sends states on to Bob that are consistent with her measurements. Alice and Bob compare basis choices and keep only the qubits measured/prepared by them in the same bases. Up to some random fluctuation, approximately what percentage of those kept qubit measurements will match, according to Alice and Bob?\n", + "- a. 100%\n", + "- b. 75%\n", + "- c. 50%\n", + "- d. 25%\n", + "- e. 12.5%\n", + "- f. 0%\n", + "\n", + "### Discussion questions\n", + "\n", + "1. Assume all basis choices are random for all participants, Alice, Bob, and Eve. Assume that after Eve listens in, she sends a state to Bob that is prepared in the same basis in which she measured, and which is consistent with that measurement. Convince your partners that 12.5% of all qubits initialized by Alice will yield measurement mismatches between Alice and Bob, indicating eavesdropping (ignoring quantum errors and noise).\n", + "Hint 1: Since there is no preferred basis, if you consider just one initial choice for Alice, the ratio for that one choice should be the same as the ratio for the sum of all choices.\n", + "Hint 2: It may not be sufficient to count the number of ways something could happen, since some outcomes may occur with different probabilities.\n", + "\n", + "\n", + "2. Assume again that all basis choices are random for all participants, Alice, Bob, and Eve. But now, consider that Eve is free to send along any state she likes after her measurement. She could even try sending states that are inconsistent with her own measurements. Discuss with your partners/neighbors whether you think there is any choice of bases that could reduce the average percentage of qubits that indicate eavesdropping to Alice and Bob." + ] + } + ], + "metadata": { + "in_page_toc_max_heading_level": 2, + "in_page_toc_min_heading_level": 2, + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/modules/computer-science/quantum-teleportation.ipynb b/learning/modules/computer-science/quantum-teleportation.ipynb index c11384c99f2..b65d3d3a99a 100644 --- a/learning/modules/computer-science/quantum-teleportation.ipynb +++ b/learning/modules/computer-science/quantum-teleportation.ipynb @@ -1,1105 +1,1111 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "3e8391e9-be00-46b0-81a9-5474ae778663", - "metadata": {}, - "source": [ - "---\n", - "title: Quantum Teleportation\n", - "description: Explore how quantum teleportation transfers a quantum state from one location to another using the principles of quantum entanglement.\n", - "---\n", - "\n", - "\n", - "# Quantum teleportation\n", - "\n", - "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", - "- `qiskit` v2.1.0 or newer\n", - "- `qiskit-ibm-runtime` v0.40.1 or newer\n", - "- `qiskit-aer` v0.17.0 or newer\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", - "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "This module was tested and used 14 seconds of QPU time. This is an estimate only. Your actual usage may vary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0df1108e-f1ac-4e75-96c2-3d8b5992504e", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "404ed202-a761-4e82-a480-ce59d4b04adf", - "metadata": {}, - "source": [ - "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/jxqnzltpDdE?si=UVL58hFOOWe2Q9qI) to watch it on YouTube.\n", - "\n", - "-------\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "a200b899-0c56-4f69-a7ba-0ca4d83e47cd", - "metadata": {}, - "source": [ - "## Introduction and background\n", - "\n", - "Quantum teleportation is a technique in quantum physics that allows the transfer of quantum information from one location to another without physically moving particles. Unlike the sci-fi concept of teleportation, this process doesn't involve transporting matter. Instead, it relies on the principle of quantum entanglement, where two particles become linked regardless of distance. Through a series of precise measurements and classical communication, the quantum state of one particle can be recreated in another particle at a distant location, effectively \"teleporting\" the quantum information. In this module, we'll see how this works mathematically, and then we will implement quantum teleportation on a real quantum computer. The introduction here will be brief; for more background on quantum information, and more explanation about teleportation, we recommend John Watrous's course on the [Basics of quantum information](/learning/courses/basics-of-quantum-information), and in particular the section on [Teleportation.](/learning/courses/basics-of-quantum-information/entanglement-in-action/quantum-teleportation)\n", - "\n", - "Classical bits can be in states 0 or 1. Quantum bits (qubits) can be in quantum states denoted $|0\\rangle$ and $|1\\rangle$ and also linear combinations of these states, called \"superpositions\", such as $|\\psi\\rangle = \\alpha_0|0\\rangle +\\alpha_1|1\\rangle$, with $\\alpha_0,\\alpha_1 \\in \\mathbb{C},$ and $|\\alpha_0|^2+|\\alpha_1|^2 = 1.$ Although the states can exist in this superposition, a measurement of the state will \"collapse\" it into either the $|0\\rangle$ or $|1\\rangle$ states. The parameters $a$ and $b$ are related to the probability of each measurement outcome according to\n", - "\n", - "$$\n", - "P_0 = |\\alpha_0|^2\n", - "$$\n", - "\n", - "$$\n", - "P_1 = |\\alpha_1|^2\n", - "$$\n", - "\n", - "Hence the constraint that $|\\alpha_0|^2+|\\alpha_1|^2 = 1.$\n", - "\n", - "Another key feature is that quantum bits can be \"entangled\", which means that the measurement of one qubit can affect the outcome of the measurement of another, entangled qubit. Understanding how entanglement is different from simple classical correlations is a bit tricky. Let's first explain our notation. Call two qubits belonging to friend 0 (Alice) and friend 1 (Bob), and each in the $|0\\rangle$ state\n", - "\n", - "$$\n", - "|0\\rangle_B|0\\rangle_A\n", - "$$\n", - "\n", - "or\n", - "\n", - "$$\n", - "|0\\rangle_1|0\\rangle_0\n", - "$$\n", - "\n", - "sometimes shortened to simply\n", - "\n", - "$$\n", - "|00\\rangle\n", - "$$\n", - "\n", - "Note that the lowest-numbered (or lettered) qubit is furthest to the right. This is a convention called \"little-endian\" notation, used throughout Qiskit.\n", - "If the two-qubit state of the friends is $|00\\rangle,$ and they measure the state of their respective qubits, they will each find a 0. Similarly if the qubits were in the state $|11\\rangle,$ each of their measurements would yield a 1. That is no different from the classical case. However, in quantum computing, we can combine this with superposition to obtain states like\n", - "\n", - "$$\n", - "\\frac{1}{\\sqrt{2}}(|00\\rangle+|11\\rangle)\n", - "$$\n", - "\n", - "In a state like this, whether Alice and Bob have qubits in the state 0 or 1 is not yet known, not even yet determined by nature, and yet we know they will measure the same state for their qubit. For example, if Bob measures his qubit to be in the state $|0\\rangle,$ the only way for that to happen is if the measurement has collapsed the two-qubit state to one of the two possible states, specifically to $|00\\rangle.$ That leaves Alice's qubit also in the $|0\\rangle$ state.\n", - "\n", - "The entangled of qubits in this way does not require that the qubits remain physically close to one another. In other words, we could entangle qubits, then separate them by a large distance, and use their entanglement to send information. An entangled state like the one above is a basic unit of entanglement, and is sometimes referred to as an \"e-bit\", a single bit of entanglement. These e-bits can be thought of as resources in quantum communication, since each e-bit shared between distant partners can be used, as we outline here, to move information from one location to another.\n", - "\n", - "The first thought for many people learning about this for the first time is about violating relativity: can we use this to send information faster than light? By all means, keep questioning and probing scientific rules, but unfortunately this won't allow us to send information faster than light, for reasons that will become clear through the course of this module. Spoiler: amazingly it is NOT due to the speed at which this collapse propagates, which does appear to happen faster than light [[1]](https://www.nature.com/articles/nature15759)." - ] - }, - { - "cell_type": "markdown", - "id": "4ce474e6-cea7-4327-9742-9e1a7dd468b0", - "metadata": {}, - "source": [ - "We start with two collaborators Alice and Bob, who are initially in the same location and can work together on the same qubits. These collaborators will entangle their qubits. Then they will move apart to two different geographic locations, bringing their respective qubits with them. Alice will then obtain quantum information on a new qubit Q. We make no assumptions about the information on Q. The state of Q could be a secret unknown to Alice; it could be unknown to all people. But Alice is given the task of transferring the information on Q to Bob. She will do this using quantum teleportation.\n", - "\n", - "To accomplish this, we will need to know some quantum operations or \"gates\"." - ] - }, - { - "cell_type": "markdown", - "id": "03fe38b1-66fe-4c50-8403-d8c64197b5c6", - "metadata": {}, - "source": [ - "## Quantum operators (gates)\n", - "\n", - "Feel free to skip this section if you are already familiar with quantum gates. If you want to understand these gates better, check out [Basics of quantum information](/learning/courses/basics-of-quantum-information), especially the first two lessons, on IBM Quantum Learning.\n", - "\n", - "For this teleportation protocol we will primarily use two types of quantum gates: the Hadamard gate, the CNOT gate. A few others will play a lesser role: the $X$ gate, $Z$ gate, and the SWAP gate.\n", - "\n", - "This module can be completed with very limited linear algebra background, but sometimes visualizing quantum mechanical gates using matrices and vectors can be helpful. So we present here the matrix/vector forms of quantum gates/states, as well.\n", - "\n", - "The states we have already presented are chosen (partly by convention and partly by constraints) to have vector forms:\n", - "$$\n", - "|0\\rangle = \\begin{pmatrix}1 \\\\ 0\\end{pmatrix}\n", - "$$\n", - "\n", - "$$\n", - "|1\\rangle = \\begin{pmatrix}0 \\\\ 1\\end{pmatrix}\n", - "$$\n", - "\n", - "In this way, an arbitrary state $|\\psi\\rangle = a|0\\rangle+b|1\\rangle$ can be written as\n", - "$$\n", - "|\\psi\\rangle =\\begin{pmatrix}a \\\\ b\\end{pmatrix}\n", - "$$\n", - "\n", - "There is some choice in how to extend the notation to multiple-qubit states, but the choice below is quite standard:\n", - "\n", - "$$\n", - "|00\\rangle = \\begin{pmatrix}1 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix},|01\\rangle = \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0\\end{pmatrix},\n", - "|10\\rangle = \\begin{pmatrix}0 \\\\ 0 \\\\ 1 \\\\0\\end{pmatrix},|11\\rangle = \\begin{pmatrix}0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix}.\n", - "$$\n", - "\n", - "With this choice of vector notation in mind, we can introduce our needed quantum gates, their effects on quantum states, and their matrix forms.\n", - "\n", - "__H Hadamard Gate:__ Creates a superposition state. Single-qubit gate.\n", - "$$\n", - "H|0\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right),\n", - "$$\n", - "\n", - "$$\n", - "H|1\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)\n", - "$$\n", - "\n", - "$$\n", - "H=\\frac{1}{\\sqrt{2}}\\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix}\n", - "$$\n", - "\n", - "A circuit with a Hadamard gate is made as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "a66b8ef2-6660-45b8-97bd-dbc98db49857", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "qc = QuantumCircuit(1)\n", - "qc.h(0)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "19b06d0f-0811-48aa-bf5d-a575f7b63876", - "metadata": {}, - "source": [ - "__CNOT Controlled-NOT Gate:__ This gate uses two qubits: a control and a target. Checks the state of a control qubit which is not changed. But if the control qubit is in the state $|1\\rangle$, the gate changes the state of the target qubit; if the state of the control qubit is $|0\\rangle$ no change is made at all. In the notation below, assume the qubit $A$ (right-most qubit) is the control, and qubit $B$ (the left-most qubit) is the target. Below, the notation used is $CNOT(q_{control},q_{target})|BA\\rangle.$\n", - "\n", - "$$\n", - "CNOT(A,B)|00\\rangle = |00\\rangle, \\\\ CNOT(A,B)|01\\rangle = |11\\rangle, \\\\ CNOT(A,B)|10\\rangle = |10\\rangle, \\\\ CNOT(A,B)|11\\rangle = |01\\rangle\n", - "$$\n", - "\n", - "You may sometimes see CNOT written with the order of the control and target simply implied. But there is no such ambiguity in code or in circuit diagrams.\n", - "\n", - "$$\n", - "CNOT=\\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 0 \\\\ 0 & 1 & 0 & 0\\end{pmatrix}\n", - "$$\n", - "\n", - "A CNOT gate looks a bit different in a circuit, since it requires two qubits. This is how it is implemented:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "df5acfc1-feb8-4008-8e51-d9709352edde", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "131d90c7-6ef3-4c32-a01b-52da9f8e0e83", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "Most gates have the same matrix form in Qiskit as everywhere else. But the CNOT gate acts on two qubits, and so suddenly ordering conventions of qubits becomes an issue. Texts that order qubits $|q_0,q_1,...\\rangle$ will show a different matrix form for their CNOT gates. Verify by explicit matrix multiplication that the CNOT matrix above has the correct action on the state $|01\\rangle.$\n", - "\n", - "\n", - "\n", - "\n", - "$$CNOT|01\\rangle =\\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 0 \\\\ 0 & 1 & 0 & 0\\end{pmatrix}\\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\0\\end{pmatrix} = \\begin{pmatrix}0 \\\\ 0 \\\\ 0 \\\\1\\end{pmatrix} = |11\\rangle$$\n", - "\n", - " \n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "2c1a61f3-770a-443e-8e01-26fc1106f92b", - "metadata": {}, - "source": [ - "**$X$ Gate**: Equivalent to a NOT operation. Single-qubit gate.\n", - "\n", - "$$\n", - "X|0\\rangle = |1\\rangle,\\\\X|1\\rangle=|0\\rangle\n", - "$$\n", - "\n", - "$$\n", - "X=\\begin{pmatrix} 0 & 1 \\\\ 1 & 0 \\end{pmatrix}\n", - "$$\n", - "\n", - "In Qiskit, creating a circuit with an $X$ gate looks like this:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "5a23a576-385b-4a7c-899d-67ef4bb0bfed", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "qc.x(0)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "81eaa9dc-740a-4a99-8950-fc7724c6440b", - "metadata": {}, - "source": [ - "**$Z$ Gate**: Adds a \"phase\" to a state (a prefactor, which in the cases of the Z eigenstates $|0\\rangle$ and $|1\\rangle$ either a 1, or -1, respectively). Single-qubit gate.\n", - "\n", - "$$\n", - "Z|0\\rangle = |0\\rangle,\\\\Z|1\\rangle=-|1\\rangle\n", - "$$\n", - "\n", - "$$\n", - "Z=\\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix}\n", - "$$\n", - "\n", - "In Qiskit, creating a circuit with an $Z$ gate looks like this:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "653b2420-eaa8-41ef-8262-3a9efa638cd3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "qc.z(0)\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "476c82d2-4c58-4c25-8f0d-646cd4afb757", - "metadata": {}, - "source": [ - "## Theory\n", - "\n", - "Let's lay out the protocol for quantum teleportation using math. Then, in the next section, we'll realize this setup using a quantum computer.\n", - "\n", - "__Alice and Bob entangle their qubits:__ Initially, Alice's qubit and Bob's qubit are each, separately in the $|0\\rangle$ state (a fine assumption and also the correct initialization for IBM® quantum computers). We can write this as $|0\\rangle_B|0\\rangle_A$ or simply as $|00\\rangle$. Let's calculate what happens when Alice and Bob act with the Hadamard gate on Alice's qubit, and then a CNOT gate with Alice's qubit as the control and Bob's as the target:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "CNOT(A,B)H_A |0\\rangle_B|0\\rangle_A &= CNOT(A,B)|0\\rangle_B\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_A+|1\\rangle_A\\right)\\\\\n", - "&=\\frac{1}{\\sqrt{2}}\\left(CNOT(A,B)|0\\rangle_B|0\\rangle_A+CNOT(A,B)|0\\rangle_B|1\\rangle_A\\right)\\\\\n", - "&=\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "Note that now Alice's and Bob's qubits are entangled. Although it is not yet determined by nature whether both their qubits are in the $|0\\rangle$ state or the $|1\\rangle$ state, it is known that their qubits are in the same state." - ] - }, - { - "cell_type": "markdown", - "id": "b51aed85-2976-4c46-a1d8-07dba8af78fe", - "metadata": {}, - "source": [ - "__Alice and Bob separate:__ The two friends move their qubits to new locations, possibly very far apart. This comes with a lot of caveats: it is not trivial to move quantum information without disturbing it. But it can be moved, and indeed you will move it in this module. But keep in mind as a caveat that we expect to encounter some errors when we move quantum information around a lot.\n", - "\n", - "__Q is introduced:__ The secret state is prepared on qubit Q:\n", - "\n", - "$$\n", - "|\\psi\\rangle_Q = \\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\n", - "$$\n", - "\n", - "At this point Q is simply adjacent to Alice's qubit (A). There has been no entanglement, so the quantum state of the three qubits together can be written as:\n", - "\n", - "$$\n", - "|\\psi\\rangle_{AB}|\\psi\\rangle_Q = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\\left(\\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\\right).\n", - "$$\n", - "\n", - "The goal is to move the information on Q from Alice's location to the location of Bob. At this point, we are not making any claims or requirements about secrecy or speed of information transfer. We are simply exploring how information can move from Alice to Bob." - ] - }, - { - "cell_type": "markdown", - "id": "cd8f8dab-601f-42b6-8b1e-027568f8726c", - "metadata": {}, - "source": [ - "Because the information begins on Q, we will assume Q is assigned the lowest number in qubit numbers, such that little endian notation causes Q to be the right-most qubit in the math below.\n", - "\n", - "__Alice entangles qubits A and Q:__ Alice now operates with a CNOT gate with her own qubit as the control and Q as the target, then applies a Hadamard gate to Q. Let's calculate the three-qubit state after that operation:\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "H_Q CNOT(A,Q)|\\psi\\rangle_{AB}|\\psi\\rangle_Q &= H_Q CNOT(A,Q)\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\\left(\\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\\right)\\\\\n", - "&= H_Q CNOT(A,Q)\\frac{1}{\\sqrt{2}}\\left(\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_1 |0\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)+\\left(\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_1 |1\\rangle_B|1\\rangle_A|1\\rangle_Q\\right)\\right)\\\\\n", - "&= H_Q \\frac{1}{\\sqrt{2}}\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_1 |0\\rangle_B|1\\rangle_A|1\\rangle_Q+\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_1 |1\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)\\\\\n", - "&= \\frac{1}{2}\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_0 |0\\rangle_B|0\\rangle_A|1\\rangle_Q + \\alpha_1 |0\\rangle_B|1\\rangle_A|0\\rangle_Q-\\alpha_1 |0\\rangle_B|1\\rangle_A|1\\rangle_Q\\right)\\\\\n", - "&+\\frac{1}{2}\\left(\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_0 |1\\rangle_B|1\\rangle_A|1\\rangle_Q + \\alpha_1 |1\\rangle_B|0\\rangle_A|0\\rangle_Q - \\alpha_1 |1\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)\n", - "\\end{aligned}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "78aaf4c7-2775-483e-91cd-11d400793776", - "metadata": {}, - "source": [ - "Because A and Q are in the same location, let us group the terms above according to the outcomes of measurements on qubits A and Q:\n", - "$$\n", - "\\begin{aligned}\n", - "|\\psi\\rangle = \\frac{1}{2}\\left((\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|0\\rangle_A|0\\rangle_Q + (\\alpha_0 |0\\rangle_B-\\alpha_1 |1\\rangle_B)|0\\rangle_A|1\\rangle_Q + (\\alpha_1 |0\\rangle_B+\\alpha_0 |1\\rangle_B)|1\\rangle_A|0\\rangle_Q+ (-\\alpha_1 |0\\rangle_B+\\alpha_0 |1\\rangle_B)|1\\rangle_A|1\\rangle_Q \\right)\\\\\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "#### Check your understanding\n", - "Given the expression above for the states of all three qubits, what is the probability that a measurement of qubits A and Q yields $|0\\rangle_A|0\\rangle_Q?$\n", - "\n", - "\n", - "\n", - "\n", - "25%. To see this, recall that Bob's state must be normalized, so\n", - "$$ |_A \\langle0|_Q\\langle0| \\frac{1}{2} |0\\rangle_A|0\\rangle_Q (\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|^2 = \\frac{1}{4}|(\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|^2 = \\frac{1}{4}$$\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "8df3034b-9807-487b-a046-e689061cb026", - "metadata": {}, - "source": [ - "Now, Alice can measure qubits A and Q . She cannot control the outcome of that measurement, since quantum measurements are probabilistic. So when she measures, there are 4 possible outcomes and all 4 are equally likely: $|0\\rangle_A|0\\rangle_Q,$ $|0\\rangle_A|1\\rangle_Q,$ $|1\\rangle_A|0\\rangle_Q,$ and $|1\\rangle_A|1\\rangle_Q.$ Note that each outcome has different implications for Bob's qubit. For example, if Alice finds her qubits to be in $|0\\rangle_A|0\\rangle_Q,$ that has collapsed the entire, 3-qubit quantum state to $(\\alpha_0|0\\rangle_B+\\alpha_1|1\\rangle_B)|0\\rangle_A|0\\rangle_Q.$ Other measurement outcomes for Alice yield different states for Bob. These are collected together in the table below." - ] - }, - { - "cell_type": "markdown", - "id": "99e01c8f-e6e1-4679-8e48-1e33a1b78e63", - "metadata": {}, - "source": [ - "| Alice outcome | Bob's state | Instruction to Bob| Result |\n", - "| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------------------------|\n", - "| $ \\vert 0\\rangle_A \\vert 0\\rangle_Q$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$ | None | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", - "| $ \\vert 0\\rangle_A \\vert 1\\rangle_Q$ | $\\alpha_0\\vert 0\\rangle_B-\\alpha_1\\vert 1\\rangle_B$ | $Z$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", - "| $ \\vert 1\\rangle_A \\vert 0\\rangle_Q$ | $\\alpha_1\\vert 0\\rangle_B+\\alpha_0\\vert 1\\rangle_B$ | $X$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", - "| $ \\vert 1\\rangle_A \\vert 1\\rangle_Q$ | $-\\alpha_1\\vert 0\\rangle_B+\\alpha_0\\vert 1\\rangle_B$ | $X$ then $Z$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|" - ] - }, - { - "cell_type": "markdown", - "id": "ff32d084-31ea-4408-b3cb-b5bebb845a3b", - "metadata": {}, - "source": [ - "For all the possible measurement outcomes on Alice's qubits, Bob's qubit is left in a state vaguely resembling the secret state originally on Q. In the case where Alice measures $|0\\rangle_C|0\\rangle_A$ (the first row of the table), Bob's qubit is left in exactly the secret state! In the other cases, there is something off about the state. The coefficients ($\\alpha$'s) are swapped, or there is a \"-\" sign where there should be a \"+\" sign, or both. In order to modify Bob's qubit to make it exactly equal to the secret state, Alice must call Bob (use some means of classical communication) and tell Bob to perform additional operations on his qubit, as outlined in the table. For example, in the third row the coefficients are swapped. If Alice calls Bob and tells him to apply an $X$ gate to his qubit, it changes a $|0\\rangle$ to a $|1\\rangle$ and vice-versa, and out comes the secret state.\n", - "\n", - "It should now be clear why we can't use this setup to send information faster than light. We might get lucky and measure $|0\\rangle_A|0\\rangle_Q,$ meaning Bob has exactly the secret state, instantly. But Bob doesn't know that until we call him and tell him \"We measured $|0\\rangle_A|0\\rangle_Q$, so you don't have to do anything.\"\n", - "\n", - "In the thought experiment, the qubits are often physically separated and taken to a new location. IBM® quantum computers use solid-state qubits on a chip that can't be separated. So instead of moving Alice and Bob to different locations, we will separate the information on the chip itself by using so-called \"swap gates\" to move the information from one qubit to another." - ] - }, - { - "cell_type": "markdown", - "id": "efa0afed-97e7-42fd-a06d-7c095e703537", - "metadata": {}, - "source": [ - "## Experiment 1: Basic teleportation\n", - "\n", - "IBM Quantum recommends tackling quantum computing problems using a framework we call \"Qiskit patterns\". It consists of the following steps.\n", - "- Step 1: Map your problem to a quantum circuit\n", - "- Step 2: Optimize your circuit for running on real quantum hardware\n", - "- Step 3: Execute your job on IBM quantum computers using Runtime Primitives\n", - "- Step 4: Post-process the results\n", - "\n", - "### Step 1: Map your problem to a quantum circuit\n", - "\n", - "All the math we did above was outlining step 1. We will implement it now, building our quantum circuit using Qiskit! We start creating a quantum circuit with three qubits, and entangling the two qubits of Alice and Bob. We will take these to be qubits 1 and 2, and we will reserve qubit 0 for the secret state." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "6e3e0d07-b52f-4e17-9233-0a00065f2d77", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map your problem to a quantum circuit\n", - "\n", - "# Import some general packages\n", - "from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister\n", - "import numpy as np\n", - "\n", - "# Define registers\n", - "secret = QuantumRegister(1, \"Q\")\n", - "Alice = QuantumRegister(1, \"A\")\n", - "Bob = QuantumRegister(1, \"B\")\n", - "\n", - "cr = ClassicalRegister(3, \"c\")\n", - "\n", - "qc = QuantumCircuit(secret, Alice, Bob, cr)\n", - "\n", - "# We entangle Alice's and Bob's qubits as in our work above. We apply a Hadamard gate and then a CNOT gate.\n", - "# Note that the second argument in the CNOT gate is the target.\n", - "qc.h(Alice)\n", - "qc.cx(Alice, Bob)\n", - "\n", - "# Inserting a barrier changes nothing about the logic. It just allows us to force gates to be positioned in \"layers\".\n", - "qc.barrier()\n", - "\n", - "# Now we will use random variables to create the secret state. Don't worry about the \"u\" gate and the details.\n", - "np.random.seed(42) # fixing seed for repeatability\n", - "theta = np.random.uniform(0.0, 1.0) * np.pi # from 0 to pi\n", - "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", - "\n", - "# Assign the secret state to the qubit on the other side of Alice's (qubit 0), labeled Q\n", - "qc.u(theta, varphi, 0.0, secret)\n", - "qc.barrier()\n", - "\n", - "# Now entangle Q and Alice's qubits as in the discussion above.\n", - "qc.cx(secret, Alice)\n", - "qc.h(secret)\n", - "qc.barrier()\n", - "\n", - "# Now Alice measures her qubits, and stores the outcomes in the \"classical registers\" cr[]\n", - "qc.measure(Alice, cr[1])\n", - "qc.measure(secret, cr[0])\n", - "\n", - "# Now we insert some conditional logic. If Alice measures Q in a \"1\" we need a Z gate, and if Alice measures A in a \"1\" we need an X gate (see the table).\n", - "with qc.if_test((cr[1], 1)):\n", - " qc.x(Bob)\n", - "with qc.if_test((cr[0], 1)):\n", - " qc.z(Bob)\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "2802dfbf-e3cb-4bd8-80cd-83ec972c7bd9", - "metadata": {}, - "source": [ - "That's all we have to do to get Alice's state teleported to Bob. However, recall that when we measure a quantum state $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$ we find either $|0\\rangle$ or $|1\\rangle.$ So at the end of all this, Bob definitely has Alice's secret state, but we can't easily verify this with a measurement. In order for a measurement to tell us that we did this correctly, we have to do a trick. We had an operator labeled \"U\" for \"unitary\" which we used to prepare Alice's secret state. We can apply the inverse of U at the end of our circuit. If U mapped Alice's $|0\\rangle$ state into $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$, then the inverse of U will map Bob's $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$ back to $|0\\rangle.$ So this last part wouldn't necessarily be done if the goal were just to move quantum information. This is only done for us to check ourselves." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "dac9d281-6c57-4e70-9a61-74efa5d4bdc2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Add the inverse of U and measure Bob's qubit.\n", - "qc.barrier()\n", - "\n", - "qc.u(theta, varphi, 0.0, Bob).inverse() # inverse of u(theta,varphi,0.0)\n", - "qc.measure(Bob, cr[2]) # add measurement gate\n", - "\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "bd6643de-e67f-44f6-b83d-d7ea9f314eb8", - "metadata": {}, - "source": [ - "So if we've done this correctly, our measurement on Bob's qubit should yield a $|0\\rangle$ state. Of course, these measurements are probabilistic. So if there is even a small chance of measuring Bob's qubit to be in the $|1\\rangle$ state, then a single measurement could result in $|1\\rangle.$ We would really want to make many measurements to be assured that the probability of $|0\\rangle$ is quite high.\n", - "\n", - "### Step 2: Optimize problem for quantum execution\n", - "\n", - "This step takes the operations we want to perform and expresses them in terms of the functionality of a specific quantum computer. It also maps our problem onto the layout of the quantum computer.\n", - "\n", - "We will start by loading several packages that are required to communicate with IBM quantum computers. We must also select a backend on which to run. We can either choose the least busy backend, or select a specific backend whose properties we know.\n", - "\n", - "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f24b6b9-db1b-4077-8660-e355e377807e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_sherbrooke\n" - ] - } - ], - "source": [ - "# Load the Qiskit Runtime service\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Load the Qiskit Runtime service\n", - "\n", - "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", - "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", - "\n", - "# Load saved credentials\n", - "service = QiskitRuntimeService()\n", - "\n", - "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", - "backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127)\n", - "# backend = service.backend(\"ibm_brisbane\")\n", - "print(backend.name)" - ] - }, - { - "cell_type": "markdown", - "id": "35bb9c51-a573-4363-89ed-2159aaa1976f", - "metadata": {}, - "source": [ - "We explicitly enable logic on measurements." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e238e397-33f2-4fa1-99b1-94c78dd966f1", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import IfElseOp\n", - "\n", - "backend.target.add_instruction(IfElseOp, name=\"if_else\")" - ] - }, - { - "cell_type": "markdown", - "id": "6074a3b9-54c3-4c52-90ee-f308bd96de76", - "metadata": {}, - "source": [ - "Now we must \"transpile\" the quantum circuit. This involves many sub-steps and is a fascinating topic. Just to give an example of a sub-step: not all quantum computers can directly implement all logical gates in Qiskit. We must write the gates from our circuit in terms of gates the quantum computer can implement. We can carry out that process, and others, using a preset pass manager. Setting ```optimization = 3``` (the highest level of optimization) ensures that the mapping from our abstract quantum circuit to the instructions given to the quantum computer is as efficient as our pre-processing can get it." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c533d4a7-c53c-4048-bf99-46e06ad9cb95", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "qc_isa = pm.run(qc)" - ] - }, - { - "cell_type": "markdown", - "id": "85f3e53c-f910-4427-a977-f6e24798542c", - "metadata": {}, - "source": [ - "A \"Sampler\" is a primitive designed to sample possible states resulting from a quantum circuit, and collect statistics on what states might be measured and with what probability. We import the Qiskit Runtime Sampler here:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "d3292506-1486-48cd-bf1e-696135572eab", - "metadata": {}, - "outputs": [], - "source": [ - "# Load the Runtime primitive and session\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", - "\n", - "sampler = Sampler(mode=backend)" - ] - }, - { - "cell_type": "markdown", - "id": "5ea27bf8-2dbf-4d68-8375-e40e89b71341", - "metadata": {}, - "source": [ - "Not all computations on a quantum computer can be reasonably simulated on classical computers. This simple teleportation definitely can be, but it isn't at all surprising that we can classically save information in one place or another. We strongly recommend carrying out these calculations using a real IBM quantum computer. But in case you have exhausted your free monthly use, or if something must be completed in class and can't wait in the queue, this module can be completed using a simulator. To do this, simply run the cell below and uncomment the associated lines in the \"Execute\" steps." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2332b984-cff6-4022-b7fa-f100d35a2719", - "metadata": {}, - "outputs": [], - "source": [ - "# Load the backend sampler\n", - "from qiskit.primitives import BackendSamplerV2\n", - "\n", - "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "\n", - "noise_model = NoiseModel.from_backend(backend)\n", - "\n", - "# Define a simulator using Aer, and use it in Sampler.\n", - "backend_sim = AerSimulator(noise_model=noise_model)\n", - "sampler_sim = BackendSamplerV2(backend=backend_sim)\n", - "\n", - "# Alternatively, load a fake backend with generic properties and define a simulator.\n", - "# backend_gen = GenericBackendV2(num_qubits=18)\n", - "# sampler_gen = BackendSamplerV2(backend=backend_gen)" - ] - }, - { - "cell_type": "markdown", - "id": "bff5bb04-fc2d-45f4-ab26-98fe9a5a0b5c", - "metadata": {}, - "source": [ - "### Step 3: Execute\n", - "\n", - "Use the sampler to run your job, with the circuit as an argument." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cbf63f9a-5b93-4bbd-b527-f3892e2188bd", - "metadata": {}, - "outputs": [], - "source": [ - "job = sampler.run([qc_isa])\n", - "# job = sampler_sim.run([qc_isa])\n", - "res = job.result()\n", - "counts = res[0].data.c.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "a6a68d85-486a-401a-8336-805713eafd57", - "metadata": {}, - "source": [ - "### Step 4: Post-processing and analysis\n", - "\n", - "Let's plot the results and interpret them." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "47488ddf-9357-41e4-a0fb-45616bee307c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "12dcb4f6-d105-41f0-8800-87f291ce1aeb", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "Which of the states above indicate successful teleportation, and how can you tell?\n", - "\n", - "\n", - "\n", - "\n", - "The states $|000\\rangle,$ $|001\\rangle,$ $|010\\rangle,$ $|011\\rangle$ are all consistent with successful teleportation. This is because we added a gate to undo the initial preparation of the secret state. If the secret state was successfully teleported to Bob's qubit, that additional gate should return Bob's qubit to the $|0\\rangle$ state. So any state above with Bob's qubit (qubit 0, also measured to the 0th component of the classical register, and hence the highest/right-most) in the $|0\\rangle$ state indicates success.\n", - "\n", - " \n", - "\n", - "\n", - "This plot is showing all measurement outcomes for the three qubits, over 5,000 trials or \"shots\". We pointed out earlier that Alice would measure all possible states for qubits A and Q with equal likelihood. We assigned qubits 0-2 in the circuit to Q, A, and B, in that order. In little-endian notation, Bob's qubit is the left-most/lowest. So the four bars on the left correspond to Bob's qubit being $|0\\rangle$, and the other two qubits being in all possible combinations with roughly equal probability. Note that almost all (usually \\~95%) of measurements yield Bob's qubit in the $|0\\rangle$ state, meaning our setup was successful! There are a handful of shots (\\~5%) that yielded Bob's qubit in the $|1\\rangle$ state. That should not logically be possible. However, all modern quantum computers suffer from noise and errors to a much greater extent than classical computers. And quantum error correction is still an emerging field." - ] - }, - { - "cell_type": "markdown", - "id": "cd75b04b-2d2d-4d87-b6cc-b0fed440d912", - "metadata": {}, - "source": [ - "## Experiment 2: Teleporting across a processor\n", - "\n", - "Arguably, the most interesting part of quantum teleportation is that a quantum state can be teleported over long distances instantly (though the classical communication of extra gates is not instant). As already stated, we can't break qubits off the processor and move them around. But we can move the information from one qubit to another, until the qubits involved in teleportation are on opposite sides of the processor. Let us repeat the steps we took above, but now we will make a larger circuit with enough qubits to span the processor.\n", - "\n", - "### Step 1: Map your problem to a quantum circuit\n", - "\n", - "This time, the qubits corresponding to Alice and Bob will change. So we will not name a single qubit \"A\" and another \"B\". Rather, we will number the qubits and use variables to represent the current position of the information on qubits belonging to Alice and Bob. All other steps except the swap gates are as described previously." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "7d7742fa-e9a2-4ca0-9d9b-f29d73e07cd9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Step 1: Map\n", - "\n", - "# Define registers\n", - "qr = QuantumRegister(13, \"q\")\n", - "\n", - "qc = QuantumCircuit(qr, cr)\n", - "\n", - "# Define registers\n", - "secret = QuantumRegister(1, \"Q\")\n", - "ebitsa = QuantumRegister(6, \"A\")\n", - "ebitsb = QuantumRegister(6, \"B\")\n", - "# q = ClassicalRegister(1, \"q meas\")\n", - "# a = ClassicalRegister(1, \"a\")\n", - "# b = ClassicalRegister(1, \"b\")\n", - "cr = ClassicalRegister(3, \"c\")\n", - "qc = QuantumCircuit(secret, ebitsa, ebitsb, cr)\n", - "\n", - "# We'll start Alice in the middle of the circuit, then move information outward in both directions.\n", - "Alice = 5\n", - "Bob = 0\n", - "qc.h(ebitsa[Alice])\n", - "qc.cx(ebitsa[Alice], ebitsb[Bob])\n", - "\n", - "# Starting with Bob and Alice in the center, we swap their information onto adjacent qubits, until the information is on distant qubits.\n", - "\n", - "for n in range(Alice):\n", - " qc.swap(ebitsb[Bob], ebitsb[Bob + 1])\n", - " qc.swap(ebitsa[Alice], ebitsa[Alice - 1])\n", - " Alice = Alice - 1\n", - " Bob = Bob + 1\n", - "\n", - "qc.barrier()\n", - "\n", - "# Create a random state for Alice (qubit zero)\n", - "np.random.seed(42) # fixing seed for repeatability\n", - "# theta = np.random.uniform(0.0, 1.0) * np.pi #from 0 to pi\n", - "theta = 0.3\n", - "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", - "\n", - "\n", - "qc.u(theta, varphi, 0.0, secret)\n", - "\n", - "# Entangle Alice's two qubits\n", - "qc.cx(secret, ebitsa[Alice])\n", - "qc.h(secret)\n", - "\n", - "qc.barrier()\n", - "\n", - "# Make measurements of Alice's qubits and store the results in the classical register.\n", - "qc.measure(ebitsa[Alice], cr[1])\n", - "qc.measure(secret, cr[0])\n", - "\n", - "# Send instructions to Bob's qubits based on the outcome of Alice's measurements.\n", - "with qc.if_test((cr[1], 1)):\n", - " qc.x(ebitsb[Bob])\n", - "with qc.if_test((cr[0], 1)):\n", - " qc.z(ebitsb[Bob])\n", - "\n", - "qc.barrier()\n", - "\n", - "# Invert the preparation we did for Carl's qubit so we can check whether we did this correctly.\n", - "qc.u(theta, varphi, 0.0, ebitsb[Bob]).inverse() # inverse of u(theta,varphi,0.0)\n", - "qc.measure(ebitsb[Bob], cr[2]) # add measurement gate\n", - "\n", - "qc.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "5174ffeb-28d4-49d4-9693-5271e026b026", - "metadata": {}, - "source": [ - "You can see in the circuit diagram that the logical steps are the same. The only difference is that we used the swap gates to bring Alice's qubit's state from qubit 6 ($A_5$) up to qubit 1 ($A_0$), right next to Q. And we used swap gates to bring Bob's initial state from qubit 7 ($B_0$) down to qubit 12 ($B_5$). Note that the state on qubit 12 is not even related to Q's secret state until measurements are made on the distant qubits 0 and 1, and the state on qubit 12 is not equal to the secret state until after the conditional $X$ and $Z$ gates are applied.\n", - "\n", - "### Step 2: Optimize your circuit\n", - "\n", - "Normally, when we use the pass manager to transpile and optimize our circuits, it makes sense to set ```optimization_level = 3```, because we want our circuits to be as efficient as possible. In this case, there is no computational reason for us to transfer states from qubits 6 and 7 over to qubits 1 and 12. That was just something we did to demonstrate teleportation over a distance. If we ask the pass manager to optimize our circuit, it will realize there is no logical reason for these swap gates, and it will remove them and carry out the gate operations on adjacent qubits. So for this special case, we use ```optimization_level = 0```." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d7865560-a8a6-4abb-af8d-6a86ecc76bff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "105\n" - ] - } - ], - "source": [ - "# Step 2: Transpile\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "target = backend.target\n", - "pmzero = generate_preset_pass_manager(target=target, optimization_level=0)\n", - "\n", - "qc_isa_zero = pmzero.run(qc)\n", - "\n", - "print(qc_isa_zero.depth())" - ] - }, - { - "cell_type": "markdown", - "id": "38c1dc99-e5e0-4969-b7a0-cdc1e4e6bedd", - "metadata": {}, - "source": [ - "We can visualize where on the quantum processor these qubits are using the ```plot_circuit_layout``` function." - ] - }, - { - "cell_type": "markdown", - "id": "6a74e5a5-d32f-41a5-adf8-29174a0d5e8a", - "metadata": {}, - "source": [ - "### Step 3: Execute\n", - "\n", - "As before, we recommend running on real IBM quantum computers. If your monthly free usage has been reached, feel free to uncomment the simulator cells to run on a simulator." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "5536d98a-480b-4193-8c63-2ef77d1aeda0", - "metadata": {}, - "outputs": [], - "source": [ - "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", - "job = sampler.run([qc_isa_zero])\n", - "# job = sampler_sim.run([qc_isa_zero])\n", - "counts = job.result()[0].data.c.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "13b6375b-73c7-436d-bf1c-98de45ef4056", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "49ed3ba0-3482-4ad5-84c3-f8fba80d8668", - "metadata": {}, - "source": [ - "### Step 4: Classical post-processing\n", - "\n", - "Again we see that the probabilities for the possible outcomes for Alice's qubits are fairly uniform. There is a strong preference for finding Bob's qubit in $|0\\rangle$ after inverting the secret code, meaning there is a strong probability that we correctly teleported the secret state across the processor from Q to Bob (qubits 0 to 12). However, we note that there is now about a higher chance of *not* measuring $|0\\rangle$ for Bob. This is an important lesson in quantum computing: the more gates you have, especially multi-qubit gates like swap gates, the more noise and errors you will encounter." - ] - }, - { - "cell_type": "markdown", - "id": "ede2724d-536b-472a-b4bd-b1276b5b851d", - "metadata": {}, - "source": [ - "## Questions\n", - "\n", - "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", - "\n", - "### Critical concepts\n", - "\n", - "- Qubits can be entangled, meaning a measurement of one qubit affects or even determines the state of another qubit.\n", - "- Entanglement differs from classical correlations; for example, qubits A and B could be in a superposition of states like $\\alpha_0|00\\rangle+\\alpha_1|11\\rangle.$ The state of A or B could be undetermined by nature, and yet A and B could still be guaranteed to be in the same state.\n", - "- Through a combination of entanglements and measurements, we can transfer a state (which can store information) from one qubit to another. This transfer can even be done over long distances, and this is called quantum teleportation.\n", - "- Quantum teleportation relies on quantum measurements, which are probabilistic. Thus, classical communication can be necessary to tweak the teleported states. This prevents quantum teleportation from moving information faster than light. Quantum teleportation does not violate relativity or causality.\n", - "- Modern quantum computers are more susceptible to noise and errors than classical computers. Expect a few percent error.\n", - "- The more gates you add in sequence (especially 2-qubit gates) the more errors and noise you can expect.\n", - "\n", - "\n", - "### True/False questions\n", - "\n", - "1. T/F Quantum teleportation can be used to send information faster than light.\n", - "2. T/F Modern evidence suggests that the collapse of a quantum state propagates faster than light.\n", - "3. T/F In Qiskit, qubits are ordered in states with the lowest-numbered qubit on the right, as in $|q_3,q_2,q_1, q_0\\rangle$\n", - "\n", - "\n", - "### MC questions\n", - "\n", - "1. Qubits A and B are entangled, then separated by a great distance $d$. Qubit A is measured. Which statement is correct about the speed at which the state of qubit B is affected?\n", - "\n", - "- a. Qubit B is affected instantly, within experimental tolerance, in experiments run so far.\n", - "- b. Qubit B is affected after a time $d/c$, meaning the quantum state \"collapses\" at approximately the speed of light, within experimental tolerance.\n", - "- c. Qubit B is affected only after classical communication has occurred, meaning it happens in a time longer than $d/c$.\n", - "- d. None of the above\n", - "\n", - "2. Recall that measurement probability is related to amplitudes in quantum states. For example, if a qubit is initially in the state $\\alpha_0|0\\rangle+\\alpha_1 |1\\rangle,$ the probability of measuring the state $|0\\rangle$ is $|\\alpha_0|^2.$ Not all sets of measurements will exactly match these probabilities, due to finite sampling (just as flipping a coin might yield heads twice in a row). The measurement histogram below could correspond to which of the following quantum states? Select the best option.\n", - "\n", - "![entangled_teleportation_fig](/learning/images/modules/computer-science/quantum-teleportation/entangled_teleportation_fig.avif)\n", - "\n", - "- a. $|0\\rangle$\n", - "- b. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)$\n", - "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)$\n", - "- d. $\\frac{4}{5}|0\\rangle+\\frac{3}{5}|1\\rangle$\n", - "- e. $\\frac{3}{5}|0\\rangle+\\frac{4}{5}|1\\rangle$\n", - "\n", - "\n", - "3. Which of the following states show(s) qubits A and B entangled? Select all that apply.\n", - "- a. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)$\n", - "- b. $\\frac{4}{5}|0\\rangle_B|0\\rangle_A+\\frac{3}{5}|1\\rangle_B|1\\rangle_A$\n", - "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|1\\rangle_A-|1\\rangle_B|0\\rangle_A\\right)$\n", - "- d. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|0\\rangle_A\\right)$\n", - "- e. $|0\\rangle_B|0\\rangle_A$\n", - "\n", - "4. In this module, we prepared an entangled state: $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right).$ But there are many other entangled states one could use for a similar protocol. Which of the states below could yield a 2-qubit measurement histogram like the following? Select the best response.\n", - "\n", - "![entangled_teleportation_fig_0110](/learning/images/modules/computer-science/quantum-teleportation/entangled_teleportation_fig_0110.avif)\n", - "\n", - "- a. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)$\n", - "- b. $\\frac{4}{5}|0\\rangle_B|0\\rangle_A+\\frac{3}{5}|1\\rangle_B|1\\rangle_A$\n", - "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|1\\rangle_A-|1\\rangle_B|0\\rangle_A\\right)$\n", - "- d. $\\frac{4}{5}|0\\rangle_B|1\\rangle_A+\\frac{3}{5}|1\\rangle_B|0\\rangle_A$\n", - "- e. $|0\\rangle_B|0\\rangle_A$\n", - "\n", - "### Discussion questions\n", - "\n", - "1. Describe the quantum teleportation protocol, from start to finish, to your partner/group. See if they have anything to add, or if they have questions.\n", - "\n", - "2. Is there anything unique about the initial entangled state between Alice and Bob: $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)?$ If so, what is unique about it? If not, what other entangled states could we have used?" - ] - } - ], - "metadata": { - "in_page_toc_max_heading_level": 2, - "in_page_toc_min_heading_level": 2, - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3e8391e9-be00-46b0-81a9-5474ae778663", + "metadata": {}, + "source": [ + "---\n", + "title: Quantum Teleportation\n", + "description: Explore how quantum teleportation transfers a quantum state from one location to another using the principles of quantum entanglement.\n", + "---\n", + "\n", + "\n", + "# Quantum teleportation\n", + "\n", + "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", + "- `qiskit` v2.1.0 or newer\n", + "- `qiskit-ibm-runtime` v0.40.1 or newer\n", + "- `qiskit-aer` v0.17.0 or newer\n", + "- `qiskit.visualization`\n", + "- `numpy`\n", + "- `pylatexenc`\n", + "\n", + "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", + "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", + "\n", + "This module was tested and used 14 seconds of QPU time. This is an estimate only. Your actual usage may vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0df1108e-f1ac-4e75-96c2-3d8b5992504e", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and modify this line as needed to install dependencies\n", + "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" + ] + }, + { + "cell_type": "markdown", + "id": "404ed202-a761-4e82-a480-ce59d4b04adf", + "metadata": {}, + "source": [ + "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://youtu.be/jxqnzltpDdE?si=UVL58hFOOWe2Q9qI) to watch it on YouTube.\n", + "\n", + "-------\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "a200b899-0c56-4f69-a7ba-0ca4d83e47cd", + "metadata": {}, + "source": [ + "## Introduction and background\n", + "\n", + "Quantum teleportation is a technique in quantum physics that allows the transfer of quantum information from one location to another without physically moving particles. Unlike the sci-fi concept of teleportation, this process doesn't involve transporting matter. Instead, it relies on the principle of quantum entanglement, where two particles become linked regardless of distance. Through a series of precise measurements and classical communication, the quantum state of one particle can be recreated in another particle at a distant location, effectively \"teleporting\" the quantum information. In this module, we'll see how this works mathematically, and then we will implement quantum teleportation on a real quantum computer. The introduction here will be brief; for more background on quantum information, and more explanation about teleportation, we recommend John Watrous's course on the [Basics of quantum information](/learning/courses/basics-of-quantum-information), and in particular the section on [Teleportation.](/learning/courses/basics-of-quantum-information/entanglement-in-action/quantum-teleportation)\n", + "\n", + "Classical bits can be in states 0 or 1. Quantum bits (qubits) can be in quantum states denoted $|0\\rangle$ and $|1\\rangle$ and also linear combinations of these states, called \"superpositions\", such as $|\\psi\\rangle = \\alpha_0|0\\rangle +\\alpha_1|1\\rangle$, with $\\alpha_0,\\alpha_1 \\in \\mathbb{C},$ and $|\\alpha_0|^2+|\\alpha_1|^2 = 1.$ Although the states can exist in this superposition, a measurement of the state will \"collapse\" it into either the $|0\\rangle$ or $|1\\rangle$ states. The parameters $a$ and $b$ are related to the probability of each measurement outcome according to\n", + "\n", + "$$\n", + "P_0 = |\\alpha_0|^2\n", + "$$\n", + "\n", + "$$\n", + "P_1 = |\\alpha_1|^2\n", + "$$\n", + "\n", + "Hence the constraint that $|\\alpha_0|^2+|\\alpha_1|^2 = 1.$\n", + "\n", + "Another key feature is that quantum bits can be \"entangled\", which means that the measurement of one qubit can affect the outcome of the measurement of another, entangled qubit. Understanding how entanglement is different from simple classical correlations is a bit tricky. Let's first explain our notation. Call two qubits belonging to friend 0 (Alice) and friend 1 (Bob), and each in the $|0\\rangle$ state\n", + "\n", + "$$\n", + "|0\\rangle_B|0\\rangle_A\n", + "$$\n", + "\n", + "or\n", + "\n", + "$$\n", + "|0\\rangle_1|0\\rangle_0\n", + "$$\n", + "\n", + "sometimes shortened to simply\n", + "\n", + "$$\n", + "|00\\rangle\n", + "$$\n", + "\n", + "Note that the lowest-numbered (or lettered) qubit is furthest to the right. This is a convention called \"little-endian\" notation, used throughout Qiskit.\n", + "If the two-qubit state of the friends is $|00\\rangle,$ and they measure the state of their respective qubits, they will each find a 0. Similarly if the qubits were in the state $|11\\rangle,$ each of their measurements would yield a 1. That is no different from the classical case. However, in quantum computing, we can combine this with superposition to obtain states like\n", + "\n", + "$$\n", + "\\frac{1}{\\sqrt{2}}(|00\\rangle+|11\\rangle)\n", + "$$\n", + "\n", + "In a state like this, whether Alice and Bob have qubits in the state 0 or 1 is not yet known, not even yet determined by nature, and yet we know they will measure the same state for their qubit. For example, if Bob measures his qubit to be in the state $|0\\rangle,$ the only way for that to happen is if the measurement has collapsed the two-qubit state to one of the two possible states, specifically to $|00\\rangle.$ That leaves Alice's qubit also in the $|0\\rangle$ state.\n", + "\n", + "The entangled of qubits in this way does not require that the qubits remain physically close to one another. In other words, we could entangle qubits, then separate them by a large distance, and use their entanglement to send information. An entangled state like the one above is a basic unit of entanglement, and is sometimes referred to as an \"e-bit\", a single bit of entanglement. These e-bits can be thought of as resources in quantum communication, since each e-bit shared between distant partners can be used, as we outline here, to move information from one location to another.\n", + "\n", + "The first thought for many people learning about this for the first time is about violating relativity: can we use this to send information faster than light? By all means, keep questioning and probing scientific rules, but unfortunately this won't allow us to send information faster than light, for reasons that will become clear through the course of this module. Spoiler: amazingly it is NOT due to the speed at which this collapse propagates, which does appear to happen faster than light [[1]](https://www.nature.com/articles/nature15759)." + ] + }, + { + "cell_type": "markdown", + "id": "4ce474e6-cea7-4327-9742-9e1a7dd468b0", + "metadata": {}, + "source": [ + "We start with two collaborators Alice and Bob, who are initially in the same location and can work together on the same qubits. These collaborators will entangle their qubits. Then they will move apart to two different geographic locations, bringing their respective qubits with them. Alice will then obtain quantum information on a new qubit Q. We make no assumptions about the information on Q. The state of Q could be a secret unknown to Alice; it could be unknown to all people. But Alice is given the task of transferring the information on Q to Bob. She will do this using quantum teleportation.\n", + "\n", + "To accomplish this, we will need to know some quantum operations or \"gates\"." + ] + }, + { + "cell_type": "markdown", + "id": "03fe38b1-66fe-4c50-8403-d8c64197b5c6", + "metadata": {}, + "source": [ + "## Quantum operators (gates)\n", + "\n", + "Feel free to skip this section if you are already familiar with quantum gates. If you want to understand these gates better, check out [Basics of quantum information](/learning/courses/basics-of-quantum-information), especially the first two lessons, on IBM Quantum Learning.\n", + "\n", + "For this teleportation protocol we will primarily use two types of quantum gates: the Hadamard gate, the CNOT gate. A few others will play a lesser role: the $X$ gate, $Z$ gate, and the SWAP gate.\n", + "\n", + "This module can be completed with very limited linear algebra background, but sometimes visualizing quantum mechanical gates using matrices and vectors can be helpful. So we present here the matrix/vector forms of quantum gates/states, as well.\n", + "\n", + "The states we have already presented are chosen (partly by convention and partly by constraints) to have vector forms:\n", + "$$\n", + "|0\\rangle = \\begin{pmatrix}1 \\\\ 0\\end{pmatrix}\n", + "$$\n", + "\n", + "$$\n", + "|1\\rangle = \\begin{pmatrix}0 \\\\ 1\\end{pmatrix}\n", + "$$\n", + "\n", + "In this way, an arbitrary state $|\\psi\\rangle = a|0\\rangle+b|1\\rangle$ can be written as\n", + "$$\n", + "|\\psi\\rangle =\\begin{pmatrix}a \\\\ b\\end{pmatrix}\n", + "$$\n", + "\n", + "There is some choice in how to extend the notation to multiple-qubit states, but the choice below is quite standard:\n", + "\n", + "$$\n", + "|00\\rangle = \\begin{pmatrix}1 \\\\ 0 \\\\ 0 \\\\ 0\\end{pmatrix},|01\\rangle = \\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\ 0\\end{pmatrix},\n", + "|10\\rangle = \\begin{pmatrix}0 \\\\ 0 \\\\ 1 \\\\0\\end{pmatrix},|11\\rangle = \\begin{pmatrix}0 \\\\ 0 \\\\ 0 \\\\ 1\\end{pmatrix}.\n", + "$$\n", + "\n", + "With this choice of vector notation in mind, we can introduce our needed quantum gates, their effects on quantum states, and their matrix forms.\n", + "\n", + "__H Hadamard Gate:__ Creates a superposition state. Single-qubit gate.\n", + "$$\n", + "H|0\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right),\n", + "$$\n", + "\n", + "$$\n", + "H|1\\rangle = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)\n", + "$$\n", + "\n", + "$$\n", + "H=\\frac{1}{\\sqrt{2}}\\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix}\n", + "$$\n", + "\n", + "A circuit with a Hadamard gate is made as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a66b8ef2-6660-45b8-97bd-dbc98db49857", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "qc = QuantumCircuit(1)\n", + "qc.h(0)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "19b06d0f-0811-48aa-bf5d-a575f7b63876", + "metadata": {}, + "source": [ + "__CNOT Controlled-NOT Gate:__ This gate uses two qubits: a control and a target. Checks the state of a control qubit which is not changed. But if the control qubit is in the state $|1\\rangle$, the gate changes the state of the target qubit; if the state of the control qubit is $|0\\rangle$ no change is made at all. In the notation below, assume the qubit $A$ (right-most qubit) is the control, and qubit $B$ (the left-most qubit) is the target. Below, the notation used is $CNOT(q_{control},q_{target})|BA\\rangle.$\n", + "\n", + "$$\n", + "CNOT(A,B)|00\\rangle = |00\\rangle, \\\\ CNOT(A,B)|01\\rangle = |11\\rangle, \\\\ CNOT(A,B)|10\\rangle = |10\\rangle, \\\\ CNOT(A,B)|11\\rangle = |01\\rangle\n", + "$$\n", + "\n", + "You may sometimes see CNOT written with the order of the control and target simply implied. But there is no such ambiguity in code or in circuit diagrams.\n", + "\n", + "$$\n", + "CNOT=\\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 0 \\\\ 0 & 1 & 0 & 0\\end{pmatrix}\n", + "$$\n", + "\n", + "A CNOT gate looks a bit different in a circuit, since it requires two qubits. This is how it is implemented:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "df5acfc1-feb8-4008-8e51-d9709352edde", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "131d90c7-6ef3-4c32-a01b-52da9f8e0e83", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "Most gates have the same matrix form in Qiskit as everywhere else. But the CNOT gate acts on two qubits, and so suddenly ordering conventions of qubits becomes an issue. Texts that order qubits $|q_0,q_1,...\\rangle$ will show a different matrix form for their CNOT gates. Verify by explicit matrix multiplication that the CNOT matrix above has the correct action on the state $|01\\rangle.$\n", + "\n", + "\n", + "\n", + "\n", + "$$CNOT|01\\rangle =\\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 0 \\\\ 0 & 1 & 0 & 0\\end{pmatrix}\\begin{pmatrix}0 \\\\ 1 \\\\ 0 \\\\0\\end{pmatrix} = \\begin{pmatrix}0 \\\\ 0 \\\\ 0 \\\\1\\end{pmatrix} = |11\\rangle$$\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "2c1a61f3-770a-443e-8e01-26fc1106f92b", + "metadata": {}, + "source": [ + "**$X$ Gate**: Equivalent to a NOT operation. Single-qubit gate.\n", + "\n", + "$$\n", + "X|0\\rangle = |1\\rangle,\\\\X|1\\rangle=|0\\rangle\n", + "$$\n", + "\n", + "$$\n", + "X=\\begin{pmatrix} 0 & 1 \\\\ 1 & 0 \\end{pmatrix}\n", + "$$\n", + "\n", + "In Qiskit, creating a circuit with an $X$ gate looks like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5a23a576-385b-4a7c-899d-67ef4bb0bfed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "qc.x(0)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "81eaa9dc-740a-4a99-8950-fc7724c6440b", + "metadata": {}, + "source": [ + "**$Z$ Gate**: Adds a \"phase\" to a state (a prefactor, which in the cases of the Z eigenstates $|0\\rangle$ and $|1\\rangle$ either a 1, or -1, respectively). Single-qubit gate.\n", + "\n", + "$$\n", + "Z|0\\rangle = |0\\rangle,\\\\Z|1\\rangle=-|1\\rangle\n", + "$$\n", + "\n", + "$$\n", + "Z=\\begin{pmatrix} 1 & 0 \\\\ 0 & -1 \\end{pmatrix}\n", + "$$\n", + "\n", + "In Qiskit, creating a circuit with an $Z$ gate looks like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "653b2420-eaa8-41ef-8262-3a9efa638cd3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "qc.z(0)\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "476c82d2-4c58-4c25-8f0d-646cd4afb757", + "metadata": {}, + "source": [ + "## Theory\n", + "\n", + "Let's lay out the protocol for quantum teleportation using math. Then, in the next section, we'll realize this setup using a quantum computer.\n", + "\n", + "__Alice and Bob entangle their qubits:__ Initially, Alice's qubit and Bob's qubit are each, separately in the $|0\\rangle$ state (a fine assumption and also the correct initialization for IBM® quantum computers). We can write this as $|0\\rangle_B|0\\rangle_A$ or simply as $|00\\rangle$. Let's calculate what happens when Alice and Bob act with the Hadamard gate on Alice's qubit, and then a CNOT gate with Alice's qubit as the control and Bob's as the target:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "CNOT(A,B)H_A |0\\rangle_B|0\\rangle_A &= CNOT(A,B)|0\\rangle_B\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_A+|1\\rangle_A\\right)\\\\\n", + "&=\\frac{1}{\\sqrt{2}}\\left(CNOT(A,B)|0\\rangle_B|0\\rangle_A+CNOT(A,B)|0\\rangle_B|1\\rangle_A\\right)\\\\\n", + "&=\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "Note that now Alice's and Bob's qubits are entangled. Although it is not yet determined by nature whether both their qubits are in the $|0\\rangle$ state or the $|1\\rangle$ state, it is known that their qubits are in the same state." + ] + }, + { + "cell_type": "markdown", + "id": "b51aed85-2976-4c46-a1d8-07dba8af78fe", + "metadata": {}, + "source": [ + "__Alice and Bob separate:__ The two friends move their qubits to new locations, possibly very far apart. This comes with a lot of caveats: it is not trivial to move quantum information without disturbing it. But it can be moved, and indeed you will move it in this module. But keep in mind as a caveat that we expect to encounter some errors when we move quantum information around a lot.\n", + "\n", + "__Q is introduced:__ The secret state is prepared on qubit Q:\n", + "\n", + "$$\n", + "|\\psi\\rangle_Q = \\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\n", + "$$\n", + "\n", + "At this point Q is simply adjacent to Alice's qubit (A). There has been no entanglement, so the quantum state of the three qubits together can be written as:\n", + "\n", + "$$\n", + "|\\psi\\rangle_{AB}|\\psi\\rangle_Q = \\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\\left(\\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\\right).\n", + "$$\n", + "\n", + "The goal is to move the information on Q from Alice's location to the location of Bob. At this point, we are not making any claims or requirements about secrecy or speed of information transfer. We are simply exploring how information can move from Alice to Bob." + ] + }, + { + "cell_type": "markdown", + "id": "cd8f8dab-601f-42b6-8b1e-027568f8726c", + "metadata": {}, + "source": [ + "Because the information begins on Q, we will assume Q is assigned the lowest number in qubit numbers, such that little endian notation causes Q to be the right-most qubit in the math below.\n", + "\n", + "__Alice entangles qubits A and Q:__ Alice now operates with a CNOT gate with her own qubit as the control and Q as the target, then applies a Hadamard gate to Q. Let's calculate the three-qubit state after that operation:\n", + "\n", + "$$\n", + "\\begin{aligned}\n", + "H_Q CNOT(A,Q)|\\psi\\rangle_{AB}|\\psi\\rangle_Q &= H_Q CNOT(A,Q)\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)\\left(\\alpha_0 |0\\rangle_Q + \\alpha_1 |1\\rangle_Q\\right)\\\\\n", + "&= H_Q CNOT(A,Q)\\frac{1}{\\sqrt{2}}\\left(\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_1 |0\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)+\\left(\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_1 |1\\rangle_B|1\\rangle_A|1\\rangle_Q\\right)\\right)\\\\\n", + "&= H_Q \\frac{1}{\\sqrt{2}}\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_1 |0\\rangle_B|1\\rangle_A|1\\rangle_Q+\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_1 |1\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)\\\\\n", + "&= \\frac{1}{2}\\left(\\alpha_0 |0\\rangle_B|0\\rangle_A|0\\rangle_Q + \\alpha_0 |0\\rangle_B|0\\rangle_A|1\\rangle_Q + \\alpha_1 |0\\rangle_B|1\\rangle_A|0\\rangle_Q-\\alpha_1 |0\\rangle_B|1\\rangle_A|1\\rangle_Q\\right)\\\\\n", + "&+\\frac{1}{2}\\left(\\alpha_0 |1\\rangle_B|1\\rangle_A|0\\rangle_Q + \\alpha_0 |1\\rangle_B|1\\rangle_A|1\\rangle_Q + \\alpha_1 |1\\rangle_B|0\\rangle_A|0\\rangle_Q - \\alpha_1 |1\\rangle_B|0\\rangle_A|1\\rangle_Q\\right)\n", + "\\end{aligned}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "78aaf4c7-2775-483e-91cd-11d400793776", + "metadata": {}, + "source": [ + "Because A and Q are in the same location, let us group the terms above according to the outcomes of measurements on qubits A and Q:\n", + "$$\n", + "\\begin{aligned}\n", + "|\\psi\\rangle = \\frac{1}{2}\\left((\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|0\\rangle_A|0\\rangle_Q + (\\alpha_0 |0\\rangle_B-\\alpha_1 |1\\rangle_B)|0\\rangle_A|1\\rangle_Q + (\\alpha_1 |0\\rangle_B+\\alpha_0 |1\\rangle_B)|1\\rangle_A|0\\rangle_Q+ (-\\alpha_1 |0\\rangle_B+\\alpha_0 |1\\rangle_B)|1\\rangle_A|1\\rangle_Q \\right)\\\\\n", + "\\end{aligned}\n", + "$$\n", + "\n", + "#### Check your understanding\n", + "Given the expression above for the states of all three qubits, what is the probability that a measurement of qubits A and Q yields $|0\\rangle_A|0\\rangle_Q?$\n", + "\n", + "\n", + "\n", + "\n", + "25%. To see this, recall that Bob's state must be normalized, so\n", + "$$ |_A \\langle0|_Q\\langle0| \\frac{1}{2} |0\\rangle_A|0\\rangle_Q (\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|^2 = \\frac{1}{4}|(\\alpha_0 |0\\rangle_B+\\alpha_1 |1\\rangle_B)|^2 = \\frac{1}{4}$$\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "8df3034b-9807-487b-a046-e689061cb026", + "metadata": {}, + "source": [ + "Now, Alice can measure qubits A and Q . She cannot control the outcome of that measurement, since quantum measurements are probabilistic. So when she measures, there are 4 possible outcomes and all 4 are equally likely: $|0\\rangle_A|0\\rangle_Q,$ $|0\\rangle_A|1\\rangle_Q,$ $|1\\rangle_A|0\\rangle_Q,$ and $|1\\rangle_A|1\\rangle_Q.$ Note that each outcome has different implications for Bob's qubit. For example, if Alice finds her qubits to be in $|0\\rangle_A|0\\rangle_Q,$ that has collapsed the entire, 3-qubit quantum state to $(\\alpha_0|0\\rangle_B+\\alpha_1|1\\rangle_B)|0\\rangle_A|0\\rangle_Q.$ Other measurement outcomes for Alice yield different states for Bob. These are collected together in the table below." + ] + }, + { + "cell_type": "markdown", + "id": "99e01c8f-e6e1-4679-8e48-1e33a1b78e63", + "metadata": {}, + "source": [ + "| Alice outcome | Bob's state | Instruction to Bob| Result |\n", + "| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------------------------|\n", + "| $ \\vert 0\\rangle_A \\vert 0\\rangle_Q$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$ | None | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", + "| $ \\vert 0\\rangle_A \\vert 1\\rangle_Q$ | $\\alpha_0\\vert 0\\rangle_B-\\alpha_1\\vert 1\\rangle_B$ | $Z$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", + "| $ \\vert 1\\rangle_A \\vert 0\\rangle_Q$ | $\\alpha_1\\vert 0\\rangle_B+\\alpha_0\\vert 1\\rangle_B$ | $X$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|\n", + "| $ \\vert 1\\rangle_A \\vert 1\\rangle_Q$ | $-\\alpha_1\\vert 0\\rangle_B+\\alpha_0\\vert 1\\rangle_B$ | $X$ then $Z$ | $\\alpha_0\\vert 0\\rangle_B+\\alpha_1\\vert 1\\rangle_B$|" + ] + }, + { + "cell_type": "markdown", + "id": "ff32d084-31ea-4408-b3cb-b5bebb845a3b", + "metadata": {}, + "source": [ + "For all the possible measurement outcomes on Alice's qubits, Bob's qubit is left in a state vaguely resembling the secret state originally on Q. In the case where Alice measures $|0\\rangle_C|0\\rangle_A$ (the first row of the table), Bob's qubit is left in exactly the secret state! In the other cases, there is something off about the state. The coefficients ($\\alpha$'s) are swapped, or there is a \"-\" sign where there should be a \"+\" sign, or both. In order to modify Bob's qubit to make it exactly equal to the secret state, Alice must call Bob (use some means of classical communication) and tell Bob to perform additional operations on his qubit, as outlined in the table. For example, in the third row the coefficients are swapped. If Alice calls Bob and tells him to apply an $X$ gate to his qubit, it changes a $|0\\rangle$ to a $|1\\rangle$ and vice-versa, and out comes the secret state.\n", + "\n", + "It should now be clear why we can't use this setup to send information faster than light. We might get lucky and measure $|0\\rangle_A|0\\rangle_Q,$ meaning Bob has exactly the secret state, instantly. But Bob doesn't know that until we call him and tell him \"We measured $|0\\rangle_A|0\\rangle_Q$, so you don't have to do anything.\"\n", + "\n", + "In the thought experiment, the qubits are often physically separated and taken to a new location. IBM® quantum computers use solid-state qubits on a chip that can't be separated. So instead of moving Alice and Bob to different locations, we will separate the information on the chip itself by using so-called \"swap gates\" to move the information from one qubit to another." + ] + }, + { + "cell_type": "markdown", + "id": "efa0afed-97e7-42fd-a06d-7c095e703537", + "metadata": {}, + "source": [ + "## Experiment 1: Basic teleportation\n", + "\n", + "IBM Quantum recommends tackling quantum computing problems using a framework we call \"Qiskit patterns\". It consists of the following steps.\n", + "- Step 1: Map your problem to a quantum circuit\n", + "- Step 2: Optimize your circuit for running on real quantum hardware\n", + "- Step 3: Execute your job on IBM quantum computers using Runtime Primitives\n", + "- Step 4: Post-process the results\n", + "\n", + "### Step 1: Map your problem to a quantum circuit\n", + "\n", + "All the math we did above was outlining step 1. We will implement it now, building our quantum circuit using Qiskit! We start creating a quantum circuit with three qubits, and entangling the two qubits of Alice and Bob. We will take these to be qubits 1 and 2, and we will reserve qubit 0 for the secret state." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6e3e0d07-b52f-4e17-9233-0a00065f2d77", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map your problem to a quantum circuit\n", + "\n", + "# Import some general packages\n", + "from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister\n", + "import numpy as np\n", + "\n", + "# Define registers\n", + "secret = QuantumRegister(1, \"Q\")\n", + "Alice = QuantumRegister(1, \"A\")\n", + "Bob = QuantumRegister(1, \"B\")\n", + "\n", + "cr = ClassicalRegister(3, \"c\")\n", + "\n", + "qc = QuantumCircuit(secret, Alice, Bob, cr)\n", + "\n", + "# We entangle Alice's and Bob's qubits as in our work above. We apply a Hadamard gate and then a\n", + "# CNOT gate.\n", + "# Note that the second argument in the CNOT gate is the target.\n", + "qc.h(Alice)\n", + "qc.cx(Alice, Bob)\n", + "\n", + "# Inserting a barrier changes nothing about the logic. It just allows us to force gates to be\n", + "# positioned in \"layers\".\n", + "qc.barrier()\n", + "\n", + "# Now we will use random variables to create the secret state. Don't worry about the \"u\" gate and\n", + "# the details.\n", + "np.random.seed(42) # fixing seed for repeatability\n", + "theta = np.random.uniform(0.0, 1.0) * np.pi # from 0 to pi\n", + "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", + "\n", + "# Assign the secret state to the qubit on the other side of Alice's (qubit 0), labeled Q\n", + "qc.u(theta, varphi, 0.0, secret)\n", + "qc.barrier()\n", + "\n", + "# Now entangle Q and Alice's qubits as in the discussion above.\n", + "qc.cx(secret, Alice)\n", + "qc.h(secret)\n", + "qc.barrier()\n", + "\n", + "# Now Alice measures her qubits, and stores the outcomes in the \"classical registers\" cr[]\n", + "qc.measure(Alice, cr[1])\n", + "qc.measure(secret, cr[0])\n", + "\n", + "# Now we insert some conditional logic. If Alice measures Q in a \"1\" we need a Z gate, and if Alice\n", + "# measures A in a \"1\" we need an X gate (see the table).\n", + "with qc.if_test((cr[1], 1)):\n", + " qc.x(Bob)\n", + "with qc.if_test((cr[0], 1)):\n", + " qc.z(Bob)\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "2802dfbf-e3cb-4bd8-80cd-83ec972c7bd9", + "metadata": {}, + "source": [ + "That's all we have to do to get Alice's state teleported to Bob. However, recall that when we measure a quantum state $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$ we find either $|0\\rangle$ or $|1\\rangle.$ So at the end of all this, Bob definitely has Alice's secret state, but we can't easily verify this with a measurement. In order for a measurement to tell us that we did this correctly, we have to do a trick. We had an operator labeled \"U\" for \"unitary\" which we used to prepare Alice's secret state. We can apply the inverse of U at the end of our circuit. If U mapped Alice's $|0\\rangle$ state into $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$, then the inverse of U will map Bob's $\\alpha_0 |0\\rangle+\\alpha_1|1\\rangle$ back to $|0\\rangle.$ So this last part wouldn't necessarily be done if the goal were just to move quantum information. This is only done for us to check ourselves." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dac9d281-6c57-4e70-9a61-74efa5d4bdc2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Add the inverse of U and measure Bob's qubit.\n", + "qc.barrier()\n", + "\n", + "qc.u(theta, varphi, 0.0, Bob).inverse() # inverse of u(theta,varphi,0.0)\n", + "qc.measure(Bob, cr[2]) # add measurement gate\n", + "\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "bd6643de-e67f-44f6-b83d-d7ea9f314eb8", + "metadata": {}, + "source": [ + "So if we've done this correctly, our measurement on Bob's qubit should yield a $|0\\rangle$ state. Of course, these measurements are probabilistic. So if there is even a small chance of measuring Bob's qubit to be in the $|1\\rangle$ state, then a single measurement could result in $|1\\rangle.$ We would really want to make many measurements to be assured that the probability of $|0\\rangle$ is quite high.\n", + "\n", + "### Step 2: Optimize problem for quantum execution\n", + "\n", + "This step takes the operations we want to perform and expresses them in terms of the functionality of a specific quantum computer. It also maps our problem onto the layout of the quantum computer.\n", + "\n", + "We will start by loading several packages that are required to communicate with IBM quantum computers. We must also select a backend on which to run. We can either choose the least busy backend, or select a specific backend whose properties we know.\n", + "\n", + "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f24b6b9-db1b-4077-8660-e355e377807e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_sherbrooke\n" + ] + } + ], + "source": [ + "# Load the Qiskit Runtime service\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Load the Qiskit Runtime service\n", + "\n", + "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance =\n", + "# '', token='', overwrite=True, set_as_default=True)\n", + "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", + "\n", + "# Load saved credentials\n", + "service = QiskitRuntimeService()\n", + "\n", + "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", + "backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127)\n", + "# backend = service.backend(\"ibm_brisbane\")\n", + "print(backend.name)" + ] + }, + { + "cell_type": "markdown", + "id": "35bb9c51-a573-4363-89ed-2159aaa1976f", + "metadata": {}, + "source": [ + "We explicitly enable logic on measurements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e238e397-33f2-4fa1-99b1-94c78dd966f1", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import IfElseOp\n", + "\n", + "backend.target.add_instruction(IfElseOp, name=\"if_else\")" + ] + }, + { + "cell_type": "markdown", + "id": "6074a3b9-54c3-4c52-90ee-f308bd96de76", + "metadata": {}, + "source": [ + "Now we must \"transpile\" the quantum circuit. This involves many sub-steps and is a fascinating topic. Just to give an example of a sub-step: not all quantum computers can directly implement all logical gates in Qiskit. We must write the gates from our circuit in terms of gates the quantum computer can implement. We can carry out that process, and others, using a preset pass manager. Setting ```optimization = 3``` (the highest level of optimization) ensures that the mapping from our abstract quantum circuit to the instructions given to the quantum computer is as efficient as our pre-processing can get it." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c533d4a7-c53c-4048-bf99-46e06ad9cb95", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "qc_isa = pm.run(qc)" + ] + }, + { + "cell_type": "markdown", + "id": "85f3e53c-f910-4427-a977-f6e24798542c", + "metadata": {}, + "source": [ + "A \"Sampler\" is a primitive designed to sample possible states resulting from a quantum circuit, and collect statistics on what states might be measured and with what probability. We import the Qiskit Runtime Sampler here:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d3292506-1486-48cd-bf1e-696135572eab", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the Runtime primitive and session\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "sampler = Sampler(mode=backend)" + ] + }, + { + "cell_type": "markdown", + "id": "5ea27bf8-2dbf-4d68-8375-e40e89b71341", + "metadata": {}, + "source": [ + "Not all computations on a quantum computer can be reasonably simulated on classical computers. This simple teleportation definitely can be, but it isn't at all surprising that we can classically save information in one place or another. We strongly recommend carrying out these calculations using a real IBM quantum computer. But in case you have exhausted your free monthly use, or if something must be completed in class and can't wait in the queue, this module can be completed using a simulator. To do this, simply run the cell below and uncomment the associated lines in the \"Execute\" steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2332b984-cff6-4022-b7fa-f100d35a2719", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the backend sampler\n", + "from qiskit.primitives import BackendSamplerV2\n", + "\n", + "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "\n", + "noise_model = NoiseModel.from_backend(backend)\n", + "\n", + "# Define a simulator using Aer, and use it in Sampler.\n", + "backend_sim = AerSimulator(noise_model=noise_model)\n", + "sampler_sim = BackendSamplerV2(backend=backend_sim)\n", + "\n", + "# Alternatively, load a fake backend with generic properties and define a simulator.\n", + "# backend_gen = GenericBackendV2(num_qubits=18)\n", + "# sampler_gen = BackendSamplerV2(backend=backend_gen)" + ] + }, + { + "cell_type": "markdown", + "id": "bff5bb04-fc2d-45f4-ab26-98fe9a5a0b5c", + "metadata": {}, + "source": [ + "### Step 3: Execute\n", + "\n", + "Use the sampler to run your job, with the circuit as an argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbf63f9a-5b93-4bbd-b527-f3892e2188bd", + "metadata": {}, + "outputs": [], + "source": [ + "job = sampler.run([qc_isa])\n", + "# job = sampler_sim.run([qc_isa])\n", + "res = job.result()\n", + "counts = res[0].data.c.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "a6a68d85-486a-401a-8336-805713eafd57", + "metadata": {}, + "source": [ + "### Step 4: Post-processing and analysis\n", + "\n", + "Let's plot the results and interpret them." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "47488ddf-9357-41e4-a0fb-45616bee307c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "12dcb4f6-d105-41f0-8800-87f291ce1aeb", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "Which of the states above indicate successful teleportation, and how can you tell?\n", + "\n", + "\n", + "\n", + "\n", + "The states $|000\\rangle,$ $|001\\rangle,$ $|010\\rangle,$ $|011\\rangle$ are all consistent with successful teleportation. This is because we added a gate to undo the initial preparation of the secret state. If the secret state was successfully teleported to Bob's qubit, that additional gate should return Bob's qubit to the $|0\\rangle$ state. So any state above with Bob's qubit (qubit 0, also measured to the 0th component of the classical register, and hence the highest/right-most) in the $|0\\rangle$ state indicates success.\n", + "\n", + " \n", + "\n", + "\n", + "This plot is showing all measurement outcomes for the three qubits, over 5,000 trials or \"shots\". We pointed out earlier that Alice would measure all possible states for qubits A and Q with equal likelihood. We assigned qubits 0-2 in the circuit to Q, A, and B, in that order. In little-endian notation, Bob's qubit is the left-most/lowest. So the four bars on the left correspond to Bob's qubit being $|0\\rangle$, and the other two qubits being in all possible combinations with roughly equal probability. Note that almost all (usually \\~95%) of measurements yield Bob's qubit in the $|0\\rangle$ state, meaning our setup was successful! There are a handful of shots (\\~5%) that yielded Bob's qubit in the $|1\\rangle$ state. That should not logically be possible. However, all modern quantum computers suffer from noise and errors to a much greater extent than classical computers. And quantum error correction is still an emerging field." + ] + }, + { + "cell_type": "markdown", + "id": "cd75b04b-2d2d-4d87-b6cc-b0fed440d912", + "metadata": {}, + "source": [ + "## Experiment 2: Teleporting across a processor\n", + "\n", + "Arguably, the most interesting part of quantum teleportation is that a quantum state can be teleported over long distances instantly (though the classical communication of extra gates is not instant). As already stated, we can't break qubits off the processor and move them around. But we can move the information from one qubit to another, until the qubits involved in teleportation are on opposite sides of the processor. Let us repeat the steps we took above, but now we will make a larger circuit with enough qubits to span the processor.\n", + "\n", + "### Step 1: Map your problem to a quantum circuit\n", + "\n", + "This time, the qubits corresponding to Alice and Bob will change. So we will not name a single qubit \"A\" and another \"B\". Rather, we will number the qubits and use variables to represent the current position of the information on qubits belonging to Alice and Bob. All other steps except the swap gates are as described previously." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7d7742fa-e9a2-4ca0-9d9b-f29d73e07cd9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Step 1: Map\n", + "\n", + "# Define registers\n", + "qr = QuantumRegister(13, \"q\")\n", + "\n", + "qc = QuantumCircuit(qr, cr)\n", + "\n", + "# Define registers\n", + "secret = QuantumRegister(1, \"Q\")\n", + "ebitsa = QuantumRegister(6, \"A\")\n", + "ebitsb = QuantumRegister(6, \"B\")\n", + "# q = ClassicalRegister(1, \"q meas\")\n", + "# a = ClassicalRegister(1, \"a\")\n", + "# b = ClassicalRegister(1, \"b\")\n", + "cr = ClassicalRegister(3, \"c\")\n", + "qc = QuantumCircuit(secret, ebitsa, ebitsb, cr)\n", + "\n", + "# We'll start Alice in the middle of the circuit, then move information outward in both directions.\n", + "Alice = 5\n", + "Bob = 0\n", + "qc.h(ebitsa[Alice])\n", + "qc.cx(ebitsa[Alice], ebitsb[Bob])\n", + "\n", + "# Starting with Bob and Alice in the center, we swap their information onto adjacent qubits, until\n", + "# the information is on distant qubits.\n", + "\n", + "for n in range(Alice):\n", + " qc.swap(ebitsb[Bob], ebitsb[Bob + 1])\n", + " qc.swap(ebitsa[Alice], ebitsa[Alice - 1])\n", + " Alice = Alice - 1\n", + " Bob = Bob + 1\n", + "\n", + "qc.barrier()\n", + "\n", + "# Create a random state for Alice (qubit zero)\n", + "np.random.seed(42) # fixing seed for repeatability\n", + "# theta = np.random.uniform(0.0, 1.0) * np.pi #from 0 to pi\n", + "theta = 0.3\n", + "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", + "\n", + "\n", + "qc.u(theta, varphi, 0.0, secret)\n", + "\n", + "# Entangle Alice's two qubits\n", + "qc.cx(secret, ebitsa[Alice])\n", + "qc.h(secret)\n", + "\n", + "qc.barrier()\n", + "\n", + "# Make measurements of Alice's qubits and store the results in the classical register.\n", + "qc.measure(ebitsa[Alice], cr[1])\n", + "qc.measure(secret, cr[0])\n", + "\n", + "# Send instructions to Bob's qubits based on the outcome of Alice's measurements.\n", + "with qc.if_test((cr[1], 1)):\n", + " qc.x(ebitsb[Bob])\n", + "with qc.if_test((cr[0], 1)):\n", + " qc.z(ebitsb[Bob])\n", + "\n", + "qc.barrier()\n", + "\n", + "# Invert the preparation we did for Carl's qubit so we can check whether we did this correctly.\n", + "qc.u(theta, varphi, 0.0, ebitsb[Bob]).inverse() # inverse of u(theta,varphi,0.0)\n", + "qc.measure(ebitsb[Bob], cr[2]) # add measurement gate\n", + "\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "5174ffeb-28d4-49d4-9693-5271e026b026", + "metadata": {}, + "source": [ + "You can see in the circuit diagram that the logical steps are the same. The only difference is that we used the swap gates to bring Alice's qubit's state from qubit 6 ($A_5$) up to qubit 1 ($A_0$), right next to Q. And we used swap gates to bring Bob's initial state from qubit 7 ($B_0$) down to qubit 12 ($B_5$). Note that the state on qubit 12 is not even related to Q's secret state until measurements are made on the distant qubits 0 and 1, and the state on qubit 12 is not equal to the secret state until after the conditional $X$ and $Z$ gates are applied.\n", + "\n", + "### Step 2: Optimize your circuit\n", + "\n", + "Normally, when we use the pass manager to transpile and optimize our circuits, it makes sense to set ```optimization_level = 3```, because we want our circuits to be as efficient as possible. In this case, there is no computational reason for us to transfer states from qubits 6 and 7 over to qubits 1 and 12. That was just something we did to demonstrate teleportation over a distance. If we ask the pass manager to optimize our circuit, it will realize there is no logical reason for these swap gates, and it will remove them and carry out the gate operations on adjacent qubits. So for this special case, we use ```optimization_level = 0```." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d7865560-a8a6-4abb-af8d-6a86ecc76bff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "105\n" + ] + } + ], + "source": [ + "# Step 2: Transpile\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "target = backend.target\n", + "pmzero = generate_preset_pass_manager(target=target, optimization_level=0)\n", + "\n", + "qc_isa_zero = pmzero.run(qc)\n", + "\n", + "print(qc_isa_zero.depth())" + ] + }, + { + "cell_type": "markdown", + "id": "38c1dc99-e5e0-4969-b7a0-cdc1e4e6bedd", + "metadata": {}, + "source": [ + "We can visualize where on the quantum processor these qubits are using the ```plot_circuit_layout``` function." + ] + }, + { + "cell_type": "markdown", + "id": "6a74e5a5-d32f-41a5-adf8-29174a0d5e8a", + "metadata": {}, + "source": [ + "### Step 3: Execute\n", + "\n", + "As before, we recommend running on real IBM quantum computers. If your monthly free usage has been reached, feel free to uncomment the simulator cells to run on a simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "5536d98a-480b-4193-8c63-2ef77d1aeda0", + "metadata": {}, + "outputs": [], + "source": [ + "# This required 5 s to run on a Heron r2 processor on 10-28-24\n", + "job = sampler.run([qc_isa_zero])\n", + "# job = sampler_sim.run([qc_isa_zero])\n", + "counts = job.result()[0].data.c.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "13b6375b-73c7-436d-bf1c-98de45ef4056", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "\n", + "plot_histogram(counts)" + ] + }, + { + "cell_type": "markdown", + "id": "49ed3ba0-3482-4ad5-84c3-f8fba80d8668", + "metadata": {}, + "source": [ + "### Step 4: Classical post-processing\n", + "\n", + "Again we see that the probabilities for the possible outcomes for Alice's qubits are fairly uniform. There is a strong preference for finding Bob's qubit in $|0\\rangle$ after inverting the secret code, meaning there is a strong probability that we correctly teleported the secret state across the processor from Q to Bob (qubits 0 to 12). However, we note that there is now about a higher chance of *not* measuring $|0\\rangle$ for Bob. This is an important lesson in quantum computing: the more gates you have, especially multi-qubit gates like swap gates, the more noise and errors you will encounter." + ] + }, + { + "cell_type": "markdown", + "id": "ede2724d-536b-472a-b4bd-b1276b5b851d", + "metadata": {}, + "source": [ + "## Questions\n", + "\n", + "Instructors can request versions of these notebooks with answer keys and guidance on placement in common curricula by filling out this [quick survey](https://ibm.biz/classrooms_instructor_key_request) on how the notebooks are being used.\n", + "\n", + "### Critical concepts\n", + "\n", + "- Qubits can be entangled, meaning a measurement of one qubit affects or even determines the state of another qubit.\n", + "- Entanglement differs from classical correlations; for example, qubits A and B could be in a superposition of states like $\\alpha_0|00\\rangle+\\alpha_1|11\\rangle.$ The state of A or B could be undetermined by nature, and yet A and B could still be guaranteed to be in the same state.\n", + "- Through a combination of entanglements and measurements, we can transfer a state (which can store information) from one qubit to another. This transfer can even be done over long distances, and this is called quantum teleportation.\n", + "- Quantum teleportation relies on quantum measurements, which are probabilistic. Thus, classical communication can be necessary to tweak the teleported states. This prevents quantum teleportation from moving information faster than light. Quantum teleportation does not violate relativity or causality.\n", + "- Modern quantum computers are more susceptible to noise and errors than classical computers. Expect a few percent error.\n", + "- The more gates you add in sequence (especially 2-qubit gates) the more errors and noise you can expect.\n", + "\n", + "\n", + "### True/False questions\n", + "\n", + "1. T/F Quantum teleportation can be used to send information faster than light.\n", + "2. T/F Modern evidence suggests that the collapse of a quantum state propagates faster than light.\n", + "3. T/F In Qiskit, qubits are ordered in states with the lowest-numbered qubit on the right, as in $|q_3,q_2,q_1, q_0\\rangle$\n", + "\n", + "\n", + "### MC questions\n", + "\n", + "1. Qubits A and B are entangled, then separated by a great distance $d$. Qubit A is measured. Which statement is correct about the speed at which the state of qubit B is affected?\n", + "\n", + "- a. Qubit B is affected instantly, within experimental tolerance, in experiments run so far.\n", + "- b. Qubit B is affected after a time $d/c$, meaning the quantum state \"collapses\" at approximately the speed of light, within experimental tolerance.\n", + "- c. Qubit B is affected only after classical communication has occurred, meaning it happens in a time longer than $d/c$.\n", + "- d. None of the above\n", + "\n", + "2. Recall that measurement probability is related to amplitudes in quantum states. For example, if a qubit is initially in the state $\\alpha_0|0\\rangle+\\alpha_1 |1\\rangle,$ the probability of measuring the state $|0\\rangle$ is $|\\alpha_0|^2.$ Not all sets of measurements will exactly match these probabilities, due to finite sampling (just as flipping a coin might yield heads twice in a row). The measurement histogram below could correspond to which of the following quantum states? Select the best option.\n", + "\n", + "![entangled_teleportation_fig](/learning/images/modules/computer-science/quantum-teleportation/entangled_teleportation_fig.avif)\n", + "\n", + "- a. $|0\\rangle$\n", + "- b. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle-|1\\rangle\\right)$\n", + "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle+|1\\rangle\\right)$\n", + "- d. $\\frac{4}{5}|0\\rangle+\\frac{3}{5}|1\\rangle$\n", + "- e. $\\frac{3}{5}|0\\rangle+\\frac{4}{5}|1\\rangle$\n", + "\n", + "\n", + "3. Which of the following states show(s) qubits A and B entangled? Select all that apply.\n", + "- a. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)$\n", + "- b. $\\frac{4}{5}|0\\rangle_B|0\\rangle_A+\\frac{3}{5}|1\\rangle_B|1\\rangle_A$\n", + "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|1\\rangle_A-|1\\rangle_B|0\\rangle_A\\right)$\n", + "- d. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|0\\rangle_A\\right)$\n", + "- e. $|0\\rangle_B|0\\rangle_A$\n", + "\n", + "4. In this module, we prepared an entangled state: $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right).$ But there are many other entangled states one could use for a similar protocol. Which of the states below could yield a 2-qubit measurement histogram like the following? Select the best response.\n", + "\n", + "![entangled_teleportation_fig_0110](/learning/images/modules/computer-science/quantum-teleportation/entangled_teleportation_fig_0110.avif)\n", + "\n", + "- a. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)$\n", + "- b. $\\frac{4}{5}|0\\rangle_B|0\\rangle_A+\\frac{3}{5}|1\\rangle_B|1\\rangle_A$\n", + "- c. $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|1\\rangle_A-|1\\rangle_B|0\\rangle_A\\right)$\n", + "- d. $\\frac{4}{5}|0\\rangle_B|1\\rangle_A+\\frac{3}{5}|1\\rangle_B|0\\rangle_A$\n", + "- e. $|0\\rangle_B|0\\rangle_A$\n", + "\n", + "### Discussion questions\n", + "\n", + "1. Describe the quantum teleportation protocol, from start to finish, to your partner/group. See if they have anything to add, or if they have questions.\n", + "\n", + "2. Is there anything unique about the initial entangled state between Alice and Bob: $\\frac{1}{\\sqrt{2}}\\left(|0\\rangle_B|0\\rangle_A+|1\\rangle_B|1\\rangle_A\\right)?$ If so, what is unique about it? If not, what other entangled states could we have used?" + ] + } + ], + "metadata": { + "in_page_toc_max_heading_level": 2, + "in_page_toc_min_heading_level": 2, + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/modules/computer-science/vqe.ipynb b/learning/modules/computer-science/vqe.ipynb index 5f5f303e4bc..6c329f6d024 100644 --- a/learning/modules/computer-science/vqe.ipynb +++ b/learning/modules/computer-science/vqe.ipynb @@ -1,1907 +1,1908 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "a9fc5202-9641-4db3-ac1a-9986f28854bc", - "metadata": {}, - "source": [ - "---\n", - "title: Variational Quantum Eigensolver\n", - "description: Learn what is a variational quantum eigensolver and compute the activation energy of H+H=H2 reaction with this.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore arrowstyle UCCSD verticalalignment horizontalalignment xytext arrowprops preparable ansätze Marov Aspuru Guzik Hartrees ansä */}" - ] - }, - { - "cell_type": "markdown", - "id": "bafc481a-d2d4-4668-8389-acbe9cd3f615", - "metadata": {}, - "source": [ - "# Variational Quantum Eigensolver (VQE)\n", - "\n", - "For this module, students must have a working Python environment, and the latest versions of the following packages installed:\n", - "- `qiskit`\n", - "- `qiskit_ibm_runtime`\n", - "- `qiskit-aer`\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "To set up and install these packages, see the [Install Qiskit](/docs/guides/install-qiskit) guide. To run jobs on real quantum computers, students will need to set up an IBM Cloud account, following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "*This module has been tested and used approximately 8 minutes of QPU time. This is an estimate, and your actual usage may vary.*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cda95261-1473-492c-8a1d-1e29773086c9", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "127021b2-3419-4b9d-bf13-7ef940b50029", - "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "Since the development of the quantum mechanical model in the early 20th century, scientists have understood that electrons do not follow fixed paths around an atom's nucleus but rather exist in regions of probability called orbitals. These orbitals correspond to specific, discrete energy levels that electrons can occupy. Electrons naturally reside in the lowest available energy levels, known as the ground state. However, if an electron absorbs sufficient energy, it can jump to a higher energy level, entering an excited state. This excited state is temporary, and the electron will eventually return to a lower energy level, releasing the absorbed energy, often in the form of light. This fundamental process of energy absorption and emission is important to understanding how atoms interact and form bonds.\n", - "\n", - "When atoms come together to form molecules, their atomic orbitals combine to form molecular orbitals. The arrangement and energy levels of electrons within these molecular orbitals dictate the properties of the resulting molecule and the strength of the chemical bonds. For instance, in the formation of a hydrogen molecule ($H_2$) from two individual hydrogen atoms, the electron from each atom occupies atomic orbitals. As the atoms approach each other, these atomic orbitals overlap and combine to form new molecular orbitals — one with lower energy (a bonding orbital) and one with higher energy (an anti-bonding orbital). The two electrons, one from each hydrogen atom, will preferentially occupy the lower-energy bonding orbital, leading to the formation of a stable covalent bond that holds the $H_2$ molecule together. The energy difference between the separated atoms and the formed molecule, particularly the energy of the electrons in the molecular orbitals, determines the stability and properties of the bond.\n", - "\n", - "In the following sections, we will explore this process of molecular formation, focusing on the $H_2$ molecule. We will use a real quantum computer, combined with classical optimization techniques, to find the energy of this simple yet fundamental process. This experiment will provide a practical demonstration of how quantum computation can be applied to solve problems in computational chemistry, providing insights into the role of electron energy." - ] - }, - { - "cell_type": "markdown", - "id": "eef4da0c-8f48-47fc-88a2-9a2979c1dbc1", - "metadata": {}, - "source": [ - "## VQE - A variational quantum algorithm for eigenvalue problems\n", - "\n", - "### Approximation techniques for chemistry - variational principle and the basis set\n", - "\n", - "Erwin Schrödinger's contributions to quantum mechanics are not limited to introducing a new electronic model; fundamentally, he established wave mechanics by developing the famous time-dependent Schrödinger equation:\n", - "\n", - "$$\n", - "i\\hbar \\frac{d}{dt}|\\psi\\rangle = \\hat{H}|\\psi\\rangle\n", - "$$\n", - "\n", - "Here, $\\hat{H}$ is the Hamiltonian operator, which represents the total energy of the system, and $|\\psi\\rangle$ is the wave function that contains all the information about the system’s quantum state. (Note: $\\frac{d}{dt}$ is the total time derivative, and we do not explicitly include the energy eigenvalue $E$ here.)\n", - "\n", - "However, in many practical applications — such as determining the allowed energy levels of atoms and molecules — we instead use the time-independent Schrödinger equation (energy eigenvalue equation), which is derived from the time-dependent form by assuming a stationary state. A stationary state is a quantum state in which the probability density of finding a particle at a given point in space does not change over time.\n", - "\n", - "$$ \\hat{H}|\\psi\\rangle = E|\\psi\\rangle $$\n", - "\n", - "In this form, $E$ represents the energy eigenvalue corresponding to the quantum state $|\\psi\\rangle$. The Hamiltonian includes various energy contributions, such as the kinetic energy of electrons and nuclei, the attractive forces between electrons and nuclei, and the repulsive forces between electrons.\n", - "\n", - "Solving the energy eigenvalue equation allows us to calculate the quantized energy levels of atomic and molecular systems. However, for molecules, solving it exactly is difficult because the wave function $\\Psi$, which describes the spatial distribution of electrons, is complex and high-dimensional.\n", - "\n", - "As a result, scientists use approximation techniques to obtain practical and accurate solutions. In this work, we will focus on two key methods:\n", - "\n", - "\n", - "1. Variational principle\n", - "\n", - " This method approximates the wave function and adjusts it to get as close as possible to the target energy, usually the ground state energy of the system. The key idea behind the variational principle is simple:\n", - "\n", - " - If we guess a wave function $\\Psi_\\text{trial}$ (a \"trial function\"), the energy calculated from it will always be equal to or higher than the ground state energy ($E_0$) of the system.\n", - " $$E_\\text{approx} = \\frac{\\langle \\Psi_\\text{trial}|\\hat{H}|\\Psi_\\text{trial}\\rangle}{\\langle \\Psi_\\text{trial}|\\Psi_\\text{trial}\\rangle} \\geq E_0$$\n", - " - By adjusting parameters $\\theta$ in the trial function, $|\\Psi_\\text{trial}(\\theta)\\rangle$, we can get a better and better approximation of the ground state energy.\n", - " - Its accuracy heavily depends on the choice of the trial wave function $\\Psi_\\text{trial}$. A poorly-chosen trial function may lead to an energy estimate that is far from accurate.\n", - "\n", - "2. Basis set approximation\n", - "\n", - " The second approximation method comes in the stage of constructing the wave function — the basis set approach. In quantum chemistry, solving the Schrödinger equation exactly for molecules is almost impossible. Instead, we approximate the complex, multi-electron wave function by building it up from simpler, predefined mathematical functions. A basis set is essentially a collection of these known mathematical functions, typically centered on the atoms in the molecule, that are used as building blocks to represent the shape and behavior of the electrons in the system. Think of it like trying to recreate a detailed sculpture using only a collection of standard LEGO bricks – the more types and sizes of bricks you have (the larger the basis set), the more accurately you can approximate the original shape.\n", - "\n", - " These basis functions are often inspired by the analytical solutions for simple systems like the hydrogen atom, taking forms like Gaussian or Slater-type functions, though they are still approximations. Instead of working with the theoretically \"exact\" but intractable full molecular orbitals, we express them as a linear combination (a sum with coefficients) of these basis functions. This method is known as the Linear Combination of Atomic Orbitals (LCAO) approach when the basis functions resemble atomic orbitals. By optimizing the coefficients in this linear combination, we can find the best possible approximate wave function and energy within the limitations of the chosen basis set.\n", - " - The more functions included in the basis set, the better the approximation, but this comes at the cost of higher computational effort.\n", - " - A small basis set provides a rough estimate, while a large basis set gives more precise results at the expense of requiring more computational resources.\n", - "\n", - "To summarize, to make calculations feasible and reduce computational cost, we use the variational principle by approximating the wave function, which reduces the computational complexity and allows for iterative optimization to minimize energy. Meanwhile, the basis set approach simplifies calculations by representing atomic orbitals as a combination of predefined functions, rather than solving for a continuous wave function directly.\n", - "\n", - "\n", - "#### Check your understanding\n", - "Consider the trial wave function $\\Psi_\\text{trial}(\\alpha,x) = Ae^{- \\alpha x^2}$ where $A$ is a normalization constant and $\\alpha$ is an adjustable parameter.\n", - "\n", - "(a) Normalize the trial wave function by determining $A$ such that $$ \\int_{-\\infty}^{\\infty} |\\Psi_\\text{trial}|^2 dx = 1 $$.\n", - "\n", - "\n", - "\n", - "\n", - "To normalize given trial wave function:\n", - "\n", - "$$ \\int_{-\\infty}^{\\infty} |\\Psi_\\text{trial}|^2 dx = \\int_{-\\infty}^{\\infty} A^2 e^{-2 \\alpha x^2} dx = 1 $$\n", - "\n", - "Use the Gaussian integral:\n", - "\n", - "$$ \\int_{-\\infty}^{\\infty} e^{-a x^2} dx = \\sqrt{\\frac{\\pi}{a}} \\text{, for } a>0$$\n", - "\n", - "set $a = 2\\alpha$ then get:\n", - "$$A^2\\sqrt{\\frac{\\pi}{a}} = 1$$\n", - "$$\\therefore A = (\\frac{2\\alpha}{\\pi})^{1/4}$$\n", - "\n", - "\n", - "\n", - "\n", - "(b) Compute the expectation value of the Hamiltonian $\\hat{H}$ given by $$ \\hat{H} = -\\frac{\\hbar^2}{2m} \\frac{d^2}{dx^2} + V(x)$$ where $V(x) = \\frac{1}{2}m\\omega^2x^2$, which corresponds to a simple harmonic oscillator potential.\n", - "\n", - "\n", - "\n", - "\n", - "The Hamiltonian for a harmonic oscillator is:\n", - "\n", - "$$ \\hat{H} = -\\frac{\\hbar^2}{2m} \\frac{d^2}{dx^2} + \\frac{1}{2} m \\omega^2 x^2 $$\n", - "\n", - "**Kinetic energy expectation value**\n", - "\n", - "$$ \\langle T \\rangle = -\\frac{\\hbar^2}{2m} \\int_{-\\infty}^{\\infty} \\Psi_\\text{trial}^* \\frac{d^2}{dx^2} \\Psi_\\text{trial} dx$$\n", - "\n", - "Taking the second derivative:\n", - "\n", - "$$\\frac{d}{dx} \\Psi_\\text{trial} = -2\\alpha x A e^{-\\alpha x^2}$$\n", - "\n", - "$$\\frac{d^2}{dx^2} \\Psi_\\text{trial} = A e^{-\\alpha x^2} (4\\alpha^2 x^2 - 2\\alpha)$$\n", - "\n", - "Thus:\n", - "\n", - "$$T = -\\frac{\\hbar^2}{2m} \\int_{-\\infty}^{\\infty} A^2 e^{-2\\alpha x^2} (4\\alpha^2 x^2 - 2\\alpha) dx$$\n", - "\n", - "Using standard Gaussian integral results:\n", - "\n", - "$$\\langle T \\rangle = \\frac{\\hbar^2 \\alpha}{2m}$$\n", - "\n", - "**Potential energy expectation value**\n", - "\n", - "$$\\langle V \\rangle = \\frac{1}{2} m \\omega^2 \\int_{-\\infty}^{\\infty} x^2 |\\Psi_\\text{trial}|^2 dx$$\n", - "\n", - "Using:\n", - "\n", - "$$\\int_{-\\infty}^{\\infty} x^2 e^{-a x^2} dx = \\frac{\\sqrt{\\pi}}{2a^{3/2}}$$\n", - "\n", - "we get:\n", - "\n", - "$$\\langle V \\rangle = \\frac{m \\omega^2}{4\\alpha}$$\n", - "\n", - "**Total energy expectation value**\n", - "\n", - "$$\\therefore E_\\text{approx}(\\alpha) = \\frac{\\hbar^2 \\alpha}{2m} + \\frac{m \\omega^2}{4\\alpha}$$\n", - "\n", - "\n", - "\n", - "\n", - "(c) Use the variational principle to find the optimal $\\alpha$ by minimizing $E_\\text{approx}(\\alpha)$.\n", - "\n", - "\n", - "\n", - "\n", - "Optimize $ \\alpha $ for minimum energy\n", - "\n", - "**Differentiate**\n", - "\n", - "$$\\frac{d}{d\\alpha} \\left( \\frac{\\hbar^2 \\alpha}{2m} + \\frac{m \\omega^2}{4\\alpha} \\right) = 0$$\n", - "\n", - "Solving:\n", - "\n", - "$$\\frac{\\hbar^2}{2m} - \\frac{m \\omega^2}{4\\alpha^2} = 0$$\n", - "\n", - "$$\\alpha_\\text{opt} = \\frac{m\\omega}{2\\hbar}$$\n", - "\n", - "Substituting $ \\alpha_\\text{opt} $ into $ E_\\text{approx} $:\n", - "\n", - "$$\\therefore E_\\text{approx} = \\frac{\\hbar \\omega}{2}$$\n", - "\n", - "which matches the exact quantum harmonic oscillator ground state energy.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "ed2bd1ba-73ad-42d3-94ad-c1d4a7db0a6f", - "metadata": {}, - "source": [ - "### VQE (Variational Quantum Eigensolver)\n", - "\n", - "The variational quantum eigensolver (VQE) is the main method that we will use to explore the $H+H = H_2$ process, and here, we will take a look at what VQE is and how it works. But let's first pause and look at one very important thing through the check-in question.\n", - "\n", - "#### Check your understanding\n", - "If we already have that many strategies for chemistry problems, then why do we need a quantum computer? And what's the purpose of using both quantum and classical computers together?\n", - "\n", - "\n", - "\n", - "\n", - "Quantum computing has a chance to revolutionize chemistry by tackling problems classical computers struggle with due to the exponential scaling of quantum states. Richard Feynman famously noted that to simulate nature, computations must also be quantum [ref 1].\n", - "\n", - "For example, simulating caffeine with the simplest basis set (STO-3G) would require $10^{48}$ bits, much larger than the total number of stars in the observable universe ($10^{24}$) [ref 2]. A quantum computer can describe the electronic orbitals of caffeine with 160 qubits.\n", - "\n", - "Quantum computers naturally process quantum interactions using superposition and entanglement, which provide a promising way of enabling accurate molecular simulations. Further, we can combine the advantages of both quantum computers (electron simulation) and classical computers (data pre/post-processing, algorithm process management, optimization, and so on). These are expected to enhance materials discovery, drug design, and reaction predictions, reducing costly trial-and-error experiments. [ref 3][ref 4]\n", - "\n", - "If you want to know why quantum computers are needed for chemistry problems and why to use both quantum and classical computing resources, check out the following articles:\n", - "\n", - "- [Emerging quantum computing algorithms for quantum chemistry](https://arxiv.org/abs/2109.02873)\n", - "- [Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer](https://arxiv.org/html/2405.05068v1)\n", - "- [Quantum-centric supercomputing for materials science: A perspective on challenges and future directions](https://www.sciencedirect.com/science/article/pii/S0167739X24002012)\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Now let's go back to VQE.\n", - "\n", - "VQE combines the power of quantum computers with classical computers, fundamentally using variational principles to get the ground state energy of the system. To understand VQE, first break it down into three parts:\n", - "\n", - "![VQE workflow](/learning/images/modules/computer-science/vqe/vqe.avif)\n", - "\n", - "\n", - "#### (Quantum) Observable: The molecular Hamiltonian (energy of a molecule)\n", - "\n", - "In VQE, the molecular/atomic Hamiltonian is an observable, meaning we can measure its value through an experiment. Our goal is to find the lowest possible energy (the ground state energy) of the molecule. To do this, we use a trial quantum state, generated by a parameterized quantum circuit (ansatz). We measure the observable and optimize the quantum state until we reach the lowest possible energy.\n", - "\n", - "The basis set used for the molecular Hamiltonian determines the number of qubits required and directly affects the accuracy of VQE. Choosing the right basis set is critical for balancing efficiency and precision. To simplify calculations without changing the basis set, we can use strategies like imposing symmetry and active space reduction. Many molecules have symmetrical shapes (like a butterfly or snowflake), meaning some parts behave the same way. Instead of calculating everything separately, we can focus only on unique parts, saving quantum resources, thus leveraging symmetry. In active space reduction, we consider only the important orbitals, as not all electrons significantly impact molecular energy. Electrons close to the nucleus remain mostly unchanged, while others influence bonding. By applying these methods, we can make VQE more efficient while maintaining accuracy.\n", - "\n", - "Once we obtain a molecular Hamiltonian using the proper basis set and strategies above, we need to transform this Hamiltonian into one suitable for quantum computers. Mapping problems to Pauli operators can be quite complicated. This is especially true in quantum chemistry, which works with indistinguishable particles (electrons), since qubits are distinguishable. We will not go into the details of the mappings here, but we refer you to the following resources. A general discussion of mapping a problem to quantum operators can be found in [Quantum computing in practice](/learning/courses/quantum-computing-in-practice/index). A more detailed discussion on mapping chemistry problems into quantum operators can be found in [Quantum chemistry with VQE](/learning/courses/quantum-chem-with-vqe/index).\n", - "\n", - "For this module, we will provide you with the appropriate (one-qubit) Hamiltonians for $H$ and $H_2$ so we can focus on using the quantum computer. These one-qubit Hamiltonians are prepared by using the [STO-6G](https://en.wikipedia.org/wiki/STO-nG_basis_sets) basis set and the [Jordan-Wigner mapping](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation), which is the most straightforward mapping with the simplest physical interpretation, because it maps the occupation of one spin-orbital to the occupation of one qubit. Also, we used a [qubit reduction technique by using a symmetry of the Hamiltonian](https://arxiv.org/abs/1701.08213), which uses the patterns in how spin occupations behave to reduce the number of qubits. For the $H_2$ molecule, we assume the distance between the two hydrogen atoms is `0.735` $\\mathring A$.\n", - "\n", - "#### (Quantum) Ansatz: The trial wave function (How to build a trivial quantum state with a quantum circuit)\n", - "\n", - "For VQE, the ansatz (plural: ansätze) consists of two key components. The first is initial state preparation, which sets up the qubit's state by applying quantum gates with no variational parameter. The second component is the parameterized quantum circuit, a special quantum circuit with adjustable parameters, similar to dials on a radio. These parameters will be used for the last part — the classical optimizer — to help us reach the best possible ground state.\n", - "\n", - "In the variational principle section, we learned that the quality of the trial state affects the quality of the results of the variational algorithm. This means that choosing a good ansatz is important in VQE. Once again, this is a rich and complex topic. We will not cover the different types of ansatz or their origins here. If you're interested in learning more about parameterized quantum circuits and ansatz, you can explore the [Ansatz and variational form](/learning/courses/variational-algorithm-design/ansaetze-and-variational-forms) lesson from the [Variational algorithm design course](/learning/courses/variational-algorithm-design/index), which provides detailed explanations and examples of ansätze.\n", - "\n", - "Since we are going to use a one-qubit Hamiltonian in this module, we need a one-qubit parameterized quantum circuit as an ansatz. We will see three types of one-qubit ansätze in the following section. We will compare them and discuss key considerations in selecting an ansatz." - ] - }, - { - "cell_type": "markdown", - "id": "a8fae507-41e6-4585-8e9d-b45db5055fb9", - "metadata": {}, - "source": [ - "#### (Classical) Optimizer: fine-tuning the quantum circuit\n", - "\n", - "Once the quantum computer measures the energy of the observable from the ansatz, the parameters of the ansatz and the energy value are sent to the classical optimizer for tuning. This optimization process is performed on a classical computer, typically using general-purpose scientific packages like SciPy.\n", - "\n", - "The classical optimizer treats the measured energy as a cost function. In optimization problems, a cost function (also sometimes called an objective function) is a mathematical function that measures how \"good\" a particular solution is. The goal of the optimizer is to find the set of parameters that minimizes this cost function. In the context of finding the ground state energy of a molecule, the energy itself serves as the cost function – we want to find the parameters for our quantum circuit (our \"solution\") that yield the lowest possible energy. The classical optimizer uses this measured energy value (the cost) and determines the next set of optimized parameters for the quantum ansatz. These updated parameters are then sent back to the quantum circuit, and the process is repeated. With each iteration, the classical optimizer adjusts the parameters to try and reduce the energy (minimize the cost function) until a predefined convergence criterion is met, ideally ensuring that the lowest possible energy (corresponding to the ground state of the molecule for that bond distance and basis set) is found.\n", - "\n", - "There are many optimization strategies provided by scientific packages like SciPy. You can find more in the [Optimization loops](/learning/courses/variational-algorithm-design/optimization-loops) lesson of the [Variational algorithm design](/learning/courses/variational-algorithm-design/index) course. Here we will use COBYLA (Constrained Optimization BY Linear Approximations), an optimization algorithm suitable for complicated energy landscapes. In particular, COBYLA does not attempt to calculate a gradient of the function being studied; this is called a gradient-free optimizer. Imagine you are trying to find the highest peak in a mountain range with your eyes closed. Since you can’t see the whole landscape, you take small steps in different directions, while checking if you’re going up or down. COBYLA works in a similar way — it moves through the parameter space, testing different values, gradually improving the result until it finds the best one.\n", - "\n", - "Now you are ready to carry out a VQE calculation. To that end, try the check-in question below, which recaps the overall process." - ] - }, - { - "cell_type": "markdown", - "id": "8cdcfcc0-a6ca-4153-a11a-bd7b598f9b35", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "Fill in the blanks with the correct terms to complete the summary of the VQE process, then click to check your answers.\n", - "\n", - "VQE is a variational quantum algorithm, which combines the power of (1) ________ and classical computing, used to find the (2) __________ of a molecule. The process begins by defining the (3) __________, which represents the total energy of the system and acts as the observable in quantum measurements. Next, we prepare an (4) __________, a quantum circuit with adjustable parameters that represents the trial wave function of the molecule. These parameters are optimized using a (5) __________, a classical algorithm that adjusts parameters iteratively to minimize the measured energy. In the discussion above we used the (6) __________ optimizer, which refines the ansatz parameters without needing derivative calculations. The process continues until we reach (7) __________, meaning we have found the lowest possible energy of the molecule.\n", - "\n", - "Word Bank:\n", - "\n", - "- classical optimizer\n", - "- ground state energy\n", - "- hardware-efficient\n", - "- ansatz\n", - "- molecular Hamiltonian\n", - "- COBYLA\n", - "- quantum computing\n", - "- convergence\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "1 → quantum computing\n", - "\n", - "2 → ground state energy\n", - "\n", - "3 → molecular Hamiltonian\n", - "\n", - "4 → ansatz\n", - "\n", - "5 → classical optimizer\n", - "\n", - "6 → COBYLA\n", - "\n", - "7 → convergence\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "64a090f7-1d18-4edb-bef3-4cea0f4cec2c", - "metadata": {}, - "source": [ - "## Compute the ground state energy of a hydrogen atom with VQE\n", - "\n", - "Now, let's use what we've learned to compute the ground state energy of a hydrogen atom. Throughout the module, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks down workflows into the following steps:\n", - "\n", - "- Step 1: Map classical inputs to a quantum problem\n", - "- Step 2: Optimize problem for quantum execution\n", - "- Step 3: Execute using Qiskit Runtime primitives\n", - "- Step 4: Post-processing and classical analysis\n", - "\n", - "![Qiskit pattern](/learning/images/modules/computer-science/vqe/patterns.svg)\n", - "\n", - "We will generally follow these steps.\n", - "\n", - "Let's start by loading some necessary packages, including Qiskit Runtime primitives. We will also select the least busy quantum computer available to us.\n", - "\n", - "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18d9c613-ee5f-4e38-bc83-57d25197b200", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ibm_brisbane\n" - ] - } - ], - "source": [ - "# Load the Qiskit Runtime service\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "# Load the Runtime primitive and session\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", - "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", - "\n", - "# Load saved credentials\n", - "service = QiskitRuntimeService()\n", - "\n", - "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", - "backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127)\n", - "# backend = service.backend(\"ibm_brisbane\")\n", - "print(backend.name)" - ] - }, - { - "cell_type": "markdown", - "id": "0c4e0ac4-7f4c-4cab-b726-1d6fe1ca10ac", - "metadata": {}, - "source": [ - "The cell below will allow you to switch between using the simulator or real hardware throughout the notebook. We recommend running it now:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "4c38f002-c6e6-4fe2-acf1-3cccf3d1f525", - "metadata": {}, - "outputs": [], - "source": [ - "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", - "from qiskit_aer import AerSimulator\n", - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "# Alternatively, load a fake backend with generic properties and define a simulator.\n", - "\n", - "\n", - "noise_model = NoiseModel.from_backend(backend)\n", - "\n", - "# Define a simulator using Aer, and use it in Sampler.\n", - "backend_sim = AerSimulator(noise_model=noise_model)" - ] - }, - { - "cell_type": "markdown", - "id": "497acce3-f34c-43b3-a014-630702a3620b", - "metadata": {}, - "source": [ - "### Step 1: Map the problem to quantum circuits and operators\n", - "\n", - "We start our VQE calculation by defining the Hamiltonian for the hydrogen molecule ($H_2$) at a specific bond distance. This Hamiltonian represents the total energy of the system in terms of qubit operators, having been produced and mapped from the molecular system using a standard procedure: 1) employing the STO-6G basis set (a specific collection of mathematical functions used to approximate the electron orbitals), 2) applying the Jordan-Wigner mapping (a technique to translate fermionic operators describing electrons into qubit operators), and 3) performing qubit reduction using parity of the Hamiltonian to simplify the problem.\n", - "\n", - "As we previously explained, the computed ground state energies depend heavily on the basis set selection and the molecular geometry (like bond distance). For this specific configuration and after these transformations, the resulting qubit Hamiltonian is simple:\n", - "\n", - "$$\\hat{H} = -0.2355 I + 0.2355 Z$$\n", - "\n", - "Here, $I$ represents the identity operator and $Z$ represents the Pauli-Z operator, acting on a single qubit. The coefficients are derived from the integrals calculated using the STO-6G basis set at this particular bond distance with proper transformation.\n", - "\n", - "With this Hamiltonian defined, we can now use VQE to compute its ground state energy. It is useful to compare our calculated ground state energy to expected values. For a single, isolated hydrogen atom (H), the ground state energy is exactly -0.5 Hartree (in the absence of relativistic effects). Let us compute the exact ground state energy of *our specific qubit Hamiltonian* as defined above and compare it to relevant known values." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "aaef7daa-6dbe-4a2e-abe2-3ad2a0baa860", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The exact ground state energy of the Hamiltonian is -0.471 hartree\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "import numpy as np\n", - "\n", - "# Qubit Hamiltonian of the hydrogen atom generated by using STO-3G basis set and parity mapping\n", - "Hamiltonian = SparsePauliOp.from_list([(\"I\", -0.2355), (\"Z\", 0.2355)])\n", - "\n", - "# exact ground state energy of Hamiltonian\n", - "\n", - "A = np.array(Hamiltonian)\n", - "eigenvalues, eigenvectors = np.linalg.eig(A)\n", - "print(\n", - " \"The exact ground state energy of the Hamiltonian is \",\n", - " min(eigenvalues).real,\n", - " \"hartree\",\n", - ")\n", - "h = min(eigenvalues.real)" - ] - }, - { - "cell_type": "markdown", - "id": "ec53e133-d8ab-4e5d-a44a-428fe8095aee", - "metadata": {}, - "source": [ - "Next, we need a parameterized quantum circuit, an ansatz, to prepare a trial wave function $\\Psi_\\text{trial}$ for the ground state. The goal is to find the parameters $\\theta$ that minimize the energy expectation value $\\langle\\psi(\\theta)|\\hat{H}|\\psi(\\theta)\\rangle$. The choice of ansatz is crucial because it determines the set of possible quantum states that our circuit can prepare. A \"good\" ansatz is one that is flexible enough to represent a state very close to the true ground state of the Hamiltonian we are studying, but not so complex that it requires too many parameters or too deep a circuit for current quantum computers.\n", - "\n", - "Here, we will try three different one-qubit ansätze to see which one provides better \"coverage\" of the possible quantum states a single qubit can be in. The \"coverage\" refers to the range of quantum states that the ansatz circuit can produce by varying its parameters.\n", - "\n", - "We will use three ansätze based on different combinations of single-qubit rotational gates:\n", - "\n", - "- One 1-axis rotational gate ansatz: This ansatz uses rotations around only a single axis ($R_x(\\theta)$). On the Bloch sphere, this corresponds to moving only along a specific circle. This is the least flexible and covers a limited set of states.\n", - "- Two 2-axis rotational gate ansätze: These ansätze combines rotations around two different axes ($R_x(\\theta_1) R_z(\\theta_2)$ and $R_x(\\theta_1) R_z(\\theta_2) R_x(\\theta_3)$). This allows us to reach a larger portion of the Bloch sphere, compared to a single-axis rotation.\n", - "\n", - "\n", - "By comparing the VQE results obtained with these three ansätze, we can see how the flexibility and state-space coverage of the ansatz impact our ability to find the true ground state energy of our simplified Hamiltonian. A more flexible ansatz has the *potential* to find a better approximation, but it might also be harder for the classical optimizer." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "653b257f-9975-4bd3-9c48-a046ec4a9fae", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit import Parameter\n", - "from qiskit.quantum_info import Statevector, DensityMatrix, Pauli\n", - "\n", - "theta = Parameter(\"θ\")\n", - "phi = Parameter(\"φ\")\n", - "lam = Parameter(\"λ\")\n", - "\n", - "ansatz1 = QuantumCircuit(1)\n", - "ansatz1.rx(theta, 0)\n", - "\n", - "ansatz2 = QuantumCircuit(1)\n", - "ansatz2.rx(theta, 0)\n", - "ansatz2.rz(phi, 0)\n", - "\n", - "ansatz3 = QuantumCircuit(1)\n", - "ansatz3.rx(theta, 0)\n", - "ansatz3.rz(phi, 0)\n", - "ansatz3.rx(lam, 0)" - ] - }, - { - "cell_type": "markdown", - "id": "26707a66-7098-4f57-93fc-eb1cb4c305f7", - "metadata": {}, - "source": [ - "Now, let's generate 5000 random numbers for each parameter and plot the distribution of random quantum states, generated by the three ansätze with these random parameters. You can think of these parameters like rotations around different axes on a spherical surface. To see the distribution of quantum state, we will use [the Bloch Sphere](https://en.wikipedia.org/wiki/Bloch_sphere), a three-dimensional sphere that shows the state of a single qubit. Any point on the sphere represents a possible state of the qubit, where the north and south poles are like the classical \"0\" and \"1\", but the qubit can also be anywhere in between, showing special quantum properties like superposition. First, prepare the necessary functions to plot the 3D Bloch sphere and prepare 5000 random parameters." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6e47463b-b0c2-44cd-b099-9f50f38ebd0e", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "\n", - "def plot_bloch(bloch_vectors):\n", - " # Extract X, Y, Z coordinates for 3D projection\n", - " X_coords = bloch_vectors[:, 0]\n", - " Z_coords = bloch_vectors[:, 2]\n", - "\n", - " # Compute Y coordinates from X and Z to approximate the full Bloch sphere projection\n", - " Y_coords = bloch_vectors[:, 1]\n", - "\n", - " # Create 3D plot\n", - " fig = plt.figure(figsize=(8, 8))\n", - " ax = fig.add_subplot(111, projection=\"3d\")\n", - " ax.scatter(X_coords, Y_coords, Z_coords, color=\"blue\", alpha=0.6)\n", - "\n", - " # Labels and title\n", - " ax.set_xlabel(\"X\")\n", - " ax.set_ylabel(\"Y\")\n", - " ax.set_zlabel(\"Z\")\n", - " ax.set_title(\"Parameterized 1-Qubit Circuit on 3D Bloch Sphere\")\n", - "\n", - " # Set axis limits and make them equal\n", - " ax.set_xlim([-1, 1])\n", - " ax.set_ylim([-1, 1])\n", - " ax.set_zlim([-1, 1])\n", - "\n", - " # Ensure equal aspect ratio for all axes\n", - " ax.set_box_aspect([1, 1, 1]) # Equal scaling for x, y, z axes\n", - "\n", - " # Show grid\n", - " ax.grid(True)\n", - "\n", - " plt.show()\n", - "\n", - "\n", - "num_samples = 5000 # Number of random states\n", - "theta_vals = np.random.uniform(0, 2 * np.pi, num_samples)\n", - "phi_vals = np.random.uniform(0, 2 * np.pi, num_samples)\n", - "lam_vals = np.random.uniform(0, 2 * np.pi, num_samples)" - ] - }, - { - "cell_type": "markdown", - "id": "af24b916-fa42-4c4c-8e22-098adfb4fcd5", - "metadata": {}, - "source": [ - "Let's see how our first ansatz works." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "6471a5d1-287a-4726-9e65-e23ef77ccf75", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# List to store Bloch Sphere XZ coordinates\n", - "bloch_vectors = []\n", - "\n", - "# Generate quantum states and extract Bloch vectors\n", - "for i in range(num_samples):\n", - " # Create a circuit and bind parameters\n", - " qc = ansatz1\n", - " bound_qc = qc.assign_parameters({theta: theta_vals[i]}) # , lam: lam_vals[i]})\n", - " state = Statevector.from_instruction(bound_qc)\n", - " rho = DensityMatrix(state)\n", - "\n", - " X = rho.expectation_value(Pauli(\"X\")).real\n", - " Y = rho.expectation_value(Pauli(\"Y\")).real\n", - " Z = rho.expectation_value(Pauli(\"Z\")).real\n", - " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", - "\n", - "# Convert to a numpy array for plotting\n", - "bloch_vectors = np.array(bloch_vectors)\n", - "\n", - "plot_bloch(bloch_vectors)" - ] - }, - { - "cell_type": "markdown", - "id": "9cb7d81e-8df5-4557-8452-98f794bdc5cd", - "metadata": {}, - "source": [ - "We can see our first ansatz returns a ring-shaped distributed quantum states of the Bloch sphere. This makes sense, because we have only given the ansatz a single rotational parameter. It can therefore only produce states rotated around one axis. Starting from the point $(0,0,1)$ and rotating around one axis will always yield a ring. Then let's check our second ansatz, which has two orthogonal rotational gates - `Rx` and `Rz`." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "9cd86049-f652-4e76-a68a-2780cb7782db", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "bloch_vectors = []\n", - "\n", - "# Generate quantum states and extract Bloch vectors\n", - "for i in range(num_samples):\n", - " # Create circuit and bind parameters\n", - " qc = ansatz2\n", - " bound_qc = qc.assign_parameters(\n", - " {theta: theta_vals[i], phi: phi_vals[i]}\n", - " ) # , lam: lam_vals[i]})\n", - " state = Statevector.from_instruction(bound_qc)\n", - " rho = DensityMatrix(state)\n", - "\n", - " X = rho.expectation_value(Pauli(\"X\")).real\n", - " Y = rho.expectation_value(Pauli(\"Y\")).real\n", - " Z = rho.expectation_value(Pauli(\"Z\")).real\n", - " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", - "\n", - "# Convert to numpy array for plotting\n", - "bloch_vectors = np.array(bloch_vectors)\n", - "\n", - "plot_bloch(bloch_vectors)" - ] - }, - { - "cell_type": "markdown", - "id": "e3fde737-d565-4200-a968-c6a6c78dfe30", - "metadata": {}, - "source": [ - "Here, we can see that our second ansatz covers a larger portion of the Bloch sphere - but note that the dots are more concentrated around the poles and more spread out around the equator. Now it is time to check our last ansatz." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ec2e9a84-46b2-4143-8cde-d981e9edd3ce", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "bloch_vectors = []\n", - "\n", - "# Generate quantum states and extract Bloch vectors\n", - "for i in range(num_samples):\n", - " # Create circuit and bind parameters\n", - " qc = ansatz3\n", - " bound_qc = qc.assign_parameters(\n", - " {theta: theta_vals[i], phi: phi_vals[i], lam: lam_vals[i]}\n", - " )\n", - " state = Statevector.from_instruction(bound_qc)\n", - " rho = DensityMatrix(state)\n", - "\n", - " X = rho.expectation_value(Pauli(\"X\")).real\n", - " Y = rho.expectation_value(Pauli(\"Y\")).real\n", - " Z = rho.expectation_value(Pauli(\"Z\")).real\n", - " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", - "\n", - "# Convert to numpy array for plotting\n", - "bloch_vectors = np.array(bloch_vectors)\n", - "\n", - "plot_bloch(bloch_vectors)" - ] - }, - { - "cell_type": "markdown", - "id": "ca886db5-e7c7-4f5c-8594-50f79fbc61dd", - "metadata": {}, - "source": [ - "Here you can see more evenly distributed quantum states generated by our last ansatz.\n", - "\n", - "As mentioned, the best thing to do is to gain knowledge about the ground state you're seeking and use an ansatz that is well-suited to probe states close to that ground state. For example, if we knew that our ground state was near a pole, we might select ansatz 2. For simplicity, we will stick with ansatz 3, which uniformly probes the entire Bloch sphere.\n", - "\n", - "Now that we have selected our ansatz, let's draw the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9f0c76d9-b4b2-46dc-a261-7c624c031177", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This circuit has 3 parameters\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", - "\n", - "ansatz = ansatz3\n", - "\n", - "num_params = ansatz.num_parameters\n", - "print(\"This circuit has \", num_params, \"parameters\")\n", - "\n", - "ansatz.draw(\"mpl\", style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "b7dc624b-37bc-4523-8532-e45c7826ce64", - "metadata": {}, - "source": [ - "### Step 2: Optimize for target hardware\n", - "\n", - "When running a calculation on a real quantum computer, we don't just care about the logic of the quantum circuit. We also care about things like what operations can be performed by that particular quantum computer, and where on the quantum computer are the qubits we are using. Are they right next to each other? Are they far apart? Therefore, the next step is to rewrite our circuit using gates that are natural for the quantum computer we'll use, and taking qubit layout into account. This can be done by `transpilation` - after this process, you can see our simple ansatz converted into a different set of gates, and our abstract qubits will be mapped into physical qubits on a real quantum computer." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "d4f50ff9-cdf4-483f-9bb8-5814fe5acc53", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Backend: {config.backend_name}\n", - "Native gates: ['ecr', 'id', 'delay', 'measure', 'reset', 'rz', 'sx', 'x'] ,\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "config = backend.configuration()\n", - "\n", - "print(\"Backend: {config.backend_name}\")\n", - "print(\"Native gates: \", config.supported_instructions, \",\")\n", - "\n", - "\n", - "target = backend.target\n", - "\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "ansatz_isa = pm.run(ansatz)\n", - "\n", - "ansatz_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" - ] - }, - { - "cell_type": "markdown", - "id": "04feccf8-4591-4919-90ba-9b0d72e1b436", - "metadata": {}, - "source": [ - "You can see the `rx, rz` gates of our ansatz were converted into a series of `rz, sx` gates, which are the native gates of our backend. Also, you can see our `q0` is now mapped into the fifth physical qubit. We also need to map our Hamiltonian according to these changes, as in the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "2e105b4d-8c3c-4b1e-9652-9857ee9e5465", - "metadata": {}, - "outputs": [], - "source": [ - "Hamiltonian_isa = Hamiltonian.apply_layout(layout=ansatz_isa.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "d3212dfa-da89-43d6-85ce-6200ecf9ca09", - "metadata": {}, - "source": [ - "### Step 3: Execute on target hardware\n", - "\n", - "Now it is time to run our VQE on a real QPU. For this, first we need a cost function for the optimization process, which evaluates the expectation value of the Hamiltonian with a quantum state, generated by the ansatz. Don't worry! You don't need to code everything by yourself. We prepared a function for this, and all you need to do is run the cell below." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "6b2178bd-6e86-4178-b82b-f7a6d7e95022", - "metadata": {}, - "outputs": [], - "source": [ - "def cost_func(params, ansatz, hamiltonian, estimator):\n", - " \"\"\"Return estimate of energy from estimator\n", - "\n", - " Parameters:\n", - " params (ndarray): Array of ansatz parameters\n", - " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", - " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", - " estimator (EstimatorV2): Estimator primitive instance\n", - " cost_history_dict: Dictionary for storing intermediate results\n", - "\n", - " Returns:\n", - " float: Energy estimate\n", - " \"\"\"\n", - " pub = (ansatz, [hamiltonian], [params])\n", - " result = estimator.run(pubs=[pub]).result()\n", - " energy = result[0].data.evs[0]\n", - "\n", - " cost_history_dict[\"iters\"] += 1\n", - " cost_history_dict[\"prev_vector\"] = params\n", - " cost_history_dict[\"cost_history\"].append(energy)\n", - " print(f\"Iters. done: {cost_history_dict['iters']} [Current cost: {energy}]\")\n", - "\n", - " return energy" - ] - }, - { - "cell_type": "markdown", - "id": "83ed09be-d475-4d66-b55f-b25bbf3412c8", - "metadata": {}, - "source": [ - "Finally, we prepare initial parameters for our ansatz and its optimization process. You can simply use all zeroes or random values. We have selected initial parameters below, but feel free to comment or uncomment lines in the cell to sample parameters randomly, uniformly from 0 to $2\\pi$." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "b2a5dab1-f724-48df-b84b-20749ffd5261", - "metadata": {}, - "outputs": [], - "source": [ - "# x0 = np.random.uniform(0, 2*pi, 3)\n", - "x0 = [1, 1, 0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b48b37d-1f1e-4aef-9fd5-ba594db832b0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iters. done: 1 [Current cost: -0.3361517318448143]\n", - "Iters. done: 2 [Current cost: -0.4682546422099432]\n", - "Iters. done: 3 [Current cost: -0.38985802144149584]\n", - "Iters. done: 4 [Current cost: -0.38319217316749354]\n", - "Iters. done: 5 [Current cost: -0.4628720756579032]\n", - "Iters. done: 6 [Current cost: -0.4683301936226905]\n", - "Iters. done: 7 [Current cost: -0.45480498699294747]\n", - "Iters. done: 8 [Current cost: -0.4690533242050814]\n", - "Iters. done: 9 [Current cost: -0.465867415110354]\n", - "Iters. done: 10 [Current cost: -0.4606882723137227]\n" - ] - } - ], - "source": [ - "# QPU Est. 2min for ibm_brisbane\n", - "\n", - "from scipy.optimize import minimize\n", - "from qiskit_ibm_runtime import Batch\n", - "\n", - "batch = Batch(backend=backend)\n", - "\n", - "cost_history_dict = {\n", - " \"prev_vector\": None,\n", - " \"iters\": 0,\n", - " \"cost_history\": [],\n", - "}\n", - "estimator = Estimator(mode=batch)\n", - "estimator.options.default_shots = 10000\n", - "\n", - "res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa, Hamiltonian_isa, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 10, \"tol\": 0.01},\n", - ")\n", - "\n", - "batch.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "124e15d2-ddce-4201-9f69-03c9e5612644", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The reference ground state energy is (-0.471+0j)\n", - "The computed ground state energy is -0.4690533242050814\n" - ] - } - ], - "source": [ - "h_vqe = res.fun\n", - "print(\"The reference ground state energy is \", min(eigenvalues))\n", - "print(\"The computed ground state energy is \", h_vqe)" - ] - }, - { - "cell_type": "markdown", - "id": "79bce65c-d84e-4bed-bfe8-71f448290fd1", - "metadata": {}, - "source": [ - "Congratulations! You have just finished your first quantum chemistry experiment successfully. We can see a difference between the exact ground state energy of the Hamiltonian and ours, but because we used a default error mitigation technique (which corrects readout errors), the difference is minor. This is a very good start!\n", - "\n", - "Note: You can get a better result by setting a level of error mitigation using [`resilience_level`](/docs/guides/error-mitigation-and-suppression-techniques). The default value is 1, and if you set a higher value, it will use more QPU time but might return a better result." - ] - }, - { - "cell_type": "markdown", - "id": "dae9eeb5-a2ac-4632-90cf-d2ac70182c7b", - "metadata": {}, - "source": [ - "### Step 4: Post-process\n", - "\n", - "It is time to take a look at how our classical optimizer worked. Run the cell below and see the convergence pattern." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "b6b32cce-cbfa-4566-9d77-e511f54cc558", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "x = np.linspace(0, 10, 10)\n", - "\n", - "# Define the constant function\n", - "y_constant = np.full_like(x, h)\n", - "ax.plot(\n", - " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", - ")\n", - "ax.set_xlabel(\"Iterations\")\n", - "ax.set_ylabel(\"Cost (Hartree)\")\n", - "ax.plot(y_constant, label=\"Target\")\n", - "plt.legend()\n", - "plt.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "909ed768-080d-42b2-aae5-0fe6fa4f088b", - "metadata": {}, - "source": [ - "We started with a fairly good initial value, such that we obtained a good final value in just 10 steps. You can see big and small peaks, and this is the typical feature of the COBYLA optimizer - it searches the space as if it cannot see the landscape and adjusts step sizes with each measurement." - ] - }, - { - "cell_type": "markdown", - "id": "4b3fe46b-fb4e-48d4-a891-732231429926", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "\n", - "What is your observation? Which part of the above process is open to improvement in order to obtain results closer to the theoretical values, or closer to the precise ground state energy of the Hamiltonian? What are some things to consider for this?\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "The first thing to consider is the change in the set of bases used in calculating the Hamiltonian of molecules. As mentioned earlier, the ground state energy of the H atom is -0.5 Hartree, as is well known, and the STO-6G basis we have chosen is not enough to accurately derive this value.\n", - "\n", - "Choosing a more complex kind of basis increases the number of qubits used by the Hamiltonian; therefore, we need to select a more complex and suitable ansatz for chemistry problems.\n", - "\n", - "The next to be optimized is the management of noise in the QPU. More advanced error mitigation techniques yield better results but may take longer to use. Also, consider how the `shot_number` affects the results.\n", - "\n", - "Finally, better convergence performance can also be achieved by trying different optimizers.\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "52e936bb-a9af-4355-94a1-82393a25124e", - "metadata": {}, - "source": [ - "## Compute the ground state energy of the hydrogen molecule with VQE\n", - "\n", - "Now that we have looked at the overall process of VQE using $H$ atoms, we will now calculate the ground state energy of the $H_2$ molecule more quickly." - ] - }, - { - "cell_type": "markdown", - "id": "bcd4b1e5-aea5-4509-af22-661937b4286d", - "metadata": {}, - "source": [ - "### Step 1: Map the problem to quantum circuits and operators\n", - "\n", - "Here we also provide you with a one-qubit Hamiltonian that uses the STO-6G basis and the Jordan-Wigner transformation, with qubit reduction by using a symmetry of the Hamiltonian. Note that we used an atomic distance between two hydrogen atoms of `0.735` $\\mathring A$.\n", - "\n", - "Unlike the calculation of a single hydrogen atom ($H$), in order to compute the ground state of a hydrogen molecule($H_2$), we must also consider the repulsive force acting between the nuclei of the two hydrogen atoms, in addition to the energy associated with the electronic orbitals. In this step, we will give this value as a constant, and we will actually calculate this value in the check-in problem.\n", - "$$\\hat{H} = -1.04886 I + -0.79674 Z + 0.18122 X$$" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "3722816b-8710-4c61-9da9-a347556496e6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Electronic ground state energy (Hartree): -1.8659468547627318\n", - "Nuclear repulsion energy (Hartree): 0.71997\n", - "Total ground state energy (Hartree): -1.1459768547627318\n" - ] - } - ], - "source": [ - "h2_hamiltonian = SparsePauliOp.from_list(\n", - " [(\"I\", -1.04886087), (\"Z\", -0.7967368), (\"X\", 0.18121804)]\n", - ")\n", - "\n", - "# exact ground state energy of hamiltonian\n", - "nuclear_repulsion = 0.71997\n", - "A = np.array(h2_hamiltonian)\n", - "eigenvalues, eigenvectors = np.linalg.eig(A)\n", - "print(\"Electronic ground state energy (Hartree): \", min(eigenvalues).real)\n", - "print(\"Nuclear repulsion energy (Hartree): \", nuclear_repulsion)\n", - "print(\n", - " \"Total ground state energy (Hartree): \", min(eigenvalues).real + nuclear_repulsion\n", - ")\n", - "h2 = min(eigenvalues).real + nuclear_repulsion" - ] - }, - { - "cell_type": "markdown", - "id": "7e275302-80ea-454b-83c1-d70b09b37442", - "metadata": {}, - "source": [ - "### Step 2: Optimize for target hardware\n", - "\n", - "Since the number of qubits used by the previous VQE and Hamiltonian is the same as the backend to be used for execution, we will use the existing ansatz and its optimized form." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "6a461ab3-fbd7-493a-aaad-e47d6bfd0079", - "metadata": {}, - "outputs": [], - "source": [ - "h2_hamiltonian_isa = h2_hamiltonian.apply_layout(layout=ansatz_isa.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "da36cda3-94a8-4b2d-9e3d-3b5ac957c180", - "metadata": {}, - "source": [ - "### Step 3: Execute on target hardware\n", - "\n", - "Now it's time to do the calculations on the actual QPU. Almost everything is the same, but we will use the appropriate initial point to fit the Hamiltonian. Also, at an iterative part, some of the settings of the `Estimator`, which is used to calculate the Hamiltonian's expectations for the ansatz in the QPU, will be set slightly differently from the previous calculations. We will discuss this change further in a check-in question." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "b5afe148-bfdc-4d77-895c-d58afc118d9e", - "metadata": {}, - "outputs": [], - "source": [ - "x0 = [2, 0, 0]" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "3fb2e079-1413-4fd2-a3db-da5d11fe58d5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iters. done: 1 [Current cost: -0.710621837568328]\n", - "Iters. done: 2 [Current cost: -0.2603208441168329]\n", - "Iters. done: 3 [Current cost: -0.25548711201326424]\n", - "Iters. done: 4 [Current cost: -0.581129450619904]\n", - "Iters. done: 5 [Current cost: -1.722920997605439]\n", - "Iters. done: 6 [Current cost: -1.6633324849371915]\n", - "Iters. done: 7 [Current cost: -1.8066989598929164]\n", - "Iters. done: 8 [Current cost: -1.8051093803839542]\n", - "Iters. done: 9 [Current cost: -1.802692217571555]\n", - "Iters. done: 10 [Current cost: -1.8233585485263144]\n", - "Iters. done: 11 [Current cost: -1.6904116652617205]\n", - "Iters. done: 12 [Current cost: -1.8245120321245392]\n", - "Iters. done: 13 [Current cost: -1.6837021361383608]\n", - "Iters. done: 14 [Current cost: -1.8166632606115467]\n", - "Iters. done: 15 [Current cost: -1.863446212658907]\n" - ] - } - ], - "source": [ - "# QPU time 4min for ibm_brisbane\n", - "batch = Batch(backend=backend)\n", - "\n", - "cost_history_dict = {\n", - " \"prev_vector\": None,\n", - " \"iters\": 0,\n", - " \"cost_history\": [],\n", - "}\n", - "estimator = Estimator(mode=batch)\n", - "estimator.options.default_shots = 10000\n", - "\n", - "res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa, h2_hamiltonian_isa, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 15},\n", - ")\n", - "\n", - "batch.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "4edf1dbd-8363-4d0d-a4ca-2223719c7059", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The reference ground state energy is -1.1459768547627318\n", - "The computed ground state energy is -1.143476212658907\n" - ] - } - ], - "source": [ - "h2_vqe = res.fun + nuclear_repulsion\n", - "print(\n", - " \"The reference ground state energy is \", min(eigenvalues).real + nuclear_repulsion\n", - ")\n", - "print(\"The computed ground state energy is \", h2_vqe)" - ] - }, - { - "cell_type": "markdown", - "id": "0d30fbc5-8f52-4b84-b9a6-55b3432bbec5", - "metadata": {}, - "source": [ - "Despite VQE theoretically providing an upper bound to the true ground state energy, practical implementations on real or noisy simulated quantum hardware, as well as approximations made in preparing the Hamiltonian (like basis sets or qubit reduction), can introduce errors that sometimes result in a measured energy slightly lower than the exact theoretical value or a specific numerical reference. Although there are some errors, the results seem to be satisfactory, especially given the small number of steps. Now, let's finish this VQE calculation by looking at how the optimizer worked." - ] - }, - { - "cell_type": "markdown", - "id": "5d5c88de-fcd1-4ef6-8055-01505d86d07e", - "metadata": {}, - "source": [ - "### Step 4: Post-process" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "b20dda12-dad9-4585-9a56-31611aa00930", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "x = np.linspace(0, 5, 15)\n", - "\n", - "# Define the constant function\n", - "y_constant = np.full_like(x, min(eigenvalues))\n", - "ax.plot(\n", - " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", - ")\n", - "ax.set_xlabel(\"Iterations\")\n", - "ax.set_ylabel(\"Cost (Hartree)\")\n", - "ax.plot(y_constant, label=\"Target\")\n", - "plt.legend()\n", - "plt.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "8eab4184-b5dd-494e-9e67-c209b3f835f3", - "metadata": {}, - "source": [ - "#### Check your understanding\n", - "Let's compute the nuclear repulsion energy of $H_2$ molecule, which we included as a constant value (0.71997 Hartree).\n", - "\n", - "![H2 molecule](/learning/images/modules/computer-science/vqe/h2.avif)\n", - "\n", - "Please use [Coulomb law](https://en.wikipedia.org/wiki/Coulomb%27s_law) and [atomic unit](https://en.wikipedia.org/wiki/Atomic_units) to make sure you get the value in `Hartree`.\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Since both hydrogen nuclei are positively charged, they repel each other due to electrostatic force. This repulsion is described by Coulomb's law:\n", - "\n", - "$$E_{repulsive} = \\frac{e^2}{4\\pi\\epsilon_0R}$$\n", - "\n", - "where $e$ is a charge of proton, $\\epsilon_0$ is a vacuum permittivity, and $R$ is the distance between the two nuclei, measured in meters or Bohr radii in unit of joules(J).\n", - "\n", - "To compute this energy in Hartrees, we need to convert the above equation into the Atomic Unit (AU) system. In AU, $e^2 = 1$, $4\\pi\\epsilon_0=1$ and Bohr radius ($a_0$) is 1 and becomes the fundamental length scale in AU. With these simplifications, Coulomb's law reduces to:\n", - "\n", - "$$E_{repulsion} = \\frac{1}{R}$$\n", - "\n", - "where $R$ must be measured in Bohr radii ($a_0$).\n", - "\n", - "To convert the given nuclear separation in $\\r{A}$ into $a_0$, we need this conversion relation:\n", - "\n", - "$$1\\r{A} = 1.88973 a_0$$\n", - "\n", - "so $0.735\\r{A}$ becomes $0.735 * 1.88973 = 1.38895 a_0$.\n", - "\n", - "Therefore, the nuclear repulsion energy of a given $H_2$ is\n", - "\n", - "$$E_{repulsion} = \\frac{1}{R} = \\frac{1}{1.38895} = 0.71997 Hartree$$\n", - "\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "f75e4a10-dfdc-4416-8087-a41c1eac039c", - "metadata": {}, - "source": [ - "## Compute reaction energy of $H + H = H_2$\n", - "\n", - "Now let's use what we've obtained! You've used VQE, a variational quantum eigensolver, to calculate the ground state energy of the $H$ atom and of the $H_2$ molecule. What's left is to use the calculated values to get the reaction energy of the $H+H=H_2$ process.\n", - "\n", - "Reaction energy is the energy change that happens when substances react to form new substances. Imagine you are building something: sometimes you need to put energy into it (like stacking blocks), and sometimes energy is released (like a ball rolling downhill). In chemistry, reactions either absorb energy (endothermic) or release energy (exothermic).\n", - "\n", - "The reaction energy of $H+H = H_2$ process can be compute by the following formula:\n", - "\n", - "$E_{reaction} = E_{H_2} - (E_H + E_H)$\n", - "\n", - "By running the cell below, let's see this visually. Here we will use the exact ground state value of each Hamiltonian, and we'll compare the reaction energy of the exact solution and VQE results." - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "aa8d1a29-fbfe-41bf-8ed7-947827d0f0e6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Theoretical values\n", - "E_H_theo = h.real\n", - "E_H2_theo = h2\n", - "\n", - "# Experimental values\n", - "E_H_exp = h_vqe\n", - "E_H2_exp = h2_vqe\n", - "\n", - "# Calculate reaction energies\n", - "E_reaction_theo = E_H2_theo - (2 * E_H_theo)\n", - "E_reaction_exp = E_H2_exp - (2 * E_H_exp)\n", - "\n", - "# Set up the plot\n", - "fig, ax = plt.subplots(figsize=(8, 6))\n", - "ax.set_xlim(0, 3)\n", - "ax.set_ylim(-1.16, -0.93) # Adjust y-axis range to highlight differences\n", - "ax.set_xticks([])\n", - "ax.set_ylabel(\"Energy (Hartree)\")\n", - "ax.set_title(\"H + H → H₂ Reaction Energy Diagram\")\n", - "\n", - "# Plot theoretical energy levels\n", - "ax.hlines(\n", - " y=2 * E_H_theo, xmin=0.5, xmax=1.3, linewidth=2, color=\"r\", label=\"2H (Exact)\"\n", - ")\n", - "ax.hlines(y=E_H2_theo, xmin=1.3, xmax=2, linewidth=2, color=\"b\", label=\"H₂ (Exact)\")\n", - "\n", - "# Plot experimental energy levels\n", - "ax.hlines(\n", - " y=2 * E_H_exp,\n", - " xmin=0.5,\n", - " xmax=1.5,\n", - " linewidth=2,\n", - " color=\"r\",\n", - " linestyle=\"dashed\",\n", - " label=\"2H (VQE)\",\n", - ")\n", - "ax.hlines(\n", - " y=E_H2_exp,\n", - " xmin=1.5,\n", - " xmax=2.5,\n", - " linewidth=2,\n", - " color=\"b\",\n", - " linestyle=\"dashed\",\n", - " label=\"H₂ (VQE)\",\n", - ")\n", - "\n", - "# Add labels\n", - "ax.text(\n", - " 1,\n", - " 2 * E_H_theo,\n", - " f\"2H: {2*E_H_theo:.4f}\",\n", - " verticalalignment=\"top\",\n", - " horizontalalignment=\"left\",\n", - ")\n", - "ax.text(\n", - " 2,\n", - " E_H2_theo,\n", - " f\"H₂: {E_H2_theo:.4f}\",\n", - " verticalalignment=\"top\",\n", - " horizontalalignment=\"left\",\n", - ")\n", - "ax.text(\n", - " 1,\n", - " 2 * E_H_exp,\n", - " f\"2H_VQE: {2*E_H_exp:.4f}\",\n", - " verticalalignment=\"bottom\",\n", - " horizontalalignment=\"right\",\n", - ")\n", - "ax.text(\n", - " 2,\n", - " E_H2_exp,\n", - " f\"H₂_VQE: {E_H2_exp:.4f}\",\n", - " verticalalignment=\"bottom\",\n", - " horizontalalignment=\"right\",\n", - ")\n", - "\n", - "# Add arrows for reaction energy with ΔE label in the middle\n", - "mid_y_theo = (2 * E_H_theo + E_H2_theo) / 2\n", - "mid_y_exp = (2 * E_H_exp + E_H2_exp) / 2\n", - "ax.annotate(\n", - " \"\",\n", - " xy=(1.3, E_H2_theo),\n", - " xytext=(1.3, 2 * E_H_theo),\n", - " arrowprops=dict(arrowstyle=\"<->\", color=\"g\"),\n", - ")\n", - "ax.text(\n", - " 1.35, mid_y_theo, f\"ΔE: {E_reaction_theo:.4f}\", color=\"g\", verticalalignment=\"top\"\n", - ")\n", - "\n", - "ax.annotate(\n", - " \"\",\n", - " xy=(1.5, E_H2_exp),\n", - " xytext=(1.5, 2 * E_H_exp),\n", - " arrowprops=dict(arrowstyle=\"<->\", color=\"g\", linestyle=\"dashed\"),\n", - ")\n", - "ax.text(\n", - " 1.55,\n", - " mid_y_exp,\n", - " f\"ΔE_VQE: {E_reaction_exp:.4f}\",\n", - " color=\"g\",\n", - " verticalalignment=\"center\",\n", - ")\n", - "\n", - "# Add legend\n", - "ax.legend()\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "f1a87af7-0c69-42c5-a415-87c95752d678", - "metadata": {}, - "source": [ - "As shown in the figure, although there are some errors, the exact ground state energy of Hamiltonians and the reaction energy calculated using the VQE results are similar, close to -0.2 Hartree.\n", - "\n", - "It should be noted here that the reaction energy of this process has a negative value, which means that the energy is released through the process, and the resulting molecule has a lower energy than two single atoms." - ] - }, - { - "cell_type": "markdown", - "id": "006ded34-29e0-4702-a46a-0459ee49f416", - "metadata": {}, - "source": [ - "6. Conclusion\n", - "\n", - "Let's summarize what we've learned so far.\n", - "\n", - "We first looked at two important approximation techniques needed to solve quantum chemistry problems: the variational principle and basis set choices, which are both fundamental to VQE. We explored the variational principle by hand, calculating the ground state energy of the simple harmonic oscillator.\n", - "\n", - "Next, we explored VQE, a widely-used algorithm for calculating the ground state energy of a quantum system. We ran code to calculate the ground state energies for atomic hydrogen ($H$) and the hydrogen molecule ($H_2$). In particular, we learned that it is necessary to obtain the appropriate molecular Hamiltonian for the system and to transform it into a form executable on a quantum computer. We also saw that the ansatz, a parameterized quantum circuit, is needed to prepare trial quantum states within VQE, and we discussed the importance of choosing an appropriate ansatz circuit structure. We also learned that VQE relies on an iterative optimization process using a classical computer, guiding the quantum circuit to find the lowest energy state, and saw how the process converges.\n", - "\n", - "Finally, we used the computed ground state energies of $H$ and $H_2$ obtained through VQE to calculate the reaction energy for the process $H + H \\rightarrow H_2$.\n", - "\n", - "VQE is a powerful near-term quantum algorithm, but it's important to be aware of its limitations. The performance of VQE heavily depends on the choice of the ansatz – finding an efficiently preparable ansatz that can accurately represent the true ground state becomes challenging for larger, more complex molecules. Furthermore, current quantum hardware is susceptible to noise, which can impact the accuracy of VQE results, particularly for deeper circuits or larger numbers of qubits. Despite these challenges, VQE serves as a foundational algorithm, and ongoing research is exploring more sophisticated variational methods and error mitigation techniques to push the boundaries of what is possible in quantum chemistry on near-term quantum computers. For instance, algorithms like Sample-based Quantum Diagonalization (SQD) are being developed, which leverage samples obtained from quantum circuits combined with classical diagonalization in a subspace to improve energy estimation and address some of the limitations faced by VQE, particularly regarding measurement efficiency and noise robustness." - ] - }, - { - "cell_type": "markdown", - "id": "03c9dfc1-7367-4d71-9598-f02288c30ac0", - "metadata": {}, - "source": [ - "## Review and questions\n", - "\n", - "### Critical concepts:\n", - "\n", - "- Variational quantum algorithm is a computing paradigm in which a classical computer and a quantum computer work together to solve a problem.\n", - "- In VQE, we start with a Hamiltonian of our system and map it onto qubits for execution on the quantum computer. We select a parameterized quantum circuit, an ansatz, and make repeated measurements, varying the parameters of the ansatz, until the lowest energy value is reached. The search through parameter space is done using a classical optimizer. To achieve good results, it is necessary to select a good ansatz and an appropriate optimizer.\n", - "- Reaction energy is the total energy change in a chemical reaction, determined by the difference between the energy of the reactants and the products.\n", - "\n", - "\n", - "### True/false\n", - "1. The variational principle states that the expectation value of the energy for any trial wave function is always greater than or equal to the true ground state energy.\n", - "2. A basis set is a collection of functions used to approximate quantum wave functions.\n", - "3. VQE is a quantum algorithm used to exactly solve the Schrödinger equation for a given Hamiltonian.\n", - "4. In VQE, a parameterized quantum circuit (an ansatz) is used to prepare trial wave functions.\n", - "5. The choice of optimizer in VQE (for example, COBYLA, SPSA, or ADAM) does not impact the quality of the result.\n", - "6. Qiskit's `Estimator` is used to directly compute expectation values of Hamiltonians in VQE.\n", - "\n", - "\n", - "### Multiple-choice questions:\n", - "\n", - "1. What is the purpose of the Hamiltonian in VQE?\n", - "\n", - "- A) To generate random quantum states\n", - "- B) To determine the energy of quantum states\n", - "- C) To optimize quantum circuits\n", - "- D) To create entanglement\n", - "\n", - "2. What is the primary objective of the VQE algorithm?\n", - "\n", - "- A) To find the ground state energy of a Hamiltonian\n", - "- B) To create entanglement between qubits\n", - "- C) To perform Grover's search\n", - "- D) To break the RSA encryption\n", - "\n", - "3. How many quantum states are generated in this notebook to compare the ansatz?\n", - "- A) 100\n", - "- B) 1000\n", - "- C) 5000\n", - "- D) 10,000\n", - "\n", - "4. Why is a classical optimizer required in VQE?\n", - "- A) To perform quantum measurements\n", - "- B) Update ansatz parameters to minimize energy\n", - "- C) To entangle qubits\n", - "- D) To generate quantum randomness\n", - "\n", - "5. Why is the ansatz designed to be parameterized?\n", - "- A) To allow quantum state preparation\n", - "- B) To allow a wide space of quantum states to be searched\n", - "- C) To reduce circuit complexity\n", - "- D) To measure eigenvalues directly\n", - "\n", - "6. Which of the following is the most correct statement about choosing a good ansatz?\n", - "- A) An ansatz must produce states evenly distributed over the Bloch sphere, or it will fail.\n", - "- B) An ansatz should be tailored to your system to make sure it can generate states close to the ground state.\n", - "- C) An ansatz should produce random states using its variational parameters.\n", - "- D) A better ansatz always has more variational parameters." - ] - }, - { - "cell_type": "markdown", - "id": "7f44807a-45f6-4e36-9d19-faa77f7c0c79", - "metadata": {}, - "source": [ - "## (Optional) Appendix: Optimizer overhead by ansatz complexity\n", - "\n", - "VQE faces several well-known challenges[ref 6], and the following are related to what we have learned above.\n", - "\n", - "1. Ansatz selection challenges\n", - "\n", - "There is an inherent challenge in selecting the right variational ansatz. Chemistry-inspired ansätze (like UCCSD) provide physical accuracy but require deep circuits, while hardware-efficient ansätze have shallower circuits but may lack physical interpretability. Also, many ansätze introduce excessive variational parameters that contribute little to improving accuracy but significantly increase optimization difficulty.\n", - "\n", - "2. Optimization difficulties\n", - "\n", - "\n", - "The optimization landscape of VQE can have regions where gradients vanish exponentially (barren plateaus), making it difficult for classical optimizers to update the variational parameters efficiently. For this, researchers have tried to use different types of optimizers - gradient-based and gradient-free, but both face challenges. Gradient-based optimizers suffer from barren plateaus, while gradient-free methods require a large number of function evaluations.\n", - "\n", - "3. Optimizer overhead\n", - "\n", - "One more well-known challenge is optimizer overhead, which is related to the scale of the problem. The quantum circuits required for VQE grow in depth and complexity as the problem size increases; this typically also increases the number of parameters to optimize. The optimization process becomes intractable as the number of parameters increases, leading to slow convergence and difficulties in finding the optimal solution.\n", - "\n", - "Here we will take a look at these challenges by using VQE for a $H_2$ molecule, with two different types of ansätze.\n", - "\n", - "(Note: This can take more QPU time, so feel free to use a simulator for this if you don't have enough time.)" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "96179a4e-33f9-43ff-a791-f5ac8114dd29", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import ParameterVector\n", - "\n", - "num_iter = 4\n", - "alpha = ParameterVector(\"alpha\", 3)\n", - "beta = ParameterVector(\"beta\", 3 * num_iter)\n", - "\n", - "# step1: Map problem to quantum circuits and operators\n", - "hamiltonian = SparsePauliOp.from_list(\n", - " [(\"I\", -1.04886087), (\"Z\", -0.7967368), (\"X\", 0.18121804)]\n", - ")\n", - "\n", - "ansatz_1 = ansatz3\n", - "ansatz_2 = QuantumCircuit(1)\n", - "for i in range(num_iter):\n", - " ansatz_2.rx(beta[i * 3 + 0], 0)\n", - " ansatz_2.rz(beta[i * 3 + 1], 0)\n", - " ansatz_2.rx(beta[i * 3 + 2], 0)" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "0eeb0370-0ec7-490e-962b-55f746c4e4a3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ansatz_1.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "105b8557-861c-4fcc-a84e-82d382ad730f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ansatz_2.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "2f3f5e3f-d41b-45af-a14f-d2f1ef506037", - "metadata": {}, - "outputs": [], - "source": [ - "# Step 2: Optimize for target hardware\n", - "\n", - "target = backend.target\n", - "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", - "\n", - "ansatz_isa_1 = pm.run(ansatz_1)\n", - "ansatz_isa_2 = pm.run(ansatz_2)\n", - "hamiltonian_isa_1 = hamiltonian.apply_layout(layout=ansatz_isa_1.layout)\n", - "hamiltonian_isa_2 = hamiltonian.apply_layout(layout=ansatz_isa_2.layout)" - ] - }, - { - "cell_type": "markdown", - "id": "1433eb47-1fcc-4e2e-8738-2d2818be8824", - "metadata": {}, - "source": [ - "Now let's run a VQE with an initial point made of all ones, with a maximum of 20 steps, and compare the convergence of both runs." - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "693444f0-5835-471f-b9f7-20b5158b3d45", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iters. done: 1 [Current cost: -0.8782202668652658]\n", - "Iters. done: 2 [Current cost: -0.43473160695469165]\n", - "Iters. done: 3 [Current cost: -0.4076372093159749]\n", - "Iters. done: 4 [Current cost: -1.3587839859772106]\n", - "Iters. done: 5 [Current cost: -1.774529906754082]\n", - "Iters. done: 6 [Current cost: -1.541934983115727]\n", - "Iters. done: 7 [Current cost: -1.2732403113465345]\n", - "Iters. done: 8 [Current cost: -1.820842221085785]\n", - "Iters. done: 9 [Current cost: -1.8065762857059005]\n", - "Iters. done: 10 [Current cost: -1.8126394095981146]\n", - "Iters. done: 11 [Current cost: -1.8205831886180421]\n", - "Iters. done: 12 [Current cost: -1.8086715778994924]\n", - "Iters. done: 13 [Current cost: -1.8307676638629322]\n", - "Iters. done: 14 [Current cost: -1.8177328827556327]\n", - "Iters. done: 15 [Current cost: -1.8179426218088064]\n", - "Iters. done: 16 [Current cost: -1.8109239667991088]\n", - "Iters. done: 17 [Current cost: -1.824271872489647]\n", - "Iters. done: 18 [Current cost: -1.813167587671394]\n", - "Iters. done: 19 [Current cost: -1.824647343397313]\n", - "Iters. done: 20 [Current cost: -1.8219785311686143]\n" - ] - } - ], - "source": [ - "# QPU time 3m 40s for ibm_brisbane\n", - "# Step 3: Execute on target hardware\n", - "\n", - "from scipy.optimize import minimize\n", - "\n", - "x0 = np.ones(ansatz_1.num_parameters)\n", - "\n", - "batch = Batch(backend=backend)\n", - "\n", - "\n", - "cost_history_dict = {\n", - " \"prev_vector\": None,\n", - " \"iters\": 0,\n", - " \"cost_history\": [],\n", - "}\n", - "estimator = Estimator(mode=batch)\n", - "estimator.options.default_shots = 2048\n", - "\n", - "res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa_1, hamiltonian_isa_1, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 20},\n", - ")\n", - "\n", - "batch.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "6922c55c-6a4f-41d8-8aad-69fa432ddf14", - "metadata": {}, - "outputs": [], - "source": [ - "# Save Cost_history as a new list\n", - "ansatz_1_history = cost_history_dict[\"cost_history\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "id": "5ba0affa-8641-4a15-9408-a99dff0c7f8c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iters. done: 1 [Current cost: -0.738191173881188]\n", - "Iters. done: 2 [Current cost: -0.42636037194506304]\n", - "Iters. done: 3 [Current cost: -1.3503788613797374]\n", - "Iters. done: 4 [Current cost: -0.9109204349776897]\n", - "Iters. done: 5 [Current cost: -0.9060873157510835]\n", - "Iters. done: 6 [Current cost: -0.7735065414083984]\n", - "Iters. done: 7 [Current cost: -1.586889197437709]\n", - "Iters. done: 8 [Current cost: -1.659215191584943]\n", - "Iters. done: 9 [Current cost: -1.245445981794618]\n", - "Iters. done: 10 [Current cost: -1.1608385766138023]\n", - "Iters. done: 11 [Current cost: -1.1551733876027737]\n", - "Iters. done: 12 [Current cost: -1.8143337768286332]\n", - "Iters. done: 13 [Current cost: -1.2510951563756598]\n", - "Iters. done: 14 [Current cost: -1.6918311531865413]\n", - "Iters. done: 15 [Current cost: -1.8163783305531838]\n", - "Iters. done: 16 [Current cost: -1.8434877732947152]\n", - "Iters. done: 17 [Current cost: -1.8461898233304472]\n", - "Iters. done: 18 [Current cost: -1.0346471214915485]\n", - "Iters. done: 19 [Current cost: -1.8322518854150687]\n", - "Iters. done: 20 [Current cost: -1.717144678705999]\n" - ] - } - ], - "source": [ - "# QPU time 3m 40s for ibm_brisbane\n", - "\n", - "x0 = np.ones(ansatz_2.num_parameters)\n", - "\n", - "batch = Batch(backend=backend)\n", - "\n", - "\n", - "cost_history_dict = {\n", - " \"prev_vector\": None,\n", - " \"iters\": 0,\n", - " \"cost_history\": [],\n", - "}\n", - "estimator = Estimator(mode=batch)\n", - "estimator.options.default_shots = 2048\n", - "\n", - "res = minimize(\n", - " cost_func,\n", - " x0,\n", - " args=(ansatz_isa_2, hamiltonian_isa_2, estimator),\n", - " method=\"cobyla\",\n", - " options={\"maxiter\": 20},\n", - ")\n", - "\n", - "batch.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "id": "7354f299-cab6-4d43-a0e3-38c6c19f0486", - "metadata": {}, - "outputs": [], - "source": [ - "ansatz_2_history = cost_history_dict[\"cost_history\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "2c6d2eb4-197b-4811-bb0b-377e445ff3a8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "\n", - "# Define the constant function)\n", - "ax.plot(\n", - " range(cost_history_dict[\"iters\"]),\n", - " ansatz_1_history,\n", - " label=\"Ansatz with 3 parameters\",\n", - ")\n", - "ax.plot(\n", - " range(cost_history_dict[\"iters\"]),\n", - " ansatz_2_history,\n", - " label=\"Ansatz with 12 parameters\",\n", - ")\n", - "ax.set_xlabel(\"Iterations\")\n", - "ax.set_ylabel(\"Cost (Hartree)\")\n", - "plt.legend()\n", - "plt.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "b6a9fcdb-020f-429a-8b6e-17d67b0caaf9", - "metadata": {}, - "source": [ - "The graph above clearly demonstrates that the optimization process of the ansatz with more variables takes more time to get to stable convergence.\n", - "\n", - "Rather than relying on simple single-qubit circuits and a straightforward ansatz, the complexity of optimization increases when larger quantum circuits and more complex structured ansätze are required. This highlights a well-known challenge in VQEs: the overhead of the optimizer.\n", - "\n", - "Researchers continue to develop various advanced methodologies that can use quantum computers for chemistry problems. You can access a variety of educational materials at [IBM Quantum Learning](/learning)." - ] - }, - { - "cell_type": "markdown", - "id": "f7c6d9a5-c2cc-4674-9d54-1f9809cfde51", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "- [[ref 1](https://link.springer.com/article/10.1007/BF02650179) ] Richard P. Feynman, Simulating Physics with Computers, International Journal of Theoretical Physics, 1982.\n", - "- [[ref 2]](https://link.springer.com/chapter/10.1007/978-1-4614-8730-2_10) Marov, M.Y. (2015). The Structure of the Universe. In: The Fundamentals of Modern Astrophysics. Springer, New York, NY.\n", - "- [[ref 3](https://www.ibm.com/quantum/blog/photoresists-quantum-chemistry-jsr)] How to solve difficult chemical engineering problems with quantum computing, IBM Research Blog, 2023.\n", - "- [[ref 4](https://ieeexplore.ieee.org/document/8585034)] Y. Cao, J. Romero and A. Aspuru-Guzik, \"Potential of quantum computing for drug discovery,\" in IBM Journal of Research and Development, vol. 62, no. 6, pp. 6:1-6:20, 1 Nov.-Dec. 2018\n", - "- [[ref 5](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.32.170)] Present State of Molecular Structure Calculation, REv. Mod. Phys. 32, 170, 1960\n", - "- [[ref 6](https://jmsh.springeropen.com/articles/10.1186/s41313-021-00032-6)] Fedorov, D.A., Peng, B., Govind, N. et al. VQE method: a short survey and recent developments. Mater Theory 6, 2 (2022)" - ] - } - ], - "metadata": { - "in_page_toc_max_heading_level": 2, - "in_page_toc_min_heading_level": 2, - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a9fc5202-9641-4db3-ac1a-9986f28854bc", + "metadata": {}, + "source": [ + "---\n", + "title: Variational Quantum Eigensolver\n", + "description: Learn what is a variational quantum eigensolver and compute the activation energy of H+H=H2 reaction with this.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore arrowstyle UCCSD verticalalignment horizontalalignment xytext arrowprops preparable ansätze Marov Aspuru Guzik Hartrees ansä */}" + ] + }, + { + "cell_type": "markdown", + "id": "bafc481a-d2d4-4668-8389-acbe9cd3f615", + "metadata": {}, + "source": [ + "# Variational Quantum Eigensolver (VQE)\n", + "\n", + "For this module, students must have a working Python environment, and the latest versions of the following packages installed:\n", + "- `qiskit`\n", + "- `qiskit_ibm_runtime`\n", + "- `qiskit-aer`\n", + "- `qiskit.visualization`\n", + "- `numpy`\n", + "- `pylatexenc`\n", + "\n", + "To set up and install these packages, see the [Install Qiskit](/docs/guides/install-qiskit) guide. To run jobs on real quantum computers, students will need to set up an IBM Cloud account, following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", + "\n", + "*This module has been tested and used approximately 8 minutes of QPU time. This is an estimate, and your actual usage may vary.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cda95261-1473-492c-8a1d-1e29773086c9", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and modify this line as needed to install dependencies\n", + "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" + ] + }, + { + "cell_type": "markdown", + "id": "127021b2-3419-4b9d-bf13-7ef940b50029", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "Since the development of the quantum mechanical model in the early 20th century, scientists have understood that electrons do not follow fixed paths around an atom's nucleus but rather exist in regions of probability called orbitals. These orbitals correspond to specific, discrete energy levels that electrons can occupy. Electrons naturally reside in the lowest available energy levels, known as the ground state. However, if an electron absorbs sufficient energy, it can jump to a higher energy level, entering an excited state. This excited state is temporary, and the electron will eventually return to a lower energy level, releasing the absorbed energy, often in the form of light. This fundamental process of energy absorption and emission is important to understanding how atoms interact and form bonds.\n", + "\n", + "When atoms come together to form molecules, their atomic orbitals combine to form molecular orbitals. The arrangement and energy levels of electrons within these molecular orbitals dictate the properties of the resulting molecule and the strength of the chemical bonds. For instance, in the formation of a hydrogen molecule ($H_2$) from two individual hydrogen atoms, the electron from each atom occupies atomic orbitals. As the atoms approach each other, these atomic orbitals overlap and combine to form new molecular orbitals — one with lower energy (a bonding orbital) and one with higher energy (an anti-bonding orbital). The two electrons, one from each hydrogen atom, will preferentially occupy the lower-energy bonding orbital, leading to the formation of a stable covalent bond that holds the $H_2$ molecule together. The energy difference between the separated atoms and the formed molecule, particularly the energy of the electrons in the molecular orbitals, determines the stability and properties of the bond.\n", + "\n", + "In the following sections, we will explore this process of molecular formation, focusing on the $H_2$ molecule. We will use a real quantum computer, combined with classical optimization techniques, to find the energy of this simple yet fundamental process. This experiment will provide a practical demonstration of how quantum computation can be applied to solve problems in computational chemistry, providing insights into the role of electron energy." + ] + }, + { + "cell_type": "markdown", + "id": "eef4da0c-8f48-47fc-88a2-9a2979c1dbc1", + "metadata": {}, + "source": [ + "## VQE - A variational quantum algorithm for eigenvalue problems\n", + "\n", + "### Approximation techniques for chemistry - variational principle and the basis set\n", + "\n", + "Erwin Schrödinger's contributions to quantum mechanics are not limited to introducing a new electronic model; fundamentally, he established wave mechanics by developing the famous time-dependent Schrödinger equation:\n", + "\n", + "$$\n", + "i\\hbar \\frac{d}{dt}|\\psi\\rangle = \\hat{H}|\\psi\\rangle\n", + "$$\n", + "\n", + "Here, $\\hat{H}$ is the Hamiltonian operator, which represents the total energy of the system, and $|\\psi\\rangle$ is the wave function that contains all the information about the system’s quantum state. (Note: $\\frac{d}{dt}$ is the total time derivative, and we do not explicitly include the energy eigenvalue $E$ here.)\n", + "\n", + "However, in many practical applications — such as determining the allowed energy levels of atoms and molecules — we instead use the time-independent Schrödinger equation (energy eigenvalue equation), which is derived from the time-dependent form by assuming a stationary state. A stationary state is a quantum state in which the probability density of finding a particle at a given point in space does not change over time.\n", + "\n", + "$$ \\hat{H}|\\psi\\rangle = E|\\psi\\rangle $$\n", + "\n", + "In this form, $E$ represents the energy eigenvalue corresponding to the quantum state $|\\psi\\rangle$. The Hamiltonian includes various energy contributions, such as the kinetic energy of electrons and nuclei, the attractive forces between electrons and nuclei, and the repulsive forces between electrons.\n", + "\n", + "Solving the energy eigenvalue equation allows us to calculate the quantized energy levels of atomic and molecular systems. However, for molecules, solving it exactly is difficult because the wave function $\\Psi$, which describes the spatial distribution of electrons, is complex and high-dimensional.\n", + "\n", + "As a result, scientists use approximation techniques to obtain practical and accurate solutions. In this work, we will focus on two key methods:\n", + "\n", + "\n", + "1. Variational principle\n", + "\n", + " This method approximates the wave function and adjusts it to get as close as possible to the target energy, usually the ground state energy of the system. The key idea behind the variational principle is simple:\n", + "\n", + " - If we guess a wave function $\\Psi_\\text{trial}$ (a \"trial function\"), the energy calculated from it will always be equal to or higher than the ground state energy ($E_0$) of the system.\n", + " $$E_\\text{approx} = \\frac{\\langle \\Psi_\\text{trial}|\\hat{H}|\\Psi_\\text{trial}\\rangle}{\\langle \\Psi_\\text{trial}|\\Psi_\\text{trial}\\rangle} \\geq E_0$$\n", + " - By adjusting parameters $\\theta$ in the trial function, $|\\Psi_\\text{trial}(\\theta)\\rangle$, we can get a better and better approximation of the ground state energy.\n", + " - Its accuracy heavily depends on the choice of the trial wave function $\\Psi_\\text{trial}$. A poorly-chosen trial function may lead to an energy estimate that is far from accurate.\n", + "\n", + "2. Basis set approximation\n", + "\n", + " The second approximation method comes in the stage of constructing the wave function — the basis set approach. In quantum chemistry, solving the Schrödinger equation exactly for molecules is almost impossible. Instead, we approximate the complex, multi-electron wave function by building it up from simpler, predefined mathematical functions. A basis set is essentially a collection of these known mathematical functions, typically centered on the atoms in the molecule, that are used as building blocks to represent the shape and behavior of the electrons in the system. Think of it like trying to recreate a detailed sculpture using only a collection of standard LEGO bricks – the more types and sizes of bricks you have (the larger the basis set), the more accurately you can approximate the original shape.\n", + "\n", + " These basis functions are often inspired by the analytical solutions for simple systems like the hydrogen atom, taking forms like Gaussian or Slater-type functions, though they are still approximations. Instead of working with the theoretically \"exact\" but intractable full molecular orbitals, we express them as a linear combination (a sum with coefficients) of these basis functions. This method is known as the Linear Combination of Atomic Orbitals (LCAO) approach when the basis functions resemble atomic orbitals. By optimizing the coefficients in this linear combination, we can find the best possible approximate wave function and energy within the limitations of the chosen basis set.\n", + " - The more functions included in the basis set, the better the approximation, but this comes at the cost of higher computational effort.\n", + " - A small basis set provides a rough estimate, while a large basis set gives more precise results at the expense of requiring more computational resources.\n", + "\n", + "To summarize, to make calculations feasible and reduce computational cost, we use the variational principle by approximating the wave function, which reduces the computational complexity and allows for iterative optimization to minimize energy. Meanwhile, the basis set approach simplifies calculations by representing atomic orbitals as a combination of predefined functions, rather than solving for a continuous wave function directly.\n", + "\n", + "\n", + "#### Check your understanding\n", + "Consider the trial wave function $\\Psi_\\text{trial}(\\alpha,x) = Ae^{- \\alpha x^2}$ where $A$ is a normalization constant and $\\alpha$ is an adjustable parameter.\n", + "\n", + "(a) Normalize the trial wave function by determining $A$ such that $$ \\int_{-\\infty}^{\\infty} |\\Psi_\\text{trial}|^2 dx = 1 $$.\n", + "\n", + "\n", + "\n", + "\n", + "To normalize given trial wave function:\n", + "\n", + "$$ \\int_{-\\infty}^{\\infty} |\\Psi_\\text{trial}|^2 dx = \\int_{-\\infty}^{\\infty} A^2 e^{-2 \\alpha x^2} dx = 1 $$\n", + "\n", + "Use the Gaussian integral:\n", + "\n", + "$$ \\int_{-\\infty}^{\\infty} e^{-a x^2} dx = \\sqrt{\\frac{\\pi}{a}} \\text{, for } a>0$$\n", + "\n", + "set $a = 2\\alpha$ then get:\n", + "$$A^2\\sqrt{\\frac{\\pi}{a}} = 1$$\n", + "$$\\therefore A = (\\frac{2\\alpha}{\\pi})^{1/4}$$\n", + "\n", + "\n", + "\n", + "\n", + "(b) Compute the expectation value of the Hamiltonian $\\hat{H}$ given by $$ \\hat{H} = -\\frac{\\hbar^2}{2m} \\frac{d^2}{dx^2} + V(x)$$ where $V(x) = \\frac{1}{2}m\\omega^2x^2$, which corresponds to a simple harmonic oscillator potential.\n", + "\n", + "\n", + "\n", + "\n", + "The Hamiltonian for a harmonic oscillator is:\n", + "\n", + "$$ \\hat{H} = -\\frac{\\hbar^2}{2m} \\frac{d^2}{dx^2} + \\frac{1}{2} m \\omega^2 x^2 $$\n", + "\n", + "**Kinetic energy expectation value**\n", + "\n", + "$$ \\langle T \\rangle = -\\frac{\\hbar^2}{2m} \\int_{-\\infty}^{\\infty} \\Psi_\\text{trial}^* \\frac{d^2}{dx^2} \\Psi_\\text{trial} dx$$\n", + "\n", + "Taking the second derivative:\n", + "\n", + "$$\\frac{d}{dx} \\Psi_\\text{trial} = -2\\alpha x A e^{-\\alpha x^2}$$\n", + "\n", + "$$\\frac{d^2}{dx^2} \\Psi_\\text{trial} = A e^{-\\alpha x^2} (4\\alpha^2 x^2 - 2\\alpha)$$\n", + "\n", + "Thus:\n", + "\n", + "$$T = -\\frac{\\hbar^2}{2m} \\int_{-\\infty}^{\\infty} A^2 e^{-2\\alpha x^2} (4\\alpha^2 x^2 - 2\\alpha) dx$$\n", + "\n", + "Using standard Gaussian integral results:\n", + "\n", + "$$\\langle T \\rangle = \\frac{\\hbar^2 \\alpha}{2m}$$\n", + "\n", + "**Potential energy expectation value**\n", + "\n", + "$$\\langle V \\rangle = \\frac{1}{2} m \\omega^2 \\int_{-\\infty}^{\\infty} x^2 |\\Psi_\\text{trial}|^2 dx$$\n", + "\n", + "Using:\n", + "\n", + "$$\\int_{-\\infty}^{\\infty} x^2 e^{-a x^2} dx = \\frac{\\sqrt{\\pi}}{2a^{3/2}}$$\n", + "\n", + "we get:\n", + "\n", + "$$\\langle V \\rangle = \\frac{m \\omega^2}{4\\alpha}$$\n", + "\n", + "**Total energy expectation value**\n", + "\n", + "$$\\therefore E_\\text{approx}(\\alpha) = \\frac{\\hbar^2 \\alpha}{2m} + \\frac{m \\omega^2}{4\\alpha}$$\n", + "\n", + "\n", + "\n", + "\n", + "(c) Use the variational principle to find the optimal $\\alpha$ by minimizing $E_\\text{approx}(\\alpha)$.\n", + "\n", + "\n", + "\n", + "\n", + "Optimize $ \\alpha $ for minimum energy\n", + "\n", + "**Differentiate**\n", + "\n", + "$$\\frac{d}{d\\alpha} \\left( \\frac{\\hbar^2 \\alpha}{2m} + \\frac{m \\omega^2}{4\\alpha} \\right) = 0$$\n", + "\n", + "Solving:\n", + "\n", + "$$\\frac{\\hbar^2}{2m} - \\frac{m \\omega^2}{4\\alpha^2} = 0$$\n", + "\n", + "$$\\alpha_\\text{opt} = \\frac{m\\omega}{2\\hbar}$$\n", + "\n", + "Substituting $ \\alpha_\\text{opt} $ into $ E_\\text{approx} $:\n", + "\n", + "$$\\therefore E_\\text{approx} = \\frac{\\hbar \\omega}{2}$$\n", + "\n", + "which matches the exact quantum harmonic oscillator ground state energy.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "ed2bd1ba-73ad-42d3-94ad-c1d4a7db0a6f", + "metadata": {}, + "source": [ + "### VQE (Variational Quantum Eigensolver)\n", + "\n", + "The variational quantum eigensolver (VQE) is the main method that we will use to explore the $H+H = H_2$ process, and here, we will take a look at what VQE is and how it works. But let's first pause and look at one very important thing through the check-in question.\n", + "\n", + "#### Check your understanding\n", + "If we already have that many strategies for chemistry problems, then why do we need a quantum computer? And what's the purpose of using both quantum and classical computers together?\n", + "\n", + "\n", + "\n", + "\n", + "Quantum computing has a chance to revolutionize chemistry by tackling problems classical computers struggle with due to the exponential scaling of quantum states. Richard Feynman famously noted that to simulate nature, computations must also be quantum [ref 1].\n", + "\n", + "For example, simulating caffeine with the simplest basis set (STO-3G) would require $10^{48}$ bits, much larger than the total number of stars in the observable universe ($10^{24}$) [ref 2]. A quantum computer can describe the electronic orbitals of caffeine with 160 qubits.\n", + "\n", + "Quantum computers naturally process quantum interactions using superposition and entanglement, which provide a promising way of enabling accurate molecular simulations. Further, we can combine the advantages of both quantum computers (electron simulation) and classical computers (data pre/post-processing, algorithm process management, optimization, and so on). These are expected to enhance materials discovery, drug design, and reaction predictions, reducing costly trial-and-error experiments. [ref 3][ref 4]\n", + "\n", + "If you want to know why quantum computers are needed for chemistry problems and why to use both quantum and classical computing resources, check out the following articles:\n", + "\n", + "- [Emerging quantum computing algorithms for quantum chemistry](https://arxiv.org/abs/2109.02873)\n", + "- [Chemistry Beyond Exact Solutions on a Quantum-Centric Supercomputer](https://arxiv.org/html/2405.05068v1)\n", + "- [Quantum-centric supercomputing for materials science: A perspective on challenges and future directions](https://www.sciencedirect.com/science/article/pii/S0167739X24002012)\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Now let's go back to VQE.\n", + "\n", + "VQE combines the power of quantum computers with classical computers, fundamentally using variational principles to get the ground state energy of the system. To understand VQE, first break it down into three parts:\n", + "\n", + "![VQE workflow](/learning/images/modules/computer-science/vqe/vqe.avif)\n", + "\n", + "\n", + "#### (Quantum) Observable: The molecular Hamiltonian (energy of a molecule)\n", + "\n", + "In VQE, the molecular/atomic Hamiltonian is an observable, meaning we can measure its value through an experiment. Our goal is to find the lowest possible energy (the ground state energy) of the molecule. To do this, we use a trial quantum state, generated by a parameterized quantum circuit (ansatz). We measure the observable and optimize the quantum state until we reach the lowest possible energy.\n", + "\n", + "The basis set used for the molecular Hamiltonian determines the number of qubits required and directly affects the accuracy of VQE. Choosing the right basis set is critical for balancing efficiency and precision. To simplify calculations without changing the basis set, we can use strategies like imposing symmetry and active space reduction. Many molecules have symmetrical shapes (like a butterfly or snowflake), meaning some parts behave the same way. Instead of calculating everything separately, we can focus only on unique parts, saving quantum resources, thus leveraging symmetry. In active space reduction, we consider only the important orbitals, as not all electrons significantly impact molecular energy. Electrons close to the nucleus remain mostly unchanged, while others influence bonding. By applying these methods, we can make VQE more efficient while maintaining accuracy.\n", + "\n", + "Once we obtain a molecular Hamiltonian using the proper basis set and strategies above, we need to transform this Hamiltonian into one suitable for quantum computers. Mapping problems to Pauli operators can be quite complicated. This is especially true in quantum chemistry, which works with indistinguishable particles (electrons), since qubits are distinguishable. We will not go into the details of the mappings here, but we refer you to the following resources. A general discussion of mapping a problem to quantum operators can be found in [Quantum computing in practice](/learning/courses/quantum-computing-in-practice/index). A more detailed discussion on mapping chemistry problems into quantum operators can be found in [Quantum chemistry with VQE](/learning/courses/quantum-chem-with-vqe/index).\n", + "\n", + "For this module, we will provide you with the appropriate (one-qubit) Hamiltonians for $H$ and $H_2$ so we can focus on using the quantum computer. These one-qubit Hamiltonians are prepared by using the [STO-6G](https://en.wikipedia.org/wiki/STO-nG_basis_sets) basis set and the [Jordan-Wigner mapping](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation), which is the most straightforward mapping with the simplest physical interpretation, because it maps the occupation of one spin-orbital to the occupation of one qubit. Also, we used a [qubit reduction technique by using a symmetry of the Hamiltonian](https://arxiv.org/abs/1701.08213), which uses the patterns in how spin occupations behave to reduce the number of qubits. For the $H_2$ molecule, we assume the distance between the two hydrogen atoms is `0.735` $\\mathring A$.\n", + "\n", + "#### (Quantum) Ansatz: The trial wave function (How to build a trivial quantum state with a quantum circuit)\n", + "\n", + "For VQE, the ansatz (plural: ansätze) consists of two key components. The first is initial state preparation, which sets up the qubit's state by applying quantum gates with no variational parameter. The second component is the parameterized quantum circuit, a special quantum circuit with adjustable parameters, similar to dials on a radio. These parameters will be used for the last part — the classical optimizer — to help us reach the best possible ground state.\n", + "\n", + "In the variational principle section, we learned that the quality of the trial state affects the quality of the results of the variational algorithm. This means that choosing a good ansatz is important in VQE. Once again, this is a rich and complex topic. We will not cover the different types of ansatz or their origins here. If you're interested in learning more about parameterized quantum circuits and ansatz, you can explore the [Ansatz and variational form](/learning/courses/variational-algorithm-design/ansaetze-and-variational-forms) lesson from the [Variational algorithm design course](/learning/courses/variational-algorithm-design/index), which provides detailed explanations and examples of ansätze.\n", + "\n", + "Since we are going to use a one-qubit Hamiltonian in this module, we need a one-qubit parameterized quantum circuit as an ansatz. We will see three types of one-qubit ansätze in the following section. We will compare them and discuss key considerations in selecting an ansatz." + ] + }, + { + "cell_type": "markdown", + "id": "a8fae507-41e6-4585-8e9d-b45db5055fb9", + "metadata": {}, + "source": [ + "#### (Classical) Optimizer: fine-tuning the quantum circuit\n", + "\n", + "Once the quantum computer measures the energy of the observable from the ansatz, the parameters of the ansatz and the energy value are sent to the classical optimizer for tuning. This optimization process is performed on a classical computer, typically using general-purpose scientific packages like SciPy.\n", + "\n", + "The classical optimizer treats the measured energy as a cost function. In optimization problems, a cost function (also sometimes called an objective function) is a mathematical function that measures how \"good\" a particular solution is. The goal of the optimizer is to find the set of parameters that minimizes this cost function. In the context of finding the ground state energy of a molecule, the energy itself serves as the cost function – we want to find the parameters for our quantum circuit (our \"solution\") that yield the lowest possible energy. The classical optimizer uses this measured energy value (the cost) and determines the next set of optimized parameters for the quantum ansatz. These updated parameters are then sent back to the quantum circuit, and the process is repeated. With each iteration, the classical optimizer adjusts the parameters to try and reduce the energy (minimize the cost function) until a predefined convergence criterion is met, ideally ensuring that the lowest possible energy (corresponding to the ground state of the molecule for that bond distance and basis set) is found.\n", + "\n", + "There are many optimization strategies provided by scientific packages like SciPy. You can find more in the [Optimization loops](/learning/courses/variational-algorithm-design/optimization-loops) lesson of the [Variational algorithm design](/learning/courses/variational-algorithm-design/index) course. Here we will use COBYLA (Constrained Optimization BY Linear Approximations), an optimization algorithm suitable for complicated energy landscapes. In particular, COBYLA does not attempt to calculate a gradient of the function being studied; this is called a gradient-free optimizer. Imagine you are trying to find the highest peak in a mountain range with your eyes closed. Since you can’t see the whole landscape, you take small steps in different directions, while checking if you’re going up or down. COBYLA works in a similar way — it moves through the parameter space, testing different values, gradually improving the result until it finds the best one.\n", + "\n", + "Now you are ready to carry out a VQE calculation. To that end, try the check-in question below, which recaps the overall process." + ] + }, + { + "cell_type": "markdown", + "id": "8cdcfcc0-a6ca-4153-a11a-bd7b598f9b35", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "Fill in the blanks with the correct terms to complete the summary of the VQE process, then click to check your answers.\n", + "\n", + "VQE is a variational quantum algorithm, which combines the power of (1) ________ and classical computing, used to find the (2) __________ of a molecule. The process begins by defining the (3) __________, which represents the total energy of the system and acts as the observable in quantum measurements. Next, we prepare an (4) __________, a quantum circuit with adjustable parameters that represents the trial wave function of the molecule. These parameters are optimized using a (5) __________, a classical algorithm that adjusts parameters iteratively to minimize the measured energy. In the discussion above we used the (6) __________ optimizer, which refines the ansatz parameters without needing derivative calculations. The process continues until we reach (7) __________, meaning we have found the lowest possible energy of the molecule.\n", + "\n", + "Word Bank:\n", + "\n", + "- classical optimizer\n", + "- ground state energy\n", + "- hardware-efficient\n", + "- ansatz\n", + "- molecular Hamiltonian\n", + "- COBYLA\n", + "- quantum computing\n", + "- convergence\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1 → quantum computing\n", + "\n", + "2 → ground state energy\n", + "\n", + "3 → molecular Hamiltonian\n", + "\n", + "4 → ansatz\n", + "\n", + "5 → classical optimizer\n", + "\n", + "6 → COBYLA\n", + "\n", + "7 → convergence\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "64a090f7-1d18-4edb-bef3-4cea0f4cec2c", + "metadata": {}, + "source": [ + "## Compute the ground state energy of a hydrogen atom with VQE\n", + "\n", + "Now, let's use what we've learned to compute the ground state energy of a hydrogen atom. Throughout the module, we will use a framework for quantum computing known as \"Qiskit patterns\", which breaks down workflows into the following steps:\n", + "\n", + "- Step 1: Map classical inputs to a quantum problem\n", + "- Step 2: Optimize problem for quantum execution\n", + "- Step 3: Execute using Qiskit Runtime primitives\n", + "- Step 4: Post-processing and classical analysis\n", + "\n", + "![Qiskit pattern](/learning/images/modules/computer-science/vqe/patterns.svg)\n", + "\n", + "We will generally follow these steps.\n", + "\n", + "Let's start by loading some necessary packages, including Qiskit Runtime primitives. We will also select the least busy quantum computer available to us.\n", + "\n", + "There is code below for saving your credentials upon first use. Be sure to delete this information from the notebook after saving it to your environment, so that your credentials are not accidentally shared when you share the notebook. See [Set up your IBM Cloud account](/docs/guides/initialize-account) and [Initialize the service in an untrusted environment](/docs/guides/cloud-setup-untrusted) for more guidance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18d9c613-ee5f-4e38-bc83-57d25197b200", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ibm_brisbane\n" + ] + } + ], + "source": [ + "# Load the Qiskit Runtime service\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "# Load the Runtime primitive and session\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance =\n", + "# '', token='', overwrite=True, set_as_default=True)\n", + "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", + "\n", + "# Load saved credentials\n", + "service = QiskitRuntimeService()\n", + "\n", + "# Use the least busy backend, or uncomment the loading of a specific backend like \"ibm_brisbane\".\n", + "backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127)\n", + "# backend = service.backend(\"ibm_brisbane\")\n", + "print(backend.name)" + ] + }, + { + "cell_type": "markdown", + "id": "0c4e0ac4-7f4c-4cab-b726-1d6fe1ca10ac", + "metadata": {}, + "source": [ + "The cell below will allow you to switch between using the simulator or real hardware throughout the notebook. We recommend running it now:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4c38f002-c6e6-4fe2-acf1-3cccf3d1f525", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the Aer simulator and generate a noise model based on the currently-selected backend.\n", + "from qiskit_aer import AerSimulator\n", + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "# Alternatively, load a fake backend with generic properties and define a simulator.\n", + "\n", + "\n", + "noise_model = NoiseModel.from_backend(backend)\n", + "\n", + "# Define a simulator using Aer, and use it in Sampler.\n", + "backend_sim = AerSimulator(noise_model=noise_model)" + ] + }, + { + "cell_type": "markdown", + "id": "497acce3-f34c-43b3-a014-630702a3620b", + "metadata": {}, + "source": [ + "### Step 1: Map the problem to quantum circuits and operators\n", + "\n", + "We start our VQE calculation by defining the Hamiltonian for the hydrogen molecule ($H_2$) at a specific bond distance. This Hamiltonian represents the total energy of the system in terms of qubit operators, having been produced and mapped from the molecular system using a standard procedure: 1) employing the STO-6G basis set (a specific collection of mathematical functions used to approximate the electron orbitals), 2) applying the Jordan-Wigner mapping (a technique to translate fermionic operators describing electrons into qubit operators), and 3) performing qubit reduction using parity of the Hamiltonian to simplify the problem.\n", + "\n", + "As we previously explained, the computed ground state energies depend heavily on the basis set selection and the molecular geometry (like bond distance). For this specific configuration and after these transformations, the resulting qubit Hamiltonian is simple:\n", + "\n", + "$$\\hat{H} = -0.2355 I + 0.2355 Z$$\n", + "\n", + "Here, $I$ represents the identity operator and $Z$ represents the Pauli-Z operator, acting on a single qubit. The coefficients are derived from the integrals calculated using the STO-6G basis set at this particular bond distance with proper transformation.\n", + "\n", + "With this Hamiltonian defined, we can now use VQE to compute its ground state energy. It is useful to compare our calculated ground state energy to expected values. For a single, isolated hydrogen atom (H), the ground state energy is exactly -0.5 Hartree (in the absence of relativistic effects). Let us compute the exact ground state energy of *our specific qubit Hamiltonian* as defined above and compare it to relevant known values." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "aaef7daa-6dbe-4a2e-abe2-3ad2a0baa860", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The exact ground state energy of the Hamiltonian is -0.471 hartree\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "import numpy as np\n", + "\n", + "# Qubit Hamiltonian of the hydrogen atom generated by using STO-3G basis set and parity mapping\n", + "Hamiltonian = SparsePauliOp.from_list([(\"I\", -0.2355), (\"Z\", 0.2355)])\n", + "\n", + "# exact ground state energy of Hamiltonian\n", + "\n", + "A = np.array(Hamiltonian)\n", + "eigenvalues, eigenvectors = np.linalg.eig(A)\n", + "print(\n", + " \"The exact ground state energy of the Hamiltonian is \",\n", + " min(eigenvalues).real,\n", + " \"hartree\",\n", + ")\n", + "h = min(eigenvalues.real)" + ] + }, + { + "cell_type": "markdown", + "id": "ec53e133-d8ab-4e5d-a44a-428fe8095aee", + "metadata": {}, + "source": [ + "Next, we need a parameterized quantum circuit, an ansatz, to prepare a trial wave function $\\Psi_\\text{trial}$ for the ground state. The goal is to find the parameters $\\theta$ that minimize the energy expectation value $\\langle\\psi(\\theta)|\\hat{H}|\\psi(\\theta)\\rangle$. The choice of ansatz is crucial because it determines the set of possible quantum states that our circuit can prepare. A \"good\" ansatz is one that is flexible enough to represent a state very close to the true ground state of the Hamiltonian we are studying, but not so complex that it requires too many parameters or too deep a circuit for current quantum computers.\n", + "\n", + "Here, we will try three different one-qubit ansätze to see which one provides better \"coverage\" of the possible quantum states a single qubit can be in. The \"coverage\" refers to the range of quantum states that the ansatz circuit can produce by varying its parameters.\n", + "\n", + "We will use three ansätze based on different combinations of single-qubit rotational gates:\n", + "\n", + "- One 1-axis rotational gate ansatz: This ansatz uses rotations around only a single axis ($R_x(\\theta)$). On the Bloch sphere, this corresponds to moving only along a specific circle. This is the least flexible and covers a limited set of states.\n", + "- Two 2-axis rotational gate ansätze: These ansätze combines rotations around two different axes ($R_x(\\theta_1) R_z(\\theta_2)$ and $R_x(\\theta_1) R_z(\\theta_2) R_x(\\theta_3)$). This allows us to reach a larger portion of the Bloch sphere, compared to a single-axis rotation.\n", + "\n", + "\n", + "By comparing the VQE results obtained with these three ansätze, we can see how the flexibility and state-space coverage of the ansatz impact our ability to find the true ground state energy of our simplified Hamiltonian. A more flexible ansatz has the *potential* to find a better approximation, but it might also be harder for the classical optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "653b257f-9975-4bd3-9c48-a046ec4a9fae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import Parameter\n", + "from qiskit.quantum_info import Statevector, DensityMatrix, Pauli\n", + "\n", + "theta = Parameter(\"θ\")\n", + "phi = Parameter(\"φ\")\n", + "lam = Parameter(\"λ\")\n", + "\n", + "ansatz1 = QuantumCircuit(1)\n", + "ansatz1.rx(theta, 0)\n", + "\n", + "ansatz2 = QuantumCircuit(1)\n", + "ansatz2.rx(theta, 0)\n", + "ansatz2.rz(phi, 0)\n", + "\n", + "ansatz3 = QuantumCircuit(1)\n", + "ansatz3.rx(theta, 0)\n", + "ansatz3.rz(phi, 0)\n", + "ansatz3.rx(lam, 0)" + ] + }, + { + "cell_type": "markdown", + "id": "26707a66-7098-4f57-93fc-eb1cb4c305f7", + "metadata": {}, + "source": [ + "Now, let's generate 5000 random numbers for each parameter and plot the distribution of random quantum states, generated by the three ansätze with these random parameters. You can think of these parameters like rotations around different axes on a spherical surface. To see the distribution of quantum state, we will use [the Bloch Sphere](https://en.wikipedia.org/wiki/Bloch_sphere), a three-dimensional sphere that shows the state of a single qubit. Any point on the sphere represents a possible state of the qubit, where the north and south poles are like the classical \"0\" and \"1\", but the qubit can also be anywhere in between, showing special quantum properties like superposition. First, prepare the necessary functions to plot the 3D Bloch sphere and prepare 5000 random parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6e47463b-b0c2-44cd-b099-9f50f38ebd0e", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "def plot_bloch(bloch_vectors):\n", + " # Extract X, Y, Z coordinates for 3D projection\n", + " X_coords = bloch_vectors[:, 0]\n", + " Z_coords = bloch_vectors[:, 2]\n", + "\n", + " # Compute Y coordinates from X and Z to approximate the full Bloch sphere projection\n", + " Y_coords = bloch_vectors[:, 1]\n", + "\n", + " # Create 3D plot\n", + " fig = plt.figure(figsize=(8, 8))\n", + " ax = fig.add_subplot(111, projection=\"3d\")\n", + " ax.scatter(X_coords, Y_coords, Z_coords, color=\"blue\", alpha=0.6)\n", + "\n", + " # Labels and title\n", + " ax.set_xlabel(\"X\")\n", + " ax.set_ylabel(\"Y\")\n", + " ax.set_zlabel(\"Z\")\n", + " ax.set_title(\"Parameterized 1-Qubit Circuit on 3D Bloch Sphere\")\n", + "\n", + " # Set axis limits and make them equal\n", + " ax.set_xlim([-1, 1])\n", + " ax.set_ylim([-1, 1])\n", + " ax.set_zlim([-1, 1])\n", + "\n", + " # Ensure equal aspect ratio for all axes\n", + " ax.set_box_aspect([1, 1, 1]) # Equal scaling for x, y, z axes\n", + "\n", + " # Show grid\n", + " ax.grid(True)\n", + "\n", + " plt.show()\n", + "\n", + "\n", + "num_samples = 5000 # Number of random states\n", + "theta_vals = np.random.uniform(0, 2 * np.pi, num_samples)\n", + "phi_vals = np.random.uniform(0, 2 * np.pi, num_samples)\n", + "lam_vals = np.random.uniform(0, 2 * np.pi, num_samples)" + ] + }, + { + "cell_type": "markdown", + "id": "af24b916-fa42-4c4c-8e22-098adfb4fcd5", + "metadata": {}, + "source": [ + "Let's see how our first ansatz works." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6471a5d1-287a-4726-9e65-e23ef77ccf75", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# List to store Bloch Sphere XZ coordinates\n", + "bloch_vectors = []\n", + "\n", + "# Generate quantum states and extract Bloch vectors\n", + "for i in range(num_samples):\n", + " # Create a circuit and bind parameters\n", + " qc = ansatz1\n", + " bound_qc = qc.assign_parameters({theta: theta_vals[i]}) # , lam: lam_vals[i]})\n", + " state = Statevector.from_instruction(bound_qc)\n", + " rho = DensityMatrix(state)\n", + "\n", + " X = rho.expectation_value(Pauli(\"X\")).real\n", + " Y = rho.expectation_value(Pauli(\"Y\")).real\n", + " Z = rho.expectation_value(Pauli(\"Z\")).real\n", + " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", + "\n", + "# Convert to a numpy array for plotting\n", + "bloch_vectors = np.array(bloch_vectors)\n", + "\n", + "plot_bloch(bloch_vectors)" + ] + }, + { + "cell_type": "markdown", + "id": "9cb7d81e-8df5-4557-8452-98f794bdc5cd", + "metadata": {}, + "source": [ + "We can see our first ansatz returns a ring-shaped distributed quantum states of the Bloch sphere. This makes sense, because we have only given the ansatz a single rotational parameter. It can therefore only produce states rotated around one axis. Starting from the point $(0,0,1)$ and rotating around one axis will always yield a ring. Then let's check our second ansatz, which has two orthogonal rotational gates - `Rx` and `Rz`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9cd86049-f652-4e76-a68a-2780cb7782db", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bloch_vectors = []\n", + "\n", + "# Generate quantum states and extract Bloch vectors\n", + "for i in range(num_samples):\n", + " # Create circuit and bind parameters\n", + " qc = ansatz2\n", + " bound_qc = qc.assign_parameters(\n", + " {theta: theta_vals[i], phi: phi_vals[i]}\n", + " ) # , lam: lam_vals[i]})\n", + " state = Statevector.from_instruction(bound_qc)\n", + " rho = DensityMatrix(state)\n", + "\n", + " X = rho.expectation_value(Pauli(\"X\")).real\n", + " Y = rho.expectation_value(Pauli(\"Y\")).real\n", + " Z = rho.expectation_value(Pauli(\"Z\")).real\n", + " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", + "\n", + "# Convert to numpy array for plotting\n", + "bloch_vectors = np.array(bloch_vectors)\n", + "\n", + "plot_bloch(bloch_vectors)" + ] + }, + { + "cell_type": "markdown", + "id": "e3fde737-d565-4200-a968-c6a6c78dfe30", + "metadata": {}, + "source": [ + "Here, we can see that our second ansatz covers a larger portion of the Bloch sphere - but note that the dots are more concentrated around the poles and more spread out around the equator. Now it is time to check our last ansatz." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ec2e9a84-46b2-4143-8cde-d981e9edd3ce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bloch_vectors = []\n", + "\n", + "# Generate quantum states and extract Bloch vectors\n", + "for i in range(num_samples):\n", + " # Create circuit and bind parameters\n", + " qc = ansatz3\n", + " bound_qc = qc.assign_parameters(\n", + " {theta: theta_vals[i], phi: phi_vals[i], lam: lam_vals[i]}\n", + " )\n", + " state = Statevector.from_instruction(bound_qc)\n", + " rho = DensityMatrix(state)\n", + "\n", + " X = rho.expectation_value(Pauli(\"X\")).real\n", + " Y = rho.expectation_value(Pauli(\"Y\")).real\n", + " Z = rho.expectation_value(Pauli(\"Z\")).real\n", + " bloch_vectors.append([X, Y, Z]) # Store X, Z components\n", + "\n", + "# Convert to numpy array for plotting\n", + "bloch_vectors = np.array(bloch_vectors)\n", + "\n", + "plot_bloch(bloch_vectors)" + ] + }, + { + "cell_type": "markdown", + "id": "ca886db5-e7c7-4f5c-8594-50f79fbc61dd", + "metadata": {}, + "source": [ + "Here you can see more evenly distributed quantum states generated by our last ansatz.\n", + "\n", + "As mentioned, the best thing to do is to gain knowledge about the ground state you're seeking and use an ansatz that is well-suited to probe states close to that ground state. For example, if we knew that our ground state was near a pole, we might select ansatz 2. For simplicity, we will stick with ansatz 3, which uniformly probes the entire Bloch sphere.\n", + "\n", + "Now that we have selected our ansatz, let's draw the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f0c76d9-b4b2-46dc-a261-7c624c031177", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This circuit has 3 parameters\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", + "\n", + "ansatz = ansatz3\n", + "\n", + "num_params = ansatz.num_parameters\n", + "print(\"This circuit has \", num_params, \"parameters\")\n", + "\n", + "ansatz.draw(\"mpl\", style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "b7dc624b-37bc-4523-8532-e45c7826ce64", + "metadata": {}, + "source": [ + "### Step 2: Optimize for target hardware\n", + "\n", + "When running a calculation on a real quantum computer, we don't just care about the logic of the quantum circuit. We also care about things like what operations can be performed by that particular quantum computer, and where on the quantum computer are the qubits we are using. Are they right next to each other? Are they far apart? Therefore, the next step is to rewrite our circuit using gates that are natural for the quantum computer we'll use, and taking qubit layout into account. This can be done by `transpilation` - after this process, you can see our simple ansatz converted into a different set of gates, and our abstract qubits will be mapped into physical qubits on a real quantum computer." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d4f50ff9-cdf4-483f-9bb8-5814fe5acc53", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Backend: {config.backend_name}\n", + "Native gates: ['ecr', 'id', 'delay', 'measure', 'reset', 'rz', 'sx', 'x'] ,\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "config = backend.configuration()\n", + "\n", + "print(\"Backend: {config.backend_name}\")\n", + "print(\"Native gates: \", config.supported_instructions, \",\")\n", + "\n", + "\n", + "target = backend.target\n", + "\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "ansatz_isa = pm.run(ansatz)\n", + "\n", + "ansatz_isa.draw(output=\"mpl\", idle_wires=False, style=\"iqp\")" + ] + }, + { + "cell_type": "markdown", + "id": "04feccf8-4591-4919-90ba-9b0d72e1b436", + "metadata": {}, + "source": [ + "You can see the `rx, rz` gates of our ansatz were converted into a series of `rz, sx` gates, which are the native gates of our backend. Also, you can see our `q0` is now mapped into the fifth physical qubit. We also need to map our Hamiltonian according to these changes, as in the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2e105b4d-8c3c-4b1e-9652-9857ee9e5465", + "metadata": {}, + "outputs": [], + "source": [ + "Hamiltonian_isa = Hamiltonian.apply_layout(layout=ansatz_isa.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "d3212dfa-da89-43d6-85ce-6200ecf9ca09", + "metadata": {}, + "source": [ + "### Step 3: Execute on target hardware\n", + "\n", + "Now it is time to run our VQE on a real QPU. For this, first we need a cost function for the optimization process, which evaluates the expectation value of the Hamiltonian with a quantum state, generated by the ansatz. Don't worry! You don't need to code everything by yourself. We prepared a function for this, and all you need to do is run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "6b2178bd-6e86-4178-b82b-f7a6d7e95022", + "metadata": {}, + "outputs": [], + "source": [ + "def cost_func(params, ansatz, hamiltonian, estimator):\n", + " \"\"\"Return estimate of energy from estimator\n", + "\n", + " Parameters:\n", + " params (ndarray): Array of ansatz parameters\n", + " ansatz (QuantumCircuit): Parameterized ansatz circuit\n", + " hamiltonian (SparsePauliOp): Operator representation of Hamiltonian\n", + " estimator (EstimatorV2): Estimator primitive instance\n", + " cost_history_dict: Dictionary for storing intermediate results\n", + "\n", + " Returns:\n", + " float: Energy estimate\n", + " \"\"\"\n", + " pub = (ansatz, [hamiltonian], [params])\n", + " result = estimator.run(pubs=[pub]).result()\n", + " energy = result[0].data.evs[0]\n", + "\n", + " cost_history_dict[\"iters\"] += 1\n", + " cost_history_dict[\"prev_vector\"] = params\n", + " cost_history_dict[\"cost_history\"].append(energy)\n", + " print(f\"Iters. done: {cost_history_dict['iters']} [Current cost: {energy}]\")\n", + "\n", + " return energy" + ] + }, + { + "cell_type": "markdown", + "id": "83ed09be-d475-4d66-b55f-b25bbf3412c8", + "metadata": {}, + "source": [ + "Finally, we prepare initial parameters for our ansatz and its optimization process. You can simply use all zeroes or random values. We have selected initial parameters below, but feel free to comment or uncomment lines in the cell to sample parameters randomly, uniformly from 0 to $2\\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b2a5dab1-f724-48df-b84b-20749ffd5261", + "metadata": {}, + "outputs": [], + "source": [ + "# x0 = np.random.uniform(0, 2*pi, 3)\n", + "x0 = [1, 1, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b48b37d-1f1e-4aef-9fd5-ba594db832b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iters. done: 1 [Current cost: -0.3361517318448143]\n", + "Iters. done: 2 [Current cost: -0.4682546422099432]\n", + "Iters. done: 3 [Current cost: -0.38985802144149584]\n", + "Iters. done: 4 [Current cost: -0.38319217316749354]\n", + "Iters. done: 5 [Current cost: -0.4628720756579032]\n", + "Iters. done: 6 [Current cost: -0.4683301936226905]\n", + "Iters. done: 7 [Current cost: -0.45480498699294747]\n", + "Iters. done: 8 [Current cost: -0.4690533242050814]\n", + "Iters. done: 9 [Current cost: -0.465867415110354]\n", + "Iters. done: 10 [Current cost: -0.4606882723137227]\n" + ] + } + ], + "source": [ + "# QPU Est. 2min for ibm_brisbane\n", + "\n", + "from scipy.optimize import minimize\n", + "from qiskit_ibm_runtime import Batch\n", + "\n", + "batch = Batch(backend=backend)\n", + "\n", + "cost_history_dict = {\n", + " \"prev_vector\": None,\n", + " \"iters\": 0,\n", + " \"cost_history\": [],\n", + "}\n", + "estimator = Estimator(mode=batch)\n", + "estimator.options.default_shots = 10000\n", + "\n", + "res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa, Hamiltonian_isa, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 10, \"tol\": 0.01},\n", + ")\n", + "\n", + "batch.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "124e15d2-ddce-4201-9f69-03c9e5612644", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The reference ground state energy is (-0.471+0j)\n", + "The computed ground state energy is -0.4690533242050814\n" + ] + } + ], + "source": [ + "h_vqe = res.fun\n", + "print(\"The reference ground state energy is \", min(eigenvalues))\n", + "print(\"The computed ground state energy is \", h_vqe)" + ] + }, + { + "cell_type": "markdown", + "id": "79bce65c-d84e-4bed-bfe8-71f448290fd1", + "metadata": {}, + "source": [ + "Congratulations! You have just finished your first quantum chemistry experiment successfully. We can see a difference between the exact ground state energy of the Hamiltonian and ours, but because we used a default error mitigation technique (which corrects readout errors), the difference is minor. This is a very good start!\n", + "\n", + "Note: You can get a better result by setting a level of error mitigation using [`resilience_level`](/docs/guides/error-mitigation-and-suppression-techniques). The default value is 1, and if you set a higher value, it will use more QPU time but might return a better result." + ] + }, + { + "cell_type": "markdown", + "id": "dae9eeb5-a2ac-4632-90cf-d2ac70182c7b", + "metadata": {}, + "source": [ + "### Step 4: Post-process\n", + "\n", + "It is time to take a look at how our classical optimizer worked. Run the cell below and see the convergence pattern." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b6b32cce-cbfa-4566-9d77-e511f54cc558", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "x = np.linspace(0, 10, 10)\n", + "\n", + "# Define the constant function\n", + "y_constant = np.full_like(x, h)\n", + "ax.plot(\n", + " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", + ")\n", + "ax.set_xlabel(\"Iterations\")\n", + "ax.set_ylabel(\"Cost (Hartree)\")\n", + "ax.plot(y_constant, label=\"Target\")\n", + "plt.legend()\n", + "plt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "909ed768-080d-42b2-aae5-0fe6fa4f088b", + "metadata": {}, + "source": [ + "We started with a fairly good initial value, such that we obtained a good final value in just 10 steps. You can see big and small peaks, and this is the typical feature of the COBYLA optimizer - it searches the space as if it cannot see the landscape and adjusts step sizes with each measurement." + ] + }, + { + "cell_type": "markdown", + "id": "4b3fe46b-fb4e-48d4-a891-732231429926", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "\n", + "What is your observation? Which part of the above process is open to improvement in order to obtain results closer to the theoretical values, or closer to the precise ground state energy of the Hamiltonian? What are some things to consider for this?\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "The first thing to consider is the change in the set of bases used in calculating the Hamiltonian of molecules. As mentioned earlier, the ground state energy of the H atom is -0.5 Hartree, as is well known, and the STO-6G basis we have chosen is not enough to accurately derive this value.\n", + "\n", + "Choosing a more complex kind of basis increases the number of qubits used by the Hamiltonian; therefore, we need to select a more complex and suitable ansatz for chemistry problems.\n", + "\n", + "The next to be optimized is the management of noise in the QPU. More advanced error mitigation techniques yield better results but may take longer to use. Also, consider how the `shot_number` affects the results.\n", + "\n", + "Finally, better convergence performance can also be achieved by trying different optimizers.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "52e936bb-a9af-4355-94a1-82393a25124e", + "metadata": {}, + "source": [ + "## Compute the ground state energy of the hydrogen molecule with VQE\n", + "\n", + "Now that we have looked at the overall process of VQE using $H$ atoms, we will now calculate the ground state energy of the $H_2$ molecule more quickly." + ] + }, + { + "cell_type": "markdown", + "id": "bcd4b1e5-aea5-4509-af22-661937b4286d", + "metadata": {}, + "source": [ + "### Step 1: Map the problem to quantum circuits and operators\n", + "\n", + "Here we also provide you with a one-qubit Hamiltonian that uses the STO-6G basis and the Jordan-Wigner transformation, with qubit reduction by using a symmetry of the Hamiltonian. Note that we used an atomic distance between two hydrogen atoms of `0.735` $\\mathring A$.\n", + "\n", + "Unlike the calculation of a single hydrogen atom ($H$), in order to compute the ground state of a hydrogen molecule($H_2$), we must also consider the repulsive force acting between the nuclei of the two hydrogen atoms, in addition to the energy associated with the electronic orbitals. In this step, we will give this value as a constant, and we will actually calculate this value in the check-in problem.\n", + "$$\\hat{H} = -1.04886 I + -0.79674 Z + 0.18122 X$$" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "3722816b-8710-4c61-9da9-a347556496e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Electronic ground state energy (Hartree): -1.8659468547627318\n", + "Nuclear repulsion energy (Hartree): 0.71997\n", + "Total ground state energy (Hartree): -1.1459768547627318\n" + ] + } + ], + "source": [ + "h2_hamiltonian = SparsePauliOp.from_list(\n", + " [(\"I\", -1.04886087), (\"Z\", -0.7967368), (\"X\", 0.18121804)]\n", + ")\n", + "\n", + "# exact ground state energy of hamiltonian\n", + "nuclear_repulsion = 0.71997\n", + "A = np.array(h2_hamiltonian)\n", + "eigenvalues, eigenvectors = np.linalg.eig(A)\n", + "print(\"Electronic ground state energy (Hartree): \", min(eigenvalues).real)\n", + "print(\"Nuclear repulsion energy (Hartree): \", nuclear_repulsion)\n", + "print(\n", + " \"Total ground state energy (Hartree): \", min(eigenvalues).real + nuclear_repulsion\n", + ")\n", + "h2 = min(eigenvalues).real + nuclear_repulsion" + ] + }, + { + "cell_type": "markdown", + "id": "7e275302-80ea-454b-83c1-d70b09b37442", + "metadata": {}, + "source": [ + "### Step 2: Optimize for target hardware\n", + "\n", + "Since the number of qubits used by the previous VQE and Hamiltonian is the same as the backend to be used for execution, we will use the existing ansatz and its optimized form." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "6a461ab3-fbd7-493a-aaad-e47d6bfd0079", + "metadata": {}, + "outputs": [], + "source": [ + "h2_hamiltonian_isa = h2_hamiltonian.apply_layout(layout=ansatz_isa.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "da36cda3-94a8-4b2d-9e3d-3b5ac957c180", + "metadata": {}, + "source": [ + "### Step 3: Execute on target hardware\n", + "\n", + "Now it's time to do the calculations on the actual QPU. Almost everything is the same, but we will use the appropriate initial point to fit the Hamiltonian. Also, at an iterative part, some of the settings of the `Estimator`, which is used to calculate the Hamiltonian's expectations for the ansatz in the QPU, will be set slightly differently from the previous calculations. We will discuss this change further in a check-in question." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b5afe148-bfdc-4d77-895c-d58afc118d9e", + "metadata": {}, + "outputs": [], + "source": [ + "x0 = [2, 0, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "3fb2e079-1413-4fd2-a3db-da5d11fe58d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iters. done: 1 [Current cost: -0.710621837568328]\n", + "Iters. done: 2 [Current cost: -0.2603208441168329]\n", + "Iters. done: 3 [Current cost: -0.25548711201326424]\n", + "Iters. done: 4 [Current cost: -0.581129450619904]\n", + "Iters. done: 5 [Current cost: -1.722920997605439]\n", + "Iters. done: 6 [Current cost: -1.6633324849371915]\n", + "Iters. done: 7 [Current cost: -1.8066989598929164]\n", + "Iters. done: 8 [Current cost: -1.8051093803839542]\n", + "Iters. done: 9 [Current cost: -1.802692217571555]\n", + "Iters. done: 10 [Current cost: -1.8233585485263144]\n", + "Iters. done: 11 [Current cost: -1.6904116652617205]\n", + "Iters. done: 12 [Current cost: -1.8245120321245392]\n", + "Iters. done: 13 [Current cost: -1.6837021361383608]\n", + "Iters. done: 14 [Current cost: -1.8166632606115467]\n", + "Iters. done: 15 [Current cost: -1.863446212658907]\n" + ] + } + ], + "source": [ + "# QPU time 4min for ibm_brisbane\n", + "batch = Batch(backend=backend)\n", + "\n", + "cost_history_dict = {\n", + " \"prev_vector\": None,\n", + " \"iters\": 0,\n", + " \"cost_history\": [],\n", + "}\n", + "estimator = Estimator(mode=batch)\n", + "estimator.options.default_shots = 10000\n", + "\n", + "res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa, h2_hamiltonian_isa, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 15},\n", + ")\n", + "\n", + "batch.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "4edf1dbd-8363-4d0d-a4ca-2223719c7059", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The reference ground state energy is -1.1459768547627318\n", + "The computed ground state energy is -1.143476212658907\n" + ] + } + ], + "source": [ + "h2_vqe = res.fun + nuclear_repulsion\n", + "print(\n", + " \"The reference ground state energy is \", min(eigenvalues).real + nuclear_repulsion\n", + ")\n", + "print(\"The computed ground state energy is \", h2_vqe)" + ] + }, + { + "cell_type": "markdown", + "id": "0d30fbc5-8f52-4b84-b9a6-55b3432bbec5", + "metadata": {}, + "source": [ + "Despite VQE theoretically providing an upper bound to the true ground state energy, practical implementations on real or noisy simulated quantum hardware, as well as approximations made in preparing the Hamiltonian (like basis sets or qubit reduction), can introduce errors that sometimes result in a measured energy slightly lower than the exact theoretical value or a specific numerical reference. Although there are some errors, the results seem to be satisfactory, especially given the small number of steps. Now, let's finish this VQE calculation by looking at how the optimizer worked." + ] + }, + { + "cell_type": "markdown", + "id": "5d5c88de-fcd1-4ef6-8055-01505d86d07e", + "metadata": {}, + "source": [ + "### Step 4: Post-process" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "b20dda12-dad9-4585-9a56-31611aa00930", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "x = np.linspace(0, 5, 15)\n", + "\n", + "# Define the constant function\n", + "y_constant = np.full_like(x, min(eigenvalues))\n", + "ax.plot(\n", + " range(cost_history_dict[\"iters\"]), cost_history_dict[\"cost_history\"], label=\"VQE\"\n", + ")\n", + "ax.set_xlabel(\"Iterations\")\n", + "ax.set_ylabel(\"Cost (Hartree)\")\n", + "ax.plot(y_constant, label=\"Target\")\n", + "plt.legend()\n", + "plt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "8eab4184-b5dd-494e-9e67-c209b3f835f3", + "metadata": {}, + "source": [ + "#### Check your understanding\n", + "Let's compute the nuclear repulsion energy of $H_2$ molecule, which we included as a constant value (0.71997 Hartree).\n", + "\n", + "![H2 molecule](/learning/images/modules/computer-science/vqe/h2.avif)\n", + "\n", + "Please use [Coulomb law](https://en.wikipedia.org/wiki/Coulomb%27s_law) and [atomic unit](https://en.wikipedia.org/wiki/Atomic_units) to make sure you get the value in `Hartree`.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Since both hydrogen nuclei are positively charged, they repel each other due to electrostatic force. This repulsion is described by Coulomb's law:\n", + "\n", + "$$E_{repulsive} = \\frac{e^2}{4\\pi\\epsilon_0R}$$\n", + "\n", + "where $e$ is a charge of proton, $\\epsilon_0$ is a vacuum permittivity, and $R$ is the distance between the two nuclei, measured in meters or Bohr radii in unit of joules(J).\n", + "\n", + "To compute this energy in Hartrees, we need to convert the above equation into the Atomic Unit (AU) system. In AU, $e^2 = 1$, $4\\pi\\epsilon_0=1$ and Bohr radius ($a_0$) is 1 and becomes the fundamental length scale in AU. With these simplifications, Coulomb's law reduces to:\n", + "\n", + "$$E_{repulsion} = \\frac{1}{R}$$\n", + "\n", + "where $R$ must be measured in Bohr radii ($a_0$).\n", + "\n", + "To convert the given nuclear separation in $\\r{A}$ into $a_0$, we need this conversion relation:\n", + "\n", + "$$1\\r{A} = 1.88973 a_0$$\n", + "\n", + "so $0.735\\r{A}$ becomes $0.735 * 1.88973 = 1.38895 a_0$.\n", + "\n", + "Therefore, the nuclear repulsion energy of a given $H_2$ is\n", + "\n", + "$$E_{repulsion} = \\frac{1}{R} = \\frac{1}{1.38895} = 0.71997 Hartree$$\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "f75e4a10-dfdc-4416-8087-a41c1eac039c", + "metadata": {}, + "source": [ + "## Compute reaction energy of $H + H = H_2$\n", + "\n", + "Now let's use what we've obtained! You've used VQE, a variational quantum eigensolver, to calculate the ground state energy of the $H$ atom and of the $H_2$ molecule. What's left is to use the calculated values to get the reaction energy of the $H+H=H_2$ process.\n", + "\n", + "Reaction energy is the energy change that happens when substances react to form new substances. Imagine you are building something: sometimes you need to put energy into it (like stacking blocks), and sometimes energy is released (like a ball rolling downhill). In chemistry, reactions either absorb energy (endothermic) or release energy (exothermic).\n", + "\n", + "The reaction energy of $H+H = H_2$ process can be compute by the following formula:\n", + "\n", + "$E_{reaction} = E_{H_2} - (E_H + E_H)$\n", + "\n", + "By running the cell below, let's see this visually. Here we will use the exact ground state value of each Hamiltonian, and we'll compare the reaction energy of the exact solution and VQE results." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "aa8d1a29-fbfe-41bf-8ed7-947827d0f0e6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Theoretical values\n", + "E_H_theo = h.real\n", + "E_H2_theo = h2\n", + "\n", + "# Experimental values\n", + "E_H_exp = h_vqe\n", + "E_H2_exp = h2_vqe\n", + "\n", + "# Calculate reaction energies\n", + "E_reaction_theo = E_H2_theo - (2 * E_H_theo)\n", + "E_reaction_exp = E_H2_exp - (2 * E_H_exp)\n", + "\n", + "# Set up the plot\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "ax.set_xlim(0, 3)\n", + "ax.set_ylim(-1.16, -0.93) # Adjust y-axis range to highlight differences\n", + "ax.set_xticks([])\n", + "ax.set_ylabel(\"Energy (Hartree)\")\n", + "ax.set_title(\"H + H → H₂ Reaction Energy Diagram\")\n", + "\n", + "# Plot theoretical energy levels\n", + "ax.hlines(\n", + " y=2 * E_H_theo, xmin=0.5, xmax=1.3, linewidth=2, color=\"r\", label=\"2H (Exact)\"\n", + ")\n", + "ax.hlines(y=E_H2_theo, xmin=1.3, xmax=2, linewidth=2, color=\"b\", label=\"H₂ (Exact)\")\n", + "\n", + "# Plot experimental energy levels\n", + "ax.hlines(\n", + " y=2 * E_H_exp,\n", + " xmin=0.5,\n", + " xmax=1.5,\n", + " linewidth=2,\n", + " color=\"r\",\n", + " linestyle=\"dashed\",\n", + " label=\"2H (VQE)\",\n", + ")\n", + "ax.hlines(\n", + " y=E_H2_exp,\n", + " xmin=1.5,\n", + " xmax=2.5,\n", + " linewidth=2,\n", + " color=\"b\",\n", + " linestyle=\"dashed\",\n", + " label=\"H₂ (VQE)\",\n", + ")\n", + "\n", + "# Add labels\n", + "ax.text(\n", + " 1,\n", + " 2 * E_H_theo,\n", + " f\"2H: {2*E_H_theo:.4f}\",\n", + " verticalalignment=\"top\",\n", + " horizontalalignment=\"left\",\n", + ")\n", + "ax.text(\n", + " 2,\n", + " E_H2_theo,\n", + " f\"H₂: {E_H2_theo:.4f}\",\n", + " verticalalignment=\"top\",\n", + " horizontalalignment=\"left\",\n", + ")\n", + "ax.text(\n", + " 1,\n", + " 2 * E_H_exp,\n", + " f\"2H_VQE: {2*E_H_exp:.4f}\",\n", + " verticalalignment=\"bottom\",\n", + " horizontalalignment=\"right\",\n", + ")\n", + "ax.text(\n", + " 2,\n", + " E_H2_exp,\n", + " f\"H₂_VQE: {E_H2_exp:.4f}\",\n", + " verticalalignment=\"bottom\",\n", + " horizontalalignment=\"right\",\n", + ")\n", + "\n", + "# Add arrows for reaction energy with ΔE label in the middle\n", + "mid_y_theo = (2 * E_H_theo + E_H2_theo) / 2\n", + "mid_y_exp = (2 * E_H_exp + E_H2_exp) / 2\n", + "ax.annotate(\n", + " \"\",\n", + " xy=(1.3, E_H2_theo),\n", + " xytext=(1.3, 2 * E_H_theo),\n", + " arrowprops=dict(arrowstyle=\"<->\", color=\"g\"),\n", + ")\n", + "ax.text(\n", + " 1.35, mid_y_theo, f\"ΔE: {E_reaction_theo:.4f}\", color=\"g\", verticalalignment=\"top\"\n", + ")\n", + "\n", + "ax.annotate(\n", + " \"\",\n", + " xy=(1.5, E_H2_exp),\n", + " xytext=(1.5, 2 * E_H_exp),\n", + " arrowprops=dict(arrowstyle=\"<->\", color=\"g\", linestyle=\"dashed\"),\n", + ")\n", + "ax.text(\n", + " 1.55,\n", + " mid_y_exp,\n", + " f\"ΔE_VQE: {E_reaction_exp:.4f}\",\n", + " color=\"g\",\n", + " verticalalignment=\"center\",\n", + ")\n", + "\n", + "# Add legend\n", + "ax.legend()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f1a87af7-0c69-42c5-a415-87c95752d678", + "metadata": {}, + "source": [ + "As shown in the figure, although there are some errors, the exact ground state energy of Hamiltonians and the reaction energy calculated using the VQE results are similar, close to -0.2 Hartree.\n", + "\n", + "It should be noted here that the reaction energy of this process has a negative value, which means that the energy is released through the process, and the resulting molecule has a lower energy than two single atoms." + ] + }, + { + "cell_type": "markdown", + "id": "006ded34-29e0-4702-a46a-0459ee49f416", + "metadata": {}, + "source": [ + "6. Conclusion\n", + "\n", + "Let's summarize what we've learned so far.\n", + "\n", + "We first looked at two important approximation techniques needed to solve quantum chemistry problems: the variational principle and basis set choices, which are both fundamental to VQE. We explored the variational principle by hand, calculating the ground state energy of the simple harmonic oscillator.\n", + "\n", + "Next, we explored VQE, a widely-used algorithm for calculating the ground state energy of a quantum system. We ran code to calculate the ground state energies for atomic hydrogen ($H$) and the hydrogen molecule ($H_2$). In particular, we learned that it is necessary to obtain the appropriate molecular Hamiltonian for the system and to transform it into a form executable on a quantum computer. We also saw that the ansatz, a parameterized quantum circuit, is needed to prepare trial quantum states within VQE, and we discussed the importance of choosing an appropriate ansatz circuit structure. We also learned that VQE relies on an iterative optimization process using a classical computer, guiding the quantum circuit to find the lowest energy state, and saw how the process converges.\n", + "\n", + "Finally, we used the computed ground state energies of $H$ and $H_2$ obtained through VQE to calculate the reaction energy for the process $H + H \\rightarrow H_2$.\n", + "\n", + "VQE is a powerful near-term quantum algorithm, but it's important to be aware of its limitations. The performance of VQE heavily depends on the choice of the ansatz – finding an efficiently preparable ansatz that can accurately represent the true ground state becomes challenging for larger, more complex molecules. Furthermore, current quantum hardware is susceptible to noise, which can impact the accuracy of VQE results, particularly for deeper circuits or larger numbers of qubits. Despite these challenges, VQE serves as a foundational algorithm, and ongoing research is exploring more sophisticated variational methods and error mitigation techniques to push the boundaries of what is possible in quantum chemistry on near-term quantum computers. For instance, algorithms like Sample-based Quantum Diagonalization (SQD) are being developed, which leverage samples obtained from quantum circuits combined with classical diagonalization in a subspace to improve energy estimation and address some of the limitations faced by VQE, particularly regarding measurement efficiency and noise robustness." + ] + }, + { + "cell_type": "markdown", + "id": "03c9dfc1-7367-4d71-9598-f02288c30ac0", + "metadata": {}, + "source": [ + "## Review and questions\n", + "\n", + "### Critical concepts:\n", + "\n", + "- Variational quantum algorithm is a computing paradigm in which a classical computer and a quantum computer work together to solve a problem.\n", + "- In VQE, we start with a Hamiltonian of our system and map it onto qubits for execution on the quantum computer. We select a parameterized quantum circuit, an ansatz, and make repeated measurements, varying the parameters of the ansatz, until the lowest energy value is reached. The search through parameter space is done using a classical optimizer. To achieve good results, it is necessary to select a good ansatz and an appropriate optimizer.\n", + "- Reaction energy is the total energy change in a chemical reaction, determined by the difference between the energy of the reactants and the products.\n", + "\n", + "\n", + "### True/false\n", + "1. The variational principle states that the expectation value of the energy for any trial wave function is always greater than or equal to the true ground state energy.\n", + "2. A basis set is a collection of functions used to approximate quantum wave functions.\n", + "3. VQE is a quantum algorithm used to exactly solve the Schrödinger equation for a given Hamiltonian.\n", + "4. In VQE, a parameterized quantum circuit (an ansatz) is used to prepare trial wave functions.\n", + "5. The choice of optimizer in VQE (for example, COBYLA, SPSA, or ADAM) does not impact the quality of the result.\n", + "6. Qiskit's `Estimator` is used to directly compute expectation values of Hamiltonians in VQE.\n", + "\n", + "\n", + "### Multiple-choice questions:\n", + "\n", + "1. What is the purpose of the Hamiltonian in VQE?\n", + "\n", + "- A) To generate random quantum states\n", + "- B) To determine the energy of quantum states\n", + "- C) To optimize quantum circuits\n", + "- D) To create entanglement\n", + "\n", + "2. What is the primary objective of the VQE algorithm?\n", + "\n", + "- A) To find the ground state energy of a Hamiltonian\n", + "- B) To create entanglement between qubits\n", + "- C) To perform Grover's search\n", + "- D) To break the RSA encryption\n", + "\n", + "3. How many quantum states are generated in this notebook to compare the ansatz?\n", + "- A) 100\n", + "- B) 1000\n", + "- C) 5000\n", + "- D) 10,000\n", + "\n", + "4. Why is a classical optimizer required in VQE?\n", + "- A) To perform quantum measurements\n", + "- B) Update ansatz parameters to minimize energy\n", + "- C) To entangle qubits\n", + "- D) To generate quantum randomness\n", + "\n", + "5. Why is the ansatz designed to be parameterized?\n", + "- A) To allow quantum state preparation\n", + "- B) To allow a wide space of quantum states to be searched\n", + "- C) To reduce circuit complexity\n", + "- D) To measure eigenvalues directly\n", + "\n", + "6. Which of the following is the most correct statement about choosing a good ansatz?\n", + "- A) An ansatz must produce states evenly distributed over the Bloch sphere, or it will fail.\n", + "- B) An ansatz should be tailored to your system to make sure it can generate states close to the ground state.\n", + "- C) An ansatz should produce random states using its variational parameters.\n", + "- D) A better ansatz always has more variational parameters." + ] + }, + { + "cell_type": "markdown", + "id": "7f44807a-45f6-4e36-9d19-faa77f7c0c79", + "metadata": {}, + "source": [ + "## (Optional) Appendix: Optimizer overhead by ansatz complexity\n", + "\n", + "VQE faces several well-known challenges[ref 6], and the following are related to what we have learned above.\n", + "\n", + "1. Ansatz selection challenges\n", + "\n", + "There is an inherent challenge in selecting the right variational ansatz. Chemistry-inspired ansätze (like UCCSD) provide physical accuracy but require deep circuits, while hardware-efficient ansätze have shallower circuits but may lack physical interpretability. Also, many ansätze introduce excessive variational parameters that contribute little to improving accuracy but significantly increase optimization difficulty.\n", + "\n", + "2. Optimization difficulties\n", + "\n", + "\n", + "The optimization landscape of VQE can have regions where gradients vanish exponentially (barren plateaus), making it difficult for classical optimizers to update the variational parameters efficiently. For this, researchers have tried to use different types of optimizers - gradient-based and gradient-free, but both face challenges. Gradient-based optimizers suffer from barren plateaus, while gradient-free methods require a large number of function evaluations.\n", + "\n", + "3. Optimizer overhead\n", + "\n", + "One more well-known challenge is optimizer overhead, which is related to the scale of the problem. The quantum circuits required for VQE grow in depth and complexity as the problem size increases; this typically also increases the number of parameters to optimize. The optimization process becomes intractable as the number of parameters increases, leading to slow convergence and difficulties in finding the optimal solution.\n", + "\n", + "Here we will take a look at these challenges by using VQE for a $H_2$ molecule, with two different types of ansätze.\n", + "\n", + "(Note: This can take more QPU time, so feel free to use a simulator for this if you don't have enough time.)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "96179a4e-33f9-43ff-a791-f5ac8114dd29", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import ParameterVector\n", + "\n", + "num_iter = 4\n", + "alpha = ParameterVector(\"alpha\", 3)\n", + "beta = ParameterVector(\"beta\", 3 * num_iter)\n", + "\n", + "# step1: Map problem to quantum circuits and operators\n", + "hamiltonian = SparsePauliOp.from_list(\n", + " [(\"I\", -1.04886087), (\"Z\", -0.7967368), (\"X\", 0.18121804)]\n", + ")\n", + "\n", + "ansatz_1 = ansatz3\n", + "ansatz_2 = QuantumCircuit(1)\n", + "for i in range(num_iter):\n", + " ansatz_2.rx(beta[i * 3 + 0], 0)\n", + " ansatz_2.rz(beta[i * 3 + 1], 0)\n", + " ansatz_2.rx(beta[i * 3 + 2], 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "0eeb0370-0ec7-490e-962b-55f746c4e4a3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ansatz_1.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "105b8557-861c-4fcc-a84e-82d382ad730f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ansatz_2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "2f3f5e3f-d41b-45af-a14f-d2f1ef506037", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Optimize for target hardware\n", + "\n", + "target = backend.target\n", + "pm = generate_preset_pass_manager(target=target, optimization_level=3)\n", + "\n", + "ansatz_isa_1 = pm.run(ansatz_1)\n", + "ansatz_isa_2 = pm.run(ansatz_2)\n", + "hamiltonian_isa_1 = hamiltonian.apply_layout(layout=ansatz_isa_1.layout)\n", + "hamiltonian_isa_2 = hamiltonian.apply_layout(layout=ansatz_isa_2.layout)" + ] + }, + { + "cell_type": "markdown", + "id": "1433eb47-1fcc-4e2e-8738-2d2818be8824", + "metadata": {}, + "source": [ + "Now let's run a VQE with an initial point made of all ones, with a maximum of 20 steps, and compare the convergence of both runs." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "693444f0-5835-471f-b9f7-20b5158b3d45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iters. done: 1 [Current cost: -0.8782202668652658]\n", + "Iters. done: 2 [Current cost: -0.43473160695469165]\n", + "Iters. done: 3 [Current cost: -0.4076372093159749]\n", + "Iters. done: 4 [Current cost: -1.3587839859772106]\n", + "Iters. done: 5 [Current cost: -1.774529906754082]\n", + "Iters. done: 6 [Current cost: -1.541934983115727]\n", + "Iters. done: 7 [Current cost: -1.2732403113465345]\n", + "Iters. done: 8 [Current cost: -1.820842221085785]\n", + "Iters. done: 9 [Current cost: -1.8065762857059005]\n", + "Iters. done: 10 [Current cost: -1.8126394095981146]\n", + "Iters. done: 11 [Current cost: -1.8205831886180421]\n", + "Iters. done: 12 [Current cost: -1.8086715778994924]\n", + "Iters. done: 13 [Current cost: -1.8307676638629322]\n", + "Iters. done: 14 [Current cost: -1.8177328827556327]\n", + "Iters. done: 15 [Current cost: -1.8179426218088064]\n", + "Iters. done: 16 [Current cost: -1.8109239667991088]\n", + "Iters. done: 17 [Current cost: -1.824271872489647]\n", + "Iters. done: 18 [Current cost: -1.813167587671394]\n", + "Iters. done: 19 [Current cost: -1.824647343397313]\n", + "Iters. done: 20 [Current cost: -1.8219785311686143]\n" + ] + } + ], + "source": [ + "# QPU time 3m 40s for ibm_brisbane\n", + "# Step 3: Execute on target hardware\n", + "\n", + "from scipy.optimize import minimize\n", + "\n", + "x0 = np.ones(ansatz_1.num_parameters)\n", + "\n", + "batch = Batch(backend=backend)\n", + "\n", + "\n", + "cost_history_dict = {\n", + " \"prev_vector\": None,\n", + " \"iters\": 0,\n", + " \"cost_history\": [],\n", + "}\n", + "estimator = Estimator(mode=batch)\n", + "estimator.options.default_shots = 2048\n", + "\n", + "res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa_1, hamiltonian_isa_1, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 20},\n", + ")\n", + "\n", + "batch.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "6922c55c-6a4f-41d8-8aad-69fa432ddf14", + "metadata": {}, + "outputs": [], + "source": [ + "# Save Cost_history as a new list\n", + "ansatz_1_history = cost_history_dict[\"cost_history\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "5ba0affa-8641-4a15-9408-a99dff0c7f8c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iters. done: 1 [Current cost: -0.738191173881188]\n", + "Iters. done: 2 [Current cost: -0.42636037194506304]\n", + "Iters. done: 3 [Current cost: -1.3503788613797374]\n", + "Iters. done: 4 [Current cost: -0.9109204349776897]\n", + "Iters. done: 5 [Current cost: -0.9060873157510835]\n", + "Iters. done: 6 [Current cost: -0.7735065414083984]\n", + "Iters. done: 7 [Current cost: -1.586889197437709]\n", + "Iters. done: 8 [Current cost: -1.659215191584943]\n", + "Iters. done: 9 [Current cost: -1.245445981794618]\n", + "Iters. done: 10 [Current cost: -1.1608385766138023]\n", + "Iters. done: 11 [Current cost: -1.1551733876027737]\n", + "Iters. done: 12 [Current cost: -1.8143337768286332]\n", + "Iters. done: 13 [Current cost: -1.2510951563756598]\n", + "Iters. done: 14 [Current cost: -1.6918311531865413]\n", + "Iters. done: 15 [Current cost: -1.8163783305531838]\n", + "Iters. done: 16 [Current cost: -1.8434877732947152]\n", + "Iters. done: 17 [Current cost: -1.8461898233304472]\n", + "Iters. done: 18 [Current cost: -1.0346471214915485]\n", + "Iters. done: 19 [Current cost: -1.8322518854150687]\n", + "Iters. done: 20 [Current cost: -1.717144678705999]\n" + ] + } + ], + "source": [ + "# QPU time 3m 40s for ibm_brisbane\n", + "\n", + "x0 = np.ones(ansatz_2.num_parameters)\n", + "\n", + "batch = Batch(backend=backend)\n", + "\n", + "\n", + "cost_history_dict = {\n", + " \"prev_vector\": None,\n", + " \"iters\": 0,\n", + " \"cost_history\": [],\n", + "}\n", + "estimator = Estimator(mode=batch)\n", + "estimator.options.default_shots = 2048\n", + "\n", + "res = minimize(\n", + " cost_func,\n", + " x0,\n", + " args=(ansatz_isa_2, hamiltonian_isa_2, estimator),\n", + " method=\"cobyla\",\n", + " options={\"maxiter\": 20},\n", + ")\n", + "\n", + "batch.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "7354f299-cab6-4d43-a0e3-38c6c19f0486", + "metadata": {}, + "outputs": [], + "source": [ + "ansatz_2_history = cost_history_dict[\"cost_history\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "2c6d2eb4-197b-4811-bb0b-377e445ff3a8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "\n", + "# Define the constant function)\n", + "ax.plot(\n", + " range(cost_history_dict[\"iters\"]),\n", + " ansatz_1_history,\n", + " label=\"Ansatz with 3 parameters\",\n", + ")\n", + "ax.plot(\n", + " range(cost_history_dict[\"iters\"]),\n", + " ansatz_2_history,\n", + " label=\"Ansatz with 12 parameters\",\n", + ")\n", + "ax.set_xlabel(\"Iterations\")\n", + "ax.set_ylabel(\"Cost (Hartree)\")\n", + "plt.legend()\n", + "plt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "b6a9fcdb-020f-429a-8b6e-17d67b0caaf9", + "metadata": {}, + "source": [ + "The graph above clearly demonstrates that the optimization process of the ansatz with more variables takes more time to get to stable convergence.\n", + "\n", + "Rather than relying on simple single-qubit circuits and a straightforward ansatz, the complexity of optimization increases when larger quantum circuits and more complex structured ansätze are required. This highlights a well-known challenge in VQEs: the overhead of the optimizer.\n", + "\n", + "Researchers continue to develop various advanced methodologies that can use quantum computers for chemistry problems. You can access a variety of educational materials at [IBM Quantum Learning](/learning)." + ] + }, + { + "cell_type": "markdown", + "id": "f7c6d9a5-c2cc-4674-9d54-1f9809cfde51", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "- [[ref 1](https://link.springer.com/article/10.1007/BF02650179) ] Richard P. Feynman, Simulating Physics with Computers, International Journal of Theoretical Physics, 1982.\n", + "- [[ref 2]](https://link.springer.com/chapter/10.1007/978-1-4614-8730-2_10) Marov, M.Y. (2015). The Structure of the Universe. In: The Fundamentals of Modern Astrophysics. Springer, New York, NY.\n", + "- [[ref 3](https://www.ibm.com/quantum/blog/photoresists-quantum-chemistry-jsr)] How to solve difficult chemical engineering problems with quantum computing, IBM Research Blog, 2023.\n", + "- [[ref 4](https://ieeexplore.ieee.org/document/8585034)] Y. Cao, J. Romero and A. Aspuru-Guzik, \"Potential of quantum computing for drug discovery,\" in IBM Journal of Research and Development, vol. 62, no. 6, pp. 6:1-6:20, 1 Nov.-Dec. 2018\n", + "- [[ref 5](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.32.170)] Present State of Molecular Structure Calculation, REv. Mod. Phys. 32, 170, 1960\n", + "- [[ref 6](https://jmsh.springeropen.com/articles/10.1186/s41313-021-00032-6)] Fedorov, D.A., Peng, B., Govind, N. et al. VQE method: a short survey and recent developments. Mater Theory 6, 2 (2022)" + ] + } + ], + "metadata": { + "in_page_toc_max_heading_level": 2, + "in_page_toc_min_heading_level": 2, + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb b/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb index e9a72eaef44..bb25dee35b6 100644 --- a/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb @@ -1,1360 +1,1372 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "503c9fb9-2720-49e4-a32f-f3f98973d312", - "metadata": {}, - "source": [ - "---\n", - "title: Bell's inequality with Qiskit\n", - "description: Learn what measurement statistics are predicted by hidden variables and by quantum mechanics, and check which is correct using a real quantum computer.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore Gott würfelt nicht outcoming Rihanna Rihanna's Marshman */}\n", - "\n", - "# The nature of quantum states: hidden variables versus Bell's inequality" - ] - }, - { - "cell_type": "markdown", - "id": "3571392e-5f59-4013-9807-b668da03189b", - "metadata": {}, - "source": [ - "For this Qiskit in Classrooms module, students must have a working Python environment with the following packages installed:\n", - "- `qiskit` v2.1.0 or newer\n", - "- `qiskit-ibm-runtime` v0.40.1 or newer\n", - "- `qiskit-aer` v0.17.0 or newer\n", - "- `qiskit.visualization`\n", - "- `numpy`\n", - "- `pylatexenc`\n", - "\n", - "To set up and install the packages above, see the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", - "In order to run jobs on real quantum computers, students will need to set up an account with IBM Quantum® by following the steps in the [Set up your IBM Cloud account](/docs/guides/cloud-setup) guide.\n", - "\n", - "This module was tested and used 12 seconds of QPU time. This is an estimate only. Your actual usage may vary." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f417af00-e7c2-4eda-a49c-aa7c48240b4b", - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and modify this line as needed to install dependencies\n", - "#!pip install 'qiskit>=2.1.0' 'qiskit-ibm-runtime>=0.40.1' 'qiskit-aer>=0.17.0' 'numpy' 'pylatexenc'" - ] - }, - { - "cell_type": "markdown", - "id": "0e714b7e-ec73-41a1-a4c3-7165b5eb1eb9", - "metadata": {}, - "source": [ - "Watch the module walkthrough by Dr. Katie McCormick below, or click [here](https://www.youtube.com/watch?v=pS69lqCMdy8&list=PLOFEBzvs-Vvrs2fuvsuT039ariYPsua3d&index=4) to watch it on YouTube.\n", - "\n", - "-------\n", - "\n", - "\n", - "